mirror of
https://github.com/jketterl/openwebrx.git
synced 2025-12-06 07:12:09 +01:00
Merge pull request #354 from 0xAF/leaflet
Preliminary Leaflet (OSM) implementation. WIP.
This commit is contained in:
commit
a5748652d1
|
|
@ -30,6 +30,24 @@ ul {
|
||||||
user-select: none;
|
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 */
|
/* show it as soon as google maps has moved it to its container */
|
||||||
.openwebrx-map .openwebrx-map-legend {
|
.openwebrx-map .openwebrx-map-legend {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<div class="button" data-toggle-panel="openwebrx-panel-status"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-status"></use></svg><br/>Status</div>
|
<div class="button" data-toggle-panel="openwebrx-panel-status"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-status"></use></svg><br/>Status</div>
|
||||||
<div class="button" data-toggle-panel="openwebrx-panel-log"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-log"></use></svg><br/>Log</div>
|
<div class="button" data-toggle-panel="openwebrx-panel-log"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-log"></use></svg><br/>Log</div>
|
||||||
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-receiver"></use></svg><br/>Receiver</div>
|
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-receiver"></use></svg><br/>Receiver</div>
|
||||||
<a class="button" href="${document_root}map" target="openwebrx-map"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-map"></use></svg><br/>Map</a>
|
<a class="button" href="${document_root}map${map_type}" target="openwebrx-map"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-map"></use></svg><br/>Map</a>
|
||||||
<a class="button" href="${document_root}settings" target="openwebrx-settings"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-settings"></use></svg><br/>Settings</a>
|
<a class="button" href="${document_root}settings" target="openwebrx-settings"><svg viewBox="0 0 80 80"><use xlink:href="${document_root}static/gfx/svg-defs.svg#panel-settings"></use></svg><br/>Settings</a>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
21
htdocs/lib/Clock.js
Normal file
21
htdocs/lib/Clock.js
Normal file
|
|
@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
143
htdocs/lib/GoogleMaps.js
Normal file
143
htdocs/lib/GoogleMaps.js
Normal file
|
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
118
htdocs/lib/Leaflet.js
Normal file
118
htdocs/lib/Leaflet.js
Normal file
|
|
@ -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; }
|
||||||
208
htdocs/lib/MapLocators.js
Normal file
208
htdocs/lib/MapLocators.js
Normal file
|
|
@ -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 '<li class="square' + disabled + '" data-selector="' + key
|
||||||
|
+ '"><span class="illustration" style="background-color:'
|
||||||
|
+ chroma(value).alpha(LocatorManager.fillOpacity) + ';border-color:'
|
||||||
|
+ chroma(value).alpha(LocatorManager.strokeOpacity) + ';"></span>'
|
||||||
|
+ key + '</li>';
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".openwebrx-map-legend .content").html('<ul>' + list.join('') + '</ul>');
|
||||||
|
}
|
||||||
|
|
||||||
|
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<keys.length ; ++j) {
|
||||||
|
this.colorKeys[keys[j]] = colors[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reColor();
|
||||||
|
this.updateLegend();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return color for the key
|
||||||
|
return this.colorKeys[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
LocatorManager.prototype.getInfoHTML = function(locator, pos, receiverMarker = null) {
|
||||||
|
var inLocator = $.map(this.rectangles, function(x, callsign) {
|
||||||
|
return { callsign: callsign, locator: x.locator, lastseen: x.lastseen, mode: x.mode, band: x.band }
|
||||||
|
}).filter(this.rectangleFilter).filter(function(d) {
|
||||||
|
return d.locator == locator;
|
||||||
|
}).sort(function(a, b){
|
||||||
|
return b.lastseen - a.lastseen;
|
||||||
|
});
|
||||||
|
|
||||||
|
var distance = receiverMarker?
|
||||||
|
" at " + Marker.distanceKm(receiverMarker.position, pos) + " km" : "";
|
||||||
|
|
||||||
|
var list = inLocator.map(function(x) {
|
||||||
|
var timestring = moment(x.lastseen).fromNow();
|
||||||
|
var message = Marker.linkify(x.callsign, callsign_url)
|
||||||
|
+ ' (' + timestring + ' using ' + x.mode;
|
||||||
|
if (x.band) message += ' on ' + x.band;
|
||||||
|
return '<li>' + message + ')</li>';
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return '<h3>Locator: ' + locator + distance +
|
||||||
|
'</h3><div>Active Callsigns:</div><ul>' + list + '</ul>';
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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, update.location.ttl);
|
||||||
|
};
|
||||||
|
|
||||||
|
Locator.prototype.age = function(age, ttl=retention_time) {
|
||||||
|
if (age <= ttl) {
|
||||||
|
this.setOpacity(Marker.getOpacityScale(age, ttl));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.setMap();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
207
htdocs/lib/MapManager.js
Normal file
207
htdocs/lib/MapManager.js
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
//
|
||||||
|
// 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-legend-toggle').css({
|
||||||
|
'cursor': 'pointer',
|
||||||
|
'display': 'flex',
|
||||||
|
'justify-content': 'space-between'
|
||||||
|
}).on('click', function () {
|
||||||
|
var el = document.getElementById('openwebrx-map-selectors');
|
||||||
|
if (el) {
|
||||||
|
$(this).find('i').removeClass()
|
||||||
|
.addClass('openwebrx-arrow-' + (el.style.display === 'none' ? 'down' : 'up'));
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
619
htdocs/lib/MapMarkers.js
Normal file
619
htdocs/lib/MapMarkers.js
Normal file
|
|
@ -0,0 +1,619 @@
|
||||||
|
//
|
||||||
|
// Map Markers Management
|
||||||
|
//
|
||||||
|
|
||||||
|
function MarkerManager() {
|
||||||
|
// Current markers
|
||||||
|
this.markers = {};
|
||||||
|
|
||||||
|
// Currently known marker types
|
||||||
|
this.types = {};
|
||||||
|
|
||||||
|
// Colors used for marker types
|
||||||
|
this.colors = {
|
||||||
|
'HFDL' : '#004000',
|
||||||
|
'VDL2' : '#000080',
|
||||||
|
'ADSB' : '#800000',
|
||||||
|
'ACARS' : '#000000'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Symbols used for marker types
|
||||||
|
this.symbols = {
|
||||||
|
'APRS' : '⚐',
|
||||||
|
'AIS' : '⩯',
|
||||||
|
'HFDL' : '✈',
|
||||||
|
'VDL2' : '✈',
|
||||||
|
'ADSB' : '✈',
|
||||||
|
'ACARS' : '✈'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Marker type shown/hidden status
|
||||||
|
this.enabled = {
|
||||||
|
'APRS' : true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
'<li class="square' + (enabled? '':' disabled') +
|
||||||
|
'" data-selector="' + type + '">' +
|
||||||
|
'<span class="feature" style="color:' + color + ';">' +
|
||||||
|
symbol + '</span>' + type + '</li>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 '<a target="callsign_info" href="' +
|
||||||
|
url.replaceAll('{}', linkEntity) + '">' + name + '</a>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 '<a target="openwebrx-rx" href="/#freq=' + freq +
|
||||||
|
',mod=' + mod + '">' + text + '</a>';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 '<div style="border-bottom:2px solid;"><b>' + name + '</b></div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert given name/value to an information section item.
|
||||||
|
Marker.makeListItem = function(name, value) {
|
||||||
|
return '<div style="display:flex;justify-content:space-between;border-bottom:1px dotted;white-space:nowrap;">'
|
||||||
|
+ '<span>' + name + ' </span>'
|
||||||
|
+ '<span>' + value + '</span>'
|
||||||
|
+ '</div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get opacity value in the 0..1 range based on the given age.
|
||||||
|
Marker.getOpacityScale = function(age, ttl=retention_time) {
|
||||||
|
var scale = 1;
|
||||||
|
if (age >= ttl / 2) {
|
||||||
|
scale = (ttl - age) / (ttl / 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, ttl=retention_time) {
|
||||||
|
if(age <= ttl) {
|
||||||
|
this.setMarkerOpacity(Marker.getOpacityScale(age, ttl));
|
||||||
|
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
|
||||||
|
// 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;
|
||||||
|
this.altitude = update.location.altitude;
|
||||||
|
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, update.location.ttl);
|
||||||
|
};
|
||||||
|
|
||||||
|
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? '<p align="center">' + this.comment + '</p>' : '';
|
||||||
|
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.mmode) {
|
||||||
|
detailsString += Marker.makeListItem('Modulation', this.mmode.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.comment && this.status && this.updated) {
|
||||||
|
commentString = '<p align="center">' + this.status
|
||||||
|
+ ', last updated on ' + this.updated + '</p>';
|
||||||
|
} 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 (detailsString.length > 0) {
|
||||||
|
detailsString = '<div>' + Marker.makeListTitle('Details') + detailsString + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduleString.length > 0) {
|
||||||
|
scheduleString = '<div>' + Marker.makeListTitle('Schedule') + scheduleString + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiverMarker) {
|
||||||
|
distance = ' at ' + Marker.distanceKm(receiverMarker.position, this.position) + ' km';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<h3>' + nameString + distance + '</h3>'
|
||||||
|
+ 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, update.location.ttl);
|
||||||
|
};
|
||||||
|
|
||||||
|
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, <x,y> = 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 += '<di>' + Marker.makeListTitle('Comment') + '<div>' +
|
||||||
|
this.comment + '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.weather) {
|
||||||
|
weatherString += '<div>' + 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 += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<div>' + Marker.makeListTitle('Details') + detailsString + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<p align="right"><i>via ' + hops.join(', ') + ' </i></p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 '<h3>' + Marker.linkify(name, url, linkEntity) + distance + '</h3>'
|
||||||
|
+ '<p align="center">' + timeString + ' using ' + this.mode
|
||||||
|
+ ( this.band ? ' on ' + this.band : '' ) + '</p>'
|
||||||
|
+ commentString + weatherString + detailsString + hopsString;
|
||||||
|
};
|
||||||
36
htdocs/map-google.html
Normal file
36
htdocs/map-google.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OpenWebRX Map</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
|
<meta name="theme-color" content="#222" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
|
<script src="compiled/map-google.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${header}
|
||||||
|
<div class="openwebrx-map" id="openwebrx-map"></div>
|
||||||
|
<div class="openwebrx-map-legend">
|
||||||
|
<div id="openwebrx-map-selectors">
|
||||||
|
<h3>Colors</h3>
|
||||||
|
<select style="width:100%;" id="openwebrx-map-colormode">
|
||||||
|
<option value="byband" selected="selected">By Band</option>
|
||||||
|
<option value="bymode">By Mode</option>
|
||||||
|
<option value="off">Off</option>
|
||||||
|
</select>
|
||||||
|
<div class="content"></div>
|
||||||
|
<h3 style="margin: 20px 0 0 0;">Features</h3>
|
||||||
|
<ul class="features">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id='openwebrx-legend-toggle'>
|
||||||
|
<i class="openwebrx-arrow-down"></i>
|
||||||
|
<h3 id="openwebrx-clock-utc" style="margin: 0;">00:00 UTC</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
310
htdocs/map-google.js
Normal file
310
htdocs/map-google.js
Normal file
|
|
@ -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(
|
||||||
|
'<h3>' + marker.config['receiver_name'] + '</h3>' +
|
||||||
|
'<div>Receiver Location</div>'
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
39
htdocs/map-leaflet.html
Normal file
39
htdocs/map-leaflet.html
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OpenWebRX Map</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
|
<meta name="theme-color" content="#222" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
|
<script src="compiled/map-leaflet.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${header}
|
||||||
|
<div class="openwebrx-map" id="openwebrx-map"></div>
|
||||||
|
<div class="openwebrx-map-legend">
|
||||||
|
<div id="openwebrx-map-selectors">
|
||||||
|
<h3>Map</h3>
|
||||||
|
<select style="width:100%;" id="openwebrx-map-source"></select>
|
||||||
|
<div id="openwebrx-map-extralayers"></div>
|
||||||
|
<h3>Colors</h3>
|
||||||
|
<select style="width:100%;" id="openwebrx-map-colormode">
|
||||||
|
<option value="byband" selected="selected">By Band</option>
|
||||||
|
<option value="bymode">By Mode</option>
|
||||||
|
<option value="off">Off</option>
|
||||||
|
</select>
|
||||||
|
<div class="content"></div>
|
||||||
|
<h3 style="margin: 20px 0 0 0;">Features</h3>
|
||||||
|
<ul class="features">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id='openwebrx-legend-toggle'>
|
||||||
|
<i class="openwebrx-arrow-down"></i>
|
||||||
|
<h3 id="openwebrx-clock-utc" style="margin: 0;">00:00 UTC</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
466
htdocs/map-leaflet.js
Normal file
466
htdocs/map-leaflet.js
Normal file
|
|
@ -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: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||||
|
subdomains: 'abcd',
|
||||||
|
maxZoom: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CartoDB DarkMatter',
|
||||||
|
url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
|
||||||
|
options: {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||||
|
subdomains: 'abcd',
|
||||||
|
maxZoom: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CartoDB Voyager',
|
||||||
|
url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
|
||||||
|
options: {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||||
|
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: '© <a href="https://stadiamaps.com/">Stadia Maps</a>, © <a href="https://openmaptiles.org/">OpenMapTiles</a> © <a href="http://openstreetmap.org">OpenStreetMap</a> 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: '© <a href="https://stadiamaps.com/">Stadia Maps</a>, © <a href="https://openmaptiles.org/">OpenMapTiles</a> © <a href="http://openstreetmap.org">OpenStreetMap</a> 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: '<h3>' + self.config['receiver_name'] + '</h3>' +
|
||||||
|
'<div>Receiver location</div>'
|
||||||
|
}).openOn(map);
|
||||||
|
});
|
||||||
|
receiverMarker.setMarkerOptions(this.config);
|
||||||
|
receiverMarker.setMap(map);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$.each(mapSources, function (idx, ms) {
|
||||||
|
$('#openwebrx-map-source').append(
|
||||||
|
$('<option></option>')
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -147,6 +147,7 @@ defaultConfig = PropertyLayer(
|
||||||
tuning_precision=2,
|
tuning_precision=2,
|
||||||
squelch_auto_margin=10,
|
squelch_auto_margin=10,
|
||||||
google_maps_api_key="",
|
google_maps_api_key="",
|
||||||
|
map_type="google",
|
||||||
map_position_retention_time=2 * 60 * 60,
|
map_position_retention_time=2 * 60 * 60,
|
||||||
decoding_queue_workers=2,
|
decoding_queue_workers=2,
|
||||||
decoding_queue_length=10,
|
decoding_queue_length=10,
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,7 @@ class MapConnection(OpenWebRxClient):
|
||||||
filtered_config = pm.filter(
|
filtered_config = pm.filter(
|
||||||
"google_maps_api_key",
|
"google_maps_api_key",
|
||||||
"receiver_gps",
|
"receiver_gps",
|
||||||
|
"map_type",
|
||||||
"callsign_service",
|
"callsign_service",
|
||||||
"aircraft_tracking_service",
|
"aircraft_tracking_service",
|
||||||
"receiver_name",
|
"receiver_name",
|
||||||
|
|
|
||||||
|
|
@ -138,12 +138,32 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
|
||||||
"lib/Modes.js",
|
"lib/Modes.js",
|
||||||
"lib/MetaPanel.js",
|
"lib/MetaPanel.js",
|
||||||
],
|
],
|
||||||
"map.js": [
|
"map.js": [ # AF: to be removed once PR is accepted
|
||||||
"lib/jquery-3.2.1.min.js",
|
"lib/jquery-3.2.1.min.js",
|
||||||
"lib/chroma.min.js",
|
"lib/chroma.min.js",
|
||||||
"lib/Header.js",
|
"lib/Header.js",
|
||||||
"map.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": [
|
"settings.js": [
|
||||||
"lib/jquery-3.2.1.min.js",
|
"lib/jquery-3.2.1.min.js",
|
||||||
"lib/bootstrap.bundle.min.js",
|
"lib/bootstrap.bundle.min.js",
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,14 @@ class GeneralSettingsController(SettingsFormController):
|
||||||
),
|
),
|
||||||
Section(
|
Section(
|
||||||
"Map settings",
|
"Map settings",
|
||||||
|
DropdownInput(
|
||||||
|
"map_type",
|
||||||
|
"Map type",
|
||||||
|
options=[
|
||||||
|
Option("google", "Google Maps"),
|
||||||
|
Option("leaflet", "OpenStreetMap, etc."),
|
||||||
|
],
|
||||||
|
),
|
||||||
TextInput(
|
TextInput(
|
||||||
"google_maps_api_key",
|
"google_maps_api_key",
|
||||||
"Google Maps API key",
|
"Google Maps API key",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from owrx.controllers import Controller
|
from owrx.controllers import Controller
|
||||||
from owrx.details import ReceiverDetails
|
from owrx.details import ReceiverDetails
|
||||||
|
from owrx.config import Config
|
||||||
from string import Template
|
from string import Template
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ class WebpageController(TemplateController):
|
||||||
return "../" * levels
|
return "../" * levels
|
||||||
|
|
||||||
def header_variables(self):
|
def header_variables(self):
|
||||||
variables = {"document_root": self.get_document_root()}
|
variables = { "document_root": self.get_document_root(), "map_type": "" }
|
||||||
variables.update(ReceiverDetails().__dict__())
|
variables.update(ReceiverDetails().__dict__())
|
||||||
return variables
|
return variables
|
||||||
|
|
||||||
|
|
@ -42,4 +43,27 @@ class IndexController(WebpageController):
|
||||||
class MapController(WebpageController):
|
class MapController(WebpageController):
|
||||||
def indexAction(self):
|
def indexAction(self):
|
||||||
# TODO check if we have a google maps api key first?
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue