Rewritten map locators, one rectangle per maidenhead square now.

This commit is contained in:
Marat Fayzullin 2023-10-10 22:30:23 -04:00
parent b5d2b92fd3
commit bd7142ad3e
8 changed files with 251 additions and 172 deletions

View File

@ -129,7 +129,8 @@ function GLocator() {
this.rect.setOptions({
strokeWeight : 0,
strokeColor : "#FFFFFF",
fillColor : "#FFFFFF"
fillColor : "#FFFFFF",
fillOpacity : 1.0
});
}

View File

@ -76,7 +76,7 @@ function LSimpleMarker() { $.extend(this, new LMarker(), new AprsMarker()); }
//
function LLocator() {
this._rect = L.rectangle([[0,0], [1,1]], { color: '#FFFFFF', weight: 0, opacity: 1 });
this._rect = L.rectangle([[0,0], [1,1]], { color: '#FFFFFF', weight: 0, fillOpacity: 1 });
}
LLocator.prototype = new Locator();
@ -97,8 +97,8 @@ LLocator.prototype.setColor = function(color) {
LLocator.prototype.setOpacity = function(opacity) {
this._rect.setStyle({
opacity : LocatorManager.strokeOpacity * opacity,
fillOpacity : LocatorManager.fillOpacity * opacity
opacity : LocatorManager.strokeOpacity * opacity,
fillOpacity : LocatorManager.fillOpacity * opacity
});
};

View File

@ -4,85 +4,144 @@
LocatorManager.strokeOpacity = 0.8;
LocatorManager.fillOpacity = 0.35;
LocatorManager.allRectangles = function() { return true; };
function LocatorManager() {
// Current rectangles
this.rectangles = {};
// Current color allocations
this.colorKeys = {};
// Current locators
this.locators = {};
this.bands = {};
this.modes = {};
// 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;
this.colorMode = 'band';
}
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.getColors = function() {
var colors =
this.colorMode === 'band'? this.bands
: this.colorMode === 'mode'? this.modes
: null;
return colors;
};
LocatorManager.prototype.add = function(id, rectangle) {
this.rectangles[id] = rectangle;
LocatorManager.prototype.find = function(id) {
return id in this.locators? this.locators[id] : null;
};
LocatorManager.prototype.add = function(id, locator) {
// Add a new locator, if missing
if (!(id in this.locators)) {
locator.create(id);
locator.colorKeys = this.getColors();
locator.colorMode = this.colorMode;
this.locators[id] = locator;
}
// Return locator
return this.locators[id];
};
LocatorManager.prototype.update = function(id, data, map) {
// Do not update unless locator present
if (!(id in this.locators)) return false;
// Keep track of bands
if(!(data.band in this.bands)) {
this.bands[data.band] = '#000000';
this.assignColors(this.bands);
if (this.colorMode === 'band') {
this.reColor();
this.updateLegend();
}
}
// Keep track modes
if(!(data.mode in this.modes)) {
this.modes[data.mode] = '#000000';
this.assignColors(this.modes);
if (this.colorMode === 'mode') {
this.reColor();
this.updateLegend();
}
}
// Update locator
this.locators[id].update(data, map);
return true;
};
LocatorManager.prototype.getSortedKeys = function(colorMap) {
var keys = colorMap? Object.keys(colorMap) : [];
// 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;
});
return keys;
};
LocatorManager.prototype.assignColors = function(colorMap) {
// Recompute colors
var keys = this.getSortedKeys(colorMap);
var colors = this.colorScale.colors(keys.length);
for(var j=0 ; j<keys.length ; ++j) {
colorMap[keys[j]] = colors[j];
}
};
LocatorManager.prototype.ageAll = function() {
var now = new Date().getTime();
var data = this.rectangles;
var data = this.locators;
$.each(data, function(id, x) {
if (!x.age(now - x.lastseen)) delete data[id];
if (!x.age(now)) 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 = {};
// Remove all locators from the map
$.each(this.locators, function(_, x) { x.setMap(); });
// Delete all locators
this.locators = {};
};
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;
};
LocatorManager.prototype.setFilter = function(filterBy = null) {
var colors = this.getColors();
this.assignColors(colors);
if (filterBy) {
$.each(colors, function(id, x) {
if (id !== filterBy) colors[id] = null;
});
}
var filter = this.rectangleFilter;
$.each(this.rectangles, function(_, x) {
x.setMap(filter(x) ? map : undefined);
});
this.reColor();
this.updateLegend();
};
LocatorManager.prototype.reColor = function() {
var self = this;
$.each(this.rectangles, function(_, x) {
x.setColor(self.getColor(x));
var mode = this.colorMode;
var keys = this.getColors();
$.each(this.locators, function(_, x) {
x.colorKeys = keys;
x.colorMode = mode;
x.reColor();
});
};
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
var colors = this.getColors();
var keys = this.getSortedKeys(colors);
var list = $.map(keys, function(key) {
var value = colors[key]? colors[key] : '#000000';
return '<li class="square' + (colors[key]? '' : ' disabled')
+ '" data-selector="' + key
+ '"><span class="illustration" style="background-color:'
+ chroma(value).alpha(LocatorManager.fillOpacity) + ';border-color:'
+ chroma(value).alpha(LocatorManager.strokeOpacity) + ';"></span>'
@ -92,64 +151,128 @@ LocatorManager.prototype.updateLegend = function() {
$(".openwebrx-map-legend .content").html('<ul>' + list.join('') + '</ul>');
}
LocatorManager.prototype.setColorMode = function(map, newColorMode) {
LocatorManager.prototype.setColorMode = function(newColorMode) {
this.colorMode = newColorMode;
this.colorKeys = {};
this.setFilter(map);
this.reColor();
this.updateLegend();
this.setFilter();
};
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(id, pos, receiverMarker = null) {
return id in this.locators? this.locators[id].getInfoHTML(id, pos, receiverMarker) : '';
}
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;
//
// Generic Map Locator
// Derived classes have to implement:
// setMap(), setCenter(), setColor(), setOpacity()
//
function Locator() {}
Locator.prototype.create = function(id) {
// No callsigns yet
this.callsigns = {};
this.lastseen = 0;
this.colorKeys = null;
this.colorMode = 'band';
// Center locator at its maidenhead id
this.setCenter(
(id.charCodeAt(1) - 65 - 9) * 10 + Number(id[3]) + 0.5,
(id.charCodeAt(0) - 65 - 9) * 20 + Number(id[2]) * 2 + 1.0
);
}
Locator.prototype.update = function(data, map) {
// Update callsign information
this.callsigns[data.callsign] = {
lastseen : data.lastseen,
mode : data.mode,
band : data.band,
age : 0
};
// Keep track of the total last-seen for this locator
this.lastseen = Math.max(data.lastseen, this.lastseen);
// Update color and opacity
this.map = map;
this.reColor();
};
Locator.prototype.reColor = function() {
var c = this.getColor();
if (!c) {
this.setMap();
} else {
this.setColor(c);
this.setMap(this.map);
}
};
Locator.prototype.getColor = function() {
var keys = this.colorKeys;
var count;
var color;
if (!this.colorKeys) return null;
var attr = this.colorMode;
var weight = [];
var colors = $.map(this.callsigns, function(x) {
var y = x[attr] in keys? keys[x[attr]] : null;
if (y) weight.push(Marker.getOpacityScale(x.age));
return y;
});
count = Object.keys(colors).length;
if (!count) return null;
return chroma.average(colors, 'lrgb', weight).alpha(0.25 + Math.min(0.6, count / 10));
};
Locator.prototype.age = function(now) {
var newest = 0;
var data = this.callsigns;
var cnt0 = Object.keys(data).length;
// Perform an initial check on the whole locator
if (now - this.lastseen > retention_time) {
this.callsigns = {};
this.setMap();
return false;
}
// Scan individual callsigns
$.each(data, function(id, x) {
x.age = now - x.lastseen;
if (x.age > retention_time) {
delete data[id];
} else {
newest = Math.max(newest, x.lastseen);
}
});
// Keep track of the total last-seen for this locator
this.lastseen = newest;
// Update locator's color and opacity
var cnt1 = Object.keys(data).length;
// if (cnt1 == cnt0) return true;
if (cnt1 > 0) {
this.reColor();
return true;
} else {
this.setMap();
return false;
}
};
Locator.prototype.getInfoHTML = function(locator, pos, receiverMarker = null) {
// Filter out currently hidden bands/modes, sort by recency
var self = this;
var inLocator = $.map(this.callsigns, function(x, id) {
return self.colorKeys && self.colorKeys[x[self.colorMode]]?
{ callsign: id, lastseen: x.lastseen, mode: x.mode, band: x.band } : null;
}).sort(function(a, b){
return b.lastseen - a.lastseen;
});
@ -161,6 +284,7 @@ LocatorManager.prototype.getInfoHTML = function(locator, pos, receiverMarker = n
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("");
@ -168,41 +292,3 @@ LocatorManager.prototype.getInfoHTML = function(locator, pos, receiverMarker = n
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);
};
Locator.prototype.age = function(age) {
if (age <= retention_time) {
this.setOpacity(Marker.getOpacityScale(age));
return true;
} else {
this.setMap();
return false;
}
};

View File

@ -43,7 +43,7 @@ function MapManager() {
// Toggle color modes on click
$('#openwebrx-map-colormode').on('change', function() {
self.lman.setColorMode(map, $(this).val());
self.lman.setColorMode($(this).val());
});
});
@ -178,13 +178,13 @@ MapManager.prototype.setupLegendFilters = function($legend) {
$lis = $content.find('li');
if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) {
$lis.removeClass('disabled');
self.lman.setFilter(map);
self.lman.setFilter();
} else {
$el.removeClass('disabled');
$lis.filter(function() {
return this != $el[0]
}).addClass('disabled');
self.lman.setFilter(map, $el.data('selector'));
self.lman.setFilter($el.data('selector'));
}
});

View File

@ -18,8 +18,8 @@
<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="band" selected="selected">By Band</option>
<option value="mode">By Mode</option>
<option value="off">Off</option>
</select>
<div class="content"></div>

View File

@ -218,23 +218,19 @@ MapManager.prototype.processUpdates = function(updates) {
break;
case 'locator':
var rectangle = self.lman.find(update.callsign);
var rectangle = self.lman.find(update.location.locator);
// If new item, create a new locator for it
if (!rectangle) {
rectangle = new GLocator();
self.lman.add(update.callsign, rectangle);
self.lman.add(update.location.locator, rectangle);
rectangle.rect.addListener('click', function() {
showLocatorInfoWindow(rectangle.locator, rectangle.center);
showLocatorInfoWindow(update.location.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));
self.lman.update(update.location.locator, update, map);
if (expectedLocator && expectedLocator === update.location.locator) {
map.panTo(rectangle.center);

View File

@ -21,8 +21,8 @@
<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="band" selected="selected">By Band</option>
<option value="mode">By Mode</option>
<option value="off">Off</option>
</select>
<div class="content"></div>

View File

@ -207,15 +207,15 @@ function getInfoWindow(name = null) {
};
// Show information bubble for a locator
function showLocatorInfoWindow(rectangle) {
function showLocatorInfoWindow(locator, rectangle) {
// Bind information bubble to the rectangle
infoWindow = getInfoWindow(rectangle.locator);
infoWindow = getInfoWindow(locator);
rectangle._rect.unbindPopup().bindPopup(infoWindow).openPopup();
// Update information inside the bubble
var p = new posObj(rectangle.center);
infoWindow.setContent(
mapManager.lman.getInfoHTML(rectangle.locator, p, receiverMarker)
mapManager.lman.getInfoHTML(locator, p, receiverMarker)
);
};
@ -302,7 +302,7 @@ MapManager.prototype.initializeMap = function(receiver_gps, api_key, weather_key
updateQueue = [];
if (!receiverMarker) {
receiverMarker = new LMarker();
receiverMarker = new LSimpleMarker();
receiverMarker.setMarkerPosition(self.config['receiver_name'], receiver_gps.lat, receiver_gps.lon);
receiverMarker.addListener('click', function () {
L.popup(receiverMarker.getPos(), {
@ -442,7 +442,7 @@ MapManager.prototype.processUpdates = function(updates) {
if (!update.location.color) update.location.color = self.mman.getColor(update.mode);
break;
default:
marker = new LSimpleMarker();
marker = new LMarker();
break;
}
@ -495,27 +495,23 @@ MapManager.prototype.processUpdates = function(updates) {
// If new item, create a new locator for it
if (!rectangle) {
rectangle = new LLocator();
self.lman.add(update.callsign, rectangle);
self.lman.add(update.location.locator, rectangle);
rectangle.addListener('click', function() {
showLocatorInfoWindow(rectangle);
showLocatorInfoWindow(update.location.locator, rectangle);
});
}
// 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));
self.lman.update(update.location.locator, update, map);
if (expectedLocator && expectedLocator === update.location.locator) {
map.setView(rectangle.center);
showLocatorInfoWindow(rectangle);
showLocatorInfoWindow(update.location.locator, rectangle);
expectedLocator = false;
}
if (infoWindow && infoWindow.name && infoWindow.name === rectangle.locator) {
showMarkerInfoWindow(rectangle);
showMarkerInfoWindow(update.location.locator, rectangle);
}
break;
}