From f7f72d50c89155c580ff70d0cd0d7cd7e55eba57 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Sat, 7 Sep 2024 18:07:45 -0400 Subject: [PATCH] Now showing latest FT8 and other calls with vectors on the map. --- htdocs/lib/GoogleMaps.js | 50 +++++++++++- htdocs/lib/Leaflet.js | 59 +++++++++++--- htdocs/lib/MapCalls.js | 106 ++++++++++++++++++++++++ htdocs/lib/MapLocators.js | 10 +-- htdocs/lib/MapManager.js | 26 +++++- htdocs/lib/MapMarkers.js | 2 +- htdocs/lib/Utils.js | 8 ++ htdocs/map-google.js | 15 +++- htdocs/map-leaflet.js | 15 +++- owrx/config/defaults.py | 2 + owrx/connection.py | 2 + owrx/controllers/assets.py | 2 + owrx/controllers/settings/general.py | 19 ++++- owrx/map.py | 116 ++++++++++++++++++--------- owrx/wsjt.py | 20 +++-- 15 files changed, 380 insertions(+), 72 deletions(-) create mode 100644 htdocs/lib/MapCalls.js diff --git a/htdocs/lib/GoogleMaps.js b/htdocs/lib/GoogleMaps.js index 92f10126..50fc5d16 100644 --- a/htdocs/lib/GoogleMaps.js +++ b/htdocs/lib/GoogleMaps.js @@ -125,8 +125,7 @@ GSimpleMarker.prototype.setMarkerOptions = function(options) { // function GLocator() { - this.rect = new google.maps.Rectangle(); - this.rect.setOptions({ + this.rect = new google.maps.Rectangle({ strokeWeight : 0, strokeColor : "#FFFFFF", fillColor : "#FFFFFF", @@ -136,7 +135,7 @@ function GLocator() { GLocator.prototype = new Locator(); -GLocator.prototype.setMap = function(map) { +GLocator.prototype.setMap = function(map = null) { this.rect.setMap(map); }; @@ -161,3 +160,48 @@ GLocator.prototype.setOpacity = function(opacity) { fillOpacity : LocatorManager.fillOpacity * opacity }); }; + +// +// GoogleMaps-Specific Call +// + +function GCall() { + const dash = { + path : 'M 0,-1 0,1', + scale : 2, + strokeWeight : 1, + strokeOpacity : 0.5 + }; + + this.line = new google.maps.Polyline({ + geodesic : true, + strokeColor : '#000000', + strokeOpacity : 0, + strokeWeight : 0, + icons : [{ icon: dash, offset: 0, repeat: '8px' }] + }); +} + +GCall.prototype = new Call(); + +GCall.prototype.setMap = function(map = null) { + this.line.setMap(map); +}; + +GCall.prototype.setEnds = function(lat1, lon1, lat2, lon2) { + this.line.setOptions({ path : [ + { lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 } + ]}); +}; + +GCall.prototype.setColor = function(color) { + this.line.icons[0].icon.strokeColor = color; + this.line.setOptions({ icons: this.line.icons }); +// this.line.setOptions({ strokeColor: color }); +}; + +GCall.prototype.setOpacity = function(opacity) { + this.line.icons[0].icon.strokeOpacity = opacity; + this.line.setOptions({ icons: this.line.icons }); +// this.line.setOptions({ strokeOpacity : opacity }); +}; diff --git a/htdocs/lib/Leaflet.js b/htdocs/lib/Leaflet.js index 3c637adc..2ae552e3 100644 --- a/htdocs/lib/Leaflet.js +++ b/htdocs/lib/Leaflet.js @@ -4,7 +4,7 @@ function LMarker () { this._marker = L.marker(); -}; +} LMarker.prototype.onAdd = function() { this.div = this.create(); @@ -24,9 +24,8 @@ LMarker.prototype.setMarkerOptions = function(options) { } }; -LMarker.prototype.setMap = function (map) { - if (map) this._marker.addTo(map); - else this._marker.remove(); +LMarker.prototype.setMap = function (map = null) { + if (map) this._marker.addTo(map); else this._marker.remove(); }; LMarker.prototype.addListener = function (e, f) { @@ -76,23 +75,26 @@ function LSimpleMarker() { $.extend(this, new LMarker(), new AprsMarker()); } // function LLocator() { - this._rect = L.rectangle([[0,0], [1,1]], { color: '#FFFFFF', weight: 0, fillOpacity: 1 }); + this._rect = L.rectangle([[0,0], [1,1]], { + color : '#FFFFFF', + weight : 0, + fillOpacity : 1 + }); } LLocator.prototype = new Locator(); -LLocator.prototype.setMap = function(map) { - if (map) this._rect.addTo(map); - else this._rect.remove(); +LLocator.prototype.setMap = function(map = null) { + if (map) this._rect.addTo(map); else this._rect.remove(); }; LLocator.prototype.setCenter = function(lat, lon) { this.center = [lat, lon]; this._rect.setBounds([[lat - 0.5, lon - 1], [lat + 0.5, lon + 1]]); -} +}; LLocator.prototype.setColor = function(color) { - this._rect.setStyle({ color }); + this._rect.setStyle({ color: color }); }; LLocator.prototype.setOpacity = function(opacity) { @@ -106,6 +108,39 @@ LLocator.prototype.addListener = function (e, f) { this._rect.on(e, f); }; +// +// Leaflet-Specific Call +// + +function LCall() { + this._line = L.polyline([[0, 0], [0, 0]], { + dashArray : [4, 4], + dashOffset : 0, + color : '#000000', + opacity : 0.5, + weight : 1 + }); +} + +LCall.prototype = new Call(); + +LCall.prototype.setMap = function(map = null) { + if (map) this._line.addTo(map); else this._line.remove(); +}; + +LCall.prototype.setEnds = function(lat1, lon1, lat2, lon2) { + this._line.setLatLngs([[lat1, lon1], [lat2, lon2]]); +}; + +LCall.prototype.setColor = function(color) { + this._line.setStyle({ color: color }); +}; + +LCall.prototype.setOpacity = function(opacity) { + this._line.setStyle({ opacity: opacity }); +}; + + // // Position object // @@ -119,5 +154,5 @@ function posObj(pos) { this._lng = pos[1]; } -posObj.prototype.lat = function () { return this._lat; } -posObj.prototype.lng = function () { return this._lng; } +posObj.prototype.lat = function () { return this._lat; }; +posObj.prototype.lng = function () { return this._lng; }; diff --git a/htdocs/lib/MapCalls.js b/htdocs/lib/MapCalls.js new file mode 100644 index 00000000..90ac567d --- /dev/null +++ b/htdocs/lib/MapCalls.js @@ -0,0 +1,106 @@ +// +// Map Calls Management +// + +CallManager.strokeOpacity = 0.5; + +function CallManager() { + // Current calls + this.calls = []; + this.colorMode = 'band'; + this.filterBy = null; +} + +CallManager.prototype.add = function(call) { + // Remove excessive calls + while (this.calls.length > 0 && this.calls.length >= max_calls) { + var old = this.calls.shift(); + old.setMap(); + } + + // Do not try adding if calls display disabled + if (max_calls <= 0) return false; + + // Add new call + call.reColor(this.colorMode, this.filterBy); + this.calls.push(call); + return true; +}; + +CallManager.prototype.ageAll = function() { + var now = new Date().getTime(); + var out = []; + this.calls.forEach((x) => { if (x.age(now)) out.push(x) }); + this.calls = out; +}; + +CallManager.prototype.clear = function() { + // Remove all calls from the map + this.calls.forEach((x) => { x.setMap(); }); + // Delete all calls + this.calls = []; +}; + +CallManager.prototype.setFilter = function(filterBy = null) { + this.filterBy = filterBy; + this.reColor(); +}; + +CallManager.prototype.setColorMode = function(colorMode) { + // Clearing filter when color mode is changed + this.colorMode = colorMode; + this.setFilter(); +}; + +CallManager.prototype.reColor = function() { + this.calls.forEach((x) => { x.reColor(this.colorMode, this.filterBy); }); +}; + +// +// Generic Map Call +// Derived classes have to implement: +// setMap(), setEnds(), setColor(), setOpacity() +// + +function Call() {} + +Call.prototype.create = function(data, map) { + // Update call information + this.caller = data.caller; + this.callee = data.callee; + this.src = data.src; + this.dst = data.dst; + this.band = data.band; + this.mode = data.mode; + this.lastseen = data.lastseen; + + // Make a call between two maidenhead squares + var src = Utils.loc2latlng(this.src.locator); + var dst = Utils.loc2latlng(this.dst.locator); + this.setEnds(src[0], src[1], dst[0], dst[1]); + + // Place on the map + this.setMap(map); + + // Age call + this.age(new Date().getTime()); +} + +Call.prototype.reColor = function(colorMode, filterBy = null) { + this.setOpacity( + colorMode==='off'? 0 + : filterBy==null? CallManager.strokeOpacity + : colorMode==='band' && this.band==filterBy? CallManager.strokeOpacity + : colorMode==='mode' && this.mode==filterBy? CallManager.strokeOpacity + : 0 + ); +}; + +Call.prototype.age = function(now) { + if (now - this.lastseen > call_retention_time) { + this.setMap(); + return false; + } + + return true; +}; diff --git a/htdocs/lib/MapLocators.js b/htdocs/lib/MapLocators.js index 14fe7427..28d754e8 100644 --- a/htdocs/lib/MapLocators.js +++ b/htdocs/lib/MapLocators.js @@ -13,6 +13,7 @@ function LocatorManager(spectral = true) { this.locators = {}; this.bands = {}; this.modes = {}; + this.calls = []; // The color scale used this.colorScale = chroma.scale(colors).mode('hsl'); @@ -64,7 +65,7 @@ LocatorManager.prototype.update = function(id, data, map) { } } - // Keep track modes + // Keep track of modes if (!(data.mode in this.modes)) { this.modes[data.mode] = '#000000'; this.assignColors(this.modes); @@ -161,6 +162,7 @@ LocatorManager.prototype.updateLegend = function() { LocatorManager.prototype.setColorMode = function(newColorMode) { $('#openwebrx-map-colormode').val(newColorMode); LS.save('mapColorMode', newColorMode); + // Clearing filter when color mode is changed this.colorMode = newColorMode; this.setFilter(); }; @@ -185,10 +187,8 @@ Locator.prototype.create = function(id) { this.colorMode = 'band'; // Center locator at its maidenhead id - this.setCenter( - (id.charCodeAt(1) - 65 - 9) * 10 + Number(id[3]) + 0.5, - (id.charCodeAt(0) - 65 - 9) * 20 + Number(id[2]) * 2 + 1.0 - ); + var center = Utils.loc2latlng(id); + this.setCenter(center[0], center[1]); } Locator.prototype.update = function(data, map) { diff --git a/htdocs/lib/MapManager.js b/htdocs/lib/MapManager.js index bc14d465..e0ee76fd 100644 --- a/htdocs/lib/MapManager.js +++ b/htdocs/lib/MapManager.js @@ -21,10 +21,14 @@ function MapManager() { // Locators management (FT8, FT4, WSPR, etc) this.lman = new LocatorManager(); + // Calls management (FT8, etc) + this.cman = new CallManager(); + // Fade out / remove positions after time setInterval(function() { self.lman.ageAll(); self.mman.ageAll(); + self.cman.ageAll(); }, 15000); // When stuff loads... @@ -39,14 +43,21 @@ function MapManager() { // Toggle color modes on click $('#openwebrx-map-colormode').on('change', function() { - self.lman.setColorMode($(this).val()); + var colorMode = $(this).val(); + self.lman.setColorMode(colorMode); + self.cman.setColorMode(colorMode); + LS.save('mapColorMode', colorMode); }); // Restore saved control settings if (LS.has('openwebrx-map-selectors')) self.toggleLegend(LS.loadBool('openwebrx-map-selectors')); - if (LS.has('mapColorMode')) - self.lman.setColorMode(LS.loadStr('mapColorMode')); + if (LS.has('mapColorMode')) { + var colorMode = LS.loadStr('mapColorMode'); + self.lman.setColorMode(colorMode); + self.cman.setColorMode(colorMode); + $('#openwebrx-map-colormode').val(colorMode); + } }); // Connect web socket @@ -98,6 +109,12 @@ MapManager.prototype.process = function(e) { if ('map_position_retention_time' in this.config) { retention_time = this.config.map_position_retention_time * 1000; } + if ('map_call_retention_time' in this.config) { + call_retention_time = this.config.map_call_retention_time * 1000; + } + if ('map_max_calls' in this.config) { + max_calls = this.config.map_max_calls; + } if ('callsign_url' in this.config) { Utils.setCallsignUrl(this.config.callsign_url); } @@ -140,6 +157,7 @@ MapManager.prototype.connect = function() { self.removeReceiver(); self.mman.clear(); self.lman.clear(); + self.cman.clear(); if (self.reconnect_timeout) { // Max value: roundabout 8 and a half minutes @@ -183,12 +201,14 @@ MapManager.prototype.setupLegendFilters = function($legend) { if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) { $lis.removeClass('disabled'); self.lman.setFilter(); + self.cman.setFilter(); } else { $el.removeClass('disabled'); $lis.filter(function() { return this != $el[0] }).addClass('disabled'); self.lman.setFilter($el.data('selector')); + self.cman.setFilter($el.data('selector')); } }); diff --git a/htdocs/lib/MapMarkers.js b/htdocs/lib/MapMarkers.js index 5dd07a19..f3908e6c 100644 --- a/htdocs/lib/MapMarkers.js +++ b/htdocs/lib/MapMarkers.js @@ -78,7 +78,7 @@ MarkerManager.prototype.toggle = function(map, type, onoff) { // Show or hide features on the map $.each(this.markers, function(_, x) { - if (x.mode === type) x.setMap(onoff ? map : undefined); + if (x.mode === type) x.setMap(onoff ? map : null); }); }; diff --git a/htdocs/lib/Utils.js b/htdocs/lib/Utils.js index 62a970bf..7b40605b 100644 --- a/htdocs/lib/Utils.js +++ b/htdocs/lib/Utils.js @@ -239,6 +239,14 @@ Utils.mmsiIsGround = function(mmsi) { return mmsi.substring(0, 2) === '00'; }; +// Convert Maidenhead locator ID to lat/lon pair. +Utils.loc2latlng = function(id) { + return [ + (id.charCodeAt(1) - 65 - 9) * 10 + Number(id[3]) + 0.5, + (id.charCodeAt(0) - 65 - 9) * 20 + Number(id[2]) * 2 + 1.0 + ]; +}; + // Save given canvas into a PNG file. Utils.saveCanvas = function(canvas) { // Get canvas by its ID diff --git a/htdocs/map-google.js b/htdocs/map-google.js index f6faa8c7..38c6cac0 100644 --- a/htdocs/map-google.js +++ b/htdocs/map-google.js @@ -1,5 +1,7 @@ -// reasonable default; will be overriden by server +// Reasonable defaults, will be overriden by server var retention_time = 2 * 60 * 60 * 1000; +var call_retention_time = 15 * 60; +var max_calls = 5; // Our Google Map var map = null; @@ -146,6 +148,15 @@ MapManager.prototype.processUpdates = function(updates) { } updates.forEach(function(update) { + // Process caller-callee updates + if ('caller' in update) { + var call = new GCall(); + call.create(update, map); + self.cman.add(call); + return; + } + + // Process position updates switch (update.location.type) { case 'latlon': var marker = self.mman.find(update.callsign); @@ -184,7 +195,7 @@ MapManager.prototype.processUpdates = function(updates) { marker.update(update); // Assign marker to map - marker.setMap(self.mman.isEnabled(update.mode)? map : undefined); + marker.setMap(self.mman.isEnabled(update.mode)? map : null); // Apply marker options if (marker instanceof GFeatureMarker) { diff --git a/htdocs/map-leaflet.js b/htdocs/map-leaflet.js index 281d951e..f159ebd4 100644 --- a/htdocs/map-leaflet.js +++ b/htdocs/map-leaflet.js @@ -147,8 +147,10 @@ var mapExtraLayers = [ }, ]; -// reasonable default; will be overriden by server +// Reasonable defaults, will be overriden by server var retention_time = 2 * 60 * 60 * 1000; +var call_retention_time = 15 * 60; +var max_calls = 5; // Our Leaflet Map and layerControl var map = null; @@ -433,6 +435,15 @@ MapManager.prototype.processUpdates = function(updates) { } updates.forEach(function(update) { + // Process caller-callee updates + if ('caller' in update) { + var call = new LCall(); + call.create(update, map); + self.cman.add(call); + return; + } + + // Process position updates switch (update.location.type) { case 'latlon': var marker = self.mman.find(update.callsign); @@ -474,7 +485,7 @@ MapManager.prototype.processUpdates = function(updates) { marker.update(update); // Assign marker to map - marker.setMap(self.mman.isEnabled(update.mode)? map : undefined); + marker.setMap(self.mman.isEnabled(update.mode)? map : null); // Apply marker options if (marker instanceof LFeatureMarker) { diff --git a/owrx/config/defaults.py b/owrx/config/defaults.py index 9559d5bb..1e3bd249 100644 --- a/owrx/config/defaults.py +++ b/owrx/config/defaults.py @@ -308,6 +308,8 @@ defaultConfig = PropertyLayer( openweathermap_api_key="", map_type="google", map_position_retention_time=2 * 60 * 60, + map_call_retention_time=15 * 60, + map_max_calls=5, map_prefer_recent_reports=True, map_ignore_indirect_reports=False, callsign_url="https://www.qrzcq.com/call/{}", diff --git a/owrx/connection.py b/owrx/connection.py index 43a8a38f..209490e0 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -549,6 +549,8 @@ class MapConnection(OpenWebRxClient): "map_position_retention_time", "map_ignore_indirect_reports", "map_prefer_recent_reports", + "map_call_retention_time", + "map_max_calls", "callsign_url", "vessel_url", "flight_url", diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index 512a5b8b..b86ca8ad 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -151,6 +151,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController): "lib/jquery-3.2.1.min.js", "lib/chroma.min.js", "lib/Header.js", + "lib/MapCalls.js", "lib/MapLocators.js", "lib/MapMarkers.js", "lib/MapManager.js", @@ -162,6 +163,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController): "lib/jquery-3.2.1.min.js", "lib/chroma.min.js", "lib/Header.js", + "lib/MapCalls.js", "lib/MapLocators.js", "lib/MapMarkers.js", "lib/MapManager.js", diff --git a/owrx/controllers/settings/general.py b/owrx/controllers/settings/general.py index 89db5da3..4224eb09 100644 --- a/owrx/controllers/settings/general.py +++ b/owrx/controllers/settings/general.py @@ -256,16 +256,29 @@ class GeneralSettingsController(SettingsFormController): NumberInput( "map_position_retention_time", "Map retention time", - infotext="Specifies how long markers / grids will remain visible on the map", + infotext="Specifies how long markers / grids will remain visible on the map.", append="s", ), + NumberInput( + "map_call_retention_time", + "Call retention time", + infotext="Specifies how long calls will remain visible on the map.", + validator=RangeValidator(15, 60*60), + append="s", + ), + NumberInput( + "map_max_calls", + "Number of calls shown", + infotext="Specifies how many calls between grids are visible on the map.", + validator=RangeValidator(0, 50), + ), CheckboxInput( "map_ignore_indirect_reports", - "Ignore position reports arriving via indirect path", + "Ignore position reports arriving via indirect path.", ), CheckboxInput( "map_prefer_recent_reports", - "Prefer more recent position reports to shorter path reports", + "Prefer more recent position reports to shorter path reports.", ), TextInput( "callsign_url", diff --git a/owrx/map.py b/owrx/map.py index 81f28690..d4a90705 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -36,6 +36,7 @@ class Map(object): def __init__(self): self.clients = [] self.positions = {} + self.calls = [] self.positionsLock = threading.Lock() def removeLoop(): @@ -66,16 +67,11 @@ class Map(object): self.clients.append(client) with self.positionsLock: positions = [ - { - "callsign": callsign, - "location": record["location"].__dict__(), - "lastseen": record["updated"].timestamp() * 1000, - "mode": record["mode"], - "band": record["band"].getName() if record["band"] is not None else None, - "hops": record["hops"], - } - for (callsign, record) in self.positions.items() + self._makeRecord(key, record) for (key, record) in self.positions.items() + ] + [ + self._makeCall(call) for call in self.calls ] + client.write_update(positions) def removeClient(self, client): @@ -84,42 +80,90 @@ class Map(object): except ValueError: pass - def updateLocation(self, key, loc: Location, mode: str, band: Band = None, hops: list[str] = [], timestamp: datetime = None): + def _makeCall(self, call): + return { + "caller": call["caller"], + "callee": call["callee"], + "src": call["src"].__dict__(), + "dst": call["dst"].__dict__(), + "lastseen": call["timestamp"].timestamp() * 1000, + "mode": call["mode"], + "band": call["band"].getName() if call["band"] is not None else None + } + + def _makeRecord(self, callsign, record): + return { + "callsign": callsign, + "location": record["location"].__dict__(), + "lastseen": record["updated"].timestamp() * 1000, + "mode": record["mode"], + "band": record["band"].getName() if record["band"] is not None else None, + "hops": record["hops"] + } + + def updateCall(self, key, callee, mode: str, band: Band = None, timestamp: datetime = None): + # if we get an external timestamp, make sure it's not already expired if timestamp is None: timestamp = datetime.now(timezone.utc) - else: - # if we get an external timestamp, make sure it's not already expired - if datetime.now(timezone.utc) - loc.getTTL() > timestamp: - return + elif datetime.now(timezone.utc) - loc.getTTL() > timestamp: + return + + max_calls = Config.get()["map_max_calls"] + broadcast = None + + # update the list of callees for existing callsigns + with self.positionsLock: + if key in self.positions and callee in self.positions: + src = self.positions[key]["location"] + dst = self.positions[callee]["location"] + call = { + "caller": key, + "callee": callee, + "timestamp": timestamp, + "mode": mode, + "band": band, + "src": src, + "dst": dst + } + logger.debug("{0} call from {1} to {2}".format(mode, key, callee)) + # remove excessive calls + while len(self.calls) > 0 and len(self.calls) >= max_calls: + self.calls.pop(0) + # add a new call + if len(self.calls) < max_calls: + broadcast = self._makeCall(call) + self.calls.append(call) + + if broadcast is not None: + self.broadcast([broadcast]) + + def updateLocation(self, key, loc: Location, mode: str, band: Band = None, hops: list[str] = [], timestamp: datetime = None): + # if we get an external timestamp, make sure it's not already expired + if timestamp is None: + timestamp = datetime.now(timezone.utc) + elif datetime.now(timezone.utc) - loc.getTTL() > timestamp: + return pm = Config.get() ignoreIndirect = pm["map_ignore_indirect_reports"] preferRecent = pm["map_prefer_recent_reports"] - needBroadcast = False + broadcast = None - with self.positionsLock: - # ignore indirect reports if ignoreIndirect set - if not ignoreIndirect or len(hops)==0: - # prefer messages with shorter hop count unless preferRecent set - if preferRecent or key not in self.positions or len(hops) <= len(self.positions[key]["hops"]): - if isinstance(loc, IncrementalUpdate) and key in self.positions: + # ignore indirect reports if ignoreIndirect set + if not ignoreIndirect or len(hops)==0: + # prefer messages with shorter hop count unless preferRecent set + with self.positionsLock: + if key not in self.positions: + self.positions[key] = { "location": loc, "updated": timestamp, "mode": mode, "band": band, "hops": hops } + broadcast = self._makeRecord(key, self.positions[key]) + elif preferRecent or len(hops) <= len(self.positions[key]["hops"]): + if isinstance(loc, IncrementalUpdate): loc.update(self.positions[key]["location"]) - self.positions[key] = {"location": loc, "updated": timestamp, "mode": mode, "band": band, "hops": hops } - needBroadcast = True + self.positions[key].update({ "location": loc, "updated": timestamp, "mode": mode, "band": band, "hops": hops }) + broadcast = self._makeRecord(key, self.positions[key]) - if needBroadcast: - self.broadcast( - [ - { - "callsign": key, - "location": loc.__dict__(), - "lastseen": timestamp.timestamp() * 1000, - "mode": mode, - "band": band.getName() if band is not None else None, - "hops": hops, - } - ] - ) + if broadcast is not None: + self.broadcast([broadcast]) def touchLocation(self, key): # not implemented on the client side yet, so do not use! diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 32f54d70..e59e201b 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -292,6 +292,10 @@ class WsjtParser(AudioChopperParser): out["callsign"], LocatorLocation(out["locator"]), mode, band ) ReportingEngine.getSharedInstance().spot(out) + if "callsign" in out and "callee" in out: + Map.getSharedInstance().updateCall( + out["callsign"], out["callee"], mode, band + ) return out except Exception: @@ -344,17 +348,23 @@ class MessageParser(ABC): # Used in QSO-style modes (FT8, FT4, FST4) class QsoMessageParser(MessageParser): - locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$") + locator_pattern = re.compile("^(.*)\\s([A-Z0-9/]{2,})(\\sR)?\\s(([A-R]{2}[0-9]{2})|73|RRR)$") + calee_pattern = re.compile("^([A-Z0-9/]{2,})(\\s.*)?$") def parse(self, msg): m = QsoMessageParser.locator_pattern.match(msg) if m is None: return {} - # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very + out = {"callsign": m.group(2)} + # RR73 is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very # likely this just means roger roger goodbye. - if m.group(3) == "RR73": - return {"callsign": m.group(1)} - return {"callsign": m.group(1), "locator": m.group(3)} + if m.group(4) not in ["RR73", "73", "RRR"]: + out["locator"] = m.group(4) + else: + m = QsoMessageParser.calee_pattern.match(m.group(1)) + if m is not None: + out["callee"] = m.group(1) + return out # Used in propagation reporting / beacon modes (WSPR / FST4W)