-
setNewCall(e.target.value.toUpperCase())}
- onKeyPress={(e) => e.key === 'Enter' && addToExclude()}
- placeholder="Enter callsign..."
- style={{
- flex: 1,
- padding: '8px 12px',
- background: 'var(--bg-tertiary)',
- border: '1px solid var(--border-color)',
- borderRadius: '4px',
- color: 'var(--text-primary)',
- fontSize: '13px',
- fontFamily: 'JetBrains Mono'
- }}
- />
-
+
+ {(filters?.watchlist || []).map(call => (
+
+ {call}
+
+ ))}
+
+
+
+
+
+
+
+ Exclude List - Hide these callsigns
-
- {(filters?.excludeList || []).map(call => (
-
- {call}
-
-
- ))}
+
+ setNewExcludeCall(e.target.value.toUpperCase())}
+ onKeyPress={(e) => e.key === 'Enter' && addToExclude()}
+ placeholder="Enter callsign..."
+ style={{
+ flex: 1,
+ padding: '8px 12px',
+ background: 'var(--bg-tertiary)',
+ border: '1px solid var(--border-color)',
+ borderRadius: '4px',
+ color: 'var(--text-primary)',
+ fontSize: '13px',
+ fontFamily: 'JetBrains Mono'
+ }}
+ />
+
- );
- };
+
+ {(filters?.excludeList || []).map(call => (
+
+ {call}
+
+
+ ))}
+
+
+ );
return (
${path.dxCall}
`,
- iconAnchor: [0, -10]
+ html: `
${path.dxCall}`,
+ iconSize: null,
+ iconAnchor: [0, 0]
});
const label = L.marker([path.dxLat, path.dxLon], { icon: labelIcon, interactive: false }).addTo(map);
dxPathsMarkersRef.current.push(label);
@@ -316,8 +317,9 @@ export const WorldMap = ({
if (spot.lat && spot.lon) {
const icon = L.divIcon({
className: '',
- html: `
${spot.call}
`,
- iconAnchor: [20, 10]
+ html: `
${spot.call}`,
+ iconSize: null,
+ iconAnchor: [0, 0]
});
const marker = L.marker([spot.lat, spot.lon], { icon })
.bindPopup(`
${spot.call}${spot.ref}
${spot.freq} ${spot.mode}`)
@@ -328,7 +330,7 @@ export const WorldMap = ({
}
}, [potaSpots, showPOTA]);
- // Update satellite markers
+ // Update satellite markers with orbit tracks
useEffect(() => {
if (!mapInstanceRef.current) return;
const map = mapInstanceRef.current;
@@ -340,14 +342,70 @@ export const WorldMap = ({
if (showSatellites && satellites && satellites.length > 0) {
satellites.forEach(sat => {
+ // Draw orbit track if available
+ if (sat.track && sat.track.length > 1) {
+ // Split track into segments to handle date line crossing
+ let segments = [];
+ let currentSegment = [sat.track[0]];
+
+ for (let i = 1; i < sat.track.length; i++) {
+ const prevLon = sat.track[i-1][1];
+ const currLon = sat.track[i][1];
+ // If longitude jumps more than 180 degrees, start new segment
+ if (Math.abs(currLon - prevLon) > 180) {
+ segments.push(currentSegment);
+ currentSegment = [];
+ }
+ currentSegment.push(sat.track[i]);
+ }
+ segments.push(currentSegment);
+
+ // Draw each segment
+ segments.forEach(segment => {
+ if (segment.length > 1) {
+ const trackLine = L.polyline(segment, {
+ color: sat.visible ? '#00ffff' : '#006688',
+ weight: 2,
+ opacity: sat.visible ? 0.8 : 0.4,
+ dashArray: sat.visible ? null : '5, 5'
+ }).addTo(map);
+ satTracksRef.current.push(trackLine);
+ }
+ });
+ }
+
+ // Draw footprint circle if available
+ if (sat.footprintRadius && sat.lat && sat.lon) {
+ const footprint = L.circle([sat.lat, sat.lon], {
+ radius: sat.footprintRadius * 1000, // Convert km to meters
+ color: '#00ffff',
+ weight: 1,
+ opacity: 0.5,
+ fillColor: '#00ffff',
+ fillOpacity: 0.1
+ }).addTo(map);
+ satTracksRef.current.push(footprint);
+ }
+
+ // Add satellite marker icon
const icon = L.divIcon({
className: '',
- html: `
🛰 ${sat.name}
`,
- iconAnchor: [25, 12]
+ html: `
🛰 ${sat.name}`,
+ iconSize: null,
+ iconAnchor: [0, 0]
});
const marker = L.marker([sat.lat, sat.lon], { icon })
- .bindPopup(`
🛰 ${sat.name}Alt: ${sat.alt} km
Az: ${sat.azimuth}° El: ${sat.elevation}°`)
+ .bindPopup(`
+
🛰 ${sat.name}
+
+ | Alt: | ${sat.alt} km |
+ | Az: | ${sat.azimuth}° |
+ | El: | ${sat.elevation}° |
+ | Range: | ${sat.range} km |
+ | Status: | ${sat.visible ? 'Visible' : 'Below horizon'} |
+
+ `)
.addTo(map);
satMarkersRef.current.push(marker);
});
diff --git a/src/hooks/useSatellites.js b/src/hooks/useSatellites.js
index 9b8a23d..d3d7499 100644
--- a/src/hooks/useSatellites.js
+++ b/src/hooks/useSatellites.js
@@ -1,6 +1,7 @@
/**
* useSatellites Hook
* Tracks amateur radio satellites using TLE data and satellite.js
+ * Includes orbit track prediction
*/
import { useState, useEffect, useCallback } from 'react';
import * as satellite from 'satellite.js';
@@ -43,7 +44,7 @@ export const useSatellites = (observerLocation) => {
return () => clearInterval(interval);
}, []);
- // Calculate satellite positions
+ // Calculate satellite positions and orbits
const calculatePositions = useCallback(() => {
if (!observerLocation || Object.keys(tleData).length === 0) {
setLoading(false);
@@ -62,7 +63,7 @@ export const useSatellites = (observerLocation) => {
};
Object.entries(tleData).forEach(([name, tle]) => {
- // Server returns tle1/tle2, handle both formats
+ // Handle both line1/line2 and tle1/tle2 formats
const line1 = tle.line1 || tle.tle1;
const line2 = tle.line2 || tle.tle2;
if (!line1 || !line2) return;
@@ -94,6 +95,29 @@ export const useSatellites = (observerLocation) => {
// Only include if above horizon or popular sat
const isPopular = AMATEUR_SATS.some(s => name.includes(s));
if (elevation > -5 || isPopular) {
+ // Calculate orbit track (past 45 min and future 45 min = 90 min total)
+ const track = [];
+ const trackMinutes = 90;
+ const stepMinutes = 1;
+
+ for (let m = -trackMinutes/2; m <= trackMinutes/2; m += stepMinutes) {
+ const trackTime = new Date(now.getTime() + m * 60 * 1000);
+ const trackPV = satellite.propagate(satrec, trackTime);
+
+ if (trackPV.position) {
+ const trackGmst = satellite.gstime(trackTime);
+ const trackGd = satellite.eciToGeodetic(trackPV.position, trackGmst);
+ const trackLat = satellite.degreesLat(trackGd.latitude);
+ const trackLon = satellite.degreesLong(trackGd.longitude);
+ track.push([trackLat, trackLon]);
+ }
+ }
+
+ // Calculate footprint radius (visibility circle)
+ // Formula: radius = Earth_radius * arccos(Earth_radius / (Earth_radius + altitude))
+ const earthRadius = 6371; // km
+ const footprintRadius = earthRadius * Math.acos(earthRadius / (earthRadius + alt));
+
positions.push({
name,
lat,
@@ -103,7 +127,9 @@ export const useSatellites = (observerLocation) => {
elevation: Math.round(elevation),
range: Math.round(rangeSat),
visible: elevation > 0,
- isPopular
+ isPopular,
+ track,
+ footprintRadius: Math.round(footprintRadius)
});
}
} catch (e) {
@@ -113,7 +139,7 @@ export const useSatellites = (observerLocation) => {
// Sort by elevation (highest first) and limit
positions.sort((a, b) => b.elevation - a.elevation);
- setData(positions.slice(0, 20));
+ setData(positions.slice(0, 15));
setLoading(false);
} catch (err) {
console.error('Satellite calculation error:', err);