summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rwxr-xr-xbin/motis532
1 files changed, 532 insertions, 0 deletions
diff --git a/bin/motis b/bin/motis
new file mode 100755
index 0000000..f6d0ab1
--- /dev/null
+++ b/bin/motis
@@ -0,0 +1,532 @@
+#!perl
+use strict;
+use warnings;
+use 5.020;
+
+our $VERSION = '0.01';
+
+use utf8;
+use DateTime;
+use Encode qw(decode);
+use JSON;
+use Getopt::Long qw(:config no_ignore_case);
+use List::Util qw(min max);
+
+use Travel::Status::MOTIS;
+
+use Data::Dumper;
+
+my ( $date, $time );
+my $modes_of_transit;
+my $developer_mode;
+my $show_trip_ids;
+my $use_cache = 1;
+my $cache;
+my ( $list_services, $service );
+my ( $json_output, $raw_json_output, $with_polyline );
+
+my %known_mode_of_transit = map { $_ => 1 }
+ (qw(TRANSIT TRAM SUBWAY FERRY AIRPLANE BUS COACH RAIL METRO HIGHSPEED_RAIL LONG_DISTANCE NIGHT_RAIL REGIONAL_FAST_RAIL REGIONAL_RAIL));
+
+binmode( STDOUT, ':encoding(utf-8)' );
+for my $arg (@ARGV) {
+ $arg = decode( 'UTF-8', $arg );
+}
+
+my $output_bold = -t STDOUT ? "\033[1m" : q{};
+my $output_reset = -t STDOUT ? "\033[0m" : q{};
+
+my $cf_first = "\e[38;5;11m";
+my $cf_mixed = "\e[38;5;208m";
+my $cf_second = "\e[0m"; #"\e[38;5;9m";
+my $cf_reset = "\e[0m";
+
+GetOptions(
+ 'd|date=s' => \$date,
+ 'h|help' => sub { show_help(0) },
+ 'i|show-trip-ids' => \$show_trip_ids,
+ 'm|modes-of-transit=s' => \$modes_of_transit,
+ 't|time=s' => \$time,
+ 's|service=s' => \$service,
+ 'V|version' => \&show_version,
+ 'cache!' => \$use_cache,
+ 'devmode' => \$developer_mode,
+ 'json' => \$json_output,
+ 'raw-json' => \$raw_json_output,
+ 'list' => \$list_services,
+) or show_help(1);
+
+if ($list_services) {
+ printf(
+ "%-40s %-14s %-15s\n\n",
+ 'operator', 'abbr. (-s)', 'languages (-l)',
+ );
+
+ for my $service ( Travel::Status::MOTIS::get_services() ) {
+ printf(
+ "%-40s %-14s %-15s\n",
+ $service->{name},
+ $service->{shortname},
+ join( q{ }, @{ $service->{languages} // [] } ),
+ );
+ }
+
+ exit 0;
+}
+
+$service //= 'transitous';
+
+if ($use_cache) {
+ my $cache_path = ( $ENV{XDG_CACHE_HOME} // "$ENV{HOME}/.cache" ) . '/Travel-Status-MOTIS';
+
+ eval {
+ require Cache::File;
+
+ $cache = Cache::File->new(
+ cache_root => $cache_path,
+ default_expires => '90 seconds',
+ lock_level => Cache::File::LOCK_LOCAL(),
+ );
+ };
+
+ if ($@) {
+ $cache = undef;
+ }
+}
+
+my ( $input ) = @ARGV;
+
+if ( not $input ) {
+ show_help(1);
+}
+
+my %opt = (
+ cache => $cache,
+ service => $service,
+ developer_mode => $developer_mode,
+);
+
+if ( $input =~ m{ ^ (?<lat> [0-9.]+ ) : (?<lon> [0-9].+ ) $ }x ) {
+ $opt{stops_by_coordinate} = {
+ lat => $+{lat},
+ lon => $+{lon},
+ };
+}
+# Format: yyyymmdd_hh:mm_feed_id
+elsif ( $input =~ m{^[0-9]{8}_[0-9]{2}:[0-9]{2}_} ) {
+ $opt{trip_id} = $input;
+}
+# Format: feed_id
+elsif ( $input =~ m{_} ) {
+ $opt{stop_id} = $input;
+}
+else {
+ $opt{stops_by_query} = $input;
+
+ my $status = Travel::Status::MOTIS->new(%opt);
+ if ( my $err = $status->errstr ) {
+ say STDERR "Request error while looking up '$opt{stops_by_query}': ${err}";
+ exit 2;
+ }
+
+ my $found;
+ for my $result ( $status->results ) {
+ if ( defined $result->id ) {
+ if ( lc( $result->name ) ne lc( $opt{stops_by_query} ) ) {
+ say $result->name;
+ }
+
+ $opt{stop_id} = $result->id;
+ $found = 1;
+ last;
+ }
+ }
+
+ if ( not $found ) {
+ say "Could not find stop '$opt{stops_by_query}'";
+ exit 1;
+ }
+}
+
+if ( $date or $time ) {
+ my $timestamp = DateTime->now( time_zone => 'local' );
+
+ if ($date) {
+ if ( $date
+ =~ m{ ^ (?<day> \d{1,2} ) [.] (?<month> \d{1,2} ) [.] (?<year> \d{4})? $ }x
+ )
+ {
+ $timestamp->set(
+ day => $+{day},
+ month => $+{month}
+ );
+ if ( $+{year} ) {
+ $timestamp->set( year => $+{year} );
+ }
+ }
+ else {
+ say '--date must be specified as DD.MM.[YYYY]';
+ exit 1;
+ }
+ }
+
+ if ($time) {
+ if ( $time =~ m{ ^ (?<hour> \d{1,2} ) : (?<minute> \d{1,2} ) $ }x ) {
+ $timestamp->set(
+ hour => $+{hour},
+ minute => $+{minute},
+ second => 0,
+ );
+ }
+ else {
+ say '--time must be specified as HH:MM';
+ exit 1;
+ }
+ }
+
+ $opt{timestamp} = $timestamp;
+}
+
+if ( $modes_of_transit and $modes_of_transit eq 'help' ) {
+ say "Supported modes of transmit (-m / --modes-of-transit):";
+ for my $mot (
+ qw(TRANSIT TRAM SUBWAY FERRY AIRPLANE BUS COACH RAIL METRO HIGHSPEED_RAIL LONG_DISTANCE NIGHT_RAIL REGIONAL_FAST_RAIL REGIONAL_RAIL))
+ {
+ say $mot;
+ }
+
+ exit 0;
+}
+
+if ($modes_of_transit) {
+ # Passing unknown MOTs to the backend results in HTTP 422 Unprocessable Entity
+ my @mots = split( qr{, *}, $modes_of_transit );
+
+ my $found_unknown;
+ for my $mot (@mots) {
+ if ( not $known_mode_of_transit{$mot} ) {
+ $found_unknown = 1;
+ say STDERR
+ "-m / --modes-of-transit: unknown mode of transit '$mot'";
+ }
+ }
+
+ if ($found_unknown) {
+ say STDERR 'supported modes of transit are: '
+ . join( q{, }, sort keys %known_mode_of_transit );
+ exit 1;
+ }
+
+ $opt{modes_of_transit} = [ grep { $known_mode_of_transit{$_} } @mots ];
+}
+
+sub show_help {
+ my ($code) = @_;
+
+ print
+ "Usage: motis [-d dd.mm.yyy] [-t hh:mm] [-i] <stopId|tripId|lat:lon>\n"
+ . "See also: man motis\n";
+
+ exit $code;
+}
+
+sub show_version {
+ say "motis version ${VERSION}";
+
+ exit 0;
+}
+
+sub spacer {
+ my ($len) = @_;
+ return ( $len % 2 ? q { } : q{} ) . ( q{ ·} x ( $len / 2 ) );
+}
+
+sub format_delay {
+ my ( $delay, $len ) = @_;
+ if ( $delay and $len ) {
+ return sprintf( "(%+${len}d)", $delay );
+ }
+ return q{};
+}
+
+my $status = Travel::Status::MOTIS->new(%opt);
+
+if ( my $err = $status->errstr ) {
+ say STDERR "Request error: ${err}";
+ exit 2;
+}
+
+if ($raw_json_output) {
+ say JSON->new->convert_blessed->encode( $status->{raw_json} );
+ exit 0;
+}
+
+if ($json_output) {
+ if ( $opt{journey} ) {
+ say JSON->new->convert_blessed->encode( $status->result );
+ }
+ else {
+ say JSON->new->convert_blessed->encode( [ $status->results ] );
+ }
+
+ exit 0;
+}
+
+if ( $opt{stop_id} ) {
+ my $max_route_name = max map { length( $_->route_name ) } $status->results;
+ my $max_headsign = max map { length( $_->headsign // q{} ) } $status->results;
+ my $max_delay = max map { length( $_->stopover->departure_delay // q{} ) } $status->results;
+ my $max_track = max map { length( $_->stopover->track // $_->stopover->scheduled_track // q{} ) } $status->results;
+
+ $max_delay += 1;
+
+ my @results = map { $_->[1] }
+ sort { $a->[0] <=> $b->[0] }
+ map { [ ( $_->stopover->departure // $_->stopover->arrival )->epoch, $_ ] } $status->results;
+
+ printf("%s\n\n", $results[0]->stopover->stop->name);
+
+ for my $result (@results) {
+ printf(
+ "%s %s %${max_route_name}s %${max_headsign}s %${max_track}s\n",
+ $result->is_cancelled ? '--:--' : $result->stopover->departure->strftime('%H:%M'),
+ $result->stopover->departure_delay
+ ? sprintf( "(%+${max_delay}d)", $result->stopover->departure_delay )
+ : q{ } x ( $max_delay + 2 ),
+ $result->route_name,
+ $result->headsign // q{???},
+ $result->stopover->track // q{}
+ );
+
+ if ( $show_trip_ids ) {
+ say $result->id;
+ }
+ }
+}
+elsif ( $opt{trip_id} ) {
+ my $trip = $status->result;
+
+ my $max_name = max map { length( $_->stop->name ) } $trip->stopovers;
+ my $max_track = max map { length( $_->track // q{} ) } $trip->stopovers;
+ my $max_delay = max map { $_->delay ? length( $_->delay ) + 3 : 0 } $trip->stopovers;
+
+ my $mark_stop = 0;
+ my $now = DateTime->now;
+
+ for my $i ( reverse 1 .. ( scalar $trip->stopovers // 0 ) ) {
+ my $stop = ( $trip->stopovers )[ $i - 1 ];
+
+ if (
+ not $stop->is_cancelled
+ and ( $stop->departure and $now <= $stop->departure
+ or $stop->arrival and $now <= $stop->arrival )
+ ) {
+ $mark_stop = $stop;
+ }
+ }
+
+ printf( "%s am %s\n\n", $trip->route_name, $trip->scheduled_arrival->strftime('%d.%m.%Y') );
+
+ for my $stop ( $trip->stopovers ) {
+ if ( $stop == $mark_stop ) {
+ print($output_bold);
+ }
+
+ if ( $stop->is_cancelled ) {
+ print(' --:-- ');
+ }
+ elsif ( $stop->arrival and $stop->departure ) {
+ printf( '%s → %s',
+ $stop->arrival->strftime('%H:%M'),
+ $stop->departure->strftime('%H:%M'),
+ );
+ }
+ elsif ( $stop->departure ) {
+ printf( ' %s', $stop->departure->strftime('%H:%M') );
+ }
+ elsif ( $stop->arrival ) {
+ printf( '%s ', $stop->arrival->strftime('%H:%M') );
+ }
+ else {
+ print(' ');
+ }
+
+ printf( " %${max_delay}s", format_delay( $stop->delay, $max_delay - 3 ) );
+ printf( " %-${max_name}s %${max_track}s\n", $stop->stop->name, $stop->track // q{} );
+
+ if ( $stop == $mark_stop ) {
+ print($output_reset);
+ }
+ }
+}
+elsif ( $opt{stops_by_coordinate} ) {
+ for my $result ( $status->results ) {
+ if ( defined $result->id ) {
+ printf( "%8d %s\n", $result->id, $result->name );
+ }
+ }
+}
+elsif ( $opt{stops_by_query} ) {
+ for my $result ( $status->results ) {
+ if ( defined $result->id ) {
+ printf( "%8d %s\n", $result->id, $result->name );
+ }
+ }
+}
+
+__END__
+
+=head1 NAME
+
+motis - An interface to the MOTIS routing services
+
+=head1 SYNOPSIS
+
+B<motis> [B<-s> I<service>] [B<-d> I<DD.MM.>] [B<-t> I<HH:MM>] [B<-i>] [I<opt>] I<station>
+
+B<motis> [B<-s> I<service>] [I<opt>] I<station>
+
+B<motis> [B<-s> I<service>] I<trip_id>
+
+B<motis> [B<-s> I<service>] B<?>I<query>|I<lat>B<:>I<lon>
+
+=head1 VERSION
+
+version 0.01
+
+=head1 DESCRIPTION
+
+B<motis> is an interface to MOTIS routing services. It can serve as an
+arrival/departure board, request details about a specific trip, and
+look up public transport stops by name or geolocation. The operating
+mode depends on the contents of its non-option argument.
+
+=head2 Departure Board (I<stop>)
+
+Show departures at I<stop>. I<stop> may be given as a stop name or
+stop id. For each departure, B<motis> shows
+
+=over
+
+=item * estimated departure time,
+
+=item * delay, if known,
+
+=item * trip route name,
+
+=item * headsign / destination if known, and
+
+=item * track, if known.
+
+=back
+
+=head2 Trip details (I<trip_id>)
+
+List intermediate stops of I<trip_id> (as given by the departure board when
+invoked with B<-i> / B<--show-trip-ids>) with arrival/departure time, delay (if
+available), track (if available), and stop name. Also includes some generic
+trip information.
+
+=head2 Stop Search (B<?>I<query>|I<lat>B<:>I<lon>)
+
+List stop that match I<query> or that are located in the vicinity of
+I<lat>B<:>I<lon> geocoordinates with stop id and name.
+
+=head1 OPTIONS
+
+Values in brackets indicate options that only apply to the corresponding
+operating mode(s).
+
+=over
+
+=item B<-d>, B<--date> I<DD.MM.[YYYY]> (departure board)
+
+Request departures on the specified date.
+Default: today.
+
+=item B<-t>, B<--time> I<HH:MM> (departure board)
+
+Request departures on the specified time.
+Default: now.
+
+=item B<-i>, B<--show-trip-ids> (departure board)
+
+Show trip id for each listed arrival/departure.
+These can be used to obtain details on individual trips with subsequent
+B<motis> invocations.
+
+=item B<-m>, B<--modes-of-transit> I<mot1>[,I<mot2>,...] (departure board)
+
+Only return results for the specified modes of transit.
+Use C<<-m help>> to get a list of supported modes of transit.
+
+=item B<--json>
+
+Print result(s) as JSON and exit. This is a dump of internal data structures
+and not guaranteed to remain stable between minor versions. Please use the
+Travel::Status::MOTIS(3pm) module if you need a proper API.
+
+=item B<--no-cache>
+
+By default, if the Cache::File module is available, server replies are cached
+for 90 seconds in F<~/.cache/Travel-Status-MOTIS> (or a path relative to
+C<$XDG_CACHE_HOME>, if set). Use this option to disable caching. You can use
+B<--cache> to re-enable it.
+
+=item B<--raw-json>
+
+Print unprocessed API response as JSON and exit.
+Useful for debugging and development purposes.
+
+=item B<-t>, B<--date> I<HH:MM> (departure board)
+
+Request departures on or after the specified time.
+Default: now.
+
+=item B<-V>, B<--version>
+
+Show version information and exit.
+
+=back
+
+=head1 EXIT STATUS
+
+0 upon success, 1 upon internal error, 2 upon backend error.
+
+=head1 CONFIGURATION
+
+None.
+
+=head1 DEPENDENCIES
+
+=over
+
+=item * Class::Accessor(3pm)
+
+=item * DateTime(3pm)
+
+=item * LWP::UserAgent(3pm)
+
+=back
+
+=head1 BUGS AND LIMITATIONS
+
+=over
+
+This module is mainly to debug the Travel::Status::MOTIS(3pm) module designed
+for use in travelynx (L<https://finalrewind.org/projects/travelynx/>) and as
+such might not contain functionality needed otherwise.
+
+=back
+
+=head1 AUTHOR
+
+Copyright (C) networkException E<lt>git@nwex.deE<gt>
+
+Based on Travel::Status::DE::DBRIS
+
+Copyright (C) 2024-2025 Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
+
+=head1 LICENSE
+
+This program is licensed under the same terms as Perl itself.