Update to include more controls, sidebar, payload data age.

bearings
Mark Jessop 2018-07-15 21:50:21 +09:30
rodzic 40d67f0757
commit 2b75824188
18 zmienionych plików z 585 dodań i 113 usunięć

Wyświetl plik

@ -13,6 +13,7 @@ from datetime import datetime
from dateutil.parser import parse
from horuslib import *
from horuslib.geometry import *
from horuslib.atmosphere import time_to_landing
from horuslib.listener import OziListener, UDPListener
from horuslib.earthmaths import *
@ -51,6 +52,7 @@ current_payload_tracks = {} # Store of payload Track objects which are used to c
# Chase car position
car_track = GenericTrack()
#
# Flask Routes
#
@ -78,30 +80,22 @@ def flask_emit_event(event_name="none", data={}):
def udp_listener_summary_callback(data):
''' Handle a Payload Summary Message from UDPListener '''
global current_payloads, current_payload_tracks
# Extract the fields we need.
print("SUMMARY:" + str(data))
_lat = data['latitude']
_lon = data['longitude']
_alt = data['altitude']
_callsign = "Payload" # data['callsign'] # Quick hack to limit to a single balloon
def handle_new_payload_position(data):
# Process the 'short time' value if we have been provided it.
if 'time' in data.keys():
_full_time = datetime.utcnow().strftime("%Y-%m-%dT") + data['time'] + "Z"
_time_dt = parse(_full_time)
else:
# Otherwise use the current UTC time.
_time_dt = datetime.utcnow()
_lat = data['lat']
_lon = data['lon']
_alt = data['alt']
_time_dt = data['time_dt']
_callsign = data['callsign']
_short_time = _time_dt.strftime("%H:%M:%S")
if _callsign not in current_payloads:
# New callsign! Create entries in data stores.
current_payload_tracks[_callsign] = GenericTrack()
current_payloads[_callsign] = {
'telem': {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':0.0},
'telem': {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':0.0, 'speed':0.0, 'short_time':_short_time, 'time_to_landing':""},
'path': [],
'pred_path': [],
'pred_landing': [],
@ -114,11 +108,43 @@ def udp_listener_summary_callback(data):
_state = current_payload_tracks[_callsign].get_latest_state()
if _state != None:
_vel_v = _state['ascent_rate']
_speed = _state['speed']
# If this payload is in descent, calculate the time to landing.
if _vel_v < 0.0:
# Try and get the altitude of the chase car - we use this as the expected 'ground' level.
_car_state = car_track.get_latest_state()
if _car_state != None:
_ground_asl = _car_state['alt']
else:
_ground_asl = 0.0
# Calculate
_ttl = time_to_landing(_alt, _vel_v, ground_asl=_ground_asl)
if _ttl is None:
_ttl = ""
elif _ttl == 0:
_ttl = "LANDED"
else:
_min = _ttl // 60
_sec = _ttl % 60
_ttl = "%02d:%02d" % (_min,_sec)
else:
_ttl = ""
else:
_vel_v = 0.0
_ttl = ""
# Now update the main telemetry store.
current_payloads[_callsign]['telem'] = {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':_vel_v}
current_payloads[_callsign]['telem'] = {
'callsign': _callsign,
'position':[_lat, _lon, _alt],
'vel_v':_vel_v,
'speed':_speed,
'short_time':_short_time,
'time_to_landing': _ttl}
current_payloads[_callsign]['path'].append([_lat, _lon, _alt])
# Update the web client.
@ -126,6 +152,33 @@ def udp_listener_summary_callback(data):
def udp_listener_summary_callback(data):
''' Handle a Payload Summary Message from UDPListener '''
global current_payloads, current_payload_tracks
# Extract the fields we need.
print("SUMMARY:" + str(data))
# Convert to something generic we can pass onwards.
output = {}
output['lat'] = data['latitude']
output['lon'] = data['longitude']
output['alt'] = data['altitude']
output['callsign'] = "Payload" # data['callsign'] # Quick hack to limit to a single balloon
# Process the 'short time' value if we have been provided it.
if 'time' in data.keys():
_full_time = datetime.utcnow().strftime("%Y-%m-%dT") + data['time'] + "Z"
output['time_dt'] = parse(_full_time)
else:
# Otherwise use the current UTC time.
output['time_dt'] = datetime.utcnow()
handle_new_payload_position(output)
def udp_listener_car_callback(data):
''' Handle car position data '''
global car_track

Wyświetl plik

@ -5,4 +5,65 @@ body {
html, body, #map {
height: 100%;
width: 100vw;
}
.slimContainer {
position: relative;
margin: 20px auto;
width: 290px;
}
.slimContainer hr {
margin-bottom: 10px;
}
.slimContainer .row {
width: 280px;
display: block;
margin: 5px;
vertical-align: middle;
position: relative;
}
.slimContainer .row.info {
margin-top: 10px;
}
.slimContainer .row > span {
float: left;
}
.slimContainer .row.option > span {
width: 270px;
}
.slimContainer .row.option > span {
line-height: 30px;
}
.slimContainer .row > span.r {
float: right;
}
.paramRow {
margin: 5px;
}
.paramSelector {
position: absolute;
right: 10px;
}
.paramEntry {
position: absolute;
right: 10px;
}
.timeToLanding {
color:red;
font-weight: bold;
font-size:5em;
}
.dataAgeOK {
color:black;
font-weight: bold;
font-size:1em;
}
.dataAgeBad {
color:red;
font-weight: bold;
font-size:1.5em;
}

File diff suppressed because one or more lines are too long

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.2 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 696 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.4 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.4 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 618 B

Wyświetl plik

@ -0,0 +1,27 @@
/***********************************************
leaflet-control-topcenter.scss,
(c) 2016, FCOO
https://github.com/FCOO/leaflet-control-topcenter
https://github.com/FCOO
************************************************/
/* control positioning */
.leaflet-center {
position: relative !important;
left: 0;
right: 0;
align-items: center;
display: flex;
flex-direction: column;
justify-content: center; }
.leaflet-center .leaflet-control {
bottom: 0; }
.leaflet-control-container .leaflet-control-bottomcenter {
position: absolute;
bottom: 0;
left: 0;
right: 0; }

Wyświetl plik

@ -0,0 +1 @@
.sidebar{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;z-index:2000;box-shadow:0 1px 5px rgba(0,0,0,.65)}.sidebar.collapsed{width:40px}@media (min-width:768px) and (max-width:991px){.sidebar{width:305px}.sidebar-pane{min-width:265px}}@media (min-width:992px) and (max-width:1199px){.sidebar{width:390px}}@media (min-width:1200px){.sidebar{width:460px}}.sidebar-left{left:0}.sidebar-right{right:0}@media (min-width:768px){.sidebar{top:10px;bottom:10px;transition:width .5s}.sidebar-left{left:10px}.sidebar-right{right:10px}}.sidebar-tabs{top:0;bottom:0;height:100%;background-color:#fff}.sidebar-left .sidebar-tabs{left:0}.sidebar-right .sidebar-tabs{right:0}.sidebar-tabs,.sidebar-tabs>ul{position:absolute;width:40px;margin:0;padding:0;list-style-type:none}.sidebar-tabs>li,.sidebar-tabs>ul>li{width:100%;height:40px;color:#333;font-size:12pt;overflow:hidden;transition:all 80ms}.sidebar-tabs>li:hover,.sidebar-tabs>ul>li:hover{color:#000;background-color:#eee}.sidebar-tabs>li.active,.sidebar-tabs>ul>li.active{color:#fff;background-color:#0074d9}.sidebar-tabs>li.disabled,.sidebar-tabs>ul>li.disabled{color:rgba(51,51,51,.4)}.sidebar-tabs>li.disabled:hover,.sidebar-tabs>ul>li.disabled:hover{background:0 0}.sidebar-tabs>li.disabled>a,.sidebar-tabs>ul>li.disabled>a{cursor:default}.sidebar-tabs>li>a,.sidebar-tabs>ul>li>a{display:block;width:100%;height:100%;line-height:40px;color:inherit;text-decoration:none;text-align:center}.sidebar-tabs>ul+ul{bottom:0}.sidebar-content{position:absolute;top:0;bottom:0;background-color:rgba(255,255,255,.95);overflow-x:hidden;overflow-y:auto}.sidebar-left .sidebar-content{left:40px;right:0}.sidebar-right .sidebar-content{left:0;right:40px}.sidebar.collapsed>.sidebar-content{overflow-y:hidden}.sidebar-pane{display:none;left:0;right:0;box-sizing:border-box;padding:10px 20px}.sidebar-pane.active{display:block}.sidebar-header{margin:-10px -20px 0;height:40px;padding:0 20px;line-height:40px;font-size:14.4pt;color:#fff;background-color:#0074d9}.sidebar-right .sidebar-header{padding-left:40px}.sidebar-close{position:absolute;top:0;width:40px;height:40px;text-align:center;cursor:pointer}.sidebar-left .sidebar-close{right:0}.sidebar-right .sidebar-close{left:0}.sidebar-left~.sidebar-map{margin-left:40px}.sidebar-right~.sidebar-map{margin-right:40px}.sidebar.leaflet-touch{box-shadow:none;border-right:2px solid rgba(0,0,0,.2)}@media (min-width:768px) and (max-width:991px){.sidebar-left~.sidebar-map .leaflet-left{left:315px}.sidebar-right~.sidebar-map .leaflet-right{right:315px}}@media (min-width:992px) and (max-width:1199px){.sidebar-pane{min-width:350px}.sidebar-left~.sidebar-map .leaflet-left{left:400px}.sidebar-right~.sidebar-map .leaflet-right{right:400px}}@media (min-width:1200px){.sidebar-pane{min-width:420px}.sidebar-left~.sidebar-map .leaflet-left{left:470px}.sidebar-right~.sidebar-map .leaflet-right{right:470px}}@media (min-width:768px){.sidebar-left~.sidebar-map{margin-left:0}.sidebar-right~.sidebar-map{margin-right:0}.sidebar{border-radius:4px}.sidebar.leaflet-touch{border:2px solid rgba(0,0,0,.2)}.sidebar-left~.sidebar-map .leaflet-left{transition:left .5s}.sidebar-left.collapsed~.sidebar-map .leaflet-left{left:50px}.sidebar-right~.sidebar-map .leaflet-right{transition:right .5s}.sidebar-right.collapsed~.sidebar-map .leaflet-right{right:50px}}

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -0,0 +1,65 @@
(function (window, document, undefined) {
L.Control.Custom = L.Control.extend({
version: '1.0.1',
options: {
position: 'topright',
id: '',
title: '',
classes: '',
content: '',
style: {},
datas: {},
events: {},
},
container: null,
onAdd: function (map) {
this.container = L.DomUtil.create('div');
this.container.id = this.options.id;
this.container.title = this.options.title;
this.container.className = this.options.classes;
this.container.innerHTML = this.options.content;
for (var option in this.options.style)
{
this.container.style[option] = this.options.style[option];
}
for (var data in this.options.datas)
{
this.container.dataset[data] = this.options.datas[data];
}
/* Prevent click events propagation to map */
L.DomEvent.disableClickPropagation(this.container);
/* Prevent right click event propagation to map */
L.DomEvent.on(this.container, 'contextmenu', function (ev)
{
L.DomEvent.stopPropagation(ev);
});
/* Prevent scroll events propagation to map when cursor on the div */
L.DomEvent.disableScrollPropagation(this.container);
for (var event in this.options.events)
{
L.DomEvent.on(this.container, event, this.options.events[event], this.container);
}
return this.container;
},
onRemove: function (map) {
for (var event in this.options.events)
{
L.DomEvent.off(this.container, event, this.options.events[event], this.container);
}
},
});
L.control.custom = function (options) {
return new L.Control.Custom(options);
};
}(window, document));

Wyświetl plik

@ -0,0 +1,2 @@
// Configuration Setting Handler.
// Mark Jessop 2018-07

Wyświetl plik

@ -0,0 +1,33 @@
/****************************************************************************
leaflet-control-topcenter.js,
(c) 2016, FCOO
https://github.com/FCOO/leaflet-control-topcenter
https://github.com/FCOO
****************************************************************************/
(function (L /*, window, document, undefined*/) {
"use strict";
//Extend Map._initControlPos to also create a topcenter-container
L.Map.prototype._initControlPos = function ( _initControlPos ) {
return function () {
//Original function/method
_initControlPos.apply(this, arguments);
//Adding new control-containers
//topcenter is the same as the rest of control-containers
this._controlCorners['topcenter'] = L.DomUtil.create('div', 'leaflet-top leaflet-center', this._controlContainer);
//bottomcenter need an extra container to be placed at the bottom
this._controlCorners['bottomcenter'] =
L.DomUtil.create(
'div',
'leaflet-bottom leaflet-center',
L.DomUtil.create('div', 'leaflet-control-bottomcenter', this._controlContainer)
);
};
} (L.Map.prototype._initControlPos);
}(L, this, document));

Wyświetl plik

@ -0,0 +1 @@
L.Control.Sidebar=L.Control.extend({includes:L.Evented.prototype||L.Mixin.Events,options:{position:"left"},initialize:function(t,s){var i,e;for(L.setOptions(this,s),this._sidebar=L.DomUtil.get(t),L.DomUtil.addClass(this._sidebar,"sidebar-"+this.options.position),L.Browser.touch&&L.DomUtil.addClass(this._sidebar,"leaflet-touch"),i=this._sidebar.children.length-1;i>=0;i--)"DIV"==(e=this._sidebar.children[i]).tagName&&L.DomUtil.hasClass(e,"sidebar-content")&&(this._container=e);for(this._tabitems=this._sidebar.querySelectorAll("ul.sidebar-tabs > li, .sidebar-tabs > ul > li"),i=this._tabitems.length-1;i>=0;i--)this._tabitems[i]._sidebar=this;for(this._panes=[],this._closeButtons=[],i=this._container.children.length-1;i>=0;i--)if("DIV"==(e=this._container.children[i]).tagName&&L.DomUtil.hasClass(e,"sidebar-pane")){this._panes.push(e);for(var o=e.querySelectorAll(".sidebar-close"),a=0,l=o.length;a<l;a++)this._closeButtons.push(o[a])}},addTo:function(t){var s,i;for(this._map=t,s=this._tabitems.length-1;s>=0;s--){var e=(i=this._tabitems[s]).querySelector("a");e.hasAttribute("href")&&"#"==e.getAttribute("href").slice(0,1)&&L.DomEvent.on(e,"click",L.DomEvent.preventDefault).on(e,"click",this._onClick,i)}for(s=this._closeButtons.length-1;s>=0;s--)i=this._closeButtons[s],L.DomEvent.on(i,"click",this._onCloseClick,this);return this},removeFrom:function(t){console.log("removeFrom() has been deprecated, please use remove() instead as support for this function will be ending soon."),this.remove(t)},remove:function(t){var s,i;for(this._map=null,s=this._tabitems.length-1;s>=0;s--)i=this._tabitems[s],L.DomEvent.off(i.querySelector("a"),"click",this._onClick);for(s=this._closeButtons.length-1;s>=0;s--)i=this._closeButtons[s],L.DomEvent.off(i,"click",this._onCloseClick,this);return this},open:function(t){var s,i;for(s=this._panes.length-1;s>=0;s--)(i=this._panes[s]).id==t?L.DomUtil.addClass(i,"active"):L.DomUtil.hasClass(i,"active")&&L.DomUtil.removeClass(i,"active");for(s=this._tabitems.length-1;s>=0;s--)(i=this._tabitems[s]).querySelector("a").hash=="#"+t?L.DomUtil.addClass(i,"active"):L.DomUtil.hasClass(i,"active")&&L.DomUtil.removeClass(i,"active");return this.fire("content",{id:t}),L.DomUtil.hasClass(this._sidebar,"collapsed")&&(this.fire("opening"),L.DomUtil.removeClass(this._sidebar,"collapsed")),this},close:function(){for(var t=this._tabitems.length-1;t>=0;t--){var s=this._tabitems[t];L.DomUtil.hasClass(s,"active")&&L.DomUtil.removeClass(s,"active")}return L.DomUtil.hasClass(this._sidebar,"collapsed")||(this.fire("closing"),L.DomUtil.addClass(this._sidebar,"collapsed")),this},_onClick:function(){L.DomUtil.hasClass(this,"active")?this._sidebar.close():L.DomUtil.hasClass(this,"disabled")||this._sidebar.open(this.querySelector("a").hash.slice(1))},_onCloseClick:function(){this.close()}}),L.control.sidebar=function(t,s){return new L.Control.Sidebar(t,s)};

Wyświetl plik

@ -0,0 +1,62 @@
//
//
// Initialise tables
function initTables(){
// Telemetry data table
$("#telem_table").tabulator({
layout:"fitData",
layoutColumnsOnNewData:true,
columns:[ //Define Table Columns
{title:"Callsign", field:"callsign", headerSort:false},
{title:"Time (Z)", field:"short_time", headerSort:false},
{title:"Latitude", field:"lat", headerSort:false},
{title:"Longitude", field:"lon", headerSort:false},
{title:"Alt (m)", field:"alt", headerSort:false},
{title:"V_rate (m/s)", field:"vel_v", headerSort:false}
]
});
$("#summary_table").tabulator({
layout:"fitData",
layoutColumnsOnNewData:true,
columns:[ //Define Table Columns
{title:"Alt (m)", field:"alt", headerSort:false},
{title:"Speed (kph)", field:"speed", headerSort:false},
{title:"Asc Rate (m/s)", field:"vel_v", headerSort:false},
{title:"Azimuth", field:"azimuth", headerSort:false},
{title:"Elevation", field:"elevation", headerSort:false},
{title:"Range", field:"range", headerSort:false},
],
data:[{alt:'-----m', speed:'---kph', vel_v:'-.-m/s', azimuth:'---°', elevation:'--°', range:'----m'}]
});
//var dummy_data = {alt:'-----m', speed:'---kph', vel_v:'-.-m/s', azimuth:'---°', elev:'--°', range:'----m'};
//$("#summary_table").tabulator("setData", dummy_data);
}
function updateTelemetryTable(){
var telem_data = [];
if (jQuery.isEmptyObject(balloon_positions)){
telem_data = [{callsign:'None'}];
}else{
for (balloon_call in balloon_positions){
var balloon_call_data = Object.assign({},balloon_positions[balloon_call].latest_data);
var balloon_call_age = balloon_positions[balloon_call].age;
//if ((Date.now()-balloon_call_age)>180000){
// balloon_call_data.callsign = "";
//}
// Modify some of the fields to fixed point values.
balloon_call_data.lat = balloon_call_data.position[0].toFixed(5);
balloon_call_data.lon = balloon_call_data.position[1].toFixed(5);
balloon_call_data.alt = balloon_call_data.position[2].toFixed(1);
balloon_call_data.vel_v = balloon_call_data.vel_v.toFixed(1);
telem_data.push(balloon_call_data);
}
}
$("#telem_table").tabulator("setData", telem_data);
}

Wyświetl plik

@ -1,14 +1,10 @@
// Utility Functions
// Mark Jessop 2018-06-30
// Color cycling for balloon traces and icons - Hopefully 4 colors should be enough for now!
var colour_values = ['blue','green','purple'];
var colour_idx = 0;
// Create a set of icons for the different colour values.
var balloonAscentIcons = {};
var balloonDescentIcons = {};
@ -60,4 +56,7 @@ var carIcon = L.icon({
// Other Global map settings
var prediction_opacity = 0.6;
var parachute_min_alt = 300; // Show the balloon as a 'landed' payload below this altitude.
var parachute_min_alt = 300; // Show the balloon as a 'landed' payload below this altitude.
var car_bad_age = 5.0;
var payload_bad_age = 30.0;

Wyświetl plik

@ -5,7 +5,10 @@
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/leaflet.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/leaflet-sidebar.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/leaflet-control-topcenter.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/tabulator_simple.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/chasemapper.css') }}" rel="stylesheet">
@ -14,8 +17,13 @@
<script src="{{ url_for('static', filename='js/jquery-ui.min.js')}}"></script>
<script src="{{ url_for('static', filename='js/socket.io-1.4.5.js') }}"></script>
<script src="{{ url_for('static', filename='js/leaflet.js') }}"></script>
<script src="{{ url_for('static', filename='js/leaflet-control-topcenter.js') }}"></script>
<script src="{{ url_for('static', filename='js/leaflet-sidebar.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/Leaflet.Control.Custom.js') }}"></script>
<script src="{{ url_for('static', filename='js/tabulator.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
<script src="{{ url_for('static', filename='js/tables.js') }}"></script>
<!-- Leaflet plugins... -->
<script src="{{ url_for('static', filename='js/leaflet.rotatedMarker.js') }}"></script>
@ -33,7 +41,6 @@
// Predictor settings
pred_enabled: true, // Enable running and display of predicted flight paths.
// Default prediction settings (actual values will be used once the flight is underway)
pred_asc_rate: 5.0,
pred_desc_rate: 6.0,
pred_burst: 28000,
show_abort: true, // Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*)
@ -58,7 +65,12 @@
// latest_data: [lat,lon, alt] (latest car position)
// heading: Car heading (to point icon appropriately.)
// marker: Leaflet marker
var chase_car_position = {latest_data: [], heading:0, marker: 'NONE'};
var chase_car_position = {latest_data: [], heading:0, marker: 'NONE', path: 'NONE'};
// Data Age variables - these are updated regularly to indicate
// if we haven't received data in a while.
var payload_data_age = 0.0;
var car_data_age = 0.0;
// Other markers which may be added. (TBD, probably other chase car positions via the LoRa payload?)
var misc_markers = {};
@ -114,49 +126,80 @@
maxZoom: 18,
});
map.addControl(new L.Control.Layers({'OSM':osm_map, 'ESRI Satellite':esri_sat_map}));
var sidebar = L.control.sidebar('sidebar').addTo(map);
// Telemetry Data Table
// Using tabulator makes this *really* easy.
$("#telem_table").tabulator({
height:180,
layout:"fitData",
layoutColumnsOnNewData:true,
columns:[ //Define Table Columns
//{title:"Source", field:"source", headerSort:false},
{title:"Callsign", field:"callsign", headerSort:false},
{title:"Timestamp", field:"datetime", headerSort:false},
{title:"Latitude", field:"lat", headerSort:false},
{title:"Longitude", field:"lon", headerSort:false},
{title:"Altitude (m)", field:"alt", headerSort:false},
{title:"Asc Rate (m/s)", field:"vel_v", headerSort:false}
]
});
// Add custom controls, which show various sets of data.
function updateTelemetryTable(){
var telem_data = [];
if (jQuery.isEmptyObject(balloon_positions)){
telem_data = [{callsign:'None'}];
}else{
for (balloon_call in balloon_positions){
var balloon_call_data = Object.assign({},balloon_positions[balloon_call].latest_data);
var balloon_call_age = balloon_positions[balloon_call].age;
//if ((Date.now()-balloon_call_age)>180000){
// balloon_call_data.callsign = "";
//}
// Modify some of the fields to fixed point values.
balloon_call_data.lat = balloon_call_data.lat.toFixed(5);
balloon_call_data.lon = balloon_call_data.lon.toFixed(5);
balloon_call_data.alt = balloon_call_data.alt.toFixed(1);
balloon_call_data.vel_v = balloon_call_data.vel_v.toFixed(1);
telem_data.push(balloon_call_data);
}
// Telemetry table - shows all payload telemetry.
L.control.custom({
position: 'bottomright',
content : "<div id='telem_table'></div>",
classes : 'btn-group-vertical btn-group-sm',
id: 'telem_display',
style :
{
margin: '5px',
padding: '0px 0 0 0',
cursor: 'pointer',
}
})
.addTo(map);
$("#telem_table").tabulator("setData", telem_data);
// Summary display - a 'quick-look' of where the currently tracked payload is and what it's doing.
L.control.custom({
position: 'topcenter',
content : "<div id='summary_table'></div>",
classes : 'btn-group-vertical btn-group-sm',
id: 'summary_display',
style :
{
margin: '5px',
padding: '0px 0 0 0',
cursor: 'pointer',
}
})
.addTo(map);
// Data age display - shows how old the various datasets are.
L.control.custom({
position: 'topleft',
content : "<div id='payload_age' class='dataAgeOK'></div><div id='car_age' class='dataAgeOK'></div>",
classes : 'btn-group-vertical btn-group-sm',
id: 'age_display',
style :
{
margin: '5px',
padding: '0px 0 0 0',
cursor: 'pointer',
}
})
.addTo(map);
// Time-to-landing display - shows the time until landing for the currently tracked payload.
L.control.custom({
position: 'bottomcenter',
content : "<div id='time_to_landing' class='timeToLanding'></div>",
classes : 'btn-group-vertical btn-group-sm',
id: 'ttl_display',
style :
{
margin: '5px',
padding: '0px 0 0 0',
cursor: 'pointer',
}
})
.addTo(map);
function mapMovedEvent(e){
// The user has panned the map, stop following things.
$('input:radio[name=autoFollow]').val(['none']);
}
map.on('dragend',mapMovedEvent);
initTables();
function add_new_balloon(data){
@ -172,7 +215,6 @@
// abort_path: Abort prediction
// abort_landing: Abort prediction landing
console.log(data);
var telem = data.telem;
var callsign = data.telem.callsign;
@ -240,7 +282,6 @@
}
colour_idx = (colour_idx+1)%colour_values.length;
}
@ -259,7 +300,7 @@
add_new_balloon(data[callsign]);
}
// Update telemetry table
//updateTelemetryTable();
updateTelemetryTable();
initial_load_complete = true;
}
});
@ -273,6 +314,7 @@
// callsign: string
// position: [lat, lon, alt]
// vel_v: float
// time_to_landing: String
// If callsign = 'CAR', the lat/lon/alt will be considered to be a car telemetry position.
if(initial_load_complete == false){
@ -290,69 +332,101 @@
// Create marker!
chase_car_position.marker = L.marker(chase_car_position.latest_data,{title:"Chase Car", icon: carIcon})
.addTo(map);
chase_car_position.path = L.polyline([chase_car_position.latest_data],{title:"Chase Car", color:'black'});
// If the user wants the chase car tail, add it to the map.
if (document.getElementById("chaseCarTrack").checked == true){
chase_car_position.path.addTo(map);
}
} else {
chase_car_position.marker.setLatLng(chase_car_position.latest_data).update();
// TODO: Rotate chase car with direction of travel.
}
return;
}
car_data_age = 0.0;
}else{
// Otherwise, we have a balloon
// Have we seen this ballon before?
if (balloon_positions.hasOwnProperty(data.callsign) == false){
// Otherwise, we have a balloon
// Have we seen this ballon before?
if (balloon_positions.hasOwnProperty(data.callsign) == false){
// Convert the incoming data into a format suitable for adding into the telem store.
var temp_data = {};
temp_data.telem = data;
temp_data.path = [data.position];
temp_data.pred_path = [];
temp_data.pred_landing = [];
temp_data.abort_path = [];
temp_data.abort_landing = [];
// Convert the incoming data into a format suitable for adding into the telem store.
var temp_data = {};
temp_data.telem = data;
temp_data.path = [data.position];
temp_data.pred_path = [];
temp_data.pred_landing = [];
temp_data.abort_path = [];
temp_data.abort_landing = [];
// Add it to the telemetry store and create markers.
add_new_balloon(temp_data);
// Add it to the telemetry store and create markers.
add_new_balloon(temp_data);
// Update data age to indicate current time.
balloon_positions[data.callsign].age = Date.now();
// Update data age to indicate current time.
balloon_positions[data.callsign].age = Date.now();
} else {
// Yep - update the sonde_positions entry.
balloon_positions[data.callsign].latest_data = data;
balloon_positions[data.callsign].age = Date.now();
balloon_positions[data.callsign].path.addLatLng(data.position);
balloon_positions[data.callsign].marker.setLatLng(data.position).update();
} else {
// Yep - update the sonde_positions entry.
balloon_positions[data.callsign].latest_data = data;
balloon_positions[data.callsign].age = Date.now();
balloon_positions[data.callsign].path.addLatLng(data.position);
balloon_positions[data.callsign].marker.setLatLng(data.position).update();
if (data.vel_v < 0){
balloon_positions[data.callsign].marker.setIcon(balloonDescentIcons[balloon_positions[data.callsign].colour]);
}else{
balloon_positions[data.callsign].marker.setIcon(balloonAscentIcons[balloon_positions[data.callsign].colour]);
if (data.vel_v < 0){
balloon_positions[data.callsign].marker.setIcon(balloonDescentIcons[balloon_positions[data.callsign].colour]);
}else{
balloon_positions[data.callsign].marker.setIcon(balloonAscentIcons[balloon_positions[data.callsign].colour]);
}
if (data.position[2] < parachute_min_alt){
balloon_positions[data.callsign].marker.setIcon(balloonPayloadIcons[balloon_positions[callsign].colour]);
}
}
if (data.position[2] < parachute_min_alt){
balloon_positions[data.callsign].marker.setIcon(balloonPayloadIcons[balloon_positions[callsign].colour]);
// Update the telemetry table display
updateTelemetryTable();
// Are we currently following any other sondes?
if (balloon_currently_following == "none"){
// If not, follow this one!
balloon_currently_following = data.callsign;
}
// Update the Summary and time-to-landing displays
if (balloon_currently_following = data.callsign){
$('#time_to_landing').text(data.time_to_landing);
// Generate data to update summary display with.
var _summary_update = {};
_summary_update.alt = data.position[2].toFixed(0) + "m";
var _speed = data.speed*3.6;
_summary_update.speed = _speed.toFixed(0) + " kph";
_summary_update.vel_v = data.vel_v.toFixed(1) + " m/s";
_summary_update.azimuth = "---°";
_summary_update.elevation = "--°";
_summary_update.range = "----m";
// Calculate az/el/range from Car position to balloon.
if (chase_car_position.latest_data.length == 3){
// Calculate relative position.
} else{
// Leave the values as they are.
}
// Update the summary table
$("#summary_table").tabulator("setData", [_summary_update]);
payload_data_age = 0.0;
}
}
// Update the telemetry table display
//updateTelemetryText();
//updateTelemetryTable();
// Are we currently following any other sondes?
if (balloon_currently_following == "none"){
// If not, follow this one!
balloon_currently_following = data.callsign;
// Auto Pan selection between balloon or car.
var _current_follow = $('input[name=autoFollow]:checked').val();
if (_current_follow == 'payload' && data.callsign == balloon_currently_following){
map.panTo(data.position);
} else if (_current_follow == 'car' && data.callsign == 'CAR'){
map.panTo(data.position);
}else{
// Don't pan to anything.
}
// // Is sonde following enabled?
// if (document.getElementById("balloonAutoFollow").checked == true){
// // If we are currently following this sonde, snap the map to it.
// if (data.callsign == balloon_currently_following){
// map.panTo(data.position);
// }
// }
// TODO: Auto Pan selection between balloon or car.
map.panTo(data.position);
});
@ -364,11 +438,101 @@
});
var age_update_rate = 500;
window.setInterval(function () {
// Update the data age displays.
payload_data_age += age_update_rate/1000.0;
car_data_age += age_update_rate/1000.0;
if (balloon_currently_following === 'none'){
$("#payload_age").text("Payload: No Data.");
}else{
$("#payload_age").text("Payload: " + payload_data_age.toFixed(1)+"s");
if (payload_data_age > payload_bad_age){
$("#payload_age").removeClass();
$("#payload_age").addClass('dataAgeBad');
}else{
$("#payload_age").removeClass();
$("#payload_age").addClass('dataAgeOK');
}
}
if(chase_car_position.latest_data.length == 0){
$("#car_age").text("Car GPS: No Data.");
}else{
$("#car_age").text("Car GPS: " + car_data_age.toFixed(1)+"s");
if (car_data_age > car_bad_age){
$("#car_age").removeClass();
$("#car_age").addClass('dataAgeBad');
}else{
$("#car_age").removeClass();
$("#car_age").addClass('dataAgeOK');
}
}
}, age_update_rate);
});
</script>
</head>
<body>
<div id="map"></div>
<div id="sidebar" class="sidebar collapsed">
<!-- Nav tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
<li><a href="#settings" role="tab"><i class="fa fa-gear"></i></a></li>
</ul>
</div>
<!-- Tab panes -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<h1 class="sidebar-header">
sidebar-v2
<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>
</h1>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</div>
<div class="sidebar-pane" id="settings">
<h1 class="sidebar-header">Settings<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
</hr>
<h4>Auto-Follow</h4>
<form>
<input type="radio" name="autoFollow" value="payload" checked> Payload<br>
<input type="radio" name="autoFollow" value="car"> Chase Car<br>
<input type="radio" name="autoFollow" value="none"> None
</form>
</hr>
<h4>Map</h4>
<div class="paramRow">
<b>Show Car Track</b> <input type="checkbox" class="paramSelector" id="chaseCarTrack" checked>
</div>
</hr>
<h4>Predictor</h4>
<div class="paramRow">
<b>Enable Predictions</b> <input type="checkbox" class="paramSelector" id="predictorEnabled">
</div>
<div class="paramRow">
<b>Burst Altitude</b><input type="text" class="paramEntry" id="burstAlt"><br/>
</div>
<div class="paramRow">
<b>Descent Rate</b><input type="text" class="paramEntry" id="descentRate"><br/>
</div>
</div>
</div>
</div>
<div id="map" class="sidebar-map"></div>
</body>
</html>