Files
weather/app/server.js
T

178 lines
5.9 KiB
JavaScript

const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.static(path.join(__dirname, 'public')));
// Geocoding proxy
app.get('/api/geocode', async (req, res) => {
const { q } = req.query;
if (!q) return res.status(400).json({ error: 'Missing query' });
try {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(q)}&count=5&language=en&format=json`;
const r = await fetch(url);
const data = await r.json();
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Geocoding failed', detail: e.message });
}
});
// Reverse geocoding proxy
app.get('/api/reverse-geocode', async (req, res) => {
const { lat, lon } = req.query;
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
try {
// Prefer Open-Meteo reverse geocoding for stable city/region labels.
const omParams = new URLSearchParams({
latitude: String(lat),
longitude: String(lon),
count: '1',
language: 'en',
format: 'json',
});
const omResp = await fetch(`https://geocoding-api.open-meteo.com/v1/reverse?${omParams}`);
if (omResp.ok) {
const omData = await omResp.json();
const top = omData?.results?.[0];
if (top) {
return res.json({
name: top.name || top.admin2 || top.admin1 || top.country || 'Detected location',
admin1: top.admin1 || top.admin2 || '',
country: top.country || '',
latitude: Number(lat),
longitude: Number(lon),
timezone: top.timezone || 'auto',
display_name: [top.name, top.admin1, top.country].filter(Boolean).join(', '),
});
}
}
const params = new URLSearchParams({
format: 'jsonv2',
lat: String(lat),
lon: String(lon),
zoom: '10',
addressdetails: '1',
});
const r = await fetch(`https://nominatim.openstreetmap.org/reverse?${params}`, {
headers: {
'User-Agent': 'weather-app-v2 (nate@necloud.us)',
Accept: 'application/json',
},
});
if (!r.ok) {
throw new Error(`Reverse geocoding failed with status ${r.status}`);
}
const data = await r.json();
const address = data.address || {};
const name = address.city || address.town || address.village || address.hamlet || address.municipality || address.county || address.state || data.name || 'Detected location';
res.json({
name,
admin1: address.state || address.region || address.county || '',
country: address.country || '',
latitude: Number(lat),
longitude: Number(lon),
timezone: 'auto',
display_name: data.display_name || '',
});
} catch (e) {
res.status(500).json({ error: 'Reverse geocoding failed', detail: e.message });
}
});
// Weather proxy
app.get('/api/weather', async (req, res) => {
const { lat, lon, tz, units } = req.query;
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
const timezone = tz || 'auto';
const useMetric = units === 'metric';
try {
const params = new URLSearchParams({
latitude: lat, longitude: lon, timezone,
current: [
'temperature_2m','relative_humidity_2m','apparent_temperature',
'is_day','precipitation','weather_code','cloud_cover',
'wind_speed_10m','wind_direction_10m','surface_pressure','visibility'
].join(','),
hourly: [
'temperature_2m','precipitation_probability','weather_code',
'wind_speed_10m','apparent_temperature','rain','snowfall'
].join(','),
daily: [
'weather_code','temperature_2m_max','temperature_2m_min',
'precipitation_sum','precipitation_probability_max',
'wind_speed_10m_max','sunrise','sunset','uv_index_max'
].join(','),
forecast_days: 16,
wind_speed_unit: useMetric ? 'kmh' : 'mph',
temperature_unit: useMetric ? 'celsius' : 'fahrenheit',
precipitation_unit: useMetric ? 'mm' : 'inch'
});
const r = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
const data = await r.json();
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Weather fetch failed', detail: e.message });
}
});
// Open-Meteo Air Quality proxy
app.get('/api/air-quality', async (req, res) => {
const { lat, lon, tz } = req.query;
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
try {
const params = new URLSearchParams({
latitude: lat,
longitude: lon,
timezone: tz || 'auto',
current: ['us_aqi', 'pm2_5', 'alder_pollen', 'birch_pollen', 'grass_pollen', 'mugwort_pollen', 'olive_pollen', 'ragweed_pollen'].join(','),
forecast_days: 1,
});
const r = await fetch(`https://air-quality-api.open-meteo.com/v1/air-quality?${params}`);
const data = await r.json();
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Air quality fetch failed', detail: e.message });
}
});
// NWS alerts proxy (US only)
app.get('/api/alerts', async (req, res) => {
const { lat, lon } = req.query;
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
try {
const r = await fetch(`https://api.weather.gov/alerts/active?point=${lat},${lon}`, {
headers: {
'User-Agent': 'weather-app-v2 (nate@necloud.us)',
Accept: 'application/geo+json',
},
});
const data = await r.json();
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Alerts fetch failed', detail: e.message });
}
});
// RainViewer timestamps proxy
app.get('/api/radar/times', async (req, res) => {
try {
const r = await fetch('https://api.rainviewer.com/public/weather-maps.json');
const data = await r.json();
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Radar times failed', detail: e.message });
}
});
app.listen(PORT, () => console.log(`Weather app running on port ${PORT}`));