mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-08-06 10:31:42 +00:00
Compare commits
7
Commits
d655354d05
..
1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af1974f36d | ||
|
|
526acf2cfd | ||
|
|
e69bb7a7ec | ||
|
|
f5f92427a8 | ||
|
|
4f56809da7 | ||
|
|
c939a5c1a1 | ||
|
|
c38be5b588 |
+11
-11
@@ -4,7 +4,7 @@ from data.sig import SIG
|
|||||||
|
|
||||||
# General software
|
# General software
|
||||||
SOFTWARE_NAME = "Spothole by M0TRT"
|
SOFTWARE_NAME = "Spothole by M0TRT"
|
||||||
SOFTWARE_VERSION = "1.3-pre"
|
SOFTWARE_VERSION = "1.3"
|
||||||
|
|
||||||
# HTTP headers used for spot providers that use HTTP
|
# HTTP headers used for spot providers that use HTTP
|
||||||
HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"}
|
HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"}
|
||||||
@@ -62,17 +62,17 @@ MODE_ALIASES = {
|
|||||||
BANDS = [
|
BANDS = [
|
||||||
Band(name="2200m", start_freq=135700, end_freq=137800),
|
Band(name="2200m", start_freq=135700, end_freq=137800),
|
||||||
Band(name="600m", start_freq=472000, end_freq=479000),
|
Band(name="600m", start_freq=472000, end_freq=479000),
|
||||||
Band(name="160m", start_freq=1800000, end_freq=2000000),
|
Band(name="160m", start_freq=1800000, end_freq=2000000, is_ham_hf=True),
|
||||||
Band(name="80m", start_freq=3500000, end_freq=4000000),
|
Band(name="80m", start_freq=3500000, end_freq=4000000, is_ham_hf=True),
|
||||||
Band(name="60m", start_freq=5250000, end_freq=5410000),
|
Band(name="60m", start_freq=5250000, end_freq=5410000, is_ham_hf=True),
|
||||||
Band(name="40m", start_freq=7000000, end_freq=7300000),
|
Band(name="40m", start_freq=7000000, end_freq=7300000, is_ham_hf=True),
|
||||||
Band(name="30m", start_freq=10100000, end_freq=10150000),
|
Band(name="30m", start_freq=10100000, end_freq=10150000, is_ham_hf=True),
|
||||||
Band(name="20m", start_freq=14000000, end_freq=14350000),
|
Band(name="20m", start_freq=14000000, end_freq=14350000, is_ham_hf=True),
|
||||||
Band(name="17m", start_freq=18068000, end_freq=18168000),
|
Band(name="17m", start_freq=18068000, end_freq=18168000, is_ham_hf=True),
|
||||||
Band(name="15m", start_freq=21000000, end_freq=21450000),
|
Band(name="15m", start_freq=21000000, end_freq=21450000, is_ham_hf=True),
|
||||||
Band(name="12m", start_freq=24890000, end_freq=24990000),
|
Band(name="12m", start_freq=24890000, end_freq=24990000, is_ham_hf=True),
|
||||||
Band(name="11m", start_freq=26965000, end_freq=27405000),
|
Band(name="11m", start_freq=26965000, end_freq=27405000),
|
||||||
Band(name="10m", start_freq=28000000, end_freq=29700000),
|
Band(name="10m", start_freq=28000000, end_freq=29700000, is_ham_hf=True),
|
||||||
Band(name="6m", start_freq=50000000, end_freq=54000000),
|
Band(name="6m", start_freq=50000000, end_freq=54000000),
|
||||||
Band(name="5m", start_freq=56000000, end_freq=60500000),
|
Band(name="5m", start_freq=56000000, end_freq=60500000),
|
||||||
Band(name="4m", start_freq=70000000, end_freq=70500000),
|
Band(name="4m", start_freq=70000000, end_freq=70500000),
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ class Band:
|
|||||||
start_freq: float
|
start_freq: float
|
||||||
# Stop frequency, in Hz
|
# Stop frequency, in Hz
|
||||||
end_freq: float
|
end_freq: float
|
||||||
|
# Whether this is an HF amateur radio band
|
||||||
|
is_ham_hf: bool = False
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ class SolarConditions:
|
|||||||
blackout_forecast_r1r2: dict = None
|
blackout_forecast_r1r2: dict = None
|
||||||
# NOAA Radio Blackout (R3 or greater) probability forecast, keyed by UNIX timestamp of start of day UTC
|
# NOAA Radio Blackout (R3 or greater) probability forecast, keyed by UNIX timestamp of start of day UTC
|
||||||
blackout_forecast_r3_or_greater: dict = None
|
blackout_forecast_r3_or_greater: dict = None
|
||||||
# Ionosonde measurements from LGDC, dict keyed by URSI code, values are dicts with keys: ursi, name, fof2, muf
|
# Ionosonde measurements from LGDC, dict keyed by URSI code, values are dicts with keys: ursi, name, fof2, muf, luf,
|
||||||
|
# band_states
|
||||||
ionosonde_data: dict = None
|
ionosonde_data: dict = None
|
||||||
|
|
||||||
# Derived values (populated by infer_descriptions())
|
# Derived values (populated by infer_descriptions())
|
||||||
@@ -196,6 +197,7 @@ class SolarConditions:
|
|||||||
self.electron_flux_desc = _lookup_by_threshold(self.electron_flux, ELECTRON_FLUX_DESCRIPTIONS)
|
self.electron_flux_desc = _lookup_by_threshold(self.electron_flux, ELECTRON_FLUX_DESCRIPTIONS)
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
"""JSON serialise"""
|
"""JSON serialise. Dict key order is insertion order (Python 3.7+ guarantee), so callers receive
|
||||||
|
fields in a predictable, logical sequence without relying on sort_keys."""
|
||||||
|
|
||||||
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
|
return json.dumps(self, default=lambda o: o.__dict__)
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ from threading import Thread, Event
|
|||||||
import pytz
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS, BANDS
|
||||||
from solarconditionsproviders.solar_conditions_provider import SolarConditionsProvider
|
from solarconditionsproviders.solar_conditions_provider import SolarConditionsProvider
|
||||||
|
|
||||||
POLL_INTERVAL = 3600 # 1 hour
|
POLL_INTERVAL = 3600 # 1 hour
|
||||||
STATIONS_INDEX = "datafiles/didbase-stations.csv"
|
STATIONS_INDEX = "datafiles/didbase-stations.csv"
|
||||||
LGDC_URL = "https://lgdc.uml.edu/common/DIDBGetValues"
|
LGDC_URL = "https://lgdc.uml.edu/common/DIDBGetValues"
|
||||||
HISTORY_HOURS = 24
|
HISTORY_HOURS = 24
|
||||||
|
HF_BANDS = [b for b in BANDS if b.is_ham_hf]
|
||||||
|
|
||||||
|
|
||||||
class GIROIonosonde(SolarConditionsProvider):
|
class GIROIonosonde(SolarConditionsProvider):
|
||||||
"""Solar conditions provider using ionosonde data from the GIRO Data Center.
|
"""Solar conditions provider using ionosonde data from the GIRO Data Center.
|
||||||
Queries foF2 and MUF measurements for all stations in datafiles/didbase-stations.csv."""
|
Queries foF2, MUF, and LUF measurements for all stations in datafiles/didbase-stations.csv."""
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(provider_config)
|
super().__init__(provider_config)
|
||||||
@@ -41,7 +42,8 @@ class GIROIonosonde(SolarConditionsProvider):
|
|||||||
|
|
||||||
super().setup(solar_conditions, solar_conditions_cache)
|
super().setup(solar_conditions, solar_conditions_cache)
|
||||||
self.update_data({"ionosonde_data": {
|
self.update_data({"ionosonde_data": {
|
||||||
s["ursi"]: {"ursi": s["ursi"], "name": s["name"], "fof2": None, "muf": None}
|
s["ursi"]: {"ursi": s["ursi"], "name": s["name"], "fof2": None, "muf": None, "luf": None,
|
||||||
|
"band_states": None}
|
||||||
for s in self._stations
|
for s in self._stations
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -73,9 +75,11 @@ class GIROIonosonde(SolarConditionsProvider):
|
|||||||
ursi = station["ursi"]
|
ursi = station["ursi"]
|
||||||
name = station["name"]
|
name = station["name"]
|
||||||
try:
|
try:
|
||||||
fof2, muf = self._fetch_station_data(ursi, from_time, now)
|
fof2, muf, luf = self._fetch_station_data(ursi, from_time, now)
|
||||||
if fof2 and muf:
|
if fof2 and muf:
|
||||||
ionosonde_data[ursi] = {"ursi": ursi, "name": name, "fof2": fof2, "muf": muf}
|
band_states = self._compute_band_statess(fof2, muf, luf or {})
|
||||||
|
ionosonde_data[ursi] = {"ursi": ursi, "name": name, "fof2": fof2, "muf": muf,
|
||||||
|
"luf": luf or None, "band_states": band_states}
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.warning(f"Could not fetch ionosonde data for {ursi} ({name})")
|
logging.warning(f"Could not fetch ionosonde data for {ursi} ({name})")
|
||||||
@@ -91,27 +95,62 @@ class GIROIonosonde(SolarConditionsProvider):
|
|||||||
self._stop_event.wait(timeout=1)
|
self._stop_event.wait(timeout=1)
|
||||||
|
|
||||||
def _fetch_station_data(self, ursi, from_time, to_time):
|
def _fetch_station_data(self, ursi, from_time, to_time):
|
||||||
"""Fetch foF2 and MUF readings for a station. Returns (fof2_dict, muf_dict) keyed by UNIX timestamp."""
|
"""Fetch foF2, MUF and LUF readings for a station. Returns (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp."""
|
||||||
|
|
||||||
from_str = from_time.strftime("%Y.%m.%d+%H:%M:%S")
|
from_str = from_time.strftime("%Y.%m.%d+%H:%M:%S")
|
||||||
to_str = to_time.strftime("%Y.%m.%d+%H:%M:%S")
|
to_str = to_time.strftime("%Y.%m.%d+%H:%M:%S")
|
||||||
url = f"{LGDC_URL}?ursiCode={ursi}&charName=foF2,MUFD&DMUF=3000&fromDate={from_str}&toDate={to_str}"
|
url = f"{LGDC_URL}?ursiCode={ursi}&charName=foF2,MUFD,fmin&DMUF=3000&fromDate={from_str}&toDate={to_str}"
|
||||||
response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15))
|
response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15))
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return None, None
|
return None, None, None
|
||||||
return self._parse_all(response.text)
|
return self._parse_all(response.text)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _latest(d):
|
||||||
|
"""Return the value with the highest timestamp key, or None if the dict is empty."""
|
||||||
|
return d[max(d.keys())] if d else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _compute_band_statess(fof2_dict, muf_dict, luf_dict):
|
||||||
|
"""Compute HF band states from the latest foF2, MUF and LUF values.
|
||||||
|
|
||||||
|
States:
|
||||||
|
Closed if band frequency is below LUF (if known) or above MUF
|
||||||
|
Short if band frequency is >= LUF and < foF2 (good for NVIS)
|
||||||
|
Long if band frequency is >= foF2 and < MUF (good for DX)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We have a list of timestamped data for each value, but for this we only want the latest value
|
||||||
|
fof2 = GIROIonosonde._latest(fof2_dict)
|
||||||
|
muf = GIROIonosonde._latest(muf_dict)
|
||||||
|
luf = GIROIonosonde._latest(luf_dict)
|
||||||
|
if fof2 is None or muf is None:
|
||||||
|
return {}
|
||||||
|
band_states = {}
|
||||||
|
|
||||||
|
# Iterate over all ham HF bands, we don't care about the others at this point
|
||||||
|
for band in HF_BANDS:
|
||||||
|
freq = band.start_freq / 1000000
|
||||||
|
if freq > muf or (luf is not None and freq < luf):
|
||||||
|
band_states[band.name] = "Closed"
|
||||||
|
elif freq < fof2:
|
||||||
|
band_states[band.name] = "Short"
|
||||||
|
else:
|
||||||
|
band_states[band.name] = "Long"
|
||||||
|
return band_states
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_all(text):
|
def _parse_all(text):
|
||||||
"""Parse web server response and return (fof2_dict, muf_dict) keyed by UNIX timestamp."""
|
"""Parse web server response and return (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp."""
|
||||||
|
|
||||||
fof2_data = {}
|
fof2_data = {}
|
||||||
muf_data = {}
|
muf_data = {}
|
||||||
|
luf_data = {}
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith('#'):
|
if not line or line.startswith('#'):
|
||||||
continue
|
continue
|
||||||
# Data rows have the following format: timestamp CS foF2 QD MUFD QD
|
# Data rows have the following format: timestamp CS foF2 QD MUFD QD fmin QD
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) >= 5:
|
if len(parts) >= 5:
|
||||||
try:
|
try:
|
||||||
@@ -127,4 +166,9 @@ class GIROIonosonde(SolarConditionsProvider):
|
|||||||
muf_data[ts] = float(parts[4])
|
muf_data[ts] = float(parts[4])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return fof2_data, muf_data
|
if len(parts) >= 7:
|
||||||
|
try:
|
||||||
|
luf_data[ts] = float(parts[6])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return fof2_data, muf_data, luf_data
|
||||||
|
|||||||
@@ -69,7 +69,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=1778927183"></script>
|
<script src="/js/common.js?v=1780424170"></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 %}
|
||||||
@@ -21,11 +21,11 @@
|
|||||||
<form class="row g-3">
|
<form class="row g-3">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="dx-call" class="form-label">DX Call *</label>
|
<label for="dx-call" class="form-label">DX Call *</label>
|
||||||
<input type="text" class="form-control" id="dx-call" placeholder="N0CALL" style="max-width: 8em;">
|
<input type="text" class="form-control input-narrow" id="dx-call" placeholder="N0CALL">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="freq" class="form-label">Frequency (kHz) *</label>
|
<label for="freq" class="form-label">Frequency (kHz) *</label>
|
||||||
<input type="text" class="form-control" id="freq" placeholder="e.g. 14100" style="max-width: 8em;">
|
<input type="text" class="form-control input-narrow" id="freq" placeholder="e.g. 14100">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="mode" class="form-label">Mode</label>
|
<label for="mode" class="form-label">Mode</label>
|
||||||
@@ -41,22 +41,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="sig-ref" class="form-label">SIG Reference</label>
|
<label for="sig-ref" class="form-label">SIG Reference</label>
|
||||||
<input type="text" class="form-control" id="sig-ref" placeholder="e.g. GB-0001" style="max-width: 8em;">
|
<input type="text" class="form-control input-narrow" id="sig-ref" placeholder="e.g. GB-0001">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="dx-grid" class="form-label">DX Grid</label>
|
<label for="dx-grid" class="form-label">DX Grid</label>
|
||||||
<input type="text" class="form-control" id="dx-grid" placeholder="e.g. AA00aa" style="max-width: 8em;">
|
<input type="text" class="form-control input-narrow" id="dx-grid" placeholder="e.g. AA00aa">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="comment" class="form-label">Comment</label>
|
<label for="comment" class="form-label">Comment</label>
|
||||||
<input type="text" class="form-control" id="comment" placeholder="e.g. 59 TNX QSO 73" style="max-width: 12em;">
|
<input type="text" class="form-control input-medium" id="comment" placeholder="e.g. 59 TNX QSO 73">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="de-call" class="form-label">Your Call *</label>
|
<label for="de-call" class="form-label">Your Call *</label>
|
||||||
<input type="text" class="form-control storeable-text" id="de-call" placeholder="N0CALL" style="max-width: 8em;">
|
<input type="text" class="form-control storeable-text input-narrow" id="de-call" placeholder="N0CALL">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button type="button" class="btn btn-primary" style="margin-top: 2em;" onclick="addSpot();">Spot</button>
|
<button type="button" class="btn btn-primary mt-2em" onclick="addSpot();">Spot</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1780424170"></script>
|
||||||
<script src="/js/add-spot.js?v=1778927183"></script>
|
<script src="/js/add-spot.js?v=1780424170"></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 %}
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1780424170"></script>
|
||||||
<script src="/js/alerts.js?v=1778927183"></script>
|
<script src="/js/alerts.js?v=1780424170"></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 %}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "skeleton.html" %}
|
||||||
{% block content %}
|
{% block body %}
|
||||||
|
|
||||||
<redoc spec-url="/apidocs/openapi.yml"></redoc>
|
<redoc spec-url="/apidocs/openapi.yml"></redoc>
|
||||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script>
|
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-api").addClass("active"); }); <!-- highlight active page in nav --></script>
|
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -76,9 +76,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1780424170"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1778927183"></script>
|
<script src="/js/spotsbandsandmap.js?v=1780424170"></script>
|
||||||
<script src="/js/bands.js?v=1778927183"></script>
|
<script src="/js/bands.js?v=1780424170"></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 %}
|
||||||
+10
-45
@@ -1,44 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "skeleton.html" %}
|
||||||
<html lang="en">
|
{% block head_extra %}
|
||||||
<head>
|
<link rel="stylesheet" href="/css/style.css?v=1780424170" type="text/css">
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
||||||
<meta name="color-scheme" content="light dark">
|
|
||||||
<meta name="theme-color" content="white"/>
|
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="white-translucent">
|
|
||||||
|
|
||||||
<meta property="og:title" content="Spothole"/>
|
|
||||||
<meta property="twitter:title" content="Spothole"/>
|
|
||||||
<meta name="description" content="An Amateur Radio spotting tool bringing together DX clusters and outdoor programmes, providing a universal JSON API and web interface."/>
|
|
||||||
<meta property="og:description" content="An Amateur Radio spotting tool bringing together DX clusters and outdoor programmes, providing a universal JSON API and web interface."/>
|
|
||||||
<link rel="canonical" href="https://spothole.app/"/>
|
|
||||||
<meta property="og:url" content="https://spothole.app/"/>
|
|
||||||
<meta property="og:image" content="https://spothole.app/img/banner.png"/>
|
|
||||||
<meta property="twitter:image" content="https://spothole.app/img/banner.png"/>
|
|
||||||
<meta name="twitter:card" content="summary_large_image"/>
|
|
||||||
<meta name="author" content="Ian Renton"/>
|
|
||||||
<meta property="og:locale" content="en_GB"/>
|
|
||||||
<meta property="og:type" content="website"/>
|
|
||||||
|
|
||||||
<title>Spothole</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/style.css?v=1778927183" type="text/css">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||||
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
|
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
|
||||||
<link href="/fa/css/solid.min.css" rel="stylesheet" />
|
<link href="/fa/css/solid.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="/img/icon-512.png">
|
|
||||||
<link rel="apple-touch-icon" href="img/icon-512-pwa.png">
|
|
||||||
<link rel="alternate icon" type="image/png" href="/img/icon-192.png">
|
|
||||||
<link rel="alternate icon" type="image/png" href="/img/icon-32.png">
|
|
||||||
<link rel="alternate icon" type="image/png" href="/img/icon-16.png">
|
|
||||||
<link rel="alternate icon" type="image/x-icon" href="/favicon.ico">
|
|
||||||
|
|
||||||
<link rel="manifest" href="manifest.webmanifest">
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
|
||||||
integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs"
|
integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
@@ -52,12 +19,11 @@
|
|||||||
integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn"
|
integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1778927183"></script>
|
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1780424170"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1778927183"></script>
|
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1780424170"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1778927183"></script>
|
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1780424170"></script>
|
||||||
|
{% end %}
|
||||||
</head>
|
{% block body %}
|
||||||
<body>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<nav id="header" class="navbar navbar-expand-lg bg-body p-0 border-bottom">
|
<nav id="header" class="navbar navbar-expand-lg bg-body p-0 border-bottom">
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
@@ -96,7 +62,7 @@
|
|||||||
<div id="footer" class="hideonmobile hideonmap">
|
<div id="footer" class="hideonmobile hideonmap">
|
||||||
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
|
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
|
||||||
<p class="col-md-4 mb-0 text-body-secondary">Made with love by <a href="https://ianrenton.com" class="text-body-secondary">Ian, MØTRT</a> and other contributors.</p>
|
<p class="col-md-4 mb-0 text-body-secondary">Made with love by <a href="https://ianrenton.com" class="text-body-secondary">Ian, MØTRT</a> and other contributors.</p>
|
||||||
<p class="col-md-4 mb-0 justify-content-center text-body-secondary" style="text-align: center;">Spothole v{{software_version}}</p>
|
<p class="col-md-4 mb-0 justify-content-center text-body-secondary text-center">Spothole v{{software_version}}</p>
|
||||||
<ul class="nav col-md-4 justify-content-end">
|
<ul class="nav col-md-4 justify-content-end">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="/about#faq" class="nav-link px-3 text-body-secondary">FAQ</a>
|
<a href="/about#faq" class="nav-link px-3 text-body-secondary">FAQ</a>
|
||||||
@@ -117,5 +83,4 @@
|
|||||||
|
|
||||||
<div id="embeddedModeFooter" class="text-body-secondary pt-2 px-3 pb-1">Powered by <img src="/img/logo.png" class="logo" width="96" height="30" alt="Spothole"></div>
|
<div id="embeddedModeFooter" class="text-body-secondary pt-2 px-3 pb-1">Powered by <img src="/img/logo.png" class="logo" width="96" height="30" alt="Spothole"></div>
|
||||||
|
|
||||||
</body>
|
{% end %}
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -181,11 +181,33 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ionosonde-station" class="form-label">Ionosonde station:</label>
|
<label for="ionosonde-station" class="form-label">Ionosonde station:</label>
|
||||||
<select id="ionosonde-station" class="form-select storeable-select d-inline-block ms-2"
|
<select id="ionosonde-station" class="form-select storeable-select d-inline-block ms-2 w-auto"
|
||||||
style="width: auto;" oninput="ionosondeStationChanged();">
|
oninput="ionosondeStationChanged();">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="ionosonde-latest" class="mb-3"></div>
|
<div id="ionosonde-latest" class="mb-3">
|
||||||
|
<div id="ionosonde-no-data" class="alert alert-warning mt-2 mb-0 py-2 js-hidden">No data available for this station.</div>
|
||||||
|
<div id="ionosonde-data-rows" class="js-hidden">
|
||||||
|
<div class="row align-items-center me-0">
|
||||||
|
<div class="col-12 py-2 text-muted">Latest values as of <span id="ionosonde-latest-time"></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="row align-items-center me-0">
|
||||||
|
<div class="col-12 col-md-4 py-2">LUF: <strong id="ionosonde-latest-luf"></strong></div>
|
||||||
|
<div class="col-12 col-md-4 py-2">foF2: <strong id="ionosonde-latest-fof2"></strong></div>
|
||||||
|
<div class="col-12 col-md-4 py-2">MUF (3000 km): <strong id="ionosonde-latest-muf"></strong></div>
|
||||||
|
</div>
|
||||||
|
<div id="ionosonde-stale-warning" class="alert alert-warning mt-2 mb-0 py-2 js-hidden">Data is more than 12 hours old!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="ionosonde-band-state" class="mb-3 js-hidden">
|
||||||
|
<table class="table table-sm table-bordered mb-0 d-none d-md-table table-fixed-on-desktop">
|
||||||
|
<thead><tr id="ionosonde-band-state-head"></tr></thead>
|
||||||
|
<tbody><tr id="ionosonde-band-state-row"></tr></tbody>
|
||||||
|
</table>
|
||||||
|
<table class="table table-sm table-bordered mb-0 d-md-none">
|
||||||
|
<tbody id="ionosonde-band-state-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
<canvas id="ionosonde-chart" class="mt-3 mb-3 hideonmobile"></canvas>
|
<canvas id="ionosonde-chart" class="mt-3 mb-3 hideonmobile"></canvas>
|
||||||
<div class="form-text mt-2">Data from the <a href="https://lgdc.uml.edu/">Lowell GIRO Data Center</a>.</div>
|
<div class="form-text mt-2">Data from the <a href="https://lgdc.uml.edu/">Lowell GIRO Data Center</a>.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,8 +221,8 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="dxstats-de-continent" class="form-label">Your continent:</label>
|
<label for="dxstats-de-continent" class="form-label">Your continent:</label>
|
||||||
<select id="dxstats-de-continent" class="form-select storeable-select d-inline-block ms-2"
|
<select id="dxstats-de-continent" class="form-select storeable-select d-inline-block ms-2 w-auto"
|
||||||
style="width: auto;" oninput="dxStatsContientChanged();">
|
oninput="dxStatsContientChanged();">
|
||||||
<option value="EU">Europe</option>
|
<option value="EU">Europe</option>
|
||||||
<option value="NA">North America</option>
|
<option value="NA">North America</option>
|
||||||
<option value="SA">South America</option>
|
<option value="SA">South America</option>
|
||||||
@@ -211,7 +233,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-bordered mb-0">
|
<table class="table table-sm table-bordered mb-0 table-fixed-on-desktop">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -249,8 +271,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1780424170"></script>
|
||||||
<script src="/js/conditions.js?v=1778927183"></script>
|
<script src="/js/conditions.js?v=1780424170"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-conditions").addClass("active");
|
$("#nav-link-conditions").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
+4
-4
@@ -2,7 +2,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div id="map">
|
<div id="map">
|
||||||
<div id="settingsButtonRowMap" class="mt-3 px-3" style="z-index: 1002; position: relative;">
|
<div id="settingsButtonRowMap" class="mt-3 px-3">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-auto me-auto pt-3"></div>
|
<div class="col-auto me-auto pt-3"></div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1780424170"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1778927183"></script>
|
<script src="/js/spotsbandsandmap.js?v=1780424170"></script>
|
||||||
<script src="/js/map.js?v=1778927183"></script>
|
<script src="/js/map.js?v=1780424170"></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 %}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
|
<meta name="theme-color" content="white"/>
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="white-translucent">
|
||||||
|
|
||||||
|
<meta property="og:title" content="Spothole"/>
|
||||||
|
<meta property="twitter:title" content="Spothole"/>
|
||||||
|
<meta name="description" content="An Amateur Radio spotting tool bringing together DX clusters and outdoor programmes, providing a universal JSON API and web interface."/>
|
||||||
|
<meta property="og:description" content="An Amateur Radio spotting tool bringing together DX clusters and outdoor programmes, providing a universal JSON API and web interface."/>
|
||||||
|
<link rel="canonical" href="https://spothole.app/"/>
|
||||||
|
<meta property="og:url" content="https://spothole.app/"/>
|
||||||
|
<meta property="og:image" content="https://spothole.app/img/banner.png"/>
|
||||||
|
<meta property="twitter:image" content="https://spothole.app/img/banner.png"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="author" content="Ian Renton"/>
|
||||||
|
<meta property="og:locale" content="en_GB"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
|
||||||
|
<title>Spothole</title>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="/img/icon-512.png">
|
||||||
|
<link rel="apple-touch-icon" href="img/icon-512-pwa.png">
|
||||||
|
<link rel="alternate icon" type="image/png" href="/img/icon-192.png">
|
||||||
|
<link rel="alternate icon" type="image/png" href="/img/icon-32.png">
|
||||||
|
<link rel="alternate icon" type="image/png" href="/img/icon-16.png">
|
||||||
|
<link rel="alternate icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
|
||||||
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
|
|
||||||
|
{% block head_extra %}{% end %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}{% end %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1780424170"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1778927183"></script>
|
<script src="/js/spotsbandsandmap.js?v=1780424170"></script>
|
||||||
<script src="/js/spots.js?v=1778927183"></script>
|
<script src="/js/spots.js?v=1780424170"></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 %}
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1780424170"></script>
|
||||||
<script src="/js/status.js?v=1778927183"></script>
|
<script src="/js/status.js?v=1780424170"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+572
-610
File diff suppressed because it is too large
Load Diff
@@ -211,6 +211,11 @@ div#map {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#settingsButtonRowMap {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1002;
|
||||||
|
}
|
||||||
|
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
font-family: var(--bs-body-font-family) !important;
|
font-family: var(--bs-body-font-family) !important;
|
||||||
}
|
}
|
||||||
@@ -323,6 +328,31 @@ div.band-spot:hover span.band-spot-info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* UTILITY CLASSES */
|
||||||
|
|
||||||
|
/* For elements initially hidden and shown/hidden by JS. Unlike Bootstrap's d-none this has no !important,
|
||||||
|
so jQuery's .show() / .hide() / .toggle() can override it with an inline style as expected. */
|
||||||
|
.js-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-fixed-on-desktop {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-narrow {
|
||||||
|
max-width: 8em;
|
||||||
|
}
|
||||||
|
.input-medium {
|
||||||
|
max-width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pushes a submit button down to align with labelled inputs in a flex row */
|
||||||
|
.mt-2em {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* GENERAL MOBILE SUPPORT */
|
/* GENERAL MOBILE SUPPORT */
|
||||||
|
|
||||||
@media (max-width: 991.99px) {
|
@media (max-width: 991.99px) {
|
||||||
@@ -346,6 +376,9 @@ div.band-spot:hover span.band-spot-info {
|
|||||||
input#search {
|
input#search {
|
||||||
max-width: 7em;
|
max-width: 7em;
|
||||||
}
|
}
|
||||||
|
.table-fixed-on-desktop {
|
||||||
|
table-layout: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
|
|||||||
+44
-19
@@ -389,11 +389,12 @@ function renderIonosondeData() {
|
|||||||
const station = ionosondeData[ursi];
|
const station = ionosondeData[ursi];
|
||||||
if (!station) return;
|
if (!station) return;
|
||||||
|
|
||||||
// Set up some styles, matching the k-index chart. We use Bootstrap's "primary" and "danger" colours not for any
|
// Set up some styles, matching the k-index chart. We use Bootstrap's "primary", "danger", and "success" colours
|
||||||
// real reason but just to get a suitable blue and red that match the other colours Spothole uses
|
// not for any real reason but just to get a suitable blue, red, and green that match the other colours Spothole uses
|
||||||
const style = getComputedStyle(document.documentElement);
|
const style = getComputedStyle(document.documentElement);
|
||||||
const fof2Color = style.getPropertyValue('--bs-primary').trim();
|
const fof2Color = style.getPropertyValue('--bs-primary').trim();
|
||||||
const mufColor = style.getPropertyValue('--bs-danger').trim();
|
const mufColor = style.getPropertyValue('--bs-success').trim();
|
||||||
|
const lufColor = style.getPropertyValue('--bs-danger').trim();
|
||||||
const textColor = style.getPropertyValue('--bs-body-color').trim() || '#666';
|
const textColor = style.getPropertyValue('--bs-body-color').trim() || '#666';
|
||||||
const gridColor = style.getPropertyValue('--bs-border-color').trim() || 'rgba(128,128,128,0.3)';
|
const gridColor = style.getPropertyValue('--bs-border-color').trim() || 'rgba(128,128,128,0.3)';
|
||||||
|
|
||||||
@@ -407,36 +408,52 @@ function renderIonosondeData() {
|
|||||||
|
|
||||||
const fof2Entries = toSeries(station.fof2);
|
const fof2Entries = toSeries(station.fof2);
|
||||||
const mufEntries = toSeries(station.muf);
|
const mufEntries = toSeries(station.muf);
|
||||||
const allTs = [...fof2Entries, ...mufEntries].map(e => e.ts);
|
const lufEntries = toSeries(station.luf);
|
||||||
|
const allTs = [...fof2Entries, ...mufEntries, ...lufEntries].map(e => e.ts);
|
||||||
if (allTs.length === 0) {
|
if (allTs.length === 0) {
|
||||||
$('#ionosonde-latest').html('<div class="alert alert-warning mt-2 mb-0 py-2">No data available for this station.</div>');
|
$('#ionosonde-no-data').show();
|
||||||
|
$('#ionosonde-data-rows').hide();
|
||||||
|
$('#ionosonde-band-state').hide();
|
||||||
$('#ionosonde-chart').hide();
|
$('#ionosonde-chart').hide();
|
||||||
if (ionosondeChart) { ionosondeChart.destroy(); ionosondeChart = null; }
|
if (ionosondeChart) { ionosondeChart.destroy(); ionosondeChart = null; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$('#ionosonde-no-data').hide();
|
||||||
|
$('#ionosonde-data-rows').show();
|
||||||
|
|
||||||
// Populate latest values summary (visible on all screen sizes)
|
// Populate latest values summary (visible on all screen sizes)
|
||||||
const latestFof2 = fof2Entries.length ? fof2Entries[fof2Entries.length - 1].val : null;
|
const latestFof2 = fof2Entries.length ? fof2Entries[fof2Entries.length - 1].val : null;
|
||||||
const latestMuf = mufEntries.length ? mufEntries[mufEntries.length - 1].val : null;
|
const latestMuf = mufEntries.length ? mufEntries[mufEntries.length - 1].val : null;
|
||||||
|
const latestLuf = lufEntries.length ? lufEntries[lufEntries.length - 1].val : null;
|
||||||
const minTs = allTs.length ? Math.min(...allTs) : null;
|
const minTs = allTs.length ? Math.min(...allTs) : null;
|
||||||
const maxTs = allTs.length ? Math.max(...allTs) : null;
|
const maxTs = allTs.length ? Math.max(...allTs) : null;
|
||||||
let latestTimeStr = '';
|
|
||||||
if (maxTs != null) {
|
if (maxTs != null) {
|
||||||
const latestDate = moment.utc(maxTs * 1000);
|
const latestDate = moment.utc(maxTs * 1000);
|
||||||
latestTimeStr = latestDate.format('DD MMM YYYY HH:mm [UTC]') + ' (' + latestDate.fromNow() + ')';
|
$('#ionosonde-latest-time').text(latestDate.format('DD MMM YYYY HH:mm [UTC]') + ' (' + latestDate.fromNow() + ')');
|
||||||
|
}
|
||||||
|
$('#ionosonde-latest-luf').text(latestLuf !== null ? latestLuf.toFixed(2) + ' MHz' : 'N/A');
|
||||||
|
$('#ionosonde-latest-fof2').text(latestFof2 !== null ? latestFof2.toFixed(2) + ' MHz' : 'N/A');
|
||||||
|
$('#ionosonde-latest-muf').text(latestMuf !== null ? latestMuf.toFixed(2) + ' MHz' : 'N/A');
|
||||||
|
$('#ionosonde-stale-warning').toggle(maxTs !== null && (Date.now() / 1000 - maxTs) > 12 * 3600);
|
||||||
|
|
||||||
|
// Populate band state tables. There are actually two tables to populate, which is pretty janky, but allows us to
|
||||||
|
// display horizontally on desktop but flip it around to become a vertical list on mobile.
|
||||||
|
const bandStateClass = {'Closed': 'bg-danger-subtle', 'Short': 'bg-primary-subtle', 'Long': 'bg-success-subtle'};
|
||||||
|
const bandStates = station.band_states;
|
||||||
|
if (bandStates && Object.keys(bandStates).length > 0) {
|
||||||
|
const headRow = $('#ionosonde-band-state-head').empty();
|
||||||
|
const dataRow = $('#ionosonde-band-state-row').empty();
|
||||||
|
const vBody = $('#ionosonde-band-state-body').empty();
|
||||||
|
Object.entries(bandStates).forEach(([band, state]) => {
|
||||||
|
const cls = bandStateClass[state] || '';
|
||||||
|
headRow.append($('<th>').addClass('text-center').text(band));
|
||||||
|
dataRow.append($('<td>').addClass('text-center ' + cls).text(state));
|
||||||
|
vBody.append($('<tr>').append($('<td>').addClass('fw-bold').text(band)).append($('<td>').addClass(cls).text(state)));
|
||||||
|
});
|
||||||
|
$('#ionosonde-band-state').show();
|
||||||
|
} else {
|
||||||
|
$('#ionosonde-band-state').hide();
|
||||||
}
|
}
|
||||||
const staleWarning = (maxTs !== null && (Date.now() / 1000 - maxTs) > 12 * 3600)
|
|
||||||
? '<div class="alert alert-warning mt-2 mb-0 py-2">Data is more than 12 hours old!</div>'
|
|
||||||
: '';
|
|
||||||
$('#ionosonde-latest').html(
|
|
||||||
'<div class="row border-bottom align-items-center me-0">' +
|
|
||||||
'<div class="col-12 col-md-6 py-2 text-muted">Latest values as of ' + latestTimeStr + '</div>' +
|
|
||||||
'<div class="col-12 col-md-2 py-2">foF2: <strong>' + (latestFof2 !== null ? latestFof2.toFixed(2) + ' MHz' : 'N/A') + '</strong></div>' +
|
|
||||||
'<div class="col-12 col-md-4 py-2">MUF (3000 km): <strong>' + (latestMuf !== null ? latestMuf.toFixed(2) + ' MHz' : 'N/A') + '</strong></div>' +
|
|
||||||
'</div>' +
|
|
||||||
staleWarning +
|
|
||||||
'</div>'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ionosondeChart) {
|
if (ionosondeChart) {
|
||||||
ionosondeChart.destroy();
|
ionosondeChart.destroy();
|
||||||
@@ -546,6 +563,14 @@ function renderIonosondeData() {
|
|||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
datasets: [
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'LUF',
|
||||||
|
data: lufEntries.map(e => ({x: e.ts, y: e.val})),
|
||||||
|
borderColor: lufColor,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.2,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'foF2',
|
label: 'foF2',
|
||||||
data: fof2Entries.map(e => ({x: e.ts, y: e.val})),
|
data: fof2Entries.map(e => ({x: e.ts, y: e.val})),
|
||||||
|
|||||||
Reference in New Issue
Block a user