diff --git a/config-example.yml b/config-example.yml index bc080b6..dd1341f 100644 --- a/config-example.yml +++ b/config-example.yml @@ -300,6 +300,11 @@ activity_ref_data_providers: - class: "Toilets" enabled: true + - class: "PARC" + enabled: false + # PARC requires an API key to download the reference list. Contact Larry F5PYI for a key. + api_key: "" + # Callsign data providers to use. These allow Spothole to provide information about callsigns, either from static # lookups or from web-based services such as QRZ. Note that use of QRZ and HamQTH is *on behalf of the user*, and diff --git a/core/enums.py b/core/enums.py index 5ff5c0d..2c9bec2 100644 --- a/core/enums.py +++ b/core/enums.py @@ -141,6 +141,7 @@ class ActivityName(str, Enum): JOTA = "JOTA" RAILWAYS = "Railways" TOILETS = "Toilets" + PARC = "PARC" def __str__(self): return str(self.value) diff --git a/data/activities.py b/data/activities.py index 72c3dec..584967b 100644 --- a/data/activities.py +++ b/data/activities.py @@ -248,6 +248,17 @@ ACTIVITIES: dict[ActivityName, Activity] = { comment_names=[], icon="fa-square", ), + ActivityName.PARC: Activity( + name=ActivityName.PARC, + description="Protected Area Radio Community", + activity_type=ActivityType.ADVENTURE, + has_refs=True, + refs_globally_unique=True, + ref_type=ActivityRefType.PARK, + ref_regex=r"[A-Za-z]{2}-[A-Za-z]{2,3}-[0-9]{4}", + comment_names=["PARC"], + icon="fa-pagelines", + ), ActivityName.ZLOTA: Activity( name=ActivityName.ZLOTA, description="New Zealand on the Air", diff --git a/data/spot.py b/data/spot.py index 387e886..2275f77 100644 --- a/data/spot.py +++ b/data/spot.py @@ -382,7 +382,7 @@ class Spot: if mode_tag and not self.propagation_mode: if mode_tag in PROPAGATION_MODES: self.propagation_mode = PROPAGATION_MODES[mode_tag] - else: + elif mode_tag != "?": self.propagation_mode = mode_tag logger.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}") diff --git a/providers/activityrefdata/parc.py b/providers/activityrefdata/parc.py new file mode 100644 index 0000000..0ac3753 --- /dev/null +++ b/providers/activityrefdata/parc.py @@ -0,0 +1,74 @@ +import json +import logging +from time import sleep + +from pyhamtools.locator import locator_to_latlong + +from core.enums import ActivityName, ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, +) + +logger = logging.getLogger(__name__) + + +class PARC(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Protected Area Radiu Community""" + + POLL_INTERVAL_DAYS = 30 + ACTIVITY = ActivityName.PARC + DATA_URL = "https://parc-community.com/api/partner/parks?format=json" + + def __init__(self, provider_config): + # Ensure there is an API key in our config, and set up the query URL using it. If no key is provided, + # disable this spot provider. + api_key = provider_config.get("api_key", "") + if api_key == "": + provider_config["enabled"] = False + logger.warning( + "PARC activity reference provider configured but no api key was provided, this API will not be queried." + ) + + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL + "&key=" + api_key, self.POLL_INTERVAL_DAYS) + + def _http_response_to_data(self, http_response): + new_data = [] + # Use stdlib json rather than http_response.json(), as the PARC data can contain NaN values which simplejson + # rejects + data = json.loads(http_response.text) + for ref in data["parks"]: + ref_id = ref["reference"] + name = ref["name"] + grid = ref["grid"] + lat = ref["latitude"] + lon = ref["longitude"] + + # grid, lat & lon can all be null, but sometimes we have grid but not lat/lon + if grid and (lat is None or lon is None): + lat, lon = locator_to_latlong(grid) + + new_data.append( + ActivityRef( + activity=self.ACTIVITY, + id=ref_id, + # Name is sometimes NaN, not even a string somehow? + name=name if isinstance(name, str) else None, + ref_type=ActivityRefType.PARK, + url=f"https://parc-community.com/parks/{ref_id}", + grid=grid, + latitude=lat, + longitude=lon, + ) + ) + + # 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 activity 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 diff --git a/templates/add-spot.html b/templates/add-spot.html index 9d9e8d8..814d685 100644 --- a/templates/add-spot.html +++ b/templates/add-spot.html @@ -130,7 +130,7 @@ const ALLOW_UPSTREAM_SPOTTING = {% raw safe_json_dumps(web_ui_options["allow_upstream_spotting"]) %}; - + diff --git a/templates/alerts.html b/templates/alerts.html index 079a826..c94444c 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -87,7 +87,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index 3e83d22..ce2b858 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -82,8 +82,8 @@ const BANDS = {% raw safe_json_dumps(options["bands"]) %}; - - + + diff --git a/templates/base.html b/templates/base.html index d1280bd..447d8a2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -16,10 +16,10 @@ window.fetchEventSource = fetchEventSource; - - - - + + + + {% end %} {% block body %}