'use strict'; (function () { var MapEditor = { metadata: { name: null, description: null }, map: null, panorama: null, selectedMarker: null, added: {}, edited: {}, deleted: {}, editMetadata: function () { var form = document.getElementById('metadataForm'); MapEditor.metadata.name = form.elements.name.value; MapEditor.metadata.description = form.elements.description.value; document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]'; MapGuesser.hideModal(); document.getElementById('saveButton').disabled = false; }, getPlace: function (placeId, marker) { MapGuesser.httpRequest('GET', '/admin/place.json/' + placeId, function () { document.getElementById('loading').style.visibility = 'hidden'; if (!this.response.panoId) { document.getElementById('noPano').style.visibility = 'visible'; places[marker.placeId].panoId = -1; places[marker.placeId].noPano = true; return; } places[marker.placeId].panoId = this.response.panoId; places[marker.placeId].noPano = false; MapEditor.loadPano(this.response.panoId, places[marker.placeId].pov); }); }, loadPano: function (panoId, pov) { MapEditor.panorama.setVisible(true); MapEditor.panorama.setPov({ heading: pov.heading, pitch: pov.pitch }); MapEditor.panorama.setZoom(pov.zoom); MapEditor.panorama.setPano(panoId); }, loadPanoForNewPlace: function (panoLocationData) { var placeId = MapEditor.selectedMarker.placeId; if (!panoLocationData) { places[placeId].panoId = -1; places[placeId].noPano = true; document.getElementById('noPano').style.visibility = 'visible'; return; } var latLng = panoLocationData.latLng; places[placeId].panoId = panoLocationData.pano; places[placeId].lat = latLng.lat(); places[placeId].lng = latLng.lng(); MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng }); MapEditor.map.panTo(MapEditor.selectedMarker.getLatLng()); MapEditor.panorama.setVisible(true); MapEditor.panorama.setPov({ heading: 0.0, pitch: 0.0 }); MapEditor.panorama.setZoom(0.0); MapEditor.panorama.setPano(panoLocationData.pano); }, requestPanoData: function (location) { var sv = new google.maps.StreetViewService(); sv.getPanorama({ location: location, preference: google.maps.StreetViewPreference.NEAREST, radius: MapEditor.map.getSearchRadius(location), }, function (data, status) { var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null; document.getElementById('loading').style.visibility = 'hidden'; MapEditor.loadPanoForNewPlace(panoLocationData); }); }, select: function (marker) { if (MapEditor.selectedMarker === marker) { MapEditor.closePlace(); return; } document.getElementById('map').classList.add('selected'); document.getElementById('control').classList.add('selected'); document.getElementById('noPano').style.visibility = 'hidden'; document.getElementById('panorama').style.visibility = 'visible'; document.getElementById('placeControl').style.visibility = 'visible'; MapEditor.resetSelected(); MapEditor.selectedMarker = marker; MapEditor.map.resize(); MapEditor.map.panTo(marker.getLatLng()); MapEditor.panorama.setVisible(false); if (marker.placeId) { MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue); document.getElementById('deleteButton').style.display = 'block'; if (places[marker.placeId].panoId) { if (places[marker.placeId].panoId === -1) { document.getElementById('noPano').style.visibility = 'visible'; } else { MapEditor.loadPano(places[marker.placeId].panoId, places[marker.placeId].pov); } return; } document.getElementById('loading').style.visibility = 'visible'; MapEditor.getPlace(marker.placeId, marker); } else { marker.placeId = 'new_' + new Date().getTime(); var latLng = marker.getLatLng(); places[marker.placeId] = { id: null, lat: latLng.lat, lng: latLng.lng, panoId: null, pov: { heading: 0.0, pitch: 0.0, zoom: 0 }, noPano: false }; document.getElementById('loading').style.visibility = 'visible'; MapEditor.requestPanoData(latLng); } }, resetSelected: function (del) { if (!MapEditor.selectedMarker) { return; } var placeId = MapEditor.selectedMarker.placeId if (places[placeId].id && !del) { MapEditor.map.changeMarkerIcon( MapEditor.selectedMarker, places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen ); } else { delete places[placeId]; MapEditor.map.removeMarker(MapEditor.selectedMarker); } document.getElementById('deleteButton').style.display = 'none'; }, applyPlace: function () { var placeId = MapEditor.selectedMarker.placeId; if (!places[placeId].noPano) { var latLng = MapEditor.panorama.getPosition(); var pov = MapEditor.panorama.getPov(); var zoom = MapEditor.panorama.getZoom(); places[placeId].lat = latLng.lat(); places[placeId].lng = latLng.lng(); places[placeId].panoId = MapEditor.panorama.getPano(); places[placeId].pov = { heading: pov.heading, pitch: pov.pitch, zoom: zoom }; } if (!places[placeId].id) { places[placeId].id = placeId; MapEditor.added[placeId] = places[placeId]; document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length); document.getElementById('deleteButton').style.display = 'block'; } else { if (!MapEditor.added[placeId]) { MapEditor.edited[placeId] = places[placeId]; document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length); } else { MapEditor.added[placeId] = places[placeId]; } } MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng }); document.getElementById('saveButton').disabled = false; }, closePlace: function (del) { document.getElementById('map').classList.remove('selected'); document.getElementById('control').classList.remove('selected'); document.getElementById('noPano').style.visibility = 'hidden'; document.getElementById('panorama').style.visibility = 'hidden'; document.getElementById('placeControl').style.visibility = 'hidden'; MapEditor.resetSelected(del); MapEditor.selectedMarker = null; MapEditor.map.resize(); }, deletePlace: function () { var placeId = MapEditor.selectedMarker.placeId; if (places[placeId].id && !MapEditor.added[placeId]) { MapEditor.deleted[placeId] = places[placeId]; document.getElementById('deleted').innerHTML = String(Object.keys(MapEditor.deleted).length); } MapEditor.closePlace(true); delete MapEditor.added[placeId]; delete MapEditor.edited[placeId]; delete places[placeId]; document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length); document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length); document.getElementById('saveButton').disabled = false; }, saveMap: function () { document.getElementById('loading').style.visibility = 'visible'; var data = new FormData(); if (MapEditor.metadata.name !== null) { data.append('name', MapEditor.metadata.name); } if (MapEditor.metadata.description !== null) { data.append('description', MapEditor.metadata.description); } for (var placeId in MapEditor.added) { if (!MapEditor.added.hasOwnProperty(placeId)) { continue; } data.append('added[]', JSON.stringify(MapEditor.added[placeId])); } for (var placeId in MapEditor.edited) { if (!MapEditor.edited.hasOwnProperty(placeId)) { continue; } data.append('edited[]', JSON.stringify(MapEditor.edited[placeId])); } for (var placeId in MapEditor.deleted) { if (!MapEditor.deleted.hasOwnProperty(placeId)) { continue; } data.append('deleted[]', JSON.stringify(MapEditor.deleted[placeId])); } MapGuesser.httpRequest('POST', '/admin/saveMap/' + mapId + '/json', function () { document.getElementById('loading').style.visibility = 'hidden'; if (this.response.error) { //TODO: handle this error return; } MapEditor.replacePlaceIdsToReal(this.response.added); if (mapId === 0) { mapId = this.response.mapId; window.history.replaceState(null, '', '/admin/mapEditor/' + mapId); } MapEditor.added = {}; MapEditor.edited = {}; MapEditor.deleted = {}; document.getElementById('added').innerHTML = '0'; document.getElementById('edited').innerHTML = '0'; document.getElementById('deleted').innerHTML = '0'; document.getElementById('saveButton').disabled = true; }, data); }, replacePlaceIdsToReal: function (addedPlaces) { for (var i = 0; i < addedPlaces.length; ++i) { var tempId = addedPlaces[i].tempId; var placeId = addedPlaces[i].id; places[tempId].id = placeId; } } }; var Util = { getHighResData: function () { if (window.devicePixelRatio >= 4) { return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 }; } else if (window.devicePixelRatio >= 2) { return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 }; } else { return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 }; } }, extractCoordinates: function (coordinatesStr) { var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } }; var delimiters = [',', ' ', ';']; coordinatesStr = coordinatesStr.trim(); if (coordinatesStr.length == 0) { return coordinates; } for (var delimiter of delimiters) { if (coordinatesStr.indexOf(delimiter) != -1) { var coordinatesArr = coordinatesStr.split(delimiter); coordinates.latlng.lat = parseFloat(coordinatesArr[0]); coordinates.latlng.lng = parseFloat(coordinatesArr[1]); if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) { coordinates.valid = true; return coordinates; } } } return coordinates; } }; var LMapWrapper = { map: null, markers: null, divId: null, iconCollection: { iconGreen: L.icon({ iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION, iconSize: [24, 32], iconAnchor: [12, 32] }), iconRed: L.icon({ iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION, iconSize: [24, 32], iconAnchor: [12, 32] }), iconBlue: L.icon({ iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION, iconSize: [24, 32], iconAnchor: [12, 32] }) }, init: function (divId, places) { document.getElementById(divId).style.display = "block"; if (!LMapWrapper.map) { LMapWrapper.divId = divId; LMapWrapper.map = L.map(LMapWrapper.divId, { center: { lat: 0., lng: 0. }, zoom: 2 }); LMapWrapper.map.on('click', function (e) { LMapWrapper.placeMarker(e.latlng); }); var highResData = Util.getHighResData(); L.tileLayer(tileUrl, { attribution: tileAttribution, subdomains: '1234', ppi: highResData.ppi, tileSize: highResData.tileSize, zoomOffset: highResData.zoomOffset, minZoom: highResData.minZoom, maxZoom: highResData.maxZoom }).addTo(LMapWrapper.map); if (mapId) { LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east })); } } LMapWrapper.loadMarkers(places); document.getElementById('streetViewCoverSelector').style.display = 'none'; }, hide: function () { document.getElementById(LMapWrapper.divId).style.display = 'none'; }, loadMarkers: function (places) { if (!LMapWrapper.markers) { LMapWrapper.markers = L.markerClusterGroup({ maxClusterRadius: 50 }); } else { LMapWrapper.markers.clearLayers(); } for (var placeId in places) { if (!places.hasOwnProperty(placeId)) { continue; } var place = places[placeId]; var marker = L.marker({ lat: place.lat, lng: place.lng }, { icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen, zIndexOffset: 1000 }) .addTo(LMapWrapper.markers) .on('click', function () { MapEditor.select(this); }); marker.placeId = placeId; } LMapWrapper.map.addLayer(LMapWrapper.markers); }, // TODO: check whether marker is already existing on the map for the coordinates // or alternatively block saving for matching coordinates placeMarker: function (latLng) { var marker = L.marker(latLng, { icon: LMapWrapper.iconCollection.iconBlue, zIndexOffset: 2000 }) .addTo(LMapWrapper.markers) .on('click', function () { MapEditor.select(this); }); MapEditor.select(marker); }, panTo: function (latLng) { LMapWrapper.map.panTo(latLng); }, resize: function () { LMapWrapper.map.invalidateSize(true); }, changeMarkerIcon: function (marker, icon) { marker.setIcon(icon); marker.setZIndexOffset(2000); }, removeMarker: function (marker) { LMapWrapper.markers.removeLayer(marker); }, toggleStreetViewCover: function () { }, getSearchRadius: function (location) { return 100; } }; var GMapWrapper = { map: null, markers: null, divId: null, streetViewCover: null, streetViewCoverOn: false, iconCollection: { iconGreen: { url: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION, scaledSize: new google.maps.Size(24, 32), // scaled size origin: new google.maps.Point(0, 0), // origin anchor: new google.maps.Point(12, 32) // anchor }, iconRed: { url: STATIC_ROOT + '/img/markers/marker-red.svg?rev' + REVISION, scaledSize: new google.maps.Size(24, 32), // scaled size origin: new google.maps.Point(0, 0), // origin anchor: new google.maps.Point(12, 32) // anchor }, iconBlue: { url: STATIC_ROOT + '/img/markers/marker-blue.svg?rev' + REVISION, scaledSize: new google.maps.Size(24, 32), // scaled size origin: new google.maps.Point(0, 0), // origin anchor: new google.maps.Point(12, 32) // anchor } }, init: function (divId, places) { document.getElementById(divId).style.display = "block"; if (!GMapWrapper.map) { GMapWrapper.divId = divId; GMapWrapper.map = new google.maps.Map(document.getElementById(GMapWrapper.divId), { center: { lat: 0., lng: 0. }, zoom: 2, fullscreenControl: false, zoomControl: true, zoomControlOptions: { position: google.maps.ControlPosition.LEFT_BOTTOM }, streetViewControl: false, draggableCursor: 'crosshair' }); GMapWrapper.streetViewCover = new google.maps.StreetViewCoverageLayer(); GMapWrapper.map.addListener('click', function (mapsMouseEvent) { GMapWrapper.placeMarker({ lat: mapsMouseEvent.latLng.lat(), lng: mapsMouseEvent.latLng.lng() }); }); if (mapId) { GMapWrapper.map.fitBounds({ south: mapBounds.south, west: mapBounds.west, north: mapBounds.north, east: mapBounds.east }); } } GMapWrapper.loadMarkers(places); document.getElementById('streetViewCoverSelector').style.display = 'block' }, hide: function () { document.getElementById(GMapWrapper.divId).style.display = 'none'; }, loadMarkers: function (places) { if (!GMapWrapper.markers) { GMapWrapper.markers = new MarkerClusterer(GMapWrapper.map, [], { imagePath: STATIC_ROOT + '/img/markers/m', imageExtension: 'png?rev' + REVISION }); } else { GMapWrapper.markers.clearMarkers(); } for (var placeId in places) { if (!places.hasOwnProperty(placeId)) { continue; } var place = places[placeId]; var marker = new google.maps.Marker({ position: { lat: place.lat, lng: place.lng }, icon: place.noPano ? GMapWrapper.iconCollection.iconRed : GMapWrapper.iconCollection.iconGreen }); marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } }; marker.setLatLng = function (coords) { this.setPosition(coords) }; marker.addListener('click', function () { MapEditor.select(this); }); marker.placeId = placeId; GMapWrapper.markers.addMarker(marker); } }, // TODO: check whether marker is already existing on the map for the coordinates // or alternatively block saving for matching coordinates placeMarker: function (latLng) { var marker = new google.maps.Marker({ map: GMapWrapper.map, position: latLng, icon: GMapWrapper.iconCollection.iconBlue, }); marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } }; marker.setLatLng = function (coords) { this.setPosition(coords) }; marker.addListener('click', function () { MapEditor.select(this); }); GMapWrapper.markers.addMarker(marker); MapEditor.select(marker); }, panTo: function (latLng) { GMapWrapper.map.panTo(latLng); }, resize: function () { google.maps.event.trigger(GMapWrapper.map, 'resize'); }, changeMarkerIcon: function (marker, icon) { marker.setIcon(icon); }, removeMarker: function (marker) { GMapWrapper.markers.removeMarker(marker); }, toggleStreetViewCover: function () { if (GMapWrapper.streetViewCoverOn) { GMapWrapper.streetViewCover.setMap(null); GMapWrapper.streetViewCoverOn = false; } else { GMapWrapper.streetViewCover.setMap(GMapWrapper.map); GMapWrapper.streetViewCoverOn = true; } }, getSearchRadius: function (location) { // source: https://www.yorku.ca/mack/CHI01.htm var movementOffset = 4; // source: https://groups.google.com/g/google-maps-js-api-v3/c/hDRO4oHVSeM/m/osOYQYXg2oUJ?pli=1 var metersPerPixel = 156543.03392 * Math.cos(location.lat * Math.PI / 180) / Math.pow(2, GMapWrapper.map.getZoom()); var minSearchRadius = 5; var searchRadius = Math.max(minSearchRadius, Math.round(movementOffset * metersPerPixel)); return searchRadius; } }; // initialize content of #map with google maps MapEditor.map = GMapWrapper; MapEditor.map.init('gmap', places); MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), { // switch off fullscreenControl because positioning doesn't work fullscreenControl: false, fullscreenControlOptions: { position: google.maps.ControlPosition.LEFT_TOP }, motionTracking: false }); document.getElementById('mapName').onclick = function (e) { e.preventDefault(); MapGuesser.showModal('metadata'); document.getElementById('metadataForm').elements.name.select(); }; document.getElementById('metadataForm').onsubmit = function (e) { e.preventDefault(); MapEditor.editMetadata(); }; document.getElementById('closeMetadataButton').onclick = function () { MapGuesser.hideModal(); }; document.getElementById('saveButton').onclick = function () { MapEditor.saveMap(); }; document.getElementById('applyButton').onclick = function () { MapEditor.applyPlace(); }; document.getElementById('closeButton').onclick = function () { MapEditor.closePlace(); }; document.getElementById('deleteButton').onclick = function () { MapEditor.deletePlace(); }; document.getElementById('jumpButton').onclick = function (e) { var coordinatesStr = document.getElementById("jumpCoordinates").value; var coordinates = Util.extractCoordinates(coordinatesStr); if (coordinates.valid) { MapEditor.map.placeMarker(coordinates.latlng); } }; document.getElementById('jumpCoordinates').onkeyup = function (e) { var coordinatesStr = document.getElementById("jumpCoordinates").value; var coordinates = Util.extractCoordinates(coordinatesStr); var jumpButton = document.getElementById("jumpButton"); if (coordinates.valid) { jumpButton.disabled = false; if (e.key == 'Enter') { MapEditor.map.placeMarker(coordinates.latlng); } } else { jumpButton.disabled = true; } }; document.getElementById('mapSelector').onclick = function () { MapEditor.closePlace(); MapEditor.map.hide(); if (MapEditor.map === GMapWrapper) { MapEditor.map = LMapWrapper; MapEditor.map.init('lmap', places); } else { MapEditor.map = GMapWrapper; MapEditor.map.init('gmap', places); } } document.getElementById('streetViewCoverSelector').onclick = function () { MapEditor.map.toggleStreetViewCover(); } })();