auto-sync: 2026-04-26 16:30:01
This commit is contained in:
@@ -10,3 +10,4 @@
|
||||
{"ts":"2026-04-26T10:02:04Z","session":"20260425-100247_fr24_schedule-aircraft-type-coalesce_78a6","host":"fr24","status":"success","agent":"stream"}
|
||||
{"ts":"2026-04-26T10:06:37Z","session":"20260426-100232_fr24_m4-outlier-filter-preprocess_ce04","host":"fr24","status":"success","agent":"stream","files":["/home/fr24/projects/fr24/ingest/preprocess/main.py"]}
|
||||
{"ts":"2026-04-26T11:09:18Z","session":"20260426-110213_mva154_plane-install_d8bb","host":"mva154","status":"success","agent":"stream","files":["/home/slin/plane-selfhost/plane-app/plane.env"]}
|
||||
{"ts":"2026-04-26T13:26:25Z","action":"cleanup","deleted_orphaned_sessions":0,"deleted_logs":0,"retention_days":30}
|
||||
|
||||
@@ -19,6 +19,8 @@ from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import orjson
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
from flask import Flask, jsonify, render_template_string, request, send_from_directory, Response
|
||||
from dotenv import load_dotenv
|
||||
|
||||
@@ -28,6 +30,18 @@ from air_corridors_model import compute_corridors
|
||||
from flask_compress import Compress
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def get_db_conn():
|
||||
"""Подключение к PostgreSQL (fr24 БД)"""
|
||||
return psycopg2.connect(
|
||||
host=os.getenv("DB_HOST", "localhost"),
|
||||
port=int(os.getenv("DB_PORT", 5432)),
|
||||
dbname=os.getenv("DB_NAME", "fr24"),
|
||||
user=os.getenv("DB_USER", "fr24"),
|
||||
password=os.getenv("DB_PASSWORD", "change-me"),
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -605,6 +619,73 @@ def api_air_corridors():
|
||||
return Response(raw, status=200, headers={"Content-Type": "application/json"})
|
||||
|
||||
|
||||
@app.route("/api/tracks", methods=["GET"])
|
||||
def get_tracks():
|
||||
"""Треки из PostgreSQL в формате GeoJSON"""
|
||||
date_filter = request.args.get("date") # YYYY-MM-DD
|
||||
limit = min(int(request.args.get("limit", 500)), 2000)
|
||||
|
||||
try:
|
||||
conn = get_db_conn()
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
|
||||
where = "WHERE 1=1"
|
||||
params = []
|
||||
if date_filter:
|
||||
where += " AND DATE(tp.observed_at) = %s"
|
||||
params.append(date_filter)
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT
|
||||
t.track_id,
|
||||
t.flight_id,
|
||||
t.point_count,
|
||||
t.min_altitude_m,
|
||||
t.max_altitude_m,
|
||||
json_agg(
|
||||
json_build_array(ST_X(tp.geom), ST_Y(tp.geom))
|
||||
ORDER BY tp.point_order
|
||||
) as coords
|
||||
FROM fr24.tracks t
|
||||
JOIN fr24.track_points tp ON tp.track_id = t.track_id
|
||||
{where}
|
||||
GROUP BY t.track_id, t.flight_id, t.point_count, t.min_altitude_m, t.max_altitude_m
|
||||
ORDER BY t.track_id
|
||||
LIMIT %s
|
||||
""", params + [limit])
|
||||
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
features = []
|
||||
for row in rows:
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": row["coords"]
|
||||
},
|
||||
"properties": {
|
||||
"track_id": row["track_id"],
|
||||
"flight_id": row["flight_id"],
|
||||
"point_count": row["point_count"],
|
||||
"min_altitude_m": float(row["min_altitude_m"]) if row["min_altitude_m"] else None,
|
||||
"max_altitude_m": float(row["max_altitude_m"]) if row["max_altitude_m"] else None,
|
||||
}
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"type": "FeatureCollection",
|
||||
"features": features,
|
||||
"meta": {"count": len(features), "date": date_filter}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"/api/tracks error: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.getenv("PORT", 5555))
|
||||
debug = os.getenv("DEBUG", "true").lower() == "true"
|
||||
|
||||
@@ -1317,10 +1317,43 @@ function setAirport(airport, btn) {
|
||||
|
||||
function toggleTracks() {
|
||||
const visible = tracksLayer.getVisible();
|
||||
if (!visible && tracksSource.getFeatures().length === 0) {
|
||||
loadTracksFromDB();
|
||||
}
|
||||
tracksLayer.setVisible(!visible);
|
||||
document.getElementById('btn-tracks').classList.toggle('active', !visible);
|
||||
}
|
||||
|
||||
async function loadTracksFromDB() {
|
||||
const dateEl = document.getElementById('date-select');
|
||||
const dateParam = dateEl && dateEl.value ? `&date=${dateEl.value}` : '';
|
||||
try {
|
||||
const resp = await fetch(`/noisemap/api/tracks?limit=500${dateParam}`);
|
||||
const geojson = await resp.json();
|
||||
if (geojson.error) { console.error('tracks error:', geojson.error); return; }
|
||||
tracksSource.clear();
|
||||
const format = new ol.format.GeoJSON();
|
||||
geojson.features.forEach(f => {
|
||||
const coords = f.geometry.coordinates.map(c => ol.proj.fromLonLat(c));
|
||||
if (coords.length < 2) return;
|
||||
const avgAlt = ((f.properties.min_altitude_m || 0) + (f.properties.max_altitude_m || 0)) / 2;
|
||||
const feat = new ol.Feature({
|
||||
geometry: new ol.geom.LineString(coords),
|
||||
track_id: f.properties.track_id,
|
||||
flight_id: f.properties.flight_id,
|
||||
point_count: f.properties.point_count,
|
||||
min_altitude_m: f.properties.min_altitude_m,
|
||||
max_altitude_m: f.properties.max_altitude_m,
|
||||
altitude_m: avgAlt,
|
||||
});
|
||||
tracksSource.addFeature(feat);
|
||||
});
|
||||
console.log(`Загружено треков из БД: ${geojson.meta?.count}`);
|
||||
} catch(e) {
|
||||
console.error('loadTracksFromDB error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Статистика
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -1598,7 +1631,22 @@ map.on('click', (evt) => {
|
||||
map.forEachFeatureAtPixel(evt.pixel, (feature) => {
|
||||
if (found || !feature.get('flight_id')) return;
|
||||
found = true;
|
||||
showFlightDetail(feature, feature.get('noise_color'));
|
||||
// Трек из БД (есть track_id, нет noise_color)
|
||||
if (feature.get('track_id')) {
|
||||
const minAlt = feature.get('min_altitude_m');
|
||||
const maxAlt = feature.get('max_altitude_m');
|
||||
document.getElementById('flight-detail').innerHTML = `
|
||||
<div class="flight-card">
|
||||
<div class="flight-id">Трек #${feature.get('track_id')}</div>
|
||||
<div class="row"><span class="key">Flight ID</span><span class="val">${feature.get('flight_id') || '—'}</span></div>
|
||||
<div class="row"><span class="key">Точек</span><span class="val">${feature.get('point_count') || '—'}</span></div>
|
||||
<div class="row"><span class="key">Высота мин</span><span class="val">${minAlt ? Math.round(minAlt) + ' м' : '—'}</span></div>
|
||||
<div class="row"><span class="key">Высота макс</span><span class="val">${maxAlt ? Math.round(maxAlt) + ' м' : '—'}</span></div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
showFlightDetail(feature, feature.get('noise_color'));
|
||||
}
|
||||
}, { layerFilter: l => l === tracksLayer, hitTolerance: 6 });
|
||||
|
||||
// ── Клик на слой плотности ──
|
||||
|
||||
@@ -2,3 +2,4 @@ flask>=3.0.0
|
||||
requests>=2.31.0
|
||||
python-dotenv>=1.0.0
|
||||
urllib3>=2.0.0
|
||||
psycopg2-binary>=2.9.0
|
||||
|
||||
Reference in New Issue
Block a user