diff --git a/electric_train/.env b/electric_train/.env new file mode 100644 index 000000000..983181cfd --- /dev/null +++ b/electric_train/.env @@ -0,0 +1,2 @@ +REACT_APP_API_KEY=5c35214b-1ca9-4216-a86e-d852deeecfaf +REACT_APP_PROXY_URL=http://localhost:3001/api/rasp diff --git a/electric_train/.gitignore b/electric_train/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/electric_train/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/electric_train/package.json b/electric_train/package.json new file mode 100644 index 000000000..62c08f8ec --- /dev/null +++ b/electric_train/package.json @@ -0,0 +1,52 @@ +{ + "name": "electric_train", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^13.5.0", + "bootstrap": "^5.3.0", + "cors": "^2.8.6", + "dotenv": "^17.4.0", + "express": "^5.2.1", + "leaflet": "^1.9.4", + "node-fetch": "^2.7.0", + "ol": "^10.8.0", + "react": "^19.2.4", + "react-bootstrap": "^2.10.0", + "react-dom": "^19.2.4", + "react-scripts": "5.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "start:all": "concurrently \"npm run server\" \"npm run start\"", + "server": "node server.js", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "concurrently": "^9.1.2" + } +} diff --git a/electric_train/public/favicon.ico b/electric_train/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/electric_train/public/favicon.ico differ diff --git a/electric_train/public/index.html b/electric_train/public/index.html new file mode 100644 index 000000000..aa069f27c --- /dev/null +++ b/electric_train/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/electric_train/public/logo192.png b/electric_train/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/electric_train/public/logo192.png differ diff --git a/electric_train/public/logo512.png b/electric_train/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/electric_train/public/logo512.png differ diff --git a/electric_train/public/manifest.json b/electric_train/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/electric_train/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/electric_train/public/robots.txt b/electric_train/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/electric_train/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/electric_train/server.js b/electric_train/server.js new file mode 100644 index 000000000..d15c5536d --- /dev/null +++ b/electric_train/server.js @@ -0,0 +1,47 @@ +const express = require('express'); +const cors = require('cors'); +const fetch = require('node-fetch'); +require('dotenv').config(); + +const app = express(); +const port = 3001; + +const API_KEY = process.env.REACT_APP_API_KEY || '5c35214b-1ca9-4216-a86e-d852deeecfaf'; + +app.use(cors()); +app.use(express.json()); + +app.use('/api/rasp', async (req, res) => { + try { + const apiPath = req.originalUrl.replace('/api/rasp', ''); + const separator = apiPath.includes('?') ? '&' : '?'; + const targetUrl = `https://api.rasp.yandex.net/v3.0${apiPath}${separator}apikey=${API_KEY}`; + + console.log('Прокси запрос к:', targetUrl.replace(API_KEY, '***HIDDEN***')); + + const response = await fetch(targetUrl, { + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0' + } + }); + + const text = await response.text(); + + try { + const data = JSON.parse(text); + res.json(data); + } catch (e) { + console.error('Ошибка парсинга JSON'); + res.status(500).json({ error: 'API вернул невалидный JSON' }); + } + + } catch (error) { + console.error('Ошибка прокси:', error.message); + res.status(500).json({ error: 'Ошибка при выполнении запроса к API' }); + } +}); + +app.listen(port, () => { + console.log(`✅ Прокси сервер запущен на http://localhost:${port}`); +}); diff --git a/electric_train/src/App.css b/electric_train/src/App.css new file mode 100644 index 000000000..be03f5aac --- /dev/null +++ b/electric_train/src/App.css @@ -0,0 +1,144 @@ +.App { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.app-header { + background: rgba(255, 255, 255, 0.95); + padding: 1rem; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.app-header h1 { + margin: 0; + font-size: 1.5rem; + color: #333; +} + +.app-header p { + margin: 0.5rem 0 0; + font-size: 0.9rem; + color: #666; +} + +.map-container { + height: 100vh; + padding: 0; + position: relative; +} + +.map-container > div { + height: 100%; + width: 100%; +} + +.ol-map { + width: 100%; + height: 100%; + min-height: 500px; +} + +.ol-viewport { + border-radius: 0; +} + +.sidebar { + background: white; + height: 100vh; + overflow-y: auto; + padding: 1rem; + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1); +} + +.nav-tabs { + margin-bottom: 1rem; +} + +.nav-tabs .nav-link { + color: #667eea; + font-weight: 500; +} + +.nav-tabs .nav-link.active { + background-color: #667eea; + color: white; + border-color: #667eea; +} + +.tab-content { + padding: 1rem 0; +} + +@media (max-width: 768px) { + .map-container { + height: 50vh; + } + + .sidebar { + height: 50vh; + } + + .app-header h1 { + font-size: 1.2rem; + } +} + +.ol-popup { + position: absolute; + background-color: white; + padding: 10px; + border-radius: 8px; + border: 1px solid #ccc; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + font-size: 14px; + min-width: 150px; + z-index: 1000; +} + +.ol-popup:before { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + border-width: 10px 10px 0; + border-style: solid; + border-color: white transparent transparent; +} + +.ol-popup button { + margin-top: 8px; + padding: 5px 10px; + background-color: #667eea; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + width: 100%; +} + +.ol-popup button:hover { + background-color: #5a67d8; +} + +.station-search .station-item { + cursor: pointer; + transition: background-color 0.2s; +} + +.station-search .station-item:hover { + background-color: #f8f9fa; +} + +.schedule-item { + transition: background-color 0.2s; +} + +.schedule-item:hover { + background-color: #f8f9fa; +} + +.route-results { + margin-top: 1rem; +} diff --git a/electric_train/src/app/App.js b/electric_train/src/app/App.js new file mode 100644 index 000000000..7a11dbe9a --- /dev/null +++ b/electric_train/src/app/App.js @@ -0,0 +1,129 @@ +import React, { useState, useRef } from 'react'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import '../App.css'; +import MapComponent from '../components/MapComponent'; +import StationSearch from '../components/StationSearch'; +import ScheduleDisplay from '../components/ScheduleDisplay'; +import RouteSearch from '../components/RouteSearch'; +import { fetchStationSchedule, fetchRouteSchedule } from '../services/YandexApi'; + +function App() { + const [selectedStation, setSelectedStation] = useState(null); + const [schedule, setSchedule] = useState(null); + const [routeSchedule, setRouteSchedule] = useState(null); + const [loading, setLoading] = useState(false); + const [activeTab, setActiveTab] = useState('station'); + const mapRef = useRef(null); + + const loadStationSchedule = async (station) => { + setLoading(true); + setSelectedStation(station); + setRouteSchedule(null); + try { + const data = await fetchStationSchedule(station.code); + setSchedule(data); + } catch (error) { + console.error('Ошибка загрузки расписания:', error); + alert('Не удалось загрузить расписание. Проверьте подключение.'); + } + setLoading(false); + }; + + const loadRouteSchedule = async (fromStation, toStation, date) => { + setLoading(true); + setSchedule(null); + try { + const data = await fetchRouteSchedule(fromStation.code, toStation.code, date); + setRouteSchedule(data); + } catch (error) { + console.error('Ошибка загрузки маршрута:', error); + alert('Не удалось загрузить расписание маршрута.'); + } + setLoading(false); + }; + + const handleStationSelect = (station) => { + loadStationSchedule(station); + setActiveTab('station'); + }; + + return ( +
+
+

🚂 Прибывалка для электричек

+

Расписание электричек

+
+ +
+
+
+ +
+ +
+
    +
  • + +
  • +
  • + +
  • +
+ +
+ {activeTab === 'station' && ( +
+ + +
+ )} + + {activeTab === 'route' && ( +
+ + {routeSchedule && ( +
+
Результаты поиска
+ +
+ )} +
+ )} +
+
+
+
+
+ ); +} + +export default App; diff --git a/electric_train/src/components/MapComponent.js b/electric_train/src/components/MapComponent.js new file mode 100644 index 000000000..761d75a1a --- /dev/null +++ b/electric_train/src/components/MapComponent.js @@ -0,0 +1,135 @@ +import React, { useEffect, useRef, useState } from 'react'; +import 'ol/ol.css'; +import Map from 'ol/Map'; +import View from 'ol/View'; +import TileLayer from 'ol/layer/Tile'; +import OSM from 'ol/source/OSM'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import Feature from 'ol/Feature'; +import Point from 'ol/geom/Point'; +import { fromLonLat } from 'ol/proj'; +import { Style, Icon, Text, Fill, Stroke } from 'ol/style'; +import Overlay from 'ol/Overlay'; +import { stationsData } from '../data/stations'; + +const MapComponent = ({ onStationSelect, selectedStation }) => { + const mapRef = useRef(null); + const mapInstanceRef = useRef(null); + const popupRef = useRef(null); + const [popupContent, setPopupContent] = useState(''); + + const samaraCenter = fromLonLat([50.15, 53.2]); + + useEffect(() => { + if (!mapRef.current) return; + + const popupOverlay = new Overlay({ + element: popupRef.current, + positioning: 'bottom-center', + offset: [0, -10], + }); + + const map = new Map({ + target: mapRef.current, + layers: [ + new TileLayer({ + source: new OSM(), + }), + ], + view: new View({ + center: samaraCenter, + zoom: 9, + }), + controls: [], + }); + + map.addOverlay(popupOverlay); + mapInstanceRef.current = map; + + const addStationsToMap = () => { + const features = []; + + stationsData.forEach(station => { + if (station.coordinates && station.coordinates.lon && station.coordinates.lat) { + const feature = new Feature({ + geometry: new Point(fromLonLat([station.coordinates.lon, station.coordinates.lat])), + stationData: station, + }); + + feature.setStyle(new Style({ + image: new Icon({ + src: 'https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/examples/data/icon.png', + scale: 0.5, + anchor: [0.5, 1], + }), + text: new Text({ + text: station.title, + font: '12px Arial', + fill: new Fill({ color: '#333' }), + stroke: new Stroke({ color: 'white', width: 2 }), + offsetY: -20, + }), + })); + + features.push(feature); + } + }); + + const vectorSource = new VectorSource({ + features: features, + }); + + const vectorLayer = new VectorLayer({ + source: vectorSource, + }); + + map.addLayer(vectorLayer); + }; + + addStationsToMap(); + + map.on('click', (event) => { + const feature = map.forEachFeatureAtPixel(event.pixel, (feature) => feature); + if (feature && feature.get('stationData')) { + const stationData = feature.get('stationData'); + const coordinate = event.coordinate; + + setPopupContent(` + ${stationData.title}
+ ${stationData.type || 'ЖД станция'}
+ + `); + popupOverlay.setPosition(coordinate); + } else { + popupOverlay.setPosition(undefined); + } + }); + + window.selectStationFromMap = (code) => { + const station = stationsData.find(s => s.code === code); + if (station) { + onStationSelect(station); + popupOverlay.setPosition(undefined); + } + }; + + return () => { + if (mapInstanceRef.current) { + mapInstanceRef.current.setTarget(null); + } + delete window.selectStationFromMap; + }; + }, [onStationSelect, samaraCenter]); + + return ( +
+
+
+
+ ); +}; + +export default MapComponent; diff --git a/electric_train/src/components/RouteSearch.js b/electric_train/src/components/RouteSearch.js new file mode 100644 index 000000000..fada00c63 --- /dev/null +++ b/electric_train/src/components/RouteSearch.js @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; +import { Form, Button, Spinner } from 'react-bootstrap'; +import { searchStations } from '../services/YandexApi'; + +const RouteSearch = ({ onRouteSearch, loading }) => { + const [fromQuery, setFromQuery] = useState(''); + const [toQuery, setToQuery] = useState(''); + const [date, setDate] = useState(''); + const [fromResults, setFromResults] = useState([]); + const [toResults, setToResults] = useState([]); + const [selectedFrom, setSelectedFrom] = useState(null); + const [selectedTo, setSelectedTo] = useState(null); + const [searchingFrom, setSearchingFrom] = useState(false); + const [searchingTo, setSearchingTo] = useState(false); + + const searchStation = async (query, setResults, setSearching) => { + if (!query.trim()) return; + setSearching(true); + try { + const stations = await searchStations(query); + setResults(stations); + } catch (error) { + console.error('Ошибка поиска:', error); + } + setSearching(false); + }; + + const handleFromSearch = () => { + searchStation(fromQuery, setFromResults, setSearchingFrom); + }; + + const handleToSearch = () => { + searchStation(toQuery, setToResults, setSearchingTo); + }; + + const selectFromStation = (station) => { + setSelectedFrom(station); + setFromQuery(station.title); + setFromResults([]); + }; + + const selectToStation = (station) => { + setSelectedTo(station); + setToQuery(station.title); + setToResults([]); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (selectedFrom && selectedTo) { + onRouteSearch(selectedFrom, selectedTo, date || undefined); + } else { + alert('Пожалуйста, выберите станции отправления и назначения'); + } + }; + + const defaultDate = new Date().toISOString().split('T')[0]; + + return ( +
+
+ + Станция отправления +
+ setFromQuery(e.target.value)} + /> + +
+ {fromResults.length > 0 && ( +
+ {fromResults.map((station, idx) => ( +
selectFromStation(station)} + style={{ cursor: 'pointer', backgroundColor: '#f8f9fa' }} + > + {station.title} + {station.type && {station.type}} +
+ ))} +
+ )} +
+ + + Станция назначения +
+ setToQuery(e.target.value)} + /> + +
+ {toResults.length > 0 && ( +
+ {toResults.map((station, idx) => ( +
selectToStation(station)} + style={{ cursor: 'pointer', backgroundColor: '#f8f9fa' }} + > + {station.title} + {station.type && {station.type}} +
+ ))} +
+ )} +
+ + + Дата поездки (опционально) + setDate(e.target.value)} + min={defaultDate} + /> + + + +
+ + {selectedFrom && selectedTo && ( +
+ Маршрут: {selectedFrom.title} → {selectedTo.title} +
+ )} +
+ ); +}; + +export default RouteSearch; diff --git a/electric_train/src/components/ScheduleDisplay.js b/electric_train/src/components/ScheduleDisplay.js new file mode 100644 index 000000000..f2d77d3dc --- /dev/null +++ b/electric_train/src/components/ScheduleDisplay.js @@ -0,0 +1,108 @@ +import React from 'react'; +import { Card, ListGroup, Badge, Spinner, Alert } from 'react-bootstrap'; + +const ScheduleDisplay = ({ schedule, station, loading, isRoute = false }) => { + if (loading) { + return ( +
+ +

Загрузка расписания...

+
+ ); + } + + if (!schedule || schedule.length === 0) { + return ( + + {station ? 'Выберите станцию для просмотра расписания' : 'Расписание не найдено'} + + ); + } + + const formatTime = (date) => { + if (!date) return 'Время уточняется'; + try { + const dateObj = typeof date === 'string' ? new Date(date) : date; + if (isNaN(dateObj.getTime())) return 'Время уточняется'; + return dateObj.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); + } catch { + return 'Время уточняется'; + } + }; + + const formatFullDate = (date) => { + if (!date) return null; + try { + const dateObj = typeof date === 'string' ? new Date(date) : date; + if (isNaN(dateObj.getTime())) return null; + return dateObj.toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } catch { + return null; + } + }; + + return ( +
+ {station && !isRoute && ( + + + {station.title} + + {station.type || 'Железнодорожная станция'} + + + + )} + +
+ {isRoute ? 'Доступные рейсы:' : 'Ближайшие отправления:'} +
+ + + {schedule.map((item, index) => ( + +
+
+ Поезд №{item.number || '—'} +
+ {item.title || 'Пригородный поезд'} +
+
+ + {formatTime(item.departure)} + +
+ +
+ {item.departure && ( +
🕐 Отправление: {formatFullDate(item.departure) || 'Время уточняется'}
+ )} + {item.arrival && ( +
🏁 Прибытие: {formatFullDate(item.arrival) || 'Время уточняется'}
+ )} + {item.duration && ( +
⏱️ В пути: {typeof item.duration === 'number' ? Math.floor(item.duration / 60) + ' мин' : item.duration}
+ )} + {item.platform && ( +
🚉 Платформа: {item.platform}
+ )} + {item.carrier?.title && ( +
🚂 Перевозчик: {item.carrier.title}
+ )} + {isRoute && item.from && item.to && ( +
📌 {item.from} → {item.to}
+ )} +
+
+ ))} +
+
+ ); +}; + +export default ScheduleDisplay; diff --git a/electric_train/src/components/StationSearch.js b/electric_train/src/components/StationSearch.js new file mode 100644 index 000000000..a6d3ab5ad --- /dev/null +++ b/electric_train/src/components/StationSearch.js @@ -0,0 +1,115 @@ +import React, { useState } from 'react'; +import { Form, Button, ListGroup, Spinner, Alert } from 'react-bootstrap'; +import { searchStations } from '../services/YandexApi'; + +const StationSearch = ({ onStationSelect, loading }) => { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [searching, setSearching] = useState(false); + const [error, setError] = useState(''); + + const handleSearch = async () => { + if (!query.trim()) { + setError('Введите название станции'); + return; + } + + setSearching(true); + setError(''); + setResults([]); + + try { + const stations = await searchStations(query.trim()); + if (stations.length === 0) { + setError('Станции не найдены. Попробуйте другой запрос'); + } else { + setResults(stations); + } + } catch (error) { + console.error('Ошибка поиска:', error); + setError('Ошибка при поиске станций'); + } finally { + setSearching(false); + } + }; + + const handleKeyPress = (e) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + const handleSelectStation = (station) => { + console.log('Выбрана станция:', station); + onStationSelect(station); + setResults([]); + setQuery(''); + }; + + return ( +
+
+ + Название станции +
+ setQuery(e.target.value)} + onKeyPress={handleKeyPress} + disabled={searching || loading} + /> + +
+
+
+ + {error && ( + + {error} + + )} + + {searching && ( +
+ + Поиск станций... +
+ )} + + {results.length > 0 && ( + + {results.map((station, index) => ( + handleSelectStation(station)} + className="station-item" + style={{ cursor: 'pointer' }} + > +
+ {station.title} +
+ {station.type && ( + {station.type} + )} + {station.region && ( + {station.region} + )} +
+ ))} +
+ )} +
+ ); +}; + +export default StationSearch; diff --git a/electric_train/src/index.css b/electric_train/src/index.css new file mode 100644 index 000000000..2b12aaaa4 --- /dev/null +++ b/electric_train/src/index.css @@ -0,0 +1,60 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +* { + box-sizing: border-box; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #555; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.schedule-item { + animation: fadeIn 0.3s ease-out; +} + +.cursor-pointer { + cursor: pointer; + transition: background-color 0.2s; +} + +.cursor-pointer:hover { + background-color: #f8f9fa; +} diff --git a/electric_train/src/index.js b/electric_train/src/index.js new file mode 100644 index 000000000..a3d7d3912 --- /dev/null +++ b/electric_train/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './app/App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/electric_train/src/logo.svg b/electric_train/src/logo.svg new file mode 100644 index 000000000..9dfc1c058 --- /dev/null +++ b/electric_train/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electric_train/src/services/YandexApi.js b/electric_train/src/services/YandexApi.js new file mode 100644 index 000000000..c475e013d --- /dev/null +++ b/electric_train/src/services/YandexApi.js @@ -0,0 +1,152 @@ +const PROXY_URL = process.env.REACT_APP_PROXY_URL || 'http://localhost:3001/api/rasp'; + +let stationsCache = null; + +export const fetchAllStations = async () => { + try { + const url = `${PROXY_URL}/stations_list/?lang=ru_RU&format=json`; + console.log('Загрузка списка станций...'); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP ошибка: ${response.status}`); + } + + const data = await response.json(); + + const stations = []; + + if (data.countries) { + for (const country of data.countries) { + if (country.regions) { + for (const region of country.regions) { + if (region.settlements) { + for (const settlement of region.settlements) { + if (settlement.stations) { + for (const station of settlement.stations) { + if (station.transport_type === 'train') { + stations.push({ + code: station.codes?.yandex_code, + title: station.title, + type: station.station_type, + lat: station.latitude, + lon: station.longitude, + region: region.title, + }); + } + } + } + } + } + } + } + } + } + + const validStations = stations.filter(s => s.code); + console.log(`Загружено станций: ${validStations.length}`); + + return validStations; + } catch (error) { + console.error('Ошибка загрузки станций:', error); + throw error; + } +}; + +export const searchStations = async (query) => { + try { + if (!stationsCache) { + stationsCache = await fetchAllStations(); + } + + const lowerQuery = query.toLowerCase(); + const results = stationsCache.filter(station => + station.title.toLowerCase().includes(lowerQuery) + ); + + return results.slice(0, 15); + } catch (error) { + console.error('Ошибка поиска станций:', error); + return []; + } +}; + +export const fetchStationSchedule = async (stationCode) => { + try { + console.log('Запрос расписания для станции:', stationCode); + + const url = `${PROXY_URL}/schedule/?station=${stationCode}&transport_types=suburban&event=departure&limit=50&lang=ru_RU`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP ошибка: ${response.status}`); + } + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error.text || data.error); + } + + if (data.schedule && data.schedule.length > 0) { + return data.schedule.map(item => ({ + number: item.thread?.number || '—', + title: item.thread?.title || 'Пригородный поезд', + departure: item.departure, + arrival: item.arrival, + platform: item.platform, + carrier: item.thread?.carrier, + duration: item.duration, + })); + } + + return []; + } catch (error) { + console.error('Ошибка при получении расписания станции:', error); + throw error; + } +}; + +export const fetchRouteSchedule = async (fromCode, toCode, date = null) => { + try { + let url = `${PROXY_URL}/search/?from=${fromCode}&to=${toCode}&transport_types=suburban&limit=50&lang=ru_RU`; + if (date) { + url += `&date=${date}`; + } + + console.log('Поиск маршрута...'); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP ошибка: ${response.status}`); + } + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error.text || data.error); + } + + if (data.segments && data.segments.length > 0) { + return data.segments.map(segment => ({ + number: segment.thread?.number || '—', + title: segment.thread?.title || 'Пригородный поезд', + departure: segment.departure, + arrival: segment.arrival, + duration: segment.duration, + platform: segment.platform, + carrier: segment.thread?.carrier, + from: segment.from?.title, + to: segment.to?.title, + })); + } + + return []; + } catch (error) { + console.error('Ошибка при получении расписания маршрута:', error); + throw error; + } +};