Files
wiki/tasks/flightradar24/frontend/static/data_sources.js
2026-04-20 23:20:01 +03:00

196 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// data_sources.js — logic for /data-sources page
function getDateRange() {
const to = document.getElementById('date_to').value;
const from = document.getElementById('date_from').value;
return { date_from: from, date_to: to };
}
function qs(params) {
return Object.entries(params).filter(([, v]) => v).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
}
async function fetchJSON(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}
function pct(n, total) {
if (!total) return '—';
return (100 * n / total).toFixed(1) + '%';
}
// ── Coverage cards ────────────────────────────────────────────
async function loadCoverage() {
const el = document.getElementById('coverage-cards');
const chart = document.getElementById('coverage-chart');
try {
const data = await fetchJSON('/api/data-sources/coverage?' + qs(getDateRange()));
const rows = data.rows || [];
const totals = data.totals || {};
// Summary cards
const total = totals.total_schedule || 0;
el.innerHTML = `
<div class="card">
<h3>Расписание <span class="badge badge-sched">Яндекс</span></h3>
<div class="stat-row"><span>Всего рейсов</span><span class="stat-val">${total.toLocaleString()}</span></div>
<div class="stat-row"><span>Дней</span><span class="stat-val">${totals.days || 0}</span></div>
</div>
<div class="card">
<h3>RTL-SDR <span class="badge badge-rtlsdr">Локальный</span></h3>
<div class="stat-row"><span>Рейсов с треком</span><span class="stat-val">${(totals.with_rtlsdr||0).toLocaleString()}<span class="pct">${pct(totals.with_rtlsdr, total)}</span></span></div>
<div class="bar-wrap" style="margin-top:8px"><div class="bar bar-rtlsdr" style="width:${pct(totals.with_rtlsdr, total)}"></div></div>
</div>
<div class="card">
<h3>FR24 API <span class="badge badge-fr24">Платный</span></h3>
<div class="stat-row"><span>Рейсов с треком</span><span class="stat-val">${(totals.with_fr24||0).toLocaleString()}<span class="pct">${pct(totals.with_fr24, total)}</span></span></div>
<div class="bar-wrap" style="margin-top:8px"><div class="bar bar-fr24" style="width:${pct(totals.with_fr24, total)}"></div></div>
</div>
<div class="card">
<h3>FlightAware <span class="badge badge-fa">AeroAPI</span></h3>
<div class="stat-row"><span>Рейсов с треком</span><span class="stat-val">${(totals.with_fa||0).toLocaleString()}<span class="pct">${pct(totals.with_fa, total)}</span></span></div>
<div class="bar-wrap" style="margin-top:8px"><div class="bar bar-fa" style="width:${pct(totals.with_fa, total)}"></div></div>
</div>
`;
// Stacked bar chart by day
if (!rows.length) { chart.innerHTML = '<div class="loading">Нет данных</div>'; return; }
const maxTotal = Math.max(...rows.map(r => r.total_schedule || 0), 1);
chart.innerHTML = rows.map(r => {
const t = r.total_schedule || 1;
const wRtl = ((r.with_rtlsdr || 0) / t * 100).toFixed(1);
const wFr = ((r.with_fr24 || 0) / t * 100).toFixed(1);
const wFa = ((r.with_fa || 0) / t * 100).toFixed(1);
const wSch = (100 - parseFloat(wRtl) - parseFloat(wFr) - parseFloat(wFa)).toFixed(1);
return `<div class="chart-row">
<span class="chart-label">${r.coverage_date}</span>
<div class="chart-bars">
<span style="width:${wRtl}%;background:#40c057" title="RTL-SDR ${wRtl}%"></span>
<span style="width:${wFr}%;background:#4dabf7" title="FR24 ${wFr}%"></span>
<span style="width:${wFa}%;background:#cc5de8" title="FA ${wFa}%"></span>
<span style="width:${Math.max(0,wSch)}%;background:#3a3a1c" title="Только расписание"></span>
</div>
<span style="font-size:11px;color:#8b8fa8;width:40px">${t}</span>
</div>`;
}).join('');
} catch (e) {
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
}
}
// ── Quality ───────────────────────────────────────────────────
async function loadQuality() {
const el = document.getElementById('quality-table');
try {
const data = await fetchJSON('/api/data-sources/quality?' + qs(getDateRange()));
const q = data.quality || {};
el.innerHTML = `<table>
<tr><th>Метрика</th><th>Значение</th></tr>
<tr><td>Рейсов с маршрутом</td><td>${pct(q.with_route, q.total)}</td></tr>
<tr><td>Рейсов с треком</td><td>${pct(q.with_track, q.total)}</td></tr>
<tr><td>Рейсов с факт. временем</td><td>${pct(q.with_actual_time, q.total)}</td></tr>
<tr><td>Рейсов с типом ВС</td><td>${pct(q.with_aircraft_type, q.total)}</td></tr>
<tr><td>Медиана точек (RTL-SDR)</td><td>${q.median_points_rtlsdr || '—'}</td></tr>
<tr><td>Медиана точек (FR24)</td><td>${q.median_points_fr24 || '—'}</td></tr>
<tr><td>Медиана точек (FA)</td><td>${q.median_points_fa || '—'}</td></tr>
</table>`;
} catch (e) {
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
}
}
// ── Airport load ──────────────────────────────────────────────
async function loadAirportLoad() {
const el = document.getElementById('airport-load');
try {
const data = await fetchJSON('/api/data-sources/airport-load?' + qs(getDateRange()));
const rows = data.rows || [];
if (!rows.length) { el.innerHTML = '<div class="loading">Нет данных</div>'; return; }
const maxCount = Math.max(...rows.map(r => r.flight_count || 0), 1);
el.innerHTML = `<table>
<tr><th>Аэропорт</th><th>Час (UTC)</th><th>Рейсов</th><th></th></tr>
${rows.map(r => `<tr>
<td>${r.airport_iata}</td>
<td>${String(r.hour).padStart(2,'0')}:00</td>
<td>${r.flight_count}</td>
<td><div class="bar-wrap" style="width:80px"><div class="bar bar-fr24" style="width:${(r.flight_count/maxCount*100).toFixed(0)}%"></div></div></td>
</tr>`).join('')}
</table>`;
} catch (e) {
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
}
}
// ── Top airlines ──────────────────────────────────────────────
async function loadTopAirlines() {
const el = document.getElementById('top-airlines');
try {
const data = await fetchJSON('/api/data-sources/top-airlines?' + qs(getDateRange()));
const rows = data.rows || [];
if (!rows.length) { el.innerHTML = '<div class="loading">Нет данных</div>'; return; }
const max = rows[0].flight_count || 1;
el.innerHTML = `<table>
<tr><th>#</th><th>Авиакомпания</th><th>Рейсов</th><th></th></tr>
${rows.map((r, i) => `<tr>
<td style="color:#8b8fa8">${i+1}</td>
<td>${r.airline_iata || '—'}</td>
<td>${r.flight_count}</td>
<td><div class="bar-wrap" style="width:80px"><div class="bar bar-rtlsdr" style="width:${(r.flight_count/max*100).toFixed(0)}%"></div></div></td>
</tr>`).join('')}
</table>`;
} catch (e) {
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
}
}
// ── Top routes ────────────────────────────────────────────────
async function loadTopRoutes() {
const el = document.getElementById('top-routes');
try {
const data = await fetchJSON('/api/data-sources/top-routes?' + qs(getDateRange()));
const rows = data.rows || [];
if (!rows.length) { el.innerHTML = '<div class="loading">Нет данных</div>'; return; }
const max = rows[0].flight_count || 1;
el.innerHTML = `<table>
<tr><th>#</th><th>Маршрут</th><th>Рейсов</th><th></th></tr>
${rows.map((r, i) => `<tr>
<td style="color:#8b8fa8">${i+1}</td>
<td>${r.origin_iata || '?'}${r.destination_iata || '?'}</td>
<td>${r.flight_count}</td>
<td><div class="bar-wrap" style="width:80px"><div class="bar bar-fa" style="width:${(r.flight_count/max*100).toFixed(0)}%"></div></div></td>
</tr>`).join('')}
</table>`;
} catch (e) {
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
}
}
// ── Init ──────────────────────────────────────────────────────
function loadAll() {
loadCoverage();
loadQuality();
loadAirportLoad();
loadTopAirlines();
loadTopRoutes();
}
// Set default date range: last 7 days
(function initDates() {
const today = new Date();
const to = today.toISOString().slice(0, 10);
today.setDate(today.getDate() - 7);
const from = today.toISOString().slice(0, 10);
document.getElementById('date_from').value = from;
document.getElementById('date_to').value = to;
})();
loadAll();