Disambiguation between Towers and Toilets on the Air

This commit is contained in:
Ian Renton
2026-06-25 19:25:05 +01:00
parent 215b61593b
commit d1df772649
17 changed files with 90 additions and 60 deletions

View File

@@ -66,8 +66,8 @@ spot-providers:
name: "LLOTA"
enabled: true
- class: "WWTOTA"
name: "WWTOTA"
- class: "Towers"
name: "Towers"
enabled: true
- class: "Tiles"

View File

@@ -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".

View File

@@ -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")"

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -76,7 +76,7 @@
</div>
<script src="/js/add-spot.js?v=1782239783"></script>
<script src="/js/add-spot.js?v=1782411905"></script>
<script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script>

View File

@@ -75,7 +75,7 @@
</div>
<script src="/js/alerts.js?v=1782239783"></script>
<script src="/js/alerts.js?v=1782411905"></script>
<script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script>

View File

@@ -77,8 +77,8 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/spotsbandsandmap.js?v=1782239783"></script>
<script src="/js/bands.js?v=1782239783"></script>
<script src="/js/spotsbandsandmap.js?v=1782411905"></script>
<script src="/js/bands.js?v=1782411905"></script>
<script>$(document).ready(function () {
$("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script>

View File

@@ -1,6 +1,6 @@
{% extends "skeleton.html" %}
{% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1782239783" type="text/css">
<link rel="stylesheet" href="/css/style.css?v=1782411905" type="text/css">
<link href="/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -10,10 +10,10 @@
<script src="/vendor/js/bootstrap-5.3.8.bundle.min.js"></script>
<script src="/vendor/js/tinycolor2-1.6.0.min.js"></script>
<script src="/js/utils.js?v=1782239783"></script>
<script src="/js/ui-ham.js?v=1782239783"></script>
<script src="/js/geo.js?v=1782239783"></script>
<script src="/js/common.js?v=1782239783"></script>
<script src="/js/utils.js?v=1782411905"></script>
<script src="/js/ui-ham.js?v=1782411905"></script>
<script src="/js/geo.js?v=1782411905"></script>
<script src="/js/common.js?v=1782411905"></script>
{% end %}
{% block body %}
<div class="container">

View File

@@ -284,7 +284,7 @@
</div>
<script src="/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/js/conditions.js?v=1782239783"></script>
<script src="/js/conditions.js?v=1782411905"></script>
<script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script>

View File

@@ -95,8 +95,8 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/spotsbandsandmap.js?v=1782239783"></script>
<script src="/js/map.js?v=1782239783"></script>
<script src="/js/spotsbandsandmap.js?v=1782411905"></script>
<script src="/js/map.js?v=1782411905"></script>
<script>$(document).ready(function () {
$("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script>

View File

@@ -116,8 +116,8 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/spotsbandsandmap.js?v=1782239783"></script>
<script src="/js/spots.js?v=1782239783"></script>
<script src="/js/spotsbandsandmap.js?v=1782411905"></script>
<script src="/js/spots.js?v=1782411905"></script>
<script>$(document).ready(function () {
$("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script>

View File

@@ -59,7 +59,7 @@
</div>
</div>
<script src="/js/status.js?v=1782239783"></script>
<script src="/js/status.js?v=1782411905"></script>
<script>
$(document).ready(function () {
$("#nav-link-status").addClass("active");

View File

@@ -18,6 +18,11 @@ info:
### 1.4
* Spots can now include a "propagation_mode" field, and the `/options` call enumerates the options that can have.
* Added support for the Diploma de Municipios Españoles (DME) SIG
* Renamed some SIGs to avoid confusion between Towers, Tiles and Toilets
* Added `comment_names` to SIGs in the `/options`, to reflect how they might be referred to in spot comments where
it differs from their `name`.
* Added `propagation_mode` field to spots
### 1.3
@@ -771,7 +776,7 @@ components:
- ZLOTA
- WOTA
- LLOTA
- WWTOTA
- Towers
- Tiles
- Cluster
- RBN
@@ -790,7 +795,7 @@ components:
- HEMA
- WCA
- MOTA
- SIOTA
- SiOTA
- ARLHS
- ILLW
- ZLOTA
@@ -799,12 +804,12 @@ components:
- WOTA
- BOTA
- LLOTA
- WWTOTA
- Towers
- Tiles
- WAB
- WAI
- DME
- TOTA
- Toilets
example: POTA
SIGNameIncludingNoSIG:
@@ -1374,6 +1379,16 @@ components:
type: string
description: The full name of the SIG
example: Parks on the Air
comment_names:
type: array
description: >
Names by which this SIG may be referred to in cluster spot comments. Most SIGs have a
single entry matching their programme name (e.g. ["POTA"]), but some have none (where
the name is ambiguous with other SIGs, such as Tiles and Toilets on the Air) or multiple
entries (e.g. WWBOTA accepts both "WWBOTA" and "BOTA" since the latter is often used).
items:
type: string
example: [ "TOTA" ]
ref_regex:
type: string
description: >

View File

@@ -332,12 +332,12 @@ const SIG_ICONS = {
"BOTA": "fa-umbrella-beach",
"KRMNPA": "fa-earth-oceania",
"LLOTA": "fa-water",
"WWTOTA": "fa-tower-observation",
"Towers": "fa-tower-observation",
"WAB": "fa-table-cells-large",
"WAI": "fa-table-cells-large",
"DME": "fa-building",
"Tiles": "fa-square",
"TOTA": "fa-toilet"
"Toilets": "fa-toilet"
}
// Get the Font Awesome icon for a given SIG. If the SIG is unknown, the provided default symbol will be returned