Start work on bands display. #48

This commit is contained in:
Ian Renton
2025-10-20 18:44:51 +01:00
parent 53977c5306
commit 6ca9f28a56
7 changed files with 365 additions and 23 deletions

199
webassets/js/bands.js Normal file
View File

@@ -0,0 +1,199 @@
// Load spots and populate the bands display.
function loadSpots() {
$.getJSON('/api/v1/spots' + buildQueryString(), function(jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
updateRefreshDisplay();
// Store data
spots = jsonData;
// Update bands display
updateBands();
});
}
// 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_type", "source", "band"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
});
str = str + "max_age=" + $("#max-spot-age option:selected").val();
// Additional filters for the bands view: No dupes, no QRT
str = str + "&dedupe=true&allow_qrt=false";
return str;
}
// Update the bands display
function updateBands() {
// Stop here if nothing to display
var bandsPanel = $("#bands-container");
if (spots.length === 0) {
// todo bootstrapify
bandsPanel.html("<p id='bandPanelNoSpots'>There are no spots matching your filters.</p>");
return;
}
// Do some harsher de-duping. Because we only display callsign, frequency and mode here, the previous
// de-duplication could have let some through that don't look like dupes on the map, but would do here.
// Typically that's a person activating two programs at the same time, e.g. POTA & WWFF.
// todo fix from here
spotList = removeDuplicatesForBandPanel(spotList);
// Convert to a map of band names to the spots on that band. Bands with no
// spots in view will not be present.
const bandToSpots = new Map();
options["bands"].forEach(function (band) {
const matchingSpots = spots.filter(function (s) {
return s.band === band.name;
});
if (matchingSpots.length > 0) {
bandToSpots.set(band.name, matchingSpots);
}
});
// Build up HTML content for each band
let html = "";
const columnWidthPercent = Math.max(30, 100 / bandToSpots.size);
let columnIndex = 0;
bandToSpots.forEach(function (spotList, bandName) {
// Get the colours for the band from the first spot, and prepare the header
html += "<div class='bandCol' style='width:" + columnWidthPercent + "%'>";
html += "<div class='bandColHeader' style='background-color:" + spotList[0].color + "; color:" + spotList[0].contrast_color + "'>" + spotList[0].band + "</div>";
html += "<div class='bandColMiddle'>";
// Get the band data to fetch start and end frequencies
let band = options["bands"].filter(function (b) {
return b.name === bandName;
})[0];
// Start printing the band
const freqStep = (band.stopFreq - band.startFreq) / 40.0;
html += "<ul>";
html += "<li><span>-</span></li>";
// Do 40 steps down the band
for (let i = 0; i <= 40; i++) {
// Work out if there are any spots in this step
const freqStepStart = band.startFreq + i * freqStep;
const freqStepEnd = freqStepStart + freqStep;
const spotsInStep = spotList.filter(function (s) {
// Normally we do >= start and < end, but in the special case where this is the last step and there is a spot
// right at the end of the band, we include this too
return s.freq >= freqStepStart && (s.freq < freqStepEnd || (s.freq === freqStepEnd && freqStepEnd === band.stopFreq));
});
if (spotsInStep.length > 0) {
// If this step has spots in it, print them
html += "<li class='withSpots'><span>";
spotsInStep.sort((a, b) => (a.freq > b.freq) ? 1 : ((b.freq > a.freq) ? -1 : 0));
spotsInStep.forEach(function (s) {
// Figure out the class to use for the spot's div, which defines its colour.
let spotDivClass = "bandColSpotCurrent";
if (currentPopupSpotUID === s.uid) {
spotDivClass = "bandColSpotSelected";
} else if (preQSYStatusShouldShowGrey(s.preqsy)) {
spotDivClass = "bandColSpotOld";
}
html += "<div class='bandColSpot " + spotDivClass + "' onClick='handleBandPanelSpotClick(\"" + s.uid + "\")'><span class='bandColSpot'>" + s.activator + "<br/><span class='bandColSpotFreq'>" + getFormattedFrequency(s.freq) + "</span>";
if (s.mode != null && s.mode.length > 0 && s.mode !== "Unknown") {
html += "<span class='bandColSpotMode'>" + s.mode + "</span>";
}
html += "</span></div>";
});
html += "</li></span>";
} else {
// Step had no spots in it, so just print a marker. This is a frequency on multiples of 4, or a dash otherwise.
if (i % 4 === 0) {
html += "<li><span>&mdash;" + (band.startFreq + i * freqStep).toFixed(3) + "</span></li>";
} else if (i % 4 === 2) {
html += "<li><span>&ndash;</span></li>";
} else {
html += "<li><span>-</span></li>";
}
}
}
html += "<li><span>-</span></li>";
html += "</ul>";
html += "</div></div>";
columnIndex++;
});
// Update the DOM with the band HTML
bandsPanel.html(html);
// Desktop mouse wheel to scroll bands horizontally if used on the headers
// noinspection JSDeprecatedSymbols
$(".bandColHeader").on("wheel", () => bandsPanel.scrollLeft(bandsPanel.scrollLeft() + event.deltaY / 10.0));
}
// 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;
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
// Load settings from settings storage now all the controls are available
loadSettings();
// 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();
saveSettings();
}
// React to toggling/closing panels
function toggleFiltersPanel() {
// If we are going to show the filters panel, hide the display panel
if (!$("#filters-area").is(":visible") && $("#display-area").is(":visible")) {
$("#display-area").hide();
$("#display-button").button("toggle");
}
$("#filters-area").toggle();
}
function closeFiltersPanel() {
$("#filters-button").button("toggle");
$("#filters-area").hide();
}
function toggleDisplayPanel() {
// If we are going to show the display panel, hide the filters panel
if (!$("#display-area").is(":visible") && $("#filters-area").is(":visible")) {
$("#filters-area").hide();
$("#filters-button").button("toggle");
}
$("#display-area").toggle();
}
function closeDisplayPanel() {
$("#display-button").button("toggle");
$("#display-area").hide();
}
// Startup
$(document).ready(function() {
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Update the refresh timing display every second
setInterval(updateRefreshDisplay, 1000);
});