diff --git a/public/index.html b/public/index.html index 2c9174a..2223436 100644 --- a/public/index.html +++ b/public/index.html @@ -725,162 +725,245 @@ return { data, loading }; }; - 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 + // ============================================ + // SHARED FILTER HELPER FUNCTIONS + // Used by both DX Cluster hook and Map component + // ============================================ + + // Helper to get band from frequency (in kHz) + const getBandFromFreq = (freq) => { + const f = parseFloat(freq); + // Handle MHz input (convert to kHz) + const freqKhz = f < 1000 ? f * 1000 : f; + if (freqKhz >= 1800 && freqKhz <= 2000) return '160m'; + if (freqKhz >= 3500 && freqKhz <= 4000) return '80m'; + if (freqKhz >= 5330 && freqKhz <= 5405) return '60m'; + if (freqKhz >= 7000 && freqKhz <= 7300) return '40m'; + if (freqKhz >= 10100 && freqKhz <= 10150) return '30m'; + if (freqKhz >= 14000 && freqKhz <= 14350) return '20m'; + if (freqKhz >= 18068 && freqKhz <= 18168) return '17m'; + if (freqKhz >= 21000 && freqKhz <= 21450) return '15m'; + if (freqKhz >= 24890 && freqKhz <= 24990) return '12m'; + 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'; + }; + + // Helper to detect mode from comment + const detectMode = (comment) => { + if (!comment) return null; + const upper = comment.toUpperCase(); + if (upper.includes('FT8')) return 'FT8'; + if (upper.includes('FT4')) return 'FT4'; + if (upper.includes('CW')) return 'CW'; + if (upper.includes('SSB') || upper.includes('LSB') || upper.includes('USB')) return 'SSB'; + if (upper.includes('RTTY')) return 'RTTY'; + if (upper.includes('PSK')) return 'PSK'; + if (upper.includes('AM')) return 'AM'; + if (upper.includes('FM')) return 'FM'; + return null; + }; + + // Callsign prefix to CQ/ITU zone and continent mapping + const getCallsignInfo = (call) => { + if (!call) return { cqZone: null, ituZone: null, continent: null }; + const upper = call.toUpperCase(); - // Helper to get band from frequency - const getBandFromFreq = (freq) => { - if (freq >= 1800 && freq <= 2000) return '160m'; - if (freq >= 3500 && freq <= 4000) return '80m'; - if (freq >= 5330 && freq <= 5405) return '60m'; - if (freq >= 7000 && freq <= 7300) return '40m'; - if (freq >= 10100 && freq <= 10150) return '30m'; - if (freq >= 14000 && freq <= 14350) return '20m'; - if (freq >= 18068 && freq <= 18168) return '17m'; - if (freq >= 21000 && freq <= 21450) return '15m'; - if (freq >= 24890 && freq <= 24990) return '12m'; - if (freq >= 28000 && freq <= 29700) return '10m'; - if (freq >= 50000 && freq <= 54000) return '6m'; - if (freq >= 144000 && freq <= 148000) return '2m'; - if (freq >= 420000 && freq <= 450000) return '70cm'; - return 'other'; + // Simplified prefix->zone mapping (common prefixes) + const prefixMap = { + // North America + 'W': { cq: 5, itu: 8, cont: 'NA' }, 'K': { cq: 5, itu: 8, cont: 'NA' }, 'N': { cq: 5, itu: 8, cont: 'NA' }, 'AA': { cq: 5, itu: 8, cont: 'NA' }, + 'VE': { cq: 5, itu: 4, cont: 'NA' }, 'VA': { cq: 5, itu: 4, cont: 'NA' }, 'VY': { cq: 2, itu: 4, cont: 'NA' }, + 'XE': { cq: 6, itu: 10, cont: 'NA' }, 'XF': { cq: 6, itu: 10, cont: 'NA' }, + // Europe + 'G': { cq: 14, itu: 27, cont: 'EU' }, 'M': { cq: 14, itu: 27, cont: 'EU' }, '2E': { cq: 14, itu: 27, cont: 'EU' }, + 'F': { cq: 14, itu: 27, cont: 'EU' }, 'DL': { cq: 14, itu: 28, cont: 'EU' }, 'D': { cq: 14, itu: 28, cont: 'EU' }, + 'I': { cq: 15, itu: 28, cont: 'EU' }, 'IK': { cq: 15, itu: 28, cont: 'EU' }, + 'EA': { cq: 14, itu: 37, cont: 'EU' }, 'EC': { cq: 14, itu: 37, cont: 'EU' }, + 'PA': { cq: 14, itu: 27, cont: 'EU' }, 'PD': { cq: 14, itu: 27, cont: 'EU' }, + 'ON': { cq: 14, itu: 27, cont: 'EU' }, 'OZ': { cq: 14, itu: 18, cont: 'EU' }, + 'SM': { cq: 14, itu: 18, cont: 'EU' }, 'LA': { cq: 14, itu: 18, cont: 'EU' }, + 'OH': { cq: 15, itu: 18, cont: 'EU' }, 'SP': { cq: 15, itu: 28, cont: 'EU' }, + 'OK': { cq: 15, itu: 28, cont: 'EU' }, 'OM': { cq: 15, itu: 28, cont: 'EU' }, + 'HA': { cq: 15, itu: 28, cont: 'EU' }, 'HB': { cq: 14, itu: 28, cont: 'EU' }, + 'OE': { cq: 15, itu: 28, cont: 'EU' }, 'LZ': { cq: 20, itu: 28, cont: 'EU' }, + 'YO': { cq: 20, itu: 28, cont: 'EU' }, 'YU': { cq: 15, itu: 28, cont: 'EU' }, + 'UA': { cq: 16, itu: 29, cont: 'EU' }, 'R': { cq: 16, itu: 29, cont: 'EU' }, + 'UR': { cq: 16, itu: 29, cont: 'EU' }, 'UT': { cq: 16, itu: 29, cont: 'EU' }, + 'LY': { cq: 15, itu: 29, cont: 'EU' }, 'ES': { cq: 15, itu: 29, cont: 'EU' }, + 'YL': { cq: 15, itu: 29, cont: 'EU' }, 'CT': { cq: 14, itu: 37, cont: 'EU' }, + 'EI': { cq: 14, itu: 27, cont: 'EU' }, 'GI': { cq: 14, itu: 27, cont: 'EU' }, + 'GM': { cq: 14, itu: 27, cont: 'EU' }, 'GW': { cq: 14, itu: 27, cont: 'EU' }, + 'SV': { cq: 20, itu: 28, cont: 'EU' }, '9A': { cq: 15, itu: 28, cont: 'EU' }, + 'S5': { cq: 15, itu: 28, cont: 'EU' }, + // Asia + 'JA': { cq: 25, itu: 45, cont: 'AS' }, 'JH': { cq: 25, itu: 45, cont: 'AS' }, 'JR': { cq: 25, itu: 45, cont: 'AS' }, + 'HL': { cq: 25, itu: 44, cont: 'AS' }, 'DS': { cq: 25, itu: 44, cont: 'AS' }, + 'BY': { cq: 24, itu: 44, cont: 'AS' }, 'BV': { cq: 24, itu: 44, cont: 'AS' }, + 'VU': { cq: 22, itu: 41, cont: 'AS' }, 'VK': { cq: 30, itu: 59, cont: 'OC' }, + 'DU': { cq: 27, itu: 50, cont: 'OC' }, '9M': { cq: 28, itu: 54, cont: 'AS' }, + 'HS': { cq: 26, itu: 49, cont: 'AS' }, 'XV': { cq: 26, itu: 49, cont: 'AS' }, + // South America + 'LU': { cq: 13, itu: 14, cont: 'SA' }, 'PY': { cq: 11, itu: 15, cont: 'SA' }, + 'CE': { cq: 12, itu: 14, cont: 'SA' }, 'CX': { cq: 13, itu: 14, cont: 'SA' }, + 'HK': { cq: 9, itu: 12, cont: 'SA' }, 'YV': { cq: 9, itu: 12, cont: 'SA' }, + 'HC': { cq: 10, itu: 12, cont: 'SA' }, 'OA': { cq: 10, itu: 12, cont: 'SA' }, + // Africa + 'ZS': { cq: 38, itu: 57, cont: 'AF' }, '5N': { cq: 35, itu: 46, cont: 'AF' }, + 'EA8': { cq: 33, itu: 36, cont: 'AF' }, 'CN': { cq: 33, itu: 37, cont: 'AF' }, + '7X': { cq: 33, itu: 37, cont: 'AF' }, 'SU': { cq: 34, itu: 38, cont: 'AF' }, + 'ST': { cq: 34, itu: 47, cont: 'AF' }, 'ET': { cq: 37, itu: 48, cont: 'AF' }, + '5Z': { cq: 37, itu: 48, cont: 'AF' }, '5H': { cq: 37, itu: 53, cont: 'AF' }, + // Oceania + 'ZL': { cq: 32, itu: 60, cont: 'OC' }, 'FK': { cq: 32, itu: 56, cont: 'OC' }, + 'VK9': { cq: 30, itu: 60, cont: 'OC' }, 'YB': { cq: 28, itu: 51, cont: 'OC' }, + 'KH6': { cq: 31, itu: 61, cont: 'OC' }, 'KH2': { cq: 27, itu: 64, cont: 'OC' }, + // Caribbean + 'VP5': { cq: 8, itu: 11, cont: 'NA' }, 'PJ': { cq: 9, itu: 11, cont: 'SA' }, + 'HI': { cq: 8, itu: 11, cont: 'NA' }, 'CO': { cq: 8, itu: 11, cont: 'NA' }, + 'KP4': { cq: 8, itu: 11, cont: 'NA' }, 'FG': { cq: 8, itu: 11, cont: 'NA' }, + // Antarctica + 'DP0': { cq: 38, itu: 67, cont: 'AN' }, 'VP8': { cq: 13, itu: 73, cont: 'AN' }, + 'KC4': { cq: 13, itu: 67, cont: 'AN' } }; - // Helper to detect mode from comment - const detectMode = (comment) => { - if (!comment) return null; - const upper = comment.toUpperCase(); - if (upper.includes('FT8')) return 'FT8'; - if (upper.includes('FT4')) return 'FT4'; - if (upper.includes('CW')) return 'CW'; - if (upper.includes('SSB') || upper.includes('LSB') || upper.includes('USB')) return 'SSB'; - if (upper.includes('RTTY')) return 'RTTY'; - if (upper.includes('PSK')) return 'PSK'; - if (upper.includes('AM')) return 'AM'; - if (upper.includes('FM')) return 'FM'; - return null; + // Try to match prefix (longest match first) + for (let len = 4; len >= 1; len--) { + const prefix = upper.substring(0, len); + if (prefixMap[prefix]) { + return { + cqZone: prefixMap[prefix].cq, + ituZone: prefixMap[prefix].itu, + continent: prefixMap[prefix].cont + }; + } + } + + // Fallback based on first character + const firstChar = upper[0]; + const fallbackMap = { + 'A': { cq: 21, itu: 39, cont: 'AS' }, + 'B': { cq: 24, itu: 44, cont: 'AS' }, + 'C': { cq: 14, itu: 27, cont: 'EU' }, + 'D': { cq: 14, itu: 28, cont: 'EU' }, + 'E': { cq: 14, itu: 27, cont: 'EU' }, + 'F': { cq: 14, itu: 27, cont: 'EU' }, + 'G': { cq: 14, itu: 27, cont: 'EU' }, + 'H': { cq: 14, itu: 27, cont: 'EU' }, + 'I': { cq: 15, itu: 28, cont: 'EU' }, + 'J': { cq: 25, itu: 45, cont: 'AS' }, + 'K': { cq: 5, itu: 8, cont: 'NA' }, + 'L': { cq: 13, itu: 14, cont: 'SA' }, + 'M': { cq: 14, itu: 27, cont: 'EU' }, + 'N': { cq: 5, itu: 8, cont: 'NA' }, + 'O': { cq: 15, itu: 18, cont: 'EU' }, + 'P': { cq: 11, itu: 15, cont: 'SA' }, + 'R': { cq: 16, itu: 29, cont: 'EU' }, + 'S': { cq: 15, itu: 28, cont: 'EU' }, + 'T': { cq: 37, itu: 48, cont: 'AF' }, + 'U': { cq: 16, itu: 29, cont: 'EU' }, + 'V': { cq: 5, itu: 4, cont: 'NA' }, + 'W': { cq: 5, itu: 8, cont: 'NA' }, + 'X': { cq: 6, itu: 10, cont: 'NA' }, + 'Y': { cq: 15, itu: 28, cont: 'EU' }, + 'Z': { cq: 38, itu: 57, cont: 'AF' } }; - // Callsign prefix to CQ/ITU zone and continent mapping - const getCallsignInfo = (call) => { - if (!call) return { cqZone: null, ituZone: null, continent: null }; - const upper = call.toUpperCase(); - - // Simplified prefix->zone mapping (common prefixes) - const prefixMap = { - // North America - 'W': { cq: 5, itu: 8, cont: 'NA' }, 'K': { cq: 5, itu: 8, cont: 'NA' }, 'N': { cq: 5, itu: 8, cont: 'NA' }, 'AA': { cq: 5, itu: 8, cont: 'NA' }, - 'VE': { cq: 5, itu: 4, cont: 'NA' }, 'VA': { cq: 5, itu: 4, cont: 'NA' }, 'VY': { cq: 2, itu: 4, cont: 'NA' }, - 'XE': { cq: 6, itu: 10, cont: 'NA' }, 'XF': { cq: 6, itu: 10, cont: 'NA' }, - // Europe - 'G': { cq: 14, itu: 27, cont: 'EU' }, 'M': { cq: 14, itu: 27, cont: 'EU' }, '2E': { cq: 14, itu: 27, cont: 'EU' }, - 'F': { cq: 14, itu: 27, cont: 'EU' }, 'DL': { cq: 14, itu: 28, cont: 'EU' }, 'D': { cq: 14, itu: 28, cont: 'EU' }, - 'I': { cq: 15, itu: 28, cont: 'EU' }, 'IK': { cq: 15, itu: 28, cont: 'EU' }, - 'EA': { cq: 14, itu: 37, cont: 'EU' }, 'EC': { cq: 14, itu: 37, cont: 'EU' }, - 'PA': { cq: 14, itu: 27, cont: 'EU' }, 'PD': { cq: 14, itu: 27, cont: 'EU' }, - 'ON': { cq: 14, itu: 27, cont: 'EU' }, 'OZ': { cq: 14, itu: 18, cont: 'EU' }, - 'SM': { cq: 14, itu: 18, cont: 'EU' }, 'LA': { cq: 14, itu: 18, cont: 'EU' }, - 'OH': { cq: 15, itu: 18, cont: 'EU' }, 'SP': { cq: 15, itu: 28, cont: 'EU' }, - 'OK': { cq: 15, itu: 28, cont: 'EU' }, 'OM': { cq: 15, itu: 28, cont: 'EU' }, - 'HA': { cq: 15, itu: 28, cont: 'EU' }, 'HB': { cq: 14, itu: 28, cont: 'EU' }, - 'OE': { cq: 15, itu: 28, cont: 'EU' }, 'LZ': { cq: 20, itu: 28, cont: 'EU' }, - 'YO': { cq: 20, itu: 28, cont: 'EU' }, 'YU': { cq: 15, itu: 28, cont: 'EU' }, - 'UA': { cq: 16, itu: 29, cont: 'EU' }, 'R': { cq: 16, itu: 29, cont: 'EU' }, - 'UR': { cq: 16, itu: 29, cont: 'EU' }, 'UT': { cq: 16, itu: 29, cont: 'EU' }, - 'LY': { cq: 15, itu: 29, cont: 'EU' }, 'ES': { cq: 15, itu: 29, cont: 'EU' }, - 'YL': { cq: 15, itu: 29, cont: 'EU' }, 'CT': { cq: 14, itu: 37, cont: 'EU' }, - 'EI': { cq: 14, itu: 27, cont: 'EU' }, 'GI': { cq: 14, itu: 27, cont: 'EU' }, - 'GM': { cq: 14, itu: 27, cont: 'EU' }, 'GW': { cq: 14, itu: 27, cont: 'EU' }, - 'SV': { cq: 20, itu: 28, cont: 'EU' }, '9A': { cq: 15, itu: 28, cont: 'EU' }, - 'S5': { cq: 15, itu: 28, cont: 'EU' }, - // Asia - 'JA': { cq: 25, itu: 45, cont: 'AS' }, 'JH': { cq: 25, itu: 45, cont: 'AS' }, 'JR': { cq: 25, itu: 45, cont: 'AS' }, - 'HL': { cq: 25, itu: 44, cont: 'AS' }, 'DS': { cq: 25, itu: 44, cont: 'AS' }, - 'BY': { cq: 24, itu: 44, cont: 'AS' }, 'BV': { cq: 24, itu: 44, cont: 'AS' }, - 'VU': { cq: 22, itu: 41, cont: 'AS' }, 'VK': { cq: 30, itu: 59, cont: 'OC' }, - 'DU': { cq: 27, itu: 50, cont: 'OC' }, '9M': { cq: 28, itu: 54, cont: 'AS' }, - 'HS': { cq: 26, itu: 49, cont: 'AS' }, 'XV': { cq: 26, itu: 49, cont: 'AS' }, - // South America - 'LU': { cq: 13, itu: 14, cont: 'SA' }, 'PY': { cq: 11, itu: 15, cont: 'SA' }, - 'CE': { cq: 12, itu: 14, cont: 'SA' }, 'CX': { cq: 13, itu: 14, cont: 'SA' }, - 'HK': { cq: 9, itu: 12, cont: 'SA' }, 'YV': { cq: 9, itu: 12, cont: 'SA' }, - 'HC': { cq: 10, itu: 12, cont: 'SA' }, 'OA': { cq: 10, itu: 12, cont: 'SA' }, - // Africa - 'ZS': { cq: 38, itu: 57, cont: 'AF' }, '5N': { cq: 35, itu: 46, cont: 'AF' }, - 'EA8': { cq: 33, itu: 36, cont: 'AF' }, 'CN': { cq: 33, itu: 37, cont: 'AF' }, - '7X': { cq: 33, itu: 37, cont: 'AF' }, 'SU': { cq: 34, itu: 38, cont: 'AF' }, - 'ST': { cq: 34, itu: 47, cont: 'AF' }, 'ET': { cq: 37, itu: 48, cont: 'AF' }, - '5Z': { cq: 37, itu: 48, cont: 'AF' }, '5H': { cq: 37, itu: 53, cont: 'AF' }, - // Oceania - 'ZL': { cq: 32, itu: 60, cont: 'OC' }, 'FK': { cq: 32, itu: 56, cont: 'OC' }, - 'VK9': { cq: 30, itu: 60, cont: 'OC' }, 'YB': { cq: 28, itu: 51, cont: 'OC' }, - 'KH6': { cq: 31, itu: 61, cont: 'OC' }, 'KH2': { cq: 27, itu: 64, cont: 'OC' }, - // Caribbean - 'VP5': { cq: 8, itu: 11, cont: 'NA' }, 'PJ': { cq: 9, itu: 11, cont: 'SA' }, - 'HI': { cq: 8, itu: 11, cont: 'NA' }, 'CO': { cq: 8, itu: 11, cont: 'NA' }, - 'KP4': { cq: 8, itu: 11, cont: 'NA' }, 'FG': { cq: 8, itu: 11, cont: 'NA' }, - // Antarctica - 'DP0': { cq: 38, itu: 67, cont: 'AN' }, 'VP8': { cq: 13, itu: 73, cont: 'AN' }, - 'KC4': { cq: 13, itu: 67, cont: 'AN' } + if (fallbackMap[firstChar]) { + return { + cqZone: fallbackMap[firstChar].cq, + ituZone: fallbackMap[firstChar].itu, + continent: fallbackMap[firstChar].cont }; + } + + 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; + } - // Try to match prefix (longest match first) - for (let len = 4; len >= 1; len--) { - const prefix = upper.substring(0, len); - if (prefixMap[prefix]) { - return { - cqZone: prefixMap[prefix].cq, - ituZone: prefixMap[prefix].itu, - continent: prefixMap[prefix].cont - }; + // 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; } } - // Fallback based on first character - const firstChar = upper[0]; - const fallbackMap = { - 'A': { cq: 21, itu: 39, cont: 'AS' }, - 'B': { cq: 24, itu: 44, cont: 'AS' }, - 'C': { cq: 14, itu: 27, cont: 'EU' }, - 'D': { cq: 14, itu: 28, cont: 'EU' }, - 'E': { cq: 14, itu: 27, cont: 'EU' }, - 'F': { cq: 14, itu: 27, cont: 'EU' }, - 'G': { cq: 14, itu: 27, cont: 'EU' }, - 'H': { cq: 14, itu: 27, cont: 'EU' }, - 'I': { cq: 15, itu: 28, cont: 'EU' }, - 'J': { cq: 25, itu: 45, cont: 'AS' }, - 'K': { cq: 5, itu: 8, cont: 'NA' }, - 'L': { cq: 13, itu: 14, cont: 'SA' }, - 'M': { cq: 14, itu: 27, cont: 'EU' }, - 'N': { cq: 5, itu: 8, cont: 'NA' }, - 'O': { cq: 15, itu: 18, cont: 'EU' }, - 'P': { cq: 11, itu: 15, cont: 'SA' }, - 'R': { cq: 16, itu: 29, cont: 'EU' }, - 'S': { cq: 15, itu: 28, cont: 'EU' }, - 'T': { cq: 37, itu: 48, cont: 'AF' }, - 'U': { cq: 16, itu: 29, cont: 'EU' }, - 'V': { cq: 5, itu: 4, cont: 'NA' }, - 'W': { cq: 5, itu: 8, cont: 'NA' }, - 'X': { cq: 6, itu: 10, cont: 'NA' }, - 'Y': { cq: 15, itu: 28, cont: 'EU' }, - 'Z': { cq: 38, itu: 57, cont: 'AF' } - }; + // ITU Zone filter (applies to DX station) + if (filters.ituZones?.length > 0) { + if (!dxCallInfo.ituZone || !filters.ituZones.includes(dxCallInfo.ituZone)) { + return false; + } + } - if (fallbackMap[firstChar]) { - return { - cqZone: fallbackMap[firstChar].cq, - ituZone: fallbackMap[firstChar].itu, - continent: fallbackMap[firstChar].cont - }; + // Continent filter (applies to DX station) + if (filters.continents?.length > 0) { + if (!dxCallInfo.continent || !filters.continents.includes(dxCallInfo.continent)) { + return false; + } } - return { cqZone: null, ituZone: null, continent: null }; - }; + // 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 const applyFilters = useCallback((spots, filters) => { @@ -2064,7 +2147,7 @@ // ============================================ // 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 mapInstanceRef = useRef(null); const tileLayerRef = useRef(null); @@ -2310,7 +2393,10 @@ // Add new DX paths if enabled if (showDXPaths && dxPaths && dxPaths.length > 0) { - dxPaths.forEach((path, index) => { + // Apply filters to paths + const filteredPaths = filterDXPaths(dxPaths, dxFilters); + + filteredPaths.forEach((path, index) => { try { // Skip if missing or invalid coordinates if (!path.spotterLat || !path.spotterLon || !path.dxLat || !path.dxLon) return; @@ -2421,7 +2507,7 @@ } }); } - }, [dxPaths, showDXPaths]); + }, [dxPaths, dxFilters, showDXPaths]); // Update POTA markers useEffect(() => { @@ -3968,6 +4054,7 @@ potaSpots={potaSpots.data} mySpots={mySpots.data} dxPaths={dxPaths.data} + dxFilters={dxFilters} satellites={satellites.positions} showDXPaths={mapLayers.showDXPaths} showPOTA={mapLayers.showPOTA} @@ -4987,7 +5074,8 @@ onDXChange={handleDXChange} potaSpots={potaSpots.data} mySpots={mySpots.data} - dxPaths={dxPaths.data} + dxPaths={dxPaths.data} + dxFilters={dxFilters} satellites={satellites.positions} showDXPaths={mapLayers.showDXPaths} showPOTA={mapLayers.showPOTA}