5 Commits

Author SHA1 Message Date
Ian Renton
059d9364eb Project version bump 2026-01-11 15:35:39 +00:00
Ian Renton
a3ca590ca3 JS import version bump 2026-01-11 15:14:34 +00:00
Ian Renton
cfff8dd832 Allow providers to be off-by-default in the web UI. Closes #93 2026-01-11 15:03:17 +00:00
Ian Renton
d1a5bfe9c3 Make allowing RBN spots via cluster a configurable option. 2026-01-11 12:09:36 +00:00
Ian Renton
da2827f559 Improve backwards compatibility by allowing login_callsign (and login_prompt) to be missing in DX cluster provider config. 2026-01-11 08:37:05 +00:00
20 changed files with 108 additions and 45 deletions

View File

@@ -60,10 +60,15 @@ spot-providers:
host: "hrd.wa9pie.net" host: "hrd.wa9pie.net"
port: 8000 port: 8000
# Prompt the cluster node gives when asking for a callsign to log in. Varies between cluster node software. # Prompt the cluster node gives when asking for a callsign to log in. Varies between cluster node software.
login_prompt: "login: " login_prompt: "login:"
# Callsign Spothole will use to log into this cluster. Ensure the SSID (e.g. -99) is different to any personal # Callsign Spothole will use to log into this cluster. Ensure the SSID (e.g. -99) is different to any personal
# connection you might make to this cluster node. # connection you might make to this cluster node.
login_callsign: "N0CALL-99" login_callsign: "N0CALL-99"
# Whether to allow RBN spots that come via this cluster. If you don't want RBN spots or you are making a separate
# connection to RBN directly, leave this as False. If you want RBN spots from this cluster, set this to True. (Make
# sure you aren't also separately connecting to RBN directly, otherwise you may get duplicate spots.) Note that not
# all clusters sent RBN spots anyway.
allow_rbn_spots: false
- -
class: "DXCluster" class: "DXCluster"
name: "W3LPL Cluster" name: "W3LPL Cluster"
@@ -71,24 +76,36 @@ spot-providers:
host: "w3lpl.net" host: "w3lpl.net"
port: 7373 port: 7373
# Prompt the cluster node gives when asking for a callsign to log in. Varies between cluster node software. # Prompt the cluster node gives when asking for a callsign to log in. Varies between cluster node software.
login_prompt: "Please enter your call: " login_prompt: "Please enter your call:"
# Callsign Spothole will use to log into this cluster. Ensure the SSID (e.g. -99) is different to any personal # Callsign Spothole will use to log into this cluster. Ensure the SSID (e.g. -99) is different to any personal
# connection you might make to this cluster node. # connection you might make to this cluster node.
login_callsign: "N0CALL-99" login_callsign: "N0CALL-99"
# Whether to allow RBN spots that come via this cluster. If you don't want RBN spots or you are making a separate
# connection to RBN directly, leave this as False. If you want RBN spots from this cluster, set this to True. (Make
# sure you aren't also separately connecting to RBN directly, otherwise you may get duplicate spots.) Note that not
# all clusters sent RBN spots anyway.
allow_rbn_spots: false
- -
class: "RBN" class: "RBN"
name: "RBN CW/RTTY" name: "RBN CW/RTTY"
enabled: false enabled: false
port: 7000 port: 7000
# This setting doesn't affect the spot provider itself, or anything in the back-end of Spothole, just the web UI.
# By default spots from all enabled providers will be shown in the web UI. However, you might want RBN data to be
# received by Spothole but not shown on the web UI unless the user explicitly turns it on. For that behaviour,
# set enabled to true, but enabled-by-default-in-web-ui to false.
enabled-by-default-in-web-ui: false
- -
class: "RBN" class: "RBN"
name: "RBN FT8" name: "RBN FT8"
enabled: false enabled: false
port: 7001 port: 7001
enabled-by-default-in-web-ui: false
- -
class: "UKPacketNet" class: "UKPacketNet"
name: "UK Packet Radio Net" name: "UK Packet Radio Net"
enabled: false enabled: false
enabled-by-default-in-web-ui: false
- -
class: "XOTA" class: "XOTA"
name: "39C3 TOTA" name: "39C3 TOTA"

View File

@@ -5,7 +5,8 @@ import yaml
# Check you have a config file # Check you have a config file
if not os.path.isfile("config.yml"): if not os.path.isfile("config.yml"):
logging.error("Your config file is missing. Ensure you have copied config-example.yml to config.yml and updated it according to your needs.") logging.error(
"Your config file is missing. Ensure you have copied config-example.yml to config.yml and updated it according to your needs.")
exit() exit()
# Load config # Load config
@@ -17,4 +18,9 @@ MAX_ALERT_AGE = config["max-alert-age-sec"]
SERVER_OWNER_CALLSIGN = config["server-owner-callsign"] SERVER_OWNER_CALLSIGN = config["server-owner-callsign"]
WEB_SERVER_PORT = config["web-server-port"] WEB_SERVER_PORT = config["web-server-port"]
ALLOW_SPOTTING = config["allow-spotting"] ALLOW_SPOTTING = config["allow-spotting"]
WEB_UI_OPTIONS = config["web-ui-options"] WEB_UI_OPTIONS = config["web-ui-options"]
# For ease of config, each spot provider owns its own config about whether it should be enabled by default in the web UI
# but for consistency we provide this to the front-end in web-ui-options because it has no impact outside of the web UI.
WEB_UI_OPTIONS["spot-providers-enabled-by-default"] = [p["name"] for p in config["spot-providers"] if p["enabled"] and (
"enabled-by-default-in-web-ui" not in p or p["enabled-by-default-in-web-ui"] == True)]

View File

@@ -4,7 +4,7 @@ from data.sig import SIG
# General software # General software
SOFTWARE_NAME = "Spothole by M0TRT" SOFTWARE_NAME = "Spothole by M0TRT"
SOFTWARE_VERSION = "1.1-pre" SOFTWARE_VERSION = "1.1"
# HTTP headers used for spot providers that use HTTP # HTTP headers used for spot providers that use HTTP
HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"} HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"}

View File

@@ -40,6 +40,7 @@ class APIOptionsHandler(tornado.web.RequestHandler):
# one of our proviers. # one of our proviers.
if ALLOW_SPOTTING: if ALLOW_SPOTTING:
options["spot_sources"].append("API") options["spot_sources"].append("API")
options["web-ui-options"]["spot-providers-enabled-by-default"].append("API")
self.write(json.dumps(options, default=serialize_everything)) self.write(json.dumps(options, default=serialize_everything))
self.set_status(200) self.set_status(200)

View File

@@ -7,27 +7,32 @@ from time import sleep
import pytz import pytz
import telnetlib3 import telnetlib3
from core.config import SERVER_OWNER_CALLSIGN
from data.spot import Spot from data.spot import Spot
from spotproviders.spot_provider import SpotProvider from spotproviders.spot_provider import SpotProvider
# Spot provider for a DX Cluster. Hostname, port, login_prompt and login_callsign are provided in config. # Spot provider for a DX Cluster. Hostname, port, login_prompt, login_callsign and allow_rbn_spots are provided in config.
# See config-example.yml for examples.
class DXCluster(SpotProvider): class DXCluster(SpotProvider):
# Note the callsign pattern deliberately excludes calls ending in "-#", which are from RBN and can be enabled by
# default on some clusters. If you want RBN spots, there is a separate provider for that.
CALLSIGN_PATTERN = "([a-z|0-9|/]+)" CALLSIGN_PATTERN = "([a-z|0-9|/]+)"
FREQUENCY_PATTERN = "([0-9|.]+)" FREQUENCY_PATTERN = "([0-9|.]+)"
LINE_PATTERN = re.compile( LINE_PATTERN_EXCLUDE_RBN = re.compile(
"^DX de " + CALLSIGN_PATTERN + ":\\s+" + FREQUENCY_PATTERN + "\\s+" + CALLSIGN_PATTERN + "\\s+(.*)\\s+(\\d{4}Z)", "^DX de " + CALLSIGN_PATTERN + ":\\s+" + FREQUENCY_PATTERN + "\\s+" + CALLSIGN_PATTERN + "\\s+(.*)\\s+(\\d{4}Z)",
re.IGNORECASE) re.IGNORECASE)
LINE_PATTERN_ALLOW_RBN = re.compile(
"^DX de " + CALLSIGN_PATTERN + "-?#?:\\s+" + FREQUENCY_PATTERN + "\\s+" + CALLSIGN_PATTERN + "\\s+(.*)\\s+(\\d{4}Z)",
re.IGNORECASE)
# Constructor requires hostname and port # Constructor requires hostname and port
def __init__(self, provider_config): def __init__(self, provider_config):
super().__init__(provider_config) super().__init__(provider_config)
self.hostname = provider_config["host"] self.hostname = provider_config["host"]
self.port = provider_config["port"] self.port = provider_config["port"]
self.login_prompt = provider_config["login_prompt"] self.login_prompt = provider_config["login_prompt"] if "login_prompt" in provider_config else "login:"
self.login_callsign = provider_config["login_callsign"] self.login_callsign = provider_config["login_callsign"] if "login_callsign" in provider_config else SERVER_OWNER_CALLSIGN
self.allow_rbn_spots = provider_config["allow_rbn_spots"] if "allow_rbn_spots" in provider_config else False
self.spot_line_pattern = self.LINE_PATTERN_ALLOW_RBN if self.allow_rbn_spots else self.LINE_PATTERN_EXCLUDE_RBN
self.telnet = None self.telnet = None
self.thread = Thread(target=self.handle) self.thread = Thread(target=self.handle)
self.thread.daemon = True self.thread.daemon = True
@@ -63,7 +68,7 @@ class DXCluster(SpotProvider):
try: try:
# Check new telnet info against regular expression # Check new telnet info against regular expression
telnet_output = self.telnet.read_until("\n".encode("latin-1")) telnet_output = self.telnet.read_until("\n".encode("latin-1"))
match = self.LINE_PATTERN.match(telnet_output.decode("latin-1")) match = self.spot_line_pattern.match(telnet_output.decode("latin-1"))
if match: if match:
spot_time = datetime.strptime(match.group(5), "%H%MZ") spot_time = datetime.strptime(match.group(5), "%H%MZ")
spot_datetime = datetime.combine(datetime.today(), spot_time.time()).replace(tzinfo=pytz.UTC) spot_datetime = datetime.combine(datetime.today(), spot_time.time()).replace(tzinfo=pytz.UTC)

View File

@@ -63,7 +63,7 @@
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p> <p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
</div> </div>
<script src="/js/common.js?v=5"></script> <script src="/js/common.js?v=6"></script>
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -69,8 +69,8 @@
</div> </div>
<script src="/js/common.js?v=5"></script> <script src="/js/common.js?v=6"></script>
<script src="/js/add-spot.js?v=5"></script> <script src="/js/add-spot.js?v=6"></script>
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -168,8 +168,8 @@
</div> </div>
<script src="/js/common.js?v=5"></script> <script src="/js/common.js?v=6"></script>
<script src="/js/alerts.js?v=5"></script> <script src="/js/alerts.js?v=6"></script>
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -134,9 +134,9 @@
</div> </div>
<script src="/js/common.js?v=5"></script> <script src="/js/common.js?v=6"></script>
<script src="/js/spotsbandsandmap.js?v=5"></script> <script src="/js/spotsbandsandmap.js?v=6"></script>
<script src="/js/bands.js?v=5"></script> <script src="/js/bands.js?v=6"></script>
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -46,10 +46,10 @@
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=5"></script> <script src="https://misc.ianrenton.com/jsutils/utils.js?v=6"></script>
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=5"></script> <script src="https://misc.ianrenton.com/jsutils/storage.js?v=6"></script>
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=5"></script> <script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=6"></script>
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=5"></script> <script src="https://misc.ianrenton.com/jsutils/geo.js?v=6"></script>
</head> </head>
<body> <body>

View File

@@ -152,9 +152,9 @@
<script src="https://cdn.jsdelivr.net/npm/leaflet.geodesic"></script> <script src="https://cdn.jsdelivr.net/npm/leaflet.geodesic"></script>
<script src="https://cdn.jsdelivr.net/npm/@joergdietrich/leaflet.terminator@1.1.0/L.Terminator.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@joergdietrich/leaflet.terminator@1.1.0/L.Terminator.min.js"></script>
<script src="/js/common.js?v=5"></script> <script src="/js/common.js?v=6"></script>
<script src="/js/spotsbandsandmap.js?v=5"></script> <script src="/js/spotsbandsandmap.js?v=6"></script>
<script src="/js/map.js?v=5"></script> <script src="/js/map.js?v=6"></script>
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -223,9 +223,9 @@
</div> </div>
<script src="/js/common.js?v=5"></script> <script src="/js/common.js?v=6"></script>
<script src="/js/spotsbandsandmap.js?v=5"></script> <script src="/js/spotsbandsandmap.js?v=6"></script>
<script src="/js/spots.js?v=5"></script> <script src="/js/spots.js?v=6"></script>
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -3,8 +3,8 @@
<div id="status-container" class="row row-cols-1 row-cols-md-4 g-4 mt-4"></div> <div id="status-container" class="row row-cols-1 row-cols-md-4 g-4 mt-4"></div>
<script src="/js/common.js?v=5"></script> <script src="/js/common.js?v=6"></script>
<script src="/js/status.js?v=5"></script> <script src="/js/status.js?v=6"></script>
<script>$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

10
webassets/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

6
webassets/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -1,4 +1,4 @@
openapi: 3.0.4 openapi: 3.1.0
info: info:
title: Spothole API title: Spothole API
description: |- description: |-
@@ -557,6 +557,12 @@ paths:
type: integer type: integer
example: 30 example: 30
description: The suggested default "maximum spot age" that the web UI should retrieve from the API description: The suggested default "maximum spot age" that the web UI should retrieve from the API
spot-providers-enabled-by-default:
type: array
description: A list of the spot providers that should be enabled in the web UI on first load, if the user hasn't already got a localStorage setting that sets their preference. This is to allow some high-volume providers like RBN to be enabled in Spothole's back-end and displayable in the web UI if the user wants, but by default the experience will not include them.
items:
type: string
example: "POTA"
alert-count: alert-count:
type: array type: array
description: An array of suggested "alert counts" that the web UI can retrieve from the API description: An array of suggested "alert counts" that the web UI can retrieve from the API

View File

@@ -252,7 +252,7 @@ function loadOptions() {
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]); generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]); generateSourcesMultiToggleFilterCard(options["spot_sources"], options["web-ui-options"]["spot-providers-enabled-by-default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs // 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 // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress

View File

@@ -184,7 +184,7 @@ function loadOptions() {
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]); generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]); generateSourcesMultiToggleFilterCard(options["spot_sources"], options["web-ui-options"]["spot-providers-enabled-by-default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs // 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 // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress

View File

@@ -422,7 +422,7 @@ function loadOptions() {
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]); generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]); generateSourcesMultiToggleFilterCard(options["spot_sources"], options["web-ui-options"]["spot-providers-enabled-by-default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs // 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 // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress

View File

@@ -6,10 +6,9 @@ var spots = []
function addBandToggleColourCSS(band_options) { function addBandToggleColourCSS(band_options) {
var $style = $('<style>'); var $style = $('<style>');
band_options.forEach(o => { band_options.forEach(o => {
// CSS doesn't like IDs with decimal points in, so we need to replace that var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown"; $style.append(`#filter-button-label-band-${domSafeName} { border-color: ${bandToColor(o['name'])}; color: var(--bs-primary);}`);
$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-${domSafeName} { background-color: ${bandToColor(o['name'])}; color: ${bandToContrastColor(o['name'])};}`);
$style.append(`.btn-check:checked + #filter-button-label-band-${cssFormattedBandName} { background-color: ${bandToColor(o['name'])}; color: ${bandToContrastColor(o['name'])};}`);
}); });
$('html > head').append($style); $('html > head').append($style);
} }
@@ -18,10 +17,8 @@ function addBandToggleColourCSS(band_options) {
function generateBandsMultiToggleFilterCard(band_options) { function generateBandsMultiToggleFilterCard(band_options) {
// Create a button for each option // Create a button for each option
band_options.forEach(o => { band_options.forEach(o => {
// CSS doesn't like IDs with decimal points in, so we need to replace that in the same way as when we originally var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
// queried the options endpoint and set our CSS. $("#band-options").append(`<input type="checkbox" class="btn-check filter-button-band storeable-checkbox" name="options" id="filter-button-band-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline" id="filter-button-label-band-${domSafeName}" for="filter-button-band-${domSafeName}">${o['name']}</label> `);
var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown";
$("#band-options").append(`<input type="checkbox" class="btn-check filter-button-band storeable-checkbox" name="options" id="filter-button-band-${cssFormattedBandName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline" id="filter-button-label-band-${cssFormattedBandName}" for="filter-button-band-${cssFormattedBandName}">${o['name']}</label> `);
}); });
// Create All/None/Ham HF buttons // Create All/None/Ham HF buttons
$("#band-options").append(` <span style="display: inline-block"><button id="filter-button-band-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', true);">All</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', false);">None</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="setHamHFBandToggles();">Ham HF</button></span>`); $("#band-options").append(` <span style="display: inline-block"><button id="filter-button-band-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', true);">All</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', false);">None</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="setHamHFBandToggles();">Ham HF</button></span>`);
@@ -41,7 +38,8 @@ function setHamHFBandToggles() {
function generateSIGsMultiToggleFilterCard(sig_options) { function generateSIGsMultiToggleFilterCard(sig_options) {
// Create a button for each option // Create a button for each option
sig_options.forEach(o => { 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 ${sigToIcon(o['name'], 'fa-tower-cell')}"></i> ${o['name']}</label> `); var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" 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 // 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> `); $("#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> `);
@@ -49,6 +47,20 @@ function generateSIGsMultiToggleFilterCard(sig_options) {
$("#sig-options").append(` <span style="display: inline-block"><button id="filter-button-sig-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('sig', true);">All</button>&nbsp;<button id="filter-button-sig-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('sig', false);">None</button></span>`); $("#sig-options").append(` <span style="display: inline-block"><button id="filter-button-sig-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('sig', true);">All</button>&nbsp;<button id="filter-button-sig-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('sig', false);">None</button></span>`);
} }
// Generate Sources filter card. This one is a minor special case as we create the buttons in the normal way, but then
// set which ones are enabled by default based on config rather than having them all enabled by default. We also sanitise
// names here for HTML elements.
function generateSourcesMultiToggleFilterCard(source_options, sources_enabled_by_default) {
// Create a button for each option
source_options.forEach(o => {
var enable = sources_enabled_by_default.includes(o);
var domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$("#source-options").append(`<input type="checkbox" class="btn-check filter-button-source storeable-checkbox" name="options" id="filter-button-source-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" ${enable ? "checked" : ""}><label class="btn btn-outline-primary" for="filter-button-source-${domSafeName}">${o}</label> `);
});
// Create All/None buttons
$("#source-options").append(` <span style="display: inline-block"><button id="filter-button-source-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('source', true);">All</button>&nbsp;<button id="filter-button-source-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('source', false);">None</button></span>`);
}
// Method called when any filter is changed to reload the spots and persist the filter settings. // Method called when any filter is changed to reload the spots and persist the filter settings.
function filtersUpdated() { function filtersUpdated() {
loadSpots(); loadSpots();