[GIS] LeafletJS animated marker with video

leafletvideo

I have used the leaflet.animatedmarker from openplans on GitHub to create a marker that moves along a line. I also have added a video that will popup above the line. This plays fine with the marker moving.

What I want to be able to do is have a button that is able to start and pause the video and marker at the same time. I would also like the added benefit of being able to move backwards and forwards in the video and marker on the line. I am just wondering if this possible, and how I might go about it?

Best Answer

While you have written the basic code for the L.animatedMarker, I will detail it for further education. I have used some external references, like the Mapbox GoPro tutorial and a JSFiddle in the StackExchange post describing Vimeo events.

You can see my result on the following JSFiddle: http://jsfiddle.net/GFarkas/4mo8e9da/. Unfortunately, you can't test the "the added benefit of being able to move backwards and forwards in the video and marker on the line". However, you can test it on a local hosted site.

On the first 9 line of code you set up a basic Mapbox map with Leaflet. It has a center and a predefined zoom level. From then, you can skip to line 638, that long code is just a copy-pasted GeoJSON code.

The next part of the code renders the GeoJSON line to the map as a simple line feature.

var line = L.geoJson(ride, {
    style: {
        weight: 7,
        opacity: 1,
        color: '#0d8709',
        opacity: 0.7
    }
});

In the next part, I had to extract the coordinates from the GeoJSON array and switch the lan/lot values, because the GeoJSON format uses lon/lat coordinate order. I have used a loop for this task.

var raw = [];

for (var i = 0; i < ride.features[0].geometry.coordinates.length; i++) {
    var tmp = [];
    tmp[0] = ride.features[0].geometry.coordinates[i][1];
    tmp[1] = ride.features[0].geometry.coordinates[i][0];
    raw.push(tmp);
}

Now that I had a correctly ordered array of coordinates, I could've made a polyline feature, which is the only valid input of L.animatedMarker as far as I know.

var coords = L.polyline(raw),
    animatedMarker = L.animatedMarker(coords.getLatLngs(), {
        distance: 100,
        interval: 2500,
        autoStart: false
});

The distance and interval options define the speed of the marker on the line. You have to fine tune it, so your video will end at the same time as your marker. I also had to set the autoStart option to false, so later I can start the marker with the video.

From now on, here comes the "magic" part. If you want to have control over your video and your marker simultaneously, you have to use your favourite site's API besides of Leaflet. In this example I've used Vimeo's Froogaloop framework. If you want to embed a video from YouTube, you have to look up how you can use its API for this task. In the next step I've added the L.popup layer and bond it to the marker.

var popup = L.popup({
    keepInView: false,
    autoPan: false,
    closeButton: false,
    closeOnClick: false,
    maxWidth: 1000
}).setContent('<iframe id="player1" src="https://player.vimeo.com/video/69426498?title=0&amp;byline=0&amp;portrait=0&amp;autoplay=0&amp;api=1&amp;player_id=player1" width="200" height="150" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>');

animatedMarker.bindPopup(popup).openPopup();

The most important option in this object is the content. You have to add an ID to the iframe tag and include it to the video's link, too as a request with &player_id=player1. You also have to include a request for using Vimeo's API with &api=1.

I used a sample code to write the event listeners for the videos. The sample code used JQuery, so do I and I will only detail the customized part of the code.

player.addEvent('ready', function() {
    player.addEvent('pause', onPause);
    player.addEvent('finish', onFinish);
    player.addEvent('play', onPlay);
    player.addEvent('seek', onSeek);
});

We will need four events from the video. We have to know if it is paused (pause), if it is finished (finish), if it is playing (play) or if we jumped in the video (seek). Caution: do not use the playProgress event to bind it with animatedMarker.start(), it will cause the marker to accelerate uncontrollably. Now to create the proper functions for the events.

function onPause(id) {
    animatedMarker.stop();
}

function onFinish(id) {
    animatedMarker.stop();
}

function onPlay(id) {
    animatedMarker.start();
}

function onSeek(data, id) {
    animatedMarker._i = Math.round(data.percent*raw.length);
}

The first three events will return a function to start or stop the marker on the line if the video has been started or stopped. The fourth event is a little bit different. To move the marker on the map with the video, we have to use a formula to set up the marker's new place on the map. The marker's current place (vertex in the polyline) is stored in the marker._i attribute if your L.animatedMarker's variable is called marker. Fortunately the seek event returns an object with the video's duration, position and played percentage (in a scale between 0 and 1). If we return the vertex closest to the number of the vertices multiplied by the desired percentage of the video, and round it to the nearest integer, we get the marker's position on the line in the desired moment of the video with a good approximation. You can optimize the accuracy of this method by making the marker's movement duration as long as the video and working with many points (of course it only works fine if the vertices are equally distributed on the line).

I hope this answer will help and sorry for my bad English.

UPDATE:

If you want your marker to follow your directions when the video is paused, you won't be able to use L.animatedMarker.update(). You have to use L.animatedMarker.start() and L.animatedMarker.stop() which will cause the marker to jump over a vertex. Unfortunately this will reduce the accuracy of the animation, but this is the price you have to pay for an interactive map (until the author(s) fix the L.animatedMarker.update() function) .