diff options
| author | networkException <git@nwex.de> | 2025-04-18 13:33:55 +0200 |
|---|---|---|
| committer | networkException <git@nwex.de> | 2025-04-18 13:36:24 +0200 |
| commit | 78b9b451b2da03a3d3261247bbbdde2f8195919d (patch) | |
| tree | 1a5d695dfd3e1aee4405930d5b028a3963651bef /lib/Travel/Status/MOTIS | |
Initial version of Travel::Status::MOTIS
This patch contains the initial implementation of Travel::Status::MOTIS,
an interface to MOTIS routing services for departures, trips
and stop search based on Travel::Status::DE::DBRIS.
While MOTIS' focus is on intermodal routing, this module
has been written for use in https://finalrewind.org/projects/travelynx,
as such it focuses on departures at stops and trips.
As MOTIS is open source and can be self hosted, there
are multiple services (sourced from the transport-apis
repository located as a submodule in `ext/`), available:
Currently RNV for local transit in Mannheim, Germany and
surrounding cities and transitous for worldwide crowdsourced
tranit feeds.
In addition to scheduled stops and trips this module supports
realtime delay predictions, tracks, polylines, cancellations,
headsigns and route colors whenever available.
Diffstat (limited to 'lib/Travel/Status/MOTIS')
| -rw-r--r-- | lib/Travel/Status/MOTIS/Polyline.pm | 98 | ||||
| -rw-r--r-- | lib/Travel/Status/MOTIS/Services.pm | 28 | ||||
| -rw-r--r-- | lib/Travel/Status/MOTIS/Services.pm.PL | 135 | ||||
| -rw-r--r-- | lib/Travel/Status/MOTIS/Stop.pm | 59 | ||||
| -rw-r--r-- | lib/Travel/Status/MOTIS/Stopover.pm | 123 | ||||
| -rw-r--r-- | lib/Travel/Status/MOTIS/Trip.pm | 186 | ||||
| -rw-r--r-- | lib/Travel/Status/MOTIS/TripAtStopover.pm | 78 |
7 files changed, 707 insertions, 0 deletions
diff --git a/lib/Travel/Status/MOTIS/Polyline.pm b/lib/Travel/Status/MOTIS/Polyline.pm new file mode 100644 index 0000000..ea46526 --- /dev/null +++ b/lib/Travel/Status/MOTIS/Polyline.pm @@ -0,0 +1,98 @@ +package Travel::Status::MOTIS::Polyline; + +use strict; +use warnings; +use 5.014; + +# Adapted from code by Slaven Rezic +# +# Copyright (C) 2009,2010,2012,2017,2018 Slaven Rezic. All rights reserved. +# This package is free software; you can redistribute it and/or +# modify it under the same terms as Perl itself. +# +# Mail: slaven@rezic.de +# WWW: http://www.rezic.de/eserte/ + +use parent 'Exporter'; + +our @EXPORT_OK = qw(decode_polyline); + +our $VERSION = '0.01'; + +# Translated this php script +# <http://unitstep.net/blog/2008/08/02/decoding-google-maps-encoded-polylines-using-php/> +# to perl +sub decode_polyline { + my ($encoded) = @_; + + my $length = length $encoded; + my $index = 0; + my @points; + my $lat = 0; + my $lon = 0; + + while ( $index < $length ) { + + # The encoded polyline consists of a latitude value followed + # by a longitude value. They should always come in pairs. Read + # the latitude value first. + for my $val ( \$lat, \$lon ) { + my $shift = 0; + my $result = 0; + + # Temporary variable to hold each ASCII byte. + my $b; + do { + # The `ord(substr($encoded, $index++))` statement returns + # the ASCII code for the character at $index. Subtract 63 + # to get the original value. (63 was added to ensure + # proper ASCII characters are displayed in the encoded + # polyline string, which is `human` readable) + $b = ord( substr( $encoded, $index++, 1 ) ) - 63; + + # AND the bits of the byte with 0x1f to get the original + # 5-bit `chunk. Then left shift the bits by the required + # amount, which increases by 5 bits each time. OR the + # value into $results, which sums up the individual 5-bit + # chunks into the original value. Since the 5-bit chunks + # were reversed in order during encoding, reading them in + # this way ensures proper summation. + $result |= ( $b & 0x1f ) << $shift; + $shift += 5; + } + + # Continue while the read byte is >= 0x20 since the last + # `chunk` was not OR'd with 0x20 during the conversion + # process. (Signals the end) + while ( $b >= 0x20 ); + + # see last paragraph of "Integer Arithmetic" in perlop.pod + use integer; + + # Check if negative, and convert. (All negative values have the last bit + # set) + my $dtmp + = ( ( $result & 1 ) ? ~( $result >> 1 ) : ( $result >> 1 ) ); + + # Compute actual latitude (resp. longitude) since value is + # offset from previous value. + $$val += $dtmp; + } + + # The actual latitude and longitude values were multiplied by + # 1e5 before encoding so that they could be converted to a 32-bit + # integer representation. (With a decimal accuracy of 7 places) + # Convert back to original values. + push( + @points, + { + lat => $lat * 1e-7, + lon => $lon * 1e-7 + } + ); + } + + return @points; +} + +1; diff --git a/lib/Travel/Status/MOTIS/Services.pm b/lib/Travel/Status/MOTIS/Services.pm new file mode 100644 index 0000000..71ec12b --- /dev/null +++ b/lib/Travel/Status/MOTIS/Services.pm @@ -0,0 +1,28 @@ +package Travel::Status::MOTIS::Services; + +# vim:readonly +# This module has been automatically generated +# by lib/Travel/Status/MOTIS/Services.pm.PL. +# Do not edit, changes will be lost. + +use strict; +use warnings; +use 5.014; +use utf8; + +our $VERSION = '0.01'; + +# Source <https://github.com/public-transport/transport-apis>. +# Many thanks to Jannis R / @derhuerst and all contributors for maintaining +# these resources. + +my $motis_instance = {'RNV' => {'coverage' => {'area' => {'coordinates' => [[['8.66147483636217','49.5668219936431'],['8.64642540920443','49.5568382715344'],['8.61042656597024','49.5503084660978'],['8.5819681642929','49.5577725222357'],['8.55635172724939','49.5487563121438'],['8.54881233439468','49.5248230928757'],['8.53276821729179','49.536837593048'],['8.49568358055447','49.5727690805899'],['8.47596243926148','49.5848639899874'],['8.44614801871398','49.5910126926984'],['8.42198404713383','49.5842342192271'],['8.41736989283368','49.5487563121438'],['8.40346035903804','49.5452129786743'],['8.37114730950125','49.555406853935'],['8.33286120248991','49.5529790328013'],['8.33137693421639','49.5375718561748'],['8.33494334548936','49.5221286699925'],['8.33690677795104','49.4988964466165'],['8.30678042064534','49.4885500590892'],['8.26784895688698','49.4838521585405'],['8.25423585654025','49.4735369859369'],['8.20632998266032','49.4760094013827'],['8.14462323584891','49.4734833746128'],['8.13045619910952','49.4443930041389'],['8.18420778670873','49.442720268909'],['8.27254930011','49.4496939870607'],['8.32155433680973','49.4591951125256'],['8.34370942136059','49.4513729891719'],['8.3668669374886','49.4323454360553'],['8.40277547897756','49.4209418648904'],['8.43005534345036','49.4239778970603'],['8.45940926837977','49.4431216465173'],['8.4917354449758','49.44388447979'],['8.51038774276287','49.4335868229186'],['8.49878755048184','49.417042238655'],['8.49553985226339','49.3996923273633'],['8.52037431311788','49.4046918305137'],['8.53117721917823','49.3953565625151'],['8.52699948591896','49.3835271603866'],['8.56649226595519','49.388735051347'],['8.60276909446185','49.3757445454189'],['8.60885726020382','49.3468487087811'],['8.64130741433121','49.3401811679553'],['8.65302897048286','49.3230983004625'],['8.70909291220218','49.312292313098'],['8.71188049820543','49.3403826797658'],['8.75572569226915','49.3430562267034'],['8.82165874069668','49.3630985437148'],['8.83347847335665','49.3927451052107'],['8.82975787417305','49.4998502106185'],['8.80422336940282','49.5074733892921'],['8.74840593219253','49.5017997361833'],['8.73188936131828','49.4590738976289'],['8.70928817502508','49.4483252857173'],['8.69685148074061','49.4688987841802'],['8.70045176672548','49.5065198846691'],['8.71373125714393','49.5462937942235'],['8.69365314006183','49.5626169448028'],['8.66147483636217','49.5668219936431']]],'type' => 'Polygon'},'regions' => ['DE-HE','DE-BW','DE-RP']},'endpoint' => 'https://directions.nwex.de/api/providers/rhein-neckar-verkehr/','homepage' => 'https://www.opendata-oepnv.de/ht/de/organisation/verkehrsunternehmen/rnv/openrnv/datensaetze','languages' => ['de'],'license' => 'https://www.govdata.de/dl-de/by-2-0','name' => 'Rhein-Neckar-Verkehr'},'transitous' => {'coverage' => {'area' => {'coordinates' => [[[['5.85','49.48'],['5.88','49.49'],['5.95','49.6'],['5.97','49.6'],['5.99','49.69'],['5.83','49.82'],['5.84','49.93'],['5.99','50.09'],['6.13','50.06'],['6.19','50.05'],['6.23','50.18'],['6.47','50.28'],['6.48','50.34'],['6.43','50.48'],['6.47','50.53'],['6.31','50.57'],['6.36','50.66'],['6.3','50.7'],['6.12','50.79'],['6.17','50.91'],['6.16','50.98'],['6.01','51.02'],['6.21','51.09'],['6.32','51.17'],['6.19','51.23'],['6.27','51.3'],['6.3','51.32'],['6.3','51.5'],['6.09','51.76'],['6.16','51.77'],['6.26','51.74'],['6.25','51.8'],['6.37','51.76'],['6.4','51.74'],['6.48','51.8'],['6.48','51.81'],['6.75','51.83'],['6.77','51.83'],['6.9','51.95'],['6.91','52.02'],['6.85','52.06'],['7.11','52.18'],['7.15','52.23'],['7.06','52.51'],['7.02','52.54'],['6.84','52.54'],['6.84','52.57'],['7.07','52.56'],['7.12','52.6'],['7.28','53.12'],['7.32','53.13'],['7.26','53.34'],['7.24','53.41'],['7.03','53.4'],['6.8','53.58'],['6.4','53.78'],['6.37','53.82'],['5.04','53.65'],['5.02','53.65'],['4.55','53.42'],['4.53','53.41'],['4.28','53.08'],['4.27','53.06'],['4.09','52.41'],['3.25','51.87'],['3.24','51.87'],['3.03','51.63'],['3.01','51.6'],['2.45','51.36'],['2.27','51.31'],['2.55','50.93'],['2.53','50.87'],['2.52','50.82'],['2.69','50.71'],['2.67','50.69'],['2.9','50.62'],['2.92','50.62'],['3.12','50.7'],['3.23','50.49'],['3.24','50.46'],['3.56','50.43'],['3.66','50.25'],['3.7','50.23'],['4.02','50.28'],['4.11','50.24'],['4.07','50.18'],['4.01','50.12'],['4.09','50.08'],['4.02','50.03'],['4.15','49.91'],['4.17','49.88'],['4.69','49.92'],['4.72','49.92'],['4.77','49.98'],['4.78','49.76'],['4.82','49.73'],['5.23','49.63'],['5.43','49.44'],['5.46','49.42'],['5.85','49.48']]],[[['9.05','45.76'],['9.19','45.91'],['9.14','45.95'],['9.09','46.02'],['9.33','46.22'],['9.32','46.25'],['9.34','46.43'],['9.42','46.44'],['9.51','46.22'],['9.57','46.24'],['9.92','46.31'],['10','46.17'],['10.04','46.16'],['10.19','46.16'],['10.2','46.21'],['10.24','46.43'],['10.2','46.47'],['10.11','46.49'],['10.11','46.5'],['10.22','46.55'],['10.26','46.49'],['10.29','46.48'],['10.5','46.48'],['10.56','46.53'],['10.46','46.69'],['10.57','46.96'],['10.52','46.99'],['10.39','47.07'],['10.35','47.06'],['10.1','46.91'],['9.94','46.98'],['9.94','47.08'],['9.88','47.09'],['9.56','47.11'],['9.59','47.23'],['9.74','47.35'],['9.74','47.42'],['9.54','47.6'],['9.27','47.72'],['9.25','47.72'],['8.92','47.71'],['8.83','47.8'],['8.75','47.8'],['8.75','47.78'],['8.57','47.89'],['8.53','47.86'],['8.32','47.69'],['8.34','47.66'],['8.21','47.69'],['8.2','47.69'],['7.76','47.61'],['7.77','47.65'],['7.7','47.67'],['7.55','47.65'],['7.54','47.63'],['7.38','47.5'],['7.28','47.49'],['7.24','47.55'],['7.21','47.56'],['6.94','47.56'],['6.93','47.53'],['6.8','47.36'],['6.83','47.29'],['6.87','47.29'],['6.9','47.28'],['6.68','47.11'],['6.34','46.96'],['6.37','46.91'],['6.38','46.81'],['5.99','46.58'],['6.04','46.55'],['5.98','46.38'],['6.04','46.36'],['6.06','46.35'],['5.85','46.1'],['5.95','46.06'],['6.33','46.17'],['6.39','46.22'],['6.31','46.31'],['6.36','46.34'],['6.43','46.35'],['6.68','46.39'],['6.75','46.37'],['6.73','46.11'],['6.76','46.07'],['6.81','46.07'],['6.79','46.02'],['6.87','45.98'],['6.91','45.99'],['7.07','45.79'],['7.12','45.79'],['7.57','45.92'],['7.87','45.85'],['7.9','45.86'],['8.22','46.12'],['8.22','46.17'],['8.18','46.24'],['8.38','46.35'],['8.38','46.19'],['8.42','46.19'],['8.59','46.06'],['8.6','46.06'],['8.74','46.03'],['8.66','45.95'],['8.77','45.93'],['8.84','45.91'],['8.86','45.75'],['8.92','45.76'],['9.02','45.75'],['9.05','45.76']]],[[['-62.79','17.53'],['-62.72','17.63'],['-63.04','17.83'],['-62.93','17.96'],['-62.88','18.03'],['-62.98','18.09'],['-62.99','18.1'],['-63.14','18.1'],['-63.17','18.09'],['-63.34','17.89'],['-63.3','17.86'],['-63.43','17.82'],['-63.45','17.8'],['-63.51','17.56'],['-63.5','17.53'],['-63.08','17.24'],['-63.04','17.22'],['-62.79','17.53']]],[[['-69.62','12.37'],['-69.62','12.39'],['-69.64','12.55'],['-69.65','12.56'],['-69.88','12.81'],['-70.02','12.87'],['-70.06','12.87'],['-70.2','12.82'],['-70.22','12.81'],['-70.31','12.49'],['-70.31','12.47'],['-70.21','12.33'],['-70.19','12.31'],['-69.79','12.23'],['-69.71','12.21'],['-69.62','12.37']]],[[['-68.53','11.76'],['-68.51','11.77'],['-68.43','11.87'],['-68.34','11.79'],['-68.32','11.79'],['-68.15','11.8'],['-68.13','11.81'],['-67.95','12.06'],['-67.95','12.08'],['-67.99','12.36'],['-68.01','12.38'],['-68.32','12.55'],['-68.34','12.55'],['-68.58','12.48'],['-68.6','12.46'],['-68.65','12.3'],['-69.06','12.62'],['-69.08','12.63'],['-69.26','12.61'],['-69.28','12.6'],['-69.4','12.46'],['-69.41','12.44'],['-69.4','12.22'],['-69.39','12.2'],['-69.26','12.03'],['-69.25','12.02'],['-68.72','11.74'],['-68.7','11.74'],['-68.53','11.76']]]],'type' => 'MultiPolygon'},'regions' => ['BE','CH','NL']},'endpoint' => 'https://api.transitous.org/','homepage' => 'https://transitous.org/','languages' => [],'license' => undef,'name' => 'Transitous open public transport routing'}}; +sub get_service_ref { + return $motis_instance; +} + +sub get_service_map { + return %{$motis_instance}; +} + +1; diff --git a/lib/Travel/Status/MOTIS/Services.pm.PL b/lib/Travel/Status/MOTIS/Services.pm.PL new file mode 100644 index 0000000..dc86963 --- /dev/null +++ b/lib/Travel/Status/MOTIS/Services.pm.PL @@ -0,0 +1,135 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use 5.014; +use utf8; +use Data::Dumper; +use Encode qw(encode); +use File::Slurp qw(read_file write_file); +use JSON; + +my $json = JSON->new->utf8; + +sub load_instance { + my ( $path, %opt ) = @_; + + my $data = $json->decode( scalar read_file("ext/transport-apis/data/${path}.json") ); + my %ret = ( + name => $data->{name} =~ s{ *[(][^)]+[)]}{}r, + license => $data->{attribution}{license}, + homepage => $data->{attribution}{homepage}, + languages => $data->{supportedLanguages}, + endpoint => $data->{options}{endpoint}, + coverage => { + area => $data->{coverage}{realtimeCoverage}{area}, + regions => $data->{coverage}{realtimeCoverage}{region} // [] + }, + ); + + my %bitmask_to_product; + for my $product ( @{ $data->{options}{products} // [] } ) { + for my $bitmask ( @{ $product->{bitmasks} // [] } ) { + $bitmask_to_product{$bitmask} = $product; + } + } + + my $skipped = 0; + for my $bit ( 0 .. 15 ) { + if ( my $p = $bitmask_to_product{ 2**$bit } ) { + for ( 1 .. $skipped ) { + push( @{ $ret{productbits} }, [ "_", undef ] ); + } + if ( $p->{name} ) { + push( @{ $ret{productbits} }, [ $p->{id}, $p->{name} ] ); + } + else { + push( @{ $ret{productbits} }, $p->{id} ); + } + } + else { + $skipped += 1; + } + } + + if ( $data->{options}{ext} ) { + $ret{request}{ext} = $data->{options}{ext}; + } + if ( $data->{options}{ver} ) { + $ret{request}{ver} = $data->{options}{ver}; + } + elsif ( $data->{options}{version} ) { + $ret{request}{ver} = $data->{options}{version}; + } + + if ( $opt{geoip_lock} ) { + $ret{geoip_lock} = $opt{geoip_lock}; + } + + if ( $opt{lang} ) { + $ret{request}{lang} = $opt{lang}; + } + if ( $opt{ua_string} ) { + $ret{ua_string} = $opt{ua_string}; + } + if ( $opt{ver} ) { + $ret{request}{ver} = $opt{ver}; + } + + return %ret; +} + +my %motis_instance = ( + RNV => { + load_instance('de/rnv-motis') + }, + transitous => { + load_instance('un/transitous') + }, +); + +my $perlobj = Data::Dumper->new( [ \%motis_instance ], ['motis_instance'] ); + +my $buf = <<'__EOF__'; +package Travel::Status::MOTIS::Services; + +# vim:readonly +# This module has been automatically generated +# by lib/Travel/Status/MOTIS/Services.pm.PL. +# Do not edit, changes will be lost. + +use strict; +use warnings; +use 5.014; +use utf8; + +our $VERSION = '0.01'; + +# Source <https://github.com/public-transport/transport-apis>. +# Many thanks to Jannis R / @derhuerst and all contributors for maintaining +# these resources. + +__EOF__ + +$buf .= 'my ' . $perlobj->Sortkeys(1)->Indent(0)->Dump; + +$buf =~ s{\Q\x{d6}\E}{Ö}g; +$buf =~ s{\Q\x{c9}\E}{É}g; +$buf =~ s{\Q\x{f3}\E}{ó}g; +$buf =~ s{\Q\x{f6}\E}{ö}g; +$buf =~ s{\Q\x{fc}\E}{ü}g; + +$buf .= <<'__EOF__'; + +sub get_service_ref { + return $motis_instance; +} + +sub get_service_map { + return %{$motis_instance}; +} + +1; +__EOF__ + +write_file( $ARGV[0], { binmode => ':utf8' }, $buf ); diff --git a/lib/Travel/Status/MOTIS/Stop.pm b/lib/Travel/Status/MOTIS/Stop.pm new file mode 100644 index 0000000..85348bf --- /dev/null +++ b/lib/Travel/Status/MOTIS/Stop.pm @@ -0,0 +1,59 @@ +package Travel::Status::MOTIS::Stop; + +use strict; +use warnings; +use 5.020; + +use parent 'Class::Accessor'; + +our $VERSION = '0.01'; + +Travel::Status::MOTIS::Stop->mk_ro_accessors(qw( + id + name + type + lat + lon +)); + +sub from_match { + my ( $obj, %opt ) = @_; + + my $json = $opt{json}; + + my $ref = { + id => $json->{id}, + name => $json->{name}, + lat => $json->{lat}, + lon => $json->{lon}, + }; + + bless( $ref, $obj ); + + return $ref; +} + +sub from_stopover { + my ( $obj, %opt ) = @_; + + my $json = $opt{json}; + + my $ref = { + id => $json->{stopId}, + name => $json->{name}, + lat => $json->{lat}, + lon => $json->{lon}, + }; + + bless( $ref, $obj ); + + return $ref; +} + +sub TO_JSON { + my ($self) = @_; + + return { %{$self} }; +} + +1; diff --git a/lib/Travel/Status/MOTIS/Stopover.pm b/lib/Travel/Status/MOTIS/Stopover.pm new file mode 100644 index 0000000..e0b03df --- /dev/null +++ b/lib/Travel/Status/MOTIS/Stopover.pm @@ -0,0 +1,123 @@ +package Travel::Status::MOTIS::Stopover; + +use strict; +use warnings; +use 5.020; + +use parent 'Class::Accessor'; + +use DateTime::Format::ISO8601; + +our $VERSION = '0.01'; + +Travel::Status::MOTIS::Stopover->mk_ro_accessors(qw( + stop + + is_cancelled + is_realtime + + arrival + scheduled_arrival + realtime_arrival + + departure + scheduled_departure + realtime_departure + + delay + arrival_delay + departure_delay + + track + scheduled_track + realtime_track +)); + +sub new { + my ( $obj, %opt ) = @_; + + my $json = $opt{json}; + my $realtime = $opt{realtime} // 0; + my $cancelled = $opt{cancelled}; + + my $ref = { + stop => Travel::Status::MOTIS::Stop->from_stopover( json => $json ), + + is_realtime => $realtime, + is_cancelled => $json->{cancelled} // $cancelled, + }; + + if ( $json->{scheduledArrival} ) { + $ref->{scheduled_arrival} = DateTime::Format::ISO8601->parse_datetime( $json->{scheduledArrival} ); + $ref->{scheduled_arrival}->set_time_zone('local'); + } + + if ( $json->{arrival} and $realtime ) { + $ref->{realtime_arrival} = DateTime::Format::ISO8601->parse_datetime( $json->{arrival} ); + $ref->{realtime_arrival}->set_time_zone('local'); + } + + if ( $json->{scheduledDeparture} ) { + $ref->{scheduled_departure} = DateTime::Format::ISO8601->parse_datetime( $json->{scheduledDeparture} ); + $ref->{scheduled_departure}->set_time_zone('local'); + } + + if ( $json->{departure} and $realtime ) { + $ref->{realtime_departure} = DateTime::Format::ISO8601->parse_datetime( $json->{departure} ); + $ref->{realtime_departure}->set_time_zone('local'); + } + + if ( $ref->{scheduled_arrival} and $ref->{realtime_arrival} ) { + $ref->{arrival_delay} = $ref->{realtime_arrival} + ->subtract_datetime( $ref->{scheduled_arrival} ) + ->in_units('minutes'); + } + + if ( $ref->{scheduled_departure} and $ref->{realtime_departure} ) { + $ref->{departure_delay} = $ref->{realtime_departure} + ->subtract_datetime( $ref->{scheduled_departure} ) + ->in_units('minutes'); + } + + if ( $json->{scheduledTrack} ) { + $ref->{scheduled_track} = $json->{scheduledTrack}; + } + + if ( $json->{track} ) { + $ref->{realtime_track} = $json->{track}; + } + + $ref->{delay} = $ref->{arrival_delay} // $ref->{departure_delay}; + + $ref->{arrival} = $ref->{realtime_arrival} // $ref->{scheduled_arrival}; + $ref->{departure} = $ref->{realtime_departure} // $ref->{scheduled_departure}; + $ref->{track} = $ref->{realtime_track} // $ref->{scheduled_track}; + + bless( $ref, $obj ); + + return $ref; +} + +sub TO_JSON { + my ($self) = @_; + + my $ret = { %{$self} }; + + for my $timestamp_key (qw( + arrival + scheduled_arrival + realtime_arrival + + departure + scheduled_departure + realtime_departure + )) { + if ( $ret->{$timestamp_key} ) { + $ret->{$timestamp_key} = $ret->{$timestamp_key}->epoch; + } + } + + return $ret; +} + +1; diff --git a/lib/Travel/Status/MOTIS/Trip.pm b/lib/Travel/Status/MOTIS/Trip.pm new file mode 100644 index 0000000..c879bee --- /dev/null +++ b/lib/Travel/Status/MOTIS/Trip.pm @@ -0,0 +1,186 @@ +package Travel::Status::MOTIS::Trip; + +use strict; +use warnings; +use 5.020; + +use parent 'Class::Accessor'; + +use DateTime::Format::ISO8601; + +use Travel::Status::MOTIS::Stop; +use Travel::Status::MOTIS::Polyline qw(decode_polyline); + +our $VERSION = '0.01'; + +Travel::Status::MOTIS::Trip->mk_ro_accessors(qw( + id + mode + agency + route_name + route_color + headsign + + is_realtime + is_cancelled + + arrival + scheduled_arrival + realtime_arrival + + departure + scheduled_departure + realtime_departure +)); + +sub new { + my ( $obj, %opt ) = @_; + + my $json = $opt{json}{legs}[0]; + + my $ref = { + id => $json->{tripId}, + mode => $json->{mode}, + agency => $json->{agencyName}, + route_name => $json->{routeShortName}, + route_color => $json->{routeColor}, + headsign => $json->{headsign}, + + is_cancelled => $json->{cancelled}, + is_realtime => $json->{realTime}, + + raw_stopovers => [ $json->{from}, @{ $json->{intermediateStops} }, $json->{to} ], + raw_polyline => $json->{legGeometry}->{points}, + }; + + $ref->{scheduled_departure} = DateTime::Format::ISO8601->parse_datetime( $json->{scheduledStartTime} ); + $ref->{scheduled_departure}->set_time_zone('local'); + + if ( $json->{realTime} ) { + $ref->{realtime_departure} = DateTime::Format::ISO8601->parse_datetime( $json->{startTime} ); + $ref->{realtime_departure}->set_time_zone('local'); + } + + $ref->{departure} = $ref->{realtime_departure} // $ref->{scheduled_departure}; + + $ref->{scheduled_arrival} = DateTime::Format::ISO8601->parse_datetime( $json->{scheduledEndTime} ); + $ref->{scheduled_arrival}->set_time_zone('local'); + + if ( $json->{realTime} ) { + $ref->{realtime_arrival} = DateTime::Format::ISO8601->parse_datetime( $json->{endTime} ); + $ref->{realtime_arrival}->set_time_zone('local'); + } + + $ref->{arrival} = $ref->{realtime_arrival} // $ref->{scheduled_arrival}; + + bless( $ref, $obj ); + + return $ref; +} + +sub polyline { + my ($self) = @_; + + if ( not $self->{raw_polyline} ) { + return; + } + + if ( $self->{polyline} ) { + return @{ $self->{polyline} }; + } + + my $polyline = [ decode_polyline( $self->{raw_polyline} ) ]; + + my $gis_distance; + + eval { + require GIS::Distance; + $gis_distance = GIS::Distance->new; + }; + + if ($gis_distance) { + my %minimum_distances; + + for my $stopover ( $self->stopovers ) { + my $stop = $stopover->stop; + + for my $polyline_index ( 0 .. $#{$polyline} ) { + my $coordinate = $polyline->[$polyline_index]; + my $distance = $gis_distance->distance_metal( + $stop->{lat}, + $stop->{lon}, + $coordinate->{lat}, + $coordinate->{lon}, + ); + + if ( not $minimum_distances{ $stop->id } + or $minimum_distances{ $stop->id }{distance} > $distance + ) { + $minimum_distances{ $stop->id } = { + distance => $distance, + index => $polyline_index, + }; + } + } + } + + for my $stopover ( $self->stopovers ) { + my $stop = $stopover->stop; + + if ( $minimum_distances{ $stop->id } ) { + $polyline->[ $minimum_distances{ $stop->id }{index} ]{stop} = $stop; + } + } + } + + $self->{polyline} = $polyline; + + return @{ $self->{polyline} }; +} + +sub stopovers { + my ($self) = @_; + + if ( $self->{stopovers} ) { + return @{ $self->{stopovers} }; + } + + @{ $self->{stopovers} } = map { + Travel::Status::MOTIS::Stopover->new( + json => $_, + realtime => $self->{is_realtime} + ) + } ( @{ $self->{raw_stopovers} // [] } ); + + return @{ $self->{stopovers} }; +} + +sub TO_JSON { + my ($self) = @_; + + # transform raw_route into route (lazy accessor) + $self->route; + + # transform raw_polyline into polyline (lazy accessor) + $self->polyline; + + my $ret = { %{$self} }; + + for my $timestamp_key (qw( + arrival + scheduled_arrival + realtime_arrival + + departure + scheduled_departure + realtime_departure + )) { + if ( $ret->{$timestamp_key} ) { + $ret->{$timestamp_key} = $ret->{$timestamp_key}->epoch; + } + } + + return $ret; +} + +1; diff --git a/lib/Travel/Status/MOTIS/TripAtStopover.pm b/lib/Travel/Status/MOTIS/TripAtStopover.pm new file mode 100644 index 0000000..3d8d390 --- /dev/null +++ b/lib/Travel/Status/MOTIS/TripAtStopover.pm @@ -0,0 +1,78 @@ +package Travel::Status::MOTIS::TripAtStopover; + +use strict; +use warnings; +use 5.020; + +use DateTime::Format::ISO8601; + +use parent 'Class::Accessor'; + +our $VERSION = '0.01'; + +Travel::Status::MOTIS::TripAtStopover->mk_ro_accessors(qw( + id + mode + agency + route_name + route_color + headsign + + is_cancelled + is_realtime + + stopover +)); + +sub new { + my ( $obj, %opt ) = @_; + + my $json = $opt{json}; + + my $ref = { + id => $json->{tripId}, + mode => $json->{mode}, + agency => $json->{agencyName}, + route_name => $json->{routeShortName}, + route_color => $json->{routeColor}, + headsign => $json->{headsign}, + + is_cancelled => $json->{cancelled}, + is_realtime => $json->{realTime}, + + stopover => Travel::Status::MOTIS::Stopover->new( + json => $json->{place}, + + # NOTE: $json->{place}->{cancelled} isn't set, we just override this here. + cancelled => $json->{cancelled}, + realtime => $json->{realTime}, + ), + }; + + bless( $ref, $obj ); + + return $ref; +} + +sub TO_JSON { + my ($self) = @_; + + my $ret = { %{$self} }; + + for my $timestamp_key (qw( + scheduled_departure + realtime_departure + departure + scheduled_arrival + realtime_arrival + arrival + )) { + if ( $ret->{$timestamp_key} ) { + $ret->{$timestamp_key} = $ret->{$timestamp_key}->epoch; + } + } + + return $ret; +} + +1; |
