' + 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.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..de382c6a
--- /dev/null
+++ b/htdocs/map-google.html
@@ -0,0 +1,36 @@
+
+
+
+ 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..2630cc39
--- /dev/null
+++ b/htdocs/map-google.js
@@ -0,0 +1,310 @@
+// 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;
+var expectedIcao = query.icao? query.icao: 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);
+};
+
+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
+//
+
+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) {
+ var key = sourceToKey(update.source);
+
+ switch (update.location.type) {
+ case 'latlon':
+ var marker = self.mman.find(key);
+ 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) {
+ // 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(key, marker);
+ marker.addListener('click', function() {
+ showMarkerInfoWindow(key, 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 (expectedIcao && expectedIcao === update.source.icao) {
+ map.panTo(marker.position);
+ showMarkerInfoWindow(key, marker.position);
+ expectedIcao = false;
+ }
+
+ if (expectedCallsign && expectedCallsign == key) {
+ map.panTo(marker.position);
+ showMarkerInfoWindow(key, marker.position);
+ expectedCallsign = false;
+ }
+
+ if (infoWindow && infoWindow.callsign && infoWindow.callsign == key) {
+ showMarkerInfoWindow(infoWindow.callsign, marker.position);
+ }
+ break;
+
+ case 'feature':
+ var marker = self.mman.find(key);
+ 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(key, marker);
+ marker.addListener('click', function() {
+ showMarkerInfoWindow(key, 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 == key) {
+ map.panTo(marker.position);
+ showMarkerInfoWindow(key, marker.position);
+ expectedCallsign = false;
+ }
+
+ if (infoWindow && infoWindow.callsign && infoWindow.callsign == key) {
+ showMarkerInfoWindow(infoWindow.callsign, marker.position);
+ }
+ break;
+
+ case 'locator':
+ var rectangle = self.lman.find(key);
+
+ // If new item, create a new locator for it
+ if (!rectangle) {
+ rectangle = new GLocator();
+ self.lman.add(key, 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..6ec55dba
--- /dev/null
+++ b/htdocs/map-leaflet.html
@@ -0,0 +1,39 @@
+
+
+
+ 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..17c3a179
--- /dev/null
+++ b/htdocs/map-leaflet.js
@@ -0,0 +1,466 @@
+// 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.'
+ },
+];
+
+// 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;
+var expectedIcao = query.icao? query.icao: 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);
+};
+
+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
+//
+
+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;
+ }
+
+ $('#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);
+ });
+
+ // 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) {
+ var key = sourceToKey(update.source);
+
+ switch (update.location.type) {
+ case 'latlon':
+ var marker = self.mman.find(key);
+ 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) {
+ // 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(key, marker);
+ marker.addListener('click', function() {
+ showMarkerInfoWindow(key, 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 (expectedIcao && expectedIcao === key) {
+ map.setView(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(key);
+ 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(key, marker);
+ marker.addListener('click', function() {
+ showMarkerInfoWindow(key, 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 == key) {
+ map.setView(marker.getPos());
+ showMarkerInfoWindow(key, marker.getPos());
+ expectedCallsign = false;
+ }
+ break;
+
+ case 'locator':
+ var rectangle = self.lman.find(key);
+
+ // If new item, create a new locator for it
+ if (!rectangle) {
+ rectangle = new LLocator();
+ self.lman.add(key, 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..118ee3f1 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
@@ -25,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
@@ -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-{}.html".format(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
+
+