fix sat paths, callsign blocks, filters

pull/6/head
accius 3 days ago
parent a19be94768
commit 1529207e7d

@ -6,6 +6,8 @@ import React, { useState } from 'react';
export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) => { export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) => {
const [activeTab, setActiveTab] = useState('zones'); const [activeTab, setActiveTab] = useState('zones');
const [newWatchlistCall, setNewWatchlistCall] = useState('');
const [newExcludeCall, setNewExcludeCall] = useState('');
if (!isOpen) return null; if (!isOpen) return null;
@ -93,6 +95,26 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
fontFamily: 'JetBrains Mono, monospace' fontFamily: 'JetBrains Mono, monospace'
}); });
const addToWatchlist = () => {
if (newWatchlistCall.trim()) {
const current = filters?.watchlist || [];
if (!current.includes(newWatchlistCall.toUpperCase())) {
onFilterChange({ ...filters, watchlist: [...current, newWatchlistCall.toUpperCase()] });
}
setNewWatchlistCall('');
}
};
const addToExclude = () => {
if (newExcludeCall.trim()) {
const current = filters?.excludeList || [];
if (!current.includes(newExcludeCall.toUpperCase())) {
onFilterChange({ ...filters, excludeList: [...current, newExcludeCall.toUpperCase()] });
}
setNewExcludeCall('');
}
};
const renderZonesTab = () => ( const renderZonesTab = () => (
<div> <div>
{/* Continents */} {/* Continents */}
@ -205,18 +227,7 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
</div> </div>
); );
const renderWatchlistTab = () => { const renderWatchlistTab = () => (
const [newCall, setNewCall] = useState('');
const addToWatchlist = () => {
if (newCall.trim()) {
const current = filters?.watchlist || [];
if (!current.includes(newCall.toUpperCase())) {
onFilterChange({ ...filters, watchlist: [...current, newCall.toUpperCase()] });
}
setNewCall('');
}
};
return (
<div> <div>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}> <div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}>
@ -225,8 +236,8 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
<div style={{ display: 'flex', gap: '8px' }}> <div style={{ display: 'flex', gap: '8px' }}>
<input <input
type="text" type="text"
value={newCall} value={newWatchlistCall}
onChange={(e) => setNewCall(e.target.value.toUpperCase())} onChange={(e) => setNewWatchlistCall(e.target.value.toUpperCase())}
onKeyPress={(e) => e.key === 'Enter' && addToWatchlist()} onKeyPress={(e) => e.key === 'Enter' && addToWatchlist()}
placeholder="Enter callsign..." placeholder="Enter callsign..."
style={{ style={{
@ -263,20 +274,8 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
</div> </div>
</div> </div>
); );
};
const renderExcludeTab = () => { const renderExcludeTab = () => (
const [newCall, setNewCall] = useState('');
const addToExclude = () => {
if (newCall.trim()) {
const current = filters?.excludeList || [];
if (!current.includes(newCall.toUpperCase())) {
onFilterChange({ ...filters, excludeList: [...current, newCall.toUpperCase()] });
}
setNewCall('');
}
};
return (
<div> <div>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}> <div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}>
@ -285,8 +284,8 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
<div style={{ display: 'flex', gap: '8px' }}> <div style={{ display: 'flex', gap: '8px' }}>
<input <input
type="text" type="text"
value={newCall} value={newExcludeCall}
onChange={(e) => setNewCall(e.target.value.toUpperCase())} onChange={(e) => setNewExcludeCall(e.target.value.toUpperCase())}
onKeyPress={(e) => e.key === 'Enter' && addToExclude()} onKeyPress={(e) => e.key === 'Enter' && addToExclude()}
placeholder="Enter callsign..." placeholder="Enter callsign..."
style={{ style={{
@ -313,7 +312,6 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
</div> </div>
</div> </div>
); );
};
return ( return (
<div style={{ <div style={{

@ -290,8 +290,9 @@ export const WorldMap = ({
if (showDXLabels || isHovered) { if (showDXLabels || isHovered) {
const labelIcon = L.divIcon({ const labelIcon = L.divIcon({
className: '', className: '',
html: `<div style="background: ${isHovered ? '#fff' : color}; color: ${isHovered ? color : '#000'}; padding: 3px 8px; border-radius: 4px; font-family: JetBrains Mono; font-size: 11px; font-weight: 700; white-space: nowrap; border: 1px solid ${isHovered ? color : 'rgba(0,0,0,0.3)'};">${path.dxCall}</div>`, html: `<span style="display:inline-block;background:${isHovered ? '#fff' : color};color:${isHovered ? color : '#000'};padding:4px 8px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;white-space:nowrap;border:2px solid ${isHovered ? color : 'rgba(0,0,0,0.5)'};box-shadow:0 2px 4px rgba(0,0,0,0.4);">${path.dxCall}</span>`,
iconAnchor: [0, -10] iconSize: null,
iconAnchor: [0, 0]
}); });
const label = L.marker([path.dxLat, path.dxLon], { icon: labelIcon, interactive: false }).addTo(map); const label = L.marker([path.dxLat, path.dxLon], { icon: labelIcon, interactive: false }).addTo(map);
dxPathsMarkersRef.current.push(label); dxPathsMarkersRef.current.push(label);
@ -316,8 +317,9 @@ export const WorldMap = ({
if (spot.lat && spot.lon) { if (spot.lat && spot.lon) {
const icon = L.divIcon({ const icon = L.divIcon({
className: '', className: '',
html: `<div style="background: #aa66ff; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-family: JetBrains Mono; white-space: nowrap; border: 1px solid white;">${spot.call}</div>`, html: `<span style="display:inline-block;background:#aa66ff;color:#fff;padding:4px 8px;border-radius:4px;font-size:12px;font-family:'JetBrains Mono',monospace;font-weight:700;white-space:nowrap;border:2px solid #fff;box-shadow:0 2px 4px rgba(0,0,0,0.4);">${spot.call}</span>`,
iconAnchor: [20, 10] iconSize: null,
iconAnchor: [0, 0]
}); });
const marker = L.marker([spot.lat, spot.lon], { icon }) const marker = L.marker([spot.lat, spot.lon], { icon })
.bindPopup(`<b>${spot.call}</b><br>${spot.ref}<br>${spot.freq} ${spot.mode}`) .bindPopup(`<b>${spot.call}</b><br>${spot.ref}<br>${spot.freq} ${spot.mode}`)
@ -328,7 +330,7 @@ export const WorldMap = ({
} }
}, [potaSpots, showPOTA]); }, [potaSpots, showPOTA]);
// Update satellite markers // Update satellite markers with orbit tracks
useEffect(() => { useEffect(() => {
if (!mapInstanceRef.current) return; if (!mapInstanceRef.current) return;
const map = mapInstanceRef.current; const map = mapInstanceRef.current;
@ -340,14 +342,70 @@ export const WorldMap = ({
if (showSatellites && satellites && satellites.length > 0) { if (showSatellites && satellites && satellites.length > 0) {
satellites.forEach(sat => { 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({ const icon = L.divIcon({
className: '', className: '',
html: `<div style="background: #00ffff; color: #000; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-family: JetBrains Mono; white-space: nowrap; border: 2px solid ${sat.visible ? '#fff' : 'rgba(255,255,255,0.3)'}; font-weight: bold; opacity: ${sat.visible ? 1 : 0.6};">🛰 ${sat.name}</div>`, html: `<span style="display:inline-block;background:${sat.visible ? '#00ffff' : '#006688'};color:${sat.visible ? '#000' : '#fff'};padding:4px 8px;border-radius:4px;font-size:11px;font-family:'JetBrains Mono',monospace;white-space:nowrap;border:2px solid ${sat.visible ? '#fff' : '#00aaaa'};font-weight:bold;box-shadow:0 2px 4px rgba(0,0,0,0.4);">🛰 ${sat.name}</span>`,
iconAnchor: [25, 12] iconSize: null,
iconAnchor: [0, 0]
}); });
const marker = L.marker([sat.lat, sat.lon], { icon }) const marker = L.marker([sat.lat, sat.lon], { icon })
.bindPopup(`<b>🛰 ${sat.name}</b><br>Alt: ${sat.alt} km<br>Az: ${sat.azimuth}° El: ${sat.elevation}°`) .bindPopup(`
<b>🛰 ${sat.name}</b><br>
<table style="font-size: 11px;">
<tr><td>Alt:</td><td>${sat.alt} km</td></tr>
<tr><td>Az:</td><td>${sat.azimuth}°</td></tr>
<tr><td>El:</td><td>${sat.elevation}°</td></tr>
<tr><td>Range:</td><td>${sat.range} km</td></tr>
<tr><td>Status:</td><td>${sat.visible ? '<span style="color:green">Visible</span>' : '<span style="color:gray">Below horizon</span>'}</td></tr>
</table>
`)
.addTo(map); .addTo(map);
satMarkersRef.current.push(marker); satMarkersRef.current.push(marker);
}); });

@ -1,6 +1,7 @@
/** /**
* useSatellites Hook * useSatellites Hook
* Tracks amateur radio satellites using TLE data and satellite.js * Tracks amateur radio satellites using TLE data and satellite.js
* Includes orbit track prediction
*/ */
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import * as satellite from 'satellite.js'; import * as satellite from 'satellite.js';
@ -43,7 +44,7 @@ export const useSatellites = (observerLocation) => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
// Calculate satellite positions // Calculate satellite positions and orbits
const calculatePositions = useCallback(() => { const calculatePositions = useCallback(() => {
if (!observerLocation || Object.keys(tleData).length === 0) { if (!observerLocation || Object.keys(tleData).length === 0) {
setLoading(false); setLoading(false);
@ -62,7 +63,7 @@ export const useSatellites = (observerLocation) => {
}; };
Object.entries(tleData).forEach(([name, tle]) => { 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 line1 = tle.line1 || tle.tle1;
const line2 = tle.line2 || tle.tle2; const line2 = tle.line2 || tle.tle2;
if (!line1 || !line2) return; if (!line1 || !line2) return;
@ -94,6 +95,29 @@ export const useSatellites = (observerLocation) => {
// Only include if above horizon or popular sat // Only include if above horizon or popular sat
const isPopular = AMATEUR_SATS.some(s => name.includes(s)); const isPopular = AMATEUR_SATS.some(s => name.includes(s));
if (elevation > -5 || isPopular) { 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({ positions.push({
name, name,
lat, lat,
@ -103,7 +127,9 @@ export const useSatellites = (observerLocation) => {
elevation: Math.round(elevation), elevation: Math.round(elevation),
range: Math.round(rangeSat), range: Math.round(rangeSat),
visible: elevation > 0, visible: elevation > 0,
isPopular isPopular,
track,
footprintRadius: Math.round(footprintRadius)
}); });
} }
} catch (e) { } catch (e) {
@ -113,7 +139,7 @@ export const useSatellites = (observerLocation) => {
// Sort by elevation (highest first) and limit // Sort by elevation (highest first) and limit
positions.sort((a, b) => b.elevation - a.elevation); positions.sort((a, b) => b.elevation - a.elevation);
setData(positions.slice(0, 20)); setData(positions.slice(0, 15));
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
console.error('Satellite calculation error:', err); console.error('Satellite calculation error:', err);

Loading…
Cancel
Save

Powered by TurnKey Linux.