26 Commits

Author SHA1 Message Date
Ian Renton
9d9f4609f0 Doc tweaks 2025-11-26 22:12:20 +00:00
Ian Renton
368e69bf00 Use tower-cell icon for cluster/unknown spots rather than the desktop icon 2025-11-26 21:49:11 +00:00
Ian Renton
9bdd0ab1de Add filtering based on SIG to the web UI. #84 2025-11-26 21:43:10 +00:00
Ian Renton
255719f3b5 Add a special 'NO_SIG' option to 'sig' query params, which will allow us to filter out all xOTA spots/alerts, leaving just the generic ones. #84 2025-11-26 21:13:14 +00:00
Ian Renton
f21ea0ae5d Remove duplicated enums in spec #83 2025-11-26 20:29:35 +00:00
Ian Renton
2be2af176c Merge branch '82-tota'
# Conflicts:
#	webassets/apidocs/openapi.yml
2025-11-26 20:29:05 +00:00
Ian Renton
583735c99f Remove start/end dates #82 2025-11-26 07:40:46 +00:00
Ian Renton
0c8973bbc6 Remove duplicated enums in spec #83 2025-11-25 22:03:09 +00:00
Ian Renton
296cdb3795 Wider ranges to detect FT8/FT4 in "Guess mode based on frequency" function #85 2025-11-25 21:32:48 +00:00
Ian Renton
6c9f3136b8 First pass at TOTA support #82 2025-11-24 21:57:29 +00:00
Ian Renton
4e427f26c3 About page updates 2025-11-23 11:23:13 +00:00
ian
714151a6b4 Update views/webpage_about.tpl 2025-11-23 10:58:26 +00:00
Ian Renton
0ccc2bd15d Minor tweaks 2025-11-17 17:58:52 +00:00
Ian Renton
5724c4c7ea Minor tweaks 2025-11-17 17:50:29 +00:00
Ian Renton
94c0cad769 Improve SIG regexes to specify numbers of digits 2025-11-17 17:41:01 +00:00
Ian Renton
452e4beb29 Fix imports 2025-11-17 17:22:12 +00:00
Ian Renton
b132fe8a39 Fix a bug where SIG API spots could be re-tagged as another SIG e.g. WAB if that was named in the comment. 2025-11-17 17:19:43 +00:00
Ian Renton
e525aaed92 Fix a bug where spothole was too keen on extracting secondary references for xOTA programmes from comments, and was not checking that the "references" it found were surrounded by whitespace. 2025-11-16 17:46:40 +00:00
Ian Renton
92b7110356 Merge remote-tracking branch 'origin/main' 2025-11-16 17:46:05 +00:00
Ian Renton
114eacb9dc Fix a bug where spothole was too keen on extracting secondary references for xOTA programmes from comments, and was not checking that the "references" it found were surrounded by whitespace. 2025-11-16 17:45:58 +00:00
Ian Renton
2a90b17b6b Fix URLs for WOTA outlying fells 2025-11-14 14:37:36 +00:00
Ian Renton
ae075f3ac7 Version number bump 2025-11-13 21:52:13 +00:00
Ian Renton
efa9806c64 Look up K0SWE's dxcc.json rather than using our own tables. Closes #80 2025-11-13 21:51:20 +00:00
Ian Renton
03829831c0 Fix debug code commit 2025-11-13 21:47:05 +00:00
Ian Renton
4f83468309 Add config for "Number of Spots" and "Spot Age" values used in the web UI. Closes #79 2025-11-13 21:18:27 +00:00
Ian Renton
2165ebc103 DXCC 999 2025-11-13 20:10:53 +00:00
44 changed files with 602 additions and 1955 deletions

View File

@@ -10,7 +10,7 @@ The API is deliberately well-defined with an OpenAPI specification and auto-gene
Spothole itself is also open source, Public Domain licenced code that anyone can take and modify.
Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, BOTA, the UK Packet Repeater Network, and NG3K.
Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, BOTA, the UK Packet Repeater Network, NG3K, and any site based on the xOTA software by nischu.
![Screenshot](/images/screenshot2.png)
@@ -196,10 +196,12 @@ Finally, simply add the appropriate config to the `providers` section of `config
As well as being my work, I have also gratefully received feature patches from Steven, M1SDH.
The project contains a self-hosted copy of Font Awesome's free library, in the `/webasset/fa/` directory. This is subject to Font Awesome's licence and is not covered by the overall licence declared in the `LICENSE` file. This approach was taken in preference to using their hosted kits due to the popularity of this project exceeding the page view limit for their free hosted offering.
The project contains a self-hosted copy of Font Awesome's free library, in the `/webassets/fa/` directory. This is subject to Font Awesome's licence and is not covered by the overall licence declared in the `LICENSE` file. This approach was taken in preference to using their hosted kits due to the popularity of this project exceeding the page view limit for their free hosted offering.
The project contains a set of flag icons generated using the "Noto Color Emoji" font on a Debian system, in the `/webassets/img/flags/` directory.
The software uses a number of Python libraries as listed in `requirements.txt`, and a number of JavaScript libraries such as jQuery, Leaflet and Bootstrap. This project would not have been possible without these libraries, so many thanks to their developers.
Particular thanks go to QRZCQ country-files.com for providing country lookup data for amateur radio, and to the developers of `pyhamtools` for making it easy to use this data as well as QRZ.com and Clublog lookup.
Particular thanks go to country-files.com for providing country lookup data for amateur radio, to K0SWE for [this JSON-formatted DXCC data](https://github.com/k0swe/dxcc-json/), and to the developers of `pyhamtools` for making it easy to use country-files.com data as well as QRZ.com and Clublog lookup.
The project's name was suggested by Harm, DK4HAA. Thanks!

View File

@@ -1,9 +1,8 @@
from datetime import datetime, timedelta
from datetime import datetime
import pytz
from core.config import SERVER_OWNER_CALLSIGN, MAX_ALERT_AGE
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION
from core.config import MAX_ALERT_AGE
# Generic alert provider class. Subclasses of this query the individual APIs for alerts.

View File

@@ -2,8 +2,8 @@ from datetime import datetime, timedelta
import pytz
from bs4 import BeautifulSoup
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef

View File

@@ -4,7 +4,6 @@ from datetime import datetime
import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef

View File

@@ -3,7 +3,6 @@ from datetime import datetime
import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef

View File

@@ -3,7 +3,6 @@ from datetime import datetime
import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef

View File

@@ -4,7 +4,6 @@ import pytz
from rss_parser import RSSParser
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef

View File

@@ -3,7 +3,6 @@ from datetime import datetime
import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef

View File

@@ -81,6 +81,18 @@ spot-providers:
class: "UKPacketNet"
name: "UK Packet Radio Net"
enabled: false
-
class: "XOTA"
name: "39C3 TOTA"
enabled: false
url: "https://39c3.c3nav.de/"
# Fixed SIG/latitude/longitude for all spots from a provider is currently only a feature for the "XOTA" provider,
# the software found at https://github.com/nischu/xOTA/. This is because this is a generic backend for xOTA
# programmes and so different URLs provide different programmes.
sig: "TOTA"
latitude: 53.5622678
longitude: 9.9855205
# Alert providers to use. Same setup as the spot providers list above.
alert-providers:
@@ -135,4 +147,13 @@ hamqth-password: ""
clublog-api-key: ""
# Allow submitting spots to the Spothole API?
allow-spotting: true
allow-spotting: true
# Options for the web UI.
web-ui-options:
spot-count: [10, 25, 50, 100]
spot-count-default: 50
max-spot-age: [5, 10, 30, 60]
max-spot-age-default: 30
alert-count: [25, 50, 100, 200, 500]
alert-count-default: 100

View File

@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timedelta
from datetime import datetime
from threading import Timer
from time import sleep

View File

@@ -16,4 +16,5 @@ MAX_SPOT_AGE = config["max-spot-age-sec"]
MAX_ALERT_AGE = config["max-alert-age-sec"]
SERVER_OWNER_CALLSIGN = config["server-owner-callsign"]
WEB_SERVER_PORT = config["web-server-port"]
ALLOW_SPOTTING = config["allow-spotting"]
ALLOW_SPOTTING = config["allow-spotting"]
WEB_UI_OPTIONS = config["web-ui-options"]

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
import gzip
import json
import logging
import re
import urllib.parse
from datetime import timedelta
@@ -14,7 +16,7 @@ from requests_cache import CachedSession
from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE
from core.config import config
from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, \
QRZCQ_CALLSIGN_LOOKUP_DATA, HTTP_HEADERS, HAMQTH_PRG
HTTP_HEADERS, HAMQTH_PRG
# Singleton class that provides lookup functionality.
@@ -46,6 +48,8 @@ class LookupHelper:
self.CALL_INFO_BASIC = None
self.LOOKUP_LIB_BASIC = None
self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION = None
self.DXCC_JSON_DOWNLOAD_LOCATION = None
self.DXCC_DATA = None
def start(self):
# Lookup helpers from pyhamtools. We use five (!) of these. The simplest is country-files.com, which downloads
@@ -84,6 +88,19 @@ class LookupHelper:
filename=self.CLUBLOG_XML_DOWNLOAD_LOCATION)
self.CLUBLOG_CALLSIGN_DATA_CACHE = Cache('cache/clublog_callsign_lookup_cache')
# We also get a lookup of DXCC data from K0SWE to use for additional lookups of e.g. flags.
self.DXCC_JSON_DOWNLOAD_LOCATION = "cache/dxcc.json"
success = self.download_dxcc_json()
if success:
with open(self.DXCC_JSON_DOWNLOAD_LOCATION) as f:
tmp_dxcc_data = json.load(f)["dxcc"]
# Reformat as a map for faster lookup
self.DXCC_DATA = {}
for dxcc in tmp_dxcc_data:
self.DXCC_DATA[dxcc["entityCode"]] = dxcc
else:
logging.error("Could not download DXCC data, flags and similar data may be missing!")
# Download the cty.plist file from country-files.com on first startup. The pyhamtools lib can actually download and use
# this itself, but it's occasionally offline which causes it to throw an error. By downloading it separately, we can
# catch errors and handle them, falling back to a previous copy of the file in the cache, and we can use the
@@ -103,6 +120,22 @@ class LookupHelper:
logging.error("Exception when downloading Clublog cty.xml", e)
return False
# Download the dxcc.json file on first startup.
def download_dxcc_json(self):
try:
logging.info("Downloading dxcc.json...")
response = SEMI_STATIC_URL_DATA_CACHE.get("https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json",
headers=HTTP_HEADERS).text
with open(self.DXCC_JSON_DOWNLOAD_LOCATION, "w") as f:
f.write(response)
f.flush()
return True
except Exception as e:
logging.error("Exception when downloading dxcc.json", e)
return False
# Download the cty.xml (gzipped) file from Clublog on first startup, so we can use it in preference to querying the
# database live if possible.
def download_clublog_ctyxml(self):
@@ -175,11 +208,11 @@ class LookupHelper:
clublog_data = self.get_clublog_api_data_for_callsign(call)
if clublog_data and "Name" in clublog_data:
country = clublog_data["Name"]
# Couldn't get anything from Clublog database, try QRZCQ data
# Couldn't get anything from Clublog database, try DXCC data
if not country:
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
if qrzcq_data and "country" in qrzcq_data:
country = qrzcq_data["country"]
dxcc_data = self.get_dxcc_data_for_callsign(call)
if dxcc_data and "name" in dxcc_data:
country = dxcc_data["name"]
return country
# Infer a DXCC ID from a callsign
@@ -208,11 +241,11 @@ class LookupHelper:
clublog_data = self.get_clublog_api_data_for_callsign(call)
if clublog_data and "DXCC" in clublog_data:
dxcc = clublog_data["DXCC"]
# Couldn't get anything from Clublog database, try QRZCQ data
# Couldn't get anything from Clublog database, try DXCC data
if not dxcc:
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
if qrzcq_data and "dxcc" in qrzcq_data:
dxcc = qrzcq_data["dxcc"]
dxcc_data = self.get_dxcc_data_for_callsign(call)
if dxcc_data and "entityCode" in dxcc_data:
dxcc = dxcc_data["entityCode"]
return dxcc
# Infer a continent shortcode from a callsign
@@ -236,11 +269,12 @@ class LookupHelper:
clublog_data = self.get_clublog_api_data_for_callsign(call)
if clublog_data and "Continent" in clublog_data:
continent = clublog_data["Continent"]
# Couldn't get anything from Clublog database, try QRZCQ data
# Couldn't get anything from Clublog database, try DXCC data
if not continent:
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
if qrzcq_data and "continent" in qrzcq_data:
continent = qrzcq_data["continent"]
dxcc_data = self.get_dxcc_data_for_callsign(call)
# Some DXCCs are in two continents, if so don't use the continent data as we can't be sure
if dxcc_data and "continent" in dxcc_data and len(dxcc_data["continent"]) == 1:
continent = dxcc_data["continent"][0]
return continent
# Infer a CQ zone from a callsign
@@ -269,11 +303,12 @@ class LookupHelper:
clublog_data = self.get_clublog_api_data_for_callsign(call)
if clublog_data and "CQZ" in clublog_data:
cqz = clublog_data["CQZ"]
# Couldn't get anything from Clublog database, try QRZCQ data
# Couldn't get anything from Clublog database, try DXCC data
if not cqz:
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
if qrzcq_data and "cqz" in qrzcq_data:
cqz = qrzcq_data["cqz"]
dxcc_data = self.get_dxcc_data_for_callsign(call)
# Some DXCCs are in multiple zones, if so don't use the zone data as we can't be sure
if dxcc_data and "cq" in dxcc_data and len(dxcc_data["cq"]) == 1:
cqz = dxcc_data["cq"][0]
return cqz
# Infer a ITU zone from a callsign
@@ -293,13 +328,18 @@ class LookupHelper:
hamqth_data = self.get_hamqth_data_for_callsign(call)
if hamqth_data and "itu" in hamqth_data:
ituz = hamqth_data["itu"]
# Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try QRZCQ data
# Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try DXCC data
if not ituz:
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
if qrzcq_data and "ituz" in qrzcq_data:
ituz = qrzcq_data["ituz"]
dxcc_data = self.get_dxcc_data_for_callsign(call)
# Some DXCCs are in multiple zones, if so don't use the zone data as we can't be sure
if dxcc_data and "itu" in dxcc_data and len(dxcc_data["itu"]) == 1:
ituz = dxcc_data["itu"]
return ituz
# Get an emoji flag for a given DXCC entity ID
def get_flag_for_dxcc(self, dxcc):
return self.DXCC_DATA[dxcc]["flag"] if dxcc in self.DXCC_DATA else None
# Infer an operator name from a callsign (requires QRZ.com/HamQTH)
def infer_name_from_callsign_online_lookup(self, call):
data = self.get_qrz_data_for_callsign(call)
@@ -378,7 +418,20 @@ class LookupHelper:
# Infer a mode from the frequency (in Hz) according to the band plan. Just a guess really.
def infer_mode_from_frequency(self, freq):
try:
return freq_to_band(freq / 1000.0)["mode"]
khz = freq / 1000.0
mode = freq_to_band(khz)["mode"]
# Some additional common digimode ranges in addition to what the 3rd-party freq_to_band function returns.
# This is mostly here just because freq_to_band is very specific about things like FT8 frequencies, and e.g.
# a spot at 7074.5 kHz will be indicated as LSB, even though it's clearly in the FT8 range. Future updates
# might include other common digimode centres of activity here, but this achieves the main goal of keeping
# large numbers of clearly-FT* spots off the list of people filtering out digimodes.
if (7074 <= khz < 7077) or (10136 <= khz < 10139) or (14074 <= khz < 14077) or (18100 <= khz < 18103) or (
21074 <= khz < 21077) or (24915 <= khz < 24918) or (28074 <= khz < 28077):
mode = "FT8"
if (7047.5 <= khz < 7050.5) or (10140 <= khz < 10143) or (14080 <= khz < 14083) or (
18104 <= khz < 18107) or (21140 <= khz < 21143) or (24919 <= khz < 24922) or (28180 <= khz < 28183):
mode = "FT4"
return mode
except KeyError:
return None
@@ -488,11 +541,10 @@ class LookupHelper:
else:
return None
# Utility method to get QRZCQ data from our constants table, if we can find it
def get_qrzcq_data_for_callsign(self, call):
# Iterate in reverse order - see comments on the data structure itself
for entry in reversed(QRZCQ_CALLSIGN_LOOKUP_DATA):
if call.startswith(entry["prefix"]):
# Utility method to get generic DXCC data from our lookup table, if we can find it
def get_dxcc_data_for_callsign(self, call):
for entry in self.DXCC_DATA.values():
if re.match(entry["prefixRegex"], call):
return entry
return None

View File

@@ -98,7 +98,12 @@ def get_sig_ref_info(sig, sig_ref_id):
for feature in data["features"]:
if feature["properties"]["wotaId"] == sig_ref_id:
sig_ref.name = feature["properties"]["title"]
# Fudge WOTA URLs. Outlying fell (LDO) URLs don't match their ID numbers but require 214 to be
# added to them
sig_ref.url = "https://www.wota.org.uk/MM_" + sig_ref_id
if sig_ref_id.upper().startswith("LDO-"):
number = int(sig_ref_id.upper().replace("LDO-", ""))
sig_ref.url = "https://www.wota.org.uk/MM_LDO-" + str(number + 214)
sig_ref.grid = feature["properties"]["qthLocator"]
sig_ref.latitude = feature["geometry"]["coordinates"][1]
sig_ref.longitude = feature["geometry"]["coordinates"][0]

View File

@@ -6,7 +6,6 @@ from datetime import datetime, timedelta
import pytz
from core.constants import DXCC_FLAGS
from core.lookup_helper import lookup_helper
from core.sig_utils import get_icon_for_sig, get_sig_ref_info
@@ -95,8 +94,8 @@ class Alert:
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0])
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0])
if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag:
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id]
if self.dx_dxcc_id and not self.dx_flag:
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs

View File

@@ -9,7 +9,6 @@ from datetime import datetime
import pytz
from pyhamtools.locator import locator_to_latlong, latlong_to_locator
from core.constants import DXCC_FLAGS
from core.lookup_helper import lookup_helper
from core.sig_utils import get_icon_for_sig, get_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
from data.sig_ref import SIGRef
@@ -174,8 +173,8 @@ class Spot:
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call)
if self.dx_call and not self.dx_dxcc_id:
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call)
if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag:
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id]
if self.dx_dxcc_id and not self.dx_flag:
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
# Clean up spotter call if it has an SSID or -# from RBN
if self.de_call and "-" in self.de_call:
@@ -207,8 +206,8 @@ class Spot:
self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call)
if not self.de_dxcc_id:
self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call)
if self.de_dxcc_id and self.de_dxcc_id in DXCC_FLAGS and not self.de_flag:
self.de_flag = DXCC_FLAGS[self.de_dxcc_id]
if self.de_dxcc_id and not self.de_flag:
self.de_flag = lookup_helper.get_flag_for_dxcc(self.de_dxcc_id)
# Band from frequency
if self.freq and not self.band:
@@ -240,13 +239,17 @@ class Spot:
if self.dx_latitude:
self.dx_location_source = "SPOT"
# See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This
# Set the top-level "SIG" if it is missing but we have at least one SIG ref.
if not self.sig and self.sig_refs and len(self.sig_refs) > 0:
self.sig = self.sig_refs[0].sig.upper()
# See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This
# should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
if self.comment and self.sig_refs and len(self.sig_refs) > 0:
sig = self.sig_refs[0].sig.upper()
all_comment_refs = re.findall(get_ref_regex_for_sig(sig), self.comment)
for ref in all_comment_refs:
self.append_sig_ref_if_missing(SIGRef(id=ref.upper(), sig=sig))
all_comment_ref_matches = re.finditer(r"(^|\W)(" + get_ref_regex_for_sig(sig) + r")(^|\W)", self.comment, re.IGNORECASE)
for ref_match in all_comment_ref_matches:
self.append_sig_ref_if_missing(SIGRef(id=ref_match.group(2).upper(), sig=sig))
# See if the comment looks like it contains any SIGs (and optionally SIG references) that we can
# add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g. POTA
@@ -297,6 +300,10 @@ class Spot:
if self.sig:
self.icon = get_icon_for_sig(self.sig)
# Default "radio" icon if nothing else has set it
if not self.icon:
self.icon = "tower-cell"
# DX Grid to lat/lon and vice versa in case one is missing
if self.dx_grid and not self.dx_latitude:
ll = locator_to_latlong(self.dx_grid)
@@ -377,7 +384,7 @@ class Spot:
self_copy.received_time_iso = ""
self.id = hashlib.sha256(str(self_copy).encode("utf-8")).hexdigest()
# JSON serialise
# JSON sspoterialise
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
@@ -385,7 +392,11 @@ class Spot:
def append_sig_ref_if_missing(self, new_sig_ref):
if not self.sig_refs:
self.sig_refs = []
new_sig_ref.id = new_sig_ref.id.strip().upper()
new_sig_ref.sig = new_sig_ref.sig.strip().upper()
if new_sig_ref.id == "":
return
for sig_ref in self.sig_refs:
if sig_ref.id.upper() == new_sig_ref.id.upper() and sig_ref.sig.upper() == new_sig_ref.sig.upper():
if sig_ref.id == new_sig_ref.id and sig_ref.sig == new_sig_ref.sig:
return
self.sig_refs.append(new_sig_ref)

View File

@@ -8,7 +8,7 @@ import bottle
import pytz
from bottle import run, request, response, template
from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING
from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING, WEB_UI_OPTIONS
from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, CONTINENTS, SOFTWARE_VERSION, UNKNOWN_BAND
from core.lookup_helper import lookup_helper
from core.prometheus_metrics_handler import page_requests_counter, get_metrics, api_requests_counter
@@ -268,8 +268,6 @@ class WebServer:
# infer missing data, and add it to our database.
spot.source = "API"
if not spot.sig:
spot.icon = "desktop"
spot.infer_missing()
self.spots.add(spot.id, spot, expire=MAX_SPOT_AGE)
@@ -338,11 +336,14 @@ class WebServer:
sources = query.get(k).split(",")
spots = [s for s in spots if s.source and s.source in sources]
case "sig":
# If a list of sigs is provided, the spot must have a sig and it must match one of them
# If a list of sigs is provided, the spot must have a sig and it must match one of them.
# The special "sig" "NO_SIG", when supplied in the list, mathches spots with no sig.
sigs = query.get(k).split(",")
spots = [s for s in spots if s.sig and s.sig in sigs]
include_no_sig = "NO_SIG" in sigs
spots = [s for s in spots if (s.sig and s.sig in sigs) or (include_no_sig and not s.sig)]
case "needs_sig":
# If true, a sig is required, regardless of what it is, it just can't be missing.
# If true, a sig is required, regardless of what it is, it just can't be missing. Mutually
# exclusive with supplying the special "NO_SIG" parameter to the "sig" query param.
needs_sig = query.get(k).upper() == "TRUE"
if needs_sig:
spots = [s for s in spots if s.sig]
@@ -441,8 +442,11 @@ class WebServer:
sources = query.get(k).split(",")
alerts = [a for a in alerts if a.source and a.source in sources]
case "sig":
# If a list of sigs is provided, the alert must have a sig and it must match one of them.
# The special "sig" "NO_SIG", when supplied in the list, mathches alerts with no sig.
sigs = query.get(k).split(",")
alerts = [a for a in alerts if a.sig and a.sig in sigs]
include_no_sig = "NO_SIG" in sigs
spots = [a for a in alerts if (a.sig and a.sig in sigs) or (include_no_sig and not a.sig)]
case "dx_continent":
dxconts = query.get(k).split(",")
alerts = [a for a in alerts if a.dx_continent and a.dx_continent in dxconts]
@@ -469,7 +473,8 @@ class WebServer:
map(lambda p: p["name"], filter(lambda p: p["enabled"], self.status_data["alert_providers"]))),
"continents": CONTINENTS,
"max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING}
"spot_allowed": ALLOW_SPOTTING,
"web-ui-options": WEB_UI_OPTIONS}
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
# one of our proviers.
if ALLOW_SPOTTING:

View File

@@ -72,7 +72,7 @@ class DXCluster(SpotProvider):
de_call=match.group(1),
freq=float(match.group(2)) * 1000,
comment=match.group(4).strip(),
icon="desktop",
icon="tower-cell",
time=spot_datetime.timestamp())
# Add to our list

View File

@@ -54,20 +54,27 @@ class GMA(HTTPSpotProvider):
match ref_info["reftype"]:
case "Summit":
spot.sig_refs[0].sig = "GMA"
spot.sig = "GMA"
case "IOTA Island":
spot.sig_refs[0].sig = "IOTA"
spot.sig = "IOTA"
case "Lighthouse (ILLW)":
spot.sig_refs[0].sig = "ILLW"
spot.sig = "ILLW"
case "Lighthouse (ARLHS)":
spot.sig_refs[0].sig = "ARLHS"
spot.sig = "ARLHS"
case "Castle":
spot.sig_refs[0].sig = "WCA"
spot.sig = "WCA"
case "Mill":
spot.sig_refs[0].sig = "MOTA"
spot.sig = "MOTA"
case _:
logging.warn("GMA spot found with ref type " + ref_info[
"reftype"] + ", developer needs to add support for this!")
spot.sig_refs[0].sig = ref_info["reftype"]
spot.sig = ref_info["reftype"]
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.

View File

@@ -5,7 +5,6 @@ import pytz
import requests
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -53,6 +52,7 @@ class HEMA(HTTPSpotProvider):
freq=float(freq_mode_match.group(1)) * 1000000,
mode=freq_mode_match.group(2).upper(),
comment=spotter_comment_match.group(2),
sig="HEMA",
sig_refs=[SIGRef(id=spot_items[3].upper(), sig="HEMA", name=spot_items[4])],
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(tzinfo=pytz.UTC).timestamp(),
dx_latitude=float(spot_items[7]),

View File

@@ -4,7 +4,6 @@ from datetime import datetime
import pytz
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -33,6 +32,7 @@ class ParksNPeaks(HTTPSpotProvider):
# Seen PNP spots with empty frequency, and with comma-separated thousands digits
mode=source_spot["actMode"].upper(),
comment=source_spot["actComments"],
sig=source_spot["actClass"].upper(),
sig_refs=[SIGRef(id=source_spot["actSiteID"], sig=source_spot["actClass"].upper())],
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp())

View File

@@ -1,9 +1,7 @@
import re
from datetime import datetime
import pytz
from core.sig_utils import get_icon_for_sig, get_ref_regex_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -31,6 +29,7 @@ class POTA(HTTPSpotProvider):
freq=float(source_spot["frequency"]) * 1000,
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
sig="POTA",
sig_refs=[SIGRef(id=source_spot["reference"], sig="POTA", name=source_spot["name"])],
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp(),

View File

@@ -3,7 +3,6 @@ from datetime import datetime
import requests
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -45,6 +44,7 @@ class SOTA(HTTPSpotProvider):
freq=(float(source_spot["frequency"]) * 1000000) if (source_spot["frequency"] is not None) else None, # Seen SOTA spots with no frequency!
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
sig="SOTA",
sig_refs=[SIGRef(id=source_spot["summitCode"], sig="SOTA", name=source_spot["summitName"])],
time=datetime.fromisoformat(source_spot["timeStamp"]).timestamp(),
activation_score=source_spot["points"])

View File

@@ -2,8 +2,7 @@ from datetime import datetime
import pytz
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION
from core.config import SERVER_OWNER_CALLSIGN, MAX_SPOT_AGE
from core.config import MAX_SPOT_AGE
# Generic spot provider class. Subclasses of this query the individual APIs for data.

View File

@@ -9,6 +9,7 @@ from requests_sse import EventSource
from core.constants import HTTP_HEADERS
from spotproviders.spot_provider import SpotProvider
# Spot provider using Server-Sent Events.
class SSESpotProvider(SpotProvider):

View File

@@ -1,11 +1,8 @@
import re
from datetime import datetime, timedelta
from datetime import datetime
import pytz
from requests_cache import CachedSession
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig, get_ref_regex_for_sig
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider

View File

@@ -13,7 +13,7 @@ from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Wainwrights on the Air
class WOTA(HTTPSpotProvider):
POLL_INTERVAL_SEC = 120
SPOTS_URL = "http://127.0.0.1:8000/spots_rss.php"
SPOTS_URL = "https://www.wota.org.uk/spots_rss.php"
LIST_URL = "https://www.wota.org.uk/mapping/data/summits.json"
RSS_DATE_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S %z"
@@ -66,6 +66,7 @@ class WOTA(HTTPSpotProvider):
freq=freq_hz,
mode=mode,
comment=comment,
sig="WOTA",
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
time=time.timestamp())

View File

@@ -1,7 +1,6 @@
import json
from datetime import datetime
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.sse_spot_provider import SSESpotProvider
@@ -29,6 +28,7 @@ class WWBOTA(SSESpotProvider):
freq=float(source_spot["freq"]) * 1000000,
mode=source_spot["mode"].upper(),
comment=source_spot["comment"],
sig="WWBOTA",
sig_refs=refs,
time=datetime.fromisoformat(source_spot["time"]).timestamp(),
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For

View File

@@ -2,7 +2,6 @@ from datetime import datetime
import pytz
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -28,6 +27,7 @@ class WWFF(HTTPSpotProvider):
freq=float(source_spot["frequency_khz"]) * 1000,
mode=source_spot["mode"].upper(),
comment=source_spot["remarks"],
sig="WWFF",
sig_refs=[SIGRef(id=source_spot["reference"], sig="WWFF", name=source_spot["reference_name"])],
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
dx_latitude=source_spot["latitude"],

43
spotproviders/xota.py Normal file
View File

@@ -0,0 +1,43 @@
from datetime import datetime
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
# The provider typically doesn't give us a lat/lon or SIG explicitly, so our own config provides this information. This
# functionality is implemented for TOTA events.
class XOTA(HTTPSpotProvider):
POLL_INTERVAL_SEC = 300
FIXED_LATITUDE = None
FIXED_LONGITUDE = None
SIG = None
def __init__(self, provider_config):
super().__init__(provider_config, provider_config["url"] + "/api/spot/all", self.POLL_INTERVAL_SEC)
self.FIXED_LATITUDE = provider_config["latitude"] if "latitude" in provider_config else None
self.FIXED_LONGITUDE = provider_config["longitude"] if "longitude" in provider_config else None
self.SIG = provider_config["sig"] if "sig" in provider_config else None
def http_response_to_spots(self, http_response):
new_spots = []
# Iterate through source data
for source_spot in http_response.json():
# Convert to our spot format
spot = Spot(source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["stationCallSign"].upper(),
freq=float(source_spot["freq"]) * 1000,
mode=source_spot["mode"].upper(),
sig=self.SIG,
sig_refs=[SIGRef(id=source_spot["reference"]["title"], sig=self.SIG, url=source_spot["reference"]["website"])],
time=datetime.fromisoformat(source_spot["modificationDate"]).timestamp(),
dx_latitude=self.FIXED_LATITUDE,
dx_longitude=self.FIXED_LONGITUDE,
qrt=source_spot["state"] != "active")
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.
new_spots.append(spot)
return new_spots

View File

@@ -2,7 +2,6 @@ from datetime import datetime
import pytz
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -34,6 +33,7 @@ class ZLOTA(HTTPSpotProvider):
freq=freq_hz,
mode=source_spot["mode"].upper().strip(),
comment=source_spot["comments"],
sig="ZLOTA",
sig_refs=[SIGRef(id=source_spot["reference"], sig="ZLOTA", name=source_spot["name"])],
time=datetime.fromisoformat(source_spot["referenced_time"]).astimezone(pytz.UTC).timestamp())

View File

@@ -14,17 +14,26 @@
<p>Spothole is an "aggregator" for those spots, so it checks lots of different services for data, and brings it all together in one place. So no matter what kinds of interesting spots you are looking for, you can find them here.</p>
<p>As well as spots, it also provides a similar feed of "alerts". This is where amateur radio users who are going to interesting places soon will announce their intentions.</p>
<h4 class="mt-4">What are "DX", "DE" and modes?</h4>
<p>In amateur radio terminology, the "DX" contact is the "interesting" one that is using the frequency shown. They might be on a remote island or just in a local park, but either way it's interesting enough that someone has "spotted" them. The callsign listed under "DE" is the person who spotted the "DX" operator. "Modes" are the type of communication they are using. You might see "CW" which is Morse Code, or voice "modes" like SSB or FM, or more exotic "data" modes which are used for computer-to-computer communication.</p>
<p>In amateur radio terminology, the "DX" contact is the "interesting" one that is using the frequency shown and looking for callers. They might be on a remote island or just in a local park, but either way it's interesting enough that someone has "spotted" them. The callsign listed under "DE" is the person who entered the spot of the "DX" operator. "Modes" are the type of communication they are using. For example you might see "CW" which is Morse Code, or voice "modes" like SSB or FM, or more exotic "data" modes which are used for computer-to-computer communication.</p>
<h4 class="mt-4">What data sources are supported?</h4>
<p>Spothole can retrieve spots from: Telnet-based DX clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, and the UK Packet Repeater Network.</p>
<p>Spothole can retrieve alerts from: NG3K, POTA, SOTA, WWFF, Parks 'n' Peaks, WOTA and BOTA.</p>
<p>Spothole can retrieve spots from: <a href="https://www.dxcluster.info/telnet/">Telnet-based DX clusters</a>, the <a href="https://www.reversebeacon.net/">Reverse Beacon Network (RBN)</a>, the <a href="https://www.aprs-is.net/">APRS Internet Service (APRS-IS)</a>, <a href="https://pota.app">POTA</a>, <a href="https://www.sota.org.uk/">SOTA</a>, <a href="https://wwff.co/">WWFF</a>, <a href="https://www.cqgma.org/">GMA</a>, <a href="https://wwbota.net/">WWBOTA</a>, <a href="http://www.hema.org.uk/">HEMA</a>, <a href="https://www.parksnpeaks.org/">Parks 'n' Peaks</a>, <a href="https://ontheair.nz">ZLOTA</a>, <a href="https://www.wota.org.uk/">WOTA</a>, the <a href="https://ukpacketradio.network/">UK Packet Repeater Network</a>, and any site based on the <a href="https://github.com/nischu/xOTA">xOTA software by nischu</a>.</p>
<p>Spothole can retrieve alerts from: <a href="https://www.ng3k.com/">NG3K</a>, <a href="https://pota.app">POTA</a>, <a href="https://www.sota.org.uk/">SOTA</a>, <a href="https://wwff.co/">WWFF</a>, <a href="https://www.parksnpeaks.org/">Parks 'n' Peaks</a>, <a href="https://www.wota.org.uk/">WOTA</a> and <a href="https://www.beachesontheair.com/">BOTA</a>.</p>
<p>Note that the server owner has not necessarily enabled all these data sources. In particular it is common to disable RBN, to avoid the server being swamped with FT8 traffic, and to disable APRS-IS and UK Packet Net so that the server only displays stations where there is likely to be an operator physically present for a QSO.</p>
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, IOTA, MOTS, ARLHS, ILLW, SIOTA, WCA, ZLOTA, KRMNPA, WOTA, BOTA, WAB & WAI.</p>
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air (POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air (MOTA), the Amateur Radio Lighthouse Socirty (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National Parks Award (KRMNPA), Wainwrights on the Air (WOTA), Beaches on the Air (BOTA), Worked All Britain (WAB), Worked All Ireland (WAI), and Toilets on the Air (TOTA).</p>
<p>As of the time of writing in November 2025, I think Spothole captures essentially all outdoor radio programmes that have a defined reference list, and almost certainly those that have a spotting/alerting API. If you know of one I've missed, please let me know!</p>
<h4 class="mt-4">Why can I filter spots by both SIG and Source? Isn't that basically the same thing?</h4>
<p>Mostly, but not quite. While POTA spots generally come from the POTA source and so on, there are a few exceptions:</p>
<ol><li>Sources like GMA and Parks 'n' Peaks provide spots for multiple different programmes (SIGs).</li>
<li>Cluster spots may name SIGs in their comment, in which case the source remains the Cluster, but a SIG is assigned.</li>
<li>Some SIGs, such as Worked all Britain (WAB), don't have their own spotting site and can <em>only</em> be identified through comments on spots retrieved from other sources.</li>
<li>SIGs have well-defined names, whereas the server owner may name the sources as they see fit.</li></ol>
<p>Spothole's web interface exists not just for the end user, but also as a reference implementation for the API, so I have chosen to demonstrate both methods of filtering.</p>
<h4 class="mt-4">How is this better than DXheat, DXsummit, POTA's own website, etc?</h4>
<p>It's probably not? But it's nice to have choice.</p>
<p>I think it's got two key advantages over those sites:</p>
<p>I think it's got three key advantages over those sites:</p>
<ol><li>It provides a public, <a href="/apidocs">well-documented API</a> with an <a href="/apidocs/openapi.yml">OpenAPI specification</a>. Other sites don't have official APIs or don't bother documenting them publicly, because they want people to use their web page. I like Spothole's web page, but you don't have to use it&mdash;if you're a programmer, you can build your own software on Spothole's API. Spothole does the hard work of taking all the various data sources and providing a consistent, well-documented data set. You can then do the fun bit of writing your own application.</li>
<li>It grabs data from a lot more sources, and it's easy to add more. Since it's open source, anyone can contribute a new data source and share it with the community.</li></ol>
<li>It grabs data from a lot more sources. I've seen other sites that pull in DX Cluster and POTA spots together, but nothing on the scale of what Spothole supports.</li>
<li>Spothole is open source, so anyone can contribute the code to support a new data source or add new features, and share them with the community.</li></ol>
<h4 class="mt-4">Why does this website ask me if I want to install it?</h4>
<p>Spothole is a Progressive Web App, which means you can install it on an Android or iOS device by opening the site in Chrome or Safari respectively, and clicking "Install" on the pop-up panel. It'll only prompt you once, so if you dismiss the prompt and change your mind, you'll find an Install / Add to Home Screen option on your browser's menu.</p>
<p>Installing Spothole on your phone is completely optional, the website works exactly the same way as the "app" does.</p>
@@ -41,7 +50,8 @@
<p>Spothole is open source, so you can audit <a href="https://git.ianrenton.com/ian/spothole">the code</a> if you like.</p>
<h2 class="mt-4">Thanks</h2>
<p>This project would not have been possible without those volunteers who have taken it upon themselves to run DX clusters, xOTA programmes, DXpedition lists, callsign lookup databases, and other online tools on which Spothole's data is based.</p>
<p>Spothole is also dependent on a number of Python libraries, in particular pyhamtools, and many JavaScript libraries, as well as the Font Awesome icon set.</p>
<p>Spothole is also dependent on a number of Python libraries, in particular pyhamtools, and many JavaScript libraries, as well as the Font Awesome icon set and flag icons from the Noto Color Emoji set.</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>
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>

View File

@@ -101,11 +101,6 @@
<h5 class="card-title">Number of Alerts</h5>
<p class="card-text spothole-card-text">Show up to
<select id="alerts-to-fetch" class="storeable-select form-select ms-2" oninput="filtersUpdated();" style="width: 5em;display: inline-block;">
<option value="25">25</option>
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
alerts
</p>

View File

@@ -26,7 +26,7 @@
</div>
<div class="card-body">
<div class="row row-cols-1 g-4 mb-4">
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
<div class="col">
<div class="card">
<div class="card-body">
@@ -35,8 +35,24 @@
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">SIGs</h5>
<p id="sig-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Sources</h5>
<p id="source-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-4 g-4">
<div class="row row-cols-1 row-cols-md-3 g-4">
<div class="col">
<div class="card">
<div class="card-body">
@@ -61,14 +77,6 @@
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Sources</h5>
<p id="source-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -93,10 +101,6 @@
<h5 class="card-title">Spot Age</h5>
<p class="card-text spothole-card-text">Last
<select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
<option value="300">5</option>
<option value="600">10</option>
<option value="1800" selected>30</option>
<option value="3600">60</option>
</select>
minutes
</p>
@@ -112,6 +116,6 @@
</div>
<script src="/js/common.js"></script>
<script src="/js/spotandmap.js"></script>
<script src="/js/spotsbandsandmap.js"></script>
<script src="/js/bands.js"></script>
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>

View File

@@ -25,7 +25,7 @@
</div>
<div class="card-body">
<div class="row row-cols-1 g-4 mb-4">
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
<div class="col">
<div class="card">
<div class="card-body">
@@ -34,8 +34,24 @@
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">SIGs</h5>
<p id="sig-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Sources</h5>
<p id="source-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-4 g-4">
<div class="row row-cols-1 row-cols-md-3 g-4">
<div class="col">
<div class="card">
<div class="card-body">
@@ -60,14 +76,6 @@
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Sources</h5>
<p id="source-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -92,10 +100,6 @@
<h5 class="card-title">Spot Age</h5>
<p class="card-text spothole-card-text">Last
<select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
<option value="300">5</option>
<option value="600">10</option>
<option value="1800" selected>30</option>
<option value="3600">60</option>
</select>
minutes
</p>
@@ -130,6 +134,6 @@
<script src="https://cdn.jsdelivr.net/npm/@joergdietrich/leaflet.terminator@1.1.0/L.Terminator.min.js"></script>
<script src="/js/common.js"></script>
<script src="/js/spotandmap.js"></script>
<script src="/js/spotsbandsandmap.js"></script>
<script src="/js/map.js"></script>
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>

View File

@@ -37,7 +37,7 @@
</div>
<div class="card-body">
<div class="row row-cols-1 g-4 mb-4">
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
<div class="col">
<div class="card">
<div class="card-body">
@@ -46,8 +46,24 @@
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">SIGs</h5>
<p id="sig-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Sources</h5>
<p id="source-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-4 g-4">
<div class="row row-cols-1 row-cols-md-3 g-4">
<div class="col">
<div class="card">
<div class="card-body">
@@ -72,14 +88,6 @@
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Sources</h5>
<p id="source-options" class="card-text spothole-card-text"></p>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -117,10 +125,6 @@
<h5 class="card-title">Number of Spots</h5>
<p class="card-text spothole-card-text">Show up to
<select id="spots-to-fetch" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
<option value="10">10</option>
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
spots
</p>
@@ -192,6 +196,6 @@
</div>
<script src="/js/common.js"></script>
<script src="/js/spotandmap.js"></script>
<script src="/js/spotsbandsandmap.js"></script>
<script src="/js/spots.js"></script>
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>

View File

@@ -55,49 +55,16 @@ paths:
description: "Limit the spots to only ones from one or more sources. To select more than one source, supply a comma-separated list."
required: false
schema:
type: string
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- BOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
$ref: "#/components/schemas/Source"
- name: sig
in: query
description: "Limit the spots to only ones from one or more Special Interest Groups provided as an argument. To select more than one SIG, supply a comma-separated list."
description: "Limit the spots to only ones from one or more Special Interest Groups provided as an argument. To select more than one SIG, supply a comma-separated list. The special `sig` name `NO_SIG` matches spots with no sig set. You can use `sig=NO_SIG` to specifically only return generic spots with no associated SIG. You can also use combinations to request for example POTA + no SIG, but reject other SIGs. If you want to request 'every SIG and not No SIG', see the `needs_sig` query parameter for a shortcut."
required: false
schema:
type: string
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- WCA
- MOTA
- SIOTA
- ARLHS
- ILLW
- ZLOTA
- IOTA
- WOTA
- BOTA
- WAB
- WAI
$ref: "#/components/schemas/SIGNameIncludingNoSIG"
- name: needs_sig
in: query
description: "Limit the spots to only ones with a Special Interest Group such as POTA. Because supplying all known SIGs as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots with *no* SIG, this parameter can be set true to return only spots with a SIG, regardless of what it is, so long as it's not blank. This is what Field Spotter uses to exclude generic cluster spots and only retrieve xOTA things."
description: "Limit the spots to only ones with a Special Interest Group such as POTA. Because supplying all known SIGs as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots with *no* SIG, this parameter can be set true to return only spots with a SIG, regardless of what it is, so long as it's not blank. This is the equivalent of supplying the `sig` query param with a list of every known SIG apart from the special `NO_SIG` value. This is what Field Spotter uses to exclude generic cluster spots and only retrieve xOTA things."
required: false
schema:
type: boolean
@@ -114,96 +81,31 @@ paths:
description: "Limit the spots to only ones from one or more bands. To select more than one band, supply a comma-separated list."
required: false
schema:
type: string
enum:
- 160m
- 80m
- 60m
- 40m
- 30m
- 20m
- 17m
- 15m
- 12m
- 10m
- 6m
- 4m
- 2m
- 70cm
- 23cm
- 13cm
$ref: "#/components/schemas/BandName"
- name: mode
in: query
description: "Limit the spots to only ones from one or more modes. To select more than one mode, supply a comma-separated list."
required: false
schema:
type: string
enum:
- CW
- PHONE
- SSB
- USB
- LSB
- AM
- FM
- DV
- DMR
- DSTAR
- C4FM
- M17
- DIGI
- DATA
- FT8
- FT4
- RTTY
- SSTV
- JS8
- HELL
- BPSK
- PSK
- BPSK31
- OLIVIA
- MFSK
- MFSK32
- PKT
$ref: "#/components/schemas/Mode"
- name: mode_type
in: query
description: "Limit the spots to only ones from one or more mode families. To select more than one mode family, supply a comma-separated list."
required: false
schema:
type: string
enum:
- CW
- PHONE
- DATA
$ref: "#/components/schemas/Mode"
- name: dx_continent
in: query
description: "Limit the spots to only ones where the DX (the operator being spotted) is on the given continent(s). To select more than one continent, supply a comma-separated list."
required: false
schema:
type: string
enum:
- EU
- NA
- SA
- AS
- AF
- OC
- AN
$ref: "#/components/schemas/Continent"
- name: de_continent
in: query
description: "Limit the spots to only ones where the spotteris on the given continent(s). To select more than one continent, supply a comma-separated list."
required: false
schema:
type: string
enum:
- EU
- NA
- SA
- AS
- AF
- OC
- AN
$ref: "#/components/schemas/Continent"
- name: dedupe
in: query
description: "\"De-duplicate\" the spots, returning only the latest spot for any given callsign."
@@ -285,60 +187,19 @@ paths:
description: "Limit the alerts to only ones from one or more sources. To select more than one source, supply a comma-separated list."
required: false
schema:
type: string
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- BOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
$ref: "#/components/schemas/Source"
- name: sig
in: query
description: "Limit the alerts to only ones from one or more Special Interest Groups. To select more than one SIG, supply a comma-separated list."
description: "Limit the alerts to only ones from one or more Special Interest Groups. To select more than one SIG, supply a comma-separated list. The special value 'NO_SIG' can be included to return alerts specifically without an associated SIG (i.e. general DXpeditions)."
required: false
schema:
type: string
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- WCA
- MOTA
- SIOTA
- ARLHS
- ILLW
- ZLOTA
- IOTA
- WOTA
- BOTA
- WAB
- WAI
$ref: "#/components/schemas/SIGNameIncludingNoSIG"
- name: dx_continent
in: query
description: "Limit the alerts to only ones where the DX operator is on the given continent(s). To select more than one continent, supply a comma-separated list."
required: false
schema:
type: string
enum:
- EU
- NA
- SA
- AS
- AF
- OC
- AN
$ref: "#/components/schemas/Continent"
- name: dx_call_includes
in: query
description: "Limit the alerts to only ones where the DX callsign includes the supplied string (case-insensitive). Generally a complete callsign, but you can supply a shorter string for partial matches."
@@ -438,7 +299,7 @@ paths:
tags:
- General
summary: Get enumeration options
description: Retrieves the list of options for various enumerated types, which can be found in the spots and also provided back to the API as query parameters. While these enumerated options are defined in this spec anyway, providing them in an API call allows us to define extra parameters, like the colours associated with bands, and also allows clients to set up their filters and features without having to have internal knowledge about, for example, what bands the server knows about.
description: Retrieves the list of options for various enumerated types, which can be found in the spots and also provided back to the API as query parameters. While these enumerated options are defined in this spec anyway, providing them in an API call allows us to define extra parameters, like the colours associated with bands, and also allows clients to set up their filters and features without having to have internal knowledge about, for example, what bands the server knows about. The call also returns a variety of other parameters that may be of use to a web UI, including the contents of the "web-ui-options" config section, which provides guidance for web UI implementations such as the built-in one on sensible configuration options such as the number of spots/alerts to retrieve, or the maximum age of spots to retrieve.
operationId: options
responses:
'200':
@@ -490,6 +351,39 @@ paths:
type: boolean
description: Whether the POST /spot call, to add spots to the server directly via its API, is permitted on this server.
example: true
web-ui-options:
type: object
properties:
spot-count:
type: array
description: An array of suggested "spot counts" that the web UI can retrieve from the API
items:
type: integer
example: 50
spot-count-default:
type: integer
example: 50
description: The suggested default "spot count" that the web UI should retrieve from the API
max-spot-age:
type: array
description: An array of suggested "maximum spot ages" that the web UI can retrieve from the API
items:
type: integer
example: 30
max-spot-age-default:
type: integer
example: 30
description: The suggested default "maximum spot age" that the web UI should retrieve from the API
alert-count:
type: array
description: An array of suggested "alert counts" that the web UI can retrieve from the API
items:
type: integer
example: 100
alert-count-default:
type: integer
example: 100
description: The suggested default "alert count" that the web UI should retrieve from the API
/lookup/call:
@@ -535,17 +429,8 @@ paths:
description: Country flag of the operator. This is limited to the range of emoji flags. For some DXCCs there may not be an official emoji flag, e.g. Northern Ireland, so the appearance may vary depending on your browser and operating system. Some small islands may also have no flag. Many DXCCs may also share a flag, e.g. mainland Spain, Balearic Islands, etc.
example: ""
continent:
type: string
description: Continent of the operator
enum:
- EU
- NA
- SA
- AS
- AF
- OC
- AN
example: EU
$ref: "#/components/schemas/Continent"
dxcc_id:
type: integer
description: DXCC ID of the operator
@@ -571,13 +456,8 @@ paths:
description: Longitude of the opertor's QTH, in degrees. This could be from an online lookup service, or just based on the DXCC.
example: -1.2345
location_source:
type: string
description: Where we got the location (grid/latitude/longitude) from. Unlike a spot where we might have a summit position or WAB square, here the only options are an online QTH lookup, or a location based purely on DXCC, or nothing.
enum:
- "HOME QTH"
- DXCC
- NONE
example: "HOME QTH"
$ref: "#/components/schemas/LocationSourceForAlert"
'422':
description: Validation error e.g. callsign missing or format incorrect
content:
@@ -599,26 +479,7 @@ paths:
in: query
description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
required: true
type: string
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- WCA
- MOTA
- SIOTA
- ARLHS
- ILLW
- ZLOTA
- IOTA
- WOTA
- BOTA
- WAB
- WAI
example: POTA
$ref: "#/components/schemas/SIGName"
- name: id
in: query
description: ID of a reference in that SIG
@@ -688,6 +549,184 @@ paths:
components:
schemas:
Source:
type: string
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
example: POTA
SIGName:
type: string
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- WCA
- MOTA
- SIOTA
- ARLHS
- ILLW
- ZLOTA
- KRMNPA
- IOTA
- WOTA
- BOTA
- WAB
- WAI
- TOTA
example: POTA
SIGNameIncludingNoSIG:
type: string
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- WCA
- MOTA
- SIOTA
- ARLHS
- ILLW
- ZLOTA
- KRMNPA
- IOTA
- WOTA
- BOTA
- WAB
- WAI
- TOTA
- NO_SIG
example: POTA
Continent:
type: string
enum:
- EU
- NA
- SA
- AS
- AF
- OC
- AN
example: EU
BandName:
type: string
enum:
- 2200m
- 600m
- 160m
- 80m
- 60m
- 40m
- 30m
- 20m
- 17m
- 15m
- 12m
- 11m
- 10m
- 6m
- 5m
- 4m
- 2m
- 1.25m
- 70cm
- 23cm
- 2.4GHz
- 5.8GHz
- 10GHz
- 24GHz
- 47GHz
- 76GHz
example: 40m
Mode:
type: string
enum:
- CW
- PHONE
- SSB
- USB
- LSB
- AM
- FM
- DV
- DMR
- DSTAR
- C4FM
- M17
- DIGI
- DATA
- FT8
- FT4
- RTTY
- SSTV
- JS8
- HELL
- BPSK
- PSK
- BPSK31
- OLIVIA
- MFSK
- MFSK32
- PKT
example: SSB
ModeType:
type: string
enum:
- CW
- PHONE
- DATA
example: CW
ModeSource:
type: string
enum:
- SPOT
- COMMENT
- BANDPLAN
- NONE
example: SPOT
LocationSourceForSpot:
type: string
enum:
- SPOT
- "SIG REF LOOKUP"
- "WAB/WAI GRID"
- "HOME QTH"
- DXCC
- NONE
example: SPOT
LocationSourceForAlert:
type: string
enum:
- "HOME QTH"
- DXCC
- NONE
example: "HOME QTH"
SIGRef:
type: object
properties:
@@ -696,27 +735,8 @@ components:
description: SIG reference ID.
example: GB-0001
sig:
type: string
description: SIG that this reference is in.
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- WCA
- MOTA
- SIOTA
- ARLHS
- ILLW
- ZLOTA
- IOTA
- WOTA
- BOTA
- WAB
- WAI
example: POTA
$ref: "#/components/schemas/SIGName"
name:
type: string
description: SIG reference name
@@ -766,17 +786,8 @@ components:
description: Country flag of the DX operator. This is limited to the range of emoji flags. For some DXCCs there may not be an official emoji flag, e.g. Northern Ireland, so the appearance may vary depending on your browser and operating system. Some small islands may also have no flag. Many DXCCs may also share a flag, e.g. mainland Spain, Balearic Islands, etc.
example: ""
dx_continent:
type: string
description: Continent of the DX operator
enum:
- EU
- NA
- SA
- AS
- AF
- OC
- AN
example: EU
$ref: "#/components/schemas/Continent"
dx_dxcc_id:
type: integer
description: DXCC ID of the DX operator
@@ -806,16 +817,8 @@ components:
description: Longitude of the DX spot, in degrees. This could be from a geographical reference e.g. POTA, or from a QRZ lookup
example: -1.2345
dx_location_source:
type: string
description: Where we got the DX location (grid/latitude/longitude) from. If this was from the spot itself, or from a lookup of the SIG ref (e.g. park) it's likely quite accurate, but if we had to fall back to QRZ lookup, or even a location based on the DXCC itself, it will be a lot less accurate.
enum:
- SPOT
- "SIG REF LOOKUP"
- "WAB/WAI GRID"
- "HOME QTH"
- DXCC
- NONE
example: SPOT
$ref: "#/components/schemas/LocationSourceForSpot"
dx_location_good:
type: boolean
description: Does the software think the location is good enough to put a marker on a map? This is true if the source is "SPOT", "SIG REF LOOKUP" or "WAB/WAI GRID", or alternatively if the source is "HOME QTH" and the callsign doesn't have a slash in it (i.e. operator likely at home).
@@ -833,17 +836,8 @@ components:
description: Country flag of the spotter. This is limited to the range of emoji flags. For some DXCCs there may not be an official emoji flag, e.g. Northern Ireland, so the appearance may vary depending on your browser and operating system. Some small islands may also have no flag. Many DXCCs may also share a flag, e.g. mainland Spain, Balearic Islands, etc.
example: ""
de_continent:
type: string
enum:
- EU
- NA
- SA
- AS
- AF
- OC
- AN
description: Continent of the spotter
example: EU
$ref: "#/components/schemas/Continent"
de_dxcc_id:
type: integer
description: DXCC ID of the spotter
@@ -865,79 +859,22 @@ components:
description: Longitude of the DX spotspotter, in degrees. This is not going to be from a xOTA reference so it will likely just be a QRZ or DXCC lookup. If the spotter is also portable, this is probably wrong, but it's good enough for some simple mapping.
example: -1.2345
mode:
type: string
description: Reported mode.
enum:
- CW
- PHONE
- SSB
- USB
- LSB
- AM
- FM
- DV
- DMR
- DSTAR
- C4FM
- M17
- DIGI
- DATA
- FT8
- FT4
- RTTY
- SSTV
- JS8
- HELL
- BPSK
- PSK
- BPSK31
- OLIVIA
- MFSK
- MFSK32
- PKT
$ref: "#/components/schemas/Mode"
example: SSB
mode_type:
type: string
description: Inferred mode "family".
enum:
- CW
- PHONE
- DATA
example: PHONE
$ref: "#/components/schemas/ModeType"
mode_source:
type: string
description: Where we got the mode from. If this was from the spot itself, it's likely quite accurate, but if we had to fall back to the bandplan, it might not be correct.
enum:
- SPOT
- COMMENT
- BANDPLAN
- NONE
$ref: "#/components/schemas/ModeSource"
freq:
type: number
description: Frequency, in Hz
example: 7150500
band:
type: string
description: Band, defined by the frequency.
enum:
- 160m
- 80m
- 60m
- 40m
- 30m
- 20m
- 17m
- 15m
- 12m
- 10m
- 6m
- 4m
- 2m
- 70cm
- 23cm
- 13cm
- Unknown
example: 40m
$ref: "#/components/schemas/BandName"
time:
type: number
description: Time of the spot, UTC seconds since UNIX epoch
@@ -959,27 +896,8 @@ components:
description: Comment left by the spotter, if any
example: "59 in NY 73"
sig:
type: string
description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- WCA
- MOTA
- SIOTA
- ARLHS
- ILLW
- ZLOTA
- IOTA
- WOTA
- BOTA
- WAB
- WAI
example: POTA
$ref: "#/components/schemas/SIGName"
sig_refs:
type: array
items:
@@ -996,7 +914,7 @@ components:
band_color:
type: string
descripton: Colour to represent this spot, if a client chooses to colour spots based on their frequency band, using PSK Reporter's default colours. HTML colour e.g. hex.
example: #ff0000"
example: "#ff0000"
band_contrast_color:
type: string
descripton: Black or white, whichever best contrasts with "band_color".
@@ -1006,23 +924,8 @@ components:
description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
example: false
source:
type: string
description: Where we got the spot from.
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
example: POTA
$ref: "#/components/schemas/Source"
source_id:
type: string
description: The ID the source gave it, if any.
@@ -1057,17 +960,8 @@ components:
description: Country flag of the DX operator. This is limited to the range of emoji flags. For some DXCCs there may not be an official emoji flag, e.g. Northern Ireland, so the appearance may vary depending on your browser and operating system. Some small islands may also have no flag. Many DXCCs may also share a flag, e.g. mainland Spain, Balearic Islands, etc.
example: ""
dx_continent:
type: string
description: Continent of the DX operator
enum:
- EU
- NA
- SA
- AS
- AF
- OC
- AN
example: EU
$ref: "#/components/schemas/Continent"
dx_dxcc_id:
type: integer
description: DXCC ID of the DX operator
@@ -1113,27 +1007,8 @@ components:
description: Comment made by the activator, if any
example: "2025 DXpedition to null island"
sig:
type: string
description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- WCA
- MOTA
- SIOTA
- ARLHS
- ILLW
- ZLOTA
- IOTA
- WOTA
- BOTA
- WAB
- WAI
example: POTA
$ref: "#/components/schemas/SIGName"
sig_refs:
type: array
items:
@@ -1150,22 +1025,7 @@ components:
source:
type: string
description: Where we got the alert from.
enum:
- POTA
- SOTA
- WWFF
- WWBOTA
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- BOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
example: POTA
$ref: "#/components/schemas/Source"
source_id:
type: string
description: The ID the source gave it, if any.
@@ -1175,9 +1035,8 @@ components:
type: object
properties:
name:
type: string
description: The name of the provider.
example: POTA
$ref: "#/components/schemas/Source"
enabled:
type: boolean
description: Whether the provider is enabled or not.
@@ -1199,9 +1058,8 @@ components:
type: object
properties:
name:
type: string
description: The name of the provider.
example: POTA
$ref: "#/components/schemas/Source"
enabled:
type: boolean
description: Whether the provider is enabled or not.
@@ -1219,9 +1077,8 @@ components:
type: object
properties:
name:
type: string
description: The name of the band
example: 40m
$ref: "#/components/schemas/BandName"
start_freq:
type: int
description: The start frequency of this band, in Hz.
@@ -1243,9 +1100,8 @@ components:
type: object
properties:
name:
type: string
description: The abbreviated name of the SIG
example: POTA
$ref: "#/components/schemas/SIGName"
description:
type: string
description: The full name of the SIG

BIN
webassets/img/flags/999.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -1,7 +1,7 @@
import os
from datetime import timedelta
from PIL import Image, ImageDraw, ImageFont
from PIL import Image, ImageDraw, ImageFont
from requests_cache import CachedSession
test = os.listdir("./")
@@ -19,4 +19,7 @@ for dxcc in data["dxcc"]:
draw = ImageDraw.Draw(image)
draw.text((0, -10), flag, font=ImageFont.truetype("/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", 109), embedded_color=True)
outfile = str(id) + ".png"
image.save(outfile, "PNG")
image.save(outfile, "PNG")
image = Image.new("RGBA", (140, 110), (255, 0, 0, 0))
image.save("999.png", "PNG")

View File

@@ -285,6 +285,13 @@ function loadOptions() {
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#source-options", "source", options["alert_sources"]);
// Populate the Display panel
options["web-ui-options"]["alert-count"].forEach(sc => $("#alerts-to-fetch").append($('<option>', {
value: sc,
text: sc
})));
$("#alerts-to-fetch").val(options["web-ui-options"]["alert-count-default"]);
// Load filters from settings storage
loadSettings();

View File

@@ -23,7 +23,7 @@ function loadSpots() {
// 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 => {
["dx_continent", "de_continent", "mode_type", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
@@ -230,11 +230,19 @@ function loadOptions() {
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateSIGsMultiToggleFilterCard(options["sigs"]);
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
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 settings from settings storage now all the controls are available
loadSettings();

View File

@@ -17,7 +17,7 @@ function loadSpots() {
// 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 => {
["dx_continent", "de_continent", "mode_type", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
@@ -158,11 +158,19 @@ function loadOptions() {
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateSIGsMultiToggleFilterCard(options["sigs"]);
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
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 settings from settings storage now all the controls are available
loadSettings();

View File

@@ -14,7 +14,7 @@ function loadSpots() {
// 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 => {
["dx_continent", "de_continent", "mode_type", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
@@ -282,11 +282,19 @@ function loadOptions() {
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateSIGsMultiToggleFilterCard(options["sigs"]);console.log(options["sigs"]);
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
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 settings from settings storage now all the controls are available
loadSettings();

View File

@@ -30,6 +30,18 @@ function generateBandsMultiToggleFilterCard(band_options) {
$("#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>&nbsp;<button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', false);">None</button></span>`);
}
// Generate SIGs filter card. This one is also a special case.
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> `);
});
// 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> `);
// Create All/None buttons
$("#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>`);
}
// Method called when any filter is changed to reload the spots and persist the filter settings.
function filtersUpdated() {
loadSpots();