// Cache for the full dxstats API response, so we can reload on the fly if the user changes the value of their continent // in the select box let dxStatsData = null; // Load solar conditions function loadSolarConditions() { $.getJSON('/api/v1/solar', function(jsonData) { // HF const hfConditionClass = { 'Good': 'bg-success-subtle', 'Fair': 'bg-warning-subtle', 'Poor': 'bg-danger-subtle' }; if (jsonData.hf_conditions) { Object.entries(jsonData.hf_conditions).forEach(function([key, condition]) { const cell = $('#hf-conditions-' + key); cell.text(condition); cell.addClass(hfConditionClass[condition]); }); } // VHF if (jsonData.vhf_conditions) { Object.entries(jsonData.vhf_conditions).forEach(function([key, condition]) { const cell = $('#vhf-conditions-' + key); cell.text(condition); let vhfClass; if (condition === 'Band Closed') { vhfClass = 'bg-danger-subtle'; } else if (condition.includes('High')) { vhfClass = 'bg-warning-subtle'; } else { vhfClass = 'bg-success-subtle'; } cell.addClass(vhfClass); }); } if (jsonData.aurora_latitude !== null && jsonData.aurora_latitude !== undefined) { $('#vhf-conditions-aurora-lat').text(jsonData.aurora_latitude + '°'); } // Solar Weather const swFields = { 'sfi': 'sw-sfi', 'sunspots': 'sw-sunspots', 'band_conditions_desc': 'sw-solar-flux-desc', 'k_index': 'sw-k-index', 'a_index': 'sw-a-index', 'geomag_field': 'sw-geomag-field', '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', 'proton_flux': 'sw-proton-flux', 'solar_storm_scale': 'sw-solar-storm-scale', 'proton_flux_desc': 'sw-proton-desc', 'electron_flux': 'sw-electron-flux', 'electron_flux_desc': 'sw-electron-desc', }; Object.entries(swFields).forEach(function([field, id]) { const val = jsonData[field]; if (val !== null && val !== undefined) { $('#' + id).text(val); } }); // Solar Weather - colouring function applySwClass(valsId, descId, cls) { $('#' + valsId).addClass(cls); $('#' + descId).addClass(cls); } const sfi = jsonData.sfi; if (sfi !== null && sfi !== undefined) { applySwClass('sw-solar-flux-vals', 'sw-solar-flux-desc', sfi > 120 ? 'bg-success-subtle' : sfi > 90 ? 'bg-warning-subtle' : 'bg-danger-subtle'); } const kIndex = jsonData.k_index; if (kIndex !== null && kIndex !== undefined) { applySwClass('sw-geomag-vals', 'sw-geomag-desc', kIndex < 5 ? 'bg-success-subtle' : kIndex < 7 ? 'bg-warning-subtle' : 'bg-danger-subtle'); } const xRay = jsonData.x_ray; if (xRay) { const letter = xRay[0].toUpperCase(); const xRayClass = (letter === 'X') ? 'bg-danger-subtle' : (letter === 'M') ? 'bg-warning-subtle' : 'bg-success-subtle'; applySwClass('sw-xray-vals', 'sw-xray-desc', xRayClass); } const protonFlux = jsonData.proton_flux; if (protonFlux !== null && protonFlux !== undefined) { applySwClass('sw-proton-vals', 'sw-proton-desc', protonFlux <= 100 ? 'bg-success-subtle' : protonFlux <= 10000 ? 'bg-warning-subtle' : 'bg-danger-subtle'); } const electronFlux = jsonData.electron_flux; if (electronFlux !== null && electronFlux !== undefined) { 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; } const deContinent = $('#dxstats-de-continent').val(); const deData = dxStatsData[deContinent]; if (!deData) { return; } const cells = []; Object.entries(deData).forEach(function([dxContinent, bands]) { Object.entries(bands).forEach(function([band, count]) { const cell = $('#dxstats-' + dxContinent + '-' + band); cell.text(count); cells.push({ cell, count }); }); }); const counts = cells.map(function(c) { return c.count; }); const min = Math.min(...counts); const max = Math.max(...counts); const range = max - min; cells.forEach(function({ cell, count }) { const t = range > 0 ? (count - min) / range : 0; const cls = t === 0 ? 'bg-danger-subtle' : t < 0.05 ? 'bg-warning-subtle' : 'bg-success-subtle'; cell.removeClass('bg-danger-subtle bg-warning-subtle bg-success-subtle').addClass(cls); }); } // Called when the DE continent select changes function dxStatsContientChanged() { saveSettings(); renderDxStats(); } // Fetch DX stats from the API and render function loadDxStats() { $.getJSON('/api/v1/dxstats', function(jsonData) { dxStatsData = jsonData; renderDxStats(); }); } // Startup $(document).ready(function() { loadSettings(); loadSolarConditions(); loadDxStats(); });