mirror of
https://github.com/arthur-pbty/discord-weather-bot.git
synced 2026-08-01 20:28:48 +02:00
256 lines
16 KiB
JavaScript
256 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
const axios = require('axios');
|
|
const { getSolarElevationForDay, getTideForDay } = require('./astronomy');
|
|
|
|
const QC_URL = 'https://quickchart.io/chart';
|
|
const BG = 'rgb(20,20,32)';
|
|
const GRID = 'rgba(255,255,255,0.07)';
|
|
const TICK = '#BBBBCC';
|
|
const TITLE = '#FFFFFF';
|
|
const LEG = '#CCCCDD';
|
|
|
|
// ─── Heure courante Paris → préfixe "YYYY-MM-DDTHH" ─────────────────────────
|
|
function nowParisPrefix() {
|
|
return new Date()
|
|
.toLocaleString('sv', { timeZone: 'Europe/Paris' })
|
|
.substring(0, 13)
|
|
.replace(' ', 'T');
|
|
}
|
|
|
|
// ─── Fenêtre de N heures à partir de maintenant ──────────────────────────────
|
|
function getWindow(hourlyTimes, count = 25) {
|
|
const prefix = nowParisPrefix();
|
|
let start = 0;
|
|
for (let i = 0; i < hourlyTimes.length; i++) {
|
|
if (hourlyTimes[i].startsWith(prefix)) { start = i; break; }
|
|
if (hourlyTimes[i] < prefix) start = i;
|
|
}
|
|
return { start, end: Math.min(start + count, hourlyTimes.length), startIdx: start };
|
|
}
|
|
|
|
// ─── Heure depuis "YYYY-MM-DDTHH:MM" ────────────────────────────────────────
|
|
const hl = t => t.substring(11, 16);
|
|
|
|
// ─── POST QuickChart (config JS string = supporte les callbacks) ──────────────
|
|
async function postChart(chartJsStr) {
|
|
const res = await axios.post(QC_URL, {
|
|
chart: chartJsStr,
|
|
width: 760, height: 295,
|
|
backgroundColor: BG,
|
|
format: 'png', version: '3',
|
|
}, { responseType: 'arraybuffer', timeout: 20000 });
|
|
return Buffer.from(res.data);
|
|
}
|
|
|
|
// ─── Options communes ─────────────────────────────────────────────────────────
|
|
function opts({ title, yUnit = '', y1 = null, yMin = null, yMax = null }) {
|
|
const yMinStr = yMin !== null ? `,min:${yMin}` : '';
|
|
const yMaxStr = yMax !== null ? `,max:${yMax}` : '';
|
|
const y1block = y1
|
|
? `,y1:{position:'right',min:0,max:100,grid:{drawOnChartArea:false},ticks:{color:'${y1.color}',callback:function(v){return v+'${y1.unit}';}}}`
|
|
: '';
|
|
return `{
|
|
plugins:{
|
|
title:{display:true,text:'${title}',color:'${TITLE}',font:{size:14,weight:'bold'}},
|
|
legend:{labels:{color:'${LEG}',font:{size:11}}}
|
|
},
|
|
scales:{
|
|
x:{ticks:{color:'${TICK}',maxRotation:45,font:{size:10}},grid:{color:'${GRID}'}},
|
|
y:{ticks:{color:'${TICK}',callback:function(v){return v+'${yUnit}';}},grid:{color:'${GRID}'},position:'left'${yMinStr}${yMaxStr}}
|
|
${y1block}
|
|
}
|
|
}`;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 1. TEMPÉRATURES
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generateTemperatureChart(hourly) {
|
|
const { start, end } = getWindow(hourly.time);
|
|
const labels = JSON.stringify(hourly.time.slice(start, end).map(hl));
|
|
const temps = JSON.stringify(hourly.temperature_2m.slice(start, end).map(v => v?.toFixed(1) ?? null));
|
|
const feels = JSON.stringify(hourly.apparent_temperature.slice(start, end).map(v => v?.toFixed(1) ?? null));
|
|
return postChart(`{type:'line',data:{labels:${labels},datasets:[
|
|
{label:'Température',data:${temps},borderColor:'rgb(255,107,53)',backgroundColor:'rgba(255,107,53,0.18)',fill:true,tension:0.4,pointRadius:3,borderWidth:2.5},
|
|
{label:'Ressenti',data:${feels},borderColor:'rgb(255,200,70)',backgroundColor:'rgba(255,200,70,0.05)',fill:false,tension:0.4,pointRadius:2,borderWidth:2,borderDash:[5,4]}
|
|
]},options:${opts({ title: '🌡️ Températures sur 24h', yUnit: '°C' })}}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 2. VENT (noeuds + km/h en tooltip)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generateWindChart(hourly) {
|
|
const { start, end } = getWindow(hourly.time);
|
|
const labels = JSON.stringify(hourly.time.slice(start, end).map(hl));
|
|
|
|
// Convertir en noeuds pour l'axe Y, garder km/h en label dataset
|
|
const windKt = JSON.stringify(hourly.wind_speed_10m.slice(start, end).map(v => v != null ? +(v / 1.852).toFixed(1) : null));
|
|
const gustsKt = JSON.stringify(hourly.wind_gusts_10m.slice(start, end).map(v => v != null ? +(v / 1.852).toFixed(1) : null));
|
|
|
|
return postChart(`{type:'line',data:{labels:${labels},datasets:[
|
|
{label:'Vent moy. (kt)',data:${windKt},borderColor:'rgb(0,191,255)',backgroundColor:'rgba(0,191,255,0.18)',fill:true,tension:0.4,pointRadius:3,borderWidth:2.5},
|
|
{label:'Rafales (kt)',data:${gustsKt},borderColor:'rgb(120,140,255)',backgroundColor:'rgba(120,140,255,0.08)',fill:false,tension:0.4,pointRadius:2,borderWidth:2,borderDash:[6,3]}
|
|
]},options:${opts({ title: '💨 Vent sur 24h', yUnit: ' kt' })}}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 3. PRÉCIPITATIONS
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generatePrecipitationChart(hourly) {
|
|
const { start, end } = getWindow(hourly.time);
|
|
const labels = JSON.stringify(hourly.time.slice(start, end).map(hl));
|
|
const precip = JSON.stringify(hourly.precipitation.slice(start, end).map(v => v?.toFixed(2) ?? 0));
|
|
const prob = JSON.stringify(hourly.precipitation_probability.slice(start, end));
|
|
return postChart(`{type:'bar',data:{labels:${labels},datasets:[
|
|
{label:'Précip. (mm)',data:${precip},backgroundColor:'rgba(30,144,255,0.75)',borderColor:'rgb(30,144,255)',borderWidth:1,yAxisID:'y'},
|
|
{label:'Probabilité (%)',data:${prob},type:'line',borderColor:'rgb(0,210,210)',backgroundColor:'rgba(0,210,210,0.1)',fill:false,tension:0.4,borderWidth:2,pointRadius:2,yAxisID:'y1'}
|
|
]},options:${opts({ title: '🌧️ Précipitations sur 24h', yUnit: ' mm', yMin: 0, y1: { color: '#00D2D2', unit: '%' } })}}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 4. PRESSION
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generatePressureChart(hourly) {
|
|
const { start, end } = getWindow(hourly.time);
|
|
const labels = JSON.stringify(hourly.time.slice(start, end).map(hl));
|
|
const pressure = JSON.stringify(hourly.pressure_msl.slice(start, end).map(v => v?.toFixed(1) ?? null));
|
|
return postChart(`{type:'line',data:{labels:${labels},datasets:[
|
|
{label:'Pression (hPa)',data:${pressure},borderColor:'rgb(147,112,219)',backgroundColor:'rgba(147,112,219,0.22)',fill:true,tension:0.4,pointRadius:3,borderWidth:2.5}
|
|
]},options:${opts({ title: '🔵 Pression atmosphérique sur 24h', yUnit: ' hPa' })}}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 5. NUAGES
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generateCloudChart(hourly) {
|
|
const { start, end } = getWindow(hourly.time);
|
|
const labels = JSON.stringify(hourly.time.slice(start, end).map(hl));
|
|
const total = JSON.stringify(hourly.cloud_cover.slice(start, end));
|
|
const low = JSON.stringify(hourly.cloud_cover_low.slice(start, end));
|
|
const mid = JSON.stringify(hourly.cloud_cover_mid.slice(start, end));
|
|
const high = JSON.stringify(hourly.cloud_cover_high.slice(start, end));
|
|
return postChart(`{type:'line',data:{labels:${labels},datasets:[
|
|
{label:'Total',data:${total},borderColor:'rgb(160,160,160)',backgroundColor:'rgba(160,160,160,0.2)',fill:true,tension:0.4,pointRadius:2,borderWidth:2.5},
|
|
{label:'Bas',data:${low},borderColor:'rgb(176,196,222)',fill:false,tension:0.4,pointRadius:2,borderWidth:1.5,borderDash:[4,3]},
|
|
{label:'Moyen',data:${mid},borderColor:'rgb(100,180,220)',fill:false,tension:0.4,pointRadius:2,borderWidth:1.5,borderDash:[6,3]},
|
|
{label:'Haut',data:${high},borderColor:'rgb(200,230,255)',fill:false,tension:0.4,pointRadius:2,borderWidth:1.5,borderDash:[8,4]}
|
|
]},options:${opts({ title: '☁️ Couverture nuageuse sur 24h', yUnit: '%', yMin: 0, yMax: 100 })}}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 6. UV INDEX
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generateUvChart(hourly) {
|
|
const { start, end } = getWindow(hourly.time);
|
|
const labels = JSON.stringify(hourly.time.slice(start, end).map(hl));
|
|
const uv = JSON.stringify(hourly.uv_index.slice(start, end).map(v => v?.toFixed(1) ?? 0));
|
|
|
|
// Zones de danger (annotations via dataset fill + background colors par zone)
|
|
return postChart(`{type:'bar',data:{labels:${labels},datasets:[
|
|
{label:'UV Index',data:${uv},backgroundColor:function(ctx){
|
|
const v=ctx.dataset.data[ctx.dataIndex];
|
|
if(v>=11)return 'rgba(148,0,211,0.85)';
|
|
if(v>=8)return 'rgba(255,0,0,0.80)';
|
|
if(v>=6)return 'rgba(255,140,0,0.82)';
|
|
if(v>=3)return 'rgba(255,215,0,0.82)';
|
|
return 'rgba(76,175,80,0.82)';
|
|
},borderColor:'rgba(0,0,0,0)',borderWidth:0,borderRadius:3}
|
|
]},options:${opts({ title: '☀️ UV Index sur 24h', yUnit: '', yMin: 0 })}}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 7. ÉLÉVATION SOLAIRE
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generateSolarElevationChart(hourlyTimes) {
|
|
const lat = parseFloat(process.env.LAT);
|
|
const lon = parseFloat(process.env.LON);
|
|
const { start, end, startIdx } = getWindow(hourlyTimes);
|
|
const labels = JSON.stringify(hourlyTimes.slice(start, end).map(hl));
|
|
|
|
// Reconstruire les dates UTC depuis les heures Paris
|
|
// Open-Meteo renvoie l'heure locale Paris sans offset → on applique l'offset manuellement
|
|
const parisOffsetH = getParisOffsetHours();
|
|
const startTimeUTC = new Date(
|
|
new Date(hourlyTimes[start] + ':00').getTime() - parisOffsetH * 3_600_000
|
|
);
|
|
|
|
const elevations = JSON.stringify(getSolarElevationForDay(lat, lon, startTimeUTC, end - start));
|
|
|
|
return postChart(`{type:'line',data:{labels:${labels},datasets:[
|
|
{label:'Élévation (°)',data:${elevations},borderColor:'rgb(255,220,50)',backgroundColor:function(ctx){
|
|
const v=ctx.dataset.data[ctx.dataIndex];
|
|
if(v<=0)return 'rgba(0,0,50,0.4)';
|
|
if(v<15)return 'rgba(255,120,30,0.3)';
|
|
return 'rgba(255,220,50,0.25)';
|
|
},fill:true,tension:0.4,pointRadius:2,borderWidth:2.5}
|
|
]},options:${opts({ title: '⬆️ Elevation solaire sur 24h', yUnit: '°', yMin: -10 })}}`);
|
|
}
|
|
|
|
// Offset UTC de Paris en heures (gère l'heure d'été)
|
|
function getParisOffsetHours() {
|
|
const now = new Date();
|
|
const paris = new Date(now.toLocaleString('en-US', { timeZone: 'Europe/Paris' }));
|
|
return (paris - new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }))) / 3_600_000;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 8. MARÉES (calcul harmonique approximatif)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generateTideChart(hourlyTimes) {
|
|
const { start, end } = getWindow(hourlyTimes);
|
|
const labels = JSON.stringify(hourlyTimes.slice(start, end).map(hl));
|
|
|
|
// Heure de départ (heure locale Paris → UTC)
|
|
const parisOffsetH = getParisOffsetHours();
|
|
const startTimeUTC = new Date(
|
|
new Date(hourlyTimes[start] + ':00').getTime() - parisOffsetH * 3_600_000
|
|
);
|
|
|
|
const tides = JSON.stringify(getTideForDay(startTimeUTC, end - start));
|
|
|
|
return postChart(`{type:'line',data:{labels:${labels},datasets:[
|
|
{label:'Hauteur marée (m)',data:${tides},borderColor:'rgb(0,180,216)',backgroundColor:'rgba(0,180,216,0.20)',fill:true,tension:0.4,pointRadius:3,borderWidth:2.5}
|
|
]},options:${opts({ title: '🌊 Marees sur 24h (estimation harmonique)', yUnit: ' m' })}}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// GÉNÉRATION PARALLÈLE DES 8 GRAPHIQUES
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
async function generateAllCharts(hourly) {
|
|
const keys = ['temperature','wind','rain','pressure','clouds','uv','solar','tide'];
|
|
const jobs = [
|
|
generateTemperatureChart(hourly),
|
|
generateWindChart(hourly),
|
|
generatePrecipitationChart(hourly),
|
|
generatePressureChart(hourly),
|
|
generateCloudChart(hourly),
|
|
generateUvChart(hourly),
|
|
generateSolarElevationChart(hourly.time),
|
|
generateTideChart(hourly.time),
|
|
];
|
|
|
|
const results = await Promise.allSettled(jobs);
|
|
const out = { errors: [] };
|
|
results.forEach((r, i) => {
|
|
if (r.status === 'fulfilled') out[keys[i]] = r.value;
|
|
else {
|
|
out[keys[i]] = null;
|
|
out.errors.push(`${keys[i]}: ${r.reason?.message}`);
|
|
}
|
|
});
|
|
return out;
|
|
}
|
|
|
|
module.exports = {
|
|
generateAllCharts,
|
|
generateTemperatureChart,
|
|
generateWindChart,
|
|
generatePrecipitationChart,
|
|
generatePressureChart,
|
|
generateCloudChart,
|
|
generateUvChart,
|
|
generateSolarElevationChart,
|
|
generateTideChart,
|
|
};
|