mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-04-29 18:25:58 +00:00
Add fetching of NOAA 3-day forecast
This commit is contained in:
@@ -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>
|
<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=1775233379"></script>
|
<script src="/js/common.js?v=1775234400"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1775233379"></script>
|
<script src="/js/common.js?v=1775234400"></script>
|
||||||
<script src="/js/add-spot.js?v=1775233379"></script>
|
<script src="/js/add-spot.js?v=1775234400"></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 %}
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1775233379"></script>
|
<script src="/js/common.js?v=1775234400"></script>
|
||||||
<script src="/js/alerts.js?v=1775233379"></script>
|
<script src="/js/alerts.js?v=1775234400"></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 %}
|
||||||
@@ -62,9 +62,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=1775233379"></script>
|
<script src="/js/common.js?v=1775234400"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1775233379"></script>
|
<script src="/js/spotsbandsandmap.js?v=1775234400"></script>
|
||||||
<script src="/js/bands.js?v=1775233379"></script>
|
<script src="/js/bands.js?v=1775234400"></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 %}
|
||||||
@@ -46,10 +46,10 @@
|
|||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
||||||
|
|
||||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1775233379"></script>
|
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1775234400"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1775233379"></script>
|
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1775234400"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1775233379"></script>
|
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1775234400"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1775233379"></script>
|
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1775234400"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -7,10 +7,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row row-cols-1 row-cols-md-2 g-3">
|
<div class="row row-cols-1 row-cols-md-2 g-3">
|
||||||
<div class="col">
|
<div class="col mt-3 px-3">
|
||||||
<div class="card h-100">
|
<h5>HF</h5>
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">HF</h5>
|
|
||||||
<table class="table table-sm mt-2">
|
<table class="table table-sm mt-2">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -43,12 +41,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col mt-3 px-3">
|
||||||
</div>
|
<h5>VHF</h5>
|
||||||
<div class="col">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">VHF</h5>
|
|
||||||
<table class="table table-sm mt-2">
|
<table class="table table-sm mt-2">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -85,8 +79,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-text mt-3">Data from <a href="https://hamqsl.com">HamQSL.com</a>.</div>
|
<div class="form-text mt-3">Data from <a href="https://hamqsl.com">HamQSL.com</a>.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +87,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Solar Weather
|
Solar Weather
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body px-3">
|
||||||
<div class="row border-bottom align-items-start me-0">
|
<div class="row border-bottom align-items-start me-0">
|
||||||
<div class="col-12 col-md-2 py-2 fw-bold">Solar Flux</div>
|
<div class="col-12 col-md-2 py-2 fw-bold">Solar Flux</div>
|
||||||
<div id="sw-solar-flux-vals" class="col-12 col-md-3 py-2">
|
<div id="sw-solar-flux-vals" class="col-12 col-md-3 py-2">
|
||||||
@@ -138,6 +130,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-header">
|
||||||
|
Forecast
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col mt-3 px-3">
|
||||||
|
<h5>K-index Forecast</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="forecast-kp-table" class="table table-sm mt-2">
|
||||||
|
<thead>
|
||||||
|
<tr></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 g-3">
|
||||||
|
<div class="col mt-3 px-3">
|
||||||
|
<h5>Solar Storm Forecast</h5>
|
||||||
|
<table id="forecast-solar-storm-table" class="table table-sm mt-2">
|
||||||
|
<thead>
|
||||||
|
<tr id="forecast-solar-storm-head"></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="forecast-solar-storm-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col mt-3 px-3">
|
||||||
|
<h5>Blackout Forecast</h5>
|
||||||
|
<table id="forecast-blackout-table" class="table table-sm mt-2">
|
||||||
|
<thead>
|
||||||
|
<tr id="forecast-blackout-head"></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="forecast-blackout-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-text mt-3">Data from <a href="https://www.swpc.noaa.gov/">NOAA Space Weather Prediction
|
||||||
|
Center</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card mt-5">
|
<div class="card mt-5">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
DX Opportunities
|
DX Opportunities
|
||||||
@@ -145,7 +181,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" style="width: auto;" oninput="dxStatsContientChanged();">
|
<select id="dxstats-de-continent" class="form-select storeable-select d-inline-block ms-2"
|
||||||
|
style="width: auto;" 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>
|
||||||
@@ -185,12 +222,18 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text mt-2">This table shows the number of spots in the past hour received in your continent, where the DX continent and band are as shown in the table. Bands with high numbers of spots are likely to be the best ones for making contact with the continent you want right now. Bear in mind that some bands and some continents are inherently much rarer than others.</div>
|
<div class="form-text mt-2">This table shows the number of spots in the past hour received in your continent,
|
||||||
|
where the DX continent and band are as shown in the table. Bands with high numbers of spots are likely to be
|
||||||
|
the best ones for making contact with the continent you want right now. Bear in mind that some bands and
|
||||||
|
some continents are inherently much rarer than others.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1775233379"></script>
|
<script src="/js/common.js?v=1775234400"></script>
|
||||||
<script src="/js/conditions.js?v=1775233379"></script>
|
<script src="/js/conditions.js?v=1775234400"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-conditions").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function () {
|
||||||
|
$("#nav-link-conditions").addClass("active");
|
||||||
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -70,9 +70,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=1775233379"></script>
|
<script src="/js/common.js?v=1775234400"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1775233379"></script>
|
<script src="/js/spotsbandsandmap.js?v=1775234400"></script>
|
||||||
<script src="/js/map.js?v=1775233379"></script>
|
<script src="/js/map.js?v=1775234400"></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 %}
|
||||||
@@ -87,9 +87,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=1775233379"></script>
|
<script src="/js/common.js?v=1775234400"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1775233379"></script>
|
<script src="/js/spotsbandsandmap.js?v=1775234400"></script>
|
||||||
<script src="/js/spots.js?v=1775233379"></script>
|
<script src="/js/spots.js?v=1775234400"></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=1775233379"></script>
|
<script src="/js/common.js?v=1775234400"></script>
|
||||||
<script src="/js/status.js?v=1775233379"></script>
|
<script src="/js/status.js?v=1775234400"></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>
|
||||||
|
|||||||
@@ -105,9 +105,131 @@ function loadSolarConditions() {
|
|||||||
applySwClass('sw-electron-vals', 'sw-electron-desc',
|
applySwClass('sw-electron-vals', 'sw-electron-desc',
|
||||||
electronFlux <= 100 ? 'bg-success-subtle' : electronFlux <= 1000 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
electronFlux <= 100 ? 'bg-success-subtle' : electronFlux <= 1000 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forecast
|
||||||
|
|
||||||
|
renderKIndexForecast(jsonData.k_index_forecast);
|
||||||
|
renderSolarStormForecast(jsonData.solar_storm_forecast);
|
||||||
|
renderBlackoutForecast(jsonData.blackout_forecast_r1r2, jsonData.blackout_forecast_r3_or_greater);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render the K-index forecast table (rows = 3-hour UTC time slots, columns = forecast dates)
|
||||||
|
function renderKIndexForecast(data) {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const entries = Object.entries(data)
|
||||||
|
.map(([tsStr, kp]) => ({ ts: parseFloat(tsStr), kp }))
|
||||||
|
.sort((a, b) => a.ts - b.ts);
|
||||||
|
if (entries.length === 0) return;
|
||||||
|
|
||||||
|
// Derive the unique UTC dates from sorted entries
|
||||||
|
const dateSet = new Set();
|
||||||
|
entries.forEach(e => dateSet.add(new Date(e.ts * 1000).toISOString().slice(0, 10)));
|
||||||
|
const dates = [...dateSet];
|
||||||
|
|
||||||
|
const kpByTs = {};
|
||||||
|
entries.forEach(e => { kpByTs[e.ts] = e.kp; });
|
||||||
|
|
||||||
|
// Header row
|
||||||
|
const headRow = $('#forecast-kp-table thead tr').empty().append('<th>Time (UTC)</th>');
|
||||||
|
dates.forEach(dateStr => {
|
||||||
|
const label = new Date(dateStr + 'T00:00:00Z')
|
||||||
|
.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' });
|
||||||
|
headRow.append(`<th>${label}</th>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Data rows: one per 3-hour slot
|
||||||
|
const tbody = $('#forecast-kp-table tbody').empty();
|
||||||
|
[0, 3, 6, 9, 12, 15, 18, 21].forEach(startHour => {
|
||||||
|
const endHour = (startHour + 3) % 24;
|
||||||
|
const timeLabel = String(startHour).padStart(2, '0') + '-' + String(endHour).padStart(2, '0') + 'UT';
|
||||||
|
const tr = $('<tr>').append(`<td>${timeLabel}</td>`);
|
||||||
|
dates.forEach(dateStr => {
|
||||||
|
const [y, m, d] = dateStr.split('-').map(Number);
|
||||||
|
const slotTs = Date.UTC(y, m - 1, d, startHour, 0, 0) / 1000;
|
||||||
|
const td = $('<td>');
|
||||||
|
const kp = kpByTs[slotTs];
|
||||||
|
if (kp !== undefined) {
|
||||||
|
td.text(kp.toFixed(2));
|
||||||
|
td.addClass(kp < 5 ? 'bg-success-subtle' : kp < 7 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||||
|
}
|
||||||
|
tr.append(td);
|
||||||
|
});
|
||||||
|
tbody.append(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the solar storm forecast table
|
||||||
|
function renderSolarStormForecast(data) {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const entries = Object.entries(data)
|
||||||
|
.map(([tsStr, pct]) => ({ ts: parseFloat(tsStr), pct }))
|
||||||
|
.sort((a, b) => a.ts - b.ts);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
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' });
|
||||||
|
headRow.append(`<th>${label}</th>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Single data row: "S1 or greater" label + one cell per date
|
||||||
|
const tr = $('<tr>').append('<td>S1 or greater</td>');
|
||||||
|
entries.forEach(({ pct }) => {
|
||||||
|
const td = $('<td>').text(pct + '%');
|
||||||
|
td.addClass(pct < 50 ? 'bg-success-subtle' : pct < 75 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||||
|
tr.append(td);
|
||||||
|
});
|
||||||
|
$('#forecast-solar-storm-tbody').empty().append(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the radio blackout forecast table
|
||||||
|
function renderBlackoutForecast(r1r2Data, r3Data) {
|
||||||
|
if (!r1r2Data && !r3Data) return;
|
||||||
|
|
||||||
|
const tsSet = new Set([
|
||||||
|
...Object.keys(r1r2Data || {}),
|
||||||
|
...Object.keys(r3Data || {})
|
||||||
|
]);
|
||||||
|
const entries = [...tsSet]
|
||||||
|
.map(tsStr => ({
|
||||||
|
ts: parseFloat(tsStr),
|
||||||
|
r1r2: r1r2Data ? r1r2Data[tsStr] : undefined,
|
||||||
|
r3: r3Data ? r3Data[tsStr] : undefined
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.ts - b.ts);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const headRow = $('#forecast-blackout-head').empty().append('<th></th>');
|
||||||
|
entries.forEach(({ ts }) => {
|
||||||
|
const label = new Date(ts * 1000)
|
||||||
|
.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' });
|
||||||
|
headRow.append(`<th>${label}</th>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Two data rows: R1-R2 and R3+
|
||||||
|
function makeRow(rowLabel, getValue) {
|
||||||
|
const tr = $('<tr>').append(`<td>${rowLabel}</td>`);
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const pct = getValue(entry);
|
||||||
|
const td = $('<td>');
|
||||||
|
if (pct !== undefined) {
|
||||||
|
td.text(pct + '%');
|
||||||
|
td.addClass(pct < 50 ? 'bg-success-subtle' : pct < 75 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||||
|
}
|
||||||
|
tr.append(td);
|
||||||
|
});
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#forecast-blackout-tbody').empty()
|
||||||
|
.append(makeRow('R1-R2', e => e.r1r2))
|
||||||
|
.append(makeRow('R3 or greater', e => e.r3));
|
||||||
|
}
|
||||||
|
|
||||||
// Render the DX stats table for the currently selected DE continent
|
// Render the DX stats table for the currently selected DE continent
|
||||||
function renderDxStats() {
|
function renderDxStats() {
|
||||||
if (!dxStatsData) { return; }
|
if (!dxStatsData) { return; }
|
||||||
|
|||||||
Reference in New Issue
Block a user