118 lines
3.9 KiB
JavaScript
118 lines
3.9 KiB
JavaScript
import { createWriteStream, existsSync, mkdirSync } from 'fs'
|
|
import { resolve, dirname } from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
import { pipeline } from 'stream/promises'
|
|
|
|
const DIR = dirname(fileURLToPath(import.meta.url))
|
|
const ICON_DIR = resolve(DIR, 'public', 'icons')
|
|
const CACHE = {}
|
|
const CACHE_MS = 15 * 60 * 1000
|
|
|
|
if (!existsSync(ICON_DIR)) mkdirSync(ICON_DIR, { recursive: true })
|
|
|
|
const WMO = {
|
|
0: 'Clear Sky',
|
|
1: 'Mainly Clear',
|
|
2: 'Partly Cloudy',
|
|
3: 'Overcast',
|
|
45: 'Fog',
|
|
48: 'Icy Fog',
|
|
51: 'Light Drizzle',
|
|
53: 'Drizzle',
|
|
55: 'Heavy Drizzle',
|
|
61: 'Light Rain',
|
|
63: 'Rain',
|
|
65: 'Heavy Rain',
|
|
71: 'Light Snow',
|
|
73: 'Snow',
|
|
75: 'Heavy Snow',
|
|
77: 'Snow Grains',
|
|
80: 'Light Showers',
|
|
81: 'Showers',
|
|
82: 'Heavy Showers',
|
|
85: 'Snow Showers',
|
|
86: 'Heavy Snow Showers',
|
|
95: 'Thunderstorm',
|
|
96: 'Thunderstorm w/ Hail',
|
|
99: 'Thunderstorm w/ Heavy Hail',
|
|
}
|
|
|
|
const OWM_ICONS = {
|
|
0: '01d', 1: '01d', 2: '02d', 3: '04d',
|
|
45: '50d', 48: '50d',
|
|
51: '09d', 53: '09d', 55: '09d',
|
|
61: '10d', 63: '10d', 65: '10d',
|
|
71: '13d', 73: '13d', 75: '13d', 77: '13d',
|
|
80: '09d', 81: '09d', 82: '09d',
|
|
85: '13d', 86: '13d',
|
|
95: '11d', 96: '11d', 99: '11d',
|
|
}
|
|
|
|
async function ensureIcon(code) {
|
|
const owmCode = OWM_ICONS[code] || '01d'
|
|
const filename = `${owmCode}.png`
|
|
const filepath = resolve(ICON_DIR, filename)
|
|
if (!existsSync(filepath)) {
|
|
const res = await fetch(`https://openweathermap.org/img/wn/${owmCode}@2x.png`)
|
|
await pipeline(res.body, createWriteStream(filepath))
|
|
}
|
|
return `/icons/${filename}`
|
|
}
|
|
|
|
async function cachedFetch(key, url) {
|
|
const now = Date.now()
|
|
if (CACHE[key] && now - CACHE[key].ts < CACHE_MS) return CACHE[key].data
|
|
const res = await fetch(url)
|
|
const data = await res.json()
|
|
CACHE[key] = { ts: now, data }
|
|
return data
|
|
}
|
|
|
|
const DAILY_FIELDS = [
|
|
'temperature_2m_max', 'temperature_2m_min',
|
|
'precipitation_sum', 'precipitation_probability_max',
|
|
'windspeed_10m_max', 'windgusts_10m_max', 'winddirection_10m_dominant',
|
|
'weathercode', 'sunrise', 'sunset',
|
|
'uv_index_max', 'shortwave_radiation_sum',
|
|
].join(',')
|
|
|
|
// start & end are Date objects
|
|
export async function getOpenMeteo(lat, lon, start, end) {
|
|
const startStr = start.toISOString().slice(0, 10)
|
|
const endStr = end.toISOString().slice(0, 10)
|
|
|
|
console.log('[meteo] startStr :', startStr)
|
|
console.log('[meteo] endStr :', endStr)
|
|
console.log('[meteo] url :', url)
|
|
|
|
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}` +
|
|
`&daily=${DAILY_FIELDS}&timezone=auto&start_date=${startStr}&end_date=${endStr}`
|
|
|
|
const data = await cachedFetch(`daily_${lat}_${lon}_${startStr}_${endStr}`, url)
|
|
|
|
if (!data?.daily?.time) return []
|
|
|
|
return Promise.all(data.daily.time.map(async (time, i) => {
|
|
const code = data.daily.weathercode[i]
|
|
const icon = await ensureIcon(code).catch(() => null)
|
|
return {
|
|
time,
|
|
label: WMO[code] || 'Unknown',
|
|
icon,
|
|
forecast_weathercode: code,
|
|
temperature: data.daily.temperature_2m_max[i],
|
|
env_temp_max_c: data.daily.temperature_2m_max[i],
|
|
env_temp_min_c: data.daily.temperature_2m_min[i],
|
|
precipitation: data.daily.precipitation_sum[i],
|
|
forecast_precipitation_probability: data.daily.precipitation_probability_max[i],
|
|
wind_speed: data.daily.windspeed_10m_max[i],
|
|
wind_gusts: data.daily.windgusts_10m_max[i],
|
|
wind_direction: data.daily.winddirection_10m_dominant[i],
|
|
uv_index: data.daily.uv_index_max[i],
|
|
solar_wm2: data.daily.shortwave_radiation_sum[i],
|
|
sunrise: data.daily.sunrise[i],
|
|
sunset: data.daily.sunset[i],
|
|
}
|
|
}))
|
|
}
|