mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-10-27 08:49:27 +00:00
226 lines
10 KiB
JavaScript
226 lines
10 KiB
JavaScript
// A couple of constants that must match what's in CSS. We need to know them before the content actually renders, so we
|
|
// can't just ask the elements themselves for their dimensions.
|
|
BAND_COLUMN_HEIGHT_EM = 62;
|
|
BAND_COLUMN_CANVAS_WIDTH_EM = 4;
|
|
BAND_COLUMN_FONT_SIZE = 16;
|
|
BAND_COLUMN_HEIGHT_PX = BAND_COLUMN_HEIGHT_EM * BAND_COLUMN_FONT_SIZE;
|
|
BAND_COLUMN_CANVAS_WIDTH_PX = BAND_COLUMN_CANVAS_WIDTH_EM * BAND_COLUMN_FONT_SIZE;
|
|
|
|
// 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 bandsContainer = $("#bands-container");
|
|
if (spots.length === 0) {
|
|
bandsContainer.html("<div class='alert alert-danger' role='alert'>No spots match your filters.</div>");
|
|
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.
|
|
spotList = removeDuplicatesForBandPanel(spots);
|
|
|
|
// 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 = spotList.filter(function (s) {
|
|
return s.band === band.name;
|
|
});
|
|
if (matchingSpots.length > 0) {
|
|
bandToSpots.set(band.name, matchingSpots);
|
|
}
|
|
});
|
|
|
|
// Build up table content for each band
|
|
var table = $('<table id="bands-table">').append('<thead><tr></tr></thead><tbody><tr></tr></tbody>');
|
|
bandToSpots.forEach(function (spotList, bandName) {
|
|
// Get the colours for the band from the first spot, and prepare the header
|
|
table.find('thead tr').append(`<th style='background-color:${spotList[0].band_color}; color:${spotList[0].band_contrast_color}'>${spotList[0].band}</th>`);
|
|
|
|
// Get the band data to fetch start and end frequencies
|
|
let band = options["bands"].filter(function (b) {
|
|
return b.name === bandName;
|
|
})[0];
|
|
|
|
// Print the frequency band markers. This is 41 steps to divide the band evenly into 40 markers. One in every
|
|
// four will show the actual frequency, the others will just be dashes.
|
|
bandMarkersDiv = $('<div class="band-markers">');
|
|
const freqStep = (band.end_freq - band.start_freq) / 40.0;
|
|
for (let i = 0; i <= 40; i++) {
|
|
if (i % 4 === 0) {
|
|
bandMarkersDiv.append("—" + ((band.start_freq + i * freqStep)/1000000).toFixed(3) + "<br/>");
|
|
} else if (i % 4 === 2) {
|
|
bandMarkersDiv.append("–<br/>");
|
|
} else {
|
|
bandMarkersDiv.append("-<br/>");
|
|
}
|
|
}
|
|
|
|
// Print the spots on the band. Each one is a div with the details, shifted down if necessary to prevent
|
|
// overlap, and a horizontal or diagonal line on the canvas linking it to where it properly appears on the
|
|
// band.
|
|
bandSpotsDiv = $("<div class='band-spots'>");
|
|
bandLinesCanvas = $(`<canvas class='band-lines-canvas' width='${BAND_COLUMN_CANVAS_WIDTH_PX}' height='${BAND_COLUMN_HEIGHT_PX}'>`);
|
|
// Sort by frequency so we print higher frequencies later, and can bump them further down the track to prevent
|
|
// overlaps
|
|
spotList.sort(function(a, b) { return a.freq - b.freq; });
|
|
var lastSpotEmDownBand = -999;
|
|
// Iterate through the spots list
|
|
spotList.forEach(s => {
|
|
// Work out how far down the div to draw it
|
|
var percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text
|
|
var emDownBand = percentDownBand * BAND_COLUMN_HEIGHT_EM;
|
|
var pxDownBandFreq = (percentDownBand + 0.015) * BAND_COLUMN_HEIGHT_PX; // same fudge but add half to put the left end of the line in the right place
|
|
if (emDownBand < lastSpotEmDownBand + 1.6) {
|
|
emDownBand = lastSpotEmDownBand + 1.6; // Prevent overlap
|
|
}
|
|
lastSpotEmDownBand = emDownBand;
|
|
var pxDownBandLabel = (emDownBand * BAND_COLUMN_FONT_SIZE) + (0.015 * BAND_COLUMN_HEIGHT_PX);
|
|
|
|
// Add spot div to DOM
|
|
bandSpotsDiv.append(`<div class="band-spot" label="${(s.freq/1000000).toFixed(3)}MHz ${s.mode}" onclick="alert('${s.dx_call} on ${(s.freq/1000000).toFixed(3)}MHz ${s.mode}');" style="top: ${emDownBand}em; border-top: 1px solid ${s.band_color}; border-left: 5px solid ${s.band_color}; border-bottom: 1px solid ${s.band_color}; border-right: 1px solid ${s.band_color};">${s.dx_call}</div>`);
|
|
|
|
// Draw the line on the canvas
|
|
var ctx = bandLinesCanvas[0].getContext('2d');
|
|
ctx.beginPath();
|
|
ctx.lineWidth = 2;
|
|
ctx.lineCap = "round";
|
|
ctx.strokeStyle = s.band_color;
|
|
ctx.moveTo(0, pxDownBandFreq);
|
|
ctx.lineTo(BAND_COLUMN_CANVAS_WIDTH_PX, pxDownBandLabel);
|
|
ctx.stroke();
|
|
});
|
|
|
|
// Assemble the table cell
|
|
td = $("<td>");
|
|
container = $("<div class='band-container'>");
|
|
container.append(bandLinesCanvas);
|
|
container.append(bandMarkersDiv);
|
|
container.append(bandSpotsDiv);
|
|
td.append(container);
|
|
table.find('tbody tr').append(td);
|
|
});
|
|
|
|
// Update the DOM with the band HTML
|
|
bandsContainer.html(table);
|
|
}
|
|
|
|
// Iterate through a temporary list of spots, merging duplicates in a way suitable for the band panel. If two or more
|
|
// spots with the activator, mode and frequency are found, these will be merged and reduced until only one remains,
|
|
// with the best data. Note that unlike removeDuplicates(), which operates on the main spot map, this operates only
|
|
// on the temporary array of spots provided as an argument, and returns the output, for use when constructing the
|
|
// band panel.
|
|
function removeDuplicatesForBandPanel(spotList) {
|
|
const spotsToRemove = [];
|
|
spotList.forEach(function (check) {
|
|
spotList.forEach(function (s) {
|
|
if (s !== check) {
|
|
if (s.dx_call === check.dx_call && s.freq === check.freq && s.mode === check.mode) {
|
|
// Find which one to keep and which to delete
|
|
const checkSpotNewer = check.time > s.time;
|
|
const keepSpot = checkSpotNewer ? check : s;
|
|
const deleteSpot = checkSpotNewer ? s : check;
|
|
// Aggregate list of spots to remove
|
|
spotsToRemove.push(deleteSpot.uid);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
// Perform the removal
|
|
return spotList.filter(s => !spotsToRemove.includes(s.uid));
|
|
}
|
|
|
|
// 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);
|
|
}); |