Giant refactor to rebrand "SIG" as "Activity" anywhere that doesn't touch API or config file (which is to be addressed in a future breaking change). #147

This commit is contained in:
Ian Renton
2026-09-18 14:54:11 +01:00
parent 556ea56378
commit 81cd686a00
96 changed files with 1208 additions and 1170 deletions
+91 -74
View File
@@ -9,18 +9,18 @@ from math import isnan
import pytz
from pyhamtools.locator import latlong_to_locator, locator_to_latlong
from core.activity_lookup_helper import populate_missing_activity_ref_info
from core.activity_utils import (
ANY_ACTIVITY_REGEX,
get_activity_name_from_comment_name,
get_ref_regex_for_activity,
)
from core.call_lookup_helper import get_call_info
from core.config import MAX_SPOT_AGE
from core.constants import PROPAGATION_MODES, SIGS
from core.constants import ACTIVITIES, PROPAGATION_MODES
from core.data_store import DATA_STORE
from core.enums import Continent, LocationSourceForSpot, Mode, ModeSource, ModeType
from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone
from core.sig_lookup_helper import populate_missing_sig_ref_info
from core.sig_utils import (
ANY_SIG_REGEX,
get_ref_regex_for_sig,
get_sig_name_from_comment_name,
)
from core.utils import (
get_flag_for_dxcc,
infer_band_from_freq,
@@ -28,7 +28,7 @@ from core.utils import (
infer_mode_from_frequency,
infer_mode_type_from_mode,
)
from data.sig_ref import SIGRef
from data.activity_ref import ActivityRef
logger = logging.getLogger(__name__)
@@ -46,8 +46,8 @@ class Spot:
dx_call: str | None = None
# Name of the operator that has been spotted
dx_name: str | None = None
# QTH of the operator that has been spotted. This could be from any SIG refs or could be from online lookup of their
# home QTH.
# QTH of the operator that has been spotted. This could be from any activity refs or could be from online lookup of
# their home QTH.
dx_qth: str | None = None
# Country of the DX operator
dx_country: str | None = None
@@ -73,8 +73,8 @@ class Spot:
# DX Location source. Indicates how accurate the location might be.
dx_location_source: LocationSourceForSpot | None = None
# DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is
# true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and the
# DX callsign doesn't have a suffix like /P.
# true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and
# the DX callsign doesn't have a suffix like /P. (Location source retains "SIG" wording for API compatibility.)
dx_location_good: bool = False
# DE (Spotter) info
@@ -120,11 +120,12 @@ class Spot:
# QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
qrt: bool = False
# Special Interest Group info
# Activity info
# Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
# Activity (e.g. outdoor activity programme such as POTA). Still named "sig" for API backwards compatibility.
sig: str | None = None
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named
# "sig_refs" for API backwards compatibility.
sig_refs: list = field(default_factory=list)
# Timing info
@@ -158,7 +159,10 @@ class Spot:
objects such as the sig_refs list.."""
if self.sig_refs:
self.sig_refs = [sig_ref if isinstance(sig_ref, SIGRef) else SIGRef(**sig_ref) for sig_ref in self.sig_refs]
self.sig_refs = [
activity_ref if isinstance(activity_ref, ActivityRef) else ActivityRef(**activity_ref)
for activity_ref in self.sig_refs
]
def infer_missing(self, credentials=None):
"""Infer missing parameters where possible"""
@@ -262,76 +266,89 @@ class Spot:
if self.dx_latitude or self.dx_grid:
self.dx_location_source = LocationSourceForSpot.SPOT
# Set the top-level "SIG" if it is missing but we have at least one SIG ref.
# Set the top-level activity if it is missing but we have at least one activity ref.
if not self.sig and self.sig_refs:
self.sig = self.sig_refs[0].sig.upper()
# See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This
# should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
# See if we already have an activity reference, but the comment looks like it contains more for the same
# activity. This should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
if self.comment and self.sig_refs and self.sig_refs[0].sig:
sig = self.sig_refs[0].sig.upper()
regex = get_ref_regex_for_sig(sig)
activity = self.sig_refs[0].sig.upper()
regex = get_ref_regex_for_activity(activity)
if regex:
all_comment_ref_matches = re.finditer(r"(^|\W)(" + regex + r")($|\W)", self.comment, re.IGNORECASE)
for ref_match in all_comment_ref_matches:
self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(2).upper(), sig=sig))
self._append_activity_ref_if_missing(ActivityRef(id=ref_match.group(2).upper(), sig=activity))
# See if the comment looks like it contains any SIGs (and optionally SIG references) that we can
# add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g. POTA
# comments like "also WWFF GFF-0001".
# See if the comment looks like it contains any activities (and optionally activity references) that we
# can add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g.
# POTA comments like "also WWFF GFF-0001".
if self.comment:
sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
for sig_match in sig_matches:
# First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things like cluster
# spots where the comment is just "POTA".
found_sig = get_sig_name_from_comment_name(sig_match.group(2))
activity_matches = re.finditer(r"(^|\W)" + ANY_ACTIVITY_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
for activity_match in activity_matches:
# First of all, if we haven't got an activity for this spot set yet, now we have. This covers
# things like cluster spots where the comment is just "POTA".
found_activity = get_activity_name_from_comment_name(activity_match.group(2))
if not self.sig:
self.sig = found_sig
self.sig = found_activity
# Now look to see if that SIG name was followed by something that looks like a reference ID for that SIG.
# If so, add that to the sig_refs list for this spot.
ref_regex = get_ref_regex_for_sig(found_sig)
# Now look to see if that activity name was followed by something that looks like a reference ID
# for that activity. If so, add that to the sig_refs list for this spot.
ref_regex = get_ref_regex_for_activity(found_activity)
if ref_regex:
ref_matches = re.finditer(
r"(^|\W)" + found_sig + r"([ -])(" + ref_regex + r")($|\W)",
r"(^|\W)" + found_activity + r"([ -])(" + ref_regex + r")($|\W)",
self.comment,
re.IGNORECASE,
)
for ref_match in ref_matches:
self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(3).upper(), sig=found_sig))
self._append_activity_ref_if_missing(
ActivityRef(id=ref_match.group(3).upper(), sig=found_activity)
)
# See if the comment looks like it contains any SIG references *without* the corresponding SIG name, but
# where the SIG reference is unique-looking enough that we can't confuse it with any other SIG.
# See if the comment looks like it contains any activity references *without* the corresponding activity
# name, but where the activity reference is unique-looking enough that we can't confuse it with any other
# activity.
if self.comment:
for sig in SIGS:
if sig.refs_globally_unique and sig.ref_regex:
ref_matches = re.finditer(r"(^|\W)(" + sig.ref_regex + r")($|\W)", self.comment, re.IGNORECASE)
for activity in ACTIVITIES:
if activity.refs_globally_unique and activity.ref_regex:
ref_matches = re.finditer(
r"(^|\W)(" + activity.ref_regex + r")($|\W)", self.comment, re.IGNORECASE
)
for ref_match in ref_matches:
# First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things
# like cluster spots where the comment is just "OHFF-1234", now we know it's WWFF.
# First of all, if we haven't got an activity for this spot set yet, now we have. This
# covers things like cluster spots where the comment is just "OHFF-1234", now we know
# it's WWFF.
if not self.sig:
self.sig = sig.name
self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(2).upper(), sig=sig.name))
self.sig = activity.name
self._append_activity_ref_if_missing(
ActivityRef(id=ref_match.group(2).upper(), sig=activity.name)
)
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs
# from WAB and WAI, which count as a SIG even though there's no real lookup, just maths
# Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a
# reference in its initial call, we use this code to populate the rest of the data. This includes working
# out grid refs from WAB and WAI, which count as an activity even though there's no real lookup, just maths
if self.sig_refs:
for sig_ref in self.sig_refs:
sig_ref = populate_missing_sig_ref_info(sig_ref)
# If the spot itself doesn't have location yet, but the SIG ref does, extract it
if sig_ref.grid and not self.dx_grid:
self.dx_grid = sig_ref.grid
if sig_ref.latitude and not self.dx_latitude and sig_ref.longitude and not self.dx_longitude:
self.dx_latitude = sig_ref.latitude
self.dx_longitude = sig_ref.longitude
for activity_ref in self.sig_refs:
activity_ref = populate_missing_activity_ref_info(activity_ref)
# If the spot itself doesn't have location yet, but the activity ref does, extract it
if activity_ref.grid and not self.dx_grid:
self.dx_grid = activity_ref.grid
if (
activity_ref.latitude
and not self.dx_latitude
and activity_ref.longitude
and not self.dx_longitude
):
self.dx_latitude = activity_ref.latitude
self.dx_longitude = activity_ref.longitude
if self.sig == "WAB" or self.sig == "WAI" or self.sig == "Tiles":
self.dx_location_source = LocationSourceForSpot.GRID
else:
self.dx_location_source = LocationSourceForSpot.SIG_REF_LOOKUP
# If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
# and apply it to the whole spot.
# If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
# reference's activity and apply it to the whole spot.
if self.sig_refs and not self.sig:
self.sig = self.sig_refs[0].sig
@@ -360,17 +377,17 @@ class Spot:
self.propagation_mode = mode_tag
logger.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}")
# Set SIGs based on propagation mode
# Set activities based on propagation mode
if self.propagation_mode == "Satellite":
if not self.sig:
self.sig = "AMSAT"
if not any(sig_ref.sig == "AMSAT" for sig_ref in self.sig_refs):
self.sig_refs.append(SIGRef(sig="AMSAT"))
if not any(activity_ref.sig == "AMSAT" for activity_ref in self.sig_refs):
self.sig_refs.append(ActivityRef(sig="AMSAT"))
if self.propagation_mode == "Earth-Moon-Earth":
if not self.sig:
self.sig = "EME"
if not any(sig_ref.sig == "EME" for sig_ref in self.sig_refs):
self.sig_refs.append(SIGRef(sig="EME"))
if not any(activity_ref.sig == "EME" for activity_ref in self.sig_refs):
self.sig_refs.append(ActivityRef(sig="EME"))
# Parse "de_grid -> dx_grid" structures from the comment
if self.comment:
@@ -426,8 +443,8 @@ class Spot:
self.dx_grid = dx_call_info.grid
self.dx_location_source = dx_call_info.location_source
# Determine a "QTH" string. If we have a SIG ref, pick the first one and turn it into a suitable string,
# otherwise see what they have set on an online lookup service.
# Determine a "QTH" string. If we have an activity ref, pick the first one and turn it into a suitable
# string, otherwise see what they have set on an online lookup service.
if self.sig_refs:
qth = self.sig_refs[0].id
if self.sig_refs[0].name:
@@ -482,7 +499,7 @@ class Spot:
self.de_longitude = de_call_info.longitude
self.de_grid = de_call_info.grid
# Icon for the spot should be the icon of the first SIG ref if present, otherwise a radio tower
# Icon for the spot should be the icon of the first activity ref if present, otherwise a radio tower
self.icon = "fa-tower-cell"
if self.sig_refs and self.sig_refs[0].icon:
self.icon = self.sig_refs[0].icon
@@ -495,17 +512,17 @@ class Spot:
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
def _append_sig_ref_if_missing(self, new_sig_ref):
"""Append a sig_ref to the list, so long as it's not already there."""
def _append_activity_ref_if_missing(self, new_activity_ref):
"""Append an activity ref to the list, so long as it's not already there."""
new_sig_ref.id = new_sig_ref.id.strip().upper()
new_sig_ref.sig = new_sig_ref.sig.strip().upper()
if new_sig_ref.id == "":
new_activity_ref.id = new_activity_ref.id.strip().upper()
new_activity_ref.sig = new_activity_ref.sig.strip().upper()
if new_activity_ref.id == "":
return
for sig_ref in self.sig_refs:
if sig_ref.id == new_sig_ref.id and sig_ref.sig == new_sig_ref.sig:
for activity_ref in self.sig_refs:
if activity_ref.id == new_activity_ref.id and activity_ref.sig == new_activity_ref.sig:
return
self.sig_refs.append(new_sig_ref)
self.sig_refs.append(new_activity_ref)
def expired(self):
"""Decide if this spot has expired (in which case it should not be added to the system in the first place, and not