File Coverage

blib/lib/Travel/Status/DE/EFA/Trip.pm
Criterion Covered Total %
statement 17 98 17.3
branch 0 28 0.0
condition 0 27 0.0
subroutine 6 11 54.5
pod 4 5 80.0
total 27 169 15.9


line stmt bran cond sub pod time code
1             package Travel::Status::DE::EFA::Trip;
2              
3 2     2   17 use strict;
  2         6  
  2         117  
4 2     2   65 use warnings;
  2         5  
  2         170  
5 2     2   41 use 5.010;
  2         9  
6              
7 2     2   14 use DateTime::Format::Strptime;
  2         5  
  2         34  
8 2     2   154 use Travel::Status::DE::EFA::Stop;
  2         5  
  2         26  
9              
10 2     2   73 use parent 'Class::Accessor';
  2         5  
  2         20  
11              
12             our $VERSION = '3.19';
13              
14             Travel::Status::DE::EFA::Trip->mk_ro_accessors(
15             qw(operator product product_class name line number type id dest_name dest_id)
16             );
17              
18             sub new {
19 0     0 1   my ( $obj, %conf ) = @_;
20              
21 0   0       my $json = $conf{json}{transportation} // $conf{json}{leg}{transportation};
22              
23             #say $json->{disassembledName} . ' <-> ' . $json->{number};
24              
25             my $ref = {
26             operator => $json->{operator}{name},
27             product => $json->{product}{name},
28             product_class => $json->{product}{class},
29             polyline_raw => $conf{json}{leg}{coords},
30             name => $json->{name},
31             line => $json->{disassembledName} // $json->{number},
32             number => $json->{properties}{trainNumber},
33             type => $json->{properties}{trainType} // $json->{product}{name},
34             id => $json->{id},
35             dest_name => $json->{destination}{name},
36             dest_id => $json->{destination}{id},
37             route_raw => $json->{locationSequence}
38             // $conf{json}{leg}{stopSequence},
39 0   0       strptime_obj => DateTime::Format::Strptime->new(
      0        
      0        
40             pattern => '%Y-%m-%dT%H:%M:%SZ',
41             time_zone => 'UTC'
42             ),
43             };
44 0 0 0       if ( ref( $ref->{polyline_raw} ) eq 'ARRAY'
45 0           and @{ $ref->{polyline_raw} } == 1 )
46             {
47 0           $ref->{polyline_raw} = $ref->{polyline_raw}[0];
48             }
49 0           return bless( $ref, $obj );
50             }
51              
52             sub polyline {
53 0     0 1   my ( $self, %opt ) = @_;
54              
55 0 0         if ( $self->{polyline} ) {
56 0           return @{ $self->{polyline} };
  0            
57             }
58              
59 0 0 0       if ( not @{ $self->{polyline_raw} // [] } ) {
  0            
60 0 0         if ( $opt{fallback} ) {
61             return map {
62 0           {
63             lat => $_->{latlon}[0],
64 0           lon => $_->{latlon}[1],
65             stop => $_,
66             }
67             } $self->route;
68             }
69 0           return;
70             }
71              
72 0           $self->{polyline} = [ map { { lat => $_->[0], lon => $_->[1] } }
73 0           @{ $self->{polyline_raw} } ];
  0            
74 0           my $distance;
75              
76 0           eval {
77 0           require GIS::Distance;
78 0           $distance = GIS::Distance->new;
79             };
80              
81             # Ggf. sollte die Abbildung andersrum laufen: Im zweiten Schritt durch die
82             # Polyline iterieren und Stops zuordnen (d.h. polyline_i als Key); bei
83             # Bedarf Polyline-Indexe duplizieren. Lässt sich wunderbar an der Linie
84             # 101/106 in Essen testen (3x Helenenstr, davon 2x am Anfang und 1x
85             # mittendrin).
86              
87 0 0         if ($distance) {
88 0           my %min_dist;
89              
90             # A single trip may pass the same stop multiple times, meaning that
91             # stop IDs alone are not guaranteed to be unique. So we need to use a
92             # stop's index in the trip's route as key in addition to the stop's ID.
93 0           my $route_i = 0;
94 0           for my $stop ( $self->route ) {
95 0           for my $polyline_index ( 0 .. $#{ $self->{polyline} } ) {
  0            
96 0           my $pl = $self->{polyline}[$polyline_index];
97             my $dist = $distance->distance_metal(
98             $stop->{latlon}[0],
99             $stop->{latlon}[1],
100             $pl->{lat}, $pl->{lon}
101 0           );
102 0           my $key = $route_i . ';' . $stop->{id_code};
103 0 0 0       if ( not $min_dist{$key}
104             or $min_dist{$key}{dist} > $dist )
105             {
106 0           $min_dist{$key} = {
107             dist => $dist,
108             index => $polyline_index,
109             };
110             }
111             }
112 0           $route_i += 1;
113             }
114 0           $route_i = 0;
115 0           for my $stop ( $self->route ) {
116 0           my $key = $route_i . ';' . $stop->{id_code};
117 0 0         if ( $min_dist{$key} ) {
118 0 0         if ( defined $self->{polyline}[ $min_dist{$key}{index} ]{stop} )
119             {
120 0           warn(
121             "$key: overwriting stop ref at $min_dist{$key}{index} with $key"
122             );
123              
124             # XXX experimental and untested
125             # one polyline entry maps to multiple stops → duplicate it; insert $stop after the already-present entry
126             #$min_dist{$key}{index} += 1;
127             #splice(
128             # @{ $self->{polyline} },
129             # $min_dist{$key}{index},
130             # 0, { %{ $self->{polyline}[ $min_dist{$key}{index} ] } }
131             #);
132             }
133             $self->{polyline}[ $min_dist{$key}{index} ]{stop}
134 0           = $stop;
135             }
136 0           $route_i += 1;
137             }
138             }
139              
140 0           return @{ $self->{polyline} };
  0            
141             }
142              
143             sub parse_dt {
144 0     0 0   my ( $self, $value ) = @_;
145              
146 0 0         if ($value) {
147 0           my $dt = $self->{strptime_obj}->parse_datetime($value);
148 0 0         if ($dt) {
149 0           return $dt->set_time_zone('Europe/Berlin');
150             }
151             }
152 0           return undef;
153             }
154              
155             sub route {
156 0     0 1   my ($self) = @_;
157              
158 0 0         if ( $self->{route} ) {
159 0           return @{ $self->{route} };
  0            
160             }
161              
162 0   0       for my $stop ( @{ $self->{route_raw} // [] } ) {
  0            
163 0           my $chain = $stop;
164 0           my ( $platform, $place, $name, $name_full, $id_num, $id_code );
165 0           while ( $chain->{type} ) {
166 0 0         if ( $chain->{type} eq 'platform' ) {
    0          
    0          
167             $platform = $chain->{properties}{platformName}
168 0   0       // $chain->{properties}{platform};
169             }
170             elsif ( $chain->{type} eq 'stop' ) {
171 0           $name = $chain->{disassembledName};
172 0           $name_full = $chain->{name};
173 0           $id_code = $chain->{id};
174 0           $id_num = $chain->{properties}{stopId};
175             }
176             elsif ( $chain->{type} eq 'locality' ) {
177 0           $place = $chain->{name};
178             }
179 0           $chain = $chain->{parent};
180             }
181             push(
182 0           @{ $self->{route} },
183             Travel::Status::DE::EFA::Stop->new(
184             sched_arr => $self->parse_dt( $stop->{arrivalTimePlanned} ),
185             sched_dep => $self->parse_dt( $stop->{departureTimePlanned} ),
186             rt_arr => $self->parse_dt( $stop->{arrivalTimeEstimated} ),
187             rt_dep => $self->parse_dt( $stop->{departureTimeEstimated} ),
188             occupancy => $stop->{properties}{occupancy},
189             is_cancelled => $stop->{isCancelled},
190             latlon => $stop->{coord},
191             full_name => $name_full,
192             name => $name,
193             place => $place,
194             niveau => $stop->{niveau},
195 0           platform => $platform,
196             id_code => $id_code,
197             id_num => $id_num,
198             )
199             );
200             }
201              
202 0           delete $self->{route_raw};
203              
204 0   0       return @{ $self->{route} // [] };
  0            
205             }
206              
207             sub TO_JSON {
208 0     0 1   my ($self) = @_;
209              
210             # lazy loading
211 0           $self->route;
212              
213             # lazy loading
214 0           $self->polyline;
215              
216 0           my $ret = { %{$self} };
  0            
217              
218 0           delete $ret->{strptime_obj};
219              
220 0           return $ret;
221             }
222              
223             1;
224              
225             __END__
226              
227             =head1 NAME
228              
229             Travel::Status::DE::EFA::Trip - Information about an individual public transit
230             trip
231              
232             =head1 SYNOPSIS
233              
234             printf( "%s %s -> %s\n", $trip->type, $trip->line // q{}, $trip->dest_name );
235             for my $stop ( $trip->route ) {
236             ...;
237             }
238              
239             =head1 VERSION
240              
241             version 3.19
242              
243             =head1 DESCRIPTION
244              
245             Travel::Status::DE::EFA::Trip describes a single trip / journey of a public
246             transport line.
247              
248             =head1 METHODS
249              
250             =head2 ACCESSORS
251              
252             Most accessors return undef if the corresponding data is not available.
253              
254             =over
255              
256             =item $trip->operator
257              
258             Operator name.
259              
260             =item $trip->product
261              
262             Product name.
263              
264             =item $trip->product_class
265              
266             Product class.
267              
268             =item $trip->name
269              
270             Trip or line name.
271              
272             =item $trip->line
273              
274             Line identifier. Note that this is not necessarily numeric.
275              
276             =item $trip->number
277              
278             Trip/journey number.
279              
280             =item $trip->type
281              
282             Transport / vehicle type, e.g. "RE" or "Bus".
283              
284             =item $trip->id
285              
286             Unique(?) trip ID
287              
288             =item $trip->dest_name
289              
290             Name of the trip's destination stop
291              
292             =item $trip->dest_id
293              
294             ID of the trip's destination stop
295              
296             =item $trip->route
297              
298             List of Travel::Status::DE::EFA::Stop(3pm) objects describing the route of this
299             trip.
300              
301             Note: The EFA API requires a stop to be specified when requesting trip details.
302             The stops returned by this accessor appear to be limited to stops after the
303             requested stop; earlier ones may be missing.
304              
305             =item $journey->polyline(I<%opt>)
306              
307             List of geocoordinates that describe the trips's route.
308             Each list entry is a hash with the following keys.
309              
310             =over
311              
312             =item * lon (longitude)
313              
314             =item * lat (latitude)
315              
316             =item * stop (Stop object for this location, if any. undef otherwise)
317              
318             =back
319              
320             Note that stop is not provided by the backend and instead inferred by this
321             module.
322              
323             If the backend does not provide geocoordinates and this accessor was called
324             with B< fallback > set to a true value, it returns the list of stop coordinates
325             instead. Otherwise, it returns an empty list.
326              
327             =back
328              
329             =head2 INTERNAL
330              
331             =over
332              
333             =item $trip = Travel::Status::DE::EFA::Trip->new(I<%data>)
334              
335             Returns a new Travel::Status::DE::EFA::Trip object. You should not need to
336             call this.
337              
338             =item $trip->TO_JSON
339              
340             Allows the object data to be serialized to JSON.
341              
342             =back
343              
344             =head1 DIAGNOSTICS
345              
346             None.
347              
348             =head1 DEPENDENCIES
349              
350             =over
351              
352             =item Class::Accessor(3pm)
353              
354             =item DateTime::Format::Strptime(3pm)
355              
356             =item Travel::Status::DE::EFA::Stop(3pm)
357              
358             =back
359              
360             =head1 BUGS AND LIMITATIONS
361              
362             This module is a Work in Progress.
363             Its API may change between minor versions.
364              
365             =head1 SEE ALSO
366              
367             Travel::Status::DE::EFA(3pm).
368              
369             =head1 AUTHOR
370              
371             Copyright (C) 2024-2025 Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
372              
373             =head1 LICENSE
374              
375             This module is licensed under the same terms as Perl itself.