- Exclude List - Hide these callsigns
+ Exclude List - Hide DX callsigns beginning with:
{
// Watchlist only mode - must match watchlist
if (filters.watchlistOnly && filters.watchlist?.length > 0) {
const matchesWatchlist = filters.watchlist.some(w =>
- spot.call?.toUpperCase().includes(w.toUpperCase()) ||
- spot.spotter?.toUpperCase().includes(w.toUpperCase())
+ spot.call?.toUpperCase().includes(w.toUpperCase())
);
if (!matchesWatchlist) return false;
}
- // Exclude list - hide matching calls
+ // Exclude list - hide matching calls - match the call as a prefix
if (filters.excludeList?.length > 0) {
const isExcluded = filters.excludeList.some(exc =>
- spot.call?.toUpperCase().includes(exc.toUpperCase()) ||
- spot.spotter?.toUpperCase().includes(exc.toUpperCase())
+ spot.call?.toUpperCase().startsWith(exc.toUpperCase())
);
if (isExcluded) return false;
}
diff --git a/src/plugins/layers/useEarthquakes.js b/src/plugins/layers/useEarthquakes.js
index 3396a89..bdca710 100644
--- a/src/plugins/layers/useEarthquakes.js
+++ b/src/plugins/layers/useEarthquakes.js
@@ -81,11 +81,27 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) {
const coords = quake.geometry.coordinates;
const props = quake.properties;
const mag = props.mag;
- const lat = coords[1];
- const lon = coords[0];
+
+ // GeoJSON standard format: [longitude, latitude, elevation]
+ // For Santa Lucía, Peru: [-70.5639, -15.6136, 206.486]
+ // coords[0] = -70.5639 = Longitude (W)
+ // coords[1] = -15.6136 = Latitude (S)
+ // coords[2] = 206.486 = Depth (km)
+ const lat = coords[1]; // Latitude (y-axis)
+ const lon = coords[0]; // Longitude (x-axis)
const depth = coords[2];
const quakeId = quake.id;
+ // Debug logging with detailed info
+ console.log(`🌋 Earthquake ${quakeId}:`, {
+ place: props.place,
+ mag: mag,
+ geojson: `[lon=${coords[0]}, lat=${coords[1]}, depth=${coords[2]}]`,
+ extracted: `lat=${lat} (coords[1]), lon=${lon} (coords[0])`,
+ leafletMarkerCall: `L.marker([${lat}, ${lon}])`,
+ explanation: `Standard Leaflet [latitude, longitude] format - CSS position fixed`
+ });
+
currentQuakeIds.add(quakeId);
// Skip if invalid coordinates
@@ -133,11 +149,20 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) {
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
">${waveIcon}
`,
iconSize: [size, size],
- iconAnchor: [size/2, size/2]
+ iconAnchor: [size/2, size/2],
+ popupAnchor: [0, 0] // Popup appears at the marker position (icon center)
});
- console.log('Creating earthquake marker:', quakeId, 'M' + mag.toFixed(1), 'at', lat, lon, 'size:', size + 'px', 'color:', color);
- const circle = L.marker([lat, lon], {
+ console.log(`📍 Creating marker for ${quakeId}: M${mag.toFixed(1)} at [lat=${lat}, lon=${lon}] - ${props.place}`);
+
+ // Use standard Leaflet [latitude, longitude] format
+ // The popup was appearing in the correct location, confirming marker position is correct
+ // The icon was appearing offset due to CSS position: relative issue (now fixed)
+ const markerCoords = [lat, lon]; // CORRECT: [latitude, longitude]
+
+ console.log(` → Creating L.marker([${markerCoords[0]}, ${markerCoords[1]}]) = [lat, lon]`);
+
+ const circle = L.marker(markerCoords, {
icon,
opacity,
zIndexOffset: 10000 // Ensure markers appear on top
@@ -170,7 +195,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) {
}
}, 10);
- // Create pulsing ring effect
+ // Create pulsing ring effect - use same [lat, lon] format
const pulseRing = L.circle([lat, lon], {
radius: 50000, // 50km radius in meters
fillColor: color,
diff --git a/src/plugins/layers/useGrayLine.js b/src/plugins/layers/useGrayLine.js
index 51df3b5..af4d115 100644
--- a/src/plugins/layers/useGrayLine.js
+++ b/src/plugins/layers/useGrayLine.js
@@ -100,6 +100,87 @@ function calculateSolarAltitude(date, latitude, longitude) {
return altitude;
}
+// Split polyline at date line to avoid lines cutting across the map
+function splitAtDateLine(points) {
+ if (points.length < 2) return [points];
+
+ // Check if line spans the full world (-180 to 180)
+ const lons = points.map(p => p[1]);
+ const minLon = Math.min(...lons);
+ const maxLon = Math.max(...lons);
+ const span = maxLon - minLon;
+
+ console.log('🔍 splitAtDateLine debug:', {
+ totalPoints: points.length,
+ lonRange: `${minLon.toFixed(1)} to ${maxLon.toFixed(1)}`,
+ span: span.toFixed(1)
+ });
+
+ // If the line spans close to 360°, it wraps around the world
+ // We need to split it at the ±180° boundary
+ if (span > 350) {
+ console.log('🔍 Full-world span detected, splitting at ±180°');
+
+ // Strategy: Create two segments that meet at ±180° longitude
+ // Segment 1: Western hemisphere (-180° to slightly past 0°)
+ // Segment 2: Eastern hemisphere (slightly before 0° to +180°)
+
+ const westSegment = []; // Points from -180° to ~0°
+ const eastSegment = []; // Points from ~0° to +180°
+
+ // Sort points by longitude to ensure correct ordering
+ const sortedPoints = [...points].sort((a, b) => a[1] - b[1]);
+
+ // Find the midpoint longitude (should be around 0°)
+ const midIndex = Math.floor(sortedPoints.length / 2);
+
+ // Split at midpoint, with some overlap
+ westSegment.push(...sortedPoints.slice(0, midIndex + 1));
+ eastSegment.push(...sortedPoints.slice(midIndex));
+
+ const segments = [];
+ if (westSegment.length >= 2) segments.push(westSegment);
+ if (eastSegment.length >= 2) segments.push(eastSegment);
+
+ console.log('🔍 Split into segments:', segments.map(s => {
+ const lons = s.map(p => p[1]);
+ return {
+ points: s.length,
+ lonRange: `${Math.min(...lons).toFixed(1)} to ${Math.max(...lons).toFixed(1)}`
+ };
+ }));
+ return segments;
+ }
+
+ // Otherwise, check for sudden longitude jumps (traditional date line crossing)
+ const segments = [];
+ let currentSegment = [points[0]];
+
+ for (let i = 1; i < points.length; i++) {
+ const prev = points[i - 1];
+ const curr = points[i];
+ const prevLon = prev[1];
+ const currLon = curr[1];
+ const lonDiff = Math.abs(currLon - prevLon);
+
+ // If longitude jumps more than 180°, we've crossed the date line
+ if (lonDiff > 180) {
+ console.log(`🔍 Date line jump detected at index ${i}: ${prevLon.toFixed(1)}° → ${currLon.toFixed(1)}°`);
+ segments.push(currentSegment);
+ currentSegment = [curr];
+ } else {
+ currentSegment.push(curr);
+ }
+ }
+
+ if (currentSegment.length > 0) {
+ segments.push(currentSegment);
+ }
+
+ console.log('🔍 splitAtDateLine result:', segments.length, 'segments');
+ return segments.filter(seg => seg.length >= 2);
+}
+
// Generate terminator line for a specific solar altitude
function generateTerminatorLine(date, solarAltitude = 0, numPoints = 360) {
const points = [];
@@ -113,64 +194,78 @@ function generateTerminatorLine(date, solarAltitude = 0, numPoints = 360) {
const hourAngle = calculateHourAngle(date, lon);
const haRad = hourAngle * Math.PI / 180;
- // Use the solar altitude equation to solve for latitude
- // sin(altitude) = sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(HA)
- // Rearranging: sin(altitude) - sin(lat) * sin(dec) = cos(lat) * cos(dec) * cos(HA)
-
- // For terminator (altitude = 0), the equation simplifies
- // We need to solve: tan(lat) = -tan(dec) / cos(HA)
-
const cosHA = Math.cos(haRad);
const sinDec = Math.sin(decRad);
const cosDec = Math.cos(decRad);
const sinAlt = Math.sin(altRad);
- // Solve using the quadratic formula or direct calculation
- // sin(lat) = (sin(alt) - cos(lat) * cos(dec) * cos(HA)) / sin(dec)
-
- // Better approach: use atan2 for proper terminator calculation
- // The terminator latitude for a given longitude is:
- // lat = atan(-cos(HA) / tan(dec)) when dec != 0
-
let lat;
+ // Check if solution exists (sun can reach this altitude at this longitude)
+ // For terminator and twilight, check if |cos(HA) * cos(dec)| <= 1 - sin(alt) * sin(dec)
+ const testValue = (sinAlt - sinDec * sinDec) / (cosDec * cosDec * cosHA * cosHA);
+
if (Math.abs(declination) < 0.01) {
// Near equinox: terminator is nearly straight along equator
lat = 0;
+ } else if (Math.abs(cosDec) < 0.001) {
+ // Near solstice: sun is directly over tropic, skip this point
+ continue;
} else {
// Standard case: calculate terminator latitude
- // Formula: cos(lat) * cos(dec) * cos(HA) = -sin(lat) * sin(dec) (for altitude = 0)
- // This gives: tan(lat) = -cos(HA) / tan(dec)
-
const tanDec = Math.tan(decRad);
- if (Math.abs(tanDec) < 0.0001) {
- lat = 0;
- } else {
- lat = Math.atan(-cosHA / tanDec) * 180 / Math.PI;
- }
- // For twilight (altitude < 0), we need to adjust
- if (solarAltitude !== 0) {
- // Use iterative solution for twilight calculations
- // cos(lat) * cos(dec) * cos(HA) + sin(lat) * sin(dec) = sin(alt)
+ if (solarAltitude === 0) {
+ // Terminator (sunrise/sunset line)
+ // Formula: tan(lat) = -cos(HA) / tan(dec)
+ if (Math.abs(tanDec) > 0.0001) {
+ lat = Math.atan(-cosHA / tanDec) * 180 / Math.PI;
+ } else {
+ lat = 0;
+ }
+ } else {
+ // Twilight zones (negative solar altitude)
+ // Use Newton-Raphson iteration to solve for latitude
+ // Equation: sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(HA) = sin(alt)
- // Newton-Raphson iteration to solve for latitude
- let testLat = lat * Math.PI / 180;
- for (let iter = 0; iter < 5; iter++) {
+ // Initial guess based on terminator
+ let testLat = Math.atan(-cosHA / tanDec);
+
+ // Iterate to find solution
+ let converged = false;
+ for (let iter = 0; iter < 10; iter++) {
const f = Math.sin(testLat) * sinDec + Math.cos(testLat) * cosDec * cosHA - sinAlt;
const fPrime = Math.cos(testLat) * sinDec - Math.sin(testLat) * cosDec * cosHA;
+
+ if (Math.abs(f) < 0.0001) {
+ converged = true;
+ break;
+ }
+
if (Math.abs(fPrime) > 0.0001) {
testLat = testLat - f / fPrime;
+ } else {
+ break;
}
+
+ // Constrain to valid latitude range during iteration
+ testLat = Math.max(-Math.PI/2, Math.min(Math.PI/2, testLat));
+ }
+
+ // Only use the point if iteration converged
+ if (!converged) {
+ continue;
}
+
lat = testLat * 180 / Math.PI;
}
}
- // Clamp latitude to valid range
- lat = Math.max(-90, Math.min(90, lat));
+ // Strict clamping to valid latitude range
+ lat = Math.max(-85, Math.min(85, lat));
- if (isFinite(lat) && isFinite(lon)) {
+ // Only add point if it's valid and not at extreme latitude
+ if (isFinite(lat) && isFinite(lon) && Math.abs(lat) < 85) {
points.push([lat, lon]);
}
}
@@ -488,104 +583,159 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) {
// Main terminator (solar altitude = 0°)
const terminator = generateTerminatorLine(currentTime, 0, 360);
- const terminatorLine = L.polyline(terminator, {
- color: '#ff6600',
- weight: 3,
- opacity: opacity * 0.8,
- dashArray: '10, 5'
+ const terminatorSegments = splitAtDateLine(terminator);
+
+ terminatorSegments.forEach(segment => {
+ const terminatorLine = L.polyline(segment, {
+ color: '#ff6600',
+ weight: 3,
+ opacity: opacity * 0.8,
+ dashArray: '10, 5'
+ });
+ terminatorLine.bindPopup(`
+
+ 🌅 Solar Terminator
+ Sun altitude: 0°
+ Enhanced HF propagation zone
+ UTC: ${currentTime.toUTCString()}
+
+ `);
+ terminatorLine.addTo(map);
+ newLayers.push(terminatorLine);
});
- terminatorLine.bindPopup(`
-
- 🌅 Solar Terminator
- Sun altitude: 0°
- Enhanced HF propagation zone
- UTC: ${currentTime.toUTCString()}
-
- `);
- terminatorLine.addTo(map);
- newLayers.push(terminatorLine);
// Enhanced DX zone (±5° from terminator)
if (showEnhancedZone) {
const enhancedUpper = generateTerminatorLine(currentTime, 5, 360);
const enhancedLower = generateTerminatorLine(currentTime, -5, 360);
- // Create polygon for enhanced zone
- const enhancedZone = [...enhancedUpper, ...enhancedLower.reverse()];
- const enhancedPoly = L.polygon(enhancedZone, {
- color: '#ffaa00',
- fillColor: '#ffaa00',
- fillOpacity: opacity * 0.15,
- weight: 1,
- opacity: opacity * 0.3
- });
- enhancedPoly.bindPopup(`
-
- ⭐ Enhanced DX Zone
- Best HF propagation window
- ±5° from terminator
- Ideal for long-distance contacts
-
- `);
- enhancedPoly.addTo(map);
- newLayers.push(enhancedPoly);
+ // Only create polygon if we have valid points
+ if (enhancedUpper.length > 2 && enhancedLower.length > 2) {
+ // Split both upper and lower lines at date line
+ const upperSegments = splitAtDateLine(enhancedUpper);
+ const lowerSegments = splitAtDateLine(enhancedLower);
+
+ console.log('🔶 Enhanced DX Zone segments:', {
+ upperCount: upperSegments.length,
+ lowerCount: lowerSegments.length,
+ upperSegmentLengths: upperSegments.map(s => s.length),
+ lowerSegmentLengths: lowerSegments.map(s => s.length)
+ });
+
+ // Create polygon for each corresponding segment pair
+ // Both upper and lower should have same number of segments
+ const numSegments = Math.min(upperSegments.length, lowerSegments.length);
+
+ for (let i = 0; i < numSegments; i++) {
+ const upperSeg = upperSegments[i];
+ const lowerSeg = lowerSegments[i];
+
+ if (upperSeg.length > 1 && lowerSeg.length > 1) {
+ // Create polygon from upper segment + reversed lower segment
+ // This creates a closed shape between the two lines
+ const enhancedZone = [...upperSeg, ...lowerSeg.slice().reverse()];
+
+ // Debug: Show longitude range of this polygon
+ const polyLons = enhancedZone.map(p => p[1]);
+ const polyMinLon = Math.min(...polyLons);
+ const polyMaxLon = Math.max(...polyLons);
+
+ console.log(`🔶 Creating Enhanced DX polygon segment ${i+1}/${numSegments}:`, {
+ upperPoints: upperSeg.length,
+ lowerPoints: lowerSeg.length,
+ totalPolygonPoints: enhancedZone.length,
+ lonRange: `${polyMinLon.toFixed(1)} to ${polyMaxLon.toFixed(1)}`
+ });
+
+ const enhancedPoly = L.polygon(enhancedZone, {
+ color: '#ffaa00',
+ fillColor: '#ffaa00',
+ fillOpacity: opacity * 0.15,
+ weight: 1,
+ opacity: opacity * 0.3
+ });
+ enhancedPoly.bindPopup(`
+
+ ⭐ Enhanced DX Zone
+ Best HF propagation window
+ ±5° from terminator
+ Ideal for long-distance contacts
+
+ `);
+ enhancedPoly.addTo(map);
+ newLayers.push(enhancedPoly);
+ }
+ }
+ }
}
// Twilight zones
if (showTwilight) {
// Civil twilight (sun altitude -6°)
const civilTwilight = generateTerminatorLine(currentTime, -6, 360);
- const civilLine = L.polyline(civilTwilight, {
- color: '#4488ff',
- weight: 2,
- opacity: twilightOpacity * 0.6,
- dashArray: '5, 5'
+ const civilSegments = splitAtDateLine(civilTwilight);
+
+ civilSegments.forEach(segment => {
+ const civilLine = L.polyline(segment, {
+ color: '#4488ff',
+ weight: 2,
+ opacity: twilightOpacity * 0.6,
+ dashArray: '5, 5'
+ });
+ civilLine.bindPopup(`
+
+ 🌆 Civil Twilight
+ Sun altitude: -6°
+ Good propagation conditions
+
+ `);
+ civilLine.addTo(map);
+ newLayers.push(civilLine);
});
- civilLine.bindPopup(`
-
- 🌆 Civil Twilight
- Sun altitude: -6°
- Good propagation conditions
-
- `);
- civilLine.addTo(map);
- newLayers.push(civilLine);
// Nautical twilight (sun altitude -12°)
const nauticalTwilight = generateTerminatorLine(currentTime, -12, 360);
- const nauticalLine = L.polyline(nauticalTwilight, {
- color: '#6666ff',
- weight: 1.5,
- opacity: twilightOpacity * 0.4,
- dashArray: '3, 3'
+ const nauticalSegments = splitAtDateLine(nauticalTwilight);
+
+ nauticalSegments.forEach(segment => {
+ const nauticalLine = L.polyline(segment, {
+ color: '#6666ff',
+ weight: 1.5,
+ opacity: twilightOpacity * 0.4,
+ dashArray: '3, 3'
+ });
+ nauticalLine.bindPopup(`
+
+ 🌃 Nautical Twilight
+ Sun altitude: -12°
+ Moderate propagation
+
+ `);
+ nauticalLine.addTo(map);
+ newLayers.push(nauticalLine);
});
- nauticalLine.bindPopup(`
-
- 🌃 Nautical Twilight
- Sun altitude: -12°
- Moderate propagation
-
- `);
- nauticalLine.addTo(map);
- newLayers.push(nauticalLine);
// Astronomical twilight (sun altitude -18°)
const astroTwilight = generateTerminatorLine(currentTime, -18, 360);
- const astroLine = L.polyline(astroTwilight, {
- color: '#8888ff',
- weight: 1,
- opacity: twilightOpacity * 0.3,
- dashArray: '2, 2'
+ const astroSegments = splitAtDateLine(astroTwilight);
+
+ astroSegments.forEach(segment => {
+ const astroLine = L.polyline(segment, {
+ color: '#8888ff',
+ weight: 1,
+ opacity: twilightOpacity * 0.3,
+ dashArray: '2, 2'
+ });
+ astroLine.bindPopup(`
+
+ 🌌 Astronomical Twilight
+ Sun altitude: -18°
+ Transition to night propagation
+
+ `);
+ astroLine.addTo(map);
+ newLayers.push(astroLine);
});
- astroLine.bindPopup(`
-
- 🌌 Astronomical Twilight
- Sun altitude: -18°
- Transition to night propagation
-
- `);
- astroLine.addTo(map);
- newLayers.push(astroLine);
}
setLayers(newLayers);
diff --git a/src/styles/main.css b/src/styles/main.css
index 022b7a4..ad8b710 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -876,7 +876,7 @@ body::before {
.earthquake-icon {
z-index: 10000 !important;
pointer-events: auto;
- position: relative !important;
+ /* Removed position: relative - was causing icons to appear offset from marker position */
}
.earthquake-icon div {
@@ -885,7 +885,7 @@ body::before {
justify-content: center;
cursor: pointer;
user-select: none;
- position: relative;
+ /* position: relative removed here too */
z-index: 10000 !important;
}
diff --git a/src/utils/callsign.js b/src/utils/callsign.js
index 2207fa2..64b829d 100644
--- a/src/utils/callsign.js
+++ b/src/utils/callsign.js
@@ -242,8 +242,7 @@ export const filterDXPaths = (paths, filters) => {
// Exclude list - hide matching callsigns
if (filters.excludeList?.length > 0) {
const isExcluded = filters.excludeList.some(e =>
- path.dxCall?.toUpperCase().includes(e.toUpperCase()) ||
- path.spotter?.toUpperCase().includes(e.toUpperCase())
+ path.dxCall?.toUpperCase().startsWith(e.toUpperCase())
);
if (isExcluded) return false;
}
diff --git a/test_coords.py b/test_coords.py
new file mode 100644
index 0000000..60cf381
--- /dev/null
+++ b/test_coords.py
@@ -0,0 +1,26 @@
+# Test coordinate wrapping for Kamchatka, Russia
+# Kamchatka is around: 56°N, 162°E
+
+lat = 56.0 # Correct latitude
+lon = 162.0 # Correct longitude (Eastern hemisphere)
+
+print(f"Kamchatka, Russia:")
+print(f" Correct coordinates: lat={lat}, lon={lon}")
+print(f" Leaflet marker: L.marker([{lat}, {lon}])")
+print(f" This should plot in: Eastern Russia (Kamchatka Peninsula)")
+print()
+
+# What if longitude is negative?
+lon_neg = -162.0
+print(f"If longitude was negative:")
+print(f" L.marker([{lat}, {lon_neg}])")
+print(f" This would plot in: Western Alaska (near Bering Strait)")
+print()
+
+# Peru example
+peru_lat = -15.6
+peru_lon = -70.6
+print(f"Peru earthquake:")
+print(f" Correct coordinates: lat={peru_lat}, lon={peru_lon}")
+print(f" Leaflet marker: L.marker([{peru_lat}, {peru_lon}])")
+print(f" This should plot in: Peru, South America")
diff --git a/test_earthquake_coords.js b/test_earthquake_coords.js
new file mode 100644
index 0000000..3e70a8b
--- /dev/null
+++ b/test_earthquake_coords.js
@@ -0,0 +1,28 @@
+// Test to understand the coordinate flow
+
+// Sample Dominican Republic earthquake from USGS
+const geojsonData = {
+ geometry: {
+ coordinates: [-68.625, 18.0365, 75.0] // [lon, lat, depth] - GeoJSON format
+ },
+ properties: {
+ place: "37 km S of Boca de Yuma, Dominican Republic"
+ }
+};
+
+// Current code extraction
+const coords = geojsonData.geometry.coordinates;
+const lat = coords[1]; // 18.0365
+const lon = coords[0]; // -68.625
+
+console.log("GeoJSON coordinates:", coords);
+console.log("Extracted lat:", lat, "(should be 18.0365)");
+console.log("Extracted lon:", lon, "(should be -68.625)");
+console.log("");
+console.log("If we call L.marker([lat, lon]):");
+console.log(" L.marker([" + lat + ", " + lon + "])");
+console.log(" This should plot at: 18.0365°N, 68.625°W (Dominican Republic)");
+console.log("");
+console.log("If we call L.marker([lon, lat]):");
+console.log(" L.marker([" + lon + ", " + lat + "])");
+console.log(" This would plot at: -68.625°S, 18.0365°E (Indian Ocean!)");