import re from time import sleep from typing import Any import requests from fastkml import kml from fastkml.containers import Document, Folder from fastkml.features import Placemark from fastkml.geometry import Point from pyhamtools.locator import latlong_to_locator from core.enums import ActivityRefType from data.activity_ref import ActivityRef from providers.activityrefdata.file_download_activity_ref_data_provider import ( FileDownloadActivityRefDataProvider, ) class ParksNPeaksKMLActivityRefDataProvider(FileDownloadActivityRefDataProvider): """Base class for activity 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: str, provider_config: dict[str, Any], url: str, poll_interval: float) -> None: """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: requests.Response) -> list[ActivityRef]: new_data: list[ActivityRef] = [] # KML content may carry an XML encoding declaration, which lxml's parser (used internally here) refuses to # accept as a decoded str, so bytes must be passed even though the type stub only declares str. k = kml.KML.from_string(http_response.content) # type: ignore[arg-type] for document in k.features: if not isinstance(document, Document): continue for folder in document.features: if not isinstance(folder, Folder): continue for placemark in folder.features: if not isinstance(placemark, Placemark): continue 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) if not isinstance(placemark.geometry, Point): # Not a point location (e.g. a boundary polygon) - skip it, we can't get a single lat/lon from it continue longitude, latitude = placemark.geometry.x, placemark.geometry.y ref = ActivityRef( sig=self.sig_name, id=ref_id, name=placemark.name, ref_type=ActivityRefType.PARK, url=f"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 # 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