Separate colours and icons out of the Spothole API and re-implement them in the client; provide new colour schemes. #88

This commit is contained in:
Ian Renton
2025-12-30 19:08:27 +00:00
parent 5bf45dba46
commit 06d582ae2d
30 changed files with 717 additions and 343 deletions

View File

@@ -243,7 +243,7 @@ function addAlertRowsToTable(tbody, alerts) {
$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>`);
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid ${sigToIcon(a["sig"], "fa-globe-africa")}'></i></span> ${sigSourceText}</td>`);
}
if (showRef) {
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
@@ -257,7 +257,7 @@ function addAlertRowsToTable(tbody, alerts) {
}
$td2 = $("<td colspan='100'>");
if (showSource) {
$td2.append(`<span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> `);
$td2.append(`<span class='icon-wrapper'><i class='fa-solid ${sigToIcon(a["sig"], "fa-globe-africa")}'></i></span> `);
}
if (showRef) {
$td2.append(`${sig_refs} `);

View File

@@ -70,7 +70,7 @@ function updateBands() {
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>`);
table.find('thead tr').append(`<th style='background-color:${bandToColor(spotList[0].band)}; color:${bandToContrastColor(spotList[0].band)}'>${spotList[0].band}</th>`);
// Get the band data to fetch start and end frequencies
let band = options["bands"].filter(function (b) {
@@ -145,7 +145,7 @@ function updateBands() {
// Now each spot is tagged with how far down the div it should go, add them to the DOM.
spotList.forEach(s => {
bandSpotsDiv.append(`<div class="band-spot" style="top: ${s['pxDownBandLabel']}px; 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};"><span class="band-spot-call">${s.dx_call}${s.dx_ssid != null ? "-" + s.dx_ssid : ""}</span><span class="band-spot-info">${s.dx_call}${s.dx_ssid != null ? "-" + s.dx_ssid : ""} ${(s.freq/1000000).toFixed(3)} ${s.mode}</span></div>`);
bandSpotsDiv.append(`<div class="band-spot" style="top: ${s['pxDownBandLabel']}px; border-top: 1px solid ${bandToColor(s['band'])}; border-left: 5px solid ${bandToColor(s['band'])}; border-bottom: 1px solid ${bandToColor(s['band'])}; border-right: 1px solid ${bandToColor(s['band'])};"><span class="band-spot-call">${s.dx_call}${s.dx_ssid != null ? "-" + s.dx_ssid : ""}</span><span class="band-spot-info">${s.dx_call}${s.dx_ssid != null ? "-" + s.dx_ssid : ""} ${(s.freq/1000000).toFixed(3)} ${s.mode}</span></div>`);
});
// Work out how tall the canvas should be. Normally this is matching the normal band column height, but if some
@@ -167,7 +167,7 @@ function updateBands() {
ctx.beginPath();
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.strokeStyle = s.band_color;
ctx.strokeStyle = bandToColor(s['band']);
ctx.moveTo(0, pxDownBandFreq);
ctx.lineTo(BAND_COLUMN_CANVAS_WIDTH_PX, pxDownBandLabel);
ctx.stroke();
@@ -228,6 +228,21 @@ function loadOptions() {
// Store options
options = jsonData;
// Populate the Display panel
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
value: sc * 60,
text: sc
})));
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
getAvailableBandColorSchemes().forEach(sc => $("#band-color-scheme").append($('<option>', {
value: sc,
text: sc
})));
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setBandColorScheme($("#band-color-scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
@@ -239,13 +254,6 @@ function loadOptions() {
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
// Populate the Display panel
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
value: sc * 60,
text: sc
})));
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
// 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.

View File

@@ -2,9 +2,6 @@
var options = {};
// Last time we updated the spots/alerts list on display.
var lastUpdateTime;
// Whether "embedded mode" is being used. This removes headers and footers, maximises the remaining content, and
// uses URL params to configure the interface options rather than using the user's localstorage.
var embeddedMode = false;
// Load and apply any URL params. This is used for "embedded mode" where another site can embed a version of
// Spothole and provide its own interface options rather than using the user's saved ones. These may select things
@@ -18,7 +15,7 @@ function loadURLParams() {
// top-level html element to use CSS selectors to remove bits of UI.
let embedded = params.get("embedded");
if (embedded != null && embedded === "true") {
embeddedMode = true;
useLocalStorage = false;
$("html").attr("embedded-mode", "true");
}
@@ -133,27 +130,6 @@ function updateRefreshDisplay() {
}
}
// Utility function to escape HTML characters from a string.
function escapeHtml(str) {
if (typeof str !== 'string') {
return '';
}
const escapeCharacter = (match) => {
switch (match) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case '\'': return '&#039;';
case '`': return '&#096;';
default: return match;
}
};
return str.replace(/[&<>"'`]/g, escapeCharacter);
}
// When the "use local time" field is changed, reload the table and save settings
function timeZoneUpdated() {
updateTable();
@@ -166,106 +142,6 @@ function columnsUpdated() {
saveSettings();
}
// Calculate great circle bearing between two lat/lon points.
function calcBearing(lat1, lon1, lat2, lon2) {
lat1 *= Math.PI / 180;
lon1 *= Math.PI / 180;
lat2 *= Math.PI / 180;
lon2 *= Math.PI / 180;
var lonDelta = lon2 - lon1;
var y = Math.sin(lonDelta) * Math.cos(lat2);
var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lonDelta);
var bearing = Math.atan2(y, x);
bearing = bearing * (180 / Math.PI);
if ( bearing < 0 ) { bearing += 360; }
return bearing;
}
// 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];
}
// Function to set dark mode on or off
function enableDarkMode(dark) {
$("html").attr("data-bs-theme", dark ? "dark" : "light");
@@ -289,37 +165,6 @@ function usePreferredTheme() {
}
}
// Save settings to local storage. Suppressed if "embedded mode" is in use.
function saveSettings() {
if (!embeddedMode) {
// Find all storeable UI elements, store a key of "element id:property name" mapped to the value of that
// property. For a checkbox, that's the "checked" property.
$(".storeable-checkbox").each(function() {
localStorage.setItem("#" + $(this)[0].id + ":checked", JSON.stringify($(this)[0].checked));
});
$(".storeable-select").each(function() {
localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value));
});
$(".storeable-text").each(function() {
localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value));
});
}
}
// Load settings from local storage and set up the filter selectors. Suppressed if "embedded mode" is in use.
function loadSettings() {
if (!embeddedMode) {
// Find all local storage entries and push their data to the corresponding UI element
Object.keys(localStorage).forEach(function(key) {
if (key.startsWith("#") && key.includes(":")) {
// Split the key back into an element ID and a property
var split = key.split(":");
$(split[0]).prop(split[1], JSON.parse(localStorage.getItem(key)));
}
});
}
}
// Startup
$(document).ready(function() {
usePreferredTheme();

View File

@@ -0,0 +1,99 @@
// Calculate great circle bearing between two lat/lon points.
function calcBearing(lat1, lon1, lat2, lon2) {
lat1 *= Math.PI / 180;
lon1 *= Math.PI / 180;
lat2 *= Math.PI / 180;
lon2 *= Math.PI / 180;
var lonDelta = lon2 - lon1;
var y = Math.sin(lonDelta) * Math.cos(lat2);
var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lonDelta);
var bearing = Math.atan2(y, x);
bearing = bearing * (180 / Math.PI);
if ( bearing < 0 ) { bearing += 360; }
return bearing;
}
// 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];
}

View File

@@ -0,0 +1,32 @@
let useLocalStorage = true;
// Save settings to local storage. Suppressed if "use local storage" is false.
function saveSettings() {
if (useLocalStorage) {
// Find all storeable UI elements, store a key of "element id:property name" mapped to the value of that
// property. For a checkbox, that's the "checked" property.
$(".storeable-checkbox").each(function() {
localStorage.setItem("#" + $(this)[0].id + ":checked", JSON.stringify($(this)[0].checked));
});
$(".storeable-select").each(function() {
localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value));
});
$(".storeable-text").each(function() {
localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value));
});
}
}
// Load settings from local storage and set up the filter selectors. Suppressed if "use local storage" is false.
function loadSettings() {
if (useLocalStorage) {
// Find all local storage entries and push their data to the corresponding UI element
Object.keys(localStorage).forEach(function(key) {
if (key.startsWith("#") && key.includes(":")) {
// Split the key back into an element ID and a property
var split = key.split(":");
$(split[0]).prop(split[1], JSON.parse(localStorage.getItem(key)));
}
});
}
}

View File

@@ -0,0 +1,372 @@
const BAND_COLOR_SCHEMES = {
"PSK Reporter": {
"2200m": "#ff4500",
"600m": "#1e90ff",
"160m": "#7cfc00",
"80m": "#e550e5",
"60m": "#00008b",
"40m": "#5959ff",
"30m": "#62d962",
"20m": "#f2c40c",
"17m": "#f2f261",
"15m": "#cca166",
"12m": "#b22222",
"11m": "#00ff00",
"10m": "#ff69b4",
"6m": "#FF0000",
"5m": "#e0e0e0",
"4m": "#cc0044",
"2m": "#FF1493",
"1.25m": "#CCFF00",
"70cm": "#999900",
"23cm": "#5AB8C7",
"2.4GHz": "#FF7F50",
"5.8GHz": "#cc0099",
"10GHz": "#696969",
"24GHz": "#f3edc6",
"47GHz": "#ffe786",
"76GHz": "#baf9d8"
},
"PSK Reporter (Adjusted)": {
"2200m": "#ff4500",
"600m": "#1e90ff",
"160m": "#7cfc00",
"80m": "#b33fb3",
"60m": "#00008b",
"40m": "#5959ff",
"30m": "#62d962",
"20m": "#f2c40c",
"17m": "#f2f261",
"15m": "#cca166",
"12m": "#b22222",
"11m": "#00ff00",
"10m": "#ff7eb4",
"6m": "#FF0000",
"5m": "#e0e0e0",
"4m": "#cc0044",
"2m": "#FF1493",
"1.25m": "#CCFF00",
"70cm": "#999900",
"23cm": "#5AB8C7",
"2.4GHz": "#FF7F50",
"5.8GHz": "#cc0099",
"10GHz": "#696969",
"24GHz": "#f3edc6",
"47GHz": "#ffe786",
"76GHz": "#baf9d8"
},
"RBN": {
"2200m": "#000000",
"600m": "#aaaaaa",
"160m": "#ffe000",
"80m": "#093F00",
"60m": "#777777",
"40m": "#ffa500",
"30m": "#ff0000",
"20m": "#800080",
"17m": "#0000ff",
"15m": "#444444",
"12m": "#00ffff",
"11m": "#000000",
"10m": "#ff00ff",
"6m": "#ffc0cb",
"5m": "#000000",
"4m": "#a276ff",
"2m": "#92FF7F",
"1.25m": "#000000",
"70cm": "#000000",
"23cm": "#000000",
"2.4GHz": "#000000",
"5.8GHz": "#000000",
"10GHz": "#000000",
"24GHz": "#000000",
"47GHz": "#000000",
"76GHz": "#000000"
},
"Ham Rainbow": {
"2200m": "#8e4f37",
"600m": "#8e4f37",
"160m": "#8e3737",
"80m": "#da2f93",
"60m": "#792fda",
"40m": "#2f4bda",
"30m": "#2fdad2",
"20m": "#68da2f",
"17m": "#dad52f",
"15m": "#da832f",
"12m": "#da5c2f",
"11m": "#8e8e8e",
"10m": "#da2f2f",
"6m": "#8e377a",
"5m": "#8e8e8e",
"4m": "#42378e",
"2m": "#37748e",
"1.25m": "#8e8e8e",
"70cm": "#378e65",
"23cm": "#8e8e37",
"2.4GHz": "#8e6037",
"5.8GHz": "#8e6037",
"10GHz": "#8e6037",
"24GHz": "#8e6037",
"47GHz": "#8e6037",
"76GHz": "#8e6037"
},
"Ham Rainbow (Reverse)": {
"2200m": "#42378e",
"600m": "#42378e",
"160m": "#8e377a",
"80m": "#da2f2f",
"60m": "#da5c2f",
"40m": "#da832f",
"30m": "#dad52f",
"20m": "#68da2f",
"17m": "#2fdad2",
"15m": "#2f4bda",
"12m": "#792fda",
"11m": "#8e8e8e",
"10m": "#da2f93",
"6m": "#8e3737",
"5m": "#8e8e8e",
"4m": "#8e4f37",
"2m": "#8e6037",
"1.25m": "#8e8e8e",
"70cm": "#8e8e37",
"23cm": "#378e65",
"2.4GHz": "#37748e",
"5.8GHz": "#37748e",
"10GHz": "#37748e",
"24GHz": "#37748e",
"47GHz": "#37748e",
"76GHz": "#37748e",
},
"Kate Morley": {
"2200m": "#817",
"600m": "#817",
"160m": "#817",
"80m": "#a35",
"60m": "#c66",
"40m": "#e94",
"30m": "#ed0",
"20m": "#9d5",
"17m": "#4d8",
"15m": "#2cb",
"12m": "#0bc",
"11m": "#09c",
"10m": "#09c",
"6m": "#36b",
"5m": "#36b",
"4m": "#36b",
"2m": "#36b",
"1.25m": "#36b",
"70cm": "#639",
"23cm": "#639",
"2.4GHz": "#639",
"5.8GHz": "#639",
"10GHz": "#639",
"24GHz": "#639",
"47GHz": "#639",
"76GHz": "#639",
},
"ColorBrewer": {
"2200m": "#54278f",
"600m": "#756bb1",
"160m": "#9e9ac8",
"80m": "#cbc9e2",
"60m": "#08519c",
"40m": "#3182bd",
"30m": "#6baed6",
"20m": "#bdd7e7",
"17m": "#006d2c",
"15m": "#31a354",
"12m": "#74c476",
"11m": "#bae4b3",
"10m": "#a63603",
"6m": "#e6550d",
"5m": "#fd8d3c",
"4m": "#fdbe85",
"2m": "#a50f15",
"1.25m": "#de2d26",
"70cm": "#fb6a4a",
"23cm": "#fcae91",
"2.4GHz": "#636363",
"5.8GHz": "#636363",
"10GHz": "#969696",
"24GHz": "#969696",
"47GHz": "#cccccc",
"76GHz": "#cccccc",
},
"IWantHue": {
"2200m": "#409271",
"600m": "#b03ce1",
"160m": "#50c640",
"80m": "#d545b7",
"60m": "#99b936",
"40m": "#7260db",
"30m": "#60af57",
"20m": "#d54788",
"17m": "#58c79f",
"15m": "#e2462a",
"12m": "#49b1d3",
"11m": "#df872f",
"10m": "#506bb0",
"6m": "#c6a639",
"5m": "#9554a3",
"4m": "#36783c",
"2m": "#da405b",
"1.25m": "#657527",
"70cm": "#8c97e2",
"23cm": "#b44f2f",
"2.4GHz": "#d386c8",
"5.8GHz": "#aaac66",
"10GHz": "#9d4760",
"24GHz": "#90672c",
"47GHz": "#e08086",
"76GHz": "#dc9769",
},
"IWantHue (Color Blind)": {
"2200m": "#bf9e3d",
"600m": "#9d2fec",
"160m": "#79df39",
"80m": "#d445db",
"60m": "#5dd175",
"40m": "#814dd8",
"30m": "#d7ce2f",
"20m": "#657af1",
"17m": "#8cc34a",
"15m": "#d635aa",
"12m": "#6cbd80",
"11m": "#b860c1",
"10m": "#e48721",
"6m": "#686ccc",
"5m": "#d44e2b",
"4m": "#51b3db",
"2m": "#d74058",
"1.25m": "#56c5ad",
"70cm": "#d0478d",
"23cm": "#708940",
"2.4GHz": "#c380c2",
"5.8GHz": "#cab775",
"10GHz": "#7a7fc2",
"24GHz": "#b87148",
"47GHz": "#bd678c",
"76GHz": "#c3666b",
},
"Mokole": {
"2200m": "#8b4513",
"600m": "#006400",
"160m": "#808000",
"80m": "#483d8b",
"60m": "#5f9ea0",
"40m": "#000080",
"30m": "#9acd32",
"20m": "#8b008b",
"17m": "#ff0000",
"15m": "#ff8c00",
"12m": "#ffd700",
"11m": "#7fff00",
"10m": "#8a2be2",
"6m": "#00ff7f",
"5m": "#dc143c",
"4m": "#00bfff",
"2m": "#0000ff",
"1.25m": "#d8bfd8",
"70cm": "#ff00ff",
"23cm": "#1e90ff",
"2.4GHz": "#db7093",
"5.8GHz": "#f0e68c",
"10GHz": "#ff1493",
"24GHz": "#ffa07a",
"47GHz": "#ee82ee",
"76GHz": "#7fffd4",
}
};
let bandColorScheme = "PSK Reporter (Adjusted)";
// Set the band colour scheme. Returns true if successful, false if the requested scheme was not known
function setBandColorScheme(scheme) {
let ret = BAND_COLOR_SCHEMES[scheme]
if (ret) {
bandColorScheme = scheme;
}
return ret;
}
// Get the list of known bands
function getKnownBands() {
return Array.from(Object.keys(BAND_COLOR_SCHEMES[bandColorScheme]));
}
// Get the list of available band colour schemes
function getAvailableBandColorSchemes() {
return Array.from(Object.keys(BAND_COLOR_SCHEMES));
}
// Band name to colour (in the current colour scheme). If the band is unknown, black will be returned.
function bandToColor(band) {
let col = (band != null) ? BAND_COLOR_SCHEMES[bandColorScheme][band] : null;
if (col) {
return col;
} else {
return "black";
}
}
// Band name to contrast colour (in the current colour scheme). This is either black or white, contrasting as well as
// possible with the band colour. If the band is unknown, white will be returned.
function bandToContrastColor(band) {
let tc = tinycolor(bandToColor(band));
return tc.isLight() ? "black" : "white";
}
const MODE_TYPE_COLOR_SCHEMES = {
"CW": "green",
"PHONE": "red",
"DATA": "blue"
}
// Mode type (CW, PHONE, DATA) to colour. If the mode type is unknown, black will be returned.
function modeTypeToColor(modeType) {
let col = (modeType != null) ? MODE_TYPE_COLOR_SCHEMES[modeType.toUpperCase()] : null;
if (col) {
return col;
} else {
return "black";
}
}
const SIG_ICONS = {
"POTA": "fa-tree",
"SOTA": "fa-mountain-sun",
"WWFF": "fa-seedling",
"GMA": "fa-person-hiking",
"WWBOTA": "fa-radiation",
"HEMA": "fa-mound",
"IOTA": "fa-umbrella-beach",
"MOTA": "fa-fan",
"ARLHS": "fa-tower-observation",
"ILLW": "fa-tower-observation",
"SIOTA": "fa-wheat-awn",
"WCA": "fa-chess-rook",
"ZLOTA": "fa-kiwi-bird",
"WOTA": "fa-w",
"BOTA": "fa-water",
"KRMNPA": "fa-earth-oceania",
"WAB": "fa-table-cells-large",
"WAI": "fa-table-cells-large",
"TOTA": "fa-toilet"
}
// Get the Font Awesome icon for a given SIG. If the SIG is unknown, the provided default symbol will be returned
function sigToIcon(sig, defaultIcon) {
let col = (sig != null) ? SIG_ICONS[sig.toUpperCase()] : null;
if (col) {
return col;
} else {
return defaultIcon;
}
}
// Get the list of known SIGs
function getKnownSIGs() {
return Array.from(Object.keys(SIG_ICONS));
}

View File

@@ -0,0 +1,20 @@
// Utility function to escape HTML characters from a string.
function escapeHtml(str) {
if (typeof str !== 'string') {
return '';
}
const escapeCharacter = (match) => {
switch (match) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case '\'': return '&#039;';
case '`': return '&#096;';
default: return match;
}
};
return str.replace(/[&<>"'`]/g, escapeCharacter);
}

View File

@@ -45,12 +45,16 @@ function updateMap() {
// Create geodesics if required
if ($("#mapShowGeodesics")[0].checked && s["de_latitude"] != null && s["de_longitude"] != null) {
var geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], {
color: s["band_color"],
wrap: false,
steps: 5
});
geodesicsLayer.addLayer(geodesic);
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
}
}
});
}
@@ -58,9 +62,9 @@ function updateMap() {
// 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: "fa-" + s["icon"],
iconColor: s["band_contrast_color"],
markerColor: s["band_color"],
icon: sigToIcon(s["sig"], "fa-tower-cell"),
iconColor: bandToContrastColor(s["band"]),
markerColor: bandToColor(s["band"]),
shape: 'circle',
prefix: 'fa',
svg: true
@@ -136,7 +140,7 @@ function getTooltipText(s) {
ttt += "<br/>";
// Source / SIG / Ref
ttt += `<span class='nowrap'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span>&nbsp;${sigSourceText} ${sig_refs}</span><br/>`;
ttt += `<span class='nowrap'><span class='icon-wrapper'><i class='fa-solid ${sigToIcon(s["sig"], "fa-tower-cell")}'></i></span>&nbsp;${sigSourceText} ${sig_refs}</span><br/>`;
// Time
ttt += `<span class='icon-wrapper'><i class='fa-solid fa-clock markerPopupIcon'></i></span>&nbsp;${moment.unix(s["time"]).fromNow()}`;
@@ -156,6 +160,21 @@ function loadOptions() {
// Store options
options = jsonData;
// Populate the Display panel
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
value: sc * 60,
text: sc
})));
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
getAvailableBandColorSchemes().forEach(sc => $("#band-color-scheme").append($('<option>', {
value: sc,
text: sc
})));
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setBandColorScheme($("#band-color-scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
@@ -167,13 +186,6 @@ function loadOptions() {
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
// Populate the Display panel
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
value: sc * 60,
text: sc
})));
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
// 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.

View File

@@ -328,7 +328,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
$tr.append(`<td class='nowrap'><span class='flag-wrapper' title='${dx_country}'>${dx_flag}</span><a class='dx-link' href='https://qrz.com/db/${s["dx_call"]}' target='_new' title='${s["dx_name"] != null ? s["dx_name"] : ""}'>${dx_call}</a></td>`);
}
if (showFreq) {
$tr.append(`<td class='nowrap'><span class='band-bullet' title='${bandFullName}' style='${(s["freq"] != null) ? "color: " + s["band_color"] : "display: none;"}'>&#9632;</span>${freq_string}</td>`);
$tr.append(`<td class='nowrap'><span class='band-bullet' title='${bandFullName}' style='${(s["freq"] != null) ? "color: " + bandToColor(s["band"]) : "display: none;"}'>&#9632;</span>${freq_string}</td>`);
}
if (showMode) {
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
@@ -340,7 +340,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
$tr.append(`<td class='nowrap hideonmobile'>${bearingText}</td>`);
}
if (showType) {
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${typeText}</td>`);
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid ${sigToIcon(s["sig"], "fa-tower-cell")}'></i></span> ${typeText}</td>`);
}
if (showRef) {
$tr.append(`<td class='hideonmobile' style='max-width: 11em;'>${sig_refs}</td>`);
@@ -366,7 +366,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
$td2 = $("<td colspan='100'>");
$td2floatleft = $(`<div style="float: left;">`);
if (showType) {
$td2floatleft.append(`<span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${typeText} `);
$td2floatleft.append(`<span class='icon-wrapper'><i class='fa-solid ${sigToIcon(s["sig"], "fa-tower-cell")}'></i></span> ${typeText} `);
}
if (showRef) {
$td2floatleft.append(`${sig_refs} `);
@@ -398,6 +398,21 @@ function loadOptions() {
// Store options
options = jsonData;
// Populate the Display panel
options["web-ui-options"]["spot-count"].forEach(sc => $("#spots-to-fetch").append($('<option>', {
value: sc,
text: sc
})));
$("#spots-to-fetch").val(options["web-ui-options"]["spot-count-default"]);
getAvailableBandColorSchemes().forEach(sc => $("#band-color-scheme").append($('<option>', {
value: sc,
text: sc
})));
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setBandColorScheme($("#band-color-scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
@@ -409,13 +424,6 @@ function loadOptions() {
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
// Populate the Display panel
options["web-ui-options"]["spot-count"].forEach(sc => $("#spots-to-fetch").append($('<option>', {
value: sc,
text: sc
})));
$("#spots-to-fetch").val(options["web-ui-options"]["spot-count-default"]);
// 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.

View File

@@ -8,8 +8,8 @@ function addBandToggleColourCSS(band_options) {
band_options.forEach(o => {
// CSS doesn't like IDs with decimal points in, so we need to replace that
var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown";
$style.append(`#filter-button-label-band-${cssFormattedBandName} { border-color: ${o['color']}; color: var(--bs-primary);}`);
$style.append(`.btn-check:checked + #filter-button-label-band-${cssFormattedBandName} { background-color: ${o['color']}; color: ${o['contrast_color']};}`);
$style.append(`#filter-button-label-band-${cssFormattedBandName} { border-color: ${bandToColor(o['name'])}; color: var(--bs-primary);}`);
$style.append(`.btn-check:checked + #filter-button-label-band-${cssFormattedBandName} { background-color: ${bandToColor(o['name'])}; color: ${bandToContrastColor(o['name'])};}`);
});
$('html > head').append($style);
}
@@ -32,7 +32,7 @@ function generateBandsMultiToggleFilterCard(band_options) {
function setHamHFBandToggles() {
const hamHFBands = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"];
$(".filter-button-band").each(function() {
$(this).prop('checked', hamHFBands.includes($(this).attr('id').replace("filter-button-band-", "")));
$(this).prop('checked', hamHFBands.includes($(this).val().replace("filter-button-band-", "")));
});
filtersUpdated();
}
@@ -41,7 +41,7 @@ function setHamHFBandToggles() {
function generateSIGsMultiToggleFilterCard(sig_options) {
// Create a button for each option
sig_options.forEach(o => {
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-${o['name']}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" id="filter-button-label-sig-${o['name']}" for="filter-button-sig-${o['name']}" title="${o['description']}"><i class="fa-solid fa-${o['icon']}"></i> ${o['name']}</label> `);
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-${o['name']}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" id="filter-button-label-sig-${o['name']}" for="filter-button-sig-${o['name']}" title="${o['description']}"><i class="fa-solid ${sigToIcon(o['name'], 'fa-tower-cell')}"></i> ${o['name']}</label> `);
});
// Create a bonus "NO_SIG" / "General DX" option
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-NO_SIG" value="NO_SIG" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" id="filter-button-label-sig-NO_SIG" for="filter-button-sig-NO_SIG"><i class="fa-solid fa-tower-cell"></i> General DX</label> `);
@@ -61,6 +61,14 @@ function toggleDarkMode() {
saveSettings();
}
// Function to update the band colour scheme in spots, bands and map pages
function setBandColorSchemeFromUI() {
setBandColorScheme($("#band-color-scheme option:selected").val());
saveSettings();
// Fudge a full reload because we need to update not just colours in the list/map/bands but also the filters
window.location.reload();
}
// Reload spots on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA
// after some time has passed with it in the background.
addEventListener("visibilitychange", (event) => {