diff --git a/htdocs/map.js b/htdocs/map.js index 08e8809..9dfb24d 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -12,6 +12,33 @@ ws.send("SERVER DE CLIENT client=map.js type=map"); }; + var map; + var markers = {}; + var updateQueue = []; + + var processUpdates = function(updates) { + if (!map) { + updateQueue = updateQueue.concat(updates); + return; + } + updates.forEach(function(update){ + // TODO maidenhead locator implementation + if (update.location.type != 'latlon') return; + var pos = new google.maps.LatLng(update.location.lat, update.location.lon) + if (markers[update.callsign]) { + console.info("updating"); + markers[update.callsign].setPosition(pos); + } else { + console.info("initializing"); + markers[update.callsign] = new google.maps.Marker({ + position: pos, + map: map, + title: update.callsign + }); + } + }); + } + ws.onmessage = function(e){ if (typeof e.data != 'string') { console.error("unsupported binary data on websocket; ignoring"); @@ -27,15 +54,19 @@ case "config": var config = json.value; $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ - var map = new google.maps.Map($('body')[0], { + map = new google.maps.Map($('body')[0], { center: { lat: config.receiver_gps[0], lng: config.receiver_gps[1] }, zoom: 8 }); + processUpdates(updateQueue); }) break + case "update": + processUpdates(json.value); + break } } catch (e) { // don't lose exception diff --git a/owrx/connection.py b/owrx/connection.py index 95fcf76..3286975 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -3,10 +3,12 @@ from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry from owrx.feature import FeatureDetector from owrx.version import openwebrx_version import json +from owrx.map import Map import logging logger = logging.getLogger(__name__) + class Client(object): def __init__(self, conn): self.conn = conn @@ -165,9 +167,17 @@ class MapConnection(Client): pm = PropertyManager.getSharedInstance() self.write_config(pm.collect("google_maps_api_key", "receiver_gps").__dict__()) + Map.getSharedInstance().addClient(self) + + def close(self): + Map.getSharedInstance().removeClient(self) + super().close() + def write_config(self, cfg): self.protected_send({"type":"config","value":cfg}) + def write_update(self, update): + self.protected_send({"type":"update","value":update}) class WebSocketMessageHandler(object): def __init__(self): diff --git a/owrx/map.py b/owrx/map.py new file mode 100644 index 0000000..a799c92 --- /dev/null +++ b/owrx/map.py @@ -0,0 +1,62 @@ +from datetime import datetime + + +class Location(object): + def __dict__(self): + return {} + + +class Map(object): + sharedInstance = None + @staticmethod + def getSharedInstance(): + if Map.sharedInstance is None: + Map.sharedInstance = Map() + return Map.sharedInstance + + def __init__(self): + self.clients = [] + self.positions = {} + super().__init__() + + def broadcast(self, update): + for c in self.clients: + c.write_update(update) + + def addClient(self, client): + self.clients.append(client) + client.write_update([{"callsign": callsign, "location": record["loc"].__dict__()} for (callsign, record) in self.positions.items()]) + + def removeClient(self, client): + try: + self.clients.remove(client) + except ValueError: + pass + + def updateLocation(self, callsign, loc: Location): + self.positions[callsign] = {"loc": loc, "updated": datetime.now()} + self.broadcast([{"callsign": callsign, "location": loc.__dict__()}]) + + +class LatLngLocation(Location): + def __init__(self, lat: float, lon: float): + self.lat = lat + self.lon = lon + + def __dict__(self): + return { + "type":"latlon", + "lat":self.lat, + "lon":self.lon + } + + +class LocatorLocation(Location): + def __init__(self, locator: str): + self.locator = locator + + def __dict__(self): + return { + "type":"locator", + "locator":self.locator + } diff --git a/owrx/meta.py b/owrx/meta.py index ec4966a..ad3f0d3 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -4,6 +4,7 @@ import json from datetime import datetime, timedelta import logging import threading +from owrx.map import Map, LatLngLocation logger = logging.getLogger(__name__) @@ -14,18 +15,22 @@ class DmrCache(object): if DmrCache.sharedInstance is None: DmrCache.sharedInstance = DmrCache() return DmrCache.sharedInstance + def __init__(self): self.cache = {} self.cacheTimeout = timedelta(seconds = 86400) + def isValid(self, key): if not key in self.cache: return False entry = self.cache[key] return entry["timestamp"] + self.cacheTimeout > datetime.now() + def put(self, key, value): self.cache[key] = { "timestamp": datetime.now(), "data": value } + def get(self, key): if not self.isValid(key): return None return self.cache[key]["data"] @@ -34,6 +39,7 @@ class DmrCache(object): class DmrMetaEnricher(object): def __init__(self): self.threads = {} + def downloadRadioIdData(self, id): cache = DmrCache.getSharedInstance() try: @@ -44,6 +50,7 @@ class DmrMetaEnricher(object): except json.JSONDecodeError: cache.put(id, None) del self.threads[id] + def enrich(self, meta): if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None if not "source" in meta: return None @@ -60,9 +67,18 @@ class DmrMetaEnricher(object): return None +class YsfMetaEnricher(object): + def enrich(self, meta): + if "source" in meta and "lat" in meta and "lon" in meta: + # TODO parsing the float values should probably happen earlier + Map.getSharedInstance().updateLocation(meta["source"], LatLngLocation(float(meta["lat"]), float(meta["lon"]))) + return None + + class MetaParser(object): enrichers = { - "DMR": DmrMetaEnricher() + "DMR": DmrMetaEnricher(), + "YSF": YsfMetaEnricher() } def __init__(self, handler):