445 lines
16 KiB
JavaScript
445 lines
16 KiB
JavaScript
// ====================
|
|
// Helper
|
|
// ====================
|
|
function $(id) { return document.getElementById(id); }
|
|
|
|
const monthNames = ["Enero","Febrero","Marzo","Abril","Mayo","Junio",
|
|
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
|
|
|
|
let selectedMonth = new Date().getMonth(); // mes actual
|
|
let ciudadActual = "Madrid"; // ciudad por defecto
|
|
|
|
// const BASE_API = "https://aplicacionesdevanguardia.es/eltiempo/servidor/api-weather-fechas.php";
|
|
const BASE_API = "https://tatvil.es/apis/api/weather/filter";
|
|
|
|
function buildApiUrl({ ciudad, desde, hasta }) {
|
|
const params = new URLSearchParams();
|
|
params.append("ciudad", ciudad);
|
|
params.append("desde", desde);
|
|
params.append("hasta", hasta);
|
|
|
|
console.log("Construyendo URL con parámetros:", { ciudad, desde, hasta });
|
|
console.log("URL API:", `${BASE_API}?${params.toString()}`);
|
|
|
|
return `${BASE_API}?${params.toString()}`;
|
|
}
|
|
|
|
// ====================
|
|
// Actualizar nombre de mes
|
|
// ====================
|
|
function updateMonthHeader() {
|
|
$("mes-nombre").textContent = monthNames[selectedMonth];
|
|
}
|
|
|
|
// ====================
|
|
// Cargar datos desde la API
|
|
// ====================
|
|
async function loadStats(options = {}) {
|
|
try {
|
|
const url = buildApiUrl({ ciudad: ciudadActual, ...options });
|
|
|
|
const response = await fetch(url);
|
|
if (!response.ok) throw new Error("Error cargando datos: " + response.status);
|
|
|
|
let data = await response.json();
|
|
if (!data || !data.length) throw new Error("Datos vacíos");
|
|
|
|
// --- FILTRADO POR CIUDAD ---
|
|
data = data.filter(d => d.ciudad === ciudadActual);
|
|
// ---------------------------
|
|
|
|
renderMonthStats(data);
|
|
renderTrend(data);
|
|
renderWindRose(data);
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
$("stats-location").textContent = "Error cargando datos";
|
|
}
|
|
}
|
|
|
|
async function loadToday() {
|
|
try {
|
|
const ahora = new Date();
|
|
const y = ahora.getFullYear();
|
|
const m = String(ahora.getMonth() + 1).padStart(2, "0");
|
|
const d = String(ahora.getDate()).padStart(2, "0");
|
|
const hoy = `${y}-${m}-${d}`;
|
|
|
|
const url = buildApiUrl({ ciudad: ciudadActual, desde: hoy, hasta: hoy });
|
|
const response = await fetch(url);
|
|
if (!response.ok) throw new Error("Error cargando hoy: " + response.status);
|
|
|
|
let data = await response.json();
|
|
data = data ? data.filter(d => d.ciudad === ciudadActual) : [];
|
|
|
|
// Si no hay datos de hoy, buscar el último día disponible
|
|
if (!data.length) {
|
|
const hace30 = new Date(ahora);
|
|
hace30.setDate(hace30.getDate() - 30);
|
|
const y2 = hace30.getFullYear();
|
|
const m2 = String(hace30.getMonth() + 1).padStart(2, "0");
|
|
const d2 = String(hace30.getDate()).padStart(2, "0");
|
|
const urlFallback = buildApiUrl({ ciudad: ciudadActual, desde: `${y2}-${m2}-${d2}`, hasta: hoy });
|
|
const r2 = await fetch(urlFallback);
|
|
let data2 = await r2.json();
|
|
data2 = data2 ? data2.filter(d => d.ciudad === ciudadActual) : [];
|
|
if (!data2.length) throw new Error("Sin datos recientes");
|
|
// Ordenar por fecha y tomar el último día
|
|
data2.sort((a, b) => a.dia.localeCompare(b.dia));
|
|
const ultimoDia = data2[data2.length - 1].dia;
|
|
data = data2.filter(d => d.dia === ultimoDia);
|
|
}
|
|
|
|
renderLastData(data);
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// ====================
|
|
// Último dato
|
|
// ====================
|
|
function renderLastData(data) {
|
|
const last = data[data.length - 1];
|
|
$("last-date").textContent = last.dia;
|
|
$("last-temp").textContent = last.temp_max + "°C / " + last.temp_min + "°C";
|
|
$("last-humidity").textContent = last.humedad + " %";
|
|
$("last-rain").textContent = last.lluvia + " mm";
|
|
$("last-wind").textContent = last.viento_velocidad + " km/h";
|
|
$("last-sunrise").textContent = last.amanecer;
|
|
$("last-sunset").textContent = last.anochecer;
|
|
|
|
$("stats-location").textContent = `Estadisticas de ${ciudadActual}`;
|
|
}
|
|
|
|
// ====================
|
|
// FASES LUNARES
|
|
// ====================
|
|
function getMoonPhase() {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = now.getMonth() + 1;
|
|
const day = now.getDate();
|
|
|
|
const c = Math.floor(365.25 * year);
|
|
const e = Math.floor(30.6 * (month + 1));
|
|
const jd = c + e + day - 694039.09;
|
|
const phase = (jd / 29.53) % 1;
|
|
const age = phase * 29.53;
|
|
|
|
let phaseName = "", icon = "";
|
|
|
|
if (age < 1.84566) { phaseName = "Luna Nueva"; icon = "🌑"; }
|
|
else if (age < 5.53699) { phaseName = "Creciente Iluminante"; icon = "🌒"; }
|
|
else if (age < 9.22831) { phaseName = "Cuarto Creciente"; icon = "🌓"; }
|
|
else if (age < 12.91963) { phaseName = "Gibosa Creciente"; icon = "🌔"; }
|
|
else if (age < 16.61096) { phaseName = "Luna Llena"; icon = "🌕"; }
|
|
else if (age < 20.30228) { phaseName = "Gibosa Menguante"; icon = "🌖"; }
|
|
else if (age < 23.99361) { phaseName = "Cuarto Menguante"; icon = "🌗"; }
|
|
else if (age < 27.68493) { phaseName = "Creciente Menguante"; icon = "🌘"; }
|
|
else { phaseName = "Luna Nueva"; icon = "🌑"; }
|
|
|
|
$("moon-phase").textContent = phaseName;
|
|
$("moon-icon").textContent = icon;
|
|
|
|
const today = new Date();
|
|
$("stats-location").textContent += ` | Hoy ${today.getDate()} de ${monthNames[today.getMonth()]} de ${today.getFullYear()}`;
|
|
}
|
|
|
|
// ====================
|
|
// Estadísticas del mes seleccionado
|
|
// ====================
|
|
function renderMonthStats(data) {
|
|
// Esta es la clave: filtrar por el mes seleccionado antes de calcular
|
|
const monthData = data.filter(d => {
|
|
const fechaDato = new Date(d.dia);
|
|
return fechaDato.getMonth() === selectedMonth;
|
|
});
|
|
|
|
if (!monthData.length) {
|
|
// Si no hay datos, ponemos a cero para que no salgan cosas raras
|
|
$("month-days").textContent = 0;
|
|
return;
|
|
}
|
|
|
|
// Agrupar por día (el cron guarda varios registros por día)
|
|
const byDay = {};
|
|
monthData.forEach(d => {
|
|
const key = d.dia;
|
|
if (!byDay[key]) byDay[key] = { maxTemps: [], minTemps: [], lluvia: [], humedad: [], viento: [] };
|
|
byDay[key].maxTemps.push(d.temp_max);
|
|
byDay[key].minTemps.push(d.temp_min);
|
|
byDay[key].lluvia.push(parseFloat(d.lluvia));
|
|
byDay[key].humedad.push(parseFloat(d.humedad));
|
|
byDay[key].viento.push(parseFloat(d.viento_velocidad ?? 0));
|
|
});
|
|
|
|
const days = Object.values(byDay);
|
|
const maxTemps = days.map(d => Math.max(...d.maxTemps));
|
|
const minTemps = days.map(d => Math.min(...d.minTemps));
|
|
// La lluvia del día es el máximo registrado ese día (acumulado), no la suma de todas las lecturas
|
|
const lluviaPorDia = days.map(d => Math.max(...d.lluvia));
|
|
const lluviaTotal = lluviaPorDia.reduce((sum, v) => sum + v, 0);
|
|
const lluviaMedia = lluviaTotal / days.length;
|
|
const humedad = (days.reduce((sum, d) => sum + d.humedad.reduce((a, b) => a + b, 0) / d.humedad.length, 0) / days.length).toFixed(1);
|
|
const vientoMaxPorDia = days.map(d => Math.max(...d.viento));
|
|
const vientoMediaPorDia = days.map(d => d.viento.reduce((a, b) => a + b, 0) / d.viento.length);
|
|
const vientoMax = Math.max(...vientoMaxPorDia).toFixed(0);
|
|
const vientoMin = Math.min(...vientoMaxPorDia).toFixed(0);
|
|
const vientoMedia = (vientoMediaPorDia.reduce((a, b) => a + b, 0) / days.length).toFixed(1);
|
|
|
|
$("month-days").textContent = days.length;
|
|
$("month-max").textContent = Math.max(...maxTemps) + "°C";
|
|
$("month-min").textContent = Math.min(...minTemps) + "°C";
|
|
$("month-rain").textContent = lluviaTotal.toFixed(1) + " mm (total) / " + lluviaMedia.toFixed(1) + " mm (media diaria)";
|
|
$("month-humidity").textContent = humedad + " % (media diaria)";
|
|
$("month-wind").textContent = `${vientoMax} km/h (máx) / ${vientoMedia} km/h (media) / ${vientoMin} km/h (mín)`;
|
|
}
|
|
|
|
// ====================
|
|
// Tendencia histórica
|
|
// ====================
|
|
function renderTrend(data) {
|
|
const byYear = {};
|
|
|
|
console.log("Datos recibidos para tendencia histórica:", data);
|
|
|
|
// Agrupar datos por año+día para el mes seleccionado (el cron guarda varios registros por día)
|
|
const byYearDay = {};
|
|
data.forEach(d => {
|
|
const date = new Date(d.dia);
|
|
if (date.getMonth() === selectedMonth) {
|
|
const year = date.getFullYear();
|
|
const dayKey = d.dia; // YYYY-MM-DD
|
|
if (!byYearDay[year]) byYearDay[year] = {};
|
|
if (!byYearDay[year][dayKey]) byYearDay[year][dayKey] = { max: [], min: [], rain: [] };
|
|
byYearDay[year][dayKey].max.push(d.temp_max);
|
|
byYearDay[year][dayKey].min.push(d.temp_min);
|
|
byYearDay[year][dayKey].rain.push(parseFloat(d.lluvia));
|
|
}
|
|
});
|
|
|
|
// Reducir por día antes de agrupar por año
|
|
Object.keys(byYearDay).forEach(year => {
|
|
byYear[year] = { max: [], min: [], rain: [] };
|
|
Object.values(byYearDay[year]).forEach(day => {
|
|
byYear[year].max.push(Math.max(...day.max));
|
|
byYear[year].min.push(Math.min(...day.min));
|
|
byYear[year].rain.push(Math.max(...day.rain));
|
|
});
|
|
});
|
|
|
|
const container = $("trend-container");
|
|
|
|
console.log("Datos agrupados por año para tendencia:", byYear);
|
|
|
|
if (Object.keys(byYear).length === 0) {
|
|
container.innerHTML = "<p>No hay datos históricos para este mes.</p>";
|
|
return;
|
|
}
|
|
|
|
let html = "<table class=\"trend-table\"><thead><tr><th>Año</th><th>Máx</th><th>Mín</th><th>Lluvia</th></tr></thead><tbody>";
|
|
|
|
Object.keys(byYear).sort((a, b) => b - a).forEach(year => {
|
|
const maxAvg = (byYear[year].max.reduce((a,b)=>a+b,0)/byYear[year].max.length).toFixed(1);
|
|
const minAvg = (byYear[year].min.reduce((a,b)=>a+b,0)/byYear[year].min.length).toFixed(1);
|
|
const rainTotal = byYear[year].rain.reduce((a,b)=>a+b,0).toFixed(1);
|
|
|
|
html += `
|
|
<tr>
|
|
<td><strong>${year}</strong></td>
|
|
<td>${maxAvg}°C</td>
|
|
<td>${minAvg}°C</td>
|
|
<td>${rainTotal} mm</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
html += "</tbody></table>";
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// ====================
|
|
// Rosa de los vientos
|
|
// ====================
|
|
let windRoseChart = null;
|
|
|
|
function renderWindRose(data) {
|
|
const sectorDefs = [
|
|
{ label: "N", min: 337.5, max: 360 },
|
|
{ label: "N", min: 0, max: 22.5 },
|
|
{ label: "NE", min: 22.5, max: 67.5 },
|
|
{ label: "E", min: 67.5, max: 112.5 },
|
|
{ label: "SE", min: 112.5, max: 157.5 },
|
|
{ label: "S", min: 157.5, max: 202.5 },
|
|
{ label: "SO", min: 202.5, max: 247.5 },
|
|
{ label: "O", min: 247.5, max: 292.5 },
|
|
{ label: "NO", min: 292.5, max: 337.5 }
|
|
];
|
|
|
|
const labels = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"];
|
|
|
|
// Agrupar lecturas por día y mes seleccionado
|
|
// vientoDireccion viene en camelCase desde Spring Boot (sin @JsonProperty)
|
|
const byDay = {};
|
|
data.forEach(d => {
|
|
const date = new Date(d.dia);
|
|
if (date.getMonth() !== selectedMonth) return;
|
|
if (!byDay[d.dia]) byDay[d.dia] = { dirs: [], speeds: [] };
|
|
const dir = parseFloat(d.vientoDireccion ?? d.viento_direccion ?? 0);
|
|
const spd = parseFloat(d.viento_velocidad ?? 0);
|
|
byDay[d.dia].dirs.push(dir);
|
|
byDay[d.dia].speeds.push(spd);
|
|
});
|
|
|
|
// Por cada día: dirección media y velocidad media
|
|
const readings = Object.values(byDay).map(day => ({
|
|
dir: day.dirs.reduce((a, b) => a + b, 0) / day.dirs.length,
|
|
spd: day.speeds.reduce((a, b) => a + b, 0) / day.speeds.length
|
|
}));
|
|
|
|
// Acumular frecuencia y velocidad media por sector
|
|
const counts = {};
|
|
const speedSum = {};
|
|
labels.forEach(l => { counts[l] = 0; speedSum[l] = 0; });
|
|
|
|
readings.forEach(({ dir, spd }) => {
|
|
for (const s of sectorDefs) {
|
|
if (dir >= s.min && dir < s.max) {
|
|
counts[s.label]++;
|
|
speedSum[s.label] += spd;
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
const values = labels.map(l => counts[l]);
|
|
const avgSpeed = labels.map(l => counts[l] ? (speedSum[l] / counts[l]).toFixed(1) : 0);
|
|
|
|
// Dirección dominante
|
|
const dominant = labels.reduce((best, l) => counts[l] > counts[best] ? l : best, labels[0]);
|
|
const totalDays = readings.length;
|
|
const globalAvgSpeed = totalDays
|
|
? (readings.reduce((s, r) => s + r.spd, 0) / totalDays).toFixed(1)
|
|
: 0;
|
|
|
|
// Actualizar subtítulo bajo la gráfica
|
|
const infoEl = $("wind-rose-info");
|
|
if (infoEl) {
|
|
infoEl.textContent = totalDays
|
|
? `Dominante: ${dominant} · Vel. media: ${globalAvgSpeed} km/h`
|
|
: "Sin datos";
|
|
}
|
|
|
|
const canvas = $("wind-rose");
|
|
if (!canvas) return;
|
|
|
|
if (windRoseChart) windRoseChart.destroy();
|
|
|
|
windRoseChart = new Chart(canvas, {
|
|
type: "radar",
|
|
data: {
|
|
labels,
|
|
datasets: [{
|
|
data: values,
|
|
backgroundColor: "rgba(164, 215, 244, 0.2)",
|
|
borderColor: "rgba(164, 215, 244, 0.9)",
|
|
borderWidth: 2,
|
|
pointBackgroundColor: "rgba(164, 215, 244, 1)",
|
|
pointRadius: 3
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: ctx => ` ${ctx.label}: ${ctx.raw} días · vel. media ${avgSpeed[ctx.dataIndex]} km/h`
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
r: {
|
|
beginAtZero: true,
|
|
ticks: { color: "#666", font: { size: 9 }, backdropColor: "transparent", stepSize: 1 },
|
|
grid: { color: "rgba(255,255,255,0.08)" },
|
|
angleLines: { color: "rgba(255,255,255,0.1)" },
|
|
pointLabels: { color: "#a4d7f4", font: { size: 13, weight: "bold" } }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ====================
|
|
// Cargar mes actual según selectedMonth
|
|
// ====================
|
|
function loadCurrentMonth() {
|
|
const ahora = new Date();
|
|
const yearActual = ahora.getFullYear();
|
|
const mesActual = ahora.getMonth();
|
|
const diaActual = ahora.getDate();
|
|
|
|
// El mes que queremos consultar
|
|
const yearBusqueda = yearActual;
|
|
const monthBusqueda = selectedMonth + 1;
|
|
|
|
// 1. Primer día del mes, retrocediendo 5 años para incluir histórico
|
|
const firstDay = `${yearBusqueda - 5}-${String(monthBusqueda).padStart(2, "0")}-01`;
|
|
|
|
// 2. Calculamos el último día teórico del mes
|
|
let ultimoDiaObj = new Date(yearBusqueda, monthBusqueda, 0);
|
|
|
|
// 3. VALIDACIÓN CRUCIAL: Si el mes seleccionado es el actual,
|
|
// limitamos la búsqueda hasta HOY para evitar el Error 500 del servidor.
|
|
if (selectedMonth === mesActual && yearBusqueda === yearActual) {
|
|
ultimoDiaObj = ahora;
|
|
}
|
|
|
|
// 4. Formateo manual YYYY-MM-DD (Evita toISOString y sus desfases UTC)
|
|
const y = ultimoDiaObj.getFullYear();
|
|
const m = String(ultimoDiaObj.getMonth() + 1).padStart(2, "0");
|
|
const d = String(ultimoDiaObj.getDate()).padStart(2, "0");
|
|
const lastDay = `${y}-${m}-${d}`;
|
|
|
|
console.log(`Petición: ${firstDay} hasta ${lastDay}`);
|
|
|
|
loadStats({ desde: firstDay, hasta: lastDay });
|
|
}
|
|
|
|
// ====================
|
|
// Inicialización
|
|
// ====================
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
updateMonthHeader();
|
|
getMoonPhase();
|
|
loadToday();
|
|
loadCurrentMonth();
|
|
|
|
$("prev-month").addEventListener("click", () => {
|
|
selectedMonth = (selectedMonth + 11) % 12;
|
|
updateMonthHeader();
|
|
loadCurrentMonth();
|
|
});
|
|
|
|
$("next-month").addEventListener("click", () => {
|
|
selectedMonth = (selectedMonth + 1) % 12;
|
|
updateMonthHeader();
|
|
loadCurrentMonth();
|
|
});
|
|
|
|
$("year").textContent = new Date().getFullYear();
|
|
|
|
// ====================
|
|
// Selector de ciudad
|
|
// ====================
|
|
$("city-select").addEventListener("change", (e) => {
|
|
ciudadActual = e.target.value;
|
|
loadToday();
|
|
loadCurrentMonth();
|
|
});
|
|
});
|