-
Notifications
You must be signed in to change notification settings - Fork 131
Eclipse/Solar Analysis Plugin #1241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Eclipse/Solar Analysis Plugin #1241
Conversation
Update Documentation and Implement User API V2
Add comprehensive eclipse and solar analysis capabilities for satellite operations: Core Features: - Real-time eclipse status monitoring (sunlit/penumbral/umbral) - Eclipse entry/exit time predictions - Solar beta angle calculations and trending - Sun-target-satellite geometry analysis - Eclipse duration statistics and analysis - CSV export for mission planning Implementation: - eclipse-types.ts: Type definitions for eclipse events and solar data - eclipse-calculations.ts: Core calculation engine for eclipse prediction and beta angles - eclipse-solar-analysis.ts: Main plugin with UI and real-time updates - Integrated with existing SatMath utilities and astronomy-engine library - Follows established plugin architecture pattern UI Components: - Current eclipse status display with color coding - Solar beta angle monitoring with rate calculation - Eclipse prediction form with configurable parameters - Event timeline table showing entry/exit times - Statistics panel with eclipse duration and fraction - Export functionality for data analysis This plugin enables critical power/thermal analysis and observation planning for satellite operations.
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds a new Eclipse/Solar Analysis plugin that provides comprehensive eclipse prediction and solar geometry analysis capabilities for satellites in KeepTrack.
Key Changes:
- New plugin for analyzing satellite eclipse events, calculating solar beta angles, and predicting eclipse transitions
- Integration with existing satellite tracking and time management systems
- UI for displaying real-time eclipse status, predictions, and statistics with CSV export functionality
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/settings/default-plugins.ts |
Registers the new EclipseSolarAnalysis plugin with order 93 |
src/plugins/plugins.ts |
Imports and initializes the EclipseSolarAnalysis plugin |
src/plugins/keeptrack-plugins-configuration.ts |
Adds TypeScript type definition for EclipseSolarAnalysis configuration |
src/plugins/eclipse-solar-analysis/eclipse-types.ts |
Defines TypeScript interfaces and enums for eclipse events, periods, statistics, and configuration |
src/plugins/eclipse-solar-analysis/eclipse-solar-analysis.ts |
Implements the main plugin UI, real-time status updates, event handling, and data export |
src/plugins/eclipse-solar-analysis/eclipse-calculations.ts |
Provides core calculation methods for eclipse predictions, beta angles, and sun geometry |
src/app/analysis/sat-math.ts |
Modifies eclipse status checking logic and reorganizes imports |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private registerEventHandlers_(): void { | ||
| // Predict eclipses button | ||
| getEl('predict-eclipses-btn')?.addEventListener('click', () => { | ||
| this.predictEclipses_(); | ||
| }); | ||
|
|
||
| // Export data button | ||
| getEl('export-eclipse-data-btn')?.addEventListener('click', () => { | ||
| this.exportEclipseData_(); | ||
| }); | ||
| } |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Event listeners are registered in registerEventHandlers_() but are never removed. If the plugin is reinitialized or the DOM elements are recreated, this could lead to duplicate event listeners being attached.
Consider either:
- Removing event listeners when the plugin is destroyed/closed
- Using a flag to ensure handlers are only registered once
- Using event delegation on a parent element that persists
| setInterval(() => { | ||
| this.updateBetaAngle_(); | ||
| }, 10000); |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A second interval is created for beta angle updates but is never stored or cleared. This causes a memory leak as the interval will continue running even after the menu is closed.
Consider storing this interval reference and clearing it in stopRealTimeUpdates_():
private betaAngleInterval_: NodeJS.Timeout | null = null;
// In startRealTimeUpdates_():
this.betaAngleInterval_ = setInterval(() => {
this.updateBetaAngle_();
}, 10000);
// In stopRealTimeUpdates_():
if (this.betaAngleInterval_) {
clearInterval(this.betaAngleInterval_);
this.betaAngleInterval_ = null;
}|
|
||
|
|
||
| if (obj instanceof BaseObject) { | ||
| if (obj instanceof BaseObject || !(obj.position instanceof Boolean)) { |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition !(obj.position instanceof Boolean) is logically incorrect. The instanceof Boolean check will never be true for primitive boolean values. If obj.position is a boolean primitive (e.g., false), false instanceof Boolean returns false, so !(false instanceof Boolean) becomes true, which would incorrectly allow the code to proceed.
The intent appears to be checking if obj.position is not a boolean. The correct check should be:
if (obj instanceof BaseObject || typeof obj.position !== 'boolean') {| if (obj instanceof BaseObject || !(obj.position instanceof Boolean)) { | |
| if (obj instanceof BaseObject || typeof obj.position !== 'boolean') { |
| x = obj.position.x; | ||
| y = obj.position.y; | ||
| z = obj.position.z; |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type assertions have been removed (e.g., changing obj.position.x as Kilometers to just obj.position.x), but the variables x, y, and z are still declared with the type Kilometers earlier in the function. This could lead to type safety issues if the values don't match the expected type.
Consider either:
- Keeping the type assertions if the values should be
Kilometers - Changing the variable declarations to match the actual types
Without seeing the full function context, if these values should be Kilometers, the original code was more type-safe.
| (<HTMLInputElement>getEl('eclipse-stats-total')).value = '--'; | ||
| (<HTMLInputElement>getEl('eclipse-stats-duration')).value = '--'; | ||
| (<HTMLInputElement>getEl('eclipse-stats-fraction')).value = '--'; | ||
| (<HTMLInputElement>getEl('eclipse-stats-avg')).value = '--'; |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference errors when clearing statistics. If any of the elements don't exist, attempting to set the value property on null will throw a runtime error.
Consider adding null checks:
const totalEl = getEl('eclipse-stats-total');
const durationEl = getEl('eclipse-stats-duration');
const fractionEl = getEl('eclipse-stats-fraction');
const avgEl = getEl('eclipse-stats-avg');
if (totalEl) (<HTMLInputElement>totalEl).value = '--';
if (durationEl) (<HTMLInputElement>durationEl).value = '--';
if (fractionEl) (<HTMLInputElement>fractionEl).value = '--';
if (avgEl) (<HTMLInputElement>avgEl).value = '--';| (<HTMLInputElement>getEl('eclipse-stats-total')).value = '--'; | |
| (<HTMLInputElement>getEl('eclipse-stats-duration')).value = '--'; | |
| (<HTMLInputElement>getEl('eclipse-stats-fraction')).value = '--'; | |
| (<HTMLInputElement>getEl('eclipse-stats-avg')).value = '--'; | |
| const totalEl = getEl('eclipse-stats-total'); | |
| if (totalEl) (<HTMLInputElement>totalEl).value = '--'; | |
| const durationEl = getEl('eclipse-stats-duration'); | |
| if (durationEl) (<HTMLInputElement>durationEl).value = '--'; | |
| const fractionEl = getEl('eclipse-stats-fraction'); | |
| if (fractionEl) (<HTMLInputElement>fractionEl).value = '--'; | |
| const avgEl = getEl('eclipse-stats-avg'); | |
| if (avgEl) (<HTMLInputElement>avgEl).value = '--'; |
| sideMenuElementHtml: string = html` | ||
| <div id="eclipse-solar-menu" class="side-menu-parent start-hidden text-select"> | ||
| <div id="eclipse-solar-content" class="side-menu"> | ||
| <div class="row"> | ||
| <h5 class="center-align">Eclipse & Solar Analysis</h5> | ||
| </div> | ||
| <!-- Current Status Section --> | ||
| <div class="row"> | ||
| <h6>Current Status</h6> | ||
| <div class="eclipse-status-container"> | ||
| <div class="input-field col s12"> | ||
| <label for="eclipse-status-current" class="active">Eclipse Status</label> | ||
| <input disabled value="Unknown" id="eclipse-status-current" type="text" /> | ||
| </div> | ||
| <div class="input-field col s12"> | ||
| <label for="eclipse-time-in-shadow" class="active">Time in Current State</label> | ||
| <input disabled value="--:--:--" id="eclipse-time-in-shadow" type="text" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <!-- Solar Beta Angle Section --> | ||
| <div class="row"> | ||
| <h6>Solar Beta Angle</h6> | ||
| <div class="input-field col s12"> | ||
| <label for="beta-angle-current" class="active">Current Beta Angle</label> | ||
| <input disabled value="0.00°" id="beta-angle-current" type="text" /> | ||
| </div> | ||
| <div class="input-field col s12"> | ||
| <label for="beta-angle-rate" class="active">Beta Angle Rate</label> | ||
| <input disabled value="0.00 °/day" id="beta-angle-rate" type="text" /> | ||
| </div> | ||
| </div> | ||
| <!-- Eclipse Prediction Section --> | ||
| <div class="row"> | ||
| <h6>Eclipse Predictions</h6> | ||
| <form id="eclipse-prediction-form"> | ||
| <div class="input-field col s12"> | ||
| <input value="24" id="prediction-duration" type="number" min="1" max="168" /> | ||
| <label for="prediction-duration" class="active">Prediction Duration (hours)</label> | ||
| </div> | ||
| <div class="input-field col s12"> | ||
| <input value="60" id="time-step" type="number" min="10" max="300" /> | ||
| <label for="time-step" class="active">Time Step (seconds)</label> | ||
| </div> | ||
| <div class="center-align"> | ||
| <button id="predict-eclipses-btn" class="btn btn-ui waves-effect waves-light" type="button"> | ||
| Predict Eclipses ► | ||
| </button> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| <!-- Eclipse Results Section --> | ||
| <div class="row"> | ||
| <h6>Eclipse Events</h6> | ||
| <div id="eclipse-events-container"> | ||
| <p class="center-align">Click "Predict Eclipses" to see upcoming events</p> | ||
| </div> | ||
| </div> | ||
| <!-- Statistics Section --> | ||
| <div class="row"> | ||
| <h6>Eclipse Statistics</h6> | ||
| <div id="eclipse-statistics-container"> | ||
| <div class="input-field col s12"> | ||
| <label for="eclipse-stats-total" class="active">Total Eclipse Events</label> | ||
| <input disabled value="--" id="eclipse-stats-total" type="text" /> | ||
| </div> | ||
| <div class="input-field col s12"> | ||
| <label for="eclipse-stats-duration" class="active">Total Eclipse Time</label> | ||
| <input disabled value="--" id="eclipse-stats-duration" type="text" /> | ||
| </div> | ||
| <div class="input-field col s12"> | ||
| <label for="eclipse-stats-fraction" class="active">Eclipse Fraction</label> | ||
| <input disabled value="--" id="eclipse-stats-fraction" type="text" /> | ||
| </div> | ||
| <div class="input-field col s12"> | ||
| <label for="eclipse-stats-avg" class="active">Average Eclipse Duration</label> | ||
| <input disabled value="--" id="eclipse-stats-avg" type="text" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <!-- Export Section --> | ||
| <div class="row"> | ||
| <div class="center-align"> | ||
| <button id="export-eclipse-data-btn" class="btn btn-ui waves-effect waves-light" type="button" disabled> | ||
| Export Data (CSV) ► | ||
| </button> | ||
| </div> | ||
| </div> |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The HTML input elements use inconsistent ID naming conventions. Most use kebab-case (e.g., eclipse-status-current, beta-angle-current), but the pattern isn't entirely consistent. Consider using a consistent prefix like eclipse- or esa- (for Eclipse Solar Analysis) for all IDs in this plugin to avoid potential conflicts with other plugins.
| // Sun elevation angle (angle above satellite's horizon) | ||
| // For simplicity, we calculate it relative to the satellite's velocity vector as "forward" | ||
| const velocity = vec3.fromValues(posvel.velocity.x, posvel.velocity.y, posvel.velocity.z); | ||
|
|
||
| vec3.normalize(velocity, velocity); | ||
|
|
||
| const elevationDot = vec3.dot(satToSun, velocity); | ||
| const sunElevation = (Math.asin(Math.max(-1, Math.min(1, elevationDot))) * RAD2DEG) as Degrees; |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The calculateSunGeometry method's sun elevation calculation could be misleading. The comment states "angle above satellite's horizon" but the implementation uses the velocity vector as "forward", which doesn't represent the satellite's actual horizon. The horizon would typically be perpendicular to the position vector (nadir direction). This could confuse future maintainers.
Consider clarifying the documentation to match the implementation:
// Sun elevation angle relative to satellite's velocity vectorOr adjust the implementation to calculate true horizon-based elevation.
| // Calculate beta angle rate (change over 1 day) | ||
| const nextDayTime = new Date(currentTime.getTime() + 24 * 3600 * 1000); | ||
| const nextDayBeta = EclipseCalculations.calculateSolarBetaAngle(this.currentSatellite_, nextDayTime); | ||
| const betaRate = nextDayBeta - betaAngle; | ||
|
|
||
| const rateEl = getEl('beta-angle-rate'); | ||
|
|
||
| if (rateEl) { | ||
| (<HTMLInputElement>rateEl).value = `${betaRate.toFixed(2)} °/day`; |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The beta angle rate calculation in updateBetaAngle_() performs a full position propagation 24 hours into the future on every call. Since this is called every 10 seconds when the menu is open, this could be a performance concern for long simulation runs or when the propagation is computationally expensive.
Consider:
- Caching the beta angle rate and updating it less frequently (e.g., every minute)
- Using a numerical derivative with a shorter time step (e.g., 1 hour) for faster computation
- Only calculating the rate when specifically requested by the user
| // Calculate beta angle rate (change over 1 day) | |
| const nextDayTime = new Date(currentTime.getTime() + 24 * 3600 * 1000); | |
| const nextDayBeta = EclipseCalculations.calculateSolarBetaAngle(this.currentSatellite_, nextDayTime); | |
| const betaRate = nextDayBeta - betaAngle; | |
| const rateEl = getEl('beta-angle-rate'); | |
| if (rateEl) { | |
| (<HTMLInputElement>rateEl).value = `${betaRate.toFixed(2)} °/day`; | |
| // Calculate beta angle rate (change over 1 hour) | |
| const nextHourTime = new Date(currentTime.getTime() + 1 * 3600 * 1000); | |
| const nextHourBeta = EclipseCalculations.calculateSolarBetaAngle(this.currentSatellite_, nextHourTime); | |
| const betaRate = nextHourBeta - betaAngle; | |
| const rateEl = getEl('beta-angle-rate'); | |
| if (rateEl) { | |
| (<HTMLInputElement>rateEl).value = `${betaRate.toFixed(2)} °/hr`; |
| eclipseFraction: totalDuration > 0 ? totalEclipseTime / totalDuration : 0, | ||
| averageEclipseDuration: (totalEclipseTime / periods.length) as Milliseconds, | ||
| maxEclipseDuration: maxDuration as Milliseconds, | ||
| minEclipseDuration: minDuration as Milliseconds, |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When no eclipse periods are found, minEclipseDuration is set to 0 as Milliseconds, but the variable minDuration is initialized to Infinity. This is inconsistent. If periods exist but all have very long durations, minDuration would remain as Infinity after the loop (though this scenario is unlikely).
Consider initializing differently:
let minDuration = periods.length > 0 ? periods[0].duration : 0;Or ensure the return value is consistent:
minEclipseDuration: (minDuration === Infinity ? 0 : minDuration) as Milliseconds,| minEclipseDuration: minDuration as Milliseconds, | |
| minEclipseDuration: (minDuration === Infinity ? 0 : minDuration) as Milliseconds, |
|



No description provided.