mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 06:17:41 +00:00
Compare commits
6
Commits
55eaa871e9
...
9d117a6069
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d117a6069 | ||
|
|
606cf1be68 | ||
|
|
5a6fc19ab5 | ||
|
|
9258d680fc | ||
|
|
8189be699e | ||
|
|
fa2e4b929a |
+68
-83
@@ -1,4 +1,5 @@
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from core.enums import SIGType
|
||||
from data.band import Band
|
||||
from data.sig import SIG
|
||||
|
||||
@@ -15,301 +16,288 @@ SIGS = [
|
||||
name="POTA",
|
||||
comment_names=["POTA"],
|
||||
description="Parks on the Air",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z]{2}\-\d{4,5}|K\-TEST",
|
||||
icon="fa-tree",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="SOTA",
|
||||
comment_names=["SOTA"],
|
||||
description="Summits on the Air",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}",
|
||||
icon="fa-mountain-sun",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="WWFF",
|
||||
comment_names=["WWFF"],
|
||||
description="World Wide Flora & Fauna",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}",
|
||||
icon="fa-seedling",
|
||||
refs_globally_unique=True
|
||||
refs_globally_unique=True,
|
||||
),
|
||||
SIG(
|
||||
name="GMA",
|
||||
comment_names=["GMA"],
|
||||
description="Global Mountain Activity",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}",
|
||||
icon="fa-person-hiking",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="WWBOTA",
|
||||
comment_names=["WWBOTA", "BOTA"],
|
||||
description="Worldwide Bunkers on the Air",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}",
|
||||
icon="fa-radiation",
|
||||
refs_globally_unique=True
|
||||
refs_globally_unique=True,
|
||||
),
|
||||
SIG(
|
||||
name="HEMA",
|
||||
comment_names=["HEMA"],
|
||||
description="HuMPs Excluding Marilyns Award",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}",
|
||||
icon="fa-mound",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="IOTA",
|
||||
comment_names=["IOTA"],
|
||||
description="Islands on the Air",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z]{2}\-\d{3}",
|
||||
icon="fa-book-atlas",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="GMA Islands",
|
||||
comment_names=[],
|
||||
description="Global Mountain Activity - Islands",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"(([A-Z]{2}\-\d{3})|([A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}))",
|
||||
icon="fa-person-hiking",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="ARLHS",
|
||||
comment_names=["ARLHS"],
|
||||
description="Amateur Radio Lighthouse Society",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z]{3}[\- ]\d{3,4}",
|
||||
icon="fa-house-flood-water",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="ILLW",
|
||||
comment_names=["ILLW"],
|
||||
description="International Lighthouse & Lightship Weekend",
|
||||
sig_type=SIGType.EVENT,
|
||||
ref_regex=r"[A-Z]{2}\d{4}",
|
||||
icon="fa-house-flood-water",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="MOTA",
|
||||
comment_names=["MOTA"],
|
||||
description="Mills on the Air",
|
||||
sig_type=SIGType.EVENT,
|
||||
ref_regex=r"X\d{4,6}",
|
||||
icon="fa-fan",
|
||||
refs_globally_unique=True
|
||||
refs_globally_unique=True,
|
||||
),
|
||||
SIG(
|
||||
name="SIOTA",
|
||||
comment_names=["SIOTA"],
|
||||
description="Silos on the Air",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d",
|
||||
icon="fa-wheat-awn",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="WCA",
|
||||
comment_names=["WCA"],
|
||||
description="World Castles Award",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z0-9]{1,3}\-\d{5}",
|
||||
icon="fa-chess-rook",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="ZLOTA",
|
||||
comment_names=["ZLOTA"],
|
||||
description="New Zealand on the Air",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}",
|
||||
icon="fa-kiwi-bird",
|
||||
region_flag="🇳🇿",
|
||||
refs_globally_unique=True
|
||||
refs_globally_unique=True,
|
||||
),
|
||||
SIG(
|
||||
name="WOTA",
|
||||
comment_names=["WOTA"],
|
||||
description="Wainwrights on the Air",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"[A-Z]{3}-[0-9]{2}",
|
||||
icon="fa-w",
|
||||
region_flag="🇬🇧",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(name="BOTA",
|
||||
SIG(
|
||||
name="BOTA",
|
||||
comment_names=[],
|
||||
description="Beaches on the Air",
|
||||
icon="fa-umbrella-beach"
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
icon="fa-umbrella-beach",
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="KRMNPA",
|
||||
comment_names=["KRMNPA"],
|
||||
description="Keith Roget Memorial National Parks Award",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"VKFF\-\d{4}",
|
||||
icon="fa-earth-oceania",
|
||||
region_flag="🇦🇺"
|
||||
region_flag="🇦🇺",
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="SANPCPA",
|
||||
comment_names=["SANPCPA"],
|
||||
description="South Australian National Parks and Conservation Parks Award",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"VKFF\-\d{4}",
|
||||
icon="fa-earth-oceania",
|
||||
region_flag="🇦🇺"
|
||||
region_flag="🇦🇺",
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="LLOTA",
|
||||
comment_names=["LLOTA"],
|
||||
description="Lagos y Lagunas on the Air",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"LL[A-Z]{2}\-\d{4}",
|
||||
icon="fa-water",
|
||||
refs_globally_unique=True
|
||||
refs_globally_unique=True,
|
||||
),
|
||||
SIG(
|
||||
name="Towers",
|
||||
comment_names=["TOTA"],
|
||||
description="Towers on the Air",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Z]{2,3}R\-\d{4}",
|
||||
icon="fa-tower-observation",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="Tiles",
|
||||
comment_names=[],
|
||||
description="Tiles on the Air",
|
||||
sig_type=SIGType.WORLDWIDE,
|
||||
ref_regex=r"[A-Za-z]{2}[0-9]{2}[A-Za-z]{2}",
|
||||
icon="fa-square",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="WAB",
|
||||
comment_names=["WAB"],
|
||||
description="Worked All Britain",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"[A-Z]{1,2}[0-9]{2}",
|
||||
icon="fa-table-cells-large",
|
||||
region_flag="🇬🇧",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="WAI",
|
||||
comment_names=["WAI"],
|
||||
description="Worked All Ireland",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"[A-Z][0-9]{2}",
|
||||
icon="fa-table-cells-large",
|
||||
region_flag="🇮🇪",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="DME",
|
||||
comment_names=["DME"],
|
||||
description="Diploma Municipios de España",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"\d{4,5}",
|
||||
icon="fa-building",
|
||||
region_flag="🇪🇸",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="FEA",
|
||||
comment_names=["FEA"],
|
||||
description="Diploma Faros de España",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
# 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
|
||||
# forms to the database.
|
||||
ref_regex=r"([DE]|FEA)[\- ]\d{4}(\.\d)?",
|
||||
icon="fa-house-flood-water",
|
||||
region_flag="🇪🇸",
|
||||
refs_globally_unique=True
|
||||
refs_globally_unique=True,
|
||||
),
|
||||
SIG(
|
||||
name="DTMBA",
|
||||
comment_names=["DTMBA"],
|
||||
description="Diploma Teatri Musei e Belle Arti",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"I-?[0-9]{3,4}\s?[A-Z]{2}",
|
||||
icon="fa-masks-theater",
|
||||
region_flag="🇮🇹",
|
||||
refs_globally_unique=True
|
||||
refs_globally_unique=True,
|
||||
),
|
||||
SIG(
|
||||
name="BIWOTA",
|
||||
comment_names=["BIWOTA"],
|
||||
description="British Inland Waterways on the Air",
|
||||
sig_type=SIGType.EVENT,
|
||||
icon="fa-ship",
|
||||
region_flag="🇬🇧"
|
||||
region_flag="🇬🇧",
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="COTA",
|
||||
comment_names=["COTA"],
|
||||
description="Castles on the Air",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"[A-Z]{3}\-[0-9]{3,5}",
|
||||
icon="fa-chess-rook",
|
||||
region_flag="🇩🇪",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="PGA",
|
||||
comment_names=["PGA"],
|
||||
description="Polish Gmina Award",
|
||||
sig_type=SIGType.REGIONAL,
|
||||
ref_regex=r"[A-Z]{2}[0-9]{2}",
|
||||
icon="fa-g",
|
||||
region_flag="🇵🇱",
|
||||
refs_globally_unique=False
|
||||
refs_globally_unique=False,
|
||||
),
|
||||
SIG(
|
||||
name="Toilets",
|
||||
comment_names=[],
|
||||
description="Toilets on the Air",
|
||||
sig_type=SIGType.EVENT,
|
||||
ref_regex=r"T\-[0-9]{2}",
|
||||
icon="fa-toilet",
|
||||
region_flag="🏴☠️",
|
||||
refs_globally_unique=True
|
||||
refs_globally_unique=True,
|
||||
),
|
||||
]
|
||||
|
||||
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".
|
||||
CW_MODES = ["CW"]
|
||||
PHONE_MODES = [
|
||||
"PHONE",
|
||||
"SSB",
|
||||
"AM",
|
||||
"FM",
|
||||
"DV"
|
||||
]
|
||||
DATA_MODES = [
|
||||
"DATA",
|
||||
"FT8",
|
||||
"FT4",
|
||||
"RTTY",
|
||||
"SSTV",
|
||||
"JS8",
|
||||
"HELL",
|
||||
"PSK",
|
||||
"FSK",
|
||||
"OLIVIA",
|
||||
"PKT",
|
||||
"MSK144",
|
||||
]
|
||||
ALL_MODES = CW_MODES + PHONE_MODES + DATA_MODES
|
||||
MODE_TYPES = ["CW", "PHONE", "DATA"]
|
||||
|
||||
# Mode aliases. Sometimes we get spots with a mode described in a different way that is effectively the same as a mode
|
||||
# we already know, or we want to normalise things for consistency. The lookup table for this is here. Incoming spots
|
||||
# that match a key in this table will be converted to the corresponding value, so only the modes above will actually be
|
||||
# present in the spots.
|
||||
MODE_ALIASES = {
|
||||
"USB": "SSB",
|
||||
"LSB": "SSB",
|
||||
"DMR": "DV",
|
||||
"DSTAR": "DV",
|
||||
"C4FM": "DV",
|
||||
"FUSION": "DV",
|
||||
"M17": "DV",
|
||||
"RTT": "RTTY",
|
||||
"BPSK": "PSK",
|
||||
"PSK31": "PSK",
|
||||
"BPSK31": "PSK",
|
||||
"MFSK": "FSK",
|
||||
"MFSK32": "FSK",
|
||||
"DIGI": "DATA",
|
||||
"DIGITAL": "DATA",
|
||||
}
|
||||
|
||||
# Band definitions
|
||||
BANDS = [
|
||||
Band(name="2200m", start_freq=135700, end_freq=137800),
|
||||
@@ -325,7 +313,7 @@ BANDS = [
|
||||
Band(name="12m", start_freq=24890000, end_freq=24990000, is_ham_hf=True),
|
||||
Band(name="11m", start_freq=26965000, end_freq=27405000),
|
||||
Band(name="10m", start_freq=28000000, end_freq=29700000, is_ham_hf=True),
|
||||
Band(name="6m", start_freq=50000000, end_freq=54000000),
|
||||
Band(name="6m", start_freq=50000000, end_freq=54000000, is_ham_hf=True),
|
||||
Band(name="5m", start_freq=56000000, end_freq=60500000),
|
||||
Band(name="4m", start_freq=70000000, end_freq=70500000),
|
||||
Band(name="2m", start_freq=144000000, end_freq=148000000),
|
||||
@@ -341,9 +329,6 @@ BANDS = [
|
||||
]
|
||||
UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0)
|
||||
|
||||
# Continents
|
||||
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"]
|
||||
|
||||
# Propagation modes used in VHF/UHF DX cluster comments, e.g. "JN61ES<ES>JM56XT". I don't think there's an official list
|
||||
# of these anywhere, but here are some I've seen or seen reference to
|
||||
PROPAGATION_MODES = {
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Continent(str, Enum):
|
||||
EU = "EU"
|
||||
NA = "NA"
|
||||
SA = "SA"
|
||||
AS = "AS"
|
||||
AF = "AF"
|
||||
OC = "OC"
|
||||
AN = "AN"
|
||||
|
||||
|
||||
class Mode(str, Enum):
|
||||
CW = "CW"
|
||||
PHONE = "PHONE"
|
||||
SSB = "SSB"
|
||||
AM = "AM"
|
||||
FM = "FM"
|
||||
DV = "DV"
|
||||
DATA = "DATA"
|
||||
FT8 = "FT8"
|
||||
FT4 = "FT4"
|
||||
RTTY = "RTTY"
|
||||
SSTV = "SSTV"
|
||||
JS8 = "JS8"
|
||||
HELL = "HELL"
|
||||
PSK = "PSK"
|
||||
FSK = "FSK"
|
||||
OLIVIA = "OLIVIA"
|
||||
PKT = "PKT"
|
||||
MSK144 = "MSK144"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
@property
|
||||
def is_cw(self) -> bool:
|
||||
return self == Mode.CW
|
||||
|
||||
@property
|
||||
def is_phone(self) -> bool:
|
||||
return self in {Mode.PHONE, Mode.SSB, Mode.AM, Mode.FM, Mode.DV}
|
||||
|
||||
@property
|
||||
def is_data(self) -> bool:
|
||||
return not (self.is_cw or self.is_phone or self == Mode.UNKNOWN)
|
||||
|
||||
@staticmethod
|
||||
def from_name(name):
|
||||
"""Convert a string to an enum mode using the alias table."""
|
||||
|
||||
try:
|
||||
return Mode(name.upper())
|
||||
except ValueError:
|
||||
try:
|
||||
return Mode(MODE_ALIASES[name.upper()])
|
||||
except ValueError:
|
||||
return Mode.UNKNOWN
|
||||
|
||||
|
||||
class ModeType(str, Enum):
|
||||
PHONE = "PHONE"
|
||||
CW = "CW"
|
||||
DATA = "DATA"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
class ModeSource(str, Enum):
|
||||
"""Where the mode data came from in a spot."""
|
||||
|
||||
SPOT = "SPOT"
|
||||
COMMENT = "COMMENT"
|
||||
BANDPLAN = "BANDPLAN"
|
||||
NONE = "NONE"
|
||||
|
||||
|
||||
class LocationSourceForSpot(str, Enum):
|
||||
"""Where the location data came from in a spot."""
|
||||
|
||||
SPOT = "SPOT"
|
||||
SIG_REF_LOOKUP = "SIG REF LOOKUP"
|
||||
GRID = "GRID"
|
||||
HOME_QTH = "HOME QTH"
|
||||
DXCC = "DXCC"
|
||||
NONE = "NONE"
|
||||
|
||||
|
||||
class LocationSourceForCallsign(str, Enum):
|
||||
"""Where the location data came from in a callsign data object."""
|
||||
|
||||
HOME_QTH = "HOME QTH"
|
||||
DXCC = "DXCC"
|
||||
NONE = "NONE"
|
||||
|
||||
|
||||
class SIGRefType(str, Enum):
|
||||
"""Type of a Special Interest Group reference."""
|
||||
|
||||
PARK = "PARK"
|
||||
SUMMIT = "SUMMIT"
|
||||
CASTLE = "CASTLE"
|
||||
LIGHTHOUSE = "LIGHTHOUSE"
|
||||
ISLAND = "ISLAND"
|
||||
BUNKER = "BUNKER"
|
||||
MILL = "MILL"
|
||||
SILO = "SILO"
|
||||
BEACH = "BEACH"
|
||||
LAKE = "LAKE"
|
||||
TOWER = "TOWER"
|
||||
WATERWAY = "WATERWAY"
|
||||
TOWN = "TOWN"
|
||||
BUILDING = "BUILDING"
|
||||
REGION = "REGION"
|
||||
GRID = "GRID"
|
||||
TOILET = "TOILET"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
class SIGType(str, Enum):
|
||||
"""Type of a Special Interest Group. Used to group them in the web UI."""
|
||||
|
||||
WORLDWIDE = "WORLDWIDE"
|
||||
REGIONAL = "REGIONAL"
|
||||
EVENT = "EVENT"
|
||||
|
||||
|
||||
# Mode aliases. Sometimes we get spots with a mode described in a different way that is effectively the same as a mode
|
||||
# we already know, or we want to normalise things for consistency. The lookup table for this is here. Incoming spots
|
||||
# that match a key in this table will be converted to the corresponding value, so only the modes above will actually be
|
||||
# present in the spots.
|
||||
MODE_ALIASES = {
|
||||
"USB": "SSB",
|
||||
"LSB": "SSB",
|
||||
"DIGITALVOICE": "DV",
|
||||
"DIGITALVOI": "DV",
|
||||
"DMR": "DV",
|
||||
"DSTAR": "DV",
|
||||
"C4FM": "DV",
|
||||
"FUSION": "DV",
|
||||
"M17": "DV",
|
||||
"RTT": "RTTY",
|
||||
"BPSK": "PSK",
|
||||
"PSK31": "PSK",
|
||||
"BPSK31": "PSK",
|
||||
"MFSK": "FSK",
|
||||
"MFSK32": "FSK",
|
||||
"DIGI": "DATA",
|
||||
"DIGITAL": "DATA",
|
||||
}
|
||||
+30
-28
@@ -5,17 +5,10 @@ import simplejson
|
||||
from pyhamtools.frequency import freq_to_band
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from core.constants import (
|
||||
ALL_MODES,
|
||||
BANDS,
|
||||
CW_MODES,
|
||||
DATA_MODES,
|
||||
MODE_ALIASES,
|
||||
PHONE_MODES,
|
||||
UNKNOWN_BAND,
|
||||
)
|
||||
from core.constants import BANDS, UNKNOWN_BAND
|
||||
from core.data_store import DATA_STORE
|
||||
from data.callsign import Callsign
|
||||
from core.enums import MODE_ALIASES, Continent, Mode, ModeType
|
||||
from data.callsign import Callsign, LocationSourceForCallsign
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,31 +20,40 @@ def safe_json_dumps(obj):
|
||||
return simplejson.dumps(obj, ensure_ascii=False, ignore_nan=True, default=lambda o: o.__dict__)
|
||||
|
||||
|
||||
def infer_mode_from_comment(comment):
|
||||
def infer_mode_from_comment(comment: str) -> Mode:
|
||||
"""Infer a mode from the comment"""
|
||||
|
||||
for mode in ALL_MODES:
|
||||
if not comment:
|
||||
return Mode.UNKNOWN
|
||||
|
||||
for mode in Mode:
|
||||
if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE):
|
||||
return mode
|
||||
for mode in MODE_ALIASES:
|
||||
if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE):
|
||||
return MODE_ALIASES[mode]
|
||||
return None
|
||||
for alias in MODE_ALIASES:
|
||||
if re.search(r"(^|\W)" + alias + r"($|\W)", comment, re.IGNORECASE):
|
||||
return Mode(MODE_ALIASES[alias])
|
||||
|
||||
return Mode.UNKNOWN
|
||||
|
||||
|
||||
def infer_mode_type_from_mode(mode):
|
||||
"""Infer a "mode family" from a mode."""
|
||||
def infer_mode_type_from_mode(mode: str) -> ModeType:
|
||||
"""Infer a "mode family" from a mode ."""
|
||||
|
||||
if mode.upper() in CW_MODES:
|
||||
return "CW"
|
||||
elif mode.upper() in PHONE_MODES:
|
||||
return "PHONE"
|
||||
elif mode.upper() in DATA_MODES:
|
||||
return "DATA"
|
||||
else:
|
||||
if not mode:
|
||||
return ModeType.UNKNOWN
|
||||
|
||||
try:
|
||||
mode = Mode(mode.upper())
|
||||
if mode.is_cw:
|
||||
return ModeType.CW
|
||||
if mode.is_phone:
|
||||
return ModeType.PHONE
|
||||
return ModeType.DATA
|
||||
|
||||
except ValueError:
|
||||
if mode.upper() != "OTHER" and mode != "?":
|
||||
logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.")
|
||||
return None
|
||||
return ModeType.UNKNOWN
|
||||
|
||||
|
||||
def infer_band_from_freq(freq):
|
||||
@@ -116,7 +118,7 @@ def get_callsign_object_from_pyhamtools_callinfo(callsign, callinfo):
|
||||
|
||||
country = data.get("country", None)
|
||||
dxcc_id = data.get("adif", None)
|
||||
continent = data.get("continent", None)
|
||||
continent = Continent(data.get("continent", None))
|
||||
cq_zone = data.get("cqz", None)
|
||||
itu_zone = data.get("ituz", None)
|
||||
lat = float(data["latitude"]) if "latitude" in data else None
|
||||
@@ -136,7 +138,7 @@ def get_callsign_object_from_pyhamtools_callinfo(callsign, callinfo):
|
||||
latitude=lat,
|
||||
longitude=lon,
|
||||
grid=grid,
|
||||
location_source="DXCC",
|
||||
location_source=LocationSourceForCallsign.DXCC,
|
||||
)
|
||||
|
||||
except (KeyError, ValueError):
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
from core.call_lookup_helper import get_call_info
|
||||
from core.enums import Continent
|
||||
from core.sig_lookup_helper import populate_missing_sig_ref_info
|
||||
from core.utils import get_flag_for_dxcc
|
||||
|
||||
@@ -28,7 +29,7 @@ class Alert:
|
||||
# Country flag of the DX operator
|
||||
dx_flag: str | None = None
|
||||
# Continent of the DX operator
|
||||
dx_continent: str | None = None
|
||||
dx_continent: Continent | None = None
|
||||
# DXCC ID of the DX operator
|
||||
dx_dxcc_id: int | None = None
|
||||
# CQ zone of the DX operator
|
||||
@@ -92,7 +93,7 @@ class Alert:
|
||||
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
|
||||
self.dx_country = call_info.country
|
||||
if self.dx_calls and self.dx_calls[0] and not self.dx_continent:
|
||||
self.dx_continent = call_info.continent
|
||||
self.dx_continent = Continent(call_info.continent) if call_info.continent else None
|
||||
if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone:
|
||||
self.dx_cq_zone = call_info.cq_zone
|
||||
if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone:
|
||||
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from core.enums import Continent, LocationSourceForCallsign
|
||||
|
||||
|
||||
@dataclass
|
||||
class Callsign:
|
||||
@@ -28,15 +30,15 @@ class Callsign:
|
||||
# Country in which the callsign indicates they are operating
|
||||
country: str | None = None
|
||||
# Continent in which the callsign indicates they are operating
|
||||
continent: str | None = None
|
||||
continent: Continent | None = None
|
||||
# DXCC ID in which the callsign indicates they are operating
|
||||
dxcc_id: int | None = None
|
||||
# CQ zone in which the callsign indicates they are operating
|
||||
cq_zone: int | None = None
|
||||
# ITU zone in which the callsign indicates they are operating
|
||||
itu_zone: int | None = None
|
||||
# Location source. This can be "HOME QTH" or "DXCC" depending on which provider gave us a location
|
||||
location_source: str | None = None
|
||||
# Location source
|
||||
location_source: LocationSourceForCallsign = LocationSourceForCallsign.NONE
|
||||
|
||||
def fully_populated(self):
|
||||
"""Utility method to indicate that the callsign data is fully populated. Multiple providers can return data for
|
||||
|
||||
+9
-5
@@ -1,5 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from core.enums import SIGType
|
||||
|
||||
|
||||
@dataclass
|
||||
class SIG:
|
||||
@@ -15,16 +17,18 @@ class SIG:
|
||||
name: str
|
||||
# Description, e.g. "Towers on the Air"
|
||||
description: str
|
||||
# SIG names as they might appear in cluster spot comments, e.g. ["TOTA"]
|
||||
comment_names: list[str] = field(default_factory=list)
|
||||
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
|
||||
ref_regex: str | None = None
|
||||
# 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 = False
|
||||
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)
|
||||
# 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
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from core.enums import SIGRefType
|
||||
|
||||
|
||||
@dataclass
|
||||
class SIGRef:
|
||||
@@ -13,7 +15,7 @@ class SIGRef:
|
||||
# Name of the reference, e.g. "Null Country Park", if known.
|
||||
name: str | None = None
|
||||
# Type of the reference, e.g. "Park", if known.
|
||||
ref_type: str | None = None
|
||||
ref_type: SIGRefType = SIGRefType.UNKNOWN
|
||||
# URL to look up more information about the reference, if known.
|
||||
url: str | None = None
|
||||
# Latitude of the reference, in degrees, if known.
|
||||
|
||||
+33
-33
@@ -11,7 +11,8 @@ from pyhamtools.locator import latlong_to_locator, locator_to_latlong
|
||||
|
||||
from core.call_lookup_helper import get_call_info
|
||||
from core.config import MAX_SPOT_AGE
|
||||
from core.constants import MODE_ALIASES, PROPAGATION_MODES, SIGS
|
||||
from core.constants import PROPAGATION_MODES, SIGS
|
||||
from core.enums import Continent, LocationSourceForSpot, Mode, ModeSource, ModeType
|
||||
from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone
|
||||
from core.sig_lookup_helper import populate_missing_sig_ref_info
|
||||
from core.sig_utils import (
|
||||
@@ -52,7 +53,7 @@ class Spot:
|
||||
# Country flag of the DX operator
|
||||
dx_flag: str | None = None
|
||||
# Continent of the DX operator
|
||||
dx_continent: str | None = None
|
||||
dx_continent: Continent | None = None
|
||||
# DXCC ID of the DX operator
|
||||
dx_dxcc_id: int | None = None
|
||||
# CQ zone of the DX operator
|
||||
@@ -68,9 +69,8 @@ class Spot:
|
||||
# lookup
|
||||
dx_latitude: float | None = None
|
||||
dx_longitude: float | None = None
|
||||
# DX Location source. Indicates how accurate the location might be. Values: "SPOT", "GRID", "SIG REF LOOKUP",
|
||||
# "HOME QTH", "DXCC", "NONE"
|
||||
dx_location_source: str = "NONE"
|
||||
# DX Location source. Indicates how accurate the location might be.
|
||||
dx_location_source: LocationSourceForSpot = LocationSourceForSpot.NONE
|
||||
# DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is
|
||||
# true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and the
|
||||
# DX callsign doesn't have a suffix like /P.
|
||||
@@ -85,7 +85,7 @@ class Spot:
|
||||
# Country flag of the spotter
|
||||
de_flag: str | None = None
|
||||
# Continent of the spotter
|
||||
de_continent: str | None = None
|
||||
de_continent: Continent | None = None
|
||||
# DXCC ID of the spotter
|
||||
de_dxcc_id: int | None = None
|
||||
# If this is an APRS/Packet/etc spot, what SSID was the spotter/receiver using?
|
||||
@@ -103,11 +103,11 @@ class Spot:
|
||||
# General QSO info
|
||||
|
||||
# Reported mode, such as SSB, PHONE, CW, FT8...
|
||||
mode: str | None = None
|
||||
# Inferred mode "family". One of "CW", "PHONE" or "DIGI".
|
||||
mode_type: str | None = None
|
||||
# Source of the mode information. "SPOT", "COMMENT", "BANDPLAN" or "NONE"
|
||||
mode_source: str = "NONE"
|
||||
mode: Mode = Mode.UNKNOWN
|
||||
# Inferred mode "family".
|
||||
mode_type: ModeType = ModeType.UNKNOWN
|
||||
# Source of the mode information.
|
||||
mode_source: ModeSource = ModeSource.NONE
|
||||
# Frequency, in Hz
|
||||
freq: float | None = None
|
||||
# Band, defined by the frequency, e.g. "40m" or "70cm"
|
||||
@@ -185,7 +185,7 @@ class Spot:
|
||||
if self.dx_call and not self.dx_country:
|
||||
self.dx_country = dx_call_info.country
|
||||
if self.dx_call and not self.dx_continent:
|
||||
self.dx_continent = dx_call_info.continent
|
||||
self.dx_continent = Continent(dx_call_info.continent)
|
||||
if self.dx_call and not self.dx_dxcc_id:
|
||||
self.dx_dxcc_id = dx_call_info.dxcc_id
|
||||
if self.dx_dxcc_id and not self.dx_flag:
|
||||
@@ -223,7 +223,7 @@ class Spot:
|
||||
if not self.de_country:
|
||||
self.de_country = de_call_info.country
|
||||
if not self.de_continent:
|
||||
self.de_continent = de_call_info.continent
|
||||
self.de_continent = Continent(de_call_info.continent)
|
||||
if not self.de_dxcc_id:
|
||||
self.de_dxcc_id = de_call_info.dxcc_id
|
||||
if self.de_dxcc_id and not self.de_flag:
|
||||
@@ -239,26 +239,26 @@ class Spot:
|
||||
self.band = band.name
|
||||
|
||||
# Mode from comments or bandplan
|
||||
if self.mode:
|
||||
self.mode_source = "SPOT"
|
||||
if self.comment and not self.mode:
|
||||
if not self.mode:
|
||||
self.mode = Mode.UNKNOWN
|
||||
if self.mode != Mode.UNKNOWN:
|
||||
self.mode_source = ModeSource.SPOT
|
||||
if self.comment and self.mode == Mode.UNKNOWN:
|
||||
self.mode = infer_mode_from_comment(self.comment)
|
||||
self.mode_source = "COMMENT"
|
||||
if self.freq and not self.mode:
|
||||
self.mode_source = ModeSource.COMMENT
|
||||
if self.freq and self.mode == Mode.UNKNOWN:
|
||||
self.mode = infer_mode_from_frequency(self.freq)
|
||||
self.mode_source = "BANDPLAN"
|
||||
|
||||
# Normalise mode if necessary.
|
||||
if self.mode in MODE_ALIASES:
|
||||
self.mode = MODE_ALIASES[self.mode]
|
||||
self.mode_source = ModeSource.BANDPLAN
|
||||
|
||||
# Mode type from mode
|
||||
if self.mode and not self.mode_type:
|
||||
if not self.mode_type:
|
||||
self.mode_type = ModeType.UNKNOWN
|
||||
if self.mode != Mode.UNKNOWN and self.mode_type == ModeType.UNKNOWN:
|
||||
self.mode_type = infer_mode_type_from_mode(self.mode)
|
||||
|
||||
# If we have a latitude or grid at this point, it can only have been provided by the spot itself
|
||||
if self.dx_latitude or self.dx_grid:
|
||||
self.dx_location_source = "SPOT"
|
||||
self.dx_location_source = LocationSourceForSpot.SPOT
|
||||
|
||||
# Set the top-level "SIG" if it is missing but we have at least one SIG ref.
|
||||
if not self.sig and self.sig_refs and len(self.sig_refs) > 0:
|
||||
@@ -324,9 +324,9 @@ class Spot:
|
||||
self.dx_latitude = sig_ref.latitude
|
||||
self.dx_longitude = sig_ref.longitude
|
||||
if self.sig == "WAB" or self.sig == "WAI" or self.sig == "Tiles":
|
||||
self.dx_location_source = "GRID"
|
||||
self.dx_location_source = LocationSourceForSpot.GRID
|
||||
else:
|
||||
self.dx_location_source = "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
|
||||
# and apply it to the whole spot.
|
||||
@@ -347,7 +347,7 @@ class Spot:
|
||||
self.de_grid = grid_mode_grid_match.group(1).upper()
|
||||
if not self.dx_grid:
|
||||
self.dx_grid = grid_mode_grid_match.group(4).upper()
|
||||
self.dx_location_source = "GRID"
|
||||
self.dx_location_source = LocationSourceForSpot.GRID
|
||||
|
||||
# And extract propagation mode (group 2 for <...>, group 3 for (...)):
|
||||
mode_tag = (grid_mode_grid_match.group(2) or grid_mode_grid_match.group(3) or "").upper()
|
||||
@@ -368,7 +368,7 @@ class Spot:
|
||||
# regex matches, so extract grids:
|
||||
if not self.dx_grid:
|
||||
self.dx_grid = grid_mode_grid_match.group(1).upper()
|
||||
self.dx_location_source = "GRID"
|
||||
self.dx_location_source = LocationSourceForSpot.GRID
|
||||
if not self.de_grid:
|
||||
self.de_grid = grid_mode_grid_match.group(2).upper()
|
||||
|
||||
@@ -440,10 +440,10 @@ class Spot:
|
||||
self.dx_latitude
|
||||
and self.dx_longitude
|
||||
and (
|
||||
self.dx_location_source == "SPOT"
|
||||
or self.dx_location_source == "SIG REF LOOKUP"
|
||||
or self.dx_location_source == "GRID"
|
||||
or (self.dx_location_source == "HOME QTH" and "/" not in (self.dx_call or ""))
|
||||
self.dx_location_source == LocationSourceForSpot.SPOT
|
||||
or self.dx_location_source == LocationSourceForSpot.SIG_REF_LOOKUP
|
||||
or self.dx_location_source == LocationSourceForSpot.GRID
|
||||
or (self.dx_location_source == LocationSourceForSpot.HOME_QTH and "/" not in (self.dx_call or ""))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+12
-11
@@ -23,15 +23,16 @@ You can replace `#main` with any other branch or tag reference, for example `#1.
|
||||
|
||||
Save the file. You will still need to create a copy of `config-example.yml` and name it `config.yml`, though with the
|
||||
Docker setup nothing has actually been downloaded yet, so you will have to copy the example from the repository some
|
||||
other way, e.g. [from the repo in a web browser](https://git.ianrenton.com/ian/spothole/src/branch/main/config-example.yml).
|
||||
other way,
|
||||
e.g. [from the repo in a web browser](https://git.ianrenton.com/ian/spothole/src/branch/main/config-example.yml).
|
||||
|
||||
With that in place, run `docker compose up` and you should be good to go. To detach, press `d` or run the command with
|
||||
the `-d` flag.
|
||||
|
||||
### nginx Reverse Proxy with Docker
|
||||
|
||||
In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal
|
||||
of HTTPS certificates, and then applications like Spothole in a separate container. In this case, there are a couple of
|
||||
In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal of
|
||||
HTTPS certificates, and then applications like Spothole in a separate container. In this case, there are a couple of
|
||||
variations of the docker compose file above, and the nginx reverse proxy configuration covered [here](./nginx.md), that
|
||||
you will want to make.
|
||||
|
||||
@@ -60,8 +61,8 @@ networks:
|
||||
```
|
||||
|
||||
In your nginx site configuration, you'll want to refer to the Spothole container directly, and drop the block that
|
||||
allows nginx to access static files directly, as these will be inaccessible in another container. So you may end up
|
||||
with something like:
|
||||
allows nginx to access static files directly, as these will be inaccessible in another container. So you may end up with
|
||||
something like:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -146,13 +147,13 @@ server {
|
||||
```
|
||||
|
||||
If desired, you could even change the port on which Spothole runs from 8080 to a plain 80, in which case your
|
||||
`proxy_pass` statements could drop the `:8080` suffix. Since Spothole is in a container, it can serve HTTP on port 80
|
||||
if desired, because it doesn't conflict with the host system.
|
||||
`proxy_pass` statements could drop the `:8080` suffix. Since Spothole is in a container, it can serve HTTP on port 80 if
|
||||
desired, because it doesn't conflict with the host system.
|
||||
|
||||
### Restoring the static files bypass
|
||||
|
||||
If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can
|
||||
do. The easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes:
|
||||
If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can do. The
|
||||
easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -183,8 +184,8 @@ networks:
|
||||
external: true
|
||||
```
|
||||
|
||||
Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time
|
||||
point it at the new container rather than at a filesystem path:
|
||||
Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time point it
|
||||
at the new container rather than at a filesystem path:
|
||||
|
||||
```nginx
|
||||
# Load static assets from the spothole-static-nginx container
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ To navigate your way around the source code, this list may help.
|
||||
### Extending the server
|
||||
|
||||
Spothole is designed to be easily extensible. If you want to write your own spot provider, for example, simply add a
|
||||
module to the `providers.spot` package containing your class. (Currently, in order to be loaded correctly, the module (
|
||||
file) name should be the same as the class name, but lower case.)
|
||||
module to the `providers.spot` package containing your class. (Currently, in order to be loaded correctly, the module
|
||||
(file) name should be the same as the class name, but lower case.)
|
||||
|
||||
Your class should extend "SpotProvider"; if it operates by polling an HTTP Server on a timer, it can instead extend "
|
||||
HTTPSpotProvider" where some of the work is done for you.
|
||||
|
||||
+10
-15
@@ -2,8 +2,8 @@
|
||||
|
||||
Web servers generally serve their pages from port 80. However, it's best not to serve Spothole's web interface directly
|
||||
on port 80, as that requires root privileges on a Linux system. It also and prevents us using HTTPS to serve a secure
|
||||
site, since Spothole itself doesn't directly support acting as an HTTPS server. The normal solution to this is to use
|
||||
a "reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then
|
||||
site, since Spothole itself doesn't directly support acting as an HTTPS server. The normal solution to this is to use a
|
||||
"reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then
|
||||
passes on the request to the back-end application (in this case Spothole). nginx is a common choice for this general web
|
||||
server.
|
||||
|
||||
@@ -89,19 +89,14 @@ server {
|
||||
```
|
||||
|
||||
One further change you might want to make to the file above is the `add_header Access-Control-Allow-Origin` statements.
|
||||
These are what's used on
|
||||
my own Spothole server to make sure that other third-party web-based software can get the data from my instance, and
|
||||
applies to any endpoint underneath `/api`. If you want
|
||||
*your* Spothole instance to be set up the same way, so that others can write software in JavaScript that can access it,
|
||||
leave this intact. But if you want your Spothole instance to only be usable by scripts running on the web server you
|
||||
write,
|
||||
you can remove these lines. (Note that this doesn't stop other people writing *non-web-based* software that accesses
|
||||
your
|
||||
Spothole API—the enforcement of cross-origin headers only happens within the user's browser. If you need to lock
|
||||
your
|
||||
instance down so that no-one else can access it with *any* software, that's an aspect of nginx or firewall config that
|
||||
you will need
|
||||
to find help with elsewhere.)
|
||||
These are what's used on my own Spothole server to make sure that other third-party web-based software can get the data
|
||||
from my instance, and applies to any endpoint underneath `/api`. If you want *your* Spothole instance to be set up the
|
||||
same way, so that others can write software in JavaScript that can access it, leave this intact. But if you want your
|
||||
Spothole instance to only be usable by scripts running on the web server you write, you can remove these lines. (Note
|
||||
that this doesn't stop other people writing *non-web-based* software that accesses your Spothole API—the
|
||||
enforcement of cross-origin headers only happens within the user's browser. If you need to lock your instance down so
|
||||
that no-one else can access it with *any* software, that's an aspect of nginx or firewall config that you will need to
|
||||
find help with elsewhere.)
|
||||
|
||||
Now, make a symbolic link to enable the site:
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ from requests_cache import CachedSession
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from core.constants import HTTP_HEADERS, SOFTWARE_VERSION
|
||||
from core.data_store import CACHE_DIR, DATA_STORE
|
||||
from core.enums import Continent
|
||||
from core.url_data_cache import URLDataCache
|
||||
from data.callsign import Callsign
|
||||
from data.callsign import Callsign, LocationSourceForCallsign
|
||||
from providers.callsigndata.api_query_callsign_data_provider import (
|
||||
APIQueryCallsignDataProvider,
|
||||
)
|
||||
@@ -142,12 +143,12 @@ class HamQTH(APIQueryCallsignDataProvider):
|
||||
name=data.get("nick", None),
|
||||
qth=data.get("qth", None),
|
||||
country=data.get("country", None),
|
||||
continent=data.get("continent", None),
|
||||
continent=Continent(data.get("continent", None)),
|
||||
latitude=lat,
|
||||
longitude=lon,
|
||||
grid=grid,
|
||||
dxcc_id=int(data["adif"]) if "adif" in data else None,
|
||||
cq_zone=int(data["cq"]) if "cq" in data else None,
|
||||
itu_zone=int(data["itu"]) if "itu" in data else None,
|
||||
location_source="HOME QTH",
|
||||
location_source=LocationSourceForCallsign.HOME_QTH,
|
||||
)
|
||||
|
||||
@@ -10,11 +10,10 @@ from requests_cache import CachedSession
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from core.data_store import CACHE_DIR, DATA_STORE
|
||||
from core.enums import Continent, LocationSourceForCallsign
|
||||
from core.url_data_cache import URLDataCache
|
||||
from data.callsign import Callsign
|
||||
from providers.callsigndata.api_query_callsign_data_provider import (
|
||||
APIQueryCallsignDataProvider,
|
||||
)
|
||||
from providers.callsigndata.api_query_callsign_data_provider import APIQueryCallsignDataProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -170,12 +169,12 @@ class QRZ(APIQueryCallsignDataProvider):
|
||||
name=name,
|
||||
qth=data.get("addr2", None),
|
||||
country=data.get("country", None),
|
||||
continent=data.get("continent", None),
|
||||
continent=Continent(data.get("continent", None)),
|
||||
latitude=lat,
|
||||
longitude=lon,
|
||||
grid=grid,
|
||||
dxcc_id=int(data["adif"]) if "adif" in data else None,
|
||||
cq_zone=int(data["cqzone"]) if "cqzone" in data else None,
|
||||
itu_zone=int(data["ituzone"]) if "ituzone" in data else None,
|
||||
location_source="HOME QTH",
|
||||
location_source=LocationSourceForCallsign.HOME_QTH,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -27,7 +28,7 @@ class ARLHS(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type="Lighthouse",
|
||||
ref_type=SIGRefType.LIGHTHOUSE,
|
||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||
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,
|
||||
|
||||
@@ -2,6 +2,7 @@ from time import sleep
|
||||
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
@@ -32,7 +33,7 @@ class COTA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=name,
|
||||
ref_type="Castle",
|
||||
ref_type=SIGRefType.CASTLE,
|
||||
grid=grid,
|
||||
latitude=lat,
|
||||
longitude=lon,
|
||||
|
||||
@@ -3,6 +3,7 @@ from time import sleep
|
||||
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.local_file_sig_ref_data_provider import (
|
||||
LocalFileSIGRefDataProvider,
|
||||
@@ -37,7 +38,7 @@ class DME(LocalFileSIGRefDataProvider):
|
||||
ref = SIGRef(
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
ref_type="Town",
|
||||
ref_type=SIGRefType.TOWN,
|
||||
name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}",
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
@@ -20,14 +21,7 @@ class DTMBA(FileDownloadSIGRefDataProvider):
|
||||
split = row.split(";")
|
||||
ref_id = split[0]
|
||||
ref_name = split[1]
|
||||
new_data.append(
|
||||
SIGRef(
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=ref_name,
|
||||
ref_type="Building"
|
||||
)
|
||||
)
|
||||
new_data.append(SIGRef(sig=self.SIG, id=ref_id, name=ref_name, ref_type=SIGRefType.BUILDING))
|
||||
|
||||
# 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
|
||||
|
||||
@@ -3,6 +3,7 @@ from time import sleep
|
||||
|
||||
import pdfplumber
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
@@ -40,8 +41,8 @@ class FEA(FileDownloadSIGRefDataProvider):
|
||||
# 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_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="Lighthouse"))
|
||||
new_data.append(SIGRef(sig=self.SIG, id=ref_id_2, name=row[1].strip(), ref_type="Lighthouse"))
|
||||
new_data.append(SIGRef(sig=self.SIG, id=ref_id_1, name=row[1].strip(), ref_type=SIGRefType.LIGHTHOUSE))
|
||||
new_data.append(SIGRef(sig=self.SIG, id=ref_id_2, name=row[1].strip(), ref_type=SIGRefType.LIGHTHOUSE))
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -26,7 +27,7 @@ class GMA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type="Summit",
|
||||
ref_type=SIGRefType.SUMMIT,
|
||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||
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,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -27,7 +28,7 @@ class ILLW(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type="Lighthouse",
|
||||
ref_type=SIGRefType.LIGHTHOUSE,
|
||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||
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,
|
||||
|
||||
@@ -3,6 +3,7 @@ from time import sleep
|
||||
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -44,7 +45,7 @@ class IOTA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=ref["name"],
|
||||
ref_type="Island",
|
||||
ref_type=SIGRefType.ISLAND,
|
||||
grid=grid,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
|
||||
@@ -2,6 +2,7 @@ from time import sleep
|
||||
|
||||
from pyhamtools.locator import locator_to_latlong
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -32,7 +33,7 @@ class LLOTA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=str(ref["name"]),
|
||||
ref_type="Lake",
|
||||
ref_type=SIGRefType.LAKE,
|
||||
url=f"https://llota.app/list/ref/{ref_id}",
|
||||
grid=grid,
|
||||
latitude=ll[0],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -26,7 +27,7 @@ class MOTA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type="Mill",
|
||||
ref_type=SIGRefType.MILL,
|
||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||
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,
|
||||
|
||||
@@ -2,6 +2,7 @@ from time import sleep
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
@@ -41,7 +42,7 @@ class PGA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=name,
|
||||
ref_type="Region",
|
||||
ref_type=SIGRefType.REGION,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from time import sleep
|
||||
from fastkml import kml
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -43,7 +44,7 @@ class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider):
|
||||
sig=self.sig_name,
|
||||
id=ref_id,
|
||||
name=placemark.name,
|
||||
ref_type="Park",
|
||||
ref_type=SIGRefType.PARK,
|
||||
url=f"https://parksnpeaks.org/getPark.php?actPark={ref_id}",
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -26,7 +27,7 @@ class POTA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("name", None),
|
||||
ref_type="Park",
|
||||
ref_type=SIGRefType.PARK,
|
||||
url=f"https://pota.app/#/park/{ref_id}",
|
||||
grid=row.get("grid", None),
|
||||
latitude=float(row["latitude"]) if "latitude" in row and row["latitude"] != "" else None,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -26,7 +27,7 @@ class SIOTA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("NAME", None),
|
||||
ref_type="Silo",
|
||||
ref_type=SIGRefType.SILO,
|
||||
grid=row.get("LOCATOR", None),
|
||||
latitude=float(row["LAT"]) if "LAT" in row else None,
|
||||
longitude=float(row["LNG"]) if "LNG" in row else None,
|
||||
|
||||
@@ -3,6 +3,7 @@ from time import sleep
|
||||
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -30,7 +31,7 @@ class SOTA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("SummitName", None),
|
||||
ref_type="Summit",
|
||||
ref_type=SIGRefType.SUMMIT,
|
||||
url=f"https://www.sotadata.org.uk/en/summit/{ref_id}",
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import csv
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.local_file_sig_ref_data_provider import (
|
||||
LocalFileSIGRefDataProvider,
|
||||
@@ -26,7 +27,7 @@ class Toilets(LocalFileSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=row["ref"],
|
||||
name=row["ref"],
|
||||
ref_type="Toilet",
|
||||
ref_type=SIGRefType.TOILET,
|
||||
latitude=float(row["lat"]),
|
||||
longitude=float(row["lon"]),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -26,7 +27,7 @@ class Towers(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("Nazev", None),
|
||||
ref_type="Tower",
|
||||
ref_type=SIGRefType.TOWER,
|
||||
url=f"https://wwtota.com/seznam/karta_rozhledny.php?ref={ref_id}",
|
||||
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,
|
||||
|
||||
@@ -4,6 +4,7 @@ from time import sleep
|
||||
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -45,7 +46,7 @@ class WCA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("CLEAN NAME", None),
|
||||
ref_type="Castle",
|
||||
ref_type=SIGRefType.CASTLE,
|
||||
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -33,7 +34,7 @@ class WOTA(FileDownloadSIGRefDataProvider):
|
||||
id=ref_id,
|
||||
name=feature["properties"]["title"],
|
||||
url=url,
|
||||
ref_type="Summit",
|
||||
ref_type=SIGRefType.SUMMIT,
|
||||
grid=feature["properties"]["qthLocator"],
|
||||
latitude=feature["geometry"]["coordinates"][1],
|
||||
longitude=feature["geometry"]["coordinates"][0],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -26,7 +27,7 @@ class WWBOTA(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type="Bunker",
|
||||
ref_type=SIGRefType.BUNKER,
|
||||
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,
|
||||
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import csv
|
||||
from time import sleep
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -26,7 +27,7 @@ class WWFF(FileDownloadSIGRefDataProvider):
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=row.get("name", None),
|
||||
ref_type="Park",
|
||||
ref_type=SIGRefType.PARK,
|
||||
url=f"https://wwff.co/directory/?showRef={ref_id}",
|
||||
grid=row["iaruLocator"] if "iaruLocator" in row and row["iaruLocator"] != "-" else None,
|
||||
latitude=float(row["latitude"])
|
||||
|
||||
@@ -2,6 +2,7 @@ from time import sleep
|
||||
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from providers.sigrefdata.file_download_sig_ref_data_provider import (
|
||||
FileDownloadSIGRefDataProvider,
|
||||
@@ -26,12 +27,16 @@ class ZLOTA(FileDownloadSIGRefDataProvider):
|
||||
ref_id = ref["code"]
|
||||
latitude = ref["latitude"]
|
||||
longitude = ref["longitude"]
|
||||
try:
|
||||
ref_type = SIGRefType(ref["asset_type"].title().upper())
|
||||
except ValueError:
|
||||
ref_type = SIGRefType.UNKNOWN
|
||||
|
||||
new_ref = SIGRef(
|
||||
sig=self.SIG,
|
||||
id=ref_id,
|
||||
name=ref["name"],
|
||||
ref_type=ref["asset_type"].title(),
|
||||
ref_type=ref_type,
|
||||
url=f"https://ontheair.nz/assets/{ref_id.replace('/', '_')}",
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
|
||||
+11
-1
@@ -4,6 +4,7 @@ from datetime import datetime
|
||||
import pytz
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from core.enums import Mode, SIGRefType
|
||||
from core.url_data_cache import URLDataCache
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
@@ -65,7 +66,9 @@ class GMA(HTTPSpotProvider):
|
||||
if (source_spot["QRG"] != "" and source_spot["QRG"] != "QRT")
|
||||
else None,
|
||||
# Filter out some weird mode strings
|
||||
mode=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 Mode.UNKNOWN,
|
||||
comment=source_spot["TEXT"],
|
||||
sig_refs=[
|
||||
SIGRef(
|
||||
@@ -113,24 +116,31 @@ class GMA(HTTPSpotProvider):
|
||||
match ref_info["reftype"]:
|
||||
case "Summit":
|
||||
spot.sig_refs[0].sig = "GMA"
|
||||
spot.sig_refs[0].ref_type = SIGRefType.SUMMIT
|
||||
spot.sig = "GMA"
|
||||
case "IOTA Island":
|
||||
spot.sig_refs[0].sig = "IOTA"
|
||||
spot.sig_refs[0].ref_type = SIGRefType.ISLAND
|
||||
spot.sig = "IOTA"
|
||||
case "GMA Island":
|
||||
spot.sig_refs[0].sig = "GMA Islands"
|
||||
spot.sig_refs[0].ref_type = SIGRefType.ISLAND
|
||||
spot.sig = "GMA Islands"
|
||||
case "Lighthouse (ILLW)":
|
||||
spot.sig_refs[0].sig = "ILLW"
|
||||
spot.sig_refs[0].ref_type = SIGRefType.LIGHTHOUSE
|
||||
spot.sig = "ILLW"
|
||||
case "Lighthouse (ARLHS)":
|
||||
spot.sig_refs[0].sig = "ARLHS"
|
||||
spot.sig_refs[0].ref_type = SIGRefType.LIGHTHOUSE
|
||||
spot.sig = "ARLHS"
|
||||
case "Castle":
|
||||
spot.sig_refs[0].sig = "WCA"
|
||||
spot.sig_refs[0].ref_type = SIGRefType.CASTLE
|
||||
spot.sig = "WCA"
|
||||
case "Mill":
|
||||
spot.sig_refs[0].sig = "MOTA"
|
||||
spot.sig_refs[0].ref_type = SIGRefType.MILL
|
||||
spot.sig = "MOTA"
|
||||
case _:
|
||||
logger.warning(
|
||||
|
||||
@@ -7,6 +7,7 @@ import requests
|
||||
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from core.enums import Mode, SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -59,7 +60,7 @@ class HEMA(HTTPSpotProvider):
|
||||
dx_call=spot_items[2].upper(),
|
||||
de_call=spotter_comment_match.group(1).upper(),
|
||||
freq=float(freq_mode_match.group(1)) * 1000000,
|
||||
mode=freq_mode_match.group(2).upper(),
|
||||
mode=Mode.from_name(freq_mode_match.group(2).upper()),
|
||||
comment=spotter_comment_match.group(2),
|
||||
sig="HEMA",
|
||||
sig_refs=[
|
||||
@@ -69,6 +70,7 @@ class HEMA(HTTPSpotProvider):
|
||||
name=spot_items[4],
|
||||
latitude=float(spot_items[7]),
|
||||
longitude=float(spot_items[8]),
|
||||
ref_type=SIGRefType.SUMMIT,
|
||||
)
|
||||
],
|
||||
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from core.enums import Mode, SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -31,7 +32,7 @@ class LLOTA(HTTPSpotProvider):
|
||||
dx_call=source_spot["callsign"].upper(),
|
||||
de_call=spotter.upper() if spotter else None,
|
||||
freq=float(source_spot["frequency"]) * 1000000,
|
||||
mode=source_spot["mode"].upper(),
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=comment,
|
||||
sig="LLOTA",
|
||||
sig_refs=[
|
||||
@@ -39,6 +40,7 @@ class LLOTA(HTTPSpotProvider):
|
||||
id=source_spot["reference"],
|
||||
sig="LLOTA",
|
||||
name=source_spot["reference_name"],
|
||||
ref_type=SIGRefType.LAKE,
|
||||
)
|
||||
],
|
||||
time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytz
|
||||
import requests
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from core.enums import Mode
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -51,7 +52,7 @@ class ParksNPeaks(HTTPSpotProvider):
|
||||
if (source_spot["actFreq"] != "")
|
||||
else None,
|
||||
# Seen PNP spots with empty frequency, and with comma-separated thousands digits
|
||||
mode=source_spot["actMode"].upper(),
|
||||
mode=Mode.from_name(source_spot["actMode"].upper()),
|
||||
comment=source_spot["actComments"],
|
||||
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S")
|
||||
.replace(tzinfo=pytz.UTC)
|
||||
@@ -86,6 +87,7 @@ class ParksNPeaks(HTTPSpotProvider):
|
||||
"POTA",
|
||||
"SOTA",
|
||||
"WWFF",
|
||||
"HEMA",
|
||||
"SIOTA",
|
||||
"ZLOTA",
|
||||
"KRMNPA",
|
||||
|
||||
@@ -4,6 +4,7 @@ import pytz
|
||||
import requests
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from core.enums import Mode, SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -29,8 +30,8 @@ class POTA(HTTPSpotProvider):
|
||||
source_id=source_spot["spotId"],
|
||||
dx_call=source_spot["activator"].upper(),
|
||||
de_call=source_spot["spotter"].upper(),
|
||||
freq=float(source_spot["frequency"]) * 1000,
|
||||
mode=source_spot["mode"].upper(),
|
||||
freq=float(source_spot["frequency"]) * 1000 if source_spot["frequency"] != "INVALID" else None,
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=source_spot["comments"],
|
||||
sig="POTA",
|
||||
sig_refs=[
|
||||
@@ -40,6 +41,7 @@ class POTA(HTTPSpotProvider):
|
||||
name=source_spot["name"],
|
||||
latitude=source_spot["latitude"],
|
||||
longitude=source_spot["longitude"],
|
||||
ref_type=SIGRefType.PARK
|
||||
)
|
||||
],
|
||||
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
@@ -5,6 +5,7 @@ import requests
|
||||
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from core.enums import Mode, SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -53,7 +54,7 @@ class SOTA(HTTPSpotProvider):
|
||||
if (source_spot["frequency"] is not None)
|
||||
else None,
|
||||
# Seen SOTA spots with no frequency!
|
||||
mode=source_spot["mode"].upper(),
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=source_spot["comments"],
|
||||
sig="SOTA",
|
||||
sig_refs=[
|
||||
@@ -64,6 +65,7 @@ class SOTA(HTTPSpotProvider):
|
||||
latitude=source_spot["latitude"],
|
||||
longitude=source_spot["longitude"],
|
||||
activation_score=source_spot["points"],
|
||||
ref_type=SIGRefType.SUMMIT,
|
||||
)
|
||||
],
|
||||
dx_latitude=source_spot["latitude"],
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import datetime
|
||||
import requests
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from core.enums import LocationSourceForSpot, Mode, SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -55,7 +56,7 @@ class Tiles(HTTPSpotProvider):
|
||||
# No separate spotter callsign, assume all spots are self-spots
|
||||
de_call=source_spot["call_sign"].upper(),
|
||||
freq=freq,
|
||||
mode=source_spot["mode"].upper(),
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=source_spot["notes"],
|
||||
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.
|
||||
@@ -67,13 +68,14 @@ class Tiles(HTTPSpotProvider):
|
||||
name=source_spot["maidenhead_grid"],
|
||||
latitude=source_spot["latitude"],
|
||||
longitude=source_spot["longitude"],
|
||||
ref_type=SIGRefType.GRID
|
||||
)
|
||||
],
|
||||
time=datetime.fromisoformat(source_spot["created_at"].replace("Z", "+00:00")).timestamp(),
|
||||
dx_grid=source_spot["maidenhead_grid"],
|
||||
dx_latitude=source_spot["latitude"],
|
||||
dx_longitude=source_spot["longitude"],
|
||||
dx_location_source="GRID",
|
||||
dx_location_source=LocationSourceForSpot.GRID,
|
||||
)
|
||||
|
||||
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from core.enums import SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -32,10 +35,10 @@ class Towers(HTTPSpotProvider):
|
||||
freq=likely_freq,
|
||||
comment=source_spot["comment"],
|
||||
sig="Towers",
|
||||
sig_refs=[SIGRef(id=source_spot["ref"], sig="Towers")],
|
||||
time=datetime.strptime(
|
||||
response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M"
|
||||
).timestamp(),
|
||||
sig_refs=[SIGRef(id=source_spot["ref"], sig="Towers", ref_type=SIGRefType.TOWER)],
|
||||
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M")
|
||||
.replace(tzinfo=pytz.utc)
|
||||
.timestamp(),
|
||||
)
|
||||
|
||||
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from core.enums import Mode
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
|
||||
@@ -77,7 +78,7 @@ class UKPacketNet(HTTPSpotProvider):
|
||||
dx_call=heard["callsign"].upper(),
|
||||
de_call=node["callsign"].upper(),
|
||||
freq=freq,
|
||||
mode="PKT",
|
||||
mode=Mode.PKT,
|
||||
comment=comment,
|
||||
time=datetime.strptime(heard["lastHeard"], "%Y-%m-%d %H:%M:%S")
|
||||
.replace(tzinfo=pytz.UTC)
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytz
|
||||
from rss_parser import Parser
|
||||
from rss_parser.models.rss import RSS
|
||||
|
||||
from core.enums import Mode, SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -76,7 +77,9 @@ class WOTA(HTTPSpotProvider):
|
||||
except Exception:
|
||||
logger.warning(f"Could not parse WOTA spot description: {source_spot.description}")
|
||||
|
||||
time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
|
||||
time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(
|
||||
pytz.UTC
|
||||
)
|
||||
|
||||
# Convert to our spot format
|
||||
spot = Spot(
|
||||
@@ -85,10 +88,10 @@ class WOTA(HTTPSpotProvider):
|
||||
dx_call=dx_call,
|
||||
de_call=spotter,
|
||||
freq=freq_hz,
|
||||
mode=mode,
|
||||
mode=Mode.from_name(mode),
|
||||
comment=comment,
|
||||
sig="WOTA",
|
||||
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
||||
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name, ref_type=SIGRefType.SUMMIT)] if ref else [],
|
||||
time=time.timestamp(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from core.enums import Mode, SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.sse_spot_provider import SSESpotProvider
|
||||
@@ -26,6 +27,7 @@ class WWBOTA(SSESpotProvider):
|
||||
name=ref["name"],
|
||||
latitude=ref["lat"],
|
||||
longitude=ref["long"],
|
||||
ref_type=SIGRefType.BUNKER
|
||||
)
|
||||
refs.append(sigref)
|
||||
|
||||
@@ -34,7 +36,7 @@ class WWBOTA(SSESpotProvider):
|
||||
dx_call=source_spot["call"].upper(),
|
||||
de_call=source_spot["spotter"].upper(),
|
||||
freq=float(source_spot["freq"]) * 1000000,
|
||||
mode=source_spot["mode"].upper() if "mode" in source_spot else None,
|
||||
mode=Mode.from_name(source_spot["mode"].upper()) if "mode" in source_spot else Mode.UNKNOWN,
|
||||
comment=source_spot["comment"],
|
||||
sig="WWBOTA",
|
||||
sig_refs=refs,
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from core.enums import Mode, SIGRefType
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -27,7 +28,7 @@ class WWFF(HTTPSpotProvider):
|
||||
dx_call=source_spot["activator"].upper(),
|
||||
de_call=source_spot["spotter"].upper(),
|
||||
freq=float(source_spot["frequency_khz"]) * 1000,
|
||||
mode=source_spot["mode"].upper(),
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=source_spot["remarks"],
|
||||
sig="WWFF",
|
||||
sig_refs=[
|
||||
@@ -37,6 +38,7 @@ class WWFF(HTTPSpotProvider):
|
||||
name=source_spot["reference_name"],
|
||||
latitude=source_spot["latitude"],
|
||||
longitude=source_spot["longitude"],
|
||||
ref_type=SIGRefType.PARK
|
||||
)
|
||||
],
|
||||
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from core.enums import Mode
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.websocket_spot_provider import WebsocketSpotProvider
|
||||
@@ -34,7 +35,7 @@ class XOTA(WebsocketSpotProvider):
|
||||
source_id=source_spot["id"],
|
||||
dx_call=source_spot["stationCallSign"].upper(),
|
||||
freq=float(source_spot["freq"]) * 1000,
|
||||
mode=source_spot["mode"].upper(),
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
sig=self.SIG,
|
||||
sig_refs=[
|
||||
SIGRef(
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from core.enums import Mode
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from providers.spot.http_spot_provider import HTTPSpotProvider
|
||||
@@ -32,7 +33,7 @@ class ZLOTA(HTTPSpotProvider):
|
||||
dx_call=source_spot["activator"].upper(),
|
||||
de_call=source_spot["spotter"].upper(),
|
||||
freq=freq_hz,
|
||||
mode=source_spot["mode"].upper().strip(),
|
||||
mode=Mode.from_name(source_spot["mode"].upper().strip()),
|
||||
comment=source_spot["comments"],
|
||||
sig="ZLOTA",
|
||||
sig_refs=[
|
||||
|
||||
@@ -9,15 +9,15 @@ import tornado
|
||||
from tornado import httputil
|
||||
from tornado.web import Application
|
||||
|
||||
from core.constants import BANDS
|
||||
from core.enums import Continent
|
||||
from core.prometheus_metrics_handler import api_requests_counter
|
||||
from core.utils import safe_json_dumps
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"]
|
||||
BANDS = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"]
|
||||
CONTINENTS_SET = frozenset(CONTINENTS)
|
||||
BANDS_SET = frozenset(BANDS)
|
||||
CONTINENTS = [c.value for c in Continent]
|
||||
HF_BANDS = [b.name for b in BANDS if b.is_ham_hf]
|
||||
|
||||
|
||||
class APIDxStatsHandler(tornado.web.RequestHandler):
|
||||
@@ -53,15 +53,11 @@ class APIDxStatsHandler(tornado.web.RequestHandler):
|
||||
continue
|
||||
if not spot.time or spot.time < one_hour_ago:
|
||||
continue
|
||||
if (
|
||||
spot.de_continent in CONTINENTS_SET
|
||||
and spot.dx_continent in CONTINENTS_SET
|
||||
and spot.band in BANDS_SET
|
||||
):
|
||||
if spot.de_continent in CONTINENTS and spot.dx_continent in CONTINENTS and spot.band in HF_BANDS:
|
||||
counts[spot.de_continent, spot.dx_continent, spot.band] += 1
|
||||
|
||||
result = {
|
||||
de: {dx: {band: counts[de, dx, band] for band in BANDS} for dx in CONTINENTS} for de in CONTINENTS
|
||||
de: {dx: {band: counts[de, dx, band] for band in HF_BANDS} for dx in CONTINENTS} for de in CONTINENTS
|
||||
}
|
||||
|
||||
self.write(json.dumps(result))
|
||||
|
||||
@@ -8,14 +8,8 @@ from tornado import httputil
|
||||
from tornado.web import Application
|
||||
|
||||
from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE
|
||||
from core.constants import (
|
||||
ALL_MODES,
|
||||
BANDS,
|
||||
CONTINENTS,
|
||||
MODE_TYPES,
|
||||
PROPAGATION_MODES,
|
||||
SIGS,
|
||||
)
|
||||
from core.constants import BANDS, PROPAGATION_MODES, SIGS
|
||||
from core.enums import Continent, Mode, ModeType
|
||||
from core.prometheus_metrics_handler import api_requests_counter
|
||||
from core.utils import safe_json_dumps
|
||||
|
||||
@@ -89,14 +83,14 @@ class APIOptionsHandler(tornado.web.RequestHandler):
|
||||
|
||||
options = {
|
||||
"bands": BANDS,
|
||||
"modes": ALL_MODES,
|
||||
"mode_types": MODE_TYPES,
|
||||
"modes": [m.value for m in Mode],
|
||||
"mode_types": [t.value for t in ModeType],
|
||||
"sigs": SIGS,
|
||||
"spot_providers": spot_providers,
|
||||
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
|
||||
"alert_providers": alert_providers,
|
||||
"callsign_data_providers": callsign_data_providers,
|
||||
"continents": CONTINENTS,
|
||||
"continents": [c.value for c in Continent],
|
||||
"propagation_modes": list(PROPAGATION_MODES.values()),
|
||||
"max_spot_age": MAX_SPOT_AGE,
|
||||
"spot_allowed": ALLOW_SPOTTING,
|
||||
|
||||
@@ -2,7 +2,6 @@ import tornado
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.httputil import HTTPHeaders
|
||||
|
||||
|
||||
_LEGACY_PARAM_TO_HEADER_MAP = {
|
||||
"qrz_username": "X-QRZ-Username",
|
||||
"qrz_password": "X-QRZ-Password",
|
||||
@@ -12,6 +11,7 @@ _LEGACY_PARAM_TO_HEADER_MAP = {
|
||||
"hamqth_session_id": "X-HamQTH-Session-ID",
|
||||
}
|
||||
|
||||
|
||||
class V1RedirectHandler(tornado.web.RequestHandler):
|
||||
"""Transparently proxies requests from the old API to the new one,
|
||||
returning whatever the v2 endpoint returns, for endpoints with no breaking changes."""
|
||||
@@ -60,4 +60,4 @@ class V1RedirectHandler(tornado.web.RequestHandler):
|
||||
await self._proxy(path)
|
||||
|
||||
async def post(self, path):
|
||||
await self._proxy(path)
|
||||
await self._proxy(path)
|
||||
|
||||
@@ -23,6 +23,7 @@ def _handle_legacy_params(handler):
|
||||
if value:
|
||||
handler.request.headers[header] = value
|
||||
|
||||
|
||||
class V1APISpotsHandler(APISpotsHandler):
|
||||
"""API request handler for /api/v1/spots (GET). Included in early Spothole v2 for backwards compatibility."""
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from core.constants import SOFTWARE_VERSION
|
||||
from core.data_providers import DATA_PROVIDERS
|
||||
from core.data_store import DATA_STORE
|
||||
from core.status_reporter import StatusReporter
|
||||
from core.utils import infer_mode_from_comment
|
||||
from server.webserver import WEB_SERVER
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,7 +19,9 @@ info:
|
||||
|
||||
* Added DTMBA, FEA, BIWOTA, COTA & PGA SIGs
|
||||
* Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV.
|
||||
* Added `icon`, `region_flag` and `refs_globally_unique` to SIG information
|
||||
* Added `sig_type`, `icon`, `region_flag` and `refs_globally_unique` to SIG information
|
||||
* Unknown modes and mode types now return "UNKNOWN" not null
|
||||
* SIG reference types (e.g. "Park") are now capitalised to match other enums
|
||||
|
||||
### 2.0
|
||||
|
||||
@@ -866,6 +868,14 @@ components:
|
||||
- Toilets
|
||||
example: POTA
|
||||
|
||||
SIGType:
|
||||
type: string
|
||||
enum:
|
||||
- WORLDWIDE
|
||||
- REGIONAL
|
||||
- EVENT
|
||||
example: WORLDWIDE
|
||||
|
||||
SIGNameIncludingNoSIG:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/SIGName"
|
||||
@@ -873,6 +883,29 @@ components:
|
||||
enum: [ NO_SIG ]
|
||||
example: POTA
|
||||
|
||||
SIGRefType:
|
||||
type: string
|
||||
enum:
|
||||
- PARK
|
||||
- SUMMIT
|
||||
- CASTLE
|
||||
- LIGHTHOUSE
|
||||
- ISLAND
|
||||
- BUNKER
|
||||
- MILL
|
||||
- SILO
|
||||
- BEACH
|
||||
- LAKE
|
||||
- TOWER
|
||||
- WATERWAY
|
||||
- TOWN
|
||||
- BUILDING
|
||||
- REGION
|
||||
- GRID
|
||||
- TOILET
|
||||
- UNKNOWN
|
||||
example: PARK
|
||||
|
||||
Continent:
|
||||
type: string
|
||||
enum:
|
||||
@@ -937,6 +970,7 @@ components:
|
||||
- FSK
|
||||
- PKT
|
||||
- MSK144
|
||||
- UNKNOWN
|
||||
example: SSB
|
||||
|
||||
ModeType:
|
||||
@@ -945,6 +979,7 @@ components:
|
||||
- CW
|
||||
- PHONE
|
||||
- DATA
|
||||
- UNKNOWN
|
||||
example: CW
|
||||
|
||||
ModeSource:
|
||||
@@ -981,7 +1016,7 @@ components:
|
||||
- NONE
|
||||
example: SPOT
|
||||
|
||||
LocationSourceForAlert:
|
||||
LocationSourceForCallsign:
|
||||
type: string
|
||||
enum:
|
||||
- "HOME QTH"
|
||||
@@ -1004,9 +1039,8 @@ components:
|
||||
description: SIG reference name
|
||||
example: Null Country Park
|
||||
ref_type:
|
||||
type: string
|
||||
description: SIG reference type
|
||||
example: Park
|
||||
$ref: "#/components/schemas/SIGRefType"
|
||||
url:
|
||||
type: string
|
||||
description: SIG reference URL, which the user can look up for more information
|
||||
@@ -1491,6 +1525,12 @@ components:
|
||||
type: string
|
||||
description: The full name of the SIG
|
||||
example: Parks on the Air
|
||||
sig_type:
|
||||
type: boolean
|
||||
description: >
|
||||
Whether the SIG is worldwide, regional, or for a specific event. Generally for Spothole's own internal use,
|
||||
clients probably won't need this. Used to group them in the web UI.
|
||||
$ref: "#/components/schemas/SIGType"
|
||||
comment_names:
|
||||
type: array
|
||||
description: >
|
||||
@@ -2197,7 +2237,7 @@ components:
|
||||
Where we got the location (grid/latitude/longitude) from. Unlike a spot where we might
|
||||
have a summit position or WAB square, here the only options are an online QTH lookup, or
|
||||
a location based purely on DXCC, or nothing.
|
||||
$ref: "#/components/schemas/LocationSourceForAlert"
|
||||
$ref: "#/components/schemas/LocationSourceForCallsign"
|
||||
|
||||
GridLookup:
|
||||
type: object
|
||||
|
||||
@@ -42,18 +42,25 @@ function setHamHFBandToggles() {
|
||||
function generateSIGsMultiToggleFilterCard(sig_options) {
|
||||
const $grid1 = $('<div class="row row-cols-2 g-1 mb-1">');
|
||||
$("#sig-options").append('<strong>Worldwide</strong>');
|
||||
sig_options.filter(o => o["region_flag"] == null).forEach(o => {
|
||||
sig_options.filter(o => o["sig_type"] === "WORLDWIDE").forEach(o => {
|
||||
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']}</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);
|
||||
const $grid2 = $('<div class="row row-cols-2 g-1 mb-1">');
|
||||
$("#sig-options").append('<strong>Regional</strong>');
|
||||
sig_options.filter(o => o["region_flag"] != null).forEach(o => {
|
||||
sig_options.filter(o => o["sig_type"] === "REGIONAL").forEach(o => {
|
||||
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']}</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);
|
||||
const $grid3 = $('<div class="row row-cols-2 g-1 mb-1">');
|
||||
$("#sig-options").append('<strong>Event</strong>');
|
||||
sig_options.filter(o => o["sig_type"] === "EVENT").forEach(o => {
|
||||
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>`);
|
||||
});
|
||||
$("#sig-options").append($grid3);
|
||||
$("#sig-options").append('<strong>Other</strong>');
|
||||
// Bonus "NO_SIG" / "General DX" option
|
||||
$("#sig-options").append(`<div class="w-100"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-NO_SIG" value="NO_SIG" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-NO_SIG" for="filter-button-sig-NO_SIG"><i class="fa-solid fa-tower-cell"></i> General DX</label></div></div>`);
|
||||
|
||||
@@ -179,13 +179,15 @@
|
||||
filters. They are also stored in your browser's local storage, so that your preferences are remembered between
|
||||
sessions.</p>
|
||||
<p>The data you provide can optionally include your login credentials for QRZ.com and HamQTH. You can provide these
|
||||
in the "Your Data" menu of most pages. If you do, Spothole will augment the data it produces with lookups from these
|
||||
in the "Your Data" menu of most pages. If you do, Spothole will augment the data it produces with lookups from
|
||||
these
|
||||
services, which can for example provide more accurate markers on the map tab, and operator names when you mouse
|
||||
over a DX callsign. Spothole will still work fine if you don't provide these. The values you enter are sent to
|
||||
Spothole via HTTPS so are protected in transit, though of course you do have to trust Spothole with this
|
||||
sensitive data in order to use this feature.</p>
|
||||
<p>Any data you send as part of a query, such as your QRZ or HamQTH credentials, is used only for the lifetime of that
|
||||
query and is not saved anywhere apart from your own device.</p>
|
||||
<p>Any data you send as part of a query, such as your QRZ or HamQTH credentials, is used only for the lifetime of
|
||||
that
|
||||
query and is not saved anywhere apart from your own device.</p>
|
||||
<p>Spothole uses no trackers, no ads, and no cookies.</p>
|
||||
{% if len(web_ui_options["support_button_html"]) > 0 %}
|
||||
<p><strong>Caveat: </strong> The owner of this server has chosen to inject their own content into the "spots" page.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
{% block content %}
|
||||
|
||||
<div id="add-spot-intro-box" class="permanently-dismissible-box mt-3">
|
||||
<div class="alert alert-primary alert-dismissible fade show" role="alert"> <!-- TODO Remove when feature available -->
|
||||
<div class="alert alert-primary alert-dismissible fade show" role="alert">
|
||||
<!-- TODO Remove when feature available -->
|
||||
<i class="fa-solid fa-circle-info"></i> <strong>Adding spots to Spothole</strong><br/>This page is implemented
|
||||
as a proof of concept for adding spots to the Spothole system. Currently, spots added in this way are only
|
||||
visible within Spothole and are not sent "upstream" to DX clusters or xOTA spotting sites. The functionality
|
||||
@@ -76,7 +77,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/add-spot.js?v=1788209684"></script>
|
||||
<script src="/static/js/add-spot.js?v=1788338285"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-add-spot").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/alerts.js?v=1788209684"></script>
|
||||
<script src="/static/js/alerts.js?v=1788338286"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-alerts").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788209684"></script>
|
||||
<script src="/static/js/bands.js?v=1788209684"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788338285"></script>
|
||||
<script src="/static/js/bands.js?v=1788338285"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-bands").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+8
-7
@@ -1,6 +1,6 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1788209684" type="text/css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1788338285" type="text/css">
|
||||
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
|
||||
@@ -11,14 +11,15 @@
|
||||
<script src="/static/vendor/js/tinycolor2-1.6.0.min.js"></script>
|
||||
|
||||
<script type="module">
|
||||
import { fetchEventSource } from '/static/vendor/js/fetch-event-source-2.0.1/index.js';
|
||||
window.fetchEventSource = fetchEventSource;
|
||||
import {fetchEventSource} from '/static/vendor/js/fetch-event-source-2.0.1/index.js';
|
||||
|
||||
window.fetchEventSource = fetchEventSource;
|
||||
</script>
|
||||
|
||||
<script src="/static/js/utils.js?v=1788209684"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1788209684"></script>
|
||||
<script src="/static/js/geo.js?v=1788209684"></script>
|
||||
<script src="/static/js/common.js?v=1788209684"></script>
|
||||
<script src="/static/js/utils.js?v=1788338285"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1788338285"></script>
|
||||
<script src="/static/js/geo.js?v=1788338285"></script>
|
||||
<script src="/static/js/common.js?v=1788338285"></script>
|
||||
{% end %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
||||
<script src="/static/js/conditions.js?v=1788209684"></script>
|
||||
<script src="/static/js/conditions.js?v=1788338285"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-conditions").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+2
-2
@@ -113,8 +113,8 @@
|
||||
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
|
||||
</script>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788209683"></script>
|
||||
<script src="/static/js/map.js?v=1788209683"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788338286"></script>
|
||||
<script src="/static/js/map.js?v=1788338286"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-map").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -113,8 +113,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788209683"></script>
|
||||
<script src="/static/js/spots.js?v=1788209683"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1788338285"></script>
|
||||
<script src="/static/js/spots.js?v=1788338285"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-spots").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/status.js?v=1788209684"></script>
|
||||
<script src="/static/js/status.js?v=1788338285"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#nav-link-status").addClass("active");
|
||||
|
||||
Reference in New Issue
Block a user