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,193 lines (1,088 loc) 94.7 kB
/* eslint-disable no-undef */ var startpos = [51.03, -1.379]; // 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 = 600; // default max age of icons on map in seconds - cleared after 10 mins var baselayername = "OSM grey"; // Default base layer OSM but uniform grey var ibmfoot = "&nbsp;&copy; IBM 2015,2019" var inIframe = false; var showUserMenu = true; var showLayerMenu = true; var showMouseCoords = false; var sidebyside; var layercontrol; var drawingColour = "#910000"; 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 }; // Polyfill assign for IE11 for now if (typeof Object.assign !== 'function') { // Must be writable: true, enumerable: false, configurable: true Object.defineProperty(Object, "assign", { value: function assign(target, varArgs) { // .length of function is 2 'use strict'; if (target === null || target === undefined) { throw new TypeError('Cannot convert undefined or null to object'); } var to = Object(target); for (var index = 1; index < arguments.length; index++) { var nextSource = arguments[index]; if (nextSource !== null && nextSource !== undefined) { for (var nextKey in nextSource) { // Avoid bugs when hasOwnProperty is shadowed if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; }, writable: true, configurable: true }); } // Create the socket var connect = function() { ws = new SockJS(location.pathname.split("index")[0] + 'socket'); ws.onopen = function() { console.log("CONNECTED"); if (!inIframe) { document.getElementById("footer").innerHTML = "<font color='#494'>"+ibmfoot+"</font>"; } ws.send(JSON.stringify({action:"connected"})); onoffline(); }; ws.onclose = function() { console.log("DISCONNECTED"); if (!inIframe) { document.getElementById("footer").innerHTML = "<font color='#900'>"+ibmfoot+"</font>"; } setTimeout(function() { connect(); }, 2500); }; ws.onmessage = function(e) { var data = JSON.parse(e.data); //console.log("DATA" typeof data,data); if (Array.isArray(data)) { //console.log("ARRAY"); // map.closePopup(); // var bnds= L.latLngBounds([0,0]); for (var prop in data) { if (data[prop].command) { doCommand(data[prop].command); delete data[prop].command; } if (data[prop].hasOwnProperty("name")) { setMarker(data[prop]); // bnds.extend(markers[data[prop].name].getLatLng()); } else { console.log("SKIP A",data[prop]); } } // map.fitBounds(bnds.pad(0.25)); } else { if (data.command) { doCommand(data.command); delete data.command; } if (data.hasOwnProperty("name")) { setMarker(data); } else if (data.hasOwnProperty("type")) { doGeojson(data); } else { console.log("SKIP",data); // if (typeof data === "string") { doDialog(data); } // else { console.log("SKIP",data); } } } }; } console.log("CONNECT TO",location.pathname + 'socket'); window.onunload = function() { if (ws) ws.close(); } var onoffline = function() { if (!navigator.onLine) map.addLayer(layers["_countries"]); } // Set Ctl-Alt-3 to switch to 3d view document.addEventListener ("keydown", function (ev) { if (ev.ctrlKey && ev.altKey && ev.code === "Digit3") { ws.close(); window.location.href = "index3d.html"; } }); // Create the Initial Map object. map = new L.map('map').setView(startpos, startzoom); // 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 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("#E7827F"); }) var blueButton = L.easyButton('fa-square wm-blue', function(btn) { changeDrawColour("#94CCE2"); }) var greenButton = L.easyButton('fa-square wm-green', function(btn) { changeDrawColour("#ACD6A4"); }) var yellowButton = L.easyButton('fa-square wm-yellow', function(btn) { changeDrawColour("#F5F08B"); }) var blackButton = L.easyButton('fa-square wm-black', function(btn) { changeDrawColour("#444444"); }) var whiteButton = L.easyButton('fa-square wm-white', function(btn) { changeDrawColour("#EEEEEE"); }) var colorControl = L.easyBar([redButton,blueButton,greenButton,yellowButton,blackButton,whiteButton]); function onLocationFound(e) { var radius = e.accuracy; //L.marker(e.latlng).addTo(map).bindPopup("You are within " + radius + " meters from this point").openPopup(); L.circle(e.latlng, radius, {color:"cyan", weight:4, opacity:0.8, fill:false, clickable:false}).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:"cyan", weight:3, opacity:0.8, clickable:false}).addTo(map); } ws.send(JSON.stringify({action:"point", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5), point:"self", bearing:e.heading, speed:(e.speed*3.6 || undefined)})); } function onLocationError(e) { console.log(e.message); } // Move some bits around if in an iframe if (window.self !== window.top) { console.log("IN an iframe"); inIframe = true; 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); // Add the locate my position button L.easyButton( 'fa-crosshairs fa-lg', function() { map.locate({setView:true, maxZoom:16}); }, "Locate me").addTo(map); map.on('locationfound', onLocationFound); map.on('locationerror', onLocationError); // Add the measure/ruler button rulerButton.addTo(map); // Create the clear heatmap button var clrHeat = L.easyButton( '<b>Reset Heatmap</b>', function() { console.log("Reset heatmap"); heat.setLatLngs([]); }, "Clears the current heatmap", "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; if (showUserMenu) { 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"); } // if ( window.localStorage.hasOwnProperty("clusterat") ) { // clusterAt = window.localStorage.getItem("clusterat"); // document.getElementById("setclus").value = clusterAt; // } if ( window.localStorage.hasOwnProperty("maxage") ) { maxage = window.localStorage.getItem("maxage"); document.getElementById("maxage").value = maxage; } } // Add graticule var showGrid = 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} ] }); 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 mb = new L.LatLngBounds([[-120,-360],[120,360]]); function doLock(v) { if (v !== undefined) { lockit = v; } if (lockit === false) { mb = new L.LatLngBounds([[-120,-360],[120,360]]); map.dragging.enable(); } else { mb = 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("clusterat", clusterAt); window.localStorage.setItem("maxage", maxage); console.log("Saved :",JSON.stringify(map.getCenter()),map.getZoom(),baselayername); } map.setMaxBounds(mb); //console.log("Map bounds lock :",lockit); } // Remove old markers function doTidyUp(l) { 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+"_"]; } if (typeof polygons[m] != "undefined") { layers[markers[m].lay].removeLayer(polygons[m]); delete polygons[m]; } layers[markers[m].lay].removeLayer(markers[m]); delete markers[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 //} //every minute //console.log("Stale time set :",maxage+"s"); } setMaxAge(); // move the daylight / nighttime boundary (if enabled) every minute function moveTerminator() { // if terminator line plotted move it every minute if (layers["_daynight"].getLayers().length > 0) { layers["_daynight"].clearLayers(); layers["_daynight"].addLayer(L.terminator()); } } setInterval( function() { moveTerminator() }, 60000 ); function setCluster(v) { clusterAt = v || 0; console.log("clusterAt set:",clusterAt); showMapCurrentZoom(); } // 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())) && (mb.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=1&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 bb = data[0].boundingbox; map.fitBounds([ [bb[0],bb[2]], [bb[1],bb[3]] ]); map.panTo([data[0].lat, data[0].lon]); } 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>"; } }); } 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())) && (mb.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.addControl(drawControl); 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.removeControl(colorControl); map.removeControl(drawControl); } 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(),". clusterAt:",clusterAt); 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(); }); //map.on('contextmenu', function(e) { // ws.send(JSON.stringify({action:"rightclick", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5)})); //}); // 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, layer, colour)'/>"; var rightmenuMap = L.popup({keepInView:true, minWidth:250}).setContent(addmenu); 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] || "_drawing").trim(); var colo = (bits[3] || "#910000").trim(); var drag = true; var regi = /^[S,G,E,I,O][A-Z]{4}.*/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}; if (regi.test(icon)) { d.SIDC = (icon.toUpperCase()+"------------").substr(0,12); } else { d.icon = icon; d.iconColor = colo; } ws.send(JSON.stringify(d)); delete d.action; setMarker(d); map.addLayer(layers[lay]); } var feedback = function(n,v,a) { ws.send(JSON.stringify({action:a||"feedback", name:n, value:v})); } // 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; rightmenuMap.setLatLng(e.latlng); map.openPopup(rightmenuMap); setTimeout( function() { document.getElementById('rinput').focus(); }, 200); } }, 300); } }); // Add all the base layer maps // Use this for OSM online maps var osmUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; //var osmUrl='https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png'; var osmAttrib='Map data © OpenStreetMap contributors'; var osmg = new L.TileLayer.Grayscale(osmUrl, {attribution:osmAttrib, maxNativeZoom:19, maxZoom:20}); basemaps["OSM grey"] = osmg; var osm = new L.TileLayer(osmUrl, {attribution:osmAttrib, maxNativeZoom:19, maxZoom:20}); basemaps["OSM"] = osm; // Extra Leaflet map layers from https://leaflet-extras.github.io/leaflet-providers/preview/ var Esri_WorldStreetMap = 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 }); basemaps["Esri"] = Esri_WorldStreetMap; var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution:'Tiles &copy; Esri', maxNativeZoom:17, maxZoom:20 }); basemaps["Esri Satellite"] = Esri_WorldImagery; var Esri_WorldTopoMap = 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' }); basemaps["Esri Topography"] = Esri_WorldTopoMap; // var Esri_WorldShadedRelief = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}', { // attribution: 'Tiles &copy; Esri', // maxNativeZoom:13 // }); // basemaps["Esri Terrain"] = Esri_WorldShadedRelief; var Esri_OceanBasemap = 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', maxZoom: 13 }); basemaps["Esri Ocean"] = Esri_OceanBasemap; var Esri_WorldGrayCanvas = 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', maxZoom: 16 }); basemaps["Esri Dark Grey"] = Esri_WorldGrayCanvas; // var OpenMapSurfer_Roads = L.tileLayer('https://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}', { // maxZoom: 18, // attribution: 'Imagery from <a href="https://giscience.uni-hd.de/">University of Heidelberg</a> &mdash; Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' // }); // basemaps["Mapsurfer"] = OpenMapSurfer_Roads; // var MapQuestOpen_OSM = L.tileLayer('https://otile{s}.mqcdn.com/tiles/1.0.0/{type}/{z}/{x}/{y}.{ext}', { // type: 'map', // ext: 'jpg', // attribution: 'Tiles Courtesy of <a href="https://www.mapquest.com/">MapQuest</a> &mdash; Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', // subdomains: '1234', // maxNativeZoom: 17 // }); //basemaps["MapQuest OSM"] = MapQuestOpen_OSM; var Esri_NatGeoWorldMap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles &copy; Esri', maxNativeZoom:12 }); basemaps["Nat Geo"] = Esri_NatGeoWorldMap; var NLS_OS_opendata = 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:18, maxZoom:18, subdomains: '0123' }); basemaps["UK OS Opendata"] = NLS_OS_opendata; var HikeBike_HikeBike = 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' }); basemaps["Hike Bike"] = HikeBike_HikeBike; var NLS_OS_1919_1947 = L.tileLayer( 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg', { attribution: 'Historical Maps Layer, from <a href="https://maps.nls.uk/projects/api/">NLS Maps</a>', bounds: [[49.6, -12], [61.7, 3]], minZoom:1, maxZoom:18, subdomains: '0123' }); basemaps["UK OS 1919-47"] = NLS_OS_1919_1947; //var NLS_OS_1900 = L.tileLayer('https://nls-{s}.tileserver.com/NLS_API/{z}/{x}/{y}.jpg', { var NLS_OS_1900 = L.tileLayer('https://nls-{s}.tileserver.com/fpsUZbzrfb5d/{z}/{x}/{y}.jpg', { 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:19, maxZoom:20, subdomains: '0123' }); basemaps["UK OS 1900"] = NLS_OS_1900; //var CartoPos = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', { // attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://cartodb.com/attributions">CartoDB</a>' //}); //basemaps["CartoDB Light"] = CartoPos; // Nice terrain based maps by Stamen Design var terrainUrl = "https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg"; basemaps["Terrain"] = 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 var watercolorUrl = "https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg"; basemaps["Watercolor"] = 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>' }); // Now add the overlays // Add the countries (world-110m) for offline use 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"]; // Add the day/night overlay layers["_daynight"] = new L.LayerGroup(); overlays["day/night"] = layers["_daynight"]; // Add the drawing layer for fun... layers["_drawing"] = new L.FeatureGroup(); overlays["drawing"] = layers["_drawing"]; map.options.drawControlTooltips = false; var drawCount = 0; var drawControl = new L.Control.Draw({ draw: { polyline: { shapeOptions: { clickable:true } }, marker: false, //circle: false, circle: { shapeOptions: { clickable:true } }, circlemarker: false, rectangle: { shapeOptions: { clickable:true } }, polygon: { shapeOptions: { clickable:true } } }, edit: false // { // featureGroup: layers["_drawing"], // remove: true, // edit: true // } }); var changeDrawColour = function(col) { drawControl.setDrawingOptions({ polyline: { shapeOptions: { color:col } }, circle: { shapeOptions: { color:col } }, rectangle: { shapeOptions: { color:col } }, polygon: { shapeOptions: { color:col } } }); } map.on('draw:created', function (e) { var name = e.layerType + drawCount; drawCount = drawCount + 1; var rightmenuMarker = L.popup({offset:[0,-12]}).setContent("<b>"+name+"</b><br/><button onclick='editPoly(\""+name+"\",true);'>Edit</button><button onclick='delMarker(\""+name+"\",true);'>Delete</button>"); e.layer.on('contextmenu', function(e) { L.DomEvent.stopPropagation(e); rightmenuMarker.setLatLng(e.latlng); map.openPopup(rightmenuMarker); }); var la, lo; if (e.layer.hasOwnProperty("_latlng")) { la = e.layer._latlng.lat; lo = e.layer._latlng.lng; } var m = {action:"draw", name:name, layer:"_drawing", options:e.layer.options, radius:e.layer._mRadius, lat:la, lon:lo}; if (e.layer.hasOwnProperty("_latlngs")) { if (e.layer.options.fill === false) { m.line = e.layer._latlngs; } else { m.area = e.layer._latlngs[0]; } } ws.send(JSON.stringify(m)); polygons[name] = e.layer; polygons[name].lay = "_drawing"; layers["_drawing"].addLayer(e.layer); }); // Add the heatmap layer var heat = L.heatLayer([], {radius:60, gradient:{0.2:'blue', 0.4:'lime', 0.6:'red', 0.8:'yellow', 1:'white'}}); layers["_heat"] = new L.LayerGroup().addLayer(heat); overlays["heatmap"] = layers["_heat"]; // Add the buildings layer overlays["buildings"] = new OSMBuildings(map).load(); map.removeLayer(overlays["buildings"]); // Hide it at start // Add Roads overlays["roads"] = L.tileLayer('https://{s}.tile.openstreetmap.se/hydda/roads_and_labels/{z}/{x}/{y}.png', { maxZoom: 18, attribution: 'Tiles courtesy of <a href="https://openstreetmap.se/" target="_blank">OpenStreetMap Sweden</a> &mdash; Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', opacity: 0.8 }); // Add Railways overlays["railways"] = L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', { maxZoom: 19, attribution: 'Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Map style: &copy; <a href="https://www.OpenRailwayMap.org">OpenRailwayMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)' }); // Add Public Transport (Buses) overlays["public transport"] = L.tileLayer('https://openptmap.org/tiles/{z}/{x}/{y}.png', { maxZoom: 17, attribution: 'Map data: &copy; <a href="https://www.openptmap.org">OpenPtMap</a> contributors' }); // Add the OpenSea markers layer overlays["ship nav"] = L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', { maxZoom: 19, attribution: 'Map data: &copy; <a href="https://www.openseamap.org">OpenSeaMap</a> contributors' }); if (showUserMenu) { if ( window.localStorage.hasOwnProperty("lastlayer") ) { if ( basemaps[window.localStorage.getItem("lastlayer")] ) { baselayername = window.localStorage.getItem("lastlayer"); } } } basemaps[baselayername].addTo(map); // 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 the layers control widget if (!inIframe) { layercontrol.addTo(map); } else { showLayerMenu = false;} var coords = L.control.coordinates({ position:"bottomleft", //optional default "bottomright" decimals:4, //optional default 4 decimalSeperator:".", //optional default "." labelTemplateLat:"&nbsp;Lat: {y}", //optional default "Lat: {y}" labelTemplateLng:"&nbsp;Lon: {x}", //optional default "Lng: {x}" enableUserInput:false, //optional default true useDMS:true, //optional default false useLatLngOrder: true, //ordering of labels, default false-> lng-lat }); // Add an optional legend var legend = L.control({ position: "bottomleft" }); // Add the dialog box for messages var dialogue = L.control.dialog({initOpen:false, size:[600,400], anchor:[50,150]}).addTo(map); dialogue.freeze(); var doDialog = function(d) { //console.log("DIALOGUE",d); dialogue.setContent(d); dialogue.open(); } var helpText = '<h3>Node-RED - Map all the things</h3><br/>'; helpText += '<p><i class="fa fa-search fa-lg fa-fw"></i> <b>Search</b> - You may enter a name, or partial name, or icon name of an object to search for.'; helpText += 'The map will then jump to centre on each of the results in turn. If nothing is found locally it will try to'; helpText += 'search for a place name if connected to a network.</p>'; helpText += '<p><i class="fa fa-spinner fa-lg fa-fw"></i> <b>Set Max Age</b> - You can set the time after which points'; helpText += 'that haven\'t been updated get removed.</p>'; helpText += '<p><i class="fa fa-search-plus fa-lg fa-fw"></i> <b>Cluster at zoom</b> - lower numbers mean less clustering. 0 means disable totally.</p>'; helpText += '<p><i class="fa fa-arrows fa-lg fa-fw"></i> <b>Auto Pan</b> - When selected, the map will'; helpText += 'automatically move to centre on each data point as they arrive.</p>'; helpText += '<p><i class="fa fa-lock fa-lg fa-fw"></i> <b>Lock Map</b> - When selected will save the'; helpText += 'currently displayed area and basemap.'; helpText += 'Reloading the map in the current browser will return to the same view.'; helpText += 'This can be used to set your initial start position.'; helpText += 'While active it also restricts the "auto pan" and "search" to within that area.</p>'; helpText += '<p><i class="fa fa-globe fa-lg fa-fw"></i> <b>Heatmap all layers</b> - When selected'; helpText += 'all layers whether hidden or not will contribute to the heatmap.'; helpText += 'The default is that only visible layers add to the heatmap.</p>'; // Delete a marker (and notify websocket) var delMarker = function(dname,note) { if (note) { map.closePopup(); } if (typeof polygons[dname] != "undefined") { layers[polygons[dname].lay].removeLayer(polygons[dname]); delete polygons[dname]; } if (typeof polygons[dname+"_"] != "undefined") { layers[polygons[dname+"_"].lay].removeLayer(polygons[dname+"_"]); delete polygons[dname+"_"]; } if (typeof markers[dname] != "undefined") { layers[markers[dname].lay].removeLayer(markers[dname]); map.removeLayer(markers[dname]); delete markers[dname]; } delete allData[dname]; if (note) { ws.send(JSON.stringify({action:"delete", name:dname, deleted:true})); } } var editPoly = function(pname) { map.closePopup(); editFeatureGroup = L.featureGroup(); editToolbar = new L.EditToolbar({ featureGroup:editFeatureGroup }); editHandler = editToolbar.getModeHandlers()[0].handler; editHandler._map = map; polygons[pname].on("dblclick", function(e) { editHandler.disable(); editFeatureGroup.removeLayer(polygons[pname]); polygons[pname].off("dblclick"); L.DomEvent.stopPropagation(e); var la, lo; if (e.target.hasOwnProperty("_latlng")) { la = e.target._latlng.lat; lo = e.target._latlng.lng; } var m = {action:"draw", name:pname, layer:polygons[pname].lay, options:e.target.options, radius:e.target._mRadius, lat:la, lon:lo}; if (e.target.hasOwnProperty("_latlngs")) { if (e.target.options.fill === false) { m.line = e.target._latlngs; } else { m.area = e.target._latlngs[0]; } } ws.send(JSON.stringify(m)); }) editFeatureGroup.addLayer(polygons[pname]); editHandler.enable(); } var rangerings = function(latlng, options) { options = L.extend({ ranges: [250,500,750,1000], pan: 0, fov: 60, color: '#910000' }, options); var rings = L.featureGroup(); if (typeof options.ranges === "number") { options.ranges = [ options.ranges ]; } for (var i = 0; i < options.ranges.length; i++) { L.semiCircle(latlng, { radius: options.ranges[i], fill: false, color: options.color, weight: 1 }).setDirection(options.pan, options.fov).addTo(rings); } return rings; } // the MAIN add something to map function function setMarker(data) { var rightmenu = function(m) { // customise right click context menu var rightcontext = ""; if (polygons[data.name] == undefined) { rightcontext = "<button id='delbutton' onclick='delMarker(\""+data.name+"\",true);'>Delete</button>"; } else if (data.editable) { rightcontext = "<button onclick='editPoly(\""+data.name+"\",true);'>Edit</button><button onclick='delMarker(\""+data.name+"\",true);'>Delete</button>"; } if ((data.contextmenu !== undefined) && (typeof data.contextmenu === "string")) { rightcontext = data.contextmenu.replace(/\$name/g,data.name); delete data.contextmenu; } if (rightcontext.length > 0) { var rightmenuMarker = L.popup({offset:[0,-12]}).setContent("<b>"+data.name+"</b><br/>"+rightcontext); if (hiderightclick !== true) { m.on('contextmenu', function(e) { L.DomEvent.stopPropagation(e); rightmenuMarker.setLatLng(e.latlng); map.openPopup(rightmenuMarker); }); } } else { if (hiderightclick !== true) { m.on('contextmenu', function(e) { L.DomEvent.stopPropagation(e); }); } } return m; } //console.log("DATA" typeof data, data); if (data.deleted) { // remove markers we are told to delMarker(data.name); return; } var ll; var lli = null; var opt = {}; opt.color = data.color || data.lineColor || "#910000"; opt.fillColor = data.fillColor || "#910000"; opt.stroke = (data.hasOwnProperty("stroke")) ? data.stroke : true; opt.weight = data.weight || 2; opt.opacity = data.opacity || 1; opt.fillOpacity = data.fillOpacity || 0.2; opt.clickable = (data.hasOwnProperty("clickable")) ? data.clickable : false; opt.fill = (data.hasOwnProperty("fill")) ? data.fill : true; if (data.hasOwnProperty("dashArray")) { opt.dashArray = data.dashArray; } // Replace building if (data.hasOwnProperty("building")) { if ((data.building === "") && layers.hasOwnProperty("buildings")) { map.removeLayer(layers["buildings"]); layercontrol._update(); layers["buildings"] = overlays["buildings"].set(""); return; } //layers["buildings"] = new OSMBuildings(map).set(data.building); layers["buildings"] = overlays["buildings"].set(data.building); map.addLayer(layers["buildings"]); return; } var lay = data.layer || "unknown"; if (!data.hasOwnProperty("action") || data.action.indexOf("layer") === -1) { if (typeof layers[lay] == "undefined") { // add layer if if doesn't exist if (clusterAt > 0) { layers[lay] = new L.MarkerClusterGroup({ maxClusterRadius:50, spiderfyDistanceMultiplier:1.8, disableClusteringAtZoom:clusterAt //zoomToBoundsOnClick:false }); } else { layers[lay] = new L.LayerGroup(); } overlays[lay] = layers[lay]; if (showLayerMenu !== false) { layercontrol.addOverlay(layers[lay],lay); } map.addLayer(overlays[lay]); //console.log("ADDED LAYER",lay,layers); } if (!allData.hasOwnProperty(data.name)) { allData[data.name] = {}; } delete data.action; Object.keys(data).forEach(function(key) { if (data[key] == null) { delete allData[data.name][key]; } else { allData[data.name][key] = data[key]; } }); data = Object.assign({},allData[data.name]); } delete data.action; if (typeof markers[data.name] != "undefined") { if (markers[data.name].lay !== lay) { delMarker(data.name); } else { try {layers[lay].removeLayer(markers[data.name]); } catch(e) { console.log("OOPS"); } } } if (typeof polygons[data.name] != "undefined") { layers[lay].removeLayer(polygons[data.name]); } if (data.hasOwnProperty("line") && Array.isArray(data.line)) { delete opt.fill; if (!data.hasOwnProperty("weight")) { opt.weight = 3; } //Standard settings different for lines if (!data.hasOwnProperty("opacity")) { opt.opacity = 0.8; } var polyln = L.polyline(data.line, opt); polygons[data.name] = polyln; } else if (data.hasOwnProperty("area") && Array.isArray(data.area)) { var polyarea; if (data.area.length === 2) { polyarea = L.rectangle(data.area, opt); } else { polyarea = L.polygon(data.area, opt); } polygons[data.name] = polyarea; } else if (data.hasOwnProperty("sdlat") && data.hasOwnProperty("sdlon")) { if (!data.hasOwnProperty("iconColor")) { opt.color = "blue"; } //different standard Color Settings if (!data.hasOwnProperty("fillColor")) { opt.fillColor = "blue"; } var ellipse = L.ellipse(new L.LatLng((data.lat*1), (data.lon*1)), [200000*data.sdlon*Math.cos(data.lat*Math.PI/180), 200000*data.sdlat], 0, opt); polygons[data.name] = ellipse; } else if (data.hasOwnProperty("radius")) { if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon")) { var polycirc; if (Array.isArray(data.radius)) { polycirc = L.ellipse(new L.LatLng((data.lat*1), (data.lon*1)), [data.radius[0]*Math.cos(data.lat*Math.PI/180), data.radius[1]], data.tilt || 0, opt); } else { polycirc = L.circle(new L.LatLng((data.lat*1), (data.lon*1)), data.radius*1, opt); } polygons[data.name] = polycirc; delete (data.lat); delete (data.lon); } } else if (data.hasOwnProperty("arc")) { if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon")) { polygons[data.name] = rangerings(new L.LatLng((data.lat*1), (data.lon*1)), data.arc); } } else if (data.hasOwnProperty("geojson")) { doGeojson(data.geojson,(data.layer || "geojson"),opt); } if (polygons[data.name] !== undefined) { polygons[data.name].lay = lay; if (opt.clickable === true) { var words = "<b>"+data.name+"</b>"; if (data.popup) { words = words + "<br/>" + data.popup; } polygons[data.name].bindPopup(words, {autoClose:false, closeButton:true, closeOnClick:false, minWidth:200}); } //polygons[data.name] = rightmenu(polygons[data.name]); // DCJ Investigate layers[lay].addLayer(polygons[data.name]); } if (typeof data.coordinates == "object") { ll = new L.LatLng(data.coordinates[1],data.coordinates[0]); } else if (data.hasOwnProperty("position") && data.position.hasOwnProperty("lat") && data.position.hasOwnProperty("lon")) { data.lat = data.position.lat*1; data.lon = data.position.lon*1; data.alt = data.position.alt; if (parseFloat(data.position.alt) == data.position.alt) { data.alt = data.position.alt + " m"; } delete data.position; ll = new L.LatLng((data.lat*1), (data.lon*1)); } else if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon")) { ll = new L.LatLng((data.lat*1), (data.lon*1)); } else if (data.hasOwnProperty("latitude") && data.hasOwnProperty("longitude")) { ll = new L.LatLng((data.latitude*1), (data.longitude*1)); } else { // console.log("No location:",data); return; } // Adding new L.LatLng object (lli) when optional intensity value is defined. Only for use in heatmap layer if (typeof data.coordinates == "object") { lli = new L.LatLng(data.coordinates[2],data.coordinates[1],data.coordinates[0]); } else if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon") && data.hasOwnProperty("intensity")) { lli = new L.LatLng((data.lat*1), (data.lon*1), (data.intensity*1)); } else if (data.hasOwnProperty("latitude") && data.hasOwnProperty("longitude") && data.hasOwnProperty("intensity")) { lli = new L.LatLng((data.latitude*1), (data.longitude*1), (data.intensity*1)); } else { lli = ll } // Create the icons... handle plane, car, ship, wind, earthquake as specials var marker, myMarker; var icon, q; var words = ""; var labelOffset = [12,0]; var drag = false; if (data.draggable === true) { drag = true; } if (data.hasOwnProperty("icon")) { if (data.icon === "ship") { marker = L.boatMarker(ll, { title: data.name, color: (data.iconColor || "blue") }); marker.setHeading(parseFloat(data.hdg || data.bearing || "0")); q = 'https://www.bing.com/images/search?q='+data.icon+'%20%2B"'+encodeURIComponent(data.name)+'"'; words += '<a href=\''+q+'\' target="_thingpic">Pictures</a><br>'; } else if (data.icon === "plane") { data.iconColor = data.iconColor || "black"; if (data.hasOwnProperty("squawk")) { if (data.squawk == 7500 || data.squawk == 7600 || data.squawk == 7700) { data.iconColor = "red"; } } icon = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="310px" height="310px" viewBox="0 0 310 310">'; icon += '<path d="M134.875,19.74c0.04-22.771,34.363-22.771,34.34,0.642v95.563L303,196.354v35.306l-133.144-43.821v71.424l30.813,24.072v27.923l-47.501-14.764l-47.501,14.764v-27.923l30.491-24.072v-71.424L3,231.66v-35.306l131.875-80.409V19.74z" fill="'+data.iconColor+'"/></svg>'; var svgplane = "data:image/svg+xml;base64," + btoa(icon); var dir = parseFloat(data.hdg || data.bearing || "0"); myMarker = L.divIcon({ className:"planeicon", iconAnchor: [16, 16], html:'<img src="'+svgplane+'" style="width:32px; height:32px; -webkit-transform:rotate('+dir+'deg); -moz-transform:rotate('+dir+'deg);"/>' }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); //q = 'https://www.bing.com/images/search?q='+data.icon+'%20'+encodeURIComponent(data.name); //words += '<a href=\''+q+'\' target="_thingpic">Pictures</a><br>'; } else if (data.icon === "helicopter") { data.iconColor = data.iconColor || "black"; if (data.hasOwnProperty("squawk")) { if (data.squawk == 7500 || data.squawk == 7600 || data.squawk == 7700) { data.iconColor = "red"; } } icon = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="314" height="314" viewBox="0 0 314.5 314.5">'; icon += '<path d="M268.8 3c-3.1-3.1-8.3-2.9-11.7 0.5L204.9 55.7C198.5 23.3 180.8 0 159.9 0c-21.9 0-40.3 25.5-45.7 60.2L57.4 3.5c-3.4-3.4-8.6-3.6-11.7-0.5 -3.1 3.1-2.9 8.4 0.5 11.7l66.3 66.3c0 0.2 0 0.4 0 0.6 0 20.9 4.6 39.9 12.1 54.4l-78.4 78.4c-3.4 3.4-3.6 8.6-0.5 11.7 3.1 3.1 8.3 2.9 11.7-0.5l76.1-76.1c3.2 3.7 6.7 6.7 10.4 8.9v105.8l-47.7 32.2v18l50.2-22.3h26.9l50.2 22.3v-18L175.8 264.2v-105.8c2.7-1.7 5.4-3.8 7.8-6.2l73.4 73.4c3.4 3.4 8.6 3.6 11.7 0.5 3.1-3.1 2.9-8.3-0.5-11.7l-74.9-74.9c8.6-14.8 14-35.2 14-57.8 0-1.9-0.1-3.8-0.2-5.8l61.2-61.2C271.7 11.3 271.9 6.1 268.8 3z" fill="'+data.iconColor+'"/></svg>'; var svgheli = "data:image/svg+xml;base64," + btoa(icon); var dir =