Allow table columns to be toggled on and off. #19

This commit is contained in:
Ian Renton
2025-10-12 17:17:26 +01:00
parent e3d342c5d6
commit bb2813c2a5
6 changed files with 306 additions and 57 deletions

View File

@@ -41,15 +41,38 @@ function updateTable() {
// Use local time instead of UTC?
var useLocalTime = $("#timeZone")[0].value == "local";
// Table data toggles
var showStartTime = $("#tableShowStartTime")[0].checked;
var showEndTime = $("#tableShowEndTime")[0].checked;
var showDX = $("#tableShowDX")[0].checked;
var showFreqsModes = $("#tableShowFreqsModes")[0].checked;
var showComment = $("#tableShowComment")[0].checked;
var showSource = $("#tableShowSource")[0].checked;
var showRef = $("#tableShowRef")[0].checked;
// Populate table with headers
let table = $('<table class="table table-striped-custom table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
table.find('thead tr').append(`<th>${useLocalTime ? "Start&nbsp;(Local)" : "Start&nbsp;UTC"}</th>`);
table.find('thead tr').append(`<th>${useLocalTime ? "End&nbsp;(Local)" : "End&nbsp;UTC"}</th>`);
table.find('thead tr').append(`<th>DX</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Freq<span class='hideonmobile'>uencie</span>s & Modes</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Comment</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Source</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Ref.</th>`);
if (showStartTime) {
table.find('thead tr').append(`<th>${useLocalTime ? "Start&nbsp;(Local)" : "Start&nbsp;UTC"}</th>`);
}
if (showEndTime) {
table.find('thead tr').append(`<th>${useLocalTime ? "End&nbsp;(Local)" : "End&nbsp;UTC"}</th>`);
}
if (showDX) {
table.find('thead tr').append(`<th>DX</th>`);
}
if (showFreqsModes) {
table.find('thead tr').append(`<th class='hideonmobile'>Freq<span class='hideonmobile'>uencie</span>s & Modes</th>`);
}
if (showComment) {
table.find('thead tr').append(`<th class='hideonmobile'>Comment</th>`);
}
if (showSource) {
table.find('thead tr').append(`<th class='hideonmobile'>Source</th>`);
}
if (showRef) {
table.find('thead tr').append(`<th class='hideonmobile'>Ref.</th>`);
}
// Split alerts into three types, each of which will get its own table header: On now, next 24h, and later. "On now"
// is considered to be events with an end_time where start<now<end, or events with no end time that started in the
@@ -91,6 +114,15 @@ function addAlertRowsToTable(tbody, alerts) {
// Use local time instead of UTC?
var useLocalTime = $("#timeZone")[0].value == "local";
// Table data toggles
var showStartTime = $("#tableShowStartTime")[0].checked;
var showEndTime = $("#tableShowEndTime")[0].checked;
var showDX = $("#tableShowDX")[0].checked;
var showFreqsModes = $("#tableShowFreqsModes")[0].checked;
var showComment = $("#tableShowComment")[0].checked;
var showSource = $("#tableShowSource")[0].checked;
var showRef = $("#tableShowRef")[0].checked;
// Get times for the alert, and convert to local time if necessary.
var start_time_unix = moment.unix(a["start_time"]);
var start_time = start_time_unix.utc();
@@ -181,18 +213,45 @@ function addAlertRowsToTable(tbody, alerts) {
}
// Populate the row
$tr.append(`<td class='nowrap'>${start_time_formatted}</td>`);
$tr.append(`<td class='nowrap'>${end_time_formatted}</td>`);
$tr.append(`<td class='nowrap'><span class='flag-wrapper hideonmobile' title='${dx_country}'>${dx_flag}</span>${dx_calls_html}${dx_country_html}</td>`);
$tr.append(`<td class='hideonmobile'>${freqsModesText}</td>`);
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> ${sigSourceText}</td>`);
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
if (showStartTime) {
$tr.append(`<td class='nowrap'>${start_time_formatted}</td>`);
}
if (showEndTime) {
$tr.append(`<td class='nowrap'>${end_time_formatted}</td>`);
}
if (showDX) {
$tr.append(`<td class='nowrap'><span class='flag-wrapper hideonmobile' title='${dx_country}'>${dx_flag}</span>${dx_calls_html}${dx_country_html}</td>`);
}
if (showFreqsModes) {
$tr.append(`<td class='hideonmobile'>${freqsModesText}</td>`);
}
if (showComment) {
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
}
if (showSource) {
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> ${sigSourceText}</td>`);
}
if (showRef) {
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
}
tbody.append($tr);
// Second row for mobile view only, containing source, ref, freqs/modes & comment
$tr2 = $("<tr class='hidenotonmobile'>");
$tr2.append(`<td colspan="100"><span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> ${sig_refs} ${freqsModesText}<br/>${commentText}</td>`);
$td2 = $("<td colspan='100'>");
if (showSource) {
$td2.append(`<span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> `);
}
if (showRef) {
$td2.append(`${sig_refs} `);
}
if (showFreqsModes) {
$td2.append(`${freqsModesText} `);
}
if (showComment) {
$td2.append(`<br/>${commentText} `);
}
$tr2.append($td2);
tbody.append($tr2);
});
}

View File

@@ -103,6 +103,97 @@ function timeZoneUpdated() {
saveSettings();
}
// When one of the column toggle checkboxes are changed, reload the table and save settings
function columnsUpdated() {
updateTable();
saveSettings();
}
// Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the centre point of the square.
// Returns null if the grid format is invalid.
function latLonForGridCentre(grid) {
let [lat, lon, latCellSize, lonCellSize] = latLonForGridSWCornerPlusSize(grid);
if (lat != null && lon != null && latCellSize != null && lonCellSize != null) {
return [lat + latCellSize / 2.0, lon + lonCellSize / 2.0];
} else {
return null;
}
}
// Convert a Maidenhead grid reference of arbitrary precision to lat/long, including in the result the size of the
// lowest grid square. This is a utility method used by the main methods that return the centre, southwest, and
// northeast coordinates of a grid square.
// The return type is always an array of size 4. The elements in it are null if the grid format is invalid.
function latLonForGridSWCornerPlusSize(grid) {
// Make sure we are in upper case so our maths works. Case is arbitrary for Maidenhead references
grid = grid.toUpperCase();
// Return null if our Maidenhead string is invalid or too short
let len = grid.length;
if (len <= 0 || (len % 2) !== 0) {
return [null, null, null, null];
}
let lat = 0.0; // aggregated latitude
let lon = 0.0; // aggregated longitude
let latCellSize = 10; // Size in degrees latitude of the current cell. Starts at 20 and gets smaller as the calculation progresses
let lonCellSize = 20; // Size in degrees longitude of the current cell. Starts at 20 and gets smaller as the calculation progresses
let latCellNo; // grid latitude cell number this time
let lonCellNo; // grid longitude cell number this time
// Iterate through blocks (two-character sections)
for (let block = 0; block * 2 < len; block += 1) {
if (block % 2 === 0) {
// Letters in this block
lonCellNo = grid.charCodeAt(block * 2) - 'A'.charCodeAt(0);
latCellNo = grid.charCodeAt(block * 2 + 1) - 'A'.charCodeAt(0);
// Bail if the values aren't in range. Allowed values are A-R (0-17) for the first letter block, or
// A-X (0-23) thereafter.
let maxCellNo = (block === 0) ? 17 : 23;
if (latCellNo < 0 || latCellNo > maxCellNo || lonCellNo < 0 || lonCellNo > maxCellNo) {
return [null, null, null, null];
}
} else {
// Numbers in this block
lonCellNo = parseInt(grid.charAt(block * 2));
latCellNo = parseInt(grid.charAt(block * 2 + 1));
// Bail if the values aren't in range 0-9..
if (latCellNo < 0 || latCellNo > 9 || lonCellNo < 0 || lonCellNo > 9) {
return [null, null, null, null];
}
}
// Aggregate the angles
lat += latCellNo * latCellSize;
lon += lonCellNo * lonCellSize;
// Reduce the cell size for the next block, unless we are on the last cell.
if (block * 2 < len - 2) {
// Still have more work to do, so reduce the cell size
if (block % 2 === 0) {
// Just dealt with letters, next block will be numbers so cells will be 1/10 the current size
latCellSize = latCellSize / 10.0;
lonCellSize = lonCellSize / 10.0;
} else {
// Just dealt with numbers, next block will be letters so cells will be 1/24 the current size
latCellSize = latCellSize / 24.0;
lonCellSize = lonCellSize / 24.0;
}
}
}
// Offset back to (-180, -90) where the grid starts
lon -= 180.0;
lat -= 90.0;
// Return nulls on maths errors
if (isNaN(lat) || isNaN(lon) || isNaN(latCellSize) || isNaN(lonCellSize)) {
return [null, null, null, null];
}
return [lat, lon, latCellSize, lonCellSize];
}
// Save settings to local storage
function saveSettings() {
// Find all storeable UI elements, store a key of "element id:property name" mapped to the value of that

View File

@@ -34,16 +34,46 @@ function updateTable() {
// Use local time instead of UTC?
var useLocalTime = $("#timeZone")[0].value == "local";
// Get user grid if valid, this will be null if it's not.
var userPos = latLonForGridCentre($("#userGrid").val());
// Table data toggles
var showTime = $("#tableShowTime")[0].checked;
var showDX = $("#tableShowDX")[0].checked;
var showFreq = $("#tableShowFreq")[0].checked;
var showMode = $("#tableShowMode")[0].checked;
var showComment = $("#tableShowComment")[0].checked;
var showBearing = $("#tableShowBearing")[0].checked && userPos != null;
var showSource = $("#tableShowSource")[0].checked;
var showRef = $("#tableShowRef")[0].checked;
var showDE = $("#tableShowDE")[0].checked;
// Populate table with headers
let table = $('<table class="table table-striped-custom table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
table.find('thead tr').append(`<th>${useLocalTime ? "Local" : "UTC"}</th>`);
table.find('thead tr').append(`<th>DX</th>`);
table.find('thead tr').append(`<th>Freq<span class='hideonmobile'>uency</span></th>`);
table.find('thead tr').append(`<th>Mode</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Comment</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Source</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Ref.</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>DE</th>`);
if (showTime) {
table.find('thead tr').append(`<th>${useLocalTime ? "Local" : "UTC"}</th>`);
}
if (showDX) {
table.find('thead tr').append(`<th>DX</th>`);
}
if (showFreq) {
table.find('thead tr').append(`<th>Freq<span class='hideonmobile'>uency</span></th>`);
}
if (showMode) {
table.find('thead tr').append(`<th>Mode</th>`);
}
if (showComment) {
table.find('thead tr').append(`<th class='hideonmobile'>Comment</th>`);
}
if (showSource) {
table.find('thead tr').append(`<th class='hideonmobile'>Source</th>`);
}
if (showRef) {
table.find('thead tr').append(`<th class='hideonmobile'>Ref.</th>`);
}
if (showDE) {
table.find('thead tr').append(`<th class='hideonmobile'>DE</th>`);
}
if (spots.length == 0) {
table.find('tbody').append('<tr class="table-danger"><td colspan="100" style="text-align:center;">No spots match your filters.</td></tr>');
@@ -142,14 +172,30 @@ function updateTable() {
var bandFullName = s['band'] ? s['band'] + " band": "Unknown band";
// Populate the row
$tr.append(`<td class='nowrap'>${time_formatted}</td>`);
$tr.append(`<td class='nowrap'><span class='flag-wrapper hideonmobile' title='${dx_country}'>${dx_flag}</span><a class='dx-link' href='https://qrz.com/db/${s["dx_call"]}' target='_new'>${s["dx_call"]}</a></td>`);
$tr.append(`<td class='nowrap'><span class='band-bullet band-bullet-${cssFormattedBandName}' title='${bandFullName}'>&#9632;</span>${freq_string}</td>`);
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${sigSourceText}</td>`);
$tr.append(`<td class='hideonmobile'><span ${sig_refs_title_string}>${sig_refs}</span></td>`);
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
if (showTime) {
$tr.append(`<td class='nowrap'>${time_formatted}</td>`);
}
if (showDX) {
$tr.append(`<td class='nowrap'><span class='flag-wrapper hideonmobile' title='${dx_country}'>${dx_flag}</span><a class='dx-link' href='https://qrz.com/db/${s["dx_call"]}' target='_new'>${s["dx_call"]}</a></td>`);
}
if (showFreq) {
$tr.append(`<td class='nowrap'><span class='band-bullet band-bullet-${cssFormattedBandName}' title='${bandFullName}'>&#9632;</span>${freq_string}</td>`);
}
if (showMode) {
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
}
if (showComment) {
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
}
if (showSource) {
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${sigSourceText}</td>`);
}
if (showRef) {
$tr.append(`<td class='hideonmobile'><span ${sig_refs_title_string}>${sig_refs}</span></td>`);
}
if (showDE) {
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
}
table.find('tbody').append($tr);
// Second row for mobile view only, containing source, ref & comment
@@ -157,7 +203,17 @@ function updateTable() {
if (s["qrt"] == true) {
$tr2.addClass("table-faded");
}
$tr2.append(`<td colspan="100"><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${sig_refs} ${commentText}</td>`);
$td2 = $("<td colspan='100'>");
if (showSource) {
$td2.append(`<span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> `);
}
if (showRef) {
$td2.append(`${sig_refs} `);
}
if (showComment) {
$td2.append(`${commentText} `);
}
$tr2.append($td2);
table.find('tbody').append($tr2);
});
@@ -182,9 +238,17 @@ function loadOptions() {
$("#filters-container-2").append(generateMultiToggleFilterCard("Modes", "mode_type", options["mode_types"]));
$("#filters-container-2").append(generateMultiToggleFilterCard("Sources", "source", options["spot_sources"]));
// Load filters from settings storage
// Load settings from settings storage now all the controls are available
loadSettings();
// Extra setting - the toggle for the "bearing" column is disabled if the user has not entered a valid grid, and
// normally this logic is handled on user input to the grid field, but we might have just loaded a value direct
// into the field, so apply the same logic here.
$("#tableShowBearing").prop('disabled', !isUserGridValid());
if (!isUserGridValid()) {
$("#tableShowBearing").prop('checked', false);
}
// Load spots and set up the timer
loadSpots();
setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000);
@@ -228,12 +292,33 @@ function generateBandsMultiToggleFilterCard(band_options) {
return $col;
}
// Work out if the user's entered grid is a valid Maidenhead grid
function isUserGridValid() {
userGrid = $("#userGrid").val().toUpperCase();
return latLonForGridCentre(userGrid) != null;
}
// Method called when any filter is changed to reload the spots and persist the filter settings.
function filtersUpdated() {
loadSpots();
saveSettings();
}
// Method called when the user's grid input is changed.
function userGridUpdated() {
var userGridValid = isUserGridValid();
if (userGridValid) {
updateTable();
}
// Enable/disable bearing column depending on grid validity
$("#tableShowBearing").prop('disabled', !userGridValid);
if (!userGridValid) {
$("#tableShowBearing").prop('checked', false);
}
// Save settings even if not a valid grid, this allows the user to clear their grid and have it save.
saveSettings();
}
// React to toggling/closing panels
function toggleFiltersPanel() {
// If we are going to display the filters panel, hide the display panel