merge to my test branch

Modular-Staging
accius 23 hours ago
parent 6ad93e381d
commit 50ab931933

@ -279,7 +279,7 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
<div> <div>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}> <div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}>
Exclude List - Hide these callsigns Exclude List - Hide DX callsigns beginning with:
</div> </div>
<div style={{ display: 'flex', gap: '8px' }}> <div style={{ display: 'flex', gap: '8px' }}>
<input <input

@ -26,17 +26,15 @@ export const useDXCluster = (source = 'auto', filters = {}) => {
// Watchlist only mode - must match watchlist // Watchlist only mode - must match watchlist
if (filters.watchlistOnly && filters.watchlist?.length > 0) { if (filters.watchlistOnly && filters.watchlist?.length > 0) {
const matchesWatchlist = filters.watchlist.some(w => const matchesWatchlist = filters.watchlist.some(w =>
spot.call?.toUpperCase().includes(w.toUpperCase()) || spot.call?.toUpperCase().includes(w.toUpperCase())
spot.spotter?.toUpperCase().includes(w.toUpperCase())
); );
if (!matchesWatchlist) return false; 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) { if (filters.excludeList?.length > 0) {
const isExcluded = filters.excludeList.some(exc => const isExcluded = filters.excludeList.some(exc =>
spot.call?.toUpperCase().includes(exc.toUpperCase()) || spot.call?.toUpperCase().startsWith(exc.toUpperCase())
spot.spotter?.toUpperCase().includes(exc.toUpperCase())
); );
if (isExcluded) return false; if (isExcluded) return false;
} }

@ -81,11 +81,27 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) {
const coords = quake.geometry.coordinates; const coords = quake.geometry.coordinates;
const props = quake.properties; const props = quake.properties;
const mag = props.mag; 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 depth = coords[2];
const quakeId = quake.id; 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); currentQuakeIds.add(quakeId);
// Skip if invalid coordinates // 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); box-shadow: 0 2px 8px rgba(0,0,0,0.5);
">${waveIcon}</div>`, ">${waveIcon}</div>`,
iconSize: [size, size], 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); console.log(`📍 Creating marker for ${quakeId}: M${mag.toFixed(1)} at [lat=${lat}, lon=${lon}] - ${props.place}`);
const circle = L.marker([lat, lon], {
// 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, icon,
opacity, opacity,
zIndexOffset: 10000 // Ensure markers appear on top zIndexOffset: 10000 // Ensure markers appear on top
@ -170,7 +195,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) {
} }
}, 10); }, 10);
// Create pulsing ring effect // Create pulsing ring effect - use same [lat, lon] format
const pulseRing = L.circle([lat, lon], { const pulseRing = L.circle([lat, lon], {
radius: 50000, // 50km radius in meters radius: 50000, // 50km radius in meters
fillColor: color, fillColor: color,

@ -100,6 +100,87 @@ function calculateSolarAltitude(date, latitude, longitude) {
return altitude; 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 // Generate terminator line for a specific solar altitude
function generateTerminatorLine(date, solarAltitude = 0, numPoints = 360) { function generateTerminatorLine(date, solarAltitude = 0, numPoints = 360) {
const points = []; const points = [];
@ -113,64 +194,78 @@ function generateTerminatorLine(date, solarAltitude = 0, numPoints = 360) {
const hourAngle = calculateHourAngle(date, lon); const hourAngle = calculateHourAngle(date, lon);
const haRad = hourAngle * Math.PI / 180; 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 cosHA = Math.cos(haRad);
const sinDec = Math.sin(decRad); const sinDec = Math.sin(decRad);
const cosDec = Math.cos(decRad); const cosDec = Math.cos(decRad);
const sinAlt = Math.sin(altRad); 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; 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) { if (Math.abs(declination) < 0.01) {
// Near equinox: terminator is nearly straight along equator // Near equinox: terminator is nearly straight along equator
lat = 0; lat = 0;
} else if (Math.abs(cosDec) < 0.001) {
// Near solstice: sun is directly over tropic, skip this point
continue;
} else { } else {
// Standard case: calculate terminator latitude // 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); const tanDec = Math.tan(decRad);
if (Math.abs(tanDec) < 0.0001) {
lat = 0; if (solarAltitude === 0) {
} else { // 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; 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)
// For twilight (altitude < 0), we need to adjust // Initial guess based on terminator
if (solarAltitude !== 0) { let testLat = Math.atan(-cosHA / tanDec);
// Use iterative solution for twilight calculations
// cos(lat) * cos(dec) * cos(HA) + sin(lat) * sin(dec) = sin(alt)
// Newton-Raphson iteration to solve for latitude // Iterate to find solution
let testLat = lat * Math.PI / 180; let converged = false;
for (let iter = 0; iter < 5; iter++) { for (let iter = 0; iter < 10; iter++) {
const f = Math.sin(testLat) * sinDec + Math.cos(testLat) * cosDec * cosHA - sinAlt; const f = Math.sin(testLat) * sinDec + Math.cos(testLat) * cosDec * cosHA - sinAlt;
const fPrime = Math.cos(testLat) * sinDec - Math.sin(testLat) * cosDec * cosHA; 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) { if (Math.abs(fPrime) > 0.0001) {
testLat = testLat - f / fPrime; 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; lat = testLat * 180 / Math.PI;
} }
} }
// Clamp latitude to valid range // Strict clamping to valid latitude range
lat = Math.max(-90, Math.min(90, lat)); 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]); points.push([lat, lon]);
} }
} }
@ -488,7 +583,10 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) {
// Main terminator (solar altitude = 0°) // Main terminator (solar altitude = 0°)
const terminator = generateTerminatorLine(currentTime, 0, 360); const terminator = generateTerminatorLine(currentTime, 0, 360);
const terminatorLine = L.polyline(terminator, { const terminatorSegments = splitAtDateLine(terminator);
terminatorSegments.forEach(segment => {
const terminatorLine = L.polyline(segment, {
color: '#ff6600', color: '#ff6600',
weight: 3, weight: 3,
opacity: opacity * 0.8, opacity: opacity * 0.8,
@ -504,14 +602,51 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) {
`); `);
terminatorLine.addTo(map); terminatorLine.addTo(map);
newLayers.push(terminatorLine); newLayers.push(terminatorLine);
});
// Enhanced DX zone (±5° from terminator) // Enhanced DX zone (±5° from terminator)
if (showEnhancedZone) { if (showEnhancedZone) {
const enhancedUpper = generateTerminatorLine(currentTime, 5, 360); const enhancedUpper = generateTerminatorLine(currentTime, 5, 360);
const enhancedLower = generateTerminatorLine(currentTime, -5, 360); const enhancedLower = generateTerminatorLine(currentTime, -5, 360);
// Create polygon for enhanced zone // Only create polygon if we have valid points
const enhancedZone = [...enhancedUpper, ...enhancedLower.reverse()]; 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, { const enhancedPoly = L.polygon(enhancedZone, {
color: '#ffaa00', color: '#ffaa00',
fillColor: '#ffaa00', fillColor: '#ffaa00',
@ -530,12 +665,18 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) {
enhancedPoly.addTo(map); enhancedPoly.addTo(map);
newLayers.push(enhancedPoly); newLayers.push(enhancedPoly);
} }
}
}
}
// Twilight zones // Twilight zones
if (showTwilight) { if (showTwilight) {
// Civil twilight (sun altitude -6°) // Civil twilight (sun altitude -6°)
const civilTwilight = generateTerminatorLine(currentTime, -6, 360); const civilTwilight = generateTerminatorLine(currentTime, -6, 360);
const civilLine = L.polyline(civilTwilight, { const civilSegments = splitAtDateLine(civilTwilight);
civilSegments.forEach(segment => {
const civilLine = L.polyline(segment, {
color: '#4488ff', color: '#4488ff',
weight: 2, weight: 2,
opacity: twilightOpacity * 0.6, opacity: twilightOpacity * 0.6,
@ -550,10 +691,14 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) {
`); `);
civilLine.addTo(map); civilLine.addTo(map);
newLayers.push(civilLine); newLayers.push(civilLine);
});
// Nautical twilight (sun altitude -12°) // Nautical twilight (sun altitude -12°)
const nauticalTwilight = generateTerminatorLine(currentTime, -12, 360); const nauticalTwilight = generateTerminatorLine(currentTime, -12, 360);
const nauticalLine = L.polyline(nauticalTwilight, { const nauticalSegments = splitAtDateLine(nauticalTwilight);
nauticalSegments.forEach(segment => {
const nauticalLine = L.polyline(segment, {
color: '#6666ff', color: '#6666ff',
weight: 1.5, weight: 1.5,
opacity: twilightOpacity * 0.4, opacity: twilightOpacity * 0.4,
@ -568,10 +713,14 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) {
`); `);
nauticalLine.addTo(map); nauticalLine.addTo(map);
newLayers.push(nauticalLine); newLayers.push(nauticalLine);
});
// Astronomical twilight (sun altitude -18°) // Astronomical twilight (sun altitude -18°)
const astroTwilight = generateTerminatorLine(currentTime, -18, 360); const astroTwilight = generateTerminatorLine(currentTime, -18, 360);
const astroLine = L.polyline(astroTwilight, { const astroSegments = splitAtDateLine(astroTwilight);
astroSegments.forEach(segment => {
const astroLine = L.polyline(segment, {
color: '#8888ff', color: '#8888ff',
weight: 1, weight: 1,
opacity: twilightOpacity * 0.3, opacity: twilightOpacity * 0.3,
@ -586,6 +735,7 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) {
`); `);
astroLine.addTo(map); astroLine.addTo(map);
newLayers.push(astroLine); newLayers.push(astroLine);
});
} }
setLayers(newLayers); setLayers(newLayers);

@ -876,7 +876,7 @@ body::before {
.earthquake-icon { .earthquake-icon {
z-index: 10000 !important; z-index: 10000 !important;
pointer-events: auto; pointer-events: auto;
position: relative !important; /* Removed position: relative - was causing icons to appear offset from marker position */
} }
.earthquake-icon div { .earthquake-icon div {
@ -885,7 +885,7 @@ body::before {
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
position: relative; /* position: relative removed here too */
z-index: 10000 !important; z-index: 10000 !important;
} }

@ -242,8 +242,7 @@ export const filterDXPaths = (paths, filters) => {
// Exclude list - hide matching callsigns // Exclude list - hide matching callsigns
if (filters.excludeList?.length > 0) { if (filters.excludeList?.length > 0) {
const isExcluded = filters.excludeList.some(e => const isExcluded = filters.excludeList.some(e =>
path.dxCall?.toUpperCase().includes(e.toUpperCase()) || path.dxCall?.toUpperCase().startsWith(e.toUpperCase())
path.spotter?.toUpperCase().includes(e.toUpperCase())
); );
if (isExcluded) return false; if (isExcluded) return false;
} }

@ -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")

@ -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!)");
Loading…
Cancel
Save

Powered by TurnKey Linux.