UNPKG

node-red-contrib-web-worldmap

Version:

A Node-RED node to provide a web page of a world map for plotting things on.

1,237 lines (1,134 loc) 175 kB
/* eslint-disable no-undef */ var startpos = [51.05, -1.38]; // Start location - somewhere in UK :-) var startzoom = 10; var ws; var map; var allData = {}; var markers = {}; var polygons = {}; var layers = {}; var overlays = {}; var basemaps = {}; var marks = []; var buttons = {}; var marksIndex = 0; var menuOpen = false; var clusterAt = 0; var maxage = 900; // default max age of icons on map in seconds - cleared after 15 mins var baselayername = "OSM grey"; // Default base layer OSM but uniform grey var pagefoot = "&nbsp;&copy; DCJ 2025"; var inIframe = false; var showUserMenu = true; var showLayerMenu = true; var showMouseCoords = false; var allowFileDrop = false; var heat; var minimap; var sidebyside; var layercontrol; var colorControl; var drawCount = 0; var drawingColour = "#910000"; var drawcontextmenu = ""; var sendDrawing; var rmenudata = {}; var sendRoute; var oldBounds = {ne:{lat:0, lng:0}, sw:{lat:0, lng:0}}; var edgeLayer = new L.layerGroup(); var edgeEnabled = true; var pmtloaded = ""; var iconSz = { "Team/Crew": 24, "Squad": 24, "Section": 24, "Platoon/detachment": 26, "Company/battery/troop": 28, "Battalion/squadron": 30, "Regiment/group": 32, "Brigade": 34, "Division": 36, "Corps/MEF": 36, "Army": 40, "Army Group/front": 40, "Region/Theater": 44, "Command": 44 }; var filesAdded = ''; var loadStatic = function(fileName) { if (filesAdded.indexOf(fileName) !== -1) { return; } var head = document.getElementsByTagName('head')[0] if (fileName.indexOf('js') !== -1) { var script = document.createElement('script'); script.src = fileName; script.type = 'text/javascript'; console.log("Loading: ",fileName); head.append(script); filesAdded += ' ' + fileName; } else if (fileName.indexOf('css') !== -1) { var style = document.createElement('link'); style.href = fileName; style.type = 'text/css'; style.rel = 'stylesheet'; console.log("Loading: ",fileName); head.append(style);; filesAdded += ' ' + fileName; } else { console.log("Unsupported file type: ",fileName); } } // Create the socket var connect = function() { // var transports = ["websocket", "xhr-streaming", "xhr-polling"], ws = new SockJS(location.pathname.split("index")[0] + 'socket'); ws.onopen = function() { console.log("CONNECTED"); if (!inIframe) { document.getElementById("footer").innerHTML = "<font color='#494'>"+pagefoot+"</font>"; } ws.send(JSON.stringify({action:"connected",parameters:Object.fromEntries((new URL(location)).searchParams),clientTimezone:Intl.DateTimeFormat().resolvedOptions().timeZone || false})); setTimeout(function() { onoffline(); }, 500); }; ws.onclose = function() { console.log("DISCONNECTED"); if (!inIframe) { document.getElementById("footer").innerHTML = "<font color='#900'>"+pagefoot+"</font>"; } setTimeout(function() { connect(); }, 2500); }; ws.onmessage = function(e) { try { var data = JSON.parse(e.data); if (data.hasOwnProperty("type") && data.hasOwnProperty("data") && data.type === "Buffer") { data = data.data.toString(); } handleData(data); } catch (e) { if (data) { console.log("BAD DATA",data); console.log(e); } } // console.log("DATA",typeof data,data); }; }; console.log("CONNECT TO",location.pathname + 'socket'); var handleData = function(data) { if (Array.isArray(data)) { // console.log("ARRAY:",data.length); for (var prop of data) { if (prop.command) { doCommand(prop.command); delete prop.command; } if (prop.hasOwnProperty("name")) { setMarker(prop); // bnds.extend(markers[prop.name].getLatLng()); } else if (prop.hasOwnProperty("filename") && prop.filename === "doc.kml") { data = {command:{map:{overlay:"KML", kml:prop.payload}}}; doCommand(data.command); return; } else { console.log("SKIP array item",prop); } } } else { // Handle some raw string data overlays if (typeof data === "string" && data.indexOf("<?xml") == 0) { if (data.indexOf("<nvg") != -1) { data = {command:{map:{overlay:"NVG", nvg:data}}}; } else if (data.indexOf("<kml") != -1) { data = {command:{map:{overlay:"KML", kml:data}}}; } else if (data.indexOf("<gpx") != -1) { data = {command:{map:{overlay:"GPX", gpx:data}}}; } } // handle any commands in the data if (data?.command) { doCommand(data.command); delete data.command; } // handle raw geojson type msg if (data?.type && data.type.indexOf("Feature") === 0) { if (data?.properties?.title) { doGeojson(data.properties.title,data,data?.layer,data?.options,data?.icon) // name, geojson, layer, options, icon } else { doGeojson("geojson",data,data?.layer,data?.options,data?.icon); } } // handle TAK json (from tak-ingest node or fastxml node) else if (data?.event?.point) { doTAKjson(data.event); } // handle TAK json (from multicast Protobuf via tak-ingest node) else if (data?.cotEvent && data.cotEvent.hasOwnProperty("lat") && data.cotEvent.hasOwnProperty("lon")) { doTAKMCjson(data.cotEvent); } // handle default worldmap json msg else if (data?.name) { setMarker(data); } else { if (JSON.stringify(data) !== '{}') { console.log("SKIP",data); } // if (typeof data === "string") { doDialog(data); } // else { console.log("SKIP",data); } } } } window.onunload = function() { if (ws) { ws.close(); } } var customTopoLayer = L.geoJson(null, {clickable:false, style: {color:"blue", weight:2, fillColor:"#cf6", fillOpacity:0.04}}); layers["_countries"] = omnivore.topojson('images/world-50m-flat.json',null,customTopoLayer); overlays["countries"] = layers["_countries"]; var onoffline = function() { if (!navigator.onLine) { if (pmtloaded !== "") { basemaps[pmtloaded].addTo(map); layercontrol._update(); } else { map.addLayer(overlays["countries"]); } } else if (Object.keys(basemaps).length === 0 ) { map.addLayer(overlays["countries"]); } } document.addEventListener ("keydown", function (ev) { // Set Ctl-Alt-3 to switch to 3d view if (ev.ctrlKey && ev.altKey && ev.code === "Digit3") { ws.close(); window.location.href = "index3d.html"; } // Set Esc key to close all open popups if (ev.keyCode === 27) { map.eachLayer(function (layer) { layer.closePopup(); }); } }); if ( window.self !== window.top ) { inIframe = true; } if (inIframe === true) { if ( window.localStorage.hasOwnProperty("lastpos") ) { var sp = JSON.parse(window.localStorage.getItem("lastpos")); startpos = [ sp.lat, sp.lng ]; } if ( window.localStorage.hasOwnProperty("lastzoom") ) { startzoom = window.localStorage.getItem("lastzoom"); } } // Create the Initial Map object. map = new L.map('map',{ zoomSnap: 0.1, rotate: true, rotateControl: false, // rotateControl: { // closeOnZeroBearing: true, // position: 'topleft' // }, bearing: 0}).setView(startpos, startzoom); map.whenReady(function() { connect(); }); // Drag Drop of files to target map var droplatlng; var target = document.getElementById("map") target.ondragover = function (ev) { ev.preventDefault() ev.dataTransfer.dropEffect = "move" } target.ondrop = function (ev) { if (allowFileDrop === true) { ev.preventDefault(); droplatlng = map.mouseEventToLatLng(ev); handleFiles(ev.dataTransfer.files); } } var handleFiles = function(files) { ([...files]).forEach(readFile); } var readFile = function(file) { // Check if the file is text or kml if (file.type && file.type.indexOf('text') === -1 && file.type.indexOf('kml') === -1 && file.type.indexOf('kmz') === -1 && file.type.indexOf('json') === -1 && file.type.indexOf('image/jpeg') === -1 && file.type.indexOf('image/webp') === -1 && file.type.indexOf('image/png') === -1 && file.type.indexOf('image/tiff') === -1) { console.log('File is not text, kml, kmz, jpeg, png, webp, or json', file.type, file); return; } const reader = new FileReader(); reader.addEventListener('load', (event) => { var content = event.target.result; var data; if (content.indexOf("base64") !== -1) { if (content.indexOf("image") === -1) { data = atob(content.split("base64,")[1]); if (data.indexOf('<?xml') !== -1) { if (data.indexOf("<gpx") !== -1) { doCommand({map:{overlay:file.name, gpx:data}}); } else if (data.indexOf("<kml") !== -1) { doCommand({map:{overlay:file.name, kml:data}}); } else if (data.indexOf("<nvg") !== -1) { doCommand({map:{overlay:file.name, nvg:data}}); } } else if (data.indexOf("<kml") !== -1) { doCommand({map:{overlay:file.name, kml:data}}); } else if (data.indexOf('PK') === 0) { if (file.name.indexOf('.kmz') !== -1) { doCommand({map:{overlay:file.name, kmz:data}}); } else { console.log("ZIP FILE",file); } } else if (file.type.indexOf('geo+json') !== -1 ) { data = JSON.parse(data); doGeojson(file.name,data,"geojson"); } else { try { data = JSON.parse(data); handleData(data); } catch(e) { console.log("NOT JSON DATA",data); } } } else if (content.indexOf("image/tiff") !== -1) { data = atob(content.split("base64,")[1]); console.log("Geotiff",typeof data) /// we now have a geotiff image to render... } ws.send(JSON.stringify({action:"file", name:file.name, type:file.type, content:content, lat:droplatlng.lat, lon:droplatlng.lng})); } else { console.log("NOT SURE WHAT THIS IS?",content) } }); reader.readAsDataURL(file); } // Create some buttons var menuButton = L.easyButton({states:[{icon:'fa-bars fa-lg', onClick:function() { toggleMenu(); }, title:'Toggle menu'}], position:"topright"}); var fullscreenButton = L.control.fullscreen(); var rulerButton = L.control.ruler({position:"topleft"}); var followMode = { accuracy:true }; var followState = false; var trackMeButton; var errRing; function onLocationFound(e) { if (followState === true) { map.panTo(e.latlng); } if (followMode.icon) { var self = {name:followMode.name || "self", lat:e.latlng.lat, lon:e.latlng.lng, hdg:isNaN(e?.heading * 1) ? undefined : e?.heading * 1, speed:isNaN(e?.speed * 1) ? undefined : e?.speed * 1, layer:followMode.layer, icon:followMode.icon, iconColor:followMode.iconColor ?? "#910000" }; setMarker(self); } if (e.heading !== null) { map.setBearing(e.heading); } if (followMode.accuracy) { errRing = L.circle(e.latlng, e.accuracy, {color:followMode.color ?? "#00ffff", weight:3, opacity:0.6, fill:false, clickable:false}); errRing.addTo(map); // if (e.hasOwnProperty("heading")) { // var lengthAsDegrees = e.speed * 60 / 110540; // var ya = e.latlng.lat + Math.sin((90-e.heading)/180*Math.PI)*lengthAsDegrees*Math.cos(e.latlng.lng/180*Math.PI); // var xa = e.latlng.lng + Math.cos((90-e.heading)/180*Math.PI)*lengthAsDegrees; // var lla = new L.LatLng(ya,xa); // L.polygon([ e.latlng, lla ], {color:"00ffff", weight:3, opacity:0.5, clickable:false}).addTo(map); // } } ws.send(JSON.stringify({action:"point", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5), point:"self", hdg:isNaN(e?.heading * 1) ? undefined : e?.heading * 1, speed:isNaN(e?.speed * 1) ? undefined : e?.speed * 1})); } function onLocationError(e) { console.log(e.message); } // Move some bits around if in an iframe if (inIframe) { console.log("IN an iframe"); if (showUserMenu) { menuButton.addTo(map); } document.getElementById("topbar").style.display="none"; document.getElementById("map").style.top="0px"; document.getElementById("results").style.right="50px"; document.getElementById("results").style.top="10px"; document.getElementById("results").style.zIndex="1"; document.getElementById("results").style.height="31px"; document.getElementById("results").style.paddingTop="6px"; document.getElementById("bars").style.display="none"; document.getElementById("menu").style.right="8px"; document.getElementById("menu").style.borderRadius="6px"; } else { //console.log("NOT in an iframe"); if (!showUserMenu) { document.getElementById("bars").style.display="none"; } // Add the fullscreen button fullscreenButton.addTo(map); trackMeButton = L.easyButton({ states: [{ stateName: 'track-on', icon: 'fa-window-close-o fa-lg', title: 'Disable tracking', onClick: function(btn, map) { btn.state('track-off'); followState = false; if (errRing) { errRing.removeFrom(map); } delMarker(followMode.name || "self") map.stopLocate(); } }, { stateName: 'track-off', icon: 'fa-crosshairs fa-lg', title: 'Enable tracking', onClick: function(btn, map) { btn.state('track-on'); followState = true; map.locate({setView:false, watch:followState, enableHighAccuracy:true}); } }] }); trackMeButton.state('track-off'); trackMeButton.addTo(map); // Add the locate my position button // L.easyButton( 'fa-crosshairs fa-lg', function() { // map.locate({setView:true, maxZoom:16}); // }, "Locate me").addTo(map); // Create the clear heatmap button var clrHeat = L.easyButton( 'fa-eraser', function() { console.log("Reset heatmap"); heat.setLatLngs([]); }, "Clears the current heatmap", {position:"bottomright"}); } var helpMenu = '<table>' helpMenu += '<tr><td><input type="text" name="search" id="search" size="20" style="width:150px;"/>&nbsp;<span onclick=\'doSearch();\'><i class="fa fa-search fa-lg"></i></span></td></tr>'; helpMenu += '<tr><td style="cursor:default"><i class="fa fa-spinner fa-lg fa-fw"></i> Set Max Age <input type="text" name="maxage" id="maxage" value="600" size="5" onchange=\'setMaxAge();\'/>s</td></tr>'; helpMenu += '<tr><td style="cursor:default"><i class="fa fa-search-plus fa-lg fa-fw"></i> Cluster at zoom &lt;<input type="text" name="setclus" id="setclus" size="2" onchange=\'setCluster(this.value);\'/></td></tr>'; helpMenu += '<tr><td style="cursor:default"><input type="checkbox" id="panit" onclick=\'doPanit(this.checked);\'/> Auto Pan Map</td></tr>'; helpMenu += '<tr><td style="cursor:default"><input type="checkbox" id="lockit" onclick=\'doLock(this.checked);\'/> Lock Map</td></tr>'; helpMenu += '<tr><td style="cursor:default"><input type="checkbox" id="heatall" onclick=\'doHeatAll(this.checked);\'/> Heatmap all layers</td></tr>'; if (!inIframe) { helpMenu += '<tr><td style="cursor:default"><span id="showHelp" onclick=\'doDialog(helpText);\'><i class="fa fa-info fa-lg fa-fw"></i>Help</span></td></tr></table>'; } else { helpMenu += '</table>' } document.getElementById('menu').innerHTML = helpMenu; // Add graticule var showGrid = false; var showRuler = false; var Lgrid = L.latlngGraticule({ font: "Verdana", fontColor: "#666", zoomInterval: [ {start:1, end:2, interval:40}, {start:3, end:3, interval:20}, {start:4, end:4, interval:10}, {start:5, end:7, interval:5}, {start:8, end:20, interval:1} ] }); // Add small sidc icons around edge of map for things just outside of view // This function based heavily on Game Aware code from Måns Beckman // Copyright (c) 2013 Måns Beckman, All rights reserved. var edgeAware = function () { map.removeLayer(edgeLayer); if (!edgeEnabled) { return; } edgeLayer = new L.layerGroup(); var mapBounds = map.getBounds(); var mapBoundsCenter = mapBounds.getCenter(); pSW = map.options.crs.latLngToPoint(mapBounds.getSouthWest(), map.getZoom()); pNE = map.options.crs.latLngToPoint(mapBounds.getNorthEast(), map.getZoom()); pCenter = map.options.crs.latLngToPoint(mapBoundsCenter, map.getZoom()); var viewBounds = L.latLngBounds(map.options.crs.pointToLatLng(L.point(pSW.x - (pCenter.x - pSW.x ), pSW.y - (pCenter.y - pSW.y )), map.getZoom()) , map.options.crs.pointToLatLng(L.point(pNE.x + (pNE.x - pCenter.x) , pNE.y + (pNE.y - pCenter.y) ), map.getZoom()) ); for (var id in markers) { if (allData[id] && allData[id].hasOwnProperty("SIDC")) { var markerLatLng = markers[id].getLatLng(); if ( viewBounds.contains(markerLatLng) && !mapBounds.contains(markerLatLng) ) { var x,y; var k = (markerLatLng.lat - mapBoundsCenter.lat) / (markerLatLng.lng - mapBoundsCenter.lng); if (markerLatLng.lng > mapBoundsCenter.lng) { x = mapBounds.getEast() - mapBoundsCenter.lng; } else { x = (mapBounds.getWest() - mapBoundsCenter.lng); } if (markerLatLng.lat < mapBoundsCenter.lat) { y = mapBounds.getSouth() - mapBoundsCenter.lat; } else { y = mapBounds.getNorth() - mapBoundsCenter.lat; } var lat = (mapBoundsCenter.lat + (k * x)); var lng = (mapBoundsCenter.lng + (y / k)); var iconAnchor = {x:5, y:5} if (lng > mapBounds.getEast()) { lng = mapBounds.getEast(); iconAnchor.x = 20; } if (lng < mapBounds.getWest()) { lng = mapBounds.getWest(); iconAnchor.x = -5; }; if (lat < mapBounds.getSouth()) { lat = mapBounds.getSouth(); iconAnchor.y = 15; } if (lat > mapBounds.getNorth()) { lat = mapBounds.getNorth(); iconAnchor.y = -5; }; var eico = new ms.Symbol(allData[id].SIDC.substr(0,5)+"-------",{size:9}); var myicon = L.icon({ iconUrl: eico.toDataURL(), iconAnchor: new L.Point(iconAnchor.x, iconAnchor.y), className: "natoicon-s", }); edgeLayer.addLayer(L.marker([lat,lng],{icon:myicon})) } } } edgeLayer.addTo(map) } // end of edgeAware function var panit = false; function doPanit(v) { if (v !== undefined) { panit = v; } // console.log("Panit set :",panit); } var heatAll = false; function doHeatAll(v) { if (v !== undefined) { heatall = v; } console.log("Heatall set :",heatAll); } var lockit = false; var mbnds = new L.LatLngBounds([[-120,-360],[120,360]]); function doLock(v) { if (v !== undefined) { lockit = v; } if (lockit === false) { mbnds = new L.LatLngBounds([[-120,-360],[120,360]]); map.dragging.enable(); } else { mbnds = map.getBounds(); map.dragging.disable(); window.localStorage.setItem("lastpos",JSON.stringify(map.getCenter())); window.localStorage.setItem("lastzoom", map.getZoom()); window.localStorage.setItem("lastlayer", baselayername); window.localStorage.setItem("maxage", maxage); console.log("Saved :",JSON.stringify(map.getCenter()),map.getZoom(),baselayername); } map.setMaxBounds(mbnds); //console.log("Map bounds lock :",lockit); } // Remove old markers function doTidyUp(l) { if (l === "heatmap") { heat.setLatLngs([]); } else { var d = parseInt(Date.now()/1000); for (var m in markers) { if ((l && (l == markers[m].lay)) || typeof markers[m].ts != "undefined") { if ((l && (l == markers[m].lay)) || (markers[m].hasOwnProperty("ts") && (Number(markers[m].ts) < d) && (markers[m].lay !== "_drawing"))) { //console.log("STALE :",m); if (typeof polygons[m+"_"] != "undefined") { layers[polygons[m+"_"].lay].removeLayer(polygons[m+"_"]); delete polygons[m+"_"]; delete allData[m+"_"]; } if (typeof polygons[m] != "undefined") { layers[markers[m].lay].removeLayer(polygons[m]); delete polygons[m]; delete allData[m]; } layers[markers[m].lay].removeLayer(markers[m]); delete markers[m]; delete allData[m]; } } } if (l) { if (layers[l]) { map.removeLayer(layers[l]); layercontrol.removeLayer(layers[l]); delete layers[l]; } if (overlays[l]) { map.removeLayer(overlays[l]); layercontrol.removeLayer(overlays[l]); delete overlays[l]; } } } } // Call tidyup every {maxage} seconds - default 10 mins var stale = null; function setMaxAge() { maxage = document.getElementById('maxage').value; if (stale) { clearInterval(stale); } //if (maxage > 0) { stale = setInterval( function() { doTidyUp() }, 20000); // check every 20 secs } setMaxAge(); // move the daylight / nighttime boundary (if enabled) every minute function moveTerminator() { // if terminator line plotted move it every minute if (layers["_daynight"] && layers["_daynight"].getLayers().length > 0) { layers["_daynight"].clearLayers(); layers["_daynight"].addLayer(L.terminator()); } } setInterval( function() { moveTerminator() }, 60000 ); // move the rainfall overlay (if enabled) every 10 minutes function moveRainfall() { if (navigator.onLine && overlays.hasOwnProperty("rainfall") && map.hasLayer(overlays["rainfall"])) { overlays["rainfall"]["_url"] = 'https://tilecache.rainviewer.com/v2/radar/' + parseInt(Date.now()/600000)*600 + '/256/{z}/{x}/{y}/2/1_1.png'; overlays["rainfall"].redraw(); } } setInterval( function() { moveRainfall() }, 600000 ); function setCluster(v) { clusterAt = v || 0; console.log("clusterAt set:",clusterAt); showMapCurrentZoom(); } var typingTimer; document.getElementById('search').addEventListener('keyup', () => { clearTimeout(typingTimer); if (document.getElementById('search').value.length >= 4) { typingTimer = setTimeout(doneTyping, 700); } }); function doneTyping () { doSearch(); } async function readPhoton(url) { const response = await fetch(url); const reader = response.body.getReader(); let v = ""; while (true) { const { done, value } = await reader.read(); if (done) { if (value !== undefined) { v += new TextDecoder().decode(value); } return v; } v += new TextDecoder().decode(value) } } // Search for markers with names of ... or icons of ... function doSearch() { var value = document.getElementById('search').value; marks = []; marksIndex = 0; for (var key in markers) { if ( (~(key.toLowerCase()).indexOf(value.toLowerCase())) && (mbnds.contains(markers[key].getLatLng()))) { marks.push(markers[key]); } if (markers[key].icon === value) { marks.push(markers[key]); } } moveToMarks(); if (marks.length === 0) { // If no markers found let's try a geolookup... var protocol = location.protocol; if (protocol == "file:") { protocol = "https:"; } var searchUrl = protocol + "//nominatim.openstreetmap.org/search?format=json&limit=5&q="; fetch(searchUrl + value) // Call the fetch function passing the url of the API as a parameter .then(function(resp) { return resp.json(); }) .then(function(data) { if (data.length > 0) { // var l = data.map((x) => x.display_name); // console.log("LIST",l) var bb = data[0].boundingbox; map.fitBounds([ [bb[0],bb[2]], [bb[1],bb[3]] ]); map.panTo([data[0].lat, data[0].lon]); document.getElementById('searchResult').innerHTML = ""; } else { document.getElementById('searchResult').innerHTML = "&nbsp;<font color='#ff0'>Not Found</font>"; } }) .catch(function(err) { if (err.toString() === "TypeError: Failed to fetch") { document.getElementById('searchResult').innerHTML = "&nbsp;<font color='#ff0'>Not Found</font>"; } }); // var searchUrl = 'https://photon.komoot.io/api?limit=5&q='; // readPhoton(searchUrl + value).then( // function(value) { // if (value.length > 0 && typeof value === "string") { // var s = JSON.parse(value); // if (s?.features) { // var l = s.features.map((x) => x.properties.name +', ' + x.properties.countrycode); // console.log("LIST",l) // if (s.features.length > 0) { // if (s.features[0].properties?.extent) { // var bb = s.features[0].properties.extent; // map.fitBounds([ [bb[3],bb[0]], [bb[1],bb[2]] ]); // } // else { // map.panTo([s.features[0].geometry.coordinates[1], s.features[0].geometry.coordinates[0]]); // } // document.getElementById('searchResult').innerHTML = ""; // } // else { // document.getElementById('searchResult').innerHTML = "&nbsp;<font color='#ff0'>Not Found</font>";} // } // } // }, // function(error) { // document.getElementById('searchResult').innerHTML = "&nbsp;<font color='#ff0'>Search Error</font>"; // } // ); } else { if (lockit) { document.getElementById('searchResult').innerHTML = "&nbsp;<font color='#ff0'>Found "+marks.length+" results within bounds.</font>"; } else { document.getElementById('searchResult').innerHTML = "&nbsp;<font color='#ff0'>Found "+marks.length+" results.</font>"; } } } // Jump to a markers position - centralise it on map function moveToMarks() { if (marks.length > marksIndex) { var m = marks[marksIndex]; map.setView(m.getLatLng(), map.getZoom()); m.openPopup(); marksIndex++; setTimeout(moveToMarks, 2500); } } // Clear Search With Marker names function clearSearch() { var value = document.getElementById('search').value; marks = []; marksIndex = 0; for (var key in markers) { if ( (~(key.toLowerCase()).indexOf(value.toLowerCase())) && (mbnds.contains(markers[key].getLatLng()))) { marks.push(markers[key]); } } removeMarks(); // if (lockit) { document.getElementById('searchResult').innerHTML = ""; // } // else { // document.getElementById('searchResult').innerHTML = ""; // } } function removeMarks() { if (marks.length > marksIndex) { var m = marks[marksIndex]; map.setView(m.getLatLng(), map.getZoom()); m.closePopup(); marksIndex++; } } function toggleMenu() { menuOpen = !menuOpen; if (menuOpen) { document.getElementById("menu").style.display = 'block'; } else { document.getElementById("menu").style.display = 'none'; dialogue.close(); } } function openMenu() { if (!menuOpen) { menuOpen = true; document.getElementById("menu").style.display = 'block'; } } function closeMenu() { if (menuOpen) { menuOpen = false; document.getElementById("menu").style.display = 'none'; } dialogue.close(); } document.getElementById("menu").style.display = 'none'; map.on('overlayadd', function(e) { if (typeof overlays[e.name].bringToFront === "function") { overlays[e.name].bringToFront(); } if (e.name == "satellite") { overlays["satellite"].bringToBack(); } if (e.name == "countries") { overlays["countries"].bringToBack(); } if (e.name == "heatmap") { // show heatmap button when it's layer is added. clrHeat.addTo(map); } if (e.name == "day/night") { layers["_daynight"].addLayer(L.terminator()); } if (e.name == "drawing") { overlays["drawing"].bringToFront(); map.pm.toggleControls(); map.addControl(colorControl); } ws.send(JSON.stringify({action:"addlayer", name:e.name})); }); map.on('overlayremove', function(e) { if (e.name == "heatmap") { // hide heatmap button when it's layer is removed. clrHeat.removeFrom(map); } if (e.name == "day/night") { layers["_daynight"].clearLayers(); } if (e.name == "drawing") { map.pm.toggleControls() map.removeControl(colorControl); } ws.send(JSON.stringify({action:"dellayer", name:e.name})); }); map.on('baselayerchange', function(e) { //console.log("base layer now :",e.name); baselayername = e.name; ws.send(JSON.stringify({action:"layer", name:e.name})); }); function showMapCurrentZoom() { //console.log("ZOOM:",map.getZoom()); for (var l in layers) { if (layers[l].hasOwnProperty("_zoom")) { if (map.getZoom() >= clusterAt) { layers[l].disableClustering(); } else { layers[l].enableClustering(); } } } setTimeout( function() { for (var key in markers) { if (polygons[key]) { if (typeof layers[markers[key].lay].getVisibleParent === 'function') { var vis = layers[markers[key].lay].getVisibleParent(markers[key]); if ((vis) && (vis.hasOwnProperty("lay"))) { polygons[key].setStyle({opacity:1}); } else { polygons[key].setStyle({opacity:0}); } } try { if (polygons[key].hasOwnProperty("_layers")) { polygons[key].eachLayer(function(layer) { layer.redraw(); }); } else { polygons[key].redraw(); } } catch(e) { console.log(key,polygons[key],e) } } } },750); } map.on('zoomend', function() { showMapCurrentZoom(); window.localStorage.setItem("lastzoom", map.getZoom()); var b = map.getBounds(); oldBounds = {sw:{lat:b._southWest.lat,lng:b._southWest.lng},ne:{lat:b._northEast.lat,lng:b._northEast.lng}}; ws.send(JSON.stringify({action:"bounds", south:b._southWest.lat, west:b._southWest.lng, north:b._northEast.lat, east:b._northEast.lng, zoom:map.getZoom() })); edgeAware(); }); map.on('moveend', function() { window.localStorage.setItem("lastpos",JSON.stringify(map.getCenter())); var b = map.getBounds(); if (b._southWest.lat !== oldBounds.sw.lat && b._southWest.lng !== oldBounds.sw.lng && b._northEast.lat !== oldBounds.ne.lat && b._northEast.lng !== oldBounds.ne.lng) { ws.send(JSON.stringify({action:"bounds", south:b._southWest.lat, west:b._southWest.lng, north:b._northEast.lat, east:b._northEast.lng, zoom:map.getZoom() })); oldBounds = {sw:{lat:b._southWest.lat,lng:b._southWest.lng},ne:{lat:b._northEast.lat,lng:b._northEast.lng}}; } edgeAware(); }); map.on('locationfound', onLocationFound); map.on('locationerror', onLocationError); // single right click to add a marker var addmenu = "<b>Add marker</b><br><input type='text' id='rinput' autofocus onkeydown='if (event.keyCode == 13) addThing();' placeholder='name (,icon/SIDC, layer, colour, heading)'/>"; addmenu += '<br/><a href="unitgenerator.html" target="_new">MilSymbol SIDC generator</a>'; var rightmenuMap = L.popup({keepInView:true, minWidth:260}).setContent(addmenu); const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}`; const colorKeywordToRGB = (colorKeyword) => { let el = document.createElement('div'); el.style.color = colorKeyword; document.body.appendChild(el); let rgbValue = window.getComputedStyle(el).color; document.body.removeChild(el); return rgba2hex(rgbValue); } var rclk = {}; var hiderightclick = false; var addThing = function() { var thing = document.getElementById('rinput').value; map.closePopup(); //popped = false; var bits = thing.split(","); var icon = (bits[1] || "circle").trim(); var lay = (bits[2] || "unknown").trim(); // TODO: Do we want _drawing here or unknown ? var colo = (bits[3] ?? "#910000").trim(); colo = colorKeywordToRGB(colo); var hdg = parseFloat(bits[4] || 0); var drag = true; var regi = /^[SGEIO][A-Z]{3}.*/i; // if it looks like a SIDC code var d = {action:"point", name:bits[0].trim(), layer:lay, draggable:drag, lat:rclk.lat, lon:rclk.lng, hdg:hdg, ttl:0 }; if (regi.test(icon)) { d.SIDC = (icon.toUpperCase()+"------------").substr(0,12); } else { d.icon = icon; d.iconColor = colo; } if (icon === "dot") { d.icon = 'fa-circle fa-fw'; } if (icon === "spot") { d.icon = 'fa-circle fa-fw'; } ws.send(JSON.stringify(d)); delete d.action; setMarker(d); map.addLayer(layers[lay]); } var form = {}; var addToForm = function(n,v) { form[n] = v; } var feedback = function(n = "map",v,a = "feedback",c) { if (v === "_form") { v = form; } // undefined is handled directly in the function declaration, but null/"" also requires explicit handling within the function. n = (n === null || n === "") ? "map" : n; var dataToSend = { "name": n, "action": a, "value": v }; //Kept only for backward compatibility, as the context menu should handle the click position values internally. if (n == "map") { dataToSend.lat = rclk.lat; dataToSend.lon = rclk.lng; } ws.send(JSON.stringify(dataToSend)); if (c === true) { map.closePopup(); } } map.on('click', function(e) { ws.send(JSON.stringify({action:"click", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5)})); }); // allow double right click to zoom out (if enabled) // single right click opens a message window that adds a marker var rclicked = false; var rtout = null; map.on('contextmenu', function(e) { if (rclicked) { rclicked = false; clearTimeout(rtout); if (map.doubleClickZoom.enabled()) { map.zoomOut(); } } else { rclicked = true; rtout = setTimeout( function() { rclicked = false; if ((hiderightclick !== true) && (addmenu.length > 0)) { rclk = e.latlng; form = {}; var ramen = ""+addmenu; if (typeof rmenudata !== "string") { for (const item in rmenudata) { ramen = ramen.replace(new RegExp("\\${"+item+"}","g"),rmenudata[item]); } } ramen = ramen.replace(/\${.*?}/g,'') rightmenuMap.setContent(ramen); rightmenuMap.setLatLng(e.latlng); map.openPopup(rightmenuMap); setTimeout( function() { try { document.getElementById('rinput').focus(); } catch(e) {} }, 200); } }, 300); } }); // Layer control based on select box rather than radio buttons. //var layercontrol = L.control.selectLayers(basemaps, overlays).addTo(map); layercontrol = L.control.layers(basemaps, overlays); // Add all the base layer maps if we are online. var addBaseMaps = function(maplist,first) { // console.log("MAPS",first,maplist) var layerlookup = { OSMG:"OSM grey", OSMC:"OSM", OSMH:"OSM Humanitarian", EsriC:"Esri", EsriS:"Esri Satellite", EsriR:"Esri Relief", EsriT:"Esri Topography", EsriO:"Esri Ocean", EsriDG:"Esri Dark Grey", NatGeo: "National Geographic", UKOS:"UK OS OpenData", OpTop:"Open Topo Map", HB:"Hike Bike OSM", ST:"Stamen Topography", SW:"Stamen Watercolor", AN:"AutoNavi (Chinese)" } if (navigator.onLine) { // Use this for OSM online maps var osmUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; var osmAttrib='Map data © OpenStreetMap contributors'; if (maplist.indexOf("MB3d")!==-1) { // handle the case of 3d by redirecting to that page instead. window.location.href("index3d.html"); } if (maplist.indexOf("OSMG")!==-1) { basemaps[layerlookup["OSMG"]] = new L.TileLayer.Grayscale(osmUrl, { attribution:osmAttrib, maxNativeZoom:19, maxZoom:20, subdomains: ['a','b','c'] }); } if (maplist.indexOf("OSMC")!==-1) { basemaps[layerlookup["OSMC"]] = new L.TileLayer(osmUrl, { attribution:osmAttrib, maxNativeZoom:19, maxZoom:20, subdomains: ['a','b','c'] }); } if (maplist.indexOf("OSMH")!==-1) { basemaps[layerlookup["OSMH"]] = new L.TileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", { attribution:"Map data © OpenStreetMap Contributors. Courtesy of Humanitarian OpenStreetMap Team", maxNativeZoom:19, maxZoom:20, subdomains: ['a','b'] }); } // Extra Leaflet map layers from https://leaflet-extras.github.io/leaflet-providers/preview/ if (maplist.indexOf("EsriC")!==-1) { basemaps[layerlookup["EsriC"]] = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', { attribution:'Tiles &copy; Esri', maxNativeZoom:19, maxZoom:20 }); } if (maplist.indexOf("EsriS")!==-1) { basemaps[layerlookup["EsriS"]] = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { //var Esri_WorldImagery = L.tileLayer('http://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {{ attribution:'Tiles &copy; Esri', maxNativeZoom:17, maxZoom:20 }); } if (maplist.indexOf("EsriT")!==-1) { basemaps[layerlookup["EsriT"]] = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', { attribution:'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' }); } if (maplist.indexOf("EsriR")!==-1) { basemaps[layerlookup["EsriR"]] = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}', { attribution:'Tiles &copy; Esri', maxNativeZoom:13, maxZoom:16 }); } if (maplist.indexOf("EsriO")!==-1) { basemaps[layerlookup["EsriO"]] = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{z}/{y}/{x}', { attribution:'Tiles &copy; Esri &mdash; Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri', maxNativeZoom:10, maxZoom:13 }); } if (maplist.indexOf("EsriDG")!==-1) { basemaps[layerlookup["EsriDG"]] = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ', maxNativeZoom:16, maxZoom:18 }); } if (maplist.indexOf("NatGeo")!==-1) { basemaps[layerlookup["NatGeo"]] = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles &copy; Esri', maxNativeZoom:12 }); } if (maplist.indexOf("UKOS")!==-1) { basemaps[layerlookup["UKOS"]] = L.tileLayer('https://geo.nls.uk/maps/opendata/{z}/{x}/{y}.png', { attribution: '<a href="https://geo.nls.uk/maps/">National Library of Scotland Historic Maps</a>', bounds: [[49.6, -12], [61.7, 3]], minZoom:1, maxNativeZoom:17, maxZoom:20, subdomains: '0123' }); } if (maplist.indexOf("OpTop")!==-1) { basemaps[layerlookup["OpTop"]] = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { subdomains: 'abc', maxZoom: 19, attribution: '&copy; <a href="https://www.opentopomap.org/copyright">OpenTopoMap</a> contributors' }); } if (maplist.indexOf("HB")!==-1) { basemaps[layerlookup["HB"]] = L.tileLayer('https://tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }); } if (maplist.indexOf("AN")!==-1) { basemaps["AutoNavi"] = L.tileLayer('https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', { attribution: 'Tiles &copy; 高德地图', maxNativeZoom:14, maxZoom: 19, }); } // Nice terrain based maps by Stamen Design if (maplist.indexOf("ST")!==-1) { var terrainUrl = "https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg"; basemaps[layerlookup["ST"]] = L.tileLayer(terrainUrl, { subdomains: ['a','b','c','d'], minZoom: 0, maxZoom: 20, type: 'jpg', attribution: 'Map tiles by <a href="https://stamen.com">Stamen Design</a>, under <a href="https://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="https://openstreetmap.org">OpenStreetMap</a>, under <a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>' }); } // Nice watercolour based maps by Stamen Design if (maplist.indexOf("SW")!==-1) { var watercolorUrl = "https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg"; basemaps[layerlookup["SW"]] = L.tileLayer(watercolorUrl, { subdomains: ['a','b','c','d'], minZoom: 0, maxZoom: 20, type: 'jpg', attribution: 'Map tiles by <a href="https://stamen.com">Stamen Design</a>, under <a href="https://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="https://openstreetmap.org">OpenStreetMap</a>, under <a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>' }); } } if (first) { if (layerlookup[first]) { baselayername = layerlookup[first]; } else { baselayername = first; } if (!basemaps[baselayername]) { baselayername = Object.keys(basemaps)[0]; } } else { baselayername = Object.keys(basemaps)[0]; } if (baselayername) { basemaps[baselayername].addTo(map); } if (showLayerMenu) { map.removeControl(layercontrol); layercontrol = L.control.layers(basemaps, overlays).addTo(map); } } // Now add the overlays var addOverlays = function(overlist) { //console.log("OVERLAYS",overlist) // var overlookup = { DR:"Drawing", CO:"Countries", DN:"Day/Night", BU:"Buildings", SN:"Ship Navigaion", HM:"Heatmap", AC:"Air corridors", TL:"Place labels" }; // "DR,CO,DN,BU,SN,HM" // Add the drawing layer... if (overlist.indexOf("DR")!==-1) { //var colorPickButton = L.easyButton({states:[{icon:'fa-tint fa-lg', onClick:function() { console.log("PICK"); }, title:'Pick Colour'}]}); var redButton = L.easyButton('fa-square wm-red', function(btn) { changeDrawColour("#FF4040"); }) var blueButton = L.easyButton('fa-square wm-blue', function(btn) { changeDrawColour("#4040F0"); }) var greenButton = L.easyButton('fa-square wm-green', function(btn) { changeDrawColour("#40D040"); }) var yellowButton = L.easyButton('fa-square wm-yellow', function(btn) { changeDrawColour("#FFFF40"); }) var cyanButton = L.easyButton('fa-square wm-cyan', function(btn) { changeDrawColour("#40F0F0"); }) var magentaButton = L.easyButton('fa-square wm-magenta', function(btn) { changeDrawColour("#F040F0"); }) var blackButton = L.easyButton('fa-square wm-black', function(btn) { changeDrawColour("#000000"); }) var whiteButton = L.easyButton('fa-square wm-white', function(btn) { changeDrawColour("#EEEEEE"); }) colorControl = L.easyBar([redButton,blueButton,greenButton,yellowButton,cyanButton,magentaButton,blackButton,whiteButton]); layers["_drawing"] = new L.FeatureGroup(); overlays["drawing"] = layers["_drawing"]; map.pm.addControls({ position: 'topleft', drawMarker: false, drawCircleMarker: false, drawText: false, editControls: false }); map.pm.toggleControls(); var changeDrawColour = function(col) { drawingColour = col; map.pm.setPathOptions({ color: drawingColour, fillColor: drawingColour, fillOpacity: 0.4 }); } var shape; map.on("pm:create", (e) => { drawCount = drawCount + 1; var name = e.shape + drawCount; e.layer.on('contextmenu', function(e) { L.DomEvent.stopPropagation(e); var name = e.target.name; var rmen = L.popup({offset:[0,-12]}).setLatLng(e.latlng); var d = drawcontextmenu || "<input type='text' value='${name}' id='dinput' placeholder='name (,icon, layer)'/><br/><button onclick='editPoly(\"${name}\");'>Edit points</button><button onclick='editPoly(\"${name}\",\"drag\");'>Drag</button><button onclick='editPoly(\"${name}\",\"rot\");'>Rotate</button><button onclick='delMarker(\"${name}\",true);'>Delete</button><button onclick='sendDrawing();'>OK</button>"; d = d.replace(/\${name}/g,name); if (e.target.value) { for (const item in e.target.value) { d = d.replace(new RegExp("\\${"+item+"}","g"),e.target.value[item]); } } rmen.setContent(d); setImmediate(function() { map.openPopup(rmen) }); }); e.layer.bindPopup(name); var la, lo, cent; if (e.layer.hasOwnProperty("_latlng")) { la = e.layer._latlng.lat; lo = e.layer._latlng.lng; cent = e.layer._latlng; } else { cent = e.layer.getBounds().getCenter(); } var m = {action:"draw", name:name, type:e.shape, layer:"_drawing", options:e.layer.options, radius:e.layer._mRadius, lat:la, lon:lo, drawCount:drawCount}; if (e.layer.hasOwnProperty("_latlngs")) { if (e.layer.options.fill === false) { m.line = e.layer._latlngs; } else { m.area = e.layer._latlngs[0]; } } shape = {m:m, layer:e.layer};