6 Commits

27 changed files with 219 additions and 356 deletions

View File

@@ -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)

View File

@@ -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"]

View File

@@ -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:

View File

@@ -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.

View File

@@ -8,8 +8,4 @@ class Band:
# Start frequency, in Hz # Start frequency, in Hz
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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"],

View File

@@ -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=3"></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 %}

View File

@@ -69,8 +69,8 @@
</div> </div>
<script src="/js/common.js?v=3"></script> <script src="/js/common.js?v=5"></script>
<script src="/js/add-spot.js?v=3"></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 %}

View File

@@ -168,8 +168,8 @@
</div> </div>
<script src="/js/common.js?v=3"></script> <script src="/js/common.js?v=5"></script>
<script src="/js/alerts.js?v=3"></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 %}

View File

@@ -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=3"></script> <script src="/js/common.js?v=5"></script>
<script src="/js/spotsbandsandmap.js?v=3"></script> <script src="/js/spotsbandsandmap.js?v=5"></script>
<script src="/js/bands.js?v=3"></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 %}

View File

@@ -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>

View File

@@ -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=3"></script> <script src="/js/common.js?v=5"></script>
<script src="/js/spotsbandsandmap.js?v=3"></script> <script src="/js/spotsbandsandmap.js?v=5"></script>
<script src="/js/map.js?v=3"></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 %}

View File

@@ -154,12 +154,17 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Theme</h5> <h5 class="card-title">Theme</h5>
<div class="form-group"> <div class="form-group">
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<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> </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=3"></script> <script src="/js/common.js?v=5"></script>
<script src="/js/spotsbandsandmap.js?v=3"></script> <script src="/js/spotsbandsandmap.js?v=5"></script>
<script src="/js/spots.js?v=3"></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 %}

View File

@@ -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=3"></script> <script src="/js/common.js?v=5"></script>
<script src="/js/status.js?v=3"></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 %}

View File

@@ -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.

View File

@@ -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 */

View File

@@ -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} `);

View File

@@ -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.

View File

@@ -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 '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case '\'': return '&#039;';
case '`': return '&#096;';
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();

View File

@@ -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) {
var geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], { try {
color: s["band_color"], var geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], {
wrap: false, color: bandToColor(s['band']),
steps: 5 wrap: false,
}); 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>&nbsp;${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>&nbsp;${sigSourceText} ${sig_refs}</span><br/>`;
// Time // Time
ttt += `<span class='icon-wrapper'><i class='fa-solid fa-clock markerPopupIcon'></i></span>&nbsp;${moment.unix(s["time"]).fromNow()}`; ttt += `<span class='icon-wrapper'><i class='fa-solid fa-clock markerPopupIcon'></i></span>&nbsp;${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.

View File

@@ -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) {
evtSource.close(); if (evtSource != null) {
setTimeout(startSSEConnection, 1000); evtSource.close();
}
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
@@ -280,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(", ");
@@ -318,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;"}'>&#9632;</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;"}'>&#9632;</span>${freq_string}</td>`);
} }
if (showMode) { if (showMode) {
$tr.append(`<td class='nowrap'>${mode_string}</td>`); $tr.append(`<td class='nowrap'>${mode_string}</td>`);
@@ -333,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>`);
@@ -357,17 +364,25 @@ function createNewTableRowsForSpot(s, highlightNew) {
} }
$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(` &nbsp; Bearing: ${bearingText} `); $td2floatright.append(`${bearingText} &nbsp;`);
} }
if (showDE) {
$td2floatright.append(` de ${de_call} &nbsp;`);
}
$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);
@@ -383,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"]);
@@ -394,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.

View File

@@ -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>&nbsp;<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> `);
@@ -51,6 +61,14 @@ function toggleDarkMode() {
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 // 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. // after some time has passed with it in the background.
addEventListener("visibilitychange", (event) => { addEventListener("visibilitychange", (event) => {