Icon caching
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Loading from '@/components/Loading.vue';
|
||||
import {inject, onMounted, ref} from 'vue';
|
||||
import {api} from '../services/api';
|
||||
import {api, BASE} from '../services/api';
|
||||
|
||||
const data = ref<any>();
|
||||
const openGraph = inject<(key: string) => void>('openGraph');
|
||||
@@ -80,7 +80,7 @@ onMounted(async () => {
|
||||
<div class="flex-r align-items-center justify-between">
|
||||
<div v-if="!data" class="pos-rel br-2 overflow-hidden" style="width: 75px; height: 50px"><Loading/></div>
|
||||
<div v-else class="flex-c justify-center align-items-center mx-3">
|
||||
<img :src="'https://openweathermap.org/img/wn/' + data.icon + '@2x.png'" class="today-icon" alt="weather" />
|
||||
<img :src="BASE + '/api/icon/' + data.icon" class="today-icon" alt="weather" />
|
||||
<div class="today-label">{{ data.label }}</div>
|
||||
</div>
|
||||
<div class="flex-r align-items-center gap-2">
|
||||
|
||||
@@ -67,7 +67,7 @@ onMounted(async () => {
|
||||
<div v-if="days?.length" class="strip">
|
||||
<div v-for="d in days" class="day">
|
||||
<div class="day-name">{{ formatDate('ddd, MMM D', d.time) }}</div>
|
||||
<img :src="BASE + d.icon" class="day-icon" :alt="(d.label?.toString() || '')" />
|
||||
<img :src="BASE + '/api/icon/' + d.icon" class="day-icon" :alt="(d.label?.toString() || '')" />
|
||||
<div class="day-label">{{ d.label }}</div>
|
||||
<div class="day-temps">
|
||||
<span>↑{{ ~~(d.temperature_max) + '°' || '—' }}</span>
|
||||
|
||||
@@ -87,7 +87,7 @@ onMounted(async () => {
|
||||
<template v-if="hours?.length">
|
||||
<div v-for="h in hours" class="hour-row">
|
||||
<span class="hour-time">{{ formatDate('h A', h.time) }}</span>
|
||||
<img :src="BASE + h.icon" class="hour-icon" :alt="h.label?.toString() || ''" />
|
||||
<img :src="BASE + '/api/icon/' + h.icon" class="hour-icon" :alt="h.label?.toString() || ''" />
|
||||
<span class="hour-label">{{ h.label }}</span>
|
||||
<div class="hour-meta">
|
||||
<span>🌧️ {{ (h.precipitation as any || 0).toFixed(1) }}mm</span>
|
||||
|
||||
|
Before Width: | Height: | Size: 948 B |
|
Before Width: | Height: | Size: 945 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -145,7 +145,6 @@ export async function get24HourForecast(currentSensors) {
|
||||
};
|
||||
|
||||
Object.assign(snapshot, getWeatherCondition(snapshot));
|
||||
snapshot.icon = `/icons/${snapshot.icon}.png`;
|
||||
return snapshot;
|
||||
});
|
||||
|
||||
|
||||
@@ -33,17 +33,6 @@ const OWM_ICONS = {
|
||||
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
|
||||
|
||||
@@ -1,48 +1,22 @@
|
||||
import {existsSync, mkdirSync, writeFileSync} from 'fs';
|
||||
import {resolve, dirname} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const DIR = resolve(dirname(fileURLToPath(import.meta.url)), 'public', 'icons');
|
||||
const DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../data/icons');
|
||||
|
||||
export const WMO = {
|
||||
0: {label: 'Clear Sky', icon: '01d'},
|
||||
1: {label: 'Mainly Clear', icon: '01d'},
|
||||
2: {label: 'Partly Cloudy', icon: '02d'},
|
||||
3: {label: 'Overcast', icon: '04d'},
|
||||
45: {label: 'Fog', icon: '50d'},
|
||||
48: {label: 'Icy Fog', icon: '50d'},
|
||||
51: {label: 'Light Drizzle', icon: '09d'},
|
||||
53: {label: 'Drizzle', icon: '09d'},
|
||||
55: {label: 'Heavy Drizzle', icon: '09d'},
|
||||
61: {label: 'Light Rain', icon: '10d'},
|
||||
63: {label: 'Rain', icon: '10d'},
|
||||
65: {label: 'Heavy Rain', icon: '10d'},
|
||||
71: {label: 'Light Snow', icon: '13d'},
|
||||
73: {label: 'Snow', icon: '13d'},
|
||||
75: {label: 'Heavy Snow', icon: '13d'},
|
||||
77: {label: 'Snow Grains', icon: '13d'},
|
||||
80: {label: 'Light Showers', icon: '09d'},
|
||||
81: {label: 'Showers', icon: '09d'},
|
||||
82: {label: 'Heavy Showers', icon: '09d'},
|
||||
85: {label: 'Snow Showers', icon: '13d'},
|
||||
86: {label: 'Heavy Snow Showers', icon: '13d'},
|
||||
95: {label: 'Thunderstorm', icon: '11d'},
|
||||
96: {label: 'Thunderstorm w/ Hail', icon: '11d'},
|
||||
99: {label: 'Thunderstorm w/ Hail', icon: '11d'},
|
||||
};
|
||||
export async function downloadIcon(icon, path) {
|
||||
if(!existsSync(DIR)) mkdirSync(dirname(path), {recursive: true});
|
||||
const res = await fetch(`https://openweathermap.org/img/wn/${icon}@2x.png`);
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
writeFileSync(path, buf);
|
||||
}
|
||||
|
||||
export async function downloadIcons() {
|
||||
if(!existsSync(DIR)) mkdirSync(DIR, {recursive: true});
|
||||
const icons = new Set(Object.values(WMO).map(w => [w.icon, w.icon.replace('d', 'n')]).flat());
|
||||
await Promise.all([...icons].map(async icon => {
|
||||
const path = resolve(DIR, `${icon}.png`);
|
||||
if(existsSync(path)) return;
|
||||
const res = await fetch(`https://openweathermap.org/img/wn/${icon}@2x.png`);
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
writeFileSync(path, buf);
|
||||
console.log(` 📥 Downloaded icon: ${icon}.png`);
|
||||
}));
|
||||
console.log('✅ Weather icons ready');
|
||||
export async function fetchIcon(icon) {
|
||||
const iconPath = path.join(DIR, icon + '.png');
|
||||
if(!existsSync(DIR) || !existsSync(iconPath))
|
||||
await downloadIcon(icon, iconPath);
|
||||
return iconPath;
|
||||
}
|
||||
|
||||
export function getWeatherCondition(data) {
|
||||
|
||||
@@ -4,14 +4,13 @@ import {fileURLToPath} from 'url';
|
||||
import {cfg, localDateStr} from './config.mjs';
|
||||
import {queryCurrent, queryHourly, queryDaily, getCoords} from './influx.mjs';
|
||||
import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs';
|
||||
import {getSpaceWeather} from './space.mjs';
|
||||
import {apiReference} from '@scalar/express-api-reference';
|
||||
import {spec} from './spec.mjs';
|
||||
import {existsSync} from 'fs';
|
||||
import {getAIS} from './ais.mjs';
|
||||
import {getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs';
|
||||
import {getWeatherCondition} from './openweather.mjs';
|
||||
import {lastForecast, getForecast, forecastTTL, get24HourForecast} from './forecast.mjs';
|
||||
import {fetchIcon, getWeatherCondition} from './openweather.mjs';
|
||||
import {lastForecast, getForecast, forecastTTL} from './forecast.mjs';
|
||||
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
|
||||
// import {Aurora} from './aurora.mjs';
|
||||
|
||||
@@ -30,7 +29,6 @@ const DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const CLIENT_DIST = resolve(DIR, '../public');
|
||||
|
||||
app.use(express.json());
|
||||
app.use('/icons', express.static(resolve(DIR, '../public', 'icons')));
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
next();
|
||||
@@ -134,6 +132,15 @@ app.get('/api/position', async (req, res) => {
|
||||
res.json(filterFields(data, fields));
|
||||
});
|
||||
|
||||
// ── Icon ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/icon/:icon', async (req, resp, next) => {
|
||||
const { icon } = req.params;
|
||||
if(!icon) return next(); // 404 fallback can handle missing icons
|
||||
resp.contentType('image/png');
|
||||
resp.sendFile(await fetchIcon(icon));
|
||||
});
|
||||
|
||||
// ── Space ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// app.get('/api/aurora', async (req, res) => res.json(await Aurora.get()));
|
||||
|
||||
@@ -131,7 +131,7 @@ export const spec = {
|
||||
// Forecast
|
||||
forecast_weathercode: { type: 'number', description: 'WMO weather code' },
|
||||
forecast_weather_label: { type: 'string', description: 'Human readable weather label' },
|
||||
forecast_weather_icon: { type: 'string', description: 'Local icon path e.g. /icons/01d.png' },
|
||||
forecast_weather_icon: { type: 'string', description: 'Local icon path e.g. 01d' },
|
||||
forecast_precipitation_mm: { type: 'number', description: 'Precipitation (mm)' },
|
||||
forecast_precipitation_probability: { type: 'number', description: 'Precipitation probability (%)' },
|
||||
forecast_snowfall_mm: { type: 'number', description: 'Snowfall (mm)' },
|
||||
|
||||