openwebrx-clone/htdocs/map.js

532 lines
20 KiB
JavaScript
Raw Permalink Normal View History

$(function(){
2022-11-30 00:07:16 +00:00
var query = new URLSearchParams(window.location.search);
2019-07-06 13:04:39 +00:00
var expectedCallsign;
2022-11-30 00:07:16 +00:00
if (query.has('callsign')) {
expectedCallsign = Object.fromEntries(query.entries());
}
var expectedLocator;
2022-11-30 00:07:16 +00:00
if (query.has('locator')) expectedLocator = query.get('locator');
2019-07-06 13:04:39 +00:00
2019-12-03 17:57:32 +00:00
var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws';
2019-12-03 17:53:57 +00:00
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/";
2019-07-01 14:49:39 +00:00
var map;
var markers = {};
2019-07-06 20:43:36 +00:00
var rectangles = {};
2020-12-11 16:47:17 +00:00
var receiverMarker;
var updateQueue = [];
2019-07-07 18:46:12 +00:00
// reasonable default; will be overriden by server
var retention_time = 2 * 60 * 60 * 1000;
2019-07-09 15:32:49 +00:00
var strokeOpacity = 0.8;
var fillOpacity = 0.35;
var callsign_service;
2019-07-07 18:46:12 +00:00
2019-07-28 13:28:39 +00:00
var colorKeys = {};
2019-07-28 20:13:55 +00:00
var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl');
2019-07-28 13:28:39 +00:00
var getColor = function(id){
if (!id) return "#000000";
if (!colorKeys[id]) {
var keys = Object.keys(colorKeys);
keys.push(id);
2020-12-10 21:22:08 +00:00
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;
});
2019-07-28 14:26:03 +00:00
var colors = colorScale.colors(keys.length);
2019-07-28 13:28:39 +00:00
colorKeys = {};
keys.forEach(function(key, index) {
colorKeys[key] = colors[index];
});
reColor();
2019-07-28 13:57:33 +00:00
updateLegend();
2019-07-28 13:28:39 +00:00
}
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));
2019-07-28 13:28:39 +00:00
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;
}
};
$(function(){
$('#openwebrx-map-colormode').on('change', function(){
colorMode = $(this).val();
colorKeys = {};
2021-01-19 23:39:34 +00:00
filterRectangles(allRectangles);
reColor();
updateLegend();
});
});
2019-07-28 13:57:33 +00:00
var updateLegend = function() {
var lis = $.map(colorKeys, function(value, key) {
2021-01-19 23:39:34 +00:00
// fake rectangle to test if the filter would match
var fakeRectangle = Object.fromEntries([[colorMode.slice(2), key]]);
var disabled = rectangleFilter(fakeRectangle) ? '' : ' disabled';
return '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
2019-07-28 13:57:33 +00:00
});
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
2022-11-30 00:07:16 +00:00
};
var shallowEquals = function(obj1, obj2) {
// basic shallow object comparison
return Object.entries(obj1).sort().toString() === Object.entries(obj2).sort().toString();
2019-07-28 13:57:33 +00:00
}
var processUpdates = function(updates) {
2019-09-21 11:41:04 +00:00
if (typeof(AprsMarker) == 'undefined') {
updateQueue = updateQueue.concat(updates);
return;
}
updates.forEach(function(update){
2022-11-30 00:07:16 +00:00
var key = sourceToKey(update.source);
2019-07-06 13:04:39 +00:00
2019-07-06 20:43:36 +00:00
switch (update.location.type) {
case 'latlon':
2019-07-09 15:32:49 +00:00
var pos = new google.maps.LatLng(update.location.lat, update.location.lon);
2019-07-07 18:46:12 +00:00
var marker;
var markerClass = google.maps.Marker;
2019-09-19 00:25:32 +00:00
var aprsOptions = {}
if (update.location.symbol) {
markerClass = AprsMarker;
2019-09-19 00:25:32 +00:00
aprsOptions.symbol = update.location.symbol;
aprsOptions.course = update.location.course;
aprsOptions.speed = update.location.speed;
}
2022-11-30 00:07:16 +00:00
if (markers[key]) {
marker = markers[key];
2019-07-06 20:43:36 +00:00
} else {
marker = new markerClass();
2019-07-11 19:21:01 +00:00
marker.addListener('click', function(){
2022-11-30 00:07:16 +00:00
showMarkerInfoWindow(update.source, pos);
2019-07-11 19:21:01 +00:00
});
2022-11-30 00:07:16 +00:00
markers[key] = marker;
2019-07-06 20:43:36 +00:00
}
2019-07-07 18:46:12 +00:00
marker.setOptions($.extend({
position: pos,
map: map,
2022-11-30 00:07:16 +00:00
title: sourceToString(update.source)
2019-09-19 00:25:32 +00:00
}, aprsOptions, getMarkerOpacityOptions(update.lastseen) ));
2019-07-07 18:46:12 +00:00
marker.lastseen = update.lastseen;
2019-07-11 21:40:09 +00:00
marker.mode = update.mode;
marker.band = update.band;
marker.comment = update.location.comment;
2019-07-06 20:43:36 +00:00
2022-11-30 00:07:16 +00:00
if (expectedCallsign && shallowEquals(expectedCallsign, update.source)) {
2019-07-06 20:43:36 +00:00
map.panTo(pos);
2022-11-30 00:07:16 +00:00
showMarkerInfoWindow(update.source, pos);
expectedCallsign = false;
}
2022-11-30 00:07:16 +00:00
if (infowindow && infowindow.source && shallowEquals(infowindow.source, update.source)) {
showMarkerInfoWindow(infowindow.source, pos);
2019-07-06 20:43:36 +00:00
}
break;
case 'locator':
var loc = update.location.locator;
var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]);
2019-07-06 21:15:33 +00:00
var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2;
var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1});
2019-07-06 20:43:36 +00:00
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));
2022-11-30 00:07:16 +00:00
if (rectangles[key]) {
rectangle = rectangles[key];
2019-07-06 20:43:36 +00:00
} else {
rectangle = new google.maps.Rectangle();
rectangle.addListener('click', function(){
2019-07-28 14:36:12 +00:00
showLocatorInfoWindow(this.locator, this.center);
});
2022-11-30 00:07:16 +00:00
rectangles[key] = rectangle;
2019-07-06 20:43:36 +00:00
}
2022-11-30 00:07:16 +00:00
rectangle.source = update.source;
2021-01-19 23:39:34 +00:00
rectangle.lastseen = update.lastseen;
rectangle.locator = update.location.locator;
rectangle.mode = update.mode;
rectangle.band = update.band;
rectangle.center = center;
2019-07-07 18:46:12 +00:00
rectangle.setOptions($.extend({
2019-07-28 13:28:39 +00:00
strokeColor: color,
2019-07-06 20:43:36 +00:00
strokeWeight: 2,
2019-07-28 13:28:39 +00:00
fillColor: color,
2021-01-19 23:39:34 +00:00
map: rectangleFilter(rectangle) ? map : undefined,
2019-07-06 20:43:36 +00:00
bounds:{
north: lat,
south: lat + 1,
west: lon,
2019-07-06 21:15:33 +00:00
east: lon + 2
2019-07-06 20:43:36 +00:00
}
2019-07-07 18:46:12 +00:00
}, getRectangleOpacityOptions(update.lastseen) ));
2022-11-30 00:07:16 +00:00
if (expectedLocator && expectedLocator === update.location.locator) {
map.panTo(center);
2019-07-11 19:21:01 +00:00
showLocatorInfoWindow(expectedLocator, center);
expectedLocator = false;
}
2022-11-30 00:07:16 +00:00
if (infowindow && infowindow.locator && infowindow.locator === update.location.locator) {
showLocatorInfoWindow(infowindow.locator, center);
}
2019-07-06 20:43:36 +00:00
break;
2019-07-06 13:04:39 +00:00
}
});
2019-07-09 15:32:49 +00:00
};
2019-07-10 21:13:03 +00:00
var clearMap = function(){
2022-11-30 00:07:16 +00:00
var reset = function(_, item) { item.setMap(); };
2019-07-10 21:13:03 +00:00
$.each(markers, reset);
$.each(rectangles, reset);
2020-12-11 16:47:17 +00:00
receiverMarker.setMap();
2019-07-10 21:13:03 +00:00
markers = {};
rectangles = {};
};
2019-07-13 19:44:48 +00:00
var reconnect_timeout = false;
var config = {}
2019-07-10 21:13:03 +00:00
var connect = function(){
var ws = new WebSocket(ws_url);
ws.onopen = function(){
ws.send("SERVER DE CLIENT client=map.js type=map");
2019-07-13 19:44:48 +00:00
reconnect_timeout = false
2019-07-10 21:13:03 +00:00
};
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,
});
2021-01-19 23:39:34 +00:00
$.getScript("static/lib/nite-overlay.js").done(function(){
nite.init(map);
setInterval(function() { nite.refresh() }, 10000); // every 10s
});
$.getScript('static/lib/AprsMarker.js').done(function(){
processUpdates(updateQueue);
updateQueue = [];
});
2020-12-11 16:47:17 +00:00
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
2020-12-11 16:47:17 +00:00
});
}
}
if ('receiver_name' in config && receiverMarker) {
2020-12-11 16:47:17 +00:00
receiverMarker.setOptions({
title: config['receiver_name']
2020-12-11 16:47:17 +00:00
});
}
if ('map_position_retention_time' in config) {
retention_time = config.map_position_retention_time * 1000;
}
if ('callsign_service' in config) {
callsign_service = config['callsign_service'];
}
2019-07-10 21:13:03 +00:00
break;
case "update":
processUpdates(json.value);
break;
2020-05-08 22:20:38 +00:00
case 'receiver_details':
2021-02-05 16:56:02 +00:00
$('.webrx-top-container').header().setDetails(json['value']);
2020-05-08 22:20:38 +00:00
break;
default:
console.warn('received message of unknown type: ' + json['type']);
2019-07-10 21:13:03 +00:00
}
} catch (e) {
// don't lose exception
console.error(e);
2019-07-01 17:49:58 +00:00
}
2019-07-10 21:13:03 +00:00
};
ws.onclose = function(){
clearMap();
2019-07-13 19:44:48 +00:00
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);
2019-07-10 21:13:03 +00:00
};
2019-07-01 14:49:39 +00:00
2019-07-10 21:13:03 +00:00
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");
};
*/
2019-07-01 14:49:39 +00:00
};
2019-07-10 21:13:03 +00:00
connect();
var getInfoWindow = function() {
if (!infowindow) {
infowindow = new google.maps.InfoWindow();
google.maps.event.addListener(infowindow, 'closeclick', function() {
delete infowindow.locator;
2022-11-30 00:07:16 +00:00
delete infowindow.source;
});
}
delete infowindow.locator;
2022-11-30 00:07:16 +00:00
delete infowindow.source;
return infowindow;
};
2022-11-30 00:07:16 +00:00
var sourceToKey = function(source) {
// special treatment for special entities
// not just for display but also in key treatment in order not to overlap with other locations sent by the same callsign
if ('item' in source) return source['item'];
if ('object' in source) return source['object'];
var key = source.callsign;
if ('ssid' in source) key += '-' + source.ssid;
return key;
};
// we can reuse the same logic for displaying and indexing
var sourceToString = sourceToKey;
var linkifySource = function(source) {
var callsignString = sourceToString(source);
switch (callsign_service) {
case "qrzcq":
return '<a target="callsign_info" href="https://www.qrzcq.com/call/' + source.callsign + '">' + callsignString + '</a>';
case "qrz":
return '<a target="callsign_info" href="https://www.qrz.com/db/' + source.callsign + '">' + callsignString + '</a>';
case 'aprsfi':
var callWithSsid = sourceToKey(source);
return '<a target="callsign_info" href="https://aprs.fi/info/a/' + callWithSsid + '">' + callsignString + '</a>';
default:
return callsignString;
}
};
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 infowindow;
2019-07-11 19:21:01 +00:00
var showLocatorInfoWindow = function(locator, pos) {
var infowindow = getInfoWindow();
infowindow.locator = locator;
2022-11-30 00:07:16 +00:00
var inLocator = Object.values(rectangles).filter(rectangleFilter).filter(function(d) {
return d.locator === locator;
2019-07-11 18:53:59 +00:00
}).sort(function(a, b){
return b.lastseen - a.lastseen;
});
var distance = receiverMarker?
" at " + distanceKm(receiverMarker.position, pos) + " km" : "";
infowindow.setContent(
'<h3>Locator: ' + locator + distance + '</h3>' +
'<div>Active Callsigns:</div>' +
'<ul>' +
inLocator.map(function(i){
var timestring = moment(i.lastseen).fromNow();
2022-11-30 00:07:16 +00:00
var message = linkifySource(i.source) + ' (' + timestring + ' using ' + i.mode;
if (i.band) message += ' on ' + i.band;
message += ')';
return '<li>' + message + '</li>'
}).join("") +
'</ul>'
);
infowindow.setPosition(pos);
infowindow.open(map);
2019-07-09 15:32:49 +00:00
};
2022-11-30 00:07:16 +00:00
var showMarkerInfoWindow = function(source, pos) {
var infowindow = getInfoWindow();
2022-11-30 00:07:16 +00:00
infowindow.source = source;
var marker = markers[sourceToKey(source)];
2019-07-11 19:21:01 +00:00
var timestring = moment(marker.lastseen).fromNow();
var commentString = "";
var distance = "";
if (marker.comment) {
commentString = '<div>' + marker.comment + '</div>';
}
if (receiverMarker) {
distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km";
}
2019-07-11 19:21:01 +00:00
infowindow.setContent(
2022-11-30 00:07:16 +00:00
'<h3>' + linkifySource(source) + distance + '</h3>' +
'<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' +
commentString
2019-07-11 19:21:01 +00:00
);
infowindow.open(map, marker);
};
2019-07-11 19:21:01 +00:00
2020-12-11 16:47:17 +00:00
var showReceiverInfoWindow = function(marker) {
var infowindow = getInfoWindow()
infowindow.setContent(
'<h3>' + marker.config['receiver_name'] + '</h3>' +
'<div>Receiver location</div>'
);
infowindow.open(map, marker);
};
2020-12-11 16:47:17 +00:00
2019-07-07 18:46:12 +00:00
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));
2019-07-09 15:32:49 +00:00
};
2019-07-07 18:46:12 +00:00
var getRectangleOpacityOptions = function(lastseen) {
var scale = getScale(lastseen);
return {
strokeOpacity: strokeOpacity * scale,
fillOpacity: fillOpacity * scale
};
2019-07-09 15:32:49 +00:00
};
2019-07-07 18:46:12 +00:00
var getMarkerOpacityOptions = function(lastseen) {
var scale = getScale(lastseen);
return {
opacity: scale
};
2019-07-09 15:32:49 +00:00
};
2019-07-07 18:46:12 +00:00
// fade out / remove positions after time
setInterval(function(){
var now = new Date().getTime();
2022-11-30 00:07:16 +00:00
Object.values(rectangles).forEach(function(m){
2019-07-07 18:46:12 +00:00
var age = now - m.lastseen;
if (age > retention_time) {
2022-11-30 00:07:16 +00:00
delete rectangles[sourceToKey(m.source)];
2019-07-07 18:46:12 +00:00
m.setMap();
return;
}
m.setOptions(getRectangleOpacityOptions(m.lastseen));
});
2022-11-30 00:07:16 +00:00
Object.values(markers).forEach(function(m) {
2019-07-07 18:46:12 +00:00
var age = now - m.lastseen;
if (age > retention_time) {
2022-11-30 00:07:16 +00:00
delete markers[sourceToKey(m.source)];
2019-07-07 18:46:12 +00:00
m.setMap();
return;
}
m.setOptions(getMarkerOpacityOptions(m.lastseen));
});
}, 1000);
2021-01-19 23:39:34 +00:00
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() {
var $el = $(this);
$lis = $content.find('li');
if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) {
$lis.removeClass('disabled');
filterRectangles(allRectangles);
} 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;
});
}
});
}
});