From c5b03ad4e57badc53dc6a0293e76cef65dc1a4ec Mon Sep 17 00:00:00 2001
From: lewa <lene.wasskog@nibio.no>
Date: Mon, 26 Aug 2024 22:55:26 +0200
Subject: [PATCH] feat: Newly created points available in dropdown and map

---
 src/main/webapp/js/mapModal.js                | 243 ++++++++++++------
 .../templates/forecastConfigurationForm.ftl   |  83 +++---
 2 files changed, 215 insertions(+), 111 deletions(-)

diff --git a/src/main/webapp/js/mapModal.js b/src/main/webapp/js/mapModal.js
index 47f23cf1..6a7a4e34 100644
--- a/src/main/webapp/js/mapModal.js
+++ b/src/main/webapp/js/mapModal.js
@@ -2,9 +2,8 @@
 import {
     map,
     tileLayer,
-    marker,
     geoJSON,
-    circleMarker,
+    circleMarker, GeoJSON,
     DomEvent
 } from 'https://unpkg.com/leaflet/dist/leaflet-src.esm.js';
 
@@ -14,18 +13,29 @@ import {
  */
 class MapModal {
 
-    constructor(mapContainerId, typeNameMap, geoJsonData, allowNewPoints = false, callback = null) {
+    /**
+     * @param mapContainerId    The id of the HTML element to which the map should be added
+     * @param typeNameMap   A mapping from pointOfInterestTypeIds to their localized names
+     * @param geoJsonData   GeoJson containing all features which should be displayed on the map
+     * @param allowNewPoints    Whether or not the user should be allowed to add new points
+     * @param callbackOnPersistNew  Callback function for persisting newly created point
+     * @param callbackOnClose   Callback function to call when closing the modal
+     */
+    constructor(mapContainerId, typeNameMap, geoJsonData, allowNewPoints = false, callbackOnPersistNew = null, callbackOnClose = null) {
         this.mapContainerId = mapContainerId;
         this.typeNameMap = typeNameMap;
-        this.geojsonData = geoJsonData;
+        this.geoJsonData = geoJsonData;
         this.allowNewPoints = allowNewPoints;
+        this.callbackOnPersistNew = callbackOnPersistNew;
+        this.callbackOnClose = callbackOnClose;
+
         this.map = null;
         this.isMapInitialized = false;
         this.selectedPointLayer = null;
         this.createdPointLayer = null;
         this.createdPoints = [];
-        this.callback = callback;
 
+        // Colours for the available types of pois
         this.typeColorMap = {
             0: "#B3CDE0",  // Light blue
             1: "#FBB4AE",  // Soft pink
@@ -42,7 +52,6 @@ class MapModal {
         if (!this.isMapInitialized) {
             // Initialize the map centered on Norway
             this.map = map(this.mapContainerId).setView([63.4226, 10.3951], 5);
-            // Add a tile layer
             tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                 maxZoom: 19
             }).addTo(this.map);
@@ -50,7 +59,7 @@ class MapModal {
             this.createSelectedPointInfo();
 
             // Add points to the map
-            geoJSON(this.geojsonData, {
+            geoJSON(this.geoJsonData, {
                 filter: (feature) => feature.geometry.type === "Point",
                 pointToLayer: (feature, latlng) => {
                     const color = this.typeColorMap[feature.properties.pointOfInterestTypeId] || "#3498DB";
@@ -78,19 +87,25 @@ class MapModal {
         }
     }
 
+    /**
+     * Create information panel for selected point, initially hidden.
+     */
     createSelectedPointInfo() {
-        console.debug("Create information panel for selected point, initially hidden");
         const selectedPointInfoHtml = `
         <div id="selectedPointInfo" style="display: none;">
             <div id="infoMessage"></div>
-            <button id="confirmButton" class="btn btn-primary">OK</button>
-        </div>
-    `   ;
+            <button id="confirmButton" class="btn btn-primary">Velg sted</button>
+        </div>`;
         document.getElementById(this.mapContainerId).insertAdjacentHTML('beforeend', selectedPointInfoHtml);
     }
 
-    updateSelectedPointInfo(feature) {
+    /**
+     * Display the panel which contains information about the currently selected point,
+     * and a button to bring you back to the original page.
+     */
+    displaySelectedPointInfo(feature) {
         const name = feature.properties.pointOfInterestName;
+        console.info("Display selected point " + name);
         const type = this.typeNameMap[feature.properties.pointOfInterestTypeId];
         const coordinates = feature.geometry.coordinates[1].toFixed(5) + ", " + feature.geometry.coordinates[0].toFixed(5);
 
@@ -107,7 +122,43 @@ class MapModal {
         };
     }
 
-    selectPoint(feature, layer) {
+    selectPointById(pointOfInterestId) {
+        const selectedFeature = this.getFeatureById(pointOfInterestId);
+        const selectedLayer = this.getLayerById(pointOfInterestId);
+        this.selectPoint(selectedFeature, selectedLayer, true);
+    }
+
+    getFeatureById(pointOfInterestId) {
+        let feature = this.geoJsonData.features.find(feature => feature.properties.pointOfInterestId == pointOfInterestId);
+        if (!feature) {
+            console.info("Feature with id=" + pointOfInterestId + " not found, assume that the user wants the newly created", this.createdPoints[0]);
+            return this.createdPoints.length > 0 ? this.createdPoints[0] : null;
+        }
+        return feature;
+    }
+
+    getLayerById(pointOfInterestId) {
+        let result = null;
+        this.map.eachLayer(layer => {
+            if (layer instanceof GeoJSON) {
+                layer.eachLayer(l => {
+                    if (l.feature && l.feature.properties.pointOfInterestId == pointOfInterestId) {
+                        result = l;
+                    }
+                });
+            }
+        });
+        if (!result) {
+            console.info("Layer with id=" + pointOfInterestId + " not found, assume that the user wants the newly created", this.createdPointLayer);
+            this.createdPointLayer.eachLayer((layer) => {
+                result = layer;
+            });
+        }
+        return result;
+    }
+
+    selectPoint(feature, layer, zoomInToSelected = false) {
+        console.info("Select point", feature);
         // Deselect previously selected point, if any
         if (this.selectedPointLayer) {
             const color = this.typeColorMap[feature.properties.pointOfInterestTypeId] || "#3498DB";
@@ -127,35 +178,41 @@ class MapModal {
             color: '#f00'
         });
 
-        this.updateSelectedPointInfo(feature)
+        if (zoomInToSelected) {
+            const latLng = layer.getLatLng();
+            this.map.setView(latLng, 10);
+        }
+
+        this.displaySelectedPointInfo(feature)
         this.selectedPointLayer = layer;
     }
 
     confirmSelection(feature) {
-        console.info("Save pointData", feature);
-        const pointData = {
-            id: feature.properties.pointOfInterestId,
-            name: feature.properties.pointOfInterestName,
-            typeId: feature.properties.pointOfInterestTypeId,
-            altitude: feature.properties.pointOfInterestAltitude,
-            longitude: feature.geometry.coordinates[0],
-            latitude: feature.geometry.coordinates[1]
-        };
-        if (typeof this.callback === 'function') {
-            console.info("Callback!", pointData);
-            this.callback(pointData);
+        console.info("Confirm selection", feature);
+        let poiId = feature.properties.pointOfInterestId;
+        if (!poiId && typeof this.callbackOnPersistNew === 'function') {
+            const pointData = {
+                name: feature.properties.pointOfInterestName,
+                typeId: feature.properties.pointOfInterestTypeId,
+                longitude: feature.geometry.coordinates[0],
+                latitude: feature.geometry.coordinates[1]
+            };
+            console.info("Persist new", pointData);
+            this.callbackOnPersistNew(pointData);
+        }
+        if (typeof this.callbackOnClose === 'function') {
+            this.callbackOnClose(poiId);
+            console.info("Goodbye from map modal!")
         }
         this.closeModal();
     }
 
     enablePointCreation() {
         this.map.on('click', (e) => {
-            console.log("Map clicked, enabling point creation...");
             const latlng = e.latlng;
 
             // If a form already exists, remove it
             this.closeNewPointFormIfOpen();
-
             // Calculate the pixel position from the map's click event
             const containerPoint = this.map.latLngToContainerPoint(latlng);
 
@@ -168,15 +225,15 @@ class MapModal {
                 <div class="panel-body">
                     <div class="form-group">
                         <label for="name">Name:</label>
-                        <input type="text" class="form-control" id="name" name="name">
-                    </div>
-                    <div class="form-group">
-                        <label for="altitude">Altitude:</label>
-                        <input type="text" class="form-control" id="altitude" name="altitude">
+                        <input type="text" class="form-control" id="name" name="name" autofocus>
                     </div>
                     <div class="form-group">
                         <label for="poiTypeSelect">Type:</label>
-                        <select class="form-control" id="poiTypeSelect" name="poiTypeSelect"></select>
+                        <select class="form-control" id="poiTypeSelect" name="poiTypeSelect">
+                            <option value="2">${this.typeNameMap[2]}</option>
+                            <option value="3">${this.typeNameMap[3]}</option>
+                            <option value="5">${this.typeNameMap[5]}</option>
+                        </select>
                     </div>
                     <div class="form-group text-right">
                         <button id="savePointButton" class="btn btn-primary">Save</button>
@@ -185,15 +242,6 @@ class MapModal {
             </div>`;
             document.getElementById(this.mapContainerId).insertAdjacentHTML('beforeend', formHtml);
 
-            const typeSelectElement = document.getElementById("poiTypeSelect");
-            typeSelectElement.innerHTML = '';
-            for (const [id, name] of Object.entries(this.typeNameMap)) {
-                const option = document.createElement('option');
-                option.value = id;
-                option.textContent = name;
-                typeSelectElement.appendChild(option);
-            }
-
             const formElement = document.getElementById('pointForm');
             DomEvent.disableClickPropagation(formElement);
 
@@ -225,53 +273,59 @@ class MapModal {
         document.removeEventListener('click', this.handleClickOutsidePointForm.bind(this), true);
     }
 
+    createFeatureForPoint(lng, lat, poiName, poiType) {
+        return {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [lng, lat]
+            },
+            "properties": {
+                "pointOfInterestName": poiName,
+                "pointOfInterestTypeId": poiType
+            }
+        };
+    }
+
+    addNewPointToMap(point) {
+        console.info("Add new point to map", point);
+        return geoJSON(point, {
+            pointToLayer: (feature, latlng) => {
+                return circleMarker(latlng, {
+                    radius: 8,
+                    fillColor: 'green',
+                    color: '#000',
+                    weight: 1,
+                    opacity: 1,
+                    fillOpacity: 0.8
+                });
+            },
+            onEachFeature: (feature, layer) => {
+                layer.bindPopup(this.popupContent(feature));
+                layer.on('click', () => this.selectPoint(feature, layer));
+            }
+        }).addTo(this.map);
+    }
+
     savePoint(lat, lng) {
-        const nameElement = document.getElementById('name');
-        const altitudeElement = document.getElementById('altitude');
-        const poiTypeSelectElement = document.getElementById("poiTypeSelect")
+        const poiNameElement = document.getElementById('name');
+        const poiTypeElement = document.getElementById("poiTypeSelect")
 
-        if (nameElement && poiTypeSelectElement) {
-            const poiName = nameElement.value;
-            const poiAltitude = altitudeElement.value;
-            const poiType = poiTypeSelectElement.value;
+        if (poiNameElement && poiTypeElement) {
+            const poiName = poiNameElement.value;
+            const poiType = parseInt(poiTypeElement.value, 10);
 
+            // There should only be one newly created point available
             if (this.createdPointLayer) {
                 this.map.removeLayer(this.createdPointLayer);
             }
-
-            const newPoint = {
-                "type": "Feature",
-                "geometry": {
-                    "type": "Point",
-                    "coordinates": [lng, lat]
-                },
-                "properties": {
-                    "pointOfInterestName": poiName,
-                    "pointOfInterestAltitude": poiAltitude,
-                    "pointOfInterestTypeId": poiType
-                }
-            };
             if (this.createdPoints.length > 0) {
-                this.createdPoints.pop(); // Remove the last created point from the list
+                this.createdPoints.pop();
             }
-            this.createdPoints.push(newPoint);
-            this.createdPointLayer = geoJSON(newPoint, {
-                pointToLayer: (feature, latlng) => {
-                    return circleMarker(latlng, {
-                        radius: 8,
-                        fillColor: 'green',
-                        color: '#000',
-                        weight: 1,
-                        opacity: 1,
-                        fillOpacity: 0.8
-                    });
-                },
-                onEachFeature: (feature, layer) => {
-                    layer.bindPopup(this.popupContent(feature));
-                    layer.on('click', () => this.selectPoint(feature, layer));
-                }
-            }).addTo(this.map);
 
+            const newPoint = this.createFeatureForPoint(lng, lat, poiName, poiType);
+            this.createdPoints.push(newPoint);
+            this.createdPointLayer = this.addNewPointToMap(newPoint);
             this.createdPointLayer.eachLayer((layer) => {
                 this.selectPoint(newPoint, layer);
             });
@@ -291,6 +345,33 @@ class MapModal {
                 </div>`;
     }
 
+    /**
+     * Function is called when newly created point of interest is successfully persisted to database.
+     * @param pointOfInterestId
+     */
+    saveSuccess(pointOfInterestId) {
+        if (this.createdPoints.length < 1 || !this.createdPointLayer) {
+            console.error('No newly created points, unable to update with pointOfInterestId=' + pointOfInterestId);
+            return;
+        }
+        let latestCreatedFeature = this.createdPoints[0];
+        latestCreatedFeature.properties.pointOfInterestId = pointOfInterestId;
+        this.geoJsonData.features.push(latestCreatedFeature);
+        this.createdPoints = [];
+
+        this.map.removeLayer(this.createdPointLayer);
+        this.addNewPointToMap(latestCreatedFeature);
+        this.createdPointLayer = null;
+
+        console.info("this.createdPoints", this.createdPoints);
+        console.info("this.createdPointLayer", this.createdPointLayer);
+        console.info("this.geoJsonData.features", this.geoJsonData.features);
+    }
+
+    setSelectedLocation(selectedValue) {
+        this.selectPointById(selectedValue);
+    }
+
     openModal(points) {
         document.getElementById('mapModal').style.display = 'block';
         this.initMap();
diff --git a/src/main/webapp/templates/forecastConfigurationForm.ftl b/src/main/webapp/templates/forecastConfigurationForm.ftl
index b07241b2..b93f95c2 100755
--- a/src/main/webapp/templates/forecastConfigurationForm.ftl
+++ b/src/main/webapp/templates/forecastConfigurationForm.ftl
@@ -36,57 +36,80 @@
     </script>
     <script type="module">
         import MapModal from '/js/mapModal.js';
-        function callbackUpdateLocationPointOfInterest(pointData) {
+        function callbackPersistNewPoint(pointData) {
+            const userId = ${user.userId};
+            const params = {
+                'name': pointData.name,
+                'typeId': pointData.typeId,
+                'longitude': pointData.longitude,
+                'latitude': pointData.latitude,
+                'altitude': '0', // default value - populate using a service for getting altitude for coordinates?
+                'userId': userId,
+            }
+            $.ajax({
+                url: "/rest/poi",
+                type: "POST",
+                contentType: "application/json",
+                data: JSON.stringify(params),
+                success: function(response) {
+                    addOption(response.pointOfInterestId, response.name);
+                    mapModalInstance.saveSuccess(response.pointOfInterestId);
+                    console.info("Success:", response);
+                },
+                error: function(jqXHR, textStatus, errorThrown) {
+                    console.error("Error:", textStatus, errorThrown);
+                }
+            });
+        }
+        function callbackUpdateLocationPointOfInterest(pointOfInterestId) {
             const selectBox = document.querySelector('select[name="locationPointOfInterestId"]');
-            if(pointData.id) {
+            if(pointOfInterestId) {
                 let optionFound = false;
                 for (let i = 0; i < selectBox.options.length; i++) {
-                    if (selectBox.options[i].value == pointData.id) {
+                    if (selectBox.options[i].value == pointOfInterestId) {
                         selectBox.selectedIndex = i; // Select the matching option
                         optionFound = true;
                         break;
                     }
                 }
                 if (!optionFound) {
-                    console.error("No matching option found for feature.id:", pointData.id);
-                }
-            } else {
-                const userId = ${user.userId};
-                const params = {
-                    'name': pointData.name,
-                    'typeId': pointData.typeId,
-                    'longitude': pointData.longitude,
-                    'latitude': pointData.latitude,
-                    'altitude': pointData.altitude,
-                    'userId': userId,
+                    console.error("No matching option found for poi.id:", pointOfInterestId);
                 }
-                $.ajax({
-                    url: "/rest/poi",
-                    type: "POST",
-                    contentType: "application/json",
-                    data: JSON.stringify(params),
-                    success: function(response) {
-                        console.info("Success:", response);
-
-                    },
-                    error: function(jqXHR, textStatus, errorThrown) {
-                        console.error("Error:", textStatus, errorThrown);
-                    }
-                });
             }
         }
+        // TODO Ensure options are sorted alphabetically..?
+        function addOption(pointOfInterestId, name) {
+            let selectElement = document.querySelector('select[name="locationPointOfInterestId"]');
+            let newOption = document.createElement("option");
+            newOption.value = pointOfInterestId;
+            newOption.text = name;
+            selectElement.insertBefore(newOption, selectElement.firstChild);
+            selectElement.value = pointOfInterestId;
+        }
         const typeNameMap = {
+            0: "${i18nBundle["pointOfInterestType_0"]}",
+            1: "${i18nBundle["pointOfInterestType_1"]}",
             2: "${i18nBundle["pointOfInterestType_2"]}",
             3: "${i18nBundle["pointOfInterestType_3"]}",
-            5: "${i18nBundle["pointOfInterestType_5"]}"
+            5: "${i18nBundle["pointOfInterestType_5"]}",
+            6: "${i18nBundle["pointOfInterestType_6"]}",
+            7: "${i18nBundle["pointOfInterestType_7"]}"
         };
 
         const poiGeoJson = JSON.parse('${locationPointOfInterestsGeoJson?json_string}');
         const stationGeoJson = JSON.parse('${weatherStationPointOfInterestsGeoJson?json_string}')
-        const mapModalInstance = new MapModal('mapContainer', typeNameMap, poiGeoJson, true, callbackUpdateLocationPointOfInterest);
+        const mapModalInstance = new MapModal('mapContainer', typeNameMap, poiGeoJson, true, callbackPersistNewPoint, callbackUpdateLocationPointOfInterest);
         window.mapModalInstance = mapModalInstance;
 
-        window.openModal = () => window.mapModalInstance && window.mapModalInstance.openModal();
+        window.openModal = () => {
+            const selectElement = document.querySelector('select[name="locationPointOfInterestId"]');
+            const selectedOption = selectElement.options[selectElement.selectedIndex];
+            if(selectedOption.value && selectedOption.value !== '-1') {
+                window.mapModalInstance.setSelectedLocation(selectedOption.value);
+            }
+            window.mapModalInstance.openModal();
+        };
+
         window.closeModal = () => window.mapModalInstance && window.mapModalInstance.closeModal();
     </script>
 	<script type="text/javascript">
-- 
GitLab