// 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;
BAND_COLUMN_SPOT_DIV_HEIGHT_PX = BAND_COLUMN_FONT_SIZE * 1.6;
// 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("
No spots match your filters.
");
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);
}
});
// Track if any columns end up taller than expected, so we can resize the container and avoid vertical scroll.
var maxHeightBand = 0;
// Build up table content for each band
var table = $('
').append('
');
bandToSpots.forEach(function (spotList, bandName) {
// Get the colours for the band from the first spot, and prepare the header
table.find('thead tr').append(`
${spotList[0].band}
`);
// 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 = $('
');
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) + " ");
} else if (i % 4 === 2) {
bandMarkersDiv.append("– ");
} else {
bandMarkersDiv.append("- ");
}
}
// Prepare the spots list
var bandSpotsDiv = $("
");
var lastSpotPxDownBand = -999;
// Sort by frequency so have a consistent order in which to plan where they will appear on the band div.
spotList.sort(function(a, b) { return a.freq - b.freq; });
// First calculate how we should be displaying the spots. There are three "modes" to try to place them in a
// visually appealing way:
// 1) Spaced normally, not going over the end of the band, so we populate them forwards.
// 2) Would go over the end, but the spots don't fill the band, so we populate them backwards.
// 3) Spots totally fill the band (or more), so we space them evenly starting at the top.
// In each case, we don't add anything to the DOM yet, we just calculate "pxDownBandLabel" (how far the *top* of
// the label is from the top of the div) and add that as a property to the spot for later use.
if (spotList.length >= BAND_COLUMN_HEIGHT_PX / BAND_COLUMN_SPOT_DIV_HEIGHT_PX) {
// Mode 3.
// Just lay out all spots simply, starting at 0px offset and working down with each one touching.
lastSpotPxDownBand = 0 - BAND_COLUMN_SPOT_DIV_HEIGHT_PX;
spotList.forEach(s => {
lastSpotPxDownBand = lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX;
s["pxDownBandLabel"] = lastSpotPxDownBand;
});
} else {
// Mode 1 or 2. Run through adding things to the list forwards as a test.
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 pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX;
if (pxDownBand < lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX) {
pxDownBand = lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap
}
s["pxDownBandLabel"] = pxDownBand;
lastSpotPxDownBand = pxDownBand;
});
// Work out if we overflowed the end.
if (lastSpotPxDownBand <= BAND_COLUMN_HEIGHT_PX) {
// Mode 1. Current positions are fine and there's nothing to do.
} else {
// Mode 2. Repeat the process but backwards, starting at the end and working upwards.
lastSpotPxDownBand = 999999;
spotList.reverse().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 pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX;
if (pxDownBand > lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX) {
pxDownBand = lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap
}
s["pxDownBandLabel"] = pxDownBand;
lastSpotPxDownBand = pxDownBand;
});
}
}
// Now each spot is tagged with how far down the div it should go, add them to the DOM.
spotList.forEach(s => {
bandSpotsDiv.append(`
`);
});
// Work out how tall the canvas should be. Normally this is matching the normal band column height, but if some
// spots have gone off the end of the band markers and stretched their div, we need to resize the canvas to
// match, otherwise we have nowhere to draw their connecting lines.
var canvasHeight = Math.max(BAND_COLUMN_HEIGHT_PX, lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX);
maxHeightBand = Math.max(maxHeightBand, canvasHeight);
// Draw horizontal or diagonal lines to join up the "real" frequency with where the spot div ended up
var bandLinesCanvas = $(`