Initial minimum-viable-product. Payload and chase car showing on map.
58
README.md
|
@ -1,2 +1,56 @@
|
|||
# chasemapper
|
||||
Browser-Based High-Altitude Balloon Chase Map
|
||||
# Project Horus - Browser-Based HAB Chase Map
|
||||
|
||||
**Note: This is a work-in-progress. Not all of the features below are functional.**
|
||||
|
||||
This folder contains code to display payload (and chase car!) position data in a web browser.
|
||||
|
||||
For this to run, you will need the horuslib library installed. Refer to the [Installation guide](https://github.com/projecthorus/horus_utils/wiki/1.-Dependencies-&-Installation).
|
||||
|
||||
This is very much a work-in-progress, with much to be completed. For now, the following works:
|
||||
|
||||
To listen for payload data from OziMux (i.e. on UDP:localhost:8942):
|
||||
```
|
||||
$ python chasemapper.py --ozimux
|
||||
```
|
||||
|
||||
To listen for payload data via the UDP broadcast 'Payload Summary' messages (which can be generated by OziMux, but also by [radiosonde_auto_rx](https://github.com/projecthorus/radiosonde_auto_rx/)):
|
||||
```
|
||||
$ python chasemapper.py --summary
|
||||
```
|
||||
|
||||
The server can be stopped with CTRL+C.
|
||||
|
||||
|
||||
## Live Predictions
|
||||
kml_server can also run live predictions of the flight path.
|
||||
|
||||
To do this you need cusf_predictor_wrapper and it's dependencies installed. Refer to the [documentation on how to install this](https://github.com/darksidelemm/cusf_predictor_wrapper/).
|
||||
Once compiled and installed, you will need to:
|
||||
* Copy the 'pred' binary into this directory. If using the Windows build, this will be `pred.exe`; under Linux/OSX, just `pred`.
|
||||
* [Download wind data](https://github.com/darksidelemm/cusf_predictor_wrapper/#3-getting-wind-data) for your area of interest, and place the .dat files into the gfs subdirectory.
|
||||
|
||||
The following additional arguments can then be used:
|
||||
|
||||
```
|
||||
--predict Enable Flight Path Predictions.
|
||||
--predict_binary PREDICT_BINARY
|
||||
Location of the CUSF predictor binary. Defaut = ./pred
|
||||
--burst_alt BURST_ALT
|
||||
Expected Burst Altitude (m). Default = 30000
|
||||
--descent_rate DESCENT_RATE
|
||||
Expected Descent Rate (m/s, positive value). Default =
|
||||
5.0
|
||||
--abort Enable 'Abort' Predictions.
|
||||
--predict_rate PREDICT_RATE
|
||||
Run predictions every X seconds. Default = 15 seconds.
|
||||
```
|
||||
|
||||
For example, to use kml_server to observe a typical radiosonde launch (using data emitted via the [payload summary messages](https://github.com/projecthorus/radiosonde_auto_rx/wiki/Configuration-Settings#payload-summary-output)), you would run:
|
||||
```
|
||||
$ python chasemapper.py --summary --predict --burst_alt=26000 --descent_rate=7.0
|
||||
```
|
||||
|
||||
|
||||
A few notes:
|
||||
* The ascent rate is calculated automatically, and is an average of the last 6 positions.
|
||||
* The 'Abort' prediction option is used to display a second prediction, which displays what would occur if the balloon burst *now*. This is useful for flights where you have a cutdown payload available, and want to know when to trigger it! This prediction disappears when the payload is either above the expected burst altitude, or is descending.
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env python2.7
|
||||
#
|
||||
# Project Horus - Browser-Based Chase Mapper
|
||||
#
|
||||
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
|
||||
# Released under GNU GPL v3 or later
|
||||
#
|
||||
import json
|
||||
import flask
|
||||
from flask_socketio import SocketIO
|
||||
import time
|
||||
from datetime import datetime
|
||||
from dateutil.parser import parse
|
||||
from horuslib import *
|
||||
from horuslib.geometry import *
|
||||
from horuslib.listener import OziListener, UDPListener
|
||||
from horuslib.earthmaths import *
|
||||
|
||||
|
||||
# Define Flask Application, and allow automatic reloading of templates for dev work
|
||||
app = flask.Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'secret!'
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
app.jinja_env.auto_reload = True
|
||||
|
||||
# SocketIO instance
|
||||
socketio = SocketIO(app)
|
||||
|
||||
|
||||
|
||||
# Global stores of data.
|
||||
|
||||
chasemapper_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_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*)
|
||||
}
|
||||
|
||||
# Payload data Stores
|
||||
current_payloads = {} # Archive data which will be passed to the web client
|
||||
current_payload_tracks = {} # Store of payload Track objects which are used to calculate instantaneous parameters.
|
||||
|
||||
# Chase car position
|
||||
car_track = GenericTrack()
|
||||
|
||||
#
|
||||
# Flask Routes
|
||||
#
|
||||
|
||||
@app.route("/")
|
||||
def flask_index():
|
||||
""" Render main index page """
|
||||
return flask.render_template('index.html')
|
||||
|
||||
|
||||
@app.route("/get_telemetry_archive")
|
||||
def flask_get_telemetry_archive():
|
||||
return json.dumps(current_payloads)
|
||||
|
||||
|
||||
@app.route("/get_config")
|
||||
def flask_get_config():
|
||||
return json.dumps(chasemapper_config)
|
||||
|
||||
|
||||
|
||||
def flask_emit_event(event_name="none", data={}):
|
||||
""" Emit a socketio event to any clients. """
|
||||
socketio.emit(event_name, data, namespace='/chasemapper')
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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()
|
||||
|
||||
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},
|
||||
'path': [],
|
||||
'pred_path': [],
|
||||
'pred_landing': [],
|
||||
'abort_path': [],
|
||||
'abort_landing': []
|
||||
}
|
||||
|
||||
# Add new data into the payload's track, and get the latest ascent rate.
|
||||
current_payload_tracks[_callsign].add_telemetry({'time': _time_dt, 'lat':_lat, 'lon': _lon, 'alt':_alt, 'comment':_callsign})
|
||||
_state = current_payload_tracks[_callsign].get_latest_state()
|
||||
if _state != None:
|
||||
_vel_v = _state['ascent_rate']
|
||||
else:
|
||||
_vel_v = 0.0
|
||||
|
||||
# Now update the main telemetry store.
|
||||
current_payloads[_callsign]['telem'] = {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':_vel_v}
|
||||
current_payloads[_callsign]['path'].append([_lat, _lon, _alt])
|
||||
|
||||
# Update the web client.
|
||||
flask_emit_event('telemetry_event', current_payloads[_callsign]['telem'])
|
||||
|
||||
|
||||
|
||||
def udp_listener_car_callback(data):
|
||||
''' Handle car position data '''
|
||||
global car_track
|
||||
print("CAR:" + str(data))
|
||||
_lat = data['latitude']
|
||||
_lon = data['longitude']
|
||||
_alt = data['altitude']
|
||||
_comment = "CAR"
|
||||
_time_dt = datetime.utcnow()
|
||||
|
||||
_car_position_update = {
|
||||
'time' : _time_dt,
|
||||
'lat' : _lat,
|
||||
'lon' : _lon,
|
||||
'alt' : _alt,
|
||||
'comment': _comment
|
||||
}
|
||||
|
||||
car_track.add_telemetry(_car_position_update)
|
||||
|
||||
_state = car_track.get_latest_state()
|
||||
_heading = _state['heading']
|
||||
|
||||
# Push the new car position to the web client
|
||||
flask_emit_event('telemetry_event', {'callsign': 'CAR', 'position':[_lat,_lon,_alt], 'vel_v':0.0, 'heading': _heading})
|
||||
|
||||
|
||||
|
||||
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.")
|
||||
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.")
|
||||
parser.add_argument("--predict_binary", type=str, default="./pred", help="Location of the CUSF predictor binary. Defaut = ./pred")
|
||||
parser.add_argument("--burst_alt", type=float, default=30000.0, help="Expected Burst Altitude (m). Default = 30000")
|
||||
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.")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 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.")
|
||||
_broadcast_listener = UDPListener(summary_callback=udp_listener_summary_callback,
|
||||
gps_callback=udp_listener_car_callback)
|
||||
else:
|
||||
_broadcast_listener = UDPListener(summary_callback=None,
|
||||
gps_callback=udp_listener_car_callback)
|
||||
|
||||
_broadcast_listener.start()
|
||||
|
||||
# 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()
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html, body, #map {
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
}
|
|
@ -0,0 +1,636 @@
|
|||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg,
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-tile {
|
||||
will-change: opacity;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
-o-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
-o-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline: 0;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-container a.leaflet-active {
|
||||
outline: 2px solid orange;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path {
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution,
|
||||
.leaflet-container .leaflet-control-scale {
|
||||
font-size: 11px;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 19px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 18px 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 4px 4px 0 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||
color: #c3c3c3;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||
color: #999;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip-container {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-clickable {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
|
@ -0,0 +1,627 @@
|
|||
/* Tabulator v3.5.3 (c) Oliver Folkerd */
|
||||
.tabulator {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
-ms-transform: translatez(0);
|
||||
transform: translatez(0);
|
||||
}
|
||||
|
||||
.tabulator[tabulator-layout="fitDataFill"] .tabulator-tableHolder .tabulator-table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.tabulator.tabulator-block-select {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #999;
|
||||
background-color: #fff;
|
||||
color: #555;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-o-user-select: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #fff;
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-moving {
|
||||
position: absolute;
|
||||
border: 1px solid #999;
|
||||
background: #e6e6e6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-col-content {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-title-editor {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border: 1px solid #999;
|
||||
padding: 1px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-arrow {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: 8px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #bbb;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols {
|
||||
position: relative;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
border-top: 1px solid #ddd;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols .tabulator-col:last-child {
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col:first-child .tabulator-col-resize-handle.prev {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.ui-sortable-helper {
|
||||
position: absolute;
|
||||
background-color: #e6e6e6 !important;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-header-filter {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
margin-top: 2px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-header-filter textarea {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-header-filter svg {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input::-ms-clear {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable:hover {
|
||||
cursor: pointer;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="none"] .tabulator-col-content .tabulator-arrow {
|
||||
border-top: none;
|
||||
border-bottom: 6px solid #bbb;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="asc"] .tabulator-col-content .tabulator-arrow {
|
||||
border-top: none;
|
||||
border-bottom: 6px solid #666;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="desc"] .tabulator-col-content .tabulator-arrow {
|
||||
border-top: 6px solid #666;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-frozen {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
||||
border-right: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right {
|
||||
border-left: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-calcs-holder {
|
||||
box-sizing: border-box;
|
||||
min-width: 200%;
|
||||
background: #f2f2f2 !important;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #999;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row {
|
||||
background: #f2f2f2 !important;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-frozen-rows-holder {
|
||||
min-width: 200%;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-frozen-rows-holder:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder .tabulator-placeholder {
|
||||
box-sizing: border-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder .tabulator-placeholder[tabulator-render-mode="virtual"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder .tabulator-placeholder span {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder .tabulator-table {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder .tabulator-table .tabulator-row.tabulator-calcs {
|
||||
font-weight: bold;
|
||||
background: #f2f2f2 !important;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-top {
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableHolder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-bottom {
|
||||
border-top: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-col-resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-col-resize-handle.prev {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-col-resize-handle:hover {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer {
|
||||
padding: 5px 10px;
|
||||
border-top: 1px solid #999;
|
||||
background-color: #fff;
|
||||
text-align: right;
|
||||
color: #555;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-o-user-select: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-calcs-holder {
|
||||
box-sizing: border-box;
|
||||
width: calc(100% + 20px);
|
||||
margin: -5px -10px 5px -10px;
|
||||
text-align: left;
|
||||
background: #f2f2f2 !important;
|
||||
border-bottom: 1px solid #fff;
|
||||
border-top: 1px solid #ddd;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row {
|
||||
background: #f2f2f2 !important;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-calcs-holder:only-child {
|
||||
margin-bottom: -5px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-pages {
|
||||
margin: 0 7px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-page {
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #555;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-page.active {
|
||||
color: #d00;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-page:disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-page:not(.disabled):hover {
|
||||
cursor: pointer;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tabulator .tablulator-loader {
|
||||
position: absolute;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tabulator .tablulator-loader .tabulator-loader-msg {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tabulator .tablulator-loader .tabulator-loader-msg.tabulator-loading {
|
||||
border: 4px solid #333;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tabulator .tablulator-loader .tabulator-loader-msg.tabulator-error {
|
||||
border: 4px solid #D00;
|
||||
color: #590000;
|
||||
}
|
||||
|
||||
.tabulator-row {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
min-height: 22px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator-row:nth-child(even) {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-selectable:hover {
|
||||
background-color: #bbb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-selected {
|
||||
background-color: #9ABCEA;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-selected:hover {
|
||||
background-color: #769BCC;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-moving {
|
||||
position: absolute;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
pointer-events: none !important;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-row-resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-row-resize-handle.prev {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-row-resize-handle:hover {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-frozen {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
background-color: inherit;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-frozen.tabulator-frozen-left {
|
||||
border-right: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-frozen.tabulator-frozen-right {
|
||||
border-left: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-responsive-collapse {
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-responsive-collapse:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-responsive-collapse table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-responsive-collapse table tr td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-responsive-collapse table tr td:first-of-type {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 4px;
|
||||
border-right: 1px solid #ddd;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell:last-of-type {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-editing {
|
||||
border: 1px solid #1D68CD;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-editing input, .tabulator-row .tabulator-cell.tabulator-editing select {
|
||||
border: 1px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-validation-fail {
|
||||
border: 1px solid #dd0000;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-validation-fail input, .tabulator-row .tabulator-cell.tabulator-validation-fail select {
|
||||
border: 1px;
|
||||
background: transparent;
|
||||
color: #dd0000;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell:first-child .tabulator-col-resize-handle.prev {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-row-handle {
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-o-user-select: none;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box .tabulator-row-handle-bar {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin-top: 2px;
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle {
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-o-user-select: none;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
border-radius: 20px;
|
||||
background: #666;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-close {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-open {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle .tabulator-responsive-collapse-toggle-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group {
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #999;
|
||||
border-right: 1px solid #ddd;
|
||||
border-top: 1px solid #999;
|
||||
padding: 5px;
|
||||
padding-left: 10px;
|
||||
background: #fafafa;
|
||||
font-weight: bold;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group.tabulator-group-visible .tabulator-arrow {
|
||||
margin-right: 10px;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid #666;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group.tabulator-group-level-1 .tabulator-arrow {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group.tabulator-group-level-2 .tabulator-arrow {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group.tabulator-group-level-3 .tabulator-arrow {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group.tabulator-group-level-4 .tabulator-arrow {
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group.tabulator-group-level-5 .tabulator-arrow {
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group .tabulator-arrow {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-right: 16px;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
border-right: 0;
|
||||
border-left: 6px solid #666;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-group span {
|
||||
margin-left: 10px;
|
||||
color: #666;
|
||||
}
|
Po Szerokość: | Wysokość: | Rozmiar: 2.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 11 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 11 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 3.6 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 11 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 11 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 9.5 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 18 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 18 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 18 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 18 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 2.3 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 2.4 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 2.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 2.3 KiB |
|
@ -0,0 +1,57 @@
|
|||
(function() {
|
||||
// save these original methods before they are overwritten
|
||||
var proto_initIcon = L.Marker.prototype._initIcon;
|
||||
var proto_setPos = L.Marker.prototype._setPos;
|
||||
|
||||
var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
|
||||
|
||||
L.Marker.addInitHook(function () {
|
||||
var iconOptions = this.options.icon && this.options.icon.options;
|
||||
var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
|
||||
if (iconAnchor) {
|
||||
iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
|
||||
}
|
||||
this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom' ;
|
||||
this.options.rotationAngle = this.options.rotationAngle || 0;
|
||||
|
||||
// Ensure marker keeps rotated during dragging
|
||||
this.on('drag', function(e) { e.target._applyRotation(); });
|
||||
});
|
||||
|
||||
L.Marker.include({
|
||||
_initIcon: function() {
|
||||
proto_initIcon.call(this);
|
||||
},
|
||||
|
||||
_setPos: function (pos) {
|
||||
proto_setPos.call(this, pos);
|
||||
this._applyRotation();
|
||||
},
|
||||
|
||||
_applyRotation: function () {
|
||||
if(this.options.rotationAngle) {
|
||||
this._icon.style[L.DomUtil.TRANSFORM+'Origin'] = this.options.rotationOrigin;
|
||||
|
||||
if(oldIE) {
|
||||
// for IE 9, use the 2D rotation
|
||||
this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
|
||||
} else {
|
||||
// for modern browsers, prefer the 3D accelerated version
|
||||
this._icon.style[L.DomUtil.TRANSFORM] += ' rotateZ(' + this.options.rotationAngle + 'deg)';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setRotationAngle: function(angle) {
|
||||
this.options.rotationAngle = angle;
|
||||
this.update();
|
||||
return this;
|
||||
},
|
||||
|
||||
setRotationOrigin: function(origin) {
|
||||
this.options.rotationOrigin = origin;
|
||||
this.update();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,63 @@
|
|||
// 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 = {};
|
||||
var balloonLandingIcons = {};
|
||||
var balloonPayloadIcons = {};
|
||||
|
||||
// TODO: Make these /static URLS be filled in with templates (or does it not matter?)
|
||||
for (_col in colour_values){
|
||||
balloonAscentIcons[colour_values[_col]] = L.icon({
|
||||
iconUrl: "/static/img/balloon-" + colour_values[_col] + '.png',
|
||||
iconSize: [46, 85],
|
||||
iconAnchor: [23, 76]
|
||||
});
|
||||
balloonDescentIcons[colour_values[_col]] = L.icon({
|
||||
iconUrl: "/static/img/parachute-" + colour_values[_col] + '.png',
|
||||
iconSize: [46, 84],
|
||||
iconAnchor: [23, 76]
|
||||
});
|
||||
balloonLandingIcons[colour_values[_col]] = L.icon({
|
||||
iconUrl: "/static/img/target-" + colour_values[_col] + '.png',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
balloonPayloadIcons[colour_values[_col]] = L.icon({
|
||||
iconUrl: "/static/img/payload-" + colour_values[_col] + '.png',
|
||||
iconSize: [17, 18],
|
||||
iconAnchor: [8, 14]
|
||||
});
|
||||
}
|
||||
|
||||
// Burst Icon
|
||||
var burstIcon = L.icon({
|
||||
iconUrl: "/static/img/balloon-pop.png",
|
||||
iconSize: [20,20],
|
||||
iconAnchor: [10,10]
|
||||
});
|
||||
|
||||
var abortIcon = L.icon({
|
||||
iconUrl: "/static/img/target-red.png",
|
||||
iconSize: [20,20],
|
||||
iconAnchor: [10,10]
|
||||
});
|
||||
|
||||
var carIcon = L.icon({
|
||||
iconUrl: "/static/img/car-blue.png",
|
||||
iconSize: [55,25],
|
||||
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.
|
|
@ -0,0 +1,374 @@
|
|||
<!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/tabulator_simple.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/tabulator.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/utils.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_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*)
|
||||
};
|
||||
|
||||
// Object which will contain balloon markers and traces.
|
||||
// properties for each key in this object (key = sonde ID)
|
||||
// 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.
|
||||
// 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'};
|
||||
|
||||
// Other markers which may be added. (TBD, probably other chase car positions via the LoRa payload?)
|
||||
var misc_markers = {};
|
||||
|
||||
|
||||
$(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);
|
||||
|
||||
// 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) {
|
||||
chase_config = data;
|
||||
}
|
||||
});
|
||||
|
||||
// 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: '© <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: '© '+esrimapLink+', '+esriwholink,
|
||||
maxZoom: 18,
|
||||
});
|
||||
map.addControl(new L.Control.Layers({'OSM':osm_map, 'ESRI Satellite':esri_sat_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}
|
||||
]
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$("#telem_table").tabulator("setData", telem_data);
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
console.log(data);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 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
|
||||
// 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);
|
||||
} else {
|
||||
chase_car_position.marker.setLatLng(chase_car_position.latest_data).update();
|
||||
// TODO: Rotate chase car with direction of travel.
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 = [];
|
||||
|
||||
// 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
|
||||
//updateTelemetryText();
|
||||
//updateTelemetryTable();
|
||||
|
||||
// Are we currently following any other sondes?
|
||||
if (balloon_currently_following == "none"){
|
||||
// If not, follow this one!
|
||||
balloon_currently_following = data.callsign;
|
||||
}
|
||||
|
||||
// // 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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 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.
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
</body>
|
||||
</html>
|