Icon lookup for SIGs in preparation for #47. Improved ZLOTA spotter lookup.

This commit is contained in:
Ian Renton
2025-10-20 10:33:18 +01:00
parent ae72649df8
commit a21782cb62
18 changed files with 89 additions and 60 deletions

View File

@@ -42,21 +42,9 @@ class ParksNPeaks(HTTPAlertProvider):
start_time=start_time,
is_dxpedition=False)
# PNP supports a bunch of programs which should have different icons
if alert.sig == "SiOTA":
alert.icon = "wheat-awn"
elif alert.sig == "ZLOTA":
alert.icon = "kiwi-bird"
elif alert.sig == "KRMNPA":
alert.icon = "earth-oceania"
elif alert.sig in ["POTA", "SOTA", "WWFF"]:
# Don't care about an icon as this will be rejected anyway, we have better data from POTA/SOTA/WWFF direct
alert.icon = ""
else:
# Unknown programme we've never seen before
logging.warn(
"PNP alert found with sig " + alert.sig + ", developer needs to add support for this and set an icon!")
alert.icon = "question"
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
if alert.sig not in ["POTA", "SOTA", "WWFF", "SiOTA", "ZLOTA", "KRMNPA"]:
logging.warn("PNP alert found with sig " + alert.sig + ", developer needs to add support for this!")
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
# the alert list.

View File

@@ -27,7 +27,6 @@ class POTA(HTTPAlertProvider):
sig="POTA",
sig_refs=[source_alert["reference"]],
sig_refs_names=[source_alert["name"]],
icon="tree",
start_time=datetime.strptime(source_alert["startDate"] + source_alert["startTime"],
"%Y-%m-%d%H:%M").replace(tzinfo=pytz.UTC).timestamp(),
end_time=datetime.strptime(source_alert["endDate"] + source_alert["endTime"],

View File

@@ -28,7 +28,6 @@ class SOTA(HTTPAlertProvider):
sig="SOTA",
sig_refs=[source_alert["associationCode"] + "/" + source_alert["summitCode"]],
sig_refs_names=[source_alert["summitDetails"]],
icon="mountain-sun",
start_time=datetime.strptime(source_alert["dateActivated"],
"%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=pytz.UTC).timestamp(),
is_dxpedition=False)

View File

@@ -26,7 +26,6 @@ class WWFF(HTTPAlertProvider):
comment=source_alert["remarks"],
sig="WWFF",
sig_refs=[source_alert["reference"]],
icon="seedling",
start_time=datetime.strptime(source_alert["utc_start"],
"%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp(),
end_time=datetime.strptime(source_alert["utc_end"],

View File

@@ -1,15 +1,31 @@
from core.config import SERVER_OWNER_CALLSIGN
from data.band import Band
from data.sig import SIG
# General software
SOFTWARE_NAME = "Spothole by M0TRT"
SOFTWARE_VERSION = "0.1"
# HTTP headers used for spot providers that use HTTP
HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + " " + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"}
HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"}
# Special Interest Groups
SIGS = ["POTA", "SOTA", "WWFF", "GMA", "WWBOTA", "HEMA", "MOTA", "ARLHS", "ILLW", "SiOTA", "WCA", "ZLOTA", "IOTA", "KRMNPA"]
SIGS = [
SIG(name="POTA", description="Parks on the Air", icon="tree"),
SIG(name="SOTA", description="Summits on the Air", icon="mountain-sun"),
SIG(name="WWFF", description="World Wide Flora & Fauna", icon="seedling"),
SIG(name="GMA", description="Global Mountain Activity", icon="person-hiking"),
SIG(name="WWBOTA", description="Worldwide Bunkers on the Air", icon="radiation"),
SIG(name="HEMA", description="HuMPs Excluding Marilyns Award", icon="mound"),
SIG(name="IOTA", description="Islands on the Air", icon="umbrella-beach"),
SIG(name="MOTA", description="Mills on the Air", icon="fan"),
SIG(name="ARLHS", description="Amateur Radio Lighthouse Society", icon="tower-observation"),
SIG(name="ILLW", description="International Lighthouse & Lightship Weekend", icon="tower-observation"),
SIG(name="SiOTA", description="Silos on the Air", icon="wheat-awn"),
SIG(name="WCA", description="World Castles Award", icon="chess-rook"),
SIG(name="ZLOTA", description="New Zealand on the Air", icon="kiwi-bird"),
SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award", icon="earth-oceania")
]
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".
CW_MODES = ["CW"]

View File

@@ -0,0 +1,8 @@
from core.constants import SIGS
# Utility function to get the icon for a named SIG. If no match is found, the "circle-question" icon will be returned.
def get_icon_for_sig(sig):
for s in SIGS:
if s.name == sig:
return s.icon
return "circle-question"

View File

@@ -9,6 +9,7 @@ import pytz
from core.constants import DXCC_FLAGS
from core.lookup_helper import lookup_helper
from core.utility_functions import get_icon_for_sig
# Data class that defines an alert.
@@ -59,7 +60,7 @@ class Alert:
# Activation score. SOTA only
activation_score: int = None
# Icon, from the Font Awesome set. This is fairly opinionated but is here to help the alerthole web UI and Field alertter. Does not include the "fa-" prefix.
icon: str = "question"
icon: str = None
# Whether this alert is for a DXpedition, as opposed to e.g. an xOTA programme.
is_dxpedition: bool = False
# Where we got the alert from, e.g. "POTA", "SOTA"...
@@ -100,6 +101,10 @@ class Alert:
if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag:
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id]
# Icon from SIG
if self.sig and not self.icon:
self.icon = get_icon_for_sig(self.sig)
# DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from
# the actual alertting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon instead of
# the one from the park reference they're at.

12
data/sig.py Normal file
View File

@@ -0,0 +1,12 @@
from dataclasses import dataclass
# Data class that defines a Special Interest Group.
@dataclass
class SIG:
# SIG name, e.g. "POTA"
name: str
# Description, e.g. "Parks on the Air"
description: str
# Icon to use for it, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI
# and Field Spotter. Does not include the "fa-" prefix.
icon: str

View File

@@ -11,6 +11,7 @@ from pyhamtools.locator import locator_to_latlong, latlong_to_locator
from core.constants import DXCC_FLAGS
from core.lookup_helper import lookup_helper
from core.utility_functions import get_icon_for_sig
# Data class that defines a spot.
@@ -113,7 +114,7 @@ class Spot:
# Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field
# Spotter. Does not include the "fa-" prefix.
icon: str = "question"
icon: str = None
# Colour to represent this spot, if a client chooses to colour spots based on their frequency band, using PSK
# Reporter's default colours. HTML colour e.g. hex. A contrast colour is also provided which will be black or white.
band_color: str = None
@@ -218,6 +219,10 @@ class Spot:
if self.mode and not self.mode_type:
self.mode_type = lookup_helper.infer_mode_type_from_mode(self.mode)
# Icon from SIG
if self.sig and not self.icon:
self.icon = get_icon_for_sig(self.sig)
# DX Grid to lat/lon and vice versa
if self.dx_grid and not self.dx_latitude:
ll = locator_to_latlong(self.dx_grid)
@@ -237,7 +242,8 @@ class Spot:
# Clean up comments
if self.comment:
comment = re.sub(r"\[.*]:", "", self.comment)
comment = re.sub(r"\(de [A-Za-z0-9]*\)", "", self.comment)
comment = re.sub(r"\[.*]:", "", comment)
comment = re.sub(r"\[.*]", "", comment)
comment = re.sub(r"\"\"", "", comment)
self.comment = comment.strip()
@@ -269,8 +275,8 @@ class Spot:
self.dx_location_good = self.dx_location_source == "SPOT" or (
self.dx_location_source == "QRZ" and not "/" in self.dx_call)
# DE of "RBNHOLE" and "SOTAMAT" are not things we can look up location for
if self.de_call != "RBNHOLE" and self.de_call != "SOTAMAT":
# DE of "RBNHOLE", "SOTAMAT" and "ZLOTA" are not things we can look up location for
if self.de_call != "RBNHOLE" and self.de_call != "SOTAMAT" and self.de_call != "ZLOTA":
# DE operator position lookup, using QRZ.com.
if self.de_call and not self.de_latitude:
latlon = lookup_helper.infer_latlon_from_callsign_qrz(self.de_call)

View File

@@ -75,6 +75,8 @@ class DXCluster(SpotProvider):
icon="desktop",
time=spot_datetime.timestamp())
# See if the comment looks like it contains a SIG (and optionally SIG reference)
# Add to our list
self.submit(spot)

View File

@@ -57,27 +57,20 @@ class GMA(HTTPSpotProvider):
match ref_info["reftype"]:
case "Summit":
spot.sig = "GMA"
spot.icon = "mountain"
case "IOTA Island":
spot.sig = "IOTA"
spot.icon = "umbrella-beach"
case "Lighthouse (ILLW)":
spot.sig = "ILLW"
spot.icon = "tower-observation"
case "Lighthouse (ARLHS)":
spot.sig = "ARLHS"
spot.icon = "tower-observation"
case "Castle":
spot.sig = "WCA/COTA"
spot.icon = "chess-rook"
spot.sig = "WCA"
case "Mill":
spot.sig = "MOTA"
spot.icon = "fan"
case _:
logging.warn("GMA spot found with ref type " + ref_info[
"reftype"] + ", developer needs to figure out an icon for this!")
"reftype"] + ", developer needs to add support for this!")
spot.sig = ref_info["reftype"]
spot.icon = "person-hiking"
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.

View File

@@ -54,7 +54,6 @@ class HEMA(HTTPSpotProvider):
sig="HEMA",
sig_refs=[spot_items[3].upper()],
sig_refs_names=[spot_items[4]],
icon="mound",
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(tzinfo=pytz.UTC).timestamp(),
dx_latitude=float(spot_items[7]),
dx_longitude=float(spot_items[8]))

View File

@@ -1,5 +1,6 @@
import csv
import logging
import re
from datetime import datetime, timedelta
import pytz
@@ -32,7 +33,7 @@ class ParksNPeaks(HTTPSpotProvider):
spot = Spot(source=self.name,
source_id=source_spot["actID"],
dx_call=source_spot["actCallsign"].upper(),
de_call=source_spot["actSpoter"].upper(), # typo exists in API
de_call=source_spot["actSpoter"].upper() if source_spot["actSpoter"] != "" else None, # typo exists in API
freq=float(source_spot["actFreq"].replace(",", "")) * 1000000 if (
source_spot["actFreq"] != "") else None,
# Seen PNP spots with empty frequency, and with comma-separated thousands digits
@@ -47,21 +48,14 @@ class ParksNPeaks(HTTPSpotProvider):
if "actLocation" in source_spot and source_spot["actLocation"] != "":
spot.sig_refs_names = [source_spot["actLocation"]]
# PNP supports a bunch of programs which should have different icons
if spot.sig == "SiOTA":
spot.icon = "wheat-awn"
elif spot.sig == "ZLOTA":
spot.icon = "kiwi-bird"
elif spot.sig == "KRMNPA":
spot.icon = "earth-oceania"
elif spot.sig in ["POTA", "SOTA", "WWFF"]:
# Don't care about an icon as this will be rejected anyway, we have better data from POTA/SOTA/WWFF direct
spot.icon = ""
else:
# Unknown programme we've never seen before
logging.warn(
"PNP spot found with sig " + spot.sig + ", developer needs to add support for icon and grid/lat/lon lookup!")
spot.icon = "question"
# Extract a de_call if it's in the comment but not in the "actSpoter" field
m = re.search(r"\(de ([A-Za-z0-9]*)\)", spot.comment)
if (not spot.de_call or spot.de_call == "ZLOTA") and m is not None:
spot.de_call = m.group(1)
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
if spot.sig not in ["POTA", "SOTA", "WWFF", "SiOTA", "ZLOTA", "KRMNPA"]:
logging.warn("PNP spot found with sig " + spot.sig + ", developer needs to add support for this!")
# SiOTA lat/lon/grid lookup
if spot.sig == "SiOTA":
@@ -82,8 +76,6 @@ class ParksNPeaks(HTTPSpotProvider):
spot.sig_refs_names = [asset["name"]]
spot.dx_latitude = asset["y"]
spot.dx_longitude = asset["x"]
# Junk the "DE call", PNP always returns "ZLOTA" as the spotter for ZLOTA spots
spot.de_call = None
break
# Note there is currently no support for KRMNPA location lookup, see issue #61.

View File

@@ -30,7 +30,6 @@ class POTA(HTTPSpotProvider):
sig_refs=[source_spot["reference"]],
sig_refs_names=[source_spot["name"]],
sig_refs_urls=["https://pota.app/#/park/" + source_spot["reference"]],
icon="tree",
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=pytz.UTC).timestamp(),
dx_grid=source_spot["grid6"],
dx_latitude=source_spot["latitude"],

View File

@@ -51,7 +51,6 @@ class SOTA(HTTPSpotProvider):
sig_refs=[source_spot["summitCode"]],
sig_refs_names=[source_spot["summitName"]],
sig_refs_urls=["https://www.sotadata.org.uk/en/summit/" + source_spot["summitCode"]],
icon="mountain-sun",
time=datetime.fromisoformat(source_spot["timeStamp"]).timestamp(),
activation_score=source_spot["points"])

View File

@@ -38,7 +38,6 @@ class WWBOTA(SSESpotProvider):
sig="WWBOTA",
sig_refs=refs,
sig_refs_names=ref_names,
icon="radiation",
time=datetime.fromisoformat(source_spot["time"]).timestamp(),
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For
# now, we will just pick the first one to use as our grid, latitude and longitude.

View File

@@ -30,7 +30,6 @@ class WWFF(HTTPSpotProvider):
sig_refs=[source_spot["reference"]],
sig_refs_names=[source_spot["reference_name"]],
sig_refs_urls=["https://wwff.co/directory/?showRef=" + source_spot["reference"]],
icon="seedling",
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"])

View File

@@ -413,8 +413,7 @@ paths:
type: array
description: An array of all the supported Special Interest Groups.
items:
type: string
example: "POTA"
$ref: '#/components/schemas/SIG'
sources:
type: array
description: An array of all the supported data sources.
@@ -976,3 +975,19 @@ components:
type: string
description: Black or white, whichever provides the best contrast against the band colour.
example: white
SIG:
type: object
properties:
name:
type: string
description: The abbreviated name of the SIG
example: POTA
description:
type: string
description: The full name of the SIG
example: Parks on the Air
icon:
type: string
description: Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field Spotter. Does not include the "fa-" prefix.
example: tree