dark mode and websockets
[kismet-logviewer.git] / logviewer / static / adsb_map_panel.html
1 <!DOCTYPE html>
2 <html>
3 <head>
4     <title>live adsb map</title>
5
6     <script src="js/jquery-3.1.0.min.js"></script>
7     <script src="js/chart.umd.js"></script>
8     <script src="js/js.storage.min.js"></script>
9     <script src="js/kismet.ui.theme.js"></script>
10
11     <link rel="stylesheet" type="text/css" href="css/font-awesome.min.css">
12     <link rel="stylesheet" href="css/leaflet.css" />
13     <link rel="stylesheet" type="text/css" href="css/jquery.jspanel.min.css" />
14     <link rel="stylesheet" type="text/css" href="css/Control.Loading.css" />
15
16     <script src="js/leaflet.js"></script>
17     <script src="js/Leaflet.MultiOptionsPolyline.min.js"></script>
18     <script src="js/Control.Loading.js"></script>
19     <script src="js/chroma.min.js"></script>
20
21     <script src="js/js.storage.min.js"></script>
22     <script src="js/kismet.utils.js"></script>
23     <script src="js/kismet.units.js"></script>
24
25     <script src="js/datatables.min.js"></script>
26     <script src="js/dataTables.scrollResize.js"></script>
27
28     <style>
29         :root {
30             --adsb-sidebar-background: white;
31             --adsb-sidebar-background-offset: #f9f9f9;
32         }
33
34         [data-theme="dark"] {
35             --adsb-sidebar-background: #222;
36             --adsb-sidebar-background-offset: #444;
37             --map-tiles-filter: brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7);
38         }
39
40         .map-tiles {
41             filter: var(--map-tiles-filter, none);
42         }
43
44         body {
45             padding: 0;
46             margin: 0;
47         }
48
49         html, body, #map {
50             height: 100%;
51             font: 10pt "Helvetica Neue", Arial, Helvetica, sans-serif;
52         }
53
54         .marker-center {
55             margin: 0;
56             position: absolute;
57             top: 50%;
58             left: 50%;
59             -ms-transform: translate(-50%, -50%);
60             transform: translate(-50%, -50%);
61         }
62
63         .right-sidebar {
64             position: absolute;
65             top: 10px;
66             bottom: 10px;
67             right: 10px;
68             width: 20%;
69             border: 1px solid black;
70             background: var(--adsb-sidebar-background);
71             z-index: 999;
72             padding: 10px;
73         }
74
75         .warning {
76             position: absolute;
77             top: 10%;
78             bottom: 10%;
79             right: 25%;
80             left: 25%;
81             border: 1px solid black;
82             background: var(--adsb-sidebar-background);
83             z-index: 10000;
84             padding: 10px;
85         }
86
87         #alt_scale {
88             width: 50%;
89             position: absolute;
90             bottom: 10px;
91             left: 25%;
92             height: 15px;
93             z-index: 999;
94             border: 1px solid black;
95             padding-left: 10px;
96             padding-right: 10px;
97             background: linear-gradient(to right, 
98                 hsl(50,100%,50%), 
99                 hsl(100,100%,50%), 
100                 hsl(150,100%,50%), 
101                 hsl(200,100%,50%), 
102                 hsl(250,100%,50%), 
103                 hsl(300,100%,50%), 
104                 hsl(360,100%,50%));
105             text-align: center;
106         }
107
108         #alt_min {
109             position: absolute;
110             left: 10px;
111         }
112
113         #alt_mini {
114             position: absolute;
115             left: 25%;
116         }
117
118         #alt_maxi {
119             position: absolute;
120             left: 75%;
121         }
122
123         #alt_max {
124             position: absolute;
125             right: 10px;
126         }
127
128         #alt_title {
129             display: inline-block;
130         }
131
132         .resize_wrapper {
133             position: relative;
134             box-sizing: border-box;
135             height: calc(100% - 125px);
136             padding: 0.5em 0.5em 1.5em 0.5em;
137             border-radius: 0.5em;
138             background: var(--adsb-sidebar-background-offset);
139             overflow: hidden;
140         }
141
142     </style>
143 </head>
144 <body>
145     <div id="warning" class="warning">
146         <p><b>Warning!</b>
147         <p>To display the live ADSB map, your browser will connect to the Leaflet and Open Street Map servers to fetch the map tiles.  This requires you have a functional Internet connection, and will reveal something about your location (the bounding region where planes have been seen.)
148         <p><input id="dontwarn" type="checkbox">Don't warn me again</input>
149         <p><button id="continue">Continue</button>
150     </div>
151     <div id="alt_scale">
152         <div id="alt_min"></div>
153         <div id="alt_mini"></div>
154         <div id="alt_maxi"></div>
155         <div id="alt_max"></div>
156         <div id="alt_title"><strong>Altitude</strong></div>
157     </div>
158     <div id="map"></div>
159     <div class="right-sidebar">
160         <div id="plane-count" style="height: 10px">
161         <i class="fa fa-plane" style="padding-right: 1em;"></i><span id="numplanes">0</span> planes in the past 10 minutes
162         </div>
163
164         <div id="plane-detail" style="padding-top: 10px; height: 75px;"></div>
165         <br>
166         <div height="100%" class="resize_wrapper">
167         <table width="100%" id="adsb_planes" style="font-size: 80%">
168             <thead>
169                 <tr>
170                     <th>ICAO</th>
171                     <th>ID</th>
172                     <th>Alt</th>
173                     <th>Spd</th>
174                     <th>Hed</th>
175                     <th>Msgs</th>
176                 </tr>
177             </thead>
178         </table>
179         </div>
180     </div>
181
182     <script>
183         units = 'i';
184
185         if (kismet.getStorage('kismet.base.unit.distance') === 'metric' ||
186             kismet.getStorage('kismet.base.unit.distance') === '')
187             units = 'm';
188
189         if (units === 'm') {
190             $('#alt_min').html("0m");
191             $('#alt_mini').html("3000m");
192             $('#alt_maxi').html("9000m");
193             $('#alt_max').html("12000m");
194         } else {
195             $('#alt_min').html("0ft");
196             $('#alt_mini').html("10000ft");
197             $('#alt_maxi').html("30000ft");
198             $('#alt_max').html("40000ft");
199         }
200
201         var window_visible = true;
202
203         // Visibility detection from https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
204         // Set the name of the hidden property and the change event for visibility
205         var hidden, visibilityChange; 
206         if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support 
207             hidden = "hidden";
208             visibilityChange = "visibilitychange";
209         } else if (typeof document.msHidden !== "undefined") {
210             hidden = "msHidden";
211             visibilityChange = "msvisibilitychange";
212         } else if (typeof document.webkitHidden !== "undefined") {
213             hidden = "webkitHidden";
214             visibilityChange = "webkitvisibilitychange";
215         }
216
217         function handleVisibilityChange() {
218             if (document[hidden]) {
219                 window_visible = false;
220             } else {
221                 window_visible = true;
222             }
223         }
224
225         // Warn if the browser doesn't support addEventListener or the Page Visibility API
226         if (typeof document.addEventListener === "undefined" || hidden === undefined) {
227             ; // Do nothing
228         } else {
229             // Handle page visibility change   
230             document.addEventListener(visibilityChange, handleVisibilityChange, false);
231         }
232
233         var urlparam = new URL(window.location.href);
234         var param_url = urlparam.searchParams.get('parent_url') + "/";
235         var param_prefix = urlparam.searchParams.get('local_uri_prefix', "");
236         var KISMET_PROXY_PREFIX = urlparam.searchParams.get('KISMET_PROXY_PREFIX', "");
237
238         if (param_prefix == 0)
239             param_prefix=""
240
241         var local_uri_prefix = param_url + param_prefix;
242         if (typeof(KISMET_URI_PREFIX) !== 'undefined')
243             local_uri_prefix = KISMET_URI_PREFIX;
244
245         var map_configured = false;
246
247         var markers = {};
248
249         var tid = -1;
250
251         var map = null;
252
253         function get_alt_color(alt, v_perc=50) {
254             // Colors go from 50 to 360 on the HSV slider, so scale to 310
255             if (units === 'm') {
256                 if (alt > 12000)
257                     alt = 12000;
258                 if (alt < 0)
259                     alt = 0;
260
261                 h = 40 + (310 * (alt / 12000));
262                 hv = h.toFixed(0);
263
264                 return `hsl(${hv}, 100%, ${v_perc}%)`
265             } else {
266                 alt_f = alt * 3.2808399;
267                 if (alt_f > 40000)
268                     alt_f = 40000;
269                 if (alt_f < 0)
270                     alt_f = 0;
271
272                 h = 40 + (310 * (alt_f / 40000));
273                 hv = h.toFixed(0);
274
275                 return `hsl(${hv}, 100%, ${v_perc}%)`
276             }
277         }
278
279         var moused_icao = null;
280         var moused_id = null;
281
282         var planes_dt = $('#adsb_planes').DataTable({
283             data: [],
284             searching: false,
285             scrollY: 500,
286             scrollResize: true,
287             scroller: true,
288             paging: true,
289             dom: "ft",
290             createdRow: function(row, data, index) {
291                 row.id = `ROW_ICAO_${data[0]}`;
292             },
293         });
294
295
296         function wrap_closure_click(k) {
297             return function() {
298                 $('#adsb_planes').DataTable().row(`#ROW_ICAO_${markers[k]['icao']}`).scrollTo();
299
300                 if (moused_icao != null)
301                     $(`#ROW_ICAO_${moused_icao}`).css('background-color', '');
302
303                 moused_id = k;
304                 moused_icao = markers[k]['icao'];
305
306                 $(`#ROW_ICAO_${markers[k]['icao']}`).css('background-color', 'red');
307             }
308
309         };
310
311         function wrap_closure_mouseover(k) {
312             return function() {
313                 if (markers[k]['path'] != null) {
314                     markers[k]['path'].setStyle({
315                         weight: 3,
316                         dashArray: '',
317                     });
318                 }
319
320                 // $('#adsb_marker_icon_' + kismet.sanitizeId(k)).css('color', 'red');
321                 $('#adsb_marker_icon_' + kismet.sanitizeId(k)).css('font-size', '24px');
322
323                 $('#plane-detail').html("<b>Flight:</b> " + markers[k]['callsign'] + "<br>" +
324                     "<b>Model:</b> " + markers[k]['model'].MiddleShorten(20) + "<br>" + 
325                     "<b>Operator: </b>" + markers[k]['operator'].MiddleShorten(20) + "<br>" + 
326                     "<b>Altitude: </b>" + kismet_units.renderHeightDistance(markers[k]['altitude'], 0, true) + "<br>" + 
327                     "<b>Speed: </b>" + kismet_units.renderSpeed(markers[k]['speed'], 0) + "<br>");
328
329             }
330         };
331
332         function wrap_closure_mouseout(k) {
333             return function() {
334                 if (markers[k]['path'] != null) {
335                     markers[k]['path'].setStyle({
336                         weight: 2,
337                         dashArray: '3',
338                     });
339                 }
340
341                 // $('#adsb_marker_icon_' + kismet.sanitizeId(k)).css('color', get_alt_color(markers[k]['altitude']));
342                 $('#adsb_marker_icon_' + kismet.sanitizeId(k)).css('font-size', '18px');
343             }
344         };
345
346         function map_cb(d) {
347             data = kismet.sanitizeObject(d);
348
349             // $('#count').html("Active in the last 10 minutes: " + data['kismet.adsb.map.devices'].length);
350             $('#numplanes').html(data['kismet.adsb.map.devices'].length);
351
352             if (!map_configured) {
353                 var lat1 = data['kismet.adsb.map.min_lat'];
354                 var lon1 = data['kismet.adsb.map.min_lon'];
355                 var lat2 = data['kismet.adsb.map.max_lat'];
356                 var lon2 = data['kismet.adsb.map.max_lon'];
357
358                 map = L.map('map', {
359                     loadingControl: true
360                 });
361                 map.fitBounds([[lat1, lon1], [lat2, lon2]])
362                 L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
363                     maxZoom: 19,
364                     attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
365                     className: 'map-tiles',
366                 }).addTo(map);
367
368                 map_configured = true;
369             }
370
371             var dt = $('#adsb_planes').DataTable();
372
373             var prev_pos = {
374                 'top': $(dt.settings()[0].nScrollBody).scrollTop(),
375                 'left': $(dt.settings()[0].nScrollBody).scrollLeft()
376             };
377
378             dt.clear();
379
380             for (var d = 0; d < data['kismet.adsb.map.devices'].length; d++) {
381                 try {
382                     var lat = data['kismet.adsb.map.devices'][d]['kismet.device.base.location']['kismet.common.location.last']['kismet.common.location.geopoint'][1];
383                     var lon = data['kismet.adsb.map.devices'][d]['kismet.device.base.location']['kismet.common.location.last']['kismet.common.location.geopoint'][0];
384                     var heading = data['kismet.adsb.map.devices'][d]['kismet.device.base.location']['kismet.common.location.last']['kismet.common.location.heading'];
385                     var altitude = data['kismet.adsb.map.devices'][d]['kismet.device.base.location']['kismet.common.location.last']['kismet.common.location.alt'];
386                     var speed = data['kismet.adsb.map.devices'][d]['kismet.device.base.location']['kismet.common.location.last']['kismet.common.location.speed'];
387                     var icao = data['kismet.adsb.map.devices'][d]['adsb.device']['adsb.device.icao'];
388                     var id = data['kismet.adsb.map.devices'][d]['adsb.device']['kismet.adsb.icao_record']['adsb.icao.regid'];
389                     var packets = data['kismet.adsb.map.devices'][d]['kismet.device.base.packets.data'];
390                     var atype = data['kismet.adsb.map.devices'][d]['adsb.device']['kismet.adsb.icao_record']['adsb.icao.atype_short'];
391
392
393                     // console.log([icao, id, altitude, speed, heading, packets]);
394
395                     dt.row.add([icao, id, kismet_units.renderHeightDistanceUnitless(altitude, 0), kismet_units.renderSpeedUnitless(speed, 0, true), heading.toFixed(0), packets]);
396
397                     if (lat == 0 || lon == 0)
398                         continue;
399
400                     key = data['kismet.adsb.map.devices'][d]['kismet.device.base.key'];
401
402                     var icontype = 'fa-plane';
403
404                     /*
405                      * 1 - Glider
406                      * 2 - Balloon
407                      * 3 - Blimp/Dirigible
408                      * 4 - Fixed wing single engine
409                      * 5 - Fixed wing multi engine
410                      * 6 - Rotorcraft
411                      * 7 - Weight-shift-control
412                      * 8 - Powered Parachute
413                      * 9 - Gyroplane
414                      * H - Hybrid Lift
415                      * O - Other
416                      */
417                     if (atype == "1".charCodeAt(0) || atype == "7".charCodeAt(0))
418                         icontype == 'fa-paper-plane';
419                     else if (atype == "6".charCodeAt(0))
420                         icontype == 'fa-helicopter';
421
422                     var myIcon = L.divIcon({
423                         className: 'plane-icon', 
424                         html: '<div id="adsb_marker_' + kismet.sanitizeId(key) + '" style="width: 24px; height: 24px; transform-origin: center;"><i id="adsb_marker_icon_' + kismet.sanitizeId(key) + '" class="marker-center fa ' + icontype + '" style="font-size: 18px; color: ' + get_alt_color(altitude) + ';"></div>',
425                         iconAnchor: [12, 12],
426                     });
427
428                     if (key in markers) {
429                         marker = markers[key]['marker'];
430                         markers[key]['keep'] = true;
431
432                         // Move the marker
433                         $('#adsb_marker_' + kismet.sanitizeId(key)).css('transform', 'rotate(' + (heading - 45) + 'deg)');
434                         var new_loc = new L.LatLng(lat, lon);
435                         marker.setLatLng(new_loc); 
436
437                         // Recolor the marker
438                         $('#adsb_marker_icon_' + kismet.sanitizeId(k)).css('color', get_alt_color(altitude));
439
440                         /*
441                         if (markers[key]['last_lat'] != lat || markers[key]['last_lon'] != lon) {
442                             markers[key]['pathlist'].push([lat, lon]);
443
444                             markers[key]['last_lat'] = lat;
445                             markers[key]['last_lon'] = lon;
446                             markers[key]['heading'] = heading;
447
448                             if (markers[key]['path'] != null) {
449                                 markers[key]['path'].addLatLng([lat, lon]);
450                             } else {
451                                 markers[key]['path'] = L.polyline(markers[key]['pathlist'], {
452                                     color: 'red',
453                                     weight: 2,
454                                     dashArray: '3',
455                                     opacity: 0.85,
456                                     smoothFactor: 1,
457                                 }).addTo(map);
458
459                                 markers[key]['path'].on('mouseover', wrap_closure_mouseover(key));
460                                 markers[key]['path'].on('mouseout', wrap_closure_mouseout(key));
461                             }
462                         }
463                         */
464
465                     } else {
466                         /* Make a new marker */
467
468                         var marker = L.marker([lat, lon], { icon: myIcon} ).addTo(map);
469                         $('#adsb_marker_' + kismet.sanitizeId(key)).css('transform', 'rotate(' + (heading - 45) + 'deg)');
470
471                         markers[key] = {};
472                         markers[key]['marker'] = marker;
473                         markers[key]['icao'] = icao;
474                         markers[key]['keep'] = true;
475                         markers[key]['pathlist'] = [[lat, lon]];
476                         markers[key]['path'] = null;
477                         markers[key]['last_path_ts'] = 0;
478
479                         markers[key]['model'] = data['kismet.adsb.map.devices'][d]['adsb.device']['kismet.adsb.icao_record']['adsb.icao.model'];
480                         markers[key]['operator'] = data['kismet.adsb.map.devices'][d]['adsb.device']['kismet.adsb.icao_record']['adsb.icao.owner'];
481                         markers[key]['callsign'] = data['kismet.adsb.map.devices'][d]['adsb.device']['adsb.device.callsign'];
482
483                         markers[key]['marker'].on('mouseover', wrap_closure_mouseover(key));
484                         markers[key]['marker'].on('mouseout', wrap_closure_mouseout(key));
485                         markers[key]['marker'].on('click', wrap_closure_click(key));
486                     }
487
488                     markers[key]['altitude'] = altitude;
489                     markers[key]['heading'] = heading;
490                     markers[key]['speed'] = speed;
491                     markers[key]['last_lat'] = lat;
492                     markers[key]['last_lon'] = lon;
493
494
495                     // Assign the historic path, if location history is available
496                     try {
497                         var history = data['kismet.adsb.map.devices'][d]['kismet.device.base.location_cloud']['kis.gps.rrd.samples_100'];
498
499                         for (var s in history) {
500                             // Ignore non-location historic points (caused by heading/altitude before we got 
501                             // a location lock
502                             var s_lat = history[s]['kismet.historic.location.geopoint'][1];
503                             var s_lon = history[s]['kismet.historic.location.geopoint'][0];
504                             var s_alt = history[s]['kismet.historic.location.alt'];
505                             var s_ts = history[s]['kismet.historic.location.time_sec'];
506
507                             if (s_lat == 0 || s_lon == 0 || s_ts < markers[key]['last_path_ts'])
508                                 continue
509
510                             markers[key]['last_path_ts'] = s_ts;
511
512                             if (markers[key]['path'] != null) {
513                                 markers[key]['path'].addLatLng([s_lat, s_lon]);
514                             } else {
515                                 markers[key]['path'] = L.polyline([[s_lat, s_lon], [s_lat, s_lon]], {
516                                     color: get_alt_color(s_alt, 25),
517                                     /*
518                                     // color: 'red',
519                                     multiOptions: {
520                                         options: function(v) {
521                                             return {'color': get_alt_color(s_alt)};
522                                         },
523                                     },
524                                     */
525                                     weight: 2,
526                                     dashArray: '3',
527                                     opacity: 0.30,
528                                     smoothFactor: 1,
529                                 }).addTo(map);
530
531                                 markers[key]['path'].on('mouseover', wrap_closure_mouseover(key));
532                                 markers[key]['path'].on('mouseout', wrap_closure_mouseout(key));
533                             }
534
535                         }
536
537                     } catch (error) {
538                         ;
539                     }
540
541                 dt.draw(0);
542
543                 if (moused_icao != null) {
544                     $(`#ROW_ICAO_${moused_icao}`).css('background-color', 'red');
545                 }
546
547                 // Restore our scroll position
548                 $(dt.settings()[0].nScrollBody).scrollTop( prev_pos.top );
549                 $(dt.settings()[0].nScrollBody).scrollLeft( prev_pos.left );
550
551
552                 } catch (error) {
553                     ;
554                 }
555
556             }
557
558             for (var k in markers) {
559                 if (markers[k]['keep']) {
560                     markers[k]['keep'] = false;
561                     continue;
562                 }
563
564                 if (markers[k]['marker'] != null)
565                     map.removeLayer(markers[k]['marker']);
566                 if (markers[k]['path'] != null)
567                     map.removeLayer(markers[k]['path']);
568
569                 delete(markers[k]);
570             }
571         }
572
573         var load_maps = kismet.getStorage('kismet.adsb.maps_ok', false);
574
575         function poll_map() {
576             if (window_visible && !$('#map').is(':hidden') && load_maps) {
577                 $.get(local_uri_prefix + KISMET_PROXY_PREFIX + "phy/ADSB/map_data.json")
578                     .done(function(d) {
579                         map_cb(d);
580                     })
581                     .always(function(d) {
582                         tid = setTimeout(function() { poll_map(); }, 2000);
583                     });
584             } else {
585                 tid = setTimeout(function() { poll_map(); }, 2000);
586             }
587         }
588
589         // Set a global timeout
590         $.ajaxSetup({
591             timeout:5000,
592             xhrFields: {
593                 withCredentials: true
594             }
595         });
596
597         if (load_maps)
598             $('#warning').hide();
599
600         $('#continue').on('click', function() {
601             if ($('#dontwarn').is(":checked"))
602                 kismet.putStorage('kismet.adsb.maps_ok', true);
603             $('#warning').hide();
604             load_maps = true;
605         });
606
607         poll_map();
608
609     </script>
610 </body>
611 </html>