kopia lustrzana https://github.com/projecthorus/chasemapper
Basic prediction support.
rodzic
2b75824188
commit
b2d5f46859
228
chasemapper.py
228
chasemapper.py
|
@ -6,9 +6,13 @@
|
|||
# Released under GNU GPL v3 or later
|
||||
#
|
||||
import json
|
||||
import logging
|
||||
import flask
|
||||
from flask_socketio import SocketIO
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from threading import Thread
|
||||
from datetime import datetime
|
||||
from dateutil.parser import parse
|
||||
from horuslib import *
|
||||
|
@ -37,12 +41,15 @@ chasemapper_config = {
|
|||
'default_lon': 138.6,
|
||||
|
||||
# Predictor settings
|
||||
'pred_enabled': True, # Enable running and display of predicted flight paths.
|
||||
'pred_enabled': False, # 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_model': "No Data",
|
||||
'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*)
|
||||
'show_abort': True, # Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*)
|
||||
'pred_binary': "./pred",
|
||||
'gfs_path': "./gfs/",
|
||||
'predictor_update_rate': 15 # Update predictor every 15 seconds.
|
||||
}
|
||||
|
||||
# Payload data Stores
|
||||
|
@ -79,7 +86,6 @@ def flask_emit_event(event_name="none", data={}):
|
|||
socketio.emit(event_name, data, namespace='/chasemapper')
|
||||
|
||||
|
||||
|
||||
def handle_new_payload_position(data):
|
||||
|
||||
_lat = data['lat']
|
||||
|
@ -99,6 +105,7 @@ def handle_new_payload_position(data):
|
|||
'path': [],
|
||||
'pred_path': [],
|
||||
'pred_landing': [],
|
||||
'burst': [],
|
||||
'abort_path': [],
|
||||
'abort_landing': []
|
||||
}
|
||||
|
@ -151,13 +158,183 @@ def handle_new_payload_position(data):
|
|||
flask_emit_event('telemetry_event', current_payloads[_callsign]['telem'])
|
||||
|
||||
|
||||
#
|
||||
# Predictor Code
|
||||
#
|
||||
predictor = None
|
||||
|
||||
predictor_thread_running = True
|
||||
predictor_thread = None
|
||||
def predictorThread():
|
||||
""" Run the predictor on a regular interval """
|
||||
global predictor_thread_running, chasemapper_config
|
||||
logging.info("Predictor loop started.")
|
||||
|
||||
while predictor_thread_running:
|
||||
run_prediction()
|
||||
for i in range(int(chasemapper_config['predictor_update_rate'])):
|
||||
time.sleep(1)
|
||||
if predictor_thread_running == False:
|
||||
return
|
||||
|
||||
|
||||
def run_prediction():
|
||||
''' Run a Flight Path prediction '''
|
||||
global chasemapper_config, current_payloads, current_payload_tracks, predictor
|
||||
|
||||
if predictor == None:
|
||||
return
|
||||
|
||||
for _payload in current_payload_tracks:
|
||||
|
||||
_current_pos = current_payload_tracks[_payload].get_latest_state()
|
||||
_current_pos_list = [0,_current_pos['lat'], _current_pos['lon'], _current_pos['alt']]
|
||||
|
||||
if _current_pos['is_descending']:
|
||||
_desc_rate = _current_pos['landing_rate']
|
||||
else:
|
||||
_desc_rate = chasemapper_config['pred_desc_rate']
|
||||
|
||||
if _current_pos['alt'] > chasemapper_config['pred_burst']:
|
||||
_burst_alt = _current_pos['alt'] + 100
|
||||
else:
|
||||
_burst_alt = chasemapper_config['pred_burst']
|
||||
|
||||
logging.info("Running Predictor for: %s." % _payload)
|
||||
_pred_path = predictor.predict(
|
||||
launch_lat=_current_pos['lat'],
|
||||
launch_lon=_current_pos['lon'],
|
||||
launch_alt=_current_pos['alt'],
|
||||
ascent_rate=_current_pos['ascent_rate'],
|
||||
descent_rate=_desc_rate,
|
||||
burst_alt=_burst_alt,
|
||||
launch_time=_current_pos['time'],
|
||||
descent_mode=_current_pos['is_descending'])
|
||||
|
||||
if len(_pred_path) > 1:
|
||||
# Valid Prediction!
|
||||
_pred_path.insert(0,_current_pos_list)
|
||||
# Convert from predictor output format to a polyline.
|
||||
_pred_output = []
|
||||
for _point in _pred_path:
|
||||
_pred_output.append([_point[1], _point[2], _point[3]])
|
||||
|
||||
current_payloads[_payload]['pred_path'] = _pred_output
|
||||
current_payloads[_payload]['pred_landing'] = _pred_output[-1]
|
||||
|
||||
if _current_pos['is_descending']:
|
||||
current_payloads[_payload]['burst'] = []
|
||||
else:
|
||||
# Determine the burst position.
|
||||
_cur_alt = 0.0
|
||||
_cur_idx = 0
|
||||
for i in range(len(_pred_output)):
|
||||
if _pred_output[i][2]>_cur_alt:
|
||||
_cur_alt = _pred_output[i][2]
|
||||
_cur_idx = i
|
||||
|
||||
current_payloads[_payload]['burst'] = _pred_output[_cur_idx]
|
||||
|
||||
|
||||
logging.info("Prediction Updated, %d data points." % len(_pred_path))
|
||||
else:
|
||||
logging.error("Prediction Failed.")
|
||||
|
||||
# if _run_abort_prediction and (_current_pos['alt'] < burst_alt) and (_current_pos['is_descending'] == False):
|
||||
# print("Running Abort Prediction... ")
|
||||
# _pred_path = _predictor.predict(
|
||||
# launch_lat=_current_pos['lat'],
|
||||
# launch_lon=_current_pos['lon'],
|
||||
# launch_alt=_current_pos['alt'],
|
||||
# ascent_rate=_current_pos['ascent_rate'],
|
||||
# descent_rate=_desc_rate,
|
||||
# burst_alt=_current_pos['alt']+200,
|
||||
# launch_time=_current_pos['time'])
|
||||
|
||||
# if len(_pred_path) > 1:
|
||||
# _pred_path.insert(0,_current_pos_list)
|
||||
# _abort_prediction = _pred_path
|
||||
# _abort_prediction_valid = True
|
||||
# print("Abort Prediction Updated, %d points." % len(_pred_path))
|
||||
# else:
|
||||
# print("Prediction Failed.")
|
||||
# else:
|
||||
# _abort_prediction_valid = False
|
||||
|
||||
# # If have been asked to run an abort prediction, but we are descent, set the is_valid
|
||||
# # flag to false, so the abort prediction is not plotted.
|
||||
# if _run_abort_prediction and _current_pos['is_descending']:
|
||||
# _abort_prediction_valid == False
|
||||
|
||||
_client_data = {
|
||||
'callsign': _payload,
|
||||
'pred_path': current_payloads[_payload]['pred_path'],
|
||||
'pred_landing': current_payloads[_payload]['pred_landing'],
|
||||
'burst': current_payloads[_payload]['burst'],
|
||||
'abort_path': [],
|
||||
'abort_landing': []
|
||||
}
|
||||
flask_emit_event('predictor_update', _client_data)
|
||||
|
||||
|
||||
def initPredictor():
|
||||
global predictor, predictor_thread, chasemapper_config
|
||||
try:
|
||||
from cusfpredict.predict import Predictor
|
||||
from cusfpredict.utils import gfs_model_age
|
||||
|
||||
# Check if we have any GFS data
|
||||
_model_age = gfs_model_age(chasemapper_config['gfs_path'])
|
||||
if _model_age == "Unknown":
|
||||
logging.error("No GFS data in directory.")
|
||||
chasemapper_config['pred_model'] = "No GFS Data."
|
||||
flask_emit_event('predictor_model_update',{'model':"No GFS data."})
|
||||
else:
|
||||
chasemapper_config['pred_model'] = _model_age
|
||||
flask_emit_event('predictor_model_update',{'model':_model_age})
|
||||
predictor = Predictor(bin_path=chasemapper_config['pred_binary'], gfs_path=chasemapper_config['gfs_path'])
|
||||
|
||||
# Start up the predictor thread.
|
||||
predictor_thread = Thread(target=predictorThread)
|
||||
predictor_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logging.error("Loading predictor failed: " + str(e))
|
||||
flask_emit_event('predictor_model_update',{'model':"Failed - Check Log."})
|
||||
chasemapper_config['pred_model'] = "Failed - Check Log."
|
||||
print("Loading Predictor failed.")
|
||||
predictor = None
|
||||
|
||||
|
||||
@socketio.on('download_model', namespace='/chasemapper')
|
||||
def download_new_model(data):
|
||||
""" Trigger a download of a new weather model """
|
||||
pass
|
||||
# TODO
|
||||
|
||||
# Incoming telemetry handlers
|
||||
|
||||
def ozi_listener_callback(data):
|
||||
""" Handle a OziMux input message """
|
||||
# OziMux message contains:
|
||||
# {'lat': -34.87915, 'comment': 'Telemetry Data', 'alt': 26493.0, 'lon': 139.11883, 'time': datetime.datetime(2018, 7, 16, 10, 55, 49, tzinfo=tzutc())}
|
||||
logging.info("OziMux Data:" + str(data))
|
||||
output = {}
|
||||
output['lat'] = data['lat']
|
||||
output['lon'] = data['lon']
|
||||
output['alt'] = data['alt']
|
||||
output['callsign'] = "Payload"
|
||||
output['time_dt'] = data['time']
|
||||
|
||||
handle_new_payload_position(output)
|
||||
|
||||
|
||||
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))
|
||||
logging.info("Payload Summary Data: " + str(data))
|
||||
|
||||
# Convert to something generic we can pass onwards.
|
||||
output = {}
|
||||
|
@ -177,12 +354,10 @@ def udp_listener_summary_callback(data):
|
|||
handle_new_payload_position(output)
|
||||
|
||||
|
||||
|
||||
|
||||
def udp_listener_car_callback(data):
|
||||
''' Handle car position data '''
|
||||
global car_track
|
||||
print("CAR:" + str(data))
|
||||
logging.debug("Car Position:" + str(data))
|
||||
_lat = data['latitude']
|
||||
_lon = data['longitude']
|
||||
_alt = data['altitude']
|
||||
|
@ -206,16 +381,16 @@ def udp_listener_car_callback(data):
|
|||
flask_emit_event('telemetry_event', {'callsign': 'CAR', 'position':[_lat,_lon,_alt], 'vel_v':0.0, 'heading': _heading})
|
||||
|
||||
|
||||
# Add other listeners here...
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
parser.add_argument("-p","--port",default=5001,help="Port to run Web Server on.")
|
||||
group.add_argument("--ozimux", action="store_true", default=False, help="Take payload input via OziMux (listen on port 8942).")
|
||||
group.add_argument("--summary", action="store_true", default=True, help="Take payload input data via Payload Summary Broadcasts.")
|
||||
group.add_argument("--summary", action="store_true", default=False, help="Take payload input data via Payload Summary Broadcasts.")
|
||||
parser.add_argument("--clamp", action="store_false", default=True, help="Clamp all tracks to ground.")
|
||||
parser.add_argument("--nolabels", action="store_true", default=False, help="Inhibit labels on placemarks.")
|
||||
parser.add_argument("--predict", action="store_true", help="Enable Flight Path Predictions.")
|
||||
|
@ -224,11 +399,30 @@ if __name__ == "__main__":
|
|||
parser.add_argument("--descent_rate", type=float, default=5.0, help="Expected Descent Rate (m/s, positive value). Default = 5.0")
|
||||
parser.add_argument("--abort", action="store_true", default=False, help="Enable 'Abort' Predictions.")
|
||||
parser.add_argument("--predict_rate", type=int, default=15, help="Run predictions every X seconds. Default = 15 seconds.")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", default=False, help="Verbose output.")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Configure logging
|
||||
if args.verbose:
|
||||
_log_level = logging.DEBUG
|
||||
else:
|
||||
_log_level = logging.INFO
|
||||
|
||||
logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', stream=sys.stdout, level=_log_level)
|
||||
# Make flask & socketio only output errors, not every damn GET request.
|
||||
logging.getLogger("requests").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
||||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||||
logging.getLogger('socketio').setLevel(logging.ERROR)
|
||||
logging.getLogger('engineio').setLevel(logging.ERROR)
|
||||
|
||||
if args.ozimux:
|
||||
logging.info("Using OziMux data source.")
|
||||
_listener = OziListener(telemetry_callback=ozi_listener_callback)
|
||||
|
||||
# Start up UDP Broadcast Listener (which we use for car positions even if not for the payload)
|
||||
if args.summary:
|
||||
print("Using Payload Summary Messages.")
|
||||
logging.info("Using Payload Summary data source.")
|
||||
_broadcast_listener = UDPListener(summary_callback=udp_listener_summary_callback,
|
||||
gps_callback=udp_listener_car_callback)
|
||||
else:
|
||||
|
@ -237,9 +431,17 @@ if __name__ == "__main__":
|
|||
|
||||
_broadcast_listener.start()
|
||||
|
||||
if args.predict:
|
||||
initPredictor()
|
||||
|
||||
# Run the Flask app, which will block until CTRL-C'd.
|
||||
socketio.run(app, host='0.0.0.0', port=args.port)
|
||||
|
||||
# Attempt to close the listener.
|
||||
_broadcast_listener.close()
|
||||
try:
|
||||
predictor_thread_running = False
|
||||
_broadcast_listener.close()
|
||||
_listener.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
|
@ -57,6 +57,12 @@ html, body, #map {
|
|||
font-size:5em;
|
||||
}
|
||||
|
||||
.dataAgeHeader {
|
||||
color:black;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
font-size:1em;
|
||||
}
|
||||
.dataAgeOK {
|
||||
color:black;
|
||||
font-weight: bold;
|
||||
|
@ -66,4 +72,16 @@ html, body, #map {
|
|||
color:red;
|
||||
font-weight: bold;
|
||||
font-size:1.5em;
|
||||
}
|
||||
|
||||
#followPayloadButton {
|
||||
width:45px;
|
||||
height:45px;
|
||||
font-size:20px;
|
||||
}
|
||||
|
||||
#followCarButton {
|
||||
width:45px;
|
||||
height:45px;
|
||||
font-size:20px;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
.leaflet-bar button,
|
||||
.leaflet-bar button:hover {
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.leaflet-bar button {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.leaflet-bar button:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.leaflet-bar button:first-of-type {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.leaflet-bar button:last-of-type {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.leaflet-bar.disabled,
|
||||
.leaflet-bar button.disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
.easy-button-button .button-state{
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.leaflet-touch .leaflet-bar button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
|
@ -1,2 +1,10 @@
|
|||
// Configuration Setting Handler.
|
||||
// Mark Jessop 2018-07
|
||||
|
||||
|
||||
// Global map settings
|
||||
var prediction_opacity = 0.6;
|
||||
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;
|
||||
|
|
|
@ -0,0 +1,371 @@
|
|||
(function(){
|
||||
|
||||
// This is for grouping buttons into a bar
|
||||
// takes an array of `L.easyButton`s and
|
||||
// then the usual `.addTo(map)`
|
||||
L.Control.EasyBar = L.Control.extend({
|
||||
|
||||
options: {
|
||||
position: 'topleft', // part of leaflet's defaults
|
||||
id: null, // an id to tag the Bar with
|
||||
leafletClasses: true // use leaflet classes?
|
||||
},
|
||||
|
||||
|
||||
initialize: function(buttons, options){
|
||||
|
||||
if(options){
|
||||
L.Util.setOptions( this, options );
|
||||
}
|
||||
|
||||
this._buildContainer();
|
||||
this._buttons = [];
|
||||
|
||||
for(var i = 0; i < buttons.length; i++){
|
||||
buttons[i]._bar = this;
|
||||
buttons[i]._container = buttons[i].button;
|
||||
this._buttons.push(buttons[i]);
|
||||
this.container.appendChild(buttons[i].button);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
_buildContainer: function(){
|
||||
this._container = this.container = L.DomUtil.create('div', '');
|
||||
this.options.leafletClasses && L.DomUtil.addClass(this.container, 'leaflet-bar easy-button-container leaflet-control');
|
||||
this.options.id && (this.container.id = this.options.id);
|
||||
},
|
||||
|
||||
|
||||
enable: function(){
|
||||
L.DomUtil.addClass(this.container, 'enabled');
|
||||
L.DomUtil.removeClass(this.container, 'disabled');
|
||||
this.container.setAttribute('aria-hidden', 'false');
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
disable: function(){
|
||||
L.DomUtil.addClass(this.container, 'disabled');
|
||||
L.DomUtil.removeClass(this.container, 'enabled');
|
||||
this.container.setAttribute('aria-hidden', 'true');
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
onAdd: function () {
|
||||
return this.container;
|
||||
},
|
||||
|
||||
addTo: function (map) {
|
||||
this._map = map;
|
||||
|
||||
for(var i = 0; i < this._buttons.length; i++){
|
||||
this._buttons[i]._map = map;
|
||||
}
|
||||
|
||||
var container = this._container = this.onAdd(map),
|
||||
pos = this.getPosition(),
|
||||
corner = map._controlCorners[pos];
|
||||
|
||||
L.DomUtil.addClass(container, 'leaflet-control');
|
||||
|
||||
if (pos.indexOf('bottom') !== -1) {
|
||||
corner.insertBefore(container, corner.firstChild);
|
||||
} else {
|
||||
corner.appendChild(container);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
L.easyBar = function(){
|
||||
var args = [L.Control.EasyBar];
|
||||
for(var i = 0; i < arguments.length; i++){
|
||||
args.push( arguments[i] );
|
||||
}
|
||||
return new (Function.prototype.bind.apply(L.Control.EasyBar, args));
|
||||
};
|
||||
|
||||
// L.EasyButton is the actual buttons
|
||||
// can be called without being grouped into a bar
|
||||
L.Control.EasyButton = L.Control.extend({
|
||||
|
||||
options: {
|
||||
position: 'topleft', // part of leaflet's defaults
|
||||
|
||||
id: null, // an id to tag the button with
|
||||
|
||||
type: 'replace', // [(replace|animate)]
|
||||
// replace swaps out elements
|
||||
// animate changes classes with all elements inserted
|
||||
|
||||
states: [], // state names look like this
|
||||
// {
|
||||
// stateName: 'untracked',
|
||||
// onClick: function(){ handle_nav_manually(); };
|
||||
// title: 'click to make inactive',
|
||||
// icon: 'fa-circle', // wrapped with <a>
|
||||
// }
|
||||
|
||||
leafletClasses: true, // use leaflet styles for the button
|
||||
tagName: 'button',
|
||||
},
|
||||
|
||||
|
||||
|
||||
initialize: function(icon, onClick, title, id){
|
||||
|
||||
// clear the states manually
|
||||
this.options.states = [];
|
||||
|
||||
// add id to options
|
||||
if(id != null){
|
||||
this.options.id = id;
|
||||
}
|
||||
|
||||
// storage between state functions
|
||||
this.storage = {};
|
||||
|
||||
// is the last item an object?
|
||||
if( typeof arguments[arguments.length-1] === 'object' ){
|
||||
|
||||
// if so, it should be the options
|
||||
L.Util.setOptions( this, arguments[arguments.length-1] );
|
||||
}
|
||||
|
||||
// if there aren't any states in options
|
||||
// use the early params
|
||||
if( this.options.states.length === 0 &&
|
||||
typeof icon === 'string' &&
|
||||
typeof onClick === 'function'){
|
||||
|
||||
// turn the options object into a state
|
||||
this.options.states.push({
|
||||
icon: icon,
|
||||
onClick: onClick,
|
||||
title: typeof title === 'string' ? title : ''
|
||||
});
|
||||
}
|
||||
|
||||
// curate and move user's states into
|
||||
// the _states for internal use
|
||||
this._states = [];
|
||||
|
||||
for(var i = 0; i < this.options.states.length; i++){
|
||||
this._states.push( new State(this.options.states[i], this) );
|
||||
}
|
||||
|
||||
this._buildButton();
|
||||
|
||||
this._activateState(this._states[0]);
|
||||
|
||||
},
|
||||
|
||||
_buildButton: function(){
|
||||
|
||||
this.button = L.DomUtil.create(this.options.tagName, '');
|
||||
|
||||
if (this.options.tagName === 'button') {
|
||||
this.button.setAttribute('type', 'button');
|
||||
}
|
||||
|
||||
if (this.options.id ){
|
||||
this.button.id = this.options.id;
|
||||
}
|
||||
|
||||
if (this.options.leafletClasses){
|
||||
L.DomUtil.addClass(this.button, 'easy-button-button leaflet-bar-part leaflet-interactive');
|
||||
}
|
||||
|
||||
// don't let double clicks and mousedown get to the map
|
||||
L.DomEvent.addListener(this.button, 'dblclick', L.DomEvent.stop);
|
||||
L.DomEvent.addListener(this.button, 'mousedown', L.DomEvent.stop);
|
||||
L.DomEvent.addListener(this.button, 'mouseup', L.DomEvent.stop);
|
||||
|
||||
// take care of normal clicks
|
||||
L.DomEvent.addListener(this.button,'click', function(e){
|
||||
L.DomEvent.stop(e);
|
||||
this._currentState.onClick(this, this._map ? this._map : null );
|
||||
this._map && this._map.getContainer().focus();
|
||||
}, this);
|
||||
|
||||
// prep the contents of the control
|
||||
if(this.options.type == 'replace'){
|
||||
this.button.appendChild(this._currentState.icon);
|
||||
} else {
|
||||
for(var i=0;i<this._states.length;i++){
|
||||
this.button.appendChild(this._states[i].icon);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
_currentState: {
|
||||
// placeholder content
|
||||
stateName: 'unnamed',
|
||||
icon: (function(){ return document.createElement('span'); })()
|
||||
},
|
||||
|
||||
|
||||
|
||||
_states: null, // populated on init
|
||||
|
||||
|
||||
|
||||
state: function(newState){
|
||||
|
||||
// activate by name
|
||||
if(typeof newState == 'string'){
|
||||
|
||||
this._activateStateNamed(newState);
|
||||
|
||||
// activate by index
|
||||
} else if (typeof newState == 'number'){
|
||||
|
||||
this._activateState(this._states[newState]);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
_activateStateNamed: function(stateName){
|
||||
for(var i = 0; i < this._states.length; i++){
|
||||
if( this._states[i].stateName == stateName ){
|
||||
this._activateState( this._states[i] );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_activateState: function(newState){
|
||||
|
||||
if( newState === this._currentState ){
|
||||
|
||||
// don't touch the dom if it'll just be the same after
|
||||
return;
|
||||
|
||||
} else {
|
||||
|
||||
// swap out elements... if you're into that kind of thing
|
||||
if( this.options.type == 'replace' ){
|
||||
this.button.appendChild(newState.icon);
|
||||
this.button.removeChild(this._currentState.icon);
|
||||
}
|
||||
|
||||
if( newState.title ){
|
||||
this.button.title = newState.title;
|
||||
} else {
|
||||
this.button.removeAttribute('title');
|
||||
}
|
||||
|
||||
// update classes for animations
|
||||
for(var i=0;i<this._states.length;i++){
|
||||
L.DomUtil.removeClass(this._states[i].icon, this._currentState.stateName + '-active');
|
||||
L.DomUtil.addClass(this._states[i].icon, newState.stateName + '-active');
|
||||
}
|
||||
|
||||
// update classes for animations
|
||||
L.DomUtil.removeClass(this.button, this._currentState.stateName + '-active');
|
||||
L.DomUtil.addClass(this.button, newState.stateName + '-active');
|
||||
|
||||
// update the record
|
||||
this._currentState = newState;
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
enable: function(){
|
||||
L.DomUtil.addClass(this.button, 'enabled');
|
||||
L.DomUtil.removeClass(this.button, 'disabled');
|
||||
this.button.setAttribute('aria-hidden', 'false');
|
||||
return this;
|
||||
},
|
||||
|
||||
disable: function(){
|
||||
L.DomUtil.addClass(this.button, 'disabled');
|
||||
L.DomUtil.removeClass(this.button, 'enabled');
|
||||
this.button.setAttribute('aria-hidden', 'true');
|
||||
return this;
|
||||
},
|
||||
|
||||
onAdd: function(map){
|
||||
var bar = L.easyBar([this], {
|
||||
position: this.options.position,
|
||||
leafletClasses: this.options.leafletClasses
|
||||
});
|
||||
this._anonymousBar = bar;
|
||||
this._container = bar.container;
|
||||
return this._anonymousBar.container;
|
||||
},
|
||||
|
||||
removeFrom: function (map) {
|
||||
if (this._map === map)
|
||||
this.remove();
|
||||
return this;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
L.easyButton = function(/* args will pass automatically */){
|
||||
var args = Array.prototype.concat.apply([L.Control.EasyButton],arguments);
|
||||
return new (Function.prototype.bind.apply(L.Control.EasyButton, args));
|
||||
};
|
||||
|
||||
/*************************
|
||||
*
|
||||
* util functions
|
||||
*
|
||||
*************************/
|
||||
|
||||
// constructor for states so only curated
|
||||
// states end up getting called
|
||||
function State(template, easyButton){
|
||||
|
||||
this.title = template.title;
|
||||
this.stateName = template.stateName ? template.stateName : 'unnamed-state';
|
||||
|
||||
// build the wrapper
|
||||
this.icon = L.DomUtil.create('span', '');
|
||||
|
||||
L.DomUtil.addClass(this.icon, 'button-state state-' + this.stateName.replace(/(^\s*|\s*$)/g,''));
|
||||
this.icon.innerHTML = buildIcon(template.icon);
|
||||
this.onClick = L.Util.bind(template.onClick?template.onClick:function(){}, easyButton);
|
||||
}
|
||||
|
||||
function buildIcon(ambiguousIconString) {
|
||||
|
||||
var tmpIcon;
|
||||
|
||||
// does this look like html? (i.e. not a class)
|
||||
if( ambiguousIconString.match(/[&;=<>"']/) ){
|
||||
|
||||
// if so, the user should have put in html
|
||||
// so move forward as such
|
||||
tmpIcon = ambiguousIconString;
|
||||
|
||||
// then it wasn't html, so
|
||||
// it's a class list, figure out what kind
|
||||
} else {
|
||||
ambiguousIconString = ambiguousIconString.replace(/(^\s*|\s*$)/g,'');
|
||||
tmpIcon = L.DomUtil.create('span', '');
|
||||
|
||||
if( ambiguousIconString.indexOf('fa-') === 0 ){
|
||||
L.DomUtil.addClass(tmpIcon, 'fa ' + ambiguousIconString)
|
||||
} else if ( ambiguousIconString.indexOf('glyphicon-') === 0 ) {
|
||||
L.DomUtil.addClass(tmpIcon, 'glyphicon ' + ambiguousIconString)
|
||||
} else {
|
||||
L.DomUtil.addClass(tmpIcon, /*rollwithit*/ ambiguousIconString)
|
||||
}
|
||||
|
||||
// make this a string so that it's easy to set innerHTML below
|
||||
tmpIcon = tmpIcon.outerHTML;
|
||||
}
|
||||
|
||||
return tmpIcon;
|
||||
}
|
||||
|
||||
})();
|
|
@ -54,9 +54,55 @@ var carIcon = L.icon({
|
|||
iconAnchor: [27,12] // Revisit this
|
||||
});
|
||||
|
||||
// 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 car_bad_age = 5.0;
|
||||
var payload_bad_age = 30.0;
|
||||
// calculates look angles between two points
|
||||
// format of a and b should be {lon: 0, lat: 0, alt: 0}
|
||||
// returns {elevention: 0, azimut: 0, bearing: "", range: 0}
|
||||
//
|
||||
// based on earthmath.py
|
||||
// Copyright 2012 (C) Daniel Richman; GNU GPL 3
|
||||
|
||||
var DEG_TO_RAD = Math.PI / 180.0;
|
||||
var EARTH_RADIUS = 6371000.0;
|
||||
|
||||
function calculate_lookangles(a, b) {
|
||||
// degrees to radii
|
||||
a.lat = a.lat * DEG_TO_RAD;
|
||||
a.lon = a.lon * DEG_TO_RAD;
|
||||
b.lat = b.lat * DEG_TO_RAD;
|
||||
b.lon = b.lon * DEG_TO_RAD;
|
||||
|
||||
var d_lon = b.lon - a.lon;
|
||||
var sa = Math.cos(b.lat) * Math.sin(d_lon);
|
||||
var sb = (Math.cos(a.lat) * Math.sin(b.lat)) - (Math.sin(a.lat) * Math.cos(b.lat) * Math.cos(d_lon));
|
||||
var bearing = Math.atan2(sa, sb);
|
||||
var aa = Math.sqrt(Math.pow(sa, 2) + Math.pow(sb, 2));
|
||||
var ab = (Math.sin(a.lat) * Math.sin(b.lat)) + (Math.cos(a.lat) * Math.cos(b.lat) * Math.cos(d_lon));
|
||||
var angle_at_centre = Math.atan2(aa, ab);
|
||||
var great_circle_distance = angle_at_centre * EARTH_RADIUS;
|
||||
|
||||
ta = EARTH_RADIUS + a.alt;
|
||||
tb = EARTH_RADIUS + b.alt;
|
||||
ea = (Math.cos(angle_at_centre) * tb) - ta;
|
||||
eb = Math.sin(angle_at_centre) * tb;
|
||||
var elevation = Math.atan2(ea, eb) / DEG_TO_RAD;
|
||||
|
||||
// Use Math.coMath.sine rule to find unknown side.
|
||||
var distance = Math.sqrt(Math.pow(ta, 2) + Math.pow(tb, 2) - 2 * tb * ta * Math.cos(angle_at_centre));
|
||||
|
||||
// Give a bearing in range 0 <= b < 2pi
|
||||
bearing += (bearing < 0) ? 2 * Math.PI : 0;
|
||||
bearing /= DEG_TO_RAD;
|
||||
|
||||
var value = Math.round(bearing % 90);
|
||||
value = ((bearing > 90 && bearing < 180) || (bearing > 270 && bearing < 360)) ? 90 - value : value;
|
||||
|
||||
var str_bearing = "" + ((bearing < 90 || bearing > 270) ? 'N' : 'S')+ " " + value + '° ' + ((bearing < 180) ? 'E' : 'W');
|
||||
|
||||
return {
|
||||
'elevation': elevation,
|
||||
'azimuth': bearing,
|
||||
'range': distance,
|
||||
'bearing': str_bearing
|
||||
};
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
<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/easy-button.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">
|
||||
|
@ -20,10 +21,12 @@
|
|||
<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/easy-button.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>
|
||||
<script src="{{ url_for('static', filename='js/config.js') }}"></script>
|
||||
|
||||
<!-- Leaflet plugins... -->
|
||||
<script src="{{ url_for('static', filename='js/leaflet.rotatedMarker.js') }}"></script>
|
||||
|
@ -43,16 +46,18 @@
|
|||
// Default prediction settings (actual values will be used once the flight is underway)
|
||||
pred_desc_rate: 6.0,
|
||||
pred_burst: 28000,
|
||||
pred_model: 'Disabled',
|
||||
show_abort: true, // Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*)
|
||||
};
|
||||
|
||||
// Object which will contain balloon markers and traces.
|
||||
// properties for each key in this object (key = sonde ID)
|
||||
// properties for each key in this object (key = callsign)
|
||||
// latest_data - latest sonde telemetry object from SondeDecoded
|
||||
// marker: Leaflet marker object.
|
||||
// path: Leaflet polyline object.
|
||||
// pred_marker: Leaflet marker for predicted landing position.
|
||||
// pred_path: Leaflet polyline object for predicted path.
|
||||
// burst_marker: Leaflet marker for burst prediction.
|
||||
// abort_marker: Leaflet marker for abort landing prediction.
|
||||
// abort_path: Leaflet marker for abort prediction track.
|
||||
var balloon_positions = {};
|
||||
|
@ -93,6 +98,10 @@
|
|||
async: false, // Yes, this is deprecated...
|
||||
success: function(data) {
|
||||
chase_config = data;
|
||||
// Update a few fields based on this data.
|
||||
$("#predictorModel").html("<b>Current Model: </b>" + chase_config.pred_model);
|
||||
$('#burstAlt').val(chase_config.pred_burst.toFixed(0));
|
||||
$('#descentRate').val(chase_config.pred_desc_rate.toFixed(1));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -163,7 +172,7 @@
|
|||
// 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>",
|
||||
content : "<div class='dataAgeHeader'>Data Age</div><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 :
|
||||
|
@ -190,6 +199,20 @@
|
|||
})
|
||||
.addTo(map);
|
||||
|
||||
// Follow buttons - these just set the radio buttons on the settings pane.
|
||||
L.easyButton('fa-paper-plane', function(btn, map){
|
||||
$('input:radio[name=autoFollow]').val(['payload']);
|
||||
}, 'Follow Payload', 'followPayloadButton', {
|
||||
position: 'topright'
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
L.easyButton('fa-car', function(btn, map){
|
||||
$('input:radio[name=autoFollow]').val(['car']);
|
||||
}, 'Follow Chase Car', 'followCarButton', {
|
||||
position: 'topright'
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
|
||||
function mapMovedEvent(e){
|
||||
|
@ -262,6 +285,15 @@
|
|||
balloon_positions[callsign].pred_marker = null;
|
||||
}
|
||||
|
||||
// Burst position marker
|
||||
// Only add if there is data to show
|
||||
if (data.burst.length == 3){
|
||||
balloon_positions[callsign].burst_marker = L.marker(data.burst,{title:callsign + " Burst", icon: burstIcon})
|
||||
.bindTooltip(callsign + " Burst",{permanent:false,direction:'right'})
|
||||
.addTo(map);
|
||||
} else{
|
||||
balloon_positions[callsign].burst_marker = null;
|
||||
}
|
||||
|
||||
// Abort path
|
||||
balloon_positions[callsign].abort_path = L.polyline(data.abort_path,{title:callsign + " Abort Prediction", color:'red', opacity:prediction_opacity});
|
||||
|
@ -306,6 +338,46 @@
|
|||
});
|
||||
|
||||
|
||||
function updateSummaryDisplay(){
|
||||
// Update the 'Payload Summary' display.
|
||||
var _summary_update = {};
|
||||
// See if there is any payload data.
|
||||
if (balloon_positions.hasOwnProperty(balloon_currently_following) == true){
|
||||
// There is balloon data!
|
||||
var _latest_telem = balloon_positions[balloon_currently_following].latest_data;
|
||||
|
||||
_summary_update.alt = _latest_telem.position[2].toFixed(0) + "m";
|
||||
var _speed = _latest_telem.speed*3.6;
|
||||
_summary_update.speed = _speed.toFixed(0) + " kph";
|
||||
_summary_update.vel_v = _latest_telem.vel_v.toFixed(1) + " m/s";
|
||||
|
||||
|
||||
if (chase_car_position.latest_data.length == 3){
|
||||
// We have a chase car position! Calculate relative position.
|
||||
var _bal = {lat:_latest_telem.position[0], lon:_latest_telem.position[1], alt:_latest_telem.position[2]};
|
||||
var _car = {lat:chase_car_position.latest_data[0], lon:chase_car_position.latest_data[1], alt:chase_car_position.latest_data[2]};
|
||||
|
||||
var _look_angles = calculate_lookangles(_car, _bal);
|
||||
|
||||
_summary_update.elevation = _look_angles.elevation.toFixed(0) + "°";
|
||||
_summary_update.azimuth = _look_angles.azimuth.toFixed(0) + "°";
|
||||
_summary_update.range = (_look_angles.range/1000).toFixed(1) + "km";
|
||||
}else{
|
||||
// No Chase car position data - insert dummy values
|
||||
_summary_update.azimuth = "---°";
|
||||
_summary_update.elevation = "--°";
|
||||
_summary_update.range = "----m";
|
||||
}
|
||||
|
||||
}else{
|
||||
// No balloon data!
|
||||
_summary_update = {alt:'-----m', speed:'---kph', vel_v:'-.-m/s', azimuth:'---°', elevation:'--°', range:'----m'}
|
||||
}
|
||||
// Update table
|
||||
$("#summary_table").tabulator("setData", [_summary_update]);
|
||||
}
|
||||
|
||||
|
||||
// Telemetry event handler.
|
||||
// We will get one of these with every new balloon position
|
||||
socket.on('telemetry_event', function(data) {
|
||||
|
@ -394,25 +466,6 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
@ -427,9 +480,79 @@
|
|||
// Don't pan to anything.
|
||||
}
|
||||
|
||||
// Updat the summary display.
|
||||
updateSummaryDisplay();
|
||||
|
||||
});
|
||||
|
||||
// Predictor Functions
|
||||
socket.on('predictor_model_update', function(data){
|
||||
var _model_data = data.model;
|
||||
$("#predictorModel").html("<b>Current Model: </b>" + _model_data);
|
||||
});
|
||||
|
||||
socket.on('predictor_update', function(data){
|
||||
// We expect the fields: callsign, pred_path, pred_landing, and abort_path and abort_landing, if abort predictions are enabled.
|
||||
var _callsign = data.callsign;
|
||||
var _pred_path = data.pred_path;
|
||||
var _pred_landing = data.pred_landing;
|
||||
|
||||
// Add the landing marker if it doesnt exist.
|
||||
if (balloon_positions[_callsign].pred_marker == null){
|
||||
balloon_positions[callsign].pred_marker = L.marker(data.pred_landing,{title:callsign + " Landing", icon: balloonLandingIcons[balloon_positions[callsign].colour]})
|
||||
.bindTooltip(callsign + " Landing",{permanent:false,direction:'right'})
|
||||
.addTo(map);
|
||||
}else{
|
||||
balloon_positions[callsign].pred_marker.setLatLng(data.pred_landing);
|
||||
}
|
||||
if(data.burst.length == 3){
|
||||
// There is burst data!
|
||||
if (balloon_positions[_callsign].burst_marker == null){
|
||||
var _burst_txt = callsign + "Burst (" + data.burst[2].toFixed(0) + "m)";
|
||||
balloon_positions[callsign].burst_marker = L.marker(data.burst,{title:_burst_txt, icon: burstIcon})
|
||||
.bindTooltip(_burst_txt,{permanent:false,direction:'right'})
|
||||
.addTo(map);
|
||||
}else{
|
||||
balloon_positions[callsign].burst_marker.setLatLng(data.burst);
|
||||
}
|
||||
}else{
|
||||
// No burst data, or we are in descent.
|
||||
if (balloon_positions[_callsign].burst_marker != null){
|
||||
// Remove the burst icon from the map.
|
||||
balloon_positions[_callsign].burst_marker.remove();
|
||||
}
|
||||
}
|
||||
// Update the predicted path.
|
||||
balloon_positions[_callsign].pred_path.setLatLngs(data.pred_path);
|
||||
|
||||
if (data.hasOwnProperty("abort_landing") == true){
|
||||
// Only update the abort data if there is actually abort data to show.
|
||||
if(data.abort_landing.length == 3){
|
||||
if (balloon_positions[_callsign].abort_marker == null){
|
||||
balloon_positions[callsign].abort_marker = L.marker(data.abort_landing,{title:callsign + " Abort", icon: abortIcon})
|
||||
.bindTooltip(callsign + " Abort Landing",{permanent:false,direction:'right'});
|
||||
if(chase_config.show_abort == true){
|
||||
balloon_positions[callsign].abort_marker.addTo(map);
|
||||
}
|
||||
}else{
|
||||
balloon_positions[callsign].abort_marker.setLatLng(data.abort_landing);
|
||||
}
|
||||
|
||||
balloon_positions[_callsign].abort_path.setLatLngs(data.abort_path);
|
||||
}
|
||||
}else{
|
||||
// Clear out the abort and abort marker data.
|
||||
balloon_positions[_callsign].abort_path.setLatLngs([]);
|
||||
|
||||
if (balloon_positions[_callsign].abort_marker != null){
|
||||
balloon_positions[callsign].abort_marker.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$("#downloadModel").click(function(){
|
||||
socket.emit('download_model', {data: 'plzkthx'});
|
||||
});
|
||||
|
||||
// Tell the server we are connected and ready for data.
|
||||
socket.on('connect', function() {
|
||||
|
@ -441,7 +564,6 @@
|
|||
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;
|
||||
|
||||
|
@ -470,10 +592,6 @@
|
|||
$("#car_age").addClass('dataAgeOK');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}, age_update_rate);
|
||||
|
||||
|
||||
|
@ -520,6 +638,12 @@
|
|||
|
||||
</hr>
|
||||
<h4>Predictor</h4>
|
||||
<div class="paramRow" id="predictorModel">
|
||||
<b>Current Model: </b> Predictor Disabled
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Download Model</b> <button type="button" class="paramSelector" id="downloadModel">Download</button>
|
||||
</div>
|
||||
<div class="paramRow">
|
||||
<b>Enable Predictions</b> <input type="checkbox" class="paramSelector" id="predictorEnabled">
|
||||
</div>
|
||||
|
|
Ładowanie…
Reference in New Issue