From d1df7726494a811b95cf1ad8d3f80a049e5af623 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Thu, 25 Jun 2026 19:25:05 +0100 Subject: [PATCH] Disambiguation between Towers and Toilets on the Air --- config-example.yml | 4 +-- core/constants.py | 46 +++++++++++++------------- core/sig_utils.py | 17 +++++++--- data/sig.py | 18 +++++++--- data/spot.py | 4 +-- server/handlers/api/lookups.py | 2 +- spotproviders/{wwtota.py => towers.py} | 2 +- templates/add_spot.html | 2 +- templates/alerts.html | 2 +- templates/bands.html | 4 +-- templates/base.html | 10 +++--- templates/conditions.html | 2 +- templates/map.html | 4 +-- templates/spots.html | 4 +-- templates/status.html | 2 +- webassets/apidocs/openapi.yml | 23 ++++++++++--- webassets/js/ui-ham.js | 4 +-- 17 files changed, 90 insertions(+), 60 deletions(-) rename spotproviders/{wwtota.py => towers.py} (98%) diff --git a/config-example.yml b/config-example.yml index 9f70b0a..3cb1822 100644 --- a/config-example.yml +++ b/config-example.yml @@ -66,8 +66,8 @@ spot-providers: name: "LLOTA" enabled: true - - class: "WWTOTA" - name: "WWTOTA" + - class: "Towers" + name: "Towers" enabled: true - class: "Tiles" diff --git a/core/constants.py b/core/constants.py index d763175..b2d844a 100644 --- a/core/constants.py +++ b/core/constants.py @@ -11,29 +11,29 @@ HAMQTH_PRG = ("Spothole v" + SOFTWARE_VERSION + " operated by " + SERVER_OWNER_C # Special Interest Groups SIGS = [ - SIG(name="POTA", description="Parks on the Air", ref_regex=r"[A-Z]{2}\-\d{4,5}"), - SIG(name="SOTA", description="Summits on the Air", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"), - SIG(name="WWFF", description="World Wide Flora & Fauna", ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}"), - SIG(name="GMA", description="Global Mountain Activity", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"), - SIG(name="WWBOTA", description="Worldwide Bunkers on the Air", ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}"), - SIG(name="HEMA", description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"), - SIG(name="IOTA", description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}"), - SIG(name="MOTA", description="Mills on the Air", ref_regex=r"X\d{4,6}"), - SIG(name="ARLHS", description="Amateur Radio Lighthouse Society", ref_regex=r"[A-Z]{3}\-\d{3,4}"), - SIG(name="ILLW", description="International Lighthouse & Lightship Weekend", ref_regex=r"[A-Z]{2}\d{4}"), - SIG(name="SIOTA", description="Silos on the Air", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"), - SIG(name="WCA", description="World Castles Award", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}"), - SIG(name="ZLOTA", description="New Zealand on the Air", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}"), - SIG(name="WOTA", description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"), - SIG(name="BOTA", description="Beaches on the Air"), - SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award"), - SIG(name="LLOTA", description="Lagos y Lagunas on the Air", ref_regex=r"[A-Z]{2}\-\d{4}"), - SIG(name="WWTOTA", description="Towers on the Air", ref_regex=r"[A-Z]{2}R\-\d{4}"), - SIG(name="Tiles", description="Tiles on the Air", ref_regex=r"[A-Za-z]{2}[0-9]{2}[A-Za-z]{2}"), - SIG(name="WAB", description="Worked All Britain", ref_regex=r"[A-Z]{1,2}[0-9]{2}"), - SIG(name="WAI", description="Worked All Ireland", ref_regex=r"[A-Z][0-9]{2}"), - SIG(name="DME", description="Diplomas de Municipios Españoles", ref_regex=r"\d{4,5}"), - SIG(name="TOTA", description="Toilets on the Air", ref_regex=r"T\-[0-9]{2}") + SIG(name="POTA", comment_names=["POTA"], description="Parks on the Air", ref_regex=r"[A-Z]{2}\-\d{4,5}"), + SIG(name="SOTA", comment_names=["SOTA"], description="Summits on the Air", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"), + SIG(name="WWFF", comment_names=["WWFF"], description="World Wide Flora & Fauna", ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}"), + SIG(name="GMA", comment_names=["GMA"], description="Global Mountain Activity", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"), + SIG(name="WWBOTA", comment_names=["WWBOTA", "BOTA"], description="Worldwide Bunkers on the Air", ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}"), + SIG(name="HEMA", comment_names=["HEMA"], description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"), + SIG(name="IOTA", comment_names=["IOTA"], description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}"), + SIG(name="MOTA", comment_names=["MOTA"], description="Mills on the Air", ref_regex=r"X\d{4,6}"), + SIG(name="ARLHS", comment_names=["ARLHS"], description="Amateur Radio Lighthouse Society", ref_regex=r"[A-Z]{3}\-\d{3,4}"), + SIG(name="ILLW", comment_names=["ILLW"], description="International Lighthouse & Lightship Weekend", ref_regex=r"[A-Z]{2}\d{4}"), + SIG(name="SiOTA", comment_names=["SIOTA"], description="Silos on the Air", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"), + SIG(name="WCA", comment_names=["WCA"], description="World Castles Award", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}"), + SIG(name="ZLOTA", comment_names=["ZLOTA"], description="New Zealand on the Air", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}"), + SIG(name="WOTA", comment_names=["WOTA"], description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"), + SIG(name="BOTA", comment_names=[], description="Beaches on the Air"), + SIG(name="KRMNPA", comment_names=["KRMNPA"], description="Keith Roget Memorial National Parks Award"), + SIG(name="LLOTA", comment_names=["LLOTA"], description="Lagos y Lagunas on the Air", ref_regex=r"[A-Z]{2}\-\d{4}"), + SIG(name="Towers", comment_names=["TOTA"], description="Towers on the Air", ref_regex=r"[A-Z]{2}R\-\d{4}"), + SIG(name="Tiles", comment_names=[], description="Tiles on the Air", ref_regex=r"[A-Za-z]{2}[0-9]{2}[A-Za-z]{2}"), + SIG(name="WAB", comment_names=["WAB"], description="Worked All Britain", ref_regex=r"[A-Z]{1,2}[0-9]{2}"), + SIG(name="WAI", comment_names=["WAI"], description="Worked All Ireland", ref_regex=r"[A-Z][0-9]{2}"), + SIG(name="DME", comment_names=["DME"], description="Diplomas de Municipios Españoles", ref_regex=r"\d{4,5}"), + SIG(name="Toilets", comment_names=[], description="Toilets on the Air", ref_regex=r"T\-[0-9]{2}") ] # Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA". diff --git a/core/sig_utils.py b/core/sig_utils.py index 8c3b899..db180dc 100644 --- a/core/sig_utils.py +++ b/core/sig_utils.py @@ -22,6 +22,16 @@ def get_ref_regex_for_sig(sig): return None +def get_sig_name_from_comment_name(sig): + """Utility function to get the name of a SIG from its "comment name". Generally these will be the same but there are + some cases (e.g. is "TOTA" Towers, Tiles or Toilets?) where we need to transform one to the other.""" + + for s in SIGS: + if any(n.upper() == sig.upper() for n in s.comment_names): + return s.name + return None + + def populate_sig_ref_info(sig_ref): """Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig_ref object which must at minimum have a "sig" and an "id". The rest of the object will be populated and returned. @@ -210,8 +220,5 @@ def populate_sig_ref_info(sig_ref): return sig_ref -# Regex matching any SIG -ANY_SIG_REGEX = r"(" + r"|".join(list(map(lambda p: p.name, SIGS))) + r")" - -# Regex matching any SIG reference -ANY_XOTA_SIG_REF_REGEX = r"[\w\/]+\-\d+" +# Regex matching any SIG's "comment name", i.e. how it may be referred to in spot comments +ANY_SIG_REGEX = r"(" + r"|".join(n for s in SIGS for n in s.comment_names) + r")" diff --git a/data/sig.py b/data/sig.py index 7ec81c6..de6aedf 100644 --- a/data/sig.py +++ b/data/sig.py @@ -1,13 +1,21 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field @dataclass class SIG: - """Data class that defines a Special Interest Group.""" + """Data class that defines a Special Interest Group. Each contains a name and a longer form description. + They also contain comment_names which attempts to separate out the way people might refer to it in + cluster comments from how it is referred to in the UI & API. (For example, "TOTA" in cluster spot comments + almost always means Towers on the Air, but no single programme is referred to in the UI as "TOTA" as + it's ambiguous between Towers, Toilets and Tiles. And while Beaches got the name "BOTA" first, "BOTA" spots + are much more likely to be bunkers.) Finally, there is a ref_regex which provides a regular expression to + match what references (such as parks and summits) look like for that programme.""" - # SIG name, e.g. "POTA" + # SIG name as used in the UI and API, e.g. "Towers" name: str - # Description, e.g. "Parks on the Air" + # Description, e.g. "Towers on the Air" description: str + # SIG names as they might appear in cluster spot comments, e.g. ["TOTA"] + comment_names: list[str] = field(default_factory=list) # Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+". - ref_regex: str = None + ref_regex: str | None = None diff --git a/data/spot.py b/data/spot.py index 59974ae..92c3cf0 100644 --- a/data/spot.py +++ b/data/spot.py @@ -14,7 +14,7 @@ from core.constants import MODE_ALIASES, PROPAGATION_MODES from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone from core.lookup_helper import lookup_helper, infer_band_from_freq, infer_mode_from_comment, \ infer_mode_from_frequency, infer_mode_type_from_mode -from core.sig_utils import populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig +from core.sig_utils import populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig, get_sig_name_from_comment_name from data.sig_ref import SIGRef @@ -255,7 +255,7 @@ class Spot: for sig_match in sig_matches: # First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things like cluster # spots where the comment is just "POTA". - found_sig = sig_match.group(2).upper() + found_sig = get_sig_name_from_comment_name(sig_match.group(2)) if not self.sig: self.sig = found_sig diff --git a/server/handlers/api/lookups.py b/server/handlers/api/lookups.py index e885c07..0fb7e21 100644 --- a/server/handlers/api/lookups.py +++ b/server/handlers/api/lookups.py @@ -111,7 +111,7 @@ class APILookupSIGRefHandler(tornado.web.RequestHandler): if "sig" in query_params.keys() and "id" in query_params.keys(): sig = str(query_params.get("sig")).upper() ref_id = str(query_params.get("id")).upper() - if sig in list(map(lambda p: p.name, SIGS)): + if sig in list(map(lambda p: p.name.upper(), SIGS)): if not get_ref_regex_for_sig(sig) or re.match(get_ref_regex_for_sig(sig), ref_id): data = populate_sig_ref_info(SIGRef(id=ref_id, sig=sig)) self.write(json.dumps(data, default=serialize_everything)) diff --git a/spotproviders/wwtota.py b/spotproviders/towers.py similarity index 98% rename from spotproviders/wwtota.py rename to spotproviders/towers.py index 61d00bd..4f337ed 100644 --- a/spotproviders/wwtota.py +++ b/spotproviders/towers.py @@ -6,7 +6,7 @@ from data.spot import Spot from spotproviders.http_spot_provider import HTTPSpotProvider -class WWTOTA(HTTPSpotProvider): +class Towers(HTTPSpotProvider): """Spot provider for Towers on the Air""" POLL_INTERVAL_SEC = 120 diff --git a/templates/add_spot.html b/templates/add_spot.html index f0b0893..0b6bfe9 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -76,7 +76,7 @@ - + diff --git a/templates/alerts.html b/templates/alerts.html index 7d3f4ad..f3720ca 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -75,7 +75,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index d81d170..758c822 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -77,8 +77,8 @@ - - + + diff --git a/templates/base.html b/templates/base.html index b044ec7..375148b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -10,10 +10,10 @@ - - - - + + + + {% end %} {% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html index bc36299..3aea838 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -284,7 +284,7 @@
- + diff --git a/templates/map.html b/templates/map.html index b5ddee4..a21d7bf 100644 --- a/templates/map.html +++ b/templates/map.html @@ -95,8 +95,8 @@ - - + + diff --git a/templates/spots.html b/templates/spots.html index 933c1a5..4b26010 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -116,8 +116,8 @@ - - + + diff --git a/templates/status.html b/templates/status.html index 6ed1759..3e8cfd9 100644 --- a/templates/status.html +++ b/templates/status.html @@ -59,7 +59,7 @@ - +