Now showing latest FT8 and other calls with vectors on the map.
This commit is contained in:
parent
1a04f95d18
commit
f7f72d50c8
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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; };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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/{}",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
116
owrx/map.py
116
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!
|
||||
|
|
|
|||
20
owrx/wsjt.py
20
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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue