From ea10c583bc77d551e120327e71cab6e5757d3767 Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Mon, 18 Sep 2023 04:47:11 +0300
Subject: [PATCH 01/12] Preliminary Leaflet (OSM) implementation. WIP.
---
htdocs/include/header.include.html | 2 +-
htdocs/lib/Clock.js | 21 +
htdocs/lib/GoogleMaps.js | 143 ++++++
htdocs/lib/Leaflet.js | 118 +++++
htdocs/lib/MapLocators.js | 208 ++++++++
htdocs/lib/MapManager.js | 202 ++++++++
htdocs/lib/MapMarkers.js | 680 +++++++++++++++++++++++++++
htdocs/map-google.html | 33 ++
htdocs/map-google.js | 287 +++++++++++
htdocs/map-leaflet.html | 36 ++
htdocs/map-leaflet.js | 525 +++++++++++++++++++++
owrx/config/defaults.py | 1 +
owrx/connection.py | 1 +
owrx/controllers/assets.py | 22 +-
owrx/controllers/settings/general.py | 8 +
owrx/controllers/template.py | 26 +-
16 files changed, 2310 insertions(+), 3 deletions(-)
create mode 100644 htdocs/lib/Clock.js
create mode 100644 htdocs/lib/GoogleMaps.js
create mode 100644 htdocs/lib/Leaflet.js
create mode 100644 htdocs/lib/MapLocators.js
create mode 100644 htdocs/lib/MapManager.js
create mode 100644 htdocs/lib/MapMarkers.js
create mode 100644 htdocs/map-google.html
create mode 100644 htdocs/map-google.js
create mode 100644 htdocs/map-leaflet.html
create mode 100644 htdocs/map-leaflet.js
diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html
index 203d6e41..6b8ae430 100644
--- a/htdocs/include/header.include.html
+++ b/htdocs/include/header.include.html
@@ -10,7 +10,7 @@
Status
Log
Receiver
-
Map
+
Map
Settings
diff --git a/htdocs/lib/Clock.js b/htdocs/lib/Clock.js
new file mode 100644
index 00000000..5a9c2eac
--- /dev/null
+++ b/htdocs/lib/Clock.js
@@ -0,0 +1,21 @@
+function Clock(el) {
+ // Save HTML element to update
+ this.el = el;
+ // Update for the first time
+ this.update();
+}
+
+Clock.prototype.update = function() {
+ const now = new Date();
+ const me = this;
+
+ // Next update at the next minute change
+ setTimeout(function() { me.update(); }, 1000 * (60 - now.getUTCSeconds()));
+
+ // Display UTC clock
+ if (this.el) {
+ const hours = ("00" + now.getUTCHours()).slice(-2);
+ const minutes = ("00" + now.getUTCMinutes()).slice(-2);
+ this.el.html(`${hours}:${minutes} UTC`);
+ }
+}
diff --git a/htdocs/lib/GoogleMaps.js b/htdocs/lib/GoogleMaps.js
new file mode 100644
index 00000000..92b27dd1
--- /dev/null
+++ b/htdocs/lib/GoogleMaps.js
@@ -0,0 +1,143 @@
+//
+// GoogleMaps-Specific Marker
+//
+
+function GMarker() {}
+GMarker.prototype = new google.maps.OverlayView();
+
+GMarker.prototype.setMarkerOptions = function(options) {
+ this.setOptions(options);
+ this.draw();
+};
+
+GMarker.prototype.onAdd = function() {
+ // Create HTML elements representing the mark
+ var div = this.create();
+
+ var self = this;
+ google.maps.event.addDomListener(div, "click", function(event) {
+ event.stopPropagation();
+ google.maps.event.trigger(self, "click", event);
+ });
+
+ var panes = this.getPanes();
+ panes.overlayImage.appendChild(div);
+};
+
+GMarker.prototype.getAnchorPoint = function() {
+ var offset = this.getAnchorOffset();
+ return new google.maps.Point(offset[0], offset[1]);
+};
+
+GMarker.prototype.setMarkerOpacity = function(opacity) {
+ this.setOptions({ opacity: opacity });
+};
+
+GMarker.prototype.setMarkerPosition = function(title, lat, lon) {
+ this.setOptions({
+ title : title,
+ position : new google.maps.LatLng(lat, lon)
+ });
+};
+
+//
+// GoogleMaps-Specific FeatureMarker
+//
+
+function GFeatureMarker() { $.extend(this, new FeatureMarker()); }
+GFeatureMarker.prototype = new GMarker();
+
+GFeatureMarker.prototype.place = function() {
+ // Project location and place symbol
+ var div = this.div;
+ if (div) {
+ var point = this.getProjection().fromLatLngToDivPixel(this.position);
+ if (point) {
+ div.style.left = point.x - this.symWidth / 2 + 'px';
+ div.style.top = point.y - this.symHeight / 2 + 'px';
+ }
+ }
+};
+
+//
+// GoogleMaps-Specific AprsMarker
+//
+
+function GAprsMarker() { $.extend(this, new AprsMarker()); }
+GAprsMarker.prototype = new GMarker();
+
+GAprsMarker.prototype.place = function() {
+ // Project location and place symbol
+ var div = this.div;
+ if (div) {
+ var point = this.getProjection().fromLatLngToDivPixel(this.position);
+ if (point) {
+ div.style.left = point.x - 12 + 'px';
+ div.style.top = point.y - 12 + 'px';
+ }
+ }
+};
+
+//
+// GoogleMaps-Specific SimpleMarker
+//
+
+function GSimpleMarker() { $.extend(this, new AprsMarker()); }
+GSimpleMarker.prototype = new google.maps.Marker();
+
+GSimpleMarker.prototype.setMarkerOpacity = function(opacity) {
+ this.setOptions({ opacity: opacity });
+};
+
+GSimpleMarker.prototype.setMarkerPosition = function(title, lat, lon) {
+ this.setOptions({
+ title : title,
+ position : new google.maps.LatLng(lat, lon)
+ });
+};
+
+GSimpleMarker.prototype.setMarkerOptions = function(options) {
+ this.setOptions(options);
+ this.draw();
+};
+
+//
+// GoogleMaps-Specific Locator
+//
+
+function GLocator() {
+ this.rect = new google.maps.Rectangle();
+ this.rect.setOptions({
+ strokeWeight : 2,
+ strokeColor : "#FFFFFF",
+ fillColor : "#FFFFFF"
+ });
+}
+
+GLocator.prototype = new Locator();
+
+GLocator.prototype.setMap = function(map) {
+ this.rect.setMap(map);
+};
+
+GLocator.prototype.setCenter = function(lat, lon) {
+ this.center = new google.maps.LatLng({lat: lat, lng: lon});
+
+ this.rect.setOptions({ bounds : {
+ north : lat - 0.5,
+ south : lat + 0.5,
+ west : lon - 1.0,
+ east : lon + 1.0
+ }});
+}
+
+GLocator.prototype.setColor = function(color) {
+ this.rect.setOptions({ strokeColor: color, fillColor: color });
+};
+
+GLocator.prototype.setOpacity = function(opacity) {
+ this.rect.setOptions({
+ strokeOpacity : LocatorManager.strokeOpacity * opacity,
+ fillOpacity : LocatorManager.fillOpacity * opacity
+ });
+};
diff --git a/htdocs/lib/Leaflet.js b/htdocs/lib/Leaflet.js
new file mode 100644
index 00000000..589c1272
--- /dev/null
+++ b/htdocs/lib/Leaflet.js
@@ -0,0 +1,118 @@
+//
+// Leaflet-Specific Marker
+//
+
+function LMarker () {
+ this._marker = L.marker();
+};
+
+LMarker.prototype.onAdd = function() {
+ this.div = this.create();
+
+ var offset = this.getAnchorOffset();
+
+ this.setIcon(L.divIcon({
+ html : this.div,
+ iconAnchor : [-offset[1], -offset[0]],
+ className : 'dummy'
+ }));
+};
+
+LMarker.prototype.setMarkerOptions = function(options) {
+ $.extend(this, options);
+ if (typeof this.draw !== 'undefined') {
+ this.draw();
+ }
+};
+
+LMarker.prototype.setMap = function (map) {
+ if (map) this._marker.addTo(map);
+ else this._marker.remove();
+};
+
+LMarker.prototype.addListener = function (e, f) {
+ this._marker.on(e, f);
+};
+
+LMarker.prototype.getPos = function () {
+ return [this.position.lat(), this.position.lng()];
+};
+
+LMarker.prototype.setMarkerOpacity = function(opacity) {
+ this._marker.setOpacity(opacity);
+};
+
+LMarker.prototype.setLatLng = function(lat, lon) {
+ this._marker.setLatLng([lat, lon]);
+ this.position = new posObj([lat, lon]);
+};
+
+LMarker.prototype.setTitle = function(title) {
+ this._marker.options.title = title;
+};
+
+LMarker.prototype.setIcon = function(opts) {
+ this._marker.setIcon(opts);
+};
+
+LMarker.prototype.setMarkerPosition = function(title, lat, lon) {
+ this.setLatLng(lat, lon);
+ this.setTitle(title);
+};
+
+// Leaflet-Specific FeatureMarker
+function LFeatureMarker() { $.extend(this, new LMarker(), new FeatureMarker()); }
+
+// Leaflet-Specific AprsMarker
+function LAprsMarker () { $.extend(this, new LMarker(), new AprsMarker()); }
+
+//
+// Leaflet-Specific Locator
+//
+
+function LLocator() {
+ this._rect = L.rectangle([[0,0], [1,1]], { color: '#FFFFFF', weight: 2, opacity: 1 });
+}
+
+LLocator.prototype = new Locator();
+
+LLocator.prototype.setMap = function(map) {
+ 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 });
+};
+
+LLocator.prototype.setOpacity = function(opacity) {
+ this._rect.setStyle({
+ opacity : LocatorManager.strokeOpacity * opacity,
+ fillOpacity : LocatorManager.fillOpacity * opacity
+ });
+};
+
+LLocator.prototype.addListener = function (e, f) {
+ this._rect.on(e, f);
+};
+
+//
+// Position object
+//
+
+function posObj(pos) {
+ if (typeof pos === 'undefined' || typeof pos[1] === 'undefined') {
+ console.error('Cannot create position object with no LatLng.');
+ return;
+ }
+ this._lat = pos[0];
+ this._lng = pos[1];
+}
+
+posObj.prototype.lat = function () { return this._lat; }
+posObj.prototype.lng = function () { return this._lng; }
diff --git a/htdocs/lib/MapLocators.js b/htdocs/lib/MapLocators.js
new file mode 100644
index 00000000..e7236d85
--- /dev/null
+++ b/htdocs/lib/MapLocators.js
@@ -0,0 +1,208 @@
+//
+// Map Locators Management
+//
+
+LocatorManager.strokeOpacity = 0.8;
+LocatorManager.fillOpacity = 0.35;
+LocatorManager.allRectangles = function() { return true; };
+
+function LocatorManager() {
+ // Current rectangles
+ this.rectangles = {};
+
+ // Current color allocations
+ this.colorKeys = {};
+
+ // The color scale used
+ this.colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl');
+
+ // Current coloring mode
+ this.colorMode = 'byband';
+
+ // Current filter
+ this.rectangleFilter = LocatorManager.allRectangles;
+}
+
+LocatorManager.prototype.filter = function(data) {
+ return this.rectangleFilter(data);
+}
+
+LocatorManager.prototype.find = function(id) {
+ return id in this.rectangles? this.rectangles[id] : null;
+};
+
+LocatorManager.prototype.add = function(id, rectangle) {
+ this.rectangles[id] = rectangle;
+};
+
+LocatorManager.prototype.ageAll = function() {
+ var now = new Date().getTime();
+ var data = this.rectangles;
+ $.each(data, function(id, x) {
+ if (!x.age(now - x.lastseen)) delete data[id];
+ });
+};
+
+LocatorManager.prototype.clear = function() {
+ // Remove all rectangles from the map
+ $.each(this.rectangles, function(_, x) { x.setMap(); });
+ // Delete all rectangles
+ this.rectangles = {};
+};
+
+LocatorManager.prototype.setFilter = function(map, filterBy = null) {
+ if (!filterBy) {
+ this.rectangleFilter = LocatorManager.allRectangles;
+ } else {
+ var key = this.colorMode.slice(2);
+ this.rectangleFilter = function(x) {
+ return x[key] === filterBy;
+ };
+ }
+
+ var filter = this.rectangleFilter;
+ $.each(this.rectangles, function(_, x) {
+ x.setMap(filter(x) ? map : undefined);
+ });
+};
+
+LocatorManager.prototype.reColor = function() {
+ var self = this;
+ $.each(this.rectangles, function(_, x) {
+ x.setColor(self.getColor(x));
+ });
+};
+
+LocatorManager.prototype.updateLegend = function() {
+ if (!this.colorKeys) return;
+ var filter = this.rectangleFilter;
+ var mode = this.colorMode.slice(2);
+ var list = $.map(this.colorKeys, function(value, key) {
+ // Fake rectangle to test if the filter would match
+ var fakeRectangle = Object.fromEntries([[mode, key]]);
+ var disabled = filter(fakeRectangle) ? '' : ' disabled';
+
+ return ''
+ + key + '';
+ });
+
+ $(".openwebrx-map-legend .content").html('');
+}
+
+LocatorManager.prototype.setColorMode = function(map, newColorMode) {
+ this.colorMode = newColorMode;
+ this.colorKeys = {};
+ this.setFilter(map);
+ this.reColor();
+ this.updateLegend();
+};
+
+LocatorManager.prototype.getType = function(data) {
+ switch (this.colorMode) {
+ case 'byband':
+ return data.band;
+ case 'bymode':
+ return data.mode;
+ default:
+ return '';
+ }
+};
+
+LocatorManager.prototype.getColor = function(data) {
+ var type = this.getType(data);
+ if (!type) return "#ffffff00";
+
+ // If adding a new key...
+ if (!this.colorKeys[type]) {
+ var keys = Object.keys(this.colorKeys);
+
+ // Add a new key
+ keys.push(type);
+
+ // Sort color keys
+ keys.sort(function(a, b) {
+ var pa = parseFloat(a);
+ var pb = parseFloat(b);
+ if (isNaN(pa) || isNaN(pb)) return a.localeCompare(b);
+ return pa - pb;
+ });
+
+ // Recompute colors
+ var colors = this.colorScale.colors(keys.length);
+ this.colorKeys = {};
+ for(var j=0 ; j' + message + ')';
+ }).join("");
+
+ return 'Locator: ' + locator + distance +
+ '
Active Callsigns:
';
+};
+
+//
+// Generic Map Locator
+// Derived classes have to implement:
+// setMap(), setCenter(), setColor(), setOpacity()
+//
+
+function Locator() {}
+
+Locator.prototype = new Locator();
+
+Locator.prototype.update = function(update) {
+ this.lastseen = update.lastseen;
+ this.locator = update.location.locator;
+ this.mode = update.mode;
+ this.band = update.band;
+
+ // Get locator's lat/lon
+ const loc = update.location.locator;
+ const lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]) + 0.5;
+ const lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2 + 1.0;
+
+ // Implementation-dependent function call
+ this.setCenter(lat, lon);
+
+ // Age locator
+ this.age(new Date().getTime() - update.lastseen);
+};
+
+Locator.prototype.age = function(age) {
+ if (age <= retention_time) {
+ this.setOpacity(Marker.getOpacityScale(age));
+ return true;
+ } else {
+ this.setMap();
+ return false;
+ }
+};
diff --git a/htdocs/lib/MapManager.js b/htdocs/lib/MapManager.js
new file mode 100644
index 00000000..28d0ebb2
--- /dev/null
+++ b/htdocs/lib/MapManager.js
@@ -0,0 +1,202 @@
+//
+// Map Manager handles web socket connection and traffic processing
+//
+
+function MapManager() {
+ var self = this;
+
+ // Determine web socket URL
+ var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws';
+ var href = window.location.href.replace(/\/[^\/]*$/,'');
+ href = protocol + '://' + href.split('://')[1];
+ this.ws_url = href + (href.endsWith('/')? '':'/') + 'ws/';
+
+ // Reset everything for now
+ this.reconnect_timeout = false;
+ this.config = {};
+
+ // Markers management (features, APRS, AIS, HFDL, etc)
+ this.mman = new MarkerManager();
+
+ // Locators management (FT8, FT4, WSPR, etc)
+ this.lman = new LocatorManager();
+
+ // Fade out / remove positions after time
+ setInterval(function() {
+ self.lman.ageAll();
+ self.mman.ageAll();
+ }, 1000);
+
+ // When stuff loads...
+ $(function() {
+ // Create clock display
+ self.clock = new Clock($('#openwebrx-clock-utc'));
+
+ // Clicking clock display toggles legend box on/off
+ $('#openwebrx-clock-utc').css('cursor', 'pointer').on('click', function() {
+ var el = document.getElementById('openwebrx-map-selectors');
+ if (el) {
+ el.style.display = el.style.display === 'none'?
+ 'block' : 'none';
+ }
+ });
+
+ // Toggle color modes on click
+ $('#openwebrx-map-colormode').on('change', function() {
+ self.lman.setColorMode(map, $(this).val());
+ });
+ });
+
+ // Connect web socket
+ this.connect();
+}
+
+//
+// Process a message received over web socket
+//
+MapManager.prototype.process = function(e) {
+ if (typeof e.data != 'string') {
+ console.error("unsupported binary data on websocket; ignoring");
+ return
+ }
+
+ if (e.data.substr(0, 16) == "CLIENT DE SERVER") {
+ return
+ }
+
+ try {
+ var json = JSON.parse(e.data);
+ switch (json.type) {
+ case "update":
+ this.processUpdates(json.value);
+ break;
+
+ case 'receiver_details':
+ $().ready(function () { // make sure header is loaded
+ $('.webrx-top-container').header().setDetails(json.value);
+ });
+ break;
+
+ case "config":
+ Object.assign(this.config, json.value);
+ if ('receiver_gps' in this.config) {
+ // Passing API key even if this particular map
+ // engine does not need it (Google Maps do)
+ this.initializeMap(
+ this.config.receiver_gps,
+ this.config.google_maps_api_key,
+ this.config.openweathermap_api_key
+ );
+ }
+ if ('receiver_name' in this.config) {
+ this.setReceiverName(this.config.receiver_name);
+ }
+ if ('map_position_retention_time' in this.config) {
+ retention_time = this.config.map_position_retention_time * 1000;
+ }
+ if ('callsign_url' in this.config) {
+ callsign_url = this.config.callsign_url;
+ }
+ if ('vessel_url' in this.config) {
+ vessel_url = this.config.vessel_url;
+ }
+ if ('flight_url' in this.config) {
+ flight_url = this.config.flight_url;
+ }
+ if ('modes_url' in this.config) {
+ modes_url = this.config.modes_url;
+ }
+ break;
+
+ default:
+ console.warn('received message of unknown type: ' + json.type);
+ }
+ } catch (e) {
+ // Don't lose exception
+ console.error(e);
+ }
+};
+
+//
+// Connect web socket
+//
+MapManager.prototype.connect = function() {
+ var ws = new WebSocket(this.ws_url);
+ var self = this;
+
+ // When socket opens...
+ ws.onopen = function() {
+ ws.send("SERVER DE CLIENT client=map.js type=map");
+ self.reconnect_timeout = false
+ };
+
+ // When socket closes...
+ ws.onclose = function() {
+ // Clear map
+ self.removeReceiver();
+ self.mman.clear();
+ self.lman.clear();
+
+ if (self.reconnect_timeout) {
+ // Max value: roundabout 8 and a half minutes
+ self.reconnect_timeout = Math.min(self.reconnect_timeout * 2, 512000);
+ } else {
+ // Initial value: 1s
+ self.reconnect_timeout = 1000;
+ }
+
+ // Try reconnecting after timeout
+ setTimeout(function() { self.connect(); }, self.reconnect_timeout);
+ };
+
+ // When socket receives a message...
+ ws.onmessage = function(e) {
+ self.process(e);
+ }
+
+ // When socket gets an error...
+ //ws.onerror = function() {
+ // console.info("websocket error");
+ //};
+
+ // http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript
+ window.onbeforeunload = function() {
+ ws.onclose = function () {};
+ ws.close();
+ };
+};
+
+//
+// Set up legend filter toggles inside given HTML element.
+//
+MapManager.prototype.setupLegendFilters = function($legend) {
+ var self = this;
+
+ $content = $legend.find('.content');
+ $content.on('click', 'li', function() {
+ var $el = $(this);
+ $lis = $content.find('li');
+ if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) {
+ $lis.removeClass('disabled');
+ self.lman.setFilter(map);
+ } else {
+ $el.removeClass('disabled');
+ $lis.filter(function() {
+ return this != $el[0]
+ }).addClass('disabled');
+ self.lman.setFilter(map, $el.data('selector'));
+ }
+ });
+
+ $content1 = $legend.find('.features');
+ $content1.on('click', 'li', function() {
+ var $el = $(this);
+ var onoff = $el.hasClass('disabled');
+ if (onoff) {
+ $el.removeClass('disabled');
+ } else {
+ $el.addClass('disabled');
+ }
+ self.mman.toggle(map, $el.data('selector'), onoff);
+ });
+};
diff --git a/htdocs/lib/MapMarkers.js b/htdocs/lib/MapMarkers.js
new file mode 100644
index 00000000..5120c3ac
--- /dev/null
+++ b/htdocs/lib/MapMarkers.js
@@ -0,0 +1,680 @@
+//
+// Map Markers Management
+//
+
+function MarkerManager() {
+ // Current markers
+ this.markers = {};
+
+ // Currently known marker types
+ this.types = {};
+
+ // Colors used for marker types
+ this.colors = {
+ 'KiwiSDR' : '#800000',
+ 'WebSDR' : '#000080',
+ 'OpenWebRX' : '#004000',
+ 'HFDL' : '#004000',
+ 'VDL2' : '#000080',
+ 'ADSB' : '#800000',
+ 'ACARS' : '#000000'
+ };
+
+ // Symbols used for marker types
+ this.symbols = {
+ 'KiwiSDR' : '◬',
+ 'WebSDR' : '◬',
+ 'OpenWebRX' : '◬',
+ 'Stations' : '⍑', //'◎',
+ 'Repeaters' : '⋈',
+ 'APRS' : '⚐',
+ 'AIS' : '⩯',
+ 'HFDL' : '✈',
+ 'VDL2' : '✈',
+ 'ADSB' : '✈',
+ 'ACARS' : '✈'
+ };
+
+ // Marker type shown/hidden status
+ this.enabled = {
+ 'KiwiSDR' : false,
+ 'WebSDR' : false,
+ 'OpenWebRX' : false,
+ 'Stations' : false,
+ 'Repeaters' : false
+ };
+}
+
+MarkerManager.prototype.getColor = function(type) {
+ // Default color is black
+ return type in this.colors? this.colors[type] : '#000000';
+};
+
+MarkerManager.prototype.getSymbol = function(type) {
+ // Default symbol is a rombus
+ return type in this.symbols? this.symbols[type] : '◇';
+};
+
+MarkerManager.prototype.isEnabled = function(type) {
+ // Features are shown by default
+ return type in this.enabled? this.enabled[type] : true;
+};
+
+MarkerManager.prototype.toggle = function(map, type, onoff) {
+ // Keep track of each feature table being show or hidden
+ this.enabled[type] = onoff;
+
+ // Show or hide features on the map
+ $.each(this.markers, function(_, x) {
+ if (x.mode === type) x.setMap(onoff ? map : undefined);
+ });
+};
+
+MarkerManager.prototype.addType = function(type) {
+ // Do not add feature twice
+ if (type in this.types) return;
+
+ // Determine symbol and its color
+ var color = this.getColor(type);
+ var symbol = this.getSymbol(type);
+ var enabled = this.isEnabled(type);
+
+ // Add type to the list of known types
+ this.types[type] = symbol;
+ this.enabled[type] = enabled;
+
+ // If there is a list of features...
+ var $content = $('.openwebrx-map-legend').find('.features');
+ if($content)
+ {
+ // Add visual list item for the type
+ $content.append(
+ '' +
+ '' +
+ symbol + '' + type + ''
+ );
+ }
+};
+
+MarkerManager.prototype.find = function(id) {
+ return id in this.markers? this.markers[id] : null;
+};
+
+MarkerManager.prototype.add = function(id, marker) {
+ this.markers[id] = marker;
+};
+
+MarkerManager.prototype.ageAll = function() {
+ var now = new Date().getTime();
+ var data = this.markers;
+ $.each(data, function(id, x) {
+ if (!x.age(now - x.lastseen)) delete data[id];
+ });
+};
+
+MarkerManager.prototype.clear = function() {
+ // Remove all markers from the map
+ $.each(this.markers, function(_, x) { x.setMap(); });
+ // Delete all markers
+ this.markers = {};
+};
+
+//
+// Generic Map Marker
+// Derived classes have to implement:
+// setMap(), setMarkerOpacity()
+//
+
+function Marker() {}
+
+// Wrap given callsign or other ID into a clickable link.
+Marker.linkify = function(name, url = null, linkEntity = null) {
+ // If no specific link entity, use the ID itself
+ if (linkEntity == null) linkEntity = name;
+
+ // Must have valid ID and lookup URL
+ if ((name == '') || (url == null) || (url == '')) {
+ return name;
+ } else {
+ return '' + name + '';
+ }
+};
+
+// Create link to tune OWRX to the given frequency and modulation.
+Marker.linkifyFreq = function(freq, mod) {
+ var text;
+ if (freq >= 30000000) {
+ text = "" + (freq / 1000000.0) + "MHz";
+ } else if (freq >= 10000) {
+ text = "" + (freq / 1000.0) + "kHz";
+ } else {
+ text = "" + freq + "Hz";
+ }
+
+ return '' + text + '';
+};
+
+// Compute distance, in kilometers, between two latlons.
+Marker.distanceKm = function(p1, p2) {
+ // Earth radius in km
+ var R = 6371.0;
+ // Convert degrees to radians
+ var rlat1 = p1.lat() * (Math.PI/180);
+ var rlat2 = p2.lat() * (Math.PI/180);
+ // Compute difference in radians
+ var difflat = rlat2-rlat1;
+ var difflon = (p2.lng()-p1.lng()) * (Math.PI/180);
+ // Compute distance
+ d = 2 * R * Math.asin(Math.sqrt(
+ Math.sin(difflat/2) * Math.sin(difflat/2) +
+ Math.cos(rlat1) * Math.cos(rlat2) * Math.sin(difflon/2) * Math.sin(difflon/2)
+ ));
+ return Math.round(d);
+};
+
+// Truncate string to a given number of characters, adding "..." to the end.
+Marker.truncate = function(str, count) {
+ return str.length > count? str.slice(0, count) + '…' : str;
+};
+
+// Convert degrees to compass direction.
+Marker.degToCompass = function(deg) {
+ dir = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
+ return dir[Math.floor((deg/22.5) + 0.5) % 16];
+};
+
+// Convert given name to an information section title.
+Marker.makeListTitle = function(name) {
+ return '' + name + '
';
+};
+
+// Convert given name/value to an information section item.
+Marker.makeListItem = function(name, value) {
+ return ''
+ + '' + name + ' '
+ + '' + value + ''
+ + '
';
+};
+
+// Get opacity value in the 0..1 range based on the given age.
+Marker.getOpacityScale = function(age) {
+ var scale = 1;
+ if (age >= retention_time / 2) {
+ scale = (retention_time - age) / (retention_time / 2);
+ }
+ return Math.max(0, Math.min(1, scale));
+};
+
+// Set marker's opacity based on the supplied age. Returns TRUE
+// if the marker should still be visible, FALSE if it has to be
+// removed.
+Marker.prototype.age = function(age) {
+ if(age <= retention_time) {
+ this.setMarkerOpacity(Marker.getOpacityScale(age));
+ return true;
+ } else {
+ this.setMap();
+ return false;
+ }
+};
+
+// Remove visual marker element from its parent, if that element exists.
+Marker.prototype.remove = function() {
+ if (this.div) {
+ this.div.parentNode.removeChild(this.div);
+ this.div = null;
+ }
+};
+
+//
+// Feature Marker
+// Represents static map features, such as stations and receivers.
+// Derived classes have to implement:
+// setMarkerOpacity()
+//
+
+function FeatureMarker() {}
+
+FeatureMarker.prototype = new Marker();
+
+FeatureMarker.prototype.update = function(update) {
+ this.lastseen = update.lastseen;
+ this.mode = update.mode;
+ this.url = update.location.url;
+ this.comment = update.location.comment;
+ // Receivers
+ this.altitude = update.location.altitude;
+ this.device = update.location.device;
+ this.antenna = update.location.antenna;
+ // EIBI
+ this.schedule = update.location.schedule;
+ // Repeaters
+ this.freq = update.location.freq;
+ this.status = update.location.status;
+ this.updated = update.location.updated;
+ this.mmode = update.location.mmode;
+ // Generic vendor-specific details
+ this.details = update.location.details;
+
+ // Implementation-dependent function call
+ this.setMarkerPosition(update.callsign, update.location.lat, update.location.lon);
+
+ // Age locator
+ this.age(new Date().getTime() - update.lastseen);
+};
+
+FeatureMarker.prototype.draw = function() {
+ var div = this.div;
+ if (!div) return;
+
+ div.style.color = this.color? this.color : '#000000';
+ div.innerHTML = this.symbol? this.symbol : '●';
+
+ if (this.place) this.place();
+};
+
+FeatureMarker.prototype.create = function() {
+ var div = this.div = document.createElement('div');
+
+ // Marker size
+ this.symWidth = 16;
+ this.symHeight = 16;
+
+ div.style.position = 'absolute';
+ div.style.cursor = 'pointer';
+ div.style.width = this.symWidth + 'px';
+ div.style.height = this.symHeight + 'px';
+ div.style.textAlign = 'center';
+ div.style.fontSize = this.symHeight + 'px';
+ div.style.lineHeight = this.symHeight + 'px';
+
+ return div;
+};
+
+FeatureMarker.prototype.getAnchorOffset = function() {
+ return [0, -this.symHeight/2];
+};
+
+FeatureMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
+ var nameString = this.url? Marker.linkify(name, this.url) : name;
+ var commentString = this.comment? '' + this.comment + '
' : '';
+ var detailsString = '';
+ var scheduleString = '';
+ var distance = '';
+
+ // If it is a repeater, its name is a callsign
+ if(!this.url && this.freq) {
+ nameString = Marker.linkify(name, callsign_url);
+ }
+
+ if (this.altitude) {
+ detailsString += Marker.makeListItem('Altitude', this.altitude.toFixed(0) + ' m');
+ }
+
+ if (this.device) {
+ detailsString += Marker.makeListItem('Device', this.device.manufacturer?
+ this.device.device + ' by ' + this.device.manufacturer : this.device
+ );
+ }
+
+ if (this.antenna) {
+ detailsString += Marker.makeListItem('Antenna', Marker.truncate(this.antenna, 24));
+ }
+
+ if (this.freq) {
+ detailsString += Marker.makeListItem('Frequency', Marker.linkifyFreq(
+ this.freq, this.mmode? this.mmode:'fm'
+ ));
+ }
+
+ if (this.mmode) {
+ detailsString += Marker.makeListItem('Modulation', this.mmode.toUpperCase());
+ }
+
+ if (!this.comment && this.status && this.updated) {
+ commentString = '' + this.status
+ + ', last updated on ' + this.updated + '
';
+ } else {
+ if (this.status) {
+ detailsString += Marker.makeListItem('Status', this.status);
+ }
+ if (this.updated) {
+ detailsString += Marker.makeListItem('Updated', this.updated);
+ }
+ }
+
+ var moreDetails = this.detailsData;
+ if (typeof moreDetails === 'object') {
+ Object.keys(moreDetails).sort().forEach(function (k, i) {
+ detailsString += Marker.makeListItem(k.charAt(0).toUpperCase() + k.slice(1), moreDetails[k]);
+ });
+ }
+
+ if (this.schedule) {
+ for (var j=0 ; j 0) {
+ detailsString = '' + Marker.makeListTitle('Details') + detailsString + '
';
+ }
+
+ if (scheduleString.length > 0) {
+ scheduleString = '' + Marker.makeListTitle('Schedule') + scheduleString + '
';
+ }
+
+ if (receiverMarker) {
+ distance = ' at ' + Marker.distanceKm(receiverMarker.position, this.position) + ' km';
+ }
+
+ return '' + nameString + distance + '
'
+ + commentString + detailsString + scheduleString;
+};
+
+//
+// APRS Marker
+// Represents APRS transmitters, as well as AIS (vessels)
+// and HFDL (planes).
+// Derived classes have to implement:
+// setMarkerOpacity()
+//
+
+function AprsMarker() {}
+
+AprsMarker.prototype = new Marker();
+
+AprsMarker.prototype.update = function(update) {
+ this.lastseen = update.lastseen;
+ this.mode = update.mode;
+ this.hops = update.hops;
+ this.band = update.band;
+ this.comment = update.location.comment;
+ this.weather = update.location.weather;
+ this.altitude = update.location.altitude;
+ this.height = update.location.height;
+ this.power = update.location.power;
+ this.gain = update.location.gain;
+ this.device = update.location.device;
+ this.directivity = update.location.directivity;
+ this.aircraft = update.location.aircraft;
+ this.destination = update.location.destination;
+ this.origin = update.location.origin;
+ this.flight = update.location.flight;
+ this.icao = update.location.icao;
+ this.vspeed = update.location.vspeed;
+
+ // Implementation-dependent function call
+ this.setMarkerPosition(update.callsign, update.location.lat, update.location.lon);
+
+ // Age locator
+ this.age(new Date().getTime() - update.lastseen);
+};
+
+AprsMarker.prototype.isFacingEast = function(symbol) {
+ var eastward = symbol.table === '/' ?
+ '(*<=>CFPUXYZabefgjkpsuv[' : '(T`efhjktuvw';
+ return eastward.includes(symbol.symbol);
+};
+
+AprsMarker.prototype.draw = function() {
+ var div = this.div;
+ var overlay = this.overlay;
+ if (!div || !overlay) return;
+
+ if (this.symbol) {
+ var tableId = this.symbol.table === '/' ? 0 : 1;
+ div.style.background = 'url(aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png)';
+ div.style['background-size'] = '384px 144px';
+ div.style['background-position-x'] = -(this.symbol.index % 16) * 24 + 'px';
+ div.style['background-position-y'] = -Math.floor(this.symbol.index / 16) * 24 + 'px';
+ }
+
+ // If entity is flying at a significant altitude...
+ if (this.altitude >= 500) {
+ // r = elevation, a = rotation, = shadow offset
+ var r = Math.round(this.altitude / 1000);
+ var a = - Math.PI * (this.course? this.course - 45 : -45) / 180;
+ var x = r * Math.cos(a);
+ var y = r * Math.sin(a);
+ div.style.filter = 'drop-shadow(' + x + 'px ' + y + 'px 0.5px rgba(0,0,0,0.5))';
+ } else {
+ div.style.filter = 'none';
+ }
+
+ if (!this.course) {
+ div.style.transform = null;
+ } else if (this.symbol && !this.isFacingEast(this.symbol)) {
+ // Airplanes and other symbols point up (to the north)
+ div.style.transform = 'rotate(' + this.course + 'deg)';
+ } else if (this.course > 180) {
+ // Vehicles and vessels point right (to the east)
+ div.style.transform = 'scalex(-1) rotate(' + (270 - this.course) + 'deg)'
+ } else {
+ // Vehicles and vessels point right (to the east)
+ div.style.transform = 'rotate(' + (this.course - 90) + 'deg)';
+ }
+
+ if (this.symbol && this.symbol.table !== '/' && this.symbol.table !== '\\') {
+ overlay.style.display = 'block';
+ overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px';
+ overlay.style['background-position-y'] = -Math.floor(this.symbol.tableindex / 16) * 24 + 'px';
+ } else {
+ overlay.style.display = 'none';
+ }
+
+ if (this.opacity) {
+ div.style.opacity = this.opacity;
+ } else {
+ div.style.opacity = null;
+ }
+
+ if (this.place) this.place();
+};
+
+AprsMarker.prototype.create = function() {
+ var div = this.div = document.createElement('div');
+
+ div.style.position = 'absolute';
+ div.style.cursor = 'pointer';
+ div.style.width = '24px';
+ div.style.height = '24px';
+
+ var overlay = this.overlay = document.createElement('div');
+ overlay.style.width = '24px';
+ overlay.style.height = '24px';
+ overlay.style.background = 'url(aprs-symbols/aprs-symbols-24-2@2x.png)';
+ overlay.style['background-size'] = '384px 144px';
+ overlay.style.display = 'none';
+
+ div.appendChild(overlay);
+
+ return div;
+};
+
+AprsMarker.prototype.getAnchorOffset = function() {
+ return [0, -12];
+};
+
+AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
+ var timeString = moment(this.lastseen).fromNow();
+ var commentString = '';
+ var weatherString = '';
+ var detailsString = '';
+ var hopsString = '';
+ var distance = '';
+
+ if (this.comment) {
+ commentString += '' + Marker.makeListTitle('Comment') + '
' +
+ this.comment + '
';
+ }
+
+ if (this.weather) {
+ weatherString += '' + Marker.makeListTitle('Weather');
+
+ if (this.weather.temperature) {
+ weatherString += Marker.makeListItem('Temperature', this.weather.temperature.toFixed(1) + ' oC');
+ }
+
+ if (this.weather.humidity) {
+ weatherString += Marker.makeListItem('Humidity', this.weather.humidity + '%');
+ }
+
+ if (this.weather.barometricpressure) {
+ weatherString += Marker.makeListItem('Pressure', this.weather.barometricpressure.toFixed(1) + ' mbar');
+ }
+
+ if (this.weather.wind) {
+ if (this.weather.wind.speed && (this.weather.wind.speed>0)) {
+ weatherString += Marker.makeListItem('Wind',
+ Marker.degToCompass(this.weather.wind.direction) + ' ' +
+ this.weather.wind.speed.toFixed(1) + ' km/h '
+ );
+ }
+
+ if (this.weather.wind.gust && (this.weather.wind.gust>0)) {
+ weatherString += Marker.makeListItem('Gusts', this.weather.wind.gust.toFixed(1) + ' km/h');
+ }
+ }
+
+ if (this.weather.rain && (this.weather.rain.day>0)) {
+ weatherString += Marker.makeListItem('Rain',
+ this.weather.rain.hour.toFixed(0) + ' mm/h, ' +
+ this.weather.rain.day.toFixed(0) + ' mm/day'
+// this.weather.rain.sincemidnight + ' mm since midnight'
+ );
+ }
+
+ if (this.weather.snowfall) {
+ weatherString += Marker.makeListItem('Snow', this.weather.snowfall.toFixed(1) + ' cm');
+ }
+
+ weatherString += '
';
+ }
+
+ if (this.device) {
+ detailsString += Marker.makeListItem('Device', this.device.manufacturer?
+ this.device.device + ' by ' + this.device.manufacturer : this.device
+ );
+ }
+
+ if (this.height) {
+ detailsString += Marker.makeListItem('Height', this.height.toFixed(0) + ' m');
+ }
+
+ if (this.power) {
+ detailsString += Marker.makeListItem('Power', this.power + ' W');
+ }
+
+ if (this.gain) {
+ detailsString += Marker.makeListItem('Gain', this.gain + ' dB');
+ }
+
+ if (this.directivity) {
+ detailsString += Marker.makeListItem('Direction', this.directivity);
+ }
+
+ if (this.icao) {
+ detailsString += Marker.makeListItem('ICAO', Marker.linkify(this.icao, modes_url));
+ }
+
+ if (this.aircraft) {
+ detailsString += Marker.makeListItem('Aircraft', Marker.linkify(this.aircraft, flight_url));
+ }
+
+ if (this.origin) {
+ detailsString += Marker.makeListItem('Origin', this.origin);
+ }
+
+ if (this.destination) {
+ detailsString += Marker.makeListItem('Destination', this.destination);
+ }
+
+ // Combine course and speed if both present
+ if (this.course && this.speed) {
+ detailsString += Marker.makeListItem('Course',
+ Marker.degToCompass(this.course) + ' ' +
+ this.speed.toFixed(1) + ' km/h'
+ );
+ } else {
+ if (this.course) {
+ detailsString += Marker.makeListItem('Course', Marker.degToCompass(this.course));
+ }
+ if (this.speed) {
+ detailsString += Marker.makeListItem('Speed', this.speed.toFixed(1) + ' km/h');
+ }
+ }
+
+ // Combine altitude and vertical speed
+ if (this.altitude) {
+ var alt = this.altitude.toFixed(0) + ' m';
+ if (this.vspeed > 0) alt += ' ↗' + this.vspeed + ' m/m';
+ else if (this.vspeed < 0) alt += ' ↘' + (-this.vspeed) + ' m/m';
+ detailsString += Marker.makeListItem('Altitude', alt);
+ }
+
+ if (detailsString.length > 0) {
+ detailsString = '' + Marker.makeListTitle('Details') + detailsString + '
';
+ }
+
+ if (receiverMarker) {
+ distance = ' at ' + Marker.distanceKm(receiverMarker.position, this.position) + ' km';
+ }
+
+ if (this.hops && this.hops.length > 0) {
+ var hops = this.hops.toString().split(',');
+ hops.forEach(function(part, index, hops) {
+ hops[index] = Marker.linkify(part, callsign_url);
+ });
+
+ hopsString = 'via ' + hops.join(', ') + '
';
+ }
+
+ // Linkify title based on what it is (vessel, flight, mode-S code, or else)
+ var linkEntity = null;
+ var url = null;
+ switch (this.mode) {
+ case 'HFDL':
+ case 'VDL2':
+ case 'ADSB':
+ case 'ACARS':
+ if (this.flight) {
+ name = this.flight;
+ url = this.flight.match(/^[A-Z]{3}[0-9]+[A-Z]*$/)? flight_url : null;
+ } else if (this.aircraft) {
+ name = this.aircraft;
+ url = flight_url;
+ } else {
+ url = modes_url;
+ }
+ break;
+ case 'AIS':
+ url = vessel_url;
+ break;
+ default:
+ linkEntity = name.replace(new RegExp('-.*$'), '');
+ url = callsign_url;
+ break;
+ }
+
+ return '' + Marker.linkify(name, url, linkEntity) + distance + '
'
+ + '' + timeString + ' using ' + this.mode
+ + ( this.band ? ' on ' + this.band : '' ) + '
'
+ + commentString + weatherString + detailsString + hopsString;
+};
diff --git a/htdocs/map-google.html b/htdocs/map-google.html
new file mode 100644
index 00000000..570a649b
--- /dev/null
+++ b/htdocs/map-google.html
@@ -0,0 +1,33 @@
+
+
+
+ OpenWebRX+ Map
+
+
+
+
+
+
+
+
+
+
+ ${header}
+
+
+
+
Colors
+
+
+
Features
+
+
+
00:00 UTC
+
+
+
diff --git a/htdocs/map-google.js b/htdocs/map-google.js
new file mode 100644
index 00000000..af48416f
--- /dev/null
+++ b/htdocs/map-google.js
@@ -0,0 +1,287 @@
+// Marker.linkify() uses these URLs
+var callsign_url = null;
+var vessel_url = null;
+var flight_url = null;
+var modes_url = null;
+
+// reasonable default; will be overriden by server
+var retention_time = 2 * 60 * 60 * 1000;
+
+// Our Google Map
+var map = null;
+
+// Receiver location marker
+var receiverMarker = null;
+
+// Information bubble window
+var infoWindow = null;
+
+// Updates are queued here
+var updateQueue = [];
+
+// Web socket connection management, message processing
+var mapManager = new MapManager();
+
+var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){
+ var s = v.split('=');
+ var r = {};
+ r[s[0]] = s.slice(1).join('=');
+ return r;
+}).reduce(function(a, b){
+ return a.assign(b);
+});
+
+var expectedCallsign = query.callsign? decodeURIComponent(query.callsign) : null;
+var expectedLocator = query.locator? query.locator : null;
+
+// Get information bubble window
+function getInfoWindow() {
+ if (!infoWindow) {
+ infoWindow = new google.maps.InfoWindow();
+ google.maps.event.addListener(infoWindow, 'closeclick', function() {
+ delete infoWindow.locator;
+ delete infoWindow.callsign;
+ });
+ }
+ delete infoWindow.locator;
+ delete infoWindow.callsign;
+ return infoWindow;
+};
+
+// Show information bubble for a locator
+function showLocatorInfoWindow(locator, pos) {
+ var iw = getInfoWindow();
+
+ iw.locator = locator;
+ iw.setContent(mapManager.lman.getInfoHTML(locator, pos, receiverMarker));
+ iw.setPosition(pos);
+ iw.open(map);
+};
+
+// Show information bubble for a marker
+function showMarkerInfoWindow(name, pos) {
+ var marker = mapManager.mman.find(name);
+ var iw = getInfoWindow();
+
+ iw.callsign = name;
+ iw.setContent(marker.getInfoHTML(name, receiverMarker));
+ iw.open(map, marker);
+};
+
+// Show information bubble for the receiver location
+function showReceiverInfoWindow(marker) {
+ var iw = getInfoWindow()
+ iw.setContent(
+ '' + marker.config['receiver_name'] + '
' +
+ 'Receiver Location
'
+ );
+ iw.open(map, marker);
+};
+
+//
+// GOOGLE-SPECIFIC MAP MANAGER METHODS
+//
+
+MapManager.prototype.setReceiverName = function(name) {
+ if (receiverMarker) receiverMarker.setOptions({ title: name });
+}
+
+MapManager.prototype.removeReceiver = function() {
+ if (receiverMarker) receiverMarker.setMap();
+}
+
+MapManager.prototype.initializeMap = function(receiver_gps, api_key, weather_key) {
+ var receiverPos = { lat: receiver_gps.lat, lng: receiver_gps.lon };
+
+ if (map) {
+ receiverMarker.setOptions({
+ map : map,
+ position : receiverPos,
+ config : this.config
+ });
+ } else {
+ var self = this;
+
+ // After Google Maps API loads...
+ $.getScript("https://maps.googleapis.com/maps/api/js?key=" + api_key).done(function() {
+ // Create a map instance
+ map = new google.maps.Map($('.openwebrx-map')[0], {
+ center : receiverPos,
+ zoom : 5,
+ });
+
+ // Load and initialize day-and-night overlay
+ $.getScript("static/lib/nite-overlay.js").done(function() {
+ nite.init(map);
+ setInterval(function() { nite.refresh() }, 10000); // every 10s
+ });
+
+ // Load and initialize OWRX-specific map item managers
+ $.getScript('static/lib/GoogleMaps.js').done(function() {
+ // Process any accumulated updates
+ self.processUpdates(updateQueue);
+ updateQueue = [];
+ });
+
+ // Create map legend selectors
+ var $legend = $(".openwebrx-map-legend");
+ self.setupLegendFilters($legend);
+ map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($legend[0]);
+
+ // Create receiver marker
+ if (!receiverMarker) {
+ receiverMarker = new google.maps.Marker();
+ receiverMarker.addListener('click', function() {
+ showReceiverInfoWindow(receiverMarker);
+ });
+ }
+
+ // Set receiver marker position, name, etc.
+ receiverMarker.setOptions({
+ map : map,
+ position : receiverPos,
+ title : self.config['receiver_name'],
+ config : self.config
+ });
+ });
+ }
+};
+
+MapManager.prototype.processUpdates = function(updates) {
+ var self = this;
+
+ if (typeof(GMarker) === 'undefined') {
+ updateQueue = updateQueue.concat(updates);
+ return;
+ }
+
+ updates.forEach(function(update) {
+ if (typeof update.source === 'undefined' || typeof update.source.callsign === 'undefined') {
+ console.error(update);
+ return;
+ }
+ var id = update.source.callsign + (update.source.ssid ? '-' + update.source.ssid : '');
+
+ switch (update.location.type) {
+ case 'latlon':
+ var marker = self.mman.find(id);
+ var markerClass = GSimpleMarker;
+ var aprsOptions = {}
+
+ if (update.location.symbol) {
+ markerClass = GAprsMarker;
+ aprsOptions.symbol = update.location.symbol;
+ aprsOptions.course = update.location.course;
+ aprsOptions.speed = update.location.speed;
+ }
+
+ // If new item, create a new marker for it
+ if (!marker) {
+ marker = new markerClass();
+ self.mman.add(id, marker);
+ marker.addListener('click', function() {
+ showMarkerInfoWindow(id, marker.position);
+ });
+ }
+
+ // Keep track of new marker types as they may change
+ self.mman.addType(update.mode);
+
+ // Update marker attributes and age
+ marker.update(update);
+
+ // Assign marker to map
+ marker.setMap(self.mman.isEnabled(update.mode)? map : undefined);
+
+ // Apply marker options
+ marker.setMarkerOptions(aprsOptions);
+
+ if (expectedCallsign && expectedCallsign == id) {
+ map.panTo(marker.position);
+ showMarkerInfoWindow(id, marker.position);
+ expectedCallsign = false;
+ }
+
+ if (infoWindow && infoWindow.callsign && infoWindow.callsign == id) {
+ showMarkerInfoWindow(infoWindow.callsign, marker.position);
+ }
+ break;
+
+ case 'feature':
+ var marker = self.mman.find(id);
+ var options = {}
+
+ // If no symbol or color supplied, use defaults by type
+ if (update.location.symbol) {
+ options.symbol = update.location.symbol;
+ } else {
+ options.symbol = self.mman.getSymbol(update.mode);
+ }
+ if (update.location.color) {
+ options.color = update.location.color;
+ } else {
+ options.color = self.mman.getColor(update.mode);
+ }
+
+ // If new item, create a new marker for it
+ if (!marker) {
+ marker = new GFeatureMarker();
+ self.mman.addType(update.mode);
+ self.mman.add(id, marker);
+ marker.addListener('click', function() {
+ showMarkerInfoWindow(id, marker.position);
+ });
+ }
+
+ // Update marker attributes and age
+ marker.update(update);
+
+ // Assign marker to map
+ marker.setMap(self.mman.isEnabled(update.mode)? map : undefined);
+
+ // Apply marker options
+ marker.setMarkerOptions(options);
+
+ if (expectedCallsign && expectedCallsign == id) {
+ map.panTo(marker.position);
+ showMarkerInfoWindow(id, marker.position);
+ expectedCallsign = false;
+ }
+
+ if (infoWindow && infoWindow.callsign && infoWindow.callsign == id) {
+ showMarkerInfoWindow(infoWindow.callsign, marker.position);
+ }
+ break;
+
+ case 'locator':
+ var rectangle = self.lman.find(id);
+
+ // If new item, create a new locator for it
+ if (!rectangle) {
+ rectangle = new GLocator();
+ self.lman.add(id, rectangle);
+ rectangle.rect.addListener('click', function() {
+ showLocatorInfoWindow(rectangle.locator, rectangle.center);
+ });
+ }
+
+ // Update locator attributes, center, age
+ rectangle.update(update);
+
+ // Assign locator to map and set its color
+ rectangle.setMap(self.lman.filter(rectangle)? map : undefined);
+ rectangle.setColor(self.lman.getColor(rectangle));
+
+ if (expectedLocator && expectedLocator == update.location.locator) {
+ map.panTo(rectangle.center);
+ showLocatorInfoWindow(expectedLocator, rectangle.center);
+ expectedLocator = false;
+ }
+
+ if (infoWindow && infoWindow.locator && infoWindow.locator == update.location.locator) {
+ showLocatorInfoWindow(infoWindow.locator, rectangle.center);
+ }
+ break;
+ }
+ });
+};
diff --git a/htdocs/map-leaflet.html b/htdocs/map-leaflet.html
new file mode 100644
index 00000000..1e24bfff
--- /dev/null
+++ b/htdocs/map-leaflet.html
@@ -0,0 +1,36 @@
+
+
+
+ OpenWebRX+ Map
+
+
+
+
+
+
+
+
+
+
+ ${header}
+
+
+
+
Map
+
+
+
Colors
+
+
+
Features
+
+
+
00:00 UTC
+
+
+
diff --git a/htdocs/map-leaflet.js b/htdocs/map-leaflet.js
new file mode 100644
index 00000000..5a73795a
--- /dev/null
+++ b/htdocs/map-leaflet.js
@@ -0,0 +1,525 @@
+// Marker.linkify() uses these URLs
+var callsign_url = null;
+var vessel_url = null;
+var flight_url = null;
+var modes_url = null;
+
+var mapSources = [
+ {
+ name: 'OpenStreetMap',
+ url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ noWrap: true,
+ attribution: '© OpenStreetMap'
+ },
+ },
+ {
+ name: 'OpenTopoMap',
+ url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 17,
+ noWrap: true,
+ attribution: 'Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
+ }
+ },
+ {
+ name: 'Esri WorldTopo',
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',
+ config: {
+ noWrap: true,
+ attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community'
+ }
+ },
+ {
+ name: 'Esri WorldStreet',
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}',
+ options: {
+ attribution: 'Tiles © Esri — Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
+ }
+ },
+ {
+ name: 'Esri WorldImagery',
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
+ options: {
+ attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
+ }
+ },
+ {
+ name: 'Esri NatGeoWorld',
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}',
+ options: {
+ attribution: 'Tiles © Esri — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC',
+ maxZoom: 16
+ }
+ },
+ {
+ name: 'Esri WorldGray',
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}',
+ options: {
+ attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ',
+ maxZoom: 16
+ }
+ },
+ {
+ name: 'CartoDB Positron',
+ url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
+ options: {
+ attribution: '© OpenStreetMap contributors © CARTO',
+ subdomains: 'abcd',
+ maxZoom: 20
+ }
+ },
+ {
+ name: 'CartoDB DarkMatter',
+ url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
+ options: {
+ attribution: '© OpenStreetMap contributors © CARTO',
+ subdomains: 'abcd',
+ maxZoom: 20
+ }
+ },
+ {
+ name: 'CartoDB Voyager',
+ url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
+ options: {
+ attribution: '© OpenStreetMap contributors © CARTO',
+ subdomains: 'abcd',
+ maxZoom: 20
+ }
+ },
+ {
+ name: 'Stadia Alidade',
+ url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png',
+ config: {
+ maxZoom: 20,
+ noWrap: true,
+ attribution: '© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors',
+ },
+ info: 'In order to use Stadia maps, you must register. Once registered, you can whitelist your domain within your account settings.'
+ },
+ {
+ name: 'Stadia AlidadeDark',
+ url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png',
+ config: {
+ maxZoom: 20,
+ noWrap: true,
+ attribution: '© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors'
+ },
+ info: 'In order to use Stadia maps, you must register. Once registered, you can whitelist your domain within your account settings.'
+ },
+];
+
+var mapExtraLayers = [
+ {
+ name: 'OpenWeatherMap',
+ url: 'https://tile.openweathermap.org/map/{layer}/{z}/{x}/{y}.png?appid={apikey}',
+ options: {
+ layer: 'clouds_new',
+ attribution: 'Map data: © OpenWeatherMap'
+ },
+ depends: "weather_key"
+ },
+ {
+ name: 'OpenWeatherMap',
+ url: 'https://tile.openweathermap.org/map/{layer}/{z}/{x}/{y}.png?appid={apikey}',
+ options: {
+ layer: 'precipitation_new',
+ attribution: 'Map data: © OpenWeatherMap'
+ },
+ depends: "weather_key"
+ },
+ {
+ name: 'WeatherRadar-USA',
+ url: 'http://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/{z}/{x}/{y}.png',
+ options: {
+ attribution: 'Map data: © Iowa State University'
+ },
+ depends: "!weather_key"
+ },
+ {
+ name: 'OpenSeaMap',
+ url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
+ options: {
+ attribution: 'Map data: © OpenSeaMap contributors'
+ },
+ },
+];
+
+// reasonable default; will be overriden by server
+var retention_time = 2 * 60 * 60 * 1000;
+
+// Our Leaflet Map and layerControl
+var map = null;
+var layerControl;
+
+// Receiver location marker
+var receiverMarker = null;
+
+// Updates are queued here
+var updateQueue = [];
+
+// Web socket connection management, message processing
+var mapManager = new MapManager();
+
+var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){
+ var s = v.split('=');
+ var r = {};
+ r[s[0]] = s.slice(1).join('=');
+ return r;
+}).reduce(function(a, b){
+ return a.assign(b);
+});
+
+var expectedCallsign = query.callsign? decodeURIComponent(query.callsign) : null;
+var expectedLocator = query.locator? query.locator : null;
+
+// https://stackoverflow.com/a/46981806/420585
+function fetchStyleSheet(url, media = 'screen') {
+ let $dfd = $.Deferred(),
+ finish = () => $dfd.resolve(),
+ $link = $(document.createElement('link')).attr({
+ media,
+ type: 'text/css',
+ rel: 'stylesheet'
+ })
+ .on('load', 'error', finish)
+ .appendTo('head'),
+ $img = $(document.createElement('img'))
+ .on('error', finish); // Support browsers that don't fire events on link elements
+ $link[0].href = $img[0].src = url;
+ return $dfd.promise();
+}
+
+
+
+// Show information bubble for a locator
+function showLocatorInfoWindow(locator, pos) {
+ var p = new posObj(pos);
+
+ L.popup(pos, {
+ content: mapManager.lman.getInfoHTML(locator, p, receiverMarker)
+ }).openOn(map);
+};
+
+// Show information bubble for a marker
+function showMarkerInfoWindow(name, pos) {
+ var marker = mapManager.mman.find(name);
+ L.popup(pos, { content: marker.getInfoHTML(name, receiverMarker) }).openOn(map);
+};
+
+//
+// Leaflet-SPECIFIC MAP MANAGER METHODS
+//
+
+MapManager.prototype.setReceiverName = function(name) {
+ if (receiverMarker) receiverMarker.setTitle(name);
+}
+
+MapManager.prototype.removeReceiver = function() {
+ if (receiverMarker) receiverMarker.setMap();
+}
+
+MapManager.prototype.initializeMap = function(receiver_gps, api_key, weather_key) {
+ if (map) {
+ receiverMarker.setLatLng(receiver_gps.lat, receiver_gps.lon);
+ receiverMarker.setMarkerOptions(this.config);
+ receiverMarker.setMap(map);
+ } else {
+ var self = this;
+
+ // load Leaflet CSS first
+ fetchStyleSheet('https://unpkg.com/leaflet@1.9.4/dist/leaflet.css').done(function () {
+ // now load Leaflet JS
+ $.getScript('https://unpkg.com/leaflet@1.9.4/dist/leaflet.js').done(function () {
+ // create map
+ map = L.map('openwebrx-map', { zoomControl: false }).setView([receiver_gps.lat, receiver_gps.lon], 5);
+
+ // add zoom control
+ new L.Control.Zoom({ position: 'bottomright' }).addTo(map);
+
+ // add night overlay
+ $.getScript('https://unpkg.com/@joergdietrich/leaflet.terminator@1.0.0/L.Terminator.js').done(function () {
+ var pane = map.createPane('nite');
+ pane.style.zIndex = 201;
+ pane.style.pointerEvents = 'none !important';
+ pane.style.cursor = 'grab !important';
+ var t = L.terminator({ fillOpacity: 0.2, interactive: false, pane });
+ t.addTo(map);
+ setInterval(function () { t.setTime(); }, 10000); // refresh every 10 secs
+ });
+
+ // create layerControl and add more maps
+ if (!layerControl) {
+ // used to open or collaps the layerControl by default
+ // function isMobile () {
+ // try { document.createEvent("TouchEvent"); return true; }
+ // catch (e) { return false; }
+ // }
+
+ layerControl = L.control.layers({
+ }, null, {
+ collapsed: false, //isMobile(), // we have collapsing already made in the utc clock
+ hideSingleBase: true,
+ position: 'bottomleft'
+ }
+ ).addTo(map);
+
+ // move legend div to our layerControl
+ layerControl.legend = $('.openwebrx-map-legend')
+ .css({'padding': '0', 'margin': '0'})
+ .insertAfter(layerControl._overlaysList);
+ } // layerControl
+
+ // Load and initialize OWRX-specific map item managers
+ $.getScript('static/lib/Leaflet.js').done(function() {
+ // Process any accumulated updates
+ self.processUpdates(updateQueue);
+ updateQueue = [];
+
+ if (!receiverMarker) {
+ receiverMarker = new LMarker();
+ receiverMarker.setMarkerPosition(self.config['receiver_name'], receiver_gps.lat, receiver_gps.lon);
+ receiverMarker.addListener('click', function () {
+ L.popup(receiverMarker.getPos(), {
+ content: '' + self.config['receiver_name'] + '
' +
+ 'Receiver location
'
+ }).openOn(map);
+ });
+ receiverMarker.setMarkerOptions(this.config);
+ receiverMarker.setMap(map);
+ }
+ });
+
+ $.each(mapSources, function (idx, ms) {
+ $('#openwebrx-map-source').append(
+ $('')
+ .attr('selected', idx == 0 ? true : false)
+ .attr('value', idx)
+ .attr('title', ms.info)
+ .text(ms.name)
+ );
+ ms.layer = L.tileLayer(ms.url, ms.options);
+ if (idx == 0) ms.layer.addTo(map);
+ });
+
+ var apiKeys = {};
+ if (weather_key) {
+ apiKeys['weather_key'] = weather_key;
+ }
+
+ function isMapEligible (m) {
+ if (!m) return false;
+ if (!m.depends || !m.depends.length) return true; // if no depends -> true
+ var looking = m.depends;
+ var invert = false;
+ if (looking.charAt(0) === '!') {
+ invert = true;
+ looking = looking.slice(1);
+ }
+ var eligible = false; // we have deps, so default is false until we find the dep keys
+ Object.keys(apiKeys).forEach(function (k) {
+ if (looking === k) eligible = true; // if we have the key and depend on it -> true
+ });
+ return invert ? !eligible : eligible;
+ }
+
+ function addMapOverlay (name) {
+ $.each(mapExtraLayers, function (idx, mel) {
+ if (mel.name === name) {
+ if (!mel.layer) {
+ mel.options.apikey = apiKeys[mel.depends];
+ mel.layer = L.tileLayer(mel.url, mel.options);
+ }
+ if (map.hasLayer(mel.layer))
+ map.removeLayer(mel.layer);
+ map.addLayer(mel.layer);
+ }
+ });
+ }
+ function removeMapOverlay (name) {
+ $.each(mapExtraLayers, function (idx, mel) {
+ if (mel.name === name) {
+ if (map.hasLayer(mel.layer))
+ map.removeLayer(mel.layer);
+ }
+ });
+ }
+ $('#openwebrx-map-source').on('change', function (e) {
+ var id = this.value;
+ var m = mapSources[id];
+ $.each(mapSources, function (idx, ms) {
+ if (map.hasLayer(ms.layer))
+ map.removeLayer(ms.layer);
+ });
+ map.addLayer(m.layer);
+ $('#openwebrx-map-extralayers').find('input').each(function (idx, inp) {
+ if ($(inp).is(':checked')) {
+ addMapOverlay($(inp).attr('name'));
+ }
+ });
+ });
+ if (0) $.each(mapExtraLayers, function (idx, mel) { // AF: disabled and will be removed (with all the functions around this) upon accpeting the PR
+ if (!isMapEligible(mel)) return;
+ if ($('#openwebrx-map-layer-' + mel.name).length)
+ return; // checkbox with that name exists already
+ $('#openwebrx-map-extralayers').append(
+ $(''
+ ).on('change', function (e) {
+ if (e.target.checked) {
+ addMapOverlay(mel.name);
+ } else {
+ removeMapOverlay(mel.name);
+ }
+ })
+ );
+ });
+
+ // Create map legend selectors
+ self.setupLegendFilters(layerControl.legend);
+
+ }); // leaflet.js
+ }); // leaflet.css
+ }
+};
+
+MapManager.prototype.processUpdates = function(updates) {
+ var self = this;
+
+ if (typeof(LMarker) === 'undefined') {
+ updateQueue = updateQueue.concat(updates);
+ return;
+ }
+
+ updates.forEach(function(update) {
+ if (typeof update.source === 'undefined' || typeof update.source.callsign === 'undefined') {
+ console.error(update);
+ return;
+ }
+ var id = update.source.callsign + (update.source.ssid ? '-' + update.source.ssid : '');
+
+ switch (update.location.type) {
+ case 'latlon':
+ var marker = self.mman.find(id);
+ var aprsOptions = {}
+
+ if (update.location.symbol) {
+ aprsOptions.symbol = update.location.symbol;
+ aprsOptions.course = update.location.course;
+ aprsOptions.speed = update.location.speed;
+ }
+
+ // If new item, create a new marker for it
+ if (!marker) {
+ marker = new LAprsMarker();
+ self.mman.add(id, marker);
+ marker.addListener('click', function() {
+ showMarkerInfoWindow(id, marker.getPos());
+ });
+
+ // If displaying a symbol, create it
+ if (update.location.symbol) marker.onAdd();
+ }
+
+ // Keep track of new marker types as they may change
+ self.mman.addType(update.mode);
+
+ // Update marker attributes and age
+ marker.update(update);
+
+ // Assign marker to map
+ marker.setMap(self.mman.isEnabled(update.mode)? map : undefined);
+
+ // Apply marker options
+ marker.setMarkerOptions(aprsOptions);
+
+ if (expectedCallsign && expectedCallsign == id) {
+ map.setView(marker.getPos());
+ showMarkerInfoWindow(id, marker.getPos());
+ expectedCallsign = false;
+ }
+ break;
+
+ case 'feature':
+ var marker = self.mman.find(id);
+ var options = {};
+
+ // If no symbol or color supplied, use defaults by type
+ if (update.location.symbol) {
+ options.symbol = update.location.symbol;
+ } else {
+ options.symbol = self.mman.getSymbol(update.mode);
+ }
+ if (update.location.color) {
+ options.color = update.location.color;
+ } else {
+ options.color = self.mman.getColor(update.mode);
+ }
+
+ // If new item, create a new marker for it
+ if (!marker) {
+ marker = new LFeatureMarker();
+ marker.div = marker.create();
+ var offset = marker.getAnchorOffset();
+ marker.setIcon(L.divIcon({
+ html: marker.div,
+ iconAnchor: [-offset[1], -offset[0]],
+ className: 'dummy'
+ }));
+
+ self.mman.addType(update.mode);
+ self.mman.add(id, marker);
+ marker.addListener('click', function() {
+ showMarkerInfoWindow(id, marker.getPos());
+ });
+ }
+
+ // Update marker attributes and age
+ marker.update(update);
+
+ // Assign marker to map
+ marker.setMap(self.mman.isEnabled(update.mode)? map : undefined);
+
+ // Apply marker options
+ marker.setMarkerOptions(options);
+
+ if (expectedCallsign && expectedCallsign == id) {
+ map.setView(marker.getPos());
+ showMarkerInfoWindow(id, marker.getPos());
+ expectedCallsign = false;
+ }
+ break;
+
+ case 'locator':
+ var rectangle = self.lman.find(id);
+
+ // If new item, create a new locator for it
+ if (!rectangle) {
+ rectangle = new LLocator();
+ self.lman.add(id, rectangle);
+ rectangle.addListener('click', function() {
+ showLocatorInfoWindow(rectangle.locator, rectangle.center);
+ });
+ }
+
+ // Update locator attributes, center, age
+ rectangle.update(update);
+
+ // Assign locator to map and set its color
+ rectangle.setMap(self.lman.filter(rectangle)? map : undefined);
+ rectangle.setColor(self.lman.getColor(rectangle));
+
+ if (expectedLocator && expectedLocator == update.location.locator) {
+ map.setView(rectangle.center);
+ showLocatorInfoWindow(expectedLocator, rectangle.center);
+ expectedLocator = false;
+ }
+ break;
+ }
+ });
+};
diff --git a/owrx/config/defaults.py b/owrx/config/defaults.py
index 7deeed4a..33acc219 100644
--- a/owrx/config/defaults.py
+++ b/owrx/config/defaults.py
@@ -147,6 +147,7 @@ defaultConfig = PropertyLayer(
tuning_precision=2,
squelch_auto_margin=10,
google_maps_api_key="",
+ map_type="google",
map_position_retention_time=2 * 60 * 60,
decoding_queue_workers=2,
decoding_queue_length=10,
diff --git a/owrx/connection.py b/owrx/connection.py
index 31d19ba6..4ab05e7f 100644
--- a/owrx/connection.py
+++ b/owrx/connection.py
@@ -458,6 +458,7 @@ class MapConnection(OpenWebRxClient):
filtered_config = pm.filter(
"google_maps_api_key",
"receiver_gps",
+ "map_type",
"callsign_service",
"aircraft_tracking_service",
"receiver_name",
diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py
index d9a273fa..57f06986 100644
--- a/owrx/controllers/assets.py
+++ b/owrx/controllers/assets.py
@@ -138,12 +138,32 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/Modes.js",
"lib/MetaPanel.js",
],
- "map.js": [
+ "map.js": [ # AF: to be removed once PR is accepted
"lib/jquery-3.2.1.min.js",
"lib/chroma.min.js",
"lib/Header.js",
"map.js",
],
+ "map-google.js": [
+ "lib/jquery-3.2.1.min.js",
+ "lib/chroma.min.js",
+ "lib/Header.js",
+ "lib/MapLocators.js",
+ "lib/MapMarkers.js",
+ "lib/MapManager.js",
+ "lib/Clock.js",
+ "map-google.js",
+ ],
+ "map-leaflet.js": [
+ "lib/jquery-3.2.1.min.js",
+ "lib/chroma.min.js",
+ "lib/Header.js",
+ "lib/MapLocators.js",
+ "lib/MapMarkers.js",
+ "lib/MapManager.js",
+ "lib/Clock.js",
+ "map-leaflet.js",
+ ],
"settings.js": [
"lib/jquery-3.2.1.min.js",
"lib/bootstrap.bundle.min.js",
diff --git a/owrx/controllers/settings/general.py b/owrx/controllers/settings/general.py
index 21ac8284..89254b53 100644
--- a/owrx/controllers/settings/general.py
+++ b/owrx/controllers/settings/general.py
@@ -158,6 +158,14 @@ class GeneralSettingsController(SettingsFormController):
),
Section(
"Map settings",
+ DropdownInput(
+ "map_type",
+ "Map type",
+ options=[
+ Option("google", "Google Maps"),
+ Option("leaflet", "OpenStreetMap, etc."),
+ ],
+ ),
TextInput(
"google_maps_api_key",
"Google Maps API key",
diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py
index f7e1a530..0d092520 100644
--- a/owrx/controllers/template.py
+++ b/owrx/controllers/template.py
@@ -1,5 +1,6 @@
from owrx.controllers import Controller
from owrx.details import ReceiverDetails
+from owrx.config import Config
from string import Template
import pkg_resources
@@ -42,4 +43,27 @@ class IndexController(WebpageController):
class MapController(WebpageController):
def indexAction(self):
# TODO check if we have a google maps api key first?
- self.serve_template("map.html", **self.template_variables())
+ # self.serve_template("map.html", **self.template_variables()) # AF: to be removed once the PR is accepted.
+ self.serve_template("map-%s.html" % self.map_type(), **self.template_variables())
+
+ def header_variables(self):
+ # Invert map type for the "map" toolbar icon
+ variables = super().header_variables();
+ type = self.map_type()
+ if type == "google":
+ variables.update({ "map_type" : "?type=leaflet" })
+ elif type == "leaflet":
+ variables.update({ "map_type" : "?type=google" })
+ return variables
+
+ def map_type(self):
+ pm = Config.get()
+ if "type" not in self.request.query:
+ type = pm["map_type"]
+ else:
+ type = self.request.query["type"][0]
+ if type not in ["google", "leaflet"]:
+ type = pm["map_type"]
+ return type
+
+
From 871cb6efd6d8051546861faa937cfb48ac5a6436 Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Tue, 19 Sep 2023 02:56:53 +0300
Subject: [PATCH 02/12] add missing map_type var
---
owrx/controllers/template.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py
index 0d092520..22723c9f 100644
--- a/owrx/controllers/template.py
+++ b/owrx/controllers/template.py
@@ -26,7 +26,7 @@ class WebpageController(TemplateController):
return "../" * levels
def header_variables(self):
- variables = {"document_root": self.get_document_root()}
+ variables = { "document_root": self.get_document_root(), "map_type": "" }
variables.update(ReceiverDetails().__dict__())
return variables
From c5c6cd98601bd69538d7c75da2fc3fde31786976 Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Tue, 19 Sep 2023 03:02:09 +0300
Subject: [PATCH 03/12] remove receivers and stations from feature markers
---
htdocs/lib/MapMarkers.js | 40 +++-------------------------------------
1 file changed, 3 insertions(+), 37 deletions(-)
diff --git a/htdocs/lib/MapMarkers.js b/htdocs/lib/MapMarkers.js
index 5120c3ac..fc61b033 100644
--- a/htdocs/lib/MapMarkers.js
+++ b/htdocs/lib/MapMarkers.js
@@ -11,9 +11,6 @@ function MarkerManager() {
// Colors used for marker types
this.colors = {
- 'KiwiSDR' : '#800000',
- 'WebSDR' : '#000080',
- 'OpenWebRX' : '#004000',
'HFDL' : '#004000',
'VDL2' : '#000080',
'ADSB' : '#800000',
@@ -22,11 +19,6 @@ function MarkerManager() {
// Symbols used for marker types
this.symbols = {
- 'KiwiSDR' : '◬',
- 'WebSDR' : '◬',
- 'OpenWebRX' : '◬',
- 'Stations' : '⍑', //'◎',
- 'Repeaters' : '⋈',
'APRS' : '⚐',
'AIS' : '⩯',
'HFDL' : '✈',
@@ -37,11 +29,7 @@ function MarkerManager() {
// Marker type shown/hidden status
this.enabled = {
- 'KiwiSDR' : false,
- 'WebSDR' : false,
- 'OpenWebRX' : false,
- 'Stations' : false,
- 'Repeaters' : false
+ 'APRS' : true
};
}
@@ -231,7 +219,6 @@ Marker.prototype.remove = function() {
//
// Feature Marker
-// Represents static map features, such as stations and receivers.
// Derived classes have to implement:
// setMarkerOpacity()
//
@@ -245,14 +232,7 @@ FeatureMarker.prototype.update = function(update) {
this.mode = update.mode;
this.url = update.location.url;
this.comment = update.location.comment;
- // Receivers
this.altitude = update.location.altitude;
- this.device = update.location.device;
- this.antenna = update.location.antenna;
- // EIBI
- this.schedule = update.location.schedule;
- // Repeaters
- this.freq = update.location.freq;
this.status = update.location.status;
this.updated = update.location.updated;
this.mmode = update.location.mmode;
@@ -298,7 +278,7 @@ FeatureMarker.prototype.getAnchorOffset = function() {
return [0, -this.symHeight/2];
};
-FeatureMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
+FeatureMarker.prototype.getInfoHTML = function(name) {
var nameString = this.url? Marker.linkify(name, this.url) : name;
var commentString = this.comment? '' + this.comment + '
' : '';
var detailsString = '';
@@ -380,10 +360,6 @@ FeatureMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
scheduleString = '' + Marker.makeListTitle('Schedule') + scheduleString + '
';
}
- if (receiverMarker) {
- distance = ' at ' + Marker.distanceKm(receiverMarker.position, this.position) + ' km';
- }
-
return '' + nameString + distance + '
'
+ commentString + detailsString + scheduleString;
};
@@ -512,7 +488,7 @@ AprsMarker.prototype.getAnchorOffset = function() {
return [0, -12];
};
-AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
+AprsMarker.prototype.getInfoHTML = function(name) {
var timeString = moment(this.lastseen).fromNow();
var commentString = '';
var weatherString = '';
@@ -568,12 +544,6 @@ AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
weatherString += '';
}
- if (this.device) {
- detailsString += Marker.makeListItem('Device', this.device.manufacturer?
- this.device.device + ' by ' + this.device.manufacturer : this.device
- );
- }
-
if (this.height) {
detailsString += Marker.makeListItem('Height', this.height.toFixed(0) + ' m');
}
@@ -633,10 +603,6 @@ AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
detailsString = '' + Marker.makeListTitle('Details') + detailsString + '
';
}
- if (receiverMarker) {
- distance = ' at ' + Marker.distanceKm(receiverMarker.position, this.position) + ' km';
- }
-
if (this.hops && this.hops.length > 0) {
var hops = this.hops.toString().split(',');
hops.forEach(function(part, index, hops) {
From ddda9f10d9332ed9a264756791551f1646361fca Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Tue, 19 Sep 2023 03:09:09 +0300
Subject: [PATCH 04/12] remove the plus sign from the name
---
htdocs/map-google.html | 2 +-
htdocs/map-leaflet.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/htdocs/map-google.html b/htdocs/map-google.html
index 570a649b..21a536e2 100644
--- a/htdocs/map-google.html
+++ b/htdocs/map-google.html
@@ -1,7 +1,7 @@
- OpenWebRX+ Map
+ OpenWebRX Map
diff --git a/htdocs/map-leaflet.html b/htdocs/map-leaflet.html
index 1e24bfff..930bf2c3 100644
--- a/htdocs/map-leaflet.html
+++ b/htdocs/map-leaflet.html
@@ -1,7 +1,7 @@
- OpenWebRX+ Map
+ OpenWebRX Map
From 900fbafaa44b2562a147825f35aa31f10a182181 Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Tue, 19 Sep 2023 03:13:33 +0300
Subject: [PATCH 05/12] remove the extra overlays (weather, seamap) for leaflet
---
htdocs/map-leaflet.js | 82 -------------------------------------------
1 file changed, 82 deletions(-)
diff --git a/htdocs/map-leaflet.js b/htdocs/map-leaflet.js
index 5a73795a..45c2bb53 100644
--- a/htdocs/map-leaflet.js
+++ b/htdocs/map-leaflet.js
@@ -110,42 +110,6 @@ var mapSources = [
},
];
-var mapExtraLayers = [
- {
- name: 'OpenWeatherMap',
- url: 'https://tile.openweathermap.org/map/{layer}/{z}/{x}/{y}.png?appid={apikey}',
- options: {
- layer: 'clouds_new',
- attribution: 'Map data: © OpenWeatherMap'
- },
- depends: "weather_key"
- },
- {
- name: 'OpenWeatherMap',
- url: 'https://tile.openweathermap.org/map/{layer}/{z}/{x}/{y}.png?appid={apikey}',
- options: {
- layer: 'precipitation_new',
- attribution: 'Map data: © OpenWeatherMap'
- },
- depends: "weather_key"
- },
- {
- name: 'WeatherRadar-USA',
- url: 'http://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/{z}/{x}/{y}.png',
- options: {
- attribution: 'Map data: © Iowa State University'
- },
- depends: "!weather_key"
- },
- {
- name: 'OpenSeaMap',
- url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
- options: {
- attribution: 'Map data: © OpenSeaMap contributors'
- },
- },
-];
-
// reasonable default; will be overriden by server
var retention_time = 2 * 60 * 60 * 1000;
@@ -324,27 +288,6 @@ MapManager.prototype.initializeMap = function(receiver_gps, api_key, weather_key
return invert ? !eligible : eligible;
}
- function addMapOverlay (name) {
- $.each(mapExtraLayers, function (idx, mel) {
- if (mel.name === name) {
- if (!mel.layer) {
- mel.options.apikey = apiKeys[mel.depends];
- mel.layer = L.tileLayer(mel.url, mel.options);
- }
- if (map.hasLayer(mel.layer))
- map.removeLayer(mel.layer);
- map.addLayer(mel.layer);
- }
- });
- }
- function removeMapOverlay (name) {
- $.each(mapExtraLayers, function (idx, mel) {
- if (mel.name === name) {
- if (map.hasLayer(mel.layer))
- map.removeLayer(mel.layer);
- }
- });
- }
$('#openwebrx-map-source').on('change', function (e) {
var id = this.value;
var m = mapSources[id];
@@ -353,32 +296,7 @@ MapManager.prototype.initializeMap = function(receiver_gps, api_key, weather_key
map.removeLayer(ms.layer);
});
map.addLayer(m.layer);
- $('#openwebrx-map-extralayers').find('input').each(function (idx, inp) {
- if ($(inp).is(':checked')) {
- addMapOverlay($(inp).attr('name'));
- }
- });
});
- if (0) $.each(mapExtraLayers, function (idx, mel) { // AF: disabled and will be removed (with all the functions around this) upon accpeting the PR
- if (!isMapEligible(mel)) return;
- if ($('#openwebrx-map-layer-' + mel.name).length)
- return; // checkbox with that name exists already
- $('#openwebrx-map-extralayers').append(
- $(''
- ).on('change', function (e) {
- if (e.target.checked) {
- addMapOverlay(mel.name);
- } else {
- removeMapOverlay(mel.name);
- }
- })
- );
- });
// Create map legend selectors
self.setupLegendFilters(layerControl.legend);
From dc390aa8b74f891609ef4ad203bf4d8d8a93d71f Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Tue, 19 Sep 2023 03:48:37 +0300
Subject: [PATCH 06/12] add arrow to toggle legend pane
---
htdocs/css/map.css | 18 ++++++++++++++++++
htdocs/lib/MapManager.js | 13 +++++++++----
htdocs/map-google.html | 5 ++++-
htdocs/map-leaflet.html | 5 ++++-
4 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/htdocs/css/map.css b/htdocs/css/map.css
index 70702b96..2ff1e6a2 100644
--- a/htdocs/css/map.css
+++ b/htdocs/css/map.css
@@ -30,6 +30,24 @@ ul {
user-select: none;
}
+.openwebrx-arrow-up {
+ margin-top: 5px;
+ width: 0;
+ height: 0;
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-bottom: 10px solid black;
+}
+
+.openwebrx-arrow-down {
+ margin-top: 5px;
+ width: 0;
+ height: 0;
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-top: 10px solid black;
+}
+
/* show it as soon as google maps has moved it to its container */
.openwebrx-map .openwebrx-map-legend {
display: block;
diff --git a/htdocs/lib/MapManager.js b/htdocs/lib/MapManager.js
index 28d0ebb2..973ef1e7 100644
--- a/htdocs/lib/MapManager.js
+++ b/htdocs/lib/MapManager.js
@@ -28,16 +28,21 @@ function MapManager() {
}, 1000);
// When stuff loads...
- $(function() {
+ $(function () {
// Create clock display
self.clock = new Clock($('#openwebrx-clock-utc'));
// Clicking clock display toggles legend box on/off
- $('#openwebrx-clock-utc').css('cursor', 'pointer').on('click', function() {
+ $('#openwebrx-legend-toggle').css({
+ 'cursor': 'pointer',
+ 'display': 'flex',
+ 'justify-content': 'space-between'
+ }).on('click', function () {
var el = document.getElementById('openwebrx-map-selectors');
if (el) {
- el.style.display = el.style.display === 'none'?
- 'block' : 'none';
+ $(this).find('i').removeClass()
+ .addClass('openwebrx-arrow-' + (el.style.display === 'none' ? 'down' : 'up'));
+ el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
});
diff --git a/htdocs/map-google.html b/htdocs/map-google.html
index 21a536e2..de382c6a 100644
--- a/htdocs/map-google.html
+++ b/htdocs/map-google.html
@@ -27,7 +27,10 @@
- 00:00 UTC
+
+
+
00:00 UTC
+
diff --git a/htdocs/map-leaflet.html b/htdocs/map-leaflet.html
index 930bf2c3..6ec55dba 100644
--- a/htdocs/map-leaflet.html
+++ b/htdocs/map-leaflet.html
@@ -30,7 +30,10 @@
- 00:00 UTC
+
+
+
00:00 UTC
+
From 26e4875ce447745e5ea33dbf3c658d1edb212504 Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Tue, 19 Sep 2023 04:07:26 +0300
Subject: [PATCH 07/12] port Marat's fix for divs
---
htdocs/lib/MapMarkers.js | 59 ++++++++--------------------------------
1 file changed, 12 insertions(+), 47 deletions(-)
diff --git a/htdocs/lib/MapMarkers.js b/htdocs/lib/MapMarkers.js
index fc61b033..973deef1 100644
--- a/htdocs/lib/MapMarkers.js
+++ b/htdocs/lib/MapMarkers.js
@@ -280,7 +280,7 @@ FeatureMarker.prototype.getAnchorOffset = function() {
FeatureMarker.prototype.getInfoHTML = function(name) {
var nameString = this.url? Marker.linkify(name, this.url) : name;
- var commentString = this.comment? '' + this.comment + '
' : '';
+ var commentString = this.comment? '' + this.comment + '
' : '';
var detailsString = '';
var scheduleString = '';
var distance = '';
@@ -294,29 +294,13 @@ FeatureMarker.prototype.getInfoHTML = function(name) {
detailsString += Marker.makeListItem('Altitude', this.altitude.toFixed(0) + ' m');
}
- if (this.device) {
- detailsString += Marker.makeListItem('Device', this.device.manufacturer?
- this.device.device + ' by ' + this.device.manufacturer : this.device
- );
- }
-
- if (this.antenna) {
- detailsString += Marker.makeListItem('Antenna', Marker.truncate(this.antenna, 24));
- }
-
- if (this.freq) {
- detailsString += Marker.makeListItem('Frequency', Marker.linkifyFreq(
- this.freq, this.mmode? this.mmode:'fm'
- ));
- }
-
if (this.mmode) {
detailsString += Marker.makeListItem('Modulation', this.mmode.toUpperCase());
}
if (!this.comment && this.status && this.updated) {
- commentString = '' + this.status
- + ', last updated on ' + this.updated + '
';
+ commentString = '' + this.status
+ + ', last updated on ' + this.updated + '
';
} else {
if (this.status) {
detailsString += Marker.makeListItem('Status', this.status);
@@ -333,31 +317,12 @@ FeatureMarker.prototype.getInfoHTML = function(name) {
});
}
- if (this.schedule) {
- for (var j=0 ; j 0) {
- detailsString = '' + Marker.makeListTitle('Details') + detailsString + '
';
+ detailsString = '' + Marker.makeListTitle('Details') + detailsString + '
';
}
if (scheduleString.length > 0) {
- scheduleString = '' + Marker.makeListTitle('Schedule') + scheduleString + '
';
+ scheduleString = '' + Marker.makeListTitle('Schedule') + scheduleString + '
';
}
return '' + nameString + distance + '
'
@@ -497,12 +462,12 @@ AprsMarker.prototype.getInfoHTML = function(name) {
var distance = '';
if (this.comment) {
- commentString += '' + Marker.makeListTitle('Comment') + '
' +
- this.comment + '
';
+ commentString += '' + Marker.makeListTitle('Comment') + '' +
+ this.comment + '
';
}
if (this.weather) {
- weatherString += '' + Marker.makeListTitle('Weather');
+ weatherString += '
' + Marker.makeListTitle('Weather');
if (this.weather.temperature) {
weatherString += Marker.makeListItem('Temperature', this.weather.temperature.toFixed(1) + ' oC');
@@ -541,7 +506,7 @@ AprsMarker.prototype.getInfoHTML = function(name) {
weatherString += Marker.makeListItem('Snow', this.weather.snowfall.toFixed(1) + ' cm');
}
- weatherString += '';
+ weatherString += '
';
}
if (this.height) {
@@ -600,7 +565,7 @@ AprsMarker.prototype.getInfoHTML = function(name) {
}
if (detailsString.length > 0) {
- detailsString = '' + Marker.makeListTitle('Details') + detailsString + '
';
+ detailsString = '' + Marker.makeListTitle('Details') + detailsString + '
';
}
if (this.hops && this.hops.length > 0) {
@@ -640,7 +605,7 @@ AprsMarker.prototype.getInfoHTML = function(name) {
}
return '' + Marker.linkify(name, url, linkEntity) + distance + '
'
- + '' + timeString + ' using ' + this.mode
- + ( this.band ? ' on ' + this.band : '' ) + '
'
+ + '' + timeString + ' using ' + this.mode
+ + ( this.band ? ' on ' + this.band : '' ) + '
'
+ commentString + weatherString + detailsString + hopsString;
};
From d7822233fc7c5410155c2df8b6ee3af688b8d3f6 Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Tue, 19 Sep 2023 04:11:40 +0300
Subject: [PATCH 08/12] address string formating (port Marat's fix)
---
owrx/controllers/template.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py
index 22723c9f..118ee3f1 100644
--- a/owrx/controllers/template.py
+++ b/owrx/controllers/template.py
@@ -44,7 +44,7 @@ class MapController(WebpageController):
def indexAction(self):
# TODO check if we have a google maps api key first?
# self.serve_template("map.html", **self.template_variables()) # AF: to be removed once the PR is accepted.
- self.serve_template("map-%s.html" % self.map_type(), **self.template_variables())
+ self.serve_template("map-{}.html".format(self.map_type()), **self.template_variables())
def header_variables(self):
# Invert map type for the "map" toolbar icon
From 26171a69f3a6b00274cd84cbea2858757e755d7b Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Thu, 21 Sep 2023 00:37:14 +0300
Subject: [PATCH 09/12] bring back distance from receiver marker
---
htdocs/lib/MapMarkers.js | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/htdocs/lib/MapMarkers.js b/htdocs/lib/MapMarkers.js
index 973deef1..f3100803 100644
--- a/htdocs/lib/MapMarkers.js
+++ b/htdocs/lib/MapMarkers.js
@@ -278,7 +278,7 @@ FeatureMarker.prototype.getAnchorOffset = function() {
return [0, -this.symHeight/2];
};
-FeatureMarker.prototype.getInfoHTML = function(name) {
+FeatureMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
var nameString = this.url? Marker.linkify(name, this.url) : name;
var commentString = this.comment? '' + this.comment + '
' : '';
var detailsString = '';
@@ -325,6 +325,10 @@ FeatureMarker.prototype.getInfoHTML = function(name) {
scheduleString = '' + Marker.makeListTitle('Schedule') + scheduleString + '
';
}
+ if (receiverMarker) {
+ distance = ' at ' + Marker.distanceKm(receiverMarker.position, this.position) + ' km';
+ }
+
return '' + nameString + distance + '
'
+ commentString + detailsString + scheduleString;
};
@@ -453,7 +457,7 @@ AprsMarker.prototype.getAnchorOffset = function() {
return [0, -12];
};
-AprsMarker.prototype.getInfoHTML = function(name) {
+AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
var timeString = moment(this.lastseen).fromNow();
var commentString = '';
var weatherString = '';
@@ -568,6 +572,10 @@ AprsMarker.prototype.getInfoHTML = function(name) {
detailsString = '' + Marker.makeListTitle('Details') + detailsString + '
';
}
+ if (receiverMarker) {
+ distance = ' at ' + Marker.distanceKm(receiverMarker.position, this.position) + ' km';
+ }
+
if (this.hops && this.hops.length > 0) {
var hops = this.hops.toString().split(',');
hops.forEach(function(part, index, hops) {
From 96c6863c93253a842fd7d7956bd634363d3f7fcb Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Thu, 21 Sep 2023 00:54:04 +0300
Subject: [PATCH 10/12] add custom retention time to markers from location.ttl
---
htdocs/lib/MapLocators.js | 8 ++++----
htdocs/lib/MapMarkers.js | 16 ++++++++--------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/htdocs/lib/MapLocators.js b/htdocs/lib/MapLocators.js
index e7236d85..84d1744d 100644
--- a/htdocs/lib/MapLocators.js
+++ b/htdocs/lib/MapLocators.js
@@ -194,12 +194,12 @@ Locator.prototype.update = function(update) {
this.setCenter(lat, lon);
// Age locator
- this.age(new Date().getTime() - update.lastseen);
+ this.age(new Date().getTime() - update.lastseen, update.location.ttl);
};
-Locator.prototype.age = function(age) {
- if (age <= retention_time) {
- this.setOpacity(Marker.getOpacityScale(age));
+Locator.prototype.age = function(age, ttl=retention_time) {
+ if (age <= ttl) {
+ this.setOpacity(Marker.getOpacityScale(age, ttl));
return true;
} else {
this.setMap();
diff --git a/htdocs/lib/MapMarkers.js b/htdocs/lib/MapMarkers.js
index f3100803..e2d7a83e 100644
--- a/htdocs/lib/MapMarkers.js
+++ b/htdocs/lib/MapMarkers.js
@@ -188,10 +188,10 @@ Marker.makeListItem = function(name, value) {
};
// Get opacity value in the 0..1 range based on the given age.
-Marker.getOpacityScale = function(age) {
+Marker.getOpacityScale = function(age, ttl=retention_time) {
var scale = 1;
- if (age >= retention_time / 2) {
- scale = (retention_time - age) / (retention_time / 2);
+ if (age >= ttl / 2) {
+ scale = (ttl - age) / (ttl / 2);
}
return Math.max(0, Math.min(1, scale));
};
@@ -199,9 +199,9 @@ Marker.getOpacityScale = function(age) {
// Set marker's opacity based on the supplied age. Returns TRUE
// if the marker should still be visible, FALSE if it has to be
// removed.
-Marker.prototype.age = function(age) {
- if(age <= retention_time) {
- this.setMarkerOpacity(Marker.getOpacityScale(age));
+Marker.prototype.age = function(age, ttl=retention_time) {
+ if(age <= ttl) {
+ this.setMarkerOpacity(Marker.getOpacityScale(age, ttl));
return true;
} else {
this.setMap();
@@ -243,7 +243,7 @@ FeatureMarker.prototype.update = function(update) {
this.setMarkerPosition(update.callsign, update.location.lat, update.location.lon);
// Age locator
- this.age(new Date().getTime() - update.lastseen);
+ this.age(new Date().getTime() - update.lastseen, update.location.ttl);
};
FeatureMarker.prototype.draw = function() {
@@ -369,7 +369,7 @@ AprsMarker.prototype.update = function(update) {
this.setMarkerPosition(update.callsign, update.location.lat, update.location.lon);
// Age locator
- this.age(new Date().getTime() - update.lastseen);
+ this.age(new Date().getTime() - update.lastseen, update.location.ttl);
};
AprsMarker.prototype.isFacingEast = function(symbol) {
From 970f7476e3cf5c8495f52c0ad97eb6b57137ef11 Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Thu, 21 Sep 2023 01:57:09 +0300
Subject: [PATCH 11/12] Preliminary work on airplane markers
---
htdocs/map-google.js | 33 +++++++++++++++++++++----
htdocs/map-leaflet.js | 57 ++++++++++++++++++++++++++++++-------------
2 files changed, 68 insertions(+), 22 deletions(-)
diff --git a/htdocs/map-google.js b/htdocs/map-google.js
index af48416f..c89e8d25 100644
--- a/htdocs/map-google.js
+++ b/htdocs/map-google.js
@@ -33,6 +33,7 @@ var query = window.location.search.replace(/^\?/, '').split('&').map(function(v)
var expectedCallsign = query.callsign? decodeURIComponent(query.callsign) : null;
var expectedLocator = query.locator? query.locator : null;
+var expectedIcao = query.icao? query.icao: null;
// Get information bubble window
function getInfoWindow() {
@@ -78,6 +79,21 @@ function showReceiverInfoWindow(marker) {
iw.open(map, marker);
};
+var sourceToKey = function(source) {
+ // special treatment for special entities
+ // not just for display but also in key treatment in order not to overlap with other locations sent by the same callsign
+ if ('item' in source) return source['item'];
+ if ('object' in source) return source['object'];
+ if ('icao' in source) return source['icao'];
+ if ('flight' in source) return source['flight'];
+ var key = source.callsign;
+ if ('ssid' in source) key += '-' + source.ssid;
+ return key;
+};
+
+// we can reuse the same logic for displaying and indexing
+var sourceToString = sourceToKey;
+
//
// GOOGLE-SPECIFIC MAP MANAGER METHODS
//
@@ -156,11 +172,7 @@ MapManager.prototype.processUpdates = function(updates) {
}
updates.forEach(function(update) {
- if (typeof update.source === 'undefined' || typeof update.source.callsign === 'undefined') {
- console.error(update);
- return;
- }
- var id = update.source.callsign + (update.source.ssid ? '-' + update.source.ssid : '');
+ var key = sourceToKey(update.source);
switch (update.location.type) {
case 'latlon':
@@ -177,6 +189,11 @@ MapManager.prototype.processUpdates = function(updates) {
// If new item, create a new marker for it
if (!marker) {
+ // AF: here shall be created ICAO markers for planes.
+ // either by adapting the PlaneMarker.js or by reusing the AprsMarkers as in OWRX+
+ // I'll leave this to someone more competent or will try to implement it myself
+ // when I have the time to spend to understand how.
+ // As of now, the planes are shown on the map, but with default icon.
marker = new markerClass();
self.mman.add(id, marker);
marker.addListener('click', function() {
@@ -196,6 +213,12 @@ MapManager.prototype.processUpdates = function(updates) {
// Apply marker options
marker.setMarkerOptions(aprsOptions);
+ if (expectedIcao && expectedIcao === update.source.icao) {
+ map.panTo(marker.position);
+ showMarkerInfoWindow(id, marker.position);
+ expectedIcao = false;
+ }
+
if (expectedCallsign && expectedCallsign == id) {
map.panTo(marker.position);
showMarkerInfoWindow(id, marker.position);
diff --git a/htdocs/map-leaflet.js b/htdocs/map-leaflet.js
index 45c2bb53..17c3a179 100644
--- a/htdocs/map-leaflet.js
+++ b/htdocs/map-leaflet.js
@@ -137,6 +137,7 @@ var query = window.location.search.replace(/^\?/, '').split('&').map(function(v)
var expectedCallsign = query.callsign? decodeURIComponent(query.callsign) : null;
var expectedLocator = query.locator? query.locator : null;
+var expectedIcao = query.icao? query.icao: null;
// https://stackoverflow.com/a/46981806/420585
function fetchStyleSheet(url, media = 'screen') {
@@ -172,6 +173,21 @@ function showMarkerInfoWindow(name, pos) {
L.popup(pos, { content: marker.getInfoHTML(name, receiverMarker) }).openOn(map);
};
+var sourceToKey = function(source) {
+ // special treatment for special entities
+ // not just for display but also in key treatment in order not to overlap with other locations sent by the same callsign
+ if ('item' in source) return source['item'];
+ if ('object' in source) return source['object'];
+ if ('icao' in source) return source['icao'];
+ if ('flight' in source) return source['flight'];
+ var key = source.callsign;
+ if ('ssid' in source) key += '-' + source.ssid;
+ return key;
+};
+
+// we can reuse the same logic for displaying and indexing
+var sourceToString = sourceToKey;
+
//
// Leaflet-SPECIFIC MAP MANAGER METHODS
//
@@ -315,15 +331,11 @@ MapManager.prototype.processUpdates = function(updates) {
}
updates.forEach(function(update) {
- if (typeof update.source === 'undefined' || typeof update.source.callsign === 'undefined') {
- console.error(update);
- return;
- }
- var id = update.source.callsign + (update.source.ssid ? '-' + update.source.ssid : '');
+ var key = sourceToKey(update.source);
switch (update.location.type) {
case 'latlon':
- var marker = self.mman.find(id);
+ var marker = self.mman.find(key);
var aprsOptions = {}
if (update.location.symbol) {
@@ -334,10 +346,15 @@ MapManager.prototype.processUpdates = function(updates) {
// If new item, create a new marker for it
if (!marker) {
+ // AF: here shall be created ICAO markers for planes.
+ // either by adapting the PlaneMarker.js or by reusing the AprsMarkers as in OWRX+
+ // I'll leave this to someone more competent or will try to implement it myself
+ // when I have the time to spend to understand how.
+ // As of now, the planes are shown on the map, but with default icon.
marker = new LAprsMarker();
- self.mman.add(id, marker);
+ self.mman.add(key, marker);
marker.addListener('click', function() {
- showMarkerInfoWindow(id, marker.getPos());
+ showMarkerInfoWindow(key, marker.getPos());
});
// If displaying a symbol, create it
@@ -356,15 +373,21 @@ MapManager.prototype.processUpdates = function(updates) {
// Apply marker options
marker.setMarkerOptions(aprsOptions);
- if (expectedCallsign && expectedCallsign == id) {
+ if (expectedIcao && expectedIcao === key) {
map.setView(marker.getPos());
- showMarkerInfoWindow(id, marker.getPos());
+ showMarkerInfoWindow(key, marker.getPos());
+ expectedIcao = false;
+ }
+
+ if (expectedCallsign && expectedCallsign == key) {
+ map.setView(marker.getPos());
+ showMarkerInfoWindow(key, marker.getPos());
expectedCallsign = false;
}
break;
case 'feature':
- var marker = self.mman.find(id);
+ var marker = self.mman.find(key);
var options = {};
// If no symbol or color supplied, use defaults by type
@@ -391,9 +414,9 @@ MapManager.prototype.processUpdates = function(updates) {
}));
self.mman.addType(update.mode);
- self.mman.add(id, marker);
+ self.mman.add(key, marker);
marker.addListener('click', function() {
- showMarkerInfoWindow(id, marker.getPos());
+ showMarkerInfoWindow(key, marker.getPos());
});
}
@@ -406,20 +429,20 @@ MapManager.prototype.processUpdates = function(updates) {
// Apply marker options
marker.setMarkerOptions(options);
- if (expectedCallsign && expectedCallsign == id) {
+ if (expectedCallsign && expectedCallsign == key) {
map.setView(marker.getPos());
- showMarkerInfoWindow(id, marker.getPos());
+ showMarkerInfoWindow(key, marker.getPos());
expectedCallsign = false;
}
break;
case 'locator':
- var rectangle = self.lman.find(id);
+ var rectangle = self.lman.find(key);
// If new item, create a new locator for it
if (!rectangle) {
rectangle = new LLocator();
- self.lman.add(id, rectangle);
+ self.lman.add(key, rectangle);
rectangle.addListener('click', function() {
showLocatorInfoWindow(rectangle.locator, rectangle.center);
});
From 8826be467eb69a1fe87d19e8c8c9446d6b8a9c16 Mon Sep 17 00:00:00 2001
From: "Stanislav Lechev [0xAF]"
Date: Thu, 21 Sep 2023 02:07:21 +0300
Subject: [PATCH 12/12] fix GMaps implementation
---
htdocs/map-google.js | 30 +++++++++++++++---------------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/htdocs/map-google.js b/htdocs/map-google.js
index c89e8d25..2630cc39 100644
--- a/htdocs/map-google.js
+++ b/htdocs/map-google.js
@@ -176,7 +176,7 @@ MapManager.prototype.processUpdates = function(updates) {
switch (update.location.type) {
case 'latlon':
- var marker = self.mman.find(id);
+ var marker = self.mman.find(key);
var markerClass = GSimpleMarker;
var aprsOptions = {}
@@ -195,9 +195,9 @@ MapManager.prototype.processUpdates = function(updates) {
// when I have the time to spend to understand how.
// As of now, the planes are shown on the map, but with default icon.
marker = new markerClass();
- self.mman.add(id, marker);
+ self.mman.add(key, marker);
marker.addListener('click', function() {
- showMarkerInfoWindow(id, marker.position);
+ showMarkerInfoWindow(key, marker.position);
});
}
@@ -215,23 +215,23 @@ MapManager.prototype.processUpdates = function(updates) {
if (expectedIcao && expectedIcao === update.source.icao) {
map.panTo(marker.position);
- showMarkerInfoWindow(id, marker.position);
+ showMarkerInfoWindow(key, marker.position);
expectedIcao = false;
}
- if (expectedCallsign && expectedCallsign == id) {
+ if (expectedCallsign && expectedCallsign == key) {
map.panTo(marker.position);
- showMarkerInfoWindow(id, marker.position);
+ showMarkerInfoWindow(key, marker.position);
expectedCallsign = false;
}
- if (infoWindow && infoWindow.callsign && infoWindow.callsign == id) {
+ if (infoWindow && infoWindow.callsign && infoWindow.callsign == key) {
showMarkerInfoWindow(infoWindow.callsign, marker.position);
}
break;
case 'feature':
- var marker = self.mman.find(id);
+ var marker = self.mman.find(key);
var options = {}
// If no symbol or color supplied, use defaults by type
@@ -250,9 +250,9 @@ MapManager.prototype.processUpdates = function(updates) {
if (!marker) {
marker = new GFeatureMarker();
self.mman.addType(update.mode);
- self.mman.add(id, marker);
+ self.mman.add(key, marker);
marker.addListener('click', function() {
- showMarkerInfoWindow(id, marker.position);
+ showMarkerInfoWindow(key, marker.position);
});
}
@@ -265,24 +265,24 @@ MapManager.prototype.processUpdates = function(updates) {
// Apply marker options
marker.setMarkerOptions(options);
- if (expectedCallsign && expectedCallsign == id) {
+ if (expectedCallsign && expectedCallsign == key) {
map.panTo(marker.position);
- showMarkerInfoWindow(id, marker.position);
+ showMarkerInfoWindow(key, marker.position);
expectedCallsign = false;
}
- if (infoWindow && infoWindow.callsign && infoWindow.callsign == id) {
+ if (infoWindow && infoWindow.callsign && infoWindow.callsign == key) {
showMarkerInfoWindow(infoWindow.callsign, marker.position);
}
break;
case 'locator':
- var rectangle = self.lman.find(id);
+ var rectangle = self.lman.find(key);
// If new item, create a new locator for it
if (!rectangle) {
rectangle = new GLocator();
- self.lman.add(id, rectangle);
+ self.lman.add(key, rectangle);
rectangle.rect.addListener('click', function() {
showLocatorInfoWindow(rectangle.locator, rectangle.center);
});