diff --git a/chasemapper.py b/chasemapper.py index 82dd9fa..efacee9 100644 --- a/chasemapper.py +++ b/chasemapper.py @@ -13,6 +13,7 @@ from datetime import datetime from dateutil.parser import parse from horuslib import * from horuslib.geometry import * +from horuslib.atmosphere import time_to_landing from horuslib.listener import OziListener, UDPListener from horuslib.earthmaths import * @@ -51,6 +52,7 @@ current_payload_tracks = {} # Store of payload Track objects which are used to c # Chase car position car_track = GenericTrack() + # # Flask Routes # @@ -78,30 +80,22 @@ def flask_emit_event(event_name="none", data={}): -def udp_listener_summary_callback(data): - ''' Handle a Payload Summary Message from UDPListener ''' - global current_payloads, current_payload_tracks - # Extract the fields we need. - print("SUMMARY:" + str(data)) - _lat = data['latitude'] - _lon = data['longitude'] - _alt = data['altitude'] - _callsign = "Payload" # data['callsign'] # Quick hack to limit to a single balloon +def handle_new_payload_position(data): - # Process the 'short time' value if we have been provided it. - if 'time' in data.keys(): - _full_time = datetime.utcnow().strftime("%Y-%m-%dT") + data['time'] + "Z" - _time_dt = parse(_full_time) - else: - # Otherwise use the current UTC time. - _time_dt = datetime.utcnow() + _lat = data['lat'] + _lon = data['lon'] + _alt = data['alt'] + _time_dt = data['time_dt'] + _callsign = data['callsign'] + + _short_time = _time_dt.strftime("%H:%M:%S") if _callsign not in current_payloads: # New callsign! Create entries in data stores. current_payload_tracks[_callsign] = GenericTrack() current_payloads[_callsign] = { - 'telem': {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':0.0}, + 'telem': {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':0.0, 'speed':0.0, 'short_time':_short_time, 'time_to_landing':""}, 'path': [], 'pred_path': [], 'pred_landing': [], @@ -114,11 +108,43 @@ def udp_listener_summary_callback(data): _state = current_payload_tracks[_callsign].get_latest_state() if _state != None: _vel_v = _state['ascent_rate'] + _speed = _state['speed'] + # If this payload is in descent, calculate the time to landing. + + if _vel_v < 0.0: + # Try and get the altitude of the chase car - we use this as the expected 'ground' level. + _car_state = car_track.get_latest_state() + if _car_state != None: + _ground_asl = _car_state['alt'] + else: + _ground_asl = 0.0 + + # Calculate + _ttl = time_to_landing(_alt, _vel_v, ground_asl=_ground_asl) + if _ttl is None: + _ttl = "" + elif _ttl == 0: + _ttl = "LANDED" + else: + _min = _ttl // 60 + _sec = _ttl % 60 + _ttl = "%02d:%02d" % (_min,_sec) + else: + _ttl = "" + else: _vel_v = 0.0 + _ttl = "" # Now update the main telemetry store. - current_payloads[_callsign]['telem'] = {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':_vel_v} + current_payloads[_callsign]['telem'] = { + 'callsign': _callsign, + 'position':[_lat, _lon, _alt], + 'vel_v':_vel_v, + 'speed':_speed, + 'short_time':_short_time, + 'time_to_landing': _ttl} + current_payloads[_callsign]['path'].append([_lat, _lon, _alt]) # Update the web client. @@ -126,6 +152,33 @@ def udp_listener_summary_callback(data): + +def udp_listener_summary_callback(data): + ''' Handle a Payload Summary Message from UDPListener ''' + global current_payloads, current_payload_tracks + # Extract the fields we need. + print("SUMMARY:" + str(data)) + + # Convert to something generic we can pass onwards. + output = {} + output['lat'] = data['latitude'] + output['lon'] = data['longitude'] + output['alt'] = data['altitude'] + output['callsign'] = "Payload" # data['callsign'] # Quick hack to limit to a single balloon + + # Process the 'short time' value if we have been provided it. + if 'time' in data.keys(): + _full_time = datetime.utcnow().strftime("%Y-%m-%dT") + data['time'] + "Z" + output['time_dt'] = parse(_full_time) + else: + # Otherwise use the current UTC time. + output['time_dt'] = datetime.utcnow() + + handle_new_payload_position(output) + + + + def udp_listener_car_callback(data): ''' Handle car position data ''' global car_track diff --git a/static/css/chasemapper.css b/static/css/chasemapper.css index 58397d6..bb5bb68 100644 --- a/static/css/chasemapper.css +++ b/static/css/chasemapper.css @@ -5,4 +5,65 @@ body { html, body, #map { height: 100%; width: 100vw; +} + +.slimContainer { + position: relative; + margin: 20px auto; + width: 290px; +} +.slimContainer hr { + margin-bottom: 10px; +} +.slimContainer .row { + width: 280px; + display: block; + margin: 5px; + vertical-align: middle; + position: relative; +} +.slimContainer .row.info { + margin-top: 10px; +} +.slimContainer .row > span { + float: left; +} +.slimContainer .row.option > span { + width: 270px; +} +.slimContainer .row.option > span { + line-height: 30px; +} +.slimContainer .row > span.r { + float: right; +} + +.paramRow { + margin: 5px; +} + +.paramSelector { + position: absolute; + right: 10px; +} +.paramEntry { + position: absolute; + right: 10px; +} + +.timeToLanding { + color:red; + font-weight: bold; + font-size:5em; +} + +.dataAgeOK { + color:black; + font-weight: bold; + font-size:1em; +} +.dataAgeBad { + color:red; + font-weight: bold; + font-size:1.5em; } \ No newline at end of file diff --git a/static/css/font-awesome.min.css b/static/css/font-awesome.min.css new file mode 100644 index 0000000..3d920fc --- /dev/null +++ b/static/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.1.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-square:before,.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"} \ No newline at end of file diff --git a/static/css/images/layers-2x.png b/static/css/images/layers-2x.png new file mode 100644 index 0000000..200c333 Binary files /dev/null and b/static/css/images/layers-2x.png differ diff --git a/static/css/images/layers.png b/static/css/images/layers.png new file mode 100644 index 0000000..1a72e57 Binary files /dev/null and b/static/css/images/layers.png differ diff --git a/static/css/images/marker-icon-2x.png b/static/css/images/marker-icon-2x.png new file mode 100644 index 0000000..88f9e50 Binary files /dev/null and b/static/css/images/marker-icon-2x.png differ diff --git a/static/css/images/marker-icon.png b/static/css/images/marker-icon.png new file mode 100644 index 0000000..950edf2 Binary files /dev/null and b/static/css/images/marker-icon.png differ diff --git a/static/css/images/marker-shadow.png b/static/css/images/marker-shadow.png new file mode 100644 index 0000000..9fd2979 Binary files /dev/null and b/static/css/images/marker-shadow.png differ diff --git a/static/css/leaflet-control-topcenter.css b/static/css/leaflet-control-topcenter.css new file mode 100644 index 0000000..101ce03 --- /dev/null +++ b/static/css/leaflet-control-topcenter.css @@ -0,0 +1,27 @@ +/*********************************************** + leaflet-control-topcenter.scss, + + (c) 2016, FCOO + + https://github.com/FCOO/leaflet-control-topcenter + https://github.com/FCOO + + +************************************************/ +/* control positioning */ +.leaflet-center { + position: relative !important; + left: 0; + right: 0; + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; } + .leaflet-center .leaflet-control { + bottom: 0; } + +.leaflet-control-container .leaflet-control-bottomcenter { + position: absolute; + bottom: 0; + left: 0; + right: 0; } diff --git a/static/css/leaflet-sidebar.min.css b/static/css/leaflet-sidebar.min.css new file mode 100644 index 0000000..43ef2ab --- /dev/null +++ b/static/css/leaflet-sidebar.min.css @@ -0,0 +1 @@ +.sidebar{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;z-index:2000;box-shadow:0 1px 5px rgba(0,0,0,.65)}.sidebar.collapsed{width:40px}@media (min-width:768px) and (max-width:991px){.sidebar{width:305px}.sidebar-pane{min-width:265px}}@media (min-width:992px) and (max-width:1199px){.sidebar{width:390px}}@media (min-width:1200px){.sidebar{width:460px}}.sidebar-left{left:0}.sidebar-right{right:0}@media (min-width:768px){.sidebar{top:10px;bottom:10px;transition:width .5s}.sidebar-left{left:10px}.sidebar-right{right:10px}}.sidebar-tabs{top:0;bottom:0;height:100%;background-color:#fff}.sidebar-left .sidebar-tabs{left:0}.sidebar-right .sidebar-tabs{right:0}.sidebar-tabs,.sidebar-tabs>ul{position:absolute;width:40px;margin:0;padding:0;list-style-type:none}.sidebar-tabs>li,.sidebar-tabs>ul>li{width:100%;height:40px;color:#333;font-size:12pt;overflow:hidden;transition:all 80ms}.sidebar-tabs>li:hover,.sidebar-tabs>ul>li:hover{color:#000;background-color:#eee}.sidebar-tabs>li.active,.sidebar-tabs>ul>li.active{color:#fff;background-color:#0074d9}.sidebar-tabs>li.disabled,.sidebar-tabs>ul>li.disabled{color:rgba(51,51,51,.4)}.sidebar-tabs>li.disabled:hover,.sidebar-tabs>ul>li.disabled:hover{background:0 0}.sidebar-tabs>li.disabled>a,.sidebar-tabs>ul>li.disabled>a{cursor:default}.sidebar-tabs>li>a,.sidebar-tabs>ul>li>a{display:block;width:100%;height:100%;line-height:40px;color:inherit;text-decoration:none;text-align:center}.sidebar-tabs>ul+ul{bottom:0}.sidebar-content{position:absolute;top:0;bottom:0;background-color:rgba(255,255,255,.95);overflow-x:hidden;overflow-y:auto}.sidebar-left .sidebar-content{left:40px;right:0}.sidebar-right .sidebar-content{left:0;right:40px}.sidebar.collapsed>.sidebar-content{overflow-y:hidden}.sidebar-pane{display:none;left:0;right:0;box-sizing:border-box;padding:10px 20px}.sidebar-pane.active{display:block}.sidebar-header{margin:-10px -20px 0;height:40px;padding:0 20px;line-height:40px;font-size:14.4pt;color:#fff;background-color:#0074d9}.sidebar-right .sidebar-header{padding-left:40px}.sidebar-close{position:absolute;top:0;width:40px;height:40px;text-align:center;cursor:pointer}.sidebar-left .sidebar-close{right:0}.sidebar-right .sidebar-close{left:0}.sidebar-left~.sidebar-map{margin-left:40px}.sidebar-right~.sidebar-map{margin-right:40px}.sidebar.leaflet-touch{box-shadow:none;border-right:2px solid rgba(0,0,0,.2)}@media (min-width:768px) and (max-width:991px){.sidebar-left~.sidebar-map .leaflet-left{left:315px}.sidebar-right~.sidebar-map .leaflet-right{right:315px}}@media (min-width:992px) and (max-width:1199px){.sidebar-pane{min-width:350px}.sidebar-left~.sidebar-map .leaflet-left{left:400px}.sidebar-right~.sidebar-map .leaflet-right{right:400px}}@media (min-width:1200px){.sidebar-pane{min-width:420px}.sidebar-left~.sidebar-map .leaflet-left{left:470px}.sidebar-right~.sidebar-map .leaflet-right{right:470px}}@media (min-width:768px){.sidebar-left~.sidebar-map{margin-left:0}.sidebar-right~.sidebar-map{margin-right:0}.sidebar{border-radius:4px}.sidebar.leaflet-touch{border:2px solid rgba(0,0,0,.2)}.sidebar-left~.sidebar-map .leaflet-left{transition:left .5s}.sidebar-left.collapsed~.sidebar-map .leaflet-left{left:50px}.sidebar-right~.sidebar-map .leaflet-right{transition:right .5s}.sidebar-right.collapsed~.sidebar-map .leaflet-right{right:50px}} \ No newline at end of file diff --git a/static/fonts/fontawesome-webfont.woff b/static/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..9eaecb3 Binary files /dev/null and b/static/fonts/fontawesome-webfont.woff differ diff --git a/static/js/Leaflet.Control.Custom.js b/static/js/Leaflet.Control.Custom.js new file mode 100644 index 0000000..0f2589d --- /dev/null +++ b/static/js/Leaflet.Control.Custom.js @@ -0,0 +1,65 @@ +(function (window, document, undefined) { + L.Control.Custom = L.Control.extend({ + version: '1.0.1', + options: { + position: 'topright', + id: '', + title: '', + classes: '', + content: '', + style: {}, + datas: {}, + events: {}, + }, + container: null, + onAdd: function (map) { + this.container = L.DomUtil.create('div'); + this.container.id = this.options.id; + this.container.title = this.options.title; + this.container.className = this.options.classes; + this.container.innerHTML = this.options.content; + + for (var option in this.options.style) + { + this.container.style[option] = this.options.style[option]; + } + + for (var data in this.options.datas) + { + this.container.dataset[data] = this.options.datas[data]; + } + + + /* Prevent click events propagation to map */ + L.DomEvent.disableClickPropagation(this.container); + + /* Prevent right click event propagation to map */ + L.DomEvent.on(this.container, 'contextmenu', function (ev) + { + L.DomEvent.stopPropagation(ev); + }); + + /* Prevent scroll events propagation to map when cursor on the div */ + L.DomEvent.disableScrollPropagation(this.container); + + for (var event in this.options.events) + { + L.DomEvent.on(this.container, event, this.options.events[event], this.container); + } + + return this.container; + }, + + onRemove: function (map) { + for (var event in this.options.events) + { + L.DomEvent.off(this.container, event, this.options.events[event], this.container); + } + }, + }); + + L.control.custom = function (options) { + return new L.Control.Custom(options); + }; + +}(window, document)); \ No newline at end of file diff --git a/static/js/config.js b/static/js/config.js new file mode 100644 index 0000000..684f3df --- /dev/null +++ b/static/js/config.js @@ -0,0 +1,2 @@ +// Configuration Setting Handler. +// Mark Jessop 2018-07 diff --git a/static/js/leaflet-control-topcenter.js b/static/js/leaflet-control-topcenter.js new file mode 100644 index 0000000..8e63c61 --- /dev/null +++ b/static/js/leaflet-control-topcenter.js @@ -0,0 +1,33 @@ +/**************************************************************************** + leaflet-control-topcenter.js, + + (c) 2016, FCOO + + https://github.com/FCOO/leaflet-control-topcenter + https://github.com/FCOO + +****************************************************************************/ +(function (L /*, window, document, undefined*/) { + "use strict"; + + //Extend Map._initControlPos to also create a topcenter-container + L.Map.prototype._initControlPos = function ( _initControlPos ) { + return function () { + //Original function/method + _initControlPos.apply(this, arguments); + + //Adding new control-containers + + //topcenter is the same as the rest of control-containers + this._controlCorners['topcenter'] = L.DomUtil.create('div', 'leaflet-top leaflet-center', this._controlContainer); + + //bottomcenter need an extra container to be placed at the bottom + this._controlCorners['bottomcenter'] = + L.DomUtil.create( + 'div', + 'leaflet-bottom leaflet-center', + L.DomUtil.create('div', 'leaflet-control-bottomcenter', this._controlContainer) + ); + }; + } (L.Map.prototype._initControlPos); +}(L, this, document)); diff --git a/static/js/leaflet-sidebar.min.js b/static/js/leaflet-sidebar.min.js new file mode 100644 index 0000000..aadcde9 --- /dev/null +++ b/static/js/leaflet-sidebar.min.js @@ -0,0 +1 @@ +L.Control.Sidebar=L.Control.extend({includes:L.Evented.prototype||L.Mixin.Events,options:{position:"left"},initialize:function(t,s){var i,e;for(L.setOptions(this,s),this._sidebar=L.DomUtil.get(t),L.DomUtil.addClass(this._sidebar,"sidebar-"+this.options.position),L.Browser.touch&&L.DomUtil.addClass(this._sidebar,"leaflet-touch"),i=this._sidebar.children.length-1;i>=0;i--)"DIV"==(e=this._sidebar.children[i]).tagName&&L.DomUtil.hasClass(e,"sidebar-content")&&(this._container=e);for(this._tabitems=this._sidebar.querySelectorAll("ul.sidebar-tabs > li, .sidebar-tabs > ul > li"),i=this._tabitems.length-1;i>=0;i--)this._tabitems[i]._sidebar=this;for(this._panes=[],this._closeButtons=[],i=this._container.children.length-1;i>=0;i--)if("DIV"==(e=this._container.children[i]).tagName&&L.DomUtil.hasClass(e,"sidebar-pane")){this._panes.push(e);for(var o=e.querySelectorAll(".sidebar-close"),a=0,l=o.length;a=0;s--){var e=(i=this._tabitems[s]).querySelector("a");e.hasAttribute("href")&&"#"==e.getAttribute("href").slice(0,1)&&L.DomEvent.on(e,"click",L.DomEvent.preventDefault).on(e,"click",this._onClick,i)}for(s=this._closeButtons.length-1;s>=0;s--)i=this._closeButtons[s],L.DomEvent.on(i,"click",this._onCloseClick,this);return this},removeFrom:function(t){console.log("removeFrom() has been deprecated, please use remove() instead as support for this function will be ending soon."),this.remove(t)},remove:function(t){var s,i;for(this._map=null,s=this._tabitems.length-1;s>=0;s--)i=this._tabitems[s],L.DomEvent.off(i.querySelector("a"),"click",this._onClick);for(s=this._closeButtons.length-1;s>=0;s--)i=this._closeButtons[s],L.DomEvent.off(i,"click",this._onCloseClick,this);return this},open:function(t){var s,i;for(s=this._panes.length-1;s>=0;s--)(i=this._panes[s]).id==t?L.DomUtil.addClass(i,"active"):L.DomUtil.hasClass(i,"active")&&L.DomUtil.removeClass(i,"active");for(s=this._tabitems.length-1;s>=0;s--)(i=this._tabitems[s]).querySelector("a").hash=="#"+t?L.DomUtil.addClass(i,"active"):L.DomUtil.hasClass(i,"active")&&L.DomUtil.removeClass(i,"active");return this.fire("content",{id:t}),L.DomUtil.hasClass(this._sidebar,"collapsed")&&(this.fire("opening"),L.DomUtil.removeClass(this._sidebar,"collapsed")),this},close:function(){for(var t=this._tabitems.length-1;t>=0;t--){var s=this._tabitems[t];L.DomUtil.hasClass(s,"active")&&L.DomUtil.removeClass(s,"active")}return L.DomUtil.hasClass(this._sidebar,"collapsed")||(this.fire("closing"),L.DomUtil.addClass(this._sidebar,"collapsed")),this},_onClick:function(){L.DomUtil.hasClass(this,"active")?this._sidebar.close():L.DomUtil.hasClass(this,"disabled")||this._sidebar.open(this.querySelector("a").hash.slice(1))},_onCloseClick:function(){this.close()}}),L.control.sidebar=function(t,s){return new L.Control.Sidebar(t,s)}; \ No newline at end of file diff --git a/static/js/tables.js b/static/js/tables.js new file mode 100644 index 0000000..435a167 --- /dev/null +++ b/static/js/tables.js @@ -0,0 +1,62 @@ +// +// + + +// Initialise tables +function initTables(){ + // Telemetry data table + $("#telem_table").tabulator({ + layout:"fitData", + layoutColumnsOnNewData:true, + columns:[ //Define Table Columns + {title:"Callsign", field:"callsign", headerSort:false}, + {title:"Time (Z)", field:"short_time", headerSort:false}, + {title:"Latitude", field:"lat", headerSort:false}, + {title:"Longitude", field:"lon", headerSort:false}, + {title:"Alt (m)", field:"alt", headerSort:false}, + {title:"V_rate (m/s)", field:"vel_v", headerSort:false} + ] + }); + + $("#summary_table").tabulator({ + layout:"fitData", + layoutColumnsOnNewData:true, + columns:[ //Define Table Columns + {title:"Alt (m)", field:"alt", headerSort:false}, + {title:"Speed (kph)", field:"speed", headerSort:false}, + {title:"Asc Rate (m/s)", field:"vel_v", headerSort:false}, + {title:"Azimuth", field:"azimuth", headerSort:false}, + {title:"Elevation", field:"elevation", headerSort:false}, + {title:"Range", field:"range", headerSort:false}, + ], + data:[{alt:'-----m', speed:'---kph', vel_v:'-.-m/s', azimuth:'---°', elevation:'--°', range:'----m'}] + }); + + //var dummy_data = {alt:'-----m', speed:'---kph', vel_v:'-.-m/s', azimuth:'---°', elev:'--°', range:'----m'}; + //$("#summary_table").tabulator("setData", dummy_data); +} + +function updateTelemetryTable(){ + var telem_data = []; + + if (jQuery.isEmptyObject(balloon_positions)){ + telem_data = [{callsign:'None'}]; + }else{ + for (balloon_call in balloon_positions){ + var balloon_call_data = Object.assign({},balloon_positions[balloon_call].latest_data); + var balloon_call_age = balloon_positions[balloon_call].age; + //if ((Date.now()-balloon_call_age)>180000){ + // balloon_call_data.callsign = ""; + //} + // Modify some of the fields to fixed point values. + balloon_call_data.lat = balloon_call_data.position[0].toFixed(5); + balloon_call_data.lon = balloon_call_data.position[1].toFixed(5); + balloon_call_data.alt = balloon_call_data.position[2].toFixed(1); + balloon_call_data.vel_v = balloon_call_data.vel_v.toFixed(1); + + telem_data.push(balloon_call_data); + } + } + + $("#telem_table").tabulator("setData", telem_data); +} \ No newline at end of file diff --git a/static/js/utils.js b/static/js/utils.js index 0777c7e..7c3eaf9 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -1,14 +1,10 @@ // Utility Functions // Mark Jessop 2018-06-30 - - - // Color cycling for balloon traces and icons - Hopefully 4 colors should be enough for now! var colour_values = ['blue','green','purple']; var colour_idx = 0; - // Create a set of icons for the different colour values. var balloonAscentIcons = {}; var balloonDescentIcons = {}; @@ -60,4 +56,7 @@ var carIcon = L.icon({ // Other Global map settings var prediction_opacity = 0.6; -var parachute_min_alt = 300; // Show the balloon as a 'landed' payload below this altitude. \ No newline at end of file +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; \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 5a8d707..fcf9511 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,7 +5,10 @@ + + + @@ -14,8 +17,13 @@ + + + + + @@ -33,7 +41,6 @@ // Predictor settings pred_enabled: true, // Enable running and display of predicted flight paths. // Default prediction settings (actual values will be used once the flight is underway) - pred_asc_rate: 5.0, pred_desc_rate: 6.0, pred_burst: 28000, show_abort: true, // Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*) @@ -58,7 +65,12 @@ // latest_data: [lat,lon, alt] (latest car position) // heading: Car heading (to point icon appropriately.) // marker: Leaflet marker - var chase_car_position = {latest_data: [], heading:0, marker: 'NONE'}; + var chase_car_position = {latest_data: [], heading:0, marker: 'NONE', path: 'NONE'}; + + // Data Age variables - these are updated regularly to indicate + // if we haven't received data in a while. + var payload_data_age = 0.0; + var car_data_age = 0.0; // Other markers which may be added. (TBD, probably other chase car positions via the LoRa payload?) var misc_markers = {}; @@ -114,49 +126,80 @@ maxZoom: 18, }); map.addControl(new L.Control.Layers({'OSM':osm_map, 'ESRI Satellite':esri_sat_map})); + var sidebar = L.control.sidebar('sidebar').addTo(map); - - // Telemetry Data Table - // Using tabulator makes this *really* easy. - $("#telem_table").tabulator({ - height:180, - layout:"fitData", - layoutColumnsOnNewData:true, - columns:[ //Define Table Columns - //{title:"Source", field:"source", headerSort:false}, - {title:"Callsign", field:"callsign", headerSort:false}, - {title:"Timestamp", field:"datetime", headerSort:false}, - {title:"Latitude", field:"lat", headerSort:false}, - {title:"Longitude", field:"lon", headerSort:false}, - {title:"Altitude (m)", field:"alt", headerSort:false}, - {title:"Asc Rate (m/s)", field:"vel_v", headerSort:false} - ] - }); + // Add custom controls, which show various sets of data. - function updateTelemetryTable(){ - var telem_data = []; - - if (jQuery.isEmptyObject(balloon_positions)){ - telem_data = [{callsign:'None'}]; - }else{ - for (balloon_call in balloon_positions){ - var balloon_call_data = Object.assign({},balloon_positions[balloon_call].latest_data); - var balloon_call_age = balloon_positions[balloon_call].age; - //if ((Date.now()-balloon_call_age)>180000){ - // balloon_call_data.callsign = ""; - //} - // Modify some of the fields to fixed point values. - balloon_call_data.lat = balloon_call_data.lat.toFixed(5); - balloon_call_data.lon = balloon_call_data.lon.toFixed(5); - balloon_call_data.alt = balloon_call_data.alt.toFixed(1); - balloon_call_data.vel_v = balloon_call_data.vel_v.toFixed(1); - - telem_data.push(balloon_call_data); - } + // Telemetry table - shows all payload telemetry. + L.control.custom({ + position: 'bottomright', + content : "
", + classes : 'btn-group-vertical btn-group-sm', + id: 'telem_display', + style : + { + margin: '5px', + padding: '0px 0 0 0', + cursor: 'pointer', } + }) + .addTo(map); - $("#telem_table").tabulator("setData", telem_data); + // Summary display - a 'quick-look' of where the currently tracked payload is and what it's doing. + L.control.custom({ + position: 'topcenter', + content : "
", + 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 : "
", + 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 : "
", + classes : 'btn-group-vertical btn-group-sm', + id: 'ttl_display', + style : + { + margin: '5px', + padding: '0px 0 0 0', + cursor: 'pointer', + } + }) + .addTo(map); + + + + function mapMovedEvent(e){ + // The user has panned the map, stop following things. + $('input:radio[name=autoFollow]').val(['none']); } + map.on('dragend',mapMovedEvent); + + + initTables(); function add_new_balloon(data){ @@ -172,7 +215,6 @@ // abort_path: Abort prediction // abort_landing: Abort prediction landing - console.log(data); var telem = data.telem; var callsign = data.telem.callsign; @@ -240,7 +282,6 @@ } - colour_idx = (colour_idx+1)%colour_values.length; } @@ -259,7 +300,7 @@ add_new_balloon(data[callsign]); } // Update telemetry table - //updateTelemetryTable(); + updateTelemetryTable(); initial_load_complete = true; } }); @@ -273,6 +314,7 @@ // callsign: string // position: [lat, lon, alt] // vel_v: float + // time_to_landing: String // If callsign = 'CAR', the lat/lon/alt will be considered to be a car telemetry position. if(initial_load_complete == false){ @@ -290,69 +332,101 @@ // Create marker! chase_car_position.marker = L.marker(chase_car_position.latest_data,{title:"Chase Car", icon: carIcon}) .addTo(map); + chase_car_position.path = L.polyline([chase_car_position.latest_data],{title:"Chase Car", color:'black'}); + // If the user wants the chase car tail, add it to the map. + if (document.getElementById("chaseCarTrack").checked == true){ + chase_car_position.path.addTo(map); + } } else { chase_car_position.marker.setLatLng(chase_car_position.latest_data).update(); // TODO: Rotate chase car with direction of travel. } - return; - } + car_data_age = 0.0; + }else{ - // Otherwise, we have a balloon - // Have we seen this ballon before? - if (balloon_positions.hasOwnProperty(data.callsign) == false){ + // Otherwise, we have a balloon + // Have we seen this ballon before? + if (balloon_positions.hasOwnProperty(data.callsign) == false){ - // Convert the incoming data into a format suitable for adding into the telem store. - var temp_data = {}; - temp_data.telem = data; - temp_data.path = [data.position]; - temp_data.pred_path = []; - temp_data.pred_landing = []; - temp_data.abort_path = []; - temp_data.abort_landing = []; + // Convert the incoming data into a format suitable for adding into the telem store. + var temp_data = {}; + temp_data.telem = data; + temp_data.path = [data.position]; + temp_data.pred_path = []; + temp_data.pred_landing = []; + temp_data.abort_path = []; + temp_data.abort_landing = []; - // Add it to the telemetry store and create markers. - add_new_balloon(temp_data); + // Add it to the telemetry store and create markers. + add_new_balloon(temp_data); - // Update data age to indicate current time. - balloon_positions[data.callsign].age = Date.now(); + // Update data age to indicate current time. + balloon_positions[data.callsign].age = Date.now(); - } else { - // Yep - update the sonde_positions entry. - balloon_positions[data.callsign].latest_data = data; - balloon_positions[data.callsign].age = Date.now(); - balloon_positions[data.callsign].path.addLatLng(data.position); - balloon_positions[data.callsign].marker.setLatLng(data.position).update(); + } else { + // Yep - update the sonde_positions entry. + balloon_positions[data.callsign].latest_data = data; + balloon_positions[data.callsign].age = Date.now(); + balloon_positions[data.callsign].path.addLatLng(data.position); + balloon_positions[data.callsign].marker.setLatLng(data.position).update(); - if (data.vel_v < 0){ - balloon_positions[data.callsign].marker.setIcon(balloonDescentIcons[balloon_positions[data.callsign].colour]); - }else{ - balloon_positions[data.callsign].marker.setIcon(balloonAscentIcons[balloon_positions[data.callsign].colour]); + if (data.vel_v < 0){ + balloon_positions[data.callsign].marker.setIcon(balloonDescentIcons[balloon_positions[data.callsign].colour]); + }else{ + balloon_positions[data.callsign].marker.setIcon(balloonAscentIcons[balloon_positions[data.callsign].colour]); + } + + if (data.position[2] < parachute_min_alt){ + balloon_positions[data.callsign].marker.setIcon(balloonPayloadIcons[balloon_positions[callsign].colour]); + } } - if (data.position[2] < parachute_min_alt){ - balloon_positions[data.callsign].marker.setIcon(balloonPayloadIcons[balloon_positions[callsign].colour]); + // Update the telemetry table display + updateTelemetryTable(); + + // Are we currently following any other sondes? + if (balloon_currently_following == "none"){ + // If not, follow this one! + balloon_currently_following = data.callsign; + } + + // Update the Summary and time-to-landing displays + if (balloon_currently_following = data.callsign){ + $('#time_to_landing').text(data.time_to_landing); + + // Generate data to update summary display with. + var _summary_update = {}; + _summary_update.alt = data.position[2].toFixed(0) + "m"; + var _speed = data.speed*3.6; + _summary_update.speed = _speed.toFixed(0) + " kph"; + _summary_update.vel_v = data.vel_v.toFixed(1) + " m/s"; + _summary_update.azimuth = "---°"; + _summary_update.elevation = "--°"; + _summary_update.range = "----m"; + + // Calculate az/el/range from Car position to balloon. + if (chase_car_position.latest_data.length == 3){ + // Calculate relative position. + } else{ + // Leave the values as they are. + } + // Update the summary table + $("#summary_table").tabulator("setData", [_summary_update]); + + payload_data_age = 0.0; } } - // Update the telemetry table display - //updateTelemetryText(); - //updateTelemetryTable(); - - // Are we currently following any other sondes? - if (balloon_currently_following == "none"){ - // If not, follow this one! - balloon_currently_following = data.callsign; + // Auto Pan selection between balloon or car. + var _current_follow = $('input[name=autoFollow]:checked').val(); + if (_current_follow == 'payload' && data.callsign == balloon_currently_following){ + map.panTo(data.position); + } else if (_current_follow == 'car' && data.callsign == 'CAR'){ + map.panTo(data.position); + }else{ + // Don't pan to anything. } - // // Is sonde following enabled? - // if (document.getElementById("balloonAutoFollow").checked == true){ - // // If we are currently following this sonde, snap the map to it. - // if (data.callsign == balloon_currently_following){ - // map.panTo(data.position); - // } - // } - // TODO: Auto Pan selection between balloon or car. - map.panTo(data.position); }); @@ -364,11 +438,101 @@ }); + var age_update_rate = 500; + window.setInterval(function () { + // Update the data age displays. + + payload_data_age += age_update_rate/1000.0; + car_data_age += age_update_rate/1000.0; + + if (balloon_currently_following === 'none'){ + $("#payload_age").text("Payload: No Data."); + }else{ + $("#payload_age").text("Payload: " + payload_data_age.toFixed(1)+"s"); + if (payload_data_age > payload_bad_age){ + $("#payload_age").removeClass(); + $("#payload_age").addClass('dataAgeBad'); + }else{ + $("#payload_age").removeClass(); + $("#payload_age").addClass('dataAgeOK'); + } + } + + if(chase_car_position.latest_data.length == 0){ + $("#car_age").text("Car GPS: No Data."); + }else{ + $("#car_age").text("Car GPS: " + car_data_age.toFixed(1)+"s"); + if (car_data_age > car_bad_age){ + $("#car_age").removeClass(); + $("#car_age").addClass('dataAgeBad'); + }else{ + $("#car_age").removeClass(); + $("#car_age").addClass('dataAgeOK'); + } + } + + + + + }, age_update_rate); + }); -
+ + + +