File Coverage

blib/lib/Travel/Status/DE/HAFAS/Journey.pm
Criterion Covered Total %
statement 137 221 61.9
branch 28 76 36.8
condition 45 117 38.4
subroutine 11 18 61.1
pod 6 9 66.6
total 227 441 51.4


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