node-red-contrib-web-worldmap 5.6.2 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/README.md +5 -0
- package/node_modules/@turf/bezier-spline/package.json +4 -5
- package/node_modules/@turf/helpers/package.json +2 -3
- package/node_modules/@turf/invariant/package.json +3 -4
- package/node_modules/body-parser/HISTORY.md +6 -0
- package/node_modules/body-parser/lib/types/json.js +1 -5
- package/node_modules/body-parser/lib/types/urlencoded.js +5 -6
- package/node_modules/body-parser/package.json +2 -2
- package/node_modules/hasown/CHANGELOG.md +11 -0
- package/node_modules/hasown/eslint.config.mjs +6 -0
- package/node_modules/hasown/index.d.ts +1 -0
- package/node_modules/hasown/package.json +14 -14
- package/node_modules/side-channel-list/CHANGELOG.md +25 -4
- package/node_modules/side-channel-list/index.js +1 -3
- package/node_modules/side-channel-list/package.json +8 -8
- package/node_modules/side-channel-list/test/index.js +50 -0
- package/package.json +2 -2
- package/worldmap/css/worldmap.css +4 -0
- package/worldmap/index.html +2 -5
- package/worldmap/worldmap.js +323 -56
- package/worldmap.js +9 -7
- package/node_modules/hasown/.eslintrc +0 -5
- package/worldmap/leaflet/Leaflet.Dialog.css +0 -106
- package/worldmap/leaflet/Leaflet.Dialog.js +0 -372
package/worldmap/worldmap.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/* eslint-disable no-undef */
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Leaflet-based interactive world map client for Node-RED worldmap node.
|
|
5
|
+
* Connects to a Node-RED backend via SockJS WebSocket and renders markers,
|
|
6
|
+
* shapes, overlays, and TAK/CoT data on a Leaflet map.
|
|
7
|
+
* @author DCJ
|
|
8
|
+
* @version 2026
|
|
9
|
+
*/
|
|
10
|
+
|
|
3
11
|
var startpos = [51.05, -1.38]; // Start location - somewhere in UK :-)
|
|
4
12
|
var startzoom = 10;
|
|
5
13
|
|
|
@@ -59,6 +67,12 @@ var iconSz = {
|
|
|
59
67
|
|
|
60
68
|
var filesAdded = '';
|
|
61
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Dynamically loads a JS or CSS file into the document <head>, skipping
|
|
72
|
+
* any file that has already been added.
|
|
73
|
+
* @param {string} fileName - URL or path of the .js or .css file to load.
|
|
74
|
+
* @returns {void}
|
|
75
|
+
*/
|
|
62
76
|
var loadStatic = function(fileName) {
|
|
63
77
|
if (filesAdded.indexOf(fileName) !== -1) { return; }
|
|
64
78
|
var head = document.getElementsByTagName('head')[0]
|
|
@@ -76,7 +90,7 @@ var loadStatic = function(fileName) {
|
|
|
76
90
|
style.type = 'text/css';
|
|
77
91
|
style.rel = 'stylesheet';
|
|
78
92
|
console.log("Loading: ",fileName);
|
|
79
|
-
head.append(style)
|
|
93
|
+
head.append(style);
|
|
80
94
|
filesAdded += ' ' + fileName;
|
|
81
95
|
}
|
|
82
96
|
else {
|
|
@@ -84,6 +98,13 @@ var loadStatic = function(fileName) {
|
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Opens a SockJS WebSocket connection to the Node-RED backend.
|
|
103
|
+
* Handles connection, disconnection (with auto-reconnect after 2.5s),
|
|
104
|
+
* and incoming messages. On connect, sends client timezone and URL
|
|
105
|
+
* parameters to the server, then triggers map layer selection.
|
|
106
|
+
* @returns {void}
|
|
107
|
+
*/
|
|
87
108
|
// Create the socket
|
|
88
109
|
var connect = function() {
|
|
89
110
|
// var transports = ["websocket", "xhr-streaming", "xhr-polling"],
|
|
@@ -115,6 +136,14 @@ var connect = function() {
|
|
|
115
136
|
};
|
|
116
137
|
console.log("CONNECT TO",location.pathname + 'socket');
|
|
117
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Routes incoming WebSocket data to the appropriate handler.
|
|
141
|
+
* Supports arrays of marker/command objects, raw XML strings (NVG, KML, GPX),
|
|
142
|
+
* GeoJSON Feature/FeatureCollection objects, TAK/CoT JSON events (both
|
|
143
|
+
* fastxml and Protobuf-decoded formats), and standard worldmap marker objects.
|
|
144
|
+
* @param {Object|Array|string} data - Parsed JSON data from the WebSocket message.
|
|
145
|
+
* @returns {void}
|
|
146
|
+
*/
|
|
118
147
|
var handleData = function(data) {
|
|
119
148
|
if (Array.isArray(data)) {
|
|
120
149
|
// console.log("ARRAY:",data.length);
|
|
@@ -169,25 +198,30 @@ var handleData = function(data) {
|
|
|
169
198
|
if (JSON.stringify(data) !== '{}') {
|
|
170
199
|
console.log("SKIP",data);
|
|
171
200
|
}
|
|
172
|
-
// if (typeof data === "string") { doDialog(data); }
|
|
173
|
-
// else { console.log("SKIP",data); }
|
|
174
201
|
}
|
|
175
202
|
}
|
|
176
203
|
}
|
|
177
204
|
|
|
178
205
|
window.onunload = function() { if (ws) { ws.close(); } }
|
|
179
206
|
|
|
180
|
-
var customTopoLayer = L.geoJson(null, {clickable:false, style: {color:"blue", weight:2, fillColor:"#cf6", fillOpacity:0.04}});
|
|
181
|
-
layers["_countries"] = omnivore.topojson('images/world-50m-flat.json',null,customTopoLayer);
|
|
182
|
-
overlays["countries"] = layers["_countries"];
|
|
183
|
-
|
|
207
|
+
// var customTopoLayer = L.geoJson(null, {clickable:false, style: {color:"blue", weight:2, fillColor:"#cf6", fillOpacity:0.04}});
|
|
208
|
+
// layers["_countries"] = omnivore.topojson('images/world-50m-flat.json',null,customTopoLayer);
|
|
209
|
+
// overlays["countries"] = layers["_countries"];
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Adds an appropriate fallback base layer when the browser is offline,
|
|
213
|
+
* or when no base maps are available online. Uses a PMTiles layer if
|
|
214
|
+
* one is loaded, otherwise falls back to the countries outline overlay
|
|
215
|
+
* if available.
|
|
216
|
+
* @returns {void}
|
|
217
|
+
*/
|
|
184
218
|
var onoffline = function() {
|
|
185
219
|
if (!navigator.onLine) {
|
|
186
220
|
if (pmtloaded !== "") { basemaps[pmtloaded].addTo(map); layercontrol._update(); }
|
|
187
|
-
else { map.addLayer(overlays["countries"]); }
|
|
221
|
+
else if (overlays["countries"]) { map.addLayer(overlays["countries"]); }
|
|
188
222
|
}
|
|
189
223
|
else if (Object.keys(basemaps).length === 0 ) {
|
|
190
|
-
map.addLayer(overlays["countries"]);
|
|
224
|
+
if (overlays["countries"]) { map.addLayer(overlays["countries"]); }
|
|
191
225
|
}
|
|
192
226
|
}
|
|
193
227
|
|
|
@@ -316,7 +350,7 @@ var readFile = function(file) {
|
|
|
316
350
|
ws.send(JSON.stringify({action:"file", name:file.name, type:file.type, content:content, lat:droplatlng.lat, lon:droplatlng.lng}));
|
|
317
351
|
}
|
|
318
352
|
else {
|
|
319
|
-
console.log("NOT SURE WHAT THIS IS?",content)
|
|
353
|
+
console.log("NOT SURE WHAT TYPE OF FILE THIS IS ?",content)
|
|
320
354
|
}
|
|
321
355
|
});
|
|
322
356
|
reader.readAsDataURL(file);
|
|
@@ -331,6 +365,7 @@ var followMode = { accuracy:true };
|
|
|
331
365
|
var followState = false;
|
|
332
366
|
var trackMeButton;
|
|
333
367
|
var errRing;
|
|
368
|
+
var clrHeat;
|
|
334
369
|
|
|
335
370
|
function onLocationFound(e) {
|
|
336
371
|
if (followState === true) { map.panTo(e.latlng); }
|
|
@@ -409,9 +444,9 @@ else {
|
|
|
409
444
|
// }, "Locate me").addTo(map);
|
|
410
445
|
|
|
411
446
|
// Create the clear heatmap button
|
|
412
|
-
|
|
447
|
+
clrHeat = L.easyButton( 'fa-eraser', function() {
|
|
413
448
|
console.log("Reset heatmap");
|
|
414
|
-
heat.setLatLngs([]);
|
|
449
|
+
if (heat) { heat.setLatLngs([]); }
|
|
415
450
|
}, "Clears the current heatmap", {position:"bottomright"});
|
|
416
451
|
}
|
|
417
452
|
|
|
@@ -451,9 +486,9 @@ var edgeAware = function () {
|
|
|
451
486
|
var mapBounds = map.getBounds();
|
|
452
487
|
var mapBoundsCenter = mapBounds.getCenter();
|
|
453
488
|
|
|
454
|
-
pSW = map.options.crs.latLngToPoint(mapBounds.getSouthWest(), map.getZoom());
|
|
455
|
-
pNE = map.options.crs.latLngToPoint(mapBounds.getNorthEast(), map.getZoom());
|
|
456
|
-
pCenter = map.options.crs.latLngToPoint(mapBoundsCenter, map.getZoom());
|
|
489
|
+
var pSW = map.options.crs.latLngToPoint(mapBounds.getSouthWest(), map.getZoom());
|
|
490
|
+
var pNE = map.options.crs.latLngToPoint(mapBounds.getNorthEast(), map.getZoom());
|
|
491
|
+
var pCenter = map.options.crs.latLngToPoint(mapBoundsCenter, map.getZoom());
|
|
457
492
|
|
|
458
493
|
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()) );
|
|
459
494
|
for (var id in markers) {
|
|
@@ -513,7 +548,7 @@ function doPanit(v) {
|
|
|
513
548
|
|
|
514
549
|
var heatAll = false;
|
|
515
550
|
function doHeatAll(v) {
|
|
516
|
-
if (v !== undefined) {
|
|
551
|
+
if (v !== undefined) { heatAll = v; }
|
|
517
552
|
console.log("Heatall set :",heatAll);
|
|
518
553
|
}
|
|
519
554
|
|
|
@@ -538,10 +573,22 @@ function doLock(v) {
|
|
|
538
573
|
//console.log("Map bounds lock :",lockit);
|
|
539
574
|
}
|
|
540
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Removes stale markers and optionally clears a named layer from the map.
|
|
578
|
+
* When called without an argument, sweeps all markers whose timestamp has
|
|
579
|
+
* expired based on the current maxage setting (marker sweep only — layer
|
|
580
|
+
* cleanup requires an explicit layer name argument).
|
|
581
|
+
* When called with a layer name, removes all markers on that layer and
|
|
582
|
+
* removes the layer itself from the map and layer control.
|
|
583
|
+
* Special case: passing "heatmap" clears all heatmap points.
|
|
584
|
+
* @param {string} [l] - Name of a specific layer to remove entirely.
|
|
585
|
+
* If omitted, only expired markers are swept.
|
|
586
|
+
* @returns {void}
|
|
587
|
+
*/
|
|
541
588
|
// Remove old markers
|
|
542
589
|
function doTidyUp(l) {
|
|
543
590
|
if (l === "heatmap") {
|
|
544
|
-
|
|
591
|
+
if (heat) { heat.setLatLngs([]); }
|
|
545
592
|
}
|
|
546
593
|
else {
|
|
547
594
|
var d = parseInt(Date.now()/1000);
|
|
@@ -573,16 +620,28 @@ function doTidyUp(l) {
|
|
|
573
620
|
}
|
|
574
621
|
}
|
|
575
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Reads the maxage value from the UI input and (re)starts the periodic
|
|
625
|
+
* marker sweep interval, which calls doTidyUp() every 20 seconds.
|
|
626
|
+
* Note: the interval invokes doTidyUp() without a layer argument, so
|
|
627
|
+
* only stale markers are swept — layer cleanup must be triggered explicitly.
|
|
628
|
+
* @returns {void}
|
|
629
|
+
*/
|
|
576
630
|
// Call tidyup every {maxage} seconds - default 10 mins
|
|
577
631
|
var stale = null;
|
|
578
632
|
function setMaxAge() {
|
|
579
633
|
maxage = document.getElementById('maxage').value;
|
|
580
634
|
if (stale) { clearInterval(stale); }
|
|
581
635
|
//if (maxage > 0) {
|
|
582
|
-
stale = setInterval( function() { doTidyUp() }, 20000); //
|
|
636
|
+
stale = setInterval( function() { doTidyUp() }, 20000); // clear markers from all layers every 20 secs
|
|
583
637
|
}
|
|
584
638
|
setMaxAge();
|
|
585
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Updates the day/night terminator line position on the map if the
|
|
642
|
+
* day/night overlay is currently active. Called on a 60-second interval.
|
|
643
|
+
* @returns {void}
|
|
644
|
+
*/
|
|
586
645
|
// move the daylight / nighttime boundary (if enabled) every minute
|
|
587
646
|
function moveTerminator() { // if terminator line plotted move it every minute
|
|
588
647
|
if (layers["_daynight"] && layers["_daynight"].getLayers().length > 0) {
|
|
@@ -592,6 +651,12 @@ function moveTerminator() { // if terminator line plotted move it every minute
|
|
|
592
651
|
}
|
|
593
652
|
setInterval( function() { moveTerminator() }, 60000 );
|
|
594
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Refreshes the rainfall radar tile layer URL to the latest 10-minute
|
|
656
|
+
* radar snapshot from RainViewer, if the layer is active and the browser
|
|
657
|
+
* is online. Called on a 600-second (10-minute) interval.
|
|
658
|
+
* @returns {void}
|
|
659
|
+
*/
|
|
595
660
|
// move the rainfall overlay (if enabled) every 10 minutes
|
|
596
661
|
function moveRainfall() {
|
|
597
662
|
if (navigator.onLine && overlays.hasOwnProperty("rainfall") && map.hasLayer(overlays["rainfall"])) {
|
|
@@ -601,6 +666,12 @@ function moveRainfall() {
|
|
|
601
666
|
}
|
|
602
667
|
setInterval( function() { moveRainfall() }, 600000 );
|
|
603
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Sets the zoom level at which markers are clustered and refreshes
|
|
671
|
+
* the current map zoom display.
|
|
672
|
+
* @param {number} [v=0] - Zoom level threshold for clustering. 0 disables clustering.
|
|
673
|
+
* @returns {void}
|
|
674
|
+
*/
|
|
604
675
|
function setCluster(v) {
|
|
605
676
|
clusterAt = v || 0;
|
|
606
677
|
console.log("clusterAt set:",clusterAt);
|
|
@@ -631,6 +702,12 @@ async function readPhoton(url) {
|
|
|
631
702
|
}
|
|
632
703
|
}
|
|
633
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Searches for map markers matching the given search input by name or icon,
|
|
707
|
+
* constrained to the current map bounds. If no markers are found, falls back
|
|
708
|
+
* to a Nominatim geocoder lookup and pans the map to the result.
|
|
709
|
+
* @returns {void}
|
|
710
|
+
*/
|
|
634
711
|
// Search for markers with names of ... or icons of ...
|
|
635
712
|
function doSearch() {
|
|
636
713
|
var value = document.getElementById('search').value;
|
|
@@ -758,7 +835,7 @@ function toggleMenu() {
|
|
|
758
835
|
}
|
|
759
836
|
else {
|
|
760
837
|
document.getElementById("menu").style.display = 'none';
|
|
761
|
-
|
|
838
|
+
if (dialog) { dialog.close(); }
|
|
762
839
|
}
|
|
763
840
|
}
|
|
764
841
|
|
|
@@ -774,7 +851,7 @@ function closeMenu() {
|
|
|
774
851
|
menuOpen = false;
|
|
775
852
|
document.getElementById("menu").style.display = 'none';
|
|
776
853
|
}
|
|
777
|
-
|
|
854
|
+
if (dialog) { dialog.close(); }
|
|
778
855
|
}
|
|
779
856
|
|
|
780
857
|
document.getElementById("menu").style.display = 'none';
|
|
@@ -943,7 +1020,20 @@ var addmenu = "<b>Add marker</b><br><input type='text' id='rinput' autofocus onk
|
|
|
943
1020
|
addmenu += '<br/><a href="unitgenerator.html" target="_new">MilSymbol SIDC generator</a>';
|
|
944
1021
|
var rightmenuMap = L.popup({keepInView:true, minWidth:260}).setContent(addmenu);
|
|
945
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Converts an RGBA CSS colour string to a hex colour string.
|
|
1025
|
+
* @param {string} rgba - CSS rgba() or rgb() colour string.
|
|
1026
|
+
* @returns {string} Hex colour string (e.g. "#ff0000").
|
|
1027
|
+
*/
|
|
946
1028
|
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('')}`;
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Resolves a CSS colour keyword (e.g. "red", "cornflowerblue") to a hex
|
|
1032
|
+
* colour string by temporarily creating a DOM element and reading its
|
|
1033
|
+
* computed colour value.
|
|
1034
|
+
* @param {string} colorKeyword - Any valid CSS colour keyword or value.
|
|
1035
|
+
* @returns {string} Hex colour string.
|
|
1036
|
+
*/
|
|
947
1037
|
const colorKeywordToRGB = (colorKeyword) => {
|
|
948
1038
|
let el = document.createElement('div');
|
|
949
1039
|
el.style.color = colorKeyword;
|
|
@@ -955,6 +1045,13 @@ const colorKeywordToRGB = (colorKeyword) => {
|
|
|
955
1045
|
|
|
956
1046
|
var rclk = {};
|
|
957
1047
|
var hiderightclick = false;
|
|
1048
|
+
/**
|
|
1049
|
+
* Reads the right-click popup input field and creates a new marker at
|
|
1050
|
+
* the last right-clicked map position. Parses a comma-separated string
|
|
1051
|
+
* of name, icon/SIDC, layer, colour, and heading. Sends the new point
|
|
1052
|
+
* to the server via WebSocket.
|
|
1053
|
+
* @returns {void}
|
|
1054
|
+
*/
|
|
958
1055
|
var addThing = function() {
|
|
959
1056
|
var thing = document.getElementById('rinput').value;
|
|
960
1057
|
map.closePopup();
|
|
@@ -980,7 +1077,7 @@ var addThing = function() {
|
|
|
980
1077
|
ws.send(JSON.stringify(d));
|
|
981
1078
|
delete d.action;
|
|
982
1079
|
setMarker(d);
|
|
983
|
-
map.addLayer(layers[lay]);
|
|
1080
|
+
if (layers[lay]) { map.addLayer(layers[lay]); }
|
|
984
1081
|
}
|
|
985
1082
|
|
|
986
1083
|
var form = {};
|
|
@@ -996,7 +1093,6 @@ var feedback = function(n = "map",v,a = "feedback",c) {
|
|
|
996
1093
|
dataToSend.lon = rclk.lng;
|
|
997
1094
|
}
|
|
998
1095
|
ws.send(JSON.stringify(dataToSend));
|
|
999
|
-
|
|
1000
1096
|
if (c === true) { map.closePopup(); }
|
|
1001
1097
|
}
|
|
1002
1098
|
|
|
@@ -1004,11 +1100,22 @@ map.on('click', function(e) {
|
|
|
1004
1100
|
ws.send(JSON.stringify({action:"click", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5)}));
|
|
1005
1101
|
});
|
|
1006
1102
|
|
|
1103
|
+
map.on('popupopen', function(e) {
|
|
1104
|
+
const mp = e.popup._source;
|
|
1105
|
+
const m = allData[mp["name"]];
|
|
1106
|
+
m.popped = true;
|
|
1107
|
+
m.action = "openPopup";
|
|
1108
|
+
ws.send(JSON.stringify(m));
|
|
1109
|
+
delete m.action;
|
|
1110
|
+
//ws.send(JSON.stringify({action:"openPopup",name:mp.name,layer:mp.lay,icon:mp.icon,iconColor:mp.iconColor,SIDC:mp.SIDC,draggable:true,lat:parseFloat(mp.getLatLng().lat.toFixed(6)),lon:parseFloat(mp.getLatLng().lng.toFixed(6))}));
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1007
1113
|
// allow double right click to zoom out (if enabled)
|
|
1008
1114
|
// single right click opens a message window that adds a marker
|
|
1009
1115
|
var rclicked = false;
|
|
1010
1116
|
var rtout = null;
|
|
1011
1117
|
|
|
1118
|
+
|
|
1012
1119
|
map.on('contextmenu', function(e) {
|
|
1013
1120
|
if (rclicked) {
|
|
1014
1121
|
rclicked = false;
|
|
@@ -1047,6 +1154,16 @@ map.on('contextmenu', function(e) {
|
|
|
1047
1154
|
//var layercontrol = L.control.selectLayers(basemaps, overlays).addTo(map);
|
|
1048
1155
|
layercontrol = L.control.layers(basemaps, overlays);
|
|
1049
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* Adds the base tile map layers to the map based on a list of layer codes.
|
|
1159
|
+
* Handles both online tile layers (OSM variants, Esri, etc.) and offline
|
|
1160
|
+
* PMTiles layers. Initialises the layer control and selects the first
|
|
1161
|
+
* available layer.
|
|
1162
|
+
* @param {string} maplist - Space- or comma-separated string of layer codes
|
|
1163
|
+
* (e.g. "OSMG OSMC EsriS").
|
|
1164
|
+
* @param {string} [first] - Name of the layer to activate first.
|
|
1165
|
+
* @returns {void}
|
|
1166
|
+
*/
|
|
1050
1167
|
// Add all the base layer maps if we are online.
|
|
1051
1168
|
var addBaseMaps = function(maplist,first) {
|
|
1052
1169
|
// console.log("MAPS",first,maplist)
|
|
@@ -1062,7 +1179,7 @@ var addBaseMaps = function(maplist,first) {
|
|
|
1062
1179
|
var osmAttrib='Map data © OpenStreetMap contributors';
|
|
1063
1180
|
|
|
1064
1181
|
if (maplist.indexOf("MB3d")!==-1) { // handle the case of 3d by redirecting to that page instead.
|
|
1065
|
-
window.location.href
|
|
1182
|
+
window.location.href = "index3d.html";
|
|
1066
1183
|
}
|
|
1067
1184
|
if (maplist.indexOf("OSMG")!==-1) {
|
|
1068
1185
|
basemaps[layerlookup["OSMG"]] = new L.TileLayer.Grayscale(osmUrl, {
|
|
@@ -1215,6 +1332,14 @@ var addBaseMaps = function(maplist,first) {
|
|
|
1215
1332
|
}
|
|
1216
1333
|
}
|
|
1217
1334
|
|
|
1335
|
+
/**
|
|
1336
|
+
* Adds optional overlay layers to the map based on a list of overlay codes.
|
|
1337
|
+
* Handles countries outline (CO), day/night terminator (DN), rainfall radar (RA),
|
|
1338
|
+
* OSM Buildings (BU), railways, heatmap, drawing tools, minimap, side-by-side
|
|
1339
|
+
* comparison, and more.
|
|
1340
|
+
* @param {string} overlist - Space- or comma-separated string of overlay codes.
|
|
1341
|
+
* @returns {void}
|
|
1342
|
+
*/
|
|
1218
1343
|
// Now add the overlays
|
|
1219
1344
|
var addOverlays = function(overlist) {
|
|
1220
1345
|
//console.log("OVERLAYS",overlist)
|
|
@@ -1255,7 +1380,7 @@ var addOverlays = function(overlist) {
|
|
|
1255
1380
|
}
|
|
1256
1381
|
|
|
1257
1382
|
var shape;
|
|
1258
|
-
map.on(
|
|
1383
|
+
map.on('pm:create', (e) => {
|
|
1259
1384
|
drawCount = drawCount + 1;
|
|
1260
1385
|
var name = e.shape + drawCount;
|
|
1261
1386
|
|
|
@@ -1301,7 +1426,7 @@ var addOverlays = function(overlist) {
|
|
|
1301
1426
|
rightmenuMarker = L.popup({offset:[0,-12]}).setContent(drawcontextmenu.replace(/\${name}/g,name).replace(/\${.*?}/g,'') || "<input type='text' autofocus 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='sendRoute(\""+name+"\");'>Route</button><button onclick='sendDrawing(\""+name+"\");'>OK</button>");
|
|
1302
1427
|
}
|
|
1303
1428
|
rightmenuMarker.setLatLng(cent);
|
|
1304
|
-
setTimeout(function() {map.openPopup(rightmenuMarker)
|
|
1429
|
+
setTimeout(function() {map.openPopup(rightmenuMarker)},25);
|
|
1305
1430
|
});
|
|
1306
1431
|
|
|
1307
1432
|
sendDrawing = function(n,v,a) {
|
|
@@ -1515,16 +1640,6 @@ var coords = L.control.mouseCoordinate({position:"bottomleft"});
|
|
|
1515
1640
|
// Add an optional legend
|
|
1516
1641
|
var legend = L.control({position:"bottomleft"});
|
|
1517
1642
|
|
|
1518
|
-
// Add the dialog box for messages
|
|
1519
|
-
// var dialogue = L.control.dialog({initOpen:false, size:[600,400], anchor:[50,150]}).addTo(map);
|
|
1520
|
-
// dialogue.freeze();
|
|
1521
|
-
|
|
1522
|
-
var doDialog = function(d) {
|
|
1523
|
-
//console.log("DIALOGUE",d);
|
|
1524
|
-
dialogue.setContent(d);
|
|
1525
|
-
dialogue.open();
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
1643
|
var helpText = '<h3>Node-RED - Map all the things</h3><br/>';
|
|
1529
1644
|
helpText += '<p><i class="fa fa-search fa-lg fa-fw"></i> <b>Search</b> - You may enter a name, or partial name, or icon name of an object to search for.';
|
|
1530
1645
|
helpText += 'The map will then jump to centre on each of the results in turn. If nothing is found locally it will try to';
|
|
@@ -1542,7 +1657,31 @@ helpText += 'While active it also restricts the "auto pan" and "search" to withi
|
|
|
1542
1657
|
helpText += '<p><i class="fa fa-globe fa-lg fa-fw"></i> <b>Heatmap all layers</b> - When selected';
|
|
1543
1658
|
helpText += 'all layers whether hidden or not will contribute to the heatmap.';
|
|
1544
1659
|
helpText += 'The default is that only visible layers add to the heatmap.</p>';
|
|
1660
|
+
helpText += '<button type="button" id="closeDialog">Close help</button>';
|
|
1661
|
+
|
|
1662
|
+
var dialog;
|
|
1663
|
+
/**
|
|
1664
|
+
* Displays the help dialog modal. Populates the dialog element with the
|
|
1665
|
+
* provided dialog content HTML string and wires up the close button.
|
|
1666
|
+
* @param {string} d - dialog content.
|
|
1667
|
+
* @returns {void}
|
|
1668
|
+
*/
|
|
1669
|
+
var doDialog = function(d) {
|
|
1670
|
+
//console.log("DIALOGUE",d);
|
|
1671
|
+
dialog = document.getElementById("helpDialog");
|
|
1672
|
+
dialog.innerHTML = d;
|
|
1673
|
+
const closeButton = document.getElementById("closeDialog");
|
|
1674
|
+
closeButton.addEventListener("click", () => { dialog.close(); });
|
|
1675
|
+
dialog.showModal();
|
|
1676
|
+
}
|
|
1545
1677
|
|
|
1678
|
+
/**
|
|
1679
|
+
* Deletes a named marker or shape from the map and all internal data stores.
|
|
1680
|
+
* Optionally notifies the server via WebSocket that the item was deleted.
|
|
1681
|
+
* @param {string} dname - Name of the marker or shape to delete.
|
|
1682
|
+
* @param {boolean} [note=false] - If true, sends a delete action to the server.
|
|
1683
|
+
* @returns {void}
|
|
1684
|
+
*/
|
|
1546
1685
|
// Delete a marker or shape (and notify websocket)
|
|
1547
1686
|
var delMarker = function(dname,note) {
|
|
1548
1687
|
if (note) { map.closePopup(); }
|
|
@@ -1572,6 +1711,15 @@ var delMarker = function(dname,note) {
|
|
|
1572
1711
|
}
|
|
1573
1712
|
}
|
|
1574
1713
|
|
|
1714
|
+
/**
|
|
1715
|
+
* Enables editing (move, rotate, or drag) of a named polygon or shape
|
|
1716
|
+
* via Leaflet-Geoman. On double-click, disables editing and sends the
|
|
1717
|
+
* updated shape geometry to the server.
|
|
1718
|
+
* @param {string} pname - Name of the polygon/shape to edit.
|
|
1719
|
+
* @param {string} [fun] - Edit mode: "rot" for rotate, "drag" for drag,
|
|
1720
|
+
* or omitted for vertex editing.
|
|
1721
|
+
* @returns {void}
|
|
1722
|
+
*/
|
|
1575
1723
|
var editPoly = function(pname,fun) {
|
|
1576
1724
|
map.closePopup();
|
|
1577
1725
|
if (fun === "rot") { polygons[pname].pm.enableRotate(); }
|
|
@@ -1597,6 +1745,17 @@ var editPoly = function(pname,fun) {
|
|
|
1597
1745
|
})
|
|
1598
1746
|
}
|
|
1599
1747
|
|
|
1748
|
+
/**
|
|
1749
|
+
* Creates a Leaflet FeatureGroup of semi-circular range rings centred on
|
|
1750
|
+
* a given latlng, used to indicate sensor field-of-view or range arcs.
|
|
1751
|
+
* @param {L.LatLng} latlng - Centre point for the rings.
|
|
1752
|
+
* @param {Object} [options] - Options object.
|
|
1753
|
+
* @param {number|number[]} [options.ranges=[250,500,750,1000]] - Ring radius/radii in metres.
|
|
1754
|
+
* @param {number} [options.pan=0] - Bearing direction in degrees.
|
|
1755
|
+
* @param {number} [options.fov=60] - Field of view angle in degrees.
|
|
1756
|
+
* @param {string} [options.color='#aaaa00'] - Ring colour.
|
|
1757
|
+
* @returns {L.FeatureGroup} Feature group containing the range ring semi-circles.
|
|
1758
|
+
*/
|
|
1600
1759
|
var rangerings = function(latlng, options) {
|
|
1601
1760
|
options = L.extend({
|
|
1602
1761
|
ranges: [250,500,750,1000],
|
|
@@ -1626,6 +1785,28 @@ var rangerings = function(latlng, options) {
|
|
|
1626
1785
|
// else { return str; }
|
|
1627
1786
|
// };
|
|
1628
1787
|
|
|
1788
|
+
/**
|
|
1789
|
+
* The primary function for adding or updating a marker, shape, or overlay
|
|
1790
|
+
* on the map. Handles points, polylines, polygons, circles, images, arcs,
|
|
1791
|
+
* NATO MilSymbol (SIDC) icons, FontAwesome icons, and custom icon URLs.
|
|
1792
|
+
* Also manages TTL-based auto-deletion, heatmap contribution, popup binding,
|
|
1793
|
+
* leader lines, and layer assignment.
|
|
1794
|
+
* @param {Object} data - Marker/shape descriptor object. Key properties:
|
|
1795
|
+
* @param {string} data.name - Unique identifier for the marker.
|
|
1796
|
+
* @param {number} [data.lat] - Latitude.
|
|
1797
|
+
* @param {number} [data.lon] - Longitude.
|
|
1798
|
+
* @param {string} [data.layer] - Layer name to add the marker to.
|
|
1799
|
+
* @param {string} [data.icon] - FontAwesome icon name or special value.
|
|
1800
|
+
* @param {string} [data.SIDC] - NATO MilSymbol SIDC code.
|
|
1801
|
+
* @param {string} [data.iconColor] - Colour for the icon.
|
|
1802
|
+
* @param {boolean} [data.deleted] - If true, removes the marker from the map.
|
|
1803
|
+
* @param {number} [data.ttl] - Time-to-live in seconds (0 = permanent).
|
|
1804
|
+
* @param {Array} [data.area] - Array of latlng objects for a polygon.
|
|
1805
|
+
* @param {Array} [data.line] - Array of latlng objects for a polyline.
|
|
1806
|
+
* @param {number} [data.radius] - Radius in metres for a circle.
|
|
1807
|
+
* @param {Object} [data.options] - Leaflet path options override.
|
|
1808
|
+
* @returns {void}
|
|
1809
|
+
*/
|
|
1629
1810
|
// the MAIN add marker or shape to map function
|
|
1630
1811
|
function setMarker(data) {
|
|
1631
1812
|
if (!data) { return; }
|
|
@@ -1633,8 +1814,8 @@ function setMarker(data) {
|
|
|
1633
1814
|
m.on('click', function(e) {
|
|
1634
1815
|
var fb = allData[data["name"]];
|
|
1635
1816
|
fb.action = "click";
|
|
1636
|
-
if (fb.sendOnClick ?? true)
|
|
1637
|
-
|
|
1817
|
+
if (fb.sendOnClick ?? true) { ws.send(JSON.stringify(fb)); }
|
|
1818
|
+
delete fb.action;
|
|
1638
1819
|
});
|
|
1639
1820
|
// customise right click context menu
|
|
1640
1821
|
var rightcontext = "";
|
|
@@ -1685,12 +1866,12 @@ function setMarker(data) {
|
|
|
1685
1866
|
var opt = data.options || {};
|
|
1686
1867
|
opt.color = opt.color ?? data.color ?? data.lineColor ?? "#910000";
|
|
1687
1868
|
opt.fillColor = opt.fillColor ?? data.fillColor ?? "#910000";
|
|
1688
|
-
opt.stroke = opt.stroke ?? (data.hasOwnProperty("stroke")
|
|
1869
|
+
opt.stroke = opt.stroke ?? (data.hasOwnProperty("stroke") ? data.stroke : true);
|
|
1689
1870
|
opt.weight = opt.weight ?? data.weight ?? 2;
|
|
1690
1871
|
opt.opacity = opt.opacity ?? data.opacity ?? 1;
|
|
1691
1872
|
if (!data.SIDC) { opt.fillOpacity = opt.fillOpacity ?? data.fillOpacity ?? 0.2; }
|
|
1692
1873
|
opt.clickable = (data.hasOwnProperty("clickable")) ? data.clickable : false;
|
|
1693
|
-
opt.fill = opt.fill ?? (data.hasOwnProperty("fill")
|
|
1874
|
+
opt.fill = opt.fill ?? (data.hasOwnProperty("fill") ? data.fill : true);
|
|
1694
1875
|
if (data.hasOwnProperty("dashArray")) { opt.dashArray = data.dashArray; }
|
|
1695
1876
|
|
|
1696
1877
|
// Replace building
|
|
@@ -2285,6 +2466,7 @@ function setMarker(data) {
|
|
|
2285
2466
|
fb.lon = parseFloat(marker.getLatLng().lng.toFixed(6));
|
|
2286
2467
|
fb.from = oldll;
|
|
2287
2468
|
ws.send(JSON.stringify(fb));
|
|
2469
|
+
delete fb.action;
|
|
2288
2470
|
});
|
|
2289
2471
|
}
|
|
2290
2472
|
|
|
@@ -2371,15 +2553,8 @@ function setMarker(data) {
|
|
|
2371
2553
|
}
|
|
2372
2554
|
delete data.weblink;
|
|
2373
2555
|
}
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
p = true;
|
|
2377
|
-
delete data.popped;
|
|
2378
|
-
}
|
|
2379
|
-
if (data.hasOwnProperty("popped") && (data.popped === false)) {
|
|
2380
|
-
marker.closePopup();
|
|
2381
|
-
p = false;
|
|
2382
|
-
delete data.popped;
|
|
2556
|
+
if (data.hasOwnProperty("popped")) {
|
|
2557
|
+
marker.popped = data.popped;
|
|
2383
2558
|
}
|
|
2384
2559
|
// If .label then use that rather than name tooltip
|
|
2385
2560
|
if (data.hasOwnProperty("label")) {
|
|
@@ -2423,7 +2598,7 @@ function setMarker(data) {
|
|
|
2423
2598
|
if (data.hasOwnProperty("radius")) { delete data.radius; }
|
|
2424
2599
|
if (data.hasOwnProperty("greatcircle")) { delete data.greatcircle; }
|
|
2425
2600
|
|
|
2426
|
-
if (!data.hasOwnProperty("clickable")
|
|
2601
|
+
if (!data.hasOwnProperty("clickable") || data.clickable == true) {
|
|
2427
2602
|
var wopt = { autoClose:false, closeButton:true, closeOnClick:false, minWidth:200 };
|
|
2428
2603
|
if (words.indexOf('<video ') >=0 || words.indexOf('<img ') >=0 ) { wopt.maxWidth="640"; } // make popup wider if it has an image or video
|
|
2429
2604
|
if (data?.popupOptions) { // allow user to override popup options eg to add className
|
|
@@ -2437,7 +2612,7 @@ function setMarker(data) {
|
|
|
2437
2612
|
else {
|
|
2438
2613
|
words += '<table>';
|
|
2439
2614
|
for (var i in data) {
|
|
2440
|
-
if ((i != "name") && (i != "length") && (i != "clickable")) {
|
|
2615
|
+
if ((i != "name") && (i != "length") && (i != "clickable") && (i != "popped")) {
|
|
2441
2616
|
if (typeof data[i] === "object") {
|
|
2442
2617
|
words += '<tr><td valign="top">'+ i +'</td><td>' + JSON.stringify(data[i]) + '</td></tr>';
|
|
2443
2618
|
}
|
|
@@ -2456,6 +2631,13 @@ function setMarker(data) {
|
|
|
2456
2631
|
if (longline > 100) { wopt.minWidth="640"; } // make popup wider if it has a long line
|
|
2457
2632
|
marker.bindPopup(words, wopt);
|
|
2458
2633
|
marker._popup.dname = data["name"];
|
|
2634
|
+
marker.getPopup().on('remove', function() {
|
|
2635
|
+
const m = allData[marker["name"]];
|
|
2636
|
+
m.popped = false;
|
|
2637
|
+
m.action = "closePopup";
|
|
2638
|
+
ws.send(JSON.stringify(m));
|
|
2639
|
+
delete m.action;
|
|
2640
|
+
});
|
|
2459
2641
|
}
|
|
2460
2642
|
|
|
2461
2643
|
if (data.hasOwnProperty("clickURL")) {
|
|
@@ -2473,6 +2655,7 @@ function setMarker(data) {
|
|
|
2473
2655
|
// var fb = allData[marker.name];
|
|
2474
2656
|
// fb.action = "click";
|
|
2475
2657
|
// ws.send(JSON.stringify(fb));
|
|
2658
|
+
// delete fb.action;
|
|
2476
2659
|
// });
|
|
2477
2660
|
if (heat && ((data.addtoheatmap != false) || (!data.hasOwnProperty("addtoheatmap")))) { // Added to give ability to control if points from active layer contribute to heatmap
|
|
2478
2661
|
if (heatAll || map.hasLayer(layers[lay])) { heat.addLatLng(lli); }
|
|
@@ -2546,10 +2729,10 @@ function setMarker(data) {
|
|
|
2546
2729
|
}
|
|
2547
2730
|
}
|
|
2548
2731
|
if (panit === true) { map.setView(ll,map.getZoom()); }
|
|
2549
|
-
if (
|
|
2732
|
+
if (marker.popped === true) { marker.openPopup(); }
|
|
2550
2733
|
}
|
|
2551
2734
|
|
|
2552
|
-
var custIco = function() {
|
|
2735
|
+
var custIco = function(cmd) {
|
|
2553
2736
|
var col = cmd.map.iconColor ?? "#910000";
|
|
2554
2737
|
var myMarker = L.VectorMarkers.icon({
|
|
2555
2738
|
icon: "circle",
|
|
@@ -2578,6 +2761,22 @@ var custIco = function() {
|
|
|
2578
2761
|
return customLayer;
|
|
2579
2762
|
}
|
|
2580
2763
|
|
|
2764
|
+
/**
|
|
2765
|
+
* Processes incoming command objects from the server to control the map
|
|
2766
|
+
* remotely. Handles a wide range of commands including: map/overlay
|
|
2767
|
+
* initialisation, pan/zoom/rotation, layer visibility, marker age limits,
|
|
2768
|
+
* clustering, UI button injection, heatmap updates, user location tracking,
|
|
2769
|
+
* legend display, custom CSS/JS loading, and arbitrary eval of custom commands.
|
|
2770
|
+
* @param {Object} cmd - Command object received from the server.
|
|
2771
|
+
* @param {boolean} [cmd.init] - If true, triggers map/overlay initialisation.
|
|
2772
|
+
* @param {string} [cmd.layer] - Base layer to activate.
|
|
2773
|
+
* @param {number} [cmd.lat] - Latitude to pan to.
|
|
2774
|
+
* @param {number} [cmd.lon] - Longitude to pan to.
|
|
2775
|
+
* @param {number} [cmd.zoom] - Zoom level to set.
|
|
2776
|
+
* @param {string|string[]} [cmd.clearlayer] - Layer name(s) to clear.
|
|
2777
|
+
* @param {Object} [cmd.map] - Overlay/basemap descriptor for adding layers.
|
|
2778
|
+
* @returns {void}
|
|
2779
|
+
*/
|
|
2581
2780
|
// handle any incoming COMMANDS to control the map remotely
|
|
2582
2781
|
function doCommand(cmd) {
|
|
2583
2782
|
// console.log("COMMAND",cmd);
|
|
@@ -2586,7 +2785,7 @@ function doCommand(cmd) {
|
|
|
2586
2785
|
addBaseMaps(cmd.maplist,cmd.layer);
|
|
2587
2786
|
}
|
|
2588
2787
|
if (cmd.init && cmd.hasOwnProperty("overlist")) {
|
|
2589
|
-
overlays =
|
|
2788
|
+
overlays = {};
|
|
2590
2789
|
addOverlays(cmd.overlist);
|
|
2591
2790
|
}
|
|
2592
2791
|
if (cmd.hasOwnProperty("toptitle")) {
|
|
@@ -2606,6 +2805,20 @@ function doCommand(cmd) {
|
|
|
2606
2805
|
if (!Array.isArray(clr)) { clr = [ clr ]; }
|
|
2607
2806
|
clr.forEach((el) => doTidyUp(el));
|
|
2608
2807
|
}
|
|
2808
|
+
if (cmd.hasOwnProperty("showdialog")) {
|
|
2809
|
+
if (typeof cmd.showdialog === "string") {
|
|
2810
|
+
if (cmd.showdialog === "") {
|
|
2811
|
+
if (dialog) { dialog.close(); }
|
|
2812
|
+
}
|
|
2813
|
+
else if (!/script/i.test(cmd.showdialog)) {
|
|
2814
|
+
let dia = cmd.showdialog;
|
|
2815
|
+
if (!/id=\"closeDialog\"/.test(cmd.showdialog)) {
|
|
2816
|
+
dia += '<p><button type="button" id="closeDialog">Close</button></p>';
|
|
2817
|
+
}
|
|
2818
|
+
doDialog(dia);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2609
2822
|
if (cmd.hasOwnProperty("panit")) {
|
|
2610
2823
|
if (cmd.panit == true || cmd.panit === "true") { panit = true; }
|
|
2611
2824
|
else { panit = false; }
|
|
@@ -2949,7 +3162,7 @@ function doCommand(cmd) {
|
|
|
2949
3162
|
var sidc = feature.properties.symbol.toUpperCase().replace("APP6A:",'')//.substr(0,13);
|
|
2950
3163
|
var country;
|
|
2951
3164
|
if (sidc.length > 12) { country = sidc.substr(12).replace(/-/g,''); sidc = sidc.substr(0,12); }
|
|
2952
|
-
myMarker = new ms.Symbol( sidc, {
|
|
3165
|
+
var myMarker = new ms.Symbol( sidc, {
|
|
2953
3166
|
uniqueDesignation:feature.properties.label,
|
|
2954
3167
|
country:country,
|
|
2955
3168
|
direction:feature.properties.course,
|
|
@@ -3073,7 +3286,7 @@ function doCommand(cmd) {
|
|
|
3073
3286
|
// var json = window.toGeoJSON.gpx(gp);
|
|
3074
3287
|
// console.log("j",json)
|
|
3075
3288
|
// doGeojson(json.features[0].properties.name,json,json.features[0].properties.type) // name,geojson,layer,options
|
|
3076
|
-
overlays[cmd.map.overlay] = omnivore.gpx.parse(cmd.map.gpx, null, custIco());
|
|
3289
|
+
overlays[cmd.map.overlay] = omnivore.gpx.parse(cmd.map.gpx, null, custIco(cmd));
|
|
3077
3290
|
if (!existsalready) {
|
|
3078
3291
|
layercontrol.addOverlay(overlays[cmd.map.overlay],cmd.map.overlay);
|
|
3079
3292
|
}
|
|
@@ -3291,6 +3504,18 @@ function doCommand(cmd) {
|
|
|
3291
3504
|
}
|
|
3292
3505
|
}
|
|
3293
3506
|
|
|
3507
|
+
/**
|
|
3508
|
+
* Renders a GeoJSON Feature or FeatureCollection on the map as a named
|
|
3509
|
+
* overlay layer. Supports SIDC symbols, FontAwesome icons, and vector
|
|
3510
|
+
* markers for point features, and applies style properties from feature
|
|
3511
|
+
* properties or the options argument for polygon/line features.
|
|
3512
|
+
* @param {string} n - Name for the overlay layer.
|
|
3513
|
+
* @param {Object} g - GeoJSON Feature or FeatureCollection object.
|
|
3514
|
+
* @param {string} [l] - Layer name override (defaults to g.name or "unknown").
|
|
3515
|
+
* @param {Object} [o] - Leaflet path style options to apply to features.
|
|
3516
|
+
* @param {string} [i] - Default icon name for point features without an icon property.
|
|
3517
|
+
* @returns {void}
|
|
3518
|
+
*/
|
|
3294
3519
|
// handle any incoming GEOJSON directly - may style badly
|
|
3295
3520
|
function doGeojson(n,g,l,o,i) { // name, geojson, layer, options, icon
|
|
3296
3521
|
var lay = l ?? g.name ?? "unknown";
|
|
@@ -3443,6 +3668,14 @@ function doGeojson(n,g,l,o,i) { // name, geojson, layer, options, icon
|
|
|
3443
3668
|
map.addLayer(layers[lay]);
|
|
3444
3669
|
}
|
|
3445
3670
|
|
|
3671
|
+
/**
|
|
3672
|
+
* Handles a TAK/CoT event object (from the tak-ingest or fastxml Node-RED nodes)
|
|
3673
|
+
* and converts it to a worldmap marker, then calls setMarker().
|
|
3674
|
+
* Processes atom types (a-) for personnel/vehicle tracks and b- types for
|
|
3675
|
+
* spot markers, position indicators, emergency alerts, and geofence events.
|
|
3676
|
+
* @param {Object} p - Parsed CoT event object with point, detail, type, uid fields.
|
|
3677
|
+
* @returns {void}
|
|
3678
|
+
*/
|
|
3446
3679
|
// handle TAK messages from TAK server tcp - XML->JSON
|
|
3447
3680
|
function doTAKjson(p) {
|
|
3448
3681
|
//console.log("TAK event",p);
|
|
@@ -3472,7 +3705,7 @@ function doTAKjson(p) {
|
|
|
3472
3705
|
}
|
|
3473
3706
|
d.type = p.type;
|
|
3474
3707
|
d.remarks = p.detail?.remarks
|
|
3475
|
-
if (p.detail?.remarks && p.detail.remarks.hasOwnProperty
|
|
3708
|
+
if (p.detail?.remarks && p.detail.remarks.hasOwnProperty("#text")) {
|
|
3476
3709
|
d.remarks = p.detail.remarks["#text"];
|
|
3477
3710
|
}
|
|
3478
3711
|
d.uid = p.uid;
|
|
@@ -3499,6 +3732,13 @@ function doTAKjson(p) {
|
|
|
3499
3732
|
}
|
|
3500
3733
|
}
|
|
3501
3734
|
|
|
3735
|
+
/**
|
|
3736
|
+
* Handles a TAK Multicast event object decoded from Protobuf via the
|
|
3737
|
+
* tak-ingest Node-RED node, converting it to a worldmap marker.
|
|
3738
|
+
* Only processes atom types (a-) for tracks.
|
|
3739
|
+
* @param {Object} p - Parsed TAK multicast CoT object with lat, lon, type, uid fields.
|
|
3740
|
+
* @returns {void}
|
|
3741
|
+
*/
|
|
3502
3742
|
// handle TAK messages from TAK Multicast - Protobuf->JSON
|
|
3503
3743
|
function doTAKMCjson(p) {
|
|
3504
3744
|
// console.log("TAK Multicast event",p);
|
|
@@ -3542,6 +3782,12 @@ function doTAKMCjson(p) {
|
|
|
3542
3782
|
}
|
|
3543
3783
|
}
|
|
3544
3784
|
|
|
3785
|
+
/**
|
|
3786
|
+
* Converts a 32-bit ARGB integer colour value (as used in CoT/TAK messages)
|
|
3787
|
+
* to a CSS hex colour string, discarding the alpha channel.
|
|
3788
|
+
* @param {number} color - 32-bit integer ARGB colour value.
|
|
3789
|
+
* @returns {string} CSS hex colour string (e.g. "#ff0000").
|
|
3790
|
+
*/
|
|
3545
3791
|
function convertCOTtoCIFColour(color) {
|
|
3546
3792
|
// const c = parseInt(color);
|
|
3547
3793
|
const arr = new ArrayBuffer(4);
|
|
@@ -3551,12 +3797,24 @@ function convertCOTtoCIFColour(color) {
|
|
|
3551
3797
|
return "#" + b2h.substr(2);
|
|
3552
3798
|
}
|
|
3553
3799
|
|
|
3800
|
+
/**
|
|
3801
|
+
* Converts an ArrayBuffer to a lowercase hex string.
|
|
3802
|
+
* @param {ArrayBuffer} buffer - The buffer to convert.
|
|
3803
|
+
* @returns {string} Hex-encoded string representation of the buffer bytes.
|
|
3804
|
+
*/
|
|
3554
3805
|
function buf2hex(buffer) { // buffer is an ArrayBuffer
|
|
3555
3806
|
return [...new Uint8Array(buffer)]
|
|
3556
3807
|
.map(x => x.toString(16).padStart(2, '0'))
|
|
3557
3808
|
.join('');
|
|
3558
3809
|
}
|
|
3559
3810
|
|
|
3811
|
+
/**
|
|
3812
|
+
* Generates an array of intermediate range ring values for a given maximum
|
|
3813
|
+
* range, using step sizes of 100, 1000, or 10000 depending on the magnitude.
|
|
3814
|
+
* Returns the value directly if it is 100 or less.
|
|
3815
|
+
* @param {number} r - Maximum range in metres.
|
|
3816
|
+
* @returns {number|number[]} Single value if r <= 100, otherwise an array of ring radii.
|
|
3817
|
+
*/
|
|
3560
3818
|
function createRings(r) {
|
|
3561
3819
|
if (r <= 100) { return r; }
|
|
3562
3820
|
var rings = [];
|
|
@@ -3570,6 +3828,15 @@ function createRings(r) {
|
|
|
3570
3828
|
return rings;
|
|
3571
3829
|
}
|
|
3572
3830
|
|
|
3831
|
+
/**
|
|
3832
|
+
* Maps a CoT type string to an appropriate SIDC code or icon on the marker
|
|
3833
|
+
* data object. Handles atom types (a-) via milsymbol conversion and a
|
|
3834
|
+
* selection of b- types (spot markers, position indicators, emergency alerts,
|
|
3835
|
+
* geofence events, waypoints). Modifies the data object in place.
|
|
3836
|
+
* @param {Object} d - Worldmap marker data object to be mutated.
|
|
3837
|
+
* @param {Object} p - Raw CoT/TAK event object containing type, detail, etc.
|
|
3838
|
+
* @returns {Object} The mutated data object.
|
|
3839
|
+
*/
|
|
3573
3840
|
function handleCoTtypes(d,p) {
|
|
3574
3841
|
if (d.type.indexOf('a-') === 0) { // handle a- types
|
|
3575
3842
|
if (p?.detail?.__milsym?.id) {
|