diff --git a/src/plugins/layers/useSatelliteLayer.js b/src/plugins/layers/useSatelliteLayer.js index 695e129d..012e212a 100644 --- a/src/plugins/layers/useSatelliteLayer.js +++ b/src/plugins/layers/useSatelliteLayer.js @@ -5,7 +5,7 @@ import { replicatePoint, replicatePath } from '../../utils/geo.js'; export const metadata = { id: 'satellites', name: 'Satellite Tracks', - description: 'Real-time satellite positions with multi-select footprints', + description: 'Real-time satellite positions with glow tracks and narrow info card', icon: '🛰', category: 'satellites', defaultEnabled: true, @@ -21,7 +21,7 @@ export const metadata = { export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, config, units }) => { const layerGroupRef = useRef(null); - // 1. Multi-select state (Wipes on browser close) + // 1. Multi-select state const [selectedSats, setSelectedSats] = useState(() => { const saved = sessionStorage.getItem('selected_satellites'); return saved ? JSON.parse(saved) : []; @@ -29,20 +29,87 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const [winPos, setWinPos] = useState({ top: 50, right: 10 }); const [winMinimized, setWinMinimized] = useState(false); - // Sync to session storage + // Sync selection to session storage useEffect(() => { sessionStorage.setItem('selected_satellites', JSON.stringify(selectedSats)); }, [selectedSats]); - // Helper to add/remove satellites from the active view const toggleSatellite = (name) => { setSelectedSats((prev) => (prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name])); }; - // Bridge to the popup window HTML + const clearAllSats = () => { + setSelectedSats([]); + }; + + // Dedicated effect for window bindings useEffect(() => { window.toggleSat = (name) => toggleSatellite(name); - }, [selectedSats]); + window.clearAllSats = () => clearAllSats(); + + return () => { + delete window.toggleSat; + delete window.clearAllSats; + }; + }, []); // Only bind once on mount + + // 2. Inject Styles with Cleanup + useEffect(() => { + const styleId = 'sat-layer-ui-styles'; + if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + @keyframes satBlink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } } + .sat-visible-blink { animation: satBlink 1s infinite !important; color: #00ff00 !important; font-weight: bold; } + + .sat-data-window { + position: absolute; + z-index: 9999 !important; + background: rgba(10, 10, 10, 0.75) !important; + backdrop-filter: blur(4px); + border: 1px solid #00ffff; + border-radius: 4px; + padding: 8px 10px; + color: white; + font-family: 'JetBrains Mono', monospace; + min-width: 180px; + max-width: 180px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.7); + pointer-events: auto; + } + + .sat-close-btn { cursor: pointer; color: #ff4444; font-size: 16px; font-weight: bold; } + .sat-label { color: #00ffff; font-size: 10px; font-weight: bold; text-shadow: 1px 1px 2px black; white-space: nowrap; margin-top: 2px; } + + .sat-mini-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 4px; } + .sat-mini-table td { padding: 2px 0; border-bottom: 1px solid rgba(255,255,255,0.05); } + + .sat-clear-btn { + width: 100%; + background: rgba(255, 68, 68, 0.1); + border: 1px solid #ff4444; + color: #ff4444; + cursor: pointer; + padding: 6px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: bold; + border-radius: 2px; + text-transform: uppercase; + margin-top: 5px; + transition: background 0.2s; + } + .sat-clear-btn:hover { background: rgba(255, 68, 68, 0.3); } + `; + document.head.appendChild(style); + } + + return () => { + const style = document.getElementById(styleId); + if (style) style.remove(); + }; + }, []); const fetchSatellites = async () => { try { @@ -60,7 +127,10 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con let isVisible = false; let az = 0, el = 0, - range = 0; + range = 0, + alt = 0, + lat = 0, + lon = 0; const leadTrack = []; if (satData.line1 && satData.line2) { @@ -70,36 +140,31 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const gmst = satellite.gstime(now); if (positionAndVelocity.position) { + const positionGd = satellite.eciToGeodetic(positionAndVelocity.position, gmst); 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); + az = satellite.radiansToDegrees(lookAngles.azimuth); + el = satellite.radiansToDegrees(lookAngles.elevation); range = lookAngles.rangeSat; isVisible = el > 0; + lat = satellite.degreesLat(positionGd.latitude); + lon = satellite.degreesLong(positionGd.longitude); + alt = positionGd.height; } - const minutesToPredict = config?.leadTimeMins || 45; - for (let i = 0; i <= minutesToPredict; i += 2) { + const minutes = config?.leadTimeMins || 45; + for (let i = 0; i <= minutes; 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); + const propagation = satellite.propagate(satrec, futureTime); + if (propagation.position) { + const geodetic = satellite.eciToGeodetic(propagation.position, satellite.gstime(futureTime)); leadTrack.push([satellite.degreesLat(geodetic.latitude), satellite.degreesLong(geodetic.longitude)]); } } } - return { - ...satData, - name, - visible: isVisible, - azimuth: az, - elevation: el, - range: range, - leadTrack, - }; + return { ...satData, name, lat, lon, alt, visible: isVisible, azimuth: az, elevation: el, range, leadTrack }; }); if (setSatellites) setSatellites(satArray); @@ -109,9 +174,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con }; const updateInfoWindow = () => { - const winId = 'sat-data-window'; const container = map.getContainer(); - let win = container.querySelector(`#${winId}`); + let win = container.querySelector('#sat-data-window'); if (!selectedSats || selectedSats.length === 0) { if (win) win.remove(); @@ -120,26 +184,11 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con if (!win) { win = document.createElement('div'); - win.id = winId; + win.id = 'sat-data-window'; win.className = 'sat-data-window leaflet-bar'; - Object.assign(win.style, { - position: 'absolute', - width: '260px', - backgroundColor: 'rgba(0, 15, 15, 0.95)', - color: '#00ffff', - borderRadius: '4px', - border: '1px solid #00ffff', - zIndex: '1000', - fontFamily: 'monospace', - pointerEvents: 'auto', - boxShadow: '0 0 15px rgba(0,0,0,0.7)', - cursor: 'default', - overflow: 'hidden', - }); container.appendChild(win); let isDragging = false; - win.onmousedown = (e) => { if (e.ctrlKey) { isDragging = true; @@ -153,10 +202,8 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con window.onmousemove = (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`; + win.style.right = `${rect.right - e.clientX - 10}px`; + win.style.top = `${e.clientY - rect.top - 10}px`; }; window.onmouseup = () => { @@ -164,91 +211,58 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con isDragging = false; win.style.cursor = 'default'; if (map.dragging) map.dragging.enable(); - setWinPos({ - top: parseInt(win.style.top), - right: parseInt(win.style.right), - }); + setWinPos({ top: parseInt(win.style.top), right: parseInt(win.style.right) }); } }; } win.style.top = `${winPos.top}px`; win.style.right = `${winPos.right}px`; - - 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} SAT${activeSats.length !== 1 ? 'S' : ''} - - -
- `; - - if (winMinimized) { - win.style.maxHeight = ''; - win.style.overflowY = 'hidden'; - win.innerHTML = titleBar; - return; - } + const activeSats = satellites.filter((s) => selectedSats.includes(s.name)); - win.style.maxHeight = 'calc(100% - 80px)'; - win.style.overflowY = 'auto'; - - const clearAllBtn = ` -
- - Ctrl + Drag to move + let html = ` +
+ 🛰 ${activeSats.length} SATS +
`; - win.innerHTML = - titleBar + - clearAllBtn + - `
` + - activeSats - .map((sat) => { - const isVisible = sat.visible === true; - const isImp = units === 'imperial'; - const conv = isImp ? 0.621371 : 1; - const distUnit = isImp ? ' mi' : ' km'; - - return ` -
-
- ${sat.name} - + if (!winMinimized) { + activeSats.forEach((sat) => { + const isVisible = sat.visible === true; + const conv = units === 'imperial' ? 0.621371 : 1; + const distUnit = units === 'imperial' ? ' mi' : ' km'; + + html += ` +
+
+ ${sat.name} + ✕ +
+ + + + + +
Az/El${Math.round(sat.azimuth)}° / ${Math.round(sat.elevation)}°
Range${Math.round(sat.range * conv).toLocaleString()}${distUnit}
Mode${sat.mode || 'N/A'}
Status + ${isVisible ? 'VISIBLE' : 'Below Horizon'} +
- - - - - - - -
Az/El:${Math.round(sat.azimuth)}° / ${Math.round(sat.elevation)}°
Range:${Math.round(sat.range * conv).toLocaleString()}${distUnit}
Mode:${sat.mode || 'N/A'}
Status: - ${isVisible ? 'Visible' : 'Below Horiz'} -
+ `; + }); + + html += ` +
+
`; - }) - .join('') + - `
`; + + html += `
Ctrl+Drag to move
`; + } + + win.innerHTML = html; }; const renderSatellites = () => { @@ -262,14 +276,12 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con const isSelected = selectedSats.includes(sat.name); if (isSelected && config?.showFootprints !== false && sat.alt) { - const EARTH_RADIUS = 6371; - const centralAngle = Math.acos(EARTH_RADIUS / (EARTH_RADIUS + sat.alt)); - const footprintRadiusMeters = centralAngle * EARTH_RADIUS * 1000; + const R = 6371; + const radiusMeters = Math.acos(R / (R + sat.alt)) * R * 1000; const footColor = sat.visible === true ? '#00ff00' : '#00ffff'; - replicatePoint(sat.lat, sat.lon).forEach((pos) => { window.L.circle(pos, { - radius: footprintRadiusMeters, + radius: radiusMeters, color: footColor, weight: 2, opacity: globalOpacity, @@ -281,8 +293,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con } if (config?.showTracks !== false && sat.track) { - const pathCoords = sat.track.map((p) => [p[0], p[1]]); - replicatePath(pathCoords).forEach((coords) => { + replicatePath(sat.track.map((p) => [p[0], p[1]])).forEach((coords) => { if (isSelected) { for (let i = 0; i < coords.length - 1; i++) { const fade = i / coords.length; @@ -313,8 +324,7 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con }); if (isSelected && sat.leadTrack && sat.leadTrack.length > 0) { - const leadCoords = sat.leadTrack.map((p) => [p[0], p[1]]); - replicatePath(leadCoords).forEach((lCoords) => { + replicatePath(sat.leadTrack.map((p) => [p[0], p[1]])).forEach((lCoords) => { window.L.polyline(lCoords, { color: '#ffff00', weight: 3, @@ -340,12 +350,10 @@ export const useLayer = ({ map, enabled, satellites, setSatellites, opacity, con }), zIndexOffset: isSelected ? 10000 : 1000, }); - marker.on('click', (e) => { window.L.DomEvent.stopPropagation(e); toggleSatellite(sat.name); }); - marker.addTo(layerGroupRef.current); }); });