node-red-contrib-web-worldmap 2.40.1 → 2.42.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.
@@ -194,6 +194,7 @@ map.whenReady(function() {
194
194
  connect();
195
195
  });
196
196
 
197
+ // Drag Drop of files to target map
197
198
  var droplatlng;
198
199
  var target = document.getElementById("map")
199
200
  target.ondragover = function (ev) {
@@ -245,8 +246,16 @@ var readFile = function(file) {
245
246
  doCommand({map:{overlay:file.name, nvg:data}});
246
247
  }
247
248
  }
249
+ else if (data.indexOf("<kml") !== -1) {
250
+ doCommand({map:{overlay:file.name, kml:data}});
251
+ }
248
252
  else if (data.indexOf('PK') === 0) {
249
- console.log("ZIP FILE");
253
+ if (file.name.indexOf('.kmz') !== -1) {
254
+ doCommand({map:{overlay:file.name, kmz:data}});
255
+ }
256
+ else {
257
+ console.log("ZIP FILE",file);
258
+ }
250
259
  }
251
260
  else {
252
261
  try {
@@ -289,14 +298,14 @@ function onLocationFound(e) {
289
298
  setMarker(self);
290
299
  }
291
300
  if (followMode.accuracy) {
292
- errRing = L.circle(e.latlng, e.accuracy, {color:followMode.color ?? "cyan", weight:3, opacity:0.6, fill:false, clickable:false});
301
+ errRing = L.circle(e.latlng, e.accuracy, {color:followMode.color ?? "#00ffff", weight:3, opacity:0.6, fill:false, clickable:false});
293
302
  errRing.addTo(map);
294
303
  // if (e.hasOwnProperty("heading")) {
295
304
  // var lengthAsDegrees = e.speed * 60 / 110540;
296
305
  // var ya = e.latlng.lat + Math.sin((90-e.heading)/180*Math.PI)*lengthAsDegrees*Math.cos(e.latlng.lng/180*Math.PI);
297
306
  // var xa = e.latlng.lng + Math.cos((90-e.heading)/180*Math.PI)*lengthAsDegrees;
298
307
  // var lla = new L.LatLng(ya,xa);
299
- // L.polygon([ e.latlng, lla ], {color:"cyan", weight:3, opacity:0.5, clickable:false}).addTo(map);
308
+ // L.polygon([ e.latlng, lla ], {color:"00ffff", weight:3, opacity:0.5, clickable:false}).addTo(map);
300
309
  // }
301
310
  }
302
311
  ws.send(JSON.stringify({action:"point", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5), point:"self", hdg:e.heading, speed:(e.speed*3.6 ?? undefined)}));
@@ -770,9 +779,20 @@ map.on('locationerror', onLocationError);
770
779
 
771
780
  // single right click to add a marker
772
781
  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)'/>";
773
- if (navigator.onLine) { addmenu += '<br/><a href="https://spatialillusions.com/unitgenerator/" target="_new">MilSymbol SIDC generator</a>'; }
782
+ if (navigator.onLine) { addmenu += '<br/><a href="https://www.spatialillusions.com/unitgenerator-legacy/" target="_new">MilSymbol SIDC generator</a>'; }
774
783
  var rightmenuMap = L.popup({keepInView:true, minWidth:260}).setContent(addmenu);
775
784
 
785
+ 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('')}`;
786
+ const colorKeywordToRGB = (colorKeyword) => {
787
+ let el = document.createElement('div');
788
+ el.style.color = colorKeyword;
789
+ document.body.appendChild(el);
790
+ let rgbValue = window.getComputedStyle(el).color;
791
+ document.body.removeChild(el);
792
+ console.log(rgbValue);
793
+ return rgba2hex(rgbValue);
794
+ }
795
+
776
796
  var rclk = {};
777
797
  var hiderightclick = false;
778
798
  var addThing = function() {
@@ -783,6 +803,7 @@ var addThing = function() {
783
803
  var icon = (bits[1] || "circle").trim();
784
804
  var lay = (bits[2] || "_drawing").trim();
785
805
  var colo = (bits[3] ?? "#910000").trim();
806
+ colo = colorKeywordToRGB(colo);
786
807
  var hdg = parseFloat(bits[4] || 0);
787
808
  var drag = true;
788
809
  var regi = /^[S,G,E,I,O][A-Z]{3}.*/i; // if it looks like a SIDC code
@@ -794,6 +815,8 @@ var addThing = function() {
794
815
  d.icon = icon;
795
816
  d.iconColor = colo;
796
817
  }
818
+ if (icon === "dot") { d.icon = 'fa-circle fa-fw'; }
819
+ if (icon === "spot") { d.icon = 'fa-circle fa-fw'; }
797
820
  ws.send(JSON.stringify(d));
798
821
  delete d.action;
799
822
  setMarker(d);
@@ -1324,8 +1347,8 @@ var coords = L.control.mouseCoordinate({position:"bottomleft"});
1324
1347
  var legend = L.control({ position: "bottomleft" });
1325
1348
 
1326
1349
  // Add the dialog box for messages
1327
- var dialogue = L.control.dialog({initOpen:false, size:[600,400], anchor:[50,150]}).addTo(map);
1328
- dialogue.freeze();
1350
+ // var dialogue = L.control.dialog({initOpen:false, size:[600,400], anchor:[50,150]}).addTo(map);
1351
+ // dialogue.freeze();
1329
1352
 
1330
1353
  var doDialog = function(d) {
1331
1354
  //console.log("DIALOGUE",d);
@@ -1836,7 +1859,7 @@ function setMarker(data) {
1836
1859
  marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag});
1837
1860
  }
1838
1861
  else if (data.icon === "locate") {
1839
- data.iconColor = data.iconColor || "cyan";
1862
+ data.iconColor = data.iconColor || "#00ffff";
1840
1863
  icon = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="468px" height="468px" viewBox="0 0 468 468">';
1841
1864
  icon += '<polygon points="32 32 104 32 104 0 0 0 0 104 32 104" fill="'+data.iconColor+'"/>';
1842
1865
  icon += '<polygon points="468 0 364 0 364 32 436 32 436 104 468 104" fill="'+data.iconColor+'"/>';
@@ -2590,23 +2613,64 @@ function doCommand(cmd) {
2590
2613
  }
2591
2614
 
2592
2615
  // Add a new KML overlay layer
2593
- if (cmd.map && cmd.map.hasOwnProperty("overlay") && cmd.map.hasOwnProperty("kml") ) {
2616
+ // if (cmd.map && cmd.map.hasOwnProperty("overlay") && cmd.map.hasOwnProperty("kml") ) {
2617
+ // if (overlays.hasOwnProperty(cmd.map.overlay)) {
2618
+ // overlays[cmd.map.overlay].removeFrom(map);
2619
+ // existsalready = true;
2620
+ // }
2621
+ // try {
2622
+ // const parser = new DOMParser();
2623
+ // if (typeof cmd.map.kml === "object") { cmd.map.kml = new TextDecoder().decode(new Uint8Array(cmd.map.kml.data).buffer); }
2624
+ // const kml = parser.parseFromString(cmd.map.kml, 'text/xml');
2625
+ // const track = new L.KML(kml);
2626
+ // overlays[cmd.map.overlay] = track;
2627
+ // } catch(e) { console.log("Failed to parse KML",e) }
2628
+ // if (!existsalready) {
2629
+ // layercontrol.addOverlay(overlays[cmd.map.overlay],cmd.map.overlay);
2630
+ // }
2631
+ // if (!cmd.map.hasOwnProperty("visible") || (cmd.map.visible != false)) {
2632
+ // overlays[cmd.map.overlay].addTo(map);
2633
+ // }
2634
+ // if (cmd.map.hasOwnProperty("fly") && cmd.map.fly === true) { map.flyToBounds(overlays[cmd.map.overlay].getBounds()); }
2635
+ // else if (cmd.map.hasOwnProperty("fit") && cmd.map.fit === true) { map.fitBounds(overlays[cmd.map.overlay].getBounds()); }
2636
+ // }
2637
+ // Add a new KMZ overlay layer (or KML)
2638
+ //if (cmd.map && cmd.map.hasOwnProperty("overlay") && cmd.map.hasOwnProperty("kmz")) {
2639
+ if (cmd.map && cmd.map.hasOwnProperty("overlay") && ( cmd.map.hasOwnProperty("kmz") || cmd.map.hasOwnProperty("kml")) ) {
2594
2640
  if (overlays.hasOwnProperty(cmd.map.overlay)) {
2595
2641
  overlays[cmd.map.overlay].removeFrom(map);
2596
2642
  existsalready = true;
2597
2643
  }
2598
2644
  try {
2599
- const parser = new DOMParser();
2600
- const kml = parser.parseFromString(cmd.map.kml, 'text/xml');
2601
- const track = new L.KML(kml);
2602
- overlays[cmd.map.overlay] = track;
2603
- } catch(e) { console.log("Failed to parse KML") }
2645
+ var kmz = L.kmzLayer().addTo(map);
2646
+ kmz.on('load', function(e) {
2647
+ overlays[cmd.map.overlay] = kmz;
2648
+ if (!cmd.map.hasOwnProperty("visible") || (cmd.map.visible != false)) {
2649
+ overlays[cmd.map.overlay].addTo(map);
2650
+ }
2651
+ });
2652
+ let arr;
2653
+ if (cmd.map.hasOwnProperty("kmz")) {
2654
+ console.log("KMZ",typeof cmd.map.kmz)
2655
+ if (typeof cmd.map.kmz === "string") {
2656
+ arr = new Uint8Array(cmd.map.kmz.length);
2657
+ for (let i=0; i<cmd.map.kmz.length; i++) {
2658
+ arr[i] = cmd.map.kmz.charCodeAt(i);
2659
+ }
2660
+ arr = arr.buffer;
2661
+ }
2662
+ else { arr = new Uint8Array(cmd.map.kmz.data).buffer; }
2663
+ }
2664
+ if (cmd.map.hasOwnProperty("kml")) {
2665
+ if (typeof cmd.map.kml === "string") { arr = cmd.map.kml; }
2666
+ else { arr = new Uint8Array(cmd.map.kml.data).buffer; }
2667
+ }
2668
+ kmz.parse(arr, { name:cmd.map.overlay, icons:{} });
2669
+ overlays[cmd.map.overlay] = kmz;
2670
+ } catch(e) { console.log("Failed to parse KML/KMZ",e) }
2604
2671
  if (!existsalready) {
2605
2672
  layercontrol.addOverlay(overlays[cmd.map.overlay],cmd.map.overlay);
2606
2673
  }
2607
- if (!cmd.map.hasOwnProperty("visible") || (cmd.map.visible != false)) {
2608
- overlays[cmd.map.overlay].addTo(map);
2609
- }
2610
2674
  if (cmd.map.hasOwnProperty("fly") && cmd.map.fly === true) { map.flyToBounds(overlays[cmd.map.overlay].getBounds()); }
2611
2675
  else if (cmd.map.hasOwnProperty("fit") && cmd.map.fit === true) { map.fitBounds(overlays[cmd.map.overlay].getBounds()); }
2612
2676
  }
@@ -2929,25 +2993,37 @@ function doGeojson(n,g,l,o) {
2929
2993
 
2930
2994
  // handle TAK messages from TAK server tcp - XML->JSON
2931
2995
  function doTAKjson(p) {
2932
- // console.log("TAK event",p);
2933
- if (p.type.indexOf('a') === 0) {
2996
+ //console.log("TAK event",p);
2997
+ if (p.type.indexOf('a-') === 0 || p.type.indexOf('b-m-p-') === 0 || p.type.indexOf('b-a-o-') === 0 || p.type.indexOf('b-a-g') === 0) {
2934
2998
  var d = {};
2999
+ d.name = p.detail?.contact?.callsign || p.uid;
2935
3000
  d.lat = Number(p.point.lat);
2936
3001
  d.lon = Number(p.point.lon);
2937
- d.group = p.detail?.__group?.name;
2938
- d.role = p.detail?.__group?.role;
3002
+ if (p.type.indexOf('a') === 0) {
3003
+ d.hdg = p.detail?.track?.course;
3004
+ d.speed = p.detail?.track?.speed;
3005
+ d.team = p.detail?.__group?.name;
3006
+ d.team = d.team + ' <i style="color:' + d.team + '" class="fa fa-square"></i>';
3007
+ d.role = p.detail?.__group?.role;
3008
+ }
2939
3009
  d.type = p.type;
3010
+ d.remarks = p.detail?.remarks
3011
+ if (p.detail?.remarks && p.detail.remarks.hasOwnProperty["#text"]) {
3012
+ d.remarks = p.detail.remarks["#text"];
3013
+ }
2940
3014
  d.uid = p.uid;
2941
- d.name = p.detail?.contact?.callsign || p.uid;
2942
- d.hdg = p.detail?.track?.course;
2943
- d.speed = p.detail?.track?.speed;
2944
- var i = d.type.split('-').join('').toUpperCase();
2945
- if (i[0] === 'A') { i = 'S' + i.substr(1,2) + 'P' + i.substr(3); }
2946
- d.SIDC = (i + '------------').substr(0,12);
2947
- d.timestamp = Date.parse(p.time);
2948
- d.ttl = Date.parse(p.stale);
2949
- // d.now = Date.now();
3015
+
3016
+ try {
3017
+ var st = (new Date(p.time)).getTime() / 1000;
3018
+ var et = (new Date(p.stale)).getTime() / 1000;
3019
+ d.timestamp = (new Date(p.time)).toISOString();
3020
+ d.staletime = (new Date(p.stale)).toISOString();
3021
+ d.ttl = parseInt(et-st);
3022
+ }
3023
+ catch(e) { console.log(e); }
2950
3024
  d.alt = Number(p.point.hae) || 9999999;
3025
+ if (d.alt === 9999999) { delete d.alt; }
3026
+ handleCoTtypes(d,p);
2951
3027
  setMarker(d);
2952
3028
  }
2953
3029
  else {
@@ -2962,23 +3038,147 @@ function doTAKMCjson(p) {
2962
3038
  var d = {};
2963
3039
  d.lat = p.lat;
2964
3040
  d.lon = p.lon;
2965
- d.group = p.detail?.group?.name;
3041
+ d.team = p.detail?.group?.name;
3042
+ d.team = d.team + ' <i style="color:' + d.team + '" class="fa fa-square"></i>';
2966
3043
  d.role = p.detail?.group?.role;
2967
3044
  d.type = p.type;
2968
3045
  d.uid = p.uid;
2969
3046
  d.name = p.detail?.contact?.callsign || p.uid;
2970
3047
  d.hdg = p.detail?.track?.course;
2971
3048
  d.speed = p.detail?.track?.speed;
2972
- var i = d.type.split('-').join('').toUpperCase();
2973
- if (i[0] === 'A') { i = 'S' + i.substr(1,2) + 'P' + i.substr(3); }
2974
- d.SIDC = (i + '------------').substr(0,12);
2975
- d.timestamp = Number(p.sendTime);
2976
- d.ttl = Number(p.staleTime);
2977
- // d.now = Date.now();
3049
+
3050
+ try {
3051
+ d.timestamp = (new Date(+p.sendTime)).toISOString();
3052
+ d.staletime = (new Date(+p.staleTime)).toISOString();
3053
+ d.ttl = parseInt((+p.staleTime / 1000) - (+p.sendTime / 1000));
3054
+ } catch(e) { console.log(e); }
2978
3055
  d.alt = p.hae || 9999999;
3056
+ if (d.alt === 9999999) { delete d.alt; }
3057
+ handleCoTtypes(d,p);
2979
3058
  setMarker(d);
2980
3059
  }
2981
3060
  else {
2982
3061
  console.log("Skip TAK type",p.type);
2983
3062
  }
3063
+ }
3064
+
3065
+ function convertCOTtoCIFColour(color) {
3066
+ const c = parseInt(color);
3067
+ const arr = new ArrayBuffer(4);
3068
+ const view = new DataView(arr);
3069
+ view.setUint32(0, color, false);
3070
+ const b2h = buf2hex(arr);
3071
+ return "#" + b2h.substr(2);
3072
+ }
3073
+
3074
+ function buf2hex(buffer) { // buffer is an ArrayBuffer
3075
+ return [...new Uint8Array(buffer)]
3076
+ .map(x => x.toString(16).padStart(2, '0'))
3077
+ .join('');
3078
+ }
3079
+
3080
+ function createRings(r) {
3081
+ if (r <= 100) { return r; }
3082
+ var rings = [];
3083
+ var step = 100;
3084
+ if (r > 1000) { step = 1000; }
3085
+ if (r > 10000) { step = 10000; }
3086
+ for (var i = step; i < r; i += step) {
3087
+ rings.push(i);
3088
+ }
3089
+ rings.push(r);
3090
+ return rings;
3091
+ }
3092
+
3093
+ function handleCoTtypes(d,p) {
3094
+ if (d.type.indexOf('a-') === 0) { // handle a- types
3095
+ var i = d.type.split('-').join('').toUpperCase();
3096
+ i = 'S' + i.substr(1,2) + 'P' + i.substr(3);
3097
+ if (d.role === 'Team Lead') { i = i + '----B'; }
3098
+ if (d.role === 'HQ') { i = 'SFGPUH' };
3099
+ if (d.role === "Medic") { i = 'SFGPUSM----A'; }
3100
+ if (d.role === "RTO") { i = 'SFGPUUS'; }
3101
+ if (d.role === 'K9') { i = 'SFGPUU'; }
3102
+ d.SIDC = (i + '-------').substr(0,12);
3103
+ // Handle "special" types
3104
+ if (d.type === "a-h-X-i-o") { d.SIDC = "EHIP--------" }
3105
+ if (d.type === "a-h-X-i-m-d") { d.SIDC = "EHNPBB------" }
3106
+ if (d.type === "a-h-X-i-g-e") { d.SIDC = "EHNPAC------" }
3107
+ return d;
3108
+ }
3109
+ else { // handle b- types
3110
+ // console.log("TYPE",d.type);
3111
+ try {
3112
+ if (d.type === 'b-m-p-s-m') { // small spot marker
3113
+ d.icon = "fa-circle fa-fw";
3114
+ d.ttl = 0;
3115
+ d.iconColor = convertCOTtoCIFColour(p.detail.color.argb);
3116
+ delete d.SIDC;
3117
+ }
3118
+ if (d.type.indexOf('b-m-p-s-p') === 0) { // it's a position indicator
3119
+ delete d.SIDC;
3120
+ d.ttl = 0;
3121
+ if (d.type.indexOf('b-m-p-s-p-loc') === 0) {
3122
+ if (p.detail?.sensor) {
3123
+ if (p.detail?.__video) {
3124
+ d.icon = "fa-video-camera";
3125
+ d.video_link = p.detail?.__video?.ConnectionEntry?.protocol+'://'+p.detail?.__video?.url
3126
+ }
3127
+ else {
3128
+ d.SIDC = "SFGPUUMRS---";
3129
+ }
3130
+ if (p.detail.sensor?.fov) {
3131
+ d.arc = {
3132
+ fov: +p.detail.sensor.fov,
3133
+ pan: +p.detail.sensor.azimuth,
3134
+ ranges: createRings(+p.detail.sensor.range),
3135
+ color: convertCOTtoCIFColour(p.detail.sensor.strokeColor)
3136
+ }
3137
+ }
3138
+ }
3139
+ else { d.icon = "locate"; }
3140
+ }
3141
+ if (d.type.indexOf('b-m-p-s-p-op') === 0) {
3142
+ d.icon = "fa-binoculars";
3143
+ }
3144
+ }
3145
+ if (d.type === 'b-m-p-w-GOTO') {
3146
+ d.SIDC = "GFGPGPRP----";
3147
+ }
3148
+ if (d.type === 'b-m-p-c') {
3149
+ d.SIDC = "GFGPGPRW----";
3150
+ }
3151
+ if (d.type === 'b-a-o-tbl' || d.type === 'b-a-o-pan' || d.type === 'b-a-o-opn') {
3152
+ d.remarks = p.detail.emergency["#text"] + " " + p.detail.emergency.type;
3153
+ d.icon = 'fa-exclamation-circle';
3154
+ if (d.type === 'b-a-o-tbl') { d.iconColor = 'gold'; }
3155
+ if (d.type === 'b-a-o-pan') { d.iconColor = 'orange'; }
3156
+ if (d.type === 'b-a-o-opn') { d.iconColor = 'red'; }
3157
+ // d.SIDC = 'ESOPB-------';
3158
+ d.ttl = 0;
3159
+ }
3160
+ if (d.type === 'b-a-g') { // geofence alert
3161
+ d.remarks = p.detail.emergency["#text"] + " " + p.detail.emergency.type;
3162
+ d.icon = 'fa-crosshairs';
3163
+ d.iconColor = 'orange';
3164
+ // d.SIDC = 'ESOPEC------';
3165
+ d.ttl = 0;
3166
+ }
3167
+ if (d.type === 'b-a-o-can') { // cancelled alert
3168
+ d.name = p.detail.emergency["#text"] + "-Alert";
3169
+ d.deleted = true;
3170
+ }
3171
+ }
3172
+ catch(e) {
3173
+ console.log(e);
3174
+ }
3175
+ // console.log("D",d)
3176
+ // Other non-atom types - tbd
3177
+ // b-i-x-i Camera image ?
3178
+ // b-m-r Route
3179
+ // b-r-f-h-c Medevac "EFOPBD------"
3180
+ // b-d Drawings -c-c circle -c-e ellipse -r rectangle -f freehand
3181
+ // b-t-f Geochat (f = file) just No
3182
+ }
3183
+ return d;
2984
3184
  }