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);
});
});