// 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)';
const ITU_ZONES_COLOR_LIGHT = 'rgba(200, 200, 140, 1.0)';
const WAB_WAI_GRID_COLOR_LIGHT = 'rgba(140, 140, 200, 1.0)';
const MAIDENHEAD_GRID_COLOR_DARK = 'rgba(120, 60, 60, 1.0)';
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)';
// Map layers
var backgroundTileLayer;
var markersLayer;
var geodesicsLayer;
var terminator;
var maidenheadGrid;
var cqZones;
var ituZones;
var wabwaiGrid;
// Tracks the currently-loaded basemap provider string to avoid unnecessary tile reloads
var loadedBasemap;
// Load spots and populate the map.
function loadSpots() {
$.getJSON('/api/v1/spots' + buildQueryString(), function(jsonData) {
// Store data
spots = jsonData;
// Update map
updateMap();
if ($("#showTerminator")[0].checked) {
terminator.setTime();
}
});
}
// Build a query string for the API, based on the filters that the user has selected.
function buildQueryString() {
var str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
});
str = str + "max_age=" + $("#max-spot-age option:selected").val();
// Additional filters for the map view: No dupes, no QRT, only spots with good locations
str = str + "&dedupe=true&allow_qrt=false&needs_good_location=true";
return str;
}
// Update the spots map
function updateMap() {
// Clear existing content
markersLayer.clearLayers();
geodesicsLayer.clearLayers();
// Make new markers for all spots that match the filter
spots.forEach(function (s) {
var m = L.marker([s["dx_latitude"], s["dx_longitude"]], {icon: getIcon(s)});
m.bindPopup(getTooltipText(s));
markersLayer.addLayer(m);
// Create geodesics if required
if ($("#mapShowGeodesics")[0].checked && s["de_latitude"] != null && s["de_longitude"] != null) {
try {
var 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
}
}
});
}
// Get an icon for a spot, based on its band, using PSK Reporter colours, its program etc.
function getIcon(s) {
return L.ExtraMarkers.icon({
icon: sigToIcon(s["sig"], "fa-tower-cell"),
iconColor: bandToContrastColor(s["band"]),
markerColor: bandToColor(s["band"]),
shape: 'circle',
prefix: 'fa',
svg: true
});
}
// Tooltip text for the markers
function getTooltipText(s) {
// Format DX call
var dx_call = s["dx_call"];
if (dx_call == null) {
dx_call = "";
dx_flag = "";
}
if (s["dx_ssid"] != null) {
dx_call = dx_call + "-" + s["dx_ssid"];
}
// Format DX flag
var dx_flag = "";
if (s["dx_flag"] && s["dx_flag"] != null && s["dx_flag"] != "") {
dx_flag = s["dx_flag"];
}
// Format the frequency
var freq_string = "Unknown"
if (s["freq"] != null) {
var mhz = Math.floor(s["freq"] / 1000000.0);
var khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0);
var hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.0));
var hz_string = (hz > 0) ? hz.toFixed(0)[0] : "";
freq_string = `${mhz.toFixed(0)}${khz.toFixed(0).padStart(3, '0')}${hz_string}`
}
// Format comment
var commentText = "";
if (s["comment"] != null) {
commentText = escapeHtml(s["comment"]);
}
// Sig or fallback to source
var sigSourceText = s["source"];
if (s["sig"]) {
sigSourceText = s["sig"];
}
// Format sig_refs
var sig_refs = "";
if (s["sig_refs"] != null) {
var items = []
for (var i = 0; i < s["sig_refs"].length; i++) {
if (s["sig_refs"][i]["url"] != null) {
items[i] = `${s["sig_refs"][i]["id"]}`
} else {
items[i] = `${s["sig_refs"][i]["id"]}`
}
}
sig_refs = items.join(", ");
}
// DX
ttt = `${dx_flag} ${dx_call}
`;
// Frequency & band
ttt += ` ${freq_string}`;
if (s["band"] != null) {
ttt += ` (${s["band"]})`;
}
// Mode
if (s["mode"] != null) {
ttt += ` ${s["mode"]}`;
}
ttt += "
";
// Source / SIG / Ref
ttt += ` ${sigSourceText} ${sig_refs}
`;
// Time
ttt += ` ${moment.unix(s["time"]).fromNow()}`;
// Comment
if (commentText.length > 0) {
ttt += `
${commentText}`;
}
return ttt;
}
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
// spots repeatedly.
function loadOptions() {
$.getJSON('/api/v1/options', function(jsonData) {
// Store options
options = jsonData;
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setColorScheme($("#color-scheme option:selected").val());
setBandColorScheme($("#band-color-scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateSIGsMultiToggleFilterCard(options["sigs"]);
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
generateSourcesMultiToggleFilterCard(options["spot_sources"], spotProvidersEnabledByDefault);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
// Load settings from settings storage now all the controls are available
loadSettings();
// Apply basemap and overlay settings now that controls have their saved values
setBasemap($("#basemap").val());
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
enableTerminator($("#showTerminator")[0].checked);
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
enableCQZones($("#showCQZones")[0].checked);
enableITUZones($("#showITUZones")[0].checked);
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
// Load spots and set up the timer
loadSpots();
setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000);
});
}
// Method called when any display property is changed to reload the map and persist the display settings.
function displayUpdated() {
updateMap();
setBasemap($("#basemap").val());
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
enableTerminator($("#showTerminator")[0].checked);
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
enableCQZones($("#showCQZones")[0].checked);
enableITUZones($("#showITUZones")[0].checked);
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
saveSettings();
}
// Set the basemap
function setBasemap(basemapname) {
// Only change if we have to, to avoid a flash of reloading content
if (loadedBasemap !== basemapname) {
loadedBasemap = basemapname;
if (typeof backgroundTileLayer !== 'undefined') {
map.removeLayer(backgroundTileLayer);
}
backgroundTileLayer = L.tileLayer.provider(basemapname, {
opacity: parseFloat($("#basemapOpacity").val()),
edgeBufferTiles: 1
});
backgroundTileLayer.addTo(map);
backgroundTileLayer.bringToBack();
// Identify dark basemaps to ensure we use white text for unselected icons
// and change the background colour appropriately
const basemapIsDark = basemapname === "CartoDB.DarkMatter" || basemapname === "Esri.WorldImagery";
$("#map").css('background-color', basemapIsDark ? "black" : "white");
// Change the colour of the grid and zone overlays to match
if (basemapIsDark) {
maidenheadGrid.options.color = MAIDENHEAD_GRID_COLOR_DARK;
cqZones.options.color = CQ_ZONES_COLOR_DARK;
ituZones.options.color = ITU_ZONES_COLOR_DARK;
wabwaiGrid.options.color = WAB_WAI_GRID_COLOR_DARK;
} else {
maidenheadGrid.options.color = MAIDENHEAD_GRID_COLOR_LIGHT;
cqZones.options.color = CQ_ZONES_COLOR_LIGHT;
ituZones.options.color = ITU_ZONES_COLOR_LIGHT;
wabwaiGrid.options.color = WAB_WAI_GRID_COLOR_LIGHT;
}
// Force regenerate overlays in the new colours
map.removeLayer(maidenheadGrid);
map.removeLayer(cqZones);
map.removeLayer(ituZones);
map.removeLayer(wabwaiGrid);
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
enableCQZones($("#showCQZones")[0].checked);
enableITUZones($("#showITUZones")[0].checked);
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
}
}
// Set the basemap opacity
function setBasemapOpacity(opacity) {
if (typeof backgroundTileLayer !== 'undefined') {
backgroundTileLayer.setOpacity(opacity);
}
}
// Shows/hides the terminator/greyline overlay
function enableTerminator(show) {
if (show) {
terminator.setTime();
terminator.addTo(map);
} else {
map.removeLayer(terminator);
}
}
// Shows/hides the Maidenhead grid overlay
function enableMaidenheadGrid(show) {
if (show) {
maidenheadGrid.addTo(map);
backgroundTileLayer.bringToBack();
} else {
map.removeLayer(maidenheadGrid);
}
}
// Shows/hides the CQ zone overlay
function enableCQZones(show) {
if (show) {
cqZones.addTo(map);
backgroundTileLayer.bringToBack();
} else {
map.removeLayer(cqZones);
}
}
// Shows/hides the ITU zone overlay
function enableITUZones(show) {
if (show) {
ituZones.addTo(map);
backgroundTileLayer.bringToBack();
} else {
map.removeLayer(ituZones);
}
}
// Shows/hides the WAB/WAI grid overlay
function enableWABWAIGrid(show) {
if (show) {
wabwaiGrid.addTo(map);
backgroundTileLayer.bringToBack();
} else {
map.removeLayer(wabwaiGrid);
}
}
// Set up the map
function setUpMap() {
// Create map
map = L.map('map', {
zoomControl: false,
minZoom: 2,
maxZoom: 12
});
// Add basemap
loadedBasemap = $("#basemap").val();
backgroundTileLayer = L.tileLayer.provider(loadedBasemap, {
opacity: parseFloat($("#basemapOpacity").val()),
edgeBufferTiles: 1
});
backgroundTileLayer.addTo(map);
backgroundTileLayer.bringToBack();
// Add marker layer
markersLayer = new L.LayerGroup();
markersLayer.addTo(map);
// Add geodesic layer
geodesicsLayer = new L.LayerGroup();
geodesicsLayer.addTo(map);
// Add terminator/greyline (toggleable)
terminator = L.terminator({
interactive: false
});
terminator.setStyle({fillColor: '#00000050'});
if ($("#showTerminator")[0].checked) {
terminator.addTo(map);
}
// Add Maidenhead grid (toggleable)
maidenheadGrid = L.maidenhead({
color : MAIDENHEAD_GRID_COLOR_LIGHT
});
if ($("#showMaidenheadGrid")[0].checked) {
maidenheadGrid.addTo(map);
backgroundTileLayer.bringToBack();
}
// Add CQ zone layer (toggleable)
cqZones = L.cqzones({
color : CQ_ZONES_COLOR_LIGHT
});
if ($("#showCQZones")[0].checked) {
cqZones.addTo(map);
backgroundTileLayer.bringToBack();
}
// Add ITU zone layer (toggleable)
ituZones = L.ituzones({
color : ITU_ZONES_COLOR_LIGHT
});
if ($("#showITUZones")[0].checked) {
ituZones.addTo(map);
backgroundTileLayer.bringToBack();
}
// Add WAB/WAI grid layer (toggleable)
wabwaiGrid = L.workedAllBritainIreland({
color : WAB_WAI_GRID_COLOR_LIGHT
});
if ($("#showWABWAIGrid")[0].checked) {
wabwaiGrid.addTo(map);
backgroundTileLayer.bringToBack();
}
// Display a default view.
map.setView([30, 0], 3);
}
// Startup
$(document).ready(function() {
// Hide the extra things that need to be hidden on this page
$(".hideonmap").hide();
// Set up map
setUpMap();
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Prevent mouse scroll and touch actions in the popup menus being passed through to the map
L.DomEvent.disableScrollPropagation(document.getElementById('settingsButtonRowMap'));
L.DomEvent.disableClickPropagation(document.getElementById('settingsButtonRowMap'));
});