diff --git a/src/components/DXClusterPanel.jsx b/src/components/DXClusterPanel.jsx
index 3cd50c7..10f6cb0 100644
--- a/src/components/DXClusterPanel.jsx
+++ b/src/components/DXClusterPanel.jsx
@@ -157,8 +157,8 @@ export const DXClusterPanel = ({
onMouseLeave={() => onHoverSpot?.(null)}
style={{
display: 'grid',
- gridTemplateColumns: '60px 1fr auto',
- gap: '8px',
+ gridTemplateColumns: '55px 1fr 1fr auto',
+ gap: '6px',
padding: '5px 6px',
borderRadius: '3px',
marginBottom: '2px',
@@ -180,6 +180,16 @@ export const DXClusterPanel = ({
}}>
{spot.call}
+
+ de {spot.spotter || '?'}
+
{spot.time || ''}
diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx
index b7043e6..7919d78 100644
--- a/src/components/WorldMap.jsx
+++ b/src/components/WorldMap.jsx
@@ -99,6 +99,9 @@ export const WorldMap = ({
maxZoom: 18,
worldCopyJump: true,
zoomControl: true,
+ zoomSnap: 0.1,
+ zoomDelta: 0.25,
+ wheelPxPerZoomLevel: 200,
maxBounds: [[-90, -Infinity], [90, Infinity]],
maxBoundsViscosity: 0.8
});
@@ -264,28 +267,28 @@ export const WorldMap = ({
const freq = parseFloat(path.freq);
const color = getBandColor(freq);
- const isHovered = hoveredSpot && hoveredSpot.call === path.dxCall &&
- Math.abs(parseFloat(hoveredSpot.freq) - parseFloat(path.freq)) < 0.01;
+ const isHovered = hoveredSpot &&
+ hoveredSpot.call?.toUpperCase() === path.dxCall?.toUpperCase();
- // Handle segments
- const isSegmented = Array.isArray(pathPoints[0]) && pathPoints[0].length > 0 && Array.isArray(pathPoints[0][0]);
- const segments = isSegmented ? pathPoints : [pathPoints];
-
- segments.forEach(segment => {
- if (segment && Array.isArray(segment) && segment.length > 1) {
- const line = L.polyline(segment, {
- color: isHovered ? '#ffffff' : color,
- weight: isHovered ? 4 : 1.5,
- opacity: isHovered ? 1 : 0.5
- }).addTo(map);
- if (isHovered) line.bringToFront();
- dxPathsLinesRef.current.push(line);
- }
- });
+ // Handle path rendering (single continuous array, unwrapped across antimeridian)
+ if (pathPoints && Array.isArray(pathPoints) && pathPoints.length > 1) {
+ const line = L.polyline(pathPoints, {
+ color: isHovered ? '#ffffff' : color,
+ weight: isHovered ? 4 : 1.5,
+ opacity: isHovered ? 1 : 0.5
+ }).addTo(map);
+ if (isHovered) line.bringToFront();
+ dxPathsLinesRef.current.push(line);
+ }
+
+ // Use unwrapped endpoint so marker sits where the line ends
+ const endPoint = pathPoints[pathPoints.length - 1];
+ const dxLatDisplay = endPoint[0];
+ const dxLonDisplay = endPoint[1];
// Add DX marker
- const dxCircle = L.circleMarker([path.dxLat, path.dxLon], {
- radius: isHovered ? 10 : 6,
+ const dxCircle = L.circleMarker([dxLatDisplay, dxLonDisplay], {
+ radius: isHovered ? 12 : 6,
fillColor: isHovered ? '#ffffff' : color,
color: isHovered ? color : '#fff',
weight: isHovered ? 3 : 1.5,
@@ -301,11 +304,15 @@ export const WorldMap = ({
if (showDXLabels || isHovered) {
const labelIcon = L.divIcon({
className: '',
- html: `${path.dxCall}`,
+ html: `${path.dxCall}`,
iconSize: null,
iconAnchor: [0, 0]
});
- const label = L.marker([path.dxLat, path.dxLon], { icon: labelIcon, interactive: false }).addTo(map);
+ const label = L.marker([dxLatDisplay, dxLonDisplay], {
+ icon: labelIcon,
+ interactive: false,
+ zIndexOffset: isHovered ? 10000 : 0
+ }).addTo(map);
dxPathsMarkersRef.current.push(label);
}
} catch (err) {
@@ -358,34 +365,20 @@ export const WorldMap = ({
// Draw orbit track if available
if (sat.track && sat.track.length > 1) {
- // Split track into segments to handle date line crossing
- let segments = [];
- let currentSegment = [sat.track[0]];
-
- for (let i = 1; i < sat.track.length; i++) {
- const prevLon = sat.track[i-1][1];
- const currLon = sat.track[i][1];
- // If longitude jumps more than 180 degrees, start new segment
- if (Math.abs(currLon - prevLon) > 180) {
- segments.push(currentSegment);
- currentSegment = [];
- }
- currentSegment.push(sat.track[i]);
+ // Unwrap longitudes for continuous rendering across antimeridian
+ const unwrapped = sat.track.map(p => [...p]);
+ for (let i = 1; i < unwrapped.length; i++) {
+ while (unwrapped[i][1] - unwrapped[i-1][1] > 180) unwrapped[i][1] -= 360;
+ while (unwrapped[i][1] - unwrapped[i-1][1] < -180) unwrapped[i][1] += 360;
}
- segments.push(currentSegment);
- // Draw each segment
- segments.forEach(segment => {
- if (segment.length > 1) {
- const trackLine = L.polyline(segment, {
- color: sat.visible ? satColor : satColorDark,
- weight: 2,
- opacity: sat.visible ? 0.8 : 0.4,
- dashArray: sat.visible ? null : '5, 5'
- }).addTo(map);
- satTracksRef.current.push(trackLine);
- }
- });
+ const trackLine = L.polyline(unwrapped, {
+ color: sat.visible ? satColor : satColorDark,
+ weight: 2,
+ opacity: sat.visible ? 0.8 : 0.4,
+ dashArray: sat.visible ? null : '5, 5'
+ }).addTo(map);
+ satTracksRef.current.push(trackLine);
}
// Draw footprint circle if available and satellite is visible
@@ -521,8 +514,8 @@ export const WorldMap = ({
if (showPSKReporter && pskReporterSpots && pskReporterSpots.length > 0 && hasValidDE) {
pskReporterSpots.forEach(spot => {
// Validate spot coordinates are valid numbers
- const spotLat = parseFloat(spot.lat);
- const spotLon = parseFloat(spot.lon);
+ let spotLat = parseFloat(spot.lat);
+ let spotLon = parseFloat(spot.lon);
if (!isNaN(spotLat) && !isNaN(spotLon)) {
const displayCall = spot.receiver || spot.sender;
@@ -537,8 +530,9 @@ export const WorldMap = ({
50
);
- // Validate points before creating polyline
- if (points && points.length > 1 && points.every(p => Array.isArray(p) && !isNaN(p[0]) && !isNaN(p[1]))) {
+ // Validate points before creating polyline (single continuous array, unwrapped across antimeridian)
+ if (points && Array.isArray(points) && points.length > 1 &&
+ points.every(p => Array.isArray(p) && !isNaN(p[0]) && !isNaN(p[1]))) {
const line = L.polyline(points, {
color: bandColor,
weight: 1.5,
@@ -546,6 +540,11 @@ export const WorldMap = ({
dashArray: '4, 4'
}).addTo(map);
pskMarkersRef.current.push(line);
+
+ // Use unwrapped endpoint so dot sits where the line ends
+ const endPoint = points[points.length - 1];
+ spotLat = endPoint[0];
+ spotLon = endPoint[1];
}
// Add small dot marker at spot location
diff --git a/src/utils/geo.js b/src/utils/geo.js
index 86e4f01..8ce24e8 100644
--- a/src/utils/geo.js
+++ b/src/utils/geo.js
@@ -205,26 +205,14 @@ export const getGreatCirclePoints = (lat1, lon1, lat2, lon2, n = 100) => {
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]];
-
+ // Unwrap longitudes to be continuous (no jumps > 180°)
+ // This lets Leaflet draw smoothly across the antimeridian and world copies
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]);
+ while (rawPoints[i][1] - rawPoints[i-1][1] > 180) rawPoints[i][1] -= 360;
+ while (rawPoints[i][1] - rawPoints[i-1][1] < -180) rawPoints[i][1] += 360;
}
- segments.push(currentSegment);
- return segments;
+ return rawPoints;
};
export default {