177 lines
5.2 KiB
Python
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.')
|