From ddaf46c83a59d597fc88c44b64603af86b15de1d Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 23 Mar 2026 21:04:09 -0400 Subject: [PATCH] fix: use DXpedition entity coordinates for active DXpedition spots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DXpedition callsigns like TX5EU (Austral Islands) were showing at the generic prefix location (TX → France) because CTY.DAT maps TX to Metropolitan France. Now cross-references DX cluster spots against the active DXpedition list from NG3K. If a spotted callsign matches a known active DXpedition, uses the DXpedition's DXCC entity coordinates from cty.dat instead of the generic prefix. This runs after grid square resolution but before HamQTH/prefix fallbacks. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/routes/dxcluster.js | 36 ++++++++++++++++++++++++++++++++++++ server/routes/dxpeditions.js | 6 +++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/server/routes/dxcluster.js b/server/routes/dxcluster.js index f9a03260..68782948 100644 --- a/server/routes/dxcluster.js +++ b/server/routes/dxcluster.js @@ -39,6 +39,34 @@ module.exports = function (app, ctx) { const CALLSIGN_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours + // Cross-reference a callsign against active DXpeditions. + // Returns { lat, lon, entity } if the call matches a known DXpedition, + // using the DXpedition's DXCC entity coordinates from cty.dat. + const { lookupCall } = require('../../src/server/ctydat.js'); + + function lookupDXpeditionLocation(call) { + const cache = ctx.dxpeditionCache; + if (!cache?.data?.dxpeditions) return null; + const upper = (call || '').toUpperCase(); + const dxped = cache.data.dxpeditions.find((d) => d.isActive && d.callsign?.toUpperCase() === upper); + if (!dxped || !dxped.entity) return null; + + // Look up the DXpedition entity in cty.dat's entity list + const { getCtyData } = require('../../src/server/ctydat.js'); + const cty = getCtyData(); + if (!cty?.entities) return null; + + const entityName = dxped.entity.toLowerCase().replace(/\s+/g, ' ').trim(); + const match = cty.entities.find((e) => { + const eName = (e.entity || '').toLowerCase().replace(/\s+/g, ' ').trim(); + return eName === entityName || eName.includes(entityName) || entityName.includes(eName); + }); + if (match && match.lat != null && match.lon != null) { + return { lat: match.lat, lon: match.lon, country: match.entity, source: 'dxpedition' }; + } + return null; + } + // DX Spider Proxy URL (sibling service on Railway or external) const DXSPIDER_PROXY_URL = process.env.DXSPIDER_PROXY_URL || 'https://spider-production-1ec7.up.railway.app'; @@ -1651,6 +1679,14 @@ module.exports = function (app, ctx) { } } + // Check if this callsign is a known active DXpedition — use entity coordinates + if (!dxLoc) { + const dxpedLoc = lookupDXpeditionLocation(spot.dxCall); + if (dxpedLoc) { + dxLoc = dxpedLoc; + } + } + // Fall back to HamQTH cached location (more accurate than prefix) // HamQTH uses home callsign — but for portable ops, prefix location wins if (!dxLoc && hamqthLocations[baseCallMap[spot.dxCall] || spot.dxCall]) { diff --git a/server/routes/dxpeditions.js b/server/routes/dxpeditions.js index e9df4430..5eab9b12 100644 --- a/server/routes/dxpeditions.js +++ b/server/routes/dxpeditions.js @@ -7,7 +7,11 @@ module.exports = function (app, ctx) { const { fetch, logDebug, logErrorOnce } = ctx; // DXpedition Calendar - fetches from NG3K ADXO plain text version - let dxpeditionCache = { data: null, timestamp: 0, maxAge: 30 * 60 * 1000 }; // 30 min cache + const dxpeditionCache = { data: null, timestamp: 0, maxAge: 30 * 60 * 1000 }; // 30 min cache + + // Expose cache so dxcluster.js can cross-reference spotted callsigns + // against active DXpeditions for accurate entity coordinates + ctx.dxpeditionCache = dxpeditionCache; app.get('/api/dxpeditions', async (req, res) => { try {