mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
feat: Weather satellite and ADS-B trail rendering improvements
Weather Satellite: - Fix duplicate event listeners on mode re-entry via locationListenersAttached guard - Add suspend() to stop countdown/SSE stream when switching away from the mode - Call WeatherSat.suspend() in switchMode() when leaving weathersat - Fix toggleScheduler() to take the checkbox element as source of truth, preventing both checkboxes fighting each other - Reset isRunning/UI state after auto-capture completes (scheduler path) - Always re-select first pass and reset selectedPassIndex after loadPasses() - Keep timeline cursor in sync inside selectPass() - Add seconds to pass ID format to avoid collisions on concurrent passes - Improve predict_passes() comment clarity; fix trajectory comment ADS-B dashboard: - Batch altitude-colour trail segments into runs of same-colour polylines, reducing Leaflet layer count from O(trail length) to O(colour changes) for significantly better rendering performance with many aircraft Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ const WeatherSat = (function() {
|
|||||||
let currentPhase = 'idle';
|
let currentPhase = 'idle';
|
||||||
let consoleAutoHideTimer = null;
|
let consoleAutoHideTimer = null;
|
||||||
let currentModalFilename = null;
|
let currentModalFilename = null;
|
||||||
|
let locationListenersAttached = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Weather Satellite mode
|
* Initialize the Weather Satellite mode
|
||||||
@@ -54,8 +55,13 @@ const WeatherSat = (function() {
|
|||||||
if (latInput && storedLat) latInput.value = storedLat;
|
if (latInput && storedLat) latInput.value = storedLat;
|
||||||
if (lonInput && storedLon) lonInput.value = storedLon;
|
if (lonInput && storedLon) lonInput.value = storedLon;
|
||||||
|
|
||||||
if (latInput) latInput.addEventListener('change', saveLocationFromInputs);
|
// Only attach listeners once — re-calling init() on mode switch must not
|
||||||
if (lonInput) lonInput.addEventListener('change', saveLocationFromInputs);
|
// accumulate duplicate listeners that fire loadPasses() multiple times.
|
||||||
|
if (!locationListenersAttached) {
|
||||||
|
if (latInput) latInput.addEventListener('change', saveLocationFromInputs);
|
||||||
|
if (lonInput) lonInput.addEventListener('change', saveLocationFromInputs);
|
||||||
|
locationListenersAttached = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -428,8 +434,17 @@ const WeatherSat = (function() {
|
|||||||
updateStatusUI('capturing', `Auto: ${p.name || p.satellite} ${p.frequency} MHz`);
|
updateStatusUI('capturing', `Auto: ${p.name || p.satellite} ${p.frequency} MHz`);
|
||||||
showNotification('Weather Sat', `Auto-capture started: ${p.name || p.satellite}`);
|
showNotification('Weather Sat', `Auto-capture started: ${p.name || p.satellite}`);
|
||||||
} else if (data.type === 'schedule_capture_complete') {
|
} else if (data.type === 'schedule_capture_complete') {
|
||||||
showNotification('Weather Sat', `Auto-capture complete: ${(data.pass || {}).name || ''}`);
|
const p = data.pass || {};
|
||||||
|
showNotification('Weather Sat', `Auto-capture complete: ${p.name || ''}`);
|
||||||
|
// Reset UI — the decoder's stop() doesn't emit a progress complete event
|
||||||
|
// when called internally by the scheduler, so we handle it here.
|
||||||
|
isRunning = false;
|
||||||
|
updateStatusUI('idle', 'Auto-capture complete');
|
||||||
|
const captureStatus = document.getElementById('wxsatCaptureStatus');
|
||||||
|
if (captureStatus) captureStatus.classList.remove('active');
|
||||||
|
updatePhaseIndicator('complete');
|
||||||
loadImages();
|
loadImages();
|
||||||
|
loadPasses();
|
||||||
} else if (data.type === 'schedule_capture_skipped') {
|
} else if (data.type === 'schedule_capture_skipped') {
|
||||||
const reason = data.reason || 'unknown';
|
const reason = data.reason || 'unknown';
|
||||||
const p = data.pass || {};
|
const p = data.pass || {};
|
||||||
@@ -474,11 +489,13 @@ const WeatherSat = (function() {
|
|||||||
|
|
||||||
if (data.status === 'ok') {
|
if (data.status === 'ok') {
|
||||||
passes = data.passes || [];
|
passes = data.passes || [];
|
||||||
|
selectedPassIndex = -1;
|
||||||
renderPasses(passes);
|
renderPasses(passes);
|
||||||
renderTimeline(passes);
|
renderTimeline(passes);
|
||||||
updateCountdownFromPasses();
|
updateCountdownFromPasses();
|
||||||
// Auto-select first pass
|
// Always select the first upcoming pass so the polar plot
|
||||||
if (passes.length > 0 && selectedPassIndex < 0) {
|
// and ground track reflect the current list after every refresh.
|
||||||
|
if (passes.length > 0) {
|
||||||
selectPass(0);
|
selectPass(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -875,6 +892,9 @@ const WeatherSat = (function() {
|
|||||||
b.classList.toggle('active', isActive);
|
b.classList.toggle('active', isActive);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep timeline cursor in sync
|
||||||
|
updateTimelineCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
@@ -939,12 +959,13 @@ const WeatherSat = (function() {
|
|||||||
/**
|
/**
|
||||||
* Toggle auto-scheduler
|
* Toggle auto-scheduler
|
||||||
*/
|
*/
|
||||||
async function toggleScheduler() {
|
async function toggleScheduler(source) {
|
||||||
|
const checked = source?.checked ?? false;
|
||||||
|
|
||||||
const stripCheckbox = document.getElementById('wxsatAutoSchedule');
|
const stripCheckbox = document.getElementById('wxsatAutoSchedule');
|
||||||
const sidebarCheckbox = document.getElementById('wxsatSidebarAutoSchedule');
|
const sidebarCheckbox = document.getElementById('wxsatSidebarAutoSchedule');
|
||||||
const checked = stripCheckbox?.checked || sidebarCheckbox?.checked;
|
|
||||||
|
|
||||||
// Sync both checkboxes
|
// Sync both checkboxes to the source of truth
|
||||||
if (stripCheckbox) stripCheckbox.checked = checked;
|
if (stripCheckbox) stripCheckbox.checked = checked;
|
||||||
if (sidebarCheckbox) sidebarCheckbox.checked = checked;
|
if (sidebarCheckbox) sidebarCheckbox.checked = checked;
|
||||||
|
|
||||||
@@ -1386,9 +1407,29 @@ const WeatherSat = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suspend background activity when leaving the mode.
|
||||||
|
* Closes the SSE stream and stops the countdown interval so they don't
|
||||||
|
* keep running while another mode is active. The stream is re-opened
|
||||||
|
* by init() or startStream() when the mode is next entered.
|
||||||
|
*/
|
||||||
|
function suspend() {
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
countdownInterval = null;
|
||||||
|
}
|
||||||
|
// Only close the stream if nothing is actively capturing/scheduling —
|
||||||
|
// if a capture or scheduler is running we want it to continue on the
|
||||||
|
// server and the stream will reconnect on next init().
|
||||||
|
if (!isRunning && !schedulerEnabled) {
|
||||||
|
stopStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
return {
|
return {
|
||||||
init,
|
init,
|
||||||
|
suspend,
|
||||||
start,
|
start,
|
||||||
stop,
|
stop,
|
||||||
startPass,
|
startPass,
|
||||||
|
|||||||
@@ -1345,21 +1345,41 @@ ACARS: ${r.statistics.acarsMessages} messages`;
|
|||||||
}
|
}
|
||||||
trailLines[icao] = [];
|
trailLines[icao] = [];
|
||||||
|
|
||||||
// Create gradient segments
|
// Group consecutive same-altitude-color points into single polylines.
|
||||||
|
// This reduces layer count from O(trail length) to O(color band changes),
|
||||||
|
// which is typically 1-2 polylines per aircraft instead of up to 99.
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (let i = 1; i < trail.length; i++) {
|
let runColor = getAltitudeColor(trail[0].alt);
|
||||||
const p1 = trail[i-1];
|
let runPoints = [[trail[0].lat, trail[0].lon]];
|
||||||
const p2 = trail[i];
|
let runEndTime = trail[0].time;
|
||||||
const age = (now - p2.time) / 1000; // seconds
|
|
||||||
const opacity = Math.max(0.2, 1 - (age / 120)); // Fade over 2 minutes
|
|
||||||
|
|
||||||
const color = getAltitudeColor(p2.alt);
|
for (let i = 1; i < trail.length; i++) {
|
||||||
const line = L.polyline([[p1.lat, p1.lon], [p2.lat, p2.lon]], {
|
const p = trail[i];
|
||||||
color: color,
|
const color = getAltitudeColor(p.alt);
|
||||||
weight: 2,
|
|
||||||
opacity: opacity
|
if (color !== runColor) {
|
||||||
}).addTo(radarMap);
|
// Flush the current color run as one polyline
|
||||||
trailLines[icao].push(line);
|
if (runPoints.length >= 2) {
|
||||||
|
const opacity = Math.max(0.2, 1 - ((now - runEndTime) / 1000 / 120));
|
||||||
|
trailLines[icao].push(
|
||||||
|
L.polyline(runPoints, { color: runColor, weight: 2, opacity }).addTo(radarMap)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Start a new run, sharing the junction point for visual continuity
|
||||||
|
runColor = color;
|
||||||
|
runPoints = [[trail[i-1].lat, trail[i-1].lon], [p.lat, p.lon]];
|
||||||
|
} else {
|
||||||
|
runPoints.push([p.lat, p.lon]);
|
||||||
|
}
|
||||||
|
runEndTime = p.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the final run
|
||||||
|
if (runPoints.length >= 2) {
|
||||||
|
const opacity = Math.max(0.2, 1 - ((now - runEndTime) / 1000 / 120));
|
||||||
|
trailLines[icao].push(
|
||||||
|
L.polyline(runPoints, { color: runColor, weight: 2, opacity }).addTo(radarMap)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2696,7 +2696,7 @@
|
|||||||
<div class="wxsat-strip-divider"></div>
|
<div class="wxsat-strip-divider"></div>
|
||||||
<div class="wxsat-strip-group">
|
<div class="wxsat-strip-group">
|
||||||
<label class="wxsat-schedule-toggle" title="Auto-capture passes">
|
<label class="wxsat-schedule-toggle" title="Auto-capture passes">
|
||||||
<input type="checkbox" id="wxsatAutoSchedule" onchange="WeatherSat.toggleScheduler()">
|
<input type="checkbox" id="wxsatAutoSchedule" onchange="WeatherSat.toggleScheduler(this)">
|
||||||
<span class="wxsat-toggle-label">AUTO</span>
|
<span class="wxsat-toggle-label">AUTO</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -4036,6 +4036,11 @@
|
|||||||
if (typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy) SpaceWeather.destroy();
|
if (typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy) SpaceWeather.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suspend Weather Satellite background timers/streams when leaving the mode
|
||||||
|
if (mode !== 'weathersat') {
|
||||||
|
if (typeof WeatherSat !== 'undefined' && WeatherSat.suspend) WeatherSat.suspend();
|
||||||
|
}
|
||||||
|
|
||||||
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
|
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
|
||||||
const reconBtn = document.getElementById('reconBtn');
|
const reconBtn = document.getElementById('reconBtn');
|
||||||
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
||||||
|
|||||||
@@ -226,7 +226,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label style="display: flex; align-items: center; gap: 6px;">
|
<label style="display: flex; align-items: center; gap: 6px;">
|
||||||
<input type="checkbox" id="wxsatSidebarAutoSchedule" onchange="WeatherSat.toggleScheduler()" style="width: auto;">
|
<input type="checkbox" id="wxsatSidebarAutoSchedule" onchange="WeatherSat.toggleScheduler(this)" style="width: auto;">
|
||||||
Enable Auto-Capture
|
Enable Auto-Capture
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def predict_passes(
|
|||||||
).total_seconds()
|
).total_seconds()
|
||||||
duration_minutes = round(duration_seconds / 60, 1)
|
duration_minutes = round(duration_seconds / 60, 1)
|
||||||
|
|
||||||
# Calculate max elevation and trajectory
|
# Calculate max elevation (always) and trajectory points (only if requested)
|
||||||
max_el = 0.0
|
max_el = 0.0
|
||||||
max_el_az = 0.0
|
max_el_az = 0.0
|
||||||
trajectory: list[dict[str, float]] = []
|
trajectory: list[dict[str, float]] = []
|
||||||
@@ -141,14 +141,14 @@ def predict_passes(
|
|||||||
_, set_az, _ = set_topo.altaz()
|
_, set_az, _ = set_topo.altaz()
|
||||||
|
|
||||||
pass_data: dict[str, Any] = {
|
pass_data: dict[str, Any] = {
|
||||||
'id': f"{sat_key}_{rise_time.utc_datetime().strftime('%Y%m%d%H%M')}",
|
'id': f"{sat_key}_{rise_time.utc_datetime().strftime('%Y%m%d%H%M%S')}",
|
||||||
'satellite': sat_key,
|
'satellite': sat_key,
|
||||||
'name': sat_info['name'],
|
'name': sat_info['name'],
|
||||||
'frequency': sat_info['frequency'],
|
'frequency': sat_info['frequency'],
|
||||||
'mode': sat_info['mode'],
|
'mode': sat_info['mode'],
|
||||||
'startTime': rise_time.utc_datetime().strftime('%Y-%m-%d %H:%M UTC'),
|
'startTime': rise_time.utc_datetime().strftime('%Y-%m-%d %H:%M UTC'),
|
||||||
'startTimeISO': rise_time.utc_datetime().isoformat(),
|
'startTimeISO': rise_time.utc_datetime().isoformat() + 'Z',
|
||||||
'endTimeISO': set_time.utc_datetime().isoformat(),
|
'endTimeISO': set_time.utc_datetime().isoformat() + 'Z',
|
||||||
'maxEl': round(max_el, 1),
|
'maxEl': round(max_el, 1),
|
||||||
'maxElAz': round(max_el_az, 1),
|
'maxElAz': round(max_el_az, 1),
|
||||||
'riseAz': round(rise_az.degrees, 1),
|
'riseAz': round(rise_az.degrees, 1),
|
||||||
|
|||||||
Reference in New Issue
Block a user