File Coverage

blib/lib/Travel/Status/DE/URA.pm
Criterion Covered Total %
statement 165 204 80.8
branch 34 54 62.9
condition 35 61 57.3
subroutine 21 26 80.7
pod 7 9 77.7
total 262 354 74.0


line stmt bran cond sub pod time code
1             package Travel::Status::DE::URA;
2              
3 5     5   305581 use strict;
  5         10  
  5         125  
4 5     5   18 use warnings;
  5         9  
  5         102  
5 5     5   89 use 5.010;
  5         12  
6 5     5   15 use utf8;
  5         5  
  5         28  
7              
8 5     5   2743 no if $] >= 5.018, warnings => 'experimental::smartmatch';
  5         41  
  5         22  
9              
10             our $VERSION = '2.01';
11              
12             # create CONSTANTS for different Return Types
13             use constant {
14 5         464 TYPE_STOP => 0,
15             TYPE_PREDICTION => 1,
16             TYPE_MESSAGE => 2,
17             TYPE_BASE => 3,
18             TYPE_URA => 4,
19 5     5   376 };
  5         6  
20              
21 5     5   18 use Carp qw(confess cluck);
  5         4  
  5         237  
22 5     5   4015 use DateTime;
  5         1614772  
  5         240  
23 5     5   1811 use Encode qw(encode decode);
  5         21559  
  5         388  
24 5     5   2804 use List::MoreUtils qw(firstval none uniq);
  5         36082  
  5         28  
25 5     5   5749 use LWP::UserAgent;
  5         163070  
  5         230  
26 5     5   2962 use Text::CSV;
  5         36882  
  5         26  
27 5     5   2474 use Travel::Status::DE::URA::Result;
  5         15  
  5         36  
28 5     5   2390 use Travel::Status::DE::URA::Stop;
  5         11  
  5         22  
29              
30             sub new {
31 13     13 1 254161 my ( $class, %opt ) = @_;
32              
33 13   50     22 my %lwp_options = %{ $opt{lwp_options} // { timeout => 10 } };
  13         126  
34              
35 13         107 my $ua = LWP::UserAgent->new(%lwp_options);
36 13         11232 my $response;
37              
38 13 100 66     83 if ( not( $opt{ura_base} and $opt{ura_version} ) ) {
39 3         578 confess('ura_base and ura_version are mandatory');
40             }
41              
42             my $self = {
43             datetime => $opt{datetime}
44             // DateTime->now( time_zone => 'Europe/Berlin' ),
45             developer_mode => $opt{developer_mode},
46             ura_base => $opt{ura_base},
47             ura_version => $opt{ura_version},
48             full_routes => $opt{calculate_routes} // 0,
49             hide_past => $opt{hide_past} // 1,
50             stop => $opt{stop},
51             via => $opt{via},
52             via_id => $opt{via_id},
53             stop_id => $opt{stop_id},
54             line_id => $opt{line_id},
55             circle => $opt{circle},
56 10   66     74 post => {
      100        
      100        
57             StopAlso => 'False',
58              
59             # for easier debugging ordered in the returned order
60             ReturnList => 'stoppointname,stopid,stoppointindicator,'
61             . 'latitude,longitude,lineid,linename,'
62             . 'directionid,destinationtext,vehicleid,tripid,estimatedtime'
63             },
64             };
65              
66 10 50       21931 if ( $opt{with_messages} ) {
67 0         0 $self->{post}{ReturnList} .= ',messagetext,messagetype';
68             }
69 10 50       31 if ( $opt{with_stops} ) {
70 0         0 $self->{post}{StopAlso} = 'True';
71             }
72              
73             $self->{ura_instant_url}
74 10         36 = $self->{ura_base} . '/instant_V' . $self->{ura_version};
75              
76 10         31 bless( $self, $class );
77              
78 10         46 $ua->env_proxy;
79              
80 10 50       14793 if ( substr( $self->{ura_instant_url}, 0, 5 ) ne 'file:' ) {
81              
82             # filter by stop_id only if full_routes is not set
83 0 0 0     0 if ( not $self->{full_routes} and $self->{stop_id} ) {
84 0         0 $self->{post}{StopID} = $self->{stop_id};
85              
86             # filter for via as well to make via work
87 0 0       0 if ( defined $self->{via_id} ) {
88 0         0 $self->{post}{StopID} .= q{,} . $self->{via_id};
89             }
90             }
91              
92             # filter by line
93 0 0       0 if ( $self->{line_id} ) {
94 0         0 $self->{post}{LineID} = $self->{line_id};
95             }
96              
97             # filter for Stops in circle (lon,lat,dist)
98 0 0       0 if ( $self->{circle} ) {
99 0         0 $self->{post}{Circle} = $self->{circle};
100             }
101              
102 0         0 $response = $ua->post( $self->{ura_instant_url}, $self->{post} );
103             }
104             else {
105 10         45 $response = $ua->get( $self->{ura_instant_url} );
106             }
107              
108 10 100       218869 if ( $response->is_error ) {
109 1         11 $self->{errstr} = $response->status_line;
110 1         21 return $self;
111             }
112              
113 9         152 my $raw_str = $response->decoded_content;
114              
115 9 50       551101 if ( $self->{developer_mode} ) {
116 0         0 say decode( 'UTF-8', $raw_str );
117             }
118              
119             # Fix encoding in case we're running through test files
120 9 50       67 if ( substr( $self->{ura_instant_url}, 0, 5 ) eq 'file:' ) {
121 9         58 $raw_str = encode( 'UTF-8', $raw_str );
122             }
123 9         275338 $self->parse_raw_data($raw_str);
124              
125 9         28770 return $self;
126             }
127              
128             sub parse_raw_data {
129 9     9 0 33 my ( $self, $raw_str ) = @_;
130 9         135 my $csv = Text::CSV->new( { binary => 1 } );
131              
132 9         54903 for my $dep ( split( /\r\n/, $raw_str ) ) {
133 155673         440750 $dep =~ s{^\[}{};
134 155673         309031 $dep =~ s{\]$}{};
135              
136 155673         300460 $csv->parse($dep);
137 155673         46691030 my @fields = $csv->fields;
138              
139             # encode all fields
140 155673         990598 for my $i ( 1, 11 ) {
141 311346         3677519 $fields[$i] = encode( 'UTF-8', $fields[$i] );
142             }
143              
144 155673         2495098 push( @{ $self->{raw_list} }, \@fields );
  155673         221393  
145              
146 155673         132626 my $type = $fields[0];
147              
148 155673 100       437864 if ( $type == TYPE_STOP ) {
    50          
    100          
149 9891         7302 my $stop_name = $fields[1];
150 9891         6765 my $stop_id = $fields[2];
151 9891         7340 my $longitude = $fields[3];
152 9891         6919 my $latitude = $fields[4];
153              
154             # create Stop Dict
155 9891 50       20071 if ( not exists $self->{stops}{$stop_id} ) {
156 9891         15378 $self->{stops}{$stop_id} = Travel::Status::DE::URA::Stop->new(
157             name => decode( 'UTF-8', $stop_name ),
158             id => $stop_id,
159             longitude => $longitude,
160             latitude => $latitude,
161             );
162             }
163             }
164             elsif ( $type == TYPE_MESSAGE ) {
165             push(
166 0         0 @{ $self->{messages} },
  0         0  
167             {
168             stop_name => $fields[1],
169             stop_id => $fields[2],
170              
171             # 0 = long text. 2 = short text for station displays?
172             type => $fields[6],
173             text => $fields[7],
174             }
175             );
176             }
177             elsif ( $type == TYPE_PREDICTION ) {
178 145773         108872 push( @{ $self->{stop_names} }, $fields[1] );
  145773         246864  
179             }
180             }
181              
182 9         32607 @{ $self->{stop_names} } = uniq @{ $self->{stop_names} };
  9         23549  
  9         64337  
183              
184 9         2548 return $self;
185             }
186              
187             sub get_stop_by_name {
188 3     3 1 939 my ( $self, $name ) = @_;
189              
190 3         7 my $nname = lc($name);
191 3     1098   11 my $actual_match = firstval { $nname eq lc($_) } @{ $self->{stop_names} };
  1098         698  
  3         42  
192              
193 3 100       16 if ($actual_match) {
194 2         9 return $actual_match;
195             }
196              
197 1         3 return ( grep { $_ =~ m{$name}i } @{ $self->{stop_names} } );
  901         896  
  1         6  
198             }
199              
200             sub get_stops {
201 0     0 1 0 my ($self) = @_;
202              
203 0         0 return $self->{stops};
204             }
205              
206             sub errstr {
207 2     2 1 1673 my ($self) = @_;
208              
209 2         10 return $self->{errstr};
210             }
211              
212             sub messages_by_stop_id {
213 0     0 1 0 my ( $self, $stop_id ) = @_;
214              
215 0         0 my @messages = grep { $_->{stop_id} == $stop_id } @{ $self->{messages} };
  0         0  
  0         0  
216 0         0 @messages = map { $_->{text} } @messages;
  0         0  
217              
218 0         0 return @messages;
219             }
220              
221             sub messages_by_stop_name {
222 0     0 1 0 my ( $self, $stop_name ) = @_;
223              
224             my @messages
225 0         0 = grep { $_->{stop_name} eq $stop_name } @{ $self->{messages} };
  0         0  
  0         0  
226 0         0 @messages = map { $_->{text} } @messages;
  0         0  
227              
228 0         0 return @messages;
229             }
230              
231             sub results {
232 12     12 1 91314 my ( $self, %opt ) = @_;
233 12         26 my @results;
234              
235 12   66     115 my $full_routes = $opt{calculate_routes} // $self->{full_routes} // 0;
      50        
236 12   66     214 my $hide_past = $opt{hide_past} // $self->{hide_past} // 1;
      50        
237 12   33     61 my $line_id = $opt{line_id} // $self->{line_id};
238 12   66     65 my $stop = $opt{stop} // $self->{stop};
239 12   33     54 my $stop_id = $opt{stop_id} // $self->{stop_id};
240 12   66     43 my $via = $opt{via} // $self->{via};
241 12   33     48 my $via_id = $opt{via_id} // $self->{via_id};
242              
243 12         26 my $dt_now = $self->{datetime};
244 12         85 my $ts_now = $dt_now->epoch;
245              
246 12 100 66     184 if ( $via or $via_id ) {
247 2         5 $full_routes = 1;
248             }
249              
250 12         18 for my $dep ( @{ $self->{raw_list} } ) {
  12         56  
251              
252             my (
253             $type, $stopname, $stopid, $stopindicator,
254             $longitude, $latitude, $lineid, $linename,
255             $directionid, $dest, $vehicleid, $tripid,
256             $timestamp
257 190267         116359 ) = @{$dep};
  190267         648981  
258 190267         172402 my ( @route_pre, @route_post );
259              
260             # only work on Prediction informations
261 190267 100       242835 if ( $type != TYPE_PREDICTION ) {
262 12100         10493 next;
263             }
264              
265 178167 50 33     234646 if ( $line_id and not( $lineid eq $line_id ) ) {
266 0         0 next;
267             }
268              
269 178167 100 100     356426 if ( $stop and not( $stopname eq $stop ) ) {
270 111872         105435 next;
271             }
272              
273 66295 50 33     92721 if ( $stop_id and not( $stopid eq $stop_id ) ) {
274 0         0 next;
275             }
276              
277 66295 50       85136 if ( not $timestamp ) {
278 0         0 cluck("departure element without timestamp: $dep");
279 0         0 next;
280             }
281              
282 66295         60036 $timestamp /= 1000;
283              
284 66295 100 66     129714 if ( $hide_past and $ts_now > $timestamp ) {
285 32394         29537 next;
286             }
287              
288 33901         75879 my $dt_dep = DateTime->from_epoch(
289             epoch => $timestamp,
290             time_zone => 'Europe/Berlin'
291             );
292 33901         13882994 my $ts_dep = $dt_dep->epoch;
293              
294 33901 100       200125 if ($full_routes) {
295             my @route
296 22577         90898 = map { [ $_->[12] / 1000, $_->[1], $_->[2], $_->[4], $_->[5] ] }
297 12261129         13416202 grep { $_->[11] == $tripid }
298 757         1091 grep { $_->[0] == 1 } @{ $self->{raw_list} };
  13093829         14277684  
  757         28816  
299              
300 757         2219 @route_pre = grep { $_->[0] < $ts_dep } @route;
  22577         24972  
301 757         1454 @route_post = grep { $_->[0] > $ts_dep } @route;
  22577         20243  
302              
303 757 100 100     15211 if ( $via
304 10790     10790   9957 and none { $_->[1] eq $via } @route_post )
305             {
306 740         19963 next;
307             }
308              
309 17 50 33     114 if ( $via_id
310 0     0   0 and none { $_->[2] eq $via_id } @route_post )
311             {
312 0         0 next;
313             }
314              
315 17 50       62 if ($hide_past) {
316 0         0 @route_pre = grep { $_->[0] >= $ts_now } @route_pre;
  0         0  
317             }
318              
319 207         203 @route_pre = map { $_->[0] }
320 553         417 sort { $a->[1] <=> $b->[1] }
321 17         48 map { [ $_, $_->[0] ] } @route_pre;
  207         353  
322 497         430 @route_post = map { $_->[0] }
323 1960         1260 sort { $a->[1] <=> $b->[1] }
324 17         68 map { [ $_, $_->[0] ] } @route_post;
  497         585  
325              
326             @route_pre = map {
327 17         99 Travel::Status::DE::URA::Stop->new(
  207         637  
328             datetime => DateTime->from_epoch(
329             epoch => $_->[0],
330             time_zone => 'Europe/Berlin'
331             ),
332             name => decode( 'UTF-8', $_->[1] ),
333             id => $_->[2],
334             longitude => $_->[3],
335             latitude => $_->[4],
336             )
337             } @route_pre;
338             @route_post = map {
339 17         66 Travel::Status::DE::URA::Stop->new(
  497         1309  
340             datetime => DateTime->from_epoch(
341             epoch => $_->[0],
342             time_zone => 'Europe/Berlin'
343             ),
344             name => decode( 'UTF-8', $_->[1] ),
345             id => $_->[2],
346             longitude => $_->[3],
347             latitude => $_->[4],
348             )
349             } @route_post;
350             }
351              
352             push(
353 33161         113335 @results,
354             Travel::Status::DE::URA::Result->new(
355             datetime => $dt_dep,
356             dt_now => $dt_now,
357             line => $linename,
358             line_id => $lineid,
359             destination => $dest,
360             route_pre => [@route_pre],
361             route_post => [@route_post],
362             stop => $stopname,
363             stop_id => $stopid,
364             stop_indicator => $stopindicator,
365             )
366             );
367             }
368              
369 33161         49355 @results = map { $_->[0] }
370 397196         261086 sort { $a->[1] <=> $b->[1] }
371 12         169 map { [ $_, $_->datetime->epoch ] } @results;
  33161         242107  
372              
373 12         118630 return @results;
374             }
375              
376             # static
377             sub get_services {
378             return (
379             {
380 0     0 0   ura_base => 'http://ivu.aseag.de/interfaces/ura',
381             ura_version => 1,
382             name => 'Aachener Straßenbahn und Energieversorgungs AG',
383             shortname => 'ASEAG',
384             },
385             {
386             ura_base => 'http://ura.itcs.mvg-mainz.de/interfaces/ura',
387             ura_version => 1,
388             name => 'MVG Mainz',
389             shortname => 'MvgMainz',
390             },
391             {
392             ura_base => 'http://countdown.api.tfl.gov.uk/interfaces/ura',
393             ura_version => 1,
394             name => 'Transport for London',
395             shortname => 'TfL',
396             }
397             );
398             }
399              
400             1;
401              
402             __END__
403              
404             =head1 NAME
405              
406             Travel::Status::DE::URA - unofficial departure monitor for "Unified Realtime
407             API" data providers (e.g. ASEAG)
408              
409             =head1 SYNOPSIS
410              
411             use Travel::Status::DE::URA;
412              
413             my $status = Travel::Status::DE::URA->new(
414             ura_base => 'http://ivu.aseag.de/interfaces/ura',
415             ura_version => '1',
416             stop => 'Aachen Bushof'
417             );
418              
419             for my $d ($status->results) {
420             printf(
421             "%s %-5s %25s (in %d min)\n",
422             $d->time, $d->line, $d->destination, $d->countdown
423             );
424             }
425              
426             =head1 VERSION
427              
428             version 2.01
429              
430             =head1 DESCRIPTION
431              
432             Travel::Status::DE::URA is an unofficial interface to URA-based realtime
433             departure monitors (as used e.g. by the ASEAG). It reports all upcoming
434             departures at a given place in real-time. Schedule information is not
435             included.
436              
437             =head1 METHODS
438              
439             =over
440              
441             =item my $status = Travel::Status::DE::URA->new(I<%opt>)
442              
443             Requests the departures as specified by I<opts> and returns a new
444             Travel::Status::DE::URA object.
445              
446             The following two parameters are mandatory:
447              
448             =over
449              
450             =item B<ura_base> => I<ura_base>
451              
452             The URA base url.
453              
454             =item B<ura_version> => I<version>
455              
456             The version, may be any string.
457              
458             =back
459              
460             The request URL is I<ura_base>/instant_VI<version>, so for
461             C<< http://ivu.aseag.de/interfaces/ura >>, C<< 1 >> this module will send
462             requests to C<< http://ivu.aseag.de/interfaces/ura/instant_V1 >>.
463              
464             All remaining parameters are optional.
465              
466             =over
467              
468             =item B<lwp_options> => I<\%hashref>
469              
470             Passed on to C<< LWP::UserAgent->new >>. Defaults to C<< { timeout => 10 } >>,
471             you can use an empty hashref to override it.
472              
473             =item B<circle> => I<lon,lat,dist>
474              
475             Only request departures for stops which are located up to I<dist> meters
476             away from the location specified by I<lon> and I<lat>. Example parameter:
477             "50.78496,6.10897,100".
478              
479             =item B<with_messages> => B<0>|B<1>
480              
481             When set to B<1> (or any other true value): Also requests stop messages from
482             the URA service. Thene can include texts such as "Expect delays due to snow and
483             ice" or "stop closed, use replacement stop X instead". Use
484             C<< $status->messages >> to access them.
485              
486             =item B<with_stops> => B<0>|B<1>
487              
488             When set to B<1> (or any other true value): Also request all stops satisfying
489             the specified parameters. They can be accessed with B<get_stops>. Defaults to
490             B<0>.
491              
492             =back
493              
494             Additionally, all options supported by C<< $status->results >> may be specified
495             here, causing them to be used as defaults. Note that while they can be
496             overridden later, they may limit the set of departures requested from the
497             server.
498              
499             =item $status->errstr
500              
501             In case of an HTTP request error, returns a string describing it. If none
502             occured, returns undef.
503              
504             =item $status->get_stop_by_name(I<$stopname>)
505              
506             Returns a list of stops matching I<$stopname>. For instance, if the stops
507             "Aachen Bushof", "Eupen Bushof", "Brand" and "Brandweiher" exist, the
508             parameter "bushof" will return "Aachen Bushof" and "Eupen Bushof", while
509             "brand" will only return "Brand".
510              
511             =item $status->get_stops
512              
513             Returns a hash reference describing all distinct stops returned by the request.
514             Each key is the unique ID of a stop and contains a
515             Travel::Status::DE::URA::Stop(3pm) object describing it.
516              
517             Only works when $status was created with B<with_stops> set to a true value.
518             Otherwise, undef is returned.
519              
520             =item $status->messages_by_stop_id($stop_id)
521              
522             Returns a list of messages for the stop with the ID I<$stop_id>.
523             At the moment, each message is a simple string. This may change in the future.
524              
525             =item $status->messages_by_stop_name($stop_id)
526              
527             Returns a list of messages for the stop with the name I<$stop_name>.
528             At the moment, each message is a simple string. This may change in the future.
529              
530             =item $status->results(I<%opt>)
531              
532             Returns a list of Travel::Status::DE::URA::Result(3pm) objects, each describing
533             one departure.
534              
535             Accepted parameters (all are optional):
536              
537             =over
538              
539             =item B<calculate_routes> => I<bool> (default 0)
540              
541             When set to a true value: Compute routes for all results, enabling use of
542             their B<route_> accessors. Otherwise, those will just return nothing
543             (undef / empty list, depending on context).
544              
545             =item B<hide_past> => I<bool> (default 1)
546              
547             Do not include past departures in the result list and the computed timetables.
548              
549             =item B<line_id> => I<ID>
550              
551             Only return departures of line I<ID>.
552              
553             =item B<stop> => I<name>
554              
555             Only return departures at stop I<name>.
556              
557             =item B<stop_id> => I<ID>
558              
559             Only return departures at stop I<ID>.
560              
561             =item B<via> => I<vianame>
562              
563             Only return departures containing I<vianame> in their route after their
564             corresponding stop. Implies B<calculate_routes>=1.
565              
566             =item B<via_id> => I<ID>
567              
568             Only return departures containing I<ID> in their route after their
569             corresponding stop. Implies B<calculate_routes>=1.
570              
571             =back
572              
573             =back
574              
575             =head1 DIAGNOSTICS
576              
577             None.
578              
579             =head1 DEPENDENCIES
580              
581             =over
582              
583             =item * Class::Accessor(3pm)
584              
585             =item * DateTime(3pm)
586              
587             =item * List::MoreUtils(3pm)
588              
589             =item * LWP::UserAgent(3pm)
590              
591             =item * Text::CSV(3pm)
592              
593             =back
594              
595             =head1 BUGS AND LIMITATIONS
596              
597             Many.
598              
599             =head1 SEE ALSO
600              
601             Travel::Status::DE::URA::Result(3pm).
602              
603             =head1 AUTHOR
604              
605             Copyright (C) 2013-2016 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>
606              
607             =head1 LICENSE
608              
609             This module is licensed under the same terms as Perl itself.