Update API to have a sensible grouping of sig_refs rather than separate arrays of sig_refs, sig_refs_names and sig_refs_urls

This commit is contained in:
Ian Renton
2025-10-31 09:51:54 +00:00
parent 6c95e845a4
commit 0c5b5f2062
21 changed files with 93 additions and 93 deletions

View File

@@ -6,6 +6,7 @@ import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef
# Alert provider for Parks n Peaks
@@ -38,8 +39,7 @@ class ParksNPeaks(HTTPAlertProvider):
freqs_modes=source_alert["Freq"] + " " + source_alert["MODE"],
comment=source_alert["Comments"],
sig=source_alert["Class"],
sig_refs=[sig_ref],
sig_refs_names=[sig_ref_name],
sig_refs=[SIGRef(id=sig_ref, name=sig_ref_name)],
icon=get_icon_for_sig(source_alert["Class"]),
start_time=start_time,
is_dxpedition=False)

View File

@@ -5,6 +5,7 @@ import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef
# Alert provider for Parks on the Air
@@ -26,8 +27,7 @@ class POTA(HTTPAlertProvider):
freqs_modes=source_alert["frequencies"],
comment=source_alert["comments"],
sig="POTA",
sig_refs=[source_alert["reference"]],
sig_refs_names=[source_alert["name"]],
sig_refs=[SIGRef(id=source_alert["reference"], name=source_alert["name"], url="https://pota.app/#/park/" + source_alert["reference"])],
icon=get_icon_for_sig("POTA"),
start_time=datetime.strptime(source_alert["startDate"] + source_alert["startTime"],
"%Y-%m-%d%H:%M").replace(tzinfo=pytz.UTC).timestamp(),

View File

@@ -5,6 +5,7 @@ import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef
# Alert provider for Summits on the Air
@@ -27,8 +28,7 @@ class SOTA(HTTPAlertProvider):
freqs_modes=source_alert["frequency"],
comment=source_alert["comments"],
sig="SOTA",
sig_refs=[source_alert["associationCode"] + "/" + source_alert["summitCode"]],
sig_refs_names=[source_alert["summitDetails"]],
sig_refs=[SIGRef(id=source_alert["associationCode"] + "/" + source_alert["summitCode"], name=source_alert["summitDetails"], url="https://www.sotadata.org.uk/en/summit/" + source_alert["summitCode"])],
icon=get_icon_for_sig("SOTA"),
start_time=datetime.strptime(source_alert["dateActivated"],
"%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=pytz.UTC).timestamp(),

View File

@@ -6,6 +6,7 @@ from rss_parser import RSSParser
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef
# Alert provider for Wainwrights on the Air
@@ -54,8 +55,7 @@ class WOTA(HTTPAlertProvider):
freqs_modes=freqs_modes,
comment=comment,
sig="WOTA",
sig_refs=[ref] if ref else [],
sig_refs_names=[ref_name] if ref_name else [],
sig_refs=[SIGRef(id=ref, name=ref_name, url="https://www.wota.org.uk/MM_" + ref)] if ref else [],
icon=get_icon_for_sig("WOTA"),
start_time=time.timestamp())

View File

@@ -5,6 +5,7 @@ import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert
from data.sig_ref import SIGRef
# Alert provider for Worldwide Flora and Fauna
@@ -26,7 +27,7 @@ class WWFF(HTTPAlertProvider):
freqs_modes=source_alert["band"] + " " + source_alert["mode"],
comment=source_alert["remarks"],
sig="WWFF",
sig_refs=[source_alert["reference"]],
sig_refs=[SIGRef(id=source_alert["reference"], url="https://wwff.co/directory/?showRef=" + source_alert["reference"])],
icon=get_icon_for_sig("WWFF"),
start_time=datetime.strptime(source_alert["utc_start"],
"%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp(),

View File

@@ -55,8 +55,6 @@ class Alert:
sig: str = None
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
sig_refs: list = None
# SIG reference names
sig_refs_names: list = None
# 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.

12
data/sig_ref.py Normal file
View File

@@ -0,0 +1,12 @@
from dataclasses import dataclass
# Data class that defines a Special Interest Group "info" or reference. As well as the basic reference ID we include a
# name and a lookup URL.
@dataclass
class SIGRef:
# Reference ID, e.g. "GB-0001".
id: str
# Name of the reference, e.g. "Null Country Park", if known.
name: str = None
# URL to look up more information about the reference, if known.
url: str = None

View File

@@ -102,10 +102,6 @@ class Spot:
sig: str = None
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
sig_refs: list = None
# SIG reference names
sig_refs_names: list = None
# SIG reference URLs
sig_refs_urls: list = None
# Activation score. SOTA only
activation_score: int = None

View File

@@ -9,6 +9,7 @@ import telnetlib3
from core.constants import SIGS
from core.sig_utils import ANY_SIG_REGEX, ANY_XOTA_SIG_REF_REGEX, get_icon_for_sig, get_ref_regex_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from core.config import SERVER_OWNER_CALLSIGN
from spotproviders.spot_provider import SpotProvider
@@ -90,7 +91,7 @@ class DXCluster(SpotProvider):
if ref_regex:
sig_ref_match = re.search(r"(^|\W)" + spot.sig + r"($|\W)(" + ref_regex + r")($|\W)", spot.comment, re.IGNORECASE)
if sig_ref_match:
spot.sig_refs = [sig_ref_match.group(3).upper()]
spot.sig_refs = [SIGRef(id=sig_ref_match.group(3).upper())]
# Add to our list
self.submit(spot)

View File

@@ -6,6 +6,7 @@ from requests_cache import CachedSession
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -35,9 +36,7 @@ class GMA(HTTPSpotProvider):
mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None,
# Filter out some weird mode strings
comment=source_spot["TEXT"],
sig_refs=[source_spot["REF"]],
sig_refs_names=[source_spot["NAME"]],
sig_refs_urls=["https://www.cqgma.org/zinfo.php?ref=" + source_spot["REF"]],
sig_refs=[SIGRef(id=source_spot["REF"], name=source_spot["NAME"], url="https://www.cqgma.org/zinfo.php?ref=" + source_spot["REF"])],
time=datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M").replace(
tzinfo=pytz.UTC).timestamp(),
dx_latitude=float(source_spot["LAT"]) if (source_spot["LAT"] and source_spot["LAT"] != "") else None,

View File

@@ -6,6 +6,7 @@ import requests
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -53,8 +54,7 @@ class HEMA(HTTPSpotProvider):
mode=freq_mode_match.group(2).upper(),
comment=spotter_comment_match.group(2),
sig="HEMA",
sig_refs=[spot_items[3].upper()],
sig_refs_names=[spot_items[4]],
sig_refs=[SIGRef(id=spot_items[3].upper(), name=spot_items[4])],
icon=get_icon_for_sig("HEMA"),
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(tzinfo=pytz.UTC).timestamp(),
dx_latitude=float(spot_items[7]),

View File

@@ -8,6 +8,7 @@ from requests_cache import CachedSession
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -38,14 +39,14 @@ class ParksNPeaks(HTTPSpotProvider):
mode=source_spot["actMode"].upper(),
comment=source_spot["actComments"],
sig=source_spot["actClass"],
sig_refs=[source_spot["actSiteID"]],
sig_refs=[SIGRef(id=source_spot["actSiteID"])],
icon=get_icon_for_sig(source_spot["actClass"]),
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp())
# Free text location is not present in all spots, so only add it if it's set
if "actLocation" in source_spot and source_spot["actLocation"] != "":
spot.sig_refs_names = [source_spot["actLocation"]]
spot.sig_refs[0].name = source_spot["actLocation"]
# 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)

View File

@@ -6,6 +6,7 @@ from requests_cache import CachedSession
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig, get_ref_regex_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -36,9 +37,7 @@ class POTA(HTTPSpotProvider):
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
sig="POTA",
sig_refs=[source_spot["reference"]],
sig_refs_names=[source_spot["name"]],
sig_refs_urls=["https://pota.app/#/park/" + source_spot["reference"]],
sig_refs=[SIGRef(id=source_spot["reference"], name=source_spot["name"], url="https://pota.app/#/park/" + source_spot["reference"])],
icon=get_icon_for_sig("POTA"),
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp(),
@@ -49,15 +48,17 @@ class POTA(HTTPSpotProvider):
# Sometimes we can get other refs in the comments for n-fer activations, extract them
all_comment_refs = re.findall(get_ref_regex_for_sig("POTA"), spot.comment)
for r in all_comment_refs:
if r not in spot.sig_refs:
spot.sig_refs.append(r.upper())
spot.sig_refs_urls.append("https://pota.app/#/park/" + r.upper())
if r not in list(map(lambda ref: ref.id, spot.sig_refs)):
ref = SIGRef(id=r.upper(), url="https://pota.app/#/park/" + r.upper())
# Now we need to look up the name of that reference from the API, because the comment won't have it
park_response = self.PARK_DATA_CACHE.get(self.PARK_URL_ROOT + r.upper(), headers=HTTP_HEADERS)
park_data = park_response.json()
if park_data and "name" in park_data:
spot.sig_refs_names.append(park_data["name"])
ref.name = park_data["name"]
# Finally append our new reference to the spot's reference list
spot.sig_refs.append(ref)
# 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

@@ -6,6 +6,7 @@ from requests_cache import CachedSession
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -49,9 +50,7 @@ class SOTA(HTTPSpotProvider):
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
sig="SOTA",
sig_refs=[source_spot["summitCode"]],
sig_refs_names=[source_spot["summitName"]],
sig_refs_urls=["https://www.sotadata.org.uk/en/summit/" + source_spot["summitCode"]],
sig_refs=[SIGRef(id=source_spot["summitCode"], name=source_spot["summitName"], url="https://www.sotadata.org.uk/en/summit/" + source_spot["summitCode"])],
icon=get_icon_for_sig("SOTA"),
time=datetime.fromisoformat(source_spot["timeStamp"]).timestamp(),
activation_score=source_spot["points"])

View File

@@ -6,6 +6,7 @@ from rss_parser import RSSParser
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -68,17 +69,15 @@ class WOTA(HTTPSpotProvider):
mode=mode,
comment=comment,
sig="WOTA",
sig_refs=[ref] if ref else [],
sig_refs_names=[ref_name] if ref_name else [],
sig_refs_urls="https://www.wota.org.uk/MM_" + ref if ref else [],
sig_refs=[SIGRef(id=ref, name=ref_name, url="https://www.wota.org.uk/MM_" + ref)] if ref else [],
icon=get_icon_for_sig("WOTA"),
time=time.timestamp())
# WOTA name/lat/lon lookup
# WOTA name/grid/lat/lon lookup
wota_data = self.LIST_CACHE.get(self.LIST_URL, headers=HTTP_HEADERS).json()
for feature in wota_data["features"]:
if feature["properties"]["wotaId"] == spot.sig_refs[0]:
spot.sig_refs_names = [feature["properties"]["title"]]
spot.sig_refs[0].name = feature["properties"]["title"]
spot.dx_latitude = feature["geometry"]["coordinates"][1]
spot.dx_longitude = feature["geometry"]["coordinates"][0]
spot.dx_grid = feature["properties"]["qthLocator"]

View File

@@ -2,6 +2,7 @@ import json
from datetime import datetime
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.sse_spot_provider import SSESpotProvider
@@ -18,17 +19,12 @@ class WWBOTA(SSESpotProvider):
# Convert to our spot format. First we unpack references, because WWBOTA spots can have more than one for
# n-fer activations.
refs = []
ref_names = []
ref_urls = []
for ref in source_spot["references"]:
refs.append(ref["reference"])
ref_names.append(ref["name"])
# Bunkerbase URLs only work for UK bunkers, so only add a URL if we have a B/G prefix. In theory this could
# lead to array alignment mismatches if there was e.g. a B/F bunker followed by a B/G one, we'd end up with
# the B/G URL in index 0. But in practice there are no overlaps between B/G bunkers and any others, so an
# activation will either be entirely B/G or not B/G at all.
sigref = SIGRef(id=ref["reference"], name=ref["name"])
# Bunkerbase URLs only work for UK bunkers, so only add a URL if we have a B/G prefix.
if ref["reference"].startswith("B/G"):
ref_urls.append("https://bunkerwiki.org/?s=" + ref["reference"])
sigref.url="https://bunkerwiki.org/?s=" + ref["reference"]
refs.append(sigref)
spot = Spot(source=self.name,
dx_call=source_spot["call"].upper(),
@@ -38,7 +34,6 @@ class WWBOTA(SSESpotProvider):
comment=source_spot["comment"],
sig="WWBOTA",
sig_refs=refs,
sig_refs_names=ref_names,
icon=get_icon_for_sig("WWBOTA"),
time=datetime.fromisoformat(source_spot["time"]).timestamp(),
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For

View File

@@ -3,6 +3,7 @@ from datetime import datetime
import pytz
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -28,9 +29,7 @@ class WWFF(HTTPSpotProvider):
mode=source_spot["mode"].upper(),
comment=source_spot["remarks"],
sig="WWFF",
sig_refs=[source_spot["reference"]],
sig_refs_names=[source_spot["reference_name"]],
sig_refs_urls=["https://wwff.co/directory/?showRef=" + source_spot["reference"]],
sig_refs=[SIGRef(id=source_spot["reference"], name=source_spot["reference_name"], url="https://wwff.co/directory/?showRef=" + source_spot["reference"])],
icon=get_icon_for_sig("WWFF"),
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
dx_latitude=source_spot["latitude"],

View File

@@ -8,6 +8,7 @@ from requests_cache import CachedSession
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -41,16 +42,14 @@ class ZLOTA(HTTPSpotProvider):
mode=source_spot["mode"].upper().strip(),
comment=source_spot["comments"],
sig="ZLOTA",
sig_refs=[source_spot["reference"]],
sig_refs_names=[source_spot["name"]],
sig_refs=[SIGRef(id=source_spot["reference"], name=source_spot["name"])],
icon=get_icon_for_sig("ZLOTA"),
time=datetime.fromisoformat(source_spot["referenced_time"]).astimezone(pytz.UTC).timestamp())
# ZLOTA name/lat/lon lookup
# ZLOTA lat/lon lookup
zlota_data = self.LIST_CACHE.get(self.LIST_URL, headers=HTTP_HEADERS).json()
for asset in zlota_data:
if asset["code"] == spot.sig_refs[0]:
spot.sig_refs_names = [asset["name"]]
spot.dx_latitude = asset["y"]
spot.dx_longitude = asset["x"]
break

View File

@@ -60,9 +60,12 @@ paths:
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
- name: sig
in: query
description: "Limit the spots to only ones from one or more Special Interest Groups provided as an argument. To select more than one SIG, supply a comma-separated list."
@@ -279,9 +282,12 @@ paths:
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
- name: sig
in: query
description: "Limit the alerts to only ones from one or more Special Interest Groups. To select more than one SIG, supply a comma-separated list."
@@ -513,6 +519,22 @@ paths:
components:
schemas:
SIGRef:
type: object
properties:
id:
type: string
description: SIG reference ID.
example: GB-0001
name:
type: string
description: SIG reference name
example: Null Country Park
url:
type: string
description: SIG reference URL, which the user can look up for more information
example: "https://pota.app/#/park/GB-0001"
Spot:
type: object
properties:
@@ -752,21 +774,8 @@ components:
sig_refs:
type: array
items:
type: string
$ref: '#/components/schemas/SIGRef'
description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
example: GB-0001
sig_refs_names:
type: array
items:
type: string
description: SIG reference names
example: Null Country Park
sig_refs_urls:
type: array
items:
type: string
description: SIG reference URLs, which the user can look up for more information
example: "https://pota.app/#/park/GB-0001"
activation_score:
type: integer
description: Activation score. SOTA only
@@ -798,9 +807,12 @@ components:
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
example: POTA
source_id:
type: string
@@ -915,15 +927,8 @@ components:
sig_refs:
type: array
items:
type: string
$ref: '#/components/schemas/SIGRef'
description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
example: GB-0001
sig_refs_names:
type: array
items:
type: string
description: SIG reference names
example: Null Country Park
activation_score:
type: integer
description: Activation score. SOTA only
@@ -943,9 +948,12 @@ components:
- GMA
- HEMA
- ParksNPeaks
- ZLOTA
- WOTA
- Cluster
- RBN
- APRS-IS
- UKPacketNet
example: POTA
source_id:
type: string

View File

@@ -106,15 +106,11 @@ function getTooltipText(s) {
// Format sig_refs
var sig_refs = "";
if (s["sig_refs"] && s["sig_refs_urls"] && s["sig_refs"].length == s["sig_refs_urls"].length) {
items = s["sig_refs"].map(s => `<span class='nowrap'>${s}</span>`)
for (var i = 0; i < items.length; i++) {
items[i] = `<a href='${s["sig_refs_urls"][i]}' target='_new' class='sig-ref-link'>${items[i]}</a>`
var items = []
for (var i = 0; i < s["sig_refs"].length; i++) {
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
}
sig_refs = items.join(", ");
} else if (s["sig_refs"]) {
sig_refs = s["sig_refs"].map(s => `<span class='nowrap'>${s}</span>`).join(", ");
}
// DX
ttt = `<span class='nowrap'><span class='icon-wrapper'>${dx_flag}</span> <a href='https://www.qrz.com/db/${dx_call}' target='_blank' class="dx-link">${dx_call}</a></span><br/>`;

View File

@@ -161,15 +161,11 @@ function updateTable() {
// Format sig_refs
var sig_refs = "";
if (s["sig_refs"] && s["sig_refs_urls"] && s["sig_refs"].length == s["sig_refs_urls"].length && s["sig_refs"].length == s["sig_refs_names"].length) {
items = s["sig_refs"].map(s => `<span class='nowrap'>${s}</span>`)
for (var i = 0; i < items.length; i++) {
items[i] = `<a href='${s["sig_refs_urls"][i]}' title='${s["sig_refs_names"][i]}' target='_new' class='sig-ref-link'>${items[i]}</a>`
var items = []
for (var i = 0; i < s["sig_refs"].length; i++) {
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
}
sig_refs = items.join(", ");
} else if (s["sig_refs"]) {
sig_refs = s["sig_refs"].map(s => `<span class='nowrap'>${s}</span>`).join(", ");
}
// Format DE flag
var de_flag = "<i class='fa-solid fa-circle-question'></i>";