summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Changelog53
-rw-r--r--README3
-rwxr-xr-xbin/db-wagenreihung107
-rw-r--r--lib/Travel/Status/DE/DBWagenreihung.pm918
-rw-r--r--lib/Travel/Status/DE/DBWagenreihung/Carriage.pm215
-rw-r--r--lib/Travel/Status/DE/DBWagenreihung/Group.pm688
-rw-r--r--lib/Travel/Status/DE/DBWagenreihung/Section.pm34
-rw-r--r--lib/Travel/Status/DE/DBWagenreihung/Sector.pm51
-rw-r--r--lib/Travel/Status/DE/DBWagenreihung/Wagon.pm267
9 files changed, 1290 insertions, 1046 deletions
diff --git a/Changelog b/Changelog
index a2306ef..aa5c8d0 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,56 @@
+Travel::Status::DE::DBWagenreihung 0.18 - Mon Aug 12 2024
+
+ * Group: IC2 trains have designations as well
+ * Group: Add static name_to_designation accessor
+
+Travel::Status::DE::DBWagenreihung 0.17 - Sat Aug 10 2024
+
+ * Group: Fix ->name accessor (thanks to Caß Dingenskirchen)
+ * Group: Add ->designation accessor (data courtesy of bahn.expert)
+
+Travel::Status::DE::DBWagenreihung 0.16 - Wed Jul 31 2024
+
+ * Carriage: (re-)add amenity and first/second class accessors
+
+Travel::Status::DE::DBWagenreihung 0.15 - Tue Jul 30 2024
+
+ * Switch to new bahn.de API; the one used until 0.14 has been discontinued
+ * DBWagenreihung->new: new constructor signature; now requires "departure",
+ "eva", "train_type" and "train_number".
+ * DBWagenreihung: Remove "wagons" accessor
+ * DBWagenreihung: Rename "sections" to "sectors"
+ * DBWagenreihung: Remove "train_nos" accessor
+ * DBWagenreihung: Remove "origins" accessor
+
+Travel::Status::DE::DBWagenreihung 0.14 - Sun Apr 28 2024
+
+ * This module now explicitly treats carriages as groups, just like the
+ backend does. Each group contains at least one carriage and has a
+ distinct number, origin, destination, and train type / description.
+ * Add Travel::Status::DE::DBWagenreihung::Group module.
+ * DBWagenreihung: Add groups, carriages, and train_nos accessors.
+ * DBWagenreihung: origins, destinations: Return hashrefs with
+ "name" / "groups" / "sections" rather than "name" / "sections".
+ * DBWagenreihung: Remove train_descriptions, wagongroup_description,
+ wagongroup_subtype, and wagongroup_model accessors. Use $wr->groups and
+ $group->description / $group->desc_short instead.
+ This is a breaking change.
+
+Travel::Status::DE::DBWagenreihung 0.13 - Sat Apr 27 2024
+
+ * DBWagenreihung: Add station accessor.
+ * DBWagenreihung: Add TO_JSON function.
+ * DBWagenreihung: remove station_ds100, station_name, and station_uic
+ accessors. Use station->{ds100}, station->{name} and station->{eva}
+ instead. This is a breaking change.
+ * DBWagenreihung: origins now returns a list of hashrefs, just like
+ destinations. It used to return a list of names. This is a breaking
+ change.
+
+Travel::Status::DE::DBWagenreihung 0.12 - Fri Mar 29 2024
+
+ * Wagon: Add is_closed accessor
+
Travel::Status::DE::DBWagenreihung 0.11 - Thu Mar 07 2024
* Add another regional train model
diff --git a/README b/README
index 0bb04b6..a4c0bcf 100644
--- a/README
+++ b/README
@@ -1,6 +1,9 @@
Travel::Status::DE::DBWagenreihung - Interface to Deutsche Bahn Wagon Order API
-------------------------------------------------------------------------------
+This project is **deprecated**.
+Please use [Travel::Status::DE::DBRIS](https://finalrewind.org/projects/Travel-Status-DE-DBRIS/) instead.
+
<https://finalrewind.org/projects/Travel-Status-DE-DBWagenreihung/>
This software is not stable yet; API, script and module names/usage may change
diff --git a/bin/db-wagenreihung b/bin/db-wagenreihung
index 73bbdb4..5455e9e 100755
--- a/bin/db-wagenreihung
+++ b/bin/db-wagenreihung
@@ -4,7 +4,7 @@ use warnings;
use 5.020;
use utf8;
-our $VERSION = '0.11';
+our $VERSION = '0.18';
use Getopt::Long;
use List::Util qw(min);
@@ -65,48 +65,56 @@ if ( @trains != 1 ) {
}
my $wr = Travel::Status::DE::DBWagenreihung->new(
- departure => $trains[0]->sched_departure || $trains[0]->sched_arrival,
developer_mode => $developer_mode,
+ departure => $trains[0]->sched_departure || $trains[0]->sched_arrival,
+ eva => $trains[0]->station_eva,
+ train_type => $trains[0]->type,
train_number => $train_number,
);
+if ( $wr->errstr ) {
+ say STDERR $wr->errstr;
+ exit 2;
+}
+
printf(
- "%s: %s → %s\n",
- join( ' / ', map { $wr->train_type . ' ' . $_ } $wr->train_numbers ),
- join( ' / ', $wr->origins ),
+ "%s → %s\n",
+ join(
+ ' / ', map { $wr->train_type . ' ' . $_->{name} } $wr->train_numbers
+ ),
join(
' / ',
map {
- sprintf( '%s (%s)', $_->{name}, join( q{}, @{ $_->{sections} } ) )
+ sprintf( '%s (%s)', $_->{name}, join( q{}, @{ $_->{sectors} } ) )
} $wr->destinations
),
);
-printf( "%s Gleis %s\n\n", $wr->station_name, $wr->platform );
+printf( "Gleis %s\n\n", $wr->platform );
-for my $section ( $wr->sections ) {
- my $section_length = $section->length_percent;
- my $spacing_left = int( ( $section_length - 2 ) / 2 ) - 1;
- my $spacing_right = int( ( $section_length - 2 ) / 2 );
+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 ( $section_length % 2 ) {
+ if ( $sector_length % 2 ) {
$spacing_left++;
}
printf( "▏%s%s%s▕",
- ' ' x $spacing_left,
- $section->name,
- ' ' x $spacing_right );
+ ( $spacing_left >= 0 ) ? ' ' x $spacing_left : q{},
+ $sector->name, ( $spacing_right >= 0 ) ? ' ' x $spacing_right : q{} );
}
print "\n";
-my @start_percentages = map { $_->{position}{start_percent} } $wr->wagons;
-print ' ' x ( ( min @start_percentages ) - 1 );
+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->wagons ) {
- my $wagon_length
- = $wagon->{position}->{end_percent} - $wagon->{position}->{start_percent};
+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;
@@ -142,29 +150,33 @@ for my $wagon ( $wr->wagons ) {
print $wr->direction == 100 ? '>' : '<';
print "\n\n";
-for my $desc ( $wr->train_descriptions ) {
- if ( $desc->{text} ) {
- printf( "%s (%s)\n",
- $desc->{text}, join( q{}, @{ $desc->{sections} } ) );
+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 "";
-
-for my $wagon ( $wr->wagons ) {
- printf(
- "%3s: %3s %10s %s\n",
- $wagon->is_closed ? 'X' : ( $wagon->number || '?' ),
- $wagon->model || '???',
- $wagon->type, join( q{ }, $wagon->attributes )
- );
+ say "";
}
__END__
=head1 NAME
-db-wagenreihung - Interface to Deutsche Bahn Wagon Order API
+db-wagenreihung - Interface to Deutsche Bahn carriage formation API
=head1 SYNOPSIS
@@ -172,21 +184,20 @@ B<db-wagenreihung> I<station> I<train-number>
=head1 VERSION
-version 0.11
+version 0.18
This is beta software: API and output format may change without notice.
=head1 DESCRIPTION
-db-wagenreihung shows the wagon order of train I<train-number> at station
-I<station> (must be a name or DS100 code) as reported by the Deutsche Bahn
-Wagon Order API. As of April 2020, long-distance IC/EC/ICFE trains are widely
-supported, and some regions (e.g. Stuttgart/Karlsruhe) also provide wagon
-orders for select regional trains. Data accuracy varies, but seems to be
-improving over time.
+db-wagenreihung shows the carriage formation (also known as coach sequence) of
+train I<train-number> at station I<station> (must be a name or DS100 code) as
+reported by the Deutsche Bahn Wagenreihung API. As of April 2024, it has mature
+support for long-distance (IC/EC/ICE) trains and a growing number of regional
+transport providers that also offer mostly correct carriage formation data.
-It is not possible to request the wagon order at a train's terminus station.
-This is a known limitation.
+It is not possible to request the carriage formation at a train's terminus
+station. This is a known limitation.
The departure of I<train-number> must be in the time range between now and
two hours in the future.
@@ -197,11 +208,11 @@ two hours in the future.
=item db-wagenreihung 'Essen Hbf' 723
-Show wagon order of ICE 723 at Essen Hbf
+Show carriage formation of ICE 723 at Essen Hbf
=item db-wagenreihung TS 3259
-Show wagon order of IRE 3259 at Stuttgart Hbf
+Show carriage formation of IRE 3259 at Stuttgart Hbf
=back
@@ -219,7 +230,7 @@ Show wagon order of IRE 3259 at Stuttgart Hbf
=head1 AUTHOR
-Copyright (C) 2018-2020 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
+Copyright (C) 2018-2024 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
=head1 LICENSE
diff --git a/lib/Travel/Status/DE/DBWagenreihung.pm b/lib/Travel/Status/DE/DBWagenreihung.pm
index 09abb79..f4914b5 100644
--- a/lib/Travel/Status/DE/DBWagenreihung.pm
+++ b/lib/Travel/Status/DE/DBWagenreihung.pm
@@ -5,98 +5,22 @@ use warnings;
use 5.020;
use utf8;
-our $VERSION = '0.11';
+use parent 'Class::Accessor';
use Carp qw(cluck confess);
use JSON;
use List::Util qw(uniq);
use LWP::UserAgent;
-use Travel::Status::DE::DBWagenreihung::Section;
-use Travel::Status::DE::DBWagenreihung::Wagon;
-
-my %is_redesign = (
- "02" => 1,
- "03" => 1,
- "06" => 1,
- "09" => 1,
- "10" => 1,
- "13" => 1,
- "14" => 1,
- "15" => 1,
- "16" => 1,
- "18" => 1,
- "19" => 1,
- "20" => 1,
- "23" => 1,
- "24" => 1,
- "27" => 1,
- "28" => 1,
- "29" => 1,
- "31" => 1,
- "32" => 1,
- "33" => 1,
- "34" => 1,
- "35" => 1,
- "36" => 1,
- "37" => 1,
- "53" => 1
-);
-
-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' ],
- '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' ],
- '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',
-);
+use Travel::Status::DE::DBWagenreihung::Group;
+use Travel::Status::DE::DBWagenreihung::Sector;
+use Travel::Status::DE::DBWagenreihung::Carriage;
+
+our $VERSION = '0.18';
+
+Travel::Status::DE::DBWagenreihung->mk_ro_accessors(
+ qw(direction platform train_type));
+
+# {{{ Constructors
sub new {
my ( $class, %opt ) = @_;
@@ -110,14 +34,15 @@ sub new {
}
my $self = {
- api_base => $opt{api_base}
- // 'https://ist-wr.noncd.db.de/wagenreihung/1.0',
+ api_base =>
+'https://www.bahn.de/web/api/reisebegleitung/wagenreihung/vehicle-sequence',
developer_mode => $opt{developer_mode},
cache => $opt{cache},
departure => $opt{departure},
+ eva => $opt{eva},
from_json => $opt{from_json},
json => JSON->new,
- serializable => $opt{serializable},
+ train_type => $opt{train_type},
train_number => $opt{train_number},
user_agent => $opt{user_agent},
};
@@ -140,26 +65,35 @@ sub get_wagonorder {
my $api_base = $self->{api_base};
my $cache = $self->{cache};
+ my $eva = $self->{eva};
+ my $train_type = $self->{train_type};
my $train_number = $self->{train_number};
- my $datetime = $self->{departure};
-
- if ( ref($datetime) eq 'DateTime' ) {
- $datetime = $datetime->strftime('%Y%m%d%H%M');
- }
-
my $json = $self->{from_json};
if ( not $json ) {
- my ( $content, $err )
- = $self->get_with_cache( $cache,
- "${api_base}/${train_number}/${datetime}" );
+ my $datetime = $self->{departure}->clone->set_time_zone('UTC');
+ my $date = $datetime->strftime('%Y-%m-%d');
+ my $time = $datetime->rfc3339 =~ s{(?=Z)}{.000}r;
+ $self->{param} = {
+ administrationId => 80,
+ category => $train_type,
+ date => $date,
+ evaNumber => $eva,
+ number => $train_number,
+ time => $time
+ };
+ my $url
+ = $api_base . '?'
+ . join( '&',
+ map { $_ . '=' . $self->{param}{$_} } keys %{ $self->{param} } );
+ my ( $content, $err ) = $self->get_with_cache( $cache, $url );
if ($err) {
- $self->{errstr} = "Failed to fetch station data: $err";
+ $self->{errstr} = "GET $url: $err";
return;
}
- $json = $self->{json}->utf8->decode($content);
+ $json = $self->{from_json} = $self->{json}->utf8->decode($content);
}
if ( exists $json->{error} ) {
@@ -167,585 +101,210 @@ sub get_wagonorder {
return;
}
- if ( @{ $json->{data}{istformation}{allFahrzeuggruppe} // [] } == 0
- and @{ $json->{data}{istformation}{halt} // [] } == 0 )
- {
- $self->{errstr} = 'No wagon order available';
+ if ( not $json->{departureID} ) {
+ $self->{errstr} = 'No carriage formation available';
return;
}
- $self->{data} = $json->{data};
- $self->{meta} = $json->{meta};
-}
-
-sub errstr {
- my ($self) = @_;
-
- return $self->{errstr};
+ return $self->parse_wagonorder;
}
-sub direction {
- my ($self) = @_;
-
- if ( not exists $self->{direction} ) {
-
- # direction is set while parsing wagons
- $self->wagons;
- }
-
- return $self->{direction};
-}
+# }}}
+# {{{ Internal Helpers
-sub has_bad_wagons {
- my ($self) = @_;
+sub get_with_cache {
+ my ( $self, $cache, $url ) = @_;
- if ( defined $self->{has_bad_wagons} ) {
- return $self->{has_bad_wagons};
+ if ( $self->{developer_mode} ) {
+ say "GET $url";
}
- for my $group ( @{ $self->{data}{istformation}{allFahrzeuggruppe} } ) {
- for my $wagon ( @{ $group->{allFahrzeug} } ) {
- my $pos = $wagon->{positionamhalt};
- if ( $pos->{startprozent} eq ''
- or $pos->{endeprozent} eq ''
- or $pos->{startmeter} eq ''
- or $pos->{endemeter} eq '' )
- {
- return $self->{has_bad_wagons} = 1;
+ if ($cache) {
+ my $content = $cache->thaw($url);
+ if ($content) {
+ if ( $self->{developer_mode} ) {
+ say ' cache hit';
}
+ return ( ${$content}, undef );
}
}
- return $self->{has_bad_wagons} = 0;
-}
-
-sub origins {
- my ($self) = @_;
-
- if ( exists $self->{origins} ) {
- return @{ $self->{origins} };
+ if ( $self->{developer_mode} ) {
+ say ' cache miss';
}
- my @origins;
+ my $ua = $self->{user_agent};
+ my $res = $ua->get($url);
- for my $group ( @{ $self->{data}{istformation}{allFahrzeuggruppe} } ) {
- push( @origins, $group->{startbetriebsstellename} );
+ if ( $res->is_error ) {
+ return ( undef, $res->status_line );
}
+ my $content = $res->decoded_content;
- @origins = uniq @origins;
-
- $self->{origins} = \@origins;
+ if ($cache) {
+ $cache->freeze( $url, \$content );
+ }
- return @origins;
+ return ( $content, undef );
}
-sub destinations {
+sub parse_wagonorder {
my ($self) = @_;
- if ( exists $self->{destinations} ) {
- return @{ $self->{destinations} };
- }
-
- my @destinations;
- my %section;
+ $self->{platform} = $self->{from_json}{departurePlatform};
+ $self->{platform_sched} = $self->{from_json}{departurePlatformSchedule};
- for my $group ( @{ $self->{data}{istformation}{allFahrzeuggruppe} } ) {
- my $destination = $group->{zielbetriebsstellename};
- my @sections = map { $_->{fahrzeugsektor} } @{ $group->{allFahrzeug} };
- push( @{ $section{$destination} }, @sections );
- push( @destinations, $destination );
- }
-
- @destinations = uniq @destinations;
+ $self->parse_carriages;
+ $self->{destinations} = $self->merge_group_attr('destination');
+ $self->{train_numbers} = $self->merge_group_attr('train_no');
+ $self->{trains} = $self->merge_group_attr('train');
+}
- @destinations
- = map { { name => $_, sections => [ uniq @{ $section{$_} } ] } }
- @destinations;
+sub merge_group_attr {
+ my ( $self, $attr ) = @_;
- $self->{destinations} = \@destinations;
+ my @attrs;
+ my %attr_to_group;
+ my %attr_to_sectors;
- return @destinations;
-}
+ for my $group ( $self->groups ) {
+ push( @attrs, $group->{$attr} );
+ push( @{ $attr_to_group{ $group->{$attr} } }, $group );
+ push( @{ $attr_to_sectors{ $group->{$attr} } }, $group->sectors );
+ }
-sub platform {
- my ($self) = @_;
+ @attrs = uniq @attrs;
- return $self->{data}{istformation}{halt}{gleisbezeichnung};
+ return [
+ map {
+ {
+ name => $_,
+ groups => $attr_to_group{$_},
+ sectors => $attr_to_sectors{$_}
+ }
+ } @attrs
+ ];
}
-sub sections {
+sub parse_carriages {
my ($self) = @_;
- if ( exists $self->{sections} ) {
- return @{ $self->{sections} };
- }
+ my $platform_length
+ = $self->{from_json}{platform}{end} - $self->{from_json}{platform}{start};
- for my $section ( @{ $self->{data}{istformation}{halt}{allSektor} } ) {
- my $pos = $section->{positionamgleis};
- if ( $pos->{startprozent} eq '' or $pos->{endeprozent} eq '' ) {
- next;
- }
+ for my $sector ( @{ $self->{from_json}{platform}{sectors} } ) {
push(
- @{ $self->{sections} },
- Travel::Status::DE::DBWagenreihung::Section->new(
- name => $section->{sektorbezeichnung},
- start_percent => $pos->{startprozent},
- end_percent => $pos->{endeprozent},
- start_meters => $pos->{startmeter},
- end_meters => $pos->{endemeter},
+ @{ $self->{sectors} },
+ Travel::Status::DE::DBWagenreihung::Sector->new(
+ json => $sector,
+ platform => {
+ start => $self->{from_json}{platform}{start},
+ end => $self->{from_json}{platform}{end},
+ }
)
);
}
- return @{ $self->{sections} // [] };
-}
+ my @groups;
+ my @numbers;
-sub station_ds100 {
- my ($self) = @_;
+ for my $group ( @{ $self->{from_json}{groups} // [] } ) {
+ my @group_carriages;
+ for my $carriage ( @{ $group->{vehicles} // [] } ) {
+ my $carriage_object
+ = Travel::Status::DE::DBWagenreihung::Carriage->new(
+ json => $carriage,
+ platform => {
+ start => $self->{from_json}{platform}{start},
+ end => $self->{from_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::DBWagenreihung::Group->new(
+ json => $group,
+ carriages => \@group_carriages,
+ );
+ push( @groups, $group_obj );
+ push( @numbers, $group_obj->train_no );
+ }
- return $self->{data}{istformation}{halt}{rl100};
-}
+ @groups = sort { $a->start_percent <=> $b->start_percent } @groups;
-sub station_name {
- my ($self) = @_;
+ @numbers = uniq @numbers;
+ $self->{train_numbers} = \@numbers;
- return $self->{data}{istformation}{halt}{bahnhofsname};
+ 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 station_uic {
+# }}}
+# {{{ Public Functions
+
+sub errstr {
my ($self) = @_;
- return $self->{data}{istformation}{halt}{evanummer};
+ return $self->{errstr};
}
-sub train_type {
+sub destinations {
my ($self) = @_;
- return $self->{data}{istformation}{zuggattung};
+ return @{ $self->{destinations} // [] };
}
sub train_numbers {
my ($self) = @_;
- if ( exists $self->{train_numbers} ) {
- return @{ $self->{train_numbers} };
- }
-
- my @numbers;
-
- for my $group ( @{ $self->{data}{istformation}{allFahrzeuggruppe} } ) {
- push( @numbers, $group->{verkehrlichezugnummer} );
- }
-
- @numbers = uniq @numbers;
-
- $self->{train_numbers} = \@numbers;
-
- return @numbers;
+ return @{ $self->{train_numbers} // [] };
}
-sub train_no {
+sub trains {
my ($self) = @_;
- return $self->{data}{istformation}{zugnummer};
+ return @{ $self->{trains} // [] };
}
-sub wagongroup_powertype {
- my ( $self, @wagons ) = @_;
-
- if ( not @wagons ) {
- @wagons = $self->wagons;
- }
-
- my %ml = map { $_ => 0 } ( 90 .. 99 );
-
- for my $wagon (@wagons) {
-
- if ( not $wagon->uic_id or length( $wagon->uic_id ) != 12 ) {
- next;
- }
-
- my $wagon_type = substr( $wagon->uic_id, 0, 2 );
- if ( $wagon_type < 90 ) {
- next;
- }
-
- $ml{$wagon_type}++;
- }
-
- my @likelihood = reverse sort { $ml{$a} <=> $ml{$b} } keys %ml;
-
- if ( $ml{ $likelihood[0] } == 0 ) {
- return undef;
- }
-
- return $likelihood[0];
-}
-
-sub train_descriptions {
+sub sectors {
my ($self) = @_;
- if ( exists $self->{train_descriptions} ) {
- return @{ $self->{train_descriptions} };
- }
-
- if ( not exists $self->{wagons} ) {
-
- # wagongroups are set while parsong wagons
- $self->wagons;
- }
-
- for my $wagons ( @{ $self->{wagongroups} } ) {
- my ( $short, $desc ) = $self->wagongroup_description( @{$wagons} );
- my @sections = uniq map { $_->section } @{$wagons};
-
- push(
- @{ $self->{train_descriptions} },
- {
- sections => [@sections],
- short => $short,
- text => $desc,
- }
- );
- }
-
- return @{ $self->{train_descriptions} };
-}
-
-sub wagongroup_description {
- my ( $self, @wagons ) = @_;
-
- my $powertype = $self->wagongroup_powertype(@wagons);
- my @model = $self->wagongroup_model(@wagons);
-
- my $short;
- my $ret = q{};
-
- if (@model) {
- $short = $model[0];
- $ret .= $model[0];
- }
-
- if ( $powertype and $power_desc{$powertype} ) {
- if ( not $ret and $power_desc{$powertype} =~ m{^mit} ) {
- $ret = "Zug";
- }
- $ret .= " $power_desc{$powertype}";
- $short //= $ret;
- $short =~ s{elektrischer }{E-};
- $short =~ s{[Ll]\Kokomotive}{ok};
- }
-
- if ( @model > 1 ) {
- $ret .= " ($model[1])";
- }
-
- return ( $short, $ret );
+ return @{ $self->{sectors} // [] };
}
-sub wagongroup_model {
- my ( $self, @wagons ) = @_;
-
- my $subtype = $self->wagongroup_subtype(@wagons);
-
- if ( $subtype and $model_name{$subtype} ) {
- return @{ $model_name{$subtype} };
- }
- if ($subtype) {
- return $subtype;
- }
- return;
+sub groups {
+ my ($self) = @_;
+ return @{ $self->{groups} // [] };
}
-sub wagongroup_subtype {
- my ( $self, @wagons ) = @_;
-
- if ( not @wagons ) {
- @wagons = $self->wagons;
- }
-
- 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,
- '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,
- '648' => 0,
- 'IC2.TWIN' => 0,
- 'IC2.KISS' => 0,
- );
-
- for my $wagon (@wagons) {
- if ( not $wagon->model ) {
- next;
- }
- if ( $wagon->model == 401
- or ( $wagon->model >= 801 and $wagon->model <= 804 ) )
- {
- $ml{'401'}++;
- }
- elsif ( $wagon->model == 402
- or ( $wagon->model >= 805 and $wagon->model <= 808 ) )
- {
- $ml{'402'}++;
- }
- elsif ( $wagon->model == 403
- and $is_redesign{ substr( $wagon->uic_id, 9, 2 ) } )
- {
- $ml{'403.R'}++;
- }
- elsif ( $wagon->model == 403 and substr( $wagon->uic_id, 9, 2 ) <= 37 )
- {
- $ml{'403.S1'}++;
- }
- elsif ( $wagon->model == 403 and substr( $wagon->uic_id, 9, 2 ) > 37 ) {
- $ml{'403.S2'}++;
- }
- elsif ( $wagon->model == 406 ) {
- $ml{'406'}++;
- }
- elsif ( $wagon->model == 407 ) {
- $ml{'407'}++;
- }
- elsif ( $wagon->model == 408 ) {
- $ml{'408'}++;
- }
- elsif ( $wagon->model == 412 or $wagon->model == 812 ) {
- $ml{'412'}++;
- }
- elsif ( $wagon->model == 411 and substr( $wagon->uic_id, 9, 2 ) <= 32 )
- {
- $ml{'411.S1'}++;
- }
- elsif ( $wagon->model == 411 and substr( $wagon->uic_id, 9, 2 ) > 32 ) {
- $ml{'411.S2'}++;
- }
- elsif ( $wagon->model == 415 ) {
- $ml{'415'}++;
- }
- elsif ( $wagon->model == 422 or $wagon->model == 432 ) {
- $ml{'422'}++;
- }
- elsif ( $wagon->model == 423 or $wagon->model == 433 ) {
- $ml{'423'}++;
- }
- elsif ( $wagon->model == 425 or $wagon->model == 435 ) {
- $ml{'425'}++;
- }
- elsif ( $wagon->model == 427 or $wagon->model == 827 ) {
- $ml{'427'}++;
- }
- elsif ( $wagon->model == 428 or $wagon->model == 828 ) {
- $ml{'428'}++;
- }
- elsif ( $wagon->model == 429 or $wagon->model == 829 ) {
- $ml{'429'}++;
- }
- elsif ( $wagon->model == 430 or $wagon->model == 431 ) {
- $ml{'430'}++;
- }
- elsif ($wagon->model == 440
- or $wagon->model == 441
- or $wagon->model == 841 )
- {
- $ml{'440'}++;
- }
- elsif ($wagon->model == 442
- or $wagon->model == 443 )
- {
- $ml{'442'}++;
- }
- elsif ($wagon->model == 462
- or $wagon->model == 862 )
- {
- $ml{'462'}++;
- }
- elsif ($wagon->model == 463
- or $wagon->model == 863 )
- {
- $ml{'463'}++;
- }
- elsif ( $wagon->model == 445 ) {
- $ml{'445'}++;
- }
- elsif ( $wagon->model == 446 ) {
- $ml{'446'}++;
- }
- elsif ( $wagon->model == 475 ) {
- $ml{'475'}++;
- }
- elsif ( $wagon->model == 612 ) {
- $ml{'612'}++;
- }
- elsif ( $wagon->model == 620 or $wagon->model == 621 ) {
- $ml{'620'}++;
- }
- elsif ( $wagon->model == 622 ) {
- $ml{'622'}++;
- }
- elsif ( $wagon->model == 631 ) {
- $ml{'631'}++;
- }
- elsif ( $wagon->model == 632 ) {
- $ml{'632'}++;
- }
- elsif ( $wagon->model == 633 ) {
- $ml{'633'}++;
- }
- elsif ( $wagon->model == 640 ) {
- $ml{'640'}++;
- }
- elsif ( $wagon->model == 642 ) {
- $ml{'642'}++;
- }
- elsif ( $wagon->model == 643 or $wagon->model == 943 ) {
- $ml{'643'}++;
- }
- elsif ( $wagon->model == 648 ) {
- $ml{'648'}++;
- }
- elsif ( $self->train_type eq 'IC' and $wagon->model == 110 ) {
- $ml{'IC2.KISS'}++;
- }
- elsif ( $self->train_type eq 'IC' and $wagon->is_dosto ) {
- $ml{'IC2.TWIN'}++;
- }
- elsif ( substr( $wagon->uic_id, 4, 4 ) eq '4011' ) {
- $ml{'011'}++;
- }
- }
-
- my @likelihood = reverse sort { $ml{$a} <=> $ml{$b} } keys %ml;
-
- # Less than two wagons are generally inconclusive.
- # Exception: BR 631 (Link I) only has a single wagon
- if ( $ml{ $likelihood[0] } < 2 and $likelihood[0] ne '631' ) {
- return undef;
- }
-
- return $likelihood[0];
+sub carriages {
+ my ($self) = @_;
+ return @{ $self->{carriages} // [] };
}
-sub wagons {
+sub TO_JSON {
my ($self) = @_;
- if ( exists $self->{wagons} ) {
- return @{ $self->{wagons} };
- }
-
- my @wagon_groups;
-
- for my $group ( @{ $self->{data}{istformation}{allFahrzeuggruppe} } ) {
- my @group;
- for my $wagon ( @{ $group->{allFahrzeug} } ) {
- my $wagon_object
- = Travel::Status::DE::DBWagenreihung::Wagon->new( %{$wagon},
- train_no => $group->{verkehrlichezugnummer} );
- push( @{ $self->{wagons} }, $wagon_object );
- push( @group, $wagon_object );
- if ( not $wagon_object->{position}{valid} ) {
- $self->{has_bad_wagons} = 1;
- }
- }
- push( @wagon_groups, [@group] );
- }
- if ( @{ $self->{wagons} // [] } > 1 and not $self->has_bad_wagons ) {
- if ( $self->{wagons}[0]->{position}{start_percent}
- > $self->{wagons}[-1]->{position}{start_percent} )
- {
- $self->{direction} = 100;
- }
- else {
- $self->{direction} = 0;
- }
- }
- if ( not $self->has_bad_wagons ) {
- @{ $self->{wagons} } = sort {
- $a->{position}->{start_percent} <=> $b->{position}->{start_percent}
- } @{ $self->{wagons} };
- }
-
- for my $i ( 0 .. $#wagon_groups ) {
- my $group = $wagon_groups[$i];
- my $tt = $self->wagongroup_subtype( @{$group} );
- if ($tt) {
- for my $wagon ( @{$group} ) {
- $wagon->set_traintype( $i, $tt );
- }
- }
- }
+ my %copy = %{$self};
- $self->{wagongroups} = [@wagon_groups];
+ delete $copy{from_json};
- return @{ $self->{wagons} // [] };
+ return {%copy};
}
-sub get_with_cache {
- my ( $self, $cache, $url ) = @_;
-
- if ( $self->{developer_mode} ) {
- say "GET $url";
- }
-
- if ($cache) {
- my $content = $cache->thaw($url);
- if ($content) {
- if ( $self->{developer_mode} ) {
- say ' cache hit';
- }
- return ( ${$content}, undef );
- }
- }
-
- if ( $self->{developer_mode} ) {
- say ' cache miss';
- }
-
- my $ua = $self->{user_agent};
- my $res = $ua->get($url);
-
- if ( $res->is_error ) {
- return ( undef, $res->status_line );
- }
- my $content = $res->decoded_content;
-
- if ($cache) {
- $cache->freeze( $url, \$content );
- }
-
- return ( $content, undef );
-}
+# }}}
1;
@@ -753,43 +312,43 @@ __END__
=head1 NAME
-Travel::Status::DE::DBWagenreihung - Interface to Deutsche Bahn Wagon Order API.
+Travel::Status::DE::DBWagenreihung - Interface to Deutsche Bahn carriage formation API.
=head1 SYNOPSIS
use Travel::Status::DE::DBWagenreihung;
my $wr = Travel::Status::DE::DBWagenreihung->new(
- departure => 'DateTime or YYYYMMDDhhmm',
- train_number => 1234,
+ eva => 8000080,
+ departure => $datetime,
+ train_type => 'IC',
+ train_number => 2045,
);
- for my $wagon ( $wr->wagons ) {
- printf("Wagen %s: Abschnitt %s\n", $wagon->number // '?', $wagon->section);
+ for my $carriage ( $wr->carriages ) {
+ printf("Wagen %s: Abschnitt %s\n", $carriage->number // '?', $carriage->sector);
}
=head1 VERSION
-version 0.11
+version 0.18
This is beta software. The API may change without notice.
=head1 DESCRIPTION
Travel:Status:DE::DBWagenreihung is an unofficial interface to the Deutsche
-Bahn Wagon Order API at L<https://www.apps-bahn.de/wr/wagenreihung/1.0>. It
-returns station-specific wagon orders for long-distance trains operated by
-Deutsche Bahn. Data includes wagon positions on the platform, the ICE series,
-wagon-specific attributes such as first/second class or family coaches, and the
-internal type and number of each wagon.
+Bahn carriage formation API. It returns station-specific carriage formations
+(also kwnown as coach sequences) for a variety of trains in the rail network
+associated with Deutsche Bahn. Data includes carriage positions on the
+platform, train type (e.g. ICE series), carriage-specific attributes such as
+first/second class, and the internal type and number of each carriage.
-Positions on the platform are given both in meters and per cent (relative to
+Positions on the platform are given both in meters and percent (relative to
platform length).
-At the time of this writing, only ICE trains are officially supported by the
-backend, and even then glitches may occur. IC/EC trains are not officially
-supported; reported wagon orders may be correct, may lack unscheduled changes,
-or may be completely bogus.
+Note that carriage formation data reported by the API is known to be bogus
+from time to time. This module does not perform thorough sanity checking.
=head1 METHODS
@@ -797,120 +356,85 @@ or may be completely bogus.
=item my $wr = Travel::Status::DE::DBWagenreihung->new(I<%opts>)
-Requests wagon order for a specific train at a specific scheduled departure
-time and date, which implicitly encodes the requested station. Use
-L<Travel::Status::DE::IRIS> or similar to map station name and train number
-to scheduled departure.
+Requests carriage formation for a specific train at a specific scheduled
+departure time and date, which implicitly encodes the requested station. Use
+L<Travel::Status::DE::IRIS> or similar to map station name and train number to
+scheduled departure.
Arguments:
=over
-=item B<departure> => I<datetime-obj> | I<YYYYMMDDhhmm>
+=item B<departure> => I<datetime-obj>
-Scheduled departure at the station of interested. Must be either a
-L<DateTime> object or a string in YYYYMMDDhhmm format. Mandatory.
+Scheduled departure at the station of interest. Must be a L<DateTime> object.
+Mandatory.
-=item B<train_number> => I<number>
+=item B<eva> => I<number>
-Train number. Do not include the train type: Use "8" for "EC 8" or
-"100" for "ICE 100".
+EVA ID of the station of interest.
-=back
+=item B<train_type> => I<string>
-=item $wr->destinations
+Train type, e.g. "S" or "ICE".
-Returns a list describing all final destinations of this train. In most
-cases, it contains one element, however, for trains consisting of multiple
-wings, it contains one element for each wing.
-
-Each destination is a hash ref containing the destination B<name> and the
-corresponding platform I<sections> (at the moment, this is a list of section
-identifiers).
-
-This function is subject to change.
+=item B<train_number> => I<number>
-=item $wr->direction
+Train number.
-Gives the train's direction of travel. Returns 0 if the train will depart
-towards position 0 and 100 if the train will depart towards the other platform
-end (mnemonic: towards the 100% position).
+=back
=item $wr->errstr
In case of a fatal HTTP or backend error, returns a string describing it.
Returns undef otherwise.
-=item $wr->origins
-
-Returns a list of stations this train originates from. In most cases, this is
-just one element; however, for trains consisting of multiple wings, it gives
-the origin of each wing unless they are identical.
-
-Each origin is a station name.
-
-This function is subject to change.
-
-=item $wr->platform
-
-Returns the platform name.
-
-=item $wr->sections
-
-Describes the sections of the platform this train will depart from.
-Returns a list of L<Travel::Status::DE::DBWagenreihung::Section> objects.
+=item $wr->groups
-=item $wr->station_ds100
+Returns a list of Travel::Status::DE::DBWagenreihung::Group(3pm) objects which
+describe the groups making up the carriage formation. Individual groups may
+have distinct destinations or train numbers. Each group contains a set of
+carriages.
-Returns the DS100 identifier of the requested station.
+=item $wr->carriages
-=item $wr->station_name
+Describes the individual carriages the train consists of. Returns a list of
+L<Travel::Status::DE::DBWagenreihung::carriage> objects.
-Returns the name of the requested station.
-
-=item $wr->station_uic
+=item $wr->direction
-Returns the international id (UIC ID / IBNR) of the requested station.
+Gives the train's direction of travel. Returns 0 if the train will depart
+towards position 0 and 100 if the train will depart towards the other platform
+end (mnemonic: towards the 100% position).
-=item $wr->train_descriptions
+=item $wr->destinations
-Returns a list of hashes describing the rolling stock used for this train based
-on model and locomotive (if present). Each hash contains the keys B<text>
-(textual representation, see C<< $wr->train_desc >>) and B<sections>
-(arrayref of corresponding sections).
+Returns a list describing the unique destinations of this train's carriage
+groups. Each destination is a hashref that contains its B<name>, a B<groups>
+arrayref to the corresponding Travel::Status::DE::DBWagenreihung::Group(3pm)
+objects, and a B<sections> arrayref to section identifiers (subject to change).
-=item $wr->wagongroup_description
+=item $wr->platform
-Returns two strings describing the rolling stock used for this train based on
-model and locomotive (if present). The first one tries to be conscise (e.g.
-"ICE 4"). The second is more detailed, e.g. "ICE 4 Hochgeschwindigkeitszug",
-"IC 2 Twindexx mit elektrischer Lokomotive", or "Diesel-Triebzug".
+Returns the platform name.
-=item $wr->wagongroup_model
+=item $wr->sectors
-Returns a string describing the rolling stock used for this train, e.g. "ICE 4"
-or "IC2 KISS".
+Describes the sectors of the platform this train will depart from.
+Returns a list of L<Travel::Status::DE::DBWagenreihung::Sector> objects.
=item $wr->train_numbers
-Returns the list of train numbers for this departure. In most cases, this is
-just one element. For trains consisting of multiple wings (which typically have
-different numbers), it contains one element for each wing.
+Returns a list describing the unique train numbers associated with this train's
+carriage groups. Each train number is a hashref that contains its B<name>
+(i.e., number), a B<groups> arrayref to the corresponding
+Travel::Status::DE::DBWagenreihung::Group(3pm) objects, and a B<sections>
+arrayref to section identifiers (subject to change).
=item $wr->train_type
Returns a string describing the train type, e.g. "ICE" or "IC".
-=item $wr->wagongroup_subtype
-
-Returns a string describing the rolling stock model used for this train, e.g.
-"412" (model 412 aka ICE 4) or "411.S2" (model 411 aka ICE T, series 2).
-
-=item $wr->wagons
-
-Describes the individual wagons the train consists of. Returns a list of
-L<Travel::Status::DE::DBWagenreihung::Wagon> objects.
-
=back
=head1 DEPENDENCIES
@@ -933,7 +457,7 @@ L<https://github.com/derf/Travel-Status-DE-DBWagenreihung>
=head1 AUTHOR
-Copyright (C) 2018-2019 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
+Copyright (C) 2018-2024 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
=head1 LICENSE
diff --git a/lib/Travel/Status/DE/DBWagenreihung/Carriage.pm b/lib/Travel/Status/DE/DBWagenreihung/Carriage.pm
new file mode 100644
index 0000000..fc63601
--- /dev/null
+++ b/lib/Travel/Status/DE/DBWagenreihung/Carriage.pm
@@ -0,0 +1,215 @@
+package Travel::Status::DE::DBWagenreihung::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::DBWagenreihung::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/DBWagenreihung/Group.pm b/lib/Travel/Status/DE/DBWagenreihung/Group.pm
new file mode 100644
index 0000000..6b5b6f5
--- /dev/null
+++ b/lib/Travel/Status/DE/DBWagenreihung/Group.pm
@@ -0,0 +1,688 @@
+package Travel::Status::DE::DBWagenreihung::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::DBWagenreihung::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/DBWagenreihung/Section.pm b/lib/Travel/Status/DE/DBWagenreihung/Section.pm
deleted file mode 100644
index c93f2dd..0000000
--- a/lib/Travel/Status/DE/DBWagenreihung/Section.pm
+++ /dev/null
@@ -1,34 +0,0 @@
-package Travel::Status::DE::DBWagenreihung::Section;
-
-use strict;
-use warnings;
-use 5.020;
-use utf8;
-
-use parent 'Class::Accessor';
-
-our $VERSION = '0.11';
-
-Travel::Status::DE::DBWagenreihung::Section->mk_ro_accessors(
- qw(name start_percent end_percent length_percent start_meters end_meters length_meters)
-);
-
-sub new {
- my ( $obj, %opt ) = @_;
- my $ref = \%opt;
-
- $ref->{length_meters} = $ref->{end_meters} - $ref->{start_meters};
- $ref->{length_percent} = $ref->{end_percent} - $ref->{start_percent};
-
- return bless( $ref, $obj );
-}
-
-sub TO_JSON {
- my ($self) = @_;
-
- my %copy = %{$self};
-
- return {%copy};
-}
-
-1;
diff --git a/lib/Travel/Status/DE/DBWagenreihung/Sector.pm b/lib/Travel/Status/DE/DBWagenreihung/Sector.pm
new file mode 100644
index 0000000..31d4c30
--- /dev/null
+++ b/lib/Travel/Status/DE/DBWagenreihung/Sector.pm
@@ -0,0 +1,51 @@
+package Travel::Status::DE::DBWagenreihung::Sector;
+
+use strict;
+use warnings;
+use 5.020;
+use utf8;
+
+use parent 'Class::Accessor';
+
+our $VERSION = '0.18';
+
+Travel::Status::DE::DBWagenreihung::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;
diff --git a/lib/Travel/Status/DE/DBWagenreihung/Wagon.pm b/lib/Travel/Status/DE/DBWagenreihung/Wagon.pm
deleted file mode 100644
index ffc3e58..0000000
--- a/lib/Travel/Status/DE/DBWagenreihung/Wagon.pm
+++ /dev/null
@@ -1,267 +0,0 @@
-package Travel::Status::DE::DBWagenreihung::Wagon;
-
-use strict;
-use warnings;
-use 5.020;
-use utf8;
-
-use parent 'Class::Accessor';
-use Carp qw(cluck);
-
-our $VERSION = '0.11';
-Travel::Status::DE::DBWagenreihung::Wagon->mk_ro_accessors(
- qw(attributes class_type group_index has_ac has_accessibility
- has_bahn_comfort has_bike_storage has_bistro has_compartments
- has_family_area has_phone_area has_quiet_area is_closed is_dosto
- is_interregio is_locomotive is_powercar number model multipurpose section
- train_no train_subtype type uic_id)
-);
-
-our %type_attributes = (
- 'ICE 1' => [
- undef, ['has_quiet_area'], undef, ['has_quiet_area'], # 1 2 3 4
- ['has_family_area'], undef, ['has_bahn_comfort'], # 5 6 7
- undef, undef, undef, ['has_bahn_comfort'], # 8 9 (10) 11
- ['has_quiet_area'], undef, undef # 12 (13) 14
- ],
- 'ICE 2' => [
- undef, ['has_quiet_area'], ['has_bahn_comfort'],
- ['has_family_area'], # 1 2 3 4
- undef, ['has_bahn_comfort'],
- [ 'has_quiet_area', 'has_phone_area' ] # 5 6 7
- ],
- 'ICE 3' => [
- ['has_quiet_area'], undef, undef, undef, # 1 2 3 (4)
- ['has_family_area'], undef, ['has_bahn_comfort'], # 5 6 7
- [ 'has_quiet_area', 'has_phone_area', 'has_bahn_comfort' ], undef # 8 9
- ],
- 'ICE 3 Velaro' => [
- ['has_quiet_area'], undef, undef, ['has_family_area'], # 1 2 3 4
- ['has_bahn_comfort'], ['has_bahn_comfort'], undef, undef, # 5 6 (7) 8
- [ 'has_quiet_area', 'has_phone_area' ] # 9
- ],
- 'ICE 4' => [
- ['has_bike_storage'], undef, ['has_quiet_area'], undef,
- undef, # 1 2 3 4 5
- undef, ['has_bahn_comfort'], undef, ['has_family_area'], # 6 7 (8) 9
- undef, ['has_bahn_comfort'], undef, undef,
- ['has_quiet_area'] # 10 11 12 (13) 14
- ],
- 'ICE T 411' => [
- ['has_quiet_area'], ['has_quiet_area'], undef,
- ['has_family_area'], # 1 2 3 4
- undef, undef, ['has_bahn_comfort'],
- [ 'has_quiet_area', 'has_bahn_comfort' ] # (5) 6 7 8
- ],
- 'ICE T 415' => [
- ['has_quiet_area'], ['has_quiet_area'], ['has_bahn_comfort'],
- undef, # 1 2 3 (4)
- undef, undef, ['has_family_area'],
- [ 'has_quiet_area', 'has_bahn_comfort' ] # (5) (6) 7 8
- ],
- 'IC2 Twindexx' => [
- [ 'has_family_area', 'has_bike_storage' ], ['has_bike_storage'], # 1 2
- ['has_bike_storage'], [ 'has_bike_storage', 'has_bahn_comfort' ], # 3 4
- [ 'has_bahn_comfort', 'has_quiet_area', 'has_phone_area' ] # 5
- ],
-);
-
-sub new {
- my ( $obj, %opt ) = @_;
- my $ref = {};
-
- $ref->{class_type} = 0;
- $ref->{has_bistro} = 0;
- $ref->{is_locomotive} = 0;
- $ref->{is_powercar} = 0;
- $ref->{is_closed} = 0;
- $ref->{train_no} = $opt{train_no};
- $ref->{number} = $opt{wagenordnungsnummer};
- $ref->{model} = $opt{fahrzeugnummer};
- $ref->{uic_id} = $opt{fahrzeugnummer};
- $ref->{section} = $opt{fahrzeugsektor};
- $ref->{type} = $opt{fahrzeugtyp};
-
- $ref->{model} =~ s{^.....(...)....$}{$1} or $ref->{model} = undef;
-
- my $self = bless( $ref, $obj );
-
- $self->parse_type;
-
- if ( $opt{status} and $opt{status} eq 'GESCHLOSSEN' ) {
- $ref->{is_closed} = 1;
- }
-
- if ( $opt{kategorie} =~ m{SPEISEWAGEN} ) {
- $ref->{has_bistro} = 1;
- }
- elsif ( $opt{kategorie} eq 'LOK' ) {
- $ref->{is_locomotive} = 1;
- }
- elsif ( $opt{kategorie} eq 'TRIEBKOPF' ) {
- $ref->{is_powercar} = 1;
- }
-
- if ( $opt{fahrzeugtyp} =~ m{AB} ) {
- $ref->{class_type} = 12;
- }
- elsif ( $opt{fahrzeugtyp} =~ m{A} ) {
- $ref->{class_type} = 1;
- }
- elsif ( $opt{fahrzeugtyp} =~ m{B|WR} ) {
- $ref->{class_type} = 2;
- }
-
- my $pos = $opt{positionamhalt};
-
- $ref->{position}{start_percent} = $pos->{startprozent};
- $ref->{position}{end_percent} = $pos->{endeprozent};
- $ref->{position}{start_meters} = $pos->{startmeter};
- $ref->{position}{end_meters} = $pos->{endemeter};
-
- if ( $pos->{startprozent} eq ''
- or $pos->{endeprozent} eq ''
- or $pos->{startmeter} eq ''
- or $pos->{endemeter} 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} ) {
- $self->{is_interregio} = 1;
- 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 set_traintype {
- my ( $self, $group_index, $tt ) = @_;
-
- $self->{group_index} = $group_index;
- $self->{train_subtype} = $tt;
-
- if ( not $self->{number} or not exists( $type_attributes{$tt} ) ) {
- return;
- }
-
- if ( $self->{number} !~ m{^\d+$} ) {
- return;
- }
-
- my $index = $self->{number} - 1;
-
- if ( $index >= 30 ) {
- $index -= 30;
- }
- elsif ( $index >= 20 ) {
- $index -= 20;
- }
-
- if ( not $type_attributes{$tt}[$index] ) {
- return;
- }
-
- for my $attr ( @{ $type_attributes{$tt}[$index] } ) {
- $self->{$attr} = 1;
- }
-}
-
-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;