Skip to content

Commit

Permalink
🚑 add fallback for departures / journeys (#3104)
Browse files Browse the repository at this point in the history
Co-authored-by: Levin <[email protected]>
  • Loading branch information
MrKrisKrisu and HerrLevin authored Jan 8, 2025
1 parent bbd9e1c commit 83c89dc
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 15 deletions.
3 changes: 3 additions & 0 deletions app/DataProviders/HafasStopoverService.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public static function refreshStopovers(stdClass $rawHafas): stdClass {
* @throws HafasException
*/
public function refreshStopover(Stopover $stopover): void {
if($stopover->departure_planned === null) {
return;
}
$departure = $this->dataProvider->getDepartures(
station: $stopover->station,
when: $stopover->departure_planned,
Expand Down
28 changes: 28 additions & 0 deletions app/Dto/Transport/Departure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Dto\Transport;

use App\Models\Trip;
use Carbon\Carbon;

readonly class Departure {

public \App\Models\Station $station;
public Carbon $plannedDeparture;
public Carbon|null $realDeparture;
public Trip $trip;

public function __construct(\App\Models\Station $station, Carbon $plannedDeparture, Carbon|null $realDeparture, Trip $trip) {
$this->station = $station;
$this->plannedDeparture = $plannedDeparture;
$this->realDeparture = $realDeparture;
$this->trip = $trip;
}

public function getDelay(): ?int {
if($this->realDeparture === null) {
return null;
}
return $this->plannedDeparture->diffInMinutes($this->realDeparture);
}
}
36 changes: 36 additions & 0 deletions app/Enum/ReiseloesungCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);

namespace App\Enum;

enum ReiseloesungCategory: string
{
case ICE = 'ICE';
case EC_IC = 'EC_IC';
case IR = 'IR';
case REGIONAL = 'REGIONAL';
case SBAHN = 'SBAHN';
case BUS = 'BUS';
case SCHIFF = 'SCHIFF';
case UBAHN = 'UBAHN';
case TRAM = 'TRAM';
case ANRUFPFLICHTIG = 'ANRUFPFLICHTIG';
case UNKNOWN = 'UNKNOWN';


public function getHTT(): HafasTravelType {
return match ($this->name) {
'ICE' => HafasTravelType::NATIONAL_EXPRESS,
'EC_IC' => HafasTravelType::NATIONAL,
'IR' => HafasTravelType::REGIONAL_EXP,
'UNKNOWN', 'REGIONAL' => HafasTravelType::REGIONAL,
'SBAHN' => HafasTravelType::SUBURBAN,
'BUS' => HafasTravelType::BUS,
'SCHIFF' => HafasTravelType::FERRY,
'UBAHN' => HafasTravelType::SUBWAY,
'TRAM' => HafasTravelType::TRAM,
'ANRUFPFLICHTIG' => HafasTravelType::TAXI,
default => HafasTravelType::REGIONAL,
};
}
}
2 changes: 2 additions & 0 deletions app/Enum/TripSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ enum TripSource: string
*/
case HAFAS = 'hafas';

case BAHN_WEB_API = 'bahn-web-api';

/**
* Trips created by the user - with manual data.
*/
Expand Down
16 changes: 16 additions & 0 deletions app/Http/Controllers/API/v1/TransportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
use App\Exceptions\CheckInCollisionException;
use App\Exceptions\HafasException;
use App\Exceptions\StationNotOnTripException;
use App\Http\Controllers\Backend\Transport\BahnWebApiController;
use App\Http\Controllers\Backend\Transport\StationController;
use App\Http\Controllers\Backend\Transport\TrainCheckinController;
use App\Http\Controllers\TransportController as TransportBackend;
use App\Http\Resources\CheckinSuccessResource;
use App\Http\Resources\DepartureResource;
use App\Http\Resources\StationResource;
use App\Http\Resources\TripResource;
use App\Hydrators\CheckinRequestHydrator;
Expand Down Expand Up @@ -176,6 +178,20 @@ public function getDepartures(Request $request, int $stationId): JsonResponse {
]
);
} catch (HafasException) {
return $this->sendResponse(
data: DepartureResource::collection(BahnWebApiController::getDepartures($station)),
additional: [
'meta' => [
'station' => StationDto::fromModel($station),
'times' => [
'now' => $timestamp,
'prev' => $timestamp->clone()->subMinutes(15),
'next' => $timestamp->clone()->addMinutes(15)
],
]
]
);

return $this->sendError(__('messages.exception.generalHafas', [], 'en'), 502);
} catch (ModelNotFoundException) {
return $this->sendError(__('controller.transport.no-station-found', [], 'en'));
Expand Down
140 changes: 132 additions & 8 deletions app/Http/Controllers/Backend/Transport/BahnWebApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,39 @@

namespace App\Http\Controllers\Backend\Transport;

use App\Dto\Transport\Departure;
use App\Enum\ReiseloesungCategory;
use App\Enum\TripSource;
use App\Http\Controllers\Controller;
use App\Models\Station;
use App\Models\User;
use App\Models\Stopover;
use App\Models\Trip;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;

abstract class BahnWebApiController extends Controller
{
abstract class BahnWebApiController extends Controller {

public static function searchStation(string $query, int $limit = 10): Collection {
$url = "https://www.bahn.de/web/api/reiseloesung/orte?suchbegriff=" . urlencode($query) . "&typ=ALL&limit=" . $limit;
$response = Http::get($url);
$json = $response->json();
$extIds = [];
foreach ($json as $rawStation) {
if (!isset($rawStation['extId'])) {
foreach($json as $rawStation) {
if(!isset($rawStation['extId'])) {
continue;
}
$extIds[] = $rawStation['extId'];
}
$stationCache = Station::whereIn('ibnr', $extIds)->get();

$stations = collect();
foreach ($json as $rawStation) {
if (!isset($rawStation['extId'])) {
foreach($json as $rawStation) {
if(!isset($rawStation['extId'])) {
continue;
}
$station = $stationCache->where('ibnr', $rawStation['extId'])->first();
if ($station === null) {
if($station === null) {
$station = Station::create([
'name' => $rawStation['name'],
'latitude' => $rawStation['lat'],
Expand All @@ -44,4 +48,124 @@ public static function searchStation(string $query, int $limit = 10): Collection

return $stations;
}

public static function getDepartures(Station $station, Carbon|null $timestamp = null): Collection {
$timezone = "Europe/Berlin";
if($timestamp === null) {
$timestamp = now();
}
$timestamp->tz($timezone);
$response = Http::get("https://www.bahn.de/web/api/reiseloesung/abfahrten", [
'ortExtId' => $station->ibnr,
'datum' => $timestamp->format('Y-m-d'),
'zeit' => $timestamp->format('H:i'),
]);
$departures = collect();
foreach($response->json('entries') as $rawDeparture) {
$journey = Trip::where('trip_id', $rawDeparture['journeyId'])->first();
if($journey) {
$departures->push(new Departure(
station: $station,
plannedDeparture: Carbon::parse($rawDeparture['zeit'], $timezone),
realDeparture: isset($rawDeparture['ezZeit']) ? Carbon::parse($rawDeparture['ezZeit'], $timezone) : null,
trip: $journey,
));
continue;
}

$rawJourney = self::fetchJourney($rawDeparture['journeyId']);
if($rawJourney === null) {
// sorry
continue;
}
$stopoverCacheFromDB = Station::whereIn('ibnr', collect($rawJourney['halte'])->pluck('extId'))->get();

$originStation = $stopoverCacheFromDB->where('ibnr', $rawJourney['halte'][0]['extId'])->first() ?? self::getStationFromHalt($rawJourney['halte'][0]);
$destinationStation = $stopoverCacheFromDB->where('ibnr', $rawJourney['halte'][count($rawJourney['halte']) - 1]['extId'])->first() ?? self::getStationFromHalt($rawJourney['halte'][count($rawJourney['halte']) - 1]);
$departure = isset($rawJourney['halte'][0]['abfahrtsZeitpunkt']) ? Carbon::parse($rawJourney['halte'][0]['abfahrtsZeitpunkt'], $timezone) : null;
$arrival = isset($rawJourney['halte'][count($rawJourney['halte']) - 1]['ankunftsZeitpunkt']) ? Carbon::parse($rawJourney['halte'][count($rawJourney['halte']) - 1]['ankunftsZeitpunkt'], $timezone) : null;
$category = isset($rawDeparture['verkehrmittel']['produktGattung']) ? ReiseloesungCategory::tryFrom($rawDeparture['verkehrmittel']['produktGattung']) : ReiseloesungCategory::UNKNOWN;
$category = $category ?? ReiseloesungCategory::UNKNOWN;

//trip
$tripLineName = $rawDeparture['verkehrmittel']['name'] ?? '';
$tripNumber = preg_replace('/\s/', '-', strtolower($tripLineName)) ?? '';
$tripJourneyNumber = preg_replace('/\D/', '', $rawDeparture['verkehrmittel']['name']);

$journey = Trip::create([
'trip_id' => $rawDeparture['journeyId'],
'category' => $category->getHTT(),
'number' => $tripNumber,
'linename' => $tripLineName,
'journey_number' => !empty($tripJourneyNumber) ? $tripJourneyNumber : 0,
'operator_id' => null, //TODO
'origin_id' => $originStation->id,
'destination_id' => $destinationStation->id,
'polyline_id' => null,
'departure' => $departure,
'arrival' => $arrival,
'source' => TripSource::BAHN_WEB_API,
]);


$stopovers = collect();
foreach($rawJourney['halte'] as $rawHalt) {
$station = $stopoverCacheFromDB->where('ibnr', $rawHalt['extId'])->first() ?? self::getStationFromHalt($rawHalt);

$departurePlanned = isset($rawHalt['abfahrtsZeitpunkt']) ? Carbon::parse($rawHalt['abfahrtsZeitpunkt'], $timezone) : null;
$departureReal = isset($rawHalt['ezAbfahrtsZeitpunkt']) ? Carbon::parse($rawHalt['ezAbfahrtsZeitpunkt'], $timezone) : null;
$arrivalPlanned = isset($rawHalt['ankunftsZeitpunkt']) ? Carbon::parse($rawHalt['ankunftsZeitpunkt'], $timezone) : null;
$arrivalReal = isset($rawHalt['ezAnkunftsZeitpunkt']) ? Carbon::parse($rawHalt['ezAnkunftsZeitpunkt'], $timezone) : null;

$stopover = new Stopover([
'train_station_id' => $station->id,
'arrival_planned' => $arrivalPlanned ?? $departurePlanned,
'arrival_real' => $arrivalReal ?? $departureReal ?? null,
'departure_planned' => $departurePlanned ?? $arrivalPlanned,
'departure_real' => $departureReal ?? $arrivalReal ?? null,
]);
$stopovers->push($stopover);
}
$journey->stopovers()->saveMany($stopovers);

$departures->push(new Departure(
station: $station,
plannedDeparture: Carbon::parse($rawDeparture['zeit'], $timezone),
realDeparture: isset($rawDeparture['ezZeit']) ? Carbon::parse($rawDeparture['ezZeit'], $timezone) : null,
trip: $journey,
));
}
return $departures;
}

private static function getStationFromHalt(array $rawHalt) {
//$station = Station::where('ibnr', $rawHalt['extId'])->first();
//if($station !== null) {
// return $station;
// }

//urgh, there is no lat/lon - extract it from id
// example id: A=1@O=Druseltal, Kassel@X=9414484@Y=51301106@U=81@L=714800@
$matches = [];
preg_match('/@X=(\d+)@Y=(\d+)/', $rawHalt['id'], $matches);
$latitude = $matches[2] / 1000000;
$longitude = $matches[1] / 1000000;

return Station::updateOrCreate([
'ibnr' => $rawHalt['extId'],
], [
'name' => $rawHalt['name'],
'latitude' => $latitude ?? 0, // Hello Null-Island
'longitude' => $longitude ?? 0, // Hello Null-Island
'source' => TripSource::BAHN_WEB_API->value,
]);
}

public static function fetchJourney(string $journeyId, bool $poly = false): array|null {
$response = Http::get("https://www.bahn.de/web/api/reiseloesung/fahrt", [
'journeyId' => $journeyId,
'poly' => $poly ? 'true' : 'false',
]);
return $response->json();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

abstract class StatusTagController extends Controller
{
public static function getVisibleTagsForUser(Status $status, User $user = null): Collection {
public static function getVisibleTagsForUser(Status $status, ?User $user = null): Collection {
return $status->tags->filter(function(StatusTag $tag) use ($user) {
return Gate::forUser($user)->allows('view', $tag);
});
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/FrontendStatusController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Http\Controllers\Backend\User\ProfilePictureController;
use App\Http\Controllers\StatusController as StatusBackend;
use App\Models\Event;
use App\Models\Station;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\RedirectResponse;
Expand All @@ -20,6 +21,7 @@
class FrontendStatusController extends Controller
{
public function getDashboard(): Renderable|RedirectResponse {

$statuses = DashboardController::getPrivateDashboard(auth()->user());

return view('dashboard', [
Expand Down
Loading

0 comments on commit 83c89dc

Please sign in to comment.