3 Commits

Author SHA1 Message Date
Ian Renton
215b61593b Seen GMA send "QRT" as a frequency 2026-06-23 19:36:23 +01:00
Ian Renton
eb1d575623 Switch to an authoritative source of Spanish municipality data #114 and credit it in README #115 2026-06-23 19:26:45 +01:00
Ian Renton
e4c3a52299 Add support for DME. Closes #114. 2026-06-23 06:45:03 +01:00
16 changed files with 8185 additions and 8165 deletions

6
.idea/encodings.xml generated Normal file
View 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>

View File

@@ -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.
![Screenshot](/images/screenshot2.png) ![Screenshot](/images/screenshot2.png)
![Screenshot](/images/screenshot3.png) ![Screenshot](/images/screenshot3.png)
@@ -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.

View File

@@ -7,8 +7,10 @@ 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
with open("datafiles/dme-geodata.csv", encoding="utf-8") as _f: # Load Spanish municipality data for the DME programme. There's no convenient lookup API for this, so we embed the data
_DME_INDEX = {row["dme"]: row for row in csv.DictReader(_f)} # 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):
@@ -192,11 +194,12 @@ def populate_sig_ref_info(sig_ref):
except: except:
logging.debug("Invalid lat/lon received for reference") logging.debug("Invalid lat/lon received for reference")
elif sig.upper() == "DME": elif sig.upper() == "DME":
row = _DME_INDEX.get(ref_id) # Zero-pad to 5 digits to match our source data
row = _DME_INDEX.get(ref_id.zfill(5))
if row: if row:
sig_ref.name = row["municipio"] + ", " + row["provincia"] sig_ref.name = row["NOMBRE_ACTUAL"] + ", " + row["PROVINCIA"]
sig_ref.latitude = float(row["lat"]) if row.get("lat") else None sig_ref.latitude = float(row["LATITUD_ETRS89_REGCAN95"].replace(",", ".")) if row.get("LATITUD_ETRS89_REGCAN95") else None
sig_ref.longitude = float(row["lon"]) if row.get("lon") 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: if sig_ref.latitude and sig_ref.longitude:
try: try:
sig_ref.grid = latlong_to_locator(sig_ref.latitude, sig_ref.longitude, 6) sig_ref.grid = latlong_to_locator(sig_ref.latitude, sig_ref.longitude, 6)

8133
datafiles/MUNICIPIOS.csv Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -76,7 +76,7 @@
</div> </div>
<script src="/js/add-spot.js?v=1782076701"></script> <script src="/js/add-spot.js?v=1782239783"></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>

View File

@@ -75,7 +75,7 @@
</div> </div>
<script src="/js/alerts.js?v=1782076701"></script> <script src="/js/alerts.js?v=1782239783"></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>

View File

@@ -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=1782076701"></script> <script src="/js/spotsbandsandmap.js?v=1782239783"></script>
<script src="/js/bands.js?v=1782076701"></script> <script src="/js/bands.js?v=1782239783"></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>

View File

@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1782076701" type="text/css"> <link rel="stylesheet" href="/css/style.css?v=1782239783" 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=1782076701"></script> <script src="/js/utils.js?v=1782239783"></script>
<script src="/js/ui-ham.js?v=1782076701"></script> <script src="/js/ui-ham.js?v=1782239783"></script>
<script src="/js/geo.js?v=1782076701"></script> <script src="/js/geo.js?v=1782239783"></script>
<script src="/js/common.js?v=1782076701"></script> <script src="/js/common.js?v=1782239783"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">

View File

@@ -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=1782076701"></script> <script src="/js/conditions.js?v=1782239783"></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>

View File

@@ -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=1782076701"></script> <script src="/js/spotsbandsandmap.js?v=1782239783"></script>
<script src="/js/map.js?v=1782076701"></script> <script src="/js/map.js?v=1782239783"></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>

View File

@@ -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=1782076701"></script> <script src="/js/spotsbandsandmap.js?v=1782239783"></script>
<script src="/js/spots.js?v=1782076701"></script> <script src="/js/spots.js?v=1782239783"></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>

View File

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

View File

@@ -803,6 +803,7 @@ components:
- Tiles - Tiles
- WAB - WAB
- WAI - WAI
- DME
- TOTA - TOTA
example: POTA example: POTA