mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-10-27 16:59:25 +00:00
ZLOTA support + misc changes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ __pycache__
|
|||||||
/gma_ref_info_cache.sqlite
|
/gma_ref_info_cache.sqlite
|
||||||
/config.yml
|
/config.yml
|
||||||
/siota_data_cache.sqlite
|
/siota_data_cache.sqlite
|
||||||
|
/zlota_data_cache.sqlite
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from diskcache import Cache
|
from diskcache import Cache
|
||||||
from pyhamtools import LookupLib, Callinfo
|
from pyhamtools import LookupLib, Callinfo
|
||||||
@@ -48,35 +47,45 @@ def infer_band_from_freq(freq):
|
|||||||
# Infer a country name from a callsign
|
# Infer a country name from a callsign
|
||||||
def infer_country_from_callsign(call):
|
def infer_country_from_callsign(call):
|
||||||
try:
|
try:
|
||||||
return CALL_INFO_BASIC.get_country_name(call)
|
# Get base callsign, assuming this will be the longest of any /-separated sections.
|
||||||
|
base_call = max(call.split("/"), key=len)
|
||||||
|
return CALL_INFO_BASIC.get_country_name(base_call)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Infer a DXCC ID from a callsign
|
# Infer a DXCC ID from a callsign
|
||||||
def infer_dxcc_id_from_callsign(call):
|
def infer_dxcc_id_from_callsign(call):
|
||||||
try:
|
try:
|
||||||
return CALL_INFO_BASIC.get_adif_id(call)
|
# Get base callsign, assuming this will be the longest of any /-separated sections.
|
||||||
|
base_call = max(call.split("/"), key=len)
|
||||||
|
return CALL_INFO_BASIC.get_adif_id(base_call)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Infer a continent shortcode from a callsign
|
# Infer a continent shortcode from a callsign
|
||||||
def infer_continent_from_callsign(call):
|
def infer_continent_from_callsign(call):
|
||||||
try:
|
try:
|
||||||
return CALL_INFO_BASIC.get_continent(call)
|
# Get base callsign, assuming this will be the longest of any /-separated sections.
|
||||||
|
base_call = max(call.split("/"), key=len)
|
||||||
|
return CALL_INFO_BASIC.get_continent(base_call)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Infer a CQ zone from a callsign
|
# Infer a CQ zone from a callsign
|
||||||
def infer_cq_zone_from_callsign(call):
|
def infer_cq_zone_from_callsign(call):
|
||||||
try:
|
try:
|
||||||
return CALL_INFO_BASIC.get_cqz(call)
|
# Get base callsign, assuming this will be the longest of any /-separated sections.
|
||||||
|
base_call = max(call.split("/"), key=len)
|
||||||
|
return CALL_INFO_BASIC.get_cqz(base_call)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Infer a ITU zone from a callsign
|
# Infer a ITU zone from a callsign
|
||||||
def infer_itu_zone_from_callsign(call):
|
def infer_itu_zone_from_callsign(call):
|
||||||
try:
|
try:
|
||||||
return CALL_INFO_BASIC.get_ituz(call)
|
# Get base callsign, assuming this will be the longest of any /-separated sections.
|
||||||
|
base_call = max(call.split("/"), key=len)
|
||||||
|
return CALL_INFO_BASIC.get_ituz(base_call)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import logging
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
|
||||||
from requests_cache import CachedSession
|
from requests_cache import CachedSession
|
||||||
|
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
@@ -14,9 +13,12 @@ from spotproviders.http_spot_provider import HTTPSpotProvider
|
|||||||
class ParksNPeaks(HTTPSpotProvider):
|
class ParksNPeaks(HTTPSpotProvider):
|
||||||
POLL_INTERVAL_SEC = 120
|
POLL_INTERVAL_SEC = 120
|
||||||
SPOTS_URL = "https://www.parksnpeaks.org/api/ALL"
|
SPOTS_URL = "https://www.parksnpeaks.org/api/ALL"
|
||||||
SIOTA_CSV_URL = "https://www.silosontheair.com/data/silos.csv"
|
SIOTA_LIST_URL = "https://www.silosontheair.com/data/silos.csv"
|
||||||
SIOTA_CSV_CACHE_TIME_DAYS = 30
|
SIOTA_LIST_CACHE_TIME_DAYS = 30
|
||||||
SIOTA_CSV_CACHE = CachedSession("siota_data_cache", expire_after=timedelta(days=SIOTA_CSV_CACHE_TIME_DAYS))
|
SIOTA_LIST_CACHE = CachedSession("siota_data_cache", expire_after=timedelta(days=SIOTA_LIST_CACHE_TIME_DAYS))
|
||||||
|
ZLOTA_LIST_URL = "https://ontheair.nz/assets/assets.json"
|
||||||
|
ZLOTA_LIST_CACHE_TIME_DAYS = 30
|
||||||
|
ZLOTA_LIST_CACHE = CachedSession("zlota_data_cache", expire_after=timedelta(days=ZLOTA_LIST_CACHE_TIME_DAYS))
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
||||||
@@ -43,6 +45,8 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
# PNP supports a bunch of programs which should have different icons
|
# PNP supports a bunch of programs which should have different icons
|
||||||
if spot.sig == "SiOTA":
|
if spot.sig == "SiOTA":
|
||||||
spot.icon = "wheat-awn"
|
spot.icon = "wheat-awn"
|
||||||
|
elif spot.sig == "ZLOTA":
|
||||||
|
spot.icon = "kiwi-bird"
|
||||||
elif spot.sig in ["POTA", "SOTA", "WWFF"]:
|
elif spot.sig in ["POTA", "SOTA", "WWFF"]:
|
||||||
# Don't care about an icon as this will be rejected anyway, we have better data from POTA/SOTA/WWFF direct
|
# Don't care about an icon as this will be rejected anyway, we have better data from POTA/SOTA/WWFF direct
|
||||||
spot.icon = ""
|
spot.icon = ""
|
||||||
@@ -54,16 +58,27 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
|
|
||||||
# SiOTA lat/lon/grid lookup
|
# SiOTA lat/lon/grid lookup
|
||||||
if spot.sig == "SiOTA":
|
if spot.sig == "SiOTA":
|
||||||
siota_csv_data = self.SIOTA_CSV_CACHE.get(self.SIOTA_CSV_URL, headers=self.HTTP_HEADERS)
|
siota_csv_data = self.SIOTA_LIST_CACHE.get(self.SIOTA_LIST_URL, headers=self.HTTP_HEADERS)
|
||||||
siota_dr = csv.DictReader(siota_csv_data.content.decode().splitlines())
|
siota_dr = csv.DictReader(siota_csv_data.content.decode().splitlines())
|
||||||
for row in siota_dr:
|
for row in siota_dr:
|
||||||
if row["SILO_CODE"] == spot.sig_refs[0]:
|
if row["SILO_CODE"] == spot.sig_refs[0]:
|
||||||
spot.dx_country = row["COUNTRY"]
|
|
||||||
spot.latitude = float(row["LAT"])
|
spot.latitude = float(row["LAT"])
|
||||||
spot.longitude = float(row["LON"])
|
spot.longitude = float(row["LON"])
|
||||||
spot.grid = row["LOCATOR"]
|
spot.grid = row["LOCATOR"]
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# ZLOTA name/lat/lon lookup
|
||||||
|
if spot.sig == "ZLOTA":
|
||||||
|
zlota_data = self.ZLOTA_LIST_CACHE.get(self.ZLOTA_LIST_URL, headers=self.HTTP_HEADERS).json()
|
||||||
|
for asset in zlota_data:
|
||||||
|
if asset["code"] == spot.sig_refs[0]:
|
||||||
|
spot.sig_refs_names = [asset["name"]]
|
||||||
|
spot.latitude = asset["y"]
|
||||||
|
spot.longitude = asset["x"]
|
||||||
|
# Junk the "DE call", PNP always returns "ZLOTA" as the spotter for ZLOTA spots
|
||||||
|
spot.de_call = None
|
||||||
|
break
|
||||||
|
|
||||||
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
|
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
|
||||||
# the spot list.
|
# the spot list.
|
||||||
if spot.sig not in ["POTA", "SOTA", "WWFF"]:
|
if spot.sig not in ["POTA", "SOTA", "WWFF"]:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<p class="d-inline-flex gap-1">
|
<p class="d-inline-flex gap-1">
|
||||||
<button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button">Filters</button>
|
<button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button"><i class="fa-solid fa-filter"></i> Filters</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<p class="d-inline-flex gap-1">
|
<p class="d-inline-flex gap-1">
|
||||||
<button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button">Filters</button>
|
<button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button"><i class="fa-solid fa-filter"></i> Filters</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ function updateTable() {
|
|||||||
sig_refs = s["sig_refs"].map(s => `<span class='nowrap'>${s}</span>`).join(", ");
|
sig_refs = s["sig_refs"].map(s => `<span class='nowrap'>${s}</span>`).join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format sig_refs title
|
||||||
|
var sig_refs_title_string = "";
|
||||||
|
if (s["sig_refs_names"]) {
|
||||||
|
sig_refs_title_string = " title=\"" + s["sig_refs_names"].join(", ") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
// Format DE flag
|
// Format DE flag
|
||||||
var de_flag = "<i class='fa-solid fa-circle-question'></i>";
|
var de_flag = "<i class='fa-solid fa-circle-question'></i>";
|
||||||
if (s["de_flag"] && s["de_flag"] != null && s["de_flag"] != "") {
|
if (s["de_flag"] && s["de_flag"] != null && s["de_flag"] != "") {
|
||||||
@@ -123,6 +129,13 @@ function updateTable() {
|
|||||||
de_country = "Unknown or not a country";
|
de_country = "Unknown or not a country";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format de call
|
||||||
|
var de_call = s["de_call"];
|
||||||
|
if (de_call == null) {
|
||||||
|
de_call = "";
|
||||||
|
de_flag = "";
|
||||||
|
}
|
||||||
|
|
||||||
// CSS doesn't like classes with decimal points in, so we need to replace that in the same way as when we originally
|
// CSS doesn't like classes with decimal points in, so we need to replace that in the same way as when we originally
|
||||||
// queried the options endpoint and set our CSS.
|
// queried the options endpoint and set our CSS.
|
||||||
var cssFormattedBandName = s['band'] ? s['band'].replace('.', 'p') : "unknown";
|
var cssFormattedBandName = s['band'] ? s['band'].replace('.', 'p') : "unknown";
|
||||||
@@ -135,8 +148,8 @@ function updateTable() {
|
|||||||
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
|
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
|
||||||
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
|
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
|
||||||
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${sigSourceText}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${sigSourceText}</td>`);
|
||||||
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
|
$tr.append(`<td class='hideonmobile'><span ${sig_refs_title_string}>${sig_refs}</span></td>`);
|
||||||
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${s["de_call"]}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
|
||||||
table.find('tbody').append($tr);
|
table.find('tbody').append($tr);
|
||||||
|
|
||||||
// Second row for mobile view only, containing source, ref & comment
|
// Second row for mobile view only, containing source, ref & comment
|
||||||
|
|||||||
Reference in New Issue
Block a user