Image
Open Street maps / Leaflet + STRAVA API (2018)
A very advanced UI/UX made with Leaflet and implementing back-end STRAVA API on for Storyteller platform. Integration of STRAVA API in back-end and a complex JavaScript factory to combine all into a visual story for users/members who actually post their rides this way.
The most advanced part here was processing available geo data json in order to create "the rhythm" and the animation on the map.
/* Main animation method in JavaScript */
Code
/** Animation loop / drawing and moving (and shaking :) that happens between each segment on the route (A > B)
@param {L.LatLng} latlng data|array for the particular animation part
@param {L.Marker} startMarker is a starting position for this animation part - A
@param {L.Marker} targetMarker is target position for this animation part - B
@param {Integer} currentIndex a current requested animation part index
*/
animatePolyline: function (latlng, startMarker, targetMarker, currentIndex) {
if (latlng && latlng.length > 0) {
// Define start and end positions
var start = startMarker ? startMarker : self.markers.moving; //photos[0];
var end = targetMarker ? new L.LatLng(targetMarker.getLatLng().lat,
targetMarker.getLatLng().lng) : {};
// "End" marker should never be visible during the animation
self.markers.end.setOpacity('0');
// And make sure that target marker is visible during the animation, if it's not "end" one
if (targetMarker != self.markers.end) {
targetMarker.setOpacity('1');
}
// Here we create an array of each nth item (panDividerFactor property) in the current polyline array in order to run camera that often
var indexes = [];
for (e = 0; e &
#60; latlng.length; e++) {
if (latlng[e]) {
if ((e % self.panDividerFactor) = =
0) {
indexes.push(latlng[e]);
}
}
}
// Listen for ongoing animation
document.addEventListener('mapAnimation', self.listeners.mapAnimation);
// Current animation was interrupted with the other one; most likely user interaction / slide prev/next navigation
if (self.timeout.delay._default == self.timeout.delay.broken) {
// If there was a previous animation make sure to remove vector(svg) and draw canvas polyline (so the line does not "fly" together with flyToBounds() below
if (self.polylineSlides[currentIndex - 1] && self.polylineSlides[currentIndex -
1].polyline) {
self.map.removeLayer(self.polylineSlides[currentIndex - 1].polyline);
self.polylineSlides[currentIndex - 1].polyline = self.setPolyline(self.polylineSlides[
currentIndex - 1].data, {
renderer: L.canvas({
padding: 0
})
});
}
// Make sure image/marker becomes visible in this case too
if (startMarker && startMarker != targetMarker) {
startMarker.setZIndexOffset(1);
$(startMarker._icon).addClass('slide-active dirty');
}
// Place marker on the requested destination/slide
self.markers.moving.setLatLng(latlng[0]);
// And flyTo requested destination/slide
self.map.flyTo(latlng[0], self.zoom, {
animate: true,
duration: self.timeout.delay.brokenFly
});
// Broadcast this event, declare this animation as complete
var event = new CustomEvent('mapAnimationComplete', {
'detail': {
broken: true,
currentIndex: parseInt(currentIndex) - 1
}
});
// Dispatch/Trigger/Fire the event
document.dispatchEvent(event);
}
var zoom = self.map.getZoom();
setTimeout(function () {
// Define timeout array for this animation part; This is to be cleared on interruption or if animation completed
self.polylineSlides[currentIndex].timeout = [];
// Define polyline for this animation part (A > B) to be "filled" progressively in a loop below
self.polylineSlides[currentIndex].polyline = self.setPolyline([latlng[0]], {
renderer: L.svg({
padding: 0
})
});
// Our glorious loop starts here
for (i = 0; i &
#60; latlng.length; i++) {
// Broadcast this event, tell everyone that animation is ongoing
var animateEvent = new CustomEvent(
'mapAnimation', {
'detail': {
currentIndex: parseInt(currentIndex),
index: i
}
}); document.dispatchEvent(animateEvent);
// Each polyline segment needs to be packed into timeout array so that we can safely clear it per need
self.polylineSlides[currentIndex].timeout[i] = setTimeout(function (
coords, index) {
var currPolyline = self.polylineSlides[currentIndex].polyline;
// The main magic, progressively draw polyline and move "moving" marker
if (!coords[index].equals(end)) {
// Move marker
self.markers.moving.setLatLng(coords[index]);
// Populate polyline with points (drawing it)
self.polylineSlides[currentIndex].polyline.addLatLng(coords[index]);
}
// Camera move during the animation; each nth (panDividerFactor property) time of the whole loop
if (indexes.indexOf(coords[index])) {
// Currently we use flyToBound() which makes the whole animation kind of fancy
var startCoords = self.polylineSlides[currentIndex].polyline.getLatLngs()[
index];;
var endCoords = targetMarker.getLatLng()
if (startCoords && endCoords) {
var p1 = new L.latLng(startCoords.lat, startCoords.lng);
var p2 = new L.latLng(endCoords.lat, endCoords.lng);
var bounds = new L.latLngBounds(p1, p2);
self.map.flyToBounds(bounds, {
maxZoom: 15,
duration: self.panDuration,
easeLinearity: 1.25
});
}
}
// Animation reached its last point (targetMarker)
if (coords.length - 1 == index) {
// Revert animation delay value to default, from "flying" one to a "moving" one
self.timeout.delay._default = self.timeout.delay.linear;
//self.map.flyTo(coords[index], newZoom, {animate: true, duration: 1.55, easeLinearity: 0.55}); //2.32
// Broadcast this event, declare this animation as complete
var event = new CustomEvent('mapAnimationComplete', {
'detail': {
currentIndex: parseInt(currentIndex)
}
});
document.dispatchEvent(event);
// Since animation for this very polyline, part of the route, was completed, make sure that line is canvas now rather than vector (svg)
// Canvas line does not "fly" together with flyToBounds() above
self.map.removeLayer(self.polylineSlides[currentIndex].polyline);
self.polylineSlides[currentIndex].polyline = self.setPolyline(
self.polylineSlides[currentIndex].data, {
renderer: L.canvas({
padding: 0
})
});
// self.map.addLayer(self.polylineSlides[currentIndex].polyline);
// Make target marker visible (if not "end" marker)
if (targetMarker !== self.markers.end) {
targetMarker.setZIndexOffset(1);
$(targetMarker._icon).addClass('slide-active dirty');
} else { // Last portion, do some specific changes
self.markers.end.setOpacity('1');
$(self.markers.end._icon).addClass('slide-active');
self.markers.moving.setOpacity('0');
// Once the "end" slide is reached fly to bounds of entire route
setTimeout(function () {
//self.map.setMaxBounds([currentPath[0], currentMarkerCoord]);
self.map.flyToBounds(self.polyline.getBounds(), {
animate: true,
duration: self.endSlideBounds.duration
});
}, self.endSlideBounds.delay);
}
}
}, self.drawingSpeed._default * i, latlng, i);
}
}, self.timeout.delay._default);
}
}
Videos
Video file