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