|
|
|
@ -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 = {
|
|
|
|
|