// 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')); });