mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 14:27:42 +00:00
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:
+6
-6
@@ -128,12 +128,12 @@ spot_providers:
|
|||||||
name: "C3 TOTA"
|
name: "C3 TOTA"
|
||||||
enabled: false
|
enabled: false
|
||||||
url: "wss://39c3.totawatch.de/api/spot/live"
|
url: "wss://39c3.totawatch.de/api/spot/live"
|
||||||
# For the "XOTA" provider, a SIG must be set menually here because xOTA is a generic backend for xOTA
|
# For the "XOTA" provider, an activity must be set manually here because xOTA is a generic backend for xOTA
|
||||||
# programmes and so different URLs potentially provide different programmes.
|
# programmes and so different URLs potentially provide different programmes.
|
||||||
sig: "Toilets"
|
sig: "Toilets"
|
||||||
# For Toilets on the Air, we prefix the SIG references (T-01 etc) with some characters that define the conference:
|
# For Toilets on the Air, we prefix the activity references (T-01 etc) with some characters that define the
|
||||||
# C3, EH or HOPE - so we can look up the correct locations in our database, because each conference starts from T-01
|
# conference: C3, EH or HOPE - so we can look up the correct locations in our database, because each conference
|
||||||
# but refers to a toilet in a different building (or continent!)
|
# starts from T-01 but refers to a toilet in a different building (or continent!)
|
||||||
sig_ref_prefix: "C3"
|
sig_ref_prefix: "C3"
|
||||||
|
|
||||||
- class: "XOTA"
|
- class: "XOTA"
|
||||||
@@ -216,8 +216,8 @@ static_data_providers:
|
|||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
|
||||||
# SIG reference data providers to use. These allow Spothole to download, for example, the WWFF directory that maps WWFF
|
# Activity reference data providers to use. These allow Spothole to download, for example, the WWFF directory that
|
||||||
# park IDs to their name and location.
|
# maps WWFF park IDs to their name and location.
|
||||||
sig_ref_data_providers:
|
sig_ref_data_providers:
|
||||||
- class: "POTA"
|
- class: "POTA"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from pyhamtools.locator import latlong_to_locator, locator_to_latlong
|
||||||
|
|
||||||
|
from core.constants import ACTIVITIES
|
||||||
|
from core.data_store import DATA_STORE
|
||||||
|
from core.enums import 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
|
||||||
|
for activity in ACTIVITIES:
|
||||||
|
if activity.name.upper() == activity_name.upper():
|
||||||
|
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() == "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() == "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() == "HEMA" or activity_name.upper() == "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() == "TILES":
|
||||||
|
# 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() == "WAB" or activity_name.upper() == "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() == "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() == "GMA Islands":
|
||||||
|
# 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("IOTA", ref_id)
|
||||||
|
gma_lookup = get_activity_ref_info("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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from core.constants import ACTIVITIES
|
||||||
|
|
||||||
|
|
||||||
|
def get_ref_regex_for_activity(activity):
|
||||||
|
"""Utility function to get the regex string for an activity reference for a named activity. If no match is
|
||||||
|
found, None will be returned."""
|
||||||
|
|
||||||
|
for a in ACTIVITIES:
|
||||||
|
if a.name.upper() == activity.upper():
|
||||||
|
return a.ref_regex
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_activity_name_from_comment_name(activity):
|
||||||
|
"""Utility function to get the name of an activity from its "comment name". Generally these will be the same
|
||||||
|
but there are some cases (e.g. is "TOTA" Towers, Tiles or Toilets?) where we need to transform one to the
|
||||||
|
other."""
|
||||||
|
|
||||||
|
for a in ACTIVITIES:
|
||||||
|
if any(n.upper() == activity.upper() for n in a.comment_names):
|
||||||
|
return a.name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Regex matching any activity's "comment name", i.e. how it may be referred to in spot comments
|
||||||
|
ANY_ACTIVITY_REGEX = rf"({'|'.join(n for a in ACTIVITIES for n in a.comment_names)})"
|
||||||
+113
-113
@@ -1,7 +1,7 @@
|
|||||||
from core.config import SERVER_OWNER_CALLSIGN
|
from core.config import SERVER_OWNER_CALLSIGN
|
||||||
from core.enums import SIGRefType, SIGType
|
from core.enums import ActivityRefType, ActivityType
|
||||||
|
from data.activity import Activity
|
||||||
from data.band import Band
|
from data.band import Band
|
||||||
from data.sig import SIG
|
|
||||||
|
|
||||||
# General software
|
# General software
|
||||||
SOFTWARE_VERSION = "2.1"
|
SOFTWARE_VERSION = "2.1"
|
||||||
@@ -10,387 +10,387 @@ SOFTWARE_VERSION = "2.1"
|
|||||||
HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"}
|
HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"}
|
||||||
HAMQTH_PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}".replace(" ", "_")
|
HAMQTH_PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}".replace(" ", "_")
|
||||||
|
|
||||||
# Special Interest Groups
|
# Activities
|
||||||
SIGS = [
|
ACTIVITIES = [
|
||||||
SIG(
|
Activity(
|
||||||
name="AMSAT",
|
name="AMSAT",
|
||||||
comment_names=[],
|
comment_names=[],
|
||||||
description="Amateur Radio Satellites",
|
description="Amateur Radio Satellites",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
icon="fa-satellite",
|
icon="fa-satellite",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="EME",
|
name="EME",
|
||||||
comment_names=[],
|
comment_names=[],
|
||||||
description="Moonbounce",
|
description="Moonbounce",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
icon="fa-moon",
|
icon="fa-moon",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="POTA",
|
name="POTA",
|
||||||
comment_names=["POTA"],
|
comment_names=["POTA"],
|
||||||
description="Parks on the Air",
|
description="Parks on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
ref_regex=r"[A-Z]{2}\-\d{4,5}|K\-TEST",
|
ref_regex=r"[A-Z]{2}\-\d{4,5}|K\-TEST",
|
||||||
icon="fa-tree",
|
icon="fa-tree",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="SOTA",
|
name="SOTA",
|
||||||
comment_names=["SOTA"],
|
comment_names=["SOTA"],
|
||||||
description="Summits on the Air",
|
description="Summits on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}",
|
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}",
|
||||||
icon="fa-mountain-sun",
|
icon="fa-mountain-sun",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="WWFF",
|
name="WWFF",
|
||||||
comment_names=["WWFF"],
|
comment_names=["WWFF"],
|
||||||
description="World Wide Flora & Fauna",
|
description="World Wide Flora & Fauna",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}",
|
ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}",
|
||||||
icon="fa-seedling",
|
icon="fa-seedling",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="GMA",
|
name="GMA",
|
||||||
comment_names=["GMA"],
|
comment_names=["GMA"],
|
||||||
description="Global Mountain Activity",
|
description="Global Mountain Activity",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}",
|
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}",
|
||||||
icon="fa-person-hiking",
|
icon="fa-person-hiking",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="WWBOTA",
|
name="WWBOTA",
|
||||||
comment_names=["WWBOTA", "BOTA"],
|
comment_names=["WWBOTA", "BOTA"],
|
||||||
description="Worldwide Bunkers on the Air",
|
description="Worldwide Bunkers on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.BUNKER,
|
ref_type=ActivityRefType.BUNKER,
|
||||||
ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}",
|
ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}",
|
||||||
icon="fa-radiation",
|
icon="fa-radiation",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="HEMA",
|
name="HEMA",
|
||||||
comment_names=["HEMA"],
|
comment_names=["HEMA"],
|
||||||
description="HuMPs Excluding Marilyns Award",
|
description="HuMPs Excluding Marilyns Award",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}",
|
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}",
|
||||||
icon="fa-mound",
|
icon="fa-mound",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="IOTA",
|
name="IOTA",
|
||||||
comment_names=["IOTA"],
|
comment_names=["IOTA"],
|
||||||
description="Islands on the Air",
|
description="Islands on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.ISLAND,
|
ref_type=ActivityRefType.ISLAND,
|
||||||
ref_regex=r"[A-Z]{2}\-\d{3}",
|
ref_regex=r"[A-Z]{2}\-\d{3}",
|
||||||
icon="fa-book-atlas",
|
icon="fa-book-atlas",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="GMA Islands",
|
name="GMA Islands",
|
||||||
comment_names=[],
|
comment_names=[],
|
||||||
description="Global Mountain Activity - Islands",
|
description="Global Mountain Activity - Islands",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.ISLAND,
|
ref_type=ActivityRefType.ISLAND,
|
||||||
ref_regex=r"(([A-Z]{2}\-\d{3})|([A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}))",
|
ref_regex=r"(([A-Z]{2}\-\d{3})|([A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}))",
|
||||||
icon="fa-person-hiking",
|
icon="fa-person-hiking",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="ARLHS",
|
name="ARLHS",
|
||||||
comment_names=["ARLHS"],
|
comment_names=["ARLHS"],
|
||||||
description="Amateur Radio Lighthouse Society",
|
description="Amateur Radio Lighthouse Society",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
ref_regex=r"[A-Z]{3}[\- ]\d{3,4}",
|
ref_regex=r"[A-Z]{3}[\- ]\d{3,4}",
|
||||||
icon="fa-house-flood-water",
|
icon="fa-house-flood-water",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="ILLW",
|
name="ILLW",
|
||||||
comment_names=["ILLW"],
|
comment_names=["ILLW"],
|
||||||
description="International Lighthouse & Lightship Weekend",
|
description="International Lighthouse & Lightship Weekend",
|
||||||
sig_type=SIGType.EVENT,
|
sig_type=ActivityType.EVENT,
|
||||||
ref_type=SIGRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
ref_regex=r"[A-Z]{2}\d{4}",
|
ref_regex=r"[A-Z]{2}\d{4}",
|
||||||
icon="fa-house-flood-water",
|
icon="fa-house-flood-water",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="MOTA",
|
name="MOTA",
|
||||||
comment_names=["MOTA"],
|
comment_names=["MOTA"],
|
||||||
description="Mills on the Air",
|
description="Mills on the Air",
|
||||||
sig_type=SIGType.EVENT,
|
sig_type=ActivityType.EVENT,
|
||||||
ref_type=SIGRefType.MILL,
|
ref_type=ActivityRefType.MILL,
|
||||||
ref_regex=r"X\d{4,6}",
|
ref_regex=r"X\d{4,6}",
|
||||||
icon="fa-fan",
|
icon="fa-fan",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="SIOTA",
|
name="SIOTA",
|
||||||
comment_names=["SIOTA"],
|
comment_names=["SIOTA"],
|
||||||
description="Silos on the Air",
|
description="Silos on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.SILO,
|
ref_type=ActivityRefType.SILO,
|
||||||
ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d",
|
ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d",
|
||||||
icon="fa-wheat-awn",
|
icon="fa-wheat-awn",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="WCA",
|
name="WCA",
|
||||||
comment_names=["WCA"],
|
comment_names=["WCA"],
|
||||||
description="World Castles Award",
|
description="World Castles Award",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
ref_regex=r"[A-Z0-9]{1,3}\-\d{5}",
|
ref_regex=r"[A-Z0-9]{1,3}\-\d{5}",
|
||||||
icon="fa-chess-rook",
|
icon="fa-chess-rook",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="ZLOTA",
|
name="ZLOTA",
|
||||||
comment_names=["ZLOTA"],
|
comment_names=["ZLOTA"],
|
||||||
description="New Zealand on the Air",
|
description="New Zealand on the Air",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=None,
|
ref_type=None,
|
||||||
ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}",
|
ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}",
|
||||||
icon="fa-kiwi-bird",
|
icon="fa-kiwi-bird",
|
||||||
region_flag="🇳🇿",
|
region_flag="🇳🇿",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="WOTA",
|
name="WOTA",
|
||||||
comment_names=["WOTA"],
|
comment_names=["WOTA"],
|
||||||
description="Wainwrights on the Air",
|
description="Wainwrights on the Air",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
ref_regex=r"[A-Z]{3}-[0-9]{2}",
|
ref_regex=r"[A-Z]{3}-[0-9]{2}",
|
||||||
icon="fa-w",
|
icon="fa-w",
|
||||||
region_flag="🇬🇧",
|
region_flag="🇬🇧",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="BOTA",
|
name="BOTA",
|
||||||
comment_names=[],
|
comment_names=[],
|
||||||
description="Beaches on the Air",
|
description="Beaches on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.BEACH,
|
ref_type=ActivityRefType.BEACH,
|
||||||
icon="fa-umbrella-beach",
|
icon="fa-umbrella-beach",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="KRMNPA",
|
name="KRMNPA",
|
||||||
comment_names=["KRMNPA"],
|
comment_names=["KRMNPA"],
|
||||||
description="Keith Roget Memorial National Parks Award",
|
description="Keith Roget Memorial National Parks Award",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
ref_regex=r"VKFF\-\d{4}",
|
ref_regex=r"VKFF\-\d{4}",
|
||||||
icon="fa-earth-oceania",
|
icon="fa-earth-oceania",
|
||||||
region_flag="🇦🇺",
|
region_flag="🇦🇺",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="SANPCPA",
|
name="SANPCPA",
|
||||||
comment_names=["SANPCPA"],
|
comment_names=["SANPCPA"],
|
||||||
description="South Australian National Parks and Conservation Parks Award",
|
description="South Australian National Parks and Conservation Parks Award",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
ref_regex=r"VKFF\-\d{4}",
|
ref_regex=r"VKFF\-\d{4}",
|
||||||
icon="fa-earth-oceania",
|
icon="fa-earth-oceania",
|
||||||
region_flag="🇦🇺",
|
region_flag="🇦🇺",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="LLOTA",
|
name="LLOTA",
|
||||||
comment_names=["LLOTA"],
|
comment_names=["LLOTA"],
|
||||||
description="Lagos y Lagunas on the Air",
|
description="Lagos y Lagunas on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.LAKE,
|
ref_type=ActivityRefType.LAKE,
|
||||||
ref_regex=r"LL[A-Z]{2}\-\d{4}",
|
ref_regex=r"LL[A-Z]{2}\-\d{4}",
|
||||||
icon="fa-water",
|
icon="fa-water",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="Towers",
|
name="Towers",
|
||||||
comment_names=["TOTA"],
|
comment_names=["TOTA"],
|
||||||
description="Towers on the Air",
|
description="Towers on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.TOWER,
|
ref_type=ActivityRefType.TOWER,
|
||||||
ref_regex=r"[A-Z]{2,3}R\-\d{4}",
|
ref_regex=r"[A-Z]{2,3}R\-\d{4}",
|
||||||
icon="fa-tower-observation",
|
icon="fa-tower-observation",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="Tiles",
|
name="Tiles",
|
||||||
comment_names=[],
|
comment_names=[],
|
||||||
description="Tiles on the Air",
|
description="Tiles on the Air",
|
||||||
sig_type=SIGType.WORLDWIDE,
|
sig_type=ActivityType.WORLDWIDE,
|
||||||
ref_type=SIGRefType.GRID,
|
ref_type=ActivityRefType.GRID,
|
||||||
ref_regex=r"[A-Za-z]{2}[0-9]{2}[A-Za-z]{2}",
|
ref_regex=r"[A-Za-z]{2}[0-9]{2}[A-Za-z]{2}",
|
||||||
icon="fa-square",
|
icon="fa-square",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="WAB",
|
name="WAB",
|
||||||
comment_names=["WAB"],
|
comment_names=["WAB"],
|
||||||
description="Worked All Britain",
|
description="Worked All Britain",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.GRID,
|
ref_type=ActivityRefType.GRID,
|
||||||
ref_regex=r"[A-Z]{1,2}[0-9]{2}",
|
ref_regex=r"[A-Z]{1,2}[0-9]{2}",
|
||||||
icon="fa-table-cells-large",
|
icon="fa-table-cells-large",
|
||||||
region_flag="🇬🇧",
|
region_flag="🇬🇧",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="WAI",
|
name="WAI",
|
||||||
comment_names=["WAI"],
|
comment_names=["WAI"],
|
||||||
description="Worked All Ireland",
|
description="Worked All Ireland",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.GRID,
|
ref_type=ActivityRefType.GRID,
|
||||||
ref_regex=r"[A-Z][0-9]{2}",
|
ref_regex=r"[A-Z][0-9]{2}",
|
||||||
icon="fa-table-cells-large",
|
icon="fa-table-cells-large",
|
||||||
region_flag="🇮🇪",
|
region_flag="🇮🇪",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="DMF",
|
name="DMF",
|
||||||
comment_names=["DMF"],
|
comment_names=["DMF"],
|
||||||
description="Diplôme des Moulins de France",
|
description="Diplôme des Moulins de France",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.MILL,
|
ref_type=ActivityRefType.MILL,
|
||||||
icon="fa-fan",
|
icon="fa-fan",
|
||||||
region_flag="🇫🇷",
|
region_flag="🇫🇷",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="DME",
|
name="DME",
|
||||||
comment_names=["DME"],
|
comment_names=["DME"],
|
||||||
description="Diploma Municipios de España",
|
description="Diploma Municipios de España",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.TOWN,
|
ref_type=ActivityRefType.TOWN,
|
||||||
ref_regex=r"DME[\- ]\d{3,5}",
|
ref_regex=r"DME[\- ]\d{3,5}",
|
||||||
icon="fa-building",
|
icon="fa-building",
|
||||||
region_flag="🇪🇸",
|
region_flag="🇪🇸",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="FEA",
|
name="FEA",
|
||||||
comment_names=["FEA"],
|
comment_names=["FEA"],
|
||||||
description="Diploma Faros de España",
|
description="Diploma Faros de España",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
# FEA references are technically [DE]\-\d{4}(\.\d)? but spotters always seem to miss out the D- or E-
|
# FEA references are technically [DE]\-\d{4}(\.\d)? but spotters always seem to miss out the D- or E-
|
||||||
# prefix and just use FEA-1234 or FEA 1234, so allow for that. The FEA sigref data provider adds both
|
# prefix and just use FEA-1234 or FEA 1234, so allow for that. The FEA activity ref data provider adds both
|
||||||
# forms to the database.
|
# forms to the database.
|
||||||
ref_regex=r"([DE]|FEA)[\- ]\d{4}(\.\d)?",
|
ref_regex=r"([DE]|FEA)[\- ]\d{4}(\.\d)?",
|
||||||
icon="fa-house-flood-water",
|
icon="fa-house-flood-water",
|
||||||
region_flag="🇪🇸",
|
region_flag="🇪🇸",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="DMUE",
|
name="DMUE",
|
||||||
comment_names=["DMUE"],
|
comment_names=["DMUE"],
|
||||||
description="Diploma Museos de España",
|
description="Diploma Museos de España",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.BUILDING,
|
ref_type=ActivityRefType.BUILDING,
|
||||||
ref_regex=r"MUE[A-Z]{2}-\d{3}",
|
ref_regex=r"MUE[A-Z]{2}-\d{3}",
|
||||||
icon="fa-landmark",
|
icon="fa-landmark",
|
||||||
region_flag="🇪🇸",
|
region_flag="🇪🇸",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="DMVE",
|
name="DMVE",
|
||||||
comment_names=["DMVE"],
|
comment_names=["DMVE"],
|
||||||
description="Diploma Monumentos y Vestigios de España",
|
description="Diploma Monumentos y Vestigios de España",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.BUILDING,
|
ref_type=ActivityRefType.BUILDING,
|
||||||
ref_regex=r"MV[A-Z]{1,2}-\d{4}",
|
ref_regex=r"MV[A-Z]{1,2}-\d{4}",
|
||||||
icon="fa-monument",
|
icon="fa-monument",
|
||||||
region_flag="🇪🇸",
|
region_flag="🇪🇸",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="DCE",
|
name="DCE",
|
||||||
comment_names=["DCE"],
|
comment_names=["DCE"],
|
||||||
description="Diploma Castillos de España",
|
description="Diploma Castillos de España",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
ref_regex=r"C[A-Z]{1,2}-\d{3}",
|
ref_regex=r"C[A-Z]{1,2}-\d{3}",
|
||||||
icon="fa-chess-rook",
|
icon="fa-chess-rook",
|
||||||
region_flag="🇪🇸",
|
region_flag="🇪🇸",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="DEFE",
|
name="DEFE",
|
||||||
comment_names=["DEFE"],
|
comment_names=["DEFE"],
|
||||||
description="Diploma Estaciones de Ferrocarril de España",
|
description="Diploma Estaciones de Ferrocarril de España",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.BUILDING,
|
ref_type=ActivityRefType.BUILDING,
|
||||||
ref_regex=r"EF[A-Z]{1,2}-\d{3}",
|
ref_regex=r"EF[A-Z]{1,2}-\d{3}",
|
||||||
icon="fa-train",
|
icon="fa-train",
|
||||||
region_flag="🇪🇸",
|
region_flag="🇪🇸",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="DTMBA",
|
name="DTMBA",
|
||||||
comment_names=["DTMBA"],
|
comment_names=["DTMBA"],
|
||||||
description="Diploma Teatri Musei e Belle Arti",
|
description="Diploma Teatri Musei e Belle Arti",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.BUILDING,
|
ref_type=ActivityRefType.BUILDING,
|
||||||
ref_regex=r"I-?[0-9]{3,4}\s?[A-Z]{2}",
|
ref_regex=r"I-?[0-9]{3,4}\s?[A-Z]{2}",
|
||||||
icon="fa-landmark",
|
icon="fa-landmark",
|
||||||
region_flag="🇮🇹",
|
region_flag="🇮🇹",
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="BIWOTA",
|
name="BIWOTA",
|
||||||
comment_names=["BIWOTA"],
|
comment_names=["BIWOTA"],
|
||||||
description="British Inland Waterways on the Air",
|
description="British Inland Waterways on the Air",
|
||||||
sig_type=SIGType.EVENT,
|
sig_type=ActivityType.EVENT,
|
||||||
ref_type=SIGRefType.WATERWAY,
|
ref_type=ActivityRefType.WATERWAY,
|
||||||
icon="fa-ship",
|
icon="fa-ship",
|
||||||
region_flag="🇬🇧",
|
region_flag="🇬🇧",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="COTA",
|
name="COTA",
|
||||||
comment_names=["COTA"],
|
comment_names=["COTA"],
|
||||||
description="Castles on the Air",
|
description="Castles on the Air",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
ref_regex=r"[A-Z]{3}\-[0-9]{3,5}",
|
ref_regex=r"[A-Z]{3}\-[0-9]{3,5}",
|
||||||
icon="fa-chess-rook",
|
icon="fa-chess-rook",
|
||||||
region_flag="🇩🇪",
|
region_flag="🇩🇪",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="PGA",
|
name="PGA",
|
||||||
comment_names=["PGA"],
|
comment_names=["PGA"],
|
||||||
description="Polish Gmina Award",
|
description="Polish Gmina Award",
|
||||||
sig_type=SIGType.REGIONAL,
|
sig_type=ActivityType.REGIONAL,
|
||||||
ref_type=SIGRefType.REGION,
|
ref_type=ActivityRefType.REGION,
|
||||||
ref_regex=r"[A-Z]{2}[0-9]{2}",
|
ref_regex=r"[A-Z]{2}[0-9]{2}",
|
||||||
icon="fa-g",
|
icon="fa-g",
|
||||||
region_flag="🇵🇱",
|
region_flag="🇵🇱",
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
),
|
),
|
||||||
SIG(
|
Activity(
|
||||||
name="Toilets",
|
name="Toilets",
|
||||||
comment_names=[],
|
comment_names=[],
|
||||||
description="Toilets on the Air",
|
description="Toilets on the Air",
|
||||||
sig_type=SIGType.EVENT,
|
sig_type=ActivityType.EVENT,
|
||||||
ref_type=SIGRefType.TOILET,
|
ref_type=ActivityRefType.TOILET,
|
||||||
ref_regex=r"T\-[0-9]{2}",
|
ref_regex=r"T\-[0-9]{2}",
|
||||||
icon="fa-toilet",
|
icon="fa-toilet",
|
||||||
region_flag="🏴☠️",
|
region_flag="🏴☠️",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class DataProviders:
|
|||||||
for entry in config.get("static_data_providers", []):
|
for entry in config.get("static_data_providers", []):
|
||||||
self.static_data_providers.append(create_provider_from_config("providers.staticdata", entry))
|
self.static_data_providers.append(create_provider_from_config("providers.staticdata", entry))
|
||||||
for entry in config.get("sig_ref_data_providers", []):
|
for entry in config.get("sig_ref_data_providers", []):
|
||||||
self.sig_ref_data_providers.append(create_provider_from_config("providers.sigrefdata", entry))
|
self.sig_ref_data_providers.append(create_provider_from_config("providers.activityrefdata", entry))
|
||||||
for entry in config.get("callsign_data_providers", []):
|
for entry in config.get("callsign_data_providers", []):
|
||||||
self.callsign_data_providers.append(create_provider_from_config("providers.callsigndata", entry))
|
self.callsign_data_providers.append(create_provider_from_config("providers.callsigndata", entry))
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ class DataProviders:
|
|||||||
25.0,
|
25.0,
|
||||||
lambda: self.start_providers(self.solar_condition_providers, "solar condition"),
|
lambda: self.start_providers(self.solar_condition_providers, "solar condition"),
|
||||||
),
|
),
|
||||||
threading.Timer(30.0, lambda: self.start_providers(self.sig_ref_data_providers, "SIG ref data")),
|
threading.Timer(30.0, lambda: self.start_providers(self.sig_ref_data_providers, "activity ref data")),
|
||||||
]
|
]
|
||||||
for t in self._startup_timers:
|
for t in self._startup_timers:
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
|
|||||||
+13
-13
@@ -15,8 +15,8 @@ CACHE_DIR = "./cache/"
|
|||||||
|
|
||||||
|
|
||||||
class DataStore:
|
class DataStore:
|
||||||
"""Data caching/storage object. Handles storage of spots, alerts, solar conditions, SIG reference data, and callsign
|
"""Data caching/storage object. Handles storage of spots, alerts, solar conditions, activity reference data, and
|
||||||
lookup data using different caching strategies for each."""
|
callsign lookup data using different caching strategies for each."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Constants
|
# Constants
|
||||||
@@ -34,7 +34,7 @@ class DataStore:
|
|||||||
self.callsign_data_hamqth = None
|
self.callsign_data_hamqth = None
|
||||||
self.dxcc_data = None
|
self.dxcc_data = None
|
||||||
self.dxcc_lookup_by_call_regex = []
|
self.dxcc_lookup_by_call_regex = []
|
||||||
self.sigrefs = None
|
self.activity_refs = None
|
||||||
self.status = None
|
self.status = None
|
||||||
self.solar_conditions = None
|
self.solar_conditions = None
|
||||||
# ITU/CQ zone GeoJSON data is only ever loaded statically from a local file so these don't even need to be
|
# ITU/CQ zone GeoJSON data is only ever loaded statically from a local file so these don't even need to be
|
||||||
@@ -51,16 +51,16 @@ class DataStore:
|
|||||||
self.solar_conditions = SingleObjectDataCache(f"{CACHE_DIR}solar", SolarConditions())
|
self.solar_conditions = SingleObjectDataCache(f"{CACHE_DIR}solar", SolarConditions())
|
||||||
self.status = SingleObjectDataCache(f"{CACHE_DIR}status", {})
|
self.status = SingleObjectDataCache(f"{CACHE_DIR}status", {})
|
||||||
|
|
||||||
# Standard disk cache for static reference and SIG ref data. Separate provider threads will repopulate these on
|
# Standard disk cache for static reference and activity ref data. Separate provider threads will repopulate
|
||||||
# a regular basis but there's no need for a TTL since old data is better than no data.
|
# these on a regular basis but there's no need for a TTL since old data is better than no data.
|
||||||
self.dxcc_data = diskcache.Cache(f"{CACHE_DIR}dxcc_data")
|
self.dxcc_data = diskcache.Cache(f"{CACHE_DIR}dxcc_data")
|
||||||
self.regenerate_call_regex_to_dxcc_entity_map()
|
self.regenerate_call_regex_to_dxcc_entity_map()
|
||||||
|
|
||||||
# For SIG reference data specifically, we need to key on both SIG *and* reference, and trying to do two layers
|
# For activity reference data specifically, we need to key on both activity *and* reference, and trying to do
|
||||||
# of dict in diskcache absolutely destroys performance with unpickling huge dicts, so we have an ugly "SIG:ref"
|
# two layers of dict in diskcache absolutely destroys performance with unpickling huge dicts, so we have an
|
||||||
# syntax for keys to keep it a single level.
|
# ugly "activity:ref" syntax for keys to keep it a single level.
|
||||||
self.sigrefs = diskcache.Cache(f"{CACHE_DIR}sigrefs")
|
self.activity_refs = diskcache.Cache(f"{CACHE_DIR}activity_refs")
|
||||||
logger.info(f"Loaded data for {len(self.sigrefs)} SIG references.")
|
logger.info(f"Loaded data for {len(self.activity_refs)} activity references.")
|
||||||
|
|
||||||
# Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup.
|
# Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup.
|
||||||
# Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign
|
# Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign
|
||||||
@@ -82,8 +82,8 @@ class DataStore:
|
|||||||
logger.info(f"Loaded data for {len(unique_keys)} callsigns.")
|
logger.info(f"Loaded data for {len(unique_keys)} callsigns.")
|
||||||
|
|
||||||
# Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We
|
# Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We
|
||||||
# specifically load these caches *last* so that any sigref and callsign data is already loaded from disk cache
|
# specifically load these caches *last* so that any activity ref and callsign data is already loaded from disk
|
||||||
# before the spots and alerts are live in the system.
|
# cache before the spots and alerts are live in the system.
|
||||||
self.spots = LiveDataCache(
|
self.spots = LiveDataCache(
|
||||||
maxsize=self._MAX_SPOT_COUNT,
|
maxsize=self._MAX_SPOT_COUNT,
|
||||||
ttl=MAX_SPOT_AGE,
|
ttl=MAX_SPOT_AGE,
|
||||||
@@ -116,7 +116,7 @@ class DataStore:
|
|||||||
self.solar_conditions.close()
|
self.solar_conditions.close()
|
||||||
self.status.close()
|
self.status.close()
|
||||||
self.dxcc_data.close()
|
self.dxcc_data.close()
|
||||||
self.sigrefs.close()
|
self.activity_refs.close()
|
||||||
self.callsign_data_countryfiles.close()
|
self.callsign_data_countryfiles.close()
|
||||||
self.callsign_data_clublogxml.close()
|
self.callsign_data_clublogxml.close()
|
||||||
self.callsign_data_clublogapi.close()
|
self.callsign_data_clublogapi.close()
|
||||||
|
|||||||
+4
-4
@@ -90,8 +90,8 @@ class LocationSourceForCallsign(str, Enum):
|
|||||||
DXCC = "DXCC"
|
DXCC = "DXCC"
|
||||||
|
|
||||||
|
|
||||||
class SIGRefType(str, Enum):
|
class ActivityRefType(str, Enum):
|
||||||
"""Type of a Special Interest Group reference."""
|
"""Type of an activity reference."""
|
||||||
|
|
||||||
PARK = "PARK"
|
PARK = "PARK"
|
||||||
SUMMIT = "SUMMIT"
|
SUMMIT = "SUMMIT"
|
||||||
@@ -121,8 +121,8 @@ class AlertType(str, Enum):
|
|||||||
CONTEST = "CONTEST"
|
CONTEST = "CONTEST"
|
||||||
|
|
||||||
|
|
||||||
class SIGType(str, Enum):
|
class ActivityType(str, Enum):
|
||||||
"""Type of a Special Interest Group. Used to group them in the web UI."""
|
"""Type of an activity. Used to group them in the web UI."""
|
||||||
|
|
||||||
WORLDWIDE = "WORLDWIDE"
|
WORLDWIDE = "WORLDWIDE"
|
||||||
REGIONAL = "REGIONAL"
|
REGIONAL = "REGIONAL"
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
from pyhamtools.locator import latlong_to_locator, locator_to_latlong
|
|
||||||
|
|
||||||
from core.constants import SIGS
|
|
||||||
from core.data_store import DATA_STORE
|
|
||||||
from core.enums import SIGRefType
|
|
||||||
from core.geo_utils import wab_wai_square_to_lat_lon
|
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_sig_ref_info(sig_name, ref_id):
|
|
||||||
"""Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig name and
|
|
||||||
a reference ID (both strings) and returns a SigRef object populated with as much data as we can find. This makes
|
|
||||||
use of SIG ref data in the data store, live lookups from the web, or just automatic calculation depending on which
|
|
||||||
SIG we are getting data for."""
|
|
||||||
|
|
||||||
if sig_name is None or sig_name == "" or ref_id is None or ref_id == "":
|
|
||||||
logger.debug("Failed to look up sig_ref info, sig 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
|
|
||||||
sig_ref = SIGRef(sig=sig_name, id=ref_id)
|
|
||||||
|
|
||||||
# We can always get the reference type and the icon from the SIG itself
|
|
||||||
for sig in SIGS:
|
|
||||||
if sig.name.upper() == sig_name.upper():
|
|
||||||
sig_ref.ref_type = sig.ref_type
|
|
||||||
sig_ref.icon = sig.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 sig_name.upper() == "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 sig_name.upper() == "DTMBA":
|
|
||||||
ref_id = ref_id.replace("-", "").replace(" ", "")
|
|
||||||
|
|
||||||
### NO DATA SIGS ###
|
|
||||||
#
|
|
||||||
# If the SIG 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 sig_name.upper() == "HEMA" or sig_name.upper() == "BIWOTA":
|
|
||||||
return sig_ref
|
|
||||||
|
|
||||||
### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ###
|
|
||||||
#
|
|
||||||
# If the SIG 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 sig_name.upper() == "TILES":
|
|
||||||
# Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same
|
|
||||||
if not sig_ref.name:
|
|
||||||
sig_ref.name = sig_ref.id
|
|
||||||
if not sig_ref.grid:
|
|
||||||
sig_ref.grid = sig_ref.id
|
|
||||||
if sig_ref.grid and (not sig_ref.latitude or not sig_ref.longitude):
|
|
||||||
ll = locator_to_latlong(str(sig_ref.grid))
|
|
||||||
sig_ref.latitude = ll[0]
|
|
||||||
sig_ref.longitude = ll[1]
|
|
||||||
return sig_ref
|
|
||||||
|
|
||||||
elif sig_name.upper() == "WAB" or sig_name.upper() == "WAI":
|
|
||||||
ll = wab_wai_square_to_lat_lon(ref_id)
|
|
||||||
if ll:
|
|
||||||
sig_ref.name = ref_id
|
|
||||||
try:
|
|
||||||
sig_ref.grid = latlong_to_locator(ll[0], ll[1], 6)
|
|
||||||
sig_ref.latitude = ll[0]
|
|
||||||
sig_ref.longitude = ll[1]
|
|
||||||
except Exception:
|
|
||||||
logger.warning("Invalid lat/lon received for WAB/WAI reference")
|
|
||||||
return sig_ref
|
|
||||||
|
|
||||||
elif sig_name.upper() == "BOTA":
|
|
||||||
# For BOTA all we can ever generate is the URL, there is no data file or lookup for lat/longs
|
|
||||||
if not sig_ref.name:
|
|
||||||
sig_ref.name = sig_ref.id
|
|
||||||
if sig_ref.name:
|
|
||||||
sig_ref.url = f"https://www.beachesontheair.com/beaches/{sig_ref.name.lower().replace(' ', '-')}"
|
|
||||||
return sig_ref
|
|
||||||
|
|
||||||
elif sig_name.upper() == "GMA Islands":
|
|
||||||
# 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_sig_ref_info("IOTA", ref_id)
|
|
||||||
gma_lookup = get_sig_ref_info("GMA", ref_id)
|
|
||||||
for key, value in iota_lookup.__dict__.items():
|
|
||||||
if value is not None and sig_ref.__dict__.get(key) is None:
|
|
||||||
sig_ref.__dict__[key] = value
|
|
||||||
for key, value in gma_lookup.__dict__.items():
|
|
||||||
if value is not None and sig_ref.__dict__.get(key) is None:
|
|
||||||
sig_ref.__dict__[key] = value
|
|
||||||
sig_ref.ref_type = SIGRefType.ISLAND
|
|
||||||
return sig_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 sig_ref object
|
|
||||||
key = f"{sig_name}:{ref_id}"
|
|
||||||
try:
|
|
||||||
lookup_data = DATA_STORE.sigrefs.get(key) if key in DATA_STORE.sigrefs else None
|
|
||||||
if lookup_data:
|
|
||||||
for attr, value in lookup_data.__dict__.items():
|
|
||||||
if value is not None and sig_ref.__dict__.get(attr) is None:
|
|
||||||
sig_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"{sig_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.sigrefs[key]
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"Exception when looking up sig_ref info for {sig_name} ref {ref_id}")
|
|
||||||
return sig_ref
|
|
||||||
|
|
||||||
|
|
||||||
def populate_missing_sig_ref_info(sig_ref):
|
|
||||||
"""Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig_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_sig_ref_info(sig_ref.sig, sig_ref.id)
|
|
||||||
|
|
||||||
if lookup_data:
|
|
||||||
# Copy new sig ref data into existing object where data was previously missing
|
|
||||||
for key, value in lookup_data.__dict__.items():
|
|
||||||
if value is not None and sig_ref.__dict__.get(key) is None:
|
|
||||||
sig_ref.__dict__[key] = value
|
|
||||||
|
|
||||||
return sig_ref
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
from core.constants import SIGS
|
|
||||||
|
|
||||||
|
|
||||||
def get_ref_regex_for_sig(sig):
|
|
||||||
"""Utility function to get the regex string for a SIG reference for a named SIG. If no match is found, None will be returned."""
|
|
||||||
|
|
||||||
for s in SIGS:
|
|
||||||
if s.name.upper() == sig.upper():
|
|
||||||
return s.ref_regex
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_sig_name_from_comment_name(sig):
|
|
||||||
"""Utility function to get the name of a SIG from its "comment name". Generally these will be the same but there are
|
|
||||||
some cases (e.g. is "TOTA" Towers, Tiles or Toilets?) where we need to transform one to the other."""
|
|
||||||
|
|
||||||
for s in SIGS:
|
|
||||||
if any(n.upper() == sig.upper() for n in s.comment_names):
|
|
||||||
return s.name
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# Regex matching any SIG's "comment name", i.e. how it may be referred to in spot comments
|
|
||||||
ANY_SIG_REGEX = rf"({'|'.join(n for s in SIGS for n in s.comment_names)})"
|
|
||||||
@@ -7,10 +7,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class SingleObjectDataCache:
|
class SingleObjectDataCache:
|
||||||
"""Cache for status and solar conditions. This uses DiskCache, but unlike the standard DiskCache users like SIG and
|
"""Cache for status and solar conditions. This uses DiskCache, but unlike the standard DiskCache users like
|
||||||
callsign lookup handlers, status and solar conditions are persisted as a single object. If we just load the object
|
activity ref and callsign lookup handlers, status and solar conditions are persisted as a single object. If we
|
||||||
from DiskCache and modify it, DiskCache doesn't know that it's been updated and needs re-caching, so we provide a
|
just load the object from DiskCache and modify it, DiskCache doesn't know that it's been updated and needs
|
||||||
store() method that any functions updating the object can call afterwards."""
|
re-caching, so we provide a store() method that any functions updating the object can call afterwards."""
|
||||||
|
|
||||||
def __init__(self, cache_dir, object_if_empty):
|
def __init__(self, cache_dir, object_if_empty):
|
||||||
"""Initialize a SingleObjectDataCache. Provide the directory to load the cache from and save it to. If the cache
|
"""Initialize a SingleObjectDataCache. Provide the directory to load the cache from and save it to. If the cache
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from core.enums import 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."""
|
||||||
|
|
||||||
|
# Activity name as used in the UI and API, e.g. "Towers"
|
||||||
|
name: str
|
||||||
|
# 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
|
||||||
|
# Identifies that the activity's reference ID structure defined by its regex is unique across all programmes and
|
||||||
|
# anything else we expect a user to put in a spot comment, and therefore we can pull references out of
|
||||||
|
# spot comments without also needing to see the activity name first. For example, "OHFF-1234" or "B/G-1234" are
|
||||||
|
# obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could
|
||||||
|
# conceivably be POTA or ILLW, etc.
|
||||||
|
refs_globally_unique: bool
|
||||||
|
# Activity names as they might appear in cluster spot comments, e.g. ["TOTA"]
|
||||||
|
comment_names: list[str] = field(default_factory=list)
|
||||||
|
# Reference type, what gets activated e.g. Park, Summit. May be None if the activity is for multiple types of
|
||||||
|
# things, in which case the spot data will have to provide this instead.
|
||||||
|
ref_type: ActivityRefType | None = None
|
||||||
|
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
|
||||||
|
ref_regex: str | None = None
|
||||||
|
# Icon to use in the UI when referencing this activity. Chosen from the Font Awesome set.
|
||||||
|
icon: str | None = None
|
||||||
|
# Emoji flag for the country or region where this activity is relevant, if any. If None, this implies the
|
||||||
|
# activity is in worldwide usage.
|
||||||
|
region_flag: str | None = None
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SIGRef:
|
class ActivityRef:
|
||||||
"""Data class that defines a Special Interest Group "info" or reference. As well as the basic reference ID we include a
|
"""Data class that defines an Activity "info" or reference. As well as the basic reference ID we include a
|
||||||
name and a lookup URL."""
|
name and a lookup URL."""
|
||||||
|
|
||||||
# SIG that this reference is in, e.g. "POTA".
|
# Activity that this reference is in, e.g. "POTA". Still named "sig" for backwards compatibility with the API.
|
||||||
sig: str
|
sig: str
|
||||||
# Reference ID, e.g. "GB-0001".
|
# Reference ID, e.g. "GB-0001".
|
||||||
id: str | None = None
|
id: str | None = None
|
||||||
# Name of the reference, e.g. "Null Country Park", if known.
|
# Name of the reference, e.g. "Null Country Park", if known.
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
# Type of the reference, e.g. "Park", if known.
|
# Type of the reference, e.g. "Park", if known.
|
||||||
ref_type: SIGRefType | None = None
|
ref_type: ActivityRefType | None = None
|
||||||
# URL to look up more information about the reference, if known.
|
# URL to look up more information about the reference, if known.
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
# Icon to use for the reference, derived from the SIG. Chosen from the Font Awesome set.
|
# Icon to use for the reference, derived from the activity. Chosen from the Font Awesome set.
|
||||||
icon: str | None = None
|
icon: str | None = None
|
||||||
# Latitude of the reference, in degrees, if known.
|
# Latitude of the reference, in degrees, if known.
|
||||||
latitude: float | None = None
|
latitude: float | None = None
|
||||||
+13
-12
@@ -6,9 +6,9 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
from core.activity_lookup_helper import populate_missing_activity_ref_info
|
||||||
from core.call_lookup_helper import get_call_info
|
from core.call_lookup_helper import get_call_info
|
||||||
from core.enums import AlertType, Continent
|
from core.enums import AlertType, Continent
|
||||||
from core.sig_lookup_helper import populate_missing_sig_ref_info
|
|
||||||
from core.utils import get_flag_for_dxcc
|
from core.utils import get_flag_for_dxcc
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -59,11 +59,12 @@ class Alert:
|
|||||||
# A URL link to more information, if any
|
# A URL link to more information, if any
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
|
|
||||||
# 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: 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)
|
sig_refs: list = field(default_factory=list)
|
||||||
|
|
||||||
# Timing info
|
# Timing info
|
||||||
@@ -125,15 +126,15 @@ class Alert:
|
|||||||
if self.dx_dxcc_id and not self.dx_flag:
|
if self.dx_dxcc_id and not self.dx_flag:
|
||||||
self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id)
|
self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id)
|
||||||
|
|
||||||
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
|
# Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a
|
||||||
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs
|
# reference in its initial call, we use this code to populate the rest of the data. This includes working
|
||||||
# from WAB and WAI, which count as a SIG even though there's no real lookup, just maths
|
# 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:
|
if self.sig_refs:
|
||||||
for sig_ref in self.sig_refs:
|
for activity_ref in self.sig_refs:
|
||||||
populate_missing_sig_ref_info(sig_ref)
|
populate_missing_activity_ref_info(activity_ref)
|
||||||
|
|
||||||
# If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
|
# If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
|
||||||
# and apply it to the whole spot.
|
# reference's activity and apply it to the whole spot.
|
||||||
if self.sig_refs and self.sig_refs[0] and not self.sig:
|
if self.sig_refs and self.sig_refs[0] and not self.sig:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.sig = self.sig_refs[0].sig
|
||||||
|
|
||||||
@@ -152,7 +153,7 @@ class Alert:
|
|||||||
if self.dx_calls and not self.dx_names:
|
if self.dx_calls and not self.dx_names:
|
||||||
self.dx_names = [get_call_info(c, credentials).name for c in self.dx_calls]
|
self.dx_names = [get_call_info(c, credentials).name for c in self.dx_calls]
|
||||||
|
|
||||||
# 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"
|
self.icon = "fa-tower-cell"
|
||||||
if self.alert_type == AlertType.DXPEDITION:
|
if self.alert_type == AlertType.DXPEDITION:
|
||||||
self.icon = "fa-globe-africa"
|
self.icon = "fa-globe-africa"
|
||||||
|
|||||||
+2
-2
@@ -7,8 +7,8 @@ from core.enums import Continent, LocationSourceForCallsign
|
|||||||
class Callsign:
|
class Callsign:
|
||||||
"""Data class that defines a callsign and the data associated with it. This will have been retrieved by a callsign
|
"""Data class that defines a callsign and the data associated with it. This will have been retrieved by a callsign
|
||||||
lookup provider using data files or online lookup. This can be used to infer missing data for a spot, though if the
|
lookup provider using data files or online lookup. This can be used to infer missing data for a spot, though if the
|
||||||
spot has a SIG (e.g. POTA) reference this data for their home location (or even just their country) will be less
|
spot has an activity (e.g. POTA) reference this data for their home location (or even just their country) will be
|
||||||
accurate and should not be used in preference to that."""
|
less accurate and should not be used in preference to that."""
|
||||||
|
|
||||||
# Callsign as spotted
|
# Callsign as spotted
|
||||||
call: str
|
call: str
|
||||||
|
|||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
from core.enums import SIGRefType, SIGType
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SIG:
|
|
||||||
"""Data class that defines a Special Interest Group. 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."""
|
|
||||||
|
|
||||||
# SIG name as used in the UI and API, e.g. "Towers"
|
|
||||||
name: str
|
|
||||||
# Description, e.g. "Towers on the Air"
|
|
||||||
description: str
|
|
||||||
# Type, either Worldwide, Regional or Event. Used for sorting in the web UI.
|
|
||||||
sig_type: SIGType
|
|
||||||
# Identifies that the SIG's reference ID structure defined by its regex is unique across all programmes and
|
|
||||||
# anything else we expect a user to put in a spot comment, and therefore we can pull references out of
|
|
||||||
# spot comments without also needing to see the SIG name first. For example, "OHFF-1234" or "B/G-1234" are
|
|
||||||
# obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could
|
|
||||||
# conceivably be POTA or ILLW, etc.
|
|
||||||
refs_globally_unique: bool
|
|
||||||
# SIG names as they might appear in cluster spot comments, e.g. ["TOTA"]
|
|
||||||
comment_names: list[str] = field(default_factory=list)
|
|
||||||
# Reference type, what gets activated e.g. Park, Summit. May be None if the SIG is for multiple types of things, in
|
|
||||||
# which case the spot data will have to provide this instead.
|
|
||||||
ref_type: SIGRefType | None = None
|
|
||||||
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
|
|
||||||
ref_regex: str | None = None
|
|
||||||
# Icon to use in the UI when referencing this SIG. Chosen from the Font Awesome set.
|
|
||||||
icon: str | None = None
|
|
||||||
# Emoji flag for the country or region where this SIG is relevant, if any. If None, this implies the SIG is in
|
|
||||||
# worldwide usage.
|
|
||||||
region_flag: str | None = None
|
|
||||||
+91
-74
@@ -9,18 +9,18 @@ from math import isnan
|
|||||||
import pytz
|
import pytz
|
||||||
from pyhamtools.locator import latlong_to_locator, locator_to_latlong
|
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.call_lookup_helper import get_call_info
|
||||||
from core.config import MAX_SPOT_AGE
|
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.data_store import DATA_STORE
|
||||||
from core.enums import Continent, LocationSourceForSpot, Mode, ModeSource, ModeType
|
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.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 (
|
from core.utils import (
|
||||||
get_flag_for_dxcc,
|
get_flag_for_dxcc,
|
||||||
infer_band_from_freq,
|
infer_band_from_freq,
|
||||||
@@ -28,7 +28,7 @@ from core.utils import (
|
|||||||
infer_mode_from_frequency,
|
infer_mode_from_frequency,
|
||||||
infer_mode_type_from_mode,
|
infer_mode_type_from_mode,
|
||||||
)
|
)
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ class Spot:
|
|||||||
dx_call: str | None = None
|
dx_call: str | None = None
|
||||||
# Name of the operator that has been spotted
|
# Name of the operator that has been spotted
|
||||||
dx_name: str | None = None
|
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
|
# QTH of the operator that has been spotted. This could be from any activity refs or could be from online lookup of
|
||||||
# home QTH.
|
# their home QTH.
|
||||||
dx_qth: str | None = None
|
dx_qth: str | None = None
|
||||||
# Country of the DX operator
|
# Country of the DX operator
|
||||||
dx_country: str | None = None
|
dx_country: str | None = None
|
||||||
@@ -73,8 +73,8 @@ class Spot:
|
|||||||
# DX Location source. Indicates how accurate the location might be.
|
# DX Location source. Indicates how accurate the location might be.
|
||||||
dx_location_source: LocationSourceForSpot | None = None
|
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
|
# 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
|
# true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and
|
||||||
# DX callsign doesn't have a suffix like /P.
|
# the DX callsign doesn't have a suffix like /P. (Location source retains "SIG" wording for API compatibility.)
|
||||||
dx_location_good: bool = False
|
dx_location_good: bool = False
|
||||||
|
|
||||||
# DE (Spotter) info
|
# 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 state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
|
||||||
qrt: bool = False
|
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: 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)
|
sig_refs: list = field(default_factory=list)
|
||||||
|
|
||||||
# Timing info
|
# Timing info
|
||||||
@@ -158,7 +159,10 @@ class Spot:
|
|||||||
objects such as the sig_refs list.."""
|
objects such as the sig_refs list.."""
|
||||||
|
|
||||||
if self.sig_refs:
|
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):
|
def infer_missing(self, credentials=None):
|
||||||
"""Infer missing parameters where possible"""
|
"""Infer missing parameters where possible"""
|
||||||
@@ -262,76 +266,89 @@ class Spot:
|
|||||||
if self.dx_latitude or self.dx_grid:
|
if self.dx_latitude or self.dx_grid:
|
||||||
self.dx_location_source = LocationSourceForSpot.SPOT
|
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:
|
if not self.sig and self.sig_refs:
|
||||||
self.sig = self.sig_refs[0].sig.upper()
|
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
|
# See if we already have an activity reference, but the comment looks like it contains more for the same
|
||||||
# should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
|
# 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:
|
if self.comment and self.sig_refs and self.sig_refs[0].sig:
|
||||||
sig = self.sig_refs[0].sig.upper()
|
activity = self.sig_refs[0].sig.upper()
|
||||||
regex = get_ref_regex_for_sig(sig)
|
regex = get_ref_regex_for_activity(activity)
|
||||||
if regex:
|
if regex:
|
||||||
all_comment_ref_matches = re.finditer(r"(^|\W)(" + regex + r")($|\W)", self.comment, re.IGNORECASE)
|
all_comment_ref_matches = re.finditer(r"(^|\W)(" + regex + r")($|\W)", self.comment, re.IGNORECASE)
|
||||||
for ref_match in all_comment_ref_matches:
|
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
|
# See if the comment looks like it contains any activities (and optionally activity references) that we
|
||||||
# add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g. POTA
|
# can add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g.
|
||||||
# comments like "also WWFF GFF-0001".
|
# POTA comments like "also WWFF GFF-0001".
|
||||||
if self.comment:
|
if self.comment:
|
||||||
sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
activity_matches = re.finditer(r"(^|\W)" + ANY_ACTIVITY_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
||||||
for sig_match in sig_matches:
|
for activity_match in activity_matches:
|
||||||
# First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things like cluster
|
# First of all, if we haven't got an activity for this spot set yet, now we have. This covers
|
||||||
# spots where the comment is just "POTA".
|
# things like cluster spots where the comment is just "POTA".
|
||||||
found_sig = get_sig_name_from_comment_name(sig_match.group(2))
|
found_activity = get_activity_name_from_comment_name(activity_match.group(2))
|
||||||
if not self.sig:
|
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.
|
# Now look to see if that activity name was followed by something that looks like a reference ID
|
||||||
# If so, add that to the sig_refs list for this spot.
|
# for that activity. If so, add that to the sig_refs list for this spot.
|
||||||
ref_regex = get_ref_regex_for_sig(found_sig)
|
ref_regex = get_ref_regex_for_activity(found_activity)
|
||||||
if ref_regex:
|
if ref_regex:
|
||||||
ref_matches = re.finditer(
|
ref_matches = re.finditer(
|
||||||
r"(^|\W)" + found_sig + r"([ -])(" + ref_regex + r")($|\W)",
|
r"(^|\W)" + found_activity + r"([ -])(" + ref_regex + r")($|\W)",
|
||||||
self.comment,
|
self.comment,
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
for ref_match in ref_matches:
|
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
|
# See if the comment looks like it contains any activity references *without* the corresponding activity
|
||||||
# where the SIG reference is unique-looking enough that we can't confuse it with any other SIG.
|
# name, but where the activity reference is unique-looking enough that we can't confuse it with any other
|
||||||
|
# activity.
|
||||||
if self.comment:
|
if self.comment:
|
||||||
for sig in SIGS:
|
for activity in ACTIVITIES:
|
||||||
if sig.refs_globally_unique and sig.ref_regex:
|
if activity.refs_globally_unique and activity.ref_regex:
|
||||||
ref_matches = re.finditer(r"(^|\W)(" + sig.ref_regex + r")($|\W)", self.comment, re.IGNORECASE)
|
ref_matches = re.finditer(
|
||||||
|
r"(^|\W)(" + activity.ref_regex + r")($|\W)", self.comment, re.IGNORECASE
|
||||||
|
)
|
||||||
for ref_match in ref_matches:
|
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
|
# First of all, if we haven't got an activity for this spot set yet, now we have. This
|
||||||
# like cluster spots where the comment is just "OHFF-1234", now we know it's WWFF.
|
# covers things like cluster spots where the comment is just "OHFF-1234", now we know
|
||||||
|
# it's WWFF.
|
||||||
if not self.sig:
|
if not self.sig:
|
||||||
self.sig = sig.name
|
self.sig = activity.name
|
||||||
self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(2).upper(), sig=sig.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
|
# Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a
|
||||||
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs
|
# reference in its initial call, we use this code to populate the rest of the data. This includes working
|
||||||
# from WAB and WAI, which count as a SIG even though there's no real lookup, just maths
|
# 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:
|
if self.sig_refs:
|
||||||
for sig_ref in self.sig_refs:
|
for activity_ref in self.sig_refs:
|
||||||
sig_ref = populate_missing_sig_ref_info(sig_ref)
|
activity_ref = populate_missing_activity_ref_info(activity_ref)
|
||||||
# If the spot itself doesn't have location yet, but the SIG ref does, extract it
|
# If the spot itself doesn't have location yet, but the activity ref does, extract it
|
||||||
if sig_ref.grid and not self.dx_grid:
|
if activity_ref.grid and not self.dx_grid:
|
||||||
self.dx_grid = sig_ref.grid
|
self.dx_grid = activity_ref.grid
|
||||||
if sig_ref.latitude and not self.dx_latitude and sig_ref.longitude and not self.dx_longitude:
|
if (
|
||||||
self.dx_latitude = sig_ref.latitude
|
activity_ref.latitude
|
||||||
self.dx_longitude = sig_ref.longitude
|
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":
|
if self.sig == "WAB" or self.sig == "WAI" or self.sig == "Tiles":
|
||||||
self.dx_location_source = LocationSourceForSpot.GRID
|
self.dx_location_source = LocationSourceForSpot.GRID
|
||||||
else:
|
else:
|
||||||
self.dx_location_source = LocationSourceForSpot.SIG_REF_LOOKUP
|
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
|
# If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
|
||||||
# and apply it to the whole spot.
|
# reference's activity and apply it to the whole spot.
|
||||||
if self.sig_refs and not self.sig:
|
if self.sig_refs and not self.sig:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.sig = self.sig_refs[0].sig
|
||||||
|
|
||||||
@@ -360,17 +377,17 @@ class Spot:
|
|||||||
self.propagation_mode = mode_tag
|
self.propagation_mode = mode_tag
|
||||||
logger.info(f"Seen a new propagation mode tag not yet in the system: {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 self.propagation_mode == "Satellite":
|
||||||
if not self.sig:
|
if not self.sig:
|
||||||
self.sig = "AMSAT"
|
self.sig = "AMSAT"
|
||||||
if not any(sig_ref.sig == "AMSAT" for sig_ref in self.sig_refs):
|
if not any(activity_ref.sig == "AMSAT" for activity_ref in self.sig_refs):
|
||||||
self.sig_refs.append(SIGRef(sig="AMSAT"))
|
self.sig_refs.append(ActivityRef(sig="AMSAT"))
|
||||||
if self.propagation_mode == "Earth-Moon-Earth":
|
if self.propagation_mode == "Earth-Moon-Earth":
|
||||||
if not self.sig:
|
if not self.sig:
|
||||||
self.sig = "EME"
|
self.sig = "EME"
|
||||||
if not any(sig_ref.sig == "EME" for sig_ref in self.sig_refs):
|
if not any(activity_ref.sig == "EME" for activity_ref in self.sig_refs):
|
||||||
self.sig_refs.append(SIGRef(sig="EME"))
|
self.sig_refs.append(ActivityRef(sig="EME"))
|
||||||
|
|
||||||
# Parse "de_grid -> dx_grid" structures from the comment
|
# Parse "de_grid -> dx_grid" structures from the comment
|
||||||
if self.comment:
|
if self.comment:
|
||||||
@@ -426,8 +443,8 @@ class Spot:
|
|||||||
self.dx_grid = dx_call_info.grid
|
self.dx_grid = dx_call_info.grid
|
||||||
self.dx_location_source = dx_call_info.location_source
|
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,
|
# Determine a "QTH" string. If we have an activity ref, pick the first one and turn it into a suitable
|
||||||
# otherwise see what they have set on an online lookup service.
|
# string, otherwise see what they have set on an online lookup service.
|
||||||
if self.sig_refs:
|
if self.sig_refs:
|
||||||
qth = self.sig_refs[0].id
|
qth = self.sig_refs[0].id
|
||||||
if self.sig_refs[0].name:
|
if self.sig_refs[0].name:
|
||||||
@@ -482,7 +499,7 @@ class Spot:
|
|||||||
self.de_longitude = de_call_info.longitude
|
self.de_longitude = de_call_info.longitude
|
||||||
self.de_grid = de_call_info.grid
|
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"
|
self.icon = "fa-tower-cell"
|
||||||
if self.sig_refs and self.sig_refs[0].icon:
|
if self.sig_refs and self.sig_refs[0].icon:
|
||||||
self.icon = 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)
|
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
|
||||||
|
|
||||||
def _append_sig_ref_if_missing(self, new_sig_ref):
|
def _append_activity_ref_if_missing(self, new_activity_ref):
|
||||||
"""Append a sig_ref to the list, so long as it's not already there."""
|
"""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_activity_ref.id = new_activity_ref.id.strip().upper()
|
||||||
new_sig_ref.sig = new_sig_ref.sig.strip().upper()
|
new_activity_ref.sig = new_activity_ref.sig.strip().upper()
|
||||||
if new_sig_ref.id == "":
|
if new_activity_ref.id == "":
|
||||||
return
|
return
|
||||||
for sig_ref in self.sig_refs:
|
for activity_ref in self.sig_refs:
|
||||||
if sig_ref.id == new_sig_ref.id and sig_ref.sig == new_sig_ref.sig:
|
if activity_ref.id == new_activity_ref.id and activity_ref.sig == new_activity_ref.sig:
|
||||||
return
|
return
|
||||||
self.sig_refs.append(new_sig_ref)
|
self.sig_refs.append(new_activity_ref)
|
||||||
|
|
||||||
def expired(self):
|
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
|
"""Decide if this spot has expired (in which case it should not be added to the system in the first place, and not
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ You can embed Spothole's web interface in another website, e.g. for use as part
|
|||||||
URL parameters can be used to trigger an "embedded" mode which hides the headers, footers and settings. In this mode,
|
URL parameters can be used to trigger an "embedded" mode which hides the headers, footers and settings. In this mode,
|
||||||
you provide configuration for the various filter and display options via additional URL parameters. Any settings that
|
you provide configuration for the various filter and display options via additional URL parameters. Any settings that
|
||||||
the user has set for Spothole are ignored. This is so that the embedding site can select, for example, their choice of
|
the user has set for Spothole are ignored. This is so that the embedding site can select, for example, their choice of
|
||||||
dark mode or SIG filters, which will not impact how Spothole appears when the user accesses it directly. Effectively, it
|
dark mode or activity filters, which will not impact how Spothole appears when the user accesses it directly. Effectively, it
|
||||||
becomes separate to their normal Spothole settings.
|
becomes separate to their normal Spothole settings.
|
||||||
|
|
||||||
Setting `embedded` to true is important for the rest of the settings to be applied; otherwise, the user's defaults will
|
Setting `embedded` to true is important for the rest of the settings to be applied; otherwise, the user's defaults will
|
||||||
@@ -29,7 +29,7 @@ a mapping exists.
|
|||||||
| `limit` | 25, 50, 100, 200, 500 | 100 | `?limit=100` | Sets the number of alerts that will be displayed on the alerts page |
|
| `limit` | 25, 50, 100, 200, 500 | 100 | `?limit=100` | Sets the number of alerts that will be displayed on the alerts page |
|
||||||
| `max_age` | 300, 600, 1800, 3600 | 1800 | `?max_age=1800` | Sets the maximum age of spots displayed on the map and bands pages, in seconds. |
|
| `max_age` | 300, 600, 1800, 3600 | 1800 | `?max_age=1800` | Sets the maximum age of spots displayed on the map and bands pages, in seconds. |
|
||||||
| `band` | Comma-separated list | (all) | `?band=20m,40m` | Sets the list of bands that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
| `band` | Comma-separated list | (all) | `?band=20m,40m` | Sets the list of bands that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||||
| `sig` | Comma-separated list | (all) | `?sig=POTA,SOTA,NO_SIG` | Sets the list of SIGs that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
| `sig` | Comma-separated list | (all) | `?sig=POTA,SOTA,NO_SIG` | Sets the list of activities that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||||
| `source` | Comma-separated list | (all) | `?source=Cluster` | Sets the list of sources that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. |
|
| `source` | Comma-separated list | (all) | `?source=Cluster` | Sets the list of sources that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. |
|
||||||
| `mode_type` | Comma-separated list | (all) | `?mode_type=PHONE,CW` | Sets the list of mode types that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
| `mode_type` | Comma-separated list | (all) | `?mode_type=PHONE,CW` | Sets the list of mode types that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||||
| `dx_continent` | Comma-separated list | (all) | `?dx_continent=NA,SA` | Sets the list of DX Continents that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. |
|
| `dx_continent` | Comma-separated list | (all) | `?dx_continent=NA,SA` | Sets the list of DX Continents that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. |
|
||||||
|
|||||||
+2
-2
@@ -18,8 +18,8 @@ To navigate your way around the source code, this list may help.
|
|||||||
services
|
services
|
||||||
* `/providers/callsign` - Classes providing callsign lookup data by accessing bundled data files or the APIs of other
|
* `/providers/callsign` - Classes providing callsign lookup data by accessing bundled data files or the APIs of other
|
||||||
services
|
services
|
||||||
* `/providers/sigrefdata` - Classes providing SIG reference lookup data by accessing bundled data files or the APIs of
|
* `/providers/activityrefdata` - Classes providing activity reference lookup data by accessing bundled data files or
|
||||||
other services
|
the APIs of other services
|
||||||
* `/webserver` - Classes for running Spothole's own web server
|
* `/webserver` - Classes for running Spothole's own web server
|
||||||
* `/telnetserver` - Classes for running Spothole's telnet server
|
* `/telnetserver` - Classes for running Spothole's telnet server
|
||||||
* `spothole.py` - Main application script
|
* `spothole.py` - Main application script
|
||||||
|
|||||||
+7
-5
@@ -9,11 +9,13 @@ from core.data_store import DATA_STORE
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SIGRefDataProvider:
|
class ActivityRefDataProvider:
|
||||||
"""Generic SIG reference data provider class. Subclasses of this query the individual URLs or files for data."""
|
"""Generic activity reference data provider class. Subclasses of this query the individual URLs or files for
|
||||||
|
data."""
|
||||||
|
|
||||||
def __init__(self, sig_name, provider_config):
|
def __init__(self, sig_name, provider_config):
|
||||||
"""Constructor"""
|
"""Constructor. Note the parameter and attribute are still named "sig_name" for consistency with the API's
|
||||||
|
"sig" field name."""
|
||||||
|
|
||||||
self.sig_name = sig_name
|
self.sig_name = sig_name
|
||||||
self.enabled = provider_config["enabled"]
|
self.enabled = provider_config["enabled"]
|
||||||
@@ -37,9 +39,9 @@ class SIGRefDataProvider:
|
|||||||
"""Add all the provided reference data objects to the data store."""
|
"""Add all the provided reference data objects to the data store."""
|
||||||
|
|
||||||
# with transact() batches all writes together to save making thousands of individual sqlite writes
|
# with transact() batches all writes together to save making thousands of individual sqlite writes
|
||||||
with DATA_STORE.sigrefs.transact():
|
with DATA_STORE.activity_refs.transact():
|
||||||
for d in new_data:
|
for d in new_data:
|
||||||
DATA_STORE.sigrefs.set(f"{self.sig_name}:{d.id}", d)
|
DATA_STORE.activity_refs.set(f"{self.sig_name}:{d.id}", d)
|
||||||
|
|
||||||
# For the big data sources, loading will take a few minutes. If we want to shut down the software neatly
|
# For the big data sources, loading will take a few minutes. If we want to shut down the software neatly
|
||||||
# within the first few minutes of startup, we need a way to abort this expensive process of filling up the
|
# within the first few minutes of startup, we need a way to abort this expensive process of filling up the
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ARLHS(FileDownloadSIGRefDataProvider):
|
class ARLHS(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Amateur Radio Light House Society"""
|
"""Activity ref data provider for Amateur Radio Light House Society"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "ARLHS"
|
ACTIVITY = "ARLHS"
|
||||||
DATA_URL = "https://www.gma.rocks/download/lighthouse.csv"
|
DATA_URL = "https://www.gma.rocks/download/lighthouse.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -24,11 +24,11 @@ class ARLHS(FileDownloadSIGRefDataProvider):
|
|||||||
if "ARLHS" in row and row["ARLHS"] != "":
|
if "ARLHS" in row and row["ARLHS"] != "":
|
||||||
ref_id = row["ARLHS"]
|
ref_id = row["ARLHS"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=SIGRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||||
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
|
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
|
||||||
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
|
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
|
||||||
@@ -41,7 +41,7 @@ class ARLHS(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -2,20 +2,20 @@ from time import sleep
|
|||||||
|
|
||||||
from pyhamtools.locator import latlong_to_locator
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class COTA(FileDownloadSIGRefDataProvider):
|
class COTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Castles on the Air"""
|
"""Activity ref data provider for Castles on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "COTA"
|
ACTIVITY = "COTA"
|
||||||
DATA_URL = "https://www.cotagroup.org/cotagroup/map/data/castles-all-7d90ee2a5e1175e5dece1bbf9dc87504.json"
|
DATA_URL = "https://www.cotagroup.org/cotagroup/map/data/castles-all-7d90ee2a5e1175e5dece1bbf9dc87504.json"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -29,11 +29,11 @@ class COTA(FileDownloadSIGRefDataProvider):
|
|||||||
grid = latlong_to_locator(lat, lon)
|
grid = latlong_to_locator(lat, lon)
|
||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=name,
|
name=name,
|
||||||
ref_type=SIGRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
grid=grid,
|
grid=grid,
|
||||||
latitude=lat,
|
latitude=lat,
|
||||||
longitude=lon,
|
longitude=lon,
|
||||||
@@ -45,7 +45,7 @@ class COTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -3,20 +3,20 @@ from time import sleep
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class DCE(FileDownloadSIGRefDataProvider):
|
class DCE(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Diploma Castillos de España"""
|
"""Activity ref data provider for Diploma Castillos de España"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 365
|
POLL_INTERVAL_DAYS = 365
|
||||||
SIG = "DCE"
|
ACTIVITY = "DCE"
|
||||||
DATA_URL = "https://www.acracb.org/dce/descargas/General/directorio_referencias_dce.xls"
|
DATA_URL = "https://www.acracb.org/dce/descargas/General/directorio_referencias_dce.xls"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -27,7 +27,7 @@ class DCE(FileDownloadSIGRefDataProvider):
|
|||||||
for index, row in df.iterrows():
|
for index, row in df.iterrows():
|
||||||
if row.iloc[0] and row.iloc[2]:
|
if row.iloc[0] and row.iloc[2]:
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(sig=self.SIG, id=row.iloc[0].strip(), name=row.iloc[2].strip(), ref_type=SIGRefType.CASTLE)
|
ActivityRef(sig=self.ACTIVITY, id=row.iloc[0].strip(), name=row.iloc[2].strip(), ref_type=ActivityRefType.CASTLE)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
@@ -35,7 +35,7 @@ class DCE(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -3,20 +3,20 @@ from time import sleep
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class DEFE(FileDownloadSIGRefDataProvider):
|
class DEFE(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Diploma Estationes de Ferrocarril de España"""
|
"""Activity ref data provider for Diploma Estationes de Ferrocarril de España"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 365
|
POLL_INTERVAL_DAYS = 365
|
||||||
SIG = "DEFE"
|
ACTIVITY = "DEFE"
|
||||||
DATA_URL = "https://www.acracb.org/defe/descargas/General/directorio_referencias_defe.xls"
|
DATA_URL = "https://www.acracb.org/defe/descargas/General/directorio_referencias_defe.xls"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -31,7 +31,7 @@ class DEFE(FileDownloadSIGRefDataProvider):
|
|||||||
|
|
||||||
if row.iloc[0] and row.iloc[1]:
|
if row.iloc[0] and row.iloc[1]:
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(sig=self.SIG, id=row.iloc[0].strip(), name=row.iloc[1].strip(), ref_type=SIGRefType.BUILDING)
|
ActivityRef(sig=self.ACTIVITY, id=row.iloc[0].strip(), name=row.iloc[1].strip(), ref_type=ActivityRefType.BUILDING)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
@@ -39,7 +39,7 @@ class DEFE(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -3,21 +3,21 @@ from time import sleep
|
|||||||
|
|
||||||
from pyhamtools.locator import latlong_to_locator
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.local_file_sig_ref_data_provider import (
|
from providers.activityrefdata.local_file_activity_ref_data_provider import (
|
||||||
LocalFileSIGRefDataProvider,
|
LocalFileActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DME(LocalFileSIGRefDataProvider):
|
class DME(LocalFileActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Diploma Municipios de Espana"""
|
"""Activity ref data provider for Diploma Municipios de Espana"""
|
||||||
|
|
||||||
SIG = "DME"
|
ACTIVITY = "DME"
|
||||||
PATH = "datafiles/MUNICIPIOS.csv"
|
PATH = "datafiles/MUNICIPIOS.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.PATH)
|
super().__init__(self.ACTIVITY, provider_config, self.PATH)
|
||||||
|
|
||||||
def _file_to_data(self, path):
|
def _file_to_data(self, path):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -26,7 +26,7 @@ class DME(LocalFileSIGRefDataProvider):
|
|||||||
# Store reference IDs with the "DME-" prefix rather than just the number. This will prevent Spothole
|
# Store reference IDs with the "DME-" prefix rather than just the number. This will prevent Spothole
|
||||||
# from agressively thinking every number in a spot comment is DME after it's seen "DME" once. The only
|
# from agressively thinking every number in a spot comment is DME after it's seen "DME" once. The only
|
||||||
# numbers that count are straight after "DME " or "DME-". The dash versus space is normalised in
|
# numbers that count are straight after "DME " or "DME-". The dash versus space is normalised in
|
||||||
# sig_lookup_helper.py.
|
# activity_lookup_helper.py.
|
||||||
ref_id = "DME-" + row["COD_INE"][:5]
|
ref_id = "DME-" + row["COD_INE"][:5]
|
||||||
latitude = (
|
latitude = (
|
||||||
float(row["LATITUD_ETRS89_REGCAN95"].replace(",", "."))
|
float(row["LATITUD_ETRS89_REGCAN95"].replace(",", "."))
|
||||||
@@ -39,10 +39,10 @@ class DME(LocalFileSIGRefDataProvider):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
ref = SIGRef(
|
ref = ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
ref_type=SIGRefType.TOWN,
|
ref_type=ActivityRefType.TOWN,
|
||||||
name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}",
|
name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}",
|
||||||
latitude=latitude,
|
latitude=latitude,
|
||||||
longitude=longitude,
|
longitude=longitude,
|
||||||
@@ -56,7 +56,7 @@ class DME(LocalFileSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class DMUE(FileDownloadSIGRefDataProvider):
|
class DMUE(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Diploma Museos de España"""
|
"""Activity ref data provider for Diploma Museos de España"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 365
|
POLL_INTERVAL_DAYS = 365
|
||||||
SIG = "DMUE"
|
ACTIVITY = "DMUE"
|
||||||
DATA_URL = "https://dmue.radiogalena.es/nom_dmue.csv"
|
DATA_URL = "https://dmue.radiogalena.es/nom_dmue.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -22,7 +22,7 @@ class DMUE(FileDownloadSIGRefDataProvider):
|
|||||||
for row in csv.reader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
|
for row in csv.reader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
|
||||||
if len(row) > 1 and row[0] and row[1]:
|
if len(row) > 1 and row[0] and row[1]:
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(sig=self.SIG, id=row[0].strip(), name=row[1].strip(), ref_type=SIGRefType.BUILDING)
|
ActivityRef(sig=self.ACTIVITY, id=row[0].strip(), name=row[1].strip(), ref_type=ActivityRefType.BUILDING)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
@@ -30,7 +30,7 @@ class DMUE(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -3,20 +3,20 @@ from time import sleep
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class DMVE(FileDownloadSIGRefDataProvider):
|
class DMVE(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Diploma Monumentos y Vestigios de España"""
|
"""Activity ref data provider for Diploma Monumentos y Vestigios de España"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 365
|
POLL_INTERVAL_DAYS = 365
|
||||||
SIG = "DMVE"
|
ACTIVITY = "DMVE"
|
||||||
DATA_URL = "https://www.acracb.org/dmve/descargas/General/directorio_referencias_dmve.xls"
|
DATA_URL = "https://www.acracb.org/dmve/descargas/General/directorio_referencias_dmve.xls"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -36,14 +36,14 @@ class DMVE(FileDownloadSIGRefDataProvider):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if ref and name:
|
if ref and name:
|
||||||
new_data.append(SIGRef(sig=self.SIG, id=ref.strip(), name=name.strip(), ref_type=SIGRefType.BUILDING))
|
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref.strip(), name=name.strip(), ref_type=ActivityRefType.BUILDING))
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
# the data in this case
|
# the data in this case
|
||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class DTMBA(FileDownloadSIGRefDataProvider):
|
class DTMBA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Diploma Teatri Musei Belle Arti"""
|
"""Activity ref data provider for Diploma Teatri Musei Belle Arti"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "DTMBA"
|
ACTIVITY = "DTMBA"
|
||||||
DATA_URL = "https://www.iu1fig.com/share/iz0eik/dtmba/export.php"
|
DATA_URL = "https://www.iu1fig.com/share/iz0eik/dtmba/export.php"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -21,14 +21,14 @@ class DTMBA(FileDownloadSIGRefDataProvider):
|
|||||||
split = row.split(";")
|
split = row.split(";")
|
||||||
ref_id = split[0]
|
ref_id = split[0]
|
||||||
ref_name = split[1]
|
ref_name = split[1]
|
||||||
new_data.append(SIGRef(sig=self.SIG, id=ref_id, name=ref_name, ref_type=SIGRefType.BUILDING))
|
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id, name=ref_name, ref_type=ActivityRefType.BUILDING))
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
# the data in this case
|
# the data in this case
|
||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -3,20 +3,20 @@ from time import sleep
|
|||||||
|
|
||||||
import pdfplumber
|
import pdfplumber
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class FEA(FileDownloadSIGRefDataProvider):
|
class FEA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Diploma Faros de España"""
|
"""Activity ref data provider for Diploma Faros de España"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "FEA"
|
ACTIVITY = "FEA"
|
||||||
DATA_URL = "http://ea5ol.net/Lista%20Faros.pdf"
|
DATA_URL = "http://ea5ol.net/Lista%20Faros.pdf"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -41,15 +41,15 @@ class FEA(FileDownloadSIGRefDataProvider):
|
|||||||
# prefix and just use FEA-1234 or FEA 1234, so we add both copies to the database.
|
# prefix and just use FEA-1234 or FEA 1234, so we add both copies to the database.
|
||||||
ref_id_1 = row[0].strip()
|
ref_id_1 = row[0].strip()
|
||||||
ref_id_2 = ref_id_1.replace("D-", "FEA-").replace("E-", "FEA-")
|
ref_id_2 = ref_id_1.replace("D-", "FEA-").replace("E-", "FEA-")
|
||||||
new_data.append(SIGRef(sig=self.SIG, id=ref_id_1, name=row[1].strip(), ref_type=SIGRefType.LIGHTHOUSE))
|
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id_1, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE))
|
||||||
new_data.append(SIGRef(sig=self.SIG, id=ref_id_2, name=row[1].strip(), ref_type=SIGRefType.LIGHTHOUSE))
|
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id_2, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE))
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
# the data in this case
|
# the data in this case
|
||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
+18
-17
@@ -7,13 +7,14 @@ from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
|
|||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
from core.url_data_cache import URLDataCache
|
from core.url_data_cache import URLDataCache
|
||||||
from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider
|
from providers.activityrefdata.activity_ref_data_provider import ActivityRefDataProvider
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
|
class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
|
||||||
"""Generic SIG ref data provider class for providers that fetch their data from the web by downloading a file."""
|
"""Generic activity ref data provider class for providers that fetch their data from the web by downloading a
|
||||||
|
file."""
|
||||||
|
|
||||||
def __init__(self, sig_name, provider_config, url, poll_interval):
|
def __init__(self, sig_name, provider_config, url, poll_interval):
|
||||||
"""Set up the provider, note poll_interval is in *days*."""
|
"""Set up the provider, note poll_interval is in *days*."""
|
||||||
@@ -21,13 +22,13 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
|
|||||||
self._url = url
|
self._url = url
|
||||||
self._poll_interval = poll_interval
|
self._poll_interval = poll_interval
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._url_data_cache = URLDataCache(f"sigrefdata_{sig_name}")
|
self._url_data_cache = URLDataCache(f"sigrefdata_{sig_name}") # cache dir name kept for continuity
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
|
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
|
||||||
# subsequent polls, so start() returns immediately and the application can continue starting.
|
# subsequent polls, so start() returns immediately and the application can continue starting.
|
||||||
logger.info(f"Set up query of {self.sig_name} SIG ref data every {self._poll_interval!s} days.")
|
logger.info(f"Set up query of {self.sig_name} activity ref data every {self._poll_interval!s} days.")
|
||||||
self._thread = Thread(target=self._run, name=f"FileDownloadSIGRefDataProvider-{self.sig_name}", daemon=True)
|
self._thread = Thread(target=self._run, name=f"FileDownloadActivityRefDataProvider-{self.sig_name}", daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@@ -35,7 +36,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
|
|||||||
if self._thread:
|
if self._thread:
|
||||||
self._thread.join(timeout=35)
|
self._thread.join(timeout=35)
|
||||||
if self._thread.is_alive():
|
if self._thread.is_alive():
|
||||||
logger.warning(f"{self.sig_name} SIG ref data worker thread did not exit on time and will be killed.")
|
logger.warning(f"{self.sig_name} activity ref data worker thread did not exit on time and will be killed.")
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
while True:
|
while True:
|
||||||
@@ -47,37 +48,37 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
|
|||||||
try:
|
try:
|
||||||
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
|
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
|
||||||
# caching, but just so continual restarts of the software during testing don't hammer the servers.
|
# caching, but just so continual restarts of the software during testing don't hammer the servers.
|
||||||
logger.debug(f"Downloading {self.sig_name} SIG ref data...")
|
logger.debug(f"Downloading {self.sig_name} activity ref data...")
|
||||||
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
|
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
|
||||||
# Check response code was good
|
# Check response code was good
|
||||||
if http_response.ok:
|
if http_response.ok:
|
||||||
# Pass off to the subclass for processing
|
# Pass off to the subclass for processing
|
||||||
new_data = self._http_response_to_data(http_response)
|
new_data = self._http_response_to_data(http_response)
|
||||||
# Add the new data to the SIG Ref data store
|
# Add the new data to the activity ref data store
|
||||||
if new_data:
|
if new_data:
|
||||||
self._add_data(new_data)
|
self._add_data(new_data)
|
||||||
|
|
||||||
self.status = "OK"
|
self.status = "OK"
|
||||||
self.last_update_time = datetime.now(pytz.UTC)
|
self.last_update_time = datetime.now(pytz.UTC)
|
||||||
logger.debug(f"Received SIG ref data for {self.sig_name}")
|
logger.debug(f"Received activity ref data for {self.sig_name}")
|
||||||
else:
|
else:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.warning(f"HTTP {http_response.status_code} when downloading SIG ref data for {self.sig_name}.")
|
logger.warning(f"HTTP {http_response.status_code} when downloading activity ref data for {self.sig_name}.")
|
||||||
|
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.warning(f"Connection error when downloading SIG ref data for {self.sig_name}.")
|
logger.warning(f"Connection error when downloading activity ref data for {self.sig_name}.")
|
||||||
except (ConnectTimeout, ReadTimeout):
|
except (ConnectTimeout, ReadTimeout):
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.warning(f"Timeout when downloading SIG ref data for {self.sig_name}.")
|
logger.warning(f"Timeout when downloading activity ref data for {self.sig_name}.")
|
||||||
except Exception:
|
except Exception:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.exception(f"Exception in HTTP SIG Ref Data Provider ({self.sig_name})")
|
logger.exception(f"Exception in HTTP Activity Ref Data Provider ({self.sig_name})")
|
||||||
self._stop_event.wait(timeout=1)
|
self._stop_event.wait(timeout=1)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
"""Convert an HTTP response returned by the server into SIG Ref data. The whole response is provided here so the
|
"""Convert an HTTP response returned by the server into activity ref data. The whole response is provided here
|
||||||
subclass implementations can check for HTTP status codes if necessary, and handle the response as JSON, CSV,
|
so the subclass implementations can check for HTTP status codes if necessary, and handle the response as
|
||||||
whatever the remote file actually is."""
|
JSON, CSV, whatever the remote file actually is."""
|
||||||
|
|
||||||
raise NotImplementedError("Subclasses must implement this method")
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GMA(FileDownloadSIGRefDataProvider):
|
class GMA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Global Mountain Activity"""
|
"""Activity ref data provider for Global Mountain Activity"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "GMA"
|
ACTIVITY = "GMA"
|
||||||
DATA_URL = "https://www.gma.rocks/download/summits.csv"
|
DATA_URL = "https://www.gma.rocks/download/summits.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
||||||
ref_id = row["Reference"]
|
ref_id = row["Reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||||
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
|
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
|
||||||
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
|
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
|
||||||
@@ -43,7 +43,7 @@ class GMA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ILLW(FileDownloadSIGRefDataProvider):
|
class ILLW(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for International Lighthouse & Lightship Weekend"""
|
"""Activity ref data provider for International Lighthouse & Lightship Weekend"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "ILLW"
|
ACTIVITY = "ILLW"
|
||||||
DATA_URL = "https://www.gma.rocks/download/lighthouse.csv"
|
DATA_URL = "https://www.gma.rocks/download/lighthouse.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -24,11 +24,11 @@ class ILLW(FileDownloadSIGRefDataProvider):
|
|||||||
if "ILLW" in row and row["ILLW"] != "":
|
if "ILLW" in row and row["ILLW"] != "":
|
||||||
ref_id = row["ILLW"]
|
ref_id = row["ILLW"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=SIGRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||||
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
|
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
|
||||||
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
|
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
|
||||||
@@ -41,7 +41,7 @@ class ILLW(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -3,24 +3,24 @@ from time import sleep
|
|||||||
|
|
||||||
from pyhamtools.locator import latlong_to_locator
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class IOTA(FileDownloadSIGRefDataProvider):
|
class IOTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Islands on the Air"""
|
"""Activity ref data provider for Islands on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 365
|
POLL_INTERVAL_DAYS = 365
|
||||||
SIG = "IOTA"
|
ACTIVITY = "IOTA"
|
||||||
DATA_URL = "https://www.iota-world.org/islands-on-the-air/downloads/download-file.html?path=groups.json"
|
DATA_URL = "https://www.iota-world.org/islands-on-the-air/downloads/download-file.html?path=groups.json"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -41,11 +41,11 @@ class IOTA(FileDownloadSIGRefDataProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=ref["name"],
|
name=ref["name"],
|
||||||
ref_type=SIGRefType.ISLAND,
|
ref_type=ActivityRefType.ISLAND,
|
||||||
grid=grid,
|
grid=grid,
|
||||||
latitude=latitude,
|
latitude=latitude,
|
||||||
longitude=longitude,
|
longitude=longitude,
|
||||||
@@ -57,7 +57,7 @@ class IOTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from providers.activityrefdata.pnp_kml_activity_ref_data_provider import (
|
||||||
|
ParksNPeaksKMLActivityRefDataProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class KRMNPA(ParksNPeaksKMLActivityRefDataProvider):
|
||||||
|
"""Activity ref data provider for the Keith Roget Memorrial National Parks Award (KRMNPA)."""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 365
|
||||||
|
ACTIVITY = "KRMNPA"
|
||||||
|
DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=KRMNPA&poiFormat=4"
|
||||||
|
|
||||||
|
def __init__(self, provider_config):
|
||||||
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
@@ -2,22 +2,22 @@ from time import sleep
|
|||||||
|
|
||||||
from pyhamtools.locator import locator_to_latlong
|
from pyhamtools.locator import locator_to_latlong
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LLOTA(FileDownloadSIGRefDataProvider):
|
class LLOTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Lagos y Lagunas on the Air"""
|
"""Activity ref data provider for Lagos y Lagunas on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 7
|
POLL_INTERVAL_DAYS = 7
|
||||||
SIG = "LLOTA"
|
ACTIVITY = "LLOTA"
|
||||||
DATA_URL = "https://llota.app/api/public/references"
|
DATA_URL = "https://llota.app/api/public/references"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -29,11 +29,11 @@ class LLOTA(FileDownloadSIGRefDataProvider):
|
|||||||
ll = locator_to_latlong(grid)
|
ll = locator_to_latlong(grid)
|
||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=str(ref["name"]),
|
name=str(ref["name"]),
|
||||||
ref_type=SIGRefType.LAKE,
|
ref_type=ActivityRefType.LAKE,
|
||||||
url=f"https://llota.app/list/ref/{ref_id}",
|
url=f"https://llota.app/list/ref/{ref_id}",
|
||||||
grid=grid,
|
grid=grid,
|
||||||
latitude=ll[0],
|
latitude=ll[0],
|
||||||
@@ -46,7 +46,7 @@ class LLOTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from providers.activityrefdata.activity_ref_data_provider import ActivityRefDataProvider
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalFileActivityRefDataProvider(ActivityRefDataProvider):
|
||||||
|
"""Generic activity ref data provider class for providers that fetch their data from a local file on startup."""
|
||||||
|
|
||||||
|
def __init__(self, sig_name, provider_config, path):
|
||||||
|
super().__init__(sig_name, provider_config)
|
||||||
|
self._path = path
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
logger.debug(f"Loading {self.sig_name} activity ref data from file.")
|
||||||
|
try:
|
||||||
|
new_data = self._file_to_data(self._path)
|
||||||
|
if new_data:
|
||||||
|
self._add_data(new_data)
|
||||||
|
self.status = "OK"
|
||||||
|
self.last_update_time = datetime.now(pytz.UTC)
|
||||||
|
else:
|
||||||
|
self.status = "Error"
|
||||||
|
logger.info(f"Failed to load activity ref data for {self.sig_name}")
|
||||||
|
except Exception:
|
||||||
|
self.status = "Error"
|
||||||
|
logger.exception(f"Exception in local file Activity Ref Data Provider ({self.sig_name})")
|
||||||
|
|
||||||
|
def _file_to_data(self, path):
|
||||||
|
"""Load a file on the given path and turn it into activity ref data."""
|
||||||
|
|
||||||
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MOTA(FileDownloadSIGRefDataProvider):
|
class MOTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Mills on the Air"""
|
"""Activity ref data provider for Mills on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "MOTA"
|
ACTIVITY = "MOTA"
|
||||||
DATA_URL = "https://www.gma.rocks/download/mills.csv"
|
DATA_URL = "https://www.gma.rocks/download/mills.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
||||||
ref_id = row["Reference"]
|
ref_id = row["Reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=SIGRefType.MILL,
|
ref_type=ActivityRefType.MILL,
|
||||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||||
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
|
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
|
||||||
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
|
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
|
||||||
@@ -40,7 +40,7 @@ class MOTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -2,20 +2,20 @@ from time import sleep
|
|||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class PGA(FileDownloadSIGRefDataProvider):
|
class PGA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Polish Gmina Award"""
|
"""Activity ref data provider for Polish Gmina Award"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "PGA"
|
ACTIVITY = "PGA"
|
||||||
DATA_URL = "http://www.spga.pl/lista_pga2.php"
|
DATA_URL = "http://www.spga.pl/lista_pga2.php"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -38,11 +38,11 @@ class PGA(FileDownloadSIGRefDataProvider):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=name,
|
name=name,
|
||||||
ref_type=SIGRefType.REGION,
|
ref_type=ActivityRefType.REGION,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ class PGA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
+10
-10
@@ -4,16 +4,16 @@ from time import sleep
|
|||||||
from fastkml import kml
|
from fastkml import kml
|
||||||
from pyhamtools.locator import latlong_to_locator
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider):
|
class ParksNPeaksKMLActivityRefDataProvider(FileDownloadActivityRefDataProvider):
|
||||||
"""Base class for SIG ref data providers that use parksnpeaks.org KML POI feeds and have references that use the
|
"""Base class for activity ref data providers that use parksnpeaks.org KML POI feeds and have references that
|
||||||
VKFF refs rather than their own system (i.e. KRMNPA and SANPCPA)."""
|
use the VKFF refs rather than their own system (i.e. KRMNPA and SANPCPA)."""
|
||||||
|
|
||||||
REF_PATTERN = re.compile(r"VKFF-\d+")
|
REF_PATTERN = re.compile(r"VKFF-\d+")
|
||||||
|
|
||||||
@@ -40,11 +40,11 @@ class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider):
|
|||||||
|
|
||||||
longitude, latitude = placemark.geometry.x, placemark.geometry.y
|
longitude, latitude = placemark.geometry.x, placemark.geometry.y
|
||||||
|
|
||||||
ref = SIGRef(
|
ref = ActivityRef(
|
||||||
sig=self.sig_name,
|
sig=self.sig_name,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=placemark.name,
|
name=placemark.name,
|
||||||
ref_type=SIGRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
url=f"https://parksnpeaks.org/getPark.php?actPark={ref_id}",
|
url=f"https://parksnpeaks.org/getPark.php?actPark={ref_id}",
|
||||||
latitude=latitude,
|
latitude=latitude,
|
||||||
longitude=longitude,
|
longitude=longitude,
|
||||||
@@ -58,7 +58,7 @@ class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
return new_data
|
return new_data
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class POTA(FileDownloadSIGRefDataProvider):
|
class POTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Parks on the Air"""
|
"""Activity ref data provider for Parks on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 7
|
POLL_INTERVAL_DAYS = 7
|
||||||
SIG = "POTA"
|
ACTIVITY = "POTA"
|
||||||
DATA_URL = "https://pota.app/all_parks_ext.csv"
|
DATA_URL = "https://pota.app/all_parks_ext.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
ref_id = row["reference"]
|
ref_id = row["reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("name", None),
|
name=row.get("name", None),
|
||||||
ref_type=SIGRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
url=f"https://pota.app/#/park/{ref_id}",
|
url=f"https://pota.app/#/park/{ref_id}",
|
||||||
grid=row.get("grid", None),
|
grid=row.get("grid", None),
|
||||||
latitude=float(row["latitude"]) if "latitude" in row and row["latitude"] != "" else None,
|
latitude=float(row["latitude"]) if "latitude" in row and row["latitude"] != "" else None,
|
||||||
@@ -40,7 +40,7 @@ class POTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from providers.activityrefdata.pnp_kml_activity_ref_data_provider import (
|
||||||
|
ParksNPeaksKMLActivityRefDataProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SANPCPA(ParksNPeaksKMLActivityRefDataProvider):
|
||||||
|
"""Activity ref data provider for the South Australia National Parks and Conservation Parks Award (SANPCPA)."""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 365
|
||||||
|
ACTIVITY = "SANPCPA"
|
||||||
|
DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=SANPCPA&poiFormat=4"
|
||||||
|
|
||||||
|
def __init__(self, provider_config):
|
||||||
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SIOTA(FileDownloadSIGRefDataProvider):
|
class SIOTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Silos on the Air"""
|
"""Activity ref data provider for Silos on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "SIOTA"
|
ACTIVITY = "SIOTA"
|
||||||
DATA_URL = "https://www.silosontheair.com/data/silos.csv"
|
DATA_URL = "https://www.silosontheair.com/data/silos.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
ref_id = row["SILO_CODE"]
|
ref_id = row["SILO_CODE"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("NAME", None),
|
name=row.get("NAME", None),
|
||||||
ref_type=SIGRefType.SILO,
|
ref_type=ActivityRefType.SILO,
|
||||||
grid=row.get("LOCATOR", None),
|
grid=row.get("LOCATOR", None),
|
||||||
latitude=float(row["LAT"]) if "LAT" in row else None,
|
latitude=float(row["LAT"]) if "LAT" in row else None,
|
||||||
longitude=float(row["LNG"]) if "LNG" in row else None,
|
longitude=float(row["LNG"]) if "LNG" in row else None,
|
||||||
@@ -39,7 +39,7 @@ class SIOTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -3,22 +3,22 @@ from time import sleep
|
|||||||
|
|
||||||
from pyhamtools.locator import latlong_to_locator
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SOTA(FileDownloadSIGRefDataProvider):
|
class SOTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Summits on the Air"""
|
"""Activity ref data provider for Summits on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "SOTA"
|
ACTIVITY = "SOTA"
|
||||||
DATA_URL = "https://storage.sota.org.uk/summitslist.csv"
|
DATA_URL = "https://storage.sota.org.uk/summitslist.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -27,11 +27,11 @@ class SOTA(FileDownloadSIGRefDataProvider):
|
|||||||
latitude = float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None
|
latitude = float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None
|
||||||
longitude = float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None
|
longitude = float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None
|
||||||
altitude = float(row["AltM"]) if "AltM" in row and row["AltM"] != "" else None
|
altitude = float(row["AltM"]) if "AltM" in row and row["AltM"] != "" else None
|
||||||
ref = SIGRef(
|
ref = ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("SummitName", None),
|
name=row.get("SummitName", None),
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
url=f"https://www.sotadata.org.uk/en/summit/{ref_id}",
|
url=f"https://www.sotadata.org.uk/en/summit/{ref_id}",
|
||||||
latitude=latitude,
|
latitude=latitude,
|
||||||
longitude=longitude,
|
longitude=longitude,
|
||||||
@@ -47,7 +47,7 @@ class SOTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import csv
|
import csv
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.local_file_sig_ref_data_provider import (
|
from providers.activityrefdata.local_file_activity_ref_data_provider import (
|
||||||
LocalFileSIGRefDataProvider,
|
LocalFileActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Toilets(LocalFileSIGRefDataProvider):
|
class Toilets(LocalFileActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Toilets on the Air"""
|
"""Activity ref data provider for Toilets on the Air"""
|
||||||
|
|
||||||
SIG = "Toilets"
|
ACTIVITY = "Toilets"
|
||||||
PATH = "datafiles/toilets.csv"
|
PATH = "datafiles/toilets.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.PATH)
|
super().__init__(self.ACTIVITY, provider_config, self.PATH)
|
||||||
|
|
||||||
def _file_to_data(self, path):
|
def _file_to_data(self, path):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -23,11 +23,11 @@ class Toilets(LocalFileSIGRefDataProvider):
|
|||||||
dr = csv.DictReader(csv_data.splitlines())
|
dr = csv.DictReader(csv_data.splitlines())
|
||||||
for row in dr:
|
for row in dr:
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=row["ref"],
|
id=row["ref"],
|
||||||
name=row["ref"],
|
name=row["ref"],
|
||||||
ref_type=SIGRefType.TOILET,
|
ref_type=ActivityRefType.TOILET,
|
||||||
latitude=float(row["lat"]),
|
latitude=float(row["lat"]),
|
||||||
longitude=float(row["lon"]),
|
longitude=float(row["lon"]),
|
||||||
)
|
)
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Towers(FileDownloadSIGRefDataProvider):
|
class Towers(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Towers on the Air"""
|
"""Activity ref data provider for Towers on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "Towers"
|
ACTIVITY = "Towers"
|
||||||
DATA_URL = "https://wwtota.com/servis/generate_csv.php?ref=&filter=all"
|
DATA_URL = "https://wwtota.com/servis/generate_csv.php?ref=&filter=all"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
|
||||||
ref_id = row["Ref"]
|
ref_id = row["Ref"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Nazev", None),
|
name=row.get("Nazev", None),
|
||||||
ref_type=SIGRefType.TOWER,
|
ref_type=ActivityRefType.TOWER,
|
||||||
url=f"https://wwtota.com/seznam/karta_rozhledny.php?ref={ref_id}",
|
url=f"https://wwtota.com/seznam/karta_rozhledny.php?ref={ref_id}",
|
||||||
grid=row["Lokator"] if "Lokator" in row and row["Lokator"] != "" else None,
|
grid=row["Lokator"] if "Lokator" in row and row["Lokator"] != "" else None,
|
||||||
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
|
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
|
||||||
@@ -40,7 +40,7 @@ class Towers(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -4,24 +4,24 @@ from time import sleep
|
|||||||
|
|
||||||
from pyhamtools.locator import latlong_to_locator
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class WCA(FileDownloadSIGRefDataProvider):
|
class WCA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for World Castles Award"""
|
"""Activity ref data provider for World Castles Award"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "WCA"
|
ACTIVITY = "WCA"
|
||||||
DATA_URL = "https://polo.ham2k.com/data/activities/wca/all-castles.csv"
|
DATA_URL = "https://polo.ham2k.com/data/activities/wca/all-castles.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -42,11 +42,11 @@ class WCA(FileDownloadSIGRefDataProvider):
|
|||||||
logger.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for {ref_id}")
|
logger.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for {ref_id}")
|
||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("CLEAN NAME", None),
|
name=row.get("CLEAN NAME", None),
|
||||||
ref_type=SIGRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||||
latitude=latitude,
|
latitude=latitude,
|
||||||
longitude=longitude,
|
longitude=longitude,
|
||||||
@@ -59,7 +59,7 @@ class WCA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WOTA(FileDownloadSIGRefDataProvider):
|
class WOTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Wainwrights on the Air"""
|
"""Activity ref data provider for Wainwrights on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 365
|
POLL_INTERVAL_DAYS = 365
|
||||||
SIG = "WOTA"
|
ACTIVITY = "WOTA"
|
||||||
DATA_URL = "https://www.wota.org.uk/mapping/data/summits.json"
|
DATA_URL = "https://www.wota.org.uk/mapping/data/summits.json"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -29,12 +29,12 @@ class WOTA(FileDownloadSIGRefDataProvider):
|
|||||||
url = f"https://www.wota.org.uk/MM_LDO-{number + 214!s}"
|
url = f"https://www.wota.org.uk/MM_LDO-{number + 214!s}"
|
||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=feature["properties"]["title"],
|
name=feature["properties"]["title"],
|
||||||
url=url,
|
url=url,
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
grid=feature["properties"]["qthLocator"],
|
grid=feature["properties"]["qthLocator"],
|
||||||
latitude=feature["geometry"]["coordinates"][1],
|
latitude=feature["geometry"]["coordinates"][1],
|
||||||
longitude=feature["geometry"]["coordinates"][0],
|
longitude=feature["geometry"]["coordinates"][0],
|
||||||
@@ -47,7 +47,7 @@ class WOTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WWBOTA(FileDownloadSIGRefDataProvider):
|
class WWBOTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Worldwide Bunkers on the Air"""
|
"""Activity ref data provider for Worldwide Bunkers on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "WWBOTA"
|
ACTIVITY = "WWBOTA"
|
||||||
DATA_URL = "https://api.wwbota.org/bunkers/?format=CSV"
|
DATA_URL = "https://api.wwbota.org/bunkers/?format=CSV"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
ref_id = row["Reference"]
|
ref_id = row["Reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=SIGRefType.BUNKER,
|
ref_type=ActivityRefType.BUNKER,
|
||||||
url=f"https://bunkerwiki.org/?s={ref_id}" if ref_id.startswith("B/G") else None,
|
url=f"https://bunkerwiki.org/?s={ref_id}" if ref_id.startswith("B/G") else None,
|
||||||
grid=row["Locator"] if "Locator" in row and row["Locator"] != "" else None,
|
grid=row["Locator"] if "Locator" in row and row["Locator"] != "" else None,
|
||||||
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
|
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
|
||||||
@@ -40,7 +40,7 @@ class WWBOTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import csv
|
import csv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WWFF(FileDownloadSIGRefDataProvider):
|
class WWFF(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for Worldwide Flora & Fauna"""
|
"""Activity ref data provider for Worldwide Flora & Fauna"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "WWFF"
|
ACTIVITY = "WWFF"
|
||||||
DATA_URL = "https://wwff.co/wwff-data/wwff_directory.csv"
|
DATA_URL = "https://wwff.co/wwff-data/wwff_directory.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
ref_id = row["reference"]
|
ref_id = row["reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("name", None),
|
name=row.get("name", None),
|
||||||
ref_type=SIGRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
url=f"https://wwff.co/directory/?showRef={ref_id}",
|
url=f"https://wwff.co/directory/?showRef={ref_id}",
|
||||||
grid=row["iaruLocator"] if "iaruLocator" in row and row["iaruLocator"] != "-" else None,
|
grid=row["iaruLocator"] if "iaruLocator" in row and row["iaruLocator"] != "-" else None,
|
||||||
latitude=float(row["latitude"])
|
latitude=float(row["latitude"])
|
||||||
@@ -44,7 +44,7 @@ class WWFF(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -2,22 +2,22 @@ from time import sleep
|
|||||||
|
|
||||||
from pyhamtools.locator import latlong_to_locator
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
from providers.activityrefdata.file_download_activity_ref_data_provider import (
|
||||||
FileDownloadSIGRefDataProvider,
|
FileDownloadActivityRefDataProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ZLOTA(FileDownloadSIGRefDataProvider):
|
class ZLOTA(FileDownloadActivityRefDataProvider):
|
||||||
"""SIG ref data provider for New Zealand on the Air"""
|
"""Activity ref data provider for New Zealand on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 30
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "ZLOTA"
|
ACTIVITY = "ZLOTA"
|
||||||
DATA_URL = "https://ontheair.nz/assets/assets.json"
|
DATA_URL = "https://ontheair.nz/assets/assets.json"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -28,12 +28,12 @@ class ZLOTA(FileDownloadSIGRefDataProvider):
|
|||||||
latitude = ref["latitude"]
|
latitude = ref["latitude"]
|
||||||
longitude = ref["longitude"]
|
longitude = ref["longitude"]
|
||||||
try:
|
try:
|
||||||
ref_type = SIGRefType(ref["asset_type"].title().upper())
|
ref_type = ActivityRefType(ref["asset_type"].title().upper())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
ref_type = None
|
ref_type = None
|
||||||
|
|
||||||
new_ref = SIGRef(
|
new_ref = ActivityRef(
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=ref["name"],
|
name=ref["name"],
|
||||||
ref_type=ref_type,
|
ref_type=ref_type,
|
||||||
@@ -58,7 +58,7 @@ class ZLOTA(FileDownloadSIGRefDataProvider):
|
|||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
|
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
|
||||||
# is available for other threads e.g. the web server.
|
# is available for other threads e.g. the web server.
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
@@ -4,8 +4,8 @@ import pytz
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from core.enums import AlertType
|
from core.enums import AlertType
|
||||||
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class BOTA(HTTPAlertProvider):
|
|||||||
alert = Alert(
|
alert = Alert(
|
||||||
source=self.name,
|
source=self.name,
|
||||||
dx_calls=[dx_call],
|
dx_calls=[dx_call],
|
||||||
sig_refs=[SIGRef(id=ref_name, sig="BOTA")],
|
sig_refs=[ActivityRef(id=ref_name, sig="BOTA")],
|
||||||
start_time=date_time.timestamp(),
|
start_time=date_time.timestamp(),
|
||||||
alert_type=AlertType.XOTA,
|
alert_type=AlertType.XOTA,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import AlertType
|
from core.enums import AlertType
|
||||||
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
|
|
||||||
@@ -35,9 +35,9 @@ class Hamsat(HTTPAlertProvider):
|
|||||||
dx_calls=[source_alert["callsign"].upper()],
|
dx_calls=[source_alert["callsign"].upper()],
|
||||||
freqs_modes=freqs_modes,
|
freqs_modes=freqs_modes,
|
||||||
comment=source_alert["comment"],
|
comment=source_alert["comment"],
|
||||||
# Fudge a SIG ref to provide the remaining bits of data we need: the satellite and the operator's grid
|
# Fudge an activity ref to provide the remaining bits of data we need: the satellite and the operator's grid
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
sig="AMSAT",
|
sig="AMSAT",
|
||||||
id=f"{source_alert['satellite']['name']} from {source_alert['grids'][0]}",
|
id=f"{source_alert['satellite']['name']} from {source_alert['grids'][0]}",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import AlertType
|
from core.enums import AlertType
|
||||||
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -25,23 +25,23 @@ class ParksNPeaks(HTTPAlertProvider):
|
|||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_alert in http_response.json():
|
for source_alert in http_response.json():
|
||||||
# Calculate some things
|
# Calculate some things
|
||||||
sig = source_alert["Class"].upper()
|
activity = source_alert["Class"].upper()
|
||||||
if " - " in source_alert["Location"]:
|
if " - " in source_alert["Location"]:
|
||||||
split = source_alert["Location"].split(" - ")
|
split = source_alert["Location"].split(" - ")
|
||||||
sig_ref = split[0]
|
ref_id = split[0]
|
||||||
sig_ref_name = split[1]
|
ref_name = split[1]
|
||||||
else:
|
else:
|
||||||
sig_ref = source_alert["WWFFID"]
|
ref_id = source_alert["WWFFID"]
|
||||||
sig_ref_name = source_alert["Location"]
|
ref_name = source_alert["Location"]
|
||||||
start_time = (
|
start_time = (
|
||||||
datetime.strptime(source_alert["alTime"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp()
|
datetime.strptime(source_alert["alTime"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp()
|
||||||
)
|
)
|
||||||
|
|
||||||
sigrefs = []
|
activity_refs = []
|
||||||
# PnP can give us an alert of class "QRP" which is the only one that's not a real SIG in Spothole's list,
|
# PnP can give us an alert of class "QRP" which is the only one that's not a real activity in Spothole's
|
||||||
# so mask this out if we got it.
|
# list, so mask this out if we got it.
|
||||||
if sig != "QRP":
|
if activity != "QRP":
|
||||||
sigrefs = [SIGRef(id=sig_ref, sig=sig, name=sig_ref_name)]
|
activity_refs = [ActivityRef(id=ref_id, sig=activity, name=ref_name)]
|
||||||
|
|
||||||
# Convert to our alert format
|
# Convert to our alert format
|
||||||
alert = Alert(
|
alert = Alert(
|
||||||
@@ -50,13 +50,13 @@ class ParksNPeaks(HTTPAlertProvider):
|
|||||||
dx_calls=[source_alert["CallSign"].upper()],
|
dx_calls=[source_alert["CallSign"].upper()],
|
||||||
freqs_modes=f"{source_alert['Freq']} {source_alert['MODE']}",
|
freqs_modes=f"{source_alert['Freq']} {source_alert['MODE']}",
|
||||||
comment=source_alert["Comments"],
|
comment=source_alert["Comments"],
|
||||||
sig_refs=sigrefs,
|
sig_refs=activity_refs,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
alert_type=AlertType.XOTA,
|
alert_type=AlertType.XOTA,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
|
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
|
||||||
if sig and sig not in [
|
if activity and activity not in [
|
||||||
"POTA",
|
"POTA",
|
||||||
"SOTA",
|
"SOTA",
|
||||||
"WWFF",
|
"WWFF",
|
||||||
@@ -68,11 +68,11 @@ class ParksNPeaks(HTTPAlertProvider):
|
|||||||
"LLOTA",
|
"LLOTA",
|
||||||
"QRP",
|
"QRP",
|
||||||
]:
|
]:
|
||||||
logger.warning(f"PNP alert found with sig {sig}, developer needs to add support for this!")
|
logger.warning(f"PNP alert found with activity {activity}, 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
|
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
|
||||||
# the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that
|
# the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that
|
||||||
# means the PnP *spot* provider rejects ZLOTA spots here, but the PnP *alerts* provider here allows ZLOTA.
|
# means the PnP *spot* provider rejects ZLOTA spots here, but the PnP *alerts* provider here allows ZLOTA.
|
||||||
if sig not in ["POTA", "SOTA", "WWFF"]:
|
if activity not in ["POTA", "SOTA", "WWFF"]:
|
||||||
new_alerts.append(alert)
|
new_alerts.append(alert)
|
||||||
return new_alerts
|
return new_alerts
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import AlertType
|
from core.enums import AlertType
|
||||||
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class POTA(HTTPAlertProvider):
|
|||||||
freqs_modes=source_alert["frequencies"],
|
freqs_modes=source_alert["frequencies"],
|
||||||
comment=source_alert["comments"],
|
comment=source_alert["comments"],
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_alert["reference"],
|
id=source_alert["reference"],
|
||||||
sig="POTA",
|
sig="POTA",
|
||||||
name=source_alert["name"],
|
name=source_alert["name"],
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import AlertType
|
from core.enums import AlertType
|
||||||
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class SOTA(HTTPAlertProvider):
|
|||||||
freqs_modes=source_alert["frequency"],
|
freqs_modes=source_alert["frequency"],
|
||||||
comment=source_alert["comments"],
|
comment=source_alert["comments"],
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=f"{source_alert['associationCode']}/{source_alert['summitCode']}",
|
id=f"{source_alert['associationCode']}/{source_alert['summitCode']}",
|
||||||
sig="SOTA",
|
sig="SOTA",
|
||||||
name=summit_name,
|
name=summit_name,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import pytz
|
|||||||
from rss_parser import Parser as RSSParser
|
from rss_parser import Parser as RSSParser
|
||||||
from rss_parser.models.rss import RSS
|
from rss_parser.models.rss import RSS
|
||||||
|
|
||||||
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -74,7 +74,7 @@ class WOTA(HTTPAlertProvider):
|
|||||||
dx_calls=[dx_call],
|
dx_calls=[dx_call],
|
||||||
freqs_modes=freqs_modes,
|
freqs_modes=freqs_modes,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
sig_refs=[ActivityRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
||||||
start_time=time.timestamp(),
|
start_time=time.timestamp(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import AlertType
|
from core.enums import AlertType
|
||||||
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class WWFF(HTTPAlertProvider):
|
|||||||
dx_calls=[source_alert["activator_call"].upper()],
|
dx_calls=[source_alert["activator_call"].upper()],
|
||||||
freqs_modes=f"{source_alert['band']} {source_alert['mode']}",
|
freqs_modes=f"{source_alert['band']} {source_alert['mode']}",
|
||||||
comment=source_alert["remarks"],
|
comment=source_alert["remarks"],
|
||||||
sig_refs=[SIGRef(id=source_alert["reference"], sig="WWFF")],
|
sig_refs=[ActivityRef(id=source_alert["reference"], sig="WWFF")],
|
||||||
start_time=datetime.strptime(source_alert["utc_start"], "%Y-%m-%d %H:%M:%S")
|
start_time=datetime.strptime(source_alert["utc_start"], "%Y-%m-%d %H:%M:%S")
|
||||||
.replace(tzinfo=pytz.UTC)
|
.replace(tzinfo=pytz.UTC)
|
||||||
.timestamp(),
|
.timestamp(),
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
from providers.sigrefdata.pnp_kml_sig_ref_data_provider import (
|
|
||||||
ParksNPeaksKMLSIGRefDataProvider,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class KRMNPA(ParksNPeaksKMLSIGRefDataProvider):
|
|
||||||
"""SIG ref data provider for the Keith Roget Memorrial National Parks Award (KRMNPA)."""
|
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 365
|
|
||||||
SIG = "KRMNPA"
|
|
||||||
DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=KRMNPA&poiFormat=4"
|
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LocalFileSIGRefDataProvider(SIGRefDataProvider):
|
|
||||||
"""Generic SIG ref data provider class for providers that fetch their data from a local file on startup."""
|
|
||||||
|
|
||||||
def __init__(self, sig, provider_config, path):
|
|
||||||
super().__init__(sig, provider_config)
|
|
||||||
self._path = path
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
logger.debug(f"Loading {self.sig_name} SIG ref data from file.")
|
|
||||||
try:
|
|
||||||
new_data = self._file_to_data(self._path)
|
|
||||||
if new_data:
|
|
||||||
self._add_data(new_data)
|
|
||||||
self.status = "OK"
|
|
||||||
self.last_update_time = datetime.now(pytz.UTC)
|
|
||||||
else:
|
|
||||||
self.status = "Error"
|
|
||||||
logger.info(f"Failed to load SIG ref data for {self.sig_name}")
|
|
||||||
except Exception:
|
|
||||||
self.status = "Error"
|
|
||||||
logger.exception(f"Exception in local file SIG Ref Data Provider ({self.sig_name})")
|
|
||||||
|
|
||||||
def _file_to_data(self, path):
|
|
||||||
"""Load a file on the given path and turn it into SIG Ref data."""
|
|
||||||
|
|
||||||
raise NotImplementedError("Subclasses must implement this method")
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from providers.sigrefdata.pnp_kml_sig_ref_data_provider import (
|
|
||||||
ParksNPeaksKMLSIGRefDataProvider,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SANPCPA(ParksNPeaksKMLSIGRefDataProvider):
|
|
||||||
"""SIG ref data provider for the South Australia National Parks and Conservation Parks Award (SANPCPA)."""
|
|
||||||
|
|
||||||
POLL_INTERVAL_DAYS = 365
|
|
||||||
SIG = "SANPCPA"
|
|
||||||
DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=SANPCPA&poiFormat=4"
|
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
|
||||||
+14
-14
@@ -4,9 +4,9 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
from core.enums import Mode, SIGRefType
|
from core.enums import ActivityRefType, Mode
|
||||||
from core.url_data_cache import URLDataCache
|
from core.url_data_cache import URLDataCache
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class GMA(HTTPSpotProvider):
|
|||||||
mode=Mode.from_name(source_spot["MODE"].upper()) if "<>" not in source_spot["MODE"] else None,
|
mode=Mode.from_name(source_spot["MODE"].upper()) if "<>" not in source_spot["MODE"] else None,
|
||||||
comment=source_spot["TEXT"],
|
comment=source_spot["TEXT"],
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_spot["REF"],
|
id=source_spot["REF"],
|
||||||
sig="",
|
sig="",
|
||||||
name=source_spot["NAME"],
|
name=source_spot["NAME"],
|
||||||
@@ -83,7 +83,7 @@ class GMA(HTTPSpotProvider):
|
|||||||
qrt=source_spot["QRG"] == "QRT",
|
qrt=source_spot["QRG"] == "QRT",
|
||||||
)
|
)
|
||||||
|
|
||||||
# GMA doesn't give what programme (SIG) the reference is for until we separately look it up.
|
# GMA doesn't give what programme (activity) the reference is for until we separately look it up.
|
||||||
if "REF" in source_spot:
|
if "REF" in source_spot:
|
||||||
try:
|
try:
|
||||||
ref_response = self._url_data_cache.get(
|
ref_response = self._url_data_cache.get(
|
||||||
@@ -114,31 +114,31 @@ class GMA(HTTPSpotProvider):
|
|||||||
match ref_info["reftype"]:
|
match ref_info["reftype"]:
|
||||||
case "Summit":
|
case "Summit":
|
||||||
spot.sig_refs[0].sig = "GMA"
|
spot.sig_refs[0].sig = "GMA"
|
||||||
spot.sig_refs[0].ref_type = SIGRefType.SUMMIT
|
spot.sig_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||||
spot.sig = "GMA"
|
spot.sig = "GMA"
|
||||||
case "IOTA Island":
|
case "IOTA Island":
|
||||||
spot.sig_refs[0].sig = "IOTA"
|
spot.sig_refs[0].sig = "IOTA"
|
||||||
spot.sig_refs[0].ref_type = SIGRefType.ISLAND
|
spot.sig_refs[0].ref_type = ActivityRefType.ISLAND
|
||||||
spot.sig = "IOTA"
|
spot.sig = "IOTA"
|
||||||
case "GMA Island":
|
case "GMA Island":
|
||||||
spot.sig_refs[0].sig = "GMA Islands"
|
spot.sig_refs[0].sig = "GMA Islands"
|
||||||
spot.sig_refs[0].ref_type = SIGRefType.ISLAND
|
spot.sig_refs[0].ref_type = ActivityRefType.ISLAND
|
||||||
spot.sig = "GMA Islands"
|
spot.sig = "GMA Islands"
|
||||||
case "Lighthouse (ILLW)":
|
case "Lighthouse (ILLW)":
|
||||||
spot.sig_refs[0].sig = "ILLW"
|
spot.sig_refs[0].sig = "ILLW"
|
||||||
spot.sig_refs[0].ref_type = SIGRefType.LIGHTHOUSE
|
spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||||
spot.sig = "ILLW"
|
spot.sig = "ILLW"
|
||||||
case "Lighthouse (ARLHS)":
|
case "Lighthouse (ARLHS)":
|
||||||
spot.sig_refs[0].sig = "ARLHS"
|
spot.sig_refs[0].sig = "ARLHS"
|
||||||
spot.sig_refs[0].ref_type = SIGRefType.LIGHTHOUSE
|
spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||||
spot.sig = "ARLHS"
|
spot.sig = "ARLHS"
|
||||||
case "Castle":
|
case "Castle":
|
||||||
spot.sig_refs[0].sig = "WCA"
|
spot.sig_refs[0].sig = "WCA"
|
||||||
spot.sig_refs[0].ref_type = SIGRefType.CASTLE
|
spot.sig_refs[0].ref_type = ActivityRefType.CASTLE
|
||||||
spot.sig = "WCA"
|
spot.sig = "WCA"
|
||||||
case "Mill":
|
case "Mill":
|
||||||
spot.sig_refs[0].sig = "MOTA"
|
spot.sig_refs[0].sig = "MOTA"
|
||||||
spot.sig_refs[0].ref_type = SIGRefType.MILL
|
spot.sig_refs[0].ref_type = ActivityRefType.MILL
|
||||||
spot.sig = "MOTA"
|
spot.sig = "MOTA"
|
||||||
case _:
|
case _:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -158,7 +158,7 @@ class GMA(HTTPSpotProvider):
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, SIG data will not be populated for the spot."
|
f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, activity data will not be populated for the spot."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point;
|
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point;
|
||||||
@@ -169,8 +169,8 @@ class GMA(HTTPSpotProvider):
|
|||||||
|
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "GMA"
|
return activity == "GMA"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# TODO: Implement.
|
# TODO: Implement.
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import requests
|
|||||||
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
|
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
|
||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
from core.enums import Mode, SIGRefType
|
from core.enums import ActivityRefType, Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -64,13 +64,13 @@ class HEMA(HTTPSpotProvider):
|
|||||||
comment=spotter_comment_match.group(2),
|
comment=spotter_comment_match.group(2),
|
||||||
sig="HEMA",
|
sig="HEMA",
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=spot_items[3].upper(),
|
id=spot_items[3].upper(),
|
||||||
sig="HEMA",
|
sig="HEMA",
|
||||||
name=spot_items[4],
|
name=spot_items[4],
|
||||||
latitude=float(spot_items[7]),
|
latitude=float(spot_items[7]),
|
||||||
longitude=float(spot_items[8]),
|
longitude=float(spot_items[8]),
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M")
|
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M")
|
||||||
@@ -89,8 +89,8 @@ class HEMA(HTTPSpotProvider):
|
|||||||
logger.warning("Connection error when accessing HEMA spots API.")
|
logger.warning("Connection error when accessing HEMA spots API.")
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "HEMA"
|
return activity == "HEMA"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# TODO: Implement. Currently blocked awaiting their API team to make a change to allow us to spot with a
|
# TODO: Implement. Currently blocked awaiting their API team to make a change to allow us to spot with a
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from core.enums import Mode, SIGRefType
|
from core.enums import ActivityRefType, Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -36,11 +36,11 @@ class LLOTA(HTTPSpotProvider):
|
|||||||
comment=comment,
|
comment=comment,
|
||||||
sig="LLOTA",
|
sig="LLOTA",
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
sig="LLOTA",
|
sig="LLOTA",
|
||||||
name=source_spot["reference_name"],
|
name=source_spot["reference_name"],
|
||||||
ref_type=SIGRefType.LAKE,
|
ref_type=ActivityRefType.LAKE,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp(),
|
time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import requests
|
|||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
from core.enums import Mode
|
from core.enums import Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
POLL_INTERVAL_SEC = 120
|
POLL_INTERVAL_SEC = 120
|
||||||
SPOTS_URL = "https://www.parksnpeaks.org/api/ALL"
|
SPOTS_URL = "https://www.parksnpeaks.org/api/ALL"
|
||||||
SUBMIT_URL = "https://www.parksnpeaks.org/api/SPOT/"
|
SUBMIT_URL = "https://www.parksnpeaks.org/api/SPOT/"
|
||||||
SUBMITTABLE_SIGS = [
|
SUBMITTABLE_ACTIVITIES = [
|
||||||
"POTA",
|
"POTA",
|
||||||
"SOTA",
|
"SOTA",
|
||||||
"WWFF",
|
"WWFF",
|
||||||
@@ -64,26 +64,26 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
if not spot.de_call and m:
|
if not spot.de_call and m:
|
||||||
spot.de_call = str(m.group(1))
|
spot.de_call = str(m.group(1))
|
||||||
|
|
||||||
# Record SIG information. Sometimes we get a "SIG" of "QRP", which we ignore as it's not a programme with a
|
# Record activity information. Sometimes we get an activity of "QRP", which we ignore as it's not a
|
||||||
# defined set of references
|
# programme with a defined set of references
|
||||||
sig = source_spot["actClass"].upper()
|
activity = source_spot["actClass"].upper()
|
||||||
sig_ref = source_spot["actSiteID"]
|
ref_id = source_spot["actSiteID"]
|
||||||
if sig and sig != "" and sig != "QRP" and sig_ref and sig_ref != "":
|
if activity and activity != "" and activity != "QRP" and ref_id and ref_id != "":
|
||||||
spot.sig = sig
|
spot.sig = activity
|
||||||
sig_refs = [
|
activity_refs = [
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_spot["actSiteID"],
|
id=source_spot["actSiteID"],
|
||||||
sig=source_spot["actClass"].upper(),
|
sig=source_spot["actClass"].upper(),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
spot.sig_refs = sig_refs
|
spot.sig_refs = activity_refs
|
||||||
|
|
||||||
# Free text location is not present in all spots, so only add it if it's set
|
# 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"] != "":
|
if "actLocation" in source_spot and source_spot["actLocation"] != "":
|
||||||
sig_refs[0].name = source_spot["actLocation"]
|
activity_refs[0].name = source_spot["actLocation"]
|
||||||
|
|
||||||
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
|
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
|
||||||
if sig not in [
|
if activity not in [
|
||||||
"POTA",
|
"POTA",
|
||||||
"SOTA",
|
"SOTA",
|
||||||
"WWFF",
|
"WWFF",
|
||||||
@@ -94,14 +94,14 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
"SANPCPA",
|
"SANPCPA",
|
||||||
"LLOTA",
|
"LLOTA",
|
||||||
]:
|
]:
|
||||||
logger.warning(f"PNP spot found with sig {sig}, developer needs to add support for this!")
|
logger.warning(f"PNP spot found with activity {activity}, developer needs to add support for this!")
|
||||||
|
|
||||||
# Add new spot to the list
|
# Add new spot to the list
|
||||||
new_spots.append(spot)
|
new_spots.append(spot)
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig in self.SUBMITTABLE_SIGS
|
return activity in self.SUBMITTABLE_ACTIVITIES
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# TODO test this works
|
# TODO test this works
|
||||||
@@ -111,11 +111,11 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account."
|
"Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account."
|
||||||
)
|
)
|
||||||
sig_ref = spot.sig_refs[0].id if spot.sig_refs else ""
|
ref_id = spot.sig_refs[0].id if spot.sig_refs else ""
|
||||||
body = {
|
body = {
|
||||||
"actClass": spot.sig or "",
|
"actClass": spot.sig or "",
|
||||||
"actCallsign": spot.dx_call,
|
"actCallsign": spot.dx_call,
|
||||||
"actSite": sig_ref,
|
"actSite": ref_id,
|
||||||
"mode": spot.mode or "",
|
"mode": spot.mode or "",
|
||||||
"freq": str(spot.freq / 1000000.0),
|
"freq": str(spot.freq / 1000000.0),
|
||||||
"comments": spot.comment or "",
|
"comments": spot.comment or "",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import pytz
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
from core.enums import Mode, SIGRefType
|
from core.enums import ActivityRefType, Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -35,13 +35,13 @@ class POTA(HTTPSpotProvider):
|
|||||||
comment=source_spot["comments"],
|
comment=source_spot["comments"],
|
||||||
sig="POTA",
|
sig="POTA",
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
sig="POTA",
|
sig="POTA",
|
||||||
name=source_spot["name"],
|
name=source_spot["name"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"],
|
longitude=source_spot["longitude"],
|
||||||
ref_type=SIGRefType.PARK
|
ref_type=ActivityRefType.PARK
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S")
|
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S")
|
||||||
@@ -57,8 +57,8 @@ class POTA(HTTPSpotProvider):
|
|||||||
new_spots.append(spot)
|
new_spots.append(spot)
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "POTA"
|
return activity == "POTA"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
sig_ref = spot.sig_refs[0].id if spot.sig_refs else None
|
sig_ref = spot.sig_refs[0].id if spot.sig_refs else None
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import requests
|
|||||||
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
|
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
|
||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
from core.enums import Mode, SIGRefType
|
from core.enums import ActivityRefType, Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -58,14 +58,14 @@ class SOTA(HTTPSpotProvider):
|
|||||||
comment=source_spot["comments"],
|
comment=source_spot["comments"],
|
||||||
sig="SOTA",
|
sig="SOTA",
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_spot["summitCode"],
|
id=source_spot["summitCode"],
|
||||||
sig="SOTA",
|
sig="SOTA",
|
||||||
name=source_spot["summitName"],
|
name=source_spot["summitName"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"],
|
longitude=source_spot["longitude"],
|
||||||
activation_score=source_spot["points"],
|
activation_score=source_spot["points"],
|
||||||
ref_type=SIGRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
dx_latitude=source_spot["latitude"],
|
dx_latitude=source_spot["latitude"],
|
||||||
@@ -82,8 +82,8 @@ class SOTA(HTTPSpotProvider):
|
|||||||
logger.warning("Timeout when accessing SOTA spots API.")
|
logger.warning("Timeout when accessing SOTA spots API.")
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "SOTA"
|
return activity == "SOTA"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# TODO test this method works
|
# TODO test this method works
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ class SpotProvider:
|
|||||||
|
|
||||||
raise NotImplementedError("Subclasses must implement this method")
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
"""Return True if this provider supports submitting spots upstream for the given SIG."""
|
"""Return True if this provider supports submitting spots upstream for the given activity."""
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from datetime import datetime
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
from core.enums import LocationSourceForSpot, Mode, SIGRefType
|
from core.enums import ActivityRefType, LocationSourceForSpot, Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -60,15 +60,15 @@ class Tiles(HTTPSpotProvider):
|
|||||||
comment=source_spot["notes"],
|
comment=source_spot["notes"],
|
||||||
sig="Tiles",
|
sig="Tiles",
|
||||||
# Tiles spots can include POTA & SOTA references, but ignore those on the basis that we will get them separately from the POTA/SOTA providers anyway.
|
# Tiles spots can include POTA & SOTA references, but ignore those on the basis that we will get them separately from the POTA/SOTA providers anyway.
|
||||||
# Just take the grid reference itself as the single Tiles SIG reference.
|
# Just take the grid reference itself as the single Tiles activity reference.
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_spot["maidenhead_grid"],
|
id=source_spot["maidenhead_grid"],
|
||||||
sig="Tiles",
|
sig="Tiles",
|
||||||
name=source_spot["maidenhead_grid"],
|
name=source_spot["maidenhead_grid"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"],
|
longitude=source_spot["longitude"],
|
||||||
ref_type=SIGRefType.GRID
|
ref_type=ActivityRefType.GRID
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
time=datetime.fromisoformat(source_spot["created_at"].replace("Z", "+00:00")).timestamp(),
|
time=datetime.fromisoformat(source_spot["created_at"].replace("Z", "+00:00")).timestamp(),
|
||||||
@@ -83,8 +83,8 @@ class Tiles(HTTPSpotProvider):
|
|||||||
new_spots.append(spot)
|
new_spots.append(spot)
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "Tiles"
|
return activity == "Tiles"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# Tiles on the air currently only supports *self* spots
|
# Tiles on the air currently only supports *self* spots
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import SIGRefType
|
from core.enums import ActivityRefType
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class Towers(HTTPSpotProvider):
|
|||||||
freq=likely_freq,
|
freq=likely_freq,
|
||||||
comment=source_spot["comment"],
|
comment=source_spot["comment"],
|
||||||
sig="Towers",
|
sig="Towers",
|
||||||
sig_refs=[SIGRef(id=source_spot["ref"], sig="Towers", ref_type=SIGRefType.TOWER)],
|
sig_refs=[ActivityRef(id=source_spot["ref"], sig="Towers", ref_type=ActivityRefType.TOWER)],
|
||||||
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M")
|
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M")
|
||||||
.replace(tzinfo=pytz.utc)
|
.replace(tzinfo=pytz.utc)
|
||||||
.timestamp(),
|
.timestamp(),
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import pytz
|
|||||||
from rss_parser import Parser
|
from rss_parser import Parser
|
||||||
from rss_parser.models.rss import RSS
|
from rss_parser.models.rss import RSS
|
||||||
|
|
||||||
from core.enums import Mode, SIGRefType
|
from core.enums import ActivityRefType, Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ class WOTA(HTTPSpotProvider):
|
|||||||
mode=Mode.from_name(mode),
|
mode=Mode.from_name(mode),
|
||||||
comment=comment,
|
comment=comment,
|
||||||
sig="WOTA",
|
sig="WOTA",
|
||||||
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name, ref_type=SIGRefType.SUMMIT)] if ref else [],
|
sig_refs=[ActivityRef(id=ref, sig="WOTA", name=ref_name, ref_type=ActivityRefType.SUMMIT)] if ref else [],
|
||||||
time=time.timestamp(),
|
time=time.timestamp(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,8 +104,8 @@ class WOTA(HTTPSpotProvider):
|
|||||||
|
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "WOTA"
|
return activity == "WOTA"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# TODO Ask M5TEA if he's happy to share how this is done from his app
|
# TODO Ask M5TEA if he's happy to share how this is done from his app
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from core.enums import Mode, SIGRefType
|
from core.enums import ActivityRefType, Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.sse_spot_provider import SSESpotProvider
|
from providers.spot.sse_spot_provider import SSESpotProvider
|
||||||
|
|
||||||
@@ -21,15 +21,15 @@ class WWBOTA(SSESpotProvider):
|
|||||||
# n-fer activations.
|
# n-fer activations.
|
||||||
refs = []
|
refs = []
|
||||||
for ref in source_spot["references"]:
|
for ref in source_spot["references"]:
|
||||||
sigref = SIGRef(
|
activity_ref = ActivityRef(
|
||||||
id=ref["reference"],
|
id=ref["reference"],
|
||||||
sig="WWBOTA",
|
sig="WWBOTA",
|
||||||
name=ref["name"],
|
name=ref["name"],
|
||||||
latitude=ref["lat"],
|
latitude=ref["lat"],
|
||||||
longitude=ref["long"],
|
longitude=ref["long"],
|
||||||
ref_type=SIGRefType.BUNKER,
|
ref_type=ActivityRefType.BUNKER,
|
||||||
)
|
)
|
||||||
refs.append(sigref)
|
refs.append(activity_ref)
|
||||||
|
|
||||||
spot = Spot(
|
spot = Spot(
|
||||||
source=self.name,
|
source=self.name,
|
||||||
@@ -52,8 +52,8 @@ class WWBOTA(SSESpotProvider):
|
|||||||
# WWBOTA does support a special "Test" spot type, we need to avoid adding that.
|
# WWBOTA does support a special "Test" spot type, we need to avoid adding that.
|
||||||
return spot if source_spot["type"] != "Test" else None
|
return spot if source_spot["type"] != "Test" else None
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "WWBOTA"
|
return activity == "WWBOTA"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# TODO: Implement. WWBOTA API docs cover this: https://api.wwbota.org/#tag/Spots/operation/create_spot_spots__post
|
# TODO: Implement. WWBOTA API docs cover this: https://api.wwbota.org/#tag/Spots/operation/create_spot_spots__post
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import Mode, SIGRefType
|
from core.enums import ActivityRefType, Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -32,13 +32,13 @@ class WWFF(HTTPSpotProvider):
|
|||||||
comment=source_spot["remarks"],
|
comment=source_spot["remarks"],
|
||||||
sig="WWFF",
|
sig="WWFF",
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
sig="WWFF",
|
sig="WWFF",
|
||||||
name=source_spot["reference_name"],
|
name=source_spot["reference_name"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"],
|
longitude=source_spot["longitude"],
|
||||||
ref_type=SIGRefType.PARK
|
ref_type=ActivityRefType.PARK
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
|
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
|
||||||
@@ -51,8 +51,8 @@ class WWFF(HTTPSpotProvider):
|
|||||||
new_spots.append(spot)
|
new_spots.append(spot)
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "WWFF"
|
return activity == "WWFF"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# TODO: Implement. Spotting to WWFF should be possible, need to look up the Spotline docs or copy approach from
|
# TODO: Implement. Spotting to WWFF should be possible, need to look up the Spotline docs or copy approach from
|
||||||
|
|||||||
+13
-13
@@ -4,43 +4,43 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import Mode
|
from core.enums import Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.websocket_spot_provider import WebsocketSpotProvider
|
from providers.spot.websocket_spot_provider import WebsocketSpotProvider
|
||||||
|
|
||||||
|
|
||||||
class XOTA(WebsocketSpotProvider):
|
class XOTA(WebsocketSpotProvider):
|
||||||
"""Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
|
"""Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
|
||||||
The provider typically doesn't give us a lat/lon or SIG explicitly, so our own config provides a SIG which we can
|
The provider typically doesn't give us a lat/lon or activity explicitly, so our own config provides an activity
|
||||||
then use for lookups. This functionality is implemented for Toilets on the Air events, of which there are
|
which we can then use for lookups. This functionality is implemented for Toilets on the Air events, of which
|
||||||
several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA, which is why we also
|
there are several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA, which
|
||||||
provide a sig_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01" at C3 might become
|
is why we also provide a sig_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01" at C3
|
||||||
"C3 T-01". This allows us to provide location lookups for TOTA at several conferences."""
|
might become "C3 T-01". This allows us to provide location lookups for TOTA at several conferences."""
|
||||||
|
|
||||||
LOCATION_DATA = {}
|
LOCATION_DATA = {}
|
||||||
SIG = None
|
ACTIVITY = None
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
name = provider_config.get("name", "xOTA")
|
name = provider_config.get("name", "xOTA")
|
||||||
super().__init__(name, provider_config, provider_config["url"])
|
super().__init__(name, provider_config, provider_config["url"])
|
||||||
self.SIG = str(provider_config["sig"]) if "sig" in provider_config else None
|
self.ACTIVITY = str(provider_config["sig"]) if "sig" in provider_config else None
|
||||||
self._sig_ref_prefix = str(provider_config["sig_ref_prefix"]) if "sig_ref_prefix" in provider_config else ""
|
self._activity_ref_prefix = str(provider_config["sig_ref_prefix"]) if "sig_ref_prefix" in provider_config else ""
|
||||||
|
|
||||||
def _ws_message_to_spot(self, b):
|
def _ws_message_to_spot(self, b):
|
||||||
string = b.decode("utf-8")
|
string = b.decode("utf-8")
|
||||||
source_spot = json.loads(string)
|
source_spot = json.loads(string)
|
||||||
ref_id = f"{self._sig_ref_prefix} {source_spot['reference']['title']}"
|
ref_id = f"{self._activity_ref_prefix} {source_spot['reference']['title']}"
|
||||||
spot = Spot(
|
spot = Spot(
|
||||||
source=self.name,
|
source=self.name,
|
||||||
source_id=source_spot["id"],
|
source_id=source_spot["id"],
|
||||||
dx_call=source_spot["stationCallSign"].upper(),
|
dx_call=source_spot["stationCallSign"].upper(),
|
||||||
freq=float(source_spot["freq"]) * 1000,
|
freq=float(source_spot["freq"]) * 1000,
|
||||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||||
sig=self.SIG,
|
sig=self.ACTIVITY,
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
sig=self.SIG or "",
|
sig=self.ACTIVITY or "",
|
||||||
url=source_spot["reference"]["website"],
|
url=source_spot["reference"]["website"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import Mode
|
from core.enums import Mode
|
||||||
from data.sig_ref import SIGRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class ZLOTA(HTTPSpotProvider):
|
|||||||
comment=source_spot["comments"],
|
comment=source_spot["comments"],
|
||||||
sig="ZLOTA",
|
sig="ZLOTA",
|
||||||
sig_refs=[
|
sig_refs=[
|
||||||
SIGRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
sig="ZLOTA",
|
sig="ZLOTA",
|
||||||
name=source_spot["name"],
|
name=source_spot["name"],
|
||||||
@@ -51,8 +51,8 @@ class ZLOTA(HTTPSpotProvider):
|
|||||||
new_spots.append(spot)
|
new_spots.append(spot)
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|
||||||
def can_submit_spot(self, sig):
|
def can_submit_spot(self, activity):
|
||||||
return sig == "ZLOTA"
|
return activity == "ZLOTA"
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
# TODO: Implement. Spotting to ZLOTA is supported via POST, see https://ontheair.nz/api
|
# TODO: Implement. Spotting to ZLOTA is supported via POST, see https://ontheair.nz/api
|
||||||
|
|||||||
+99
-94
@@ -109,7 +109,7 @@ tags:
|
|||||||
- name: General
|
- name: General
|
||||||
description: Server status and enumeration options.
|
description: Server status and enumeration options.
|
||||||
- name: Utilities
|
- name: Utilities
|
||||||
description: Utility lookups for callsigns, SIG references, and Maidenhead grids.
|
description: Utility lookups for callsigns, activity references, and Maidenhead grids.
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/spots:
|
/spots:
|
||||||
@@ -129,9 +129,9 @@ paths:
|
|||||||
- $ref: '#/components/parameters/SpotMaxAge'
|
- $ref: '#/components/parameters/SpotMaxAge'
|
||||||
- $ref: '#/components/parameters/SpotReceivedSince'
|
- $ref: '#/components/parameters/SpotReceivedSince'
|
||||||
- $ref: '#/components/parameters/SpotSource'
|
- $ref: '#/components/parameters/SpotSource'
|
||||||
- $ref: '#/components/parameters/SpotSig'
|
- $ref: '#/components/parameters/SpotActivity'
|
||||||
- $ref: '#/components/parameters/SpotNeedsSig'
|
- $ref: '#/components/parameters/SpotNeedsActivity'
|
||||||
- $ref: '#/components/parameters/SpotNeedsSigRef'
|
- $ref: '#/components/parameters/SpotNeedsActivityRef'
|
||||||
- $ref: '#/components/parameters/SpotBand'
|
- $ref: '#/components/parameters/SpotBand'
|
||||||
- $ref: '#/components/parameters/SpotMode'
|
- $ref: '#/components/parameters/SpotMode'
|
||||||
- $ref: '#/components/parameters/SpotModeType'
|
- $ref: '#/components/parameters/SpotModeType'
|
||||||
@@ -171,9 +171,9 @@ paths:
|
|||||||
operationId: spots-stream
|
operationId: spots-stream
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/SpotSource'
|
- $ref: '#/components/parameters/SpotSource'
|
||||||
- $ref: '#/components/parameters/SpotSig'
|
- $ref: '#/components/parameters/SpotActivity'
|
||||||
- $ref: '#/components/parameters/SpotNeedsSig'
|
- $ref: '#/components/parameters/SpotNeedsActivity'
|
||||||
- $ref: '#/components/parameters/SpotNeedsSigRef'
|
- $ref: '#/components/parameters/SpotNeedsActivityRef'
|
||||||
- $ref: '#/components/parameters/SpotBand'
|
- $ref: '#/components/parameters/SpotBand'
|
||||||
- $ref: '#/components/parameters/SpotMode'
|
- $ref: '#/components/parameters/SpotMode'
|
||||||
- $ref: '#/components/parameters/SpotModeType'
|
- $ref: '#/components/parameters/SpotModeType'
|
||||||
@@ -218,7 +218,7 @@ paths:
|
|||||||
- $ref: '#/components/parameters/AlertDxpeditionsSkipMaxDurationCheck'
|
- $ref: '#/components/parameters/AlertDxpeditionsSkipMaxDurationCheck'
|
||||||
- $ref: '#/components/parameters/AlertContestsSkipMaxDurationCheck'
|
- $ref: '#/components/parameters/AlertContestsSkipMaxDurationCheck'
|
||||||
- $ref: '#/components/parameters/AlertSource'
|
- $ref: '#/components/parameters/AlertSource'
|
||||||
- $ref: '#/components/parameters/AlertSig'
|
- $ref: '#/components/parameters/AlertActivity'
|
||||||
- $ref: '#/components/parameters/AlertDxContinent'
|
- $ref: '#/components/parameters/AlertDxContinent'
|
||||||
- $ref: '#/components/parameters/AlertDxCallIncludes'
|
- $ref: '#/components/parameters/AlertDxCallIncludes'
|
||||||
- $ref: '#/components/parameters/AlertTextIncludes'
|
- $ref: '#/components/parameters/AlertTextIncludes'
|
||||||
@@ -254,7 +254,7 @@ paths:
|
|||||||
- $ref: '#/components/parameters/AlertDxpeditionsSkipMaxDurationCheck'
|
- $ref: '#/components/parameters/AlertDxpeditionsSkipMaxDurationCheck'
|
||||||
- $ref: '#/components/parameters/AlertContestsSkipMaxDurationCheck'
|
- $ref: '#/components/parameters/AlertContestsSkipMaxDurationCheck'
|
||||||
- $ref: '#/components/parameters/AlertSource'
|
- $ref: '#/components/parameters/AlertSource'
|
||||||
- $ref: '#/components/parameters/AlertSig'
|
- $ref: '#/components/parameters/AlertActivity'
|
||||||
- $ref: '#/components/parameters/AlertDxContinent'
|
- $ref: '#/components/parameters/AlertDxContinent'
|
||||||
- $ref: '#/components/parameters/AlertDxCallIncludes'
|
- $ref: '#/components/parameters/AlertDxCallIncludes'
|
||||||
- $ref: '#/components/parameters/AlertTextIncludes'
|
- $ref: '#/components/parameters/AlertTextIncludes'
|
||||||
@@ -392,24 +392,24 @@ paths:
|
|||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Utilities
|
- Utilities
|
||||||
summary: Look up SIG ref details
|
summary: Look up activity ref details
|
||||||
description: >
|
description: >
|
||||||
Perform a lookup of data about a certain reference, providing the SIG and the ID of the
|
Perform a lookup of data about a certain reference, providing the activity and the ID of the
|
||||||
reference. A SIGRef structure will be returned containing the SIG and ID, plus any other
|
reference. An ActivityRef structure will be returned containing the activity and ID, plus any other
|
||||||
information Spothole could find about it.
|
information Spothole could find about it.
|
||||||
operationId: sigref
|
operationId: sigref
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/SigRefSig'
|
- $ref: '#/components/parameters/ActivityRefLookupActivity'
|
||||||
- $ref: '#/components/parameters/SigRefId'
|
- $ref: '#/components/parameters/ActivityRefLookupId'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SIGRef'
|
$ref: '#/components/schemas/ActivityRef'
|
||||||
'422':
|
'422':
|
||||||
description: Validation error e.g. SIG not supported or reference format incorrect
|
description: Validation error e.g. activity not supported or reference format incorrect
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -452,7 +452,7 @@ paths:
|
|||||||
description: >
|
description: >
|
||||||
Supply a JSON object containing a `spot` sub-object (the spot data) and an optional `handling` sub-object
|
Supply a JSON object containing a `spot` sub-object (the spot data) and an optional `handling` sub-object
|
||||||
containing server-side instructions such as upstream submission). Check `spot_submit_providers` in the
|
containing server-side instructions such as upstream submission). Check `spot_submit_providers` in the
|
||||||
`/options` response to see which SIGs and providers support upstream submission. cURL example:
|
`/options` response to see which activities and providers support upstream submission. cURL example:
|
||||||
`curl --request POST --header \"Content-Type: application/json\" --data '{\"spot\":{\"dx_call\":\"M0TRT\",\"time\":1760019539,\"freq\":14200000,\"comment\":\"Test spot please ignore\",\"de_call\":\"M0TRT\"}}' https://spothole.app/api/v2/spot`"
|
`curl --request POST --header \"Content-Type: application/json\" --data '{\"spot\":{\"dx_call\":\"M0TRT\",\"time\":1760019539,\"freq\":14200000,\"comment\":\"Test spot please ignore\",\"de_call\":\"M0TRT\"}}' https://spothole.app/api/v2/spot`"
|
||||||
operationId: spot
|
operationId: spot
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -550,37 +550,37 @@ components:
|
|||||||
named some sources like clusters differently, and provided multiple options.
|
named some sources like clusters differently, and provided multiple options.
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Source"
|
$ref: "#/components/schemas/Source"
|
||||||
SpotSig:
|
SpotActivity:
|
||||||
name: sig
|
name: sig
|
||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the spots to only ones from one or more Special Interest Groups provided as an argument.
|
Limit the spots to only ones from one or more activities provided as an argument.
|
||||||
To select more than one SIG, supply a comma-separated list. The special `sig` name `NO_SIG`
|
To select more than one activity, supply a comma-separated list. The special `sig` name `NO_SIG`
|
||||||
matches spots with no sig set. You can use `sig=NO_SIG` to specifically only return generic
|
matches spots with no activity set. You can use `sig=NO_SIG` to specifically only return generic
|
||||||
spots with no associated SIG. You can also use combinations to request for example POTA + no
|
spots with no associated activity. You can also use combinations to request for example POTA + no
|
||||||
SIG, but reject other SIGs. If you want to request 'every SIG and not No SIG', see the
|
activity, but reject other activities. If you want to request 'every activity and not No Activity', see the
|
||||||
`needs_sig` query parameter for a shortcut.
|
`needs_sig` query parameter for a shortcut.
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/SIGNameIncludingNoSIG"
|
$ref: "#/components/schemas/ActivityNameIncludingNoSig"
|
||||||
SpotNeedsSig:
|
SpotNeedsActivity:
|
||||||
name: needs_sig
|
name: needs_sig
|
||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the spots to only ones with a Special Interest Group such as POTA. Because supplying all
|
Limit the spots to only ones with an activity such as POTA. Because supplying all
|
||||||
known SIGs as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots
|
known activities as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots
|
||||||
with *no* SIG, this parameter can be set true to return only spots with a SIG, regardless of
|
with *no* activity, this parameter can be set true to return only spots with an activity, regardless of
|
||||||
what it is, so long as it's not blank. This is the equivalent of supplying the `sig` query
|
what it is, so long as it's not blank. This is the equivalent of supplying the `sig` query
|
||||||
param with a list of every known SIG apart from the special `NO_SIG` value. This is what Field
|
param with a list of every known activity apart from the special `NO_SIG` value. This is what Field
|
||||||
Spotter uses to exclude generic cluster spots and only retrieve xOTA things.
|
Spotter uses to exclude generic cluster spots and only retrieve xOTA things.
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
SpotNeedsSigRef:
|
SpotNeedsActivityRef:
|
||||||
name: needs_sig_ref
|
name: needs_sig_ref
|
||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the spots to only ones which have at least one reference (e.g. a park reference) for
|
Limit the spots to only ones which have at least one reference (e.g. a park reference) for
|
||||||
Special Interest Groups such as POTA.
|
activities such as POTA.
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
@@ -714,15 +714,15 @@ components:
|
|||||||
comma-separated list.
|
comma-separated list.
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Source"
|
$ref: "#/components/schemas/Source"
|
||||||
AlertSig:
|
AlertActivity:
|
||||||
name: sig
|
name: sig
|
||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the alerts to only ones from one or more Special Interest Groups. To select more than one
|
Limit the alerts to only ones from one or more activities. To select more than one
|
||||||
SIG, supply a comma-separated list. The special value 'NO_SIG' can be included to return alerts
|
activity, supply a comma-separated list. The special value 'NO_SIG' can be included to return alerts
|
||||||
specifically without an associated SIG (i.e. general DXpeditions).
|
specifically without an associated activity (i.e. general DXpeditions).
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/SIGNameIncludingNoSIG"
|
$ref: "#/components/schemas/ActivityNameIncludingNoSig"
|
||||||
AlertDxContinent:
|
AlertDxContinent:
|
||||||
name: dx_continent
|
name: dx_continent
|
||||||
in: query
|
in: query
|
||||||
@@ -830,17 +830,17 @@ components:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: M0TRT
|
example: M0TRT
|
||||||
SigRefSig:
|
ActivityRefLookupActivity:
|
||||||
name: sig
|
name: sig
|
||||||
in: query
|
in: query
|
||||||
description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/SIGName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
SigRefId:
|
ActivityRefLookupId:
|
||||||
name: id
|
name: id
|
||||||
in: query
|
in: query
|
||||||
description: ID of a reference in that SIG
|
description: ID of a reference in that activity
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
@@ -876,7 +876,7 @@ components:
|
|||||||
- UKPacketNet
|
- UKPacketNet
|
||||||
example: POTA
|
example: POTA
|
||||||
|
|
||||||
SIGName:
|
ActivityName:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- POTA
|
- POTA
|
||||||
@@ -914,7 +914,7 @@ components:
|
|||||||
- Toilets
|
- Toilets
|
||||||
example: POTA
|
example: POTA
|
||||||
|
|
||||||
SIGType:
|
ActivityType:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- WORLDWIDE
|
- WORLDWIDE
|
||||||
@@ -922,14 +922,14 @@ components:
|
|||||||
- EVENT
|
- EVENT
|
||||||
example: WORLDWIDE
|
example: WORLDWIDE
|
||||||
|
|
||||||
SIGNameIncludingNoSIG:
|
ActivityNameIncludingNoSig:
|
||||||
oneOf:
|
oneOf:
|
||||||
- $ref: "#/components/schemas/SIGName"
|
- $ref: "#/components/schemas/ActivityName"
|
||||||
- type: string
|
- type: string
|
||||||
enum: [ NO_SIG ]
|
enum: [ NO_SIG ]
|
||||||
example: POTA
|
example: POTA
|
||||||
|
|
||||||
SIGRefType:
|
ActivityRefType:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- PARK
|
- PARK
|
||||||
@@ -1075,26 +1075,26 @@ components:
|
|||||||
- NONE
|
- NONE
|
||||||
example: "HOME QTH"
|
example: "HOME QTH"
|
||||||
|
|
||||||
SIGRef:
|
ActivityRef:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
description: SIG reference ID.
|
description: Activity reference ID.
|
||||||
example: GB-0001
|
example: GB-0001
|
||||||
sig:
|
sig:
|
||||||
description: SIG that this reference is in.
|
description: Activity that this reference is in. Still named "sig" in the API for backwards compatibility.
|
||||||
$ref: "#/components/schemas/SIGName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: SIG reference name
|
description: Activity reference name
|
||||||
example: Null Country Park
|
example: Null Country Park
|
||||||
ref_type:
|
ref_type:
|
||||||
description: SIG reference type
|
description: Activity reference type
|
||||||
$ref: "#/components/schemas/SIGRefType"
|
$ref: "#/components/schemas/ActivityRefType"
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
description: SIG reference URL, which the user can look up for more information
|
description: Activity reference URL, which the user can look up for more information
|
||||||
example: "https://pota.app/#/park/GB-0001"
|
example: "https://pota.app/#/park/GB-0001"
|
||||||
grid:
|
grid:
|
||||||
type: string
|
type: string
|
||||||
@@ -1136,7 +1136,7 @@ components:
|
|||||||
dx_qth:
|
dx_qth:
|
||||||
type: string
|
type: string
|
||||||
description: >
|
description: >
|
||||||
QTH of the operator that has been spotted. This could be from any SIG refs or could be
|
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.
|
from online lookup of their home QTH.
|
||||||
example: Dorset
|
example: Dorset
|
||||||
dx_country:
|
dx_country:
|
||||||
@@ -1194,7 +1194,7 @@ components:
|
|||||||
dx_location_source:
|
dx_location_source:
|
||||||
description: >
|
description: >
|
||||||
Where we got the DX location (grid/latitude/longitude) from. If this was from the spot
|
Where we got the DX location (grid/latitude/longitude) from. If this was from the spot
|
||||||
itself, or from a lookup of the SIG ref (e.g. park) it's likely quite accurate, but if
|
itself, or from a lookup of the activity ref (e.g. park) it's likely quite accurate, but if
|
||||||
we had to fall back to QRZ lookup, or even a location based on the DXCC itself, it will
|
we had to fall back to QRZ lookup, or even a location based on the DXCC itself, it will
|
||||||
be a lot less accurate. "SPOT" indicates the location source was the spot itself from the
|
be a lot less accurate. "SPOT" indicates the location source was the spot itself from the
|
||||||
spotting service. "SIG REF LOOKUP" indicates that the spot didn't provide a location,
|
spotting service. "SIG REF LOOKUP" indicates that the spot didn't provide a location,
|
||||||
@@ -1308,13 +1308,13 @@ components:
|
|||||||
description: Comment left by the spotter, if any
|
description: Comment left by the spotter, if any
|
||||||
example: "59 in NY 73"
|
example: "59 in NY 73"
|
||||||
sig:
|
sig:
|
||||||
description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
|
||||||
$ref: "#/components/schemas/SIGName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
sig_refs:
|
sig_refs:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/SIGRef'
|
$ref: '#/components/schemas/ActivityRef'
|
||||||
description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
|
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility.
|
||||||
qrt:
|
qrt:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
|
description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
|
||||||
@@ -1358,13 +1358,13 @@ components:
|
|||||||
If true, forward the spot to an external upstream provider (e.g. POTA, SOTA) rather
|
If true, forward the spot to an external upstream provider (e.g. POTA, SOTA) rather
|
||||||
than only adding it to this Spothole server. Requires `sig`, at least one `sig_refs`
|
than only adding it to this Spothole server. Requires `sig`, at least one `sig_refs`
|
||||||
entry, and `upstream_provider` to be set. Check `spot_submit_providers` in the
|
entry, and `upstream_provider` to be set. Check `spot_submit_providers` in the
|
||||||
/options response to see which SIGs and providers support this.
|
/options response to see which activities and providers support this.
|
||||||
default: false
|
default: false
|
||||||
upstream_provider:
|
upstream_provider:
|
||||||
type: string
|
type: string
|
||||||
description: >
|
description: >
|
||||||
Name of the upstream provider to submit the spot to, e.g. "POTA" or "SOTA". Must
|
Name of the upstream provider to submit the spot to, e.g. "POTA" or "SOTA". Must
|
||||||
match one of the provider names returned in `spot_submit_providers` for the chosen SIG.
|
match one of the provider names returned in `spot_submit_providers` for the chosen activity.
|
||||||
example: POTA
|
example: POTA
|
||||||
upstream_credentials:
|
upstream_credentials:
|
||||||
type: object
|
type: object
|
||||||
@@ -1481,13 +1481,13 @@ components:
|
|||||||
description: Comment made by the activator, if any
|
description: Comment made by the activator, if any
|
||||||
example: "2025 DXpedition to null island"
|
example: "2025 DXpedition to null island"
|
||||||
sig:
|
sig:
|
||||||
description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
|
||||||
$ref: "#/components/schemas/SIGName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
sig_refs:
|
sig_refs:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/SIGRef'
|
$ref: '#/components/schemas/ActivityRef'
|
||||||
description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
|
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility.
|
||||||
alert_type:
|
alert_type:
|
||||||
description: "The type of alert this is: xOTA, DXpedition, or Contest."
|
description: "The type of alert this is: xOTA, DXpedition, or Contest."
|
||||||
$ref: "#/components/schemas/AlertType"
|
$ref: "#/components/schemas/AlertType"
|
||||||
@@ -1581,28 +1581,32 @@ components:
|
|||||||
description: The end frequency of this band, in Hz.
|
description: The end frequency of this band, in Hz.
|
||||||
example: 7200000
|
example: 7200000
|
||||||
|
|
||||||
SIG:
|
Activity:
|
||||||
type: object
|
type: object
|
||||||
|
description: >
|
||||||
|
Represents an activity (a term which replaces the older "Special Interest Group" or "SIG" terminology,
|
||||||
|
though `sig`-prefixed field names remain for API backwards compatibility).
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
description: The abbreviated name of the SIG
|
description: The abbreviated name of the activity
|
||||||
$ref: "#/components/schemas/SIGName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
description: The full name of the SIG
|
description: The full name of the activity
|
||||||
example: Parks on the Air
|
example: Parks on the Air
|
||||||
sig_type:
|
sig_type:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: >
|
description: >
|
||||||
Whether the SIG is worldwide, regional, or for a specific event. Generally for Spothole's own internal use,
|
Whether the activity is worldwide, regional, or for a specific event. Generally for Spothole's own
|
||||||
clients probably won't need this. Used to group them in the web UI.
|
internal use, clients probably won't need this. Used to group them in the web UI. Still named
|
||||||
$ref: "#/components/schemas/SIGType"
|
"sig_type" in the API for backwards compatibility.
|
||||||
|
$ref: "#/components/schemas/ActivityType"
|
||||||
comment_names:
|
comment_names:
|
||||||
type: array
|
type: array
|
||||||
description: >
|
description: >
|
||||||
Names by which this SIG may be referred to in cluster spot comments. Most SIGs have a
|
Names by which this activity may be referred to in cluster spot comments. Most activities have a
|
||||||
single entry matching their programme name (e.g. ["POTA"]), but some have none (where
|
single entry matching their programme name (e.g. ["POTA"]), but some have none (where
|
||||||
the name is ambiguous with other SIGs, such as Tiles and Toilets on the Air) or multiple
|
the name is ambiguous with other activities, such as Tiles and Toilets on the Air) or multiple
|
||||||
entries (e.g. WWBOTA accepts both "WWBOTA" and "BOTA" since the latter is often used).
|
entries (e.g. WWBOTA accepts both "WWBOTA" and "BOTA" since the latter is often used).
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -1610,26 +1614,27 @@ components:
|
|||||||
ref_regex:
|
ref_regex:
|
||||||
type: string
|
type: string
|
||||||
description: >
|
description: >
|
||||||
Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use,
|
Regex that matches this activity's reference IDs. Generally for Spothole's own internal use,
|
||||||
clients probably won't need this.
|
clients probably won't need this.
|
||||||
example: "[A-Z]{2}\\-\\d+"
|
example: "[A-Z]{2}\\-\\d+"
|
||||||
refs_globally_unique:
|
refs_globally_unique:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: >
|
description: >
|
||||||
Identifies that the SIG's reference ID structure defined by its regex is unique across all programmes and
|
Identifies that the activity's reference ID structure defined by its regex is unique across all
|
||||||
anything else we expect a user to put in a spot comment, and therefore we can pull references out of
|
programmes and anything else we expect a user to put in a spot comment, and therefore we can pull
|
||||||
spot comments without also needing to see the SIG name first. For example, "OHFF-1234" or "B/G-1234" are
|
references out of spot comments without also needing to see the activity name first. For example,
|
||||||
obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could
|
"OHFF-1234" or "B/G-1234" are obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09"
|
||||||
conceivably be POTA or ILLW, etc. Generally for Spothole's own internal use, clients probably won't need
|
could be WAB or Tiles, "GB1234" could conceivably be POTA or ILLW, etc. Generally for Spothole's own
|
||||||
this.
|
internal use, clients probably won't need this.
|
||||||
icon:
|
icon:
|
||||||
type: string
|
type: string
|
||||||
description: Icon from the Font Awesome set that represents this SIG, for use in the front end.
|
description: Icon from the Font Awesome set that represents this activity, for use in the front end.
|
||||||
example: "fa-tree"
|
example: "fa-tree"
|
||||||
region_flag:
|
region_flag:
|
||||||
type: string
|
type: string
|
||||||
description: >
|
description: >
|
||||||
Flag emoji, if this SIG is specific to a country or region. If null, this SIG is treated as worldwide.
|
Flag emoji, if this activity is specific to a country or region. If null, this activity is treated as
|
||||||
|
worldwide.
|
||||||
example: "🇺🇳"
|
example: "🇺🇳"
|
||||||
|
|
||||||
SolarConditions:
|
SolarConditions:
|
||||||
@@ -1978,12 +1983,12 @@ components:
|
|||||||
is zero, the provider has never updated.
|
is zero, the provider has never updated.
|
||||||
example: 1759579508
|
example: 1759579508
|
||||||
|
|
||||||
SIGRefDataProviderStatus:
|
ActivityRefDataProviderStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sig_name:
|
sig_name:
|
||||||
type: string
|
type: string
|
||||||
description: The name of the SIG.
|
description: The name of the activity. Still named "sig_name" in the API for backwards compatibility.
|
||||||
example: WWFF
|
example: WWFF
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -2148,9 +2153,9 @@ components:
|
|||||||
$ref: '#/components/schemas/StaticDataProviderStatus'
|
$ref: '#/components/schemas/StaticDataProviderStatus'
|
||||||
sig_ref_data_providers:
|
sig_ref_data_providers:
|
||||||
type: array
|
type: array
|
||||||
description: An array of all the SIG reference data providers.
|
description: An array of all the activity reference data providers.
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/SIGRefDataProviderStatus'
|
$ref: '#/components/schemas/ActivityRefDataProviderStatus'
|
||||||
callsign_data_providers:
|
callsign_data_providers:
|
||||||
type: array
|
type: array
|
||||||
description: An array of all the callsign data providers.
|
description: An array of all the callsign data providers.
|
||||||
@@ -2179,9 +2184,9 @@ components:
|
|||||||
example: "PHONE"
|
example: "PHONE"
|
||||||
sigs:
|
sigs:
|
||||||
type: array
|
type: array
|
||||||
description: An array of all the supported Special Interest Groups.
|
description: An array of all the supported activities.
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/SIG'
|
$ref: '#/components/schemas/Activity'
|
||||||
spot_providers:
|
spot_providers:
|
||||||
type: array
|
type: array
|
||||||
description: An array of all the supported spot data sources.
|
description: An array of all the supported spot data sources.
|
||||||
@@ -2228,9 +2233,9 @@ components:
|
|||||||
spot_submit_providers:
|
spot_submit_providers:
|
||||||
type: object
|
type: object
|
||||||
description: >
|
description: >
|
||||||
A map of SIG name to a list of provider names that support upstream spot submission for that SIG.
|
A map of activity name to a list of provider names that support upstream spot submission for that
|
||||||
If a SIG appears as a key here, the POST /spot endpoint accepts `submit_upstream: true` for
|
activity. If an activity appears as a key here, the POST /spot endpoint accepts `submit_upstream: true`
|
||||||
spots with that SIG, and will forward the spot to one of the listed providers. Omitted if no
|
for spots with that activity, and will forward the spot to one of the listed providers. Omitted if no
|
||||||
providers support upstream submission.
|
providers support upstream submission.
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: array
|
type: array
|
||||||
@@ -2258,7 +2263,7 @@ components:
|
|||||||
qth:
|
qth:
|
||||||
type: string
|
type: string
|
||||||
description: >
|
description: >
|
||||||
QTH of the operator. This could be from any SIG refs or could be from online lookup of
|
QTH of the operator. This could be from any activity refs or could be from online lookup of
|
||||||
their home QTH.
|
their home QTH.
|
||||||
example: Dorset
|
example: Dorset
|
||||||
country:
|
country:
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ a.dx-link {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.sig-ref-link {
|
a.activity-ref-link {
|
||||||
color: var(--bs-emphasis-color);
|
color: var(--bs-emphasis-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ function loadOptions() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate SIG drop-down
|
// Populate activity drop-down
|
||||||
$.each(options["sigs"], function (i, sig) {
|
$.each(options["sigs"], function (i, activity) {
|
||||||
$('#sig').append($('<option>', {
|
$('#sig').append($('<option>', {
|
||||||
value: sig.name,
|
value: activity.name,
|
||||||
text: sig.name
|
text: activity.name
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ function loadOptions() {
|
|||||||
// Load settings from settings storage now all the controls are available
|
// Load settings from settings storage now all the controls are available
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|
||||||
// Update the upstream area for any pre-selected SIG
|
// Update the upstream area for any pre-selected activity
|
||||||
updateUpstreamArea();
|
updateUpstreamArea();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ function renderRecaptcha() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the "Send spot to..." area based on the currently selected SIG
|
// Update the "Send spot to..." area based on the currently selected activity
|
||||||
function updateUpstreamArea() {
|
function updateUpstreamArea() {
|
||||||
if (!window._allowUpstreamSpotting || !options || !options["spot_submit_providers"]) {
|
if (!window._allowUpstreamSpotting || !options || !options["spot_submit_providers"]) {
|
||||||
$("#upstream-area").hide();
|
$("#upstream-area").hide();
|
||||||
@@ -290,7 +290,7 @@ $("#mode").change(function () {
|
|||||||
$(this).val($(this).val().trim().toUpperCase());
|
$(this).val($(this).val().trim().toUpperCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update upstream area and credentials button when SIG changes
|
// Update upstream area and credentials button when activity changes
|
||||||
$("#sig").change(function () {
|
$("#sig").change(function () {
|
||||||
updateUpstreamArea();
|
updateUpstreamArea();
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-14
@@ -250,34 +250,34 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Type, SIG or fallback to source
|
// Type, activity or fallback to source
|
||||||
let sigTypeText = a["source"];
|
let activityTypeText = a["source"];
|
||||||
if (a["alert_type"] === "CONTEST") {
|
if (a["alert_type"] === "CONTEST") {
|
||||||
sigTypeText = "Contest";
|
activityTypeText = "Contest";
|
||||||
} else if (a["alert_type"] === "DXPEDITION") {
|
} else if (a["alert_type"] === "DXPEDITION") {
|
||||||
sigTypeText = "DXpedition";
|
activityTypeText = "DXpedition";
|
||||||
} else if (a["alert_type"] === "SATELLITE") {
|
} else if (a["alert_type"] === "SATELLITE") {
|
||||||
sigTypeText = "Satellite";
|
activityTypeText = "Satellite";
|
||||||
} else if (a["alert_type"] === "XOTA") {
|
} else if (a["alert_type"] === "XOTA") {
|
||||||
if (a["sig"]) {
|
if (a["sig"]) {
|
||||||
sigTypeText = a["sig"];
|
activityTypeText = a["sig"];
|
||||||
} else {
|
} else {
|
||||||
sigTypeText = "xOTA";
|
activityTypeText = "xOTA";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format sig_refs
|
// Format activity refs
|
||||||
let sig_refs = "";
|
let activityRefs = "";
|
||||||
if (a["sig_refs"] != null) {
|
if (a["sig_refs"] != null) {
|
||||||
const items = [];
|
const items = [];
|
||||||
for (let i = 0; i < a["sig_refs"].length; i++) {
|
for (let i = 0; i < a["sig_refs"].length; i++) {
|
||||||
if (a["sig_refs"][i]["url"] != null) {
|
if (a["sig_refs"][i]["url"] != null) {
|
||||||
items[i] = `<a href='${encodeURI(a["sig_refs"][i]["url"])}' title='${escapeHtml(a["sig_refs"][i]["name"])}' target='_new' class='sig-ref-link'>${escapeHtml(a["sig_refs"][i]["id"])}</a>`
|
items[i] = `<a href='${encodeURI(a["sig_refs"][i]["url"])}' title='${escapeHtml(a["sig_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(a["sig_refs"][i]["id"])}</a>`
|
||||||
} else {
|
} else {
|
||||||
items[i] = `${escapeHtml(a["sig_refs"][i]["id"])}`
|
items[i] = `${escapeHtml(a["sig_refs"][i]["id"])}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sig_refs = items.join(", ");
|
activityRefs = items.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the row
|
// Populate the row
|
||||||
@@ -297,10 +297,10 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
|
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
|
||||||
}
|
}
|
||||||
if (showType) {
|
if (showType) {
|
||||||
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid ${a["icon"]}'></i></span> ${sigTypeText}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid ${a["icon"]}'></i></span> ${activityTypeText}</td>`);
|
||||||
}
|
}
|
||||||
if (showRef) {
|
if (showRef) {
|
||||||
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
|
$tr.append(`<td class='hideonmobile'>${activityRefs}</td>`);
|
||||||
}
|
}
|
||||||
tbody.append($tr);
|
tbody.append($tr);
|
||||||
|
|
||||||
@@ -318,7 +318,7 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
$td2.append(`<span class='icon-wrapper'><i class='fa-solid ${a["icon"]}'></i></span> `);
|
$td2.append(`<span class='icon-wrapper'><i class='fa-solid ${a["icon"]}'></i></span> `);
|
||||||
}
|
}
|
||||||
if (showRef) {
|
if (showRef) {
|
||||||
$td2.append(`${sig_refs} `);
|
$td2.append(`${activityRefs} `);
|
||||||
}
|
}
|
||||||
if (showFreqsModes) {
|
if (showFreqsModes) {
|
||||||
$td2.append(`${freqsModesText} `);
|
$td2.append(`${freqsModesText} `);
|
||||||
|
|||||||
+1
-1
@@ -295,7 +295,7 @@ function loadOptions() {
|
|||||||
|
|
||||||
// Populate the filters panel
|
// Populate the filters panel
|
||||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||||
generateSIGsMultiToggleFilterCard(options["sigs"]);
|
generateActivitiesMultiToggleFilterCard(options["sigs"]);
|
||||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||||
generateModesMultiToggleFilterCard(options["modes"]);
|
generateModesMultiToggleFilterCard(options["modes"]);
|
||||||
|
|||||||
+10
-10
@@ -274,24 +274,24 @@ function getTooltipText(s) {
|
|||||||
commentText = escapeHtml(s["comment"]);
|
commentText = escapeHtml(s["comment"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sig or fallback to source
|
// Activity or fallback to source
|
||||||
let sigSourceText = s["source"];
|
let activitySourceText = s["source"];
|
||||||
if (s["sig"]) {
|
if (s["sig"]) {
|
||||||
sigSourceText = s["sig"];
|
activitySourceText = s["sig"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format sig_refs
|
// Format activity refs
|
||||||
let sig_refs = "";
|
let activityRefs = "";
|
||||||
if (s["sig_refs"] != null) {
|
if (s["sig_refs"] != null) {
|
||||||
const items = [];
|
const items = [];
|
||||||
for (let i = 0; i < s["sig_refs"].length; i++) {
|
for (let i = 0; i < s["sig_refs"].length; i++) {
|
||||||
if (s["sig_refs"][i]["url"] != null) {
|
if (s["sig_refs"][i]["url"] != null) {
|
||||||
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>`
|
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='activity-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||||
} else {
|
} else {
|
||||||
items[i] = `${s["sig_refs"][i]["id"]}`
|
items[i] = `${s["sig_refs"][i]["id"]}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sig_refs = items.join(", ");
|
activityRefs = items.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// DX
|
// DX
|
||||||
@@ -308,8 +308,8 @@ function getTooltipText(s) {
|
|||||||
}
|
}
|
||||||
ttt += "<br/>";
|
ttt += "<br/>";
|
||||||
|
|
||||||
// Source / SIG / Ref
|
// Source / Activity / Ref
|
||||||
ttt += `<span class='nowrap'><span class='icon-wrapper'><i class='fa-solid ${s["icon"]}'></i></span> ${sigSourceText} ${sig_refs}</span><br/>`;
|
ttt += `<span class='nowrap'><span class='icon-wrapper'><i class='fa-solid ${s["icon"]}'></i></span> ${activitySourceText} ${activityRefs}</span><br/>`;
|
||||||
|
|
||||||
// Time
|
// Time
|
||||||
ttt += `<span class='icon-wrapper'><i class='fa-solid fa-clock markerPopupIcon'></i></span> ${moment.unix(s["time"]).fromNow()}`;
|
ttt += `<span class='icon-wrapper'><i class='fa-solid fa-clock markerPopupIcon'></i></span> ${moment.unix(s["time"]).fromNow()}`;
|
||||||
@@ -339,7 +339,7 @@ function loadOptions() {
|
|||||||
|
|
||||||
// Populate the filters panel
|
// Populate the filters panel
|
||||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||||
generateSIGsMultiToggleFilterCard(options["sigs"]);
|
generateActivitiesMultiToggleFilterCard(options["sigs"]);
|
||||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||||
generateModesMultiToggleFilterCard(options["modes"]);
|
generateModesMultiToggleFilterCard(options["modes"]);
|
||||||
|
|||||||
+8
-8
@@ -327,24 +327,24 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format "type" (Sig or fallback to source)
|
// Format "type" (activity or fallback to source)
|
||||||
let typeText = s["source"];
|
let typeText = s["source"];
|
||||||
if (s["sig"]) {
|
if (s["sig"]) {
|
||||||
typeText = s["sig"];
|
typeText = s["sig"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format sig_refs
|
// Format activity refs
|
||||||
let sig_refs = "";
|
let activityRefs = "";
|
||||||
if (s["sig_refs"] != null) {
|
if (s["sig_refs"] != null) {
|
||||||
const items = [];
|
const items = [];
|
||||||
for (let i = 0; i < s["sig_refs"].length; i++) {
|
for (let i = 0; i < s["sig_refs"].length; i++) {
|
||||||
if (s["sig_refs"][i]["url"] != null) {
|
if (s["sig_refs"][i]["url"] != null) {
|
||||||
items[i] = `<span style="white-space: nowrap;"><a href='${encodeURI(s["sig_refs"][i]["url"])}' title='${escapeHtml(s["sig_refs"][i]["name"])}' target='_new' class='sig-ref-link'>${escapeHtml(s["sig_refs"][i]["id"])}</a></span>`
|
items[i] = `<span style="white-space: nowrap;"><a href='${encodeURI(s["sig_refs"][i]["url"])}' title='${escapeHtml(s["sig_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(s["sig_refs"][i]["id"])}</a></span>`
|
||||||
} else {
|
} else {
|
||||||
items[i] = `<span style="white-space: nowrap;">${escapeHtml(s["sig_refs"][i]["id"])}</span>`
|
items[i] = `<span style="white-space: nowrap;">${escapeHtml(s["sig_refs"][i]["id"])}</span>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sig_refs = items.join(", ");
|
activityRefs = items.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format de country
|
// Format de country
|
||||||
@@ -401,7 +401,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid ${s["icon"]}'></i></span> ${typeText}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid ${s["icon"]}'></i></span> ${typeText}</td>`);
|
||||||
}
|
}
|
||||||
if (showRef) {
|
if (showRef) {
|
||||||
$tr.append(`<td class='hideonmobile' style='max-width: 11em;'>${sig_refs}</td>`);
|
$tr.append(`<td class='hideonmobile' style='max-width: 11em;'>${activityRefs}</td>`);
|
||||||
}
|
}
|
||||||
if (showDE) {
|
if (showDE) {
|
||||||
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
|
||||||
@@ -433,7 +433,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
$td2floatleft.append(`<span class='icon-wrapper'><i class='fa-solid ${s["icon"]}'></i></span> ${typeText} `);
|
$td2floatleft.append(`<span class='icon-wrapper'><i class='fa-solid ${s["icon"]}'></i></span> ${typeText} `);
|
||||||
}
|
}
|
||||||
if (showRef) {
|
if (showRef) {
|
||||||
$td2floatleft.append(`${sig_refs} `);
|
$td2floatleft.append(`${activityRefs} `);
|
||||||
}
|
}
|
||||||
$td2.append($td2floatleft);
|
$td2.append($td2floatleft);
|
||||||
const $td2floatright = $(`<div style="float: right;">`);
|
const $td2floatright = $(`<div style="float: right;">`);
|
||||||
@@ -478,7 +478,7 @@ function loadOptions() {
|
|||||||
|
|
||||||
// Populate the filters panel
|
// Populate the filters panel
|
||||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||||
generateSIGsMultiToggleFilterCard(options["sigs"]);
|
generateActivitiesMultiToggleFilterCard(options["sigs"]);
|
||||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||||
generateModesMultiToggleFilterCard(options["modes"]);
|
generateModesMultiToggleFilterCard(options["modes"]);
|
||||||
|
|||||||
@@ -38,25 +38,25 @@ function setHamHFBandToggles() {
|
|||||||
filtersUpdated();
|
filtersUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate SIGs filter card. This one is also a special case.
|
// Generate activities filter card. This one is also a special case.
|
||||||
function generateSIGsMultiToggleFilterCard(sig_options) {
|
function generateActivitiesMultiToggleFilterCard(activity_options) {
|
||||||
const $grid1 = $('<div class="row row-cols-2 g-1 mb-1">');
|
const $grid1 = $('<div class="row row-cols-2 g-1 mb-1">');
|
||||||
$("#sig-options").append('<strong>Worldwide</strong>');
|
$("#sig-options").append('<strong>Worldwide</strong>');
|
||||||
sig_options.filter(o => o["sig_type"] === "WORLDWIDE").forEach(o => {
|
activity_options.filter(o => o["sig_type"] === "WORLDWIDE").forEach(o => {
|
||||||
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||||
$grid1.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
$grid1.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
||||||
});
|
});
|
||||||
$("#sig-options").append($grid1);
|
$("#sig-options").append($grid1);
|
||||||
const $grid2 = $('<div class="row row-cols-2 g-1 mb-1">');
|
const $grid2 = $('<div class="row row-cols-2 g-1 mb-1">');
|
||||||
$("#sig-options").append('<strong>Regional</strong>');
|
$("#sig-options").append('<strong>Regional</strong>');
|
||||||
sig_options.filter(o => o["sig_type"] === "REGIONAL").forEach(o => {
|
activity_options.filter(o => o["sig_type"] === "REGIONAL").forEach(o => {
|
||||||
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||||
$grid2.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
$grid2.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
||||||
});
|
});
|
||||||
$("#sig-options").append($grid2);
|
$("#sig-options").append($grid2);
|
||||||
const $grid3 = $('<div class="row row-cols-2 g-1 mb-1">');
|
const $grid3 = $('<div class="row row-cols-2 g-1 mb-1">');
|
||||||
$("#sig-options").append('<strong>Event</strong>');
|
$("#sig-options").append('<strong>Event</strong>');
|
||||||
sig_options.filter(o => o["sig_type"] === "EVENT").forEach(o => {
|
activity_options.filter(o => o["sig_type"] === "EVENT").forEach(o => {
|
||||||
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||||
$grid3.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
$grid3.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// USER INTERFACE FUNCTIONS (AMATEUR RADIO)
|
// USER INTERFACE FUNCTIONS (AMATEUR RADIO)
|
||||||
// Functions providing colour schemes for ham radio bands, SIG icons etc.
|
// Functions providing colour schemes for ham radio bands, activity icons etc.
|
||||||
//
|
//
|
||||||
|
|
||||||
const BAND_COLOR_SCHEMES = {
|
const BAND_COLOR_SCHEMES = {
|
||||||
|
|||||||
@@ -103,7 +103,8 @@
|
|||||||
<p>Note that the server owner has not necessarily enabled all these data sources. In particular it is common to
|
<p>Note that the server owner has not necessarily enabled all these data sources. In particular it is common to
|
||||||
disable RBN, to avoid the server being swamped with FT8 traffic, and to disable APRS-IS and UK Packet Net so
|
disable RBN, to avoid the server being swamped with FT8 traffic, and to disable APRS-IS and UK Packet Net so
|
||||||
that the server only displays stations where there is likely to be an operator physically present for a QSO.</p>
|
that the server only displays stations where there is likely to be an operator physically present for a QSO.</p>
|
||||||
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air
|
<p>Between the various data sources, the following activities / special interest groups are supported: Parks on the
|
||||||
|
Air
|
||||||
(POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide
|
(POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide
|
||||||
Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air
|
Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air
|
||||||
(MOTA), the Amateur Radio Lighthouse Society (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos
|
(MOTA), the Amateur Radio Lighthouse Society (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos
|
||||||
@@ -118,16 +119,16 @@
|
|||||||
<p>As of the time of writing in August 2026, I think Spothole captures most outdoor radio programmes that have a
|
<p>As of the time of writing in August 2026, I think Spothole captures most outdoor radio programmes that have a
|
||||||
defined, downloadable reference list, and almost certainly those that have a spotting/alerting API. If you know
|
defined, downloadable reference list, and almost certainly those that have a spotting/alerting API. If you know
|
||||||
of one I've missed, please let me know!</p>
|
of one I've missed, please let me know!</p>
|
||||||
<h4 class="mt-4">Why can I filter spots by both SIG and Source? Isn't that basically the same thing?</h4>
|
<h4 class="mt-4">Why can I filter spots by both Activity and Source? Isn't that basically the same thing?</h4>
|
||||||
<p>Mostly, but not quite. While POTA spots generally come from the POTA source and so on, there are a few
|
<p>Mostly, but not quite. While POTA spots generally come from the POTA source and so on, there are a few
|
||||||
exceptions:</p>
|
exceptions:</p>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Sources like GMA and Parks 'n' Peaks provide spots for multiple different programmes (SIGs).</li>
|
<li>Sources like GMA and Parks 'n' Peaks provide spots for multiple different programmes (activities).</li>
|
||||||
<li>Cluster spots may name SIGs in their comment, in which case the source remains the Cluster, but a SIG is
|
<li>Cluster spots may name activities in their comment, in which case the source remains the Cluster, but an
|
||||||
assigned.
|
activity is assigned.
|
||||||
</li>
|
</li>
|
||||||
<li>Some SIGs, such as Worked all Britain (WAB), don't have their own spotting site and can <em>only</em> be
|
<li>Some activities, such as Worked all Britain (WAB), don't have their own spotting site and can <em>only</em>
|
||||||
identified through comments on spots retrieved from other sources.
|
be identified through comments on spots retrieved from other sources.
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p>Spothole's web interface exists not just for the end user, but also as a reference implementation for the API, so
|
<p>Spothole's web interface exists not just for the end user, but also as a reference implementation for the API, so
|
||||||
|
|||||||
@@ -41,13 +41,13 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="sig" class="form-label">SIG</label>
|
<label for="sig" class="form-label">Activity</label>
|
||||||
<select id="sig" class="form-select">
|
<select id="sig" class="form-select">
|
||||||
<option value="" selected></option>
|
<option value="" selected></option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="sig-ref" class="form-label">SIG Reference</label>
|
<label for="sig-ref" class="form-label">Activity Reference</label>
|
||||||
<input type="text" class="form-control input-narrow" id="sig-ref" placeholder="e.g. GB-0001">
|
<input type="text" class="form-control input-narrow" id="sig-ref" placeholder="e.g. GB-0001">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% module Template("cards/sigs.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">SIGs</h5>
|
<h5 class="card-title">Activities</h5>
|
||||||
<div id="sig-options" class="card-text spothole-card-text"></div>
|
<div id="sig-options" class="card-text spothole-card-text"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
+1
-1
@@ -37,7 +37,7 @@
|
|||||||
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% module Template("cards/sigs.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% module Template("cards/sigs.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
|
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
SIG Reference Data Providers
|
Activity Reference Data Providers
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body" id="sig_ref_data_providers-status-container">
|
<div class="card-body" id="sig_ref_data_providers-status-container">
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import tornado
|
|||||||
from tornado import httputil
|
from tornado import httputil
|
||||||
from tornado.web import Application
|
from tornado.web import Application
|
||||||
|
|
||||||
|
from core.activity_utils import get_ref_regex_for_activity
|
||||||
from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY
|
from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY
|
||||||
from core.constants import UNKNOWN_BAND
|
from core.constants import UNKNOWN_BAND
|
||||||
from core.sig_utils import get_ref_regex_for_sig
|
|
||||||
from core.utils import infer_band_from_freq, safe_json_dumps
|
from core.utils import infer_band_from_freq, safe_json_dumps
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from providers.spot.spot_provider import SpotProvider
|
from providers.spot.spot_provider import SpotProvider
|
||||||
@@ -142,14 +142,14 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Reject if sig_ref format incorrect for sig
|
# Reject if activity ref format incorrect for activity
|
||||||
if (
|
if (
|
||||||
spot.sig
|
spot.sig
|
||||||
and spot.sig_refs
|
and spot.sig_refs
|
||||||
and len(spot.sig_refs) > 0
|
and len(spot.sig_refs) > 0
|
||||||
and spot.sig_refs[0].id
|
and spot.sig_refs[0].id
|
||||||
and get_ref_regex_for_sig(spot.sig)
|
and get_ref_regex_for_activity(spot.sig)
|
||||||
and not re.match(get_ref_regex_for_sig(spot.sig), spot.sig_refs[0].id)
|
and not re.match(get_ref_regex_for_activity(spot.sig), spot.sig_refs[0].id)
|
||||||
):
|
):
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(
|
self.write(
|
||||||
@@ -173,13 +173,13 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
if submit_upstream and upstream_provider_name:
|
if submit_upstream and upstream_provider_name:
|
||||||
if not spot.sig:
|
if not spot.sig:
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(safe_json_dumps("Error - a SIG must be selected to submit upstream."))
|
self.write(safe_json_dumps("Error - an activity must be selected to submit upstream."))
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
if not spot.sig_refs and upstream_provider_name != "Tiles":
|
if not spot.sig_refs and upstream_provider_name != "Tiles":
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(safe_json_dumps("Error - a SIG reference is required to submit upstream."))
|
self.write(safe_json_dumps("Error - an activity reference is required to submit upstream."))
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
@@ -241,11 +241,11 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
|
|
||||||
def _find_provider(self, provider_name, sig) -> SpotProvider | None:
|
def _find_provider(self, provider_name, activity) -> SpotProvider | None:
|
||||||
"""Find an enabled provider by name that can submit spots for the given SIG."""
|
"""Find an enabled provider by name that can submit spots for the given activity."""
|
||||||
|
|
||||||
for p in self._spot_providers:
|
for p in self._spot_providers:
|
||||||
if p.enabled and p.name == provider_name and p.can_submit_spot(sig):
|
if p.enabled and p.name == provider_name and p.can_submit_spot(activity):
|
||||||
return p
|
return p
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -187,13 +187,13 @@ def alert_allowed_by_query(alert, query):
|
|||||||
if not alert.source or alert.source not in sources:
|
if not alert.source or alert.source not in sources:
|
||||||
return False
|
return False
|
||||||
case "sig":
|
case "sig":
|
||||||
# If a list of sigs is provided, the alert must have a sig and it must match one of them.
|
# If a list of activities is provided, the alert must have an activity and it must match one of them.
|
||||||
# The special "sig" "NO_SIG", when supplied in the list, mathches alerts with no sig.
|
# The special activity "NO_SIG", when supplied in the list, matches alerts with no activity.
|
||||||
sigs = query.get(k).split(",")
|
activities = query.get(k).split(",")
|
||||||
include_no_sig = "NO_SIG" in sigs
|
include_no_activity = "NO_SIG" in activities
|
||||||
if not alert.sig and not include_no_sig:
|
if not alert.sig and not include_no_activity:
|
||||||
return False
|
return False
|
||||||
if alert.sig and alert.sig not in sigs:
|
if alert.sig and alert.sig not in activities:
|
||||||
return False
|
return False
|
||||||
case "dx_continent":
|
case "dx_continent":
|
||||||
dxconts = query.get(k).split(",")
|
dxconts = query.get(k).split(",")
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ import tornado
|
|||||||
from tornado import httputil
|
from tornado import httputil
|
||||||
from tornado.web import Application
|
from tornado.web import Application
|
||||||
|
|
||||||
|
from core.activity_lookup_helper import populate_missing_activity_ref_info
|
||||||
|
from core.activity_utils import get_ref_regex_for_activity
|
||||||
from core.call_lookup_helper import get_call_info
|
from core.call_lookup_helper import get_call_info
|
||||||
from core.constants import SIGS
|
from core.constants import ACTIVITIES
|
||||||
from core.geo_utils import (
|
from core.geo_utils import (
|
||||||
lat_lon_for_grid_sw_corner_plus_size,
|
lat_lon_for_grid_sw_corner_plus_size,
|
||||||
lat_lon_to_cq_zone,
|
lat_lon_to_cq_zone,
|
||||||
lat_lon_to_itu_zone,
|
lat_lon_to_itu_zone,
|
||||||
)
|
)
|
||||||
from core.sig_lookup_helper import populate_missing_sig_ref_info
|
|
||||||
from core.sig_utils import get_ref_regex_for_sig
|
|
||||||
from core.utils import safe_json_dumps
|
from core.utils import safe_json_dumps
|
||||||
|
from data.activity_ref import ActivityRef
|
||||||
from data.lookup_credentials import extract_credentials
|
from data.lookup_credentials import extract_credentials
|
||||||
from data.sig_ref import SIGRef
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class APILookupCallHandler(tornado.web.RequestHandler):
|
|||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
|
|
||||||
|
|
||||||
class APILookupSIGRefHandler(tornado.web.RequestHandler):
|
class APILookupActivityRefHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/lookup/sigref"""
|
"""API request handler for /api/v2/lookup/sigref"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -80,30 +80,34 @@ class APILookupSIGRefHandler(tornado.web.RequestHandler):
|
|||||||
# reduce that to just the first entry, and convert bytes to string
|
# reduce that to just the first entry, and convert bytes to string
|
||||||
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
||||||
|
|
||||||
# "sig" and "id" query params must exist, SIG must be known, and if we have a reference regex for that SIG,
|
# "sig" and "id" query params must exist, the activity must be known, and if we have a reference regex for
|
||||||
# the provided id must match it.
|
# that activity, the provided id must match it.
|
||||||
if "sig" in query_params and "id" in query_params:
|
if "sig" in query_params and "id" in query_params:
|
||||||
sig = str(query_params.get("sig")).upper()
|
activity = str(query_params.get("sig")).upper()
|
||||||
ref_id = str(query_params.get("id")).upper()
|
ref_id = str(query_params.get("id")).upper()
|
||||||
if sig in [p.name.upper() for p in SIGS]:
|
if activity in [a.name.upper() for a in ACTIVITIES]:
|
||||||
if not get_ref_regex_for_sig(sig) or re.match(get_ref_regex_for_sig(sig), ref_id):
|
if not get_ref_regex_for_activity(activity) or re.match(
|
||||||
data = populate_missing_sig_ref_info(SIGRef(id=ref_id, sig=sig))
|
get_ref_regex_for_activity(activity), ref_id
|
||||||
|
):
|
||||||
|
data = populate_missing_activity_ref_info(ActivityRef(id=ref_id, sig=activity))
|
||||||
self.write(safe_json_dumps(data))
|
self.write(safe_json_dumps(data))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.write(
|
self.write(
|
||||||
safe_json_dumps(f"Error - '{ref_id}' does not look like a valid reference ID for {sig}.")
|
safe_json_dumps(
|
||||||
|
f"Error - '{ref_id}' does not look like a valid reference ID for {activity}."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
else:
|
else:
|
||||||
self.write(safe_json_dumps(f"Error - sig '{sig}' is not known."))
|
self.write(safe_json_dumps(f"Error - sig '{activity}' is not known."))
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
else:
|
else:
|
||||||
self.write(safe_json_dumps("Error - sig and id must be provided"))
|
self.write(safe_json_dumps("Error - sig and id must be provided"))
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Exception when handling client request to sig ref lookup API")
|
logger.exception("Exception when handling client request to activity ref lookup API")
|
||||||
self.write(safe_json_dumps("Error - an internal server error occurred."))
|
self.write(safe_json_dumps("Error - an internal server error occurred."))
|
||||||
self.set_status(500)
|
self.set_status(500)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from tornado import httputil
|
|||||||
from tornado.web import Application
|
from tornado.web import Application
|
||||||
|
|
||||||
from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE
|
from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE
|
||||||
from core.constants import BANDS, PROPAGATION_MODES, SIGS
|
from core.constants import ACTIVITIES, BANDS, PROPAGATION_MODES
|
||||||
from core.enums import Continent, Mode, ModeType
|
from core.enums import Continent, Mode, ModeType
|
||||||
from core.utils import safe_json_dumps
|
from core.utils import safe_json_dumps
|
||||||
|
|
||||||
@@ -32,16 +32,16 @@ class APIOptionsHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
try:
|
try:
|
||||||
# Build a map of SIG name -> list of provider names that can submit spots for that SIG
|
# Build a map of activity name -> list of provider names that can submit spots for that activity
|
||||||
spot_submit_providers = {}
|
spot_submit_providers = {}
|
||||||
|
|
||||||
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
|
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
|
||||||
# for provider in self._spot_providers:
|
# for provider in self._spot_providers:
|
||||||
# if not provider.enabled:
|
# if not provider.enabled:
|
||||||
# continue
|
# continue
|
||||||
# for sig in SIGS:
|
# for activity in ACTIVITIES:
|
||||||
# if provider.can_submit_spot(sig.name):
|
# if provider.can_submit_spot(activity.name):
|
||||||
# spot_submit_providers.setdefault(sig.name, []).append(provider.name)
|
# spot_submit_providers.setdefault(activity.name, []).append(provider.name)
|
||||||
|
|
||||||
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
|
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
|
||||||
# things that aren't even available.
|
# things that aren't even available.
|
||||||
@@ -74,7 +74,7 @@ class APIOptionsHandler(tornado.web.RequestHandler):
|
|||||||
"bands": BANDS,
|
"bands": BANDS,
|
||||||
"modes": [m.value for m in Mode],
|
"modes": [m.value for m in Mode],
|
||||||
"mode_types": [t.value for t in ModeType],
|
"mode_types": [t.value for t in ModeType],
|
||||||
"sigs": SIGS,
|
"sigs": ACTIVITIES,
|
||||||
"spot_providers": spot_providers,
|
"spot_providers": spot_providers,
|
||||||
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
|
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
|
||||||
"alert_providers": alert_providers,
|
"alert_providers": alert_providers,
|
||||||
|
|||||||
@@ -197,24 +197,24 @@ def spot_allowed_by_query(spot, query):
|
|||||||
if not spot.source or spot.source not in sources:
|
if not spot.source or spot.source not in sources:
|
||||||
return False
|
return False
|
||||||
case "sig":
|
case "sig":
|
||||||
# If a list of sigs is provided, the spot must have a sig and it must match one of them.
|
# If a list of activities is provided, the spot must have an activity and it must match one of them.
|
||||||
# The special "sig" "NO_SIG", when supplied in the list, mathches spots with no sig.
|
# The special activity "NO_SIG", when supplied in the list, matches spots with no activity.
|
||||||
sigs = query.get(k).split(",")
|
activities = query.get(k).split(",")
|
||||||
include_no_sig = "NO_SIG" in sigs
|
include_no_activity = "NO_SIG" in activities
|
||||||
if not spot.sig and not include_no_sig:
|
if not spot.sig and not include_no_activity:
|
||||||
return False
|
return False
|
||||||
if spot.sig and spot.sig not in sigs:
|
if spot.sig and spot.sig not in activities:
|
||||||
return False
|
return False
|
||||||
case "needs_sig":
|
case "needs_sig":
|
||||||
# If true, a sig is required, regardless of what it is, it just can't be missing. Mutually
|
# If true, an activity is required, regardless of what it is, it just can't be missing. Mutually
|
||||||
# exclusive with supplying the special "NO_SIG" parameter to the "sig" query param.
|
# exclusive with supplying the special "NO_SIG" parameter to the "sig" query param.
|
||||||
needs_sig = query.get(k).upper() == "TRUE"
|
needs_activity = query.get(k).upper() == "TRUE"
|
||||||
if needs_sig and not spot.sig:
|
if needs_activity and not spot.sig:
|
||||||
return False
|
return False
|
||||||
case "needs_sig_ref":
|
case "needs_sig_ref":
|
||||||
# If true, at least one sig ref is required, regardless of what it is, it just can't be missing.
|
# If true, at least one activity ref is required, regardless of what it is, it just can't be missing.
|
||||||
needs_sig_ref = query.get(k).upper() == "TRUE"
|
needs_activity_ref = query.get(k).upper() == "TRUE"
|
||||||
if needs_sig_ref and (not spot.sig_refs or len(spot.sig_refs) == 0):
|
if needs_activity_ref and (not spot.sig_refs or len(spot.sig_refs) == 0):
|
||||||
return False
|
return False
|
||||||
case "band":
|
case "band":
|
||||||
bands = query.get(k).split(",")
|
bands = query.get(k).split(",")
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import tornado
|
|||||||
from tornado import httputil
|
from tornado import httputil
|
||||||
from tornado.web import Application
|
from tornado.web import Application
|
||||||
|
|
||||||
|
from core.activity_utils import get_ref_regex_for_activity
|
||||||
from core.config import ALLOW_SPOTTING
|
from core.config import ALLOW_SPOTTING
|
||||||
from core.constants import UNKNOWN_BAND
|
from core.constants import UNKNOWN_BAND
|
||||||
from core.sig_utils import get_ref_regex_for_sig
|
|
||||||
from core.utils import infer_band_from_freq, safe_json_dumps
|
from core.utils import infer_band_from_freq, safe_json_dumps
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
|
|
||||||
@@ -104,14 +104,14 @@ class V1APISpotHandler(tornado.web.RequestHandler):
|
|||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Reject if sig_ref format incorrect for sig
|
# Reject if activity ref format incorrect for activity
|
||||||
if (
|
if (
|
||||||
spot.sig
|
spot.sig
|
||||||
and spot.sig_refs
|
and spot.sig_refs
|
||||||
and len(spot.sig_refs) > 0
|
and len(spot.sig_refs) > 0
|
||||||
and spot.sig_refs[0].id
|
and spot.sig_refs[0].id
|
||||||
and get_ref_regex_for_sig(spot.sig)
|
and get_ref_regex_for_activity(spot.sig)
|
||||||
and not re.match(get_ref_regex_for_sig(spot.sig), spot.sig_refs[0].id)
|
and not re.match(get_ref_regex_for_activity(spot.sig), spot.sig_refs[0].id)
|
||||||
):
|
):
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(
|
self.write(
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ from webserver.handlers.api.addspot import APISpotHandler
|
|||||||
from webserver.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
from webserver.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
||||||
from webserver.handlers.api.dxstats import APIDxStatsHandler
|
from webserver.handlers.api.dxstats import APIDxStatsHandler
|
||||||
from webserver.handlers.api.lookups import (
|
from webserver.handlers.api.lookups import (
|
||||||
|
APILookupActivityRefHandler,
|
||||||
APILookupCallHandler,
|
APILookupCallHandler,
|
||||||
APILookupGridHandler,
|
APILookupGridHandler,
|
||||||
APILookupSIGRefHandler,
|
|
||||||
)
|
)
|
||||||
from webserver.handlers.api.options import APIOptionsHandler
|
from webserver.handlers.api.options import APIOptionsHandler
|
||||||
from webserver.handlers.api.solar_conditions import APISolarConditionsHandler
|
from webserver.handlers.api.solar_conditions import APISolarConditionsHandler
|
||||||
@@ -147,7 +147,7 @@ class WebServer:
|
|||||||
{"status_data": self._data_store.status.get()},
|
{"status_data": self._data_store.status.get()},
|
||||||
),
|
),
|
||||||
(r"/api/v2/lookup/call", APILookupCallHandler),
|
(r"/api/v2/lookup/call", APILookupCallHandler),
|
||||||
(r"/api/v2/lookup/sigref", APILookupSIGRefHandler),
|
(r"/api/v2/lookup/sigref", APILookupActivityRefHandler),
|
||||||
(r"/api/v2/lookup/grid", APILookupGridHandler),
|
(r"/api/v2/lookup/grid", APILookupGridHandler),
|
||||||
(
|
(
|
||||||
r"/api/v2/spot",
|
r"/api/v2/spot",
|
||||||
|
|||||||
Reference in New Issue
Block a user