Skip to content
Snippets Groups Projects
Commit c5b03ad4 authored by Lene Wasskog's avatar Lene Wasskog
Browse files

feat: Newly created points available in dropdown and map

parent 49dfd273
Branches
No related tags found
1 merge request!191Add map module and Open-Meteo support
...@@ -2,9 +2,8 @@ ...@@ -2,9 +2,8 @@
import { import {
map, map,
tileLayer, tileLayer,
marker,
geoJSON, geoJSON,
circleMarker, circleMarker, GeoJSON,
DomEvent DomEvent
} from 'https://unpkg.com/leaflet/dist/leaflet-src.esm.js'; } from 'https://unpkg.com/leaflet/dist/leaflet-src.esm.js';
...@@ -14,18 +13,29 @@ import { ...@@ -14,18 +13,29 @@ import {
*/ */
class MapModal { 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.mapContainerId = mapContainerId;
this.typeNameMap = typeNameMap; this.typeNameMap = typeNameMap;
this.geojsonData = geoJsonData; this.geoJsonData = geoJsonData;
this.allowNewPoints = allowNewPoints; this.allowNewPoints = allowNewPoints;
this.callbackOnPersistNew = callbackOnPersistNew;
this.callbackOnClose = callbackOnClose;
this.map = null; this.map = null;
this.isMapInitialized = false; this.isMapInitialized = false;
this.selectedPointLayer = null; this.selectedPointLayer = null;
this.createdPointLayer = null; this.createdPointLayer = null;
this.createdPoints = []; this.createdPoints = [];
this.callback = callback;
// Colours for the available types of pois
this.typeColorMap = { this.typeColorMap = {
0: "#B3CDE0", // Light blue 0: "#B3CDE0", // Light blue
1: "#FBB4AE", // Soft pink 1: "#FBB4AE", // Soft pink
...@@ -42,7 +52,6 @@ class MapModal { ...@@ -42,7 +52,6 @@ class MapModal {
if (!this.isMapInitialized) { if (!this.isMapInitialized) {
// Initialize the map centered on Norway // Initialize the map centered on Norway
this.map = map(this.mapContainerId).setView([63.4226, 10.3951], 5); 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', { tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19 maxZoom: 19
}).addTo(this.map); }).addTo(this.map);
...@@ -50,7 +59,7 @@ class MapModal { ...@@ -50,7 +59,7 @@ class MapModal {
this.createSelectedPointInfo(); this.createSelectedPointInfo();
// Add points to the map // Add points to the map
geoJSON(this.geojsonData, { geoJSON(this.geoJsonData, {
filter: (feature) => feature.geometry.type === "Point", filter: (feature) => feature.geometry.type === "Point",
pointToLayer: (feature, latlng) => { pointToLayer: (feature, latlng) => {
const color = this.typeColorMap[feature.properties.pointOfInterestTypeId] || "#3498DB"; const color = this.typeColorMap[feature.properties.pointOfInterestTypeId] || "#3498DB";
...@@ -78,19 +87,25 @@ class MapModal { ...@@ -78,19 +87,25 @@ class MapModal {
} }
} }
/**
* Create information panel for selected point, initially hidden.
*/
createSelectedPointInfo() { createSelectedPointInfo() {
console.debug("Create information panel for selected point, initially hidden");
const selectedPointInfoHtml = ` const selectedPointInfoHtml = `
<div id="selectedPointInfo" style="display: none;"> <div id="selectedPointInfo" style="display: none;">
<div id="infoMessage"></div> <div id="infoMessage"></div>
<button id="confirmButton" class="btn btn-primary">OK</button> <button id="confirmButton" class="btn btn-primary">Velg sted</button>
</div> </div>`;
` ;
document.getElementById(this.mapContainerId).insertAdjacentHTML('beforeend', selectedPointInfoHtml); 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; const name = feature.properties.pointOfInterestName;
console.info("Display selected point " + name);
const type = this.typeNameMap[feature.properties.pointOfInterestTypeId]; const type = this.typeNameMap[feature.properties.pointOfInterestTypeId];
const coordinates = feature.geometry.coordinates[1].toFixed(5) + ", " + feature.geometry.coordinates[0].toFixed(5); const coordinates = feature.geometry.coordinates[1].toFixed(5) + ", " + feature.geometry.coordinates[0].toFixed(5);
...@@ -107,7 +122,43 @@ class MapModal { ...@@ -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 // Deselect previously selected point, if any
if (this.selectedPointLayer) { if (this.selectedPointLayer) {
const color = this.typeColorMap[feature.properties.pointOfInterestTypeId] || "#3498DB"; const color = this.typeColorMap[feature.properties.pointOfInterestTypeId] || "#3498DB";
...@@ -127,35 +178,41 @@ class MapModal { ...@@ -127,35 +178,41 @@ class MapModal {
color: '#f00' color: '#f00'
}); });
this.updateSelectedPointInfo(feature) if (zoomInToSelected) {
const latLng = layer.getLatLng();
this.map.setView(latLng, 10);
}
this.displaySelectedPointInfo(feature)
this.selectedPointLayer = layer; this.selectedPointLayer = layer;
} }
confirmSelection(feature) { confirmSelection(feature) {
console.info("Save pointData", feature); console.info("Confirm selection", feature);
const pointData = { let poiId = feature.properties.pointOfInterestId;
id: feature.properties.pointOfInterestId, if (!poiId && typeof this.callbackOnPersistNew === 'function') {
name: feature.properties.pointOfInterestName, const pointData = {
typeId: feature.properties.pointOfInterestTypeId, name: feature.properties.pointOfInterestName,
altitude: feature.properties.pointOfInterestAltitude, typeId: feature.properties.pointOfInterestTypeId,
longitude: feature.geometry.coordinates[0], longitude: feature.geometry.coordinates[0],
latitude: feature.geometry.coordinates[1] latitude: feature.geometry.coordinates[1]
}; };
if (typeof this.callback === 'function') { console.info("Persist new", pointData);
console.info("Callback!", pointData); this.callbackOnPersistNew(pointData);
this.callback(pointData); }
if (typeof this.callbackOnClose === 'function') {
this.callbackOnClose(poiId);
console.info("Goodbye from map modal!")
} }
this.closeModal(); this.closeModal();
} }
enablePointCreation() { enablePointCreation() {
this.map.on('click', (e) => { this.map.on('click', (e) => {
console.log("Map clicked, enabling point creation...");
const latlng = e.latlng; const latlng = e.latlng;
// If a form already exists, remove it // If a form already exists, remove it
this.closeNewPointFormIfOpen(); this.closeNewPointFormIfOpen();
// Calculate the pixel position from the map's click event // Calculate the pixel position from the map's click event
const containerPoint = this.map.latLngToContainerPoint(latlng); const containerPoint = this.map.latLngToContainerPoint(latlng);
...@@ -168,15 +225,15 @@ class MapModal { ...@@ -168,15 +225,15 @@ class MapModal {
<div class="panel-body"> <div class="panel-body">
<div class="form-group"> <div class="form-group">
<label for="name">Name:</label> <label for="name">Name:</label>
<input type="text" class="form-control" id="name" name="name"> <input type="text" class="form-control" id="name" name="name" autofocus>
</div>
<div class="form-group">
<label for="altitude">Altitude:</label>
<input type="text" class="form-control" id="altitude" name="altitude">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="poiTypeSelect">Type:</label> <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>
<div class="form-group text-right"> <div class="form-group text-right">
<button id="savePointButton" class="btn btn-primary">Save</button> <button id="savePointButton" class="btn btn-primary">Save</button>
...@@ -185,15 +242,6 @@ class MapModal { ...@@ -185,15 +242,6 @@ class MapModal {
</div>`; </div>`;
document.getElementById(this.mapContainerId).insertAdjacentHTML('beforeend', formHtml); 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'); const formElement = document.getElementById('pointForm');
DomEvent.disableClickPropagation(formElement); DomEvent.disableClickPropagation(formElement);
...@@ -225,53 +273,59 @@ class MapModal { ...@@ -225,53 +273,59 @@ class MapModal {
document.removeEventListener('click', this.handleClickOutsidePointForm.bind(this), true); 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) { savePoint(lat, lng) {
const nameElement = document.getElementById('name'); const poiNameElement = document.getElementById('name');
const altitudeElement = document.getElementById('altitude'); const poiTypeElement = document.getElementById("poiTypeSelect")
const poiTypeSelectElement = document.getElementById("poiTypeSelect")
if (nameElement && poiTypeSelectElement) { if (poiNameElement && poiTypeElement) {
const poiName = nameElement.value; const poiName = poiNameElement.value;
const poiAltitude = altitudeElement.value; const poiType = parseInt(poiTypeElement.value, 10);
const poiType = poiTypeSelectElement.value;
// There should only be one newly created point available
if (this.createdPointLayer) { if (this.createdPointLayer) {
this.map.removeLayer(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) { 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.createdPointLayer.eachLayer((layer) => {
this.selectPoint(newPoint, layer); this.selectPoint(newPoint, layer);
}); });
...@@ -291,6 +345,33 @@ class MapModal { ...@@ -291,6 +345,33 @@ class MapModal {
</div>`; </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) { openModal(points) {
document.getElementById('mapModal').style.display = 'block'; document.getElementById('mapModal').style.display = 'block';
this.initMap(); this.initMap();
......
...@@ -36,57 +36,80 @@ ...@@ -36,57 +36,80 @@
</script> </script>
<script type="module"> <script type="module">
import MapModal from '/js/mapModal.js'; 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"]'); const selectBox = document.querySelector('select[name="locationPointOfInterestId"]');
if(pointData.id) { if(pointOfInterestId) {
let optionFound = false; let optionFound = false;
for (let i = 0; i < selectBox.options.length; i++) { 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 selectBox.selectedIndex = i; // Select the matching option
optionFound = true; optionFound = true;
break; break;
} }
} }
if (!optionFound) { if (!optionFound) {
console.error("No matching option found for feature.id:", pointData.id); console.error("No matching option found for poi.id:", pointOfInterestId);
}
} else {
const userId = ${user.userId};
const params = {
'name': pointData.name,
'typeId': pointData.typeId,
'longitude': pointData.longitude,
'latitude': pointData.latitude,
'altitude': pointData.altitude,
'userId': userId,
} }
$.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 = { const typeNameMap = {
0: "${i18nBundle["pointOfInterestType_0"]}",
1: "${i18nBundle["pointOfInterestType_1"]}",
2: "${i18nBundle["pointOfInterestType_2"]}", 2: "${i18nBundle["pointOfInterestType_2"]}",
3: "${i18nBundle["pointOfInterestType_3"]}", 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 poiGeoJson = JSON.parse('${locationPointOfInterestsGeoJson?json_string}');
const stationGeoJson = JSON.parse('${weatherStationPointOfInterestsGeoJson?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.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(); window.closeModal = () => window.mapModalInstance && window.mapModalInstance.closeModal();
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment