Skip to content

Commit 69061b7

Browse files
authored
✨ Update live data from Motis API (#3400)
1 parent ff5245c commit 69061b7

File tree

8 files changed

+432
-294
lines changed

8 files changed

+432
-294
lines changed

app/Console/Commands/RefreshCurrentTrips.php

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
use App\DataProviders\DataProviderBuilder;
66
use App\DataProviders\DataProviderInterface;
7-
use App\DataProviders\HafasStopoverService;
7+
use App\DataProviders\Hydrators\MotisHydrator;
8+
use App\DataProviders\Motis;
9+
use App\DataProviders\Repositories\TripRepository;
10+
use App\Enum\DataProvider;
811
use App\Enum\TripSource;
912
use App\Exceptions\HafasException;
1013
use App\Models\Checkin;
11-
use App\Models\Trip;
1214
use Illuminate\Console\Command;
1315
use PDOException;
1416

@@ -17,38 +19,32 @@ class RefreshCurrentTrips extends Command
1719
protected $signature = 'trwl:refreshTrips';
1820
protected $description = 'Refresh delay data from current active trips';
1921

22+
private TripRepository $tripRepository;
23+
private MotisHydrator $motisHydrator;
24+
25+
public function __construct(?TripRepository $tripRepository = null, ?MotisHydrator $motisHydrator = null) {
26+
parent::__construct();
27+
$this->tripRepository = $tripRepository ?? new TripRepository();
28+
$this->motisHydrator = $motisHydrator ?? new MotisHydrator();
29+
}
30+
2031
private function getDataProvider(): DataProviderInterface {
2132
// Probably only HafasController is needed here, because this Command is very Hafas specific
2233
return (new DataProviderBuilder)->build();
2334
}
2435

2536
public function handle(): int {
37+
if ($this->getDataProvider() instanceof Motis === false) {
38+
$this->error('Currently only Motis is supported for this command.');
39+
return 1;
40+
}
41+
2642
$this->info('Getting trips to be refreshed...');
2743

28-
// To only refresh checked in trips join train_checkins:
29-
$trips = Trip::join('train_checkins', 'train_checkins.trip_id', '=', 'hafas_trips.trip_id')
30-
->join('train_stopovers as origin_stopovers', 'origin_stopovers.id', '=', 'train_checkins.origin_stopover_id')
31-
->join('train_stopovers as destination_stopovers', 'destination_stopovers.id', '=', 'train_checkins.destination_stopover_id')
32-
->where(function($query) {
33-
$query->where('destination_stopovers.arrival_planned', '>=', now()->subMinutes(20))
34-
->orWhere('destination_stopovers.arrival_real', '>=', now()->subMinutes(20));
35-
})
36-
->where(function($query) {
37-
$query->where('origin_stopovers.departure_planned', '<=', now()->addMinutes(20))
38-
->orWhere('origin_stopovers.departure_real', '<=', now()->addMinutes(20));
39-
})
40-
->where(function($query) {
41-
$query->where('hafas_trips.last_refreshed', '<', now()->subMinutes(5))
42-
->orWhereNull('hafas_trips.last_refreshed');
43-
})
44-
->where('hafas_trips.source', TripSource::HAFAS->value)
45-
->select('hafas_trips.*')
46-
->distinct()
47-
->orderBy('hafas_trips.last_refreshed')
48-
->get();
44+
$trips = $this->tripRepository->getCurrentActiveTrips(TripSource::TRANSITOUS);
4945

5046
if ($trips->isEmpty()) {
51-
$this->warn('No trips to be refreshed');
47+
$this->info('No trips to be refreshed');
5248
return 0;
5349
}
5450

@@ -60,9 +56,14 @@ public function handle(): int {
6056
$this->info('Refreshing trip ' . $trip->trip_id . ' (' . $trip->linename . ')...');
6157
$trip->update(['last_refreshed' => now()]);
6258

63-
$rawHafas = $this->getDataProvider()->fetchRawHafasTrip($trip->trip_id, $trip->linename);
64-
$updatedCounts = HafasStopoverService::refreshStopovers($rawHafas);
65-
$this->info('Updated ' . $updatedCounts->stopovers . ' stopovers.');
59+
$rawJourney = $this->getDataProvider()->fetchRawHafasTrip($trip->trip_id, $trip->linename);
60+
$stopovers = $this->motisHydrator->parseLegToUpdateStopovers(
61+
$rawJourney['legs'][0],
62+
$trip,
63+
DataProvider::TRANSITOUS
64+
);
65+
66+
$this->info(sprintf('Updated stopovers: %d', $stopovers->count()));
6667

6768
//set duration for refreshed trips to null, so it will be recalculated
6869
Checkin::where('trip_id', $trip->trip_id)->update(['duration' => null]);
@@ -73,7 +74,7 @@ public function handle(): int {
7374
report($exception);
7475
}
7576
} catch (HafasException) {
76-
// Do nothing
77+
$this->error('-> Skipping, due to HafasException');
7778
} catch (\Exception $exception) {
7879
report($exception);
7980
}

app/DataProviders/HafasStopoverService.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
use Carbon\Carbon;
88
use stdClass;
99

10+
/**
11+
* @deprecated
12+
*/
1013
class HafasStopoverService
1114
{
1215
private DataProviderInterface $dataProvider;
@@ -85,7 +88,7 @@ public static function refreshStopovers(stdClass $rawHafas): stdClass {
8588
* @throws HafasException
8689
*/
8790
public function refreshStopover(Stopover $stopover): void {
88-
if($stopover->departure_planned === null) {
91+
if ($stopover->departure_planned === null) {
8992
return;
9093
}
9194
$departure = $this->dataProvider->getDepartures(
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\DataProviders\Hydrators;
6+
7+
use App\DataProviders\Repositories\MotisLicenseRepository;
8+
use App\DataProviders\Repositories\StationRepository;
9+
use App\Dto\Internal\BahnTrip;
10+
use App\Dto\Internal\Departure;
11+
use App\Enum\DataProvider;
12+
use App\Enum\HafasTravelType;
13+
use App\Enum\MotisCategory;
14+
use App\Hydrators\DepartureHydrator;
15+
use App\Models\HafasOperator;
16+
use App\Models\Station;
17+
use App\Models\Stopover;
18+
use App\Models\Trip;
19+
use App\Services\OperatorService;
20+
use Carbon\Carbon;
21+
use Exception;
22+
use Illuminate\Support\Collection;
23+
use Illuminate\Support\Facades\Log;
24+
25+
class MotisHydrator
26+
{
27+
28+
private MotisLicenseRepository $motisRepository;
29+
private StationRepository $stationRepository;
30+
private OperatorService $operatorService;
31+
32+
public function __construct(
33+
?MotisLicenseRepository $motisRepository = null,
34+
?StationRepository $stationRepository = null,
35+
?OperatorService $operatorService = null
36+
)
37+
{
38+
$this->motisRepository = $motisRepository ?? new MotisLicenseRepository();
39+
$this->stationRepository = $stationRepository ?? new StationRepository();
40+
$this->operatorService = $operatorService ?? new OperatorService();
41+
}
42+
43+
public function parseLegToNewStopovers(mixed $leg, DataProvider $source): Collection
44+
{
45+
$rawStopovers = $leg['intermediateStops'];
46+
$stopoverCacheFromDB = $this->stationRepository->getStationsByIdentifiers(array_column($rawStopovers, 'stopId'), $source);
47+
48+
// add origin and destination to stopovers
49+
$rawStopovers[] = $leg['from'];
50+
$rawStopovers[] = $leg['to'];
51+
52+
$stopovers = collect();
53+
foreach ($rawStopovers as $rawStop) {
54+
$station = $stopoverCacheFromDB->where('stationIdentifiers', function ($query) use ($rawStop, $source) {
55+
$query->where('identifier', $rawStop['stopId'])
56+
->where('type', 'motis')
57+
->where('origin', $source->value);
58+
})->first();
59+
60+
$stopover = new Stopover($this->getStopoverData($station, $rawStop, $source));
61+
$stopovers->push($stopover);
62+
}
63+
return $stopovers;
64+
}
65+
66+
public function parseLegToUpdateStopovers(mixed $leg, Trip $trip, DataProvider $source): Collection
67+
{
68+
$rawStopovers = $leg['intermediateStops'];
69+
$stopoverCacheFromDB = $this->stationRepository->getStationsByIdentifiers(array_column($rawStopovers, 'stopId'), $source);
70+
71+
// add origin and destination to stopovers
72+
$rawStopovers[] = $leg['from'];
73+
$rawStopovers[] = $leg['to'];
74+
75+
$stopovers = collect();
76+
$key = ['trip_id', 'train_station_id', 'departure_planned', 'arrival_planned'];
77+
foreach ($rawStopovers as $rawStop) {
78+
$station = $stopoverCacheFromDB->where('stationIdentifiers', function ($query) use ($rawStop, $source) {
79+
$query->where('identifier', $rawStop['stopId'])
80+
->where('type', 'motis')
81+
->where('origin', $source->value);
82+
})->first();
83+
$stopoverData = $this->getStopoverData($station, $rawStop, $source);
84+
$stopoverData['trip_id'] = $trip->trip_id;
85+
86+
try {
87+
$stopover = Stopover::upsert(
88+
$stopoverData,
89+
$key,
90+
[
91+
'arrival_real',
92+
'departure_real',
93+
'arrival_platform_real',
94+
'departure_platform_real',
95+
'cancelled',
96+
]
97+
);
98+
$stopovers->push($stopover);
99+
} catch (Exception $exception) {
100+
Log::error('Failed to upsert stopover', [
101+
'stopover' => $rawStop,
102+
'error' => $exception->getMessage(),
103+
]);
104+
}
105+
}
106+
107+
return $stopovers;
108+
}
109+
110+
public function getStopoverData($station, mixed $rawStop, DataProvider $source): array
111+
{
112+
$station = $station ?? $this->stationRepository->createMotisStation($rawStop, $source);
113+
114+
$departurePlanned = isset($rawStop['scheduledDeparture']) ? Carbon::parse($rawStop['scheduledDeparture']) : null;
115+
$departureReal = isset($rawStop['departure']) ? Carbon::parse($rawStop['departure']) : null;
116+
$arrivalPlanned = isset($rawStop['scheduledArrival']) ? Carbon::parse($rawStop['scheduledArrival']) : null;
117+
$arrivalReal = isset($rawStop['arrival']) ? Carbon::parse($rawStop['arrival']) : null;
118+
// new API does not differ between departure and arrival platform
119+
$platformPlanned = $rawStop['scheduledTrack'] ?? null;
120+
$platformReal = $rawStop['track'] ?? $platformPlanned;
121+
122+
return [
123+
'train_station_id' => $station->id,
124+
'arrival_planned' => $arrivalPlanned ?? $departurePlanned,
125+
'arrival_real' => $arrivalReal ?? $departureReal ?? null,
126+
'departure_planned' => $departurePlanned ?? $arrivalPlanned,
127+
'departure_real' => $departureReal ?? $arrivalReal ?? null,
128+
'arrival_platform_planned' => $platformPlanned,
129+
'departure_platform_planned' => $platformPlanned,
130+
'arrival_platform_real' => $platformReal,
131+
'departure_platform_real' => $platformReal,
132+
];
133+
}
134+
135+
public function getTripData(mixed $leg, string $lineName, DataProvider $source): array
136+
{
137+
$originStation = $this->stationRepository->getStationsByIdentifiers($leg['from']['stopId'], $source)->first() ?? $this->stationRepository->createMotisStation($leg['from'], $source);
138+
$destinationStation = $this->stationRepository->getStationsByIdentifiers($leg['to']['stopId'], $source)->first() ?? $this->stationRepository->createMotisStation($leg['to'], $source);
139+
$departure = isset($leg['from']['departure']) ? Carbon::parse($leg['from']['departure']) : null;
140+
$arrival = isset($leg['to']['arrival']) ? Carbon::parse($leg['to']['arrival']) : null;
141+
$category = MotisCategory::tryFrom($leg['mode'])?->getHTT()->value ?? HafasTravelType::REGIONAL;
142+
$tripLineName = !empty($leg['routeShortName']) ? $leg['routeShortName'] : $lineName;
143+
$license = $this->motisRepository->getLicense($leg['source'], $source);
144+
$operator = $this->parseOperator($leg);
145+
146+
147+
return [
148+
'category' => $category,
149+
'number' => $tripLineName,
150+
'linename' => $tripLineName,
151+
'journey_number' => null,
152+
'operator_id' => $operator?->id,
153+
'origin_id' => $originStation->id,
154+
'destination_id' => $destinationStation->id,
155+
'polyline_id' => null, //TODO
156+
'departure' => $departure,
157+
'arrival' => $arrival,
158+
'source' => $source->value,
159+
'motis_source' => $source->value . '/' . $leg['source'],
160+
'motis_source_license_id' => $license?->id ?? null,
161+
];
162+
}
163+
164+
public function parseOperator(array $leg): ?HafasOperator
165+
{
166+
return $this->operatorService->parseTransitousOperator(
167+
agencyId: $leg['agencyId'] ?? null,
168+
agencyName: $leg['agencyName'] ?? null,
169+
);
170+
}
171+
172+
public function mapDepartures(mixed $entries, Station $station, Collection $departures, DataProvider $source): Collection
173+
{
174+
foreach ($entries as $rawDeparture) {
175+
if (config('trwl.motis.filter_licenses')) {
176+
// Check if the source is licensed under an acceptable license
177+
$license = $this->motisRepository->getLicense($rawDeparture['source'], $source);
178+
if (empty($license)) {
179+
continue;
180+
}
181+
}
182+
183+
//trip
184+
$tripId = $rawDeparture['tripId'];
185+
$rawDepartureStation = $rawDeparture['place'];
186+
$tripLineName = $rawDeparture['routeShortName'] ?? '';
187+
$category = MotisCategory::tryFrom($rawDeparture['mode']);
188+
$hafasTravelType = $category->getHTT()->value;
189+
190+
$platformPlanned = $rawDepartureStation['scheduledTrack'] ?? '';
191+
$platformReal = $rawDepartureStation['track'] ?? $platformPlanned;
192+
try {
193+
$departureStation = $this->stationRepository->getStationsByIdentifiers([$rawDepartureStation['stopId']], $source)->first();
194+
if ($departureStation === null) {
195+
// if station does not exist, request it from API
196+
$departureStation = $this->stationRepository->createMotisStation($rawDepartureStation, $source);
197+
}
198+
} catch (Exception $exception) {
199+
Log::error($exception->getMessage());
200+
$departureStation = $station;
201+
}
202+
203+
$departure = new Departure(
204+
station: $departureStation,
205+
plannedDeparture: Carbon::parse($rawDepartureStation['scheduledDeparture']),
206+
realDeparture: !empty($rawDeparture['realTime']) ? Carbon::parse($rawDepartureStation['departure']) : null,
207+
trip: new BahnTrip(
208+
tripId: $tripId,
209+
direction: $rawDeparture['headsign'],
210+
lineName: $tripLineName,
211+
number: $tripId,
212+
category: $hafasTravelType,
213+
journeyNumber: $tripId,
214+
operator: $this->parseOperator($rawDeparture),
215+
),
216+
plannedPlatform: $platformPlanned,
217+
realPlatform: $platformReal,
218+
);
219+
220+
$departures->push($departure);
221+
}
222+
223+
return DepartureHydrator::map($departures);
224+
}
225+
}

0 commit comments

Comments
 (0)