First attempt at converting "sig" to "activity" for v3

This commit is contained in:
Ian Renton
2026-09-24 07:09:37 +01:00
parent 1d0129f7bb
commit d91fa70655
90 changed files with 822 additions and 496 deletions
+43 -43
View File
@@ -5,7 +5,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.CONTEST: Activity(
name=ActivityName.CONTEST,
description="Contest",
sig_type=ActivityType.TRADITIONAL,
activity_type=ActivityType.TRADITIONAL,
has_refs=False,
refs_globally_unique=False,
# No sensible way to determine *which* contest, but if we set comment_names=["CONTEST"] then at least
@@ -17,7 +17,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.DXPEDITION: Activity(
name=ActivityName.DXPEDITION,
description="Radio expedition to a remote location",
sig_type=ActivityType.TRADITIONAL,
activity_type=ActivityType.TRADITIONAL,
has_refs=False,
refs_globally_unique=False,
comment_names=["DXPEDITION"],
@@ -27,7 +27,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.SATELLITE: Activity(
name=ActivityName.SATELLITE,
description="Amateur Radio Satellite",
sig_type=ActivityType.TRADITIONAL,
activity_type=ActivityType.TRADITIONAL,
# Satellite "references" are the names of the satellites themselves. This is not an exhaustive list, it just
# matches some of the most commonly used amateur radio satellites so they can be picked out of spot comments.
has_refs=True,
@@ -41,7 +41,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.EME: Activity(
name=ActivityName.EME,
description="Earth-Moon-Earth (Moonbounce)",
sig_type=ActivityType.TRADITIONAL,
activity_type=ActivityType.TRADITIONAL,
has_refs=False,
refs_globally_unique=False,
comment_names=[],
@@ -50,7 +50,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.AERONAUTICAL_MOBILE: Activity(
name=ActivityName.AERONAUTICAL_MOBILE,
description="Aeronautical Mobile",
sig_type=ActivityType.TRADITIONAL,
activity_type=ActivityType.TRADITIONAL,
has_refs=False,
refs_globally_unique=False,
# Don't pick /AM out of comments, spot.py will handle picking it out of the callsign
@@ -60,7 +60,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.MARITIME_MOBILE: Activity(
name=ActivityName.MARITIME_MOBILE,
description="Maritime Mobile",
sig_type=ActivityType.TRADITIONAL,
activity_type=ActivityType.TRADITIONAL,
has_refs=False,
refs_globally_unique=False,
# Don't pick /MM out of comments, spot.py will handle picking it out of the callsign
@@ -70,7 +70,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.QRP: Activity(
name=ActivityName.QRP,
description="Low power",
sig_type=ActivityType.TRADITIONAL,
activity_type=ActivityType.TRADITIONAL,
has_refs=False,
refs_globally_unique=False,
comment_names=["QRP"],
@@ -80,7 +80,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.POTA: Activity(
name=ActivityName.POTA,
description="Parks on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.PARK,
@@ -92,7 +92,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.SOTA: Activity(
name=ActivityName.SOTA,
description="Summits on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.SUMMIT,
@@ -104,7 +104,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.WWFF: Activity(
name=ActivityName.WWFF,
description="World Wide Flora & Fauna",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.PARK,
@@ -116,7 +116,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.GMA: Activity(
name=ActivityName.GMA,
description="Global Mountain Activity",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.SUMMIT,
@@ -127,7 +127,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.WWBOTA: Activity(
name=ActivityName.WWBOTA,
description="Worldwide Bunkers on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.BUNKER,
@@ -138,7 +138,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.HEMA: Activity(
name=ActivityName.HEMA,
description="HuMPs Excluding Marilyns Award",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.SUMMIT,
@@ -150,7 +150,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.IOTA: Activity(
name=ActivityName.IOTA,
description="Islands on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.ISLAND,
@@ -161,7 +161,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.GMA_ISLANDS: Activity(
name=ActivityName.GMA_ISLANDS,
description="Global Mountain Activity - Islands",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.ISLAND,
@@ -172,7 +172,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.ARLHS: Activity(
name=ActivityName.ARLHS,
description="Amateur Radio Lighthouse Society",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.LIGHTHOUSE,
@@ -183,7 +183,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.ILLW: Activity(
name=ActivityName.ILLW,
description="International Lighthouse & Lightship Weekend",
sig_type=ActivityType.EVENT,
activity_type=ActivityType.EVENT,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.LIGHTHOUSE,
@@ -194,7 +194,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.MOTA: Activity(
name=ActivityName.MOTA,
description="Mills on the Air",
sig_type=ActivityType.EVENT,
activity_type=ActivityType.EVENT,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.MILL,
@@ -205,7 +205,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.SIOTA: Activity(
name=ActivityName.SIOTA,
description="Silos on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.SILO,
@@ -217,7 +217,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.WCA: Activity(
name=ActivityName.WCA,
description="World Castles Award",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.CASTLE,
@@ -228,7 +228,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.ZLOTA: Activity(
name=ActivityName.ZLOTA,
description="New Zealand on the Air",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=True,
ref_type=None,
@@ -241,7 +241,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.WOTA: Activity(
name=ActivityName.WOTA,
description="Wainwrights on the Air",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.SUMMIT,
@@ -254,7 +254,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.BOTA: Activity(
name=ActivityName.BOTA,
description="Beaches on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.BEACH,
@@ -265,7 +265,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.KRMNPA: Activity(
name=ActivityName.KRMNPA,
description="Keith Roget Memorial National Parks Award",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.PARK,
@@ -278,7 +278,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.SANPCPA: Activity(
name=ActivityName.SANPCPA,
description="South Australian National Parks and Conservation Parks Award",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.PARK,
@@ -291,7 +291,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.LLOTA: Activity(
name=ActivityName.LLOTA,
description="Lagos y Lagunas on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.LAKE,
@@ -303,7 +303,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.TOWERS: Activity(
name=ActivityName.TOWERS,
description="Towers on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.TOWER,
@@ -314,7 +314,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.TILES: Activity(
name=ActivityName.TILES,
description="Tiles on the Air",
sig_type=ActivityType.ADVENTURE,
activity_type=ActivityType.ADVENTURE,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.GRID,
@@ -325,7 +325,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.RADAR_RALLY: Activity(
name=ActivityName.RADAR_RALLY,
description="RaDAR Rally",
sig_type=ActivityType.EVENT,
activity_type=ActivityType.EVENT,
has_refs=False,
refs_globally_unique=False,
comment_names=["RaDAR"],
@@ -334,7 +334,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.WAB: Activity(
name=ActivityName.WAB,
description="Worked All Britain",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.GRID,
@@ -346,7 +346,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.WAI: Activity(
name=ActivityName.WAI,
description="Worked All Ireland",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.GRID,
@@ -358,7 +358,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.DMF: Activity(
name=ActivityName.DMF,
description="Diplôme des Moulins de France",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.MILL,
@@ -369,7 +369,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.DME: Activity(
name=ActivityName.DME,
description="Diploma Municipios de España",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.TOWN,
@@ -381,7 +381,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.FEA: Activity(
name=ActivityName.FEA,
description="Diploma Faros de España",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.LIGHTHOUSE,
@@ -396,7 +396,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.DMUE: Activity(
name=ActivityName.DMUE,
description="Diploma Museos de España",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.BUILDING,
@@ -408,7 +408,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.DMVE: Activity(
name=ActivityName.DMVE,
description="Diploma Monumentos y Vestigios de España",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.BUILDING,
@@ -420,7 +420,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.DCE: Activity(
name=ActivityName.DCE,
description="Diploma Castillos de España",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.CASTLE,
@@ -432,7 +432,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.DEFE: Activity(
name=ActivityName.DEFE,
description="Diploma Estaciones de Ferrocarril de España",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.BUILDING,
@@ -444,7 +444,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.DTMBA: Activity(
name=ActivityName.DTMBA,
description="Diploma Teatri Musei e Belle Arti",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.BUILDING,
@@ -456,7 +456,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.BIWOTA: Activity(
name=ActivityName.BIWOTA,
description="British Inland Waterways on the Air",
sig_type=ActivityType.EVENT,
activity_type=ActivityType.EVENT,
has_refs=False,
refs_globally_unique=False,
ref_type=ActivityRefType.WATERWAY,
@@ -467,7 +467,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.COTA: Activity(
name=ActivityName.COTA,
description="Castles on the Air",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.CASTLE,
@@ -479,7 +479,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.PGA: Activity(
name=ActivityName.PGA,
description="Polish Gmina Award",
sig_type=ActivityType.REGIONAL,
activity_type=ActivityType.REGIONAL,
has_refs=True,
refs_globally_unique=False,
ref_type=ActivityRefType.REGION,
@@ -491,7 +491,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
ActivityName.TOILETS: Activity(
name=ActivityName.TOILETS,
description="Toilets on the Air",
sig_type=ActivityType.EVENT,
activity_type=ActivityType.EVENT,
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.TOILET,
+8 -10
View File
@@ -5,22 +5,20 @@ from core.enums import ActivityName, ActivityRefType, ActivityType
@dataclass
class Activity:
"""Data class that defines an Activity (formerly referred to as a "Special Interest Group" or "SIG", a term
which is still used for the `sig` field name in the API for backwards compatibility). Each contains a name and
a longer form description. They also contain comment_names which attempts to separate out the way people might
refer to it in cluster comments from how it is referred to in the UI & API. (For example, "TOTA" in cluster
spot comments almost always means Towers on the Air, but no single programme is referred to in the UI as "TOTA"
as it's ambiguous between Towers, Toilets and Tiles. And while Beaches got the name "BOTA" first, "BOTA" spots
are much more likely to be bunkers.) Finally, there is a ref_regex which provides a regular expression to
match what references (such as parks and summits) look like for that programme."""
"""Data class that defines an Activity. Each contains a name and a longer form description. They also contain
comment_names which attempts to separate out the way people might refer to it in cluster comments from how it is
referred to in the UI & API. (For example, "TOTA" in cluster spot comments almost always means Towers on the Air,
but no single programme is referred to in the UI as "TOTA" as it's ambiguous between Towers, Toilets and Tiles.
And while Beaches got the name "BOTA" first, "BOTA" spots are much more likely to be bunkers.) Finally, there is a
ref_regex which provides a regular expression to match what references (such as parks and summits) look like for
that programme."""
# Activity name as used in the UI and API, e.g. "Towers"
name: ActivityName
# Description, e.g. "Towers on the Air"
description: str
# Type, either Worldwide, Regional or Event. Used for sorting in the web UI.
# Note: this field is still named "sig_type" in the API for backwards compatibility.
sig_type: ActivityType
activity_type: ActivityType
# Whether this activity has a fixed set of references (e.g. parks) with some sort of ID to "activate"
has_refs: bool
# Identifies that the activity's reference ID structure defined by its regex is unique across all programmes and
+2 -2
View File
@@ -8,8 +8,8 @@ class ActivityRef:
"""Data class that defines an Activity "info" or reference. As well as the basic reference ID we include a
name and a lookup URL."""
# Activity that this reference is in, e.g. "POTA". Still named "sig" for backwards compatibility with the API.
sig: str
# Activity that this reference is in, e.g. "POTA".
activity: str
# Reference ID, e.g. "GB-0001".
id: str | None = None
# Name of the reference, e.g. "Null Country Park", if known.
+10 -11
View File
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
from datetime import datetime, timedelta
import pytz
from pyhamtools.locator import locator_to_latlong, latlong_to_locator
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 get_icon_for_activity
@@ -66,11 +66,10 @@ class Alert:
# Activity info
# Activity (e.g. outdoor activity programme such as POTA). Still named "sig" for API backwards compatibility.
sig: str | None = None
# 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)
# Activity (e.g. outdoor activity programme such as POTA).
activity: str | None = None
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
activity_refs: list = field(default_factory=list)
# Timing info
@@ -131,8 +130,8 @@ class Alert:
self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id)
# Fetch activity data, and set a real position if we can get one.
if self.sig_refs:
for activity_ref in self.sig_refs:
if self.activity_refs:
for activity_ref in self.activity_refs:
activity_ref = populate_missing_activity_ref_info(activity_ref)
# If the alert itself doesn't have location yet, but the activity ref does, extract it
if activity_ref.grid and not self.dx_grid:
@@ -148,8 +147,8 @@ class Alert:
# 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 self.sig_refs[0] and not self.sig:
self.sig = self.sig_refs[0].sig
if self.activity_refs and self.activity_refs[0] and not self.activity:
self.activity = self.activity_refs[0].activity
# DX Grid to lat/lon and vice versa in case one is missing
if self.dx_grid and (not self.dx_latitude or not self.dx_longitude):
@@ -182,7 +181,7 @@ class Alert:
# Icon for the alert should be the icon of its activity if known, otherwise a radio tower
self.icon = "fa-tower-cell"
if self.sig and (activity_icon := get_icon_for_activity(self.sig)):
if self.activity and (activity_icon := get_icon_for_activity(self.activity)):
self.icon = activity_icon
except Exception:
+50 -49
View File
@@ -76,8 +76,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. (Location source retains "SIG" wording for API compatibility.)
# true if the location source is "SPOT", "ACTIVITY REF LOOKUP" or "GRID", or if the location source is "HOME QTH"
# and the DX callsign doesn't have a suffix like /P.
dx_location_good: bool = False
# DE (Spotter) info
@@ -125,11 +125,10 @@ class Spot:
# Activity info
# Activity (e.g. outdoor activity programme such as POTA). Still named "sig" for API backwards compatibility.
sig: str | None = None
# 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)
# Activity (e.g. outdoor activity programme such as POTA).
activity: str | None = None
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
activity_refs: list = field(default_factory=list)
# Timing info
@@ -159,12 +158,12 @@ class Spot:
def __post_init__(self):
"""Normalise fields that don't survive a plain dict to Spot conversion. This is used in the "add spot" API
endpoint where the client is submitting JSON, and we want to recreate a full Spot object, including nested
objects such as the sig_refs list.."""
objects such as the activity_refs list.."""
if self.sig_refs:
self.sig_refs = [
if self.activity_refs:
self.activity_refs = [
activity_ref if isinstance(activity_ref, ActivityRef) else ActivityRef(**activity_ref)
for activity_ref in self.sig_refs
for activity_ref in self.activity_refs
]
def infer_missing(self, credentials=None):
@@ -270,18 +269,20 @@ class Spot:
self.dx_location_source = LocationSourceForSpot.SPOT
# 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()
if not self.activity and self.activity_refs:
self.activity = self.activity_refs[0].activity.upper()
# 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:
activity = self.sig_refs[0].sig.upper()
if self.comment and self.activity_refs and self.activity_refs[0].activity:
activity = self.activity_refs[0].activity.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_activity_ref_if_missing(ActivityRef(id=ref_match.group(2).upper(), sig=activity))
self._append_activity_ref_if_missing(
ActivityRef(id=ref_match.group(2).upper(), activity=activity)
)
# 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.
@@ -292,11 +293,11 @@ class Spot:
# 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_activity
if not self.activity:
self.activity = found_activity
# 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.
# for that activity. If so, add that to the activity_refs list for this spot.
found_activity_info = get_activity_by_name(found_activity)
if found_activity_info and found_activity_info.has_refs and found_activity_info.ref_regex:
ref_matches = re.finditer(
@@ -306,7 +307,7 @@ class Spot:
)
for ref_match in ref_matches:
self._append_activity_ref_if_missing(
ActivityRef(id=ref_match.group(3).upper(), sig=found_activity)
ActivityRef(id=ref_match.group(3).upper(), activity=found_activity)
)
# See if the comment looks like it contains any activity references *without* the corresponding activity
@@ -322,17 +323,17 @@ class Spot:
# 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 = activity.name
if not self.activity:
self.activity = activity.name
self._append_activity_ref_if_missing(
ActivityRef(id=ref_match.group(2).upper(), sig=activity.name)
ActivityRef(id=ref_match.group(2).upper(), activity=activity.name)
)
# 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 activity_ref in self.sig_refs:
if self.activity_refs:
for activity_ref in self.activity_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:
@@ -345,15 +346,15 @@ class Spot:
):
self.dx_latitude = activity_ref.latitude
self.dx_longitude = activity_ref.longitude
if self.sig in (ActivityName.WAB, ActivityName.WAI, ActivityName.TILES):
if self.activity in (ActivityName.WAB, ActivityName.WAI, ActivityName.TILES):
self.dx_location_source = LocationSourceForSpot.GRID
else:
self.dx_location_source = LocationSourceForSpot.SIG_REF_LOOKUP
self.dx_location_source = LocationSourceForSpot.ACTIVITY_REF_LOOKUP
# 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
if self.activity_refs and not self.activity:
self.activity = self.activity_refs[0].activity
# Parse "de_grid<prop_mode>dx_grid" structures from the comment, e.g. "JN61ES(ES)JM56XT" or "JO02GQ<>KN17LG".
# These are common on cluster spots and can provide grid references in preference to e.g. QRZ lookup, as well as
@@ -406,32 +407,32 @@ class Spot:
self.dx_location_source = LocationSourceForSpot.GRID
# Set activities based on propagation mode
if self.propagation_mode == "Satellite" and not self.sig:
self.sig = ActivityName.SATELLITE
if self.propagation_mode == "Earth-Moon-Earth" and not self.sig:
self.sig = ActivityName.EME
if self.propagation_mode == "Satellite" and not self.activity:
self.activity = ActivityName.SATELLITE
if self.propagation_mode == "Earth-Moon-Earth" and not self.activity:
self.activity = ActivityName.EME
# Set activities based on the DX callsign suffix
if self.dx_call and not self.sig:
if self.dx_call and not self.activity:
if self.dx_call.upper().endswith(ActivityName.AERONAUTICAL_MOBILE):
self.sig = ActivityName.AERONAUTICAL_MOBILE
self.activity = ActivityName.AERONAUTICAL_MOBILE
elif self.dx_call.upper().endswith(ActivityName.MARITIME_MOBILE):
self.sig = ActivityName.MARITIME_MOBILE
self.activity = ActivityName.MARITIME_MOBILE
# Alright, now let's get really fancy. Check if the DX callsign matches one taking part in a currently
# running DXpedition which we know from the alerts list.
if self.dx_call and not self.sig:
if self.dx_call and not self.activity:
now = datetime.now(pytz.UTC).timestamp()
for alert in DATA_STORE.alerts.values():
if (
alert.sig == ActivityName.DXPEDITION
alert.activity == ActivityName.DXPEDITION
and alert.dx_calls
and alert.start_time
and alert.end_time
and alert.start_time < now < alert.end_time
and self.dx_call.upper() in [c.upper() for c in alert.dx_calls if c]
):
self.sig = ActivityName.DXPEDITION
self.activity = ActivityName.DXPEDITION
break
# DX Grid to lat/lon and vice versa in case one is missing
@@ -476,10 +477,10 @@ class Spot:
# 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:
qth += f" {self.sig_refs[0].name}"
if self.activity_refs:
qth = self.activity_refs[0].id
if self.activity_refs[0].name:
qth += f" {self.activity_refs[0].name}"
self.dx_qth = qth
else:
self.dx_qth = dx_call_info.qth
@@ -512,7 +513,7 @@ class Spot:
and self.dx_longitude
and (
self.dx_location_source == LocationSourceForSpot.SPOT
or self.dx_location_source == LocationSourceForSpot.SIG_REF_LOOKUP
or self.dx_location_source == LocationSourceForSpot.ACTIVITY_REF_LOOKUP
or self.dx_location_source == LocationSourceForSpot.GRID
or (self.dx_location_source == LocationSourceForSpot.HOME_QTH and "/" not in (self.dx_call or ""))
)
@@ -532,7 +533,7 @@ class Spot:
# Icon for the spot should be the icon of its activity if known, otherwise a radio tower
self.icon = "fa-tower-cell"
if self.sig and (activity_icon := get_icon_for_activity(self.sig)):
if self.activity and (activity_icon := get_icon_for_activity(self.activity)):
self.icon = activity_icon
except Exception:
@@ -547,13 +548,13 @@ class Spot:
"""Append an activity ref to the list, so long as it's not already there."""
new_activity_ref.id = new_activity_ref.id.strip().upper()
new_activity_ref.sig = new_activity_ref.sig.strip().upper()
new_activity_ref.activity = new_activity_ref.activity.strip().upper()
if new_activity_ref.id == "":
return
for activity_ref in self.sig_refs:
if activity_ref.id == new_activity_ref.id and activity_ref.sig == new_activity_ref.sig:
for activity_ref in self.activity_refs:
if activity_ref.id == new_activity_ref.id and activity_ref.activity == new_activity_ref.activity:
return
self.sig_refs.append(new_activity_ref)
self.activity_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