From 7e0239d9b5653ec786c956983dacedcca3442403 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Sun, 30 Jul 2023 12:18:50 -0400 Subject: [PATCH] Separated locators from the map implementation. --- htdocs/lib/LocatorManager.js | 232 +++++++++++++++++++++++++++++++++++ htdocs/lib/MarkerManager.js | 5 +- htdocs/map.js | 227 ++++++++-------------------------- 3 files changed, 286 insertions(+), 178 deletions(-) create mode 100644 htdocs/lib/LocatorManager.js 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:
    ' + - '' - ); + 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')); } });