I attended a FOSS4G 2014 workshop by Rafa Gutierrez on Leaflet and Mapbox JavaScript API Fundamentals. Not too long ago I came across one of Rafa's posts entitled GoPro video maps that got me thinking more about combining map and video content.

An animated marker moving along a cycle route with associated GoPro video view, video and animation. I got really excited about the possibilities and immediately started cooking up some ideas.

 

Functionality

  1. Adjust animation marker speed - It is unlikely that a marker will travel at a constant speed from start to end, stop and start for observations and speed fluctuations depending on terrain or other factors are likely to be a reality.
  2. An unobstructed view of the animated path - Place video to the side of the main map content and animation path.
  3. Control video based on interaction with map - Introduction of map click nearest to and marker drag and snap functionality, both of which update the video to the time associated with the nearest location.
  4. Control map based on interaction with video - Enter HTML5 video API. Play, pause and seek functionality controls marker behavior on map.
  5. Rotate marker to reflect direction of sight - The thinking here was, how can I support the user experience by providing an indicator as to which direction the video is pointed in. Think quad copter moving in one direction but capturing video from the opposite direction - the idea, to introduce a directional icon that will minimize confusion- code for this functionality courtesy of Benjamin Becquet.
 

Code

A JSON variable that stores the latitude and longitude coordinates, a time in seconds that corresponds with when each location features in the video and an angle that represents direction of sight at that location in time. The JSON would be better off stored in a database but I have bundled it into the JavaScript for a local quick start solution.

var pjson  = [
	{"x":-84.364889217934, "y":33.757611419708, "sec":1, "dir":20},
	{"x":-84.364838909406, "y":33.757808413322, "sec":3.5, "dir":300},
	{"x":-84.364781972888, "y":33.757970183215, "sec":4, "dir":20},
	{"x":-84.364759490265, "y":33.758084616287, "sec":10, "dir":300}
];

Next, construct the animation path based on the JSON data points and display it on the map.

var point_locs = [];
for (var i = 0, e = pjson.length; i < e; ++i) {
	point_locs[i] = [pjson[i].x, pjson[i].y];
}
var line_geojson = {
	type: "Feature",
	geometry: {type: "LineString", coordinates: point_locs},
	properties: {"stroke": "#fc4353", "stroke-width": 5}
};
L.geoJson(line_geojson, { style: L.mapbox.simplestyle.style }).addTo(map);

To rotate a marker based on the JSON direction value, I have used Benjamin Becquet's code featured in the mapbox rotating and controllable marker example.

L.RotatedMarker = L.Marker.extend({
  options: { angle: 0 },
  _setPos: function(pos) {
	L.Marker.prototype._setPos.call(this, pos);
	if (L.DomUtil.TRANSFORM) {
	  // use the CSS transform rule if available
	  this._icon.style[L.DomUtil.TRANSFORM] += ' rotate(' + this.options.angle + 'deg)';
	} else if (L.Browser.ie) {
	  // fallback for IE6, IE7, IE8
	  var rad = this.options.angle * L.LatLng.DEG_TO_RAD,
	  costheta = Math.cos(rad),
	  sintheta = Math.sin(rad);
	  this._icon.style.filter += ' progid:DXImageTransform.Microsoft.Matrix(sizingMethod=\'auto expand\', M11=' +
		costheta + ', M12=' + (-sintheta) + ', M21=' + sintheta + ', M22=' + costheta + ')';
	}
  }
});

L.rotatedMarker = function(pos, options) {
	return new L.RotatedMarker(pos, options);
};

Add a marker to map, make it draggable and setup animation functionality. Ensure the video and marker are in-sync with regards to location and associated time featured in the video and feed the location direction in to rotate the marker accordingly.

var marker = L.rotatedMarker(new L.LatLng(pjson[0].y, pjson[0].x), {
	icon: L.icon({
	iconUrl: 'north.png',
	iconSize: [37, 37],
	}),
	draggable: true
}).addTo(map);

marker.on('dragend', function(event) {
	var marker = event.target;
	var result = marker.getLatLng();
	video.get(0).currentTime = nearest_point(result.lat, result.lng).sec;
});

function set_marker(point) {
	marker.setLatLng(L.latLng(point.y, point.x));
	set_marker.now = point;
	marker.options.angle = point.dir;
}
set_marker.now = pjson[0];

function point_at_time(timeindex_sec) {
	var time_index = pjson[0];
	var min_time = Number.POSITIVE_INFINITY;
	for (var i = 0, e = pjson.length; i < e; ++i) {
		var this_time = Math.abs(pjson[i].sec - timeindex_sec);
		if (this_time < min_time) {
			time_index = pjson[i];
			min_time = this_time;
		}
	}
	return time_index;
}

Setup video listeners to work with animated marker location based on HTML5 video API.

video.on('seeked', function () {
	set_marker(point_at_time(this.currentTime));
});

video.on('timeupdate', function() {
	var time_index = point_at_time(this.currentTime);
	if (time_index !== set_marker.now) {
		set_marker(time_index);
	}
});

video.on('ended', function() {
	set_marker(pjson[pjson.length-1]);
});

Handle nearest point functionality based on map click and animated marker drag drop.

function lineDistance(x1, y1, x2, y2) {
	var xs = 0, ys = 0;
	xs = x2 - x1;
	xs = xs * xs;
	ys = y2 - y1;
	ys = ys * ys;
	return Math.sqrt(xs + ys);
}

function nearest_point(lat, lng) {
	var dist = Number.POSITIVE_INFINITY;
	var time_index;
	for (var i = 0, e = pjson.length; i < e; ++i) {
		var this_distance = lineDistance(lng.toString(), lat.toString(), pjson[i].x, pjson[i].y);
		if (this_distance < dist) {
			dist = this_distance
			time_index = pjson[i];
		}
	}
	return time_index;
}

map.on('click', function (e) {
	video.get(0).currentTime = nearest_point(e.latlng.lat, e.latlng.lng).sec;
});

The GoPro unit used for this demo did not capture GPS or direction so the JSON build was very much a manual effort. QGIS was used to generate a path and extract point locations. A time and direction was then assigned to each location, the more points the smoother the animation. The assumption being that, with a GPS enabled video DEVICE, the JSON build process could be automated.

A working example can be found here

The full source code is available on GitHub