2025-11-17 12:23:12 +08:00
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
const stationInput = document.getElementById('station-input');
|
|
|
|
|
|
const daysInput = document.getElementById('days-input');
|
|
|
|
|
|
const fetchButton = document.getElementById('fetch-button');
|
|
|
|
|
|
const loadingDiv = document.getElementById('loading');
|
|
|
|
|
|
const errorDiv = document.getElementById('error');
|
|
|
|
|
|
const resultsDiv = document.getElementById('results');
|
|
|
|
|
|
const resultsTitle = document.getElementById('results-title');
|
|
|
|
|
|
const analysisSummary = document.getElementById('analysis-summary');
|
|
|
|
|
|
const rawDataContainer = document.getElementById('raw-data-container');
|
|
|
|
|
|
|
|
|
|
|
|
// Устанавливаем значения по умолчанию
|
2025-12-08 16:08:01 +08:00
|
|
|
|
// stationInput.value = 'UIII'; // Иркутск
|
2025-11-17 12:23:12 +08:00
|
|
|
|
daysInput.value = '14';
|
|
|
|
|
|
|
|
|
|
|
|
const charts = {};
|
|
|
|
|
|
|
|
|
|
|
|
const fetchAndDrawData = async () => {
|
|
|
|
|
|
const station = stationInput.value.toUpperCase() || 'UIII';
|
|
|
|
|
|
const days = daysInput.value || '14';
|
|
|
|
|
|
|
|
|
|
|
|
loadingDiv.classList.remove('hidden');
|
|
|
|
|
|
resultsDiv.classList.add('hidden');
|
|
|
|
|
|
errorDiv.classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/metar?station=${station}&days=${days}`);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
const errorData = await response.json();
|
|
|
|
|
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const apiResponse = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (!apiResponse.data || apiResponse.data.length === 0) {
|
|
|
|
|
|
throw new Error('Данные для указанной станции или периода не найдены.');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
processData(apiResponse.data, station, days);
|
|
|
|
|
|
|
|
|
|
|
|
loadingDiv.classList.add('hidden');
|
|
|
|
|
|
resultsDiv.classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
loadingDiv.classList.add('hidden');
|
|
|
|
|
|
errorDiv.textContent = `Ошибка: ${e.message}`;
|
|
|
|
|
|
errorDiv.classList.remove('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const processData = (data, station, days) => {
|
|
|
|
|
|
resultsTitle.textContent = `Результаты для ${station} за последние ${days} дней`;
|
|
|
|
|
|
|
|
|
|
|
|
const labels = [];
|
|
|
|
|
|
const temperatures = [];
|
|
|
|
|
|
const dewPoints = [];
|
|
|
|
|
|
const pressures = [];
|
|
|
|
|
|
const windSpeeds = [];
|
|
|
|
|
|
const windDirections = [];
|
|
|
|
|
|
|
|
|
|
|
|
const fToC = (f) => f !== null ? ((f - 32) * 5 / 9).toFixed(1) : null;
|
|
|
|
|
|
const inHgToHpa = (inHg) => inHg !== null ? (inHg * 33.8639).toFixed(1) : null;
|
|
|
|
|
|
const knotsToMs = (knots) => knots !== null ? (knots * 0.514444).toFixed(1) : null;
|
|
|
|
|
|
|
|
|
|
|
|
data.forEach(report => {
|
|
|
|
|
|
labels.push(new Date(report.valid).toLocaleString('ru-RU', { timeZone: 'UTC' }));
|
|
|
|
|
|
temperatures.push(fToC(report.tmpf));
|
|
|
|
|
|
dewPoints.push(fToC(report.dwpf));
|
|
|
|
|
|
pressures.push(inHgToHpa(report.alti));
|
|
|
|
|
|
windSpeeds.push(knotsToMs(report.sknt));
|
|
|
|
|
|
if (report.drct) {
|
|
|
|
|
|
windDirections.push(report.drct);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// графики
|
|
|
|
|
|
drawChart('temp-chart', 'line', {
|
|
|
|
|
|
labels,
|
|
|
|
|
|
datasets: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Температура (°C)',
|
|
|
|
|
|
data: temperatures,
|
|
|
|
|
|
borderColor: 'rgba(255, 99, 132, 1)',
|
|
|
|
|
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
|
|
|
|
fill: false,
|
|
|
|
|
|
tension: 0.1
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Точка росы (°C)',
|
|
|
|
|
|
data: dewPoints,
|
|
|
|
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
|
|
|
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
|
|
|
|
fill: false,
|
|
|
|
|
|
tension: 0.1
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}, 'Температура и точка росы');
|
|
|
|
|
|
|
|
|
|
|
|
drawChart('pressure-chart', 'line', {
|
|
|
|
|
|
labels,
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: 'Давление (гПа)',
|
|
|
|
|
|
data: pressures,
|
|
|
|
|
|
borderColor: 'rgba(75, 192, 192, 1)',
|
|
|
|
|
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
|
|
|
|
fill: false,
|
|
|
|
|
|
tension: 0.1
|
|
|
|
|
|
}]
|
|
|
|
|
|
}, 'Атмосферное давление (QNH)');
|
|
|
|
|
|
|
|
|
|
|
|
drawChart('wind-speed-chart', 'line', {
|
|
|
|
|
|
labels,
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: 'Скорость ветра (м/с)',
|
|
|
|
|
|
data: windSpeeds,
|
|
|
|
|
|
borderColor: 'rgba(153, 102, 255, 1)',
|
|
|
|
|
|
backgroundColor: 'rgba(153, 102, 255, 0.2)',
|
|
|
|
|
|
fill: false,
|
|
|
|
|
|
tension: 0.1
|
|
|
|
|
|
}]
|
|
|
|
|
|
}, 'Скорость ветра');
|
|
|
|
|
|
|
|
|
|
|
|
// анализ
|
|
|
|
|
|
performAnalysis(temperatures, windSpeeds, windDirections);
|
|
|
|
|
|
|
|
|
|
|
|
// отображение данных тлько последние 50
|
|
|
|
|
|
displayRawData(data.slice(-20));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const drawChart = (canvasId, type, data, title) => {
|
|
|
|
|
|
const ctx = document.getElementById(canvasId).getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
if (charts[canvasId]) {
|
|
|
|
|
|
charts[canvasId].destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
charts[canvasId] = new Chart(ctx, {
|
|
|
|
|
|
type: type,
|
|
|
|
|
|
data: data,
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: title,
|
|
|
|
|
|
font: { size: 16 }
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
x: {
|
|
|
|
|
|
ticks: {
|
|
|
|
|
|
maxTicksLimit: 15
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const performAnalysis = (temps, speeds, directions) => {
|
|
|
|
|
|
const validTemps = temps.filter(t => t !== null).map(Number);
|
|
|
|
|
|
const validSpeeds = speeds.filter(s => s !== null).map(Number);
|
|
|
|
|
|
|
|
|
|
|
|
const maxTemp = Math.max(...validTemps);
|
|
|
|
|
|
const minTemp = Math.min(...validTemps);
|
|
|
|
|
|
const avgTemp = (validTemps.reduce((a, b) => a + b, 0) / validTemps.length).toFixed(1);
|
|
|
|
|
|
|
|
|
|
|
|
const maxWind = Math.max(...validSpeeds);
|
|
|
|
|
|
|
|
|
|
|
|
const getWindDirection = (deg) => {
|
|
|
|
|
|
if (deg > 337.5 || deg <= 22.5) return 'С';
|
|
|
|
|
|
if (deg > 22.5 && deg <= 67.5) return 'СВ';
|
|
|
|
|
|
if (deg > 67.5 && deg <= 112.5) return 'В';
|
|
|
|
|
|
if (deg > 112.5 && deg <= 157.5) return 'ЮВ';
|
|
|
|
|
|
if (deg > 157.5 && deg <= 202.5) return 'Ю';
|
|
|
|
|
|
if (deg > 202.5 && deg <= 247.5) return 'ЮЗ';
|
|
|
|
|
|
if (deg > 247.5 && deg <= 292.5) return 'З';
|
|
|
|
|
|
if (deg > 292.5 && deg <= 337.5) return 'СЗ';
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const dirCounts = directions.map(getWindDirection)
|
|
|
|
|
|
.filter(d => d !== null)
|
|
|
|
|
|
.reduce((acc, dir) => {
|
|
|
|
|
|
acc[dir] = (acc[dir] || 0) + 1;
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
|
|
const prevailingDir = Object.keys(dirCounts).length > 0 ? Object.keys(dirCounts).reduce((a, b) => dirCounts[a] > dirCounts[b] ? a : b) : 'Нет данных';
|
|
|
|
|
|
|
|
|
|
|
|
analysisSummary.innerHTML = `
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li><strong>Температура:</strong></li>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li>Максимальная: <strong>${maxTemp}°C</strong></li>
|
|
|
|
|
|
<li>Минимальная: <strong>${minTemp}°C</strong></li>
|
|
|
|
|
|
<li>Средняя: <strong>${avgTemp}°C</strong></li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<li><strong>Ветер:</strong></li>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li>Максимальная скорость: <strong>${maxWind} м/с</strong></li>
|
|
|
|
|
|
<li>Преобладающее направление: <strong>${prevailingDir}</strong></li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const displayRawData = (data) => {
|
|
|
|
|
|
let tableHTML = `
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>Время (UTC)</th>
|
|
|
|
|
|
<th>Сводка METAR</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
`;
|
|
|
|
|
|
data.forEach(report => {
|
|
|
|
|
|
const date = new Date(report.valid);
|
|
|
|
|
|
|
|
|
|
|
|
const formattedDate = date.toLocaleString('ru-RU', {
|
|
|
|
|
|
timeZone: 'UTC',
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
tableHTML += `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>${formattedDate}</td>
|
|
|
|
|
|
<td><code>${report.metar}</code></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`;
|
|
|
|
|
|
});
|
|
|
|
|
|
tableHTML += `</tbody></table>`;
|
|
|
|
|
|
rawDataContainer.innerHTML = tableHTML;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fetchButton.addEventListener('click', fetchAndDrawData);
|
|
|
|
|
|
|
|
|
|
|
|
fetchAndDrawData();
|
|
|
|
|
|
});
|