Files
spothole/core/activity_lookup_helper.py
T

157 lines
7.5 KiB
Python

import logging
import re
from pyhamtools.locator import latlong_to_locator, locator_to_latlong
from core.activity_utils import get_activity_by_name
from core.data_store import DATA_STORE
from core.enums import ActivityName, ActivityRefType
from core.geo_utils import wab_wai_square_to_lat_lon
from data.activity_ref import ActivityRef
logger = logging.getLogger(__name__)
def get_activity_ref_info(activity_name, ref_id):
"""Look up details of an activity reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in an
activity name and a reference ID (both strings) and returns an ActivityRef object populated with as much data
as we can find. This makes use of activity ref data in the data store, live lookups from the web, or just
automatic calculation depending on which activity we are getting data for."""
if activity_name is None or activity_name == "" or ref_id is None or ref_id == "":
logger.debug("Failed to look up activity ref info, activity or ref were not set.")
return None
# Sometimes we allow spaces instead of dashes in references due to common usage that way, but official reference
# lists never do, so convert them here.
ref_id = ref_id.replace(" ", "-")
# Prepare the object to be returned
activity_ref = ActivityRef(sig=activity_name, id=ref_id)
# We can always get the reference type and the icon from the activity itself
activity = get_activity_by_name(activity_name)
if activity:
activity_ref.ref_type = activity.ref_type
activity_ref.icon = activity.icon
try:
### FUDGES ###
#
# DME fudge. Our database has leading zeros padding to 5 digits which is the expected format, but not all
# activators add leading zeros. We also need to normalise "DME 01234" to "DME-01234" to match what's in our
# database.
if activity_name.upper() == ActivityName.DME:
match = re.match(r"DME[\- ](\d{3,5})", ref_id, re.IGNORECASE)
if match:
number = match.group(1)
ref_id = f"DME-{number.zfill(5)}"
# DTMBA spotters sometimes include spaces and dashes, our regex allows them but they must be removed here so we
# can look up against the official list which doesn't have them
if activity_name.upper() == ActivityName.DTMBA:
ref_id = ref_id.replace("-", "").replace(" ", "")
### NO DATA ACTIVITIES ###
#
# If the activity is HEMA or BIWOTA, we have no way to either generate useful data or look it up on a
# reference list, so just skip the lookup here.
if activity_name.upper() == ActivityName.HEMA or activity_name.upper() == ActivityName.BIWOTA:
return activity_ref
### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ###
#
# If the activity is Tiles, WAB, WAI or BOTA (Beaches), we don't have anything to look up from the data
# store, we can calculate all the information we are going to get directly.
if activity_name.upper() == ActivityName.TILES.upper():
# Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same
if not activity_ref.name:
activity_ref.name = activity_ref.id
if not activity_ref.grid:
activity_ref.grid = activity_ref.id
if activity_ref.grid and (not activity_ref.latitude or not activity_ref.longitude):
ll = locator_to_latlong(str(activity_ref.grid))
activity_ref.latitude = ll[0]
activity_ref.longitude = ll[1]
return activity_ref
elif activity_name.upper() == ActivityName.WAB or activity_name.upper() == ActivityName.WAI:
ll = wab_wai_square_to_lat_lon(ref_id)
if ll:
activity_ref.name = ref_id
try:
activity_ref.grid = latlong_to_locator(ll[0], ll[1], 6)
activity_ref.latitude = ll[0]
activity_ref.longitude = ll[1]
except Exception:
logger.warning("Invalid lat/lon received for WAB/WAI reference")
return activity_ref
elif activity_name.upper() == ActivityName.BOTA:
# For BOTA all we can ever generate is the URL, there is no data file or lookup for lat/longs
if not activity_ref.name:
activity_ref.name = activity_ref.id
if activity_ref.name:
activity_ref.url = (
f"https://www.beachesontheair.com/beaches/{activity_ref.name.lower().replace(' ', '-')}"
)
return activity_ref
elif activity_name.upper() == ActivityName.GMA_ISLANDS.upper():
# GMA Islands is a bit of a mess of GMA and IOTA references. Try looking them both up and see what returns
# the best result.
iota_lookup = get_activity_ref_info(ActivityName.IOTA, ref_id)
gma_lookup = get_activity_ref_info(ActivityName.GMA, ref_id)
for key, value in iota_lookup.__dict__.items():
if value is not None and activity_ref.__dict__.get(key) is None:
activity_ref.__dict__[key] = value
for key, value in gma_lookup.__dict__.items():
if value is not None and activity_ref.__dict__.get(key) is None:
activity_ref.__dict__[key] = value
activity_ref.ref_type = ActivityRefType.ISLAND
return activity_ref
### ACTUAL LOOKUP ###
#
# OK, this is something we have to look up. Now check to see if our data store contains reference data and if
# so, copy the data into the activity_ref object
key = f"{activity_name}:{ref_id}"
try:
lookup_data = DATA_STORE.activity_refs.get(key) if key in DATA_STORE.activity_refs else None
if lookup_data:
for attr, value in lookup_data.__dict__.items():
if value is not None and activity_ref.__dict__.get(attr) is None:
activity_ref.__dict__[attr] = value
else:
# Maybe a super new reference we don't know about yet, but more likely a typo or a test reference,
# just silently ignore it.
logger.debug(f"{activity_name} database did not contain data for ref {ref_id}")
except (ValueError, KeyError):
# Catch exceptions due to e.g. old versions of objects in the cache that are no longer compatible,
# and remove them from the cache.
del DATA_STORE.activity_refs[key]
return None
except Exception:
logger.exception(f"Exception when looking up activity ref info for {activity_name} ref {ref_id}")
return activity_ref
def populate_missing_activity_ref_info(activity_ref):
"""Look up details of an activity reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in an
activity_ref object which must at minimum have a "sig" and an "id". The rest of the object will be populated
and returned. Any data currently in the object will be kept, only missing data in the object will be populated
if it can be determined."""
lookup_data = get_activity_ref_info(activity_ref.sig, activity_ref.id)
if lookup_data:
# Copy new activity ref data into existing object where data was previously missing
for key, value in lookup_data.__dict__.items():
if value is not None and activity_ref.__dict__.get(key) is None:
activity_ref.__dict__[key] = value
return activity_ref