diff --git a/config-example.yml b/config-example.yml index dd9aed7..dd02187 100644 --- a/config-example.yml +++ b/config-example.yml @@ -172,6 +172,12 @@ static-data-providers: - class: "K0SWE" enabled: true + - class: "CQZoneData" + enabled: true + + - class: "ITUZoneData" + enabled: true + # SIG reference data providers to use. This allows Spothole to download, for example, the WWFF directory that maps WWFF # park IDs to their name and location. diff --git a/core/data_store.py b/core/data_store.py index 6018bdd..e39f89c 100644 --- a/core/data_store.py +++ b/core/data_store.py @@ -16,10 +16,12 @@ class DataStore: lookup data using different caching strategies for each.""" def __init__(self): + # Constants self._MAX_SPOT_COUNT = 100000 self._MAX_ALERT_COUNT = 100000 self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC = 300 self._CALLSIGN_DATA_TTL_SEC = 30 * 24 * 60 * 60 + # Caches self.alerts = None self.spots = None self.callsigns = None @@ -30,6 +32,10 @@ class DataStore: self._status = None self.solar_conditions = None self._solar = None + # ITU/CQ zone GeoJSON data is only ever loaded statically from a local file so these don't even need to be + # caches, they can just be straight objects + self.cq_zone_data = None + self.itu_zone_data = None def setup(self): Path(CACHE_DIR).mkdir(parents=True, exist_ok=True) diff --git a/core/geo_utils.py b/core/geo_utils.py index 8d110ea..05d1050 100644 --- a/core/geo_utils.py +++ b/core/geo_utils.py @@ -1,66 +1,58 @@ -import json import logging import re from math import floor -import geopandas from pyproj import Transformer -from shapely import prepare from shapely.geometry import Point, Polygon +from core.data_store import DATA_STORE + TRANSFORMER_OS_GRID_TO_WGS84 = Transformer.from_crs("EPSG:27700", "EPSG:4326") TRANSFORMER_IRISH_GRID_TO_WGS84 = Transformer.from_crs("EPSG:29903", "EPSG:4326") TRANSFORMER_CI_UTM_GRID_TO_WGS84 = Transformer.from_crs("+proj=utm +zone=30 +ellps=WGS84", "EPSG:4326") -with open("datafiles/cqzones.geojson") as f: - cq_zone_data = geopandas.GeoDataFrame.from_features(json.load(f)["features"]) -with open("datafiles/ituzones.geojson") as f: - itu_zone_data = geopandas.GeoDataFrame.from_features(json.load(f)["features"]) -for idx in cq_zone_data.index: - prepare(cq_zone_data.at[idx, 'geometry']) -for idx in itu_zone_data.index: - prepare(itu_zone_data.at[idx, 'geometry']) - def lat_lon_to_cq_zone(lat, lon): """Finds out which CQ zone a lat/lon point is in.""" - lon = ((lon + 180) % 360) - 180 - for index, row in cq_zone_data.iterrows(): - polygon = Polygon(row["geometry"]) - test_point = Point(lon, lat) - if polygon.contains(test_point): - return int(row["name"]) + if DATA_STORE.cq_zone_data is not None: + lon = ((lon + 180) % 360) - 180 + for index, row in DATA_STORE.cq_zone_data.iterrows(): + polygon = Polygon(row["geometry"]) + test_point = Point(lon, lat) + if polygon.contains(test_point): + return int(row["name"]) - # Might have problems around the antemeridian, so if we didn't find a match, try offsetting the point by + or - - # 360 degrees longitude to try the other side of the Earth - if lon < 0: - test_point = Point(lon + 360, lat) - else: - test_point = Point(lon - 360, lat) - if polygon.contains(test_point): - return int(row["name"]) + # Might have problems around the antemeridian, so if we didn't find a match, try offsetting the point by + or - + # 360 degrees longitude to try the other side of the Earth + if lon < 0: + test_point = Point(lon + 360, lat) + else: + test_point = Point(lon - 360, lat) + if polygon.contains(test_point): + return int(row["name"]) return None def lat_lon_to_itu_zone(lat, lon): """Finds out which ITU zone a lat/lon point is in.""" - lon = ((lon + 180) % 360) - 180 - for index, row in itu_zone_data.iterrows(): - polygon = Polygon(row["geometry"]) - test_point = Point(lon, lat) - if polygon.contains(test_point): - return int(row["name"]) + if DATA_STORE.itu_zone_data is not None: + lon = ((lon + 180) % 360) - 180 + for index, row in DATA_STORE.itu_zone_data.iterrows(): + polygon = Polygon(row["geometry"]) + test_point = Point(lon, lat) + if polygon.contains(test_point): + return int(row["name"]) - # Might have problems around the antemeridian, so if we didn't find a match, try offsetting the point by + or - - # 360 degrees longitude to try the other side of the Earth - if lon < 0: - test_point = Point(lon + 360, lat) - else: - test_point = Point(lon - 360, lat) - if polygon.contains(test_point): - return int(row["name"]) + # Might have problems around the antemeridian, so if we didn't find a match, try offsetting the point by + or - + # 360 degrees longitude to try the other side of the Earth + if lon < 0: + test_point = Point(lon + 360, lat) + else: + test_point = Point(lon - 360, lat) + if polygon.contains(test_point): + return int(row["name"]) return None diff --git a/staticdataproviders/cqzonedata.py b/staticdataproviders/cqzonedata.py new file mode 100644 index 0000000..ba46865 --- /dev/null +++ b/staticdataproviders/cqzonedata.py @@ -0,0 +1,30 @@ +import json +import logging + +import geopandas +from shapely import prepare + +from core.data_store import DATA_STORE +from staticdataproviders.local_file_static_data_provider import LocalFileStaticDataProvider + + +class CQZoneData(LocalFileStaticDataProvider): + """Static data provider for CQ zone geodata.""" + + PATH = "datafiles/cqzones.geojson" + + def __init__(self, provider_config): + super().__init__("CQ Zone Data", provider_config, self.PATH) + + def _load_data(self, path): + try: + with open(path) as f: + cq_zone_data = geopandas.GeoDataFrame.from_features(json.load(f)["features"]) + for idx in cq_zone_data.index: + prepare(cq_zone_data.at[idx, 'geometry']) + DATA_STORE.cq_zone_data = cq_zone_data + return True + + except Exception as e: + logging.error("Exception when loading CQ zone data.", e, exc_info=True) + return False diff --git a/staticdataproviders/ituzonedata.py b/staticdataproviders/ituzonedata.py new file mode 100644 index 0000000..67e0049 --- /dev/null +++ b/staticdataproviders/ituzonedata.py @@ -0,0 +1,30 @@ +import json +import logging + +import geopandas +from shapely import prepare + +from core.data_store import DATA_STORE +from staticdataproviders.local_file_static_data_provider import LocalFileStaticDataProvider + + +class ITUZoneData(LocalFileStaticDataProvider): + """Static data provider for ITU zone geodata.""" + + PATH = "datafiles/ituzones.geojson" + + def __init__(self, provider_config): + super().__init__("ITU Zone Data", provider_config, self.PATH) + + def _load_data(self, path): + try: + with open(path) as f: + itu_zone_data = geopandas.GeoDataFrame.from_features(json.load(f)["features"]) + for idx in itu_zone_data.index: + prepare(itu_zone_data.at[idx, 'geometry']) + DATA_STORE.itu_zone_data = itu_zone_data + return True + + except Exception as e: + logging.error("Exception when loading ITU zone data.", e, exc_info=True) + return False diff --git a/staticdataproviders/local_file_static_data_provider.py b/staticdataproviders/local_file_static_data_provider.py index 11b12d7..7ab9a89 100644 --- a/staticdataproviders/local_file_static_data_provider.py +++ b/staticdataproviders/local_file_static_data_provider.py @@ -20,9 +20,10 @@ class LocalFileStaticDataProvider(StaticDataProvider): if ok: self.status = "OK" self.last_update_time = datetime.now(pytz.UTC) + logging.info("Updated static reference data for " + self.name) else: self.status = "Error" - logging.info("Failed to load data for " + self.name) + logging.error("Failed to load data for " + self.name) except Exception as e: self.status = "Error" logging.error("Exception in local file Static Data Provider (" + self.name + ")", e, exc_info=True)