From 4a6d9da03131973d3605af84d0af7ff7efcba843 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Fri, 3 Apr 2026 17:40:00 +0100 Subject: [PATCH] Add fetching of NOAA 3-day forecast --- templates/about.html | 2 +- templates/add_spot.html | 4 +- templates/alerts.html | 4 +- templates/bands.html | 6 +- templates/base.html | 8 +- templates/conditions.html | 251 ++++++++++++++++++++++--------------- templates/map.html | 6 +- templates/spots.html | 6 +- templates/status.html | 4 +- webassets/js/conditions.js | 122 ++++++++++++++++++ 10 files changed, 289 insertions(+), 124 deletions(-) diff --git a/templates/about.html b/templates/about.html index 8783c97..e936997 100644 --- a/templates/about.html +++ b/templates/about.html @@ -67,7 +67,7 @@

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.

- + {% end %} \ No newline at end of file diff --git a/templates/add_spot.html b/templates/add_spot.html index dd1f73e..6a1f181 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -69,8 +69,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/alerts.html b/templates/alerts.html index c4c1bbc..983a93f 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -56,8 +56,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/bands.html b/templates/bands.html index b496f7e..9c1c3bd 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -62,9 +62,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index eac3105..4259bf8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -46,10 +46,10 @@ crossorigin="anonymous"> - - - - + + + + diff --git a/templates/conditions.html b/templates/conditions.html index c71ca44..c05d037 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -7,84 +7,76 @@
-
-
-
-
HF
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
BandDayNight
80-40m
30-20m
17-15m
12-10m
-
-
+
+
HF
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BandDayNight
80-40m
30-20m
17-15m
12-10m
-
-
-
-
VHF
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Propagation ModeCondition
Sporadic-E 6m (Europe)
Sporadic-E 4m (Europe)
Sporadic-E 2m (Europe)
Sporadic-E 2m (North America)
Aurora (Northern Hemisphere)
Aurora Minimum Latitude
-
-
+
+
VHF
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Propagation ModeCondition
Sporadic-E 6m (Europe)
Sporadic-E 4m (Europe)
Sporadic-E 2m (Europe)
Sporadic-E 2m (North America)
Aurora (Northern Hemisphere)
Aurora Minimum Latitude
Data from HamQSL.com.
@@ -95,7 +87,7 @@
Solar Weather
-
+
Solar Flux
@@ -138,6 +130,50 @@
+
+
+ Forecast +
+
+
+
+
K-index Forecast
+
+ + + + + +
+
+
+
+
+
+
Solar Storm Forecast
+ + + + + +
+
+
+
Blackout Forecast
+ + + + + +
+
+
+ +
+
+
DX Opportunities @@ -145,7 +181,8 @@
- @@ -158,39 +195,45 @@
- - - - - - - - - - - - - - + + + + + + + + + + + + + + - {% for continent in ["EU", "NA", "SA", "AS", "AF", "OC", "AN"] %} - - - {% for band in ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"] %} - - {% end %} - + {% for continent in ["EU", "NA", "SA", "AS", "AF", "OC", "AN"] %} + + + {% for band in ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"] %} + {% end %} + + {% end %}
160m80m60m40m30m20m17m15m12m10m6m
160m80m60m40m30m20m17m15m12m10m6m
{{ continent }}
{{ continent }}
-
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.
+
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. +
- - - + + + {% end %} \ No newline at end of file diff --git a/templates/map.html b/templates/map.html index 9ad1237..366494f 100644 --- a/templates/map.html +++ b/templates/map.html @@ -70,9 +70,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/spots.html b/templates/spots.html index 330927d..aab18cd 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -87,9 +87,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index fa1d432..7d37923 100644 --- a/templates/status.html +++ b/templates/status.html @@ -59,8 +59,8 @@
- - + + diff --git a/webassets/js/conditions.js b/webassets/js/conditions.js index 159f704..dd1e4a6 100644 --- a/webassets/js/conditions.js +++ b/webassets/js/conditions.js @@ -105,9 +105,131 @@ function loadSolarConditions() { applySwClass('sw-electron-vals', 'sw-electron-desc', 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('Time (UTC)'); + dates.forEach(dateStr => { + const label = new Date(dateStr + 'T00:00:00Z') + .toLocaleDateString('en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' }); + headRow.append(`${label}`); + }); + + // 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 = $('').append(`${timeLabel}`); + 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 = $(''); + 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(''); + entries.forEach(({ ts }) => { + const label = new Date(ts * 1000) + .toLocaleDateString('en-US', { day: '2-digit', month: 'short', timeZone: 'UTC' }); + headRow.append(`${label}`); + }); + + // Single data row: "S1 or greater" label + one cell per date + const tr = $('').append('S1 or greater'); + entries.forEach(({ pct }) => { + const 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(''); + entries.forEach(({ ts }) => { + const label = new Date(ts * 1000) + .toLocaleDateString('en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' }); + headRow.append(`${label}`); + }); + + // Two data rows: R1-R2 and R3+ + function makeRow(rowLabel, getValue) { + const tr = $('').append(`${rowLabel}`); + entries.forEach(entry => { + const pct = getValue(entry); + const 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 function renderDxStats() { if (!dxStatsData) { return; }