/* 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 = " © DCJ 2023"; 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 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 = ""+pagefoot+""; } ws.send(JSON.stringify({action:"connected",parameters:Object.fromEntries((new URL(location)).searchParams),clientTimezone:Intl.DateTimeFormat().resolvedOptions().timeZone || false})); onoffline(); }; ws.onclose = function() { console.log("DISCONNECTED"); if (!inIframe) { document.getElementById("footer").innerHTML = ""+pagefoot+""; } 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"); 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 if (data[prop].hasOwnProperty("filename") && data[prop].filename === "doc.kml") { data = {command:{map:{overlay:"KML", kml:data[prop].payload}}}; doCommand(data.command); return; } else { console.log("SKIP array item",data[prop]); } } } else { // Handle some raw string data overlays if (typeof data === "string" && data.indexOf(" { 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('' helpMenu += ' '; helpMenu += ' Set Max Age s'; helpMenu += ' Cluster at zoom <'; helpMenu += ' Auto Pan Map'; helpMenu += ' Lock Map'; helpMenu += ' Heatmap all layers'; if (!inIframe) { helpMenu += 'Help'; } else { helpMenu += '' } 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() { if (!edgeEnabled) { return; } map.removeLayer(edgeLayer) 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")) { markerLatLng = markers[id].getLatLng(); if ( viewBounds.contains(markerLatLng) && !mapBounds.contains(markerLatLng) ) { 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+"_"]; } 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 } 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 && 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(); } // 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=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 = " Not Found"; } }) .catch(function(err) { if (err.toString() === "TypeError: Failed to fetch") { document.getElementById('searchResult').innerHTML = " Not Found"; } }); } else { if (lockit) { document.getElementById('searchResult').innerHTML = " Found "+marks.length+" results within bounds."; } else { document.getElementById('searchResult').innerHTML = " Found "+marks.length+" results."; } } } // 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 = "Add marker
"; if (navigator.onLine) { addmenu += '
MilSymbol SIDC generator'; } 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 = /^[S,G,E,I,O][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,v,a,c) { if (v === "_form") { v = form; } if (markers[n]) { console.log("FB1",n,v,a,c) allData[n].action = a || "feedback"; if (v !== undefined) { allData[n][a||"value"] = v; } ws.send(JSON.stringify(allData[n])); setMarker(allData[n]); } else if (polygons[n]) { console.log("FB2",n,v,a) sendDrawing(n,v,a) } else { if (n === undefined) { n = "map"; } console.log("FB3",n,v,a,c) rmenudata = v; ws.send(JSON.stringify({action:a||"feedback", name:n, value:v, lat:rclk.lat, lon:rclk.lng})); } 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; 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 © 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 © 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 © Esri — 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 © 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 © Esri — 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 © Esri — 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 © Esri', maxNativeZoom:12 }); } if (maplist.indexOf("UKOS")!==-1) { basemaps[layerlookup["UKOS"]] = L.tileLayer('https://geo.nls.uk/maps/opendata/{z}/{x}/{y}.png', { attribution: 'National Library of Scotland Historic Maps', 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: '© OpenTopoMap contributors' }); } if (maplist.indexOf("HB")!==-1) { basemaps[layerlookup["HB"]] = L.tileLayer('https://tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap 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 © 高德地图', 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 Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA' }); } // 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 Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA' }); } } if (first) { if (layerlookup[first]) { baselayername = layerlookup[first]; } else { basenayername = first; } } else { basenayername = Object.keys(basemaps)[0]; } 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 || "
"; 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}; polygons[name] = shape.layer; polygons[name].lay = "_drawing"; polygons[name].name = name; layers["_drawing"].addLayer(shape.layer); var rightmenuMarker = L.popup({offset:[0,-12]}).setContent(drawcontextmenu.replace(/\${name}/g,name).replace(/\${.*?}/g,'') || "
"); if (e.layer.options.fill === false && navigator.onLine) { rightmenuMarker = L.popup({offset:[0,-12]}).setContent(drawcontextmenu.replace(/\${name}/g,name).replace(/\${.*?}/g,'') || "
"); } rightmenuMarker.setLatLng(cent); setTimeout(function() {map.openPopup(rightmenuMarker)},25); }); sendDrawing = function(n,v,a) { var thing = document.getElementById('dinput')?.value || n; map.closePopup(); shape.m.name = thing; shape.layer.bindPopup(thing); delMarker(n,true); if (v) { shape.layer.value = v; shape.m.value = v; } polygons[thing] = shape.layer; polygons[thing].lay = "_drawing"; polygons[thing].name = thing; layers["_drawing"].addLayer(shape.layer); ws.send(JSON.stringify(shape.m)); } var defaultOptions = function () { var options = {}; options.precision = 5; options.factor = Math.pow(10, options.precision); options.dimension = 2; return options; }; var decode = function (encoded, options) { options = defaultOptions(options); var flatPoints = decodeDeltas(encoded); var points = []; for (var i = 0, len = flatPoints.length; i + (options.dimension - 1) < len;) { var point = []; for (var dim = 0; dim < options.dimension; ++dim) { point.push(flatPoints[i++]); } points.push(point); } return points; } var decodeDeltas = function (encoded, options) { options = defaultOptions(options); var lastNumbers = []; var numbers = decodeFloats(encoded, options); for (var i = 0, len = numbers.length; i < len;) { for (var d = 0; d < options.dimension; ++d, ++i) { numbers[i] = Math.round((lastNumbers[d] = numbers[i] + (lastNumbers[d] || 0)) * options.factor) / options.factor; } } return numbers; } var decodeFloats = function (encoded, options) { options = defaultOptions(options); var numbers = decodeSignedIntegers(encoded); for (var i = 0, len = numbers.length; i < len; ++i) { numbers[i] /= options.factor; } return numbers; } var decodeSignedIntegers = function (encoded) { var numbers = decodeUnsignedIntegers(encoded); for (var i = 0, len = numbers.length; i < len; ++i) { var num = numbers[i]; numbers[i] = (num & 1) ? ~(num >> 1) : (num >> 1); } return numbers; } var decodeUnsignedIntegers = function (encoded) { var numbers = []; var current = 0; var shift = 0; for (var i = 0, len = encoded.length; i < len; ++i) { var b = encoded.charCodeAt(i) - 63; current |= (b & 0x1f) << shift; if (b < 0x20) { numbers.push(current); current = 0; shift = 0; } else { shift += 5; } } return numbers; } sendRoute = function(n) { var p = (polygons[n]._latlngs.map(function(x) { return x.lng+","+x.lat; })).join(';'); fetch('https://router.project-osrm.org/route/v1/driving/'+p) .then(response => response.json()) .then(data => { if (data.code !== "Ok") { sendDrawing(n); } var r = decode(data.routes[0].geometry).map( x => L.latLng(x[0],x[1]) ); polygons[n]._latlngs = r; shape.m.line = r; // shape.m.type = {label:"routing", distance:data.routes[0].distance, duration:data.routes[0].duration} shape.m.type = "route"; shape.m.distance = data.routes[0].distance; shape.m.duration = data.routes[0].duration; sendDrawing(n); }); } changeDrawColour("#4040F0"); // Set default drawing color to blue on start } // Add the countries (world-110m) for offline use if (overlist.indexOf("CO") !== -1 || !navigator.onLine) { 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 if (overlist.indexOf("DN")!==-1) { layers["_daynight"] = new L.LayerGroup(); overlays["day/night"] = layers["_daynight"]; } // Add live rain data if (overlist.indexOf("RA")!==-1) { if (navigator.onLine) { overlays["rainfall"] = new L.TileLayer('https://tilecache.rainviewer.com/v2/radar/' + parseInt(Date.now()/600000)*600 + '/256/{z}/{x}/{y}/2/1_1.png', { tileSize: 256, opacity: 0.4, transparent: true, attribution: 'rainviewer.com' }); } } // Add the buildings layer if (overlist.indexOf("BU")!==-1) { overlays["buildings"] = new OSMBuildings(map).load(); // map.removeLayer(overlays["buildings"]); // Hide it at start } // Add Railways if (overlist.indexOf("RW")!==-1) { // eg https://a.tiles.openrailwaymap.org/standard/11/1015/686.png overlays["railways"] = L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', { maxZoom: 19, attribution: 'Map data: © OpenStreetMap | Map style: © OpenRailwayMap (CC-BY-SA)' }); } // Add Air Corridors if (overlist.indexOf("AC")!==-1) { overlays["air corridors"] = L.tileLayer('https://{s}.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/{z}/{x}/{y}.{ext}', { attribution: 'openAIP Data (CC-BY-NC-SA)', ext: 'png', minZoom: 4, maxZoom: 15, maxNativeZoom: 14, tms: true, detectRetina: true, subdomains: '12' }); } // Add the OpenSea markers layer if (overlist.indexOf("SN")!==-1) { overlays["ship navigation"] = L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', { maxZoom: 19, attribution: 'Map data: © OpenSeaMap contributors' }); } // Add the Stamen Toner Labels layer if (overlist.indexOf("TL")!==-1) { overlays["place labels"] = L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner-labels/{z}/{x}/{y}{r}.{ext}', { attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', subdomains: 'abcd', minZoom: 0, maxZoom: 20, ext: 'png' }); } // Add the heatmap layer (and add delete LatLng function) if (overlist.indexOf("HM") !== -1) { heat = L.heatLayer([], {radius:60, gradient:{0.2:'blue', 0.4:'lime', 0.6:'red', 0.8:'yellow', 1:'white'}}); heat.delLatLng = function(ll) { heat._latlngs = heat._latlngs.filter(v => { return v != ll; } ); heat._redraw(); } layers["_heat"] = new L.LayerGroup().addLayer(heat); overlays["heatmap"] = layers["_heat"]; } if (showLayerMenu) { map.removeControl(layercontrol); layercontrol = L.control.layers(basemaps, overlays).addTo(map); } } // Add the layers control widget if (!inIframe) { layercontrol.addTo(map); } else { showLayerMenu = false;} // Add optional mouse co-ordinates display var coords = L.control.mouseCoordinate({position:"bottomleft"}); // 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 = '

Node-RED - Map all the things


'; helpText += '

Search - 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.

'; helpText += '

Set Max Age - You can set the time after which points'; helpText += 'that haven\'t been updated get removed.

'; helpText += '

Cluster at zoom - lower numbers mean less clustering. 0 means disable totally.

'; helpText += '

Auto Pan - When selected, the map will'; helpText += 'automatically move to centre on each data point as they arrive.

'; helpText += '

Lock Map - 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.

'; helpText += '

Heatmap all layers - 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.

'; // Delete a marker or shape (and notify websocket) var delMarker = function(dname,note) { if (note) { map.closePopup(); } var pol = false; if (typeof polygons[dname] != "undefined") { layers[polygons[dname].lay].removeLayer(polygons[dname]); delete polygons[dname]; pol = true; } if (typeof polygons[dname+"_"] != "undefined") { layers[polygons[dname+"_"].lay].removeLayer(polygons[dname+"_"]); delete polygons[dname+"_"]; } if (typeof markers[dname] != "undefined") { if (heat && markers[dname].hasOwnProperty("_latlng")) { try { heat.delLatLng(markers[dname]._latlng); } catch(e) { } } layers[markers[dname].lay].removeLayer(markers[dname]); map.removeLayer(markers[dname]); delete markers[dname]; } delete allData[dname]; if (note) { if (pol === true) { ws.send(JSON.stringify({action:"drawdelete", name:dname, deleted:true})); } else { ws.send(JSON.stringify({action:"delete", name:dname, deleted:true})); } } } var editPoly = function(pname,fun) { map.closePopup(); if (fun === "rot") { polygons[pname].pm.enableRotate(); } else if (fun === "drag") { polygons[pname].pm.enableLayerDrag(); } else { polygons[pname].pm.enable(); } polygons[pname].on("dblclick", function(e) { if (fun === "rot") { polygons[pname].pm.disableRotate(); } else if (fun === "drag") { polygons[pname].pm.disableLayerDrag(); } else { polygons[pname].pm.disable(); } 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.value) { m.value = e.target.value; } 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)); }) } var rangerings = function(latlng, options) { options = L.extend({ ranges: [250,500,750,1000], pan: 0, fov: 60, color: '#aaaa00' }, 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: options.weight ?? 1 }).setDirection(options.pan, options.fov).addTo(rings); } return rings; } // the MAIN add marker or shape to map function function setMarker(data) { var rightmenu = function(m) { m.on('click', function(e) { var fb = allData[data.name]; fb.action = "click"; ws.send(JSON.stringify(fb)); }); // customise right click context menu var rightcontext = ""; //if (polygons[data.name] == undefined) { rightcontext = ""; //} if (data.editable) { rightcontext = ""; } if ((data.contextmenu !== undefined) && (typeof data.contextmenu === "string")) { rightcontext = data.contextmenu.replace(/\${name}/g,data.name); delete data.contextmenu; } for (const item in allData[data.name].value) { rightcontext = rightcontext.replace(new RegExp("\\${"+item+"}","g"),allData[data.name].value[item]); } rightcontext = rightcontext.replace(/\${.*?}/g,'') if (rightcontext.length > 0) { var rightmenuMarker = L.popup({offset:[0,-12]}).setContent(""+data.name+"
"+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 == true) { // remove markers we are told to delMarker(data.name); return; } var ll; var lli = null; var opt = data.options || {}; opt.color = opt.color ?? data.color ?? data.lineColor ?? "#910000"; opt.fillColor = opt.fillColor ?? data.fillColor ?? "#910000"; opt.stroke = opt.stroke ?? (data.hasOwnProperty("stroke")) ? data.stroke : true; opt.weight = opt.weight ?? data.weight; opt.opacity = opt.opacity ?? data.opacity; opt.fillOpacity = opt.fillOpacity ?? data.fillOpacity; opt.clickable = (data.hasOwnProperty("clickable")) ? data.clickable : false; opt.fill = opt.fill ?? (data.hasOwnProperty("fill")) ? data.fill : true; if (data.hasOwnProperty("dashArray")) { opt.dashArray = data.dashArray; } if (opt.fillOpacity === undefined) { opt.fillOpacity = 0.2; } if (opt.opacity === undefined) { opt.opacity = 1; } if (opt.weight === undefined) { opt.weight = 2; } // 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 lll = "unknown"; if (markers.hasOwnProperty(data.name) && markers[data.name].hasOwnProperty("lay")) { lll = markers[data.name].lay; } var lay = data.layer ?? lll; 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("drawCount")) { drawCount = data.drawCount; } // Draw lines 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] = rightmenu(polyln); } // Draw Areas 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] = rightmenu(polyarea); } // Draw Great circles if (data.hasOwnProperty("greatcircle") && Array.isArray(data.greatcircle) && data.greatcircle.length === 2) { delete opt.fill; opt.vertices = opt.vertices || 20; if (!data.hasOwnProperty("weight")) { opt.weight = 3; } //Standard settings different for lines if (!data.hasOwnProperty("opacity")) { opt.opacity = 0.8; } var greatc = L.Polyline.Arc(data.greatcircle[0], data.greatcircle[1], opt); var aml = new L.Wrapped.Polyline(greatc._latlngs, opt); polygons[data.name] = rightmenu(aml); } // Draw error ellipses 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] = rightmenu(ellipse); } // Draw circles and ellipses 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], 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] = rightmenu(polycirc); if (!data.hasOwnProperty("icon")) { delete (data.lat); delete (data.lon); } } } // Draw arcs (and range rings) 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); } } // Draw a geojson "shape" else if (data.hasOwnProperty("geojson")) { doGeojson(data.name,data.geojson,(data.layer || "unknown"),opt); } // If we created a shape then apply some generic things to it if (polygons[data.name] !== undefined) { // Set the layer polygons[data.name].lay = lay; // if clickable then add popup if (opt.clickable === true) { var words = ""+data.name+""; if (data.popup) { words = words + "
" + data.popup; } polygons[data.name].bindPopup(words, {autoClose:false, closeButton:true, closeOnClick:true, minWidth:200}); } // add a tooltip (if supplied) if (data.hasOwnProperty("tooltip")) { polygons[data.name].bindTooltip(data.tooltip); } // add to the layers layers[lay].addLayer(polygons[data.name]); // fly or fit to the bounds if required if (data.hasOwnProperty("fly") && data.fly === true) { map.flyToBounds(polygons[data.name].getBounds(),{padding:[50,50]}) } else if (data.hasOwnProperty("fit") && data.fit === true) { map.fitBounds(polygons[data.name].getBounds(),{padding:[50,50]}) } } // Now handle the markers 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; } if (ll.lat === 0 && ll.lng === 0) { // Add a little wobble so we can zoom into each if required. console.log(data.name,"is at null island."); ll.lat = Math.round(1000000 * ll.lat + Math.random() * 10000 - 5000) / 1000000; ll.lng = Math.round(1000000 * ll.lng + Math.random() * 10000 - 5000) / 1000000; } // 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")) { var dir = parseFloat(data.track ?? data.hdg ?? data.heading ?? data.bearing ?? "0") + map.getBearing(); if (data.icon === "ship") { marker = L.boatMarker(ll, { title: data.name, color: (data.iconColor ?? "#5DADE2") }); marker.setHeading(dir); q = 'https://www.bing.com/images/search?q='+data.icon+'%20%2B"'+encodeURIComponent(data.name)+'"'; words += 'Pictures
'; } 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 = ''; icon += ''; var svgplane = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"planeicon", iconAnchor: [16, 16], html:'' }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "smallplane") { data.iconColor = data.iconColor ?? "black"; icon = ''; icon += ''; var svgplane = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"planeicon", iconAnchor: [16, 16], html:'' }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "bus") { dir = dir - 90; var sc = 1; if (dir < -90 || dir >= 90) { sc = -1; } data.iconColor = data.iconColor ?? "#910000"; var p = "m595.5 97.332-30.898-68.199c-11.141-24.223-35.344-39.762-62.004-39.801h-443.3c-32.738 0.035157-59.266 26.562-59.301 59.305v148.2c0 17.949 14.551 32.5 32.5 32.5h48.5c4.7344 23.309 25.219 40.051 49 40.051s44.266-16.742 49-40.051h242c4.7344 23.309 25.219 40.051 49 40.051s44.266-16.742 49-40.051h53.203c12.348-0.003906 23.219-8.1484 26.699-20 0.72266-2.5391 1.0898-5.1602 1.0977-7.7969v-83.5c-0.003906-7.1445-1.5391-14.203-4.5-20.703zm-545.5 12c-5.5234 0-10-4.4766-10-10v-80c0-5.5195 4.4766-10 10-10h70c5.5234 0 10 4.4805 10 10v80c0 5.5234-4.4766 10-10 10zm80 140c-16.566 0-30-13.43-30-30 0-16.566 13.434-30 30-30s30 13.434 30 30c-0.046875 16.551-13.453 29.953-30 30zm110-150c0 5.5234-4.4766 10-10 10h-70c-5.5234 0-10-4.4766-10-10v-80c0-5.5195 4.4766-10 10-10h70c5.5234 0 10 4.4805 10 10zm110 0c0 5.5234-4.4766 10-10 10h-70c-5.5234 0-10-4.4766-10-10v-80c0-5.5195 4.4766-10 10-10h70c5.5234 0 10 4.4805 10 10zm30 10c-5.5234 0-10-4.4766-10-10v-80c0-5.5195 4.4766-10 10-10h70c5.5234 0 10 4.4805 10 10v80c0 5.5234-4.4766 10-10 10zm90 140c-16.566 0-30-13.43-30-30 0-16.566 13.434-30 30-30s30 13.434 30 30c-0.046875 16.551-13.453 29.953-30 30zm19.199-140c-5.1836-0.46094-9.168-4.793-9.1992-10v-80.086c0-5.4727 4.4375-9.9141 9.9141-9.9141h12.684c18.824 0.050781 35.914 11.012 43.805 28.102l30.898 68.199c1.6133 3.5547 2.5 7.3984 2.6016 11.297z"; icon = ''; icon += ''; var svgbus = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"busicon", iconAnchor: [16, 16], html:'' }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } 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 = ''; icon += ''; var svgheli = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"heliicon", iconAnchor: [16, 16], html:'' }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "uav") { data.iconColor = data.iconColor || "black"; if (data.hasOwnProperty("squawk")) { if (data.squawk == 7500 || data.squawk == 7600 || data.squawk == 7700) { data.iconColor = "red"; } } icon = ''; icon+= ''; var svguav = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"uavicon", iconAnchor: [16, 16], html:'', }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "car") { data.iconColor = data.iconColor || "black"; icon = ''; icon += ''; var svgcar = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"caricon", iconAnchor: [16, 16], html:'', }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "sensor") { data.iconColor = data.iconColor || "#F39C12"; icon = ''; var svgcam = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"camicon", iconAnchor: [12, 12], html:'', }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "arrow") { data.iconColor = data.iconColor || "black"; icon = ''; icon += ''; var svgarrow = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"arrowicon", iconAnchor: [16, 16], html:"'", }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "wind") { data.iconColor = data.iconColor || "black"; icon = ''; icon += ''; var svgwind = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"windicon", iconAnchor: [16, 16], html:'', }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "satellite") { data.iconColor = data.iconColor || "black"; icon = ''; icon += ''; icon += ''; icon += ''; icon += ''; icon += ''; icon += ''; var svgsat = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"satelliteicon", iconAnchor: [16, 16], html:'', }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if ((data.icon === "iss") || (data.icon === "ISS")) { data.iconColor = data.iconColor || "black"; icon = ''; icon += ''; icon += ''; var svgiss = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"issicon", iconAnchor: [25, 25], html:'', }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "mayflower") { data.iconColor = data.iconColor || "#910000"; icon = ''; var svgmay = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"mayflowericon", iconAnchor: [12, 24], html:'', }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); } else if (data.icon === "locate") { data.iconColor = data.iconColor || "#00ffff"; icon = ''; icon += ''; icon += ''; icon += ''; icon += ''; //icon += ''; icon += ''; var svglocate = "data:image/svg+xml;base64," + btoa(icon); myMarker = L.divIcon({ className:"locateicon", iconAnchor: [16, 16], html:'', }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); labelOffset = [12,-4]; } else if (data.icon === "friend") { marker = L.marker(ll, { icon: L.divIcon({ className: 'circle f', iconSize: [20, 12] }), title: data.name, draggable:drag }); } else if (data.icon === "hostile") { marker = L.marker(ll, { icon: L.divIcon({ className: 'circle h', iconSize: [16, 16] }), title: data.name, draggable:drag }); } else if (data.icon === "neutral") { marker = L.marker(ll, { icon: L.divIcon({ className: 'circle n', iconSize: [16, 16] }), title: data.name, draggable:drag }); } else if (data.icon === "unknown") { marker = L.marker(ll, { icon: L.divIcon({ className: 'circle', iconSize: [16, 16] }), title: data.name, draggable:drag }); } else if (data.icon === "danger") { marker = L.marker(ll, { icon: L.divIcon({ className: 'up-triangle' }), title: data.name, draggable:drag }); } else if (data.icon === "earthquake") { marker = L.marker(ll, { icon: L.divIcon({ className: 'circle e', iconSize: [data.mag*5, data.mag*5] }), title: data.name, draggable:drag }); } else if (data.icon.match(/^:.*:$/g)) { // emoji icon :smile: var em = emojify(data.icon); var col = data.iconColor ?? "#910000"; myMarker = L.divIcon({ className:"emicon", html: '
'+em+'
', iconSize: [32, 32] }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); labelOffset = [12,-4]; } else if (data.icon.match(/^https?:.*$|^\/|^data:image\//)) { // web url icon https://... var sz = data.iconSize ?? 32; myMarker = L.icon({ iconUrl: data.icon, iconSize: [sz, sz], iconAnchor: [sz/2, sz/2], popupAnchor: [0, -sz/2] }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag, rotationAngle:dir, rotationOrigin:"center"}); labelOffset = [sz/2-4,-4]; delete data.iconSize; } else if (data.icon.substr(0,3) === "fa-") { // fa icon var col = data.iconColor ?? "#910000"; var imod = ""; if (data.icon.indexOf(" ") === -1) { imod = "fa-2x "; } myMarker = L.divIcon({ className:"faicon", html: '
', iconSize: [32, 32], iconAnchor: [16, 12], popupAnchor: [0, -16] }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); labelOffset = [8,-8]; } else if (data.icon.substr(0,3) === "wi-") { // weather icon var col = data.iconColor ?? "#910000"; var imod = ""; if (data.icon.indexOf(" ") === -1) { imod = "wi-2x "; } myMarker = L.divIcon({ className:"wiicon", html: '
', iconSize: [32, 32], iconAnchor: [16, 16], popupAnchor: [0, -16] }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); labelOffset = [16,-16]; } else { myMarker = L.VectorMarkers.icon({ // default - fa-icon in a marker shape icon: data.icon ?? "circle", markerColor: (data.iconColor ?? "#910000"), prefix: 'fa', iconColor: 'white' }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); labelOffset = [6,-6]; } } else if (data.hasOwnProperty("SIDC")) { // NATO mil2525 icons // "SIDC":"SFGPU------E***","name":"1.C2 komp","fullname":"1.C2 komp/FTS/INSS" myMarker = new ms.Symbol( data.SIDC.toUpperCase(), { uniqueDesignation:unescape(encodeURIComponent(data.name)) }); // Now that we have a symbol we can ask for the echelon and set the symbol size var opts = data.options || {}; var sz = 25; if (myMarker.hasOwnProperty("getProperties") && myMarker.getProperties().hasOwnProperty("echelon")) { sz = iconSz[myMarker.getProperties().echelon]; } opts.size = opts.size || sz; opts.size = opts.size * (opts.scale || 1); // escape out any isocodes eg flag symbols var optfields = ["additionalInformation","higherFormation","specialHeadquarters","staffComments","type","uniqueDesignation","speed","country"]; //const regex = /\p{Extended_Pictographic}/ug; const regex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/gi; optfields.forEach(function (item) { if (opts.hasOwnProperty(item) && regex.test(opts[item])) { opts[item] = unescape(encodeURIComponent(opts[item])); } }); myMarker = myMarker.setOptions(opts); var myicon = L.icon({ iconUrl: myMarker.toDataURL(), iconAnchor: [myMarker.getAnchor().x, myMarker.getAnchor().y], className: "natoicon", }); marker = L.marker(ll, { title:data.name, icon:myicon, draggable:drag }); edgeAware(); } else { // Otherwise just a generic map marker pin myMarker = L.VectorMarkers.icon({ icon: "circle", markerColor: (data.iconColor ?? "#910000"), prefix: 'fa', iconColor: 'white' }); marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag}); labelOffset = [6,-6]; } marker.name = data.name; // var createLabelIcon = function(labelText) { // return L.marker(new L.LatLng(51.05, -1.35), {icon:L.divIcon({ html:labelText })}); // } // send new position at end of move event if point is draggable if (data.draggable === true) { if (data.icon) { marker.icon = data.icon; } if (data.iconColor) { marker.iconColor = data.iconColor; } if (data.SIDC) { marker.SIDC = data.SIDC.toUpperCase(); } var oldll; marker.on('dragstart', function (e) { oldll = marker.getLatLng(); var ola = parseFloat(oldll.lat.toFixed(6)); var olo = parseFloat(oldll.lng.toFixed(6)); oldll = {lat:ola, lon:olo}; }); marker.on('dragend', function (e) { var l = marker.getLatLng().toString().replace('LatLng(','lat, lon : ').replace(')','') marker.setPopupContent(marker.getPopup().getContent().split("lat, lon")[0] + l); var b = marker.getPopup().getContent().split("heading : "); if (b.length === 2) { b = parseFloat(b[1].split("
"; delete data.photourl; } if (data.hasOwnProperty("photoUrl")) { words += "
"; delete data.photoUrl; } if (data.hasOwnProperty("videoUrl")) { words += '
'; delete data.videoUrl; } if (data.hasOwnProperty("ttl")) { // save expiry time for this marker if (data.ttl != 0) { marker.ts = parseInt(Date.now()/1000) + Number(data.ttl); } delete data.ttl; } else if (maxage != 0) { marker.ts = parseInt(Date.now()/1000) + Number(maxage); } if (data.hasOwnProperty("weblink")) { if (!Array.isArray(data.weblink) || !data.weblink.length) { if (typeof data.weblink === "string") { words += "more information...
"; } else { var tgt = data.weblink.target || "_new"; words += "" + data.weblink.name + "
"; } } else { data.weblink.forEach(function(weblink) { if (typeof weblink === "string") { words += "more information...
"; } else { var tgt = weblink.target || "_new"; words += "" + weblink.name + "
"; } }); } delete data.weblink; } var p; if (data.hasOwnProperty("popped") && (data.popped === true)) { p = true; delete data.popped; } if (data.hasOwnProperty("popped") && (data.popped === false)) { marker.closePopup(); p = false; delete data.popped; } // If .label then use that rather than name tooltip if (data.label) { if (typeof data.label === "boolean" && data.label === true) { marker.bindTooltip(data.name, { permanent:true, direction:"right", offset:labelOffset }); } else if (typeof data.label === "string" && data.label.length > 0) { marker.bindTooltip(data.label, { permanent:true, direction:"right", offset:labelOffset }); } delete marker.options.title; delete data.label; } // otherwise check for .tooltip then use that rather than name tooltip else if (data.tooltip) { if (typeof data.tooltip === "string" && data.tooltip.length > 0) { marker.bindTooltip(data.tooltip, { direction:"bottom", offset:[0,4] }); delete marker.options.title; delete data.tooltip; } } // Add right click contextmenu marker = rightmenu(marker); // Delete more already handled properties var llc = data.lineColor ?? data.color; delete data.lat; delete data.latitude; delete data.lon; delete data.longitude; if (data.arc) { delete data.arc; } if (data.layer) { delete data.layer; } if (data.lineColor) { delete data.lineColor; } if (data.color) { delete data.color; } if (data.weight) { delete data.weight; } if (data.tracklength) { delete data.tracklength; } if (data.dashArray) { delete data.dashArray; } if (data.fill) { delete data.fill; } if (data.draggable) { delete data.draggable; } //if (!isNaN(data.speed)) { data.speed = data.speed.toFixed(2); } if (data.hasOwnProperty("fillColor")) { delete data.fillColor; } if (data.hasOwnProperty("radius")) { delete data.radius; } if (data.hasOwnProperty("greatcircle")) { delete data.greatcircle; } // then any remaining properties to the info box if (data.popup) { words = data.popup; } else { words += ''; for (var i in data) { if ((i != "name") && (i != "length") && (i != "clickable")) { if (typeof data[i] === "object") { words += ''; } else { // words += i +" : "+data[i]+"
"; words += ''; } } } words += ''; words += '
'+ i +'' + JSON.stringify(data[i]) + '
'+ i +'' + data[i] + '
lat, lon'+ marker.getLatLng().toString().replace('LatLng(','').replace(')','') + '
'; } words = ""+data.name+"
" + words.replace(/\${name}/g,data.name); //"
" + words; var wopt = {autoClose:false, closeButton:true, closeOnClick:false, minWidth:200}; if (words.indexOf('