From f47e485b4ced9ceb3fc1bc6f70af5e64daab2677 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 18:22:47 +0000 Subject: [PATCH] Draw gray line as a smooth analytic terminator band Old drawGrayLine() built the twilight zone by sweeping a 2-degree lat/lon grid for points where |solar altitude| < 6 deg, then bucketing them into 5-degree latitude bands and emitting one rectangle per band+segment. The result rendered as a stack of 10-degree-tall rectangles -- a visible staircase along the terminator on the map. Replace with a closed-form solve. For each longitude, solve sin(alpha) = sin(decl)*sin(lat) + cos(decl)*cos(HA)*cos(lat) = R * sin(lat + phi) for alpha = +/-6 deg, giving the day-edge and night-edge latitude of the civil-twilight band. Walk longitude west-to-east to build the day edge, east-to-west to close the night edge, and render the result as a single L.polygon. No bucketing, no rectangles, no staircase. Sweep at 1-degree longitude steps (361 vertices per edge) for a smooth curve at the price of a single L.polygon instead of dozens of small ones, so total draw cost is comparable. served.html kept in sync with dashboard.html. https://claude.ai/code/session_01BLfWHK9hf8k4aBBaQjfQa8 --- served.html | 105 +++++++++------------------ src/dvoacap/dashboard/dashboard.html | 105 +++++++++------------------ 2 files changed, 68 insertions(+), 142 deletions(-) diff --git a/served.html b/served.html index b5bd1ba..00a5f75 100644 --- a/served.html +++ b/served.html @@ -2509,79 +2509,42 @@

Understanding the Graph Behavior

const declination = 23.45 * Math.sin((360/365) * (dayOfYear - 81) * Math.PI / 180); const declinationRad = declination * Math.PI / 180; - // Draw gray line as a band (civil twilight: solar altitude between -6° and +6°) - const grayLinePoints = []; - - // Sweep across all longitudes and latitudes to find twilight zone - for (let lon = -180; lon <= 180; lon += 2) { - for (let lat = -90; lat <= 90; lat += 2) { - const latRad = lat * Math.PI / 180; - - // Calculate hour angle from longitude and time - const solarLon = -15 * (hours - 12); // Sun's longitude at this time - const hourAngle = (lon - solarLon) * Math.PI / 180; - - // Calculate solar altitude using spherical astronomy - const sinAlt = Math.sin(latRad) * Math.sin(declinationRad) + - Math.cos(latRad) * Math.cos(declinationRad) * Math.cos(hourAngle); - const solarAlt = Math.asin(Math.max(-1, Math.min(1, sinAlt))) * 180 / Math.PI; - - // Gray line is where solar altitude is near 0 (±6 degrees for civil twilight) - if (Math.abs(solarAlt) < 6) { - grayLinePoints.push([lat, lon]); - } - } + // Draw gray line as a band (civil twilight: solar altitude between -6° and +6°). + // Solve sin(α) = sinDecl·sin(lat) + cosDecl·cos(HA)·cos(lat) + // = R·sin(lat + φ) + // for α = ±6° at every longitude, where + // R = √(sin²δ + cos²δ·cos²HA), φ = atan2(cosδ·cosHA, sinδ). + // Then lat = asin(sin(α)/R) − φ, clamped to [−90°, 90°]. + const sinDecl = Math.sin(declinationRad); + const cosDecl = Math.cos(declinationRad); + const solarLon = -15 * (hours - 12); + const sinAltDay = Math.sin(6 * Math.PI / 180); + const sinAltNight = Math.sin(-6 * Math.PI / 180); + + const dayEdge = []; + const nightEdge = []; + for (let lon = -180; lon <= 180; lon += 1) { + const hourAngle = (lon - solarLon) * Math.PI / 180; + const cosHA = Math.cos(hourAngle); + const R = Math.sqrt(sinDecl * sinDecl + cosDecl * cosDecl * cosHA * cosHA); + const phi = Math.atan2(cosDecl * cosHA, sinDecl); + const argDay = Math.max(-1, Math.min(1, sinAltDay / R)); + const argNight = Math.max(-1, Math.min(1, sinAltNight / R)); + const latDay = (Math.asin(argDay) - phi) * 180 / Math.PI; + const latNight = (Math.asin(argNight) - phi) * 180 / Math.PI; + dayEdge.push([Math.max(-90, Math.min(90, latDay)), lon]); + nightEdge.push([Math.max(-90, Math.min(90, latNight)), lon]); } - if (grayLinePoints.length > 0) { - // Create a feature group for better visualization - const features = []; - - // Group nearby points to create a continuous band - const grouped = {}; - grayLinePoints.forEach(([lat, lon]) => { - const latKey = Math.round(lat / 5) * 5; // Group by 5-degree latitude bands - if (!grouped[latKey]) grouped[latKey] = []; - grouped[latKey].push(lon); - }); - - // For each latitude band, create a polygon strip - const polygons = []; - for (const [latKey, lons] of Object.entries(grouped)) { - const lat = parseFloat(latKey); - lons.sort((a, b) => a - b); - - // Create rectangles for continuous longitude segments - let segmentStart = lons[0]; - for (let i = 1; i < lons.length; i++) { - if (lons[i] - lons[i-1] > 10) { // Gap detected - // Close current segment - polygons.push([ - [lat - 5, segmentStart], - [lat + 5, segmentStart], - [lat + 5, lons[i-1]], - [lat - 5, lons[i-1]] - ]); - segmentStart = lons[i]; - } - } - // Close final segment - polygons.push([ - [lat - 5, segmentStart], - [lat + 5, segmentStart], - [lat + 5, lons[lons.length - 1]], - [lat - 5, lons[lons.length - 1]] - ]); - } - - grayLineLayer = L.polygon(polygons, { - color: '#fbbf24', - fillColor: '#fbbf24', - weight: 1, - opacity: 0.5, - fillOpacity: 0.3 - }).addTo(map); - } + // Closed polygon: day edge west→east, then night edge east→west. + const polygon = dayEdge.concat(nightEdge.reverse()); + grayLineLayer = L.polygon(polygon, { + color: '#fbbf24', + fillColor: '#fbbf24', + weight: 1, + opacity: 0.5, + fillOpacity: 0.3 + }).addTo(map); // Update time display if (grayLineHour !== null) { diff --git a/src/dvoacap/dashboard/dashboard.html b/src/dvoacap/dashboard/dashboard.html index b5bd1ba..00a5f75 100644 --- a/src/dvoacap/dashboard/dashboard.html +++ b/src/dvoacap/dashboard/dashboard.html @@ -2509,79 +2509,42 @@

Understanding the Graph Behavior

const declination = 23.45 * Math.sin((360/365) * (dayOfYear - 81) * Math.PI / 180); const declinationRad = declination * Math.PI / 180; - // Draw gray line as a band (civil twilight: solar altitude between -6° and +6°) - const grayLinePoints = []; - - // Sweep across all longitudes and latitudes to find twilight zone - for (let lon = -180; lon <= 180; lon += 2) { - for (let lat = -90; lat <= 90; lat += 2) { - const latRad = lat * Math.PI / 180; - - // Calculate hour angle from longitude and time - const solarLon = -15 * (hours - 12); // Sun's longitude at this time - const hourAngle = (lon - solarLon) * Math.PI / 180; - - // Calculate solar altitude using spherical astronomy - const sinAlt = Math.sin(latRad) * Math.sin(declinationRad) + - Math.cos(latRad) * Math.cos(declinationRad) * Math.cos(hourAngle); - const solarAlt = Math.asin(Math.max(-1, Math.min(1, sinAlt))) * 180 / Math.PI; - - // Gray line is where solar altitude is near 0 (±6 degrees for civil twilight) - if (Math.abs(solarAlt) < 6) { - grayLinePoints.push([lat, lon]); - } - } + // Draw gray line as a band (civil twilight: solar altitude between -6° and +6°). + // Solve sin(α) = sinDecl·sin(lat) + cosDecl·cos(HA)·cos(lat) + // = R·sin(lat + φ) + // for α = ±6° at every longitude, where + // R = √(sin²δ + cos²δ·cos²HA), φ = atan2(cosδ·cosHA, sinδ). + // Then lat = asin(sin(α)/R) − φ, clamped to [−90°, 90°]. + const sinDecl = Math.sin(declinationRad); + const cosDecl = Math.cos(declinationRad); + const solarLon = -15 * (hours - 12); + const sinAltDay = Math.sin(6 * Math.PI / 180); + const sinAltNight = Math.sin(-6 * Math.PI / 180); + + const dayEdge = []; + const nightEdge = []; + for (let lon = -180; lon <= 180; lon += 1) { + const hourAngle = (lon - solarLon) * Math.PI / 180; + const cosHA = Math.cos(hourAngle); + const R = Math.sqrt(sinDecl * sinDecl + cosDecl * cosDecl * cosHA * cosHA); + const phi = Math.atan2(cosDecl * cosHA, sinDecl); + const argDay = Math.max(-1, Math.min(1, sinAltDay / R)); + const argNight = Math.max(-1, Math.min(1, sinAltNight / R)); + const latDay = (Math.asin(argDay) - phi) * 180 / Math.PI; + const latNight = (Math.asin(argNight) - phi) * 180 / Math.PI; + dayEdge.push([Math.max(-90, Math.min(90, latDay)), lon]); + nightEdge.push([Math.max(-90, Math.min(90, latNight)), lon]); } - if (grayLinePoints.length > 0) { - // Create a feature group for better visualization - const features = []; - - // Group nearby points to create a continuous band - const grouped = {}; - grayLinePoints.forEach(([lat, lon]) => { - const latKey = Math.round(lat / 5) * 5; // Group by 5-degree latitude bands - if (!grouped[latKey]) grouped[latKey] = []; - grouped[latKey].push(lon); - }); - - // For each latitude band, create a polygon strip - const polygons = []; - for (const [latKey, lons] of Object.entries(grouped)) { - const lat = parseFloat(latKey); - lons.sort((a, b) => a - b); - - // Create rectangles for continuous longitude segments - let segmentStart = lons[0]; - for (let i = 1; i < lons.length; i++) { - if (lons[i] - lons[i-1] > 10) { // Gap detected - // Close current segment - polygons.push([ - [lat - 5, segmentStart], - [lat + 5, segmentStart], - [lat + 5, lons[i-1]], - [lat - 5, lons[i-1]] - ]); - segmentStart = lons[i]; - } - } - // Close final segment - polygons.push([ - [lat - 5, segmentStart], - [lat + 5, segmentStart], - [lat + 5, lons[lons.length - 1]], - [lat - 5, lons[lons.length - 1]] - ]); - } - - grayLineLayer = L.polygon(polygons, { - color: '#fbbf24', - fillColor: '#fbbf24', - weight: 1, - opacity: 0.5, - fillOpacity: 0.3 - }).addTo(map); - } + // Closed polygon: day edge west→east, then night edge east→west. + const polygon = dayEdge.concat(nightEdge.reverse()); + grayLineLayer = L.polygon(polygon, { + color: '#fbbf24', + fillColor: '#fbbf24', + weight: 1, + opacity: 0.5, + fillOpacity: 0.3 + }).addTo(map); // Update time display if (grayLineHour !== null) {