Separated locators from the map implementation.

This commit is contained in:
Marat Fayzullin 2023-07-30 12:18:50 -04:00
parent eb11d65f92
commit 7e0239d9b5
3 changed files with 286 additions and 178 deletions

View File

@ -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 '<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) + ' (' + 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 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;
}
};

View File

@ -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];
});
};

View File

@ -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 '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
});
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
}
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(
'<h3>Locator: ' + locator + distance + '</h3>' +
'<div>Active Callsigns:</div>' +
'<ul>' +
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 '<li>' + message + '</li>'
}).join("") +
'</ul>'
);
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'));
}
});