mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-04-29 18:25:58 +00:00
Compare commits
18 Commits
76b0ec24b7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
461ce94204 | ||
|
|
49949a0b2e | ||
|
|
a3332aa023 | ||
|
|
ac1ab4bd2d | ||
|
|
82944b9c38 | ||
|
|
36dba30089 | ||
|
|
1ed175e099 | ||
|
|
3870e560ec | ||
|
|
236ac1a584 | ||
|
|
9243f98604 | ||
|
|
8f062320d3 | ||
|
|
60126b0010 | ||
|
|
06c16e2f1f | ||
|
|
b3353b168c | ||
|
|
e170f9c6c2 | ||
|
|
497b84f5dc | ||
|
|
d51e5184a1 | ||
|
|
429b278bca |
@@ -35,7 +35,7 @@ These are supplied with the URL to the page you want to embed, for example for a
|
||||
The supported parameters are as follows. Generally these match the equivalent parameters in the real Spothole API, where a mapping exists.
|
||||
|
||||
| Name | Allowed Values | Default | Example | Description |
|
||||
|----------------|-------------------------|---------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|-------------------|-------------------------|---------|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `embedded` | `true`, `false` | `false` | `?embedded=true` | Enables embedded mode. |
|
||||
| `color-scheme` | `light`, `dark`, `auto` | `auto` | `?color-scheme=dark` | Forces light or dark mode in preference to the operating system default. |
|
||||
| `time-zone` | `UTC`, `local` | `UTC` | `?time-zone=local` | Sets times to be in UTC or local time. |
|
||||
@@ -48,6 +48,9 @@ The supported parameters are as follows. Generally these match the equivalent pa
|
||||
| `mode_type` | Comma-separated list | (all) | `?mode_type=PHONE,CW` | Sets the list of mode types that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `dx_continent` | Comma-separated list | (all) | `?dx_continent=NA,SA` | Sets the list of DX Continents that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `de_continent` | Comma-separated list | (all) | `?de_continent=EU` | Sets the list of DE Continents that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `map-center-lat` | Numeric (decimal) | (auto) | `?map-center-lat=51.5` | Sets the initial latitude of the map centre on the map page. If omitted, the map auto-fits to the loaded spots. |
|
||||
| `map-center-lon` | Numeric (decimal) | (auto) | `?map-center-lon=-0.1` | Sets the initial longitude of the map centre on the map page. If omitted, the map auto-fits to the loaded spots. |
|
||||
| `map-zoom` | Numeric (integer) | (auto) | `?map-zoom=6` | Sets the initial zoom level of the map on the map page. If omitted, the map auto-fits to the loaded spots. |
|
||||
|
||||
More will be added soon to allow customisation of filters and other display properties.
|
||||
|
||||
|
||||
@@ -68,9 +68,9 @@ class NG3K(HTTPAlertProvider):
|
||||
|
||||
dx_country = parts[1]
|
||||
qsl_info = parts[3]
|
||||
bands = extra_parts[1]
|
||||
modes = extra_parts[2] if len(extra_parts) > 3 else ""
|
||||
comment = extra_parts[-1]
|
||||
bands = extra_parts[1] if len(extra_parts) > 1 else ""
|
||||
modes = extra_parts[2] if len(extra_parts) > 2 else ""
|
||||
comment = extra_parts[3] if len(extra_parts) > 3 else ""
|
||||
|
||||
# Convert to our alert format
|
||||
alert = Alert(source=self.name,
|
||||
|
||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass
|
||||
# Each threshold-based table is a list of (min_value, description) pairs in descending order;
|
||||
# the first entry whose threshold the value meets or exceeds is used.
|
||||
|
||||
BLACKOUT_DESCRIPTIONS = {
|
||||
XRAY_CLASS_DESCRIPTIONS = {
|
||||
"X": "Wide area HF radio blackout across sunlit side",
|
||||
"M": "Occasional loss of HF communications on sunlit side",
|
||||
"C": "Low absorption of HF signals on sunlit side",
|
||||
@@ -71,6 +71,28 @@ ELECTRON_FLUX_DESCRIPTIONS = [
|
||||
]
|
||||
|
||||
|
||||
def _xray_blackout_scale(xray):
|
||||
"""Return the NOAA Radio Blackout scale number (R0-R5) for the given X-ray flux class string
|
||||
(e.g. "M4.5", "X12")."""
|
||||
|
||||
if not xray or len(xray) < 2:
|
||||
return 0
|
||||
letter = xray[0].upper()
|
||||
try:
|
||||
number = float(xray[1:])
|
||||
except ValueError:
|
||||
return 0
|
||||
if letter == 'M':
|
||||
return 1 if number < 5 else 2
|
||||
if letter == 'X':
|
||||
if number < 10:
|
||||
return 3
|
||||
if number < 20:
|
||||
return 4
|
||||
return 5
|
||||
return 0
|
||||
|
||||
|
||||
def _lookup_by_threshold(value, table, default=None):
|
||||
"""Return the description from a threshold table for the given numeric value.
|
||||
The table is a list of (min_value, description) pairs in descending order."""
|
||||
@@ -108,7 +130,7 @@ class SolarConditions:
|
||||
# K-index (3-hour geomagnetic activity)
|
||||
k_index: int = None
|
||||
# X-ray flux class, e.g. "B2.3", "C1.0"
|
||||
x_ray: str = None
|
||||
xray: str = None
|
||||
# Proton flux
|
||||
proton_flux: int = None
|
||||
# Electron flux
|
||||
@@ -141,8 +163,10 @@ class SolarConditions:
|
||||
blackout_forecast_r3_or_greater: dict = None
|
||||
|
||||
# Derived values (populated by infer_descriptions())
|
||||
# HF radio blackout risk description, derived from x_ray
|
||||
blackout_desc: str = None
|
||||
# HF radio blackout risk description, derived from xray
|
||||
xray_desc: str = None
|
||||
# HF radio blackout scale number (R0-R5), derived from xray
|
||||
radio_blackout_scale: int = None
|
||||
# Solar radiation storm level description, derived from proton_flux
|
||||
proton_flux_desc: str = None
|
||||
# Solar radiation storm scale number (S0-S5), derived from proton_flux
|
||||
@@ -159,10 +183,9 @@ class SolarConditions:
|
||||
def infer_descriptions(self):
|
||||
"""Populate derived text description fields from the current numeric/raw field values."""
|
||||
|
||||
# blackout_desc: use the X-ray flux class letter (first character of x_ray)
|
||||
if self.x_ray and len(self.x_ray) > 0:
|
||||
self.blackout_desc = BLACKOUT_DESCRIPTIONS.get(self.x_ray[0].upper())
|
||||
|
||||
if self.xray and len(self.xray) > 0:
|
||||
self.xray_desc = XRAY_CLASS_DESCRIPTIONS.get(self.xray[0].upper())
|
||||
self.radio_blackout_scale = _xray_blackout_scale(self.xray)
|
||||
self.proton_flux_desc = _lookup_by_threshold(self.proton_flux, PROTON_FLUX_DESCRIPTIONS)
|
||||
self.solar_storm_scale = _lookup_by_threshold(self.proton_flux, SOLAR_STORM_SCALES)
|
||||
self.geomag_storm_desc = _lookup_by_threshold(self.k_index, GEOMAG_STORM_DESCRIPTIONS)
|
||||
|
||||
@@ -86,7 +86,7 @@ class HamQSL(HTTPSolarConditionsProvider):
|
||||
"sfi": int_val("solarflux"),
|
||||
"a_index": int_val("aindex"),
|
||||
"k_index": int_val("kindex"),
|
||||
"x_ray": text("xray"),
|
||||
"xray": text("xray"),
|
||||
"sunspots": int_val("sunspots"),
|
||||
"proton_flux": int_val("protonflux"),
|
||||
"electron_flux": int_val("electonflux"),
|
||||
|
||||
@@ -67,7 +67,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>
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775249255"></script>
|
||||
<script src="/js/common.js?v=1776849830"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -69,8 +69,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775249255"></script>
|
||||
<script src="/js/add-spot.js?v=1775249255"></script>
|
||||
<script src="/js/common.js?v=1776849830"></script>
|
||||
<script src="/js/add-spot.js?v=1776849830"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -33,7 +33,7 @@
|
||||
<div id="display-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/display-area-header.html", web_ui_options=web_ui_options) %}
|
||||
<div class="card-body">
|
||||
<div id="display-container" class="row row-cols-1 row-cols-md-3 g-4">
|
||||
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
|
||||
<div class="col">
|
||||
{% module Template("cards/time-zone.html", web_ui_options=web_ui_options) %}
|
||||
</div>
|
||||
@@ -56,8 +56,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775249255"></script>
|
||||
<script src="/js/alerts.js?v=1775249255"></script>
|
||||
<script src="/js/common.js?v=1776849830"></script>
|
||||
<script src="/js/alerts.js?v=1776849830"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -62,9 +62,9 @@
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/common.js?v=1775249255"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775249255"></script>
|
||||
<script src="/js/bands.js?v=1775249255"></script>
|
||||
<script src="/js/common.js?v=1776849830"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1776849830"></script>
|
||||
<script src="/js/bands.js?v=1776849830"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<title>Spothole</title>
|
||||
|
||||
<link rel="stylesheet" href="/css/style.css" type="text/css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=1776849830" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
|
||||
@@ -45,12 +45,10 @@
|
||||
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
||||
|
||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1775249255"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1775249255"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1775249255"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1775249255"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1776849830"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1776849830"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1776849830"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
11
templates/cards/audio.html
Normal file
11
templates/cards/audio.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">Audio</h5>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="pingOnNewSpots" value="pingOnNewSpots" oninput="saveSettings();">
|
||||
<label class="form-check-label" for="pingOnNewSpots">Ping on new spots</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Bands</h5>
|
||||
<p id="band-options" class="card-text spothole-card-text"></p>
|
||||
<div id="band-options" class="card-text spothole-card-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
27
templates/cards/basemap.html
Normal file
27
templates/cards/basemap.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Map Style</h5>
|
||||
<p class="card-text spothole-card-text">
|
||||
<label for="basemap" class="form-label">Basemap</label>
|
||||
<select id="basemap" class="storeable-select form-select" oninput="displayUpdated();">
|
||||
<option value="OpenStreetMap.Mapnik" selected>OpenStreetMap Mapnik</option>
|
||||
<option value="OpenStreetMap.Mapnik.Dark">OpenStreetMap Mapnik (Dark)</option>
|
||||
<option value="Esri.NatGeoWorldMap">ESRI NatGeo World Map</option>
|
||||
<option value="Esri.WorldTopoMap">ESRI World Topo Map</option>
|
||||
<option value="Esri.WorldShadedRelief">ESRI World Shaded Relief</option>
|
||||
<option value="Esri.WorldImagery">ESRI World Imagery</option>
|
||||
<option value="CartoDB.Voyager">CartoDB Voyager</option>
|
||||
<option value="CartoDB.DarkMatter">CartoDB DarkMatter</option>
|
||||
</select>
|
||||
</p>
|
||||
<p class="card-text spothole-card-text">
|
||||
<label for="basemapOpacity" class="form-label">Opacity</label>
|
||||
<select id="basemapOpacity" class="storeable-select form-select" oninput="displayUpdated();">
|
||||
<option value="1">100%</option>
|
||||
<option value="0.75">75%</option>
|
||||
<option value="0.5">50%</option>
|
||||
<option value="0.25">25%</option>
|
||||
</select>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">DE Continent</h5>
|
||||
<p id="de-continent-options" class="card-text spothole-card-text"></p>
|
||||
<div id="de-continent-options" class="card-text spothole-card-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Duration Limit <i class='fa-solid fa-circle-question' title='Some users create long-duration alerts for the period they will be generally in and around xOTA references, when they are not indending to be on the air most of the time. Use this control to restrict the maximum duration of spots that the software will display, and exclude any with a long duration, to avoid these filling up the list. By default, we allow DXpeditions to be displayed even if they are longer than this limit, because on a DXpedition the operators typically ARE on the air most of the time.'></i></h5>
|
||||
<p class="card-text spothole-card-text">
|
||||
Hide any alerts lasting more than:<br/>
|
||||
<label for="max-duration" class="form-label">Hide any alerts lasting more than</label>
|
||||
<select id="max-duration" class="storeable-select form-select" onclick="filtersUpdated();" style="width: 8em; display: inline-block;">
|
||||
<option value="10800">3 hours</option>
|
||||
<option value="43200">12 hours</option>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">DX Continent</h5>
|
||||
<p id="dx-continent-options" class="card-text spothole-card-text"></p>
|
||||
<div id="dx-continent-options" class="card-text spothole-card-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,41 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Map Features</h5>
|
||||
<h5 class="card-title mb-3">Map Features</h5>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="mapShowGeodesics" value="mapShowGeodesics" oninput="displayUpdated();">
|
||||
<label class="form-check-label" for="mapShowGeodesics">Geodesic Lines</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="showTerminator" oninput="displayUpdated();" checked>
|
||||
<label class="form-check-label" for="showTerminator">Terminator / Greyline</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="showMaidenheadGrid" oninput="displayUpdated();">
|
||||
<label class="form-check-label" for="showMaidenheadGrid">Maidenhead Grid</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="showCQZones" oninput="displayUpdated();">
|
||||
<label class="form-check-label" for="showCQZones">CQ Zones</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="showITUZones" oninput="displayUpdated();">
|
||||
<label class="form-check-label" for="showITUZones">ITU Zones</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="showWABWAIGrid" oninput="displayUpdated();">
|
||||
<label class="form-check-label" for="showWABWAIGrid">WAB/WAI Grid</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Modes</h5>
|
||||
<p id="mode-options" class="card-text spothole-card-text"></p>
|
||||
<div id="mode-options" class="card-text spothole-card-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Number of Alerts</h5>
|
||||
<p class="card-text spothole-card-text">Show up to
|
||||
<select id="alerts-to-fetch" class="storeable-select form-select ms-2" oninput="filtersUpdated();" style="width: 5em;display: inline-block;">
|
||||
<select id="alerts-to-fetch" class="storeable-select form-select ms-2 me-2" oninput="filtersUpdated();" style="width: 5em;display: inline-block;">
|
||||
{% for c in web_ui_options["alert-count"] %}
|
||||
<option value="{{c}}" {% if web_ui_options["alert-count-default"] == c %}selected{% end %}>{{c}}</option>
|
||||
{% end %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">SIGs</h5>
|
||||
<p id="sig-options" class="card-text spothole-card-text"></p>
|
||||
<div id="sig-options" class="card-text spothole-card-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sources</h5>
|
||||
<p id="source-options" class="card-text spothole-card-text"></p>
|
||||
<div id="source-options" class="card-text spothole-card-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +1,35 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Table Columns</h5>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<div class="row row-cols-2 g-1">
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowStartTime" value="tableShowStartTime" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowStartTime">Start Time</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowEndTime" value="tableShowEndTime" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowEndTime">End Time</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowDX" value="tableShowDX" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowDX">DX</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowFreqsModes" value="tableShowFreqsModes" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowFreqsModes">Frequencies & Modes</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<label class="form-check-label" for="tableShowFreqsModes">Freq & Mode</label>
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowComment" value="tableShowComment" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowComment">Comment</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowSource" value="tableShowSource" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowSource">Source</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowRef" value="tableShowRef" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowRef">Ref.</label>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,47 +1,47 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Table Columns</h5>
|
||||
<div class="form-group">
|
||||
<div class="form-check form-check-inline">
|
||||
<div class="row row-cols-2 g-1">
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowTime" value="tableShowTime" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowTime">Time</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowDX" value="tableShowDX" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowDX">DX</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowFreq" value="tableShowFreq" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowFreq">Frequency</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowMode" value="tableShowMode" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowMode">Mode</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowComment" value="tableShowComment" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowComment">Comment</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowBearing" value="tableShowBearing" oninput="columnsUpdated();">
|
||||
<label class="form-check-label" for="tableShowBearing">Bearing</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowType" value="tableShowType" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowType">Type</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowRef" value="tableShowRef" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowRef">Ref.</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowDE" value="tableShowDE" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowDE">DE</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
</div></div>
|
||||
<div class="col"><div class="form-check">
|
||||
<input class="form-check-input storeable-checkbox" type="checkbox" id="tableShowWorkedCheckbox" value="tableShowWorkedCheckbox" oninput="columnsUpdated();" checked>
|
||||
<label class="form-check-label" for="tableShowWorkedCheckbox">Worked?</label>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +111,9 @@
|
||||
</div>
|
||||
<div class="row border-bottom align-items-start me-0">
|
||||
<div class="col-12 col-md-2 py-2 fw-bold">X-ray Flux</div>
|
||||
<div id="sw-xray-vals" class="col-12 col-md-3 py-2"><strong id="sw-x-ray"></strong></div>
|
||||
<div id="sw-xray-vals" class="col-12 col-md-3 py-2">
|
||||
<span class="me-3"><strong id="sw-xray"></strong></span>
|
||||
<span class="me-3"><strong>R</strong><strong id="sw-radio-blackout-scale"></strong></span></div>
|
||||
<div id="sw-xray-desc" class="col-12 col-md-7 py-2"></div>
|
||||
</div>
|
||||
<div class="row border-bottom align-items-start me-0">
|
||||
@@ -155,7 +157,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="col mt-3 px-3">
|
||||
<h5>Blackout Forecast</h5>
|
||||
<h5>Radio Blackout Forecast</h5>
|
||||
<table id="forecast-blackout-table" class="table table-sm mt-2">
|
||||
<thead>
|
||||
<tr id="forecast-blackout-head"></tr>
|
||||
@@ -227,8 +229,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775249255"></script>
|
||||
<script src="/js/conditions.js?v=1775249255"></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=1776849830"></script>
|
||||
<script src="/js/conditions.js?v=1776849830"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-conditions").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
<div class="col">
|
||||
{% module Template("cards/spot-age.html", web_ui_options=web_ui_options) %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/basemap.html", web_ui_options=web_ui_options) %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/map-features.html", web_ui_options=web_ui_options) %}
|
||||
</div>
|
||||
@@ -65,14 +68,20 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet-providers@2.0.0/leaflet-providers.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet-extra-markers@1.2.2/src/assets/js/leaflet.extra-markers.min.js" type="module"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet.geodesic"></script>
|
||||
<script src="https://unpkg.com/leaflet.vectorgrid@latest/dist/Leaflet.VectorGrid.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/text-image/dist/text-image.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@joergdietrich/leaflet.terminator@1.1.0/L.Terminator.min.js"></script>
|
||||
<script src="https://ianrenton.github.io/Leaflet.Maidenhead/src/L.Maidenhead.js"></script>
|
||||
<script src="https://ha8tks.github.io/Leaflet.ITUzones/src/L.ITUzones.js"></script>
|
||||
<script src="https://ha8tks.github.io/Leaflet.CQzones/src/L.CQzones.js"></script>
|
||||
<script src="https://misc.ianrenton.com/Leaflet.WorkedAllBritainIreland/L.WorkedAllBritainIreland.js"></script>
|
||||
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/common.js?v=1775249255"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775249255"></script>
|
||||
<script src="/js/map.js?v=1775249255"></script>
|
||||
<script src="/js/common.js?v=1776849830"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1776849830"></script>
|
||||
<script src="/js/map.js?v=1776849830"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -74,6 +74,9 @@
|
||||
<div class="col">
|
||||
{% module Template("cards/table-columns-spots.html", web_ui_options=web_ui_options) %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/audio.html", web_ui_options=web_ui_options) %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,9 +90,9 @@
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/common.js?v=1775249255"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775249255"></script>
|
||||
<script src="/js/spots.js?v=1775249255"></script>
|
||||
<script src="/js/common.js?v=1776849830"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1776849830"></script>
|
||||
<script src="/js/spots.js?v=1776849830"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -59,8 +59,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775249255"></script>
|
||||
<script src="/js/status.js?v=1775249255"></script>
|
||||
<script src="/js/common.js?v=1776849830"></script>
|
||||
<script src="/js/status.js?v=1776849830"></script>
|
||||
<script>
|
||||
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<label class="form-check-label" for="band-color-scheme">Band color scheme</label><br/>
|
||||
<label class="form-check-label form-label" for="band-color-scheme">Band color scheme</label><br/>
|
||||
<select id="band-color-scheme" class="storeable-select form-select d-inline-block" oninput="setBandColorSchemeFromUI();" style="display: inline-block;">
|
||||
<option value="PSK Reporter" {% if web_ui_options["band-color-scheme-default"] == "PSK Reporter" %}selected{% end %}>PSK Reporter</option>
|
||||
<option value="PSK Reporter (Adjusted)" {% if web_ui_options["band-color-scheme-default"] == "PSK Reporter (Adjusted)" %}selected{% end %}>PSK Reporter (Adjusted)</option>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<label class="form-check-label" for="color-scheme">UI color scheme</label>
|
||||
<label class="form-check-label form-label" for="color-scheme">UI color scheme</label>
|
||||
<select id="color-scheme" class="storeable-select form-select d-inline-block" oninput="setColorSchemeFromUI();" style="display: inline-block;">
|
||||
<option value="auto" {% if web_ui_options["color-scheme-default"] == "auto" %}selected{% end %}>Automatic</option>
|
||||
<option value="light" {% if web_ui_options["color-scheme-default"] == "light" %}selected{% end %}>Light</option>
|
||||
|
||||
@@ -1415,7 +1415,7 @@ components:
|
||||
type: integer
|
||||
description: 3-hour geomagnetic activity index, 0–9
|
||||
example: 2
|
||||
x_ray:
|
||||
xray:
|
||||
type: string
|
||||
description: Current X-ray flux class
|
||||
example: "B2.3"
|
||||
@@ -1564,10 +1564,16 @@ components:
|
||||
"1743638400.0": 25
|
||||
"1743724800.0": 25
|
||||
"1743811200.0": 25
|
||||
blackout_desc:
|
||||
xray_desc:
|
||||
type: string
|
||||
description: HF radio blackout risk description, derived from the X-ray flux class.
|
||||
example: "No significant radio blackout"
|
||||
radio_blackout_scale:
|
||||
type: integer
|
||||
description: HF radio blackout scale number (R0-R5), derived from the X-ray flux class.
|
||||
minimum: 0
|
||||
maximum: 5
|
||||
example: 0
|
||||
proton_flux_desc:
|
||||
type: string
|
||||
description: Solar radiation storm level description, derived from proton flux.
|
||||
|
||||
BIN
webassets/audio/ping.mp3
Normal file
BIN
webassets/audio/ping.mp3
Normal file
Binary file not shown.
@@ -100,11 +100,6 @@ div.appearing-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spothole-card-text {
|
||||
line-height: 2.5em !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* SPOTS/ALERTS PAGES, MAIN TABLE */
|
||||
|
||||
@@ -219,10 +214,8 @@ div#map {
|
||||
.leaflet-container {
|
||||
font-family: var(--bs-body-font-family) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .leaflet-layer,
|
||||
[data-bs-theme=dark] .leaflet-control-attribution {
|
||||
filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%);
|
||||
.leaflet-control-attribution {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Make buttons overlaid on the map have a non-transparent fill so you can see the text better */
|
||||
|
||||
@@ -2,6 +2,39 @@
|
||||
var options = {};
|
||||
// Last time we updated the spots/alerts list on display.
|
||||
var lastUpdateTime;
|
||||
// Normally load user settings from local storage, unless embedded mode is in use
|
||||
let useLocalStorage = true;
|
||||
|
||||
// Save settings to local storage. Suppressed if "use local storage" is false.
|
||||
function saveSettings() {
|
||||
if (useLocalStorage) {
|
||||
// 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 "use local storage" is false.
|
||||
function loadSettings() {
|
||||
if (useLocalStorage) {
|
||||
// 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)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -92,14 +125,14 @@ function allFilterOptionsSelected(parameter) {
|
||||
}
|
||||
|
||||
|
||||
// Generate a filter card with multiple toggle buttons plus All/None buttons.
|
||||
// Generate a filter card with inline checkboxes plus All/None links.
|
||||
function generateMultiToggleFilterCard(elementID, filterQuery, options) {
|
||||
// Create a button for each option
|
||||
var $row = $('<div>');
|
||||
options.forEach(o => {
|
||||
$(elementID).append(`<input type="checkbox" class="btn-check filter-button-${filterQuery} storeable-checkbox" name="options" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-success" for="filter-button-${filterQuery}-${o}">${o}</label> `);
|
||||
$row.append(`<div class="form-check form-check-inline"><input type="checkbox" class="form-check-input filter-button-${filterQuery} storeable-checkbox" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" for="filter-button-${filterQuery}-${o}">${o}</label></div>`);
|
||||
});
|
||||
// Create All/None buttons
|
||||
$(elementID).append(` <span style="display: inline-block"><button id="filter-button-${filterQuery}-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('${filterQuery}', true);">All</button> <button id="filter-button-${filterQuery}-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('${filterQuery}', false);">None</button></span>`);
|
||||
$(elementID).append($row);
|
||||
$(elementID).append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('${filterQuery}', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('${filterQuery}', false); return false;">None</a></div>`);
|
||||
}
|
||||
|
||||
// Method called when "All" or "None" is clicked
|
||||
|
||||
@@ -53,8 +53,9 @@ function loadSolarConditions() {
|
||||
'geomag_storm_scale': 'sw-geomag-storm-scale',
|
||||
'geomag_storm_desc': 'sw-geomag-storm-desc',
|
||||
'geomag_noise': 'sw-geomag-noise',
|
||||
'x_ray': 'sw-x-ray',
|
||||
'blackout_desc': 'sw-xray-desc',
|
||||
'xray': 'sw-xray',
|
||||
'radio_blackout_scale': 'sw-radio-blackout-scale',
|
||||
'xray_desc': 'sw-xray-desc',
|
||||
'proton_flux': 'sw-proton-flux',
|
||||
'solar_storm_scale': 'sw-solar-storm-scale',
|
||||
'proton_flux_desc': 'sw-proton-desc',
|
||||
@@ -87,7 +88,7 @@ function loadSolarConditions() {
|
||||
kIndex < 5 ? 'bg-success-subtle' : kIndex < 6 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||
}
|
||||
|
||||
const xRay = jsonData.x_ray;
|
||||
const xRay = jsonData.xray;
|
||||
if (xRay) {
|
||||
const letter = xRay[0].toUpperCase();
|
||||
const xRayClass = (letter === 'X') ? 'bg-danger-subtle'
|
||||
@@ -125,15 +126,12 @@ function renderKIndexForecast(data) {
|
||||
.sort((a, b) => a.ts - b.ts);
|
||||
if (entries.length === 0) return;
|
||||
|
||||
// x-axis labels. Show date only on the first bar of each day, time on all bars
|
||||
const labels = entries.map((e, i) => {
|
||||
const dt = new Date(e.ts * 1000);
|
||||
const timeStr = String(dt.getUTCHours()).padStart(2, '0') + ':00';
|
||||
const dateStr = dt.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' });
|
||||
const prev = i > 0 ? new Date(entries[i - 1].ts * 1000) : null;
|
||||
const newDay = !prev || prev.toISOString().slice(0, 10) !== dt.toISOString().slice(0, 10);
|
||||
return newDay ? [timeStr, dateStr] : timeStr;
|
||||
});
|
||||
// Use a simple integer index axis: ticks at 0, 1, 2, ..., N (period boundaries) and bars
|
||||
// centred at 0.5, 1.5, ..., N-0.5 (midpoints). This guarantees tick marks fall exactly on
|
||||
// bar edges regardless of how Chart.js rounds large timestamp values.
|
||||
// "axisMin = 0" is the left/top edge of bar 0; "axisMax = N" is the right/bottom edge of bar N-1.
|
||||
const N = entries.length;
|
||||
const periodSecs = 3 * 3600;
|
||||
|
||||
// Inherit colours from Bootstrap CSS variables so that dark mode inherently works. We want bar colours that are not
|
||||
// quite as saturated as the Bootstrap success/warning/danger colours but not as desaturated as the "subtle"
|
||||
@@ -148,7 +146,9 @@ function renderKIndexForecast(data) {
|
||||
const textColor = style.getPropertyValue('--bs-body-color').trim() || '#666';
|
||||
const gridColor = style.getPropertyValue('--bs-border-color').trim() || 'rgba(128,128,128,0.3)';
|
||||
|
||||
if (kpChart) { kpChart.destroy(); }
|
||||
if (kpChart) {
|
||||
kpChart.destroy();
|
||||
}
|
||||
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const kpAxisTicks = {
|
||||
@@ -164,10 +164,34 @@ function renderKIndexForecast(data) {
|
||||
ticks: kpAxisTicks,
|
||||
grid: {color: gridColor},
|
||||
};
|
||||
// Linear scale using integer indices. Ticks at 0..N (period boundary indices);
|
||||
// the callback converts each integer index back to a UTC time string.
|
||||
// On mobile the time axis is vertical, so reverse it to keep time running top-to-bottom.
|
||||
const timeAxis = {
|
||||
type: 'linear',
|
||||
min: 0,
|
||||
max: N,
|
||||
offset: false,
|
||||
reverse: isMobile,
|
||||
title: {display: true, text: 'Time (UTC)', color: textColor},
|
||||
ticks: { color: textColor, maxRotation: 45, minRotation: 0 },
|
||||
grid: { color: gridColor },
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
color: textColor,
|
||||
maxRotation: 45,
|
||||
minRotation: 0,
|
||||
callback(value) {
|
||||
if (!Number.isInteger(value) || value < 0 || value > N) return null;
|
||||
const ts = value < N ? entries[value].ts : entries[N - 1].ts + periodSecs;
|
||||
const dt = new Date(ts * 1000);
|
||||
const h = dt.getUTCHours(), m = dt.getUTCMinutes();
|
||||
const timeStr = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
||||
if (h === 0 && m === 0) {
|
||||
return [timeStr, dt.toLocaleDateString('en-GB', {day: '2-digit', month: 'short', timeZone: 'UTC'})];
|
||||
}
|
||||
return timeStr;
|
||||
},
|
||||
},
|
||||
grid: {color: gridColor, offset: false},
|
||||
};
|
||||
|
||||
// Draw a "now" line at the current time position
|
||||
@@ -175,15 +199,11 @@ function renderKIndexForecast(data) {
|
||||
id: 'nowLine',
|
||||
afterDraw(chart) {
|
||||
const nowTs = Date.now() / 1000;
|
||||
// Find the fractional bar index for the current time
|
||||
let fracIndex = null;
|
||||
for (let i = 0; i < entries.length - 1; i++) {
|
||||
if (nowTs >= entries[i].ts && nowTs < entries[i + 1].ts) {
|
||||
fracIndex = i + (nowTs - entries[i].ts) / (entries[i + 1].ts - entries[i].ts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fracIndex === null) return; // now is outside the chart range
|
||||
// Find which bar (if any) the current time falls in and compute a fractional index
|
||||
const firstTs = entries[0].ts;
|
||||
const lastTs = entries[N - 1].ts + periodSecs;
|
||||
if (nowTs < firstTs || nowTs > lastTs) return;
|
||||
const fracIndex = (nowTs - firstTs) / periodSecs;
|
||||
|
||||
const {ctx, chartArea} = chart;
|
||||
const scale = isMobile ? chart.scales.y : chart.scales.x;
|
||||
@@ -218,15 +238,22 @@ function renderKIndexForecast(data) {
|
||||
}
|
||||
};
|
||||
|
||||
// Bars centred at i+0.5 (midpoint between tick i and tick i+1) so each bar spans
|
||||
// exactly from tick i to tick i+1 with barPercentage/categoryPercentage = 1.0.
|
||||
const chartData = isMobile
|
||||
? entries.map((e, i) => ({x: e.kp, y: i + 0.5}))
|
||||
: entries.map((e, i) => ({x: i + 0.5, y: e.kp}));
|
||||
|
||||
kpChart = new Chart(document.getElementById('forecast-kp-chart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
data: entries.map(e => e.kp),
|
||||
data: chartData,
|
||||
backgroundColor: colors,
|
||||
hoverBackgroundColor: colors,
|
||||
borderWidth: 0,
|
||||
barPercentage: 1.0,
|
||||
categoryPercentage: 1.0,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
@@ -263,7 +290,7 @@ function renderSolarStormForecast(data) {
|
||||
const headRow = $('#forecast-solar-storm-head').empty().append('<th></th>');
|
||||
entries.forEach(({ts}) => {
|
||||
const label = new Date(ts * 1000)
|
||||
.toLocaleDateString('en-US', { day: '2-digit', month: 'short', timeZone: 'UTC' });
|
||||
.toLocaleDateString('en-GB', {day: '2-digit', month: 'short', timeZone: 'UTC'});
|
||||
headRow.append(`<th>${label}</th>`);
|
||||
});
|
||||
|
||||
@@ -323,10 +350,14 @@ function renderBlackoutForecast(r1r2Data, r3Data) {
|
||||
|
||||
// Render the DX stats table for the currently selected DE continent
|
||||
function renderDxStats() {
|
||||
if (!dxStatsData) { return; }
|
||||
if (!dxStatsData) {
|
||||
return;
|
||||
}
|
||||
const deContinent = $('#dxstats-de-continent').val();
|
||||
const deData = dxStatsData[deContinent];
|
||||
if (!deData) { return; }
|
||||
if (!deData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cells = [];
|
||||
Object.entries(deData).forEach(function ([dxContinent, bands]) {
|
||||
@@ -337,7 +368,9 @@ function renderDxStats() {
|
||||
});
|
||||
});
|
||||
|
||||
const counts = cells.map(function(c) { return c.count; });
|
||||
const counts = cells.map(function (c) {
|
||||
return c.count;
|
||||
});
|
||||
const min = Math.min(...counts);
|
||||
const max = Math.max(...counts);
|
||||
const range = max - min;
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
// How often to query the server?
|
||||
const REFRESH_INTERVAL_SEC = 60;
|
||||
|
||||
// Colours
|
||||
const MAIDENHEAD_GRID_COLOR_LIGHT = 'rgba(200, 140, 140, 1.0)';
|
||||
const CQ_ZONES_COLOR_LIGHT = 'rgba(140, 200, 140, 1.0)';
|
||||
const ITU_ZONES_COLOR_LIGHT = 'rgba(200, 200, 140, 1.0)';
|
||||
const WAB_WAI_GRID_COLOR_LIGHT = 'rgba(140, 140, 200, 1.0)';
|
||||
const MAIDENHEAD_GRID_COLOR_DARK = 'rgba(120, 60, 60, 1.0)';
|
||||
const CQ_ZONES_COLOR_DARK = 'rgba(60, 120, 60, 1.0)';
|
||||
const ITU_ZONES_COLOR_DARK = 'rgba(120, 120, 60, 1.0)';
|
||||
const WAB_WAI_GRID_COLOR_DARK = 'rgba(60, 60, 120, 1.0)';
|
||||
|
||||
// Map layers
|
||||
var backgroundTileLayer;
|
||||
var markersLayer;
|
||||
var geodesicsLayer;
|
||||
var terminator;
|
||||
var maidenheadGrid;
|
||||
var cqZones;
|
||||
var ituZones;
|
||||
var wabwaiGrid;
|
||||
// Tracks the currently-loaded basemap provider string to avoid unnecessary tile reloads
|
||||
var loadedBasemap;
|
||||
// Tracks whether this is the first display of markers after page load
|
||||
var firstLoad = true;
|
||||
|
||||
// Load spots and populate the map.
|
||||
function loadSpots() {
|
||||
@@ -13,7 +32,9 @@ function loadSpots() {
|
||||
spots = jsonData;
|
||||
// Update map
|
||||
updateMap();
|
||||
if ($("#showTerminator")[0].checked) {
|
||||
terminator.setTime();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,6 +78,15 @@ function updateMap() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On first load, zoom to the extent of the markers
|
||||
if (firstLoad) {
|
||||
if (markersLayer.getLayers().length >= 2) {
|
||||
var group = new L.featureGroup(markersLayer.getLayers());
|
||||
map.fitBounds(group.getBounds().pad(0.1));
|
||||
}
|
||||
firstLoad = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get an icon for a spot, based on its band, using PSK Reporter colours, its program etc.
|
||||
@@ -180,10 +210,27 @@ function loadOptions() {
|
||||
// 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.
|
||||
loadURLParams();
|
||||
loadMapURLParams();
|
||||
|
||||
// Load settings from settings storage now all the controls are available
|
||||
loadSettings();
|
||||
|
||||
// If no basemap has been explicitly saved and the UI is in dark mode, default to dark Mapnik
|
||||
if (localStorage.getItem("#basemap:value") === null) {
|
||||
if (document.documentElement.getAttribute("data-bs-theme") === "dark") {
|
||||
$("#basemap").val("OpenStreetMap.Mapnik.Dark");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply basemap and overlay settings now that controls have their saved values
|
||||
setBasemap($("#basemap").val());
|
||||
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
|
||||
enableTerminator($("#showTerminator")[0].checked);
|
||||
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
|
||||
enableCQZones($("#showCQZones")[0].checked);
|
||||
enableITUZones($("#showITUZones")[0].checked);
|
||||
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
|
||||
|
||||
// Load spots and set up the timer
|
||||
loadSpots();
|
||||
setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000);
|
||||
@@ -193,9 +240,150 @@ function loadOptions() {
|
||||
// Method called when any display property is changed to reload the map and persist the display settings.
|
||||
function displayUpdated() {
|
||||
updateMap();
|
||||
setBasemap($("#basemap").val());
|
||||
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
|
||||
enableTerminator($("#showTerminator")[0].checked);
|
||||
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
|
||||
enableCQZones($("#showCQZones")[0].checked);
|
||||
enableITUZones($("#showITUZones")[0].checked);
|
||||
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
// Set the basemap
|
||||
function setBasemap(basemapname) {
|
||||
// Only change if we have to, to avoid a flash of reloading content
|
||||
if (loadedBasemap !== basemapname) {
|
||||
loadedBasemap = basemapname;
|
||||
if (typeof backgroundTileLayer !== 'undefined') {
|
||||
map.removeLayer(backgroundTileLayer);
|
||||
}
|
||||
// OpenStreetMap.Mapnik.Dark is a synthetic variant that uses Mapnik tiles with a CSS filter applied
|
||||
const providerName = basemapname === "OpenStreetMap.Mapnik.Dark" ? "OpenStreetMap.Mapnik" : basemapname;
|
||||
backgroundTileLayer = L.tileLayer.provider(providerName, {
|
||||
opacity: parseFloat($("#basemapOpacity").val()),
|
||||
edgeBufferTiles: 1
|
||||
});
|
||||
backgroundTileLayer.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
if (basemapname === "OpenStreetMap.Mapnik.Dark") {
|
||||
var container = backgroundTileLayer.getContainer();
|
||||
if (container) {
|
||||
container.style.filter = 'invert(100%) hue-rotate(180deg) brightness(80%)';
|
||||
}
|
||||
}
|
||||
|
||||
// Identify dark basemaps to ensure we use white text for unselected icons
|
||||
// and change the background colour appropriately
|
||||
const basemapIsDark = basemapname === "CartoDB.DarkMatter" || basemapname === "Esri.WorldImagery" || basemapname === "OpenStreetMap.Mapnik.Dark";
|
||||
$("#map").css('background-color', basemapIsDark ? "black" : "white");
|
||||
|
||||
// Change the colour of the grid and zone overlays to match
|
||||
if (basemapIsDark) {
|
||||
maidenheadGrid.options.color = MAIDENHEAD_GRID_COLOR_DARK;
|
||||
cqZones.options.color = CQ_ZONES_COLOR_DARK;
|
||||
ituZones.options.color = ITU_ZONES_COLOR_DARK;
|
||||
wabwaiGrid.options.color = WAB_WAI_GRID_COLOR_DARK;
|
||||
} else {
|
||||
maidenheadGrid.options.color = MAIDENHEAD_GRID_COLOR_LIGHT;
|
||||
cqZones.options.color = CQ_ZONES_COLOR_LIGHT;
|
||||
ituZones.options.color = ITU_ZONES_COLOR_LIGHT;
|
||||
wabwaiGrid.options.color = WAB_WAI_GRID_COLOR_LIGHT;
|
||||
}
|
||||
|
||||
// Force regenerate overlays in the new colours
|
||||
map.removeLayer(maidenheadGrid);
|
||||
map.removeLayer(cqZones);
|
||||
map.removeLayer(ituZones);
|
||||
map.removeLayer(wabwaiGrid);
|
||||
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
|
||||
enableCQZones($("#showCQZones")[0].checked);
|
||||
enableITUZones($("#showITUZones")[0].checked);
|
||||
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the basemap opacity
|
||||
function setBasemapOpacity(opacity) {
|
||||
if (typeof backgroundTileLayer !== 'undefined') {
|
||||
backgroundTileLayer.setOpacity(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Shows/hides the terminator/greyline overlay
|
||||
function enableTerminator(show) {
|
||||
if (show) {
|
||||
terminator.setTime();
|
||||
terminator.addTo(map);
|
||||
} else {
|
||||
map.removeLayer(terminator);
|
||||
}
|
||||
}
|
||||
|
||||
// Shows/hides the Maidenhead grid overlay
|
||||
function enableMaidenheadGrid(show) {
|
||||
if (show) {
|
||||
maidenheadGrid.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
} else {
|
||||
map.removeLayer(maidenheadGrid);
|
||||
}
|
||||
}
|
||||
|
||||
// Shows/hides the CQ zone overlay
|
||||
function enableCQZones(show) {
|
||||
if (show) {
|
||||
cqZones.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
} else {
|
||||
map.removeLayer(cqZones);
|
||||
}
|
||||
}
|
||||
|
||||
// Shows/hides the ITU zone overlay
|
||||
function enableITUZones(show) {
|
||||
if (show) {
|
||||
ituZones.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
} else {
|
||||
map.removeLayer(ituZones);
|
||||
}
|
||||
}
|
||||
|
||||
// Shows/hides the WAB/WAI grid overlay
|
||||
function enableWABWAIGrid(show) {
|
||||
if (show) {
|
||||
wabwaiGrid.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
} else {
|
||||
map.removeLayer(wabwaiGrid);
|
||||
}
|
||||
}
|
||||
|
||||
// Load map-specific URL parameters for center position and zoom level.
|
||||
// These set Leaflet state directly rather than form controls, so they live here rather than in loadURLParams().
|
||||
// If any parameter is applied, firstLoad is set to false so updateMap() does not override the position.
|
||||
function loadMapURLParams() {
|
||||
let params = new URLSearchParams(document.location.search);
|
||||
let lat = parseFloat(params.get("map-center-lat"));
|
||||
let lon = parseFloat(params.get("map-center-lon"));
|
||||
let zoom = parseFloat(params.get("map-zoom"));
|
||||
|
||||
let hasLatLon = !isNaN(lat) && !isNaN(lon);
|
||||
let hasZoom = !isNaN(zoom);
|
||||
|
||||
if (hasLatLon || hasZoom) {
|
||||
if (hasLatLon && hasZoom) {
|
||||
map.setView([lat, lon], zoom);
|
||||
} else if (hasLatLon) {
|
||||
map.setView([lat, lon], map.getZoom());
|
||||
} else {
|
||||
map.setZoom(zoom);
|
||||
}
|
||||
firstLoad = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the map
|
||||
function setUpMap() {
|
||||
// Create map
|
||||
@@ -206,12 +394,20 @@ function setUpMap() {
|
||||
});
|
||||
|
||||
// Add basemap
|
||||
backgroundTileLayer = L.tileLayer.provider("OpenStreetMap.Mapnik", {
|
||||
opacity: 1,
|
||||
loadedBasemap = $("#basemap").val();
|
||||
const initialProviderName = loadedBasemap === "OpenStreetMap.Mapnik.Dark" ? "OpenStreetMap.Mapnik" : loadedBasemap;
|
||||
backgroundTileLayer = L.tileLayer.provider(initialProviderName, {
|
||||
opacity: parseFloat($("#basemapOpacity").val()),
|
||||
edgeBufferTiles: 1
|
||||
});
|
||||
backgroundTileLayer.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
if (loadedBasemap === "OpenStreetMap.Mapnik.Dark") {
|
||||
var container = backgroundTileLayer.getContainer();
|
||||
if (container) {
|
||||
container.style.filter = 'invert(100%) hue-rotate(180deg) brightness(80%)';
|
||||
}
|
||||
}
|
||||
|
||||
// Add marker layer
|
||||
markersLayer = new L.LayerGroup();
|
||||
@@ -221,14 +417,53 @@ function setUpMap() {
|
||||
geodesicsLayer = new L.LayerGroup();
|
||||
geodesicsLayer.addTo(map);
|
||||
|
||||
// Add terminator/greyline
|
||||
// Add terminator/greyline (toggleable)
|
||||
terminator = L.terminator({
|
||||
interactive: false
|
||||
});
|
||||
terminator.setStyle({fillColor: '#00000050'});
|
||||
if ($("#showTerminator")[0].checked) {
|
||||
terminator.addTo(map);
|
||||
}
|
||||
|
||||
// Display a default view.
|
||||
// Add Maidenhead grid (toggleable)
|
||||
maidenheadGrid = L.maidenhead({
|
||||
color : MAIDENHEAD_GRID_COLOR_LIGHT
|
||||
});
|
||||
if ($("#showMaidenheadGrid")[0].checked) {
|
||||
maidenheadGrid.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
}
|
||||
|
||||
// Add CQ zone layer (toggleable)
|
||||
cqZones = L.cqzones({
|
||||
color : CQ_ZONES_COLOR_LIGHT
|
||||
});
|
||||
if ($("#showCQZones")[0].checked) {
|
||||
cqZones.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
}
|
||||
|
||||
// Add ITU zone layer (toggleable)
|
||||
ituZones = L.ituzones({
|
||||
color : ITU_ZONES_COLOR_LIGHT
|
||||
});
|
||||
if ($("#showITUZones")[0].checked) {
|
||||
ituZones.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
}
|
||||
|
||||
// Add WAB/WAI grid layer (toggleable)
|
||||
wabwaiGrid = L.workedAllBritainIreland({
|
||||
color : WAB_WAI_GRID_COLOR_LIGHT
|
||||
});
|
||||
if ($("#showWABWAIGrid")[0].checked) {
|
||||
wabwaiGrid.addTo(map);
|
||||
backgroundTileLayer.bringToBack();
|
||||
}
|
||||
|
||||
// Display a default view. This will only last until the spots are first loaded, at which point the map will zoom
|
||||
// to the extent of ths spots.
|
||||
map.setView([30, 0], 3);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,11 @@ function startSSEConnection() {
|
||||
|
||||
// Add the new spot to table
|
||||
addSpotToTopOfTable(newSpot, true);
|
||||
|
||||
// Ping if we need to
|
||||
if ($("#pingOnNewSpots")[0].checked) {
|
||||
new Audio("/audio/ping.mp3").play();
|
||||
}
|
||||
};
|
||||
|
||||
evtSource.onerror = function(err) {
|
||||
|
||||
@@ -6,27 +6,26 @@ var spots = []
|
||||
// to localStorage.
|
||||
let worked = []
|
||||
|
||||
// Dynamically add CSS code for the band toggle buttons to be in the appropriate colour.
|
||||
// Dynamically add CSS code for the band checkboxes to show in the appropriate colour.
|
||||
// Some band names contain decimal points which are not allowed in CSS classes, so we text-replace them to "p".
|
||||
function addBandToggleColourCSS(band_options) {
|
||||
var $style = $('<style>');
|
||||
band_options.forEach(o => {
|
||||
var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$style.append(`#filter-button-label-band-${domSafeName} { border-color: ${bandToColor(o['name'])}; color: var(--bs-secondary);}`);
|
||||
$style.append(`.btn-check:checked + #filter-button-label-band-${domSafeName} { background-color: ${bandToColor(o['name'])}; color: ${bandToContrastColor(o['name'])};}`);
|
||||
$style.append(`#filter-button-label-band-${domSafeName} { padding-left: 0.3em; border-left: 5px solid ${bandToColor(o['name'])};}`);
|
||||
});
|
||||
$('html > head').append($style);
|
||||
}
|
||||
|
||||
// Generate bands filter card. This one is a special case.
|
||||
function generateBandsMultiToggleFilterCard(band_options) {
|
||||
// Create a button for each option
|
||||
var $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 row-cols-xxl-4 g-1 mb-1">');
|
||||
band_options.forEach(o => {
|
||||
var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$("#band-options").append(`<input type="checkbox" class="btn-check filter-button-band storeable-checkbox" name="options" id="filter-button-band-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-secondary" id="filter-button-label-band-${domSafeName}" for="filter-button-band-${domSafeName}">${o['name']}</label> `);
|
||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-band storeable-checkbox" id="filter-button-band-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked> <label class="form-check-label" id="filter-button-label-band-${domSafeName}" for="filter-button-band-${domSafeName}">${o['name']}</label></div></div>`);
|
||||
});
|
||||
// Create All/None/Ham HF buttons
|
||||
$("#band-options").append(` <span style="display: inline-block"><button id="filter-button-band-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', true);">All</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('band', false);">None</button> <button id="filter-button-band-none" type="button" class="btn btn-outline-secondary" onclick="setHamHFBandToggles();">Ham HF</button></span>`);
|
||||
$("#band-options").append($grid);
|
||||
$("#band-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('band', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('band', false); return false;">None</a> <a href="#" onclick="setHamHFBandToggles(); return false;">Ham HF only</a></div>`);
|
||||
}
|
||||
|
||||
// Set the band toggles so that only the amateur radio HF bands are selected. This includes 160m and 6m because that's
|
||||
@@ -41,72 +40,58 @@ function setHamHFBandToggles() {
|
||||
|
||||
// Generate SIGs filter card. This one is also a special case.
|
||||
function generateSIGsMultiToggleFilterCard(sig_options) {
|
||||
// Create a button for each option
|
||||
var $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">');
|
||||
sig_options.forEach(o => {
|
||||
var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$("#sig-options").append(`<input type="checkbox" class="btn-check filter-button-sig storeable-checkbox" name="options" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-success" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${sigToIcon(o['name'], 'fa-tower-cell')}"></i> ${o['name']}</label> `);
|
||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${sigToIcon(o['name'], 'fa-tower-cell')}"></i> ${o['name']}</label></div></div>`);
|
||||
});
|
||||
// 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-success" id="filter-button-label-sig-NO_SIG" for="filter-button-sig-NO_SIG"><i class="fa-solid fa-tower-cell"></i> General DX</label> `);
|
||||
// Create All/None buttons
|
||||
$("#sig-options").append(` <span style="display: inline-block"><button id="filter-button-sig-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('sig', true);">All</button> <button id="filter-button-sig-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('sig', false);">None</button></span>`);
|
||||
// Bonus "NO_SIG" / "General DX" option
|
||||
$grid.append(`<div class="w-100"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-NO_SIG" value="NO_SIG" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-NO_SIG" for="filter-button-sig-NO_SIG"><i class="fa-solid fa-tower-cell"></i> General DX</label></div></div>`);
|
||||
$("#sig-options").append($grid);
|
||||
$("#sig-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('sig', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('sig', false); return false;">None</a></div>`);
|
||||
}
|
||||
|
||||
// Generate modes filter card. This one is also a special case.
|
||||
function generateModesMultiToggleFilterCard(mode_options) {
|
||||
// Create a button for each option
|
||||
var $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 g-1 mb-1">');
|
||||
mode_options.forEach(o => {
|
||||
var domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$("#mode-options").append(`<input type="checkbox" class="btn-check filter-button-mode storeable-checkbox" name="options" id="filter-button-mode-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-success" id="filter-button-label-mode-${domSafeName}" for="filter-button-mode-${domSafeName}">${o}</label> `);
|
||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-mode storeable-checkbox" id="filter-button-mode-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-mode-${domSafeName}" for="filter-button-mode-${domSafeName}">${o}</label></div></div>`);
|
||||
});
|
||||
// Create All/None buttons
|
||||
$("#mode-options").append(` <span style="display: inline-block"><button id="filter-button-mode-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('mode', true);">All</button> <button id="filter-button-mode-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('mode', false);">None</button></span>`);
|
||||
// Create category buttons
|
||||
$("#mode-options").append(` <button id="filter-button-mode-av" type="button" class="btn btn-outline-secondary" onclick="toggleAnalogVoiceModeToggles();">Analog Voice</button> <button id="filter-button-mode-dv" type="button" class="btn btn-outline-secondary" onclick="toggleDigitalVoiceModeToggles();">Digital Voice</button> <button id="filter-button-mode-digi" type="button" class="btn btn-outline-secondary" onclick="toggleDigiModeToggles();">Digimodes</button></span>`);
|
||||
$("#mode-options").append($grid);
|
||||
$("#mode-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('mode', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('mode', false); return false;">None</a> <a href="#" onclick="setVoiceModeToggles(); return false;">Voice only</a> <a href="#" onclick="setDigiModeToggles(); return false;">Digimodes only</a></div>`);
|
||||
}
|
||||
|
||||
// Toggle the mode toggles that relate to Analog Voice.
|
||||
function toggleAnalogVoiceModeToggles() {
|
||||
toggleToggles("mode", ["PHONE", "SSB", "LSB", "USB", "AM", "FM"]);
|
||||
}
|
||||
|
||||
// Toggle the mode toggles that relate to Digital Voice.
|
||||
function toggleDigitalVoiceModeToggles() {
|
||||
toggleToggles("mode", ["DV", "DMR", "DSTAR", "C4FM", "M17"]);
|
||||
}
|
||||
|
||||
// Toggle the mode toggles that relate to Digimodes.
|
||||
function toggleDigiModeToggles() {
|
||||
toggleToggles("mode", ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "PSK", "OLIVIA", "PKT", "MSK144"]);
|
||||
}
|
||||
|
||||
// Toggle the a set of toggles of the given type (e.g. "mode") that match the given values (e.g. ["SSB", "AM", "FM"]).
|
||||
function toggleToggles(type, values) {
|
||||
let toggle = null;
|
||||
$(".filter-button-" + type).each(function() {
|
||||
console.log($(this));
|
||||
if (values.includes($(this).val().replace("filter-button-" + type, ""))) {
|
||||
if (toggle == null) {
|
||||
toggle = !$(this).prop('checked');
|
||||
}
|
||||
$(this).prop('checked', toggle);
|
||||
}
|
||||
// Set the mode toggles that relate to Analog Voice.
|
||||
function setVoiceModeToggles() {
|
||||
const modes = ["PHONE", "SSB", "LSB", "USB", "AM", "FM", "DV", "DMR", "DSTAR", "C4FM", "M17"];
|
||||
$(".filter-button-mode").each(function() {
|
||||
$(this).prop('checked', modes.includes($(this).val().replace("filter-button-mode-", "")));
|
||||
});
|
||||
filtersUpdated();
|
||||
}
|
||||
|
||||
// Generate Sources filter card. This one is a minor special case as we create the buttons in the normal way, but then
|
||||
// Set the mode toggles that relate to Digimodes.
|
||||
function setDigiModeToggles() {
|
||||
const modes = ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "PSK", "OLIVIA", "PKT", "MSK144"];
|
||||
$(".filter-button-mode").each(function() {
|
||||
$(this).prop('checked', modes.includes($(this).val().replace("filter-button-mode-", "")));
|
||||
});
|
||||
filtersUpdated();
|
||||
}
|
||||
|
||||
// Generate Sources filter card. This one is a minor special case as we create the checkboxes in the normal way, but
|
||||
// set which ones are enabled by default based on config rather than having them all enabled by default. We also sanitise
|
||||
// names here for HTML elements.
|
||||
function generateSourcesMultiToggleFilterCard(source_options, sources_enabled_by_default) {
|
||||
// Create a button for each option
|
||||
var $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">');
|
||||
source_options.forEach(o => {
|
||||
var enable = sources_enabled_by_default.includes(o);
|
||||
var domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$("#source-options").append(`<input type="checkbox" class="btn-check filter-button-source storeable-checkbox" name="options" id="filter-button-source-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" ${enable ? "checked" : ""}><label class="btn btn-outline-success" for="filter-button-source-${domSafeName}">${o}</label> `);
|
||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-source storeable-checkbox" id="filter-button-source-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" ${enable ? "checked" : ""}><label class="form-check-label" for="filter-button-source-${domSafeName}">${o}</label></div></div>`);
|
||||
});
|
||||
// Create All/None buttons
|
||||
$("#source-options").append(` <span style="display: inline-block"><button id="filter-button-source-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('source', true);">All</button> <button id="filter-button-source-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('source', false);">None</button></span>`);
|
||||
$("#source-options").append($grid);
|
||||
$("#source-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('source', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('source', false); return false;">None</a></div>`);
|
||||
}
|
||||
|
||||
// Method called when any filter is changed to reload the spots and persist the filter settings.
|
||||
|
||||
@@ -13,7 +13,7 @@ function loadStatus() {
|
||||
$("#web-server-last-page").text(moment.unix(jsonData["webserver"]["last_page_access"]).utc().fromNow());
|
||||
|
||||
$("#cleanup-status").text(jsonData["cleanup"]["status"]);
|
||||
$("#cleanu-last-ran").text(moment.unix(jsonData["cleanup"]["last_ran"]).utc().fromNow());
|
||||
$("#cleanup-last-ran").text(moment.unix(jsonData["cleanup"]["last_ran"]).utc().fromNow());
|
||||
|
||||
jsonData["spot_providers"].forEach(p => {
|
||||
$("#spot-providers-status-container").append(`
|
||||
|
||||
Reference in New Issue
Block a user