diff --git a/README.md b/README.md index 908dc0f..5310644 100644 --- a/README.md +++ b/README.md @@ -19,7 +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 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. +Additional Special Interest Groups (SIGs) without their own specific data source include KRMNPA, SANPCPA, WAB, WAI and +DME. ![Screenshot](/images/screenshot2.png) diff --git a/config-example.yml b/config-example.yml index 0dd3866..502e532 100644 --- a/config-example.yml +++ b/config-example.yml @@ -246,6 +246,12 @@ sig-ref-data-providers: - class: "DME" enabled: true + - class: "KRMNPA" + enabled: true + + - class: "SANPCPA" + enabled: true + - class: "Toilets" enabled: true diff --git a/core/sig_lookup_helper.py b/core/sig_lookup_helper.py index b64bb3e..b977ce3 100644 --- a/core/sig_lookup_helper.py +++ b/core/sig_lookup_helper.py @@ -27,13 +27,6 @@ def get_sig_ref_info(sig, ref_id): if sig.upper() == "DME": ref_id = ref_id.zfill(5) - # KRMNPA & SANPCPA fudge. These don't have their own reference system, they just use VKFF references, so pretend - # the sig is WWFF and carry on - if sig.upper() == "KRMNPA" or sig.upper() == "SANPCPA": - sig = "WWFF" - - ### SKIPS ### - # # If the SIG is HEMA, we have no current lookup for this so just skip the lookup here. if sig.upper() == "HEMA": return sig_ref @@ -75,8 +68,8 @@ def get_sig_ref_info(sig, ref_id): ### ACTUAL LOOKUP ### # - # OK, this is something we have to look up. Now check to see if our data store contains SIG ref information for - # this SIG. If so, check for the reference data and use that. + # OK, this is something we have to look up. Now check to see if our data store contains reference data and use + # that. key = sig + ":" + ref_id lookup_data = DATA_STORE.sigrefs.get(key) if key in DATA_STORE.sigrefs else None if lookup_data: diff --git a/providers/alert/parksnpeaks.py b/providers/alert/parksnpeaks.py index 7974c42..dea7b04 100644 --- a/providers/alert/parksnpeaks.py +++ b/providers/alert/parksnpeaks.py @@ -50,7 +50,7 @@ class ParksNPeaks(HTTPAlertProvider): is_dxpedition=False) # Log a warning for the developer if PnP gives us an unknown programme we've never seen before - if sig and sig not in ["POTA", "SOTA", "WWFF", "SIOTA", "ZLOTA", "KRMNPA", "LLOTA", "QRP"]: + if sig and sig not in ["POTA", "SOTA", "WWFF", "SIOTA", "ZLOTA", "KRMNPA", "SANPCPA", "LLOTA", "QRP"]: logging.warning("PNP alert found with sig " + sig + ", developer needs to add support for this!") # If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to diff --git a/providers/sigrefdata/krmnpa.py b/providers/sigrefdata/krmnpa.py new file mode 100644 index 0000000..29d3eeb --- /dev/null +++ b/providers/sigrefdata/krmnpa.py @@ -0,0 +1,12 @@ +from providers.sigrefdata.pnp_kml_sig_ref_data_provider import ParksNPeaksKMLSIGRefDataProvider + + +class KRMNPA(ParksNPeaksKMLSIGRefDataProvider): + """SIG ref data provider for the Keith Roget Memorrial National Parks Award (KRMNPA).""" + + POLL_INTERVAL_DAYS = 365 + SIG = "KRMNPA" + DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=KRMNPA&poiFormat=4" + + def __init__(self, provider_config): + super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) \ No newline at end of file diff --git a/providers/sigrefdata/pnp_kml_sig_ref_data_provider.py b/providers/sigrefdata/pnp_kml_sig_ref_data_provider.py new file mode 100644 index 0000000..c7d018a --- /dev/null +++ b/providers/sigrefdata/pnp_kml_sig_ref_data_provider.py @@ -0,0 +1,50 @@ +import re + +from fastkml import kml +from pyhamtools.locator import latlong_to_locator + +from data.sig_ref import SIGRef +from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider + + +class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider): + """Base class for SIG ref data providers that use parksnpeaks.org KML POI feeds and have references that use the + VKFF refs rather than their own system (i.e. KRMNPA and SANPCPA).""" + + REF_PATTERN = re.compile(r"VKFF-\d+") + + def __init__(self, sig_name, provider_config, url, poll_interval): + """ Set up the provider, note poll_interval is in *days*.""" + super().__init__(sig_name, provider_config, url, poll_interval) + + def _http_response_to_data(self, http_response): + new_data = [] + + k = kml.KML.from_string(http_response.content) + + for document in k.features: + for folder in document.features: + for placemark in folder.features: + description = placemark.description or "" + match = self.REF_PATTERN.search(description) + if not match: + # No VKFF reference found in this placemark - skip it (e.g. non-park waypoints) + continue + ref_id = match.group(0) + + longitude, latitude = placemark.geometry.x, placemark.geometry.y + + ref = SIGRef(sig=self.sig_name, id=ref_id, name=placemark.name, + url="https://parksnpeaks.org/getPark.php?actPark=" + ref_id, + latitude=latitude, + longitude=longitude) + if latitude and longitude: + ref.grid = latlong_to_locator(latitude, longitude, 6) + new_data.append(ref) + + # 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(): + return new_data + + return new_data diff --git a/providers/sigrefdata/sanpcpa.py b/providers/sigrefdata/sanpcpa.py new file mode 100644 index 0000000..5e7175b --- /dev/null +++ b/providers/sigrefdata/sanpcpa.py @@ -0,0 +1,12 @@ +from providers.sigrefdata.pnp_kml_sig_ref_data_provider import ParksNPeaksKMLSIGRefDataProvider + + +class SANPCPA(ParksNPeaksKMLSIGRefDataProvider): + """SIG ref data provider for the South Australia National Parks and Conservation Parks Award (SANPCPA).""" + + POLL_INTERVAL_DAYS = 365 + SIG = "SANPCPA" + DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=SANPCPA&poiFormat=4" + + def __init__(self, provider_config): + super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) diff --git a/providers/spot/parksnpeaks.py b/providers/spot/parksnpeaks.py index 08008a9..391422c 100644 --- a/providers/spot/parksnpeaks.py +++ b/providers/spot/parksnpeaks.py @@ -18,7 +18,7 @@ class ParksNPeaks(HTTPSpotProvider): SPOTS_URL = "https://www.parksnpeaks.org/api/ALL" SUBMIT_URL = "https://www.parksnpeaks.org/api/SPOT/" SIOTA_LIST_URL = "https://www.silosontheair.com/data/silos.csv" - SUBMITTABLE_SIGS = ["POTA", "SOTA", "WWFF", "HEMA", "WOTA", "ZLOTA", "SIOTA", "KRMNPA"] + SUBMITTABLE_SIGS = ["POTA", "SOTA", "WWFF", "HEMA", "WOTA", "ZLOTA", "SIOTA", "KRMNPA", "SANPCPA"] def __init__(self, provider_config): super().__init__("ParksNPeaks", provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC) @@ -61,7 +61,7 @@ class ParksNPeaks(HTTPSpotProvider): sig_refs[0].name = source_spot["actLocation"] # Log a warning for the developer if PnP gives us an unknown programme we've never seen before - if sig not in ["POTA", "SOTA", "WWFF", "SIOTA", "ZLOTA", "KRMNPA", "LLOTA"]: + if sig not in ["POTA", "SOTA", "WWFF", "SIOTA", "ZLOTA", "KRMNPA", "SANPCPA", "LLOTA"]: logging.warning("PNP spot found with sig " + sig + ", developer needs to add support for this!") # Add new spot to the list diff --git a/requirements.txt b/requirements.txt index 9dadff3..2eeab4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,5 @@ tornado~=6.4.2 tornado_eventsource~=3.0.0 geopandas~=0.13.2 simplejson~=4.1.1 -cachetools~=7.1.6 \ No newline at end of file +cachetools~=7.1.6 +fastkml~=1.4.0 \ No newline at end of file diff --git a/static/js/ui-ham.js b/static/js/ui-ham.js index 1035138..e63224e 100644 --- a/static/js/ui-ham.js +++ b/static/js/ui-ham.js @@ -357,6 +357,7 @@ const SIG_ICONS = { "WOTA": "fa-w", "BOTA": "fa-umbrella-beach", "KRMNPA": "fa-earth-oceania", + "SANPCPA": "fa-earth-oceania", "LLOTA": "fa-water", "Towers": "fa-tower-observation", "WAB": "fa-table-cells-large", @@ -383,6 +384,7 @@ const SIG_NAMES = { "WOTA": "Wainwrights on the Air", "BOTA": "Beaches on the Air", "KRMNPA": "Keith Roget Memorial National Parks Award", + "SANPCPA": "South Australia National Parks and Conservation Parks Award", "LLOTA": "Lagos y Lagunas on the Air", "WWTOTA": "Towers on the Air", "WAB": "Worked All Britain", diff --git a/templates/about.html b/templates/about.html index e958702..780c34b 100644 --- a/templates/about.html +++ b/templates/about.html @@ -100,9 +100,10 @@ Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air (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 - 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), el - Diploma Municipios de España (DME) and Toilets on the Air (TOTA).

+ 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 (WWTOTA), Tiles on + the Air, Worked All Britain (WAB), Worked All Ireland (WAI), el Diploma Municipios de España (DME) and Toilets + on the Air (TOTA).

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 one I've missed, please let me know!