koleo-izochrona/backend/app.py

177 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""
Flask API do obliczania izochron dla połączeń kolejowych
"""
from flask import Flask, jsonify, request
from flask_cors import CORS
from pathlib import Path
from gtfs_loader import GTFSLoader
from isochrone_calculator import IsochroneCalculator
app = Flask(__name__)
CORS(app) # Pozwala na żądania z frontendu
# Globalne zmienne dla danych
gtfs_loader = None
calculator = None
route_graph = None
def init_data():
"""Inicjalizuje dane GTFS i graf"""
global gtfs_loader, calculator, route_graph
print('=' * 60)
print('🚂 INICJALIZACJA BACKENDU IZOCHRONA')
print('=' * 60)
data_dir = Path(__file__).parent.parent / 'data'
gtfs_file = data_dir / 'polish_trains.zip'
if not gtfs_file.exists():
print(f'❌ BŁĄD: Nie znaleziono pliku GTFS: {gtfs_file}')
print('Uruchom: python backend/download_gtfs.py')
return False
try:
print(f'\n📍 Plik GTFS: {gtfs_file}')
print(f'📏 Rozmiar: {gtfs_file.stat().st_size / 1024 / 1024:.2f} MB\n')
gtfs_loader = GTFSLoader(str(gtfs_file))
print()
route_graph = gtfs_loader.build_route_graph()
print()
calculator = IsochroneCalculator(route_graph)
print('=' * 60)
print('✅ DANE ZAŁADOWANE POMYŚLNIE!')
print('=' * 60)
return True
except Exception as e:
print(f'\n❌ Błąd podczas ładowania danych: {e}')
import traceback
traceback.print_exc()
return False
@app.route('/api/stops', methods=['GET'])
def get_stops():
"""Zwraca listę wszystkich przystanków"""
if gtfs_loader is None:
return jsonify({'error': 'Dane nie zostały załadowane'}), 500
stops = gtfs_loader.get_stops()
stops_list = stops.to_dict('records')
return jsonify({'stops': stops_list, 'count': len(stops_list)})
@app.route('/api/stops/search', methods=['GET'])
def search_stops():
"""Wyszukuje przystanki po nazwie"""
query = request.args.get('q', '')
if not query or len(query) < 2:
return jsonify({'error': 'Zapytanie musi mieć min. 2 znaki'}), 400
if gtfs_loader is None:
return jsonify({'error': 'Dane nie zostały załadowane'}), 500
stops = gtfs_loader.find_stops_by_name(query)
stops_list = stops.to_dict('records')
return jsonify({'stops': stops_list, 'count': len(stops_list)})
@app.route('/api/isochrones', methods=['POST'])
def calculate_isochrones():
"""
Oblicza izochrony dla wybranego przystanku
Body (JSON):
{
"origin_stop_id": "...",
"time_intervals": [30, 60, 90, 120] # opcjonalne
}
"""
if calculator is None:
return jsonify({'error': 'Dane nie zostały załadowane'}), 500
data = request.get_json()
origin_stop_id = data.get('origin_stop_id')
if not origin_stop_id:
return jsonify({'error': 'Brak origin_stop_id'}), 400
time_intervals = data.get('time_intervals', [30, 60, 90, 120])
try:
isochrones = calculator.create_isochrones(origin_stop_id, time_intervals)
reachable_stops = calculator.get_reachable_stops(
origin_stop_id,
max_time=max(time_intervals)
)
return jsonify({
'origin_stop_id': origin_stop_id,
'isochrones': isochrones,
'reachable_stops': reachable_stops
})
except ValueError as e:
return jsonify({'error': str(e)}), 404
except Exception as e:
return jsonify({'error': f'Błąd obliczeń: {str(e)}'}), 500
@app.route('/api/reachable', methods=['POST'])
def get_reachable():
"""
Zwraca listę osiągalnych przystanków
Body (JSON):
{
"origin_stop_id": "...",
"max_time": 120 # w minutach
}
"""
if calculator is None:
return jsonify({'error': 'Dane nie zostały załadowane'}), 500
data = request.get_json()
origin_stop_id = data.get('origin_stop_id')
max_time = data.get('max_time', 120)
if not origin_stop_id:
return jsonify({'error': 'Brak origin_stop_id'}), 400
try:
reachable = calculator.get_reachable_stops(origin_stop_id, max_time)
return jsonify({'reachable_stops': reachable, 'count': len(reachable)})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/health', methods=['GET'])
def health():
"""Sprawdza status serwera"""
status = {
'status': 'ok' if gtfs_loader is not None else 'error',
'data_loaded': gtfs_loader is not None,
'stops_count': len(gtfs_loader.get_stops()) if gtfs_loader else 0,
'graph_nodes': route_graph.number_of_nodes() if route_graph else 0,
'graph_edges': route_graph.number_of_edges() if route_graph else 0,
}
return jsonify(status)
# Inicjalizacja danych przy starcie (działa zarówno z gunicornem jak i bezpośrednio)
init_data()
if __name__ == '__main__':
print('=== Izochrona API Server ===\n')
if gtfs_loader is not None:
print('\n🚀 Serwer uruchomiony na http://localhost:5000')
app.run(debug=True, host='0.0.0.0', port=5000)
else:
print('\n❌ Nie udało się załadować danych. Sprawdź logi powyżej.')