You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
7.5 KiB
229 lines
7.5 KiB
/**
|
|
* Geographic Calculation Utilities
|
|
* Grid squares, bearings, distances, sun/moon positions
|
|
*/
|
|
|
|
/**
|
|
* Calculate Maidenhead grid square from coordinates
|
|
*/
|
|
export const calculateGridSquare = (lat, lon) => {
|
|
const lonNorm = lon + 180;
|
|
const latNorm = lat + 90;
|
|
const field1 = String.fromCharCode(65 + Math.floor(lonNorm / 20));
|
|
const field2 = String.fromCharCode(65 + Math.floor(latNorm / 10));
|
|
const square1 = Math.floor((lonNorm % 20) / 2);
|
|
const square2 = Math.floor(latNorm % 10);
|
|
const subsq1 = String.fromCharCode(97 + Math.floor((lonNorm % 2) * 12));
|
|
const subsq2 = String.fromCharCode(97 + Math.floor((latNorm % 1) * 24));
|
|
return `${field1}${field2}${square1}${square2}${subsq1}${subsq2}`;
|
|
};
|
|
|
|
/**
|
|
* Calculate bearing between two points
|
|
*/
|
|
export const calculateBearing = (lat1, lon1, lat2, lon2) => {
|
|
const φ1 = lat1 * Math.PI / 180;
|
|
const φ2 = lat2 * Math.PI / 180;
|
|
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
|
const y = Math.sin(Δλ) * Math.cos(φ2);
|
|
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
|
|
return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
|
|
};
|
|
|
|
/**
|
|
* Calculate distance between two points in km
|
|
*/
|
|
export const calculateDistance = (lat1, lon1, lat2, lon2) => {
|
|
const R = 6371;
|
|
const φ1 = lat1 * Math.PI / 180;
|
|
const φ2 = lat2 * Math.PI / 180;
|
|
const Δφ = (lat2 - lat1) * Math.PI / 180;
|
|
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
|
const a = Math.sin(Δφ/2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) ** 2;
|
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
};
|
|
|
|
/**
|
|
* Get subsolar point (position where sun is directly overhead)
|
|
*/
|
|
export const getSunPosition = (date) => {
|
|
const dayOfYear = Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 86400000);
|
|
const declination = -23.45 * Math.cos((360/365) * (dayOfYear + 10) * Math.PI / 180);
|
|
const hours = date.getUTCHours() + date.getUTCMinutes() / 60;
|
|
const longitude = (12 - hours) * 15;
|
|
return { lat: declination, lon: longitude };
|
|
};
|
|
|
|
/**
|
|
* Calculate sublunar point (position where moon is directly overhead)
|
|
*/
|
|
export const getMoonPosition = (date) => {
|
|
// Julian date calculation
|
|
const JD = date.getTime() / 86400000 + 2440587.5;
|
|
const T = (JD - 2451545.0) / 36525; // Julian centuries from J2000
|
|
|
|
// Moon's mean longitude
|
|
const L0 = (218.316 + 481267.8813 * T) % 360;
|
|
|
|
// Moon's mean anomaly
|
|
const M = (134.963 + 477198.8676 * T) % 360;
|
|
const MRad = M * Math.PI / 180;
|
|
|
|
// Moon's mean elongation
|
|
const D = (297.850 + 445267.1115 * T) % 360;
|
|
const DRad = D * Math.PI / 180;
|
|
|
|
// Sun's mean anomaly
|
|
const Ms = (357.529 + 35999.0503 * T) % 360;
|
|
const MsRad = Ms * Math.PI / 180;
|
|
|
|
// Moon's argument of latitude
|
|
const F = (93.272 + 483202.0175 * T) % 360;
|
|
const FRad = F * Math.PI / 180;
|
|
|
|
// Longitude corrections (simplified)
|
|
const dL = 6.289 * Math.sin(MRad)
|
|
+ 1.274 * Math.sin(2 * DRad - MRad)
|
|
+ 0.658 * Math.sin(2 * DRad)
|
|
+ 0.214 * Math.sin(2 * MRad)
|
|
- 0.186 * Math.sin(MsRad)
|
|
- 0.114 * Math.sin(2 * FRad);
|
|
|
|
// Moon's ecliptic longitude
|
|
const moonLon = ((L0 + dL) % 360 + 360) % 360;
|
|
|
|
// Moon's ecliptic latitude (simplified)
|
|
const moonLat = 5.128 * Math.sin(FRad)
|
|
+ 0.281 * Math.sin(MRad + FRad)
|
|
+ 0.278 * Math.sin(MRad - FRad);
|
|
|
|
// Convert ecliptic to equatorial coordinates
|
|
const obliquity = 23.439 - 0.0000004 * (JD - 2451545.0);
|
|
const oblRad = obliquity * Math.PI / 180;
|
|
const moonLonRad = moonLon * Math.PI / 180;
|
|
const moonLatRad = moonLat * Math.PI / 180;
|
|
|
|
// Right ascension
|
|
const RA = Math.atan2(
|
|
Math.sin(moonLonRad) * Math.cos(oblRad) - Math.tan(moonLatRad) * Math.sin(oblRad),
|
|
Math.cos(moonLonRad)
|
|
) * 180 / Math.PI;
|
|
|
|
// Declination
|
|
const dec = Math.asin(
|
|
Math.sin(moonLatRad) * Math.cos(oblRad) +
|
|
Math.cos(moonLatRad) * Math.sin(oblRad) * Math.sin(moonLonRad)
|
|
) * 180 / Math.PI;
|
|
|
|
// Greenwich Mean Sidereal Time
|
|
const GMST = (280.46061837 + 360.98564736629 * (JD - 2451545.0)) % 360;
|
|
|
|
// Sublunar point longitude
|
|
const sublunarLon = ((RA - GMST) % 360 + 540) % 360 - 180;
|
|
|
|
return { lat: dec, lon: sublunarLon };
|
|
};
|
|
|
|
/**
|
|
* Calculate moon phase (0-1, 0=new, 0.5=full)
|
|
*/
|
|
export const getMoonPhase = (date) => {
|
|
const JD = date.getTime() / 86400000 + 2440587.5;
|
|
const T = (JD - 2451545.0) / 36525;
|
|
const D = (297.850 + 445267.1115 * T) % 360; // Mean elongation
|
|
// Phase angle (simplified)
|
|
const phase = ((D + 180) % 360) / 360;
|
|
return phase;
|
|
};
|
|
|
|
/**
|
|
* Get moon phase emoji
|
|
*/
|
|
export const getMoonPhaseEmoji = (phase) => {
|
|
if (phase < 0.0625) return '🌑'; // New moon
|
|
if (phase < 0.1875) return '🌒'; // Waxing crescent
|
|
if (phase < 0.3125) return '🌓'; // First quarter
|
|
if (phase < 0.4375) return '🌔'; // Waxing gibbous
|
|
if (phase < 0.5625) return '🌕'; // Full moon
|
|
if (phase < 0.6875) return '🌖'; // Waning gibbous
|
|
if (phase < 0.8125) return '🌗'; // Last quarter
|
|
if (phase < 0.9375) return '🌘'; // Waning crescent
|
|
return '🌑'; // New moon
|
|
};
|
|
|
|
/**
|
|
* Calculate sunrise and sunset times
|
|
*/
|
|
export const calculateSunTimes = (lat, lon, date) => {
|
|
const dayOfYear = Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 86400000);
|
|
const declination = -23.45 * Math.cos((360/365) * (dayOfYear + 10) * Math.PI / 180);
|
|
const latRad = lat * Math.PI / 180;
|
|
const decRad = declination * Math.PI / 180;
|
|
const cosHA = -Math.tan(latRad) * Math.tan(decRad);
|
|
|
|
if (cosHA > 1) return { sunrise: 'Polar night', sunset: '' };
|
|
if (cosHA < -1) return { sunrise: 'Midnight sun', sunset: '' };
|
|
|
|
const ha = Math.acos(cosHA) * 180 / Math.PI;
|
|
const noon = 12 - lon / 15;
|
|
const fmt = (h) => {
|
|
const hr = Math.floor(((h % 24) + 24) % 24);
|
|
const mn = Math.round((h - Math.floor(h)) * 60);
|
|
return `${hr.toString().padStart(2,'0')}:${mn.toString().padStart(2,'0')}`;
|
|
};
|
|
return { sunrise: fmt(noon - ha/15), sunset: fmt(noon + ha/15) };
|
|
};
|
|
|
|
/**
|
|
* Calculate great circle path points for Leaflet
|
|
* Handles antimeridian crossing by returning multiple segments
|
|
*/
|
|
export const getGreatCirclePoints = (lat1, lon1, lat2, lon2, n = 100) => {
|
|
const toRad = d => d * Math.PI / 180;
|
|
const toDeg = r => r * 180 / Math.PI;
|
|
|
|
const φ1 = toRad(lat1), λ1 = toRad(lon1);
|
|
const φ2 = toRad(lat2), λ2 = toRad(lon2);
|
|
|
|
const d = 2 * Math.asin(Math.sqrt(
|
|
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);
|
|
const B = Math.sin(f*d) / Math.sin(d);
|
|
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);
|
|
rawPoints.push([toDeg(Math.atan2(z, Math.sqrt(x*x+y*y))), toDeg(Math.atan2(y, x))]);
|
|
}
|
|
|
|
// 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++) {
|
|
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;
|
|
}
|
|
|
|
return rawPoints;
|
|
};
|
|
|
|
export default {
|
|
calculateGridSquare,
|
|
calculateBearing,
|
|
calculateDistance,
|
|
getSunPosition,
|
|
getMoonPosition,
|
|
getMoonPhase,
|
|
getMoonPhaseEmoji,
|
|
calculateSunTimes,
|
|
getGreatCirclePoints
|
|
};
|