mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-02-04 17:24:30 +00:00
Compare commits
14 Commits
3-sse-endp
...
88-colour-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f31148686d | ||
|
|
a444be8fe9 | ||
|
|
3f117a47d6 | ||
|
|
06d582ae2d | ||
|
|
5bf45dba46 | ||
|
|
f4ae6b610e | ||
|
|
6af15e4cfd | ||
|
|
6d9bf3d4ec | ||
|
|
9b737a8176 | ||
|
|
05bc65337f | ||
|
|
d2c1dbb377 | ||
|
|
6cf1b38355 | ||
|
|
ac566553d8 | ||
|
|
bcc40d1416 |
@@ -30,7 +30,7 @@ URL parameters can be used to trigger an "embedded" mode which hides the headers
|
|||||||
|
|
||||||
Setting `embedded` to true is important for the rest of the settings to be applied; otherwise, the user's defaults will be used in preference to the URL params.
|
Setting `embedded` to true is important for the rest of the settings to be applied; otherwise, the user's defaults will be used in preference to the URL params.
|
||||||
|
|
||||||
These are supplied with the URL to the page you want to embed, for example for an embedded version of the band map in dark mode, use `https://spothole.com/bands?embedded=true&dark-mode=true`. For an embedded version of the main spots/home page in the system light/dark mode, use `https://spothole.com/?embedded=true`. For dark mode showing 70cm TOTA spots only, use `https://spothole.com/?embedded=true&dark-mode=true&filter-sigs=TOTA&filter-bands=70cm`. Providing no URL params causes the page to be loaded in the normal way it would when accessed directly in the user's browser.
|
These are supplied with the URL to the page you want to embed, for example for an embedded version of the band map in dark mode, use `https://spothole.app/bands?embedded=true&dark-mode=true`. For an embedded version of the main spots/home page in the system light/dark mode, use `https://spothole.app/?embedded=true`. For dark mode showing 70cm TOTA spots only, use `https://spothole.app/?embedded=true&dark-mode=true&sig=TOTA&band=70cm`. Providing no URL params causes the page to be loaded in the normal way it would when accessed directly in the user's browser.
|
||||||
|
|
||||||
The supported parameters are as follows. Generally these match the equivalent parameters in the real Spothole API, where a mapping exists.
|
The supported parameters are as follows. Generally these match the equivalent parameters in the real Spothole API, where a mapping exists.
|
||||||
|
|
||||||
@@ -157,6 +157,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
add_header Access-Control-Allow-Origin $xssorigin;
|
add_header Access-Control-Allow-Origin $xssorigin;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
proxy_pass http://127.0.0.1:8080;
|
proxy_pass http://127.0.0.1:8080;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,6 +221,7 @@ To navigate your way around the source code, this list may help.
|
|||||||
|
|
||||||
* `/` - Main script (`spothole.py`), pip `requirements.txt`, config, README, etc.
|
* `/` - Main script (`spothole.py`), pip `requirements.txt`, config, README, etc.
|
||||||
* `/images` - Image sources
|
* `/images` - Image sources
|
||||||
|
* `/datafiles` - Local data sources (differentiated from the majority of data files which are loaded from URLs and cached in `/cache`)
|
||||||
* `/cache` - Directory where static-ish data downloaded from the internet is cached to avoid rapid re-requests, and where spot/alert data is cached so that it survives a software restart. Created on first run.
|
* `/cache` - Directory where static-ish data downloaded from the internet is cached to avoid rapid re-requests, and where spot/alert data is cached so that it survives a software restart. Created on first run.
|
||||||
|
|
||||||
### Extending the server
|
### Extending the server
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ class NG3K(HTTPAlertProvider):
|
|||||||
dx_country=dx_country,
|
dx_country=dx_country,
|
||||||
freqs_modes=bands + (("; " + modes) if modes != "" else ""),
|
freqs_modes=bands + (("; " + modes) if modes != "" else ""),
|
||||||
comment=by + "; " + comment + "; " + qsl_info,
|
comment=by + "; " + comment + "; " + qsl_info,
|
||||||
icon="globe-africa",
|
|
||||||
start_time=start_timestamp,
|
start_time=start_timestamp,
|
||||||
end_time=end_timestamp,
|
end_time=end_timestamp,
|
||||||
is_dxpedition=True)
|
is_dxpedition=True)
|
||||||
|
|||||||
@@ -86,12 +86,11 @@ spot-providers:
|
|||||||
name: "39C3 TOTA"
|
name: "39C3 TOTA"
|
||||||
enabled: false
|
enabled: false
|
||||||
url: "wss://dev.39c3.totawatch.de/api/spot/live"
|
url: "wss://dev.39c3.totawatch.de/api/spot/live"
|
||||||
# Fixed SIG/latitude/longitude for all spots from a provider is currently only a feature for the "XOTA" provider,
|
# Fixed SIG for all spots from a provider & location CSV are currently only a feature for the "XOTA" provider,
|
||||||
# the software found at https://github.com/nischu/xOTA/. This is because this is a generic backend for xOTA
|
# the software found at https://github.com/nischu/xOTA/. This is because this is a generic backend for xOTA
|
||||||
# programmes and so different URLs provide different programmes.
|
# programmes and so different URLs provide different programmes.
|
||||||
sig: "TOTA"
|
sig: "TOTA"
|
||||||
latitude: 53.5622678
|
locations-csv: "datafiles/39c3-tota.csv"
|
||||||
longitude: 9.9855205
|
|
||||||
|
|
||||||
|
|
||||||
# Alert providers to use. Same setup as the spot providers list above.
|
# Alert providers to use. Same setup as the spot providers list above.
|
||||||
|
|||||||
@@ -12,25 +12,25 @@ HAMQTH_PRG = (SOFTWARE_NAME + " v" + SOFTWARE_VERSION + " operated by " + SERVER
|
|||||||
|
|
||||||
# Special Interest Groups
|
# Special Interest Groups
|
||||||
SIGS = [
|
SIGS = [
|
||||||
SIG(name="POTA", description="Parks on the Air", icon="tree", ref_regex=r"[A-Z]{2}\-\d{4,5}"),
|
SIG(name="POTA", description="Parks on the Air", ref_regex=r"[A-Z]{2}\-\d{4,5}"),
|
||||||
SIG(name="SOTA", description="Summits on the Air", icon="mountain-sun", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
|
SIG(name="SOTA", description="Summits on the Air", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
|
||||||
SIG(name="WWFF", description="World Wide Flora & Fauna", icon="seedling", ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}"),
|
SIG(name="WWFF", description="World Wide Flora & Fauna", ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}"),
|
||||||
SIG(name="GMA", description="Global Mountain Activity", icon="person-hiking", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
|
SIG(name="GMA", description="Global Mountain Activity", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
|
||||||
SIG(name="WWBOTA", description="Worldwide Bunkers on the Air", icon="radiation", ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}"),
|
SIG(name="WWBOTA", description="Worldwide Bunkers on the Air", ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}"),
|
||||||
SIG(name="HEMA", description="HuMPs Excluding Marilyns Award", icon="mound", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"),
|
SIG(name="HEMA", description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"),
|
||||||
SIG(name="IOTA", description="Islands on the Air", icon="umbrella-beach", ref_regex=r"[A-Z]{2}\-\d{3}"),
|
SIG(name="IOTA", description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}"),
|
||||||
SIG(name="MOTA", description="Mills on the Air", icon="fan", ref_regex=r"X\d{4-6}"),
|
SIG(name="MOTA", description="Mills on the Air", ref_regex=r"X\d{4-6}"),
|
||||||
SIG(name="ARLHS", description="Amateur Radio Lighthouse Society", icon="tower-observation", ref_regex=r"[A-Z]{3}\-\d{3,4}"),
|
SIG(name="ARLHS", description="Amateur Radio Lighthouse Society", ref_regex=r"[A-Z]{3}\-\d{3,4}"),
|
||||||
SIG(name="ILLW", description="International Lighthouse & Lightship Weekend", icon="tower-observation", ref_regex=r"[A-Z]{2}\d{4}"),
|
SIG(name="ILLW", description="International Lighthouse & Lightship Weekend", ref_regex=r"[A-Z]{2}\d{4}"),
|
||||||
SIG(name="SIOTA", description="Silos on the Air", icon="wheat-awn", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"),
|
SIG(name="SIOTA", description="Silos on the Air", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"),
|
||||||
SIG(name="WCA", description="World Castles Award", icon="chess-rook", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}"),
|
SIG(name="WCA", description="World Castles Award", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}"),
|
||||||
SIG(name="ZLOTA", description="New Zealand on the Air", icon="kiwi-bird", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}"),
|
SIG(name="ZLOTA", description="New Zealand on the Air", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}"),
|
||||||
SIG(name="WOTA", description="Wainwrights on the Air", icon="w", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
|
SIG(name="WOTA", description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
|
||||||
SIG(name="BOTA", description="Beaches on the Air", icon="water"),
|
SIG(name="BOTA", description="Beaches on the Air"),
|
||||||
SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award", icon="earth-oceania"),
|
SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award"),
|
||||||
SIG(name="WAB", description="Worked All Britain", icon="table-cells-large", ref_regex=r"[A-Z]{1,2}[0-9]{2}"),
|
SIG(name="WAB", description="Worked All Britain", ref_regex=r"[A-Z]{1,2}[0-9]{2}"),
|
||||||
SIG(name="WAI", description="Worked All Ireland", icon="table-cells-large", ref_regex=r"[A-Z][0-9]{2}"),
|
SIG(name="WAI", description="Worked All Ireland", ref_regex=r"[A-Z][0-9]{2}"),
|
||||||
SIG(name="TOTA", description="Toilets on the Air", icon="toilet", ref_regex=r"T\-[0-9]{2}")
|
SIG(name="TOTA", description="Toilets on the Air", ref_regex=r"T\-[0-9]{2}")
|
||||||
]
|
]
|
||||||
|
|
||||||
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".
|
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".
|
||||||
@@ -42,33 +42,33 @@ MODE_TYPES = ["CW", "PHONE", "DATA"]
|
|||||||
|
|
||||||
# Band definitions
|
# Band definitions
|
||||||
BANDS = [
|
BANDS = [
|
||||||
Band(name="2200m", start_freq=135700, end_freq=137800, color="#ff4500", contrast_color="white"),
|
Band(name="2200m", start_freq=135700, end_freq=137800),
|
||||||
Band(name="600m", start_freq=472000, end_freq=479000, color="#1e90ff", contrast_color="white"),
|
Band(name="600m", start_freq=472000, end_freq=479000),
|
||||||
Band(name="160m", start_freq=1800000, end_freq=2000000, color="#7cfc00", contrast_color="black"),
|
Band(name="160m", start_freq=1800000, end_freq=2000000),
|
||||||
Band(name="80m", start_freq=3500000, end_freq=4000000, color="#e550e5", contrast_color="black"),
|
Band(name="80m", start_freq=3500000, end_freq=4000000),
|
||||||
Band(name="60m", start_freq=5250000, end_freq=5410000, color="#00008b", contrast_color="white"),
|
Band(name="60m", start_freq=5250000, end_freq=5410000),
|
||||||
Band(name="40m", start_freq=7000000, end_freq=7300000, color="#5959ff", contrast_color="white"),
|
Band(name="40m", start_freq=7000000, end_freq=7300000),
|
||||||
Band(name="30m", start_freq=10100000, end_freq=10150000, color="#62d962", contrast_color="black"),
|
Band(name="30m", start_freq=10100000, end_freq=10150000),
|
||||||
Band(name="20m", start_freq=14000000, end_freq=14350000, color="#f2c40c", contrast_color="black"),
|
Band(name="20m", start_freq=14000000, end_freq=14350000),
|
||||||
Band(name="17m", start_freq=18068000, end_freq=18168000, color="#f2f261", contrast_color="black"),
|
Band(name="17m", start_freq=18068000, end_freq=18168000),
|
||||||
Band(name="15m", start_freq=21000000, end_freq=21450000, color="#cca166", contrast_color="black"),
|
Band(name="15m", start_freq=21000000, end_freq=21450000),
|
||||||
Band(name="12m", start_freq=24890000, end_freq=24990000, color="#b22222", contrast_color="white"),
|
Band(name="12m", start_freq=24890000, end_freq=24990000),
|
||||||
Band(name="11m", start_freq=26965000, end_freq=27405000, color="#00ff00", contrast_color="black"),
|
Band(name="11m", start_freq=26965000, end_freq=27405000),
|
||||||
Band(name="10m", start_freq=28000000, end_freq=29700000, color="#ff69b4", contrast_color="black"),
|
Band(name="10m", start_freq=28000000, end_freq=29700000),
|
||||||
Band(name="6m", start_freq=50000000, end_freq=54000000, color="#FF0000", contrast_color="white"),
|
Band(name="6m", start_freq=50000000, end_freq=54000000),
|
||||||
Band(name="5m", start_freq=56000000, end_freq=60500000, color="#e0e0e0", contrast_color="black"),
|
Band(name="5m", start_freq=56000000, end_freq=60500000),
|
||||||
Band(name="4m", start_freq=70000000, end_freq=70500000, color="#cc0044", contrast_color="white"),
|
Band(name="4m", start_freq=70000000, end_freq=70500000),
|
||||||
Band(name="2m", start_freq=144000000, end_freq=148000000, color="#FF1493", contrast_color="black"),
|
Band(name="2m", start_freq=144000000, end_freq=148000000),
|
||||||
Band(name="1.25m", start_freq=219000000, end_freq=225000000, color="#CCFF00", contrast_color="black"),
|
Band(name="1.25m", start_freq=219000000, end_freq=225000000),
|
||||||
Band(name="70cm", start_freq=420000000, end_freq=450000000, color="#999900", contrast_color="white"),
|
Band(name="70cm", start_freq=420000000, end_freq=450000000),
|
||||||
Band(name="23cm", start_freq=1240000000, end_freq=1325000000, color="#5AB8C7", contrast_color="black"),
|
Band(name="23cm", start_freq=1240000000, end_freq=1325000000),
|
||||||
Band(name="2.4GHz", start_freq=2300000000, end_freq=2450000000, color="#FF7F50", contrast_color="black"),
|
Band(name="13cm", start_freq=2300000000, end_freq=2450000000),
|
||||||
Band(name="5.8GHz", start_freq=5725000000, end_freq=5850000000, color="#cc0099", contrast_color="white"),
|
Band(name="5.8GHz", start_freq=5725000000, end_freq=5850000000),
|
||||||
Band(name="10GHz", start_freq=10000000000, end_freq=10500000000, color="#696969", contrast_color="white"),
|
Band(name="10GHz", start_freq=10000000000, end_freq=10500000000),
|
||||||
Band(name="24GHz", start_freq=24000000000, end_freq=24050000000, color="#f3edc6", contrast_color="black"),
|
Band(name="24GHz", start_freq=24000000000, end_freq=24050000000),
|
||||||
Band(name="47GHz", start_freq=47000000000, end_freq=47200000000, color="#ffe786", contrast_color="black"),
|
Band(name="47GHz", start_freq=47000000000, end_freq=47200000000),
|
||||||
Band(name="76GHz", start_freq=75500000000, end_freq=81500000000, color="#baf9d8", contrast_color="black")]
|
Band(name="76GHz", start_freq=75500000000, end_freq=81500000000)]
|
||||||
UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0, color="black", contrast_color="white")
|
UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0)
|
||||||
|
|
||||||
# Continents
|
# Continents
|
||||||
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"]
|
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"]
|
||||||
|
|||||||
@@ -8,14 +8,6 @@ from core.constants import SIGS, HTTP_HEADERS
|
|||||||
from core.geo_utils import wab_wai_square_to_lat_lon
|
from core.geo_utils import wab_wai_square_to_lat_lon
|
||||||
|
|
||||||
|
|
||||||
# Utility function to get the icon for a named SIG. If no match is found, the "circle-question" icon will be returned.
|
|
||||||
def get_icon_for_sig(sig):
|
|
||||||
for s in SIGS:
|
|
||||||
if s.name == sig:
|
|
||||||
return s.icon
|
|
||||||
return "circle-question"
|
|
||||||
|
|
||||||
|
|
||||||
# Utility function to get the regex string for a SIG reference for a named SIG. If no match is found, None will be returned.
|
# Utility function to get the regex string for a SIG reference for a named SIG. If no match is found, None will be returned.
|
||||||
def get_ref_regex_for_sig(sig):
|
def get_ref_regex_for_sig(sig):
|
||||||
for s in SIGS:
|
for s in SIGS:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.lookup_helper import lookup_helper
|
from core.lookup_helper import lookup_helper
|
||||||
from core.sig_utils import get_icon_for_sig, populate_sig_ref_info
|
from core.sig_utils import populate_sig_ref_info
|
||||||
|
|
||||||
|
|
||||||
# Data class that defines an alert.
|
# Data class that defines an alert.
|
||||||
@@ -55,8 +55,6 @@ class Alert:
|
|||||||
sig_refs: list = None
|
sig_refs: list = None
|
||||||
# Activation score. SOTA only
|
# Activation score. SOTA only
|
||||||
activation_score: int = None
|
activation_score: int = None
|
||||||
# Icon, from the Font Awesome set. This is fairly opinionated but is here to help the alerthole web UI and Field alertter. Does not include the "fa-" prefix.
|
|
||||||
icon: str = None
|
|
||||||
# Whether this alert is for a DXpedition, as opposed to e.g. an xOTA programme.
|
# Whether this alert is for a DXpedition, as opposed to e.g. an xOTA programme.
|
||||||
is_dxpedition: bool = False
|
is_dxpedition: bool = False
|
||||||
# Where we got the alert from, e.g. "POTA", "SOTA"...
|
# Where we got the alert from, e.g. "POTA", "SOTA"...
|
||||||
@@ -109,10 +107,6 @@ class Alert:
|
|||||||
if self.sig_refs and len(self.sig_refs) > 0 and not self.sig:
|
if self.sig_refs and len(self.sig_refs) > 0 and not self.sig:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.sig = self.sig_refs[0].sig
|
||||||
|
|
||||||
# Icon from SIG
|
|
||||||
if self.sig and not self.icon:
|
|
||||||
self.icon = get_icon_for_sig(self.sig)
|
|
||||||
|
|
||||||
# DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from
|
# DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from
|
||||||
# the actual alertting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon instead of
|
# the actual alertting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon instead of
|
||||||
# the one from the park reference they're at.
|
# the one from the park reference they're at.
|
||||||
|
|||||||
@@ -9,7 +9,3 @@ class Band:
|
|||||||
start_freq: float
|
start_freq: float
|
||||||
# Stop frequency, in Hz
|
# Stop frequency, in Hz
|
||||||
end_freq: float
|
end_freq: float
|
||||||
# Colour to use for this band, as per PSK Reporter
|
|
||||||
color: str
|
|
||||||
# Contrast colour to use for text against a background of the band colour
|
|
||||||
contrast_color: str
|
|
||||||
@@ -7,8 +7,5 @@ class SIG:
|
|||||||
name: str
|
name: str
|
||||||
# Description, e.g. "Parks on the Air"
|
# Description, e.g. "Parks on the Air"
|
||||||
description: str
|
description: str
|
||||||
# Icon to use for it, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI
|
|
||||||
# and Field Spotter. Does not include the "fa-" prefix.
|
|
||||||
icon: str
|
|
||||||
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
|
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
|
||||||
ref_regex: str = None
|
ref_regex: str = None
|
||||||
22
data/spot.py
22
data/spot.py
@@ -11,7 +11,7 @@ from pyhamtools.locator import locator_to_latlong, latlong_to_locator
|
|||||||
|
|
||||||
from core.config import MAX_SPOT_AGE
|
from core.config import MAX_SPOT_AGE
|
||||||
from core.lookup_helper import lookup_helper
|
from core.lookup_helper import lookup_helper
|
||||||
from core.sig_utils import get_icon_for_sig, populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
|
from core.sig_utils import populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
|
||||||
from data.sig_ref import SIGRef
|
from data.sig_ref import SIGRef
|
||||||
|
|
||||||
|
|
||||||
@@ -109,16 +109,6 @@ class Spot:
|
|||||||
# Activation score. SOTA only
|
# Activation score. SOTA only
|
||||||
activation_score: int = None
|
activation_score: int = None
|
||||||
|
|
||||||
# Display guidance (optional)
|
|
||||||
|
|
||||||
# Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field
|
|
||||||
# Spotter. Does not include the "fa-" prefix.
|
|
||||||
icon: str = None
|
|
||||||
# Colour to represent this spot, if a client chooses to colour spots based on their frequency band, using PSK
|
|
||||||
# Reporter's default colours. HTML colour e.g. hex. A contrast colour is also provided which will be black or white.
|
|
||||||
band_color: str = None
|
|
||||||
band_contrast_color: str = None
|
|
||||||
|
|
||||||
# Timing info
|
# Timing info
|
||||||
|
|
||||||
# Time of the spot, UTC seconds since UNIX epoch
|
# Time of the spot, UTC seconds since UNIX epoch
|
||||||
@@ -214,8 +204,6 @@ class Spot:
|
|||||||
if self.freq and not self.band:
|
if self.freq and not self.band:
|
||||||
band = lookup_helper.infer_band_from_freq(self.freq)
|
band = lookup_helper.infer_band_from_freq(self.freq)
|
||||||
self.band = band.name
|
self.band = band.name
|
||||||
self.band_color = band.color
|
|
||||||
self.band_contrast_color = band.contrast_color
|
|
||||||
|
|
||||||
# Mode from comments or bandplan
|
# Mode from comments or bandplan
|
||||||
if self.mode:
|
if self.mode:
|
||||||
@@ -296,14 +284,6 @@ class Spot:
|
|||||||
if self.sig_refs and len(self.sig_refs) > 0 and not self.sig:
|
if self.sig_refs and len(self.sig_refs) > 0 and not self.sig:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.sig = self.sig_refs[0].sig
|
||||||
|
|
||||||
# Icon from SIG if we have one
|
|
||||||
if self.sig:
|
|
||||||
self.icon = get_icon_for_sig(self.sig)
|
|
||||||
|
|
||||||
# Default "radio" icon if nothing else has set it
|
|
||||||
if not self.icon:
|
|
||||||
self.icon = "tower-cell"
|
|
||||||
|
|
||||||
# DX Grid to lat/lon and vice versa in case one is missing
|
# DX Grid to lat/lon and vice versa in case one is missing
|
||||||
if self.dx_grid and not self.dx_latitude:
|
if self.dx_grid and not self.dx_latitude:
|
||||||
ll = locator_to_latlong(self.dx_grid)
|
ll = locator_to_latlong(self.dx_grid)
|
||||||
|
|||||||
18
datafiles/39c3-tota.csv
Normal file
18
datafiles/39c3-tota.csv
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
ref,lat,lon
|
||||||
|
T-01,53.56278090617755,9.984341869295505
|
||||||
|
T-02,53.562383404176416,9.98551893027115
|
||||||
|
T-03,53.56170184391514,9.985416035619778
|
||||||
|
T-04,53.562026534393176,9.986372919078974
|
||||||
|
T-11,53.56284641242506,9.98475590239655
|
||||||
|
T-12,53.562431705517035,9.98551675702443
|
||||||
|
T-13,53.56223704898424,9.985774520335664
|
||||||
|
T-14,53.5617893512591,9.986344302837976
|
||||||
|
T-21,53.56284641242506,9.98475590239655
|
||||||
|
T-22,53.56245816412497,9.985456089490567
|
||||||
|
T-23,53.56199560857136,9.985636761412673
|
||||||
|
T-24,53.5617893512591,9.986344302837976
|
||||||
|
T-31,53.56247470064887,9.985611427551902
|
||||||
|
T-32,53.5617893512591,9.986344302837976
|
||||||
|
T-41,53.56245039134992,9.985486136112701
|
||||||
|
T-91,53.56147934973529,9.984626806439744
|
||||||
|
T-92,53.561396810300735,9.987553052152899
|
||||||
|
@@ -53,6 +53,11 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
|||||||
self.sse_alert_queues = sse_alert_queues
|
self.sse_alert_queues = sse_alert_queues
|
||||||
self.web_server_metrics = web_server_metrics
|
self.web_server_metrics = web_server_metrics
|
||||||
|
|
||||||
|
# Custom headers to avoid e.g. nginx reverse proxy from buffering SSE data
|
||||||
|
def custom_headers(self):
|
||||||
|
return {"Cache-Control": "no-store",
|
||||||
|
"X-Accel-Buffering": "no"}
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
try:
|
try:
|
||||||
# Metrics
|
# Metrics
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
|||||||
self.sse_spot_queues = sse_spot_queues
|
self.sse_spot_queues = sse_spot_queues
|
||||||
self.web_server_metrics = web_server_metrics
|
self.web_server_metrics = web_server_metrics
|
||||||
|
|
||||||
|
# Custom headers to avoid e.g. nginx reverse proxy from buffering SSE data
|
||||||
|
def custom_headers(self):
|
||||||
|
return {"Cache-Control": "no-store",
|
||||||
|
"X-Accel-Buffering": "no"}
|
||||||
|
|
||||||
# Called once on the client opening a connection, set things up
|
# Called once on the client opening a connection, set things up
|
||||||
def open(self):
|
def open(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import tornado
|
import tornado
|
||||||
from prometheus_client.openmetrics.exposition import CONTENT_TYPE_LATEST
|
from prometheus_client import CONTENT_TYPE_LATEST
|
||||||
|
|
||||||
from core.prometheus_metrics_handler import get_metrics
|
from core.prometheus_metrics_handler import get_metrics
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ class APRSIS(SpotProvider):
|
|||||||
comment=data["comment"] if "comment" in data else None,
|
comment=data["comment"] if "comment" in data else None,
|
||||||
dx_latitude=data["latitude"] if "latitude" in data else None,
|
dx_latitude=data["latitude"] if "latitude" in data else None,
|
||||||
dx_longitude=data["longitude"] if "longitude" in data else None,
|
dx_longitude=data["longitude"] if "longitude" in data else None,
|
||||||
icon="tower-cell",
|
|
||||||
time=datetime.now(pytz.UTC).timestamp()) # APRS-IS spots are live so we can assume spot time is "now"
|
time=datetime.now(pytz.UTC).timestamp()) # APRS-IS spots are live so we can assume spot time is "now"
|
||||||
|
|
||||||
# Add to our list
|
# Add to our list
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ class DXCluster(SpotProvider):
|
|||||||
de_call=match.group(1),
|
de_call=match.group(1),
|
||||||
freq=float(match.group(2)) * 1000,
|
freq=float(match.group(2)) * 1000,
|
||||||
comment=match.group(4).strip(),
|
comment=match.group(4).strip(),
|
||||||
icon="tower-cell",
|
|
||||||
time=spot_datetime.timestamp())
|
time=spot_datetime.timestamp())
|
||||||
|
|
||||||
# Add to our list
|
# Add to our list
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class RBN(SpotProvider):
|
|||||||
de_call=match.group(1),
|
de_call=match.group(1),
|
||||||
freq=float(match.group(2)) * 1000,
|
freq=float(match.group(2)) * 1000,
|
||||||
comment=match.group(4).strip(),
|
comment=match.group(4).strip(),
|
||||||
icon="tower-cell",
|
|
||||||
time=spot_datetime.timestamp())
|
time=spot_datetime.timestamp())
|
||||||
|
|
||||||
# Add to our list
|
# Add to our list
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class UKPacketNet(HTTPSpotProvider):
|
|||||||
freq=freq,
|
freq=freq,
|
||||||
mode="PKT",
|
mode="PKT",
|
||||||
comment=comment,
|
comment=comment,
|
||||||
icon="tower-cell",
|
|
||||||
time=datetime.strptime(heard["lastHeard"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp(),
|
time=datetime.strptime(heard["lastHeard"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp(),
|
||||||
de_grid=node["location"]["locator"] if "locator" in node["location"] else None,
|
de_grid=node["location"]["locator"] if "locator" in node["location"] else None,
|
||||||
de_latitude=node["location"]["coords"]["lat"],
|
de_latitude=node["location"]["coords"]["lat"],
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import csv
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
@@ -9,31 +11,45 @@ from spotproviders.websocket_spot_provider import WebsocketSpotProvider
|
|||||||
|
|
||||||
|
|
||||||
# Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
|
# Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
|
||||||
# The provider typically doesn't give us a lat/lon or SIG explicitly, so our own config provides this information. This
|
# The provider typically doesn't give us a lat/lon or SIG explicitly, so our own config provides a SIG and a reference
|
||||||
# functionality is implemented for TOTA events.
|
# to a local CSV file with location information. This functionality is implemented for TOTA events, of which there are
|
||||||
|
# several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA and hence which server
|
||||||
|
# supplied the data, which is why the CSV location lookup is here and not in sig_utils.
|
||||||
class XOTA(WebsocketSpotProvider):
|
class XOTA(WebsocketSpotProvider):
|
||||||
FIXED_LATITUDE = None
|
LOCATION_DATA = {}
|
||||||
FIXED_LONGITUDE = None
|
|
||||||
SIG = None
|
SIG = None
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(provider_config, provider_config["url"])
|
super().__init__(provider_config, provider_config["url"])
|
||||||
self.FIXED_LATITUDE = provider_config["latitude"] if "latitude" in provider_config else None
|
locations_csv = provider_config["locations-csv"] if "locations-csv" in provider_config else None
|
||||||
self.FIXED_LONGITUDE = provider_config["longitude"] if "longitude" in provider_config else None
|
|
||||||
self.SIG = provider_config["sig"] if "sig" in provider_config else None
|
self.SIG = provider_config["sig"] if "sig" in provider_config else None
|
||||||
|
|
||||||
|
# Load location data
|
||||||
|
if locations_csv:
|
||||||
|
try:
|
||||||
|
f = open(locations_csv)
|
||||||
|
csv_data = f.read()
|
||||||
|
dr = csv.DictReader(csv_data.splitlines())
|
||||||
|
for row in dr:
|
||||||
|
self.LOCATION_DATA[row["ref"]] = {"lat": row["lat"], "lon": row["lon"]}
|
||||||
|
except:
|
||||||
|
logging.exception("Could not look up location data for XOTA source.")
|
||||||
|
|
||||||
def ws_message_to_spot(self, bytes):
|
def ws_message_to_spot(self, bytes):
|
||||||
string = bytes.decode("utf-8")
|
string = bytes.decode("utf-8")
|
||||||
source_spot = json.loads(string)
|
source_spot = json.loads(string)
|
||||||
|
ref_id = source_spot["reference"]["title"]
|
||||||
|
lat = float(self.LOCATION_DATA[ref_id]["lat"]) if ref_id in self.LOCATION_DATA else None
|
||||||
|
lon = float(self.LOCATION_DATA[ref_id]["lon"]) if ref_id in self.LOCATION_DATA else None
|
||||||
spot = Spot(source=self.name,
|
spot = Spot(source=self.name,
|
||||||
source_id=source_spot["id"],
|
source_id=source_spot["id"],
|
||||||
dx_call=source_spot["stationCallSign"].upper(),
|
dx_call=source_spot["stationCallSign"].upper(),
|
||||||
freq=float(source_spot["freq"]) * 1000,
|
freq=float(source_spot["freq"]) * 1000,
|
||||||
mode=source_spot["mode"].upper(),
|
mode=source_spot["mode"].upper(),
|
||||||
sig=self.SIG,
|
sig=self.SIG,
|
||||||
sig_refs=[SIGRef(id=source_spot["reference"]["title"], sig=self.SIG, url=source_spot["reference"]["website"])],
|
sig_refs=[SIGRef(id=ref_id, sig=self.SIG, url=source_spot["reference"]["website"], latitude=lat, longitude=lon)],
|
||||||
time=datetime.now(pytz.UTC).timestamp(),
|
time=datetime.now(pytz.UTC).timestamp(),
|
||||||
dx_latitude=self.FIXED_LATITUDE,
|
dx_latitude=lat,
|
||||||
dx_longitude=self.FIXED_LONGITUDE,
|
dx_longitude=lon,
|
||||||
qrt=source_spot["state"] != "active")
|
qrt=source_spot["state"] != "active")
|
||||||
return spot
|
return spot
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=2"></script>
|
<script src="/js/common.js?v=5"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=2"></script>
|
<script src="/js/common.js?v=5"></script>
|
||||||
<script src="/js/add-spot.js?v=2"></script>
|
<script src="/js/add-spot.js?v=5"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -168,8 +168,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=2"></script>
|
<script src="/js/common.js?v=5"></script>
|
||||||
<script src="/js/alerts.js?v=2"></script>
|
<script src="/js/alerts.js?v=5"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -117,6 +117,11 @@
|
|||||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="darkMode" value="darkMode" oninput="toggleDarkMode();">
|
<input class="form-check-input storeable-checkbox" type="checkbox" id="darkMode" value="darkMode" oninput="toggleDarkMode();">
|
||||||
<label class="form-check-label" for="darkMode">Dark mode</label>
|
<label class="form-check-label" for="darkMode">Dark mode</label>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="card-text spothole-card-text">
|
||||||
|
Band color scheme<br/>
|
||||||
|
<select id="band-color-scheme" class="storeable-select form-select d-inline-block" oninput="setBandColorSchemeFromUI();" style="display: inline-block;">
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,9 +134,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=2"></script>
|
<script src="/js/common.js?v=5"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=2"></script>
|
<script src="/js/spotsbandsandmap.js?v=5"></script>
|
||||||
<script src="/js/bands.js?v=2"></script>
|
<script src="/js/bands.js?v=5"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -44,6 +44,12 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=5"></script>
|
||||||
|
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=5"></script>
|
||||||
|
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=5"></script>
|
||||||
|
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=5"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="darkMode" value="darkMode" oninput="toggleDarkMode();">
|
<input class="form-check-input storeable-checkbox" type="checkbox" id="darkMode" value="darkMode" oninput="toggleDarkMode();">
|
||||||
<label class="form-check-label" for="darkMode">Dark mode</label>
|
<label class="form-check-label" for="darkMode">Dark mode</label>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="card-text spothole-card-text">
|
||||||
|
Band color scheme<br/>
|
||||||
|
<select id="band-color-scheme" class="storeable-select form-select d-inline-block" oninput="setBandColorSchemeFromUI();" style="display: inline-block;">
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,9 +152,9 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/leaflet.geodesic"></script>
|
<script src="https://cdn.jsdelivr.net/npm/leaflet.geodesic"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@joergdietrich/leaflet.terminator@1.1.0/L.Terminator.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@joergdietrich/leaflet.terminator@1.1.0/L.Terminator.min.js"></script>
|
||||||
|
|
||||||
<script src="/js/common.js?v=2"></script>
|
<script src="/js/common.js?v=5"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=2"></script>
|
<script src="/js/spotsbandsandmap.js?v=5"></script>
|
||||||
<script src="/js/map.js?v=2"></script>
|
<script src="/js/map.js?v=5"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -160,6 +160,11 @@
|
|||||||
<label class="form-check-label" for="darkMode">Dark mode</label>
|
<label class="form-check-label" for="darkMode">Dark mode</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="card-text spothole-card-text">
|
||||||
|
Band color scheme<br/>
|
||||||
|
<select id="band-color-scheme" class="storeable-select form-select d-inline-block" oninput="setBandColorSchemeFromUI();" style="display: inline-block;">
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,9 +223,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=2"></script>
|
<script src="/js/common.js?v=5"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=2"></script>
|
<script src="/js/spotsbandsandmap.js?v=5"></script>
|
||||||
<script src="/js/spots.js?v=2"></script>
|
<script src="/js/spots.js?v=5"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
<div id="status-container" class="row row-cols-1 row-cols-md-4 g-4 mt-4"></div>
|
<div id="status-container" class="row row-cols-1 row-cols-md-4 g-4 mt-4"></div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=2"></script>
|
<script src="/js/common.js?v=5"></script>
|
||||||
<script src="/js/status.js?v=2"></script>
|
<script src="/js/status.js?v=5"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -9,12 +9,18 @@ info:
|
|||||||
The API calls described below allow third-party software to access data from Spothole, and receive data on spots and alerts in a consistent format regardless of the data sources used by Spothole itself. Utility calls are also provided for general data lookups.
|
The API calls described below allow third-party software to access data from Spothole, and receive data on spots and alerts in a consistent format regardless of the data sources used by Spothole itself. Utility calls are also provided for general data lookups.
|
||||||
|
|
||||||
Please note that the data coming out of Spothole is only as good as the data going in. People mis-hear and make typos when spotting callsigns all the time, and there are plenty of areas where Spothole's location data may be inaccurate. If you are doing something where accuracy is important, such as contesting, you should not rely on Spothole's data to fill in any gaps in your log.
|
Please note that the data coming out of Spothole is only as good as the data going in. People mis-hear and make typos when spotting callsigns all the time, and there are plenty of areas where Spothole's location data may be inaccurate. If you are doing something where accuracy is important, such as contesting, you should not rely on Spothole's data to fill in any gaps in your log.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 1.1
|
||||||
|
|
||||||
|
Added Server-Sent Event API endpoint. Removed band colour and icon information from spots.
|
||||||
contact:
|
contact:
|
||||||
email: ian@ianrenton.com
|
email: ian@ianrenton.com
|
||||||
license:
|
license:
|
||||||
name: The Unlicense
|
name: The Unlicense
|
||||||
url: https://unlicense.org/#the-unlicense
|
url: https://unlicense.org/#the-unlicense
|
||||||
version: v1
|
version: v1.1
|
||||||
servers:
|
servers:
|
||||||
- url: https://spothole.app/api/v1
|
- url: https://spothole.app/api/v1
|
||||||
paths:
|
paths:
|
||||||
@@ -1084,18 +1090,6 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
description: Activation score. SOTA only
|
description: Activation score. SOTA only
|
||||||
example: 0
|
example: 0
|
||||||
icon:
|
|
||||||
type: string
|
|
||||||
descripton: Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field Spotter. Does not include the "fa-" prefix.
|
|
||||||
example: tree
|
|
||||||
band_color:
|
|
||||||
type: string
|
|
||||||
descripton: Colour to represent this spot, if a client chooses to colour spots based on their frequency band, using PSK Reporter's default colours. HTML colour e.g. hex.
|
|
||||||
example: "#ff0000"
|
|
||||||
band_contrast_color:
|
|
||||||
type: string
|
|
||||||
descripton: Black or white, whichever best contrasts with "band_color".
|
|
||||||
example: "white"
|
|
||||||
qrt:
|
qrt:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
|
description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
|
||||||
@@ -1204,10 +1198,6 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
description: Activation score. SOTA only
|
description: Activation score. SOTA only
|
||||||
example: 0
|
example: 0
|
||||||
icon:
|
|
||||||
type: string
|
|
||||||
descripton: Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field Spotter. Does not include the "fa-" prefix.
|
|
||||||
example: tree
|
|
||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
description: Where we got the alert from.
|
description: Where we got the alert from.
|
||||||
@@ -1283,14 +1273,6 @@ components:
|
|||||||
type: int
|
type: int
|
||||||
description: The end frequency of this band, in Hz.
|
description: The end frequency of this band, in Hz.
|
||||||
example: 7200000
|
example: 7200000
|
||||||
color:
|
|
||||||
type: string
|
|
||||||
description: The color associated with this mode, as used on PSK Reporter.
|
|
||||||
example: "#5959ff"
|
|
||||||
contrast_color:
|
|
||||||
type: string
|
|
||||||
description: Black or white, whichever provides the best contrast against the band colour.
|
|
||||||
example: white
|
|
||||||
|
|
||||||
SIG:
|
SIG:
|
||||||
type: object
|
type: object
|
||||||
@@ -1302,10 +1284,6 @@ 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
|
||||||
icon:
|
|
||||||
type: string
|
|
||||||
description: Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field Spotter. Does not include the "fa-" prefix.
|
|
||||||
example: tree
|
|
||||||
ref_regex:
|
ref_regex:
|
||||||
type: string
|
type: string
|
||||||
description: Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use, clients probably won't need this.
|
description: Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use, clients probably won't need this.
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ div#map {
|
|||||||
filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%);
|
filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Make buttons overlaid on the map have a non-transparent fill so you can see the text better */
|
||||||
|
.btn-outline-primary {
|
||||||
|
--bs-btn-bg: var(--bs-body-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* BANDS PANEL */
|
/* BANDS PANEL */
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
|
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
|
||||||
}
|
}
|
||||||
if (showSource) {
|
if (showSource) {
|
||||||
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> ${sigSourceText}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid ${sigToIcon(a["sig"], "fa-globe-africa")}'></i></span> ${sigSourceText}</td>`);
|
||||||
}
|
}
|
||||||
if (showRef) {
|
if (showRef) {
|
||||||
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
|
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
|
||||||
@@ -257,7 +257,7 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
}
|
}
|
||||||
$td2 = $("<td colspan='100'>");
|
$td2 = $("<td colspan='100'>");
|
||||||
if (showSource) {
|
if (showSource) {
|
||||||
$td2.append(`<span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> `);
|
$td2.append(`<span class='icon-wrapper'><i class='fa-solid ${sigToIcon(a["sig"], "fa-globe-africa")}'></i></span> `);
|
||||||
}
|
}
|
||||||
if (showRef) {
|
if (showRef) {
|
||||||
$td2.append(`${sig_refs} `);
|
$td2.append(`${sig_refs} `);
|
||||||
@@ -352,3 +352,11 @@ $(document).ready(function() {
|
|||||||
// Update the refresh timing display every second
|
// Update the refresh timing display every second
|
||||||
setInterval(updateRefreshDisplay, 1000);
|
setInterval(updateRefreshDisplay, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reload alerts on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA
|
||||||
|
// after some time has passed with it in the background.
|
||||||
|
addEventListener("visibilitychange", (event) => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
loadAlerts();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -70,7 +70,7 @@ function updateBands() {
|
|||||||
var table = $('<table id="bands-table">').append('<thead><tr></tr></thead><tbody><tr></tr></tbody>');
|
var table = $('<table id="bands-table">').append('<thead><tr></tr></thead><tbody><tr></tr></tbody>');
|
||||||
bandToSpots.forEach(function (spotList, bandName) {
|
bandToSpots.forEach(function (spotList, bandName) {
|
||||||
// Get the colours for the band from the first spot, and prepare the header
|
// Get the colours for the band from the first spot, and prepare the header
|
||||||
table.find('thead tr').append(`<th style='background-color:${spotList[0].band_color}; color:${spotList[0].band_contrast_color}'>${spotList[0].band}</th>`);
|
table.find('thead tr').append(`<th style='background-color:${bandToColor(spotList[0].band)}; color:${bandToContrastColor(spotList[0].band)}'>${spotList[0].band}</th>`);
|
||||||
|
|
||||||
// Get the band data to fetch start and end frequencies
|
// Get the band data to fetch start and end frequencies
|
||||||
let band = options["bands"].filter(function (b) {
|
let band = options["bands"].filter(function (b) {
|
||||||
@@ -145,7 +145,7 @@ function updateBands() {
|
|||||||
|
|
||||||
// Now each spot is tagged with how far down the div it should go, add them to the DOM.
|
// Now each spot is tagged with how far down the div it should go, add them to the DOM.
|
||||||
spotList.forEach(s => {
|
spotList.forEach(s => {
|
||||||
bandSpotsDiv.append(`<div class="band-spot" style="top: ${s['pxDownBandLabel']}px; border-top: 1px solid ${s.band_color}; border-left: 5px solid ${s.band_color}; border-bottom: 1px solid ${s.band_color}; border-right: 1px solid ${s.band_color};"><span class="band-spot-call">${s.dx_call}${s.dx_ssid != null ? "-" + s.dx_ssid : ""}</span><span class="band-spot-info">${s.dx_call}${s.dx_ssid != null ? "-" + s.dx_ssid : ""} ${(s.freq/1000000).toFixed(3)} ${s.mode}</span></div>`);
|
bandSpotsDiv.append(`<div class="band-spot" style="top: ${s['pxDownBandLabel']}px; border-top: 1px solid ${bandToColor(s['band'])}; border-left: 5px solid ${bandToColor(s['band'])}; border-bottom: 1px solid ${bandToColor(s['band'])}; border-right: 1px solid ${bandToColor(s['band'])};"><span class="band-spot-call">${s.dx_call}${s.dx_ssid != null ? "-" + s.dx_ssid : ""}</span><span class="band-spot-info">${s.dx_call}${s.dx_ssid != null ? "-" + s.dx_ssid : ""} ${(s.freq/1000000).toFixed(3)} ${s.mode}</span></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Work out how tall the canvas should be. Normally this is matching the normal band column height, but if some
|
// Work out how tall the canvas should be. Normally this is matching the normal band column height, but if some
|
||||||
@@ -167,7 +167,7 @@ function updateBands() {
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.lineCap = "round";
|
ctx.lineCap = "round";
|
||||||
ctx.strokeStyle = s.band_color;
|
ctx.strokeStyle = bandToColor(s['band']);
|
||||||
ctx.moveTo(0, pxDownBandFreq);
|
ctx.moveTo(0, pxDownBandFreq);
|
||||||
ctx.lineTo(BAND_COLUMN_CANVAS_WIDTH_PX, pxDownBandLabel);
|
ctx.lineTo(BAND_COLUMN_CANVAS_WIDTH_PX, pxDownBandLabel);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
@@ -228,6 +228,21 @@ function loadOptions() {
|
|||||||
// Store options
|
// Store options
|
||||||
options = jsonData;
|
options = jsonData;
|
||||||
|
|
||||||
|
// Populate the Display panel
|
||||||
|
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
|
||||||
|
value: sc * 60,
|
||||||
|
text: sc
|
||||||
|
})));
|
||||||
|
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
|
||||||
|
getAvailableBandColorSchemes().forEach(sc => $("#band-color-scheme").append($('<option>', {
|
||||||
|
value: sc,
|
||||||
|
text: sc
|
||||||
|
})));
|
||||||
|
|
||||||
|
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
|
||||||
|
loadSettings();
|
||||||
|
setBandColorScheme($("#band-color-scheme option:selected").val());
|
||||||
|
|
||||||
// Add CSS for band toggle buttons
|
// Add CSS for band toggle buttons
|
||||||
addBandToggleColourCSS(options["bands"]);
|
addBandToggleColourCSS(options["bands"]);
|
||||||
|
|
||||||
@@ -239,13 +254,6 @@ function loadOptions() {
|
|||||||
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
|
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
|
||||||
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
|
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
|
||||||
|
|
||||||
// Populate the Display panel
|
|
||||||
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
|
|
||||||
value: sc * 60,
|
|
||||||
text: sc
|
|
||||||
})));
|
|
||||||
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
|
|
||||||
|
|
||||||
// Load URL params. These may select things from the various filter & display options, so the function needs
|
// Load URL params. These may select things from the various filter & display options, so the function needs
|
||||||
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
||||||
// loading settings, so this needs to be called before that.
|
// loading settings, so this needs to be called before that.
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
var options = {};
|
var options = {};
|
||||||
// Last time we updated the spots/alerts list on display.
|
// Last time we updated the spots/alerts list on display.
|
||||||
var lastUpdateTime;
|
var lastUpdateTime;
|
||||||
// Whether "embedded mode" is being used. This removes headers and footers, maximises the remaining content, and
|
|
||||||
// uses URL params to configure the interface options rather than using the user's localstorage.
|
|
||||||
var embeddedMode = false;
|
|
||||||
|
|
||||||
// Load and apply any URL params. This is used for "embedded mode" where another site can embed a version of
|
// Load and apply any URL params. This is used for "embedded mode" where another site can embed a version of
|
||||||
// Spothole and provide its own interface options rather than using the user's saved ones. These may select things
|
// Spothole and provide its own interface options rather than using the user's saved ones. These may select things
|
||||||
@@ -18,7 +15,7 @@ function loadURLParams() {
|
|||||||
// top-level html element to use CSS selectors to remove bits of UI.
|
// top-level html element to use CSS selectors to remove bits of UI.
|
||||||
let embedded = params.get("embedded");
|
let embedded = params.get("embedded");
|
||||||
if (embedded != null && embedded === "true") {
|
if (embedded != null && embedded === "true") {
|
||||||
embeddedMode = true;
|
useLocalStorage = false;
|
||||||
$("html").attr("embedded-mode", "true");
|
$("html").attr("embedded-mode", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,27 +130,6 @@ function updateRefreshDisplay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility function to escape HTML characters from a string.
|
|
||||||
function escapeHtml(str) {
|
|
||||||
if (typeof str !== 'string') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const escapeCharacter = (match) => {
|
|
||||||
switch (match) {
|
|
||||||
case '&': return '&';
|
|
||||||
case '<': return '<';
|
|
||||||
case '>': return '>';
|
|
||||||
case '"': return '"';
|
|
||||||
case '\'': return ''';
|
|
||||||
case '`': return '`';
|
|
||||||
default: return match;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return str.replace(/[&<>"'`]/g, escapeCharacter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the "use local time" field is changed, reload the table and save settings
|
// When the "use local time" field is changed, reload the table and save settings
|
||||||
function timeZoneUpdated() {
|
function timeZoneUpdated() {
|
||||||
updateTable();
|
updateTable();
|
||||||
@@ -166,106 +142,6 @@ function columnsUpdated() {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate great circle bearing between two lat/lon points.
|
|
||||||
function calcBearing(lat1, lon1, lat2, lon2) {
|
|
||||||
lat1 *= Math.PI / 180;
|
|
||||||
lon1 *= Math.PI / 180;
|
|
||||||
lat2 *= Math.PI / 180;
|
|
||||||
lon2 *= Math.PI / 180;
|
|
||||||
var lonDelta = lon2 - lon1;
|
|
||||||
var y = Math.sin(lonDelta) * Math.cos(lat2);
|
|
||||||
var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lonDelta);
|
|
||||||
var bearing = Math.atan2(y, x);
|
|
||||||
bearing = bearing * (180 / Math.PI);
|
|
||||||
if ( bearing < 0 ) { bearing += 360; }
|
|
||||||
return bearing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the centre point of the square.
|
|
||||||
// Returns null if the grid format is invalid.
|
|
||||||
function latLonForGridCentre(grid) {
|
|
||||||
let [lat, lon, latCellSize, lonCellSize] = latLonForGridSWCornerPlusSize(grid);
|
|
||||||
if (lat != null && lon != null && latCellSize != null && lonCellSize != null) {
|
|
||||||
return [lat + latCellSize / 2.0, lon + lonCellSize / 2.0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a Maidenhead grid reference of arbitrary precision to lat/long, including in the result the size of the
|
|
||||||
// lowest grid square. This is a utility method used by the main methods that return the centre, southwest, and
|
|
||||||
// northeast coordinates of a grid square.
|
|
||||||
// The return type is always an array of size 4. The elements in it are null if the grid format is invalid.
|
|
||||||
function latLonForGridSWCornerPlusSize(grid) {
|
|
||||||
// Make sure we are in upper case so our maths works. Case is arbitrary for Maidenhead references
|
|
||||||
grid = grid.toUpperCase();
|
|
||||||
|
|
||||||
// Return null if our Maidenhead string is invalid or too short
|
|
||||||
let len = grid.length;
|
|
||||||
if (len <= 0 || (len % 2) !== 0) {
|
|
||||||
return [null, null, null, null];
|
|
||||||
}
|
|
||||||
|
|
||||||
let lat = 0.0; // aggregated latitude
|
|
||||||
let lon = 0.0; // aggregated longitude
|
|
||||||
let latCellSize = 10; // Size in degrees latitude of the current cell. Starts at 20 and gets smaller as the calculation progresses
|
|
||||||
let lonCellSize = 20; // Size in degrees longitude of the current cell. Starts at 20 and gets smaller as the calculation progresses
|
|
||||||
let latCellNo; // grid latitude cell number this time
|
|
||||||
let lonCellNo; // grid longitude cell number this time
|
|
||||||
|
|
||||||
// Iterate through blocks (two-character sections)
|
|
||||||
for (let block = 0; block * 2 < len; block += 1) {
|
|
||||||
if (block % 2 === 0) {
|
|
||||||
// Letters in this block
|
|
||||||
lonCellNo = grid.charCodeAt(block * 2) - 'A'.charCodeAt(0);
|
|
||||||
latCellNo = grid.charCodeAt(block * 2 + 1) - 'A'.charCodeAt(0);
|
|
||||||
// Bail if the values aren't in range. Allowed values are A-R (0-17) for the first letter block, or
|
|
||||||
// A-X (0-23) thereafter.
|
|
||||||
let maxCellNo = (block === 0) ? 17 : 23;
|
|
||||||
if (latCellNo < 0 || latCellNo > maxCellNo || lonCellNo < 0 || lonCellNo > maxCellNo) {
|
|
||||||
return [null, null, null, null];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Numbers in this block
|
|
||||||
lonCellNo = parseInt(grid.charAt(block * 2));
|
|
||||||
latCellNo = parseInt(grid.charAt(block * 2 + 1));
|
|
||||||
// Bail if the values aren't in range 0-9..
|
|
||||||
if (latCellNo < 0 || latCellNo > 9 || lonCellNo < 0 || lonCellNo > 9) {
|
|
||||||
return [null, null, null, null];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aggregate the angles
|
|
||||||
lat += latCellNo * latCellSize;
|
|
||||||
lon += lonCellNo * lonCellSize;
|
|
||||||
|
|
||||||
// Reduce the cell size for the next block, unless we are on the last cell.
|
|
||||||
if (block * 2 < len - 2) {
|
|
||||||
// Still have more work to do, so reduce the cell size
|
|
||||||
if (block % 2 === 0) {
|
|
||||||
// Just dealt with letters, next block will be numbers so cells will be 1/10 the current size
|
|
||||||
latCellSize = latCellSize / 10.0;
|
|
||||||
lonCellSize = lonCellSize / 10.0;
|
|
||||||
} else {
|
|
||||||
// Just dealt with numbers, next block will be letters so cells will be 1/24 the current size
|
|
||||||
latCellSize = latCellSize / 24.0;
|
|
||||||
lonCellSize = lonCellSize / 24.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset back to (-180, -90) where the grid starts
|
|
||||||
lon -= 180.0;
|
|
||||||
lat -= 90.0;
|
|
||||||
|
|
||||||
// Return nulls on maths errors
|
|
||||||
if (isNaN(lat) || isNaN(lon) || isNaN(latCellSize) || isNaN(lonCellSize)) {
|
|
||||||
return [null, null, null, null];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [lat, lon, latCellSize, lonCellSize];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to set dark mode on or off
|
// Function to set dark mode on or off
|
||||||
function enableDarkMode(dark) {
|
function enableDarkMode(dark) {
|
||||||
$("html").attr("data-bs-theme", dark ? "dark" : "light");
|
$("html").attr("data-bs-theme", dark ? "dark" : "light");
|
||||||
@@ -289,37 +165,6 @@ function usePreferredTheme() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save settings to local storage. Suppressed if "embedded mode" is in use.
|
|
||||||
function saveSettings() {
|
|
||||||
if (!embeddedMode) {
|
|
||||||
// Find all storeable UI elements, store a key of "element id:property name" mapped to the value of that
|
|
||||||
// property. For a checkbox, that's the "checked" property.
|
|
||||||
$(".storeable-checkbox").each(function() {
|
|
||||||
localStorage.setItem("#" + $(this)[0].id + ":checked", JSON.stringify($(this)[0].checked));
|
|
||||||
});
|
|
||||||
$(".storeable-select").each(function() {
|
|
||||||
localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value));
|
|
||||||
});
|
|
||||||
$(".storeable-text").each(function() {
|
|
||||||
localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load settings from local storage and set up the filter selectors. Suppressed if "embedded mode" is in use.
|
|
||||||
function loadSettings() {
|
|
||||||
if (!embeddedMode) {
|
|
||||||
// Find all local storage entries and push their data to the corresponding UI element
|
|
||||||
Object.keys(localStorage).forEach(function(key) {
|
|
||||||
if (key.startsWith("#") && key.includes(":")) {
|
|
||||||
// Split the key back into an element ID and a property
|
|
||||||
var split = key.split(":");
|
|
||||||
$(split[0]).prop(split[1], JSON.parse(localStorage.getItem(key)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Startup
|
// Startup
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
usePreferredTheme();
|
usePreferredTheme();
|
||||||
|
|||||||
@@ -45,12 +45,16 @@ function updateMap() {
|
|||||||
|
|
||||||
// Create geodesics if required
|
// Create geodesics if required
|
||||||
if ($("#mapShowGeodesics")[0].checked && s["de_latitude"] != null && s["de_longitude"] != null) {
|
if ($("#mapShowGeodesics")[0].checked && s["de_latitude"] != null && s["de_longitude"] != null) {
|
||||||
|
try {
|
||||||
var geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], {
|
var geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], {
|
||||||
color: s["band_color"],
|
color: bandToColor(s['band']),
|
||||||
wrap: false,
|
wrap: false,
|
||||||
steps: 5
|
steps: 5
|
||||||
});
|
});
|
||||||
geodesicsLayer.addLayer(geodesic);
|
geodesicsLayer.addLayer(geodesic);
|
||||||
|
} catch (e) {
|
||||||
|
// Not sure what causes these but better to continue than to crash out
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -58,9 +62,9 @@ function updateMap() {
|
|||||||
// Get an icon for a spot, based on its band, using PSK Reporter colours, its program etc.
|
// Get an icon for a spot, based on its band, using PSK Reporter colours, its program etc.
|
||||||
function getIcon(s) {
|
function getIcon(s) {
|
||||||
return L.ExtraMarkers.icon({
|
return L.ExtraMarkers.icon({
|
||||||
icon: "fa-" + s["icon"],
|
icon: sigToIcon(s["sig"], "fa-tower-cell"),
|
||||||
iconColor: s["band_contrast_color"],
|
iconColor: bandToContrastColor(s["band"]),
|
||||||
markerColor: s["band_color"],
|
markerColor: bandToColor(s["band"]),
|
||||||
shape: 'circle',
|
shape: 'circle',
|
||||||
prefix: 'fa',
|
prefix: 'fa',
|
||||||
svg: true
|
svg: true
|
||||||
@@ -136,7 +140,7 @@ function getTooltipText(s) {
|
|||||||
ttt += "<br/>";
|
ttt += "<br/>";
|
||||||
|
|
||||||
// Source / SIG / Ref
|
// Source / SIG / Ref
|
||||||
ttt += `<span class='nowrap'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${sigSourceText} ${sig_refs}</span><br/>`;
|
ttt += `<span class='nowrap'><span class='icon-wrapper'><i class='fa-solid ${sigToIcon(s["sig"], "fa-tower-cell")}'></i></span> ${sigSourceText} ${sig_refs}</span><br/>`;
|
||||||
|
|
||||||
// Time
|
// Time
|
||||||
ttt += `<span class='icon-wrapper'><i class='fa-solid fa-clock markerPopupIcon'></i></span> ${moment.unix(s["time"]).fromNow()}`;
|
ttt += `<span class='icon-wrapper'><i class='fa-solid fa-clock markerPopupIcon'></i></span> ${moment.unix(s["time"]).fromNow()}`;
|
||||||
@@ -156,6 +160,21 @@ function loadOptions() {
|
|||||||
// Store options
|
// Store options
|
||||||
options = jsonData;
|
options = jsonData;
|
||||||
|
|
||||||
|
// Populate the Display panel
|
||||||
|
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
|
||||||
|
value: sc * 60,
|
||||||
|
text: sc
|
||||||
|
})));
|
||||||
|
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
|
||||||
|
getAvailableBandColorSchemes().forEach(sc => $("#band-color-scheme").append($('<option>', {
|
||||||
|
value: sc,
|
||||||
|
text: sc
|
||||||
|
})));
|
||||||
|
|
||||||
|
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
|
||||||
|
loadSettings();
|
||||||
|
setBandColorScheme($("#band-color-scheme option:selected").val());
|
||||||
|
|
||||||
// Add CSS for band toggle buttons
|
// Add CSS for band toggle buttons
|
||||||
addBandToggleColourCSS(options["bands"]);
|
addBandToggleColourCSS(options["bands"]);
|
||||||
|
|
||||||
@@ -167,13 +186,6 @@ function loadOptions() {
|
|||||||
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
|
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
|
||||||
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
|
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
|
||||||
|
|
||||||
// Populate the Display panel
|
|
||||||
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
|
|
||||||
value: sc * 60,
|
|
||||||
text: sc
|
|
||||||
})));
|
|
||||||
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
|
|
||||||
|
|
||||||
// Load URL params. These may select things from the various filter & display options, so the function needs
|
// Load URL params. These may select things from the various filter & display options, so the function needs
|
||||||
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
||||||
// loading settings, so this needs to be called before that.
|
// loading settings, so this needs to be called before that.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// SSE event source
|
// SSE event source
|
||||||
let evtSource;
|
let evtSource;
|
||||||
|
let restartSSEOnErrorTimeoutId;
|
||||||
// Table row count, to alternate shading
|
// Table row count, to alternate shading
|
||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
|
|
||||||
@@ -30,6 +31,9 @@ function loadSpots() {
|
|||||||
// Start an SSE connection (closing an existing one if it exists). This will then be used to add to the table on the
|
// Start an SSE connection (closing an existing one if it exists). This will then be used to add to the table on the
|
||||||
// fly.
|
// fly.
|
||||||
function startSSEConnection() {
|
function startSSEConnection() {
|
||||||
|
if (evtSource != null) {
|
||||||
|
evtSource.close();
|
||||||
|
}
|
||||||
evtSource = new EventSource('/api/v1/spots/stream' + buildQueryString());
|
evtSource = new EventSource('/api/v1/spots/stream' + buildQueryString());
|
||||||
|
|
||||||
evtSource.onmessage = function(event) {
|
evtSource.onmessage = function(event) {
|
||||||
@@ -66,8 +70,11 @@ function startSSEConnection() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
evtSource.onerror = function(err) {
|
evtSource.onerror = function(err) {
|
||||||
|
if (evtSource != null) {
|
||||||
evtSource.close();
|
evtSource.close();
|
||||||
setTimeout(startSSEConnection, 1000);
|
}
|
||||||
|
clearTimeout(restartSSEOnErrorTimeoutId)
|
||||||
|
restartSSEOnErrorTimeoutId = setTimeout(startSSEConnection, 1000);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,8 +154,8 @@ function updateTable() {
|
|||||||
table.find('tbody').append('<tr class="table-danger"><td colspan="100" style="text-align:center;">No spots match your filters.</td></tr>');
|
table.find('tbody').append('<tr class="table-danger"><td colspan="100" style="text-align:center;">No spots match your filters.</td></tr>');
|
||||||
}
|
}
|
||||||
|
|
||||||
spots.reverse();
|
let spotsNewestFirst = spots.toReversed();
|
||||||
spots.forEach(s => addSpotToTopOfTable(s, false));
|
spotsNewestFirst.forEach(s => addSpotToTopOfTable(s, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add rows corresponding to a new spot to the top of the table
|
// Add rows corresponding to a new spot to the top of the table
|
||||||
@@ -182,9 +189,6 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
|
|
||||||
// Create row
|
// Create row
|
||||||
let $tr = $('<tr>');
|
let $tr = $('<tr>');
|
||||||
if (highlightNew) {
|
|
||||||
$tr.addClass("new");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of
|
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of
|
||||||
// extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners
|
// extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners
|
||||||
@@ -198,6 +202,12 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
$tr.addClass("table-faded");
|
$tr.addClass("table-faded");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we are asked to highlight new rows (i.e. this row is being added "live" via the SSE client and not as a bulk
|
||||||
|
// reload of the whole table)
|
||||||
|
if (highlightNew) {
|
||||||
|
$tr.addClass("new");
|
||||||
|
}
|
||||||
|
|
||||||
// Format a UTC or local time for display
|
// Format a UTC or local time for display
|
||||||
var time = moment.unix(s["time"]).utc();
|
var time = moment.unix(s["time"]).utc();
|
||||||
if (useLocalTime) {
|
if (useLocalTime) {
|
||||||
@@ -277,9 +287,9 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
var items = []
|
var items = []
|
||||||
for (var i = 0; i < s["sig_refs"].length; i++) {
|
for (var i = 0; i < s["sig_refs"].length; i++) {
|
||||||
if (s["sig_refs"][i]["url"] != null) {
|
if (s["sig_refs"][i]["url"] != null) {
|
||||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
items[i] = `<span style="white-space: nowrap;"><a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a></span>`
|
||||||
} else {
|
} else {
|
||||||
items[i] = `${s["sig_refs"][i]["id"]}`
|
items[i] = `<span style="white-space: nowrap;">${s["sig_refs"][i]["id"]}</span>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sig_refs = items.join(", ");
|
sig_refs = items.join(", ");
|
||||||
@@ -315,10 +325,10 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
$tr.append(`<td class='nowrap'>${time_formatted}</td>`);
|
$tr.append(`<td class='nowrap'>${time_formatted}</td>`);
|
||||||
}
|
}
|
||||||
if (showDX) {
|
if (showDX) {
|
||||||
$tr.append(`<td class='nowrap'><span class='flag-wrapper hideonmobile' title='${dx_country}'>${dx_flag}</span><a class='dx-link' href='https://qrz.com/db/${s["dx_call"]}' target='_new' title='${s["dx_name"] != null ? s["dx_name"] : ""}'>${dx_call}</a></td>`);
|
$tr.append(`<td class='nowrap'><span class='flag-wrapper' title='${dx_country}'>${dx_flag}</span><a class='dx-link' href='https://qrz.com/db/${s["dx_call"]}' target='_new' title='${s["dx_name"] != null ? s["dx_name"] : ""}'>${dx_call}</a></td>`);
|
||||||
}
|
}
|
||||||
if (showFreq) {
|
if (showFreq) {
|
||||||
$tr.append(`<td class='nowrap'><span class='band-bullet' title='${bandFullName}' style='${(s["freq"] != null) ? "color: " + s["band_color"] : "display: none;"}'>■</span>${freq_string}</td>`);
|
$tr.append(`<td class='nowrap'><span class='band-bullet' title='${bandFullName}' style='${(s["freq"] != null) ? "color: " + bandToColor(s["band"]) : "display: none;"}'>■</span>${freq_string}</td>`);
|
||||||
}
|
}
|
||||||
if (showMode) {
|
if (showMode) {
|
||||||
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
|
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
|
||||||
@@ -330,10 +340,10 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
$tr.append(`<td class='nowrap hideonmobile'>${bearingText}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'>${bearingText}</td>`);
|
||||||
}
|
}
|
||||||
if (showType) {
|
if (showType) {
|
||||||
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${typeText}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid ${sigToIcon(s["sig"], "fa-tower-cell")}'></i></span> ${typeText}</td>`);
|
||||||
}
|
}
|
||||||
if (showRef) {
|
if (showRef) {
|
||||||
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
|
$tr.append(`<td class='hideonmobile' style='max-width: 11em;'>${sig_refs}</td>`);
|
||||||
}
|
}
|
||||||
if (showDE) {
|
if (showDE) {
|
||||||
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
|
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
|
||||||
@@ -341,24 +351,38 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
|
|
||||||
// Second row for mobile view only, containing type, ref & comment
|
// Second row for mobile view only, containing type, ref & comment
|
||||||
$tr2 = $("<tr class='hidenotonmobile'>");
|
$tr2 = $("<tr class='hidenotonmobile'>");
|
||||||
|
|
||||||
|
// Apply styles as per the first row
|
||||||
if (rowCount % 2 == 1) {
|
if (rowCount % 2 == 1) {
|
||||||
$tr2.addClass("table-active");
|
$tr2.addClass("table-active");
|
||||||
}
|
}
|
||||||
if (s["qrt"] == true) {
|
if (s["qrt"] == true) {
|
||||||
$tr2.addClass("table-faded");
|
$tr2.addClass("table-faded");
|
||||||
}
|
}
|
||||||
|
if (highlightNew) {
|
||||||
|
$tr2.addClass("new");
|
||||||
|
}
|
||||||
|
|
||||||
$td2 = $("<td colspan='100'>");
|
$td2 = $("<td colspan='100'>");
|
||||||
|
$td2floatleft = $(`<div style="float: left;">`);
|
||||||
if (showType) {
|
if (showType) {
|
||||||
$td2.append(`<span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${typeText} `);
|
$td2floatleft.append(`<span class='icon-wrapper'><i class='fa-solid ${sigToIcon(s["sig"], "fa-tower-cell")}'></i></span> ${typeText} `);
|
||||||
}
|
}
|
||||||
if (showRef) {
|
if (showRef) {
|
||||||
$td2.append(`${sig_refs} `);
|
$td2floatleft.append(`${sig_refs} `);
|
||||||
}
|
}
|
||||||
|
$td2.append($td2floatleft);
|
||||||
|
$td2floatright = $(`<div style="float: right;">`);
|
||||||
if (showBearing) {
|
if (showBearing) {
|
||||||
$td2.append(` Bearing: ${bearingText} `);
|
$td2floatright.append(`${bearingText} `);
|
||||||
}
|
}
|
||||||
|
if (showDE) {
|
||||||
|
$td2floatright.append(` de ${de_call} `);
|
||||||
|
}
|
||||||
|
$td2.append($td2floatright);
|
||||||
|
$td2.append(`</div><div style="clear: both;"></div>`);
|
||||||
if (showComment) {
|
if (showComment) {
|
||||||
$td2.append(`<br/>${commentText}`);
|
$td2.append(`${commentText}`);
|
||||||
}
|
}
|
||||||
$tr2.append($td2);
|
$tr2.append($td2);
|
||||||
|
|
||||||
@@ -374,6 +398,21 @@ function loadOptions() {
|
|||||||
// Store options
|
// Store options
|
||||||
options = jsonData;
|
options = jsonData;
|
||||||
|
|
||||||
|
// Populate the Display panel
|
||||||
|
options["web-ui-options"]["spot-count"].forEach(sc => $("#spots-to-fetch").append($('<option>', {
|
||||||
|
value: sc,
|
||||||
|
text: sc
|
||||||
|
})));
|
||||||
|
$("#spots-to-fetch").val(options["web-ui-options"]["spot-count-default"]);
|
||||||
|
getAvailableBandColorSchemes().forEach(sc => $("#band-color-scheme").append($('<option>', {
|
||||||
|
value: sc,
|
||||||
|
text: sc
|
||||||
|
})));
|
||||||
|
|
||||||
|
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
|
||||||
|
loadSettings();
|
||||||
|
setBandColorScheme($("#band-color-scheme option:selected").val());
|
||||||
|
|
||||||
// Add CSS for band toggle buttons
|
// Add CSS for band toggle buttons
|
||||||
addBandToggleColourCSS(options["bands"]);
|
addBandToggleColourCSS(options["bands"]);
|
||||||
|
|
||||||
@@ -385,13 +424,6 @@ function loadOptions() {
|
|||||||
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
|
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
|
||||||
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
|
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
|
||||||
|
|
||||||
// Populate the Display panel
|
|
||||||
options["web-ui-options"]["spot-count"].forEach(sc => $("#spots-to-fetch").append($('<option>', {
|
|
||||||
value: sc,
|
|
||||||
text: sc
|
|
||||||
})));
|
|
||||||
$("#spots-to-fetch").val(options["web-ui-options"]["spot-count-default"]);
|
|
||||||
|
|
||||||
// Load URL params. These may select things from the various filter & display options, so the function needs
|
// Load URL params. These may select things from the various filter & display options, so the function needs
|
||||||
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
||||||
// loading settings, so this needs to be called before that.
|
// loading settings, so this needs to be called before that.
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ function addBandToggleColourCSS(band_options) {
|
|||||||
band_options.forEach(o => {
|
band_options.forEach(o => {
|
||||||
// CSS doesn't like IDs with decimal points in, so we need to replace that
|
// CSS doesn't like IDs with decimal points in, so we need to replace that
|
||||||
var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown";
|
var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown";
|
||||||
$style.append(`#filter-button-label-band-${cssFormattedBandName} { border-color: ${o['color']}; color: var(--bs-primary);}`);
|
$style.append(`#filter-button-label-band-${cssFormattedBandName} { border-color: ${bandToColor(o['name'])}; color: var(--bs-primary);}`);
|
||||||
$style.append(`.btn-check:checked + #filter-button-label-band-${cssFormattedBandName} { background-color: ${o['color']}; color: ${o['contrast_color']};}`);
|
$style.append(`.btn-check:checked + #filter-button-label-band-${cssFormattedBandName} { background-color: ${bandToColor(o['name'])}; color: ${bandToContrastColor(o['name'])};}`);
|
||||||
});
|
});
|
||||||
$('html > head').append($style);
|
$('html > head').append($style);
|
||||||
}
|
}
|
||||||
@@ -23,15 +23,25 @@ function generateBandsMultiToggleFilterCard(band_options) {
|
|||||||
var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown";
|
var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown";
|
||||||
$("#band-options").append(`<input type="checkbox" class="btn-check filter-button-band storeable-checkbox" name="options" id="filter-button-band-${cssFormattedBandName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline" id="filter-button-label-band-${cssFormattedBandName}" for="filter-button-band-${cssFormattedBandName}">${o['name']}</label> `);
|
$("#band-options").append(`<input type="checkbox" class="btn-check filter-button-band storeable-checkbox" name="options" id="filter-button-band-${cssFormattedBandName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline" id="filter-button-label-band-${cssFormattedBandName}" for="filter-button-band-${cssFormattedBandName}">${o['name']}</label> `);
|
||||||
});
|
});
|
||||||
// Create All/None buttons
|
// Create All/None/Ham HF buttons
|
||||||
$("#band-options").append(` <span style="display: inline-block"><button id="filter-button-band-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', true);">All</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', false);">None</button></span>`);
|
$("#band-options").append(` <span style="display: inline-block"><button id="filter-button-band-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', true);">All</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', false);">None</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="setHamHFBandToggles();">Ham HF</button></span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the band toggles so that only the amateur radio HF bands are selected. This includes 160m and 6m because that's
|
||||||
|
// widely expected by hams to be included. Special case of toggleFilterButtons().
|
||||||
|
function setHamHFBandToggles() {
|
||||||
|
const hamHFBands = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"];
|
||||||
|
$(".filter-button-band").each(function() {
|
||||||
|
$(this).prop('checked', hamHFBands.includes($(this).val().replace("filter-button-band-", "")));
|
||||||
|
});
|
||||||
|
filtersUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate SIGs filter card. This one is also a special case.
|
// Generate SIGs filter card. This one is also a special case.
|
||||||
function generateSIGsMultiToggleFilterCard(sig_options) {
|
function generateSIGsMultiToggleFilterCard(sig_options) {
|
||||||
// Create a button for each option
|
// Create a button for each option
|
||||||
sig_options.forEach(o => {
|
sig_options.forEach(o => {
|
||||||
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-${o['name']}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" id="filter-button-label-sig-${o['name']}" for="filter-button-sig-${o['name']}" title="${o['description']}"><i class="fa-solid fa-${o['icon']}"></i> ${o['name']}</label> `);
|
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-${o['name']}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" id="filter-button-label-sig-${o['name']}" for="filter-button-sig-${o['name']}" title="${o['description']}"><i class="fa-solid ${sigToIcon(o['name'], 'fa-tower-cell')}"></i> ${o['name']}</label> `);
|
||||||
});
|
});
|
||||||
// Create a bonus "NO_SIG" / "General DX" option
|
// Create a bonus "NO_SIG" / "General DX" option
|
||||||
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-NO_SIG" value="NO_SIG" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" id="filter-button-label-sig-NO_SIG" for="filter-button-sig-NO_SIG"><i class="fa-solid fa-tower-cell"></i> General DX</label> `);
|
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-NO_SIG" value="NO_SIG" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" id="filter-button-label-sig-NO_SIG" for="filter-button-sig-NO_SIG"><i class="fa-solid fa-tower-cell"></i> General DX</label> `);
|
||||||
@@ -50,3 +60,19 @@ function toggleDarkMode() {
|
|||||||
enableDarkMode($("#darkMode")[0].checked);
|
enableDarkMode($("#darkMode")[0].checked);
|
||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to update the band colour scheme in spots, bands and map pages
|
||||||
|
function setBandColorSchemeFromUI() {
|
||||||
|
setBandColorScheme($("#band-color-scheme option:selected").val());
|
||||||
|
saveSettings();
|
||||||
|
// Fudge a full reload because we need to update not just colours in the list/map/bands but also the filters
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload spots on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA
|
||||||
|
// after some time has passed with it in the background.
|
||||||
|
addEventListener("visibilitychange", (event) => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
loadSpots();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user