From 2092e2ca6fc853d97a84a8fd9bd33c3c3892d46a Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Thu, 2 Apr 2026 18:50:34 -0700 Subject: [PATCH 01/17] add station altitude, minimum elevation settings pass tle data from useSatellites.js --- src/App.jsx | 2 +- src/components/SettingsPanel.jsx | 76 +++++++++++++++++++++++++++++++- src/hooks/useSatellites.js | 10 +++-- 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 74dca8ee..41c3099d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -320,7 +320,7 @@ const App = () => { const propagation = usePropagation(config.location, dxLocation, config.propagation); const mySpots = useMySpots(config.callsign); - const satellites = useSatellites(config.location); + const satellites = useSatellites(config.location, config.satellite); const localWeather = useWeather(config.location, config.allUnits); const dxWeather = useWeather(dxLocation, config.allUnits); const localAlerts = useWeatherAlerts(config.location); diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index fccd37ac..5d636c2a 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -49,6 +49,8 @@ export const SettingsPanel = ({ const [gridSquare, setGridSquare] = useState(config?.locator || ''); const [lat, setLat] = useState(config?.location?.lat ?? 0); const [lon, setLon] = useState(config?.location?.lon ?? 0); + const [stationAlt, setStationAlt] = useState(config?.location?.stationAlt ?? 100); + const [minElev, setMinElev] = useState(config?.satellite?.minElev ?? 5.0); const [layout, setLayout] = useState(config?.layout || 'modern'); const [mouseZoom, setMouseZoom] = useState(config?.mouseZoom || 50); const [timezone, setTimezone] = useState(config?.timezone || ''); @@ -178,6 +180,8 @@ export const SettingsPanel = ({ setheaderSize(config.headerSize || 1.0); setLat(config.location?.lat ?? 0); setLon(config.location?.lon ?? 0); + setStationAlt(config.location?.stationAlt ?? 100); + setMinElev(config.satellite?.minElev ?? 5.0); setLayout(config.layout || 'modern'); setMouseZoom(config.mouseZoom || 50); setTimezone(config.timezone || ''); @@ -417,7 +421,8 @@ export const SettingsPanel = ({ headerSize: headerSize, swapHeaderClocks, showMutualReception, - location: { lat: parseFloat(lat), lon: parseFloat(lon) }, + location: { lat: parseFloat(lat) || 0, lon: parseFloat(lon) || 0, stationAlt: parseInt(stationAlt) || 100 }, + satellite: { minElev: parseFloat(minElev) || 5.0 }, theme, customTheme, layout, @@ -3294,6 +3299,75 @@ export const SettingsPanel = ({ Footprints + + {/* station altitude and minimum elevation inputs */} +
+
+ + setStationAlt(parseInt(e.target.value) || 100)} + style={{ + width: '100%', + padding: '10px', + background: 'var(--bg-tertiary)', + border: '1px solid var(--border-color)', + borderRadius: '6px', + color: 'var(--text-primary)', + fontSize: '14px', + fontFamily: 'JetBrains Mono, monospace', + boxSizing: 'border-box', + }} + /> +
+
+ + setMinElev(parseFloat(e.target.value) || 5.0)} + style={{ + width: '100%', + padding: '10px', + background: 'var(--bg-tertiary)', + border: '1px solid var(--border-color)', + borderRadius: '6px', + color: 'var(--text-primary)', + fontSize: '14px', + fontFamily: 'JetBrains Mono, monospace', + boxSizing: 'border-box', + }} + /> +
+
+ {/* Lead Time Slider WIP
+ + +
- + +
${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.speed')}:${speedStr}
+ + + ${ @@ -279,17 +286,24 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con : `` } - - ${sat.downlink ? `` : ''} - ${sat.uplink ? `` : ''} - ${sat.tone ? `` : ''} -
${t('station.settings.satellites.azimuth_elevation')}:${sat.azimuth}° / ${sat.elevation}°
${t('station.settings.satellites.mode')}:${sat.mode || 'N/A'}
${t('station.settings.satellites.downlink')}:${sat.downlink}
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
${t('station.settings.satellites.status')}: ${isVisible ? `${t('station.settings.satellites.visible')}` : `${t('station.settings.satellites.belowHorizon')}`}
- ${sat.notes ? `
${sat.notes}
` : ''} + + + + + + ${sat.downlink ? `` : ''} + ${sat.uplink ? `` : ''} + ${sat.tone ? `` : ''} +
${t('station.settings.satellites.mode')}:${sat.mode || 'N/A'}
${t('station.settings.satellites.downlink')}:${sat.downlink}
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
+ + + + ${sat.notes ? `
${sat.notes}
` : ''} `; }) From 61c147810464567fa99aa4bddbde13cd6d2483fa Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Fri, 3 Apr 2026 14:40:27 -0700 Subject: [PATCH 06/17] minor correction to input variables --- src/components/SettingsPanel.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index eabc9f03..34b58588 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -3386,7 +3386,7 @@ export const SettingsPanel = ({ type="number" step="1" value={isNaN(stationAlt) ? '' : stationAlt} - onChange={(e) => setStationAlt(parseInt(e.target.value) || 100)} + onChange={(e) => setStationAlt(e.target.valueAsNumber ?? 100)} style={{ width: '100%', padding: '10px', @@ -3418,7 +3418,7 @@ export const SettingsPanel = ({ min="-89.0" max="89.0" value={isNaN(minElev) ? '' : minElev} - onChange={(e) => setMinElev(parseFloat(e.target.value) || 5.0)} + onChange={(e) => setMinElev(e.target.valueAsNumber ?? 5.0)} style={{ width: '100%', padding: '10px', From 2df9780c113b077fe2008b40416a835f35d9ea27 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sat, 4 Apr 2026 10:01:38 -0700 Subject: [PATCH 07/17] style highliting green background when satellite is visible --- src/plugins/layers/useSatelliteLayer.js | 65 +++++++++++++++++-------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 0a916a5d..20b39395 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -197,7 +197,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con @@ -258,47 +258,70 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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.altitude')}:${altitudeStr}
${t('station.settings.satellites.speed')}:${speedStr}
+ + + + + + + + + + + + + + + +
${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.azimuth_elevation')}:${sat.azimuth}° / ${sat.elevation}°
${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.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')}`}
- - - ${sat.downlink ? `` : ''} - ${sat.uplink ? `` : ''} - ${sat.tone ? `` : ''} +
${t('station.settings.satellites.mode')}:${sat.mode || 'N/A'}
${t('station.settings.satellites.downlink')}:${sat.downlink}
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
+ + + + + ${sat.downlink ? `` : ''} + ${sat.uplink ? `` : ''} + ${sat.tone ? `` : ''}
${t('station.settings.satellites.mode')}:${sat.mode || 'N/A'}
${t('station.settings.satellites.downlink')}:${sat.downlink}
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
From 0ecd4b49f1b81a8b82be506bd1c4385f8fdbd406 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sat, 4 Apr 2026 15:27:44 -0700 Subject: [PATCH 08/17] orbit predict --- package-lock.json | 7 + package.json | 1 + src/plugins/layers/satelliteOrbit.js | 265 ++++++++++++++++++++++++ src/plugins/layers/useSatelliteLayer.js | 183 ++++++++++++++++ 4 files changed, 456 insertions(+) create mode 100644 src/plugins/layers/satelliteOrbit.js diff --git a/package-lock.json b/package-lock.json index 31a3d8b4..b120a6ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.6.2", "compression": "^1.7.4", "cors": "^2.8.5", + "dayjs": "^1.11.20", "dotenv": "^16.3.1", "express": "^4.21.2", "express-rate-limit": "^7.5.0", @@ -3901,6 +3902,12 @@ "node": ">=18" } }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index fd230181..777d7f44 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "axios": "^1.6.2", "compression": "^1.7.4", "cors": "^2.8.5", + "dayjs": "^1.11.20", "dotenv": "^16.3.1", "express": "^4.21.2", "express-rate-limit": "^7.5.0", diff --git a/src/plugins/layers/satelliteOrbit.js b/src/plugins/layers/satelliteOrbit.js new file mode 100644 index 00000000..e9daf1e2 --- /dev/null +++ b/src/plugins/layers/satelliteOrbit.js @@ -0,0 +1,265 @@ +/* +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'; +import dayjs from 'dayjs'; + +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 = dayjs().toDate(), + endDate = dayjs(startDate).add(7, 'day').toDate(), + 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 = 0; + // 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 (elevation > minElevation) { + if (!ongoingPass) { + // Start of new pass + pass = { + name: this.name, + start: date.getTime(), + azimuthStart: lookAngles.azimuth, + maxElevation: elevation, + azimuthApex: lookAngles.azimuth, + }; + ongoingPass = true; + } else if (elevation > pass.maxElevation) { + // Ongoing pass + pass.maxElevation = elevation; + pass.apex = date.getTime(); + pass.azimuthApex = lookAngles.azimuth; + } + date.setSeconds(date.getSeconds() + 5); + } else if (ongoingPass) { + // 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 = -180; + date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.5); + } else { + const deltaElevation = elevation - lastElevation; + lastElevation = elevation; + if (deltaElevation < 0) { + date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.5); + lastElevation = -180; + } else if (elevation < -20) { + date.setMinutes(date.getMinutes() + 5); + } else if (elevation < -5) { + date.setMinutes(date.getMinutes() + 1); + } else if (elevation < -1) { + date.setSeconds(date.getSeconds() + 5); + } else { + date.setSeconds(date.getSeconds() + 2); + } + } + } + return passes; + } + + computePassesSwath( + groundStationPosition, + swathKm, + startDate = dayjs().toDate(), + endDate = dayjs(startDate).add(7, 'day').toDate(), + 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 lastDistance = Number.MAX_VALUE; + + // eslint-disable-next-line no-unmodified-loop-condition -- date is mutated via setMinutes/setSeconds + while (date < endDate) { + const positionGeodetic = this.positionGeodetic(date); + if (!positionGeodetic) { + date.setMinutes(date.getMinutes() + 1); + continue; + } + + // Convert satellite position to radians for calculations + const satLat = positionGeodetic.latitude * deg2rad; + const satLon = positionGeodetic.longitude * deg2rad; + + // Calculate great circle distance between satellite and ground station + const deltaLat = satLat - groundStation.latitude; + const deltaLon = satLon - groundStation.longitude; + const a = + Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + + Math.cos(groundStation.latitude) * Math.cos(satLat) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const earthRadius = 6371; // Earth radius in km + const distanceKm = earthRadius * c; + + // Check if ground station is within swath + const halfSwath = swathKm / 2; + const withinSwath = distanceKm <= halfSwath; + + if (withinSwath) { + if (!ongoingPass) { + // Start of new pass + pass = { + name: this.name, + start: date.getTime(), + minDistance: distanceKm, + minDistanceTime: date.getTime(), + swathWidth: swathKm, + }; + ongoingPass = true; + } else if (distanceKm < pass.minDistance) { + // Update minimum distance (closest approach) + pass.minDistance = distanceKm; + pass.minDistanceTime = date.getTime(); + } + date.setSeconds(date.getSeconds() + 30); // 30 second steps during pass + } else if (ongoingPass) { + // End of pass + pass.end = date.getTime(); + pass.duration = pass.end - pass.start; + passes.push(pass); + if (passes.length >= maxPasses) { + break; + } + ongoingPass = false; + lastDistance = Number.MAX_VALUE; + // Skip ahead to avoid immediate re-entry + date.setMinutes(date.getMinutes() + Math.max(5, this.orbitalPeriod * 0.1)); + } else { + // Not in pass, adjust time step based on distance and previous distance + const deltaDistance = distanceKm - lastDistance; + lastDistance = distanceKm; + + if (deltaDistance > 0 && distanceKm > halfSwath * 3) { + // Moving away and far from swath, skip ahead more + date.setMinutes(date.getMinutes() + Math.max(10, this.orbitalPeriod * 0.2)); + } else if (distanceKm > halfSwath * 2) { + // Moderately far from swath + date.setMinutes(date.getMinutes() + 5); + } else { + // Getting closer to swath, use smaller time steps + date.setMinutes(date.getMinutes() + 1); + } + } + } + + return passes; + } +} diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 20b39395..ac45d5ef 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next'; import * as satellite from 'satellite.js'; import { addMinimizeToggle } from './addMinimizeToggle.js'; import { replicatePoint, replicatePath } from '../../utils/geo.js'; +import Orbit from './satelliteOrbit.js'; +import dayjs from 'dayjs'; export const metadata = { id: 'satellites', @@ -258,7 +260,10 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con return `
+
@@ -464,5 +469,183 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con if (enabled) renderSatellites(); }, [satellites, selectedSats, allUnits, opacity, config, winMinimized]); + /********************************************************************************************/ + // Expose satellite prediction panel function + useEffect(() => { + window.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; + } + + console.log('[Satellite] found satellite for prediction:', sat.name); + const orbit = new Orbit(sat.name, `${sat.name}\n${tle1}\n${tle2}`); + orbit.error && console.warn('Satellite orbit error:', orbit.error); + console.log('[Satellite] created orbit object:', orbit); + + const groundStation = { + latitude: 32.895, + longitude: -117.125, + height: 231, + }; + const startDate = dayjs().toDate(); + const endDate = dayjs(startDate).add(7, 'day').toDate(); + const minElevation = 20; + const maxPasses = 50; + const passes = orbit.computePassesElevation(groundStation, startDate, endDate, minElevation, maxPasses); + console.log(`[Satellite] computed ${passes.length} passes for ${sat.name}`); + + passes.forEach((pass, index) => { + const azimuthStart = pass.azimuthStart.toFixed(1); + const azimuthApex = pass.azimuthApex.toFixed(1); + const azimuthEnd = pass.azimuthEnd.toFixed(1); + const maxElevation = pass.maxElevation.toFixed(1); + const durationMins = (pass.duration / 60000).toFixed(1); + const startTime = dayjs(pass.start).format('YYYY-MM-DD HH:mm:ss'); + const apexTime = dayjs(pass.apex).format('YYYY-MM-DD HH:mm:ss'); + const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); + + console.log( + `[Satellite] Pass ${index + 1}: ${sat.name} - Start: ${startTime} (Az: ${azimuthStart}°), Apex: ${apexTime} (Az: ${azimuthApex}°, El: ${maxElevation}°), End: ${endTime} (Az: ${azimuthEnd}°), Duration: ${durationMins} mins`, + ); + }); + + // Create a modal overlay + const modalId = 'satellite-predict-modal'; + 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: rgba(0, 0, 0, 0.6); + 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; + max-width: 95vw; + width: 50vw; + 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); + `; + + const isMetric = allUnits.dist === 'metric'; + const distanceUnitsStr = isMetric ? 'km' : 'miles'; + const km_to_miles_factor = 0.621371; + + let altitude = Math.round(sat.alt * (isMetric ? 1 : km_to_miles_factor)); + let altitudeStr = sat.alt ? `${altitude.toLocaleString()} ${distanceUnitsStr}` : 'N/A'; + + content.innerHTML = ` +
+

🛰 ${satName}

+

Satellite Prediction Details

+
+ +
+

Upcoming Passes

+ + + + + + + + + + + + + + + ${passes + .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 startTime = dayjs(pass.start).format('YYYY-MM-DD HH:mm:ss'); + const apexTime = dayjs(pass.apex).format('YYYY-MM-DD HH:mm:ss'); + const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); + return ` + + + + + + + + + `; + }) + .join('')} + +
Start TimeAz Start [°]Apex TimeAz Apex [°]El Apex [°]End TimeAz End [°]Duration [mins]
${startTime}${azimuthStart}${apexTime}${azimuthApex}${maxElevation}${endTime}${azimuthEnd}${durationMins}
+
+ +
+ +
+ `; + + modal.appendChild(content); + document.body.appendChild(modal); + + // Close on backdrop click + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.remove(); + } + }); + + // Close on Enter or Escape key + const handleKeyDown = (e) => { + if (e.key === 'Enter' || e.key === 'Escape') { + modal.remove(); + document.removeEventListener('keydown', handleKeyDown); + } + }; + document.addEventListener('keydown', handleKeyDown); + }; + }, [satellites, allUnits]); + /********************************************************************************************/ + return null; }; From e1c967b439d385dac260b5681284e938b7271f17 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sat, 4 Apr 2026 16:57:01 -0700 Subject: [PATCH 09/17] adjust table columns --- src/plugins/layers/useSatelliteLayer.js | 49 ++++++++++--------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index ac45d5ef..5147f859 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -488,32 +488,17 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con console.log('[Satellite] created orbit object:', orbit); const groundStation = { - latitude: 32.895, + latitude: 32.896, longitude: -117.125, - height: 231, + height: 0, }; - const startDate = dayjs().toDate(); - const endDate = dayjs(startDate).add(7, 'day').toDate(); - const minElevation = 20; - const maxPasses = 50; + const startDate = dayjs().toDate(); // from now + const endDate = dayjs(startDate).add(7, 'day').toDate(); // until 7 days from now + const minElevation = 0; + const maxPasses = 25; const passes = orbit.computePassesElevation(groundStation, startDate, endDate, minElevation, maxPasses); console.log(`[Satellite] computed ${passes.length} passes for ${sat.name}`); - passes.forEach((pass, index) => { - const azimuthStart = pass.azimuthStart.toFixed(1); - const azimuthApex = pass.azimuthApex.toFixed(1); - const azimuthEnd = pass.azimuthEnd.toFixed(1); - const maxElevation = pass.maxElevation.toFixed(1); - const durationMins = (pass.duration / 60000).toFixed(1); - const startTime = dayjs(pass.start).format('YYYY-MM-DD HH:mm:ss'); - const apexTime = dayjs(pass.apex).format('YYYY-MM-DD HH:mm:ss'); - const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); - - console.log( - `[Satellite] Pass ${index + 1}: ${sat.name} - Start: ${startTime} (Az: ${azimuthStart}°), Apex: ${apexTime} (Az: ${azimuthApex}°, El: ${maxElevation}°), End: ${endTime} (Az: ${azimuthEnd}°), Duration: ${durationMins} mins`, - ); - }); - // Create a modal overlay const modalId = 'satellite-predict-modal'; let modal = document.getElementById(modalId); @@ -572,14 +557,20 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con - - - - - - - - + + + + + + + + + + + + + + From 6603c7b76ed3e0a82e8afa311d6faac108cc36fa Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sat, 4 Apr 2026 17:05:10 -0700 Subject: [PATCH 10/17] make satellite details with scrollable by blocking propagation of mouse to main map --- src/plugins/layers/useSatelliteLayer.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 5147f859..a5b0a01e 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -179,6 +179,20 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con }); } }; + + // Prevent map from capturing events on the window + win.addEventListener('wheel', (e) => { + e.stopPropagation(); + }); + win.addEventListener('mousedown', (e) => { + e.stopPropagation(); + }); + win.addEventListener('mousemove', (e) => { + e.stopPropagation(); + }); + win.addEventListener('mouseup', (e) => { + e.stopPropagation(); + }); } win.style.top = `${winPos.top}px`; From 67b3029f93b9ed9b906b1e985f2934cf4cb01f6b Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sat, 4 Apr 2026 17:12:33 -0700 Subject: [PATCH 11/17] add time from now column --- src/plugins/layers/useSatelliteLayer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index a5b0a01e..dd08a719 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -571,13 +571,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
Start TimeAz Start [°]Apex TimeAz Apex [°]El Apex [°]End TimeAz End [°]Duration [mins]StartApexEndDuration
TimeAz [°]TimeAz [°]El [°]TimeAz [°][mins]
- + + @@ -598,8 +599,12 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const startTime = dayjs(pass.start).format('YYYY-MM-DD HH:mm:ss'); const apexTime = dayjs(pass.apex).format('YYYY-MM-DD HH:mm:ss'); const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); + const minsFromNow = dayjs(pass.start).diff(dayjs(), 'minutes'); + const timeFromNow = + minsFromNow > 60 ? `${Math.floor(minsFromNow / 60)}hr ${minsFromNow % 60}min` : `${minsFromNow}m`; return ` + From ed9cb3f299a1b8c9d6fbcad3c8a3a1e12fcaf700 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sat, 4 Apr 2026 17:30:28 -0700 Subject: [PATCH 12/17] updates table every minute --- src/plugins/layers/useSatelliteLayer.js | 181 +++++++++++++----------- 1 file changed, 100 insertions(+), 81 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index dd08a719..e665d5ef 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -506,6 +506,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con longitude: -117.125, height: 0, }; + const startDate = dayjs().toDate(); // from now const endDate = dayjs(startDate).add(7, 'day').toDate(); // until 7 days from now const minElevation = 0; @@ -513,6 +514,86 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const passes = orbit.computePassesElevation(groundStation, startDate, endDate, minElevation, maxPasses); console.log(`[Satellite] computed ${passes.length} passes for ${sat.name}`); + // Function to generate modal content + const generateModalContent = (currentPasses) => { + return ` +
+

🛰 ${satName}

+

Satellite Prediction Details

+
+ +
+

Upcoming Passes

+
StartStart Apex End Duration
TimeFrom Now Az [°] Time Az [°]
${startTime}${timeFromNow} ${azimuthStart} ${apexTime} ${azimuthApex}
+ + + + + + + + + + + + + + + + + + + + + ${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 startTime = dayjs(pass.start).format('YYYY-MM-DD HH:mm:ss'); + const apexTime = dayjs(pass.apex).format('YYYY-MM-DD HH:mm:ss'); + const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); + const minsFromNow = dayjs(pass.start).diff(dayjs(), 'minutes'); + const timeFromNow = + minsFromNow > 60 + ? `${Math.floor(minsFromNow / 60)}hr ${minsFromNow % 60}min` + : `${minsFromNow}min`; + return ` + + + + + + + + + + `; + }) + .join('')} + +
StartApexEndDuration
TimeFrom NowAz [°]TimeAz [°]El [°]TimeAz [°][mins]
${startTime}${timeFromNow}${azimuthStart}${apexTime}${azimuthApex}${maxElevation}${endTime}${azimuthEnd}${durationMins}
+
+ +
+ +
+ `; + }; + // Create a modal overlay const modalId = 'satellite-predict-modal'; let modal = document.getElementById(modalId); @@ -553,95 +634,32 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con color: var(--text-primary); `; - const isMetric = allUnits.dist === 'metric'; - const distanceUnitsStr = isMetric ? 'km' : 'miles'; - const km_to_miles_factor = 0.621371; - - let altitude = Math.round(sat.alt * (isMetric ? 1 : km_to_miles_factor)); - let altitudeStr = sat.alt ? `${altitude.toLocaleString()} ${distanceUnitsStr}` : 'N/A'; - - content.innerHTML = ` -
-

🛰 ${satName}

-

Satellite Prediction Details

-
- -
-

Upcoming Passes

- - - - - - - - - - - - - - - - - - - - - - ${passes - .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 startTime = dayjs(pass.start).format('YYYY-MM-DD HH:mm:ss'); - const apexTime = dayjs(pass.apex).format('YYYY-MM-DD HH:mm:ss'); - const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); - const minsFromNow = dayjs(pass.start).diff(dayjs(), 'minutes'); - const timeFromNow = - minsFromNow > 60 ? `${Math.floor(minsFromNow / 60)}hr ${minsFromNow % 60}min` : `${minsFromNow}m`; - return ` - - - - - - - - - - `; - }) - .join('')} - -
StartApexEndDuration
TimeFrom NowAz [°]TimeAz [°]El [°]TimeAz [°][mins]
${startTime}${timeFromNow}${azimuthStart}${apexTime}${azimuthApex}${maxElevation}${endTime}${azimuthEnd}${durationMins}
-
- -
- -
- `; + content.innerHTML = generateModalContent(passes); modal.appendChild(content); document.body.appendChild(modal); + // Set up periodic updates every minute + const updatePasses = () => { + const currentStartDate = dayjs().toDate(); + const currentEndDate = dayjs(currentStartDate).add(7, 'day').toDate(); + const currentPasses = orbit.computePassesElevation( + groundStation, + currentStartDate, + currentEndDate, + minElevation, + maxPasses, + ); + content.innerHTML = generateModalContent(currentPasses); + }; + + window.satellitePredictInterval = setInterval(updatePasses, 60000); + // Close on backdrop click modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); + clearInterval(window.satellitePredictInterval); } }); @@ -649,6 +667,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const handleKeyDown = (e) => { if (e.key === 'Enter' || e.key === 'Escape') { modal.remove(); + clearInterval(window.satellitePredictInterval); document.removeEventListener('keydown', handleKeyDown); } }; From 52a5a0cd2d32f2cffcf2d0e2e6fa1aa8b27e9beb Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sat, 4 Apr 2026 21:34:49 -0700 Subject: [PATCH 13/17] wire global variables to satellite prediction --- src/plugins/layers/satelliteOrbit.js | 4 +-- src/plugins/layers/useSatelliteLayer.js | 33 ++++++++++++++----------- src/utils/config.js | 3 ++- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/plugins/layers/satelliteOrbit.js b/src/plugins/layers/satelliteOrbit.js index e9daf1e2..4fe89b8d 100644 --- a/src/plugins/layers/satelliteOrbit.js +++ b/src/plugins/layers/satelliteOrbit.js @@ -148,12 +148,12 @@ export default class Orbit { } ongoingPass = false; lastElevation = -180; - date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.5); + date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.05); // modified from original 0.5 value to make first pass calculation more reliable } else { const deltaElevation = elevation - lastElevation; lastElevation = elevation; if (deltaElevation < 0) { - date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.5); + date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.05); // modified from original 0.5 value to make first pass calculation more reliable lastElevation = -180; } else if (elevation < -20) { date.setMinutes(date.getMinutes() + 5); diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index e665d5ef..ec8a67b0 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -5,6 +5,7 @@ import { addMinimizeToggle } from './addMinimizeToggle.js'; import { replicatePoint, replicatePath } from '../../utils/geo.js'; import Orbit from './satelliteOrbit.js'; import dayjs from 'dayjs'; +import useAppConfig from '../../hooks/app/useAppConfig.js'; export const metadata = { id: 'satellites', @@ -25,6 +26,7 @@ export const metadata = { export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, config, allUnits }) => { const layerGroupRef = useRef(null); const { t } = useTranslation(); + const { config: globalConfig } = useAppConfig(); // 1. Multi-select state (Wipes on browser close) const [selectedSats, setSelectedSats] = useState(() => { @@ -49,19 +51,25 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con window.toggleSat = (name) => toggleSatellite(name); }, [selectedSats]); + // MRW note, much of the following code appears unused since satellite location is calculated in the useSatellites hook. + // I have commented it out and will leave for someone else to review and potentially remove. const fetchSatellites = async () => { try { const response = await fetch('/api/satellites/tle'); const data = await response.json(); + /* const observerGd = { latitude: satellite.degreesToRadians(config?.lat ?? 0.0), longitude: satellite.degreesToRadians(config?.lon ?? 0.0), height: (config?.stationAlt || 100) / 1000, // above sea level [km], stationAlt is [m], defaults to 100m }; + */ const satArray = Object.keys(data).map((name) => { const satData = data[name]; + + /* let isVisible = false; let az = 0, el = 0, @@ -94,16 +102,16 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con leadTrack.push([satellite.degreesLat(geodetic.latitude), satellite.degreesLong(geodetic.longitude)]); } } - } + }*/ return { ...satData, name, - visible: isVisible, - azimuth: az, - elevation: el, - range: range, - leadTrack, + //visible: isVisible, + //azimuth: az, + //elevation: el, + //range: range, + //leadTrack, }; }); @@ -496,23 +504,20 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con return; } - console.log('[Satellite] found satellite for prediction:', sat.name); const orbit = new Orbit(sat.name, `${sat.name}\n${tle1}\n${tle2}`); orbit.error && console.warn('Satellite orbit error:', orbit.error); - console.log('[Satellite] created orbit object:', orbit); const groundStation = { - latitude: 32.896, - longitude: -117.125, - height: 0, + latitude: globalConfig.location.lat, + longitude: globalConfig.location.lon, + height: globalConfig.location.stationAlt, // above sea level [m] }; const startDate = dayjs().toDate(); // from now const endDate = dayjs(startDate).add(7, 'day').toDate(); // until 7 days from now - const minElevation = 0; + const minElevation = globalConfig.satellite.minElev; const maxPasses = 25; const passes = orbit.computePassesElevation(groundStation, startDate, endDate, minElevation, maxPasses); - console.log(`[Satellite] computed ${passes.length} passes for ${sat.name}`); // Function to generate modal content const generateModalContent = (currentPasses) => { @@ -673,7 +678,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con }; document.addEventListener('keydown', handleKeyDown); }; - }, [satellites, allUnits]); + }, [satellites, globalConfig]); /********************************************************************************************/ return null; diff --git a/src/utils/config.js b/src/utils/config.js index ac5a62be..84058d78 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: 100 }, // 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' }, From 840c60ecd6e2058854531fc12af98d0505bf691e Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sun, 5 Apr 2026 12:45:21 -0700 Subject: [PATCH 14/17] remove dead code in useSatelliteLayer.js, this appears to be unutilized legacy code since satellite location calculations are now done as a hook in useSatellites.js --- src/plugins/layers/useSatelliteLayer.js | 50 ------------------------- 1 file changed, 50 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index ec8a67b0..3d2097b8 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -51,67 +51,17 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con window.toggleSat = (name) => toggleSatellite(name); }, [selectedSats]); - // MRW note, much of the following code appears unused since satellite location is calculated in the useSatellites hook. - // I have commented it out and will leave for someone else to review and potentially remove. const fetchSatellites = async () => { try { const response = await fetch('/api/satellites/tle'); const data = await response.json(); - /* - const observerGd = { - latitude: satellite.degreesToRadians(config?.lat ?? 0.0), - longitude: satellite.degreesToRadians(config?.lon ?? 0.0), - height: (config?.stationAlt || 100) / 1000, // above sea level [km], stationAlt is [m], defaults to 100m - }; - */ - const satArray = Object.keys(data).map((name) => { const satData = data[name]; - /* - let isVisible = false; - let az = 0, - el = 0, - range = 0; - const leadTrack = []; - - if (satData.line1 && satData.line2) { - const satrec = satellite.twoline2satrec(satData.line1, satData.line2); - const now = new Date(); - const positionAndVelocity = satellite.propagate(satrec, now); - const gmst = satellite.gstime(now); - - if (positionAndVelocity.position) { - const positionEcf = satellite.eciToEcf(positionAndVelocity.position, gmst); - const lookAngles = satellite.ecfToLookAngles(observerGd, positionEcf); - - az = lookAngles.azimuth * (180 / Math.PI); - el = lookAngles.elevation * (180 / Math.PI); - range = lookAngles.rangeSat; - isVisible = el >= (config?.satellite?.minElev || 0); // visible only if above minimum elevation - } - - const minutesToPredict = config?.leadTimeMins || 45; - for (let i = 0; i <= minutesToPredict; i += 2) { - const futureTime = new Date(now.getTime() + i * 60000); - const posVel = satellite.propagate(satrec, futureTime); - if (posVel.position) { - const fGmst = satellite.gstime(futureTime); - const geodetic = satellite.eciToGeodetic(posVel.position, fGmst); - leadTrack.push([satellite.degreesLat(geodetic.latitude), satellite.degreesLong(geodetic.longitude)]); - } - } - }*/ - return { ...satData, name, - //visible: isVisible, - //azimuth: az, - //elevation: el, - //range: range, - //leadTrack, }; }); From b13b635dd94cbf5cf61e8f1a4ed8eef65a5debee Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sun, 5 Apr 2026 15:57:06 -0700 Subject: [PATCH 15/17] satellite prediction modal updated every 1s however satellite passes no longer updated unless modal is reopened or if 'satellites' is updated. Display Time From Now as hours:minutes:seconds --- src/plugins/layers/useSatelliteLayer.js | 35 ++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 3d2097b8..b016ff86 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -510,11 +510,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const startTime = dayjs(pass.start).format('YYYY-MM-DD HH:mm:ss'); const apexTime = dayjs(pass.apex).format('YYYY-MM-DD HH:mm:ss'); const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); - const minsFromNow = dayjs(pass.start).diff(dayjs(), 'minutes'); + const secsFromNow = dayjs(pass.start).diff(dayjs(), 'second'); const timeFromNow = - minsFromNow > 60 - ? `${Math.floor(minsFromNow / 60)}hr ${minsFromNow % 60}min` - : `${minsFromNow}min`; + 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} @@ -594,21 +597,23 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con modal.appendChild(content); document.body.appendChild(modal); - // Set up periodic updates every minute + const currentStartDate = dayjs().toDate(); + const currentEndDate = dayjs(currentStartDate).add(7, 'day').toDate(); + 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 = () => { - const currentStartDate = dayjs().toDate(); - const currentEndDate = dayjs(currentStartDate).add(7, 'day').toDate(); - const currentPasses = orbit.computePassesElevation( - groundStation, - currentStartDate, - currentEndDate, - minElevation, - maxPasses, - ); content.innerHTML = generateModalContent(currentPasses); }; - window.satellitePredictInterval = setInterval(updatePasses, 60000); + window.satellitePredictInterval = setInterval(updatePasses, 1000); // one second // Close on backdrop click modal.addEventListener('click', (e) => { From 5897abc93d7eb8418245fe6aca3c693d3630e675 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sun, 5 Apr 2026 16:09:49 -0700 Subject: [PATCH 16/17] move ./src/plugins/layers/satelliteOrbit.js to ./src/utils/orbit.js --- src/plugins/layers/useSatelliteLayer.js | 3 +-- src/{plugins/layers/satelliteOrbit.js => utils/orbit.js} | 0 2 files changed, 1 insertion(+), 2 deletions(-) rename src/{plugins/layers/satelliteOrbit.js => utils/orbit.js} (100%) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index b016ff86..f1943efd 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -1,9 +1,8 @@ import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import * as satellite from 'satellite.js'; import { addMinimizeToggle } from './addMinimizeToggle.js'; import { replicatePoint, replicatePath } from '../../utils/geo.js'; -import Orbit from './satelliteOrbit.js'; +import Orbit from '../../utils/orbit.js'; import dayjs from 'dayjs'; import useAppConfig from '../../hooks/app/useAppConfig.js'; diff --git a/src/plugins/layers/satelliteOrbit.js b/src/utils/orbit.js similarity index 100% rename from src/plugins/layers/satelliteOrbit.js rename to src/utils/orbit.js From b0751096095d610c746952cd5650bdc055c520b7 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sun, 5 Apr 2026 19:02:18 -0700 Subject: [PATCH 17/17] enhance table countdown, add ACTIVE, and drop passes ended --- src/plugins/layers/useSatelliteLayer.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index f1943efd..e6019a96 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -510,8 +510,17 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const apexTime = dayjs(pass.apex).format('YYYY-MM-DD HH:mm:ss'); const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); const secsFromNow = dayjs(pass.start).diff(dayjs(), 'second'); - const timeFromNow = - secsFromNow > 3600 + + const isActive = secsFromNow <= 0 && dayjs().isBefore(dayjs(pass.end)); + const isPast = secsFromNow <= 0 && dayjs().isAfter(dayjs(pass.end)); + + if (isPast) { + return ``; // skip past passes + } + + const timeFromNow = isActive + ? 'ACTIVE' + : 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')}`