diff --git a/@kiva/kv-components/utils/mapAnimation.js b/@kiva/kv-components/utils/mapAnimation.js
new file mode 100644
index 00000000..d481cc94
--- /dev/null
+++ b/@kiva/kv-components/utils/mapAnimation.js
@@ -0,0 +1,246 @@
+/* eslint-disable import/prefer-default-export */
+
+export function generateMapMarkers(mapInstance, borrowerPoints) {
+ const geojson = {
+ type: 'FeatureCollection',
+ };
+
+ geojson.features = borrowerPoints.map((borrower) => ({
+ type: 'Feature',
+ properties: {
+ message: 'test',
+ image: borrower.image,
+ iconSize: [80, 80],
+ },
+ geometry: {
+ type: 'Point',
+ coordinates: borrower.location,
+ },
+ }));
+ // add markers to map
+ geojson.features.forEach((marker) => {
+ // create a DOM element for the marker
+ const el = document.createElement('div');
+ el.className = 'marker';
+ el.style.backgroundImage = `url(${marker.properties.image})`;
+ el.style.width = `${marker.properties.iconSize[0]}px`;
+ el.style.height = `${marker.properties.iconSize[1]}px`;
+
+ // el.addEventListener('click', () => {
+ // window.alert(marker.properties.message);
+ // });
+
+ // add marker to map
+ // eslint-disable-next-line no-undef
+ new maplibregl.Marker({ element: el })
+ .setLngLat(marker.geometry.coordinates)
+ .addTo(mapInstance);
+ });
+}
+/**
+ * Code to generate random coordinates
+ * */
+function getRandomInRange(from, to, fixed) {
+ return (Math.random() * (to - from) + from).toFixed(fixed) * 1;
+ // .toFixed() returns string, so ' * 1' is a trick to convert to number
+}
+const randomCoordinates = Array.from(
+ { length: 20 },
+ () => [getRandomInRange(-180, 180, 3), getRandomInRange(-90, 90, 3)],
+);
+
+/**
+ * Given 2 coordinates and the number of steps return an array of coordinates in between
+ * @param {Array} startCoordinates - starting coordinates in the format [latitude, longitude]
+ * @param {Array} endCoordinates - ending coordinates in the format [latitude, longitude]
+ * @param {Number} numberOfSteps - number of steps to take between the start and end coordinates
+ * @returns {Array} - array of coordinates in the format [[latitude, longitude], [latitude, longitude]]
+*/
+function getCoordinatesBetween(startCoordinates, endCoordinates, numberOfSteps) {
+ const diffX = endCoordinates[0] - startCoordinates[0];
+ const diffY = endCoordinates[1] - startCoordinates[1];
+
+ const sfX = diffX / numberOfSteps;
+ const sfY = diffY / numberOfSteps;
+
+ let i = 0;
+ let j = 0;
+
+ const lineCoordinates = [];
+
+ while (Math.abs(i) < Math.abs(diffX) || Math.abs(j) < Math.abs(diffY)) {
+ lineCoordinates.push([startCoordinates[0] + i, startCoordinates[1] + j]);
+
+ if (Math.abs(i) < Math.abs(diffX)) {
+ i += sfX;
+ }
+
+ if (Math.abs(j) < Math.abs(diffY)) {
+ j += sfY;
+ }
+ }
+ // Because of rounding errors, lets push the exact end coordinates
+ // as the last item in the array to make sure the line ends precisely
+ lineCoordinates.push([endCoordinates[0], endCoordinates[1]]);
+ return lineCoordinates;
+}
+
+/**
+ * This function animates a series of lines from an array of starting coordinates to a single end point
+ * returns a promise when the animation is complete
+ * @param {Map Instance} mapInstance - the map instance
+ * @param {Array} originPoints - array of starting coordinates in the format [[latitude, longitude], [latitude, longitude]]
+ * @param {Array} endPoint - single end point in the format [latitude, longitude]
+ * @returns {Promise} - promise that resolves when the animation is complete
+*/
+function animateLines(mapInstance, originPoints, endPoint) {
+ const speedFactor = 100; // number of frames per degree, controls animation speed
+ return new Promise((resolve) => {
+ // EndPoint
+ mapInstance.addSource('endpoint', {
+ type: 'geojson',
+ data: {
+ type: 'Point',
+ coordinates: [
+ endPoint[0], endPoint[1],
+ ],
+ },
+ });
+
+ const lineFlight = (startCoordinates, endCoordinates, index, lastLine = false) => {
+ const lineCoordinates = getCoordinatesBetween(startCoordinates, endCoordinates, speedFactor);
+ let animationCounter = 0;
+
+ // Create a GeoJSON source with an empty lineString.
+ const geojson = {
+ type: 'FeatureCollection',
+ features: [{
+ type: 'Feature',
+ geometry: {
+ type: 'LineString',
+ coordinates: [],
+ },
+ }],
+ };
+
+ // Start Point
+ mapInstance.addSource(`startPoint${index}`, {
+ type: 'geojson',
+ data: {
+ type: 'Point',
+ coordinates: [
+ startCoordinates[0], startCoordinates[1],
+ ],
+ },
+ });
+
+ // Line
+ mapInstance.addLayer({
+ id: `line-animation${index}`,
+ type: 'line',
+ source: {
+ type: 'geojson',
+ data: geojson,
+ },
+ layout: {
+ 'line-cap': 'round',
+ 'line-join': 'round',
+ },
+ paint: {
+ 'line-color': '#277056',
+ 'line-width': 2,
+ },
+ });
+
+ const animateLine = () => {
+ if (animationCounter < lineCoordinates.length) {
+ geojson.features[0].geometry.coordinates.push(lineCoordinates[animationCounter]);
+ mapInstance.getSource(`line-animation${index}`).setData(geojson);
+
+ requestAnimationFrame(animateLine);
+ // eslint-disable-next-line no-plusplus
+ animationCounter++;
+ } else {
+ // This else block is for "removing the line from point1 to 2"
+ const coord = geojson.features[0].geometry.coordinates;
+ // remove 2 points at a time
+ coord.shift();
+ coord.shift();
+
+ if (coord.length > 0) {
+ geojson.features[0].geometry.coordinates = coord;
+ mapInstance.getSource(`line-animation${index}`).setData(geojson);
+ requestAnimationFrame(animateLine);
+ } else {
+ // remove all sources to allow for new animation
+ mapInstance.removeLayer(`line-animation${index}`);
+ mapInstance.removeSource(`line-animation${index}`);
+ mapInstance.removeSource(`startPoint${index}`);
+
+ if (lastLine) {
+ mapInstance.removeSource('endpoint');
+ resolve();
+ }
+ }
+ }
+ };
+
+ animateLine();
+ };
+
+ originPoints.forEach((coordinate, index) => {
+ lineFlight(coordinate, endPoint, index, index === originPoints.length - 1);
+ });
+ });
+}
+
+export function animationCoordinator(mapInstance, borrowerPoints) {
+ return new Promise((resolve) => {
+ const destinationPoints = borrowerPoints.map((borrower) => borrower.location);
+ const totalNumberOfPoints = destinationPoints.length;
+ let currentPointIndex = 0;
+
+ const flyToPoint = (index) => {
+ mapInstance.flyTo({
+ // These options control the ending camera position: centered at
+ // the target, at zoom level 9, and north up.
+ center: destinationPoints[index],
+ zoom: 4,
+ bearing: 0,
+
+ // These options control the flight curve, making it move
+ // slowly and zoom out almost completely before starting
+ // to pan.
+ speed: 0.9, // make the flying slow
+ curve: 1, // change the speed at which it zooms out
+
+ // This can be any easing function: it takes a number between
+ // 0 and 1 and returns another number between 0 and 1.
+ easing(t) {
+ return t;
+ },
+
+ // this animation is considered essential with respect to prefers-reduced-motion
+ essential: true,
+ }, { flyEnd: true });
+ };
+
+ // This will trigger the next steps in the animation chain
+ mapInstance.on('moveend', (event) => {
+ if (event.flyEnd === true) {
+ animateLines(mapInstance, randomCoordinates, destinationPoints[currentPointIndex])
+ .then(() => {
+ if (currentPointIndex < totalNumberOfPoints - 1) {
+ currentPointIndex += 1;
+ flyToPoint(currentPointIndex);
+ } else {
+ resolve();
+ }
+ });
+ }
+ });
+
+ // fly to point 1
+ flyToPoint(currentPointIndex);
+ });
+}
diff --git a/@kiva/kv-components/vue/KvMap.vue b/@kiva/kv-components/vue/KvMap.vue
index cd95868c..f87dff3a 100644
--- a/@kiva/kv-components/vue/KvMap.vue
+++ b/@kiva/kv-components/vue/KvMap.vue
@@ -13,6 +13,8 @@
+
+
diff --git a/@kiva/kv-components/vue/stories/KvMap.stories.js b/@kiva/kv-components/vue/stories/KvMap.stories.js
index 7c6b077b..e0498181 100644
--- a/@kiva/kv-components/vue/stories/KvMap.stories.js
+++ b/@kiva/kv-components/vue/stories/KvMap.stories.js
@@ -13,6 +13,7 @@ export default {
useLeaflet: false,
width: null,
zoomLevel: 4,
+ advancedAnimation: {},
},
};
@@ -30,6 +31,7 @@ const Template = (args, { argTypes }) => ({
:use-leaflet="useLeaflet"
:width="width"
:zoom-level="zoomLevel"
+ :advanced-animation="advancedAnimation"
/>`,
});
@@ -67,3 +69,32 @@ Leaflet.args = {
useLeaflet: true,
zoomLevel: 6,
};
+
+export const AdvancedAnimation = Template.bind({});
+const advancedAnimation = {
+ borrowerPoints: [
+ {
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/e60a3d61ff052d60991c5d6bbf4a45d3.jpg',
+ location: [-77.032, 38.913],
+ },
+ {
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/6101929097c6e5de48232a4d1ae3b71c.jpg',
+ location: [41.402, 7.160],
+ },
+ {
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/11e018ee3d8b9c5adee459c16a29d264.jpg',
+ location: [-73.356596, 3.501],
+ },
+ ],
+};
+AdvancedAnimation.args = {
+ initialZoom: null,
+ mapId: 5,
+ useLeaflet: false,
+ zoomLevel: 3,
+ height: 600,
+ width: 1000,
+ lat: 0.913,
+ long: 100,
+ advancedAnimation,
+};