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