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

@@ -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 '&';
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();