From 33199038e48e759682b624c080f507462d8b8e71 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Fri, 28 Jul 2023 00:24:45 -0400 Subject: [PATCH 01/10] Moving common markers functionality to MarkerManager.js. --- htdocs/lib/MarkerManager.js | 271 +++++++++++++++++++++++++++++++++++- htdocs/map.js | 71 +--------- 2 files changed, 270 insertions(+), 72 deletions(-) diff --git a/htdocs/lib/MarkerManager.js b/htdocs/lib/MarkerManager.js index 700ff71f..d495eb21 100644 --- a/htdocs/lib/MarkerManager.js +++ b/htdocs/lib/MarkerManager.js @@ -72,7 +72,7 @@ MarkerManager.prototype.addType = function(type) { this.enabled[type] = enabled; // If there is a list of features... - var $content = $(".openwebrx-map-legend").find('.features'); + var $content = $('.openwebrx-map-legend').find('.features'); if($content) { // Add visual list item for the type @@ -85,6 +85,81 @@ MarkerManager.prototype.addType = function(type) { } }; +// +// Generic marker functionality +// + +function Marker() {} + +// Wrap given callsign or other ID into a clickable link. When +// URL not supplied, guess the correct URL by ID type. +Marker.linkify = function(callsign, url = null) { + // Leave passed URLs as they are + if (url && (url != '')) + { /* leave as is */ } + // 9-character strings may be AIS MMSI numbers + else if (callsign.match(new RegExp('^[0-9]{9}$'))) + url = vessel_url; + // 3 characters and a number may be a flight number + else if (callsign.match(new RegExp('^[A-Z]{3,4}[0-9]{1,4}[A-Z]{0,2}$'))) + url = flight_url; + // 2 characters and a long number may be a flight number + else if (callsign.match(new RegExp('^[A-Z]{2}[0-9]{2,4}[A-Z]{0,2}$'))) + url = flight_url; + // Everything else is a HAM callsign + else + url = callsign_url; + + // Must have valid lookup URL + if ((url == null) || (url == '')) + return callsign; + else + return '' + callsign + ''; +} + +// 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]; +} + +Marker.makeListTitle = function(name) { + return '
' + name + '
'; +} + +Marker.makeListItem = function(name, value) { + return '
' + + '' + name + '    ' + + '' + value + '' + + '
'; +} + // // Feature Markers // @@ -128,9 +203,9 @@ FeatureMarker.prototype.onAdd = function() { div.style.lineHeight = this.symHeight + 'px'; var self = this; - google.maps.event.addDomListener(div, "click", function(event) { + google.maps.event.addDomListener(div, 'click', function(event) { event.stopPropagation(); - google.maps.event.trigger(self, "click", event); + google.maps.event.trigger(self, 'click', event); }); var panes = this.getPanes(); @@ -147,3 +222,193 @@ FeatureMarker.prototype.remove = function() { FeatureMarker.prototype.getAnchorPoint = function() { return new google.maps.Point(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? '
' + this.comment + '
' : ''; + var detailsString = ''; + var scheduleString = ''; + var distance = ''; + + if (this.altitude) { + detailsString += Marker.makeListItem('Altitude', this.altitude.toFixed(0) + ' m'); + } + + if (this.device) { + detailsString += Marker.makeListItem('Device', this.device.manufacturer? + this.device.device + ' by ' + this.device.manufacturer : this.device + ); + } + + if (this.antenna) { + detailsString += Marker.makeListItem('Antenna', Marker.truncate(this.antenna, 24)); + } + + if (this.schedule) { + for (var j=0 ; j' + + Math.round(this.schedule[j].freq/1000) + 'kHz'; + + scheduleString += Marker.makeListItem(name, freq); + } + } + + if (detailsString.length > 0) { + detailsString = '

' + Marker.makeListTitle('Details') + detailsString + '

'; + } + + if (scheduleString.length > 0) { + scheduleString = '

' + Marker.makeListTitle('Schedule') + scheduleString + '

'; + } + + if (receiverMarker) { + distance = ' at ' + Marker.distanceKm(receiverMarker.position, this.position) + ' km'; + } + + return '

' + nameString + distance + '

' + + commentString + detailsString + scheduleString; +} + +// +// APRS Markers (also AIS and HFDL) +// + +function NewAprsMarker() {} + +NewAprsMarker.prototype = new google.maps.OverlayView(); + +NewAprsMarker.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 += '

' + Marker.makeListTitle('Comment') + '

' + + this.comment + '

'; + } + + if (this.weather) { + weatherString += '

' + 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', + 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 += '

'; + } + + if (this.altitude) { + detailsString += Marker.makeListItem('Altitude', this.altitude.toFixed(0) + ' m'); + } + + if (this.device) { + detailsString += Marker.makeListItem('Device', this.device.manufacturer? + this.device.device + ' by ' + this.device.manufacturer : this.device + ); + } + + 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.aircraft) { + detailsString += Marker.makeListItem('Aircraft', this.aircraft); + } + + // 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', degToCompass(this.course)); + } + if (this.speed) { + detailsString += Marker.makeListItem('Speed', this.speed.toFixed(1) + ' km/h'); + } + } + + if (detailsString.length > 0) { + detailsString = '

' + Marker.makeListTitle('Details') + detailsString + '

'; + } + + 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); + }); + + hopsString = '

via ' + hops.join(', ') + ' 

'; + } + + return '

' + Marker.linkify(name) + distance + '

' + + '
' + timeString + ' using ' + this.mode + + ( this.band ? ' on ' + this.band : '' ) + '
' + + commentString + weatherString + detailsString + hopsString; +} diff --git a/htdocs/map.js b/htdocs/map.js index 6ecd3263..24decccc 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -663,77 +663,10 @@ $(function(){ var showFeatureInfoWindow = function(name, pos) { var infowindow = getInfoWindow(); - infowindow.callsign = name; var marker = markers[name]; - var commentString = ""; - var detailsString = ""; - var scheduleString = ""; - var nameString = ""; - var distance = ""; - - if (marker.url) { - nameString += linkifyCallsign(name, marker.url); - } else { - nameString += name; - } - - if (marker.comment) { - commentString += '
' + marker.comment + '
'; - } - - if (marker.altitude) { - detailsString += makeListItem('Altitude', marker.altitude.toFixed(0) + ' m'); - } - - if (marker.device) { - detailsString += makeListItem('Device', marker.device.manufacturer? - marker.device.device + " by " + marker.device.manufacturer - : marker.device - ); - } - - if (marker.antenna) { - detailsString += makeListItem('Antenna', truncate(marker.antenna, 24)); - } - - if (marker.schedule) { - for (var j=0 ; j' - + Math.round(marker.schedule[j].freq/1000) + 'kHz'; - - scheduleString += makeListItem(name, freq); - } - } - - if (detailsString.length > 0) { - detailsString = '

' + makeListTitle('Details') + detailsString + '

'; - } - - if (scheduleString.length > 0) { - scheduleString = '

' + makeListTitle('Schedule') + scheduleString + '

'; - } - - if (receiverMarker) { - distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km"; - } - - infowindow.setContent( - '

' + nameString + distance + '

' + - commentString + detailsString + scheduleString - ); + infowindow.callsign = name; + infowindow.setContent(marker.getInfoHTML(name, receiverMarker)); infowindow.open(map, marker); } From 62da3e296b30b7b4a1da5f722b136a1f8361b6ce Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Fri, 28 Jul 2023 17:57:03 -0400 Subject: [PATCH 02/10] Refactoring markers. --- htdocs/lib/MarkerManager.js | 147 +++++++++++++++++--- htdocs/map.js | 260 +++--------------------------------- 2 files changed, 152 insertions(+), 255 deletions(-) diff --git a/htdocs/lib/MarkerManager.js b/htdocs/lib/MarkerManager.js index d495eb21..f026225a 100644 --- a/htdocs/lib/MarkerManager.js +++ b/htdocs/lib/MarkerManager.js @@ -91,8 +91,8 @@ MarkerManager.prototype.addType = function(type) { function Marker() {} -// Wrap given callsign or other ID into a clickable link. When -// URL not supplied, guess the correct URL by ID type. +// Wrap given callsign or other ID into a clickable link. +// When URL not supplied, guess the correct URL by ID type. Marker.linkify = function(callsign, url = null) { // Leave passed URLs as they are if (url && (url != '')) @@ -117,7 +117,7 @@ Marker.linkify = function(callsign, url = null) { return '' + callsign + ''; -} +}; // Compute distance, in kilometers, between two latlons. Marker.distanceKm = function(p1, p2) { @@ -135,30 +135,31 @@ Marker.distanceKm = function(p1, p2) { 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. +// 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 '
' + name + '
'; -} +}; +// Convert given name/value to an information section item. Marker.makeListItem = function(name, value) { return '
' + '' + name + '    ' + '' + value + '' + '
'; -} +}; // // Feature Markers @@ -168,6 +169,17 @@ function FeatureMarker() {} FeatureMarker.prototype = new google.maps.OverlayView(); +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.device = update.location.device; + this.antenna = update.location.antenna; + this.schedule = update.location.schedule; +}; + FeatureMarker.prototype.draw = function() { var div = this.div; if (!div) return; @@ -279,17 +291,118 @@ FeatureMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { return '

' + nameString + distance + '

' + commentString + detailsString + scheduleString; -} +}; // // APRS Markers (also AIS and HFDL) // -function NewAprsMarker() {} +function AprsMarker() {} -NewAprsMarker.prototype = new google.maps.OverlayView(); +AprsMarker.prototype = new google.maps.OverlayView(); -NewAprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { +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.aircraft = update.location.aircraft; + this.directivity = update.location.directivity; +}; + +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 (!this.course) { + div.style.transform = null; + } else if (this.course > 180) { + div.style.transform = 'scalex(-1) rotate(' + (270 - this.course) + 'deg)' + } else { + div.style.transform = 'rotate(' + (this.course - 90) + 'deg)'; + } + + if (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; + } + + var point = this.getProjection().fromLatLngToDivPixel(this.position); + if (point) { + div.style.left = point.x - 12 + 'px'; + div.style.top = point.y - 12 + 'px'; + } +}; + +AprsMarker.prototype.setOptions = function(options) { + google.maps.OverlayView.prototype.setOptions.apply(this, arguments); + this.draw(); +}; + +AprsMarker.prototype.onAdd = 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); + + 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); +}; + +AprsMarker.prototype.remove = function() { + if (this.div) { + this.div.parentNode.removeChild(this.div); + this.div = null; + } +}; + +AprsMarker.prototype.getAnchorPoint = function() { + return new google.maps.Point(0, -12); +}; + +AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { var timeString = moment(this.lastseen).fromNow(); var commentString = ''; var weatherString = ''; @@ -320,7 +433,7 @@ NewAprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { if (this.weather.wind) { if (this.weather.wind.speed && (this.weather.wind.speed>0)) { weatherString += Marker.makeListItem('Wind', - degToCompass(this.weather.wind.direction) + ' ' + + Marker.degToCompass(this.weather.wind.direction) + ' ' + this.weather.wind.speed.toFixed(1) + ' km/h ' ); } @@ -383,7 +496,7 @@ NewAprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { ); } else { if (this.course) { - detailsString += Marker.makeListItem('Course', degToCompass(this.course)); + detailsString += Marker.makeListItem('Course', Marker.degToCompass(this.course)); } if (this.speed) { detailsString += Marker.makeListItem('Speed', this.speed.toFixed(1) + ' km/h'); @@ -411,4 +524,4 @@ NewAprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { + '
' + timeString + ' using ' + this.mode + ( this.band ? ' on ' + this.band : '' ) + '
' + commentString + weatherString + detailsString + hopsString; -} +}; diff --git a/htdocs/map.js b/htdocs/map.js index 24decccc..1cf5af38 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -1,3 +1,10 @@ +// +// Marker.linkify() uses these URLs +// +var callsign_url = null; +var vessel_url = null; +var flight_url = null; + $(function(){ var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){ var s = v.split('='); @@ -37,9 +44,6 @@ $(function(){ var retention_time = 2 * 60 * 60 * 1000; var strokeOpacity = 0.8; var fillOpacity = 0.35; - var callsign_url = null; - var vessel_url = null; - var flight_url = null; // marker manager var markmanager = null; @@ -148,19 +152,9 @@ $(function(){ map: markmanager.isEnabled(update.mode)? map : undefined, title: update.callsign }, aprsOptions, getMarkerOpacityOptions(update.lastseen) )); - marker.lastseen = update.lastseen; - marker.mode = update.mode; - marker.hops = update.hops; - marker.band = update.band; - marker.comment = update.location.comment; - marker.weather = update.location.weather; - marker.altitude = update.location.altitude; - marker.height = update.location.height; - marker.power = update.location.power; - marker.gain = update.location.gain; - marker.device = update.location.device; - marker.aircraft = update.location.aircraft; - marker.directivity = update.location.directivity; + + // Update marker attributes + marker.update(update); if (expectedCallsign && expectedCallsign == update.callsign) { map.panTo(pos); @@ -195,7 +189,7 @@ $(function(){ } else { marker = new FeatureMarker(); marker.addListener('click', function(){ - showFeatureInfoWindow(update.callsign, pos); + showMarkerInfoWindow(update.callsign, pos); }); markers[update.callsign] = marker; markmanager.addType(update.mode); @@ -208,24 +202,17 @@ $(function(){ title: update.callsign }, options)); - // Get attributes - marker.lastseen = update.lastseen; - marker.mode = update.mode; - marker.url = update.location.url; - marker.comment = update.location.comment; - marker.altitude = update.location.altitude; - marker.device = update.location.device; - marker.antenna = update.location.antenna; - marker.schedule = update.location.schedule; + // Update marker attributes + marker.update(update); if (expectedCallsign && expectedCallsign == update.callsign) { map.panTo(pos); - showFeatureInfoWindow(update.callsign, pos); + showMarkerInfoWindow(update.callsign, pos); expectedCallsign = false; } if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) { - showFeatureInfoWindow(infowindow.callsign, pos); + showMarkerInfoWindow(infowindow.callsign, pos); } break; case 'locator': @@ -326,19 +313,11 @@ $(function(){ nite.init(map); setInterval(function() { nite.refresh() }, 10000); // every 10s }); - $.getScript('static/lib/AprsMarker.js').done(function(){ - if(typeof(FeatureMarker) != 'undefined') { - markmanager = new MarkerManager(); - processUpdates(updateQueue); - updateQueue = []; - } - }); + $.getScript('static/lib/MarkerManager.js').done(function(){ - if(typeof(AprsMarker) != 'undefined') { - markmanager = new MarkerManager(); - processUpdates(updateQueue); - updateQueue = []; - } + markmanager = new MarkerManager(); + processUpdates(updateQueue); + updateQueue = []; }); var $legend = $(".openwebrx-map-legend"); @@ -436,53 +415,6 @@ $(function(){ return infowindow; } - var linkifyCallsign = function(callsign, url = null) { - // Leave passed URLs as they are - if (url && (url != '')) - { /* leave as is */ } - // 9-character strings may be AIS MMSI numbers - else if (callsign.match(new RegExp('^[0-9]{9}$'))) - url = vessel_url; - // 3 characters and a number may be a flight number - else if (callsign.match(new RegExp('^[A-Z]{3,4}[0-9]{1,4}[A-Z]{0,2}$'))) - url = flight_url; - // 2 characters and a long number may be a flight number - else if (callsign.match(new RegExp('^[A-Z]{2}[0-9]{2,4}[A-Z]{0,2}$'))) - url = flight_url; - // Everything else is a HAM callsign - else - url = callsign_url; - - // Must have valid lookup URL - if ((url == null) || (url == '')) - return callsign; - else - return '' + callsign + ''; - } - - var 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); - } - - var truncate = function(str, count) { - return str.length > count? str.slice(0, count) + '…' : str; - } - var infowindow; var showLocatorInfoWindow = function(locator, pos) { var infowindow = getInfoWindow(); @@ -495,14 +427,14 @@ $(function(){ return b.lastseen - a.lastseen; }); var distance = receiverMarker? - " at " + distanceKm(receiverMarker.position, pos) + " km" : ""; + " 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 = linkifyCallsign(i.callsign) + ' (' + timestring + ' using ' + i.mode; + var message = Marker.linkify(i.callsign) + ' (' + timestring + ' using ' + i.mode; if (i.band) message += ' on ' + i.band; message += ')'; return '
  • ' + message + '
  • ' @@ -513,155 +445,7 @@ $(function(){ infowindow.open(map); }; - var 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]; - } - - var makeListTitle = function(name) { - return '
    ' + name + '
    '; - } - - var makeListItem = function(name, value) { - return '
    ' - + '' + name + '    ' - + '' + value + '' - + '
    '; - } - - var showMarkerInfoWindow = function(callsign, pos) { - var infowindow = getInfoWindow(); - infowindow.callsign = callsign; - var marker = markers[callsign]; - var timeString = moment(marker.lastseen).fromNow(); - var commentString = ""; - var weatherString = ""; - var detailsString = ""; - var hopsString = ""; - var distance = ""; - - if (marker.comment) { - commentString += '

    ' + makeListTitle('Comment') + '

    ' + - marker.comment + '

    '; - } - - if (marker.weather) { - weatherString += '

    ' + makeListTitle('Weather'); - - if (marker.weather.temperature) { - weatherString += makeListItem('Temperature', marker.weather.temperature.toFixed(1) + ' oC'); - } - - if (marker.weather.humidity) { - weatherString += makeListItem('Humidity', marker.weather.humidity + '%'); - } - - if (marker.weather.barometricpressure) { - weatherString += makeListItem('Pressure', marker.weather.barometricpressure.toFixed(1) + ' mbar'); - } - - if (marker.weather.wind) { - if (marker.weather.wind.speed && (marker.weather.wind.speed>0)) { - weatherString += makeListItem('Wind', - degToCompass(marker.weather.wind.direction) + ' ' + - marker.weather.wind.speed.toFixed(1) + ' km/h ' - ); - } - - if (marker.weather.wind.gust && (marker.weather.wind.gust>0)) { - weatherString += makeListItem('Gusts', marker.weather.wind.gust.toFixed(1) + ' km/h'); - } - } - - if (marker.weather.rain && (marker.weather.rain.day>0)) { - weatherString += makeListItem('Rain', - marker.weather.rain.hour.toFixed(0) + ' mm/h, ' + - marker.weather.rain.day.toFixed(0) + ' mm/day' -// marker.weather.rain.sincemidnight + ' mm since midnight' - ); - } - - if (marker.weather.snowfall) { - weatherString += makeListItem('Snow', marker.weather.snowfall.toFixed(1) + ' cm'); - } - - weatherString += '

    '; - } - - if (marker.altitude) { - detailsString += makeListItem('Altitude', marker.altitude.toFixed(0) + ' m'); - } - - if (marker.device) { - detailsString += makeListItem('Device', marker.device.manufacturer? - marker.device.device + " by " + marker.device.manufacturer - : marker.device - ); - } - - if (marker.height) { - detailsString += makeListItem('Height', marker.height.toFixed(0) + ' m'); - } - - if (marker.power) { - detailsString += makeListItem('Power', marker.power + " W"); - } - - if (marker.gain) { - detailsString += makeListItem('Gain', marker.gain + " dB"); - } - - if (marker.directivity) { - detailsString += makeListItem('Direction', marker.directivity); - } - - if (marker.aircraft) { - detailsString += makeListItem('Aircraft', marker.aircraft); - } - - // Combine course and speed if both present - if (marker.course && marker.speed) { - detailsString += makeListItem('Course', - degToCompass(marker.course) + " " + - marker.speed.toFixed(1) + " km/h" - ); - } else { - if (marker.course) { - detailsString += makeListItem('Course', degToCompass(marker.course)); - } - if (marker.speed) { - detailsString += makeListItem('Speed', marker.speed.toFixed(1) + " km/h"); - } - } - - if (detailsString.length > 0) { - detailsString = '

    ' + makeListTitle('Details') + detailsString + '

    '; - } - - if (receiverMarker) { - distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km"; - } - - if (marker.hops && marker.hops.length > 0) { - var hops = marker.hops.toString().split(','); - hops.forEach(function(part, index, hops) { - hops[index] = linkifyCallsign(part); - }); - - hopsString = '

    via ' + hops.join(', ') + ' 

    '; - } - - infowindow.setContent( - '

    ' + linkifyCallsign(callsign) + distance + '

    ' + - '
    ' + timeString + ' using ' + marker.mode + - ( marker.band ? ' on ' + marker.band : '' ) + '
    ' + - commentString + weatherString + detailsString + hopsString - ); - - infowindow.open(map, marker); - } - - var showFeatureInfoWindow = function(name, pos) { + var showMarkerInfoWindow = function(name, pos) { var infowindow = getInfoWindow(); var marker = markers[name]; From ba8f66497a68fbbc569ce49619622d452db1543b Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Fri, 28 Jul 2023 23:13:24 -0400 Subject: [PATCH 03/10] Proper inheritance now. --- htdocs/lib/MarkerManager.js | 83 ++++++++++++++++++++++--------------- htdocs/map.js | 12 +++--- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/htdocs/lib/MarkerManager.js b/htdocs/lib/MarkerManager.js index f026225a..54f2f983 100644 --- a/htdocs/lib/MarkerManager.js +++ b/htdocs/lib/MarkerManager.js @@ -167,7 +167,7 @@ Marker.makeListItem = function(name, value) { function FeatureMarker() {} -FeatureMarker.prototype = new google.maps.OverlayView(); +FeatureMarker.prototype = new FeatureMarker(); FeatureMarker.prototype.update = function(update) { this.lastseen = update.lastseen; @@ -194,12 +194,7 @@ FeatureMarker.prototype.draw = function() { } }; -FeatureMarker.prototype.setOptions = function(options) { - google.maps.OverlayView.prototype.setOptions.apply(this, arguments); - this.draw(); -}; - -FeatureMarker.prototype.onAdd = function() { +FeatureMarker.prototype.create = function() { var div = this.div = document.createElement('div'); // Marker size @@ -214,14 +209,7 @@ FeatureMarker.prototype.onAdd = function() { div.style.fontSize = this.symHeight + 'px'; div.style.lineHeight = this.symHeight + 'px'; - 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); + return div; }; FeatureMarker.prototype.remove = function() { @@ -231,8 +219,8 @@ FeatureMarker.prototype.remove = function() { } }; -FeatureMarker.prototype.getAnchorPoint = function() { - return new google.maps.Point(0, -this.symHeight/2); +FeatureMarker.prototype.getAnchorOffset = function() { + return [0, -this.symHeight/2]; }; FeatureMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { @@ -299,7 +287,7 @@ FeatureMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { function AprsMarker() {} -AprsMarker.prototype = new google.maps.OverlayView(); +AprsMarker.prototype = new AprsMarker(); AprsMarker.prototype.update = function(update) { this.lastseen = update.lastseen; @@ -359,12 +347,7 @@ AprsMarker.prototype.draw = function() { } }; -AprsMarker.prototype.setOptions = function(options) { - google.maps.OverlayView.prototype.setOptions.apply(this, arguments); - this.draw(); -}; - -AprsMarker.prototype.onAdd = function() { +AprsMarker.prototype.create = function() { var div = this.div = document.createElement('div'); div.style.position = 'absolute'; @@ -381,14 +364,7 @@ AprsMarker.prototype.onAdd = function() { div.appendChild(overlay); - 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); + return div; }; AprsMarker.prototype.remove = function() { @@ -398,8 +374,8 @@ AprsMarker.prototype.remove = function() { } }; -AprsMarker.prototype.getAnchorPoint = function() { - return new google.maps.Point(0, -12); +AprsMarker.prototype.getAnchorOffset = function() { + return [0, -12]; }; AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { @@ -525,3 +501,42 @@ AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { + ( this.band ? ' on ' + this.band : '' ) + '' + commentString + weatherString + detailsString + hopsString; }; + +// +// GoogleMaps-Specific Markers (derived from generic markers) +// + +function GMarker() {} +GMarker.prototype = new google.maps.OverlayView(); + +GMarker.prototype.setOptions = function(options) { + google.maps.OverlayView.prototype.setOptions.apply(this, arguments); + 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]); +}; + +// GoogleMaps-Specific FeatureMarker +function GFeatureMarker() { $.extend(this, new FeatureMarker()); } +GFeatureMarker.prototype = new GMarker(); + +// GoogleMaps-Specific AprsMarker +function GAprsMarker() { $.extend(this, new AprsMarker()); } +GAprsMarker.prototype = new GMarker(); diff --git a/htdocs/map.js b/htdocs/map.js index 1cf5af38..e8de67c1 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -132,7 +132,7 @@ $(function(){ var markerClass = google.maps.Marker; var aprsOptions = {} if (update.location.symbol) { - markerClass = AprsMarker; + markerClass = GAprsMarker; aprsOptions.symbol = update.location.symbol; aprsOptions.course = update.location.course; aprsOptions.speed = update.location.speed; @@ -140,12 +140,11 @@ $(function(){ if (markers[update.callsign]) { marker = markers[update.callsign]; } else { - marker = new markerClass(); + markmanager.addType(update.mode); + marker = markers[update.callsign] = new markerClass(); marker.addListener('click', function(){ showMarkerInfoWindow(update.callsign, pos); }); - markers[update.callsign] = marker; - markmanager.addType(update.mode); } marker.setOptions($.extend({ position: pos, @@ -187,12 +186,11 @@ $(function(){ if (markers[update.callsign]) { marker = markers[update.callsign]; } else { - marker = new FeatureMarker(); + markmanager.addType(update.mode); + marker = markers[update.callsign] = new GFeatureMarker(); marker.addListener('click', function(){ showMarkerInfoWindow(update.callsign, pos); }); - markers[update.callsign] = marker; - markmanager.addType(update.mode); } // Apply marker options From 90a99f7a32778ed28a151697d51a1528e82be196 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Sat, 29 Jul 2023 14:01:50 -0400 Subject: [PATCH 04/10] Moved markers storage into MarkerManager. --- htdocs/lib/MarkerManager.js | 57 ++++++++++++++++++++++---- htdocs/map.js | 80 +++++++++++++++---------------------- 2 files changed, 82 insertions(+), 55 deletions(-) diff --git a/htdocs/lib/MarkerManager.js b/htdocs/lib/MarkerManager.js index 54f2f983..64756a9b 100644 --- a/htdocs/lib/MarkerManager.js +++ b/htdocs/lib/MarkerManager.js @@ -3,17 +3,20 @@ // function MarkerManager() { - // Currently known features + // Current markers + this.markers = {}; + + // Currently known marker types this.types = {}; - // Colors used for features + // Colors used for marker types this.colors = { 'KiwiSDR' : '#800000', 'WebSDR' : '#000080', 'OpenWebRX' : '#004000' }; - // Symbols used for features + // Symbols used for marker types this.symbols = { 'KiwiSDR' : '◬', 'WebSDR' : '◬', @@ -24,7 +27,7 @@ function MarkerManager() { 'HFDL' : '✈' }; - // Feature type shown/hidden status + // Marker type shown/hidden status this.enabled = { 'KiwiSDR' : false, 'WebSDR' : false, @@ -48,13 +51,13 @@ MarkerManager.prototype.isEnabled = function(type) { return type in this.enabled? this.enabled[type] : true; }; -MarkerManager.prototype.toggle = function(map, markers, type, onoff) { +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(markers, function(_, r) { - if (r.mode === type) r.setMap(onoff ? map : undefined); + $.each(this.markers, function(_, x) { + if (x.mode === type) x.setMap(onoff ? map : undefined); }); }; @@ -85,6 +88,28 @@ MarkerManager.prototype.addType = function(type) { } }; +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(); + $.each(this.markers, function(id, x) { + if (!x.age(now - x.lastseen)) delete this.markers[id]; + }); +}; + +MarkerManager.prototype.clear = function() { + // Remove all markers from the map + $.each(this.markers, function(_, x) { x.setMap(); }); + // Delete all markers + this.markers = {}; +}; + // // Generic marker functionality // @@ -161,6 +186,14 @@ Marker.makeListItem = function(name, value) { + ''; }; +Marker.getOpacityScale = function(age) { + var scale = 1; + if (age >= retention_time / 2) { + scale = (retention_time - age) / (retention_time / 2); + } + return Math.max(0, Math.min(1, scale)); +}; + // // Feature Markers // @@ -533,6 +566,16 @@ GMarker.prototype.getAnchorPoint = function() { return new google.maps.Point(offset[0], offset[1]); }; +GMarker.prototype.age = function(age) { + if(age <= retention_time) { + this.setOptions({ opacity: Marker.getOpacityScale(age) }); + return true; + } else { + this.setMap(); + return false; + } +}; + // GoogleMaps-Specific FeatureMarker function GFeatureMarker() { $.extend(this, new FeatureMarker()); } GFeatureMarker.prototype = new GMarker(); diff --git a/htdocs/map.js b/htdocs/map.js index e8de67c1..4db1500a 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -1,10 +1,17 @@ -// // Marker.linkify() uses these URLs -// var callsign_url = null; var vessel_url = null; var flight_url = null; +// reasonable default; will be overriden by server +var retention_time = 2 * 60 * 60 * 1000; + +// Function to toggle legend box on/off +function toggleElement(el_name) { + var el = document.getElementById(el_name); + if (el) el.style.display = el.style.display === 'none'? 'block' : 'none'; +} + $(function(){ var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){ var s = v.split('='); @@ -35,13 +42,10 @@ $(function(){ var ws_url = href + "ws/"; var map; - var markers = {}; var rectangles = {}; var receiverMarker; var updateQueue = []; - // reasonable default; will be overriden by server - var retention_time = 2 * 60 * 60 * 1000; var strokeOpacity = 0.8; var fillOpacity = 0.35; @@ -128,7 +132,7 @@ $(function(){ switch (update.location.type) { case 'latlon': var pos = new google.maps.LatLng(update.location.lat, update.location.lon); - var marker; + var marker = markmanager.find(update.callsign); var markerClass = google.maps.Marker; var aprsOptions = {} if (update.location.symbol) { @@ -137,22 +141,26 @@ $(function(){ aprsOptions.course = update.location.course; aprsOptions.speed = update.location.speed; } - if (markers[update.callsign]) { - marker = markers[update.callsign]; - } else { + + // If new item, create a new marker for it + if (!marker) { + marker = new markerClass(); markmanager.addType(update.mode); - marker = markers[update.callsign] = new markerClass(); + markmanager.add(update.callsign, marker); marker.addListener('click', function(){ showMarkerInfoWindow(update.callsign, pos); }); } + + // Apply marker options marker.setOptions($.extend({ position: pos, map: markmanager.isEnabled(update.mode)? map : undefined, title: update.callsign - }, aprsOptions, getMarkerOpacityOptions(update.lastseen) )); + }, aprsOptions)); - // Update marker attributes + // Update marker attributes and age + marker.age(new Date().getTime() - update.lastseen); marker.update(update); if (expectedCallsign && expectedCallsign == update.callsign) { @@ -167,8 +175,8 @@ $(function(){ break; case 'feature': var pos = new google.maps.LatLng(update.location.lat, update.location.lon); + var marker = markmanager.find(update.callsign); var options = {} - var marker; // If no symbol or color supplied, use defaults by type if (update.location.symbol) { @@ -182,12 +190,11 @@ $(function(){ options.color = markmanager.getColor(update.mode); } - // If new item, create a new feature marker for it - if (markers[update.callsign]) { - marker = markers[update.callsign]; - } else { + // If new item, create a new marker for it + if (!marker) { + marker = new GFeatureMarker(); markmanager.addType(update.mode); - marker = markers[update.callsign] = new GFeatureMarker(); + markmanager.add(update.callsign, marker); marker.addListener('click', function(){ showMarkerInfoWindow(update.callsign, pos); }); @@ -200,7 +207,8 @@ $(function(){ title: update.callsign }, options)); - // Update marker attributes + // Update marker attributes and age + marker.age(new Date().getTime() - update.lastseen); marker.update(update); if (expectedCallsign && expectedCallsign == update.callsign) { @@ -265,10 +273,9 @@ $(function(){ var clearMap = function(){ var reset = function(callsign, item) { item.setMap(); }; - $.each(markers, reset); - $.each(rectangles, reset); receiverMarker.setMap(); - markers = {}; + markmanager.clear(); + $.each(rectangles, reset); rectangles = {}; }; @@ -445,7 +452,7 @@ $(function(){ var showMarkerInfoWindow = function(name, pos) { var infowindow = getInfoWindow(); - var marker = markers[name]; + var marker = markmanager.find(name); infowindow.callsign = name; infowindow.setContent(marker.getInfoHTML(name, receiverMarker)); @@ -478,13 +485,6 @@ $(function(){ }; }; - var getMarkerOpacityOptions = function(lastseen) { - var scale = getScale(lastseen); - return { - opacity: scale - }; - }; - // fade out / remove positions after time setInterval(function(){ var now = new Date().getTime(); @@ -497,15 +497,7 @@ $(function(){ } m.setOptions(getRectangleOpacityOptions(m.lastseen)); }); - $.each(markers, function(callsign, m) { - var age = now - m.lastseen; - if (age > retention_time) { - delete markers[callsign]; - m.setMap(); - return; - } - m.setOptions(getMarkerOpacityOptions(m.lastseen)); - }); + markmanager.ageAll(); }, 1000); var rectangleFilter = allRectangles = function() { return true; }; @@ -548,16 +540,8 @@ $(function(){ } else { $el.addClass('disabled'); } - markmanager.toggle(map, markers, $el.data('selector'), onoff); + markmanager.toggle(map, $el.data('selector'), onoff); }); } }); - -function toggleElement(el_name) { - var el = document.getElementById(el_name); - if (el) { - el.style.display = el.style.display === 'none'? - 'block' : 'none'; - } -} From 8aae159bad7c80b8eb2ca91c814966c72f706684 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Sun, 30 Jul 2023 12:18:50 -0400 Subject: [PATCH 05/10] 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('
      ' + list.join('') + '
    '); +} + +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:
      ' + list + '
    '; +}; + +// +// 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('
      ' + lis.join('') + '
    '); - } - 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')); } }); From e7a88cc68abea23924566efec418179e20444928 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Sun, 30 Jul 2023 12:34:26 -0400 Subject: [PATCH 06/10] Performance issue with GMap rectangles solved. --- htdocs/lib/LocatorManager.js | 24 ++++++++++++++---------- htdocs/map.js | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/htdocs/lib/LocatorManager.js b/htdocs/lib/LocatorManager.js index 11d5eedb..28bdaf01 100644 --- a/htdocs/lib/LocatorManager.js +++ b/htdocs/lib/LocatorManager.js @@ -45,7 +45,7 @@ LocatorManager.prototype.ageAll = function() { LocatorManager.prototype.clear = function() { // Remove all rectangles from the map - $.each(this.markers, function(_, x) { x.setMap(); }); + $.each(this.rectangles, function(_, x) { x.setMap(); }); // Delete all rectangles this.rectangles = {}; }; @@ -196,12 +196,18 @@ Locator.prototype.update = function(update) { // GoogleMaps-Specific Locators (derived from generic locators) // -function GLocator() { $.extend(this, new Locator()); } +function GLocator() { + this.rect = new google.maps.Rectangle(); +} -GLocator.prototype = new google.maps.Rectangle(); +GLocator.prototype = new Locator(); GLocator.prototype.setOptions = function(options) { - google.maps.Rectangle.prototype.setOptions.apply(this, arguments); + this.rect.setOptions(options); +}; + +GLocator.prototype.setMap = function(map) { + this.rect.setMap(map); }; GLocator.prototype.setCenter = function(lat, lon) { @@ -218,12 +224,10 @@ GLocator.prototype.setCenter = function(lat, lon) { 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 -// }); + this.setOptions({ + strokeOpacity : LocatorManager.strokeOpacity * scale, + fillOpacity : LocatorManager.fillOpacity * scale + }); return true; } else { this.setMap(); diff --git a/htdocs/map.js b/htdocs/map.js index 65310a43..095951fa 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -165,8 +165,8 @@ $(function(){ if (!rectangle) { rectangle = new GLocator(); locmanager.add(update.callsign, rectangle); - rectangle.addListener('click', function() { - showLocatorInfoWindow(this.locator, this.center); + rectangle.rect.addListener('click', function() { + showLocatorInfoWindow(rectangle.locator, rectangle.center); }); } From 10401f0bdb936b1b8bdabbfd2258d3a1fa822ebd Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Sun, 30 Jul 2023 13:13:44 -0400 Subject: [PATCH 07/10] Defined API between abstract and implementation-specific Locators. --- htdocs/lib/LocatorManager.js | 54 ++++++++++++++++++++++-------------- htdocs/map.js | 14 +++------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/htdocs/lib/LocatorManager.js b/htdocs/lib/LocatorManager.js index 28bdaf01..3e2cf799 100644 --- a/htdocs/lib/LocatorManager.js +++ b/htdocs/lib/LocatorManager.js @@ -69,8 +69,7 @@ LocatorManager.prototype.setFilter = function(map, filterBy = null) { LocatorManager.prototype.reColor = function() { var self = this; $.each(this.rectangles, function(_, x) { - var color = self.getColor(x); - x.setOptions({ strokeColor: color, fillColor: color }); + x.setColor(self.getColor(x)); }); }; @@ -170,7 +169,9 @@ LocatorManager.prototype.getInfoHTML = function(locator, pos, receiverMarker = n }; // -// Generic locator functionality +// Generic Map Locator +// Derived classes have to implement: +// setMap(), setCenter(), setColor(), setOpacity() // function Locator() {} @@ -190,22 +191,36 @@ Locator.prototype.update = function(update) { // 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; + } }; // -// GoogleMaps-Specific Locators (derived from generic locators) +// GoogleMaps-specific Map Locator (derived from generic locator) // function GLocator() { this.rect = new google.maps.Rectangle(); + this.rect.setOptions({ + strokeWeight : 2, + strokeColor : "#FFFFFF", + fillColor : "#FFFFFF" + }); } GLocator.prototype = new Locator(); -GLocator.prototype.setOptions = function(options) { - this.rect.setOptions(options); -}; - GLocator.prototype.setMap = function(map) { this.rect.setMap(map); }; @@ -213,7 +228,7 @@ GLocator.prototype.setMap = function(map) { GLocator.prototype.setCenter = function(lat, lon) { this.center = new google.maps.LatLng({lat: lat, lng: lon}); - this.setOptions({ bounds : { + this.rect.setOptions({ bounds : { north : lat - 0.5, south : lat + 0.5, west : lon - 1.0, @@ -221,16 +236,13 @@ GLocator.prototype.setCenter = function(lat, lon) { }}); } -GLocator.prototype.age = function(age) { - if (age <= retention_time) { - var scale = Marker.getOpacityScale(age); - this.setOptions({ - strokeOpacity : LocatorManager.strokeOpacity * scale, - fillOpacity : LocatorManager.fillOpacity * scale - }); - return true; - } else { - this.setMap(); - return false; - } +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 + }); }; diff --git a/htdocs/map.js b/htdocs/map.js index 095951fa..531ab16d 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -170,18 +170,12 @@ $(function(){ }); } - // Update locator attributes, center, and age - rectangle.age(new Date().getTime() - update.lastseen); + // Update locator attributes, center, age 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 - }); + // Assign locator to map and set its color + rectangle.setMap(locmanager.filter(rectangle)? map : undefined); + rectangle.setColor(locmanager.getColor(rectangle)); if (expectedLocator && expectedLocator == update.location.locator) { map.panTo(rectangle.center); From 263864700617e81d87c9ebb84477ca90bca761c1 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Sun, 30 Jul 2023 14:10:42 -0400 Subject: [PATCH 08/10] Added proper derivation for markers. --- htdocs/lib/MarkerManager.js | 88 +++++++++++++++++++++++++------------ htdocs/map.js | 51 ++++++++++----------- 2 files changed, 82 insertions(+), 57 deletions(-) diff --git a/htdocs/lib/MarkerManager.js b/htdocs/lib/MarkerManager.js index 9a538e25..0f41aa4f 100644 --- a/htdocs/lib/MarkerManager.js +++ b/htdocs/lib/MarkerManager.js @@ -112,7 +112,9 @@ MarkerManager.prototype.clear = function() { }; // -// Generic marker functionality +// Generic Map Marker +// Derived classes have to implement: +// setMap(), setMarkerOpacity() // function Marker() {} @@ -187,6 +189,7 @@ Marker.makeListItem = function(name, value) { + ''; }; +// Get opacity value in the 0..1 range based on the given age. Marker.getOpacityScale = function(age) { var scale = 1; if (age >= retention_time / 2) { @@ -195,13 +198,37 @@ Marker.getOpacityScale = function(age) { 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) { + if(age <= retention_time) { + this.setMarkerOpacity(Marker.getOpacityScale(age)); + 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 Markers +// Feature Marker +// Represents static map features, such as stations and receivers. +// Derived classes have to implement: +// setMarkerOpacity() // function FeatureMarker() {} -FeatureMarker.prototype = new FeatureMarker(); +FeatureMarker.prototype = new Marker(); FeatureMarker.prototype.update = function(update) { this.lastseen = update.lastseen; @@ -212,6 +239,12 @@ FeatureMarker.prototype.update = function(update) { this.device = update.location.device; this.antenna = update.location.antenna; this.schedule = update.location.schedule; + + // Implementation-dependent function call + this.setMarkerPosition(update.callsign, update.location.lat, update.location.lon); + + // Age locator + this.age(new Date().getTime() - update.lastseen); }; FeatureMarker.prototype.draw = function() { @@ -246,13 +279,6 @@ FeatureMarker.prototype.create = function() { return div; }; -FeatureMarker.prototype.remove = function() { - if (this.div) { - this.div.parentNode.removeChild(this.div); - this.div = null; - } -}; - FeatureMarker.prototype.getAnchorOffset = function() { return [0, -this.symHeight/2]; }; @@ -316,12 +342,16 @@ FeatureMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { }; // -// APRS Markers (also AIS and HFDL) +// APRS Marker +// Represents APRS transmitters, as well as AIS (vessels) +// and HFDL (planes). +// Derived classes have to implement: +// setMarkerOpacity() // function AprsMarker() {} -AprsMarker.prototype = new AprsMarker(); +AprsMarker.prototype = new Marker(); AprsMarker.prototype.update = function(update) { this.lastseen = update.lastseen; @@ -337,6 +367,12 @@ AprsMarker.prototype.update = function(update) { this.device = update.location.device; this.aircraft = update.location.aircraft; this.directivity = update.location.directivity; + + // Implementation-dependent function call + this.setMarkerPosition(update.callsign, update.location.lat, update.location.lon); + + // Age locator + this.age(new Date().getTime() - update.lastseen); }; AprsMarker.prototype.draw = function() { @@ -401,13 +437,6 @@ AprsMarker.prototype.create = function() { return div; }; -AprsMarker.prototype.remove = function() { - if (this.div) { - this.div.parentNode.removeChild(this.div); - this.div = null; - } -}; - AprsMarker.prototype.getAnchorOffset = function() { return [0, -12]; }; @@ -543,8 +572,8 @@ AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { function GMarker() {} GMarker.prototype = new google.maps.OverlayView(); -GMarker.prototype.setOptions = function(options) { - google.maps.OverlayView.prototype.setOptions.apply(this, arguments); +GMarker.prototype.setMarkerOptions = function(options) { + this.setOptions(options); this.draw(); }; @@ -567,14 +596,15 @@ GMarker.prototype.getAnchorPoint = function() { return new google.maps.Point(offset[0], offset[1]); }; -GMarker.prototype.age = function(age) { - if(age <= retention_time) { - this.setOptions({ opacity: Marker.getOpacityScale(age) }); - return true; - } else { - this.setMap(); - return false; - } +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 diff --git a/htdocs/map.js b/htdocs/map.js index 531ab16d..73ec14fc 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -66,10 +66,10 @@ $(function(){ updates.forEach(function(update) { switch (update.location.type) { case 'latlon': - var pos = new google.maps.LatLng(update.location.lat, update.location.lon); var marker = markmanager.find(update.callsign); var markerClass = google.maps.Marker; var aprsOptions = {} + if (update.location.symbol) { markerClass = GAprsMarker; aprsOptions.symbol = update.location.symbol; @@ -82,35 +82,32 @@ $(function(){ marker = new markerClass(); markmanager.addType(update.mode); markmanager.add(update.callsign, marker); - marker.addListener('click', function(){ - showMarkerInfoWindow(update.callsign, pos); + marker.addListener('click', function() { + showMarkerInfoWindow(update.callsign, marker.pos); }); } - // Apply marker options - marker.setOptions($.extend({ - position: pos, - map: markmanager.isEnabled(update.mode)? map : undefined, - title: update.callsign - }, aprsOptions)); - // Update marker attributes and age - marker.age(new Date().getTime() - update.lastseen); marker.update(update); + // Assign marker to map + marker.setMap(markmanager.isEnabled(update.mode)? map : undefined); + + // Apply marker options + marker.setMarkerOptions(aprsOptions); + if (expectedCallsign && expectedCallsign == update.callsign) { - map.panTo(pos); - showMarkerInfoWindow(update.callsign, pos); + map.panTo(marker.pos); + showMarkerInfoWindow(update.callsign, marker.pos); expectedCallsign = false; } if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) { - showMarkerInfoWindow(infowindow.callsign, pos); + showMarkerInfoWindow(infowindow.callsign, marker.pos); } break; case 'feature': - var pos = new google.maps.LatLng(update.location.lat, update.location.lon); var marker = markmanager.find(update.callsign); var options = {} @@ -131,30 +128,28 @@ $(function(){ marker = new GFeatureMarker(); markmanager.addType(update.mode); markmanager.add(update.callsign, marker); - marker.addListener('click', function(){ - showMarkerInfoWindow(update.callsign, pos); + marker.addListener('click', function() { + showMarkerInfoWindow(update.callsign, marker.pos); }); } - // Apply marker options - marker.setOptions($.extend({ - position: pos, - map: markmanager.isEnabled(update.mode)? map : undefined, - title: update.callsign - }, options)); - // Update marker attributes and age - marker.age(new Date().getTime() - update.lastseen); marker.update(update); + // Assign marker to map + marker.setMap(markmanager.isEnabled(update.mode)? map : undefined); + + // Apply marker options + marker.setMarkerOptions(options); + if (expectedCallsign && expectedCallsign == update.callsign) { - map.panTo(pos); - showMarkerInfoWindow(update.callsign, pos); + map.panTo(marker.pos); + showMarkerInfoWindow(update.callsign, marker.pos); expectedCallsign = false; } if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) { - showMarkerInfoWindow(infowindow.callsign, pos); + showMarkerInfoWindow(infowindow.callsign, marker.pos); } break; From b846fb2d8846712fdbee8c809fe2150ce3960d0d Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Mon, 31 Jul 2023 21:53:07 -0400 Subject: [PATCH 09/10] Finished refactoring maps. --- htdocs/lib/Map.js | 197 ++++++ .../lib/{LocatorManager.js => MapLocators.js} | 41 -- .../lib/{MarkerManager.js => MapMarkers.js} | 50 -- htdocs/map.js | 657 +++++++----------- owrx/controllers/assets.py | 3 + 5 files changed, 462 insertions(+), 486 deletions(-) create mode 100644 htdocs/lib/Map.js rename htdocs/lib/{LocatorManager.js => MapLocators.js} (85%) rename htdocs/lib/{MarkerManager.js => MapMarkers.js} (93%) diff --git a/htdocs/lib/Map.js b/htdocs/lib/Map.js new file mode 100644 index 00000000..22b96efd --- /dev/null +++ b/htdocs/lib/Map.js @@ -0,0 +1,197 @@ +// +// 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(); + + // Clock display + this.clock = new Clock($("#openwebrx-clock-utc")); + + // Toggle color modes on click + $(function() { + $('#openwebrx-map-colormode').on('change', function() { + self.lman.setColorMode(map, $(this).val()); + }); + }); + + // Toggle legend box on/off when clicking the clock + $(function() { + $('#openwebrx-clock-utc').on('click', function() { + var el = document.getElementById('openwebrx-map-selectors'); + if (el) { + el.style.display = el.style.display === 'none'? + 'block' : 'none'; + } + }); + }); + + // Fade out / remove positions after time + setInterval(function() { + self.lman.ageAll(); + self.mman.ageAll(); + }, 1000); + + // 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': + $('.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 + ); + } + 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; + } + 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(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); + }); +}; diff --git a/htdocs/lib/LocatorManager.js b/htdocs/lib/MapLocators.js similarity index 85% rename from htdocs/lib/LocatorManager.js rename to htdocs/lib/MapLocators.js index 3e2cf799..9e8f7523 100644 --- a/htdocs/lib/LocatorManager.js +++ b/htdocs/lib/MapLocators.js @@ -205,44 +205,3 @@ Locator.prototype.age = function(age) { return false; } }; - -// -// GoogleMaps-specific Map Locator (derived from generic 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 - }); -}; diff --git a/htdocs/lib/MarkerManager.js b/htdocs/lib/MapMarkers.js similarity index 93% rename from htdocs/lib/MarkerManager.js rename to htdocs/lib/MapMarkers.js index 0f41aa4f..e51c279a 100644 --- a/htdocs/lib/MarkerManager.js +++ b/htdocs/lib/MapMarkers.js @@ -564,53 +564,3 @@ AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) { + ( this.band ? ' on ' + this.band : '' ) + '' + commentString + weatherString + detailsString + hopsString; }; - -// -// GoogleMaps-Specific Markers (derived from generic markers) -// - -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(); - -// GoogleMaps-Specific AprsMarker -function GAprsMarker() { $.extend(this, new AprsMarker()); } -GAprsMarker.prototype = new GMarker(); diff --git a/htdocs/map.js b/htdocs/map.js index 73ec14fc..08e8ef31 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -6,406 +6,273 @@ var flight_url = null; // reasonable default; will be overriden by server var retention_time = 2 * 60 * 60 * 1000; -// Function to toggle legend box on/off -function toggleElement(el_name) { - var el = document.getElementById(el_name); - if (el) el.style.display = el.style.display === 'none'? 'block' : 'none'; +// 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; + +// 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( + '

    ' + marker.config['receiver_name'] + '

    ' + + '
    Receiver Location
    ' + ); + iw.open(map, marker); +}; + +// +// GOOGLE-SPECIFIC MAP MANAGER METHODS +// + +MapManager.prototype.setReceiverName = function(name) { + if (this.receiverMarker) this.receiverMarker.setOptions({ title: name }); } -$(function(){ - 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); - }); +MapManager.prototype.removeReceiver = function() { + if (this.receiverMarker) this.receiverMarker.setMap(); +} - var expectedCallsign; - if (query.callsign) expectedCallsign = decodeURIComponent(query.callsign); - var expectedLocator; - if (query.locator) expectedLocator = query.locator; +MapManager.prototype.initializeMap = function(receiver_gps, api_key) { + var receiverPos = { lat: receiver_gps.lat, lng: receiver_gps.lon }; - var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws'; - - var href = window.location.href; - var index = href.lastIndexOf('/'); - if (index > 0) { - href = href.substr(0, index + 1); - } - href = href.split("://")[1]; - href = protocol + "://" + href; - if (!href.endsWith('/')) { - href += '/'; - } - var ws_url = href + "ws/"; - - var map; - var receiverMarker; - var updateQueue = []; - - // marker and locator managers - var markmanager = null; - var locmanager = null; - - // clock - var clock = new Clock($("#openwebrx-clock-utc")); - - $(function() { - $('#openwebrx-map-colormode').on('change', function() { - locmanager.setColorMode(map, $(this).val()); + if (map) { + this.receiverMarker.setOptions({ + map : map, + position : receiverPos, + config : this.config }); - }); + } else { + var self = this; - var processUpdates = function(updates) { - if (!markmanager || !locmanager) { - updateQueue = updateQueue.concat(updates); - return; - } - updates.forEach(function(update) { - switch (update.location.type) { - case 'latlon': - var marker = markmanager.find(update.callsign); - var markerClass = google.maps.Marker; - 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) { - marker = new markerClass(); - markmanager.addType(update.mode); - markmanager.add(update.callsign, marker); - marker.addListener('click', function() { - showMarkerInfoWindow(update.callsign, marker.pos); - }); - } - - // Update marker attributes and age - marker.update(update); - - // Assign marker to map - marker.setMap(markmanager.isEnabled(update.mode)? map : undefined); - - // Apply marker options - marker.setMarkerOptions(aprsOptions); - - if (expectedCallsign && expectedCallsign == update.callsign) { - map.panTo(marker.pos); - showMarkerInfoWindow(update.callsign, marker.pos); - expectedCallsign = false; - } - - if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) { - showMarkerInfoWindow(infowindow.callsign, marker.pos); - } - break; - - case 'feature': - var marker = markmanager.find(update.callsign); - var options = {} - - // If no symbol or color supplied, use defaults by type - if (update.location.symbol) { - options.symbol = update.location.symbol; - } else { - options.symbol = markmanager.getSymbol(update.mode); - } - if (update.location.color) { - options.color = update.location.color; - } else { - options.color = markmanager.getColor(update.mode); - } - - // If new item, create a new marker for it - if (!marker) { - marker = new GFeatureMarker(); - markmanager.addType(update.mode); - markmanager.add(update.callsign, marker); - marker.addListener('click', function() { - showMarkerInfoWindow(update.callsign, marker.pos); - }); - } - - // Update marker attributes and age - marker.update(update); - - // Assign marker to map - marker.setMap(markmanager.isEnabled(update.mode)? map : undefined); - - // Apply marker options - marker.setMarkerOptions(options); - - if (expectedCallsign && expectedCallsign == update.callsign) { - map.panTo(marker.pos); - showMarkerInfoWindow(update.callsign, marker.pos); - expectedCallsign = false; - } - - if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) { - showMarkerInfoWindow(infowindow.callsign, marker.pos); - } - break; - - case 'locator': - 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.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(locmanager.filter(rectangle)? map : undefined); - rectangle.setColor(locmanager.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; - } - }); - }; - - var clearMap = function(){ - var reset = function(callsign, item) { item.setMap(); }; - receiverMarker.setMap(); - markmanager.clear(); - locmanager.clear(); - }; - - var reconnect_timeout = false; - - var config = {} - - var connect = function(){ - var ws = new WebSocket(ws_url); - ws.onopen = function(){ - ws.send("SERVER DE CLIENT client=map.js type=map"); - reconnect_timeout = false - }; - - ws.onmessage = 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 "config": - Object.assign(config, json.value); - if ('receiver_gps' in config) { - var receiverPos = { - lat: config.receiver_gps.lat, - lng: config.receiver_gps.lon - }; - if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ - map = new google.maps.Map($('.openwebrx-map')[0], { - center: receiverPos, - zoom: 5, - }); - - $.getScript("static/lib/nite-overlay.js").done(function(){ - nite.init(map); - setInterval(function() { nite.refresh() }, 10000); // every 10s - }); - - $.getScript('static/lib/MarkerManager.js').done(function(){ - markmanager = new MarkerManager(); - 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"); - setupLegendFilters($legend); - map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($legend[0]); - - if (!receiverMarker) { - receiverMarker = new google.maps.Marker(); - receiverMarker.addListener('click', function() { - showReceiverInfoWindow(receiverMarker); - }); - } - receiverMarker.setOptions({ - map: map, - position: receiverPos, - title: config['receiver_name'], - config: config - }); - - }); else { - receiverMarker.setOptions({ - map: map, - position: receiverPos, - config: config - }); - } - } - if ('receiver_name' in config && receiverMarker) { - receiverMarker.setOptions({ - title: config['receiver_name'] - }); - } - if ('map_position_retention_time' in config) { - retention_time = config.map_position_retention_time * 1000; - } - if ('callsign_url' in config) { - callsign_url = config['callsign_url']; - } - if ('vessel_url' in config) { - vessel_url = config['vessel_url']; - } - if ('flight_url' in config) { - flight_url = config['flight_url']; - } - break; - case "update": - processUpdates(json.value); - break; - case 'receiver_details': - $('.webrx-top-container').header().setDetails(json['value']); - break; - default: - console.warn('received message of unknown type: ' + json['type']); - } - } catch (e) { - // don't lose exception - console.error(e); - } - }; - ws.onclose = function(){ - clearMap(); - if (reconnect_timeout) { - // max value: roundabout 8 and a half minutes - reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); - } else { - // initial value: 1s - reconnect_timeout = 1000; - } - setTimeout(connect, reconnect_timeout); - }; - - window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript - ws.onclose = function () {}; - ws.close(); - }; - - /* - ws.onerror = function(){ - console.info("websocket error"); - }; - */ - }; - - connect(); - - var getInfoWindow = function() { - if (!infowindow) { - infowindow = new google.maps.InfoWindow(); - google.maps.event.addListener(infowindow, 'closeclick', function() { - delete infowindow.locator; - delete infowindow.callsign; + // 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) { + switch (update.location.type) { + case 'latlon': + var marker = self.mman.find(update.callsign); + var markerClass = google.maps.Marker; + 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) { + marker = new markerClass(); + self.mman.addType(update.mode); + self.mman.add(update.callsign, marker); + marker.addListener('click', function() { + showMarkerInfoWindow(update.callsign, marker.pos); + }); + } + + // 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 (expectedCallsign && expectedCallsign == update.callsign) { + map.panTo(marker.pos); + showMarkerInfoWindow(update.callsign, marker.pos); + expectedCallsign = false; + } + + if (infoWindow && infoWindow.callsign && infoWindow.callsign == update.callsign) { + showMarkerInfoWindow(infoWindow.callsign, marker.pos); + } + break; + + case 'feature': + var marker = self.mman.find(update.callsign); + 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(update.callsign, marker); + marker.addListener('click', function() { + showMarkerInfoWindow(update.callsign, marker.pos); + }); + } + + // 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 == update.callsign) { + map.panTo(marker.pos); + showMarkerInfoWindow(update.callsign, marker.pos); + expectedCallsign = false; + } + + if (infoWindow && infoWindow.callsign && infoWindow.callsign == update.callsign) { + showMarkerInfoWindow(infoWindow.callsign, marker.pos); + } + break; + + case 'locator': + var rectangle = self.lman.find(update.callsign); + + // If new item, create a new locator for it + if (!rectangle) { + rectangle = new GLocator(); + self.lman.add(update.callsign, 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; } - delete infowindow.locator; - delete infowindow.callsign; - return infowindow; - } - - var infowindow; - var showLocatorInfoWindow = function(locator, pos) { - var infowindow = getInfoWindow(); - infowindow.locator = locator; - infowindow.setContent(locmanager.getInfoHTML(locator, pos, receiverMarker)); - infowindow.setPosition(pos); - infowindow.open(map); - }; - - var showMarkerInfoWindow = function(name, pos) { - var infowindow = getInfoWindow(); - var marker = markmanager.find(name); - - infowindow.callsign = name; - infowindow.setContent(marker.getInfoHTML(name, receiverMarker)); - infowindow.open(map, marker); - } - - var showReceiverInfoWindow = function(marker) { - var infowindow = getInfoWindow() - infowindow.setContent( - '

    ' + marker.config['receiver_name'] + '

    ' + - '
    Receiver location
    ' - ); - infowindow.open(map, marker); - } - - // Fade out / remove positions after time - setInterval(function(){ - if (locmanager) locmanager.ageAll(); - if (markmanager) markmanager.ageAll(); - }, 1000); - - var setupLegendFilters = function($legend) { - $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'); - locmanager.setFilter(map); - } else { - $el.removeClass('disabled'); - $lis.filter(function() { - return this != $el[0] - }).addClass('disabled'); - locmanager.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'); - } - markmanager.toggle(map, $el.data('selector'), onoff); - }); - } - -}); + }); +}; diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index d5d180e3..a11b0540 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -145,6 +145,9 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController): "lib/jquery-3.2.1.min.js", "lib/chroma.min.js", "lib/Header.js", + "lib/MapLocators.js", + "lib/MapMarkers.js", + "lib/Map.js", "lib/Clock.js", "map.js", ], From f9d45e014d8c309031ac499807087b68bb19b28e Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Mon, 31 Jul 2023 21:58:07 -0400 Subject: [PATCH 10/10] Fixing UTC clock event. --- htdocs/map.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/map.html b/htdocs/map.html index 01daa0ea..242608eb 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -27,7 +27,7 @@
    -

    00:00 UTC

    +

    00:00 UTC