Commit 7789f820 authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files

animate train position in map

parent c1812cec
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -315,6 +315,7 @@ sub startup {

	$r->get('/_wr/:train/:departure')->to('wagenreihung#wagenreihung');

	$r->get('/_ajax_mapinfo/:tripid/:lineno')->to('map#ajax_route');
	$r->get('/map/:tripid/:lineno')->to('map#route');

	$self->defaults( layout => 'app' );
+328 −147
Original line number Diff line number Diff line
@@ -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,109 +135,189 @@ 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]
				);
			}
				for my $i ( @train_positions .. $#marker_distances ) {
					my $marker_distance = $marker_distances[$i];
					if ( $total_distance > $marker_distance ) {

				# return (lat, lon)
				return ( $this->[1], $this->[0] );
						# 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 );
						}

						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 ] );
					}
				}
	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 );
				if ( @train_positions == @completion_ratios ) {
					return @train_positions;
				}
	return ( $opt{to}{lat}, $opt{to}{lon} );
			}
		}
		if (@train_positions) {
			return @train_positions;
		}
	}
	else {
		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} // [] };

sub route {
	my ($self)  = @_;
	my $trip_id = $self->stash('tripid');
	my $line_no = $self->stash('lineno');

	my $from_name = $self->param('from');
	my $to_name   = $self->param('to');
	my @train_positions;
	my $next_stop;

	$self->render_later;
	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} )
		{

	$self->get_hafas_polyline_p( $trip_id, $line_no )->then(
		sub {
			my ($pl) = @_;
			# (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},
			);

			my @polyline = @{ $pl->{polyline} };
			my @line_pairs;
			my @station_coordinates;
			my @route;
			$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;
		}
	}

			my @markers;
			my $next_stop;
	if ( not $next_stop ) {
		@train_positions = ( [ $route[-1]{lat}, $route[-1]{lon} ] );
		$next_stop       = {
			type    => 'present',
			station => $route[-1]
		};
	}

			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 $position_now = shift @train_positions;

			for my $i ( 1 .. $#polyline ) {
				push(
					@line_pairs,
					[
						[ $polyline[ $i - 1 ][1], $polyline[ $i - 1 ][0] ],
						[ $polyline[$i][1],       $polyline[$i][0] ]
					]
				);
	return {
		next_stop    => $next_stop,
		position_now => $position_now,
		positions    => \@train_positions,
	};
}

			for my $stop ( @{ $pl->{raw}{stopovers} // [] } ) {
				my @stop_lines = ( $stop->{stop}{name} );
				my ( $platform, $arr, $dep, $arr_delay, $dep_delay );
sub route_to_ajax {
	my (@stopovers) = @_;

				if ( $from_name and $stop->{stop}{name} eq $from_name ) {
					push(
						@markers,
	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} ) )
		{
							lon   => $stop->{stop}{location}{longitude},
							lat   => $stop->{stop}{location}{latitude},
							title => $stop->{stop}{name},
							icon  => 'goldIcon',
			my $delay = ( $stop->{arrivalDelay} // 0 ) / 60;
			$platform = $stop->{arrivalPlatform};

			push( @stop_entries, $arr->epoch, $delay );
		}
					);
		else {
			push( @stop_entries, q{}, q{} );
		}
				if ( $to_name and $stop->{stop}{name} eq $to_name ) {
					push(
						@markers,

		if ( $stop->{departure}
			and my $dep = $strp->parse_datetime( $stop->{departure} ) )
		{
							lon   => $stop->{stop}{location}{longitude},
							lat   => $stop->{stop}{location}{latitude},
							title => $stop->{stop}{name},
							icon  => 'greenIcon',
			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};
					my $arr_line = $arr->strftime('Ankunft: %H:%M');
					if ($arr_delay) {
						$arr_line .= sprintf( ' (%+d)', $arr_delay );
					}
					push( @stop_lines, $arr_line );
		}

		if (    $stop->{departure}
@@ -219,27 +325,8 @@ sub route {
		{
			$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 );
					}
					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,
			{
@@ -255,84 +342,122 @@ sub route {
		);

	}
	return @route;
}

			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} )
				{
sub route {
	my ($self)  = @_;
	my $trip_id = $self->stash('tripid');
	my $line_no = $self->stash('lineno');

					my $title = $pl->{name};
					if ( $route[$i]{arr_delay} ) {
						$title .= sprintf( ' (%+d)', $route[$i]{arr_delay} );
					}
	my $from_name = $self->param('from');
	my $to_name   = $self->param('to');

					my ( $train_lat, $train_lon ) = estimate_train_position(
						from     => $route[ $i - 1 ],
						to       => $route[$i],
						now      => $now,
						features => $pl->{raw}{polyline}{features},
	$self->render_later;

	$self->get_hafas_polyline_p( $trip_id, $line_no )->then(
		sub {
			my ($pl) = @_;

			my @polyline = @{ $pl->{polyline} };
			my @line_pairs;
			my @station_coordinates;

			my @markers;
			my $next_stop;

			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,
					[
						[ $polyline[ $i - 1 ][1], $polyline[ $i - 1 ][0] ],
						[ $polyline[$i][1],       $polyline[$i][0] ]
					]
				);
			}

			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->{name} eq $from_name ) {
					push(
						@markers,
						{
							lat   => $train_lat,
							lon   => $train_lon,
							title => $title
							lon   => $stop->{lon},
							lat   => $stop->{lat},
							title => $stop->{name},
							icon  => 'goldIcon',
						}
					);

					$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} );
				}
				if ( $to_name and $stop->{name} eq $to_name ) {
					push(
						@markers,
						{
							lat   => $route[ $i - 1 ]{lat},
							lon   => $route[ $i - 1 ]{lon},
							title => $title
							lon   => $stop->{lon},
							lat   => $stop->{lat},
							title => $stop->{name},
							icon  => 'greenIcon',
						}
					);
					$next_stop = {
						type    => 'present',
						station => $route[ $i - 1 ],
					};
					last;
				}

				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} );
					}
			if ( not @markers ) {
					push( @stop_lines, $arr_line );
				}
				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 );
				}

				push( @station_coordinates,
					[ [ $stop->{lat}, $stop->{lon} ], [@stop_lines], ] );
			}

			my $train_pos = estimate_train_positions2(
				now      => $now,
				route    => \@route,
				features => $pl->{raw}{polyline}{features},
			);

			push(
				@markers,
				{
						lat   => $route[-1]{lat},
						lon   => $route[-1]{lon},
						title => $route[-1]{name} . ' - Endstation',
					lat   => $train_pos->{position_now}[0],
					lon   => $train_pos->{position_now}[1],
					title => $pl->{name}
				}
			);
				$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,
				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;
+81 −0
Original line number Diff line number Diff line
var j_reqid;
//var j_stops = [];
var j_positions = [];
var j_frame = [];
var j_frame_i = [];

function dbf_map_parse() {
	$('#jdata').each(function() {
		j_reqid = $(this).data('req');
		/*var route_data = $(this).data('route');
		if (route_data) {
			route_data = route_data.split('|');
			j_stops = [];
			for (var stop_id in route_data) {
				var stopdata = route_data[stop_id].split(';');
				for (var i = 1; i < 5; i++) {
					stopdata[i] = parseInt(stopdata[i]);
				}
				j_stops.push(stopdata);
			}
		}*/
		var positions = $(this).data('poly');
		if (positions) {
			positions = positions.split('|');
			j_positions = [];
			for (var pos_id in positions) {
				var posdata = positions[pos_id].split(';');
				posdata[0] = parseFloat(posdata[0]);
				posdata[1] = parseFloat(posdata[1]);
				j_positions.push(posdata);
			}
		}
	});
}

function dbf_anim_coarse() {
	if (j_positions.length) {
		var pos1 = marker.getLatLng();
		var pos1lat = pos1.lat;
		var pos1lon = pos1.lng;
		var pos2 = j_positions.shift();
		var pos2lat = pos2[0];
		var pos2lon = pos2[1];

		j_frame_i = 200;
		j_frame = [];

		// approx 30 Hz -> 60 frames per 2 seconds
		for (var i = 1; i <= 60; i++) {
			var ratio = i / 60;
			j_frame.push([pos1lat + ((pos2lat - pos1lat) * ratio), pos1lon + ((pos2lon - pos1lon) * ratio)]);
		}

		j_frame_i = 0;
	}
}

function dbf_anim_fine() {
	if (j_frame[j_frame_i]) {
		marker.setLatLng(j_frame[j_frame_i++]);
	}
}

function dbf_map_reload() {
	$.get('/_ajax_mapinfo/' + j_reqid, function(data) {
		$('#infobox').html(data);
		dbf_map_parse();
		setTimeout(dbf_map_reload, 61000);
	}).fail(function() {
		setTimeout(dbf_map_reload, 5000);
	});
}

$(document).ready(function() {
	if ($('#infobox').length) {
		dbf_map_parse();
		setInterval(dbf_anim_coarse, 2000);
		setInterval(dbf_anim_fine, 33);
		setTimeout(dbf_map_reload, 61000);
	}
});
+1 −0
Original line number Diff line number Diff line
function dbf_map_parse(){$("#jdata").each(function(){j_reqid=$(this).data("req");var a=$(this).data("poly");if(a){a=a.split("|"),j_positions=[];for(var e in a){var i=a[e].split(";");i[0]=parseFloat(i[0]),i[1]=parseFloat(i[1]),j_positions.push(i)}}})}function dbf_anim_coarse(){if(j_positions.length){var a=marker.getLatLng(),e=a.lat,i=a.lng,_=j_positions.shift(),t=_[0],r=_[1];j_frame_i=200,j_frame=[];for(var f=1;f<=60;f++){var n=f/60;j_frame.push([e+(t-e)*n,i+(r-i)*n])}j_frame_i=0}}function dbf_anim_fine(){j_frame[j_frame_i]&&marker.setLatLng(j_frame[j_frame_i++])}function dbf_map_reload(){$.get("/_ajax_mapinfo/"+j_reqid,function(a){$("#infobox").html(a),dbf_map_parse(),setTimeout(dbf_map_reload,61e3)}).fail(function(){setTimeout(dbf_map_reload,5e3)})}var j_reqid,j_positions=[],j_frame=[],j_frame_i=[];$(document).ready(function(){$("#infobox").length&&(dbf_map_parse(),setInterval(dbf_anim_coarse,2e3),setInterval(dbf_anim_fine,33),setTimeout(dbf_map_reload,61e3))});
+5 −0
Original line number Diff line number Diff line
<div class="error"><strong>Fehler:</strong>
<pre>
%= $error
</pre>
</div>
Loading