diff --git a/src/main/java/no/nibio/vips/logic/service/POIService.java b/src/main/java/no/nibio/vips/logic/service/POIService.java index 399cb26acf2078c0d0c4e9da7b48020e75605359..8f75290e2ae43490f406dbf1b7c24b1cc51d5a41 100644 --- a/src/main/java/no/nibio/vips/logic/service/POIService.java +++ b/src/main/java/no/nibio/vips/logic/service/POIService.java @@ -19,21 +19,13 @@ package no.nibio.vips.logic.service; import java.io.IOException; import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -48,9 +40,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.webcohesion.enunciate.metadata.Facet; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import java.util.Arrays; + import java.util.stream.Collectors; -import javax.ws.rs.QueryParam; import no.nibio.vips.gis.GISUtil; import no.nibio.vips.logic.entity.Country; @@ -109,7 +100,6 @@ public class POIService { @Consumes("application/json;charset=UTF-8") @Produces("application/json;charset=UTF-8") public Response postPoi(String poiJson) { - // TODO Fix authentication ObjectMapper oM = new ObjectMapper(); Map<Object, Object> poiMap; try { @@ -119,17 +109,14 @@ public class POIService { return Response.status(Status.BAD_REQUEST).entity("Unable to parse input").build(); } - Integer poiUserId = poiMap.get("userId") != null ? Integer.parseInt(poiMap.get("userId").toString()) : null; - - VipsLogicUser user = SessionControllerGetter.getUserBean().getVipsLogicUser(poiUserId); + VipsLogicUser user = SessionControllerGetter.getUserBean().getUserFromUUID(httpServletRequest); if (user == null) { - LOGGER.error("No user found for userId={}", poiUserId); + LOGGER.error("No user found for UUID in Authorization"); return Response.status(Status.UNAUTHORIZED).build(); } LOGGER.error("Remember to check for roles as well, if necessary!"); PointOfInterestBean poiBean = SessionControllerGetter.getPointOfInterestBean(); - Integer poiTypeId = poiMap.get("typeId") != null ? Integer.parseInt(poiMap.get("typeId").toString()) : null; if(poiTypeId == null) { return Response.status(Status.BAD_REQUEST).entity("Point of interest type is required").build(); @@ -154,7 +141,6 @@ public class POIService { Point p3d = gisUtil.createPointWGS84(coordinate); poiToSave.setGisGeom(p3d); } - poiToSave = poiBean.storePoi(poiToSave); if (poiToSave != null) { @@ -189,6 +175,19 @@ public class POIService { return Response.ok().entity(SessionControllerGetter.getPointOfInterestBean().getPoisAsGeoJson(pois)).build(); } + @POST + @Path("geojson") + @Produces("application/json;charset=UTF-8") + @Consumes(MediaType.APPLICATION_JSON) + public Response getPoisGeoJson(Set<Integer> ids) { + // Retrieve POIs from data source + List<PointOfInterest> pois = SessionControllerGetter.getPointOfInterestBean().getPois(ids); + if (pois == null || pois.isEmpty()) { + return Response.noContent().build(); + } + return Response.ok(SessionControllerGetter.getPointOfInterestBean().getPoisAsGeoJson(pois)).build(); + } + /** * Find a POI (Point of interest) by name * diff --git a/src/main/webapp/css/mapModal.css b/src/main/webapp/css/mapModal.css index dd9694b061060a316023b1f9ae9faabe4f3695be..12a63250166a164ab6758a91abb75025de46af77 100644 --- a/src/main/webapp/css/mapModal.css +++ b/src/main/webapp/css/mapModal.css @@ -1,4 +1,4 @@ -.modal { +.map-modal { display: none; position: fixed; z-index: 1000; @@ -10,7 +10,7 @@ background-color: rgba(0, 0, 0, 0.9); } -.modal-content { +.map-modal-content { position: relative; height: 100%; width: 100%; diff --git a/src/main/webapp/js/mapModal.js b/src/main/webapp/js/mapModal.js index 1e522eacf17b7b393b943accd3c2f79cc89755b7..0ba8dcee50fedfbdc30ee4cc01d9efb82efa97c1 100644 --- a/src/main/webapp/js/mapModal.js +++ b/src/main/webapp/js/mapModal.js @@ -16,19 +16,18 @@ import { class MapModal { /** - * @param mapContainerId The id of the HTML element to which the map should be added + * @param mapModalId The id of the HTML element in which the modal should be opened * @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; + constructor(mapModalId, typeNameMap, geoJsonData, allowNewPoints = false, callbackOnClose = null) { + this.mapModalId = mapModalId; + this.mapContainerId = mapModalId + "-container"; this.typeNameMap = typeNameMap; this.geoJsonData = geoJsonData; this.allowNewPoints = allowNewPoints; - this.callbackOnPersistNew = callbackOnPersistNew; this.callbackOnClose = callbackOnClose; this.map = null; @@ -57,6 +56,7 @@ class MapModal { tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(this.map); + console.info("Create map " + this.mapContainerId + " with points", this.geoJsonData); this.setUpSelectedPointInfoPanel(); this.setUpZoomToCurrentLocation(); @@ -208,19 +208,15 @@ class MapModal { confirmSelection(feature) { console.info("Confirm selection", feature); - let poiId = feature.properties.pointOfInterestId; - if (!poiId && typeof this.callbackOnPersistNew === 'function') { + if (typeof this.callbackOnClose === 'function') { const pointData = { + id: feature.properties.pointOfInterestId, 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); + this.callbackOnClose(pointData); console.info("Goodbye from map modal!") } this.closeModal(); @@ -423,15 +419,20 @@ class MapModal { } openModal(selectedPointOfInterestId) { + document.getElementById(this.mapModalId).style.display = 'block'; + this.initMap(); if(selectedPointOfInterestId) { this.selectPointById(selectedPointOfInterestId); } - document.getElementById('mapModal').style.display = 'block'; - this.initMap(); } closeModal() { - document.getElementById('mapModal').style.display = 'none'; + document.getElementById(this.mapModalId).style.display = 'none'; + if (this.map) { + console.info("Remove map"); + this.map.remove(); + this.map = null; + } } } diff --git a/src/main/webapp/templates/forecastConfigurationForm.ftl b/src/main/webapp/templates/forecastConfigurationForm.ftl index c574fa7f0d5b5c9069bdfdc4945925cb955d5bb3..3ef4e59cbad08ecd0374c5440af0b5877102c6c0 100755 --- a/src/main/webapp/templates/forecastConfigurationForm.ftl +++ b/src/main/webapp/templates/forecastConfigurationForm.ftl @@ -37,73 +37,85 @@ </script> <script type="module"> import MapModal from '/js/mapModal.js'; - 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); - } + + // Read the list of locationPointOfInterest into javascript array, to be able to dynamically manipulate it + let poiList = [ + <#list locationPointOfInterests as poi> + { + "pointOfInterestId": "${poi.pointOfInterestId}", + "name": "${poi.name?json_string}", + "latitude": "${poi.latitude!''}", + "longitude": "${poi.longitude!''}", + "pointOfInterestTypeId": "${poi.pointOfInterestTypeId!''}" + }<#if poi_has_next>,</#if> + </#list> + ]; + renderLocationPointOfInterestSelect(poiList); + + function renderLocationPointOfInterestSelect(elementList, selectedId) { + let selectElement = document.querySelector('select[name="locationPointOfInterestId"]'); + elementList.forEach(poi => { + let option = document.createElement('option'); + option.value = poi.pointOfInterestId; + option.textContent = poi.name; + selectElement.appendChild(option); }); + selectPointOfInterest(selectedId); } - function callbackUpdateLocationPointOfInterest(pointOfInterestId) { - const selectBox = document.querySelector('select[name="locationPointOfInterestId"]'); - if(pointOfInterestId) { + + function selectPointOfInterest(selectedId) { + const selectElement = document.querySelector('select[name="locationPointOfInterestId"]'); + if(selectedId) { let optionFound = false; - for (let i = 0; i < selectBox.options.length; i++) { - if (selectBox.options[i].value == pointOfInterestId) { - selectBox.selectedIndex = i; // Select the matching option + for (let i = 0; i < selectElement.options.length; i++) { + if (selectElement.options[i].value == selectedId) { + selectElement.selectedIndex = i; optionFound = true; break; } } if (!optionFound) { - console.error("No matching option found for poi.id:", pointOfInterestId); + console.error("No matching option found for poi.id:", selectedId); } } } - // 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; + + function persistNewPoint(pointData) { + 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? + } + fetch("/rest/poi", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': '${user.userUuid}' + }, + body: JSON.stringify(params) + }) + .then(response => response.json()) + .then(pointOfInterest => { + poiList.push(pointOfInterest); + renderLocationPointOfInterestSelect(poiList, pointOfInterest.pointOfInterestId); + console.info("Point of interest successfully persisted", pointOfInterest); + }) + .catch(error => { + console.error("Unable to persist new point of interest", error); + }); } - const typeNameMap = { - 0: "${i18nBundle["pointOfInterestType_0"]}", - 1: "${i18nBundle["pointOfInterestType_1"]}", - 2: "${i18nBundle["pointOfInterestType_2"]}", - 3: "${i18nBundle["pointOfInterestType_3"]}", - 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, callbackPersistNewPoint, callbackUpdateLocationPointOfInterest); - window.mapModalInstance = mapModalInstance; + function callbackOnClose(pointOfInterestData) { + if(!pointOfInterestData.id) { + persistNewPoint(pointOfInterestData); + } + selectPointOfInterest(pointOfInterestData.id); + } - // If poi is selected, send id to map modal before opening - window.openModal = () => { + function getSelectedPointOfInterestId() { const selectElement = document.querySelector('select[name="locationPointOfInterestId"]'); const selectedOption = selectElement.options[selectElement.selectedIndex]; @@ -115,7 +127,37 @@ selectedPointOfInterestId = parsedValue; } } - window.mapModalInstance.openModal(selectedPointOfInterestId); + return selectedPointOfInterestId; + } + + const typeNameMap = { + 0: "${i18nBundle["pointOfInterestType_0"]}", + 1: "${i18nBundle["pointOfInterestType_1"]}", + 2: "${i18nBundle["pointOfInterestType_2"]}", + 3: "${i18nBundle["pointOfInterestType_3"]}", + 5: "${i18nBundle["pointOfInterestType_5"]}", + 6: "${i18nBundle["pointOfInterestType_6"]}", + 7: "${i18nBundle["pointOfInterestType_7"]}" + }; + + window.openModal = () => { + let poiIds = poiList.map(poi => poi.pointOfInterestId); + fetch("/rest/poi/geojson", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(poiIds) + }) + .then(response => response.json()) + .then(geoJson => { + window.mapModalInstance = new MapModal('map-modal', typeNameMap, geoJson, true, callbackOnClose); + window.mapModalInstance.openModal(getSelectedPointOfInterestId()); + }) + .catch(error => { + console.error('Unable to convert poi list to GeoJson, cannot open map', error); + }); }; window.closeModal = () => window.mapModalInstance && window.mapModalInstance.closeModal(); @@ -407,16 +449,13 @@ <div class="select-container" style="flex: 1; display: flex; align-items: center;"> <select class="form-control" id="locationPointOfInterestId" name="locationPointOfInterestId" onblur="validateField(this);" style="width: calc(100% - 30px);"> <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.locationPointOfInterestId?lower_case}</option> - <#list locationPointOfInterests?sort_by("name") as poi> - <option value="${poi.pointOfInterestId}"<#if forecastConfiguration.locationPointOfInterestId?has_content && poi.pointOfInterestId == forecastConfiguration.locationPointOfInterestId.pointOfInterestId> selected="selected"</#if>>${poi.name}</option> - </#list> </select> <i id="open-map-modal-icon" class="fa fa-map-marker" onclick="openModal()"></i> </div> - <div id="mapModal" class="modal"> - <div class="modal-content"> + <div id="map-modal" class="map-modal"> + <div class="map-modal-content"> <span class="close-button" onclick="closeModal()">×</span> - <div id="mapContainer" style="height: 100vh; width: 100%; position: relative;"></div> + <div id="map-modal-container" style="height: 100vh; width: 100%; position: relative;"></div> </div> </div> <span class="help-block" id="${formId}_locationPointOfInterestId_validation"></span>