Compare commits

...
6 Commits
65 changed files with 531 additions and 296 deletions
+68 -83
View File
@@ -1,4 +1,5 @@
from core.config import SERVER_OWNER_CALLSIGN from core.config import SERVER_OWNER_CALLSIGN
from core.enums import SIGType
from data.band import Band from data.band import Band
from data.sig import SIG from data.sig import SIG
@@ -15,301 +16,288 @@ SIGS = [
name="POTA", name="POTA",
comment_names=["POTA"], comment_names=["POTA"],
description="Parks on the Air", description="Parks on the Air",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="SOTA", name="SOTA",
comment_names=["SOTA"], comment_names=["SOTA"],
description="Summits on the Air", description="Summits on the Air",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="WWFF", name="WWFF",
comment_names=["WWFF"], comment_names=["WWFF"],
description="World Wide Flora & Fauna", description="World Wide Flora & Fauna",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="GMA", name="GMA",
comment_names=["GMA"], comment_names=["GMA"],
description="Global Mountain Activity", description="Global Mountain Activity",
sig_type=SIGType.WORLDWIDE,
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( SIG(
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,
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( SIG(
name="HEMA", name="HEMA",
comment_names=["HEMA"], comment_names=["HEMA"],
description="HuMPs Excluding Marilyns Award", description="HuMPs Excluding Marilyns Award",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="IOTA", name="IOTA",
comment_names=["IOTA"], comment_names=["IOTA"],
description="Islands on the Air", description="Islands on the Air",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="GMA Islands", name="GMA Islands",
comment_names=[], comment_names=[],
description="Global Mountain Activity - Islands", 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}))", 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( SIG(
name="ARLHS", name="ARLHS",
comment_names=["ARLHS"], comment_names=["ARLHS"],
description="Amateur Radio Lighthouse Society", description="Amateur Radio Lighthouse Society",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="ILLW", name="ILLW",
comment_names=["ILLW"], comment_names=["ILLW"],
description="International Lighthouse & Lightship Weekend", description="International Lighthouse & Lightship Weekend",
sig_type=SIGType.EVENT,
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( SIG(
name="MOTA", name="MOTA",
comment_names=["MOTA"], comment_names=["MOTA"],
description="Mills on the Air", description="Mills on the Air",
sig_type=SIGType.EVENT,
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( SIG(
name="SIOTA", name="SIOTA",
comment_names=["SIOTA"], comment_names=["SIOTA"],
description="Silos on the Air", description="Silos on the Air",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="WCA", name="WCA",
comment_names=["WCA"], comment_names=["WCA"],
description="World Castles Award", description="World Castles Award",
sig_type=SIGType.WORLDWIDE,
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( SIG(
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,
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( SIG(
name="WOTA", name="WOTA",
comment_names=["WOTA"], comment_names=["WOTA"],
description="Wainwrights on the Air", description="Wainwrights on the Air",
sig_type=SIGType.REGIONAL,
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(name="BOTA", SIG(
name="BOTA",
comment_names=[], comment_names=[],
description="Beaches on the Air", description="Beaches on the Air",
icon="fa-umbrella-beach" sig_type=SIGType.WORLDWIDE,
icon="fa-umbrella-beach",
refs_globally_unique=False,
), ),
SIG( SIG(
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,
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,
), ),
SIG( SIG(
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,
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,
), ),
SIG( SIG(
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,
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( SIG(
name="Towers", name="Towers",
comment_names=["TOTA"], comment_names=["TOTA"],
description="Towers on the Air", description="Towers on the Air",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="Tiles", name="Tiles",
comment_names=[], comment_names=[],
description="Tiles on the Air", description="Tiles on the Air",
sig_type=SIGType.WORLDWIDE,
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( SIG(
name="WAB", name="WAB",
comment_names=["WAB"], comment_names=["WAB"],
description="Worked All Britain", description="Worked All Britain",
sig_type=SIGType.REGIONAL,
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( SIG(
name="WAI", name="WAI",
comment_names=["WAI"], comment_names=["WAI"],
description="Worked All Ireland", description="Worked All Ireland",
sig_type=SIGType.REGIONAL,
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( SIG(
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,
ref_regex=r"\d{4,5}", ref_regex=r"\d{4,5}",
icon="fa-building", icon="fa-building",
region_flag="🇪🇸", region_flag="🇪🇸",
refs_globally_unique=False refs_globally_unique=False,
), ),
SIG( SIG(
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,
# 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 sigref 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( SIG(
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,
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-masks-theater", icon="fa-masks-theater",
region_flag="🇮🇹", region_flag="🇮🇹",
refs_globally_unique=True refs_globally_unique=True,
), ),
SIG( SIG(
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,
icon="fa-ship", icon="fa-ship",
region_flag="🇬🇧" region_flag="🇬🇧",
refs_globally_unique=False,
), ),
SIG( SIG(
name="COTA", name="COTA",
comment_names=["COTA"], comment_names=["COTA"],
description="Castles on the Air", description="Castles on the Air",
sig_type=SIGType.REGIONAL,
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( SIG(
name="PGA", name="PGA",
comment_names=["PGA"], comment_names=["PGA"],
description="Polish Gmina Award", description="Polish Gmina Award",
sig_type=SIGType.REGIONAL,
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( SIG(
name="Toilets", name="Toilets",
comment_names=[], comment_names=[],
description="Toilets on the Air", description="Toilets on the Air",
sig_type=SIGType.EVENT,
ref_regex=r"T\-[0-9]{2}", ref_regex=r"T\-[0-9]{2}",
icon="fa-toilet", icon="fa-toilet",
region_flag="🏴‍☠️", 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 # Band definitions
BANDS = [ BANDS = [
Band(name="2200m", start_freq=135700, end_freq=137800), 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="12m", start_freq=24890000, end_freq=24990000, is_ham_hf=True),
Band(name="11m", start_freq=26965000, end_freq=27405000), Band(name="11m", start_freq=26965000, end_freq=27405000),
Band(name="10m", start_freq=28000000, end_freq=29700000, is_ham_hf=True), 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="5m", start_freq=56000000, end_freq=60500000),
Band(name="4m", start_freq=70000000, end_freq=70500000), Band(name="4m", start_freq=70000000, end_freq=70500000),
Band(name="2m", start_freq=144000000, end_freq=148000000), 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) 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 # 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 # of these anywhere, but here are some I've seen or seen reference to
PROPAGATION_MODES = { PROPAGATION_MODES = {
+148
View File
@@ -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
View File
@@ -5,17 +5,10 @@ import simplejson
from pyhamtools.frequency import freq_to_band from pyhamtools.frequency import freq_to_band
from pyhamtools.locator import latlong_to_locator from pyhamtools.locator import latlong_to_locator
from core.constants import ( from core.constants import BANDS, UNKNOWN_BAND
ALL_MODES,
BANDS,
CW_MODES,
DATA_MODES,
MODE_ALIASES,
PHONE_MODES,
UNKNOWN_BAND,
)
from core.data_store import DATA_STORE 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__) 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__) 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""" """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): if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE):
return mode return mode
for mode in MODE_ALIASES: for alias in MODE_ALIASES:
if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE): if re.search(r"(^|\W)" + alias + r"($|\W)", comment, re.IGNORECASE):
return MODE_ALIASES[mode] return Mode(MODE_ALIASES[alias])
return None
return Mode.UNKNOWN
def infer_mode_type_from_mode(mode): def infer_mode_type_from_mode(mode: str) -> ModeType:
"""Infer a "mode family" from a mode.""" """Infer a "mode family" from a mode ."""
if mode.upper() in CW_MODES: if not mode:
return "CW" return ModeType.UNKNOWN
elif mode.upper() in PHONE_MODES:
return "PHONE" try:
elif mode.upper() in DATA_MODES: mode = Mode(mode.upper())
return "DATA" if mode.is_cw:
else: return ModeType.CW
if mode.is_phone:
return ModeType.PHONE
return ModeType.DATA
except ValueError:
if mode.upper() != "OTHER" and mode != "?": if mode.upper() != "OTHER" and mode != "?":
logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.") logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.")
return None return ModeType.UNKNOWN
def infer_band_from_freq(freq): def infer_band_from_freq(freq):
@@ -116,7 +118,7 @@ def get_callsign_object_from_pyhamtools_callinfo(callsign, callinfo):
country = data.get("country", None) country = data.get("country", None)
dxcc_id = data.get("adif", None) dxcc_id = data.get("adif", None)
continent = data.get("continent", None) continent = Continent(data.get("continent", None))
cq_zone = data.get("cqz", None) cq_zone = data.get("cqz", None)
itu_zone = data.get("ituz", None) itu_zone = data.get("ituz", None)
lat = float(data["latitude"]) if "latitude" in data else 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, latitude=lat,
longitude=lon, longitude=lon,
grid=grid, grid=grid,
location_source="DXCC", location_source=LocationSourceForCallsign.DXCC,
) )
except (KeyError, ValueError): except (KeyError, ValueError):
+3 -2
View File
@@ -7,6 +7,7 @@ from datetime import datetime, timedelta
import pytz import pytz
from core.call_lookup_helper import get_call_info 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.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
@@ -28,7 +29,7 @@ class Alert:
# Country flag of the DX operator # Country flag of the DX operator
dx_flag: str | None = None dx_flag: str | None = None
# Continent of the DX operator # Continent of the DX operator
dx_continent: str | None = None dx_continent: Continent | None = None
# DXCC ID of the DX operator # DXCC ID of the DX operator
dx_dxcc_id: int | None = None dx_dxcc_id: int | None = None
# CQ zone of the DX operator # 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: if self.dx_calls and self.dx_calls[0] and not self.dx_country:
self.dx_country = call_info.country self.dx_country = call_info.country
if self.dx_calls and self.dx_calls[0] and not self.dx_continent: 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: if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone:
self.dx_cq_zone = call_info.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: if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone:
+5 -3
View File
@@ -1,5 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from core.enums import Continent, LocationSourceForCallsign
@dataclass @dataclass
class Callsign: class Callsign:
@@ -28,15 +30,15 @@ class Callsign:
# Country in which the callsign indicates they are operating # Country in which the callsign indicates they are operating
country: str | None = None country: str | None = None
# Continent in which the callsign indicates they are operating # 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 in which the callsign indicates they are operating
dxcc_id: int | None = None dxcc_id: int | None = None
# CQ zone in which the callsign indicates they are operating # CQ zone in which the callsign indicates they are operating
cq_zone: int | None = None cq_zone: int | None = None
# ITU zone in which the callsign indicates they are operating # ITU zone in which the callsign indicates they are operating
itu_zone: int | None = None itu_zone: int | None = None
# Location source. This can be "HOME QTH" or "DXCC" depending on which provider gave us a location # Location source
location_source: str | None = None location_source: LocationSourceForCallsign = LocationSourceForCallsign.NONE
def fully_populated(self): def fully_populated(self):
"""Utility method to indicate that the callsign data is fully populated. Multiple providers can return data for """Utility method to indicate that the callsign data is fully populated. Multiple providers can return data for
+9 -5
View File
@@ -1,5 +1,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from core.enums import SIGType
@dataclass @dataclass
class SIG: class SIG:
@@ -15,16 +17,18 @@ class SIG:
name: str name: str
# Description, e.g. "Towers on the Air" # Description, e.g. "Towers on the Air"
description: str description: str
# SIG names as they might appear in cluster spot comments, e.g. ["TOTA"] # Type, either Worldwide, Regional or Event. Used for sorting in the web UI.
comment_names: list[str] = field(default_factory=list) sig_type: SIGType
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
ref_regex: str | None = None
# Identifies that the SIG's reference ID structure defined by its regex is unique across all programmes and # 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 # 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 # 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 # obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could
# conceivably be POTA or ILLW, etc. # 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 to use in the UI when referencing this SIG. Chosen from the Font Awesome set.
icon: str | None = None 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 # Emoji flag for the country or region where this SIG is relevant, if any. If None, this implies the SIG is in
+3 -1
View File
@@ -1,5 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from core.enums import SIGRefType
@dataclass @dataclass
class SIGRef: class SIGRef:
@@ -13,7 +15,7 @@ class SIGRef:
# 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: str | None = None ref_type: SIGRefType = SIGRefType.UNKNOWN
# 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
# Latitude of the reference, in degrees, if known. # Latitude of the reference, in degrees, if known.
+33 -33
View File
@@ -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.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 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.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_lookup_helper import populate_missing_sig_ref_info
from core.sig_utils import ( from core.sig_utils import (
@@ -52,7 +53,7 @@ class Spot:
# Country flag of the DX operator # Country flag of the DX operator
dx_flag: str | None = None dx_flag: str | None = None
# Continent of the DX operator # Continent of the DX operator
dx_continent: str | None = None dx_continent: Continent | None = None
# DXCC ID of the DX operator # DXCC ID of the DX operator
dx_dxcc_id: int | None = None dx_dxcc_id: int | None = None
# CQ zone of the DX operator # CQ zone of the DX operator
@@ -68,9 +69,8 @@ class Spot:
# lookup # lookup
dx_latitude: float | None = None dx_latitude: float | None = None
dx_longitude: float | None = None dx_longitude: float | None = None
# DX Location source. Indicates how accurate the location might be. Values: "SPOT", "GRID", "SIG REF LOOKUP", # DX Location source. Indicates how accurate the location might be.
# "HOME QTH", "DXCC", "NONE" dx_location_source: LocationSourceForSpot = LocationSourceForSpot.NONE
dx_location_source: str = "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 the
# DX callsign doesn't have a suffix like /P. # DX callsign doesn't have a suffix like /P.
@@ -85,7 +85,7 @@ class Spot:
# Country flag of the spotter # Country flag of the spotter
de_flag: str | None = None de_flag: str | None = None
# Continent of the spotter # Continent of the spotter
de_continent: str | None = None de_continent: Continent | None = None
# DXCC ID of the spotter # DXCC ID of the spotter
de_dxcc_id: int | None = None de_dxcc_id: int | None = None
# If this is an APRS/Packet/etc spot, what SSID was the spotter/receiver using? # If this is an APRS/Packet/etc spot, what SSID was the spotter/receiver using?
@@ -103,11 +103,11 @@ class Spot:
# General QSO info # General QSO info
# Reported mode, such as SSB, PHONE, CW, FT8... # Reported mode, such as SSB, PHONE, CW, FT8...
mode: str | None = None mode: Mode = Mode.UNKNOWN
# Inferred mode "family". One of "CW", "PHONE" or "DIGI". # Inferred mode "family".
mode_type: str | None = None mode_type: ModeType = ModeType.UNKNOWN
# Source of the mode information. "SPOT", "COMMENT", "BANDPLAN" or "NONE" # Source of the mode information.
mode_source: str = "NONE" mode_source: ModeSource = ModeSource.NONE
# Frequency, in Hz # Frequency, in Hz
freq: float | None = None freq: float | None = None
# Band, defined by the frequency, e.g. "40m" or "70cm" # 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: if self.dx_call and not self.dx_country:
self.dx_country = dx_call_info.country self.dx_country = dx_call_info.country
if self.dx_call and not self.dx_continent: 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: if self.dx_call and not self.dx_dxcc_id:
self.dx_dxcc_id = dx_call_info.dxcc_id self.dx_dxcc_id = dx_call_info.dxcc_id
if self.dx_dxcc_id and not self.dx_flag: if self.dx_dxcc_id and not self.dx_flag:
@@ -223,7 +223,7 @@ class Spot:
if not self.de_country: if not self.de_country:
self.de_country = de_call_info.country self.de_country = de_call_info.country
if not self.de_continent: 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: if not self.de_dxcc_id:
self.de_dxcc_id = de_call_info.dxcc_id self.de_dxcc_id = de_call_info.dxcc_id
if self.de_dxcc_id and not self.de_flag: if self.de_dxcc_id and not self.de_flag:
@@ -239,26 +239,26 @@ class Spot:
self.band = band.name self.band = band.name
# Mode from comments or bandplan # Mode from comments or bandplan
if self.mode: if not self.mode:
self.mode_source = "SPOT" self.mode = Mode.UNKNOWN
if self.comment and not self.mode: 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 = infer_mode_from_comment(self.comment)
self.mode_source = "COMMENT" self.mode_source = ModeSource.COMMENT
if self.freq and not self.mode: if self.freq and self.mode == Mode.UNKNOWN:
self.mode = infer_mode_from_frequency(self.freq) self.mode = infer_mode_from_frequency(self.freq)
self.mode_source = "BANDPLAN" self.mode_source = ModeSource.BANDPLAN
# Normalise mode if necessary.
if self.mode in MODE_ALIASES:
self.mode = MODE_ALIASES[self.mode]
# Mode type from mode # 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) 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 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: 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. # 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: 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_latitude = sig_ref.latitude
self.dx_longitude = sig_ref.longitude self.dx_longitude = sig_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 = "GRID" self.dx_location_source = LocationSourceForSpot.GRID
else: 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 # 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. # and apply it to the whole spot.
@@ -347,7 +347,7 @@ class Spot:
self.de_grid = grid_mode_grid_match.group(1).upper() self.de_grid = grid_mode_grid_match.group(1).upper()
if not self.dx_grid: if not self.dx_grid:
self.dx_grid = grid_mode_grid_match.group(4).upper() 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 (...)): # 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() 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: # regex matches, so extract grids:
if not self.dx_grid: if not self.dx_grid:
self.dx_grid = grid_mode_grid_match.group(1).upper() 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: if not self.de_grid:
self.de_grid = grid_mode_grid_match.group(2).upper() self.de_grid = grid_mode_grid_match.group(2).upper()
@@ -440,10 +440,10 @@ class Spot:
self.dx_latitude self.dx_latitude
and self.dx_longitude and self.dx_longitude
and ( and (
self.dx_location_source == "SPOT" self.dx_location_source == LocationSourceForSpot.SPOT
or self.dx_location_source == "SIG REF LOOKUP" or self.dx_location_source == LocationSourceForSpot.SIG_REF_LOOKUP
or self.dx_location_source == "GRID" or self.dx_location_source == LocationSourceForSpot.GRID
or (self.dx_location_source == "HOME QTH" and "/" not in (self.dx_call or "")) or (self.dx_location_source == LocationSourceForSpot.HOME_QTH and "/" not in (self.dx_call or ""))
) )
) )
+12 -11
View File
@@ -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 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 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 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. the `-d` flag.
### nginx Reverse Proxy with Docker ### 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 In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal of
of HTTPS certificates, and then applications like Spothole in a separate container. In this case, there are a couple 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 variations of the docker compose file above, and the nginx reverse proxy configuration covered [here](./nginx.md), that
you will want to make. 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 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 allows nginx to access static files directly, as these will be inaccessible in another container. So you may end up with
with something like: something like:
```nginx ```nginx
server { 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 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 `proxy_pass` statements could drop the `:8080` suffix. Since Spothole is in a container, it can serve HTTP on port 80 if
if desired, because it doesn't conflict with the host system. desired, because it doesn't conflict with the host system.
### Restoring the static files bypass ### 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 If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can do. The
do. The easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes: easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes:
```yaml ```yaml
services: services:
@@ -183,8 +184,8 @@ networks:
external: true external: true
``` ```
Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time point it
point it at the new container rather than at a filesystem path: at the new container rather than at a filesystem path:
```nginx ```nginx
# Load static assets from the spothole-static-nginx container # Load static assets from the spothole-static-nginx container
+2 -2
View File
@@ -48,8 +48,8 @@ To navigate your way around the source code, this list may help.
### Extending the server ### Extending the server
Spothole is designed to be easily extensible. If you want to write your own spot provider, for example, simply add a 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 ( 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.) (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 " 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. HTTPSpotProvider" where some of the work is done for you.
+10 -15
View File
@@ -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 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 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 site, since Spothole itself doesn't directly support acting as an HTTPS server. The normal solution to this is to use a
a "reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then "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 passes on the request to the back-end application (in this case Spothole). nginx is a common choice for this general web
server. 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. 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 These are what's used on my own Spothole server to make sure that other third-party web-based software can get the data
my own Spothole server to make sure that other third-party web-based software can get the data from my instance, and from my instance, and applies to any endpoint underneath `/api`. If you want *your* Spothole instance to be set up the
applies to any endpoint underneath `/api`. If you want same way, so that others can write software in JavaScript that can access it, leave this intact. But if you want your
*your* Spothole instance to be set up the same way, so that others can write software in JavaScript that can access it, Spothole instance to only be usable by scripts running on the web server you write, you can remove these lines. (Note
leave this intact. But if you want your Spothole instance to only be usable by scripts running on the web server you that this doesn't stop other people writing *non-web-based* software that accesses your Spothole API&mdash;the
write, enforcement of cross-origin headers only happens within the user's browser. If you need to lock your instance down so
you can remove these lines. (Note that this doesn't stop other people writing *non-web-based* software that accesses that no-one else can access it with *any* software, that's an aspect of nginx or firewall config that you will need to
your find help with elsewhere.)
Spothole API&mdash;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: Now, make a symbolic link to enable the site:
+4 -3
View File
@@ -11,8 +11,9 @@ from requests_cache import CachedSession
from core.config import SERVER_OWNER_CALLSIGN from core.config import SERVER_OWNER_CALLSIGN
from core.constants import HTTP_HEADERS, SOFTWARE_VERSION from core.constants import HTTP_HEADERS, SOFTWARE_VERSION
from core.data_store import CACHE_DIR, DATA_STORE from core.data_store import CACHE_DIR, DATA_STORE
from core.enums import Continent
from core.url_data_cache import URLDataCache 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 ( from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider, APIQueryCallsignDataProvider,
) )
@@ -142,12 +143,12 @@ class HamQTH(APIQueryCallsignDataProvider):
name=data.get("nick", None), name=data.get("nick", None),
qth=data.get("qth", None), qth=data.get("qth", None),
country=data.get("country", None), country=data.get("country", None),
continent=data.get("continent", None), continent=Continent(data.get("continent", None)),
latitude=lat, latitude=lat,
longitude=lon, longitude=lon,
grid=grid, grid=grid,
dxcc_id=int(data["adif"]) if "adif" in data else None, dxcc_id=int(data["adif"]) if "adif" in data else None,
cq_zone=int(data["cq"]) if "cq" 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, itu_zone=int(data["itu"]) if "itu" in data else None,
location_source="HOME QTH", location_source=LocationSourceForCallsign.HOME_QTH,
) )
+4 -5
View File
@@ -10,11 +10,10 @@ from requests_cache import CachedSession
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from core.data_store import CACHE_DIR, DATA_STORE from core.data_store import CACHE_DIR, DATA_STORE
from core.enums import Continent, LocationSourceForCallsign
from core.url_data_cache import URLDataCache from core.url_data_cache import URLDataCache
from data.callsign import Callsign from data.callsign import Callsign
from providers.callsigndata.api_query_callsign_data_provider import ( from providers.callsigndata.api_query_callsign_data_provider import APIQueryCallsignDataProvider
APIQueryCallsignDataProvider,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -170,12 +169,12 @@ class QRZ(APIQueryCallsignDataProvider):
name=name, name=name,
qth=data.get("addr2", None), qth=data.get("addr2", None),
country=data.get("country", None), country=data.get("country", None),
continent=data.get("continent", None), continent=Continent(data.get("continent", None)),
latitude=lat, latitude=lat,
longitude=lon, longitude=lon,
grid=grid, grid=grid,
dxcc_id=int(data["adif"]) if "adif" in data else None, dxcc_id=int(data["adif"]) if "adif" in data else None,
cq_zone=int(data["cqzone"]) if "cqzone" 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, itu_zone=int(data["ituzone"]) if "ituzone" in data else None,
location_source="HOME QTH", location_source=LocationSourceForCallsign.HOME_QTH,
) )
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -27,7 +28,7 @@ class ARLHS(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("Name", None), name=row.get("Name", None),
ref_type="Lighthouse", ref_type=SIGRefType.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,
+2 -1
View File
@@ -2,6 +2,7 @@ from time import sleep
from pyhamtools.locator import latlong_to_locator from pyhamtools.locator import latlong_to_locator
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
@@ -32,7 +33,7 @@ class COTA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=name, name=name,
ref_type="Castle", ref_type=SIGRefType.CASTLE,
grid=grid, grid=grid,
latitude=lat, latitude=lat,
longitude=lon, longitude=lon,
+2 -1
View File
@@ -3,6 +3,7 @@ from time import sleep
from pyhamtools.locator import latlong_to_locator from pyhamtools.locator import latlong_to_locator
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.local_file_sig_ref_data_provider import ( from providers.sigrefdata.local_file_sig_ref_data_provider import (
LocalFileSIGRefDataProvider, LocalFileSIGRefDataProvider,
@@ -37,7 +38,7 @@ class DME(LocalFileSIGRefDataProvider):
ref = SIGRef( ref = SIGRef(
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
ref_type="Town", ref_type=SIGRefType.TOWN,
name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}", name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}",
latitude=latitude, latitude=latitude,
longitude=longitude, longitude=longitude,
+2 -8
View File
@@ -1,5 +1,6 @@
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
@@ -20,14 +21,7 @@ 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( new_data.append(SIGRef(sig=self.SIG, id=ref_id, name=ref_name, ref_type=SIGRefType.BUILDING))
SIGRef(
sig=self.SIG,
id=ref_id,
name=ref_name,
ref_type="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
+3 -2
View File
@@ -3,6 +3,7 @@ from time import sleep
import pdfplumber import pdfplumber
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider 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. # 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="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="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 # 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
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -26,7 +27,7 @@ class GMA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("Name", None), name=row.get("Name", None),
ref_type="Summit", ref_type=SIGRefType.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,
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -27,7 +28,7 @@ class ILLW(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("Name", None), name=row.get("Name", None),
ref_type="Lighthouse", ref_type=SIGRefType.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,
+2 -1
View File
@@ -3,6 +3,7 @@ from time import sleep
from pyhamtools.locator import latlong_to_locator from pyhamtools.locator import latlong_to_locator
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -44,7 +45,7 @@ class IOTA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=ref["name"], name=ref["name"],
ref_type="Island", ref_type=SIGRefType.ISLAND,
grid=grid, grid=grid,
latitude=latitude, latitude=latitude,
longitude=longitude, longitude=longitude,
+2 -1
View File
@@ -2,6 +2,7 @@ from time import sleep
from pyhamtools.locator import locator_to_latlong from pyhamtools.locator import locator_to_latlong
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -32,7 +33,7 @@ class LLOTA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=str(ref["name"]), name=str(ref["name"]),
ref_type="Lake", ref_type=SIGRefType.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],
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -26,7 +27,7 @@ class MOTA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("Name", None), name=row.get("Name", None),
ref_type="Mill", ref_type=SIGRefType.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,
+2 -1
View File
@@ -2,6 +2,7 @@ from time import sleep
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
@@ -41,7 +42,7 @@ class PGA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=name, name=name,
ref_type="Region", ref_type=SIGRefType.REGION,
) )
) )
@@ -4,6 +4,7 @@ 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 data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -43,7 +44,7 @@ class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider):
sig=self.sig_name, sig=self.sig_name,
id=ref_id, id=ref_id,
name=placemark.name, name=placemark.name,
ref_type="Park", ref_type=SIGRefType.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,
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -26,7 +27,7 @@ class POTA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("name", None), name=row.get("name", None),
ref_type="Park", ref_type=SIGRefType.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,
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -26,7 +27,7 @@ class SIOTA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("NAME", None), name=row.get("NAME", None),
ref_type="Silo", ref_type=SIGRefType.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,
+2 -1
View File
@@ -3,6 +3,7 @@ from time import sleep
from pyhamtools.locator import latlong_to_locator from pyhamtools.locator import latlong_to_locator
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -30,7 +31,7 @@ class SOTA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("SummitName", None), name=row.get("SummitName", None),
ref_type="Summit", ref_type=SIGRefType.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,
+2 -1
View File
@@ -1,5 +1,6 @@
import csv import csv
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.local_file_sig_ref_data_provider import ( from providers.sigrefdata.local_file_sig_ref_data_provider import (
LocalFileSIGRefDataProvider, LocalFileSIGRefDataProvider,
@@ -26,7 +27,7 @@ class Toilets(LocalFileSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=row["ref"], id=row["ref"],
name=row["ref"], name=row["ref"],
ref_type="Toilet", ref_type=SIGRefType.TOILET,
latitude=float(row["lat"]), latitude=float(row["lat"]),
longitude=float(row["lon"]), longitude=float(row["lon"]),
) )
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -26,7 +27,7 @@ class Towers(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("Nazev", None), name=row.get("Nazev", None),
ref_type="Tower", ref_type=SIGRefType.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,
+2 -1
View File
@@ -4,6 +4,7 @@ from time import sleep
from pyhamtools.locator import latlong_to_locator from pyhamtools.locator import latlong_to_locator
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -45,7 +46,7 @@ class WCA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("CLEAN NAME", None), name=row.get("CLEAN NAME", None),
ref_type="Castle", ref_type=SIGRefType.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,
+2 -1
View File
@@ -1,5 +1,6 @@
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -33,7 +34,7 @@ class WOTA(FileDownloadSIGRefDataProvider):
id=ref_id, id=ref_id,
name=feature["properties"]["title"], name=feature["properties"]["title"],
url=url, url=url,
ref_type="Summit", ref_type=SIGRefType.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],
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -26,7 +27,7 @@ class WWBOTA(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("Name", None), 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, 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,
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
from time import sleep from time import sleep
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -26,7 +27,7 @@ class WWFF(FileDownloadSIGRefDataProvider):
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=row.get("name", None), name=row.get("name", None),
ref_type="Park", ref_type=SIGRefType.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"])
+6 -1
View File
@@ -2,6 +2,7 @@ from time import sleep
from pyhamtools.locator import latlong_to_locator from pyhamtools.locator import latlong_to_locator
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.sigrefdata.file_download_sig_ref_data_provider import ( from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
@@ -26,12 +27,16 @@ class ZLOTA(FileDownloadSIGRefDataProvider):
ref_id = ref["code"] ref_id = ref["code"]
latitude = ref["latitude"] latitude = ref["latitude"]
longitude = ref["longitude"] longitude = ref["longitude"]
try:
ref_type = SIGRefType(ref["asset_type"].title().upper())
except ValueError:
ref_type = SIGRefType.UNKNOWN
new_ref = SIGRef( new_ref = SIGRef(
sig=self.SIG, sig=self.SIG,
id=ref_id, id=ref_id,
name=ref["name"], name=ref["name"],
ref_type=ref["asset_type"].title(), ref_type=ref_type,
url=f"https://ontheair.nz/assets/{ref_id.replace('/', '_')}", url=f"https://ontheair.nz/assets/{ref_id.replace('/', '_')}",
latitude=latitude, latitude=latitude,
longitude=longitude, longitude=longitude,
+11 -1
View File
@@ -4,6 +4,7 @@ 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.url_data_cache import URLDataCache from core.url_data_cache import URLDataCache
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
@@ -65,7 +66,9 @@ class GMA(HTTPSpotProvider):
if (source_spot["QRG"] != "" and source_spot["QRG"] != "QRT") if (source_spot["QRG"] != "" and source_spot["QRG"] != "QRT")
else None, else None,
# Filter out some weird mode strings # 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"], comment=source_spot["TEXT"],
sig_refs=[ sig_refs=[
SIGRef( SIGRef(
@@ -113,24 +116,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 = "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 = "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 = "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 = "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 = "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 = "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 = "MOTA" spot.sig = "MOTA"
case _: case _:
logger.warning( logger.warning(
+3 -1
View File
@@ -7,6 +7,7 @@ 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 data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -59,7 +60,7 @@ class HEMA(HTTPSpotProvider):
dx_call=spot_items[2].upper(), dx_call=spot_items[2].upper(),
de_call=spotter_comment_match.group(1).upper(), de_call=spotter_comment_match.group(1).upper(),
freq=float(freq_mode_match.group(1)) * 1000000, 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), comment=spotter_comment_match.group(2),
sig="HEMA", sig="HEMA",
sig_refs=[ sig_refs=[
@@ -69,6 +70,7 @@ class HEMA(HTTPSpotProvider):
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,
) )
], ],
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M") time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M")
+3 -1
View File
@@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from core.enums import Mode, SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -31,7 +32,7 @@ class LLOTA(HTTPSpotProvider):
dx_call=source_spot["callsign"].upper(), dx_call=source_spot["callsign"].upper(),
de_call=spotter.upper() if spotter else None, de_call=spotter.upper() if spotter else None,
freq=float(source_spot["frequency"]) * 1000000, freq=float(source_spot["frequency"]) * 1000000,
mode=source_spot["mode"].upper(), mode=Mode.from_name(source_spot["mode"].upper()),
comment=comment, comment=comment,
sig="LLOTA", sig="LLOTA",
sig_refs=[ sig_refs=[
@@ -39,6 +40,7 @@ class LLOTA(HTTPSpotProvider):
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,
) )
], ],
time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp(), time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp(),
+3 -1
View File
@@ -6,6 +6,7 @@ import pytz
import requests import requests
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from core.enums import Mode
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -51,7 +52,7 @@ class ParksNPeaks(HTTPSpotProvider):
if (source_spot["actFreq"] != "") if (source_spot["actFreq"] != "")
else None, else None,
# Seen PNP spots with empty frequency, and with comma-separated thousands digits # 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"], comment=source_spot["actComments"],
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S") time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S")
.replace(tzinfo=pytz.UTC) .replace(tzinfo=pytz.UTC)
@@ -86,6 +87,7 @@ class ParksNPeaks(HTTPSpotProvider):
"POTA", "POTA",
"SOTA", "SOTA",
"WWFF", "WWFF",
"HEMA",
"SIOTA", "SIOTA",
"ZLOTA", "ZLOTA",
"KRMNPA", "KRMNPA",
+4 -2
View File
@@ -4,6 +4,7 @@ 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 data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -29,8 +30,8 @@ class POTA(HTTPSpotProvider):
source_id=source_spot["spotId"], source_id=source_spot["spotId"],
dx_call=source_spot["activator"].upper(), dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(), de_call=source_spot["spotter"].upper(),
freq=float(source_spot["frequency"]) * 1000, freq=float(source_spot["frequency"]) * 1000 if source_spot["frequency"] != "INVALID" else None,
mode=source_spot["mode"].upper(), mode=Mode.from_name(source_spot["mode"].upper()),
comment=source_spot["comments"], comment=source_spot["comments"],
sig="POTA", sig="POTA",
sig_refs=[ sig_refs=[
@@ -40,6 +41,7 @@ class POTA(HTTPSpotProvider):
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
) )
], ],
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S") time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S")
+3 -1
View File
@@ -5,6 +5,7 @@ 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 data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -53,7 +54,7 @@ class SOTA(HTTPSpotProvider):
if (source_spot["frequency"] is not None) if (source_spot["frequency"] is not None)
else None, else None,
# Seen SOTA spots with no frequency! # Seen SOTA spots with no frequency!
mode=source_spot["mode"].upper(), mode=Mode.from_name(source_spot["mode"].upper()),
comment=source_spot["comments"], comment=source_spot["comments"],
sig="SOTA", sig="SOTA",
sig_refs=[ sig_refs=[
@@ -64,6 +65,7 @@ class SOTA(HTTPSpotProvider):
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,
) )
], ],
dx_latitude=source_spot["latitude"], dx_latitude=source_spot["latitude"],
+4 -2
View File
@@ -4,6 +4,7 @@ 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 data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -55,7 +56,7 @@ class Tiles(HTTPSpotProvider):
# No separate spotter callsign, assume all spots are self-spots # No separate spotter callsign, assume all spots are self-spots
de_call=source_spot["call_sign"].upper(), de_call=source_spot["call_sign"].upper(),
freq=freq, freq=freq,
mode=source_spot["mode"].upper(), mode=Mode.from_name(source_spot["mode"].upper()),
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.
@@ -67,13 +68,14 @@ class Tiles(HTTPSpotProvider):
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
) )
], ],
time=datetime.fromisoformat(source_spot["created_at"].replace("Z", "+00:00")).timestamp(), time=datetime.fromisoformat(source_spot["created_at"].replace("Z", "+00:00")).timestamp(),
dx_grid=source_spot["maidenhead_grid"], dx_grid=source_spot["maidenhead_grid"],
dx_latitude=source_spot["latitude"], dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"], 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 # Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
+7 -4
View File
@@ -1,6 +1,9 @@
import json import json
from datetime import datetime from datetime import datetime
import pytz
from core.enums import SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
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,10 +35,10 @@ 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")], sig_refs=[SIGRef(id=source_spot["ref"], sig="Towers", ref_type=SIGRefType.TOWER)],
time=datetime.strptime( time=datetime.strptime(response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M")
response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M" .replace(tzinfo=pytz.utc)
).timestamp(), .timestamp(),
) )
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do # Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
+2 -1
View File
@@ -3,6 +3,7 @@ from datetime import datetime
import pytz import pytz
from core.enums import Mode
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
@@ -77,7 +78,7 @@ class UKPacketNet(HTTPSpotProvider):
dx_call=heard["callsign"].upper(), dx_call=heard["callsign"].upper(),
de_call=node["callsign"].upper(), de_call=node["callsign"].upper(),
freq=freq, freq=freq,
mode="PKT", mode=Mode.PKT,
comment=comment, comment=comment,
time=datetime.strptime(heard["lastHeard"], "%Y-%m-%d %H:%M:%S") time=datetime.strptime(heard["lastHeard"], "%Y-%m-%d %H:%M:%S")
.replace(tzinfo=pytz.UTC) .replace(tzinfo=pytz.UTC)
+6 -3
View File
@@ -8,6 +8,7 @@ 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 data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -76,7 +77,9 @@ class WOTA(HTTPSpotProvider):
except Exception: except Exception:
logger.warning(f"Could not parse WOTA spot description: {source_spot.description}") 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 # Convert to our spot format
spot = Spot( spot = Spot(
@@ -85,10 +88,10 @@ class WOTA(HTTPSpotProvider):
dx_call=dx_call, dx_call=dx_call,
de_call=spotter, de_call=spotter,
freq=freq_hz, freq=freq_hz,
mode=mode, mode=Mode.from_name(mode),
comment=comment, comment=comment,
sig="WOTA", 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(), time=time.timestamp(),
) )
+3 -1
View File
@@ -1,6 +1,7 @@
import json import json
from datetime import datetime from datetime import datetime
from core.enums import Mode, SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -26,6 +27,7 @@ class WWBOTA(SSESpotProvider):
name=ref["name"], name=ref["name"],
latitude=ref["lat"], latitude=ref["lat"],
longitude=ref["long"], longitude=ref["long"],
ref_type=SIGRefType.BUNKER
) )
refs.append(sigref) refs.append(sigref)
@@ -34,7 +36,7 @@ class WWBOTA(SSESpotProvider):
dx_call=source_spot["call"].upper(), dx_call=source_spot["call"].upper(),
de_call=source_spot["spotter"].upper(), de_call=source_spot["spotter"].upper(),
freq=float(source_spot["freq"]) * 1000000, 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"], comment=source_spot["comment"],
sig="WWBOTA", sig="WWBOTA",
sig_refs=refs, sig_refs=refs,
+3 -1
View File
@@ -2,6 +2,7 @@ from datetime import datetime
import pytz import pytz
from core.enums import Mode, SIGRefType
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -27,7 +28,7 @@ class WWFF(HTTPSpotProvider):
dx_call=source_spot["activator"].upper(), dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(), de_call=source_spot["spotter"].upper(),
freq=float(source_spot["frequency_khz"]) * 1000, freq=float(source_spot["frequency_khz"]) * 1000,
mode=source_spot["mode"].upper(), mode=Mode.from_name(source_spot["mode"].upper()),
comment=source_spot["remarks"], comment=source_spot["remarks"],
sig="WWFF", sig="WWFF",
sig_refs=[ sig_refs=[
@@ -37,6 +38,7 @@ class WWFF(HTTPSpotProvider):
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
) )
], ],
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(), time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
+2 -1
View File
@@ -3,6 +3,7 @@ from datetime import datetime
import pytz import pytz
from core.enums import Mode
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
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
@@ -34,7 +35,7 @@ class XOTA(WebsocketSpotProvider):
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=source_spot["mode"].upper(), mode=Mode.from_name(source_spot["mode"].upper()),
sig=self.SIG, sig=self.SIG,
sig_refs=[ sig_refs=[
SIGRef( SIGRef(
+2 -1
View File
@@ -2,6 +2,7 @@ from datetime import datetime
import pytz import pytz
from core.enums import Mode
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
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,7 +33,7 @@ class ZLOTA(HTTPSpotProvider):
dx_call=source_spot["activator"].upper(), dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(), de_call=source_spot["spotter"].upper(),
freq=freq_hz, freq=freq_hz,
mode=source_spot["mode"].upper().strip(), mode=Mode.from_name(source_spot["mode"].upper().strip()),
comment=source_spot["comments"], comment=source_spot["comments"],
sig="ZLOTA", sig="ZLOTA",
sig_refs=[ sig_refs=[
+6 -10
View File
@@ -9,15 +9,15 @@ import tornado
from tornado import httputil from tornado import httputil
from tornado.web import Application 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.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"] CONTINENTS = [c.value for c in Continent]
BANDS = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"] HF_BANDS = [b.name for b in BANDS if b.is_ham_hf]
CONTINENTS_SET = frozenset(CONTINENTS)
BANDS_SET = frozenset(BANDS)
class APIDxStatsHandler(tornado.web.RequestHandler): class APIDxStatsHandler(tornado.web.RequestHandler):
@@ -53,15 +53,11 @@ class APIDxStatsHandler(tornado.web.RequestHandler):
continue continue
if not spot.time or spot.time < one_hour_ago: if not spot.time or spot.time < one_hour_ago:
continue continue
if ( if spot.de_continent in CONTINENTS and spot.dx_continent in CONTINENTS and spot.band in HF_BANDS:
spot.de_continent in CONTINENTS_SET
and spot.dx_continent in CONTINENTS_SET
and spot.band in BANDS_SET
):
counts[spot.de_continent, spot.dx_continent, spot.band] += 1 counts[spot.de_continent, spot.dx_continent, spot.band] += 1
result = { 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)) self.write(json.dumps(result))
+5 -11
View File
@@ -8,14 +8,8 @@ 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 ( from core.constants import BANDS, PROPAGATION_MODES, SIGS
ALL_MODES, from core.enums import Continent, Mode, ModeType
BANDS,
CONTINENTS,
MODE_TYPES,
PROPAGATION_MODES,
SIGS,
)
from core.prometheus_metrics_handler import api_requests_counter from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
@@ -89,14 +83,14 @@ class APIOptionsHandler(tornado.web.RequestHandler):
options = { options = {
"bands": BANDS, "bands": BANDS,
"modes": ALL_MODES, "modes": [m.value for m in Mode],
"mode_types": MODE_TYPES, "mode_types": [t.value for t in ModeType],
"sigs": SIGS, "sigs": SIGS,
"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,
"callsign_data_providers": callsign_data_providers, "callsign_data_providers": callsign_data_providers,
"continents": CONTINENTS, "continents": [c.value for c in Continent],
"propagation_modes": list(PROPAGATION_MODES.values()), "propagation_modes": list(PROPAGATION_MODES.values()),
"max_spot_age": MAX_SPOT_AGE, "max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING, "spot_allowed": ALLOW_SPOTTING,
+1 -1
View File
@@ -2,7 +2,6 @@ import tornado
from tornado.httpclient import AsyncHTTPClient from tornado.httpclient import AsyncHTTPClient
from tornado.httputil import HTTPHeaders from tornado.httputil import HTTPHeaders
_LEGACY_PARAM_TO_HEADER_MAP = { _LEGACY_PARAM_TO_HEADER_MAP = {
"qrz_username": "X-QRZ-Username", "qrz_username": "X-QRZ-Username",
"qrz_password": "X-QRZ-Password", "qrz_password": "X-QRZ-Password",
@@ -12,6 +11,7 @@ _LEGACY_PARAM_TO_HEADER_MAP = {
"hamqth_session_id": "X-HamQTH-Session-ID", "hamqth_session_id": "X-HamQTH-Session-ID",
} }
class V1RedirectHandler(tornado.web.RequestHandler): class V1RedirectHandler(tornado.web.RequestHandler):
"""Transparently proxies requests from the old API to the new one, """Transparently proxies requests from the old API to the new one,
returning whatever the v2 endpoint returns, for endpoints with no breaking changes.""" returning whatever the v2 endpoint returns, for endpoints with no breaking changes."""
+1
View File
@@ -23,6 +23,7 @@ def _handle_legacy_params(handler):
if value: if value:
handler.request.headers[header] = value handler.request.headers[header] = value
class V1APISpotsHandler(APISpotsHandler): class V1APISpotsHandler(APISpotsHandler):
"""API request handler for /api/v1/spots (GET). Included in early Spothole v2 for backwards compatibility.""" """API request handler for /api/v1/spots (GET). Included in early Spothole v2 for backwards compatibility."""
-1
View File
@@ -10,7 +10,6 @@ from core.constants import SOFTWARE_VERSION
from core.data_providers import DATA_PROVIDERS from core.data_providers import DATA_PROVIDERS
from core.data_store import DATA_STORE from core.data_store import DATA_STORE
from core.status_reporter import StatusReporter from core.status_reporter import StatusReporter
from core.utils import infer_mode_from_comment
from server.webserver import WEB_SERVER from server.webserver import WEB_SERVER
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+45 -5
View File
@@ -19,7 +19,9 @@ info:
* Added DTMBA, FEA, BIWOTA, COTA & PGA SIGs * 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. * 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 ### 2.0
@@ -866,6 +868,14 @@ components:
- Toilets - Toilets
example: POTA example: POTA
SIGType:
type: string
enum:
- WORLDWIDE
- REGIONAL
- EVENT
example: WORLDWIDE
SIGNameIncludingNoSIG: SIGNameIncludingNoSIG:
oneOf: oneOf:
- $ref: "#/components/schemas/SIGName" - $ref: "#/components/schemas/SIGName"
@@ -873,6 +883,29 @@ components:
enum: [ NO_SIG ] enum: [ NO_SIG ]
example: POTA 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: Continent:
type: string type: string
enum: enum:
@@ -937,6 +970,7 @@ components:
- FSK - FSK
- PKT - PKT
- MSK144 - MSK144
- UNKNOWN
example: SSB example: SSB
ModeType: ModeType:
@@ -945,6 +979,7 @@ components:
- CW - CW
- PHONE - PHONE
- DATA - DATA
- UNKNOWN
example: CW example: CW
ModeSource: ModeSource:
@@ -981,7 +1016,7 @@ components:
- NONE - NONE
example: SPOT example: SPOT
LocationSourceForAlert: LocationSourceForCallsign:
type: string type: string
enum: enum:
- "HOME QTH" - "HOME QTH"
@@ -1004,9 +1039,8 @@ components:
description: SIG reference name description: SIG reference name
example: Null Country Park example: Null Country Park
ref_type: ref_type:
type: string
description: SIG reference type description: SIG reference type
example: Park $ref: "#/components/schemas/SIGRefType"
url: url:
type: string type: string
description: SIG reference URL, which the user can look up for more information description: SIG reference URL, which the user can look up for more information
@@ -1491,6 +1525,12 @@ components:
type: string type: string
description: The full name of the SIG description: The full name of the SIG
example: Parks on the Air 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: comment_names:
type: array type: array
description: > description: >
@@ -2197,7 +2237,7 @@ components:
Where we got the location (grid/latitude/longitude) from. Unlike a spot where we might 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 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. a location based purely on DXCC, or nothing.
$ref: "#/components/schemas/LocationSourceForAlert" $ref: "#/components/schemas/LocationSourceForCallsign"
GridLookup: GridLookup:
type: object type: object
+11 -4
View File
@@ -42,18 +42,25 @@ function setHamHFBandToggles() {
function generateSIGsMultiToggleFilterCard(sig_options) { function generateSIGsMultiToggleFilterCard(sig_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["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, ""); 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); $("#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["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, ""); 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); $("#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>'); $("#sig-options").append('<strong>Other</strong>');
// Bonus "NO_SIG" / "General DX" option // 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>`); $("#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>`);
+5 -3
View File
@@ -179,13 +179,15 @@
filters. They are also stored in your browser's local storage, so that your preferences are remembered between filters. They are also stored in your browser's local storage, so that your preferences are remembered between
sessions.</p> sessions.</p>
<p>The data you provide can optionally include your login credentials for QRZ.com and HamQTH. You can provide these <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 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 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 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> 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 <p>Any data you send as part of a query, such as your QRZ or HamQTH credentials, is used only for the lifetime of
query and is not saved anywhere apart from your own device.</p> that
query and is not saved anywhere apart from your own device.</p>
<p>Spothole uses no trackers, no ads, and no cookies.</p> <p>Spothole uses no trackers, no ads, and no cookies.</p>
{% if len(web_ui_options["support_button_html"]) > 0 %} {% 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. <p><strong>Caveat: </strong> The owner of this server has chosen to inject their own content into the "spots" page.
+3 -2
View File
@@ -2,7 +2,8 @@
{% block content %} {% block content %}
<div id="add-spot-intro-box" class="permanently-dismissible-box mt-3"> <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 <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 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 visible within Spothole and are not sent "upstream" to DX clusters or xOTA spotting sites. The functionality
@@ -76,7 +77,7 @@
</div> </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 () { <script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active"); $("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -84,7 +84,7 @@
</div> </div>
<script src="/static/js/alerts.js?v=1788209684"></script> <script src="/static/js/alerts.js?v=1788338286"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active"); $("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -76,8 +76,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1788209684"></script> <script src="/static/js/spotsbandsandmap.js?v=1788338285"></script>
<script src="/static/js/bands.js?v=1788209684"></script> <script src="/static/js/bands.js?v=1788338285"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-bands").addClass("active"); $("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+8 -7
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% 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/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/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-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 src="/static/vendor/js/tinycolor2-1.6.0.min.js"></script>
<script type="module"> <script type="module">
import { fetchEventSource } from '/static/vendor/js/fetch-event-source-2.0.1/index.js'; import {fetchEventSource} from '/static/vendor/js/fetch-event-source-2.0.1/index.js';
window.fetchEventSource = fetchEventSource;
window.fetchEventSource = fetchEventSource;
</script> </script>
<script src="/static/js/utils.js?v=1788209684"></script> <script src="/static/js/utils.js?v=1788338285"></script>
<script src="/static/js/ui-ham.js?v=1788209684"></script> <script src="/static/js/ui-ham.js?v=1788338285"></script>
<script src="/static/js/geo.js?v=1788209684"></script> <script src="/static/js/geo.js?v=1788338285"></script>
<script src="/static/js/common.js?v=1788209684"></script> <script src="/static/js/common.js?v=1788338285"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div> </div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script> <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 () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}"; const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
</script> </script>
<script src="/static/js/spotsbandsandmap.js?v=1788209683"></script> <script src="/static/js/spotsbandsandmap.js?v=1788338286"></script>
<script src="/static/js/map.js?v=1788209683"></script> <script src="/static/js/map.js?v=1788338286"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-map").addClass("active"); $("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1788209683"></script> <script src="/static/js/spotsbandsandmap.js?v=1788338285"></script>
<script src="/static/js/spots.js?v=1788209683"></script> <script src="/static/js/spots.js?v=1788338285"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-spots").addClass("active"); $("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -86,7 +86,7 @@
</div> </div>
</div> </div>
<script src="/static/js/status.js?v=1788209684"></script> <script src="/static/js/status.js?v=1788338285"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#nav-link-status").addClass("active"); $("#nav-link-status").addClass("active");