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