mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-06-26 22:47:25 +00:00
Update map and bands pages to use SSE endpoint after initial load. Closes #43.
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
// How often to query the server?
|
||||
const REFRESH_INTERVAL_SEC = 60;
|
||||
|
||||
// Colours
|
||||
const MAIDENHEAD_GRID_COLOR_LIGHT = 'rgba(200, 140, 140, 1.0)';
|
||||
const CQ_ZONES_COLOR_LIGHT = 'rgba(140, 200, 140, 1.0)';
|
||||
@@ -11,6 +8,15 @@ const CQ_ZONES_COLOR_DARK = 'rgba(60, 120, 60, 1.0)';
|
||||
const ITU_ZONES_COLOR_DARK = 'rgba(120, 120, 60, 1.0)';
|
||||
const WAB_WAI_GRID_COLOR_DARK = 'rgba(60, 60, 120, 1.0)';
|
||||
|
||||
// SSE connection for live spot updates
|
||||
let evtSource;
|
||||
let restartSSEOnErrorTimeoutId;
|
||||
// Map dx_call to a pair of marker & geodesic line, so we can de-duplicate spots as they arrive and only show
|
||||
// one marker per dx_call. The key here is actually dx_call + SSID if the spot gives us an SSID; this is mostly for
|
||||
// if APRS spots are enabled so we can ID a home and mobile station separately rather than our marker oscillating
|
||||
// between the two as updates come in.
|
||||
let spotMarkers = new Map();
|
||||
|
||||
// Map layers
|
||||
let backgroundTileLayer;
|
||||
let markersLayer;
|
||||
@@ -28,7 +34,13 @@ let firstLoad = true;
|
||||
|
||||
// Load spots and populate the map.
|
||||
function loadSpots() {
|
||||
$.getJSON('/api/v1/spots' + buildQueryString(true), function (jsonData) {
|
||||
// Close any existing SSE connection before fetching fresh data
|
||||
if (evtSource != null) {
|
||||
evtSource.close();
|
||||
}
|
||||
// On first fetch, don't include QRZ/HamQTH credentials. This will cause some inaccurate
|
||||
// marker positions but avoids having to wait a minute or more for all the lookups to fire.
|
||||
$.getJSON('/api/v1/spots' + buildQueryString(false), function (jsonData) {
|
||||
// Store data
|
||||
spots = jsonData;
|
||||
// Update map
|
||||
@@ -36,9 +48,102 @@ function loadSpots() {
|
||||
if ($("#showTerminator")[0].checked) {
|
||||
terminator.setTime();
|
||||
}
|
||||
// Start the ongoing SSE connection
|
||||
startSSEConnection();
|
||||
});
|
||||
}
|
||||
|
||||
// Start the SSE connection to receive new spots as they arrive
|
||||
function startSSEConnection() {
|
||||
if (evtSource != null) {
|
||||
evtSource.close();
|
||||
}
|
||||
// SSE is going to fetch only a few spots at a time, so now we include QRZ/HamQTH credentials because the delay won't be significant.
|
||||
evtSource = new EventSource('/api/v1/spots/stream' + buildQueryString(true));
|
||||
|
||||
evtSource.onmessage = function (event) {
|
||||
const newSpot = JSON.parse(event.data);
|
||||
const key = spotKey(newSpot);
|
||||
|
||||
// Remove existing marker/geodesic for this callsign if present
|
||||
removeSpotFromMap(key);
|
||||
spots = spots.filter(s => spotKey(s) !== key);
|
||||
|
||||
// Skip spots with no map coordinates
|
||||
if (newSpot["dx_latitude"] == null || newSpot["dx_longitude"] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to data store and map
|
||||
spots.unshift(newSpot);
|
||||
addSpotToMap(newSpot);
|
||||
};
|
||||
|
||||
evtSource.onerror = function () {
|
||||
if (evtSource != null) {
|
||||
evtSource.close();
|
||||
}
|
||||
clearTimeout(restartSSEOnErrorTimeoutId);
|
||||
restartSSEOnErrorTimeoutId = setTimeout(startSSEConnection, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
// Remove spots from the map that are older than the selected max age.
|
||||
function expireOldSpots() {
|
||||
const maxAgeSeconds = parseInt($("#max-spot-age option:selected").val());
|
||||
const cutoff = (Date.now() / 1000) - maxAgeSeconds;
|
||||
spots = spots.filter(function (s) {
|
||||
if (s["time"] && s["time"] < cutoff) {
|
||||
removeSpotFromMap(spotKey(s));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a unique key for a spot based on its callsign and SSID. This is in case the APRS spot source is enabled and
|
||||
// we want to display all SSIDs for a given callsign, not just the latest one.
|
||||
function spotKey(s) {
|
||||
return s["dx_call"] + (s["dx_ssid"] ? "-" + s["dx_ssid"] : "");
|
||||
}
|
||||
|
||||
// Add a single spot's marker and geodesic to the map and to spotMarkers
|
||||
function addSpotToMap(s) {
|
||||
const m = L.marker([s["dx_latitude"], s["dx_longitude"]], {icon: getIcon(s)});
|
||||
m.bindPopup(getTooltipText(s));
|
||||
markersLayer.addLayer(m);
|
||||
oms.addMarker(m);
|
||||
|
||||
let geodesic = null;
|
||||
if ($("#mapShowGeodesics")[0].checked && s["de_latitude"] != null && s["de_longitude"] != null) {
|
||||
try {
|
||||
geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], {
|
||||
color: bandToColor(s['band']),
|
||||
wrap: false,
|
||||
steps: 5
|
||||
});
|
||||
geodesicsLayer.addLayer(geodesic);
|
||||
} catch (e) {
|
||||
// Not sure what causes these but better to continue than to crash out
|
||||
}
|
||||
}
|
||||
|
||||
spotMarkers.set(spotKey(s), {marker: m, geodesic: geodesic});
|
||||
}
|
||||
|
||||
// Remove a spot's marker and geodesic from the map and from spotMarkers.
|
||||
function removeSpotFromMap(key) {
|
||||
const entry = spotMarkers.get(key);
|
||||
if (entry) {
|
||||
markersLayer.removeLayer(entry.marker);
|
||||
oms.removeMarker(entry.marker);
|
||||
if (entry.geodesic) {
|
||||
geodesicsLayer.removeLayer(entry.geodesic);
|
||||
}
|
||||
spotMarkers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Build a query string for the API, based on the filters that the user has selected.
|
||||
function buildQueryString(includeCredentials) {
|
||||
let str = "?";
|
||||
@@ -62,27 +167,14 @@ function updateMap() {
|
||||
markersLayer.clearLayers();
|
||||
geodesicsLayer.clearLayers();
|
||||
oms.clearMarkers();
|
||||
spotMarkers.clear();
|
||||
|
||||
// Make new markers for all spots that match the filter
|
||||
// Make new markers for all spots
|
||||
spots.forEach(function (s) {
|
||||
const m = L.marker([s["dx_latitude"], s["dx_longitude"]], {icon: getIcon(s)});
|
||||
m.bindPopup(getTooltipText(s));
|
||||
markersLayer.addLayer(m);
|
||||
oms.addMarker(m);
|
||||
|
||||
// Create geodesics if required
|
||||
if ($("#mapShowGeodesics")[0].checked && s["de_latitude"] != null && s["de_longitude"] != null) {
|
||||
try {
|
||||
const geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], {
|
||||
color: bandToColor(s['band']),
|
||||
wrap: false,
|
||||
steps: 5
|
||||
});
|
||||
geodesicsLayer.addLayer(geodesic);
|
||||
} catch (e) {
|
||||
// Not sure what causes these but better to continue than to crash out
|
||||
}
|
||||
if (s["dx_latitude"] == null || s["dx_longitude"] == null) {
|
||||
return;
|
||||
}
|
||||
addSpotToMap(s);
|
||||
});
|
||||
|
||||
// On first load, zoom to the extent of the markers
|
||||
@@ -239,9 +331,10 @@ function loadOptions() {
|
||||
enableITUZones($("#showITUZones")[0].checked);
|
||||
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
|
||||
|
||||
// Load spots and set up the timer
|
||||
// Load spots and start SSE for live updates
|
||||
loadSpots();
|
||||
setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000);
|
||||
// Set up spot expiry checker
|
||||
setInterval(expireOldSpots, 60 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -483,6 +576,13 @@ function setUpMap() {
|
||||
|
||||
// Startup
|
||||
$(document).ready(function () {
|
||||
// Close SSE connection cleanly when navigating away
|
||||
window.addEventListener('beforeunload', function () {
|
||||
if (evtSource != null) {
|
||||
evtSource.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Hide the extra things that need to be hidden on this page
|
||||
$(".hideonmap").hide();
|
||||
// Set up map
|
||||
|
||||
Reference in New Issue
Block a user