|
|
|
@ -725,29 +725,29 @@
|
|
|
|
return { data, loading };
|
|
|
|
return { data, loading };
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const useDXCluster = (source = 'auto', filters = {}) => {
|
|
|
|
// ============================================
|
|
|
|
const [allSpots, setAllSpots] = useState([]); // All accumulated spots
|
|
|
|
// SHARED FILTER HELPER FUNCTIONS
|
|
|
|
const [data, setData] = useState([]); // Filtered spots for display
|
|
|
|
// Used by both DX Cluster hook and Map component
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// ============================================
|
|
|
|
const [activeSource, setActiveSource] = useState('');
|
|
|
|
|
|
|
|
const spotRetentionMs = 30 * 60 * 1000; // 30 minutes
|
|
|
|
|
|
|
|
const pollInterval = 5000; // 5 seconds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Helper to get band from frequency
|
|
|
|
// Helper to get band from frequency (in kHz)
|
|
|
|
const getBandFromFreq = (freq) => {
|
|
|
|
const getBandFromFreq = (freq) => {
|
|
|
|
if (freq >= 1800 && freq <= 2000) return '160m';
|
|
|
|
const f = parseFloat(freq);
|
|
|
|
if (freq >= 3500 && freq <= 4000) return '80m';
|
|
|
|
// Handle MHz input (convert to kHz)
|
|
|
|
if (freq >= 5330 && freq <= 5405) return '60m';
|
|
|
|
const freqKhz = f < 1000 ? f * 1000 : f;
|
|
|
|
if (freq >= 7000 && freq <= 7300) return '40m';
|
|
|
|
if (freqKhz >= 1800 && freqKhz <= 2000) return '160m';
|
|
|
|
if (freq >= 10100 && freq <= 10150) return '30m';
|
|
|
|
if (freqKhz >= 3500 && freqKhz <= 4000) return '80m';
|
|
|
|
if (freq >= 14000 && freq <= 14350) return '20m';
|
|
|
|
if (freqKhz >= 5330 && freqKhz <= 5405) return '60m';
|
|
|
|
if (freq >= 18068 && freq <= 18168) return '17m';
|
|
|
|
if (freqKhz >= 7000 && freqKhz <= 7300) return '40m';
|
|
|
|
if (freq >= 21000 && freq <= 21450) return '15m';
|
|
|
|
if (freqKhz >= 10100 && freqKhz <= 10150) return '30m';
|
|
|
|
if (freq >= 24890 && freq <= 24990) return '12m';
|
|
|
|
if (freqKhz >= 14000 && freqKhz <= 14350) return '20m';
|
|
|
|
if (freq >= 28000 && freq <= 29700) return '10m';
|
|
|
|
if (freqKhz >= 18068 && freqKhz <= 18168) return '17m';
|
|
|
|
if (freq >= 50000 && freq <= 54000) return '6m';
|
|
|
|
if (freqKhz >= 21000 && freqKhz <= 21450) return '15m';
|
|
|
|
if (freq >= 144000 && freq <= 148000) return '2m';
|
|
|
|
if (freqKhz >= 24890 && freqKhz <= 24990) return '12m';
|
|
|
|
if (freq >= 420000 && freq <= 450000) return '70cm';
|
|
|
|
if (freqKhz >= 28000 && freqKhz <= 29700) return '10m';
|
|
|
|
|
|
|
|
if (freqKhz >= 50000 && freqKhz <= 54000) return '6m';
|
|
|
|
|
|
|
|
if (freqKhz >= 144000 && freqKhz <= 148000) return '2m';
|
|
|
|
|
|
|
|
if (freqKhz >= 420000 && freqKhz <= 450000) return '70cm';
|
|
|
|
return 'other';
|
|
|
|
return 'other';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@ -882,6 +882,89 @@
|
|
|
|
return { cqZone: null, ituZone: null, continent: null };
|
|
|
|
return { cqZone: null, ituZone: null, continent: null };
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Filter DX paths based on filters (same logic as spot filtering)
|
|
|
|
|
|
|
|
const filterDXPaths = (paths, filters) => {
|
|
|
|
|
|
|
|
if (!paths || !filters) return paths;
|
|
|
|
|
|
|
|
if (Object.keys(filters).length === 0) return paths;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return paths.filter(path => {
|
|
|
|
|
|
|
|
// Get info for DX call (the target station)
|
|
|
|
|
|
|
|
const dxCallInfo = getCallsignInfo(path.dxCall);
|
|
|
|
|
|
|
|
// Get info for spotter (origin)
|
|
|
|
|
|
|
|
const spotterInfo = getCallsignInfo(path.spotter);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Watchlist filter - show ONLY watchlist if enabled
|
|
|
|
|
|
|
|
if (filters.watchlistOnly && filters.watchlist?.length > 0) {
|
|
|
|
|
|
|
|
const inWatchlist = filters.watchlist.some(w =>
|
|
|
|
|
|
|
|
path.dxCall?.toUpperCase().includes(w.toUpperCase()) ||
|
|
|
|
|
|
|
|
path.spotter?.toUpperCase().includes(w.toUpperCase())
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!inWatchlist) return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Exclude list - hide matching callsigns
|
|
|
|
|
|
|
|
if (filters.excludeList?.length > 0) {
|
|
|
|
|
|
|
|
const isExcluded = filters.excludeList.some(e =>
|
|
|
|
|
|
|
|
path.dxCall?.toUpperCase().includes(e.toUpperCase()) ||
|
|
|
|
|
|
|
|
path.spotter?.toUpperCase().includes(e.toUpperCase())
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
if (isExcluded) return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CQ Zone filter (applies to DX station)
|
|
|
|
|
|
|
|
if (filters.cqZones?.length > 0) {
|
|
|
|
|
|
|
|
if (!dxCallInfo.cqZone || !filters.cqZones.includes(dxCallInfo.cqZone)) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ITU Zone filter (applies to DX station)
|
|
|
|
|
|
|
|
if (filters.ituZones?.length > 0) {
|
|
|
|
|
|
|
|
if (!dxCallInfo.ituZone || !filters.ituZones.includes(dxCallInfo.ituZone)) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Continent filter (applies to DX station)
|
|
|
|
|
|
|
|
if (filters.continents?.length > 0) {
|
|
|
|
|
|
|
|
if (!dxCallInfo.continent || !filters.continents.includes(dxCallInfo.continent)) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Band filter
|
|
|
|
|
|
|
|
if (filters.bands?.length > 0) {
|
|
|
|
|
|
|
|
const freqKhz = parseFloat(path.freq) * 1000; // Convert MHz to kHz
|
|
|
|
|
|
|
|
const band = getBandFromFreq(freqKhz);
|
|
|
|
|
|
|
|
if (!filters.bands.includes(band)) return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Mode filter
|
|
|
|
|
|
|
|
if (filters.modes?.length > 0) {
|
|
|
|
|
|
|
|
const mode = detectMode(path.comment);
|
|
|
|
|
|
|
|
if (!mode || !filters.modes.includes(mode)) return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Callsign search filter
|
|
|
|
|
|
|
|
if (filters.callsign && filters.callsign.trim()) {
|
|
|
|
|
|
|
|
const search = filters.callsign.trim().toUpperCase();
|
|
|
|
|
|
|
|
const matchesDX = path.dxCall?.toUpperCase().includes(search);
|
|
|
|
|
|
|
|
const matchesSpotter = path.spotter?.toUpperCase().includes(search);
|
|
|
|
|
|
|
|
if (!matchesDX && !matchesSpotter) return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const useDXCluster = (source = 'auto', filters = {}) => {
|
|
|
|
|
|
|
|
const [allSpots, setAllSpots] = useState([]); // All accumulated spots
|
|
|
|
|
|
|
|
const [data, setData] = useState([]); // Filtered spots for display
|
|
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
const [activeSource, setActiveSource] = useState('');
|
|
|
|
|
|
|
|
const spotRetentionMs = 30 * 60 * 1000; // 30 minutes
|
|
|
|
|
|
|
|
const pollInterval = 5000; // 5 seconds
|
|
|
|
|
|
|
|
|
|
|
|
// Apply filters to spots
|
|
|
|
// Apply filters to spots
|
|
|
|
const applyFilters = useCallback((spots, filters) => {
|
|
|
|
const applyFilters = useCallback((spots, filters) => {
|
|
|
|
if (!filters || Object.keys(filters).length === 0) return spots;
|
|
|
|
if (!filters || Object.keys(filters).length === 0) return spots;
|
|
|
|
@ -2064,7 +2147,7 @@
|
|
|
|
// ============================================
|
|
|
|
// ============================================
|
|
|
|
// LEAFLET MAP COMPONENT
|
|
|
|
// LEAFLET MAP COMPONENT
|
|
|
|
// ============================================
|
|
|
|
// ============================================
|
|
|
|
const WorldMap = ({ deLocation, dxLocation, onDXChange, potaSpots, mySpots, dxPaths, satellites, showDXPaths, showPOTA, showSatellites, onToggleSatellites }) => {
|
|
|
|
const WorldMap = ({ deLocation, dxLocation, onDXChange, potaSpots, mySpots, dxPaths, dxFilters, satellites, showDXPaths, showPOTA, showSatellites, onToggleSatellites }) => {
|
|
|
|
const mapRef = useRef(null);
|
|
|
|
const mapRef = useRef(null);
|
|
|
|
const mapInstanceRef = useRef(null);
|
|
|
|
const mapInstanceRef = useRef(null);
|
|
|
|
const tileLayerRef = useRef(null);
|
|
|
|
const tileLayerRef = useRef(null);
|
|
|
|
@ -2310,7 +2393,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
// Add new DX paths if enabled
|
|
|
|
// Add new DX paths if enabled
|
|
|
|
if (showDXPaths && dxPaths && dxPaths.length > 0) {
|
|
|
|
if (showDXPaths && dxPaths && dxPaths.length > 0) {
|
|
|
|
dxPaths.forEach((path, index) => {
|
|
|
|
// Apply filters to paths
|
|
|
|
|
|
|
|
const filteredPaths = filterDXPaths(dxPaths, dxFilters);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
filteredPaths.forEach((path, index) => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Skip if missing or invalid coordinates
|
|
|
|
// Skip if missing or invalid coordinates
|
|
|
|
if (!path.spotterLat || !path.spotterLon || !path.dxLat || !path.dxLon) return;
|
|
|
|
if (!path.spotterLat || !path.spotterLon || !path.dxLat || !path.dxLon) return;
|
|
|
|
@ -2421,7 +2507,7 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [dxPaths, showDXPaths]);
|
|
|
|
}, [dxPaths, dxFilters, showDXPaths]);
|
|
|
|
|
|
|
|
|
|
|
|
// Update POTA markers
|
|
|
|
// Update POTA markers
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
@ -3968,6 +4054,7 @@
|
|
|
|
potaSpots={potaSpots.data}
|
|
|
|
potaSpots={potaSpots.data}
|
|
|
|
mySpots={mySpots.data}
|
|
|
|
mySpots={mySpots.data}
|
|
|
|
dxPaths={dxPaths.data}
|
|
|
|
dxPaths={dxPaths.data}
|
|
|
|
|
|
|
|
dxFilters={dxFilters}
|
|
|
|
satellites={satellites.positions}
|
|
|
|
satellites={satellites.positions}
|
|
|
|
showDXPaths={mapLayers.showDXPaths}
|
|
|
|
showDXPaths={mapLayers.showDXPaths}
|
|
|
|
showPOTA={mapLayers.showPOTA}
|
|
|
|
showPOTA={mapLayers.showPOTA}
|
|
|
|
@ -4988,6 +5075,7 @@
|
|
|
|
potaSpots={potaSpots.data}
|
|
|
|
potaSpots={potaSpots.data}
|
|
|
|
mySpots={mySpots.data}
|
|
|
|
mySpots={mySpots.data}
|
|
|
|
dxPaths={dxPaths.data}
|
|
|
|
dxPaths={dxPaths.data}
|
|
|
|
|
|
|
|
dxFilters={dxFilters}
|
|
|
|
satellites={satellites.positions}
|
|
|
|
satellites={satellites.positions}
|
|
|
|
showDXPaths={mapLayers.showDXPaths}
|
|
|
|
showDXPaths={mapLayers.showDXPaths}
|
|
|
|
showPOTA={mapLayers.showPOTA}
|
|
|
|
showPOTA={mapLayers.showPOTA}
|
|
|
|
|