File Coverage

blib/lib/Travel/Status/DE/HAFAS/Journey.pm
Criterion Covered Total %
statement 137 194 70.6
branch 44 82 53.6
condition 42 104 40.3
subroutine 13 18 72.2
pod 8 10 80.0
total 244 408 59.8


line stmt bran cond sub pod time code
1             package Travel::Status::DE::HAFAS::Journey;
2              
3             # vim:foldmethod=marker
4              
5 5     5   34 use strict;
  5         16  
  5         190  
6 5     5   24 use warnings;
  5         12  
  5         746  
7 5     5   94 use 5.014;
  5         17  
8              
9 5     5   23 use parent 'Class::Accessor';
  5         19  
  5         46  
10 5     5   12752 use DateTime::Format::Strptime;
  5         11  
  5         82  
11 5     5   376 use List::Util qw(any uniq);
  5         11  
  5         455  
12 5     5   2669 use Travel::Status::DE::HAFAS::Stop;
  5         18  
  5         30  
13              
14             our $VERSION = '6.25';
15              
16             Travel::Status::DE::HAFAS::Journey->mk_ro_accessors(
17             qw(datetime sched_datetime rt_datetime tz_offset
18             is_additional is_cancelled is_partially_cancelled
19             station station_eva platform sched_platform rt_platform operator
20             product
21             id name type type_long class number line line_no load delay
22             route_end route_start origin destination direction)
23             );
24              
25             # {{{ Constructor
26              
27             sub new {
28 34     34 1 167 my ( $obj, %opt ) = @_;
29              
30 34   50     53 my @icoL = @{ $opt{common}{icoL} // [] };
  34         141  
31 34   100     56 my @tcocL = @{ $opt{common}{tcocL} // [] };
  34         181  
32 34   50     52 my @remL = @{ $opt{common}{remL} // [] };
  34         99  
33 34   50     46 my @himL = @{ $opt{common}{himL} // [] };
  34         129  
34              
35 34         65 my $prodL = $opt{prodL};
36 34         66 my $locL = $opt{locL};
37 34         70 my $hafas = $opt{hafas};
38 34         63 my $journey = $opt{journey};
39              
40 34   33     148 my $date = $opt{date} // $journey->{date};
41              
42 34         85 my $direction = $journey->{dirTxt};
43 34         71 my $jid = $journey->{jid};
44              
45 34         57 my $is_cancelled = $journey->{isCncl};
46 34         62 my $partially_cancelled = $journey->{isPartCncl};
47              
48 34         68 my $product = $prodL->[ $journey->{prodX} ];
49              
50 34         58 my @messages;
51 34   100     50 for my $msg ( @{ $journey->{msgL} // [] } ) {
  34         127  
52 25 50 33     127 if ( $msg->{type} eq 'REM' and defined $msg->{remX} ) {
    0 0        
53 25         115 push( @messages, $hafas->add_message( $remL[ $msg->{remX} ] ) );
54             }
55             elsif ( $msg->{type} eq 'HIM' and defined $msg->{himX} ) {
56 0         0 push( @messages, $hafas->add_message( $himL[ $msg->{himX} ], 1 ) );
57             }
58             else {
59 0         0 say "Unknown message type $msg->{type}";
60             }
61             }
62              
63 34         55 my $datetime_ref;
64              
65 34 50 50     62 if ( @{ $journey->{stopL} // [] } or $journey->{stbStop} ) {
  34   33     230  
66 34         104 my ( $date_ref, $parse_fmt );
67 34 50       146 if ( $jid =~ /#/ ) {
68              
69             # modern trip ID, used e.g. by DB and ÖBB
70 0         0 $date_ref = ( split( /#/, $jid ) )[12];
71 0         0 $parse_fmt = '%d%m%y';
72 0 0       0 if ( length($date_ref) < 5 ) {
    0          
73 0         0 warn(
74             "HAFAS, not even once -- midnight crossing may be bogus -- date_ref $date_ref"
75             );
76             }
77             elsif ( length($date_ref) == 5 ) {
78 0         0 $date_ref = "0${date_ref}";
79             }
80             }
81             else {
82             # old (legacy?) trip ID
83 34         302 $date_ref = ( split( qr{[|]}, $jid ) )[4];
84 34         133 $parse_fmt = '%d%m%Y';
85 34 50       144 if ( length($date_ref) < 7 ) {
    100          
86 0         0 warn(
87             "HAFAS, not even once -- midnight crossing may be bogus -- date_ref $date_ref"
88             );
89             }
90             elsif ( length($date_ref) == 7 ) {
91 30         55 $date_ref = "0${date_ref}";
92             }
93             }
94             $datetime_ref = DateTime::Format::Strptime->new(
95             pattern => $parse_fmt,
96             time_zone => $hafas->get_active_service->{time_zone}
97 34   50     170 // 'Europe/Berlin'
98             )->parse_datetime($date_ref);
99             }
100              
101 34         79738 my @stops;
102             my $route_end;
103 34   50     73 for my $stop ( @{ $journey->{stopL} // [] } ) {
  34         158  
104 438         3981 my $loc = $locL->[ $stop->{locX} ];
105              
106             my $stopref = {
107             loc => $loc,
108             stop => $stop,
109             common => $opt{common},
110 438         1393 prodL => $prodL,
111             hafas => $hafas,
112             date => $date,
113             datetime_ref => $datetime_ref,
114             };
115              
116 438         684 push( @stops, $stopref );
117              
118 438         866 $route_end = $loc->name;
119             }
120              
121 34 100       345 if ( $journey->{stbStop} ) {
122 30 50       75 if ( $hafas->{arrivals} ) {
123 0         0 $route_end = $stops[0]->{name};
124 0         0 pop(@stops);
125             }
126             else {
127 30         43 shift(@stops);
128             }
129             }
130              
131 34   33     150 my $ref = {
132             id => $jid,
133             product => $product,
134             name => $product->name,
135             number => $product->number,
136             line => $product->name,
137             line_no => $product->line_no,
138             type => $product->type,
139             type_long => $product->type_long,
140             class => $product->class,
141             operator => $product->operator,
142             direction => $direction,
143             is_cancelled => $is_cancelled,
144             is_partially_cancelled => $partially_cancelled,
145             route_end => $route_end // $direction,
146             messages => \@messages,
147             route => \@stops,
148             };
149              
150 34 100 33     2339 if ( $journey->{stbStop} ) {
    50          
151 30 50       61 if ( $hafas->{arrivals} ) {
152 0         0 $ref->{origin} = $ref->{route_end};
153 0   0     0 $ref->{is_cancelled} ||= $journey->{stbStop}{aCncl};
154             }
155             else {
156 30         62 $ref->{destination} = $ref->{route_end};
157 30   66     173 $ref->{is_cancelled} ||= $journey->{stbStop}{dCncl};
158             }
159 30         103 $ref->{is_additional} = $journey->{stbStop}{isAdd};
160             }
161             elsif ( $stops[0] and $stops[0]{loc} ) {
162 4         16 $ref->{route_start} = $stops[0]{loc}->name;
163             }
164              
165 34         136 bless( $ref, $obj );
166              
167 34 100       81 if ( $journey->{stbStop} ) {
168 30         81 $ref->{station} = $locL->[ $journey->{stbStop}{locX} ]->name;
169 30         313 $ref->{station_eva} = 0 + $locL->[ $journey->{stbStop}{locX} ]->eva;
170             $ref->{sched_platform} = $journey->{stbStop}{dPlatfS}
171 30   66     344 // $journey->{stbStop}{dPltfS}{txt};
172             $ref->{rt_platform} = $journey->{stbStop}{dPlatfR}
173 30   33     157 // $journey->{stbStop}{dPltfR}{txt};
174 30   66     139 $ref->{platform} = $ref->{rt_platform} // $ref->{sched_platform};
175              
176             my $datetime_s = Travel::Status::DE::HAFAS::Stop::handle_day_change(
177             $ref,
178             input =>
179             $journey->{stbStop}{ $hafas->{arrivals} ? 'aTimeS' : 'dTimeS' },
180             offset => $journey->{stbStop}{
181             $hafas->{arrivals}
182             ? 'aTZOffset'
183             : 'dTZOffset'
184             },
185             date => $date,
186             strp_obj => $hafas->{strptime_obj},
187 30 50       204 ref => $datetime_ref,
    50          
188             );
189              
190             my $datetime_r = Travel::Status::DE::HAFAS::Stop::handle_day_change(
191             $ref,
192             input =>
193             $journey->{stbStop}{ $hafas->{arrivals} ? 'aTimeR' : 'dTimeR' },
194             offset => $journey->{stbStop}{
195             $hafas->{arrivals}
196             ? 'aTZOffset'
197             : 'dTZOffset'
198             },
199             date => $date,
200             strp_obj => $hafas->{strptime_obj},
201 30 50       179 ref => $datetime_ref,
    50          
202             );
203              
204 30 100       95 my $delay
205             = $datetime_r
206             ? ( $datetime_r->epoch - $datetime_s->epoch ) / 60
207             : undef;
208              
209 30         508 $ref->{sched_datetime} = $datetime_s;
210 30         47 $ref->{rt_datetime} = $datetime_r;
211 30   66     74 $ref->{datetime} = $datetime_r // $datetime_s;
212 30         66 $ref->{delay} = $delay;
213              
214 30 100       56 if ( $ref->{delay} ) {
215 2         5 $ref->{datetime} = $ref->{rt_datetime};
216             }
217             else {
218 28         52 $ref->{datetime} = $ref->{sched_datetime};
219             }
220              
221 30         46 my %tco;
222 30   50     37 for my $tco_id ( @{ $journey->{stbStop}{dTrnCmpSX}{tcocX} // [] } ) {
  30         173  
223 0         0 my $tco_kv = $tcocL[$tco_id];
224              
225             # BVG has rRT (real-time?) and r (prognosed?); others only have r
226 0   0     0 my $load = $tco_kv->{rRT} // $tco_kv->{r};
227              
228             # BVG uses 11 .. 13 rather than 1 .. 4
229 0 0 0     0 if ( defined $load and $load > 10 ) {
230 0         0 $load -= 10;
231             }
232              
233 0         0 $tco{ $tco_kv->{c} } = $load;
234             }
235 30 50       88 if (%tco) {
236 0         0 $ref->{load} = \%tco;
237             }
238             }
239 34 100       79 if ( $opt{polyline} ) {
240 3         21 $ref->{polyline} = $opt{polyline};
241             }
242              
243 34         287 return $ref;
244             }
245              
246             # }}}
247              
248             # {{{ Accessors
249              
250             # Legacy
251             sub station_uic {
252 0     0 0 0 my ($self) = @_;
253 0         0 return $self->{station_eva};
254             }
255              
256             sub is_changed_platform {
257 5     5 1 20385 my ($self) = @_;
258              
259 5 50 33     34 if ( defined $self->{rt_platform} and defined $self->{sched_platform} ) {
260 0 0       0 if ( $self->{rt_platform} ne $self->{sched_platform} ) {
261 0         0 return 1;
262             }
263 0         0 return 0;
264             }
265 5 50       24 if ( defined $self->{rt_platform} ) {
266 0         0 return 1;
267             }
268              
269 5         26 return 0;
270             }
271              
272             sub messages {
273 2     2 1 2841 my ($self) = @_;
274              
275 2 50       9 if ( $self->{messages} ) {
276 2         4 return @{ $self->{messages} };
  2         12  
277             }
278 0         0 return;
279             }
280              
281             sub operators {
282 0     0 1 0 my ($self) = @_;
283              
284 0 0       0 if ( $self->{operators} ) {
285 0         0 return @{ $self->{operators} };
  0         0  
286             }
287              
288             $self->{operators} = [
289 0   0     0 uniq map { ( $_->prod_arr // $_->prod_dep )->operator } grep {
290 0 0 0     0 ( $_->prod_arr or $_->prod_dep )
  0   0     0  
291             and ( $_->prod_arr // $_->prod_dep )->operator
292             } $self->route
293             ];
294              
295 0         0 return @{ $self->{operators} };
  0         0  
296             }
297              
298             sub polyline {
299 0     0 1 0 my ($self) = @_;
300              
301 0 0       0 if ( $self->{polyline} ) {
302 0         0 return @{ $self->{polyline} };
  0         0  
303             }
304 0         0 return;
305             }
306              
307             sub route {
308 215     215 1 229913 my ($self) = @_;
309              
310 215 50 33     930 if ( $self->{route} and @{ $self->{route} } ) {
  215         1100  
311 215 100 66     1140 if ( $self->{route}[0] and $self->{route}[0]{stop} ) {
312             $self->{route}
313 85         228 = [ map { Travel::Status::DE::HAFAS::Stop->new( %{$_} ) }
  85         606  
314 7         20 @{ $self->{route} } ];
  7         25  
315             }
316 215         366 return @{ $self->{route} };
  215         1864  
317             }
318 0         0 return;
319             }
320              
321             sub route_interesting {
322 18     18 1 58078 my ( $self, $max_parts ) = @_;
323              
324 18         91 my @via = $self->route;
325 18         43 my ( @via_main, @via_show, $last_stop );
326 18   50     106 $max_parts //= 3;
327              
328             # Centraal: dutch main station (Hbf in .nl)
329             # HB: swiss main station (Hbf in .ch)
330             # hl.n.: czech main station (Hbf in .cz)
331 18         48 for my $stop (@via) {
332 232 100       3740 if ( $stop->loc->name
333             =~ m{ HB $ | hl\.n\. $ | Hbf | Hauptbahnhof | Bf | Bahnhof | Centraal | Flughafen }x
334             )
335             {
336 84         2061 push( @via_main, $stop );
337             }
338             }
339 18         285 $last_stop = pop(@via);
340              
341 18 100 100     100 if ( @via_main and $via_main[-1]->loc->name eq $last_stop->loc->name ) {
342 6         324 pop(@via_main);
343             }
344 18 50 33     246 if ( @via and $via[-1]->loc->name eq $last_stop->loc->name ) {
345 0         0 pop(@via);
346             }
347              
348 18 100 66     800 if ( @via_main and @via and $via[0]->loc->name eq $via_main[0]->loc->name )
      100        
349             {
350 6         198 shift(@via_main);
351             }
352              
353 18 100       196 if ( @via < $max_parts ) {
354 2         4 @via_show = @via;
355             }
356             else {
357 16 100       65 if ( @via_main >= $max_parts ) {
358 4         12 @via_show = ( $via[0] );
359             }
360             else {
361 12         44 @via_show = splice( @via, 0, $max_parts - @via_main );
362             }
363              
364 16   66     99 while ( @via_show < $max_parts and @via_main ) {
365 12         28 my $stop = shift(@via_main);
366 12 50 33 20   105 if ( any { $_->loc->name eq $stop->loc->name } @via_show
  20         310  
367             or $stop->loc->name eq $last_stop->loc->name )
368             {
369 0         0 next;
370             }
371 12         1125 push( @via_show, $stop );
372             }
373             }
374              
375 18         134 return @via_show;
376              
377             }
378              
379             sub product_at {
380 0     0 1   my ( $self, $req_stop ) = @_;
381 0           for my $stop ( $self->route ) {
382 0 0 0       if ( $stop->loc->name eq $req_stop or $stop->loc->eva eq $req_stop ) {
383 0   0       return $stop->prod_dep // $stop->prod_arr;
384             }
385             }
386 0           return;
387             }
388              
389             sub TO_JSON {
390 0     0 0   my ($self) = @_;
391              
392 0           my $ret = { %{$self} };
  0            
393              
394 0           for my $k ( keys %{$ret} ) {
  0            
395 0 0         if ( ref( $ret->{$k} ) eq 'DateTime' ) {
396 0           $ret->{$k} = $ret->{$k}->epoch;
397             }
398             }
399 0           $ret->{route} = [ map { $_->TO_JSON } $self->route ];
  0            
400              
401 0           return $ret;
402             }
403              
404             # }}}
405              
406             1;
407              
408             __END__
409              
410             =head1 NAME
411              
412             Travel::Status::DE::HAFAS::Journey - Information about a single
413             journey received by Travel::Status::DE::HAFAS
414              
415             =head1 SYNOPSIS
416              
417             for my $departure ($status->results) {
418             printf(
419             "At %s: %s to %s from platform %s\n",
420             $departure->datetime->strftime('%H:%M'),
421             $departure->line,
422             $departure->destination,
423             $departure->platform,
424             );
425             }
426              
427             # or (depending on module setup)
428             for my $arrival ($status->results) {
429             printf(
430             "At %s: %s from %s on platform %s\n",
431             $arrival->datetime->strftime('%H:%M'),
432             $arrival->line,
433             $arrival->origin,
434             $arrival->platform,
435             );
436             }
437              
438             =head1 VERSION
439              
440             version 6.25
441              
442             =head1 DESCRIPTION
443              
444             Travel::Status::DE::HAFAS::Journey describes a single journey. It is either
445             a station-specific arrival/departure obtained by a stationboard query, or a
446             train journey that does not belong to a specific station.
447              
448             stationboard-specific accessors are annotated with "(station only)" and return
449             undef for non-station journeys. All date and time entries refer to the
450             backend time zone (Europe/Berlin in most cases) and do not take local time
451             into account; see B<tz_offset> for the latter.
452              
453             =head1 METHODS
454              
455             =head2 ACCESSORS
456              
457             =over
458              
459             =item $journey->name
460              
461             Journey or line name, either in a format like "Bus SB16" (Bus line
462             SB16) or "RE 10111" (RegionalExpress train 10111, no line information). May
463             contain extraneous whitespace characters.
464              
465             =item $journey->type
466              
467             Type of this journey, e.g. "S" for S-Bahn, "RE" for Regional Express
468             or "STR" for tram / StraE<szlig>enbahn.
469              
470             =item $journey->type_long
471              
472             Long type of this journey, e.g. "S-Bahn" or "Regional-Express".
473              
474             =item $journey->class
475              
476             An integer identifying the the mode of transport class.
477             Semantics depend on backend, e.g. "1" and "2" for long-distance trains and
478             "4" and "8" for regional trains.
479              
480             =item $journey->line
481              
482             Journey or line name, either in a format like "Bus SB16" (Bus line
483             SB16), "RE 42" (RegionalExpress train 42) or "IC 2901" (InterCity train 2901,
484             no line information). May contain extraneous whitespace characters. Note that
485             this accessor does not return line information for IC/ICE/EC services, even if
486             it is available. Use B<line_no> for those.
487              
488             =item $journey->line_no
489              
490             Line identifier, or undef if it is unknown.
491             The line identifier may be a single number such as "11" (underground train
492             line U 11), a single word (e.g. "AIR") or a combination (e.g. "SB16").
493             May also provide line numbers of IC/ICE services.
494              
495             =item $journey->number
496              
497             Journey number (e.g. train number), or undef if it is unknown.
498              
499             =item $journey->id
500              
501             HAFAS-internal journey ID.
502              
503             =item $journey->rt_datetime (station only)
504              
505             DateTime object indicating the actual arrival/departure date and time.
506             undef if no real-time data is available.
507              
508             =item $journey->sched_datetime (station only)
509              
510             DateTime object indicating the scheduled arrival/departure date and time.
511             undef if no schedule data is available.
512              
513             =item $journey->datetime (station only)
514              
515             DateTime object indicating the arrival/departure date and time.
516             Real-time data if available, schedule data otherwise.
517             undef if neither is available.
518              
519             =item $journey->tz_offset
520              
521             Offset between backend time zone (default: Europe/Berlin) and this journey's
522             time zone in minutes, if any. For instance, if the backend uses UTC+2 (CEST)
523             and the journey uses UTC+1 (IST), tz_offset is -60. Returns undef if both use
524             the same time zone (or rather, the same UTC offset).
525              
526             =item $journey->delay (station only)
527              
528             Delay in minutes, or undef if it is unknown.
529             Also returns undef if the arrival/departure has been cancelled.
530              
531             =item $journey->is_additional (station only)
532              
533             True if the journey's stop at the requested station is an unscheduled addition
534             to its route.
535              
536             =item $journey->is_cancelled
537              
538             True if the journey was cancelled, false otherwise.
539              
540             =item $journey->is_partially_cancelled
541              
542             True if part of the journey was cancelled, false otherwise.
543              
544             =item $journey->product
545              
546             Travel::Status::DE::HAFAS::Product(3pm) instance describing the product (mode
547             of transport, line number / ID, operator, ...) associated with this journey.
548             Note that journeys may be associated with multiple products -- see also
549             C<< $journey->route >> and C<< $stop->product >>.
550              
551             =item $journey->product_at(I<stop>)
552              
553             Travel::Status::DE::HAFAS::Product(3pm) instance describing the product
554             associated with I<stop> (name or EVA ID). Returns undef if product or I<stop>
555             are unknown.
556              
557             =item $journey->rt_platform (station only)
558              
559             Actual arrival/departure platform.
560             undef if no real-time data is available.
561              
562             =item $journey->sched_platform (station only)
563              
564             Scheduled arrival/departure platform.
565             undef if no scheduled platform is available.
566              
567             =item $journey->platform (station only)
568              
569             Arrival/Departure platform. Real-time data if available, schedule data
570             otherwise. May be undef.
571              
572             =item $journey->is_changed_platform (station only)
573              
574             True if the real-time platform is known and it is not the scheduled one.
575              
576             =item $journey->load (station only)
577              
578             Expected passenger load (i.e., how full the vehicle is) at the requested stop.
579             If known, returns a hashref that maps classes (typically FIRST/SECOND) to
580             load estimation numbers. The DB backend uses 1 (low to medium), 2 (high),
581             3 (very high), and 4 (exceptionally high, train is booked out).
582             Undef if unknown.
583              
584             =item $journey->messages
585              
586             List of Travel::Status::DE::HAFAS::Message(3pm) instances related to this
587             journey. Messages usually are service notices (e.g. "missing carriage") or
588             detailed delay reasons (e.g. "switch damage between X and Y, expect delays").
589              
590             =item $journey->operator
591              
592             The operator responsible for this journey. Returns undef
593             if the backend does not provide an operator. Note that the operator may
594             change along the journey -- in this case, the returned operator depends on
595             the backend and appears to be the first one in most cases.
596              
597             =item $journey->operators
598              
599             List of all operators observed along the journey.
600              
601             =item $journey->station (station only)
602              
603             Name of the station at which this journey was requested.
604              
605             =item $journey->station_eva (station only)
606              
607             UIC/EVA ID of the station at which this journey was requested.
608              
609             =item $journey->route
610              
611             List of Travel::Status::DE::HAFAS::Stop(3pm) objects that describe individual
612             stops along the journey. In stationboard mode, the list only contains arrivals
613             prior to the requested station or departures after the requested station. In
614             journey mode, it contains the entire route.
615              
616             =item $journey->route_interesting([I<count>])
617              
618             Up to I<count> (default: B<3>) parts of C<< $journey->route >> that may
619             be particularly helpful, e.g. main stations or airports.
620              
621             =item $journey->route_end
622              
623             Name of the last route station. In arrival mode, this is where the train
624             started; in all other cases, it is the terminus.
625              
626             =item $journey->destination
627              
628             Alias for route_end; only set when requesting departures in station mode.
629              
630             =item $journey->origin
631              
632             Alias for route_end; only set when requesting arrivals in station mode.
633              
634             =item $journey->direction
635              
636             Train direction; this is typically the text printed on the train itself.
637             May be different from destination / route_end and may change along the route,
638             see above.
639              
640             =item $journey->polyline (journey only)
641              
642             List of geocoordinates that describe the train's route. Only available if the
643             HAFAS object constructor was passed a true B<with_polyline> value. Each list
644             entry is a hash with the following keys.
645              
646             =over
647              
648             =item * lon (longitude)
649              
650             =item * lat (latitude)
651              
652             =item * name (name of stop at this location, if any. undef otherwise)
653              
654             =item * eva (EVA ID of stop at this location, if any. undef otherwise)
655              
656             =back
657              
658             Note that stop locations in B<polyline> may differ from the coordinates
659             returned in B<route>. This is a backend issue; Travel::Status::DE::HAFAS
660             simply passes the returned coordinates on.
661              
662             =back
663              
664             =head1 DIAGNOSTICS
665              
666             None.
667              
668             =head1 DEPENDENCIES
669              
670             =over
671              
672             =item Class::Accessor(3pm)
673              
674             =back
675              
676             =head1 BUGS AND LIMITATIONS
677              
678             None known.
679              
680             =head1 SEE ALSO
681              
682             Travel::Status::DE::HAFAS(3pm).
683              
684             =head1 AUTHOR
685              
686             Copyright (C) 2015-2023 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
687              
688             =head1 LICENSE
689              
690             This module is licensed under the same terms as Perl itself.