fixed dx paths

pull/27/head
accius 4 days ago
parent 8c24eed8f1
commit 44e5c9ddba

@ -285,6 +285,35 @@
margin-bottom: 8px; margin-bottom: 8px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
/* DX Cluster map tooltips */
.dx-tooltip {
background: rgba(20, 20, 30, 0.95) !important;
border: 1px solid rgba(0, 170, 255, 0.5) !important;
border-radius: 4px !important;
padding: 4px 8px !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 11px !important;
color: #00aaff !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5) !important;
}
.dx-tooltip::before {
border-top-color: rgba(0, 170, 255, 0.5) !important;
}
/* Leaflet popup styling for DX spots */
.leaflet-popup-content-wrapper {
background: rgba(20, 20, 30, 0.95) !important;
border: 1px solid rgba(100, 100, 100, 0.5) !important;
border-radius: 8px !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5) !important;
}
.leaflet-popup-content {
margin: 10px 12px !important;
}
.leaflet-popup-tip {
background: rgba(20, 20, 30, 0.95) !important;
}
</style> </style>
</head> </head>
<body> <body>
@ -2326,26 +2355,66 @@
} }
}); });
// Add small markers at DX station end only (to reduce clutter) // Create popup content for spots
const dxIcon = L.divIcon({ const dxPopupContent = `
className: '', <div style="font-family: JetBrains Mono, monospace; font-size: 12px; min-width: 150px;">
html: `<div style="width: 6px; height: 6px; background: ${color}; border-radius: 50%; border: 1px solid white; box-shadow: 0 0 3px ${color};"></div>`, <div style="font-weight: bold; color: ${color}; font-size: 14px; margin-bottom: 4px;">${path.dxCall}</div>
iconSize: [6, 6], ${path.dxGrid ? `<div style="color: #00ff88; font-size: 11px;">📍 ${path.dxGrid}</div>` : ''}
iconAnchor: [3, 3] <div style="color: #aaa; margin-top: 4px;"><b>${path.freq} MHz</b></div>
}); <div style="color: #888; font-size: 11px; margin-top: 2px;">spotted by <b>${path.spotter}</b></div>
${path.comment ? `<div style="color: #666; font-size: 10px; margin-top: 4px; max-width: 180px; word-wrap: break-word;">${path.comment}</div>` : ''}
const marker = L.marker([path.dxLat, path.dxLon], { icon: dxIcon }) <div style="color: #555; font-size: 10px; margin-top: 4px;">${path.time}</div>
.bindPopup(` </div>
<div style="font-family: JetBrains Mono, monospace; font-size: 12px;"> `;
<b style="color: ${color}">${path.dxCall}</b><br>
<span style="color: #888">spotted by</span> <b>${path.spotter}</b><br> const spotterPopupContent = `
<b>${path.freq} MHz</b><br> <div style="font-family: JetBrains Mono, monospace; font-size: 12px; min-width: 150px;">
${path.comment || ''}<br> <div style="font-weight: bold; color: #00aaff; font-size: 14px; margin-bottom: 4px;">${path.spotter}</div>
<span style="color: #666">${path.time}</span> <div style="color: #888; font-size: 11px;">Spotter</div>
</div> <div style="color: #aaa; margin-top: 4px;">spotted <b style="color: ${color}">${path.dxCall}</b></div>
`) <div style="color: #666; font-size: 11px; margin-top: 2px;">on ${path.freq} MHz</div>
<div style="color: #555; font-size: 10px; margin-top: 4px;">${path.time}</div>
</div>
`;
// Add hoverable circle at DX station end
const dxCircle = L.circleMarker([path.dxLat, path.dxLon], {
radius: 6,
fillColor: color,
color: '#fff',
weight: 1.5,
opacity: 1,
fillOpacity: 0.9
})
.bindPopup(dxPopupContent)
.bindTooltip(path.dxCall, {
permanent: false,
direction: 'top',
offset: [0, -8],
className: 'dx-tooltip'
})
.addTo(map);
dxPathsMarkersRef.current.push(dxCircle);
// Add hoverable circle at spotter end (smaller, different style)
const spotterCircle = L.circleMarker([path.spotterLat, path.spotterLon], {
radius: 4,
fillColor: '#00aaff',
color: '#fff',
weight: 1,
opacity: 0.8,
fillOpacity: 0.7
})
.bindPopup(spotterPopupContent)
.bindTooltip(path.spotter, {
permanent: false,
direction: 'top',
offset: [0, -6],
className: 'dx-tooltip'
})
.addTo(map); .addTo(map);
dxPathsMarkersRef.current.push(marker); dxPathsMarkersRef.current.push(spotterCircle);
} catch (err) { } catch (err) {
console.error('[DX Paths] Error rendering path:', err, path); console.error('[DX Paths] Error rendering path:', err, path);
} }

@ -801,33 +801,54 @@ app.get('/api/dxcluster/paths', async (req, res) => {
allCalls.add(s.dxCall); allCalls.add(s.dxCall);
}); });
// Look up locations for all callsigns // Look up locations for all callsigns (fallback)
const locations = {}; const prefixLocations = {};
const callsToLookup = [...allCalls].slice(0, 40); const callsToLookup = [...allCalls].slice(0, 60);
for (const call of callsToLookup) { for (const call of callsToLookup) {
const loc = estimateLocationFromPrefix(call); const loc = estimateLocationFromPrefix(call);
if (loc) { if (loc) {
locations[call] = { lat: loc.lat, lon: loc.lon, country: loc.country }; prefixLocations[call] = { lat: loc.lat, lon: loc.lon, country: loc.country, source: 'prefix' };
} }
} }
// Build new paths with locations // Build new paths with locations - try grid from comment first
const newPaths = newSpots const newPaths = newSpots
.map(spot => { .map(spot => {
const spotterLoc = locations[spot.spotter]; // Try to extract grid from comment for DX station location
const dxLoc = locations[spot.dxCall]; const dxGrid = extractGridFromComment(spot.comment);
let dxLoc = null;
let dxGridSquare = null;
if (dxGrid) {
const gridLoc = maidenheadToLatLon(dxGrid);
if (gridLoc) {
dxLoc = { lat: gridLoc.lat, lon: gridLoc.lon, country: '', source: 'grid' };
dxGridSquare = dxGrid;
}
}
// Fall back to prefix location if no grid
if (!dxLoc) {
dxLoc = prefixLocations[spot.dxCall];
}
// Spotter location from prefix (usually no grid available)
const spotterLoc = prefixLocations[spot.spotter];
if (spotterLoc && dxLoc) { if (spotterLoc && dxLoc) {
return { return {
spotter: spot.spotter, spotter: spot.spotter,
spotterLat: spotterLoc.lat, spotterLat: spotterLoc.lat,
spotterLon: spotterLoc.lon, spotterLon: spotterLoc.lon,
spotterCountry: spotterLoc.country, spotterCountry: spotterLoc.country || '',
spotterLocSource: spotterLoc.source,
dxCall: spot.dxCall, dxCall: spot.dxCall,
dxLat: dxLoc.lat, dxLat: dxLoc.lat,
dxLon: dxLoc.lon, dxLon: dxLoc.lon,
dxCountry: dxLoc.country, dxCountry: dxLoc.country || '',
dxGrid: dxGridSquare,
dxLocSource: dxLoc.source,
freq: spot.freq, freq: spot.freq,
comment: spot.comment, comment: spot.comment,
time: spot.time, time: spot.time,
@ -927,6 +948,76 @@ app.get('/api/callsign/:call', async (req, res) => {
} }
}); });
// Convert Maidenhead grid locator to lat/lon (center of grid square)
function maidenheadToLatLon(grid) {
if (!grid || typeof grid !== 'string') return null;
grid = grid.toUpperCase().trim();
// Validate grid format (2, 4, 6, or 8 characters)
if (!/^[A-R]{2}([0-9]{2}([A-X]{2}([0-9]{2})?)?)?$/.test(grid)) return null;
let lon = -180;
let lat = -90;
// Field (2 chars): 20° lon x 10° lat
lon += (grid.charCodeAt(0) - 65) * 20;
lat += (grid.charCodeAt(1) - 65) * 10;
if (grid.length >= 4) {
// Square (2 digits): 2° lon x 1° lat
lon += parseInt(grid[2]) * 2;
lat += parseInt(grid[3]) * 1;
}
if (grid.length >= 6) {
// Subsquare (2 chars): 5' lon x 2.5' lat
lon += (grid.charCodeAt(4) - 65) * (5 / 60);
lat += (grid.charCodeAt(5) - 65) * (2.5 / 60);
}
if (grid.length >= 8) {
// Extended square (2 digits): 0.5' lon x 0.25' lat
lon += parseInt(grid[6]) * (0.5 / 60);
lat += parseInt(grid[7]) * (0.25 / 60);
}
// Add offset to center of the grid square
if (grid.length === 2) {
lon += 10; lat += 5;
} else if (grid.length === 4) {
lon += 1; lat += 0.5;
} else if (grid.length === 6) {
lon += 2.5 / 60; lat += 1.25 / 60;
} else if (grid.length === 8) {
lon += 0.25 / 60; lat += 0.125 / 60;
}
return { lat, lon, grid };
}
// Try to extract a grid locator from a comment string
function extractGridFromComment(comment) {
if (!comment || typeof comment !== 'string') return null;
// Look for 4 or 6 character grid squares (most common)
// Pattern: 2 letters + 2 digits + optional 2 letters
const match = comment.match(/\b([A-Ra-r]{2}[0-9]{2}(?:[A-Xa-x]{2})?)\b/);
if (match) {
const grid = match[1].toUpperCase();
// Validate it's a reasonable grid (not something like "CQ00" or "DE12")
const firstChar = grid.charCodeAt(0);
const secondChar = grid.charCodeAt(1);
// First char should be A-R, second char should be A-R
if (firstChar >= 65 && firstChar <= 82 && secondChar >= 65 && secondChar <= 82) {
return grid;
}
}
return null;
}
// Estimate location from callsign prefix (fallback) // Estimate location from callsign prefix (fallback)
function estimateLocationFromPrefix(callsign) { function estimateLocationFromPrefix(callsign) {
const prefixLocations = { const prefixLocations = {

Loading…
Cancel
Save

Powered by TurnKey Linux.