diff options
Diffstat (limited to 'lib/DBInfoscreen')
| -rw-r--r-- | lib/DBInfoscreen/Controller/Map.pm | 475 | 
1 files changed, 328 insertions, 147 deletions
| diff --git a/lib/DBInfoscreen/Controller/Map.pm b/lib/DBInfoscreen/Controller/Map.pm index d618e8c..7013b63 100644 --- a/lib/DBInfoscreen/Controller/Map.pm +++ b/lib/DBInfoscreen/Controller/Map.pm @@ -10,8 +10,17 @@ use Geo::Distance;  my $dbf_version = qx{git describe --dirty} || 'experimental'; +my $strp = DateTime::Format::Strptime->new( +	pattern   => '%Y-%m-%dT%H:%M:%S.000%z', +	time_zone => 'Europe/Berlin', +); +  chomp $dbf_version; +# Input: (HAFAS TripID, line number) +# Output: Promise returning a +# https://github.com/public-transport/hafas-client/blob/4/docs/trip.md instance +# on success  sub get_hafas_polyline_p {  	my ( $self, $trip_id, $line ) = @_; @@ -63,7 +72,19 @@ sub get_hafas_polyline_p {  	return $promise;  } -sub estimate_train_position { +# Input: +#   now: DateTime +#   from: current/previous stop +#         {dep => DateTime, name => str, lat => float, lon => float} +#   to: next stop +#       {arr => DateTime, name => str, lat => float, lon => float} +#   features: https://github.com/public-transport/hafas-client/blob/4/docs/trip.md features array +# Output: list of estimated train positions in [lat, lon] format. +# - current position +# - position 2 seconds from now +# - position 4 seconds from now +# - ... +sub estimate_train_positions {  	my (%opt) = @_;  	my $now = $opt{now}; @@ -74,8 +95,13 @@ sub estimate_train_position {  	my $to_name   = $opt{to}{name};  	my $features  = $opt{features}; -	my $route_part_completion_ratio -	  = ( $now->epoch - $from_dt->epoch ) / ( $to_dt->epoch - $from_dt->epoch ); +	my @train_positions; + +	my $time_complete = $now->epoch - $from_dt->epoch; +	my $time_total    = $to_dt->epoch - $from_dt->epoch; + +	my @completion_ratios +	  = map { ( $time_complete + ( $_ * 2 ) ) / $time_total } ( 0 .. 45 );  	my $geo = Geo::Distance->new;  	my ( $from_index, $to_index ); @@ -109,32 +135,214 @@ sub estimate_train_position {  				);  			}  		} -		my $marker_distance = $total_distance * $route_part_completion_ratio; +		my @marker_distances = map { $total_distance * $_ } @completion_ratios;  		$total_distance = 0;  		for my $j ( $from_index + 1 .. $to_index ) {  			my $prev = $features->[ $j - 1 ]{geometry}{coordinates};  			my $this = $features->[$j]{geometry}{coordinates};  			if ( $prev and $this ) { +				my $prev_distance = $total_distance;  				$total_distance += $geo->distance(  					'kilometer', $prev->[0], $prev->[1],  					$this->[0],  $this->[1]  				); -			} -			if ( $total_distance > $marker_distance ) { +				for my $i ( @train_positions .. $#marker_distances ) { +					my $marker_distance = $marker_distances[$i]; +					if ( $total_distance > $marker_distance ) { + +						# completion ratio for the line between (prev, this) +						my $sub_ratio = 1; +						if ( $total_distance != $prev_distance ) { +							$sub_ratio = ( $marker_distance - $prev_distance ) +							  / ( $total_distance - $prev_distance ); +						} -				# return (lat, lon) -				return ( $this->[1], $this->[0] ); +						my $lat = $prev->[1] +						  + ( $this->[1] - $prev->[1] ) * $sub_ratio; +						my $lon = $prev->[0] +						  + ( $this->[0] - $prev->[0] ) * $sub_ratio; + +						push( @train_positions, [ $lat, $lon ] ); +					} +				} +				if ( @train_positions == @completion_ratios ) { +					return @train_positions; +				}  			}  		} +		if (@train_positions) { +			return @train_positions; +		}  	}  	else { -		my $lat = $opt{from}{lat} -		  + ( $opt{to}{lat} - $opt{from}{lat} ) * $route_part_completion_ratio; -		my $lon = $opt{from}{lon} -		  + ( $opt{to}{lon} - $opt{from}{lon} ) * $route_part_completion_ratio; -		return ( $lat, $lon ); +		for my $ratio (@completion_ratios) { +			my $lat +			  = $opt{from}{lat} + ( $opt{to}{lat} - $opt{from}{lat} ) * $ratio; +			my $lon +			  = $opt{from}{lon} + ( $opt{to}{lon} - $opt{from}{lon} ) * $ratio; +			push( @train_positions, [ $lat, $lon ] ); +		} +		return @train_positions; +	} +	return [ $opt{to}{lat}, $opt{to}{lon} ]; +} + +# Input: +#   now: DateTime +#   route: hash +#     lat: float +#     lon: float +#     name: str +#     arr: DateTime +#     dep: DateTime +#   features: ref to transport.rest features list +#  Output: +#    next_stop: {type, station} +#    positions: [current position [lat, lon], 2s from now, 4s from now, ...] +sub estimate_train_positions2 { +	my (%opt) = @_; +	my $now   = $opt{now}; +	my @route = @{ $opt{route} // [] }; + +	my @train_positions; +	my $next_stop; + +	for my $i ( 1 .. $#route ) { +		if (    $route[$i]{arr} +			and $route[ $i - 1 ]{dep} +			and $now > $route[ $i - 1 ]{dep} +			and $now < $route[$i]{arr} ) +		{ + +			# (current position, future positons...) in 2 second steps +			@train_positions = estimate_train_positions( +				from     => $route[ $i - 1 ], +				to       => $route[$i], +				now      => $now, +				features => $opt{features}, +			); + +			$next_stop = { +				type    => 'next', +				station => $route[$i], +			}; +			last; +		} +		if ( $route[ $i - 1 ]{dep} and $now <= $route[ $i - 1 ]{dep} ) { +			@train_positions +			  = ( [ $route[ $i - 1 ]{lat}, $route[ $i - 1 ]{lon} ] ); +			$next_stop = { +				type    => 'present', +				station => $route[ $i - 1 ], +			}; +			last; +		}  	} -	return ( $opt{to}{lat}, $opt{to}{lon} ); + +	if ( not $next_stop ) { +		@train_positions = ( [ $route[-1]{lat}, $route[-1]{lon} ] ); +		$next_stop       = { +			type    => 'present', +			station => $route[-1] +		}; +	} + +	my $position_now = shift @train_positions; + +	return { +		next_stop    => $next_stop, +		position_now => $position_now, +		positions    => \@train_positions, +	}; +} + +sub route_to_ajax { +	my (@stopovers) = @_; + +	my @route_entries; + +	for my $stop (@stopovers) { +		my @stop_entries = ( $stop->{stop}{name} ); +		my $platform; + +		if ( $stop->{arrival} +			and my $arr = $strp->parse_datetime( $stop->{arrival} ) ) +		{ +			my $delay = ( $stop->{arrivalDelay} // 0 ) / 60; +			$platform = $stop->{arrivalPlatform}; + +			push( @stop_entries, $arr->epoch, $delay ); +		} +		else { +			push( @stop_entries, q{}, q{} ); +		} + +		if ( $stop->{departure} +			and my $dep = $strp->parse_datetime( $stop->{departure} ) ) +		{ +			my $delay = ( $stop->{departureDelay} // 0 ) / 60; +			$platform //= $stop->{departurePlatform} // q{}; + +			push( @stop_entries, $dep->epoch, $delay, $platform ); +		} +		else { +			push( @stop_entries, q{}, q{}, q{} ); +		} + +		push( @route_entries, join( ';', @stop_entries ) ); +	} + +	return join( '|', @route_entries ); +} + +# Input: List of transport.rest stopovers +# Output: List of preprocessed stops. Each is a hash with the following keys: +#   lat: float +#   lon: float +#   name: str +#   arr: DateTime +#   dep: DateTime +#   arr_delay: int +#   dep_delay: int +#   platform: str +sub stopovers_to_route { +	my (@stopovers) = @_; +	my @route; + +	for my $stop (@stopovers) { +		my @stop_lines = ( $stop->{stop}{name} ); +		my ( $platform, $arr, $dep, $arr_delay, $dep_delay ); + +		if (    $stop->{arrival} +			and $arr = $strp->parse_datetime( $stop->{arrival} ) ) +		{ +			$arr_delay = ( $stop->{arrivalDelay} // 0 ) / 60; +			$platform //= $stop->{arrivalPlatform}; +		} + +		if (    $stop->{departure} +			and $dep = $strp->parse_datetime( $stop->{departure} ) ) +		{ +			$dep_delay = ( $stop->{departureDelay} // 0 ) / 60; +			$platform //= $stop->{departurePlatform}; +		} + +		push( +			@route, +			{ +				lat       => $stop->{stop}{location}{latitude}, +				lon       => $stop->{stop}{location}{longitude}, +				name      => $stop->{stop}{name}, +				arr       => $arr, +				dep       => $dep, +				arr_delay => $arr_delay, +				dep_delay => $dep_delay, +				platform  => $platform, +			} +		); + +	} +	return @route;  }  sub route { @@ -154,17 +362,13 @@ sub route {  			my @polyline = @{ $pl->{polyline} };  			my @line_pairs;  			my @station_coordinates; -			my @route;  			my @markers;  			my $next_stop; -			my $now  = DateTime->now( time_zone => 'Europe/Berlin' ); -			my $strp = DateTime::Format::Strptime->new( -				pattern   => '%Y-%m-%dT%H:%M:%S.000%z', -				time_zone => 'Europe/Berlin', -			); +			my $now = DateTime->now( time_zone => 'Europe/Berlin' ); +			# @line_pairs are used to draw the train's journey on the map  			for my $i ( 1 .. $#polyline ) {  				push(  					@line_pairs, @@ -175,164 +379,85 @@ sub route {  				);  			} -			for my $stop ( @{ $pl->{raw}{stopovers} // [] } ) { -				my @stop_lines = ( $stop->{stop}{name} ); -				my ( $platform, $arr, $dep, $arr_delay, $dep_delay ); +			my @route = stopovers_to_route( @{ $pl->{raw}{stopovers} // [] } ); + +			# Prepare from/to markers and name/time/delay overlays for stations +			for my $stop (@route) { +				my @stop_lines = ( $stop->{name} ); -				if ( $from_name and $stop->{stop}{name} eq $from_name ) { +				if ( $from_name and $stop->{name} eq $from_name ) {  					push(  						@markers,  						{ -							lon   => $stop->{stop}{location}{longitude}, -							lat   => $stop->{stop}{location}{latitude}, -							title => $stop->{stop}{name}, +							lon   => $stop->{lon}, +							lat   => $stop->{lat}, +							title => $stop->{name},  							icon  => 'goldIcon',  						}  					);  				} -				if ( $to_name and $stop->{stop}{name} eq $to_name ) { +				if ( $to_name and $stop->{name} eq $to_name ) {  					push(  						@markers,  						{ -							lon   => $stop->{stop}{location}{longitude}, -							lat   => $stop->{stop}{location}{latitude}, -							title => $stop->{stop}{name}, +							lon   => $stop->{lon}, +							lat   => $stop->{lat}, +							title => $stop->{name},  							icon  => 'greenIcon',  						}  					);  				} -				if (    $stop->{arrival} -					and $arr = $strp->parse_datetime( $stop->{arrival} ) ) -				{ -					$arr_delay = ( $stop->{arrivalDelay} // 0 ) / 60; -					$platform //= $stop->{arrivalPlatform}; -					my $arr_line = $arr->strftime('Ankunft: %H:%M'); -					if ($arr_delay) { -						$arr_line .= sprintf( ' (%+d)', $arr_delay ); +				if ( $stop->{platform} ) { +					push( @stop_lines, 'Gleis ' . $stop->{platform} ); +				} +				if ( $stop->{arr} ) { +					my $arr_line = $stop->{arr}->strftime('Ankunft: %H:%M'); +					if ( $stop->{arr_delay} ) { +						$arr_line .= sprintf( ' (%+d)', $stop->{arr_delay} );  					}  					push( @stop_lines, $arr_line );  				} - -				if (    $stop->{departure} -					and $dep = $strp->parse_datetime( $stop->{departure} ) ) -				{ -					$dep_delay = ( $stop->{departureDelay} // 0 ) / 60; -					$platform //= $stop->{departurePlatform}; -					my $dep_line = $dep->strftime('Abfahrt: %H:%M'); -					if ($dep_delay) { -						$dep_line .= sprintf( ' (%+d)', $dep_delay ); +				if ( $stop->{dep} ) { +					my $dep_line = $stop->{dep}->strftime('Abfahrt: %H:%M'); +					if ( $stop->{dep_delay} ) { +						$dep_line .= sprintf( ' (%+d)', $stop->{dep_delay} );  					}  					push( @stop_lines, $dep_line );  				} -				if ($platform) { -					splice( @stop_lines, 1, 0, "Gleis $platform" ); -				} - -				push( -					@station_coordinates, -					[ -						[ -							$stop->{stop}{location}{latitude}, -							$stop->{stop}{location}{longitude} -						], -						[@stop_lines], -					] -				); -				push( -					@route, -					{ -						lat       => $stop->{stop}{location}{latitude}, -						lon       => $stop->{stop}{location}{longitude}, -						name      => $stop->{stop}{name}, -						arr       => $arr, -						dep       => $dep, -						arr_delay => $arr_delay, -						dep_delay => $dep_delay, -						platform  => $platform, -					} -				); - +				push( @station_coordinates, +					[ [ $stop->{lat}, $stop->{lon} ], [@stop_lines], ] );  			} -			for my $i ( 1 .. $#route ) { -				if (    $route[$i]{arr} -					and $route[ $i - 1 ]{dep} -					and $now > $route[ $i - 1 ]{dep} -					and $now < $route[$i]{arr} ) -				{ - -					my $title = $pl->{name}; -					if ( $route[$i]{arr_delay} ) { -						$title .= sprintf( ' (%+d)', $route[$i]{arr_delay} ); -					} - -					my ( $train_lat, $train_lon ) = estimate_train_position( -						from     => $route[ $i - 1 ], -						to       => $route[$i], -						now      => $now, -						features => $pl->{raw}{polyline}{features}, -					); - -					push( -						@markers, -						{ -							lat   => $train_lat, -							lon   => $train_lon, -							title => $title -						} -					); +			my $train_pos = estimate_train_positions2( +				now      => $now, +				route    => \@route, +				features => $pl->{raw}{polyline}{features}, +			); -					$next_stop = { -						type    => 'next', -						station => $route[$i], -					}; -					last; -				} -				if ( $route[ $i - 1 ]{dep} and $now <= $route[ $i - 1 ]{dep} ) { -					my $title = $pl->{name}; -					if ( $route[$i]{arr_delay} ) { -						$title .= sprintf( ' (%+d)', $route[$i]{arr_delay} ); -					} -					push( -						@markers, -						{ -							lat   => $route[ $i - 1 ]{lat}, -							lon   => $route[ $i - 1 ]{lon}, -							title => $title -						} -					); -					$next_stop = { -						type    => 'present', -						station => $route[ $i - 1 ], -					}; -					last; +			push( +				@markers, +				{ +					lat   => $train_pos->{position_now}[0], +					lon   => $train_pos->{position_now}[1], +					title => $pl->{name}  				} -			} -			if ( not @markers ) { -				push( -					@markers, -					{ -						lat   => $route[-1]{lat}, -						lon   => $route[-1]{lon}, -						title => $route[-1]{name} . ' - Endstation', -					} -				); -				$next_stop = { -					type    => 'present', -					station => $route[-1] -				}; -			} +			); +			$next_stop = $train_pos->{next_stop};  			$self->render(  				'route_map', -				title     => $pl->{name}, -				hide_opts => 1, -				with_map  => 1, -				origin    => { +				title      => $pl->{name}, +				hide_opts  => 1, +				with_map   => 1, +				ajax_req   => "${trip_id}/${line_no}", +				ajax_route => route_to_ajax( @{ $pl->{raw}{stopovers} // [] } ), +				ajax_polyline => join( '|', +					map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ), +				origin => {  					name => $pl->{raw}{origin}{name}, -					ts   => $pl->{raw}{dep_line} +					ts   => $pl->{raw}{departure}  					? scalar $strp->parse_datetime( $pl->{raw}{departure} )  					: undef,  				}, @@ -374,4 +499,60 @@ sub route {  	)->wait;  } +sub ajax_route { +	my ($self)  = @_; +	my $trip_id = $self->stash('tripid'); +	my $line_no = $self->stash('lineno'); + +	delete $self->stash->{layout}; + +	$self->render_later; + +	$self->get_hafas_polyline_p( $trip_id, $line_no )->then( +		sub { +			my ($pl) = @_; + +			my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + +			my @route = stopovers_to_route( @{ $pl->{raw}{stopovers} // [] } ); + +			my $train_pos = estimate_train_positions2( +				now      => $now, +				route    => \@route, +				features => $pl->{raw}{polyline}{features}, +			); + +			my @polyline = @{ $pl->{polyline} }; +			$self->render( +				'_map_infobox', +				ajax_req   => "${trip_id}/${line_no}", +				ajax_route => route_to_ajax( @{ $pl->{raw}{stopovers} // [] } ), +				ajax_polyline => join( '|', +					map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ), +				origin => { +					name => $pl->{raw}{origin}{name}, +					ts   => $pl->{raw}{departure} +					? scalar $strp->parse_datetime( $pl->{raw}{departure} ) +					: undef, +				}, +				destination => { +					name => $pl->{raw}{destination}{name}, +					ts   => $pl->{raw}{arrival} +					? scalar $strp->parse_datetime( $pl->{raw}{arrival} ) +					: undef, +				}, +				next_stop => $train_pos->{next_stop}, +			); +		} +	)->catch( +		sub { +			my ($err) = @_; +			$self->render( +				'_error', +				error => $err, +			); +		} +	)->wait; +} +  1; | 
