chasemapper/templates/index.html

717 wiersze
34 KiB
HTML

<!DOCTYPE HTML>
<html>
<head>
<title>Project Horus Chase Mapper</title>
<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/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">
<!-- I should probably feel bad for using so many libraries, but apparently this is the way thing are done :-/ -->
<script src="{{ url_for('static', filename='js/jquery-3.3.1.min.js')}}"></script>
<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/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>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<script type="text/javascript" charset="utf-8">
// Chase Mapper Configuration Parameters.
// These are dummy values which will be loaded on startup.
var chase_config = {
// Start location for the map (until either a chase car position, or balloon position is available.)
default_lat: -34.9,
default_lon: 138.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_desc_rate: 6.0,
pred_burst: 28000,
pred_update_rate: 15,
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 = 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 = {};
// The sonde we are currently following on the map
var balloon_currently_following = "none";
// Chase car position.
// properties will contain:
// 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', 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 = {};
var updateSettings;
$(document).ready(function() {
// Use the 'chasemapper' namespace for all of our traffic
namespace = '/chasemapper';
// Connect to the Socket.IO server.
// The connection URL has the following format:
// http[s]://<domain>:<port>[/<namespace>]
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
function serverSettingsUpdate(data){
// Accept a json blob of settings data from the client, and update our local store.
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));
$('#predUpdateRate').val(chase_config.pred_update_rate.toFixed(0));
$("#predictorEnabled").prop('checked', chase_config.pred_enabled);
$("#abortPredictionEnabled").prop('checked', chase_config.show_abort);
}
// Grab the System config on startup.
// Refer to config.py for the contents of the configuration blob.
$.ajax({
url: "/get_config",
dataType: 'json',
async: false, // Yes, this is deprecated...
success: function(data) {
serverSettingsUpdate(data);
}
});
// Handler for further settings updates.
socket.on('server_settings_update', function(data){
serverSettingsUpdate(data);
});
// Settings updates.
updateSettings = function(){
chase_config.pred_enabled = document.getElementById("predictorEnabled").checked;
chase_config.show_abort = document.getElementById("abortPredictionEnabled").checked;
// Attempt to parse the text field values.
var _burst_alt = parseFloat($('#burstAlt').val());
if (isNaN(_burst_alt) == false){
chase_config.pred_burst = _burst_alt;
}
var _desc_rate = parseFloat($('#descentRate').val());
if (isNaN(_desc_rate) == false){
chase_config.pred_desc_rate = _desc_rate
}
var _update_rate = parseInt($('#predUpdateRate').val());
if (isNaN(_update_rate) == false){
chase_config.pred_update_rate = _update_rate
}
socket.emit('client_settings_update', chase_config);
};
// Use the jquery on-changed call for text entry fields,
// so they only fire after they lose focus.
$("#burstAlt").change(function(){
updateSettings();
});
$("#descentRate").change(function(){
updateSettings();
});
$("#predUpdateRate").change(function(){
updateSettings();
});
// Event handler for Log data.
socket.on('log_event', function(msg) {
$('#log_data').append('<br>' + $('<div/>').text(msg.timestamp + ": " + msg.msg).html());
// Scroll to the bottom of the log table
$("#log_data").scrollTop($("#log_data")[0].scrollHeight);
});
// Sonde position Map
// Setup a basic Leaflet map
var map = L.map('map').setView([chase_config.default_lat, chase_config.default_lon], 8);
// Add OSM Map.
var osm_map = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Add ESRI Satellite Maps.
var esrimapLink =
'<a href="http://www.esri.com/">Esri</a>';
var esriwholink =
'i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community';
var esri_sat_map = L.tileLayer(
'http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{
attribution: '&copy; '+esrimapLink+', '+esriwholink,
maxZoom: 18,
});
map.addControl(new L.Control.Layers({'OSM':osm_map, 'ESRI Satellite':esri_sat_map}));
var sidebar = L.control.sidebar('sidebar').addTo(map);
// Add custom controls, which show various sets of 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);
// 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 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 :
{
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);
// 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){
// 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){
// Add a new balloon to the telemetry store.
// This function accepts a dictionary which conttains:
// telem: Latest telemetry dictionary, containing:
// callsign:
// position: [lat, lon, alt]
// vel_v
// path: Flight path so far.
// pred_path: Predicted flight path (can be empty)
// pred_landing: [lat, lon, alt] coordinate for predicted landing.
// abort_path: Abort prediction
// abort_landing: Abort prediction landing
var telem = data.telem;
var callsign = data.telem.callsign;
balloon_positions[callsign] = {
latest_data: telem,
age: 0,
colour: colour_values[colour_idx]
};
// Balloon Path
balloon_positions[callsign].path = L.polyline(data.path,{title:callsign + " Path", color:balloon_positions[callsign].colour}).addTo(map);
// Balloon position marker
balloon_positions[callsign].marker = L.marker(telem.position,{title:callsign, icon: balloonAscentIcons[balloon_positions[callsign].colour]})
.bindTooltip(callsign,{permanent:false,direction:'right'})
.addTo(map);
// Set the balloon icon to a parachute if it is descending.
if (telem.vel_v < 0){
balloon_positions[callsign].marker.setIcon(balloonDescentIcons[balloon_positions[callsign].colour]);
}
// If we have 'landed' (this is a bit of a guess), set the payload icon.
if (telem.position[2] < parachute_min_alt){
balloon_positions[callsign].marker.setIcon(balloonPayloadIcons[balloon_positions[callsign].colour]);
}
// If the balloon is in descent, or is above the burst altitude, clear out the abort path and marker
// so they don't get shown.
if (telem.position[2] > chase_config.pred_burst || telem.vel_v < 0.0){
balloon_positions[callsign].abort_path = [];
balloon_positions[callsign].abort_landing = [];
}
// Add predicted landing path
balloon_positions[callsign].pred_path = L.polyline(data.pred_path,{title:callsign + " Prediction", color:balloon_positions[callsign].colour, opacity:prediction_opacity}).addTo(map);
// Landing position marker
// Only add if there is data to show
if (data.pred_landing.length == 3){
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 = 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});
if (chase_config.show_abort == true){
balloon_positions[callsign].abort_path.addTo(map);
}
// Abort position marker
if (data.abort_landing.length == 3){
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 = null;
}
colour_idx = (colour_idx+1)%colour_values.length;
}
// Grab the recent archive of telemetry data
// This should only ever be run once - on page load.
var initial_load_complete = false;
$.ajax({
url: "/get_telemetry_archive",
dataType: 'json',
async: true,
success: function(data) {
for (callsign in data){
add_new_balloon(data[callsign]);
}
// Update telemetry table
updateTelemetryTable();
initial_load_complete = true;
}
});
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) {
// Telemetry Event messages contain a dictionary of position data.
// It should have the fields:
// 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){
// If we have not completed our initial load of telemetry data, discard this data.
return
}
// Handle chase car position updates.
if (data.callsign == 'CAR'){
// Update car position.
chase_car_position.latest_data = data.position;
chase_car_position.heading = data.heading;
// TODO: Update car marker.
if (chase_car_position.marker == 'NONE'){
// 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.
}
car_data_age = 0.0;
}else{
// 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.burst = [];
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);
// 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();
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]);
}
}
// 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);
payload_data_age = 0.0;
}
}
// 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.
}
// 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.abort_landing.length == 3){
// Only update the abort data if there is actually abort data to show.
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() {
socket.emit('client_connected', {data: 'I\'m connected!'});
// This will cause the server to emit a few messages telling us to fetch data.
});
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="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" 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" onclick='updateSettings();'>
</div>
<div class="paramRow">
<b>Show 'Abort' Predictions</b> <input type="checkbox" class="paramSelector" id="abortPredictionEnabled" onclick='updateSettings();'>
</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 class="paramRow">
<b>Update Rate</b><input type="text" class="paramEntry" id="predUpdateRate"><br/>
</div>
</div>
</div>
</div>
<div id="map" class="sidebar-map"></div>
</body>
</html>