diff --git a/htdocs/lib/LocatorManager.js b/htdocs/lib/LocatorManager.js
new file mode 100644
index 00000000..11d5eedb
--- /dev/null
+++ b/htdocs/lib/LocatorManager.js
@@ -0,0 +1,232 @@
+//
+// 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.markers, 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) {
+ var color = self.getColor(x);
+ x.setOptions({ strokeColor: color, fillColor: color });
+ });
+};
+
+LocatorManager.prototype.updateLegend = function() {
+ if (!this.colorKeys) return;
+ var filter = this.rectangleFilter;
+ var mode = this.colorMode.slice(2);
+ var list = $.map(this.colorKeys, function(value, key) {
+ // Fake rectangle to test if the filter would match
+ var fakeRectangle = Object.fromEntries([[mode, key]]);
+ var disabled = filter(fakeRectangle) ? '' : ' disabled';
+
+ return '
'
+ + key + '';
+ });
+
+ $(".openwebrx-map-legend .content").html('');
+}
+
+LocatorManager.prototype.setColorMode = function(map, newColorMode) {
+ this.colorMode = newColorMode;
+ this.colorKeys = {};
+ this.setFilter(map);
+ this.reColor();
+ this.updateLegend();
+};
+
+LocatorManager.prototype.getType = function(data) {
+ switch (this.colorMode) {
+ case 'byband':
+ return data.band;
+ case 'bymode':
+ return data.mode;
+ default:
+ return '';
+ }
+};
+
+LocatorManager.prototype.getColor = function(data) {
+ var type = this.getType(data);
+ if (!type) return "#ffffff00";
+
+ // If adding a new key...
+ if (!this.colorKeys[type]) {
+ var keys = Object.keys(this.colorKeys);
+
+ // Add a new key
+ keys.push(type);
+
+ // Sort color keys
+ keys.sort(function(a, b) {
+ var pa = parseFloat(a);
+ var pb = parseFloat(b);
+ if (isNaN(pa) || isNaN(pb)) return a.localeCompare(b);
+ return pa - pb;
+ });
+
+ // Recompute colors
+ var colors = this.colorScale.colors(keys.length);
+ this.colorKeys = {};
+ for(var j=0 ; j' + message + ')';
+ }).join("");
+
+ return 'Locator: ' + locator + distance +
+ '
Active Callsigns:
';
+};
+
+//
+// Generic locator functionality
+//
+
+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);
+};
+
+//
+// GoogleMaps-Specific Locators (derived from generic locators)
+//
+
+function GLocator() { $.extend(this, new Locator()); }
+
+GLocator.prototype = new google.maps.Rectangle();
+
+GLocator.prototype.setOptions = function(options) {
+ google.maps.Rectangle.prototype.setOptions.apply(this, arguments);
+};
+
+GLocator.prototype.setCenter = function(lat, lon) {
+ this.center = new google.maps.LatLng({lat: lat, lng: lon});
+
+ this.setOptions({ bounds : {
+ north : lat - 0.5,
+ south : lat + 0.5,
+ west : lon - 1.0,
+ east : lon + 1.0
+ }});
+}
+
+GLocator.prototype.age = function(age) {
+ if (age <= retention_time) {
+ var scale = Marker.getOpacityScale(age);
+ var stroke = LocatorManager.strokeOpacity * scale;
+ var fill = LocatorManager.fillOpacity * scale;
+// this.setOptions({
+// strokeOpacity : stroke,
+// fillOpacity : fill
+// });
+ return true;
+ } else {
+ this.setMap();
+ return false;
+ }
+};
diff --git a/htdocs/lib/MarkerManager.js b/htdocs/lib/MarkerManager.js
index 64756a9b..9a538e25 100644
--- a/htdocs/lib/MarkerManager.js
+++ b/htdocs/lib/MarkerManager.js
@@ -98,8 +98,9 @@ MarkerManager.prototype.add = function(id, marker) {
MarkerManager.prototype.ageAll = function() {
var now = new Date().getTime();
- $.each(this.markers, function(id, x) {
- if (!x.age(now - x.lastseen)) delete this.markers[id];
+ var data = this.markers;
+ $.each(data, function(id, x) {
+ if (!x.age(now - x.lastseen)) delete data[id];
});
};
diff --git a/htdocs/map.js b/htdocs/map.js
index 4db1500a..65310a43 100644
--- a/htdocs/map.js
+++ b/htdocs/map.js
@@ -42,93 +42,28 @@ $(function(){
var ws_url = href + "ws/";
var map;
- var rectangles = {};
var receiverMarker;
var updateQueue = [];
- var strokeOpacity = 0.8;
- var fillOpacity = 0.35;
-
- // marker manager
+ // marker and locator managers
var markmanager = null;
+ var locmanager = null;
// clock
var clock = new Clock($("#openwebrx-clock-utc"));
- var colorKeys = {};
- var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl');
- var getColor = function(id){
- if (!id) return "#ffffff00";
- if (!colorKeys[id]) {
- var keys = Object.keys(colorKeys);
- keys.push(id);
- 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;
- });
- var colors = colorScale.colors(keys.length);
- colorKeys = {};
- keys.forEach(function(key, index) {
- colorKeys[key] = colors[index];
- });
- reColor();
- updateLegend();
- }
- return colorKeys[id];
- }
-
- // when the color palette changes, update all grid squares with new color
- var reColor = function() {
- $.each(rectangles, function(_, r) {
- var color = getColor(colorAccessor(r));
- r.setOptions({
- strokeColor: color,
- fillColor: color
- });
- });
- }
-
- var colorMode = 'byband';
- var colorAccessor = function(r) {
- switch (colorMode) {
- case 'byband':
- return r.band;
- case 'bymode':
- return r.mode;
- case 'off':
- return '';
- }
- };
-
- $(function(){
- $('#openwebrx-map-colormode').on('change', function(){
- colorMode = $(this).val();
- colorKeys = {};
- filterRectangles(allRectangles);
- reColor();
- updateLegend();
+ $(function() {
+ $('#openwebrx-map-colormode').on('change', function() {
+ locmanager.setColorMode(map, $(this).val());
});
});
- var updateLegend = function() {
- var lis = $.map(colorKeys, function(value, key) {
- // fake rectangle to test if the filter would match
- var fakeRectangle = Object.fromEntries([[colorMode.slice(2), key]]);
- var disabled = rectangleFilter(fakeRectangle) ? '' : ' disabled';
- return '' + key + '';
- });
- $(".openwebrx-map-legend .content").html('');
- }
-
var processUpdates = function(updates) {
- if ((typeof(AprsMarker) == 'undefined') || (typeof(FeatureMarker) == 'undefined')) {
+ if (!markmanager || !locmanager) {
updateQueue = updateQueue.concat(updates);
return;
}
- updates.forEach(function(update){
-
+ updates.forEach(function(update) {
switch (update.location.type) {
case 'latlon':
var pos = new google.maps.LatLng(update.location.lat, update.location.lon);
@@ -173,6 +108,7 @@ $(function(){
showMarkerInfoWindow(infowindow.callsign, pos);
}
break;
+
case 'feature':
var pos = new google.maps.LatLng(update.location.lat, update.location.lon);
var marker = markmanager.find(update.callsign);
@@ -221,50 +157,40 @@ $(function(){
showMarkerInfoWindow(infowindow.callsign, pos);
}
break;
+
case 'locator':
- var loc = update.location.locator;
- var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]);
- var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2;
- var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1});
- var rectangle;
- // the accessor is designed to work on the rectangle... but it should work on the update object, too
- var color = getColor(colorAccessor(update));
- if (rectangles[update.callsign]) {
- rectangle = rectangles[update.callsign];
- } else {
- rectangle = new google.maps.Rectangle();
- rectangle.addListener('click', function(){
+ var rectangle = locmanager.find(update.callsign);
+
+ // If new item, create a new locator for it
+ if (!rectangle) {
+ rectangle = new GLocator();
+ locmanager.add(update.callsign, rectangle);
+ rectangle.addListener('click', function() {
showLocatorInfoWindow(this.locator, this.center);
});
- rectangles[update.callsign] = rectangle;
}
- rectangle.lastseen = update.lastseen;
- rectangle.locator = update.location.locator;
- rectangle.mode = update.mode;
- rectangle.band = update.band;
- rectangle.center = center;
- rectangle.setOptions($.extend({
- strokeColor: color,
- strokeWeight: 2,
- fillColor: color,
- map: rectangleFilter(rectangle) ? map : undefined,
- bounds:{
- north: lat,
- south: lat + 1,
- west: lon,
- east: lon + 2
- }
- }, getRectangleOpacityOptions(update.lastseen) ));
+ // Update locator attributes, center, and age
+ rectangle.age(new Date().getTime() - update.lastseen);
+ rectangle.update(update);
+
+ // Apply locator options
+ var color = locmanager.getColor(rectangle);
+ rectangle.setOptions({
+ map : locmanager.filter(rectangle)? map : undefined,
+ strokeColor : color,
+ strokeWeight : 2,
+ fillColor : color
+ });
if (expectedLocator && expectedLocator == update.location.locator) {
- map.panTo(center);
- showLocatorInfoWindow(expectedLocator, center);
+ map.panTo(rectangle.center);
+ showLocatorInfoWindow(expectedLocator, rectangle.center);
expectedLocator = false;
}
if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) {
- showLocatorInfoWindow(infowindow.locator, center);
+ showLocatorInfoWindow(infowindow.locator, rectangle.center);
}
break;
}
@@ -275,8 +201,7 @@ $(function(){
var reset = function(callsign, item) { item.setMap(); };
receiverMarker.setMap();
markmanager.clear();
- $.each(rectangles, reset);
- rectangles = {};
+ locmanager.clear();
};
var reconnect_timeout = false;
@@ -321,8 +246,18 @@ $(function(){
$.getScript('static/lib/MarkerManager.js').done(function(){
markmanager = new MarkerManager();
- processUpdates(updateQueue);
- updateQueue = [];
+ if (locmanager) {
+ processUpdates(updateQueue);
+ updateQueue = [];
+ }
+ });
+
+ $.getScript('static/lib/LocatorManager.js').done(function(){
+ locmanager = new LocatorManager();
+ if (markmanager) {
+ processUpdates(updateQueue);
+ updateQueue = [];
+ }
});
var $legend = $(".openwebrx-map-legend");
@@ -341,6 +276,7 @@ $(function(){
title: config['receiver_name'],
config: config
});
+
}); else {
receiverMarker.setOptions({
map: map,
@@ -424,28 +360,7 @@ $(function(){
var showLocatorInfoWindow = function(locator, pos) {
var infowindow = getInfoWindow();
infowindow.locator = locator;
- var inLocator = $.map(rectangles, function(r, callsign) {
- return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
- }).filter(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" : "";
- infowindow.setContent(
- 'Locator: ' + locator + distance + '
' +
- 'Active Callsigns:
' +
- '' +
- inLocator.map(function(i){
- var timestring = moment(i.lastseen).fromNow();
- var message = Marker.linkify(i.callsign) + ' (' + timestring + ' using ' + i.mode;
- if (i.band) message += ' on ' + i.band;
- message += ')';
- return '- ' + message + '
'
- }).join("") +
- '
'
- );
+ infowindow.setContent(locmanager.getInfoHTML(locator, pos, receiverMarker));
infowindow.setPosition(pos);
infowindow.open(map);
};
@@ -468,47 +383,12 @@ $(function(){
infowindow.open(map, marker);
}
- var getScale = function(lastseen) {
- var age = new Date().getTime() - lastseen;
- var scale = 1;
- if (age >= retention_time / 2) {
- scale = (retention_time - age) / (retention_time / 2);
- }
- return Math.max(0, Math.min(1, scale));
- };
-
- var getRectangleOpacityOptions = function(lastseen) {
- var scale = getScale(lastseen);
- return {
- strokeOpacity: strokeOpacity * scale,
- fillOpacity: fillOpacity * scale
- };
- };
-
- // fade out / remove positions after time
+ // Fade out / remove positions after time
setInterval(function(){
- var now = new Date().getTime();
- $.each(rectangles, function(callsign, m) {
- var age = now - m.lastseen;
- if (age > retention_time) {
- delete rectangles[callsign];
- m.setMap();
- return;
- }
- m.setOptions(getRectangleOpacityOptions(m.lastseen));
- });
- markmanager.ageAll();
+ if (locmanager) locmanager.ageAll();
+ if (markmanager) markmanager.ageAll();
}, 1000);
- var rectangleFilter = allRectangles = function() { return true; };
-
- var filterRectangles = function(filter) {
- rectangleFilter = filter;
- $.each(rectangles, function(_, r) {
- r.setMap(rectangleFilter(r) ? map : undefined);
- });
- };
-
var setupLegendFilters = function($legend) {
$content = $legend.find('.content');
$content.on('click', 'li', function() {
@@ -516,18 +396,13 @@ $(function(){
$lis = $content.find('li');
if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) {
$lis.removeClass('disabled');
- filterRectangles(allRectangles);
+ locmanager.setFilter(map);
} else {
$el.removeClass('disabled');
$lis.filter(function() {
return this != $el[0]
}).addClass('disabled');
-
- var key = colorMode.slice(2);
- var selector = $el.data('selector');
- filterRectangles(function(r) {
- return r[key] === selector;
- });
+ locmanager.setFilter(map, $el.data('selector'));
}
});