diff --git a/public/index.html b/public/index.html
index 8329532..bcc7355 100644
--- a/public/index.html
+++ b/public/index.html
@@ -429,7 +429,6 @@
// Great circle path for Leaflet
const getGreatCirclePoints = (lat1, lon1, lat2, lon2, n = 100) => {
- const points = [];
const toRad = d => d * Math.PI / 180;
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
));
+ // 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++) {
const f = i / n;
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 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);
- 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 (dxMarkerRef.current) map.removeLayer(dxMarkerRef.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
const deIcon = L.divIcon({
@@ -868,14 +899,29 @@
.bindPopup('Subsolar Point')
.addTo(map);
- // Great circle path
- const pathPoints = getGreatCirclePoints(deLocation.lat, deLocation.lon, dxLocation.lat, dxLocation.lon);
- pathRef.current = L.polyline(pathPoints, {
- color: '#00ddff',
- weight: 3,
- opacity: 0.8,
- dashArray: '10, 6'
- }).addTo(map);
+ // Great circle path (handles antimeridian crossing)
+ const pathSegments = getGreatCirclePoints(deLocation.lat, deLocation.lon, dxLocation.lat, dxLocation.lon);
+
+ // pathRef now holds an array of polylines for each segment
+ if (!Array.isArray(pathRef.current)) {
+ pathRef.current = [];
+ }
+ // 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]);
diff --git a/server.js b/server.js
index 951311e..595c2e0 100644
--- a/server.js
+++ b/server.js
@@ -119,14 +119,55 @@ app.get('/api/hamqsl/conditions', async (req, res) => {
app.get('/api/dxcluster/spots', async (req, res) => {
console.log('[DX Cluster] Fetching spots...');
- // Try DX Heat API first (most reliable)
+ // Try DXCluster.co API first (very reliable JSON API)
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: {
- 'User-Agent': 'OpenHamClock/3.1',
+ 'User-Agent': 'OpenHamClock/3.3',
'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) {
const text = await response.text();
@@ -149,51 +190,58 @@ app.get('/api/dxcluster/spots', async (req, res) => {
}
}
} 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 {
- 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: {
- 'User-Agent': 'OpenHamClock/3.1'
- }
+ 'User-Agent': 'OpenHamClock/3.3'
+ },
+ signal: controller.signal
});
+ clearTimeout(timeout);
if (response.ok) {
- const text = await response.text();
- console.log('[DX Cluster] PSKReporter response length:', text.length);
- // PSK Reporter returns XML, parse it
- const callMatches = text.match(/senderCallsign="([^"]+)"/g) || [];
- const freqMatches = text.match(/frequency="([^"]+)"/g) || [];
- const modeMatches = text.match(/mode="([^"]+)"/g) || [];
-
- if (callMatches.length > 0) {
- const spots = callMatches.slice(0, 20).map((match, i) => {
- 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');
+ const data = await response.json();
+ console.log('[DX Cluster] RBN returned', data?.length || 0, 'spots');
+ if (data && data.length > 0) {
+ const spots = data.slice(0, 20).map(spot => ({
+ freq: spot.freq ? (parseFloat(spot.freq) / 1000).toFixed(3) : '0.000',
+ call: spot.dx || spot.callsign || 'UNKNOWN',
+ comment: spot.mode ? `${spot.mode} ${spot.db || ''}dB` : '',
+ time: spot.time || new Date().toISOString().substring(11, 16) + 'z',
+ spotter: spot.de || ''
+ }));
return res.json(spots);
}
}
} 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 {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 5000);
+
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) {
const text = await response.text();
@@ -219,17 +267,26 @@ app.get('/api/dxcluster/spots', async (req, res) => {
}
}
} 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 {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 5000);
+
const response = await fetch('https://dxwatch.com/dxsd1/s.php?s=0&r=30', {
headers: {
- 'User-Agent': 'OpenHamClock/3.1',
+ 'User-Agent': 'OpenHamClock/3.3',
'Accept': '*/*'
- }
+ },
+ signal: controller.signal
});
+ clearTimeout(timeout);
if (response.ok) {
const text = await response.text();
@@ -252,11 +309,14 @@ app.get('/api/dxcluster/spots', async (req, res) => {
}
}
} 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');
- // Return empty array if all sources fail
res.json([]);
});