Skip to content
Open
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
43 changes: 35 additions & 8 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9545,6 +9545,23 @@ function normalizeCallsign(value) {
return (value || '').trim().toUpperCase();
}

function parseXmlBoolean(value) {
if (value == null) return null;
const normalized = String(value).trim().toLowerCase();
if (!normalized) return null;
if (normalized === 'true' || normalized === '1' || normalized === 'yes') return true;
if (normalized === 'false' || normalized === '0' || normalized === 'no') return false;
return null;
}

function detectContestSource(xml) {
const app = getXmlTag(xml, 'app').toLowerCase();
const logger = getXmlTag(xml, 'logger').toLowerCase();
if (app.includes('dxlog') || logger.includes('dxlog')) return 'dxlog';
if (app.includes('n1mm') || logger.includes('n1mm')) return 'n1mm';
return 'contest';
}

function n1mmFreqToMHz(value, bandMHz) {
const v = parseFloat(value);
if (!v || Number.isNaN(v)) return bandMHz || null;
Expand Down Expand Up @@ -9637,10 +9654,11 @@ function addContestQso(qso) {
return true;
}

function parseN1MMContactInfo(xml) {
function parseContestContactInfo(xml) {
const dxCall = normalizeCallsign(getXmlTag(xml, 'call'));
if (!dxCall) return null;

const source = detectContestSource(xml);
const myCall = normalizeCallsign(getXmlTag(xml, 'mycall')) ||
normalizeCallsign(getXmlTag(xml, 'stationprefix')) ||
CONFIG.callsign;
Expand All @@ -9657,13 +9675,22 @@ function parseN1MMContactInfo(xml) {
const contestName = getXmlTag(xml, 'contestname') || '';
const timestampStr = getXmlTag(xml, 'timestamp') || '';
const timestamp = parseN1MMTimestamp(timestampStr) || Date.now();
const id = getXmlTag(xml, 'ID') || '';
const id = getXmlTag(xml, 'ID') || getXmlTag(xml, 'guid') || getXmlTag(xml, 'qsoid') || '';
const isNewQso = parseXmlBoolean(getXmlTag(xml, 'newqso'));
const isDuplicate = parseXmlBoolean(getXmlTag(xml, 'duplicate'));
const isInvalid = parseXmlBoolean(getXmlTag(xml, 'invalid'));
const isDeleted = parseXmlBoolean(getXmlTag(xml, 'xqso'));

// DXLog may send non-new / duplicate / invalid/deleted updates on the same stream.
// Only ingest genuine new, valid QSOs so the map and auto-DX-follow stay clean.
if (isNewQso === false) return null;
if (isDuplicate === true || isInvalid === true || isDeleted === true) return null;

const loc = resolveQsoLocation(dxCall, grid, comment);

const qso = {
id,
source: 'n1mm',
source,
timestamp,
time: timestampStr,
myCall,
Expand Down Expand Up @@ -9748,17 +9775,17 @@ if (N1MM_ENABLED) {
const text = buf.toString('utf8');
const xml = extractContactInfoXml(text);
if (!xml) return;
const qso = parseN1MMContactInfo(xml);
const qso = parseContestContactInfo(xml);
if (qso) addContestQso(qso);
});

n1mmSocket.on('error', (err) => {
logErrorOnce('N1MM UDP', err.message);
logErrorOnce('Contest UDP', err.message);
});

n1mmSocket.on('listening', () => {
const addr = n1mmSocket.address();
console.log(`[N1MM] UDP listener on ${addr.address}:${addr.port}`);
console.log(`[Contest UDP] listener on ${addr.address}:${addr.port}`);
});

n1mmSocket.bind(N1MM_UDP_PORT, '0.0.0.0');
Expand Down Expand Up @@ -9857,8 +9884,8 @@ app.listen(PORT, '0.0.0.0', () => {
if (WSJTX_RELAY_KEY) {
console.log(` 🔁 WSJT-X relay endpoint enabled (POST /api/wsjtx/relay)`);
}
if (N1MM_ENABLED) {
console.log(` 📥 N1MM UDP listener on port ${N1MM_UDP_PORT}`);
if (N1MM_ENABLED) {
console.log(` 📥 Contest logger UDP listener (N1MM/DXLog) on port ${N1MM_UDP_PORT}`);
}
if (AUTO_UPDATE_ENABLED) {
console.log(` 🔄 Auto-update enabled every ${AUTO_UPDATE_INTERVAL_MINUTES || 60} minutes`);
Expand Down
42 changes: 24 additions & 18 deletions src/components/PluginLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,42 @@
*/
import React from 'react';

export const PluginLayer = ({
plugin,
enabled,
opacity,
map,
callsign,
locator,
export const PluginLayer = ({
plugin,
enabled,
opacity,
map,
callsign,
locator,
lowMemoryMode,
satellites,
units,
config
config,
onDXChange,
dxLocked,
dxLocation
}) => {

const layerFunc = plugin.useLayer || plugin.hook;

if (typeof layerFunc === 'function') {
layerFunc({
map,
enabled,
opacity,
callsign,
locator,
lowMemoryMode,
satellites,
layerFunc({
map,
enabled,
opacity,
callsign,
locator,
lowMemoryMode,
satellites,
units,
config
config,
onDXChange,
dxLocked,
dxLocation
});
}

return null;
};

export default PluginLayer;
export default PluginLayer;
25 changes: 24 additions & 1 deletion src/components/WorldMap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,26 @@ export const WorldMap = ({

return (
<div style={{ position: 'relative', height: '100%', minHeight: '200px' }}>
<div ref={mapRef} style={{ height: '100%', width: '100%', borderRadius: '8px', background: mapStyle === 'countries' ? '#4a90d9' : undefined }} />

{/* Render all plugin layers */}
{mapInstanceRef.current && getAvailableLayers().map(layerDef => (
<PluginLayer
key={layerDef.id}
plugin={layerDef}
// Use the exact metadata names as fallbacks
enabled={pluginLayerStates[layerDef.id]?.enabled ?? layerDef.defaultEnabled}
opacity={pluginLayerStates[layerDef.id]?.opacity ?? layerDef.defaultOpacity}
map={mapInstanceRef.current}
callsign={callsign}
locator={deLocator}
lowMemoryMode={lowMemoryMode}
onDXChange={onDXChange}
dxLocked={dxLocked}
dxLocation={dxLocation}
/>
))}
// MODIS SLIDER CODE HERE
{/* Azimuthal equidistant projection (canvas-based) */}
{mapStyle === 'azimuthal' && (
<AzimuthalMap
Expand Down Expand Up @@ -1346,6 +1366,9 @@ export const WorldMap = ({
callsign={callsign}
locator={deLocator}
lowMemoryMode={lowMemoryMode}
onDXChange={onDXChange}
dxLocked={dxLocked}
dxLocation={dxLocation}
/>
))}

Expand Down Expand Up @@ -1592,4 +1615,4 @@ export const WorldMap = ({
);
};

export default WorldMap;
export default WorldMap;
29 changes: 28 additions & 1 deletion src/plugins/layers/useContestQsos.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ export const metadata = {
version: '1.0.0'
};

export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
export function useLayer({ enabled = false, opacity = 0.7, map = null, onDXChange = null }) {
const [qsos, setQsos] = useState([]);
const [deLocation, setDeLocation] = useState(null);
const markersRef = useRef([]);
const linesRef = useRef([]);
const pollRef = useRef(null);
const configLoadedRef = useRef(false);
const lastAutoDxQsoIdRef = useRef(null);

useEffect(() => {
if (!enabled || configLoadedRef.current) return;
Expand Down Expand Up @@ -129,5 +130,31 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
});
}, [qsos, enabled, opacity, map, deLocation]);

useEffect(() => {
if (!enabled || typeof onDXChange !== 'function' || !Array.isArray(qsos) || qsos.length === 0) return;

const latestWithCoords = [...qsos]
.reverse()
.find((qso) => Number.isFinite(parseFloat(qso?.lat)) && Number.isFinite(parseFloat(qso?.lon)));

if (!latestWithCoords) return;

const qsoId = latestWithCoords.id ||
`${latestWithCoords.dxCall || ''}-${latestWithCoords.timestamp || ''}-${latestWithCoords.lat}-${latestWithCoords.lon}`;

if (!qsoId || qsoId === lastAutoDxQsoIdRef.current) return;

const lat = parseFloat(latestWithCoords.lat);
let lon = parseFloat(latestWithCoords.lon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;

while (lon > 180) lon -= 360;
while (lon < -180) lon += 360;

// Auto-follow newest contest QSO so propagation/DX panel refresh from logger activity.
onDXChange({ lat, lon });
lastAutoDxQsoIdRef.current = qsoId;
}, [enabled, qsos, onDXChange]);

return { layer: markersRef.current };
}