Fix great circle path wrapping, improve DX cluster reliability

Fix great circle path wrapping, improve DX cluster reliability
pull/1/head
accius 6 days ago
parent 66377d2d2f
commit bbee2b26a9

@ -429,7 +429,6 @@
// Great circle path for Leaflet // Great circle path for Leaflet
const getGreatCirclePoints = (lat1, lon1, lat2, lon2, n = 100) => { const getGreatCirclePoints = (lat1, lon1, lat2, lon2, n = 100) => {
const points = [];
const toRad = d => d * Math.PI / 180; const toRad = d => d * Math.PI / 180;
const toDeg = r => r * 180 / Math.PI; const toDeg = r => r * 180 / Math.PI;
@ -440,6 +439,12 @@
Math.sin((φ1-φ2)/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin((λ1-λ2)/2)**2 Math.sin((φ1-φ2)/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin((λ1-λ2)/2)**2
)); ));
// If distance is essentially zero, return just the two points
if (d < 0.0001) {
return [[lat1, lon1], [lat2, lon2]];
}
const rawPoints = [];
for (let i = 0; i <= n; i++) { for (let i = 0; i <= n; i++) {
const f = i / n; const f = i / n;
const A = Math.sin((1-f)*d) / Math.sin(d); const A = Math.sin((1-f)*d) / Math.sin(d);
@ -447,9 +452,29 @@
const x = A*Math.cos(φ1)*Math.cos(λ1) + B*Math.cos(φ2)*Math.cos(λ2); const x = A*Math.cos(φ1)*Math.cos(λ1) + B*Math.cos(φ2)*Math.cos(λ2);
const y = A*Math.cos(φ1)*Math.sin(λ1) + B*Math.cos(φ2)*Math.sin(λ2); const y = A*Math.cos(φ1)*Math.sin(λ1) + B*Math.cos(φ2)*Math.sin(λ2);
const z = A*Math.sin(φ1) + B*Math.sin(φ2); const z = A*Math.sin(φ1) + B*Math.sin(φ2);
points.push([toDeg(Math.atan2(z, Math.sqrt(x*x+y*y))), toDeg(Math.atan2(y, x))]); rawPoints.push([toDeg(Math.atan2(z, Math.sqrt(x*x+y*y))), toDeg(Math.atan2(y, x))]);
}
// Split path at antimeridian crossings for proper Leaflet rendering
const segments = [];
let currentSegment = [rawPoints[0]];
for (let i = 1; i < rawPoints.length; i++) {
const prevLon = rawPoints[i-1][1];
const currLon = rawPoints[i][1];
// Check if we crossed the antimeridian (lon jumps more than 180°)
if (Math.abs(currLon - prevLon) > 180) {
// Finish current segment
segments.push(currentSegment);
// Start new segment
currentSegment = [];
}
currentSegment.push(rawPoints[i]);
} }
return points; segments.push(currentSegment);
return segments;
}; };
// ============================================ // ============================================
@ -832,7 +857,13 @@
if (deMarkerRef.current) map.removeLayer(deMarkerRef.current); if (deMarkerRef.current) map.removeLayer(deMarkerRef.current);
if (dxMarkerRef.current) map.removeLayer(dxMarkerRef.current); if (dxMarkerRef.current) map.removeLayer(dxMarkerRef.current);
if (sunMarkerRef.current) map.removeLayer(sunMarkerRef.current); if (sunMarkerRef.current) map.removeLayer(sunMarkerRef.current);
if (pathRef.current) map.removeLayer(pathRef.current); if (pathRef.current) {
if (Array.isArray(pathRef.current)) {
pathRef.current.forEach(p => map.removeLayer(p));
} else {
map.removeLayer(pathRef.current);
}
}
// DE Marker // DE Marker
const deIcon = L.divIcon({ const deIcon = L.divIcon({
@ -868,14 +899,29 @@
.bindPopup('Subsolar Point') .bindPopup('Subsolar Point')
.addTo(map); .addTo(map);
// Great circle path // Great circle path (handles antimeridian crossing)
const pathPoints = getGreatCirclePoints(deLocation.lat, deLocation.lon, dxLocation.lat, dxLocation.lon); const pathSegments = getGreatCirclePoints(deLocation.lat, deLocation.lon, dxLocation.lat, dxLocation.lon);
pathRef.current = L.polyline(pathPoints, {
color: '#00ddff', // pathRef now holds an array of polylines for each segment
weight: 3, if (!Array.isArray(pathRef.current)) {
opacity: 0.8, pathRef.current = [];
dashArray: '10, 6' }
}).addTo(map); // Clear old paths
pathRef.current.forEach(p => map.removeLayer(p));
pathRef.current = [];
// Draw each segment
pathSegments.forEach(segment => {
if (segment.length > 1) {
const polyline = L.polyline(segment, {
color: '#00ddff',
weight: 3,
opacity: 0.8,
dashArray: '10, 6'
}).addTo(map);
pathRef.current.push(polyline);
}
});
}, [deLocation, dxLocation]); }, [deLocation, dxLocation]);

@ -119,14 +119,55 @@ app.get('/api/hamqsl/conditions', async (req, res) => {
app.get('/api/dxcluster/spots', async (req, res) => { app.get('/api/dxcluster/spots', async (req, res) => {
console.log('[DX Cluster] Fetching spots...'); console.log('[DX Cluster] Fetching spots...');
// Try DX Heat API first (most reliable) // Try DXCluster.co API first (very reliable JSON API)
try { try {
const response = await fetch('https://dxheat.com/dxc/data.php?include_modes=cw,ssb,ft8,ft4,rtty&include_bands=160,80,60,40,30,20,17,15,12,10,6&limit=30', { const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch('https://dxcluster.co/api/v1/spots?limit=30', {
headers: { headers: {
'User-Agent': 'OpenHamClock/3.1', 'User-Agent': 'OpenHamClock/3.3',
'Accept': 'application/json' 'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeout);
if (response.ok) {
const data = await response.json();
console.log('[DX Cluster] dxcluster.co returned', data.length, 'spots');
if (data && data.length > 0) {
const spots = data.slice(0, 20).map(spot => ({
freq: spot.frequency ? (parseFloat(spot.frequency) / 1000).toFixed(3) : '0.000',
call: spot.dx_callsign || spot.callsign || 'UNKNOWN',
comment: spot.comment || '',
time: spot.time ? spot.time.substring(11, 16) + 'z' : '',
spotter: spot.spotter_callsign || ''
}));
return res.json(spots);
} }
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('[DX Cluster] dxcluster.co timeout');
} else {
console.error('[DX Cluster] dxcluster.co error:', error.message);
}
}
// Try DX Heat API
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch('https://dxheat.com/dxc/data.php?include_modes=cw,ssb,ft8,ft4,rtty&include_bands=160,80,60,40,30,20,17,15,12,10,6&limit=30', {
headers: {
'User-Agent': 'OpenHamClock/3.3',
'Accept': 'application/json'
},
signal: controller.signal
}); });
clearTimeout(timeout);
if (response.ok) { if (response.ok) {
const text = await response.text(); const text = await response.text();
@ -149,51 +190,58 @@ app.get('/api/dxcluster/spots', async (req, res) => {
} }
} }
} catch (error) { } catch (error) {
console.error('[DX Cluster] DXHeat error:', error.message); if (error.name === 'AbortError') {
console.log('[DX Cluster] DXHeat timeout');
} else {
console.error('[DX Cluster] DXHeat error:', error.message);
}
} }
// Try PSK Reporter as fallback (very reliable) // Try RBN (Reverse Beacon Network) API
try { try {
const response = await fetch('https://pskreporter.info/cgi-bin/pskquery5.pl?encap=1&callback=0&statistics=0&noactive=1&nolocator=1&rronly=1&flowStartSeconds=-900&limit=30', { const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch('https://reversebeacon.net/api/spots.php?r=30', {
headers: { headers: {
'User-Agent': 'OpenHamClock/3.1' 'User-Agent': 'OpenHamClock/3.3'
} },
signal: controller.signal
}); });
clearTimeout(timeout);
if (response.ok) { if (response.ok) {
const text = await response.text(); const data = await response.json();
console.log('[DX Cluster] PSKReporter response length:', text.length); console.log('[DX Cluster] RBN returned', data?.length || 0, 'spots');
// PSK Reporter returns XML, parse it if (data && data.length > 0) {
const callMatches = text.match(/senderCallsign="([^"]+)"/g) || []; const spots = data.slice(0, 20).map(spot => ({
const freqMatches = text.match(/frequency="([^"]+)"/g) || []; freq: spot.freq ? (parseFloat(spot.freq) / 1000).toFixed(3) : '0.000',
const modeMatches = text.match(/mode="([^"]+)"/g) || []; call: spot.dx || spot.callsign || 'UNKNOWN',
comment: spot.mode ? `${spot.mode} ${spot.db || ''}dB` : '',
if (callMatches.length > 0) { time: spot.time || new Date().toISOString().substring(11, 16) + 'z',
const spots = callMatches.slice(0, 20).map((match, i) => { spotter: spot.de || ''
const call = match.replace('senderCallsign="', '').replace('"', ''); }));
const freq = freqMatches[i] ? (parseFloat(freqMatches[i].replace('frequency="', '').replace('"', '')) / 1000000).toFixed(3) : '0.000';
const mode = modeMatches[i] ? modeMatches[i].replace('mode="', '').replace('"', '') : '';
return {
freq: freq,
call: call,
comment: mode,
time: new Date().toISOString().substring(11, 16) + 'z',
spotter: 'PSK'
};
});
console.log('[DX Cluster] PSKReporter returned', spots.length, 'spots');
return res.json(spots); return res.json(spots);
} }
} }
} catch (error) { } catch (error) {
console.error('[DX Cluster] PSKReporter error:', error.message); if (error.name === 'AbortError') {
console.log('[DX Cluster] RBN timeout');
} else {
console.error('[DX Cluster] RBN error:', error.message);
}
} }
// Try HamQTH DX Cluster // Try HamQTH DX Cluster
try { try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch('https://www.hamqth.com/dxc_csv.php?limit=30', { const response = await fetch('https://www.hamqth.com/dxc_csv.php?limit=30', {
headers: { 'User-Agent': 'OpenHamClock/3.1' } headers: { 'User-Agent': 'OpenHamClock/3.3' },
signal: controller.signal
}); });
clearTimeout(timeout);
if (response.ok) { if (response.ok) {
const text = await response.text(); const text = await response.text();
@ -219,17 +267,26 @@ app.get('/api/dxcluster/spots', async (req, res) => {
} }
} }
} catch (error) { } catch (error) {
console.error('[DX Cluster] HamQTH error:', error.message); if (error.name === 'AbortError') {
console.log('[DX Cluster] HamQTH timeout');
} else {
console.error('[DX Cluster] HamQTH error:', error.message);
}
} }
// Try DX Watch legacy endpoint // Try DXWatch as last resort
try { try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch('https://dxwatch.com/dxsd1/s.php?s=0&r=30', { const response = await fetch('https://dxwatch.com/dxsd1/s.php?s=0&r=30', {
headers: { headers: {
'User-Agent': 'OpenHamClock/3.1', 'User-Agent': 'OpenHamClock/3.3',
'Accept': '*/*' 'Accept': '*/*'
} },
signal: controller.signal
}); });
clearTimeout(timeout);
if (response.ok) { if (response.ok) {
const text = await response.text(); const text = await response.text();
@ -252,11 +309,14 @@ app.get('/api/dxcluster/spots', async (req, res) => {
} }
} }
} catch (error) { } catch (error) {
console.error('[DX Cluster] DXWatch error:', error.message); if (error.name === 'AbortError') {
console.log('[DX Cluster] DXWatch timeout');
} else {
console.error('[DX Cluster] DXWatch error:', error.message);
}
} }
console.log('[DX Cluster] All sources failed, returning empty'); console.log('[DX Cluster] All sources failed, returning empty');
// Return empty array if all sources fail
res.json([]); res.json([]);
}); });

Loading…
Cancel
Save

Powered by TurnKey Linux.