Compare commits

..
3 Commits
25 changed files with 278 additions and 31 deletions
+7
View File
@@ -65,6 +65,13 @@
</list> </list>
</option> </option>
</inspection_tool> </inspection_tool>
<inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<list>
<option value="pandas" />
</list>
</option>
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" /> <option name="processCode" value="true" />
<option name="processLiterals" value="true" /> <option name="processLiterals" value="true" />
+12
View File
@@ -249,6 +249,18 @@ sig_ref_data_providers:
- class: "FEA" - class: "FEA"
enabled: true enabled: true
- class: "DMVE"
enabled: true
- class: "DMUE"
enabled: true
- class: "DCE"
enabled: true
- class: "DEFE"
enabled: true
- class: "KRMNPA" - class: "KRMNPA"
enabled: true enabled: true
+42 -1
View File
@@ -247,13 +247,53 @@ SIGS = [
region_flag="🇪🇸", region_flag="🇪🇸",
refs_globally_unique=True, refs_globally_unique=True,
), ),
SIG(
name="DMUE",
comment_names=["DMUE"],
description="Diploma Museos de España",
sig_type=SIGType.REGIONAL,
ref_regex=r"MUE[A-Z]{2}-\d{3}",
icon="fa-landmark",
region_flag="🇪🇸",
refs_globally_unique=True,
),
SIG(
name="DMVE",
comment_names=["DMVE"],
description="Diploma Monumentos y Vestigios de España",
sig_type=SIGType.REGIONAL,
ref_regex=r"MV[A-Z]{1,2}-\d{4}",
icon="fa-monument",
region_flag="🇪🇸",
refs_globally_unique=True,
),
SIG(
name="DCE",
comment_names=["DCE"],
description="Diploma Castillos de España",
sig_type=SIGType.REGIONAL,
ref_regex=r"C[A-Z]{1,2}-\d{3}",
icon="fa-chess-rook",
region_flag="🇪🇸",
refs_globally_unique=False,
),
SIG(
name="DEFE",
comment_names=["DEFE"],
description="Diploma Estaciones de Ferrocarril de España",
sig_type=SIGType.REGIONAL,
ref_regex=r"EF[A-Z]{1,2}-\d{3}",
icon="fa-train",
region_flag="🇪🇸",
refs_globally_unique=True,
),
SIG( SIG(
name="DTMBA", name="DTMBA",
comment_names=["DTMBA"], comment_names=["DTMBA"],
description="Diploma Teatri Musei e Belle Arti", description="Diploma Teatri Musei e Belle Arti",
sig_type=SIGType.REGIONAL, sig_type=SIGType.REGIONAL,
ref_regex=r"I-?[0-9]{3,4}\s?[A-Z]{2}", ref_regex=r"I-?[0-9]{3,4}\s?[A-Z]{2}",
icon="fa-masks-theater", icon="fa-landmark",
region_flag="🇮🇹", region_flag="🇮🇹",
refs_globally_unique=True, refs_globally_unique=True,
), ),
@@ -341,5 +381,6 @@ PROPAGATION_MODES = {
"MS": "Meteor scatter", "MS": "Meteor scatter",
"RS": "Rain scatter", "RS": "Rain scatter",
"AS": "Aircraft scatter", "AS": "Aircraft scatter",
"ACS": "Aircraft scatter",
"SAT": "Satellite", "SAT": "Satellite",
} }
+5 -2
View File
@@ -48,12 +48,15 @@ class Mode(str, Enum):
def from_name(name): def from_name(name):
"""Convert a string to an enum mode using the alias table.""" """Convert a string to an enum mode using the alias table."""
if not name:
return Mode.UNKNOWN
try: try:
return Mode(name.upper()) return Mode(name.upper())
except ValueError: except (KeyError, ValueError):
try: try:
return Mode(MODE_ALIASES[name.upper()]) return Mode(MODE_ALIASES[name.upper()])
except ValueError: except (KeyError, ValueError):
return Mode.UNKNOWN return Mode.UNKNOWN
+4 -1
View File
@@ -42,6 +42,9 @@ def infer_mode_type_from_mode(mode: str) -> ModeType:
if not mode: if not mode:
return ModeType.UNKNOWN return ModeType.UNKNOWN
if mode in MODE_ALIASES:
mode = MODE_ALIASES[mode]
try: try:
mode = Mode(mode.upper()) mode = Mode(mode.upper())
if mode.is_cw: if mode.is_cw:
@@ -118,7 +121,7 @@ def get_callsign_object_from_pyhamtools_callinfo(callsign, callinfo):
country = data.get("country", None) country = data.get("country", None)
dxcc_id = data.get("adif", None) dxcc_id = data.get("adif", None)
continent = Continent(data.get("continent", None)) continent = Continent(data["continent"]) if "continent" in data else None
cq_zone = data.get("cqz", None) cq_zone = data.get("cqz", None)
itu_zone = data.get("ituz", None) itu_zone = data.get("ituz", None)
lat = float(data["latitude"]) if "latitude" in data else None lat = float(data["latitude"]) if "latitude" in data else None
+2 -2
View File
@@ -92,8 +92,8 @@ class Alert:
call_info = get_call_info(self.dx_calls[0], credentials) call_info = get_call_info(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_country: if self.dx_calls and self.dx_calls[0] and not self.dx_country:
self.dx_country = call_info.country self.dx_country = call_info.country
if self.dx_calls and self.dx_calls[0] and not self.dx_continent: if self.dx_calls and self.dx_calls[0] and call_info.continent and not self.dx_continent:
self.dx_continent = Continent(call_info.continent) if call_info.continent else None self.dx_continent = Continent(call_info.continent)
if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone: if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone:
self.dx_cq_zone = call_info.cq_zone self.dx_cq_zone = call_info.cq_zone
if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone: if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone:
+2 -2
View File
@@ -184,7 +184,7 @@ class Spot:
dx_call_info = get_call_info(self.dx_call, credentials) dx_call_info = get_call_info(self.dx_call, credentials)
if self.dx_call and not self.dx_country: if self.dx_call and not self.dx_country:
self.dx_country = dx_call_info.country self.dx_country = dx_call_info.country
if self.dx_call and not self.dx_continent: if self.dx_call and dx_call_info.continent and not self.dx_continent:
self.dx_continent = Continent(dx_call_info.continent) self.dx_continent = Continent(dx_call_info.continent)
if self.dx_call and not self.dx_dxcc_id: if self.dx_call and not self.dx_dxcc_id:
self.dx_dxcc_id = dx_call_info.dxcc_id self.dx_dxcc_id = dx_call_info.dxcc_id
@@ -222,7 +222,7 @@ class Spot:
): ):
if not self.de_country: if not self.de_country:
self.de_country = de_call_info.country self.de_country = de_call_info.country
if not self.de_continent: if de_call_info.continent and not self.de_continent:
self.de_continent = Continent(de_call_info.continent) self.de_continent = Continent(de_call_info.continent)
if not self.de_dxcc_id: if not self.de_dxcc_id:
self.de_dxcc_id = de_call_info.dxcc_id self.de_dxcc_id = de_call_info.dxcc_id
+1
View File
@@ -59,6 +59,7 @@ class ParksNPeaks(HTTPAlertProvider):
"POTA", "POTA",
"SOTA", "SOTA",
"WWFF", "WWFF",
"HEMA",
"SIOTA", "SIOTA",
"ZLOTA", "ZLOTA",
"KRMNPA", "KRMNPA",
+1 -1
View File
@@ -143,7 +143,7 @@ class HamQTH(APIQueryCallsignDataProvider):
name=data.get("nick", None), name=data.get("nick", None),
qth=data.get("qth", None), qth=data.get("qth", None),
country=data.get("country", None), country=data.get("country", None),
continent=Continent(data.get("continent", None)), continent=Continent(data["continent"]) if "continent" in data else None,
latitude=lat, latitude=lat,
longitude=lon, longitude=lon,
grid=grid, grid=grid,
+1 -1
View File
@@ -169,7 +169,7 @@ class QRZ(APIQueryCallsignDataProvider):
name=name, name=name,
qth=data.get("addr2", None), qth=data.get("addr2", None),
country=data.get("country", None), country=data.get("country", None),
continent=Continent(data.get("continent", None)), continent=Continent(data["continent"]) if "continent" in data else None,
latitude=lat, latitude=lat,
longitude=lon, longitude=lon,
grid=grid, grid=grid,
+42
View File
@@ -0,0 +1,42 @@
import io
from time import sleep
import pandas as pd
from core.enums import SIGRefType
from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
class DCE(FileDownloadSIGRefDataProvider):
"""SIG ref data provider for Diploma Castillos de España"""
POLL_INTERVAL_DAYS = 365
SIG = "DCE"
DATA_URL = "https://www.acracb.org/dce/descargas/General/directorio_referencias_dce.xls"
def __init__(self, provider_config):
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
file_stream = io.BytesIO(http_response.content)
df = pd.read_excel(file_stream, engine="xlrd", header=None)
for index, row in df.iterrows():
if row.iloc[0] and row.iloc[2]:
new_data.append(
SIGRef(sig=self.SIG, id=row.iloc[0].strip(), name=row.iloc[2].strip(), ref_type=SIGRefType.CASTLE)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+46
View File
@@ -0,0 +1,46 @@
import io
from time import sleep
import pandas as pd
from core.enums import SIGRefType
from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
class DEFE(FileDownloadSIGRefDataProvider):
"""SIG ref data provider for Diploma Estationes de Ferrocarril de España"""
POLL_INTERVAL_DAYS = 365
SIG = "DEFE"
DATA_URL = "https://www.acracb.org/defe/descargas/General/directorio_referencias_defe.xls"
def __init__(self, provider_config):
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
file_stream = io.BytesIO(http_response.content)
df = pd.read_excel(file_stream, engine="xlrd", header=None)
for index, row in df.iterrows():
# Skip the header row
if str(row.iloc[0]) == "NºDEFE":
continue
if row.iloc[0] and row.iloc[1]:
new_data.append(
SIGRef(sig=self.SIG, id=row.iloc[0].strip(), name=row.iloc[1].strip(), ref_type=SIGRefType.BUILDING)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+37
View File
@@ -0,0 +1,37 @@
import csv
from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
class DMUE(FileDownloadSIGRefDataProvider):
"""SIG ref data provider for Diploma Museos de España"""
POLL_INTERVAL_DAYS = 365
SIG = "DMUE"
DATA_URL = "https://dmue.radiogalena.es/nom_dmue.csv"
def __init__(self, provider_config):
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.reader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
if len(row) > 1 and row[0] and row[1]:
new_data.append(
SIGRef(sig=self.SIG, id=row[0].strip(), name=row[1].strip(), ref_type=SIGRefType.BUILDING)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+50
View File
@@ -0,0 +1,50 @@
import io
from time import sleep
import pandas as pd
from core.enums import SIGRefType
from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
class DMVE(FileDownloadSIGRefDataProvider):
"""SIG ref data provider for Diploma Monumentos y Vestigios de España"""
POLL_INTERVAL_DAYS = 365
SIG = "DMVE"
DATA_URL = "https://www.acracb.org/dmve/descargas/General/directorio_referencias_dmve.xls"
def __init__(self, provider_config):
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
file_stream = io.BytesIO(http_response.content)
# Despide the .xls extension this is actually an xlsx file, so we need openpyxl not xlrd
df = pd.read_excel(file_stream, engine="openpyxl", header=None)
for index, row in df.iterrows():
ref = row.iloc[0]
name = row.iloc[1]
# Skip the header row and blank rows
if str(ref) == "REF.":
continue
if pd.isna(ref) or pd.isna(name):
continue
if ref and name:
new_data.append(SIGRef(sig=self.SIG, id=ref.strip(), name=name.strip(), ref_type=SIGRefType.BUILDING))
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+4 -1
View File
@@ -16,9 +16,12 @@ beautifulsoup4~=4.14.2
websocket-client~=1.8.0 websocket-client~=1.8.0
tornado~=6.4.2 tornado~=6.4.2
tornado_eventsource~=3.0.0 tornado_eventsource~=3.0.0
pandas~=3.0.0
geopandas~=0.13.2 geopandas~=0.13.2
simplejson~=4.1.1 simplejson~=4.1.1
cachetools~=7.1.6 cachetools~=7.1.6
fastkml~=1.4.0 fastkml~=1.4.0
ruff~=0.16.3 ruff~=0.16.3
pdfplumber~=0.11.10 pdfplumber~=0.11.10
xlrd~=2.0.2
openpyxl~=3.1.5
+5 -3
View File
@@ -102,9 +102,11 @@
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), South Australia National Parks and Conservation Parks Award (SANPCPA), Wainwrights on the Parks Award (KRMNPA), South Australia National Parks and Conservation Parks Award (SANPCPA), Wainwrights on the
Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air, Tiles on Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air, Tiles on
the Air, Worked All Britain (WAB), Worked All Ireland (WAI), el Diploma Municipios de España (DME), el Diploma the Air, Worked All Britain (WAB), Worked All Ireland (WAI), Diploma Municipios de España (DME), Diploma
Faros de España (FEA), il Diploma Teatri Musei e Belle Arti (DTMBA), British Inland Waterways on the Air Faros de España (FEA), Diploma Muesos de España (DMUE), Diploma Castillos de España (DCE), Diploma Monumentos y
(BIWOTA), Castles on the Air (COTA), Polish Gmina Award (PGA), and Toilets on the Air.</p> Vestigios de España (DMVE), Diploma Estaciones de Ferrocarril de España (DEFE), Diploma Teatri Musei e Belle
Arti (DTMBA), British Inland Waterways on the Air (BIWOTA), Castles on the Air (COTA), Polish Gmina Award (PGA),
and Toilets on the Air.</p>
<p>As of the time of writing in August 2026, I think Spothole captures most outdoor radio programmes that have a <p>As of the time of writing in August 2026, I think Spothole captures most outdoor radio programmes that have a
defined, downloadable reference list, and almost certainly those that have a spotting/alerting API. If you know defined, downloadable 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> of one I've missed, please let me know!</p>
+1 -1
View File
@@ -77,7 +77,7 @@
</div> </div>
<script src="/static/js/add-spot.js?v=1788338285"></script> <script src="/static/js/add-spot.js?v=1788546948"></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>
+1 -1
View File
@@ -84,7 +84,7 @@
</div> </div>
<script src="/static/js/alerts.js?v=1788338286"></script> <script src="/static/js/alerts.js?v=1788546948"></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>
+2 -2
View File
@@ -76,8 +76,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1788338285"></script> <script src="/static/js/spotsbandsandmap.js?v=1788546948"></script>
<script src="/static/js/bands.js?v=1788338285"></script> <script src="/static/js/bands.js?v=1788546948"></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>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1788338285" type="text/css"> <link rel="stylesheet" href="/static/css/style.css?v=1788546948" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet"> <link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -16,10 +16,10 @@
window.fetchEventSource = fetchEventSource; window.fetchEventSource = fetchEventSource;
</script> </script>
<script src="/static/js/utils.js?v=1788338285"></script> <script src="/static/js/utils.js?v=1788546948"></script>
<script src="/static/js/ui-ham.js?v=1788338285"></script> <script src="/static/js/ui-ham.js?v=1788546948"></script>
<script src="/static/js/geo.js?v=1788338285"></script> <script src="/static/js/geo.js?v=1788546948"></script>
<script src="/static/js/common.js?v=1788338285"></script> <script src="/static/js/common.js?v=1788546948"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div> </div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script> <script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1788338285"></script> <script src="/static/js/conditions.js?v=1788546948"></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>
+2 -2
View File
@@ -16,13 +16,13 @@
"src": "/static/img/icon-192-pwa.png", "src": "/static/img/icon-192-pwa.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192", "sizes": "192x192",
"purpose": "maskable" "purpose": "maskable any"
}, },
{ {
"src": "/static/img/icon-512-pwa.png", "src": "/static/img/icon-512-pwa.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512", "sizes": "512x512",
"purpose": "maskable" "purpose": "maskable any"
} }
], ],
"url": "{{ baseurl }}" "url": "{{ baseurl }}"
+2 -2
View File
@@ -113,8 +113,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}"; const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
</script> </script>
<script src="/static/js/spotsbandsandmap.js?v=1788338286"></script> <script src="/static/js/spotsbandsandmap.js?v=1788546948"></script>
<script src="/static/js/map.js?v=1788338286"></script> <script src="/static/js/map.js?v=1788546948"></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>
+2 -2
View File
@@ -113,8 +113,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1788338285"></script> <script src="/static/js/spotsbandsandmap.js?v=1788546948"></script>
<script src="/static/js/spots.js?v=1788338285"></script> <script src="/static/js/spots.js?v=1788546948"></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>
+1 -1
View File
@@ -86,7 +86,7 @@
</div> </div>
</div> </div>
<script src="/static/js/status.js?v=1788338285"></script> <script src="/static/js/status.js?v=1788546948"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#nav-link-status").addClass("active"); $("#nav-link-status").addClass("active");