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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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 a5fd0ac1a0d8fa64d613a67105489876e9bab8d8 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sun, 5 Apr 2026 12:10:45 -0700 Subject: [PATCH 14/51] fix table within a table html color changes replace ?? with || incase of NaN minElev default consistency to 5.0 restrict minElev to -5 to 89 --- src/components/SettingsPanel.jsx | 20 ++--- src/plugins/layers/useSatelliteLayer.js | 101 ++++++++++++------------ 2 files changed, 59 insertions(+), 62 deletions(-) diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index 34b58588..40ff2a53 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -47,10 +47,10 @@ export const SettingsPanel = ({ const [swapHeaderClocks, setSwapHeaderClocks] = useState(config?.swapHeaderClocks || false); const [showMutualReception, setShowMutualReception] = useState(config?.showMutualReception ?? true); 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 [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 || ''); @@ -179,10 +179,10 @@ export const SettingsPanel = ({ if (config) { setCallsign(config.callsign || ''); 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); + 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 || ''); @@ -3415,10 +3415,10 @@ export const SettingsPanel = ({ setMinElev(e.target.valueAsNumber ?? 5.0)} + onChange={(e) => setMinElev(isNaN(e.target.valueAsNumber) ? 5.0 : e.target.valueAsNumber)} style={{ width: '100%', padding: '10px', diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 20b39395..8359add1 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -79,7 +79,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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 + isVisible = el >= (config?.satellite?.minElev || 5.0); // visible only if above minimum elevation } const minutesToPredict = config?.leadTimeMins || 45; @@ -129,9 +129,9 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con position: 'absolute', width: '260px', backgroundColor: 'rgba(0, 15, 15, 0.95)', - color: '#00ffff', + color: '#0ff', borderRadius: '4px', - border: '1px solid #00ffff', + border: '1px solid #0ff', zIndex: '1000', fontFamily: 'monospace', pointerEvents: 'auto', @@ -190,8 +190,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const titleBar = `
- + padding: 8px 10px; border-bottom: 1px solid #044; background: rgba(0,40,40,0.6);"> + 🛰 ${activeSats.length} ${activeSats.length !== 1 ? t('station.settings.satellites.name_plural') : t('station.settings.satellites.name')} @@ -256,77 +256,74 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con let altitudeStr = `${altitude.toLocaleString()} ${distanceUnitsStr}`; return ` -
+
- ${sat.name} + ${sat.name} + style="background:none; border:none; color: #f44; cursor:pointer; font-weight:bold; font-size:20px; padding: 0 5px;">✕
-
- - - + + + - - - + + + - - - + + + - - - + + + -
${t('station.settings.satellites.latitude')}:${sat.lat.toFixed(2)}°
${t('station.settings.satellites.latitude')}:${sat.lat.toFixed(2)}°
${t('station.settings.satellites.longitude')}:${sat.lon.toFixed(2)}°
${t('station.settings.satellites.longitude')}:${sat.lon.toFixed(2)}°
${t('station.settings.satellites.altitude')}:${altitudeStr}
${t('station.settings.satellites.altitude')}:${altitudeStr}
${t('station.settings.satellites.speed')}:${speedStr}
${t('station.settings.satellites.speed')}:${speedStr}
- - + + + + ${ isVisible ? ` - - - + + + - - - + + + - - - + + + ` : `` } - - - + + + -
${t('station.settings.satellites.azimuth_elevation')}:${sat.azimuth}° / ${sat.elevation}°
${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.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.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.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.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 ? `${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}
` : ''} + ${sat.notes ? `
${sat.notes}
` : ''}
`; }) @@ -357,7 +354,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 ? '#00ff00' : '#0ff'; replicatePoint(sat.lat, sat.lon).forEach((pos) => { window.L.circle(pos, { @@ -379,7 +376,7 @@ 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: '#0ff', weight: 6, opacity: fade * 0.3 * globalOpacity, lineCap: 'round', @@ -395,7 +392,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con } } else { window.L.polyline(coords, { - color: '#00ffff', + color: '#0ff', weight: 1, opacity: 0.15 * globalOpacity, dashArray: '5, 10', @@ -424,8 +421,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], From 840c60ecd6e2058854531fc12af98d0505bf691e Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sun, 5 Apr 2026 12:45:21 -0700 Subject: [PATCH 15/51] 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 16/51] 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 17/51] 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 18/51] 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')}` From ffaa64f12bff40aae91e56f3f11ffc875f6369ce Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Mon, 6 Apr 2026 11:00:32 -0700 Subject: [PATCH 19/51] remove # hardcoded colors, replace with rgba(), modify opacity on selected colors --- src/plugins/layers/useSatelliteLayer.js | 64 ++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 8359add1..fc92eddb 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -129,9 +129,9 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con position: 'absolute', width: '260px', backgroundColor: 'rgba(0, 15, 15, 0.95)', - color: '#0ff', + color: 'rgba(0, 255, 255, 1)', borderRadius: '4px', - border: '1px solid #0ff', + border: '1px solid rgba(0, 255, 255, 1)', zIndex: '1000', fontFamily: 'monospace', pointerEvents: 'auto', @@ -190,14 +190,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const titleBar = `
- + padding: 8px 10px; border-bottom: 1px solid rgba(0, 68, 68, 1); background: rgba(0,40,40,0.6);"> + 🛰 ${activeSats.length} ${activeSats.length !== 1 ? t('station.settings.satellites.name_plural') : t('station.settings.satellites.name')} @@ -225,11 +225,11 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const clearAllBtn = `
- ${t('station.settings.satellites.dragTitle')} + ${t('station.settings.satellites.dragTitle')}
`; @@ -256,34 +256,34 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con let altitudeStr = `${altitude.toLocaleString()} ${distanceUnitsStr}`; return ` -
+
- ${sat.name} + ${sat.name} + style="background:none; border:none; color: rgba(255, 68, 68, 1); cursor:pointer; font-weight:bold; font-size:20px; padding: 0 5px;">✕
- + - + - + - + - + @@ -291,15 +291,15 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con ${ isVisible ? ` - + - + - + @@ -307,23 +307,23 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con : `` } - + - + - + - ${sat.downlink ? `` : ''} - ${sat.uplink ? `` : ''} - ${sat.tone ? `` : ''} + ${sat.downlink ? `` : ''} + ${sat.uplink ? `` : ''} + ${sat.tone ? `` : ''}
${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}°
${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.mode')}:${sat.mode || 'N/A'}${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.downlink')}:${sat.downlink}
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
- ${sat.notes ? `
${sat.notes}
` : ''} + ${sat.notes ? `
${sat.notes}
` : ''}
`; }) @@ -354,7 +354,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' : '#0ff'; + const footColor = sat.isVisible === true ? 'rgba(0, 255, 0, 1)' : 'rgba(0, 255, 255, 1)'; replicatePoint(sat.lat, sat.lon).forEach((pos) => { window.L.circle(pos, { @@ -376,14 +376,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: '#0ff', + color: 'rgba(0, 255, 255, 1)', 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)fff', weight: 2, opacity: fade * globalOpacity, lineCap: 'round', @@ -392,7 +392,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con } } else { window.L.polyline(coords, { - color: '#0ff', + color: 'rgba(0, 255, 255, 1)', weight: 1, opacity: 0.15 * globalOpacity, dashArray: '5, 10', @@ -405,7 +405,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: 'rgba(255, 255, 255, 1)f00', weight: 3, opacity: 0.8 * globalOpacity, dashArray: '8, 12', @@ -421,8 +421,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], From 4ec9e6bef7106d361788d31a9bf02d9f32db75ef Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Mon, 6 Apr 2026 18:13:30 -0700 Subject: [PATCH 20/51] must fix per https://github.com/accius/openhamclock/pull/877 --- src/components/SettingsPanel.jsx | 4 +++- src/hooks/useSatellites.js | 2 +- src/plugins/layers/useSatelliteLayer.js | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index 40ff2a53..d7cf074d 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -3386,7 +3386,9 @@ export const SettingsPanel = ({ type="number" step="1" value={isNaN(stationAlt) ? '' : stationAlt} - onChange={(e) => setStationAlt(e.target.valueAsNumber ?? 100)} + onChange={(e) => + setStationAlt(isNaN(e.target.valueAsNumber) ? 100 : e.target.valueAsNumber) + } style={{ width: '100%', padding: '10px', diff --git a/src/hooks/useSatellites.js b/src/hooks/useSatellites.js index d1ad387f..20aa0c1d 100644 --- a/src/hooks/useSatellites.js +++ b/src/hooks/useSatellites.js @@ -82,7 +82,7 @@ export const useSatellites = (observerLocation, satelliteConfig) => { const elevation = satellite.radiansToDegrees(lookAngles.elevation); const rangeSat = lookAngles.rangeSat; - const isVisible = elevation >= satelliteConfig.minElev; // visible only if above minimum elevation + const isVisible = elevation >= (satelliteConfig?.minElev || 5.0); // visible only if above minimum elevation // Calculate range-rate and doppler factor, only if satellite is visible let dopplerFactor = 1; diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index fc92eddb..ec6edf66 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -383,7 +383,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con interactive: false, }).addTo(layerGroupRef.current); window.L.polyline([coords[i], coords[i + 1]], { - color: 'rgba(255, 255, 255, 1)fff', + color: 'rgba(255, 255, 255, 1)', weight: 2, opacity: fade * globalOpacity, lineCap: 'round', @@ -405,7 +405,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: 'rgba(255, 255, 255, 1)f00', + color: 'rgba(255, 255, 0, 1)', weight: 3, opacity: 0.8 * globalOpacity, dashArray: '8, 12', From 74f8d02639ddd76ad15cc80a2b72791e725bca4a Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Mon, 6 Apr 2026 18:22:41 -0700 Subject: [PATCH 21/51] settingsPanel.jsx ?? vs || --- src/components/SettingsPanel.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index d7cf074d..f9373433 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -47,10 +47,10 @@ export const SettingsPanel = ({ const [swapHeaderClocks, setSwapHeaderClocks] = useState(config?.swapHeaderClocks || false); const [showMutualReception, setShowMutualReception] = useState(config?.showMutualReception ?? true); 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 [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 || ''); From 7c83c34ca5c60d16bf46678e40275c9cac9b1b5c Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 7 Apr 2026 14:58:37 -0700 Subject: [PATCH 22/51] clear interval on load --- src/plugins/layers/useSatelliteLayer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 467172c7..bce70fa2 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -618,6 +618,10 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con content.innerHTML = generateModalContent(currentPasses); }; + if (window.satellitePredictInterval) { + clearInterval(window.satellitePredictInterval); + } + window.satellitePredictInterval = setInterval(updatePasses, 1000); // one second // Close on backdrop click From 803476c42a98f89b091e9f458a11f3f89ef67d94 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 7 Apr 2026 15:20:17 -0700 Subject: [PATCH 23/51] introduced handlers --- src/plugins/layers/useSatelliteLayer.js | 158 +++++++++++++++++------- 1 file changed, 111 insertions(+), 47 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index bce70fa2..a56e55b7 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -45,11 +45,6 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con setSelectedSats((prev) => (prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name])); }; - // Bridge to the popup window HTML - useEffect(() => { - window.toggleSat = (name) => toggleSatellite(name); - }, [selectedSats]); - const fetchSatellites = async () => { try { const response = await fetch('/api/satellites/tle'); @@ -157,9 +152,6 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const activeSats = satellites.filter((s) => selectedSats.includes(s.name)); - // Expose minimize toggle so the inline onclick can reach it - window.__satWinToggleMinimize = () => setWinMinimized((prev) => !prev); - const titleBar = `
🛰 ${activeSats.length} ${activeSats.length !== 1 ? t('station.settings.satellites.name_plural') : t('station.settings.satellites.name')} -
`; + const clearAllBtn = ` +
+ + ${t('station.settings.satellites.dragTitle')} +
+ `; + if (winMinimized) { win.style.maxHeight = ''; win.style.overflowY = 'hidden'; @@ -187,7 +192,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con getIsMinimized: () => winMinimized, onToggle: setWinMinimized, persist: false, - manageButtonEvents: false, + manageButtonEvents: true, }); return; } @@ -195,17 +200,6 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con win.style.maxHeight = 'calc(100% - 80px)'; win.style.overflowY = 'auto'; - const clearAllBtn = ` -
- - ${t('station.settings.satellites.dragTitle')} -
- `; - win.innerHTML = titleBar + `
` + @@ -231,12 +225,22 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con return `
- - +
@@ -312,7 +316,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con getIsMinimized: () => winMinimized, onToggle: setWinMinimized, persist: false, - manageButtonEvents: false, + manageButtonEvents: true, }); }; @@ -437,10 +441,48 @@ 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 === 'clear-all-satellites') { + sessionStorage.removeItem('selected_satellites'); + window.location.reload(); + return; + } + + if (action === 'toggle-satellite') { + const name = actionEl.dataset.satName; + if (name) toggleSatellite(name); + return; + } + + if (action === 'open-predict') { + 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; + } + }; + + container.addEventListener('click', handleClick); + return () => container.removeEventListener('click', handleClick); + }, [map, toggleSatellite, satellites]); + /********************************************************************************************/ // Expose satellite prediction panel function useEffect(() => { - window.openSatellitePredict = (satName, tle1, tle2) => { + const openSatellitePredict = (satName, tle1, tle2) => { if (!satName || !satellites) return; // Find the satellite data @@ -465,6 +507,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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 ` @@ -541,16 +585,19 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con
-
@@ -558,7 +605,6 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con }; // Create a modal overlay - const modalId = 'satellite-predict-modal'; let modal = document.getElementById(modalId); if (modal) { @@ -616,6 +662,18 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con // or if satellite layer is updated for instance if TLE data changes const updatePasses = () => { content.innerHTML = generateModalContent(currentPasses); + const closeBtn = content.querySelector('[data-action="close-predict-modal"]'); + if (closeBtn) { + closeBtn.addEventListener('click', closeModal); + } + }; + + const closeModal = () => { + modal.remove(); + if (window.satellitePredictInterval) { + clearInterval(window.satellitePredictInterval); + } + document.removeEventListener('keydown', handleKeyDown); }; if (window.satellitePredictInterval) { @@ -627,21 +685,27 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con // Close on backdrop click modal.addEventListener('click', (e) => { if (e.target === modal) { - modal.remove(); - clearInterval(window.satellitePredictInterval); + closeModal(); } }); // Close on Enter or Escape key const handleKeyDown = (e) => { if (e.key === 'Enter' || e.key === 'Escape') { - modal.remove(); - clearInterval(window.satellitePredictInterval); - document.removeEventListener('keydown', handleKeyDown); + closeModal(); } }; document.addEventListener('keydown', handleKeyDown); + + // Wire initial close button + const initialCloseBtn = content.querySelector('[data-action="close-predict-modal"]'); + if (initialCloseBtn) { + initialCloseBtn.addEventListener('click', closeModal); + } }; + + // expose for other callers if needed + window.openSatellitePredict = openSatellitePredict; }, [satellites, globalConfig]); /********************************************************************************************/ From 25f8180200ea980a1c9dd89c530936f319e44fc5 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 7 Apr 2026 16:00:28 -0700 Subject: [PATCH 24/51] handle mouse dragging --- src/plugins/layers/useSatelliteLayer.js | 42 ++++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index a56e55b7..70432fcc 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -97,13 +97,11 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con let isDragging = false; - // This panel predates the shared Leaflet control widgets and is positioned - // relative to the map container using top/right, so it keeps its own drag - // logic instead of makeDraggable()'s fixed-position viewport model. - win.onmousedown = (e) => { + const handleMouseDown = (e) => { if (e.button !== 0) return; if (!e.target.closest('.sat-data-window-title')) return; if (e.target.closest('button')) return; + isDragging = true; win.style.cursor = 'move'; if (map.dragging) map.dragging.disable(); @@ -111,27 +109,41 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con e.stopPropagation(); }; - window.onmousemove = (e) => { + const handleMouseMove = (e) => { if (!isDragging) return; + const rect = container.getBoundingClientRect(); const x = rect.right - e.clientX; const y = e.clientY - rect.top; + win.style.right = `${x - 10}px`; win.style.top = `${y - 10}px`; }; - window.onmouseup = () => { - if (isDragging) { - isDragging = false; - win.style.cursor = 'default'; - if (map.dragging) map.dragging.enable(); - setWinPos({ - top: parseInt(win.style.top), - right: parseInt(win.style.right), - }); - } + const handleMouseUp = () => { + if (!isDragging) return; + + isDragging = false; + win.style.cursor = 'default'; + if (map.dragging) map.dragging.enable(); + + setWinPos({ + top: parseInt(win.style.top), + right: parseInt(win.style.right), + }); }; + win.addEventListener('mousedown', handleMouseDown); + window.addEventListener('mousemove', handleMouseMove, { capture: true }); + window.addEventListener('mouseup', handleMouseUp, { capture: true }); + + // Make sure we clean up if the window is ever removed + win.addEventListener('remove', () => { + win.removeEventListener('mousedown', handleMouseDown); + window.removeEventListener('mousemove', handleMouseMove, { capture: true }); + window.removeEventListener('mouseup', handleMouseUp, { capture: true }); + }); + // Prevent map from capturing events on the window win.addEventListener('wheel', (e) => { e.stopPropagation(); From f2e1c5d96a2ccd2055c1c6fd1d17a5f31aa3caa3 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 7 Apr 2026 20:10:57 -0700 Subject: [PATCH 25/51] intermedite color dev --- src/plugins/layers/useSatelliteLayer.js | 80 +++++++++++++++---------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 70432fcc..532ddeea 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -82,14 +82,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con Object.assign(win.style, { position: 'absolute', width: '260px', - backgroundColor: 'rgba(0, 15, 15, 0.95)', + backgroundColor: 'var(--bg-primary)', color: 'rgba(0, 255, 255, 1)', borderRadius: '4px', border: '1px solid rgba(0, 255, 255, 1)', zIndex: '1000', fontFamily: 'monospace', pointerEvents: 'auto', - boxShadow: '0 0 15px rgba(0,0,0,0.7)', + boxShadow: '0 0 15px rgba(0, 0, 0, 0.7)', cursor: 'default', overflow: 'hidden', }); @@ -167,14 +167,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const titleBar = `
- + padding: 8px 10px; border-bottom: 1px solid rgba(0, 68, 68, 1); background: rgba(0, 40, 40, 0.6);"> + 🛰 ${activeSats.length} ${activeSats.length !== 1 ? t('station.settings.satellites.name_plural') : t('station.settings.satellites.name')} @@ -186,11 +186,11 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con - ${t('station.settings.satellites.dragTitle')} + ${t('station.settings.satellites.dragTitle')}
`; @@ -237,45 +237,60 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con return `
+ ${sat.name} + +
+
+
+ +
+ ${t('station.settings.satellites.dragTitle')}
+
- + - + - + - + - + @@ -283,15 +298,15 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con ${ isVisible ? ` - + - + - + @@ -299,19 +314,19 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con : `` } - + - + - ${sat.downlink ? `` : ''} - ${sat.uplink ? `` : ''} - ${sat.tone ? `` : ''} + ${sat.downlink ? `` : ''} + ${sat.uplink ? `` : ''} + ${sat.tone ? `` : ''}
${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}°
${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.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.downlink')}:${sat.downlink}
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
@@ -525,21 +540,20 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const generateModalContent = (currentPasses) => { return `
-

🛰 ${satName}

+

🛰 ${satName}

Satellite Prediction Details

-

Upcoming Passes

- + - + @@ -579,7 +593,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con ? `00:${String(Math.floor(secsFromNow / 60)).padStart(2, '0')}:${String(secsFromNow % 60).padStart(2, '0')}` : `00:00:${String(secsFromNow).padStart(2, '0')}`; - return ` + return ` @@ -632,7 +646,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.6); + background: var(--bg-primary); display: flex; align-items: center; justify-content: center; From ba4e6df20f15353ce8fa0ad8dc839e64f76a5e80 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Wed, 8 Apr 2026 09:25:29 -0700 Subject: [PATCH 26/51] intermdiate color dev --- src/plugins/layers/useSatelliteLayer.js | 75 +++++++++++++++---------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 532ddeea..ebaee320 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -186,7 +186,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con @@ -236,7 +236,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con return `
-
+ +
${sat.name}
-
- -
- -
- - ${t('station.settings.satellites.dragTitle')} -
Start Apex End Duration
Time From Now Az [°]
${startTime} ${timeFromNow} ${azimuthStart}
@@ -328,10 +307,48 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con ${sat.uplink ? `` : ''} ${sat.tone ? `` : ''} -
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
+ + + + + ${sat.notes ? `
${sat.notes}
` : ''} -
+
+ + `; }) .join('') + @@ -578,15 +595,15 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const endTime = dayjs(pass.end).format('YYYY-MM-DD HH:mm:ss'); const secsFromNow = dayjs(pass.start).diff(dayjs(), 'second'); - const isActive = secsFromNow <= 0 && dayjs().isBefore(dayjs(pass.end)); + const isVisibleNow = 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' + 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 From dbb4f2a0b40e7fcf7056996be9e3457993af5e71 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Wed, 8 Apr 2026 13:14:26 -0700 Subject: [PATCH 27/51] satellite color updates --- src/plugins/layers/useSatelliteLayer.js | 53 +++++++++---------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index ebaee320..5ac26ef5 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -167,7 +167,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const titleBar = `
+ padding: 8px 10px; border-bottom: 1px solid var(--border-color); background: var(--bg-tertiary);"> 🛰 ${activeSats.length} ${activeSats.length !== 1 ? t('station.settings.satellites.name_plural') : t('station.settings.satellites.name')} @@ -251,25 +251,25 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con - + - + - + - + - + @@ -277,15 +277,15 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con ${ isVisible ? ` - + - + - + @@ -293,21 +293,21 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con : `` } - + - - - + + + - ${sat.downlink ? `` : ''} - ${sat.uplink ? `` : ''} - ${sat.tone ? `` : ''} + ${sat.downlink ? `` : ''} + ${sat.uplink ? `` : ''} + ${sat.tone ? `` : ''} - From 7658dabf04e230557cd0f8988ad16f4f44423f3e Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 14 Apr 2026 12:03:29 -0700 Subject: [PATCH 29/51] forward copy satellite color style changes --- src/plugins/layers/useSatelliteLayer.js | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index ec6edf66..4872ea16 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -128,14 +128,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con Object.assign(win.style, { position: 'absolute', width: '260px', - backgroundColor: 'rgba(0, 15, 15, 0.95)', + backgroundColor: 'var(--bg-primary)', color: 'rgba(0, 255, 255, 1)', borderRadius: '4px', border: '1px solid rgba(0, 255, 255, 1)', zIndex: '1000', fontFamily: 'monospace', pointerEvents: 'auto', - boxShadow: '0 0 15px rgba(0,0,0,0.7)', + boxShadow: '0 0 15px rgba(0, 0, 0, 0.7)', cursor: 'default', overflow: 'hidden', }); @@ -190,14 +190,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const titleBar = `
- + padding: 8px 10px; border-bottom: 1px solid var(--border-color); background: var(--bg-tertiary);"> + 🛰 ${activeSats.length} ${activeSats.length !== 1 ? t('station.settings.satellites.name_plural') : t('station.settings.satellites.name')} @@ -225,11 +225,11 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const clearAllBtn = `
- ${t('station.settings.satellites.dragTitle')} + ${t('station.settings.satellites.dragTitle')}
`; @@ -258,32 +258,32 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con return `
- ${sat.name} + ${sat.name} + style="background:none; border:none; color: var(--accent-red); cursor:pointer; font-weight:bold; font-size:20px; padding: 0 5px;">✕
${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}°
${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.mode')}:${sat.mode || 'N/A'}
${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.downlink')}:${sat.downlink}
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
+
${startTime}
- + - + - + - + - + @@ -291,15 +291,15 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con ${ isVisible ? ` - + - + - + @@ -307,23 +307,23 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con : `` } - + - + - + - ${sat.downlink ? `` : ''} - ${sat.uplink ? `` : ''} - ${sat.tone ? `` : ''} + ${sat.downlink ? `` : ''} + ${sat.uplink ? `` : ''} + ${sat.tone ? `` : ''}
${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}°
${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.mode')}:${sat.mode || 'N/A'}${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.downlink')}:${sat.downlink}
${t('station.settings.satellites.uplink')}:${sat.uplink}
${t('station.settings.satellites.tone')}:${sat.tone}
- ${sat.notes ? `
${sat.notes}
` : ''} + ${sat.notes ? `
${sat.notes}
` : ''}
`; }) From 2c215e3ed7def579c67c5857e8c093b74e2e0cf2 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 14 Apr 2026 13:40:21 -0700 Subject: [PATCH 30/51] limit station altitude [-500, 9000]m, defaults from || to ??, remove tle1, tle2 from positions --- src/components/SettingsPanel.jsx | 10 ++++++---- src/hooks/useSatellites.js | 2 -- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index f9373433..14378656 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -179,10 +179,10 @@ export const SettingsPanel = ({ if (config) { setCallsign(config.callsign || ''); 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); + 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 || ''); @@ -3385,6 +3385,8 @@ export const SettingsPanel = ({ setStationAlt(isNaN(e.target.valueAsNumber) ? 100 : e.target.valueAsNumber) diff --git a/src/hooks/useSatellites.js b/src/hooks/useSatellites.js index 20aa0c1d..6753632f 100644 --- a/src/hooks/useSatellites.js +++ b/src/hooks/useSatellites.js @@ -128,8 +128,6 @@ export const useSatellites = (observerLocation, satelliteConfig) => { positions.push({ name: tle.name || name, - tle1: line1, - tle2: line2, lat, lon, alt: round(alt, 1), From ed5903f1186107851af6b206d592a7f66f87df89 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 14 Apr 2026 17:42:31 -0700 Subject: [PATCH 31/51] remove unused computePassesSwath() from orbit.js --- src/utils/orbit.js | 94 ---------------------------------------------- 1 file changed, 94 deletions(-) diff --git a/src/utils/orbit.js b/src/utils/orbit.js index 4fe89b8d..ef93a85a 100644 --- a/src/utils/orbit.js +++ b/src/utils/orbit.js @@ -168,98 +168,4 @@ export default class Orbit { } 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; - } } From 57ca9dd38d74c6231af506210795d1026d53758e Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 14 Apr 2026 18:01:06 -0700 Subject: [PATCH 32/51] remove enter key as closure key for modal --- src/plugins/layers/useSatelliteLayer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 5c73576e..64251a01 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -711,9 +711,9 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con } }); - // Close on Enter or Escape key + // Close on Escape key const handleKeyDown = (e) => { - if (e.key === 'Enter' || e.key === 'Escape') { + if (e.key === 'Escape') { closeModal(); } }; From 73d6a3f6b707cfacb22d56ba0367e266f77bcf19 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 14 Apr 2026 19:00:26 -0700 Subject: [PATCH 33/51] set min/max height/width --- src/plugins/layers/useSatelliteLayer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 64251a01..f20d15cf 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -655,8 +655,9 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con border: 2px solid var(--accent-red); border-radius: 8px; padding: 20px; + min-width: 50vw; max-width: 95vw; - width: 50vw; + min-height: 25vh; max-height: 90vh; overflow-y: auto; overflow-x: auto; From 2811f7fd9bf90a3f29edbdd29952a0ecedfd985b Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Tue, 14 Apr 2026 19:48:24 -0700 Subject: [PATCH 34/51] eliminate dayjs as dependency --- package-lock.json | 7 ------- package.json | 1 - src/plugins/layers/useSatelliteLayer.js | 21 ++++++++++----------- src/utils/orbit.js | 5 ++--- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index b120a6ca..31a3d8b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "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", @@ -3902,12 +3901,6 @@ "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 777d7f44..fd230181 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "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/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index f20d15cf..5c292903 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { addMinimizeToggle } from './addMinimizeToggle.js'; import { replicatePoint, replicatePath } from '../../utils/geo.js'; import Orbit from '../../utils/orbit.js'; -import dayjs from 'dayjs'; import useAppConfig from '../../hooks/app/useAppConfig.js'; export const metadata = { @@ -524,8 +523,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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 startDate = new Date(); // from now + const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1000); // until 7 days from now const minElevation = globalConfig.satellite.minElev; const maxPasses = 25; const passes = orbit.computePassesElevation(groundStation, startDate, endDate, minElevation, maxPasses); @@ -569,13 +568,13 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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 secsFromNow = dayjs(pass.start).diff(dayjs(), 'second'); + const startTime = new Date(pass.start).toISOString().slice(0, 19).replace('T', ' '); + const apexTime = new Date(pass.apex).toISOString().slice(0, 19).replace('T', ' '); + const endTime = new Date(pass.end).toISOString().slice(0, 19).replace('T', ' '); + const secsFromNow = Math.floor((pass.start - new Date()) / 1000); - const isVisibleNow = secsFromNow <= 0 && dayjs().isBefore(dayjs(pass.end)); - const isPast = secsFromNow <= 0 && dayjs().isAfter(dayjs(pass.end)); + 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 @@ -671,8 +670,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con modal.appendChild(content); document.body.appendChild(modal); - const currentStartDate = dayjs().toDate(); - const currentEndDate = dayjs(currentStartDate).add(7, 'day').toDate(); + const currentStartDate = new Date(); + const currentEndDate = new Date(currentStartDate.getTime() + 7 * 24 * 60 * 60 * 1000); const currentPasses = orbit.computePassesElevation( groundStation, currentStartDate, diff --git a/src/utils/orbit.js b/src/utils/orbit.js index ef93a85a..fc43bae8 100644 --- a/src/utils/orbit.js +++ b/src/utils/orbit.js @@ -25,7 +25,6 @@ SOFTWARE. */ import * as satellitejs from 'satellite.js'; -import dayjs from 'dayjs'; const deg2rad = Math.PI / 180; const rad2deg = 180 / Math.PI; @@ -91,8 +90,8 @@ export default class Orbit { computePassesElevation( groundStationPosition, - startDate = dayjs().toDate(), - endDate = dayjs(startDate).add(7, 'day').toDate(), + startDate = new Date(), + endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1000), // 7 days later minElevation = 5, maxPasses = 50, ) { From 76d8d9d6b84d92d5a83d1cdefd88840559d3eea8 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Wed, 15 Apr 2026 18:54:10 -0700 Subject: [PATCH 35/51] orbit.js add optional logging, fix prediction algorithm --- src/utils/orbit.js | 76 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/src/utils/orbit.js b/src/utils/orbit.js index fc43bae8..9d2019dc 100644 --- a/src/utils/orbit.js +++ b/src/utils/orbit.js @@ -26,6 +26,7 @@ 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; @@ -104,7 +105,7 @@ export default class Orbit { const passes = []; let pass = false; let ongoingPass = false; - let lastElevation = 0; + 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); @@ -114,27 +115,49 @@ export default class Orbit { } 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 + // 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; @@ -146,22 +169,51 @@ export default class Orbit { break; } ongoingPass = false; - lastElevation = -180; - date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.05); // modified from original 0.5 value to make first pass calculation more reliable + 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 { - const deltaElevation = elevation - lastElevation; + // 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) { - 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) { + // 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); - } else if (elevation < -5) { + if (enableLogging) { + logMessage += '\n Elevation is low, advancing time by 5 minutes'; + } + } else if (elevation < -10) { date.setMinutes(date.getMinutes() + 1); - } else if (elevation < -1) { - date.setSeconds(date.getSeconds() + 5); + 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() + 2); + date.setSeconds(date.getSeconds() + 5); + if (enableLogging) { + logMessage += '\n Advancing time by 5 seconds'; + } + } + + if (enableLogging) { + console.log(logMessage); } } } From dfb3aba6381d0867728e4e2143a28124b53d39d0 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Wed, 15 Apr 2026 20:58:03 -0700 Subject: [PATCH 36/51] fix click propagation problem preventing PREDICT button working when DX location lock is unchecked --- src/plugins/layers/useSatelliteLayer.js | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 5c292903..6e617ac9 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -474,31 +474,37 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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; } - - if (action === 'open-predict') { - 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; - } }; - container.addEventListener('click', handleClick); - return () => container.removeEventListener('click', handleClick); + container.addEventListener('click', handleClick, true); // Use capture phase + return () => container.removeEventListener('click', handleClick, true); }, [map, toggleSatellite, satellites]); /********************************************************************************************/ From d71a5303efd69817f0400c390b6f38d4abdd37c5 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Thu, 16 Apr 2026 09:14:26 -0700 Subject: [PATCH 37/51] fix bug where zero minimum elevation is reverting to 5 --- src/components/SettingsPanel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index 14378656..d086974e 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -424,7 +424,7 @@ export const SettingsPanel = ({ swapHeaderClocks, showMutualReception, location: { lat: parseFloat(lat) || 0, lon: parseFloat(lon) || 0, stationAlt: parseInt(stationAlt) || 100 }, - satellite: { minElev: parseFloat(minElev) || 5.0 }, + satellite: { minElev: isNaN(parseFloat(minElev)) ? 5.0 : parseFloat(minElev) }, theme, customTheme, layout, From e1a1b9251f6ad8bc642b24e2df03444c91cfd7c4 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Thu, 16 Apr 2026 12:31:27 -0700 Subject: [PATCH 38/51] format with local time and not UTC --- src/plugins/layers/useSatelliteLayer.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 6e617ac9..c94515e6 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -555,13 +555,13 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con Duration - Time + Local Time From Now Az [°] - Time + Local Time Az [°] El [°] - Time + Local Time Az [°] [mins] @@ -574,9 +574,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const azimuthEnd = pass.azimuthEnd.toFixed(0); const maxElevation = pass.maxElevation.toFixed(0); const durationMins = (pass.duration / 60000).toFixed(1); - const startTime = new Date(pass.start).toISOString().slice(0, 19).replace('T', ' '); - const apexTime = new Date(pass.apex).toISOString().slice(0, 19).replace('T', ' '); - const endTime = new Date(pass.end).toISOString().slice(0, 19).replace('T', ' '); + 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); From b7c2be29d6642bbbba5f2c02ef045560e13f611c Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Fri, 17 Apr 2026 09:50:34 -0700 Subject: [PATCH 39/51] clean-up event listeners on modal close --- src/plugins/layers/useSatelliteLayer.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index c94515e6..fc0980ba 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -681,6 +681,13 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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( @@ -691,6 +698,9 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con maxPasses, ); + // Get close button reference early so it can be used in closeModal + const initialCloseBtn = content.querySelector('[data-action="close-predict-modal"]'); + // 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 = () => { @@ -702,11 +712,17 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con }; const closeModal = () => { + // Clean up all event listeners before removing modal + if (initialCloseBtn) { + initialCloseBtn.removeEventListener('click', closeModal); + } + modal.removeEventListener('click', handleModalClick); + document.removeEventListener('keydown', handleKeyDown); + modal.remove(); if (window.satellitePredictInterval) { clearInterval(window.satellitePredictInterval); } - document.removeEventListener('keydown', handleKeyDown); }; if (window.satellitePredictInterval) { @@ -716,11 +732,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con window.satellitePredictInterval = setInterval(updatePasses, 1000); // one second // Close on backdrop click - modal.addEventListener('click', (e) => { - if (e.target === modal) { - closeModal(); - } - }); + modal.addEventListener('click', handleModalClick); // Close on Escape key const handleKeyDown = (e) => { @@ -731,7 +743,6 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con document.addEventListener('keydown', handleKeyDown); // Wire initial close button - const initialCloseBtn = content.querySelector('[data-action="close-predict-modal"]'); if (initialCloseBtn) { initialCloseBtn.addEventListener('click', closeModal); } From 2862fd45789fc4ad2c18b12dd6e5dff9c3d2a6e6 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Fri, 17 Apr 2026 09:53:40 -0700 Subject: [PATCH 40/51] fix modal 1sec refresh accumulation of event listeners --- src/plugins/layers/useSatelliteLayer.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index fc0980ba..82594424 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -698,24 +698,15 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con maxPasses, ); - // Get close button reference early so it can be used in closeModal - const initialCloseBtn = content.querySelector('[data-action="close-predict-modal"]'); - // 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 closeBtn = content.querySelector('[data-action="close-predict-modal"]'); - if (closeBtn) { - closeBtn.addEventListener('click', closeModal); - } }; const closeModal = () => { // Clean up all event listeners before removing modal - if (initialCloseBtn) { - initialCloseBtn.removeEventListener('click', closeModal); - } + content.removeEventListener('click', handleContentClick); modal.removeEventListener('click', handleModalClick); document.removeEventListener('keydown', handleKeyDown); @@ -725,6 +716,13 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con } }; + // 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); } @@ -742,10 +740,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con }; document.addEventListener('keydown', handleKeyDown); - // Wire initial close button - if (initialCloseBtn) { - initialCloseBtn.addEventListener('click', closeModal); - } + // Wire close button using event delegation (one listener for all updates) + content.addEventListener('click', handleContentClick); }; // expose for other callers if needed From e9fda12e97c2aefd9425afadeff05803bd5a36ec Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Fri, 17 Apr 2026 09:58:54 -0700 Subject: [PATCH 41/51] fix other event listener clean-up issues --- src/plugins/layers/useSatelliteLayer.js | 79 +++++++++++++++++++------ 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 82594424..4f6116d4 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -23,6 +23,7 @@ export const metadata = { export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, config, allUnits }) => { const layerGroupRef = useRef(null); + const winListenersRef = useRef(null); // Store window event listener references for cleanup const { t } = useTranslation(); const { config: globalConfig } = useAppConfig(); @@ -69,7 +70,22 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con let win = container.querySelector(`#${winId}`); if (!selectedSats || selectedSats.length === 0) { - 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(); + } return; } @@ -135,26 +151,38 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con window.addEventListener('mousemove', handleMouseMove, { capture: true }); window.addEventListener('mouseup', handleMouseUp, { capture: true }); - // Make sure we clean up if the window is ever removed - win.addEventListener('remove', () => { - win.removeEventListener('mousedown', handleMouseDown); - window.removeEventListener('mousemove', handleMouseMove, { capture: true }); - window.removeEventListener('mouseup', handleMouseUp, { capture: true }); - }); - - // Prevent map from capturing events on the window - win.addEventListener('wheel', (e) => { + // Named functions for preventing map event capture + const handleWheelPropagation = (e) => { e.stopPropagation(); - }); - win.addEventListener('mousedown', (e) => { + }; + const handleMouseDownPropagation = (e) => { e.stopPropagation(); - }); - win.addEventListener('mousemove', (e) => { + }; + const handleMouseMovePropagation = (e) => { e.stopPropagation(); - }); - win.addEventListener('mouseup', (e) => { + }; + const handleMouseUpPropagation = (e) => { e.stopPropagation(); - }); + }; + + // Prevent map from capturing events on the window + win.addEventListener('wheel', handleWheelPropagation); + win.addEventListener('mousedown', handleMouseDownPropagation); + win.addEventListener('mousemove', handleMouseMovePropagation); + win.addEventListener('mouseup', handleMouseUpPropagation); + + // Store all listener references for cleanup + winListenersRef.current = { + mouseDownHandler: handleMouseDown, + mouseMoveHandler: handleMouseMove, + mouseUpHandler: handleMouseUp, + wheelHandler: handleWheelPropagation, + propagationHandler: { + mousedown: handleMouseDownPropagation, + mousemove: handleMouseMovePropagation, + mouseup: handleMouseUpPropagation, + }, + }; } win.style.top = `${winPos.top}px`; @@ -455,7 +483,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]); From 6363f9872d79e0522e2a965a12d4d90f0877c865 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Fri, 17 Apr 2026 10:06:07 -0700 Subject: [PATCH 42/51] cleanup added to global window.openSatellitePredict --- src/plugins/layers/useSatelliteLayer.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 4f6116d4..28f038ec 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -789,6 +789,11 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con // 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, globalConfig]); /********************************************************************************************/ From a70958bc72c2a0283d366aff55ed94499a0233a6 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Fri, 17 Apr 2026 10:23:08 -0700 Subject: [PATCH 43/51] remove reference to useAppConfig --- src/components/WorldMap.jsx | 51 ++++++++++++++++--------- src/plugins/layers/useSatelliteLayer.js | 20 ++++++---- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx index 7facc252..489a0366 100644 --- a/src/components/WorldMap.jsx +++ b/src/components/WorldMap.jsx @@ -1931,23 +1931,40 @@ export const WorldMap = ({ {/* Key includes projection so hooks fully remount when map instance changes. This resets internal refs (layerGroupRef, controlRef) that are bound to a specific Leaflet map — without this, layers stay on the hidden old map. */} - {getAllLayers().map((layerDef) => ( - - ))} + {getAllLayers().map((layerDef) => { + // Merge location config into satellite layer to keep config access consistent + const layerConfig = pluginLayerStates[layerDef.id]?.config ?? layerDef.config; + const finalConfig = + layerDef.id === 'satellites' && deLocation + ? { + ...layerConfig, + location: { + lat: deLocation.lat, + lon: deLocation.lon, + stationAlt: deLocation.stationAlt || 0, + }, + satellite: layerConfig?.satellite || { minElev: 0 }, + } + : layerConfig; + + return ( + + ); + })} {/* Unified map control dock */} {!isAzimuthal && ( diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 28f038ec..91951713 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { addMinimizeToggle } from './addMinimizeToggle.js'; import { replicatePoint, replicatePath } from '../../utils/geo.js'; import Orbit from '../../utils/orbit.js'; -import useAppConfig from '../../hooks/app/useAppConfig.js'; export const metadata = { id: 'satellites', @@ -18,6 +17,14 @@ export const metadata = { tailTimeMins: 15, showTracks: true, showFootprints: true, + location: { + lat: 0.0, + lon: 0.0, + stationAlt: 100, + }, + satellite: { + minElev: 0, + }, }, }; @@ -25,7 +32,6 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const layerGroupRef = useRef(null); const winListenersRef = useRef(null); // Store window event listener references for cleanup const { t } = useTranslation(); - const { config: globalConfig } = useAppConfig(); // 1. Multi-select state (Wipes on browser close) const [selectedSats, setSelectedSats] = useState(() => { @@ -567,14 +573,14 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con orbit.error && console.warn('Satellite orbit error:', orbit.error); const groundStation = { - latitude: globalConfig.location.lat, - longitude: globalConfig.location.lon, - height: globalConfig.location.stationAlt, // above sea level [m] + 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 = globalConfig.satellite.minElev; + const minElevation = config?.satellite?.minElev || 0; const maxPasses = 25; const passes = orbit.computePassesElevation(groundStation, startDate, endDate, minElevation, maxPasses); @@ -794,7 +800,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con return () => { delete window.openSatellitePredict; }; - }, [satellites, globalConfig]); + }, [satellites, config]); /********************************************************************************************/ return null; From 92d8eedd72860820e05dbd261df7fbb042a85f20 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Fri, 17 Apr 2026 10:40:29 -0700 Subject: [PATCH 44/51] ensure config and satellite propagate in all views --- src/DockableApp.jsx | 1 + src/components/WorldMap.jsx | 5 ++++- src/layouts/ClassicLayout.jsx | 3 +++ src/layouts/EmcommLayout.jsx | 1 + src/layouts/ModernLayout.jsx | 1 + 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/DockableApp.jsx b/src/DockableApp.jsx index 01177085..d25ec4ce 100644 --- a/src/DockableApp.jsx +++ b/src/DockableApp.jsx @@ -638,6 +638,7 @@ export const DockableApp = ({ const renderWorldMap = () => (
(
Date: Fri, 17 Apr 2026 11:01:18 -0700 Subject: [PATCH 45/51] staging merge fix conflict fix, add deLat, deLon to WorldMap.jsx --- src/components/WorldMap.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx index 134017c9..580f2e1d 100644 --- a/src/components/WorldMap.jsx +++ b/src/components/WorldMap.jsx @@ -1964,6 +1964,8 @@ export const WorldMap = ({ allUnits={allUnits} callsign={callsign} locator={deLocator} + deLat={deLocation?.lat ?? null} + deLon={deLocation?.lon ?? null} lowMemoryMode={lowMemoryMode} /> ); From 90f2d3e337cca082f5f4563aa89fb18f271d4ed0 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Fri, 17 Apr 2026 11:44:23 -0700 Subject: [PATCH 46/51] consistency, default station altitude 100m, default minimum elevation 5 degrees. Set default Boulder CO altitude 1630m. --- src/components/WorldMap.jsx | 4 ++-- src/plugins/layers/useSatelliteLayer.js | 4 ++-- src/utils/config.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx index d869af7d..141e070d 100644 --- a/src/components/WorldMap.jsx +++ b/src/components/WorldMap.jsx @@ -2222,10 +2222,10 @@ export const WorldMap = ({ location: { lat: deLocation.lat, lon: deLocation.lon, - stationAlt: deLocation.stationAlt || 0, + stationAlt: deLocation.stationAlt || 100, }, satellite: { - minElev: config?.satellite?.minElev ?? layerConfig?.satellite?.minElev ?? 0, + minElev: config?.satellite?.minElev ?? layerConfig?.satellite?.minElev ?? 5, }, } : layerConfig; diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 91951713..aa6a1afd 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -23,7 +23,7 @@ export const metadata = { stationAlt: 100, }, satellite: { - minElev: 0, + minElev: 5, }, }, }; @@ -580,7 +580,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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 || 0; + const minElevation = config?.satellite?.minElev || 5; const maxPasses = 25; const passes = orbit.computePassesElevation(groundStation, startDate, endDate, minElevation, maxPasses); diff --git a/src/utils/config.js b/src/utils/config.js index bf0cb4e2..62c81045 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -20,7 +20,7 @@ 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, stationAlt: 100 }, // Boulder, CO (default), altitude [m] + 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' From b562a479c2ed73294e3ecc428106434cbbf9e257 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sun, 19 Apr 2026 20:22:55 -0700 Subject: [PATCH 47/51] Fix: change all four sites from || to ?? --- src/hooks/useSatellites.js | 4 ++-- src/plugins/layers/useSatelliteLayer.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useSatellites.js b/src/hooks/useSatellites.js index 6753632f..af1274e7 100644 --- a/src/hooks/useSatellites.js +++ b/src/hooks/useSatellites.js @@ -51,7 +51,7 @@ export const useSatellites = (observerLocation, satelliteConfig) => { const observerGd = { longitude: satellite.degreesToRadians(observerLocation.lon), latitude: satellite.degreesToRadians(observerLocation.lat), - height: (observerLocation.stationAlt || 100) / 1000, // above sea level [km], stationAlt is [m]), defaults to 100m + height: (observerLocation.stationAlt ?? 100) / 1000, // above sea level [km], stationAlt is [m]), defaults to 100m }; Object.entries(tleData).forEach(([name, tle]) => { @@ -82,7 +82,7 @@ export const useSatellites = (observerLocation, satelliteConfig) => { const elevation = satellite.radiansToDegrees(lookAngles.elevation); const rangeSat = lookAngles.rangeSat; - const isVisible = elevation >= (satelliteConfig?.minElev || 5.0); // visible only if above minimum elevation + const isVisible = elevation >= (satelliteConfig?.minElev ?? 5.0); // visible only if above minimum elevation // Calculate range-rate and doppler factor, only if satellite is visible let dopplerFactor = 1; diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 4872ea16..4ef568f7 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -55,7 +55,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con 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 + height: (config?.stationAlt ?? 100) / 1000, // above sea level [km], stationAlt is [m], defaults to 100m }; const satArray = Object.keys(data).map((name) => { @@ -79,7 +79,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con az = lookAngles.azimuth * (180 / Math.PI); el = lookAngles.elevation * (180 / Math.PI); range = lookAngles.rangeSat; - isVisible = el >= (config?.satellite?.minElev || 5.0); // visible only if above minimum elevation + isVisible = el >= (config?.satellite?.minElev ?? 5.0); // visible only if above minimum elevation } const minutesToPredict = config?.leadTimeMins || 45; From 1976a24a1925dd3615c4c7d5d3923074872fbfa5 Mon Sep 17 00:00:00 2001 From: Michael R Wheeley Date: Sun, 19 Apr 2026 20:26:15 -0700 Subject: [PATCH 48/51] border-bottom to var(--border-color) --- src/plugins/layers/useSatelliteLayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 4ef568f7..b6166716 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -256,7 +256,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con let altitudeStr = `${altitude.toLocaleString()} ${distanceUnitsStr}`; return ` -
+
${sat.name}