From 40a3a5e6a33dbedd00e2cda676aaebcc852428f4 Mon Sep 17 00:00:00 2001 From: accius Date: Sat, 31 Jan 2026 21:31:24 -0500 Subject: [PATCH] highlight mode --- public/index.html | 173 ++++++++++++++++++++++++++++++++-------------- server.js | 40 ++++++++++- 2 files changed, 162 insertions(+), 51 deletions(-) diff --git a/public/index.html b/public/index.html index 869a933..4da5977 100644 --- a/public/index.html +++ b/public/index.html @@ -307,6 +307,17 @@ .dx-tooltip::before { border-top-color: rgba(0, 170, 255, 0.5) !important; } + .dx-tooltip-highlighted { + background: rgba(68, 136, 255, 0.95) !important; + border: 2px solid #ffffff !important; + color: #ffffff !important; + font-weight: bold !important; + font-size: 13px !important; + box-shadow: 0 4px 16px rgba(68, 136, 255, 0.8) !important; + } + .dx-tooltip-highlighted::before { + border-top-color: #ffffff !important; + } /* Leaflet popup styling for DX spots */ .leaflet-popup-content-wrapper { @@ -2245,7 +2256,7 @@ // ============================================ // LEAFLET MAP COMPONENT // ============================================ - const WorldMap = ({ deLocation, dxLocation, onDXChange, potaSpots, mySpots, dxPaths, dxFilters, satellites, showDXPaths, showDXLabels, onToggleDXLabels, showPOTA, showSatellites, onToggleSatellites }) => { + const WorldMap = ({ deLocation, dxLocation, onDXChange, potaSpots, mySpots, dxPaths, dxFilters, satellites, showDXPaths, showDXLabels, onToggleDXLabels, showPOTA, showSatellites, onToggleSatellites, hoveredSpot }) => { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const tileLayerRef = useRef(null); @@ -2573,6 +2584,10 @@ else if (freq >= 28 && freq < 30) color = '#9966ff'; // 10m - purple else if (freq >= 50 && freq < 54) color = '#ff66ff'; // 6m - magenta + // Check if this path is hovered + const isHovered = hoveredSpot && hoveredSpot.call === path.dxCall && + Math.abs(parseFloat(hoveredSpot.freq) - parseFloat(path.freq)) < 0.01; + // Handle antimeridian crossing - pathPoints may be array of segments or single segment // Check if first element's first element is an array (segment structure) vs just [lat, lon] const isSegmented = Array.isArray(pathPoints[0]) && pathPoints[0].length > 0 && Array.isArray(pathPoints[0][0]); @@ -2581,10 +2596,13 @@ segments.forEach(segment => { if (segment && Array.isArray(segment) && segment.length > 1) { const line = L.polyline(segment, { - color: color, - weight: 1.5, - opacity: 0.5 + color: isHovered ? '#ffffff' : color, + weight: isHovered ? 4 : 1.5, + opacity: isHovered ? 1 : 0.5 }).addTo(map); + if (isHovered) { + line.bringToFront(); + } dxPathsLinesRef.current.push(line); } }); @@ -2614,40 +2632,46 @@ // Add hoverable circle at DX station end const dxCircle = L.circleMarker([path.dxLat, path.dxLon], { - radius: 6, - fillColor: color, - color: '#fff', - weight: 1.5, + radius: isHovered ? 10 : 6, + fillColor: isHovered ? '#ffffff' : color, + color: isHovered ? color : '#fff', + weight: isHovered ? 3 : 1.5, opacity: 1, - fillOpacity: 0.9 + fillOpacity: isHovered ? 1 : 0.9 }) .bindPopup(dxPopupContent) .bindTooltip(path.dxCall, { - permanent: showDXLabels, + permanent: showDXLabels || isHovered, direction: 'top', offset: [0, -8], - className: 'dx-tooltip' + className: isHovered ? 'dx-tooltip dx-tooltip-highlighted' : 'dx-tooltip' }) .addTo(map); + if (isHovered) { + dxCircle.bringToFront(); + } dxPathsMarkersRef.current.push(dxCircle); // Add hoverable circle at spotter end (smaller, different style) - no permanent label const spotterCircle = L.circleMarker([path.spotterLat, path.spotterLon], { - radius: 4, - fillColor: '#00aaff', - color: '#fff', - weight: 1, - opacity: 0.8, - fillOpacity: 0.7 + radius: isHovered ? 6 : 4, + fillColor: isHovered ? '#ffffff' : '#00aaff', + color: isHovered ? '#00aaff' : '#fff', + weight: isHovered ? 2 : 1, + opacity: isHovered ? 1 : 0.8, + fillOpacity: isHovered ? 1 : 0.7 }) .bindPopup(spotterPopupContent) .bindTooltip(path.spotter, { - permanent: false, // Always hover-only for spotter + permanent: isHovered, // Show on hover from list direction: 'top', offset: [0, -6], className: 'dx-tooltip' }) .addTo(map); + if (isHovered) { + spotterCircle.bringToFront(); + } dxPathsMarkersRef.current.push(spotterCircle); } catch (err) { @@ -2655,7 +2679,7 @@ } }); } - }, [dxPaths, dxFilters, showDXPaths, showDXLabels]); + }, [dxPaths, dxFilters, showDXPaths, showDXLabels, hoveredSpot]); // Update POTA markers useEffect(() => { @@ -3656,7 +3680,7 @@ }; // Simple DX Cluster Panel (for sidebars) - const DXClusterPanel = ({ spots, loading, activeSource, showOnMap, onToggleMap, spotCount, filteredCount, filters, onFilterChange, onOpenFilters }) => { + const DXClusterPanel = ({ spots, loading, activeSource, showOnMap, onToggleMap, spotCount, filteredCount, filters, onFilterChange, onOpenFilters, hoveredSpot, onHoverSpot }) => { const hasActiveFilters = (filters?.bands?.length > 0) || (filters?.modes?.length > 0) || (filters?.cqZones?.length > 0) || (filters?.ituZones?.length > 0) || (filters?.continents?.length > 0) || (filters?.watchlist?.length > 0) || @@ -3721,22 +3745,29 @@
{spots.length > 0 ? spots.map((s, i) => { const isHighlighted = isWatchlistMatch(s); + const isHovered = hoveredSpot && hoveredSpot.call === s.call && hoveredSpot.freq === s.freq; return ( -
+
onHoverSpot && onHoverSpot(s)} + onMouseLeave={() => onHoverSpot && onHoverSpot(null)} + style={{ + display: 'grid', + gridTemplateColumns: '65px 75px 1fr auto', + gap: '10px', + padding: '8px 4px', + borderBottom: '1px solid rgba(255,255,255,0.03)', + fontFamily: 'JetBrains Mono, monospace', + fontSize: '13px', + alignItems: 'center', + background: isHovered ? 'rgba(68, 136, 255, 0.3)' : isHighlighted ? 'rgba(0, 255, 136, 0.15)' : 'transparent', + borderLeft: isHovered ? '3px solid #4488ff' : isHighlighted ? '3px solid #00ff88' : '3px solid transparent', + marginLeft: '-4px', + cursor: 'pointer', + transition: 'background 0.15s ease' + }}> {s.freq} - {s.call} + {s.call} {s.comment} {s.time}
@@ -4238,6 +4269,7 @@ showPOTA={mapLayers.showPOTA} showSatellites={mapLayers.showSatellites} onToggleSatellites={toggleSatellites} + hoveredSpot={hoveredSpot} />
@@ -4328,15 +4360,33 @@
)}
- {dxCluster.data.slice(0, 8).map((s, i) => ( -
-
- {s.freq} - {s.call} + {dxCluster.data.slice(0, 8).map((s, i) => { + const isHovered = hoveredSpot && hoveredSpot.call === s.call && hoveredSpot.freq === s.freq; + return ( +
setHoveredSpot(s)} + onMouseLeave={() => setHoveredSpot(null)} + style={{ + padding: '4px 0', + borderBottom: '1px solid rgba(255,255,255,0.05)', + fontSize: '12px', + background: isHovered ? 'rgba(68, 136, 255, 0.3)' : 'transparent', + borderLeft: isHovered ? '3px solid #4488ff' : '3px solid transparent', + paddingLeft: '4px', + marginLeft: '-4px', + cursor: 'pointer', + transition: 'background 0.15s ease' + }} + > +
+ {s.freq} + {s.call} +
+
{s.comment}
-
{s.comment}
-
- ))} + ); + })} {dxCluster.data.length === 0 &&
No spots match filter
}
@@ -4895,6 +4945,9 @@ } catch (e) { console.error('Failed to save map layers:', e); } }, [mapLayers]); + // Hovered spot state for highlighting paths on map + const [hoveredSpot, setHoveredSpot] = useState(null); + // Toggle handlers for map layers const toggleDXPaths = useCallback(() => setMapLayers(prev => ({ ...prev, showDXPaths: !prev.showDXPaths })), []); const toggleDXLabels = useCallback(() => setMapLayers(prev => ({ ...prev, showDXLabels: !prev.showDXLabels })), []); @@ -5262,6 +5315,7 @@ showPOTA={mapLayers.showPOTA} showSatellites={mapLayers.showSatellites} onToggleSatellites={toggleSatellites} + hoveredSpot={hoveredSpot} />
Click map to set DX • 73 de {config.callsign} @@ -5356,15 +5410,34 @@
)}
- {dxCluster.data.slice(0, 8).map((s, i) => ( -
-
- {s.freq} - {s.call} - {s.time} + {dxCluster.data.slice(0, 8).map((s, i) => { + const isHovered = hoveredSpot && hoveredSpot.call === s.call && hoveredSpot.freq === s.freq; + return ( +
setHoveredSpot(s)} + onMouseLeave={() => setHoveredSpot(null)} + style={{ + padding: '3px 0', + borderBottom: '1px solid rgba(255,255,255,0.05)', + fontSize: '12px', + fontFamily: 'JetBrains Mono', + background: isHovered ? 'rgba(68, 136, 255, 0.3)' : 'transparent', + borderLeft: isHovered ? '3px solid #4488ff' : '3px solid transparent', + paddingLeft: '4px', + marginLeft: '-4px', + cursor: 'pointer', + transition: 'background 0.15s ease' + }} + > +
+ {s.freq} + {s.call} + {s.time} +
-
- ))} + ); + })} {dxCluster.data.length === 0 &&
No spots
}
diff --git a/server.js b/server.js index 9d1da85..3e35d8f 100644 --- a/server.js +++ b/server.js @@ -1279,7 +1279,45 @@ function estimateLocationFromPrefix(callsign) { const upper = callsign.toUpperCase(); - // Try longest prefix match first (up to 4 chars) + // Smart US callsign detection - US prefixes follow specific patterns + // K, N, W + anything = USA + // A[A-L] + digit = USA (e.g., AA0, AE5, AL7) + const usCallPattern = /^([KNW][0-9]?|A[A-L][0-9])/; + const usMatch = upper.match(usCallPattern); + if (usMatch) { + // Extract call district (the digit) for more precise location + const districtMatch = upper.match(/^[KNWA][A-L]?([0-9])/); + const district = districtMatch ? districtMatch[1] : null; + + const usDistrictGrids = { + '0': 'EN31', // Central (CO, IA, KS, MN, MO, NE, ND, SD) + '1': 'FN41', // New England (CT, MA, ME, NH, RI, VT) + '2': 'FN20', // NY, NJ + '3': 'FM19', // PA, MD, DE + '4': 'EM73', // Southeast (AL, FL, GA, KY, NC, SC, TN, VA) + '5': 'EM12', // TX, OK, LA, AR, MS, NM + '6': 'CM97', // California + '7': 'DN31', // Pacific NW/Mountain (AZ, ID, MT, NV, OR, UT, WA, WY) + '8': 'EN81', // MI, OH, WV + '9': 'EN52', // IL, IN, WI + }; + + const grid = district && usDistrictGrids[district] ? usDistrictGrids[district] : 'EM79'; + const gridLoc = maidenheadToLatLon(grid); + if (gridLoc) { + return { + callsign, + lat: gridLoc.lat, + lon: gridLoc.lon, + grid: grid, + country: 'USA', + estimated: true, + source: 'prefix-grid' + }; + } + } + + // Try longest prefix match first (up to 4 chars) for non-US calls for (let len = 4; len >= 1; len--) { const prefix = upper.substring(0, len); if (prefixGrids[prefix]) {