` +
@@ -256,41 +267,102 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
let altitudeStr = `${altitude.toLocaleString()} ${distanceUnitsStr}`;
return `
-
+
- ${sat.name}
-
+ ${sat.name}
+
+
- | ${t('station.settings.satellites.latitude')}: | ${sat.lat.toFixed(2)}° |
- | ${t('station.settings.satellites.longitude')}: | ${sat.lon.toFixed(2)}° |
- | ${t('station.settings.satellites.speed')}: | ${speedStr} |
- | ${t('station.settings.satellites.altitude')}: | ${altitudeStr} |
- | ${t('station.settings.satellites.azimuth_elevation')}: | ${sat.azimuth}° / ${sat.elevation}° |
+
+
+
+ | ${t('station.settings.satellites.latitude')}: |
+ ${sat.lat.toFixed(2)}° |
+
+
+ | ${t('station.settings.satellites.longitude')}: |
+ ${sat.lon.toFixed(2)}° |
+
+
+ | ${t('station.settings.satellites.altitude')}: |
+ ${altitudeStr} |
+
+
+ | ${t('station.settings.satellites.speed')}: |
+ ${speedStr} |
+
+
+
+
+ | ${t('station.settings.satellites.azimuth_elevation')}: |
+ ${sat.azimuth}° / ${sat.elevation}° |
+
${
isVisible
? `
- | ${t('station.settings.satellites.range')}: | ${(sat.range * (isMetric ? 1 : km_to_miles_factor)).toFixed(0)} ${distanceUnitsStr} |
- | ${t('station.settings.satellites.rangeRate')}: | ${(sat.rangeRate * (isMetric ? 1 : km_to_miles_factor)).toFixed(2)} ${rangeRateUnitsStr} |
- | ${t('station.settings.satellites.dopplerFactor')}: | ${sat.dopplerFactor.toFixed(7)} |
+
+ | ${t('station.settings.satellites.range')}: |
+ ${(sat.range * (isMetric ? 1 : km_to_miles_factor)).toFixed(0)} ${distanceUnitsStr} |
+
+
+ | ${t('station.settings.satellites.rangeRate')}: |
+ ${(sat.rangeRate * (isMetric ? 1 : km_to_miles_factor)).toFixed(2)} ${rangeRateUnitsStr} |
+
+
+ | ${t('station.settings.satellites.dopplerFactor')}: |
+ ${sat.dopplerFactor.toFixed(7)} |
+
`
: ``
}
- | ${t('station.settings.satellites.mode')}: | ${sat.mode || 'N/A'} |
- ${sat.downlink ? `| ${t('station.settings.satellites.downlink')}: | ${sat.downlink} |
` : ''}
- ${sat.uplink ? `| ${t('station.settings.satellites.uplink')}: | ${sat.uplink} |
` : ''}
- ${sat.tone ? `| ${t('station.settings.satellites.tone')}: | ${sat.tone} |
` : ''}
- | ${t('station.settings.satellites.status')}: |
-
- ${isVisible ? `${t('station.settings.satellites.visible')}` : `${t('station.settings.satellites.belowHorizon')}`}
- |
+
+ | ${t('station.settings.satellites.status')}: |
+ ${isVisible ? `${t('station.settings.satellites.visible')}` : `${t('station.settings.satellites.belowHorizon')}`} |
+
+
+
+
+ | ${t('station.settings.satellites.mode')}: |
+ ${sat.mode || 'N/A'} |
-
- ${sat.notes ? `
${sat.notes}
` : ''}
-
+ ${sat.downlink ? `
| ${t('station.settings.satellites.downlink')}: | ${sat.downlink} |
` : ''}
+ ${sat.uplink ? `
| ${t('station.settings.satellites.uplink')}: | ${sat.uplink} |
` : ''}
+ ${sat.tone ? `
| ${t('station.settings.satellites.tone')}: | ${sat.tone} |
` : ''}
+
+
|
+
+ |
+
+
+
+ ${sat.notes ? `
${sat.notes}
` : ''}
+
`;
})
.join('') +
@@ -302,7 +374,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
getIsMinimized: () => winMinimized,
onToggle: setWinMinimized,
persist: false,
- manageButtonEvents: false,
+ manageButtonEvents: true,
});
};
@@ -312,6 +384,9 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
if (!satellites || satellites.length === 0) return;
const globalOpacity = opacity !== undefined ? opacity : 1.0;
+ const accentCyan = getComputedStyle(document.documentElement).getPropertyValue('--accent-cyan');
+ const accentGreen = getComputedStyle(document.documentElement).getPropertyValue('--accent-green');
+ const accentLeadTrack = getComputedStyle(document.documentElement).getPropertyValue('--accent-amber');
satellites.forEach((sat) => {
const isSelected = selectedSats.includes(sat.name);
@@ -320,7 +395,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
const EARTH_RADIUS = 6371;
const centralAngle = Math.acos(EARTH_RADIUS / (EARTH_RADIUS + sat.alt));
const footprintRadiusMeters = centralAngle * EARTH_RADIUS * 1000;
- const footColor = sat.isVisible === true ? '#00ff00' : '#00ffff';
+ const footColor = sat.isVisible === true ? accentGreen : accentCyan;
replicatePoint(sat.lat, sat.lon).forEach((pos) => {
window.L.circle(pos, {
@@ -342,14 +417,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
for (let i = 0; i < coords.length - 1; i++) {
const fade = i / coords.length;
window.L.polyline([coords[i], coords[i + 1]], {
- color: '#00ffff',
+ color: accentCyan,
weight: 6,
opacity: fade * 0.3 * globalOpacity,
lineCap: 'round',
interactive: false,
}).addTo(layerGroupRef.current);
window.L.polyline([coords[i], coords[i + 1]], {
- color: '#ffffff',
+ color: 'rgba(255, 255, 255, 1)',
weight: 2,
opacity: fade * globalOpacity,
lineCap: 'round',
@@ -358,7 +433,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
}
} else {
window.L.polyline(coords, {
- color: '#00ffff',
+ color: accentCyan,
weight: 1,
opacity: 0.15 * globalOpacity,
dashArray: '5, 10',
@@ -371,7 +446,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
const leadCoords = sat.leadTrack.map((p) => [p[0], p[1]]);
replicatePath(leadCoords).forEach((lCoords) => {
window.L.polyline(lCoords, {
- color: '#ffff00',
+ color: accentLeadTrack,
weight: 3,
opacity: 0.8 * globalOpacity,
dashArray: '8, 12',
@@ -387,8 +462,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
icon: window.L.divIcon({
className: 'sat-marker',
html: `
-
🛰
-
${sat.name}
+
🛰
+
${sat.name}
`,
iconSize: [80, 50],
iconAnchor: [40, 25],
@@ -419,7 +494,22 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
} else {
layerGroupRef.current.clearLayers();
const win = document.getElementById('sat-data-window');
- if (win) win.remove();
+ if (win) {
+ // Clean up listeners before removing window
+ if (winListenersRef.current) {
+ const { mouseDownHandler, mouseMoveHandler, mouseUpHandler, wheelHandler, propagationHandler } =
+ winListenersRef.current;
+ win.removeEventListener('mousedown', mouseDownHandler);
+ window.removeEventListener('mousemove', mouseMoveHandler, { capture: true });
+ window.removeEventListener('mouseup', mouseUpHandler, { capture: true });
+ win.removeEventListener('wheel', wheelHandler);
+ win.removeEventListener('mousemove', propagationHandler.mousemove);
+ win.removeEventListener('mousedown', propagationHandler.mousedown);
+ win.removeEventListener('mouseup', propagationHandler.mouseup);
+ winListenersRef.current = null;
+ }
+ win.remove();
+ }
}
}, [enabled, map, config]);
@@ -427,5 +517,296 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
if (enabled) renderSatellites();
}, [satellites, selectedSats, allUnits, opacity, config, winMinimized]);
+ // Delegated click handling for window buttons
+ useEffect(() => {
+ if (!map) return;
+ const container = map.getContainer();
+
+ const handleClick = (e) => {
+ const actionEl = e.target.closest('[data-action]');
+ if (!actionEl || !container.contains(actionEl)) return;
+
+ const action = actionEl.dataset.action;
+
+ if (action === 'open-predict') {
+ e.stopPropagation();
+ e.preventDefault();
+ const name = actionEl.dataset.satName;
+ const tle1 = actionEl.dataset.tle1;
+ const tle2 = actionEl.dataset.tle2;
+ if (name && tle1 && tle2 && window.openSatellitePredict) {
+ window.openSatellitePredict(name, tle1, tle2);
+ }
+ return;
+ }
+
+ if (action === 'clear-all-satellites') {
+ e.stopPropagation();
+ e.preventDefault();
+ sessionStorage.removeItem('selected_satellites');
+ window.location.reload();
+ return;
+ }
+
+ if (action === 'toggle-satellite') {
+ e.stopPropagation();
+ e.preventDefault();
+ const name = actionEl.dataset.satName;
+ if (name) toggleSatellite(name);
+ return;
+ }
+ };
+
+ container.addEventListener('click', handleClick, true); // Use capture phase
+ return () => container.removeEventListener('click', handleClick, true);
+ }, [map, toggleSatellite, satellites]);
+
+ /********************************************************************************************/
+ // Expose satellite prediction panel function
+ useEffect(() => {
+ const openSatellitePredict = (satName, tle1, tle2) => {
+ if (!satName || !satellites) return;
+
+ // Find the satellite data
+ const sat = satellites.find((s) => s.name === satName);
+ if (!sat) {
+ alert(`Satellite ${satName} not found`);
+ return;
+ }
+
+ const orbit = new Orbit(sat.name, `${sat.name}\n${tle1}\n${tle2}`);
+ orbit.error && console.warn('Satellite orbit error:', orbit.error);
+
+ const groundStation = {
+ latitude: config?.location?.lat || 0.0,
+ longitude: config?.location?.lon || 0.0,
+ height: config?.location?.stationAlt || 100, // above sea level [m]
+ };
+
+ const startDate = new Date(); // from now
+ const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1000); // until 7 days from now
+ const minElevation = config?.satellite?.minElev || 5;
+ const maxPasses = 25;
+ const passes = orbit.computePassesElevation(groundStation, startDate, endDate, minElevation, maxPasses);
+
+ const modalId = 'satellite-predict-modal';
+
+ // Function to generate modal content
+ const generateModalContent = (currentPasses) => {
+ return `
+
+
🛰 ${satName}
+
Satellite Prediction Details
+
+
+
+
+
+
+ | Start |
+ Apex |
+ End |
+ Duration |
+
+
+ | Local Time |
+ From Now |
+ Az [°] |
+ Local Time |
+ Az [°] |
+ El [°] |
+ Local Time |
+ Az [°] |
+ [mins] |
+
+
+
+ ${currentPasses
+ .map((pass) => {
+ const azimuthStart = pass.azimuthStart.toFixed(0);
+ const azimuthApex = pass.azimuthApex.toFixed(0);
+ const azimuthEnd = pass.azimuthEnd.toFixed(0);
+ const maxElevation = pass.maxElevation.toFixed(0);
+ const durationMins = (pass.duration / 60000).toFixed(1);
+ const formatLocalTime = (ts) => {
+ const d = new Date(ts);
+ const pad = (n) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+ };
+ const startTime = formatLocalTime(pass.start);
+ const apexTime = formatLocalTime(pass.apex);
+ const endTime = formatLocalTime(pass.end);
+ const secsFromNow = Math.floor((pass.start - new Date()) / 1000);
+
+ const isVisibleNow = secsFromNow <= 0 && new Date() < new Date(pass.end);
+ const isPast = secsFromNow <= 0 && new Date() > new Date(pass.end);
+
+ if (isPast) {
+ return ``; // skip past passes
+ }
+
+ const timeFromNow = isVisibleNow
+ ? 'VISIBLE'
+ : secsFromNow > 3600
+ ? `+${String(Math.floor(secsFromNow / 3600)).padStart(2, '0')}:${String(Math.floor((secsFromNow % 3600) / 60)).padStart(2, '0')}:${String(secsFromNow % 60).padStart(2, '0')}`
+ : secsFromNow > 60
+ ? `+00:${String(Math.floor(secsFromNow / 60)).padStart(2, '0')}:${String(secsFromNow % 60).padStart(2, '0')}`
+ : `+00:00:${String(secsFromNow).padStart(2, '0')}`;
+
+ return `
+ | ${startTime} |
+ ${timeFromNow} |
+ ${azimuthStart} |
+ ${apexTime} |
+ ${azimuthApex} |
+ ${maxElevation} |
+ ${endTime} |
+ ${azimuthEnd} |
+ ${durationMins} |
+
`;
+ })
+ .join('')}
+
+
+
+
+
+
+
+ `;
+ };
+
+ // Create a modal overlay
+ let modal = document.getElementById(modalId);
+
+ if (modal) {
+ modal.remove();
+ }
+
+ // Create modal elements
+ modal = document.createElement('div');
+ modal.id = modalId;
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: var(--bg-primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ `;
+
+ const content = document.createElement('div');
+ content.style.cssText = `
+ background: var(--bg-primary);
+ border: 2px solid var(--accent-red);
+ border-radius: 8px;
+ padding: 20px;
+ min-width: 50vw;
+ max-width: 95vw;
+ min-height: 25vh;
+ max-height: 90vh;
+ overflow-y: auto;
+ overflow-x: auto;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
+ font-family: 'JetBrains Mono', monospace;
+ color: var(--text-primary);
+ `;
+
+ content.innerHTML = generateModalContent(passes);
+
+ modal.appendChild(content);
+ document.body.appendChild(modal);
+
+ // Named function so it can be removed later
+ const handleModalClick = (e) => {
+ if (e.target === modal) {
+ closeModal();
+ }
+ };
+
+ const currentStartDate = new Date();
+ const currentEndDate = new Date(currentStartDate.getTime() + 7 * 24 * 60 * 60 * 1000);
+ const currentPasses = orbit.computePassesElevation(
+ groundStation,
+ currentStartDate,
+ currentEndDate,
+ minElevation,
+ maxPasses,
+ );
+
+ // update modal every second, satellite data currentPasses is not updated unless modal is reopened,
+ // or if satellite layer is updated for instance if TLE data changes
+ const updatePasses = () => {
+ content.innerHTML = generateModalContent(currentPasses);
+ };
+
+ const closeModal = () => {
+ // Clean up all event listeners before removing modal
+ content.removeEventListener('click', handleContentClick);
+ modal.removeEventListener('click', handleModalClick);
+ document.removeEventListener('keydown', handleKeyDown);
+
+ modal.remove();
+ if (window.satellitePredictInterval) {
+ clearInterval(window.satellitePredictInterval);
+ }
+ };
+
+ // Use event delegation for close button so it works after HTML regeneration
+ const handleContentClick = (e) => {
+ if (e.target.matches('[data-action="close-predict-modal"]')) {
+ closeModal();
+ }
+ };
+
+ if (window.satellitePredictInterval) {
+ clearInterval(window.satellitePredictInterval);
+ }
+
+ window.satellitePredictInterval = setInterval(updatePasses, 1000); // one second
+
+ // Close on backdrop click
+ modal.addEventListener('click', handleModalClick);
+
+ // Close on Escape key
+ const handleKeyDown = (e) => {
+ if (e.key === 'Escape') {
+ closeModal();
+ }
+ };
+ document.addEventListener('keydown', handleKeyDown);
+
+ // Wire close button using event delegation (one listener for all updates)
+ content.addEventListener('click', handleContentClick);
+ };
+
+ // expose for other callers if needed
+ window.openSatellitePredict = openSatellitePredict;
+
+ // Cleanup: remove the global reference when effect re-runs or component unmounts
+ return () => {
+ delete window.openSatellitePredict;
+ };
+ }, [satellites, config]);
+ /********************************************************************************************/
+
return null;
};
diff --git a/src/utils/config.js b/src/utils/config.js
index 0da8204c..62c81045 100644
--- a/src/utils/config.js
+++ b/src/utils/config.js
@@ -20,7 +20,8 @@ export const DEFAULT_CONFIG = {
callsign: 'N0CALL',
headerSize: 1.0, // Float multiplies base px size (0.1 to 2.0)
locator: '',
- location: { lat: 40.015, lon: -105.2705 }, // Boulder, CO (default)
+ location: { lat: 40.015, lon: -105.2705, stationAlt: 1630 }, // Boulder, CO (default), altitude [m]
+ satellite: { minElev: 5 }, // Minimum elevation for satellite visibility (degrees)
defaultDX: { lat: 35.6762, lon: 139.6503 }, // Tokyo
units: 'imperial', // 'imperial' or 'metric'
allUnits: { dist: 'imperial', temp: 'imperial', press: 'imperial' },
diff --git a/src/utils/orbit.js b/src/utils/orbit.js
new file mode 100644
index 00000000..9d2019dc
--- /dev/null
+++ b/src/utils/orbit.js
@@ -0,0 +1,222 @@
+/*
+https://github.com/Flowm/satvis/blob/next/src/modules/Orbit.js
+
+MIT License
+
+Copyright (c) 2018 Florian Mauracher
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import * as satellitejs from 'satellite.js';
+
+const enableLogging = false; // Set to true to enable detailed logging of the pass calculation process
+const deg2rad = Math.PI / 180;
+const rad2deg = 180 / Math.PI;
+
+export default class Orbit {
+ constructor(name, tle) {
+ this.name = name;
+ this.tle = tle.split('\n');
+ this.satrec = satellitejs.twoline2satrec(this.tle[1], this.tle[2]);
+ }
+
+ get satnum() {
+ return this.satrec.satnum;
+ }
+
+ get error() {
+ return this.satrec.error;
+ }
+
+ get julianDate() {
+ return this.satrec.jdsatepoch;
+ }
+
+ get orbitalPeriod() {
+ const meanMotionRad = this.satrec.no;
+ const period = (2 * Math.PI) / meanMotionRad;
+ return period;
+ }
+
+ positionECI(time) {
+ const result = satellitejs.propagate(this.satrec, time);
+ return result ? result.position : null;
+ }
+
+ positionECF(time) {
+ const positionEci = this.positionECI(time);
+ if (!positionEci) return null;
+ const gmst = satellitejs.gstime(time);
+ const positionEcf = satellitejs.eciToEcf(positionEci, gmst);
+ return positionEcf;
+ }
+
+ positionGeodetic(timestamp, calculateVelocity = false) {
+ const result = satellitejs.propagate(this.satrec, timestamp);
+ if (!result) return null;
+ const { position: positionEci, velocity: velocityVector } = result;
+ const gmst = satellitejs.gstime(timestamp);
+ const positionGd = satellitejs.eciToGeodetic(positionEci, gmst);
+
+ return {
+ longitude: positionGd.longitude * rad2deg,
+ latitude: positionGd.latitude * rad2deg,
+ height: positionGd.height * 1000,
+ ...(calculateVelocity && {
+ velocity: Math.sqrt(
+ velocityVector.x * velocityVector.x +
+ velocityVector.y * velocityVector.y +
+ velocityVector.z * velocityVector.z,
+ ),
+ }),
+ };
+ }
+
+ computePassesElevation(
+ groundStationPosition,
+ startDate = new Date(),
+ endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1000), // 7 days later
+ minElevation = 5,
+ maxPasses = 50,
+ ) {
+ const groundStation = { ...groundStationPosition };
+ groundStation.latitude *= deg2rad;
+ groundStation.longitude *= deg2rad;
+ groundStation.height /= 1000;
+
+ const date = new Date(startDate);
+ const passes = [];
+ let pass = false;
+ let ongoingPass = false;
+ let lastElevation = null;
+ // eslint-disable-next-line no-unmodified-loop-condition -- date is mutated via setMinutes/setSeconds
+ while (date < endDate) {
+ const positionEcf = this.positionECF(date);
+ if (!positionEcf) {
+ date.setMinutes(date.getMinutes() + 1);
+ continue;
+ }
+ const lookAngles = satellitejs.ecfToLookAngles(groundStation, positionEcf);
+ const elevation = lookAngles.elevation / deg2rad;
+ if (enableLogging) {
+ let logMessage = '[Satellite] **********************';
+ logMessage += '\n Date: ' + date.toISOString();
+ logMessage += '\n Elevation (calculated): ' + elevation.toFixed(2) + ' degrees';
+ }
+
+ if (elevation > minElevation) {
+ // satellite is above minimum elevation, part of a pass
+
+ if (!ongoingPass) {
+ // Start of new pass
+ if (enableLogging) {
+ logMessage += '\n NEW PASS STARTED';
+ }
+ pass = {
+ name: this.name,
+ start: date.getTime(),
+ azimuthStart: lookAngles.azimuth,
+ maxElevation: elevation,
+ apex: date.getTime(),
+ azimuthApex: lookAngles.azimuth,
+ };
+ ongoingPass = true;
+ } else if (elevation > pass.maxElevation) {
+ // Ongoing pass, update max elevation and apex time
+ if (enableLogging) {
+ logMessage += '\n RISING Ongoing pass, new max elevation: ' + elevation.toFixed(2) + ' degrees';
+ }
+ pass.maxElevation = elevation;
+ pass.apex = date.getTime();
+ pass.azimuthApex = lookAngles.azimuth;
+ }
+
+ // advance 5s in next iteration
+ date.setSeconds(date.getSeconds() + 5);
+ if (enableLogging) {
+ logMessage += '\n Advancing time by 5 seconds for next calculation';
+ }
+ } else if (ongoingPass) {
+ // End of pass
+ if (enableLogging) {
+ logMessage += '\n END OF PASS';
+ }
+ pass.end = date.getTime();
+ pass.duration = pass.end - pass.start;
+ pass.azimuthEnd = lookAngles.azimuth;
+ pass.azimuthStart /= deg2rad;
+ pass.azimuthApex /= deg2rad;
+ pass.azimuthEnd /= deg2rad;
+ passes.push(pass);
+ if (passes.length > maxPasses) {
+ break;
+ }
+ ongoingPass = false;
+ lastElevation = null;
+ date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.5); // skip ahead to next potential pass
+ if (enableLogging) {
+ logMessage += '\n Advancing by half the orbital period';
+ }
+ } else {
+ // satellite is below minimum elevation and not currently in a pass
+ const deltaElevation = elevation - (lastElevation || elevation); // if lastElevation is null then delta will be zero, which will not trigger the descending logic
+ lastElevation = elevation;
+ if (deltaElevation < 0) {
+ // deltaElevation is negative, satellite is descending, skip ahead to speed up calculation
+ lastElevation = null;
+ date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.5); // skip ahead to next potential pass
+ if (enableLogging) {
+ logMessage += '\n Delta is negative, advancing time by half orbital period';
+ }
+ } else if (elevation < -60) {
+ date.setMinutes(date.getMinutes() + 15);
+ if (enableLogging) {
+ logMessage += '\n Elevation is very low, advancing time by 15 minutes';
+ }
+ } else if (elevation < -30) {
+ date.setMinutes(date.getMinutes() + 5);
+ if (enableLogging) {
+ logMessage += '\n Elevation is low, advancing time by 5 minutes';
+ }
+ } else if (elevation < -10) {
+ date.setMinutes(date.getMinutes() + 1);
+ if (enableLogging) {
+ logMessage += '\n Elevation is slightly lower than minimum elevation, advancing time by 1 minute';
+ }
+ } else if (elevation < minElevation - 3) {
+ date.setSeconds(date.getSeconds() + 30);
+ if (enableLogging) {
+ logMessage += '\n Elevation is close to minimum elevation, advancing time by 30 seconds';
+ }
+ } else {
+ date.setSeconds(date.getSeconds() + 5);
+ if (enableLogging) {
+ logMessage += '\n Advancing time by 5 seconds';
+ }
+ }
+
+ if (enableLogging) {
+ console.log(logMessage);
+ }
+ }
+ }
+ return passes;
+ }
+}