sun-service.ts•10.1 kB
import SunCalc from 'suncalc';
import { SunTimesParams, SunTimesRangeParams, SunPositionParams, NextSunEventParams } from '../interfaces/sun.js';
import { SunTimesInfo, SunPositionInfo, SunEventType } from '../types/sun.js';
/**
* Service for sun calculations
*/
export class SunService {
/**
* Get sun times for a specific date and location
* @param params Parameters for the request
* @returns Sun times information
*/
getSunTimes(params: SunTimesParams): SunTimesInfo {
const date = params.date ? new Date(params.date) : new Date();
const { latitude, longitude } = params;
// Get sun times data
const sunTimes = SunCalc.getTimes(date, latitude, longitude);
// Format times or return null if not available
const formatTime = (time: Date | null): string | null => {
if (!time || isNaN(time.getTime())) return null;
if (params.timezone) {
try {
return time.toLocaleTimeString('en-US', { timeZone: params.timezone });
} catch (error) {
// If timezone is invalid, fall back to ISO string
console.warn(`Invalid timezone: ${params.timezone}. Using UTC.`);
}
}
return time.toISOString();
};
// Calculate day length in minutes
const sunrise = sunTimes.sunrise;
const sunset = sunTimes.sunset;
let dayLength = 0;
if (sunrise && sunset && !isNaN(sunrise.getTime()) && !isNaN(sunset.getTime())) {
dayLength = (sunset.getTime() - sunrise.getTime()) / (60 * 1000);
}
return {
date: date.toISOString().split('T')[0],
sunrise: formatTime(sunTimes.sunrise),
sunset: formatTime(sunTimes.sunset),
solarNoon: formatTime(sunTimes.solarNoon),
dawn: formatTime(sunTimes.dawn),
dusk: formatTime(sunTimes.dusk),
nightStart: formatTime(sunTimes.night),
nightEnd: formatTime(sunTimes.nightEnd),
goldenHourStart: formatTime(sunTimes.goldenHour),
goldenHourEnd: formatTime(sunTimes.goldenHourEnd),
nauticalDawn: formatTime(sunTimes.nauticalDawn),
nauticalDusk: formatTime(sunTimes.nauticalDusk),
astronomicalDawn: formatTime(sunTimes.astronomicalDawn),
astronomicalDusk: formatTime(sunTimes.astronomicalDusk),
dayLength
};
}
/**
* Get sun times for a date range
* @param params Parameters for the request
* @returns Array of sun times information
*/
getSunTimesRange(params: SunTimesRangeParams): SunTimesInfo[] {
const startDate = new Date(params.start_date);
const endDate = new Date(params.end_date);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new Error('Invalid date format. Please use YYYY-MM-DD format.');
}
if (startDate > endDate) {
throw new Error('Start date must be before end date.');
}
const result: SunTimesInfo[] = [];
const currentDate = new Date(startDate);
while (currentDate <= endDate) {
result.push(this.getSunTimes({
date: currentDate.toISOString().split('T')[0],
latitude: params.latitude,
longitude: params.longitude,
timezone: params.timezone
}));
// Move to next day
currentDate.setDate(currentDate.getDate() + 1);
}
return result;
}
/**
* Get sun position for a specific date, time, and location
* @param params Parameters for the request
* @returns Sun position information
*/
getSunPosition(params: SunPositionParams): SunPositionInfo {
const date = params.date ? new Date(params.date) : new Date();
const time = params.time;
const { latitude, longitude } = params;
// Set the time if provided
if (time) {
const [hours, minutes, seconds] = time.split(':').map(Number);
if (!isNaN(hours) && !isNaN(minutes) && (!seconds || !isNaN(seconds))) {
date.setHours(hours, minutes, seconds || 0, 0);
} else {
throw new Error('Invalid time format. Please use HH:MM:SS format.');
}
}
// Get sun position data
const position = SunCalc.getPosition(date, latitude, longitude);
// Calculate right ascension and declination (approximate values)
// Note: These are approximate calculations and may not be precise
const equatorialCoords = this.calculateEquatorialCoordinates(date, position.azimuth, position.altitude, latitude, longitude);
return {
date: date.toISOString().split('T')[0],
time: date.toISOString().split('T')[1].split('.')[0],
azimuth: position.azimuth * (180 / Math.PI),
altitude: position.altitude * (180 / Math.PI),
declination: equatorialCoords.declination,
rightAscension: equatorialCoords.rightAscension
};
}
/**
* Get the next occurrence(s) of a specific sun event
* @param params Parameters for the request
* @returns Array of dates for the next occurrences of the specified event
*/
getNextSunEvent(params: NextSunEventParams): { date: string, time: string, event: string }[] {
const startDate = params.date ? new Date(params.date) : new Date();
const count = params.count !== undefined ? params.count : 1;
const { latitude, longitude } = params;
const timezone = params.timezone !== undefined ? params.timezone : 'UTC';
const results: { date: string, time: string, event: string }[] = [];
let currentDate = new Date(startDate);
// Find the next occurrences
while (results.length < count) {
const sunTimes = SunCalc.getTimes(currentDate, latitude, longitude);
const eventTime = sunTimes[params.event as keyof typeof sunTimes];
if (eventTime && !isNaN(eventTime.getTime()) && eventTime > startDate) {
let formattedTime: string;
try {
formattedTime = eventTime.toLocaleTimeString('en-US', { timeZone: timezone });
} catch (error) {
// If timezone is invalid, fall back to ISO string
console.warn(`Invalid timezone: ${timezone}. Using UTC.`);
formattedTime = eventTime.toISOString().split('T')[1].split('.')[0];
}
results.push({
date: eventTime.toISOString().split('T')[0],
time: formattedTime,
event: params.event as string
});
// Move to next day to find the next occurrence
currentDate.setDate(currentDate.getDate() + 1);
} else {
// Event not found for this day, try next day
currentDate.setDate(currentDate.getDate() + 1);
}
// Safety check to prevent infinite loops
if (results.length === 0 && currentDate.getTime() - startDate.getTime() > 366 * 24 * 60 * 60 * 1000) {
throw new Error('Could not find the specified sun event within a year.');
}
}
return results;
}
/**
* Calculate approximate equatorial coordinates (right ascension and declination)
* from horizontal coordinates (azimuth and altitude)
* Note: This is a simplified calculation and may not be precise
* @param date Date of observation
* @param azimuth Azimuth in radians
* @param altitude Altitude in radians
* @param latitude Observer's latitude
* @param longitude Observer's longitude
* @returns Approximate equatorial coordinates
*/
private calculateEquatorialCoordinates(date: Date, azimuth: number, altitude: number, latitude: number, longitude: number): { rightAscension: number, declination: number } {
// Convert degrees to radians
const lat = latitude * (Math.PI / 180);
// Calculate hour angle and declination
const sinDec = Math.sin(altitude) * Math.sin(lat) + Math.cos(altitude) * Math.cos(lat) * Math.cos(azimuth);
const declination = Math.asin(sinDec) * (180 / Math.PI);
const cosH = (Math.sin(altitude) - Math.sin(lat) * sinDec) / (Math.cos(lat) * Math.cos(declination * (Math.PI / 180)));
const hourAngle = Math.acos(Math.max(-1, Math.min(1, cosH)));
// Adjust hour angle based on azimuth
const adjustedHourAngle = (azimuth > 0 && azimuth < Math.PI) ? (2 * Math.PI - hourAngle) : hourAngle;
// Calculate right ascension
const localSiderealTime = this.calculateLocalSiderealTime(date, longitude);
let rightAscension = (localSiderealTime - adjustedHourAngle) * (12 / Math.PI);
// Normalize right ascension to 0-24 hours
rightAscension = rightAscension % 24;
if (rightAscension < 0) rightAscension += 24;
return { rightAscension, declination };
}
/**
* Calculate approximate local sidereal time
* @param date Date of observation
* @param longitude Observer's longitude
* @returns Local sidereal time in radians
*/
private calculateLocalSiderealTime(date: Date, longitude: number): number {
// Calculate days since J2000.0
const jd = this.calculateJulianDay(date);
const d = jd - 2451545.0;
// Calculate Greenwich Mean Sidereal Time
const gmst = (18.697374558 + 24.06570982441908 * d) % 24;
// Convert longitude to hours and calculate local sidereal time
const longitudeHours = longitude / 15;
let lst = gmst + longitudeHours;
// Normalize to 0-24 hours
lst = lst % 24;
if (lst < 0) lst += 24;
// Convert to radians
return lst * (Math.PI / 12);
}
/**
* Calculate Julian day from date
* @param date Date to convert
* @returns Julian day
*/
private calculateJulianDay(date: Date): number {
const y = date.getFullYear();
const m = date.getMonth() + 1;
const d = date.getDate();
// Calculate Julian day
const jd = 367 * y - Math.floor(7 * (y + Math.floor((m + 9) / 12)) / 4) -
Math.floor(3 * (Math.floor((y + (m - 9) / 7) / 100) + 1) / 4) +
Math.floor(275 * m / 9) + d + 1721028.5;
// Add time of day
const hours = date.getUTCHours();
const minutes = date.getUTCMinutes();
const seconds = date.getUTCSeconds();
const milliseconds = date.getUTCMilliseconds();
return jd + (hours + minutes / 60 + seconds / 3600 + milliseconds / 3600000) / 24;
}
}