Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions server/routes/dxcluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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]) {
Expand Down
6 changes: 5 additions & 1 deletion server/routes/dxpeditions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading