diff --git a/README.md b/README.md index ba20d06..e324d21 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ It will quite happily run alongside other Project Horus applications such as [ra You can often find me in the #highaltitude IRC Channel on [Freenode](https://webchat.freenode.net/). -### Help Wanted! -Currently Chasemapper is a bit mandrolic to set up, and this could be improved considerably, perhaps through the use of a docker container or similar. This isn't my area of expertise, so any assistance with this would be much appreciated! +## Update Notes +* If you have previously had chasemapper or auto_rx installed, you may need to update flask-socketio to the most recent version. You can do this by running `sudo pip3 install -U flask-socketio` +* As of mid-April, there have been additions to the chasemapper configuration file format to enable interfacing with the new [v2 Sondehub Tracker](https://v2.sondehub.org/), including a selection for the online tracker in use. You will need to update your configuration files for chasemapper to continue working. Integration with Sondehub v2 is still in progress. ## Dependencies @@ -32,8 +33,6 @@ You also need flask-socketio (>=5.0.0) and pytz, which can be installed using pi $ sudo pip3 install flask-socketio pytz ``` -**Note: If you have previously had chasemapper or auto_rx installed, you may need to update flask-socketio to the most recent version. You can do this by running `sudo pip3 install -U flask-socketio`** - You can then clone this repository with: ``` $ git clone https://github.com/projecthorus/chasemapper.git diff --git a/chasemapper/__init__.py b/chasemapper/__init__.py index 2320329..7df061c 100644 --- a/chasemapper/__init__.py +++ b/chasemapper/__init__.py @@ -8,4 +8,4 @@ # Now using Semantic Versioning (https://semver.org/) MAJOR.MINOR.PATCH -__version__ = "1.1.0" \ No newline at end of file +__version__ = "1.2.0" diff --git a/chasemapper/config.py b/chasemapper/config.py index 5fbb803..cd802ba 100644 --- a/chasemapper/config.py +++ b/chasemapper/config.py @@ -199,12 +199,15 @@ def parse_config_file(filename): _profile_section, "car_source_port" ) + _profile_online_tracker = config.get(_profile_section, "online_tracker") + chase_config["profiles"][_profile_name] = { "name": _profile_name, "telemetry_source_type": _profile_telem_source_type, "telemetry_source_port": _profile_telem_source_port, "car_source_type": _profile_car_source_type, "car_source_port": _profile_car_source_port, + "online_tracker": _profile_online_tracker, } if _default_profile == i: chase_config["selected_profile"] = _profile_name diff --git a/chasemapper/habitat.py b/chasemapper/habitat.py index 5a03f4d..3f69d70 100644 --- a/chasemapper/habitat.py +++ b/chasemapper/habitat.py @@ -205,6 +205,20 @@ class HabitatChaseUploader(object): """ Set the callsign """ self.callsign = call + def mark_payload_recovered(self, callsign, latitude, longitude, altitude, message): + """ Upload an indication that a payload (radiosonde or otherwise) has been recovered """ + + try: + initListenerCallsign(callsign, radio="", antenna=message) + uploadListenerPosition(callsign, latitude, longitude, altitude, chase=False) + except Exception as e: + logging.error( + "Habitat - Unable to mark payload as recovered - %s" % (str(e)) + ) + return + + logging.info("Habitat - Payload marked as recovered.") + def close(self): self.uploader_thread_running = False try: diff --git a/chasemapper/logread.py b/chasemapper/logread.py index c17bdd7..1cfa443 100644 --- a/chasemapper/logread.py +++ b/chasemapper/logread.py @@ -52,7 +52,7 @@ def read_last_balloon_telemetry(): if _entry["log_type"] == "BALLOON TELEMETRY": telemetry_found = True _last_telemetry = _entry - + if telemetry_found == True: _last_telemetry["time_dt"] = parse(_last_telemetry.pop("time")) return _last_telemetry diff --git a/chasemapper/sondehub.py b/chasemapper/sondehub.py new file mode 100644 index 0000000..a0a5957 --- /dev/null +++ b/chasemapper/sondehub.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# +# Project Horus - Browser-Based Chase Mapper +# Sondehub Communication (Chase car position upload) +# +# Copyright (C) 2021 Mark Jessop +# Released under GNU GPL v3 or later +# +import chasemapper +import datetime +import logging +import requests +import time +import traceback +import json +from base64 import b64encode +from hashlib import sha256 +from threading import Thread, Lock + +try: + # Python 2 + from Queue import Queue +except ImportError: + # Python 3 + from queue import Queue + + +class SondehubChaseUploader(object): + """ Upload supplied chase car positions to Sondehub on a regular basis """ + + SONDEHUB_STATION_POSITION_URL = "https://api.v2.sondehub.org/listeners" + + def __init__( + self, + update_rate=30, + callsign="N0CALL", + upload_enabled=True, + upload_timeout=10, + upload_retries=2, + ): + """ Initialise the Sondehub Chase uploader, and start the update thread """ + + self.update_rate = update_rate + self.callsign = callsign + self.callsign_init = False + self.upload_enabled = upload_enabled + self.upload_timeout = upload_timeout + self.upload_retries = upload_retries + + self.car_position = None + self.car_position_lock = Lock() + + self.uploader_thread_running = True + self.uploader_thread = Thread(target=self.upload_thread) + self.uploader_thread.start() + + logging.info("Sondehub - Chase-Car Position Uploader Started") + + def update_position(self, position): + """ Update the chase car position state + This function accepts and stores a copy of the same dictionary structure produced by both + Horus UDP broadcasts, and the serial GPS and GPSD modules + """ + + with self.car_position_lock: + self.car_position = position.copy() + + def upload_thread(self): + """ Uploader thread """ + while self.uploader_thread_running: + + # Grab a copy of the most recent car position. + with self.car_position_lock: + if self.car_position != None: + _position = self.car_position.copy() + else: + _position = None + + if self.upload_enabled and _position != None: + try: + + # Upload the listener position. + self.upload_position( + self.callsign, + _position["latitude"], + _position["longitude"], + _position["altitude"], + ) + except Exception as e: + logging.error( + "Sondehub - Error uploading chase-car position - %s" % str(e) + ) + + # Wait for next update. + _i = 0 + while (_i < self.update_rate) and self.uploader_thread_running: + time.sleep(1) + _i += 1 + + def set_update_rate(self, rate): + """ Set the update rate """ + self.update_rate = int(rate) + + def set_callsign(self, call): + """ Set the callsign """ + self.callsign = call + + def upload_position( + self, callsign, latitude, longitude, altitude, antenna="Chase Car", mobile=True + ): + """ Upload a chase car position to Sondehub + This uses the PUT /listeners API described here: + https://github.com/projecthorus/sondehub-infra/wiki/API-(Beta) + """ + + _position = { + "software_name": "ChaseMapper", + "software_version": chasemapper.__version__, + "uploader_callsign": callsign, + "uploader_position": [latitude, longitude, altitude], + "uploader_antenna": antenna, + "uploader_contact_email": "none@none.com", + "mobile": mobile, + } + + _retries = 0 + _upload_success = False + + _start_time = time.time() + + while _retries < self.upload_retries: + # Run the request. + try: + headers = { + "User-Agent": "chasemapper-" + chasemapper.__version__, + "Content-Type": "application/json", + } + _req = requests.put( + self.SONDEHUB_STATION_POSITION_URL, + json=_position, + # TODO: Revisit this second timeout value. + timeout=(self.upload_timeout, 6.1), + headers=headers, + ) + except Exception as e: + logging.error("Sondehub - Upload Failed: %s" % str(e)) + return + + if _req.status_code == 200: + # 200 is the only status code that we accept. + _upload_time = time.time() - _start_time + logging.debug("Sondehub - Uploaded chase-car position to Sondehub.") + _upload_success = True + break + + elif _req.status_code == 500: + # Server Error, Retry. + _retries += 1 + continue + + else: + logging.error( + "Sondehub - Error uploading chase-car position to Sondehub. Status Code: %d %s." + % (_req.status_code, _req.text) + ) + break + + if not _upload_success: + logging.error( + "Sondehub - Chase-car position upload failed after %d retries" + % (_retries) + ) + logging.debug(f"Attempted to upload {json.dumps(_position)}") + + def mark_payload_recovered(self, callsign, latitude, longitude, altitude, message): + """ Upload an indication that a payload (radiosonde or otherwise) has been recovered """ + # TODO + pass + + def close(self): + self.uploader_thread_running = False + try: + self.uploader_thread.join() + except: + pass + logging.info("Sondehub - Chase-Car Position Uploader Closed") diff --git a/horusmapper.cfg.example b/horusmapper.cfg.example index fcdcf27..019754b 100644 --- a/horusmapper.cfg.example +++ b/horusmapper.cfg.example @@ -37,11 +37,11 @@ car_source_type = gpsd # Car position source port (UDP) - only used if horus_udp is selected, but still needs to be provided. car_source_port = 12345 -# Car Position Upload System -# Where to upload chase-car positions to +# Online Tracker System +# Where to upload chase-car positions and balloon recovery notifications to. # sondehub = Sondehub v2 Database, for viewing on the SondeHub tracker (https://tracker.sondehub.org) # habitat = Habitat Database, for viewing on the HabHub tracker (https://tracker.habhub.org) -car_upload = sondehub +online_tracker = sondehub # Other profiles can be defined in sections like the following: [profile_2] @@ -58,11 +58,11 @@ car_source_type = serial # Make sure to update the gps_serial settings further down in the configuration file! car_source_port = 55672 -# Car Upload System -# Where to upload chase-car positions to +# Online Tracker System +# Where to upload chase-car positions and balloon recovery notifications to. # sondehub = Sondehub v2 Database, for viewing on the SondeHub tracker (https://tracker.sondehub.org) # habitat = Habitat Database, for viewing on the HabHub tracker (https://tracker.habhub.org) -car_upload = habitat +online_tracker = habitat # If you want add more profiles, you can do so here, e.g. # [profile_3] @@ -165,8 +165,8 @@ tile_server_path = /home/pi/Maps/ # -# Habitat Chase-Car Position Upload -# If you want, this application can upload your chase-car position to the Habhub tracker, +# Habitat / SondeHub Chase-Car Position Upload +# If you want, this application can upload your chase-car position to the Habhub / Sondehub trackers, # for those follwing along at home. # The settings below can be modified from the web interface, but they will default to what is set below on startup. # diff --git a/horusmapper.py b/horusmapper.py index 8c10586..7a7be69 100644 --- a/horusmapper.py +++ b/horusmapper.py @@ -6,6 +6,7 @@ # Released under GNU GPL v3 or later # import sys + # Version check. if sys.version_info < (3, 6): print("CRITICAL - chasemapper requires Python 3.6 or newer!") @@ -37,6 +38,7 @@ from chasemapper.habitat import ( initListenerCallsign, uploadListenerPosition, ) +from chasemapper.sondehub import SondehubChaseUploader from chasemapper.logger import ChaseLogger from chasemapper.logread import read_last_balloon_telemetry from chasemapper.bearings import Bearings @@ -84,8 +86,8 @@ car_track = GenericTrack() # Bearing store bearing_store = None -# Habitat Chase-Car uploader object -habitat_uploader = None +# Habitat/Sondehub Chase-Car uploader object +online_uploader = None # Copy out any extra fields from incoming telemetry that we want to pass on to the GUI. # At the moment we're really only using the burst timer field. @@ -146,7 +148,7 @@ def flask_emit_event(event_name="none", data={}): @socketio.on("client_settings_update", namespace="/chasemapper") def client_settings_update(data): - global chasemapper_config, habitat_uploader + global chasemapper_config, online_uploader _predictor_change = "none" if (chasemapper_config["pred_enabled"] == False) and (data["pred_enabled"] == True): @@ -184,20 +186,33 @@ def client_settings_update(data): # Start or Stop the Habitat Chase-Car Uploader. if _habitat_change == "start": - if habitat_uploader == None: - habitat_uploader = HabitatChaseUploader( - update_rate=chasemapper_config["habitat_update_rate"], - callsign=chasemapper_config["habitat_call"], - ) + if online_uploader == None: + _tracker = chasemapper_config["profiles"][ + chasemapper_config["selected_profile"] + ]["online_tracker"] + if _tracker == "habitat": + online_uploader = HabitatChaseUploader( + update_rate=chasemapper_config["habitat_update_rate"], + callsign=chasemapper_config["habitat_call"], + ) + elif _tracker == "sondehub": + online_uploader = SondehubChaseUploader( + update_rate=chasemapper_config["habitat_update_rate"], + callsign=chasemapper_config["habitat_call"], + ) + else: + logging.error( + "Unknown Online Tracker %s, not starting uploader." % _tracker + ) elif _habitat_change == "stop": - habitat_uploader.close() - habitat_uploader = None + online_uploader.close() + online_uploader = None # Update the habitat uploader with a new update rate, if one has changed. - if habitat_uploader != None: - habitat_uploader.set_update_rate(chasemapper_config["habitat_update_rate"]) - habitat_uploader.set_callsign(chasemapper_config["habitat_call"]) + if online_uploader != None: + online_uploader.set_update_rate(chasemapper_config["habitat_update_rate"]) + online_uploader.set_callsign(chasemapper_config["habitat_call"]) # Push settings back out to all clients. flask_emit_event("server_settings_update", chasemapper_config) @@ -690,24 +705,22 @@ def clear_bearing_data(data): @socketio.on("mark_recovered", namespace="/chasemapper") def mark_payload_recovered(data): """ Mark a payload as recovered, by uploading a station position """ + global online_uploader _callsign = data["recovery_title"] _lat = data["last_pos"][0] _lon = data["last_pos"][1] _alt = data["last_pos"][2] - _msg = data["message"] - _timestamp = "Recovered at " + datetime.utcnow().strftime("%Y-%m-%d %H:%MZ") + _msg = ( + data["message"] + + " Recovered at " + + datetime.utcnow().strftime("%Y-%m-%d %H:%MZ") + ) - try: - initListenerCallsign(_callsign, radio=_msg, antenna=_timestamp) - uploadListenerPosition(_callsign, _lat, _lon, _alt, chase=False) - except Exception as e: - logging.error( - "Unable to mark %s as recovered - %s" % (data["payload_call"], str(e)) - ) - return - - logging.info("Payload %s marked as recovered." % data["payload_call"]) + if online_uploader != None: + online_uploader.mark_payload_recovered(_callsign, _lat, _lon, _alt, _msg) + else: + logging.error("No Online Tracker enabled, could not mark payload as recovered.") # Incoming telemetry handlers @@ -788,7 +801,7 @@ def udp_listener_car_callback(data): """ Handle car position data """ # TODO: Make a generic car position function, and have this function pass data into it # so we can add support for other chase car position inputs. - global car_track, habitat_uploader, bearing_store + global car_track, online_uploader, bearing_store _lat = float(data["latitude"]) _lon = float(data["longitude"]) @@ -833,9 +846,9 @@ def udp_listener_car_callback(data): }, ) - # Update the Habitat Uploader, if one exists. - if habitat_uploader != None: - habitat_uploader.update_position(data) + # Update the Online Position Uploader, if one exists. + if online_uploader != None: + online_uploader.update_position(data) # Update the bearing store with the current car state (position & bearing) if bearing_store != None: @@ -904,8 +917,12 @@ def start_listeners(profile): 'telemetry_source_port' (int): Data source port 'car_source_type' (str): Car Position source type (none, horus_udp, gpsd, or station) 'car_source_port' (int): Car Position source port + 'online_tracker' (str): Which online tracker to upload chase-car info to ('habitat' or 'sondehub') """ - global data_listeners + global data_listeners, current_profile, online_uploader, chasemapper_config + + current_profile = profile + # Stop any existing listeners. for _thread in data_listeners: try: @@ -913,9 +930,32 @@ def start_listeners(profile): except Exception as e: logging.error("Error closing thread - %s" % str(e)) + # Shut-down any online uploaders + if online_uploader != None: + online_uploader.close() + online_uploader = None + # Reset the listeners array. data_listeners = [] + # Start up a new online uploader immediately if uploading is already enabled. + if chasemapper_config["habitat_upload_enabled"] == True: + if profile["online_tracker"] == "habitat": + online_uploader = HabitatChaseUploader( + update_rate=chasemapper_config["habitat_update_rate"], + callsign=chasemapper_config["habitat_call"], + ) + elif profile["online_tracker"] == "sondehub": + online_uploader = SondehubChaseUploader( + update_rate=chasemapper_config["habitat_update_rate"], + callsign=chasemapper_config["habitat_call"], + ) + else: + logging.error( + "Unknown Online Tracker %s, not starting uploader" + % (profile["online_tracker"]) + ) + # Start up a OziMux listener, if we are using one. if profile["telemetry_source_type"] == "ozimux": logging.info( @@ -1023,6 +1063,7 @@ def profile_change(data): # Update all clients with the new profile selection flask_emit_event("server_settings_update", chasemapper_config) + @socketio.on("device_position", namespace="/chasemapper") def device_position_update(data): """ Accept a device position update from a client and process it as if it was a chase car position """ @@ -1030,8 +1071,6 @@ def device_position_update(data): udp_listener_car_callback(data) except: pass - - class WebHandler(logging.Handler): @@ -1113,7 +1152,7 @@ if __name__ == "__main__": sys.exit(1) # Add in Chasemapper version information. - chasemapper_config['version'] = CHASEMAPPER_VERSION + chasemapper_config["version"] = CHASEMAPPER_VERSION # Copy out the predictor settings to another dictionary. pred_settings = { @@ -1147,13 +1186,6 @@ if __name__ == "__main__": if chasemapper_config["pred_enabled"]: initPredictor() - # Start up the Habitat Chase-Car Uploader, if enabled - if chasemapper_config["habitat_upload_enabled"]: - habitat_uploader = HabitatChaseUploader( - update_rate=chasemapper_config["habitat_update_rate"], - callsign=chasemapper_config["habitat_call"], - ) - # Read in last known position, if enabled if chasemapper_config["reload_last_position"]: @@ -1188,8 +1220,8 @@ if __name__ == "__main__": if chase_logger: chase_logger.close() - if habitat_uploader != None: - habitat_uploader.close() + if online_uploader != None: + online_uploader.close() # Attempt to close the running listeners. for _thread in data_listeners: diff --git a/static/js/habitat.js b/static/js/habitat.js index 01a41bf..5f3599d 100644 --- a/static/js/habitat.js +++ b/static/js/habitat.js @@ -1,7 +1,7 @@ // // Project Horus - Browser-Based Chase Mapper - Habitat Data Scraping // -// Copyright (C) 2019 Mark Jessop +// Copyright (C) 2021 Mark Jessop // Released under GNU GPL v3 or later // @@ -15,11 +15,6 @@ var spacenearus_last_position_id = 0; // Not really sure if this is necessary. var snear_request_running = false; -// Store for vehicle data. -var habitat_vehicles = {}; -// Only add chase cars which are (initially) within this range limit (km). -var habitat_vehicle_max_range = 200.0; - function process_habitat_vehicles(data){ // Check we have a 'valid' response to process. @@ -55,37 +50,37 @@ function process_habitat_vehicles(data){ // If the vehicle is already known to us, then update it position. // Update any existing entries (even if the range is above the threshold) - if (habitat_vehicles.hasOwnProperty(vcallsign)){ + if (chase_vehicles.hasOwnProperty(vcallsign)){ // Only update if the position ID of this position is newer than that last seen. - if (habitat_vehicles[vcallsign].position_id < position.position_id){ + if (chase_vehicles[vcallsign].position_id < position.position_id){ //console.log("Updating: " + vcallsign); // Update the position ID. - habitat_vehicles[vcallsign].position_id = position.position_id; + chase_vehicles[vcallsign].position_id = position.position_id; // Since we don't always get a heading with the vehicle position, calculate it. - var old_v_pos = {lat:habitat_vehicles[vcallsign].latest_data[0], - lon: habitat_vehicles[vcallsign].latest_data[1], - alt:habitat_vehicles[vcallsign].latest_data[2]}; + var old_v_pos = {lat:chase_vehicles[vcallsign].latest_data[0], + lon: chase_vehicles[vcallsign].latest_data[1], + alt:chase_vehicles[vcallsign].latest_data[2]}; var new_v_pos = {lat: v_lat, lon:v_lon, alt:v_alt}; - habitat_vehicles[vcallsign].heading = calculate_lookangles(old_v_pos, new_v_pos).azimuth; + chase_vehicles[vcallsign].heading = calculate_lookangles(old_v_pos, new_v_pos).azimuth; // Update the position data. - habitat_vehicles[vcallsign].latest_data = [v_lat, v_lon, v_alt]; + chase_vehicles[vcallsign].latest_data = [v_lat, v_lon, v_alt]; // Update the marker position. - habitat_vehicles[vcallsign].marker.setLatLng(habitat_vehicles[vcallsign].latest_data).update(); + chase_vehicles[vcallsign].marker.setLatLng(chase_vehicles[vcallsign].latest_data).update(); // Rotate/replace the icon to match the bearing. - var _car_heading = habitat_vehicles[vcallsign].heading - 90.0; + var _car_heading = chase_vehicles[vcallsign].heading - 90.0; if (_car_heading<=90.0){ - habitat_vehicles[vcallsign].marker.setIcon(habitat_car_icons[habitat_vehicles[vcallsign].colour]); - habitat_vehicles[vcallsign].marker.setRotationAngle(_car_heading); + chase_vehicles[vcallsign].marker.setIcon(habitat_car_icons[chase_vehicles[vcallsign].colour]); + chase_vehicles[vcallsign].marker.setRotationAngle(_car_heading); }else{ // We are travelling West - we need to use the flipped car icon. _car_heading = _car_heading - 180.0; - habitat_vehicles[vcallsign].marker.setIcon(habitat_car_icons_flipped[habitat_vehicles[vcallsign].colour]); - habitat_vehicles[vcallsign].marker.setRotationAngle(_car_heading); + chase_vehicles[vcallsign].marker.setIcon(habitat_car_icons_flipped[chase_vehicles[vcallsign].colour]); + chase_vehicles[vcallsign].marker.setRotationAngle(_car_heading); } return; } @@ -107,31 +102,31 @@ function process_habitat_vehicles(data){ var v_range = calculate_lookangles(my_pos, v_pos).range/1000.0; // If the range is less than the threshold, add it to our list of chase vehicles. - if(v_range < habitat_vehicle_max_range){ + if(v_range < vehicle_max_range){ //console.log("Adding: " + vcallsign); - habitat_vehicles[vcallsign] = {}; + chase_vehicles[vcallsign] = {}; // Initialise a few default values - habitat_vehicles[vcallsign].heading = 90; - habitat_vehicles[vcallsign].latest_data = [v_lat, v_lon, v_alt]; - habitat_vehicles[vcallsign].position_id = position.position_id; + chase_vehicles[vcallsign].heading = 90; + chase_vehicles[vcallsign].latest_data = [v_lat, v_lon, v_alt]; + chase_vehicles[vcallsign].position_id = position.position_id; // Get an index for the car icon. This is incremented for each vehicle, // giving each a different colour. - habitat_vehicles[vcallsign].colour = car_colour_values[car_colour_idx]; + chase_vehicles[vcallsign].colour = car_colour_values[car_colour_idx]; car_colour_idx = (car_colour_idx+1)%car_colour_values.length; // Create marker - habitat_vehicles[vcallsign].marker = L.marker(habitat_vehicles[vcallsign].latest_data, + chase_vehicles[vcallsign].marker = L.marker(chase_vehicles[vcallsign].latest_data, {title:vcallsign, - icon: habitat_car_icons[habitat_vehicles[vcallsign].colour], + icon: habitat_car_icons[chase_vehicles[vcallsign].colour], rotationOrigin: "center center"}) .addTo(map); // Keep our own record of if this marker has been added to a map, // as we shouldn't be using the private _map property of the marker object. - habitat_vehicles[vcallsign].onmap = true; + chase_vehicles[vcallsign].onmap = true; // Add tooltip, with custom CSS which removes all tooltip borders, and adds a text shadow. - habitat_vehicles[vcallsign].marker.bindTooltip(vcallsign, + chase_vehicles[vcallsign].marker.bindTooltip(vcallsign, {permanent: true, direction: 'center', offset:[0,25], @@ -150,6 +145,7 @@ function get_habitat_vehicles(){ if(!snear_request_running){ snear_request_running = true; + console.log("Requesting vehicles from Habitat...") $.ajax({ url: snear_request_url, dataType: 'json', @@ -166,19 +162,19 @@ function get_habitat_vehicles(){ // Show/Hide all vehicles. function show_habitat_vehicles(){ var state = document.getElementById("showOtherCars").checked; - for (_car in habitat_vehicles){ + for (_car in chase_vehicles){ // Add to map, if its not already on there. if(state){ - if(!habitat_vehicles[_car].onmap){ - habitat_vehicles[_car].marker.addTo(map); - habitat_vehicles[_car].onmap = true; + if(!chase_vehicles[_car].onmap){ + chase_vehicles[_car].marker.addTo(map); + chase_vehicles[_car].onmap = true; } } else{ - if(habitat_vehicles[_car].onmap){ - habitat_vehicles[_car].marker.remove(); - habitat_vehicles[_car].onmap = false; + if(chase_vehicles[_car].onmap){ + chase_vehicles[_car].marker.remove(); + chase_vehicles[_car].onmap = false; } } } -} \ No newline at end of file +} diff --git a/static/js/sondehub.js b/static/js/sondehub.js new file mode 100644 index 0000000..d258764 --- /dev/null +++ b/static/js/sondehub.js @@ -0,0 +1,143 @@ +// +// Project Horus - Browser-Based Chase Mapper - SondeHub Data Scraping +// +// Copyright (C) 2021 Mark Jessop +// Released under GNU GPL v3 or later +// + + +// URL to scrape recent vehicle position data from. +// TODO: Allow adjustment of the number of positions to request. +var sondehub_vehicle_url = "https://v2.api"; + +function process_sondehub_vehicles(data){ + // Check we have a 'valid' response to process. + if (data === null || + !data.positions || + !data.positions.position || + !data.positions.position.length) { + snear_request_running = false; + return; + } + + data.positions.position.forEach(function(position){ + // Update the highest position ID, so we don't request old data. + if (position.position_id > spacenearus_last_position_id){ + spacenearus_last_position_id = position.position_id; + } + + var vcallsign = position.vehicle; + + // Check this isn't our callsign. + // If it is, don't process it. + if (vcallsign.startsWith(chase_config.habitat_call)){ + return; + } + + // Determine if the vehicle is a chase car. + // This is denoted by _chase at the end of the callsign. + if(vcallsign.search(/(chase)/i) != -1) { + + var v_lat = parseFloat(position.gps_lat); + var v_lon = parseFloat(position.gps_lon); + var v_alt = parseFloat(position.gps_alt); + + // If the vehicle is already known to us, then update it position. + // Update any existing entries (even if the range is above the threshold) + if (chase_vehicles.hasOwnProperty(vcallsign)){ + + // Only update if the position ID of this position is newer than that last seen. + if (chase_vehicles[vcallsign].position_id < position.position_id){ + //console.log("Updating: " + vcallsign); + // Update the position ID. + chase_vehicles[vcallsign].position_id = position.position_id; + + // Since we don't always get a heading with the vehicle position, calculate it. + var old_v_pos = {lat:chase_vehicles[vcallsign].latest_data[0], + lon: chase_vehicles[vcallsign].latest_data[1], + alt:chase_vehicles[vcallsign].latest_data[2]}; + var new_v_pos = {lat: v_lat, lon:v_lon, alt:v_alt}; + chase_vehicles[vcallsign].heading = calculate_lookangles(old_v_pos, new_v_pos).azimuth; + + // Update the position data. + chase_vehicles[vcallsign].latest_data = [v_lat, v_lon, v_alt]; + + // Update the marker position. + chase_vehicles[vcallsign].marker.setLatLng(chase_vehicles[vcallsign].latest_data).update(); + + // Rotate/replace the icon to match the bearing. + var _car_heading = chase_vehicles[vcallsign].heading - 90.0; + if (_car_heading<=90.0){ + chase_vehicles[vcallsign].marker.setIcon(habitat_car_icons[chase_vehicles[vcallsign].colour]); + chase_vehicles[vcallsign].marker.setRotationAngle(_car_heading); + }else{ + // We are travelling West - we need to use the flipped car icon. + _car_heading = _car_heading - 180.0; + chase_vehicles[vcallsign].marker.setIcon(habitat_car_icons_flipped[chase_vehicles[vcallsign].colour]); + chase_vehicles[vcallsign].marker.setRotationAngle(_car_heading); + } + return; + } + + + // No need to go any further. + + return; + } + + // Otherwise, we need to decide if we're going to add it or not. + // Determine the vehicle distance from our current position. + var v_pos = {lat: v_lat, lon:v_lon, alt:v_alt}; + if (chase_car_position.marker === "NONE"){ + var my_pos = {lat:chase_config.default_lat, lon:chase_config.default_lon, alt:0}; + }else{ + var my_pos = {lat:chase_car_position.latest_data[0], lon:chase_car_position.latest_data[1], alt:chase_car_position.latest_data[2]}; + } + var v_range = calculate_lookangles(my_pos, v_pos).range/1000.0; + + // If the range is less than the threshold, add it to our list of chase vehicles. + if(v_range < vehicle_max_range){ + //console.log("Adding: " + vcallsign); + chase_vehicles[vcallsign] = {}; + // Initialise a few default values + chase_vehicles[vcallsign].heading = 90; + chase_vehicles[vcallsign].latest_data = [v_lat, v_lon, v_alt]; + chase_vehicles[vcallsign].position_id = position.position_id; + + // Get an index for the car icon. This is incremented for each vehicle, + // giving each a different colour. + chase_vehicles[vcallsign].colour = car_colour_values[car_colour_idx]; + car_colour_idx = (car_colour_idx+1)%car_colour_values.length; + + // Create marker + chase_vehicles[vcallsign].marker = L.marker(chase_vehicles[vcallsign].latest_data, + {title:vcallsign, + icon: habitat_car_icons[chase_vehicles[vcallsign].colour], + rotationOrigin: "center center"}) + .addTo(map); + // Keep our own record of if this marker has been added to a map, + // as we shouldn't be using the private _map property of the marker object. + chase_vehicles[vcallsign].onmap = true; + + // Add tooltip, with custom CSS which removes all tooltip borders, and adds a text shadow. + chase_vehicles[vcallsign].marker.bindTooltip(vcallsign, + {permanent: true, + direction: 'center', + offset:[0,25], + className:'custom_label'}).openTooltip(); + } + } + + }); + + snear_request_running = false; +} + + +function get_sondehub_vehicles(){ + // nothing here yet. + console.log("Requesting vehicles from Sondehub...") +} + + + diff --git a/static/js/tables.js b/static/js/tables.js index 3143654..b22c11f 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -63,7 +63,7 @@ function markPayloadRecovered(callsign){ "Submit": function() { $( this ).dialog( "close" ); _recovery_data.message = $('#customRecoveryMessage').val(); - _recovery_data.title = $('#customRecoveryTitle').val(); + _recovery_data.recovery_title = $('#customRecoveryTitle').val(); // If the user has requested to use the chase car position, override the last position with it. if(document.getElementById("recoveryCarPosition").checked == true){ diff --git a/templates/index.html b/templates/index.html index 34d30f0..100b4a0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -36,6 +36,7 @@ + @@ -65,6 +66,11 @@ // The sonde we are currently following on the map var balloon_currently_following = "none"; + // Other chase vehicles + var chase_vehicles = {}; + // Only add chase cars which are (initially) within this range limit (km). + var vehicle_max_range = 200.0; + // Chase car position. // properties will contain: // latest_data: [lat,lon, alt] (latest car position) @@ -614,11 +620,19 @@ }, age_update_rate); - // Habitat Chase Car Position Grabber + // Habitat/Sondehub Chase Car Position Grabber var habitat_update_rate = 20000; window.setInterval(function(){ if(document.getElementById("showOtherCars").checked){ - get_habitat_vehicles(); + if (chase_config.profiles[chase_config.selected_profile].online_tracker === "habitat"){ + get_habitat_vehicles(); + } + else if (chase_config.profiles[chase_config.selected_profile].online_tracker === "sondehub"){ + get_sondehub_vehicles(); + } + else{ + // Do nothing... + } } }, habitat_update_rate);