diff options
author | Birte Kristina Friesel <derf@finalrewind.org> | 2025-01-29 21:58:16 +0100 |
---|---|---|
committer | Birte Kristina Friesel <derf@finalrewind.org> | 2025-01-29 21:58:16 +0100 |
commit | a5b392d6866557bc0dffc948d804f21a7543aa21 (patch) | |
tree | 81936710b9d5cfb555eb20c8e52e01f1c6f2e41b | |
parent | 9392e11dcb7843b3ce04e9014864e0486dba6cef (diff) |
-rwxr-xr-x | bin/dbris-m | 115 | ||||
-rw-r--r-- | lib/Travel/Status/DE/DBRIS.pm | 30 | ||||
-rw-r--r-- | lib/Travel/Status/DE/DBRIS/Formation.pm | 175 | ||||
-rw-r--r-- | lib/Travel/Status/DE/DBRIS/Formation/Carriage.pm | 216 | ||||
-rw-r--r-- | lib/Travel/Status/DE/DBRIS/Formation/Group.pm | 689 | ||||
-rw-r--r-- | lib/Travel/Status/DE/DBRIS/Formation/Sector.pm | 52 |
6 files changed, 1276 insertions, 1 deletions
diff --git a/bin/dbris-m b/bin/dbris-m index 545d696..ef3325b 100755 --- a/bin/dbris-m +++ b/bin/dbris-m @@ -10,7 +10,7 @@ use DateTime; use Encode qw(decode); use JSON; use Getopt::Long qw(:config no_ignore_case); -use List::Util qw(max); +use List::Util qw(min max); use Travel::Status::DE::DBRIS; my ( $date, $time ); @@ -32,6 +32,11 @@ for my $arg (@ARGV) { my $output_bold = -t STDOUT ? "\033[1m" : q{}; my $output_reset = -t STDOUT ? "\033[0m" : q{}; +my $cf_first = "\e[38;5;11m"; +my $cf_mixed = "\e[38;5;208m"; +my $cf_second = "\e[0m"; #"\e[38;5;9m"; +my $cf_reset = "\e[0m"; + GetOptions( 'd|date=s' => \$date, 'h|help' => sub { show_help(0) }, @@ -430,6 +435,114 @@ elsif ( $opt{locationSearch} ) { } } +if ($train_no) { + $status = Travel::Status::DE::DBRIS->new( + cache => $cache, + developer_mode => $developer_mode, + formation => $train_no + ); + + my $wr = $status->result; + + printf( + "\n%s → %s\n", + join( ' / ', + map { $wr->train_type . ' ' . $_->{name} } $wr->train_numbers ), + join( + ' / ', + map { + sprintf( '%s (%s)', + $_->{name}, join( q{}, @{ $_->{sectors} } ) ) + } $wr->destinations + ), + ); + + printf( "Gleis %s\n\n", $wr->platform ); + + for my $sector ( $wr->sectors ) { + my $sector_length = $sector->length_percent; + my $spacing_left = int( ( $sector_length - 2 ) / 2 ) - 1; + my $spacing_right = int( ( $sector_length - 2 ) / 2 ); + + if ( $sector_length % 2 ) { + $spacing_left++; + } + + printf( "▏%s%s%s▕", + ( $spacing_left >= 0 ) ? ' ' x $spacing_left : q{}, + $sector->name, + ( $spacing_right >= 0 ) ? ' ' x $spacing_right : q{} ); + } + print "\n"; + + my @start_percentages = map { $_->start_percent } $wr->carriages; + if ( my $min_percentage = min @start_percentages ) { + print ' ' x ( $min_percentage - 1 ); + } + print $wr->direction == 100 ? '>' : '<'; + + for my $wagon ( $wr->carriages ) { + my $wagon_length = $wagon->length_percent; + my $spacing_left = int( $wagon_length / 2 ) - 2; + my $spacing_right = int( $wagon_length / 2 ) - 1; + + if ( $wagon_length % 2 ) { + $spacing_left++; + } + + my $wagon_desc = $wagon->number || '?'; + + if ( $wagon->is_closed ) { + $wagon_desc = 'X'; + } + + if ( $wagon->is_locomotive or $wagon->is_powercar ) { + $wagon_desc = ' ■ '; + } + + my $class_color = ''; + if ( $wagon->class_type == 1 ) { + $class_color = $cf_first; + } + elsif ( $wagon->class_type == 2 ) { + $class_color = $cf_second; + } + elsif ( $wagon->class_type == 12 ) { + $class_color = $cf_mixed; + } + + printf( "%s%s%3s%s%s", + ' ' x $spacing_left, $class_color, $wagon_desc, + $cf_reset, ' ' x $spacing_right ); + } + print $wr->direction == 100 ? '>' : '<'; + print "\n\n"; + + for my $group ( $wr->groups ) { + printf( "%s%s%s\n", + $group->description || 'Zug', + $group->designation ? ' „' . $group->designation . '“' : q{}, + $group->has_sectors + ? ' (' . join( q{}, $group->sectors ) . ')' + : q{} ); + printf( "%s %s → %s\n\n", + $group->train_type, $group->train_no, $group->destination ); + + for my $wagon ( $group->carriages ) { + printf( + "%3s: %3s %10s %s\n", + $wagon->is_closed ? 'X' + : $wagon->is_locomotive ? 'Lok' + : $wagon->number || '?', + $wagon->model || '???', + $wagon->type, + join( q{ }, $wagon->attributes ) + ); + } + say ""; + } +} + __END__ =head1 NAME diff --git a/lib/Travel/Status/DE/DBRIS.pm b/lib/Travel/Status/DE/DBRIS.pm index 5fd7a2e..fe03809 100644 --- a/lib/Travel/Status/DE/DBRIS.pm +++ b/lib/Travel/Status/DE/DBRIS.pm @@ -13,6 +13,8 @@ use DateTime::Format::Strptime; use Encode qw(decode encode); use JSON; use LWP::UserAgent; + +use Travel::Status::DE::DBRIS::Formation; use Travel::Status::DE::DBRIS::JourneyAtStop; use Travel::Status::DE::DBRIS::Journey; use Travel::Status::DE::DBRIS::Location; @@ -87,6 +89,22 @@ sub new { $req = "https://www.bahn.de/web/api/reiseloesung/fahrt?journeyId=${journey_id}&poly=${poly}"; } + elsif ( my $cf = $conf{formation} ) { + my $datetime = $cf->{departure}->clone->set_time_zone('UTC'); + my $date = $datetime->strftime('%Y-%m-%d'); + my $time = $datetime->rfc3339 =~ s{(?=Z)}{.000}r; + my %param = ( + administrationId => 80, + category => $cf->{train_type}, + date => $date, + evaNumber => $cf->{eva}, + number => $cf->{train_number}, + time => $time + ); + $req + = 'https://www.bahn.de/web/api/reisebegleitung/wagenreihung/vehicle-sequence?' + . join( '&', map { $_ . '=' . $param{$_} } sort keys %param ); + } else { confess( 'station / geoSearch / locationSearch / journey must be specified' @@ -141,6 +159,9 @@ sub new { elsif ( $conf{geoSearch} or $conf{locationSearch} ) { $self->parse_search; } + elsif ( $conf{formation} ) { + $self->parse_formation( $conf{formation} ); + } return $self; } @@ -310,6 +331,15 @@ sub parse_stationboard { return $self; } +sub parse_formation { + my ( $self, $conf ) = @_; + + $self->{result} = Travel::Status::DE::DBRIS::Formation->new( + json => $self->{raw_json}, + train_type => $conf->{train_type} + ); +} + # }}} # {{{ Public Functions diff --git a/lib/Travel/Status/DE/DBRIS/Formation.pm b/lib/Travel/Status/DE/DBRIS/Formation.pm new file mode 100644 index 0000000..49a5e40 --- /dev/null +++ b/lib/Travel/Status/DE/DBRIS/Formation.pm @@ -0,0 +1,175 @@ +package Travel::Status::DE::DBRIS::Formation; + +use strict; +use warnings; +use 5.020; + +use List::Util qw(uniq); + +use parent 'Class::Accessor'; + +use Travel::Status::DE::DBRIS::Formation::Group; +use Travel::Status::DE::DBRIS::Formation::Sector; +use Travel::Status::DE::DBRIS::Formation::Carriage; + +our $VERSION = '0.01'; + +Travel::Status::DE::DBRIS::Formation->mk_ro_accessors( + qw(direction platform train_type)); + +sub new { + my ( $obj, %opt ) = @_; + + my $json = $opt{json}; + + my $ref = { + json => $opt{json}, + train_type => $opt{train_type}, + platform => $json->{departurePlatform}, + platform_sched => $json->{departurePlatformSchedule}, + }; + + bless( $ref, $obj ); + + $ref->parse_carriages; + $ref->{destinations} = $ref->merge_group_attr('destination'); + $ref->{train_numbers} = $ref->merge_group_attr('train_no'); + $ref->{trains} = $ref->merge_group_attr('train'); + + return $ref; +} + +sub merge_group_attr { + my ( $self, $attr ) = @_; + + my @attrs; + my %attr_to_group; + my %attr_to_sectors; + + for my $group ( $self->groups ) { + push( @attrs, $group->{$attr} ); + push( @{ $attr_to_group{ $group->{$attr} } }, $group ); + push( @{ $attr_to_sectors{ $group->{$attr} } }, $group->sectors ); + } + + @attrs = uniq @attrs; + + return [ + map { + { + name => $_, + groups => $attr_to_group{$_}, + sectors => $attr_to_sectors{$_} + } + } @attrs + ]; +} + +sub parse_carriages { + my ($self) = @_; + + my $platform_length + = $self->{json}{platform}{end} - $self->{json}{platform}{start}; + + for my $sector ( @{ $self->{json}{platform}{sectors} } ) { + push( + @{ $self->{sectors} }, + Travel::Status::DE::DBRIS::Formation::Sector->new( + json => $sector, + platform => { + start => $self->{json}{platform}{start}, + end => $self->{json}{platform}{end}, + } + ) + ); + } + + my @groups; + my @numbers; + + for my $group ( @{ $self->{json}{groups} // [] } ) { + my @group_carriages; + for my $carriage ( @{ $group->{vehicles} // [] } ) { + my $carriage_object + = Travel::Status::DE::DBRIS::Formation::Carriage->new( + json => $carriage, + platform => { + start => $self->{json}{platform}{start}, + end => $self->{json}{platform}{end}, + } + ); + push( @group_carriages, $carriage_object ); + push( @{ $self->{carriages} }, $carriage_object ); + } + @group_carriages + = sort { $a->start_percent <=> $b->start_percent } @group_carriages; + my $group_obj = Travel::Status::DE::DBRIS::Formation::Group->new( + json => $group, + carriages => \@group_carriages, + ); + push( @groups, $group_obj ); + push( @numbers, $group_obj->train_no ); + } + + @groups = sort { $a->start_percent <=> $b->start_percent } @groups; + + @numbers = uniq @numbers; + $self->{train_numbers} = \@numbers; + + if ( @{ $self->{carriages} // [] } > 1 ) { + if ( $self->{carriages}[0]->{start_percent} + > $self->{carriages}[-1]->{start_percent} ) + { + $self->{direction} = 100; + } + else { + $self->{direction} = 0; + } + } + + $self->{groups} = [@groups]; +} + +sub destinations { + my ($self) = @_; + + return @{ $self->{destinations} // [] }; +} + +sub train_numbers { + my ($self) = @_; + + return @{ $self->{train_numbers} // [] }; +} + +sub trains { + my ($self) = @_; + + return @{ $self->{trains} // [] }; +} + +sub sectors { + my ($self) = @_; + + return @{ $self->{sectors} // [] }; +} + +sub groups { + my ($self) = @_; + return @{ $self->{groups} // [] }; +} + +sub carriages { + my ($self) = @_; + return @{ $self->{carriages} // [] }; +} + +sub TO_JSON { + my ($self) = @_; + + my $ret = { %{$self} }; + + return $ret; +} + +1; diff --git a/lib/Travel/Status/DE/DBRIS/Formation/Carriage.pm b/lib/Travel/Status/DE/DBRIS/Formation/Carriage.pm new file mode 100644 index 0000000..b779bd0 --- /dev/null +++ b/lib/Travel/Status/DE/DBRIS/Formation/Carriage.pm @@ -0,0 +1,216 @@ +package Travel::Status::DE::DBRIS::Formation::Carriage; + +use strict; +use warnings; +use 5.020; +use utf8; + +use parent 'Class::Accessor'; +use Carp qw(cluck); + +our $VERSION = '0.18'; +Travel::Status::DE::DBRIS::Formation::Carriage->mk_ro_accessors( + qw(class_type is_closed is_dosto is_locomotive is_powercar + number model section uic_id type + start_meters end_meters length_meters start_percent end_percent length_percent + has_priority_seats has_ac has_quiet_zone has_bahn_comfort has_wheelchair_space + has_wheelchair_toilet has_family_zone has_infant_cabin has_info has_bistro + has_first_class has_second_class + ) +); + +my %type_map = ( + SEATS_SEVERELY_DISABLE => 'priority_seats', + AIR_CONDITION => 'ac', + ZONE_QUIET => 'quiet_zone', + SEATS_BAHN_COMFORT => 'bahn_comfort', + INFO => 'info', + TOILET_WHEELCHAIR => 'wheelchair_toilet', + WHEELCHAIR_SPACE => 'wheelchair_space', + ZONE_FAMILY => 'family_zone', + CABIN_INFANT => 'infant_cabin', +); + +sub new { + my ( $obj, %opt ) = @_; + my $ref = {}; + + my %json = %{ $opt{json} }; + my $platform = $opt{platform}; + + $ref->{class_type} = 0; + $ref->{has_bistro} = 0; + $ref->{is_locomotive} = 0; + $ref->{is_powercar} = 0; + $ref->{is_closed} = 0; + $ref->{number} = $json{wagonIdentificationNumber}; + $ref->{model} = $json{vehicleID}; + $ref->{uic_id} = $json{vehicleID}; + $ref->{section} = $json{platformPosition}{sector}; + $ref->{type} = $json{type}{constructionType}; + + $ref->{model} =~ s{^.....(...)....(?:-.)?$}{$1} or $ref->{model} = undef; + + my $self = bless( $ref, $obj ); + + $self->parse_type; + + for my $amenity ( @{ $json{amenities} // [] } ) { + my $type = $amenity->{type}; + if ( $type_map{$type} ) { + my $key = 'has_' . $type_map{$type}; + $self->{$key} = 1; + } + } + + if ( $json{status} and $json{status} eq 'CLOSED' ) { + $ref->{is_closed} = 1; + } + + if ( $json{type}{category} =~ m{DININGCAR} ) { + $ref->{has_bistro} = 1; + } + elsif ( $json{type}{category} eq 'LOCOMOTIVE' ) { + $ref->{is_locomotive} = 1; + } + elsif ( $json{type}{category} eq 'POWERCAR' ) { + $ref->{is_powercar} = 1; + } + + $ref->{has_first_class} = $json{type}{hasFirstClass}; + $ref->{has_second_class} = $json{type}{hasEconomyClass}; + + if ( $ref->{type} =~ m{AB} ) { + $ref->{class_type} = 12; + } + elsif ( $ref->{type} =~ m{A} ) { + $ref->{class_type} = 1; + } + elsif ( $ref->{type} =~ m{B|WR} ) { + $ref->{class_type} = 2; + } + + my $pos = $json{platformPosition}; + my $platform_length = $platform->{end} - $platform->{start}; + + $ref->{start_meters} = $pos->{start}; + $ref->{end_meters} = $pos->{end}; + $ref->{start_percent} + = ( $pos->{start} - $platform->{start} ) * 100 / $platform_length, + $ref->{end_percent} + = ( $pos->{end} - $platform->{start} ) * 100 / $platform_length, + $ref->{length_meters} = $pos->{start} - $pos->{end}; + $ref->{length_percent} = $ref->{end_percent} - $ref->{start_percent}; + + if ( $pos->{start} eq '' + or $pos->{end} eq '' ) + { + $ref->{position}{valid} = 0; + } + else { + $ref->{position}{valid} = 1; + } + + return $self; +} + +sub attributes { + my ($self) = @_; + + return @{ $self->{attributes} // [] }; +} + +# See also: +# https://de.wikipedia.org/wiki/UIC-Bauart-Bezeichnungssystem_f%C3%BCr_Reisezugwagen#Kennbuchstaben +# https://www.deutsche-reisezugwagen.de/lexikon/erklarung-der-gattungszeichen/ +sub parse_type { + my ($self) = @_; + + my $type = $self->{type}; + my @desc; + + if ( $type =~ m{^D} ) { + $self->{is_dosto} = 1; + push( @desc, 'Doppelstock' ); + } + + if ( $type =~ m{b} ) { + $self->{has_accessibility} = 1; + push( @desc, 'Behindertengerechte Ausstattung' ); + } + + if ( $type =~ m{d} ) { + $self->{multipurpose} = 1; + push( @desc, 'Mehrzweck' ); + } + + if ( $type =~ m{f} ) { + push( @desc, 'Steuerabteil' ); + } + + if ( $type =~ m{i} ) { + push( @desc, 'Interregio' ); + } + + if ( $type =~ m{mm} ) { + push( @desc, 'modernisiert' ); + } + + if ( $type =~ m{p} ) { + $self->{has_ac} = 1; + push( @desc, 'Großraum' ); + } + + if ( $type =~ m{s} ) { + push( @desc, 'Sonderabteil' ); + } + + if ( $type =~ m{v} ) { + $self->{has_ac} = 1; + $self->{has_compartments} = 1; + push( @desc, 'Abteil' ); + } + + if ( $type =~ m{w} ) { + $self->{has_ac} = 1; + $self->{has_compartments} = 1; + push( @desc, 'Abteil' ); + } + + $self->{attributes} = \@desc; +} + +sub is_first_class { + my ($self) = @_; + + if ( $self->{type} =~ m{^D?A} ) { + return 1; + } + return 0; +} + +sub is_second_class { + my ($self) = @_; + + if ( $self->{type} =~ m{^D?A?B} ) { + return 1; + } + return 0; +} + +sub sections { + my ($self) = @_; + + return @{ $self->{sections} }; +} + +sub TO_JSON { + my ($self) = @_; + + my %copy = %{$self}; + + return {%copy}; +} + +1; + diff --git a/lib/Travel/Status/DE/DBRIS/Formation/Group.pm b/lib/Travel/Status/DE/DBRIS/Formation/Group.pm new file mode 100644 index 0000000..1e66c06 --- /dev/null +++ b/lib/Travel/Status/DE/DBRIS/Formation/Group.pm @@ -0,0 +1,689 @@ +package Travel::Status::DE::DBRIS::Formation::Group; + +use strict; +use warnings; +use 5.020; +use utf8; + +use parent 'Class::Accessor'; +use List::Util qw(uniq); + +our $VERSION = '0.18'; + +Travel::Status::DE::DBRIS::Formation::Group->mk_ro_accessors( + qw(designation name train_no train_type description desc_short destination has_sectors model series start_percent end_percent) +); + +# {{{ ICE designations + +# Courtesy of https://github.com/marudor/bahn.expert +# cat src/server/coachSequence/TrainNames.ts | perl -nE 'if (m{(\d+): ''([^'']+)''}) { say "$1 => ''$2''," }' | xclip -i + +my %ice_name = ( + 101 => 'Gießen', + 102 => 'Jever', + 103 => 'Neu-Isenburg', + 104 => 'Fulda', + 105 => 'Offenbach am Main', + 106 => 'Itzehoe', + 107 => 'Plattling', + 108 => 'Lichtenfels', + 110 => 'Gelsenkirchen', + 111 => 'Nürnberg', + 112 => 'Memmingen', + 113 => 'Frankenthal/Pfalz', + 114 => 'Friedrichshafen', + 115 => 'Regensburg', + 116 => 'Pforzheim', + 117 => 'Hof', + 119 => 'Osnabrück', + 120 => 'Lüneburg', + 152 => 'Hanau', + 153 => 'Neumünster', + 154 => 'Flensburg', + 155 => 'Rosenheim', + 156 => 'Heppenheim/Bergstraße', + 157 => 'Landshut', + 158 => 'Gütersloh', + 159 => 'Bad Oldesloe', + 160 => 'Mülheim an der Ruhr', + 161 => 'Bebra', + 162 => 'Geisenheim/Rheingau', + 166 => 'Gelnhausen', + 167 => 'Garmisch-Partenkirchen', + 168 => 'Crailsheim', + 169 => 'Worms', + 171 => 'Heusenstamm', + 172 => 'Aschaffenburg', + 173 => 'Basel', + 174 => 'Zürich', + 175 => 'Nürnberg', + 176 => 'Bremen', + 177 => 'Rendsburg', + 178 => 'Bremerhaven', + 180 => 'Castrop-Rauxel', + 181 => 'Interlaken', + 182 => 'Rüdesheim am Rhein', + 183 => 'Timmendorfer Strand', + 184 => 'Bruchsal', + 185 => 'Freilassing', + 186 => 'Chur', + 187 => 'Mühldorf a. Inn', + 188 => 'Hildesheim', + 190 => 'Ludwigshafen am Rhein', + 201 => 'Rheinsberg', + 202 => 'Wuppertal', + 203 => 'Cottbus/Chóśebuz', + 204 => 'Bielefeld', + 205 => 'Zwickau', + 206 => 'Magdeburg', + 207 => 'Stendal', + 208 => 'Bonn', + 209 => 'Riesa', + 210 => 'Fontanestadt Neuruppin', + 211 => 'Uelzen', + 212 => 'Potsdam', + 213 => 'Nauen', + 214 => 'Hamm (Westf.)', + 215 => 'Bitterfeld-Wolfen', + 216 => 'Dessau', + 217 => 'Bergen auf Rügen', + 218 => 'Braunschweig', + 219 => 'Hagen', + 220 => 'Meiningen', + 221 => 'Lübbenau/Spreewald', + 222 => 'Eberswalde', + 223 => 'Schwerin', + 224 => 'Saalfeld (Saale)', + 225 => 'Oldenburg (Oldb)', + 226 => 'Lutherstadt Wittenberg', + 227 => 'Ludwigslust', + 228 => 'Altenburg', + 229 => 'Templin', + 230 => 'Delitzsch', + 231 => 'Brandenburg an der Havel', + 232 => 'Frankfurt (Oder)', + 233 => 'Ulm', + 234 => 'Minden', + 235 => 'Görlitz', + 236 => 'Jüterbog', + 237 => 'Neustrelitz', + 238 => 'Saarbrücken', + 239 => 'Essen', + 240 => 'Bochum', + 241 => 'Bad Hersfeld', + 242 => 'Quedlinburg', + 243 => 'Bautzen/Budyšin', + 244 => 'Koblenz', + 301 => 'Freiburg im Breisgau', + 302 => 'Hansestadt Lübeck', + 303 => 'Dortmund', + 304 => 'München', + 305 => 'Baden-Baden', + 306 => 'Nördlingen', + 307 => 'Oberhausen', + 308 => 'Murnau am Staffelsee', + 309 => 'Aalen', + 310 => 'Wolfsburg', + 311 => 'Wiesbaden', + 312 => 'Montabaur', + 313 => 'Treuchtlingen', + 314 => 'Bergisch Gladbach', + 315 => 'Singen (Hohentwiel)', + 316 => 'Siegburg', + 317 => 'Recklinghausen', + 318 => 'Münster (Westf.)', + 319 => 'Duisburg', + 320 => 'Weil am Rhein', + 321 => 'Krefeld', + 322 => 'Solingen', + 323 => 'Schaffhausen', + 324 => 'Fürth', + 325 => 'Ravensburg', + 326 => 'Neunkirchen', + 327 => 'Siegen', + 328 => 'Aachen', + 330 => 'Göttingen', + 331 => 'Westerland/Sylt', + 332 => 'Augsburg', + 333 => 'Goslar', + 334 => 'Offenburg', + 335 => 'Konstanz', + 336 => 'Ingolstadt', + 337 => 'Stuttgart', + 351 => 'Herford', + 352 => 'Mönchengladbach', + 353 => 'Neu-Ulm', + 354 => 'Mittenwald', + 355 => 'Tuttlingen', + 357 => 'Esslingen am Neckar', + 358 => 'St. Ingbert', + 359 => 'Leverkusen', + 360 => 'Linz am Rhein', + 361 => 'Celle', + 362 => 'Schwerte (Ruhr)', + 363 => 'Weilheim i. OB', + 1101 => 'Neustadt an der Weinstraße', + 1102 => 'Neubrandenburg', + 1103 => 'Paderborn', + 1104 => 'Erfurt', + 1105 => 'Dresden', + 1107 => 'Pirna', + 1108 => 'Berlin', + 1109 => 'Güstrow', + 1110 => 'Naumburg (Saale)', + 1111 => 'Hansestadt Wismar', + 1112 => 'Freie und Hansestadt Hamburg', + 1113 => 'Hansestadt Stralsund', + 1117 => 'Erlangen', + 1118 => 'Plauen/Vogtland', + 1119 => 'Meißen', + 1125 => 'Arnstadt', + 1126 => 'Leipzig', + 1127 => 'Weimar', + 1128 => 'Reutlingen', + 1129 => 'Kiel', + 1130 => 'Jena', + 1131 => 'Trier', + 1132 => 'Wittenberge', + 1151 => 'Elsterwerda', + 1152 => 'Travemünde', + 1153 => 'Ilmenau', + 1154 => 'Sonneberg', + 1155 => 'Mühlhausen/Thüringen', + 1156 => 'Waren (Müritz)', + 1157 => 'Innsbruck', + 1158 => 'Falkenberg/Elster', + 1159 => 'Passau', + 1160 => 'Markt Holzkirchen', + 1161 => 'Andernach', + 1162 => 'Vaihingen an der Enz', + 1163 => 'Ostseebad Binz', + 1164 => 'Rödental', + 1165 => 'Bad Oeynhausen', + 1166 => 'Bingen am Rhein', + 1167 => 'Traunstein', + 1168 => 'Ellwangen', + 1169 => 'Tutzing', + 1170 => 'Prenzlau', + 1171 => 'Oschatz', + 1172 => 'Bamberg', + 1173 => 'Halle (Saale)', + 1174 => 'Hansestadt Warburg', + 1175 => 'Villingen-Schwenningen', + 1176 => 'Coburg', + 1177 => 'Rathenow', + 1178 => 'Ostseebad Warnemünde', + 1180 => 'Darmstadt', + 1181 => 'Horb am Neckar', + 1182 => 'Mainz', + 1183 => 'Oberursel (Taunus)', + 1184 => 'Kaiserslautern', + 1190 => 'Wien', + 1191 => 'Salzburg', + 1192 => 'Linz', + 1501 => 'Eisenach', + 1502 => 'Karlsruhe', + 1503 => 'Altenbeken', + 1504 => 'Heidelberg', + 1505 => 'Marburg/Lahn', + 1506 => 'Kassel', + 1520 => 'Gotha', + 1521 => 'Homburg/Saar', + 1522 => 'Torgau', + 1523 => 'Hansestadt Greifswald', + 1524 => 'Hansestadt Rostock', + 2853 => 'Nationalpark Sächsische Schweiz', + 2865 => 'Remstal', + 2868 => 'Nationalpark Niedersächsisches Wattenmeer', + 2871 => 'Leipziger Neuseenland', + 2874 => 'Oberer Neckar', + 2875 => 'Magdeburger Börde', + 4103 => 'Allgäu', + 4111 => 'Gäu', + 4114 => 'Dresden Elbland', + 4117 => 'Mecklenburgische Ostseeküste', + 4601 => 'Europa/Europe', + 4602 => 'Euregio Maas-Rhein', + 4603 => 'Mannheim', + 4604 => 'Brussel/Bruxelles', + 4607 => 'Hannover', + 4610 => 'Frankfurt am Main', + 4611 => 'Düsseldorf', + 4651 => 'Amsterdam', + 4652 => 'Arnhem', + 4680 => 'Würzburg', + 4682 => 'Köln', + 4683 => 'Limburg an der Lahn', + 4684 => 'Forbach-Lorraine', + 4685 => 'Schwäbisch Hall', + 4712 => 'Dillingen a.d. Donau', + 4710 => 'Ansbach', + 4717 => 'Paris', + 8007 => 'Rheinland', + 8022 => 'Waldecker Land', + 9006 => 'Martin Luther', + 9018 => 'Freistaat Bayern', + 9025 => 'Nordrhein-Westfalen', + 9026 => 'Zürichsee', + 9028 => 'Freistaat Sachsen', + 9041 => 'Baden-Württemberg', + 9046 => 'Female ICE', + 9050 => 'Metropole Ruhr', + 9202 => 'Schleswig-Holstein', + 9212 => 'Fan-Hauptstadt Hamburg', + 9237 => 'Spree', + 9457 => 'Bundesrepublik Deutschland', + 9481 => 'Rheinland-Pfalz' +); + +# }}} + +# {{{ Rolling Stock Models + +my %model_name = ( + '011' => [ 'ICE T', 'ÖBB 4011' ], + '401' => ['ICE 1'], + '402' => ['ICE 2'], + '403.S1' => [ 'ICE 3', 'BR 403, 1. Serie' ], + '403.S2' => [ 'ICE 3', 'BR 403, 2. Serie' ], + '403.R' => [ 'ICE 3', 'BR 403 Redesign' ], + '406' => [ 'ICE 3', 'BR 406' ], + '406.R' => [ 'ICE 3', 'BR 406 Redesign' ], + '407' => [ 'ICE 3 Velaro', 'BR 407' ], + '408' => [ 'ICE 3neo', 'BR 408' ], + '411.S1' => [ 'ICE T', 'BR 411, 1. Serie' ], + '411.S2' => [ 'ICE T', 'BR 411, 2. Serie' ], + '412' => ['ICE 4'], + '415' => [ 'ICE T', 'BR 415' ], + '420' => ['BR 420'], + '422' => ['BR 422'], + '423' => ['BR 423'], + '425' => ['BR 425'], + '427' => [ 'FLIRT', 'BR 427' ], + '428' => [ 'FLIRT', 'BR 428' ], + '429' => [ 'FLIRT', 'BR 429' ], + '430' => ['BR 430'], + '440' => [ 'Coradia Continental', 'BR 440' ], + '442' => [ 'Talent 2', 'BR 442' ], + '445' => [ 'Twindexx Vario', 'BR 445' ], + '446' => [ 'Twindexx Vario', 'BR 446' ], + '462' => [ 'Desiro HC', 'BR 462' ], + '463' => [ 'Mireo', 'BR 463' ], + '475' => [ 'TGV', 'BR 475' ], + '612' => [ 'RegioSwinger', 'BR 612' ], + '620' => [ 'LINT 81', 'BR 620' ], + '622' => [ 'LINT 54', 'BR 622' ], + '631' => [ 'Link I', 'BR 631' ], + '632' => [ 'Link II', 'BR 632' ], + '633' => [ 'Link III', 'BR 633' ], + '640' => [ 'LINT 27', 'BR 640' ], + '642' => [ 'Desiro Classic', 'BR 642' ], + '643' => [ 'TALENT', 'BR 643' ], + '644' => [ 'TALENT', 'BR 644' ], + '648' => [ 'LINT 41', 'BR 648' ], + 'IC2.TWIN' => ['IC 2 Twindexx'], + 'IC2.KISS' => ['IC 2 KISS'], +); + +my %power_desc = ( + 90 => 'mit sonstigem Antrieb', + 91 => 'mit elektrischer Lokomotive', + 92 => 'mit Diesellokomotive', + 93 => 'Hochgeschwindigkeitszug', + 94 => 'Elektrischer Triebzug', + 95 => 'Diesel-Triebzug', + 96 => 'mit speziellen Beiwagen', + 97 => 'mit elektrischer Rangierlok', + 98 => 'mit Diesel-Rangierlok', + 99 => 'Sonderfahrzeug', +); + +# }}} + +sub new { + my ( $obj, %opt ) = @_; + + my %json = %{ $opt{json} }; + + my $ref = { + carriages => $opt{carriages}, + destination => $json{transport}{destination}{name}, + train_type => $json{transport}{category}, + name => $json{name}, + line => $json{transport}{numberwline}, + train_no => $json{transport}{number}, + }; + + if ( $ref->{name} =~ m{ ^ IC[DE] 0* (\d+) $ }x and exists $ice_name{$1} ) { + $ref->{designation} = $ice_name{$1}; + } + + $ref->{train} = $ref->{train_type} . ' ' . $ref->{train_no}; + + $ref->{sectors} = [ + uniq grep { defined } + map { $_->{platformPosition}{sector} } @{ $json{vehicles} // [] } + ]; + if ( @{ $ref->{sectors} } ) { + $ref->{has_sectors} = 1; + } + + $ref->{start_percent} = $ref->{carriages}[0]->start_percent; + $ref->{end_percent} = $ref->{carriages}[-1]->end_percent; + + bless( $ref, $obj ); + + $ref->parse_description; + + return $ref; +} + +sub parse_powertype { + my ($self) = @_; + + my %ml = map { $_ => 0 } ( 90 .. 99 ); + + for my $carriage ( $self->carriages ) { + + if ( not $carriage->uic_id or length( $carriage->uic_id ) != 12 ) { + next; + } + + my $carriage_type = substr( $carriage->uic_id, 0, 2 ); + if ( $carriage_type < 90 ) { + next; + } + + $ml{$carriage_type}++; + } + + my @likelihood = reverse sort { $ml{$a} <=> $ml{$b} } keys %ml; + + if ( $ml{ $likelihood[0] } == 0 ) { + return; + } + + $self->{powertype} = $likelihood[0]; +} + +sub parse_model { + my ($self) = @_; + + my %ml = ( + '011' => 0, + '401' => 0, + '402' => 0, + '403.S1' => 0, + '403.S2' => 0, + '403.R' => 0, + '406' => 0, + '407' => 0, + '408' => 0, + '411.S1' => 0, + '411.S2' => 0, + '412' => 0, + '415' => 0, + '420' => 0, + '422' => 0, + '423' => 0, + '425' => 0, + '427' => 0, + '428' => 0, + '429' => 0, + '430' => 0, + '440' => 0, + '442' => 0, + '445' => 0, + '446' => 0, + '462' => 0, + '463' => 0, + '475' => 0, + '612' => 0, + '620' => 0, + '622' => 0, + '631' => 0, + '632' => 0, + '633' => 0, + '640' => 0, + '642' => 0, + '643' => 0, + '644' => 0, + '648' => 0, + 'IC2.TWIN' => 0, + 'IC2.KISS' => 0, + ); + + my @carriages = $self->carriages; + + for my $carriage (@carriages) { + if ( not $carriage->model ) { + next; + } + if ( $carriage->model == 401 + or ( $carriage->model >= 801 and $carriage->model <= 804 ) ) + { + $ml{'401'}++; + } + elsif ( $carriage->model == 402 + or ( $carriage->model >= 805 and $carriage->model <= 808 ) ) + { + $ml{'402'}++; + } + elsif ( $carriage->model == 403 + and substr( $carriage->uic_id, 9, 2 ) <= 37 ) + { + $ml{'403.S1'}++; + } + elsif ( $carriage->model == 403 + and substr( $carriage->uic_id, 9, 2 ) > 37 ) + { + $ml{'403.S2'}++; + } + elsif ( $carriage->model == 406 ) { + $ml{'406'}++; + } + elsif ( $carriage->model == 407 ) { + $ml{'407'}++; + } + elsif ( $carriage->model == 408 ) { + $ml{'408'}++; + } + elsif ( $carriage->model == 412 or $carriage->model == 812 ) { + $ml{'412'}++; + } + elsif ( $carriage->model == 411 + and substr( $carriage->uic_id, 9, 2 ) <= 32 ) + { + $ml{'411.S1'}++; + } + elsif ( $carriage->model == 411 + and substr( $carriage->uic_id, 9, 2 ) > 32 ) + { + $ml{'411.S2'}++; + } + elsif ( $carriage->model == 415 ) { + $ml{'415'}++; + } + elsif ( $carriage->model == 420 or $carriage->model == 421 ) { + $ml{'420'}++; + } + elsif ( $carriage->model == 422 or $carriage->model == 432 ) { + $ml{'422'}++; + } + elsif ( $carriage->model == 423 or $carriage->model == 433 ) { + $ml{'423'}++; + } + elsif ( $carriage->model == 425 or $carriage->model == 435 ) { + $ml{'425'}++; + } + elsif ( $carriage->model == 427 or $carriage->model == 827 ) { + $ml{'427'}++; + } + elsif ( $carriage->model == 428 or $carriage->model == 828 ) { + $ml{'428'}++; + } + elsif ( $carriage->model == 429 or $carriage->model == 829 ) { + $ml{'429'}++; + } + elsif ( $carriage->model == 430 or $carriage->model == 431 ) { + $ml{'430'}++; + } + elsif ($carriage->model == 440 + or $carriage->model == 441 + or $carriage->model == 841 ) + { + $ml{'440'}++; + } + elsif ($carriage->model == 442 + or $carriage->model == 443 ) + { + $ml{'442'}++; + } + elsif ($carriage->model == 462 + or $carriage->model == 862 ) + { + $ml{'462'}++; + } + elsif ($carriage->model == 463 + or $carriage->model == 863 ) + { + $ml{'463'}++; + } + elsif ( $carriage->model == 445 ) { + $ml{'445'}++; + } + elsif ( $carriage->model == 446 ) { + $ml{'446'}++; + } + elsif ( $carriage->model == 475 ) { + $ml{'475'}++; + } + elsif ( $carriage->model == 612 ) { + $ml{'612'}++; + } + elsif ( $carriage->model == 620 or $carriage->model == 621 ) { + $ml{'620'}++; + } + elsif ( $carriage->model == 622 ) { + $ml{'622'}++; + } + elsif ( $carriage->model == 631 ) { + $ml{'631'}++; + } + elsif ( $carriage->model == 632 ) { + $ml{'632'}++; + } + elsif ( $carriage->model == 633 ) { + $ml{'633'}++; + } + elsif ( $carriage->model == 640 ) { + $ml{'640'}++; + } + elsif ( $carriage->model == 642 ) { + $ml{'642'}++; + } + elsif ( $carriage->model == 643 or $carriage->model == 943 ) { + $ml{'643'}++; + } + elsif ( $carriage->model == 644 or $carriage->model == 944 ) { + $ml{'644'}++; + } + elsif ( $carriage->model == 648 ) { + $ml{'648'}++; + } + elsif ( $self->train_type eq 'IC' and $carriage->model == 110 ) { + $ml{'IC2.KISS'}++; + } + elsif ( $self->train_type eq 'IC' and $carriage->is_dosto ) { + $ml{'IC2.TWIN'}++; + } + elsif ( substr( $carriage->uic_id, 4, 4 ) eq '4011' ) { + $ml{'011'}++; + } + } + + my @likelihood = reverse sort { $ml{$a} <=> $ml{$b} } keys %ml; + + # Less than two carriages are generally inconclusive. + # Exception: BR 631 (Link I) only has a single carriage + if ( + $ml{ $likelihood[0] } < 2 + and not($likelihood[0] eq '631' + and @carriages == 1 + and substr( $carriages[0]->uic_id, 0, 2 ) eq '95' ) + ) + { + $self->{subtype} = undef; + } + else { + $self->{subtype} = $likelihood[0]; + } + + if ( $self->{subtype} and $model_name{ $self->{subtype} } ) { + my @model = @{ $model_name{ $self->{subtype} } }; + $self->{model} = $model[0]; + $self->{series} = $model[-1]; + } +} + +sub parse_description { + my ($self) = @_; + + $self->parse_powertype; + $self->parse_model; + + my $short; + my $ret = q{}; + + if ( $self->{model} ) { + $short = $self->{model}; + $ret .= $self->{model}; + } + + if ( $self->{powertype} and $power_desc{ $self->{powertype} } ) { + if ( not $ret and $power_desc{ $self->{powertype} } =~ m{^mit} ) { + $ret = "Zug"; + } + $ret .= ' ' . $power_desc{ $self->{powertype} }; + $short //= $ret; + $short =~ s{elektrischer }{E-}; + $short =~ s{[Ll]\Kokomotive}{ok}; + } + + if ( $self->{series} and $self->{series} ne $self->{model} ) { + $ret .= ' (' . $self->{series} . ')'; + } + + $self->{desc_short} = $short; + $self->{description} = $ret; +} + +sub name_to_designation { + my ($self) = @_; + + return %ice_name; +} + +sub sectors { + my ($self) = @_; + + return @{ $self->{sectors} // [] }; +} + +sub carriages { + my ($self) = @_; + + return @{ $self->{carriages} // [] }; +} + +sub TO_JSON { + my ($self) = @_; + + my %copy = %{$self}; + + return {%copy}; +} + +1; + diff --git a/lib/Travel/Status/DE/DBRIS/Formation/Sector.pm b/lib/Travel/Status/DE/DBRIS/Formation/Sector.pm new file mode 100644 index 0000000..ac01647 --- /dev/null +++ b/lib/Travel/Status/DE/DBRIS/Formation/Sector.pm @@ -0,0 +1,52 @@ +package Travel::Status::DE::DBRIS::Formation::Sector; + +use strict; +use warnings; +use 5.020; +use utf8; + +use parent 'Class::Accessor'; + +our $VERSION = '0.18'; + +Travel::Status::DE::DBRIS::Formation::Sector->mk_ro_accessors( + qw(name start_percent end_percent length_percent start_meters end_meters length_meters cube_meters cube_percent) +); + +sub new { + my ( $obj, %opt ) = @_; + + my %section = %{ $opt{json} }; + my %platform = %{ $opt{platform} }; + + my $platform_length = $platform{end} - $platform{start}; + + my $ref = { + name => $section{name}, + start_meters => $section{start}, + end_meters => $section{end}, + length_meters => $section{end} - $section{start}, + cube_meters => $section{cubePosition}, + start_percent => ( $section{start} - $platform{start} ) + * 100 / $platform_length, + end_percent => ( $section{end} - $platform{start} ) + * 100 / $platform_length, + cube_percent => ( $section{cubePosition} - $platform{start} ) + * 100 / $platform_length, + }; + + $ref->{length_percent} = $ref->{end_percent} - $ref->{start_percent}; + + return bless( $ref, $obj ); +} + +sub TO_JSON { + my ($self) = @_; + + my %copy = %{$self}; + + return {%copy}; +} + +1; + |