mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-06-26 14:37:25 +00:00
Merge branch 'main' into 95-send-spots-to-xota
# Conflicts: # core/constants.py # data/spot.py
This commit is contained in:
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/datafiles/MUNICIPIOS.csv" charset="ISO-8859-1" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -19,6 +19,8 @@ Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), th
|
|||||||
SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, BOTA, LLOTA, WWTOTA, Tiles on the Air, the UK Packet
|
SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, BOTA, LLOTA, WWTOTA, Tiles on the Air, the UK Packet
|
||||||
Repeater Network, NG3K, and any site based on the xOTA software by nischu.
|
Repeater Network, NG3K, and any site based on the xOTA software by nischu.
|
||||||
|
|
||||||
|
Additional Special Interest Groups (SIGs) without their own specific data source include WAB, WAI and DME.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
@@ -515,7 +517,11 @@ The same approach as above is also used for alert providers.
|
|||||||
As well as being my work, I have also gratefully received feature patches from Steven, M1SDH.
|
As well as being my work, I have also gratefully received feature patches from Steven, M1SDH.
|
||||||
|
|
||||||
The project contains GeoJSON files for CQ and ITU zones, in the `/datafiles/` directory. These are MIT-licenced and, to
|
The project contains GeoJSON files for CQ and ITU zones, in the `/datafiles/` directory. These are MIT-licenced and, to
|
||||||
my knowledge, created by HA8TKS for his CQ and ITU zone layers for Leaflet.
|
my knowledge, created by HA8TKS for his CQ and ITU zone layers for Leaflet. `/datafiles` also contains a
|
||||||
|
`MUNICIPIOS.csv` file, from the "Nomenclátor Geográfico de Municipios y Entidades de Población" data set sourced from
|
||||||
|
[el Centro Nacional de Información Geográfica](https://centrodedescargas.cnig.es/CentroDescargas/home).
|
||||||
|
`didbase-stations.csv` and the TOTA CSV files were created by me based on publicly available data from GIRO and from
|
||||||
|
maps of conference centres.
|
||||||
|
|
||||||
The project contains a set of flag icons generated using the "Noto Color Emoji" font on a Debian system, in the
|
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.
|
`/webassets/img/flags/` directory.
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ spot-providers:
|
|||||||
name: "LLOTA"
|
name: "LLOTA"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
- class: "WWTOTA"
|
- class: "Towers"
|
||||||
name: "WWTOTA"
|
name: "Towers"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
- class: "Tiles"
|
- class: "Tiles"
|
||||||
|
|||||||
@@ -11,28 +11,29 @@ HAMQTH_PRG = ("Spothole v" + SOFTWARE_VERSION + " operated by " + SERVER_OWNER_C
|
|||||||
|
|
||||||
# Special Interest Groups
|
# Special Interest Groups
|
||||||
SIGS = [
|
SIGS = [
|
||||||
SIG(name="POTA", description="Parks on the Air", ref_regex=r"([A-Z]{2}\-\d{4,5}|K\-TEST)"),
|
SIG(name="POTA", comment_names=["POTA"], description="Parks on the Air", ref_regex=r"[A-Z]{2}\-\d{4,5}|K\-TEST"),
|
||||||
SIG(name="SOTA", description="Summits on the Air", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
|
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", description="World Wide Flora & Fauna", ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}"),
|
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", description="Global Mountain Activity", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
|
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", description="Worldwide Bunkers on the Air", ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}"),
|
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", description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"),
|
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", description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}"),
|
SIG(name="IOTA", comment_names=["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="MOTA", comment_names=["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="ARLHS", comment_names=["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="ILLW", comment_names=["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="SiOTA", comment_names=["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="WCA", comment_names=["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="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", description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
|
SIG(name="WOTA", comment_names=["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="BOTA", comment_names=[], description="Beaches on the Air"),
|
||||||
SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award"),
|
SIG(name="KRMNPA", comment_names=["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="LLOTA", comment_names=["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="Towers", comment_names=["TOTA"], 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="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", description="Worked All Britain", ref_regex=r"[A-Z]{1,2}[0-9]{2}"),
|
SIG(name="WAB", comment_names=["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="WAI", comment_names=["WAI"], description="Worked All Ireland", ref_regex=r"[A-Z][0-9]{2}"),
|
||||||
SIG(name="TOTA", description="Toilets on the Air", ref_regex=r"T\-[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".
|
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE
|
|||||||
from core.constants import SIGS, HTTP_HEADERS
|
from core.constants import SIGS, HTTP_HEADERS
|
||||||
from core.geo_utils import wab_wai_square_to_lat_lon
|
from core.geo_utils import wab_wai_square_to_lat_lon
|
||||||
|
|
||||||
|
# Load Spanish municipality data for the DME programme. There's no convenient lookup API for this, so we embed the data
|
||||||
|
# file in Spothole and load it on startup.
|
||||||
|
with open("datafiles/MUNICIPIOS.csv", encoding="latin-1") as _f:
|
||||||
|
_DME_INDEX = {row["COD_INE"][:5]: row for row in csv.DictReader(_f, delimiter=";")}
|
||||||
|
|
||||||
|
|
||||||
def get_ref_regex_for_sig(sig):
|
def get_ref_regex_for_sig(sig):
|
||||||
"""Utility function to get the regex string for a SIG reference for a named SIG. If no match is found, None will be returned."""
|
"""Utility function to get the regex string for a SIG reference for a named SIG. If no match is found, None will be returned."""
|
||||||
@@ -17,6 +22,16 @@ def get_ref_regex_for_sig(sig):
|
|||||||
return None
|
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):
|
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
|
"""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.
|
must at minimum have a "sig" and an "id". The rest of the object will be populated and returned.
|
||||||
@@ -188,13 +203,22 @@ def populate_sig_ref_info(sig_ref):
|
|||||||
sig_ref.longitude = ll[1]
|
sig_ref.longitude = ll[1]
|
||||||
except:
|
except:
|
||||||
logging.debug("Invalid lat/lon received for reference")
|
logging.debug("Invalid lat/lon received for reference")
|
||||||
|
elif sig.upper() == "DME":
|
||||||
|
# Zero-pad to 5 digits to match our source data
|
||||||
|
row = _DME_INDEX.get(ref_id.zfill(5))
|
||||||
|
if row:
|
||||||
|
sig_ref.name = row["NOMBRE_ACTUAL"] + ", " + row["PROVINCIA"]
|
||||||
|
sig_ref.latitude = float(row["LATITUD_ETRS89_REGCAN95"].replace(",", ".")) if row.get("LATITUD_ETRS89_REGCAN95") else None
|
||||||
|
sig_ref.longitude = float(row["LONGITUD_ETRS89_REGCAN95"].replace(",", ".")) if row.get("LONGITUD_ETRS89_REGCAN95") else None
|
||||||
|
if sig_ref.latitude and sig_ref.longitude:
|
||||||
|
try:
|
||||||
|
sig_ref.grid = latlong_to_locator(sig_ref.latitude, sig_ref.longitude, 6)
|
||||||
|
except Exception:
|
||||||
|
logging.debug("Invalid lat/lon received for reference")
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.warning("Failed to look up sig_ref info for " + sig + " ref " + ref_id, exc_info=True)
|
logging.warning("Failed to look up sig_ref info for " + sig + " ref " + ref_id, exc_info=True)
|
||||||
return sig_ref
|
return sig_ref
|
||||||
|
|
||||||
|
|
||||||
# Regex matching any SIG
|
# Regex matching any SIG's "comment name", i.e. how it may be referred to in spot comments
|
||||||
ANY_SIG_REGEX = r"(" + r"|".join(list(map(lambda p: p.name, SIGS))) + r")"
|
ANY_SIG_REGEX = r"(" + r"|".join(n for s in SIGS for n in s.comment_names) + r")"
|
||||||
|
|
||||||
# Regex matching any SIG reference
|
|
||||||
ANY_XOTA_SIG_REF_REGEX = r"[\w\/]+\-\d+"
|
|
||||||
|
|||||||
18
data/sig.py
18
data/sig.py
@@ -1,13 +1,21 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SIG:
|
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
|
name: str
|
||||||
# Description, e.g. "Parks on the Air"
|
# Description, e.g. "Towers on the Air"
|
||||||
description: str
|
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+".
|
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
|
||||||
ref_regex: str = None
|
ref_regex: str | None = None
|
||||||
|
|||||||
27
data/spot.py
27
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.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, \
|
from core.lookup_helper import lookup_helper, infer_band_from_freq, infer_mode_from_comment, \
|
||||||
infer_mode_from_frequency, infer_mode_type_from_mode
|
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
|
from data.sig_ref import SIGRef
|
||||||
|
|
||||||
|
|
||||||
@@ -253,16 +253,9 @@ class Spot:
|
|||||||
if self.comment:
|
if self.comment:
|
||||||
sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
||||||
for sig_match in sig_matches:
|
for sig_match in sig_matches:
|
||||||
# See what SIG we think this is
|
# First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things like cluster
|
||||||
found_sig = sig_match.group(2).upper()
|
|
||||||
|
|
||||||
# "TOTA" is now ambiguous, with Toilets and Towers both using it. If we have found "TOTA" in a comment,
|
|
||||||
# ignore it as we can't tell what it is.
|
|
||||||
if found_sig != "TOTA":
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Now, 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".
|
# spots where the comment is just "POTA".
|
||||||
|
found_sig = get_sig_name_from_comment_name(sig_match.group(2))
|
||||||
if not self.sig:
|
if not self.sig:
|
||||||
self.sig = found_sig
|
self.sig = found_sig
|
||||||
|
|
||||||
@@ -270,7 +263,7 @@ class Spot:
|
|||||||
# If so, add that to the sig_refs list for this spot.
|
# If so, add that to the sig_refs list for this spot.
|
||||||
ref_regex = get_ref_regex_for_sig(found_sig)
|
ref_regex = get_ref_regex_for_sig(found_sig)
|
||||||
if ref_regex:
|
if ref_regex:
|
||||||
ref_matches = re.finditer(r"(^|\W)" + found_sig + r"($|\W)(" + ref_regex + r")($|\W)", self.comment,
|
ref_matches = re.finditer(r"(^|\W)" + found_sig + r"([ -])(" + ref_regex + r")($|\W)", self.comment,
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
for ref_match in ref_matches:
|
for ref_match in ref_matches:
|
||||||
self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(3).upper(), sig=found_sig))
|
self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(3).upper(), sig=found_sig))
|
||||||
@@ -297,23 +290,23 @@ class Spot:
|
|||||||
if self.sig_refs and len(self.sig_refs) > 0 and not self.sig:
|
if self.sig_refs and len(self.sig_refs) > 0 and not self.sig:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.sig = self.sig_refs[0].sig
|
||||||
|
|
||||||
# Parse "de_grid<prop_mode>dx_grid" structures from the comment, e.g. "JN61ES<ES>JM56XT" or "JO02GQ<>KN17LG".
|
# Parse "de_grid<prop_mode>dx_grid" structures from the comment, e.g. "JN61ES(ES)JM56XT" or "JO02GQ<>KN17LG".
|
||||||
# These are common on cluster spots and can provide grid references in preference to e.g. QRZ lookup, as well as
|
# These are common on cluster spots and can provide grid references in preference to e.g. QRZ lookup, as well as
|
||||||
# being the only source we have for propagation mode.
|
# being the only source we have for propagation mode. Brace for nightmare regex from hell.
|
||||||
if self.comment:
|
if self.comment:
|
||||||
grid_mode_grid_match = re.search(
|
grid_mode_grid_match = re.search(
|
||||||
r'\b([A-Ra-r]{2}\d{2}(?:[A-Xa-x]{2}(?:\d{2})?)?)<([^>]*)>([A-Ra-r]{2}\d{2}(?:[A-Xa-x]{2}(?:\d{2})?)?)\b',
|
r'\b([A-Ra-r]{2}\d{2}(?:[A-Xa-x]{2}(?:\d{2})?)?)(?:<([^>]*)>|\(([^)]*)\))([A-Ra-r]{2}\d{2}(?:[A-Xa-x]{2}(?:\d{2})?)?)\b',
|
||||||
self.comment)
|
self.comment)
|
||||||
if grid_mode_grid_match:
|
if grid_mode_grid_match:
|
||||||
# regex matches, so extract grids:
|
# regex matches, so extract grids:
|
||||||
if not self.de_grid:
|
if not self.de_grid:
|
||||||
self.de_grid = grid_mode_grid_match.group(1).upper()
|
self.de_grid = grid_mode_grid_match.group(1).upper()
|
||||||
if not self.dx_grid:
|
if not self.dx_grid:
|
||||||
self.dx_grid = grid_mode_grid_match.group(3).upper()
|
self.dx_grid = grid_mode_grid_match.group(4).upper()
|
||||||
self.dx_location_source = "SPOT"
|
self.dx_location_source = "SPOT"
|
||||||
|
|
||||||
# And extract propagation mode:
|
# And extract propagation mode (group 2 for <...>, group 3 for (...)):
|
||||||
mode_tag = grid_mode_grid_match.group(2).upper()
|
mode_tag = (grid_mode_grid_match.group(2) or grid_mode_grid_match.group(3) or "").upper()
|
||||||
if mode_tag and not self.propagation_mode:
|
if mode_tag and not self.propagation_mode:
|
||||||
if mode_tag in PROPAGATION_MODES:
|
if mode_tag in PROPAGATION_MODES:
|
||||||
self.propagation_mode = PROPAGATION_MODES[mode_tag]
|
self.propagation_mode = PROPAGATION_MODES[mode_tag]
|
||||||
|
|||||||
8133
datafiles/MUNICIPIOS.csv
Normal file
8133
datafiles/MUNICIPIOS.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -111,7 +111,7 @@ class APILookupSIGRefHandler(tornado.web.RequestHandler):
|
|||||||
if "sig" in query_params.keys() and "id" in query_params.keys():
|
if "sig" in query_params.keys() and "id" in query_params.keys():
|
||||||
sig = str(query_params.get("sig")).upper()
|
sig = str(query_params.get("sig")).upper()
|
||||||
ref_id = str(query_params.get("id")).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):
|
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))
|
data = populate_sig_ref_info(SIGRef(id=ref_id, sig=sig))
|
||||||
self.write(json.dumps(data, default=serialize_everything))
|
self.write(json.dumps(data, default=serialize_everything))
|
||||||
|
|||||||
@@ -37,19 +37,21 @@ class GMA(HTTPSpotProvider):
|
|||||||
spot = Spot(source=self.name,
|
spot = Spot(source=self.name,
|
||||||
dx_call=source_spot["ACTIVATOR"].upper(),
|
dx_call=source_spot["ACTIVATOR"].upper(),
|
||||||
de_call=source_spot["SPOTTER"].upper(),
|
de_call=source_spot["SPOTTER"].upper(),
|
||||||
freq=float(source_spot["QRG"]) * 1000 if (source_spot["QRG"] != "") else None,
|
# Seen GMA spots with no frequency or with "QRT" in this field
|
||||||
# Seen GMA spots with no frequency
|
freq=float(source_spot["QRG"]) * 1000 if (
|
||||||
mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None,
|
source_spot["QRG"] != "" and source_spot["QRG"] != "QRT") else None,
|
||||||
# Filter out some weird mode strings
|
# Filter out some weird mode strings
|
||||||
|
mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None,
|
||||||
comment=source_spot["TEXT"],
|
comment=source_spot["TEXT"],
|
||||||
sig_refs=[SIGRef(id=source_spot["REF"], sig="", name=source_spot["NAME"])],
|
sig_refs=[SIGRef(id=source_spot["REF"], sig="", name=source_spot["NAME"])],
|
||||||
time=datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M").replace(
|
time=datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M").replace(
|
||||||
tzinfo=pytz.UTC).timestamp(),
|
tzinfo=pytz.UTC).timestamp(),
|
||||||
|
# Seen GMA spots with no (or empty) lat/lon
|
||||||
dx_latitude=float(source_spot["LAT"]) if (
|
dx_latitude=float(source_spot["LAT"]) if (
|
||||||
source_spot["LAT"] and source_spot["LAT"] != "") else None,
|
source_spot["LAT"] and source_spot["LAT"] != "") else None,
|
||||||
# Seen GMA spots with no (or empty) lat/lon
|
|
||||||
dx_longitude=float(source_spot["LON"]) if (
|
dx_longitude=float(source_spot["LON"]) if (
|
||||||
source_spot["LON"] and source_spot["LON"] != "") else None)
|
source_spot["LON"] and source_spot["LON"] != "") else None,
|
||||||
|
qrt=source_spot["QRG"] == "QRT")
|
||||||
|
|
||||||
# GMA doesn't give what programme (SIG) the reference is for until we separately look it up.
|
# GMA doesn't give what programme (SIG) the reference is for until we separately look it up.
|
||||||
if "REF" in source_spot:
|
if "REF" in source_spot:
|
||||||
@@ -63,8 +65,10 @@ class GMA(HTTPSpotProvider):
|
|||||||
# spots come through with reftype=POTA or reftype=WWFF. SOTA is harder to figure out because both SOTA
|
# spots come through with reftype=POTA or reftype=WWFF. SOTA is harder to figure out because both SOTA
|
||||||
# and GMA summits come through with reftype=Summit, so we must check for the presence of a "sota" entry
|
# and GMA summits come through with reftype=Summit, so we must check for the presence of a "sota" entry
|
||||||
# to determine if it's a SOTA summit.
|
# to determine if it's a SOTA summit.
|
||||||
if spot.sig_refs and "reftype" in ref_info and ref_info["reftype"] not in ["POTA", "WWFF"] and (
|
if spot.sig_refs and "reftype" in ref_info and ref_info["reftype"] not in ["POTA",
|
||||||
ref_info["reftype"] != "Summit" or "sota" not in ref_info or ref_info["sota"] == ""):
|
"WWFF"] and (
|
||||||
|
ref_info["reftype"] != "Summit" or "sota" not in ref_info or ref_info[
|
||||||
|
"sota"] == ""):
|
||||||
match ref_info["reftype"]:
|
match ref_info["reftype"]:
|
||||||
case "Summit":
|
case "Summit":
|
||||||
spot.sig_refs[0].sig = "GMA"
|
spot.sig_refs[0].sig = "GMA"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from data.spot import Spot
|
|||||||
from spotproviders.http_spot_provider import HTTPSpotProvider
|
from spotproviders.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
|
|
||||||
class WWTOTA(HTTPSpotProvider):
|
class Towers(HTTPSpotProvider):
|
||||||
"""Spot provider for Towers on the Air"""
|
"""Spot provider for Towers on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_SEC = 120
|
POLL_INTERVAL_SEC = 120
|
||||||
@@ -101,9 +101,9 @@
|
|||||||
(MOTA), the Amateur Radio Lighthouse Socirty (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos
|
(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
|
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), Lagos y Lagunas On the Air
|
Parks Award (KRMNPA), Wainwrights on the Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air
|
||||||
(LLOTA), Towers on the Air (WWTOTA), Tiles on the Air, Worked All Britain (WAB), Worked All Ireland (WAI), and
|
(LLOTA), Towers on the Air (WWTOTA), Tiles on the Air, Worked All Britain (WAB), Worked All Ireland (WAI), el
|
||||||
Toilets on the Air (TOTA).</p>
|
Diploma Municipios de España (DME) 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
|
<p>As of the time of writing in June 2026, 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
|
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>
|
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>
|
<h4 class="mt-4">Why can I filter spots by both SIG and Source? Isn't that basically the same thing?</h4>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/add-spot.js?v=1782076050"></script>
|
<script src="/js/add-spot.js?v=1782411905"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-add-spot").addClass("active");
|
$("#nav-link-add-spot").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/alerts.js?v=1782076050"></script>
|
<script src="/js/alerts.js?v=1782411905"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-alerts").addClass("active");
|
$("#nav-link-alerts").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1782076050"></script>
|
<script src="/js/spotsbandsandmap.js?v=1782411905"></script>
|
||||||
<script src="/js/bands.js?v=1782076050"></script>
|
<script src="/js/bands.js?v=1782411905"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-bands").addClass("active");
|
$("#nav-link-bands").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "skeleton.html" %}
|
{% extends "skeleton.html" %}
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/css/style.css?v=1782076050" 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/bootstrap-5.3.8.min.css" rel="stylesheet">
|
||||||
<link href="/vendor/css/fontawesome-6.7.2.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">
|
<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/bootstrap-5.3.8.bundle.min.js"></script>
|
||||||
<script src="/vendor/js/tinycolor2-1.6.0.min.js"></script>
|
<script src="/vendor/js/tinycolor2-1.6.0.min.js"></script>
|
||||||
|
|
||||||
<script src="/js/utils.js?v=1782076050"></script>
|
<script src="/js/utils.js?v=1782411905"></script>
|
||||||
<script src="/js/ui-ham.js?v=1782076050"></script>
|
<script src="/js/ui-ham.js?v=1782411905"></script>
|
||||||
<script src="/js/geo.js?v=1782076050"></script>
|
<script src="/js/geo.js?v=1782411905"></script>
|
||||||
<script src="/js/common.js?v=1782076050"></script>
|
<script src="/js/common.js?v=1782411905"></script>
|
||||||
{% end %}
|
{% end %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/vendor/js/chart-4.4.9.umd.min.js"></script>
|
<script src="/vendor/js/chart-4.4.9.umd.min.js"></script>
|
||||||
<script src="/js/conditions.js?v=1782076050"></script>
|
<script src="/js/conditions.js?v=1782411905"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-conditions").addClass("active");
|
$("#nav-link-conditions").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -95,8 +95,8 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1782076049"></script>
|
<script src="/js/spotsbandsandmap.js?v=1782411905"></script>
|
||||||
<script src="/js/map.js?v=1782076049"></script>
|
<script src="/js/map.js?v=1782411905"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-map").addClass("active");
|
$("#nav-link-map").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -116,8 +116,8 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1782076049"></script>
|
<script src="/js/spotsbandsandmap.js?v=1782411905"></script>
|
||||||
<script src="/js/spots.js?v=1782076049"></script>
|
<script src="/js/spots.js?v=1782411905"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-spots").addClass("active");
|
$("#nav-link-spots").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/status.js?v=1782076050"></script>
|
<script src="/js/status.js?v=1782411905"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$("#nav-link-status").addClass("active");
|
$("#nav-link-status").addClass("active");
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ info:
|
|||||||
### 1.4
|
### 1.4
|
||||||
|
|
||||||
* Spots can now include a "propagation_mode" field, and the `/options` call enumerates the options that can have.
|
* 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
|
### 1.3
|
||||||
|
|
||||||
@@ -785,7 +790,7 @@ components:
|
|||||||
- ZLOTA
|
- ZLOTA
|
||||||
- WOTA
|
- WOTA
|
||||||
- LLOTA
|
- LLOTA
|
||||||
- WWTOTA
|
- Towers
|
||||||
- Tiles
|
- Tiles
|
||||||
- Cluster
|
- Cluster
|
||||||
- RBN
|
- RBN
|
||||||
@@ -804,7 +809,7 @@ components:
|
|||||||
- HEMA
|
- HEMA
|
||||||
- WCA
|
- WCA
|
||||||
- MOTA
|
- MOTA
|
||||||
- SIOTA
|
- SiOTA
|
||||||
- ARLHS
|
- ARLHS
|
||||||
- ILLW
|
- ILLW
|
||||||
- ZLOTA
|
- ZLOTA
|
||||||
@@ -813,11 +818,12 @@ components:
|
|||||||
- WOTA
|
- WOTA
|
||||||
- BOTA
|
- BOTA
|
||||||
- LLOTA
|
- LLOTA
|
||||||
- WWTOTA
|
- Towers
|
||||||
- Tiles
|
- Tiles
|
||||||
- WAB
|
- WAB
|
||||||
- WAI
|
- WAI
|
||||||
- TOTA
|
- DME
|
||||||
|
- Toilets
|
||||||
example: POTA
|
example: POTA
|
||||||
|
|
||||||
SIGNameIncludingNoSIG:
|
SIGNameIncludingNoSIG:
|
||||||
@@ -1435,6 +1441,16 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: The full name of the SIG
|
description: The full name of the SIG
|
||||||
example: Parks on the Air
|
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:
|
ref_regex:
|
||||||
type: string
|
type: string
|
||||||
description: >
|
description: >
|
||||||
|
|||||||
@@ -358,11 +358,12 @@ const SIG_ICONS = {
|
|||||||
"BOTA": "fa-umbrella-beach",
|
"BOTA": "fa-umbrella-beach",
|
||||||
"KRMNPA": "fa-earth-oceania",
|
"KRMNPA": "fa-earth-oceania",
|
||||||
"LLOTA": "fa-water",
|
"LLOTA": "fa-water",
|
||||||
"WWTOTA": "fa-tower-observation",
|
"Towers": "fa-tower-observation",
|
||||||
"WAB": "fa-table-cells-large",
|
"WAB": "fa-table-cells-large",
|
||||||
"WAI": "fa-table-cells-large",
|
"WAI": "fa-table-cells-large",
|
||||||
|
"DME": "fa-building",
|
||||||
"Tiles": "fa-square",
|
"Tiles": "fa-square",
|
||||||
"TOTA": "fa-toilet"
|
"Toilets": "fa-toilet"
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIG_NAMES = {
|
const SIG_NAMES = {
|
||||||
|
|||||||
Reference in New Issue
Block a user