Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features Animation #111

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
77 changes: 77 additions & 0 deletions examples/_debug/animation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Animation Example</title>

<!-- Custom CSS -->
<link rel="stylesheet" type="text/css" href="https://api.tiles.mapbox.com/mapbox-gl-js/v1.10.0/mapbox-gl.css" />
<link rel="stylesheet" href="https://libs.cartocdn.com/airship-style/v2.4.0-rc.0/airship.css" />

<style>
body {
margin: 0;
padding: 0;
}

#map {
width: 100vw;
height: 100vh;
}

.panel {
width: 200px;
}
</style>
</head>

<body class="as-app-body as-app">
<div class="as-content">
<main class="as-main">
<div class="as-map-area">
<!-- map -->
<div id="map"></div>
</div>
</main>
</div>

<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v1.10.0/mapbox-gl.js"></script>
<script src="https://unpkg.com/[email protected]/dist.min.js"></script>
<script src="https://libs.cartocdn.com/airship-components/v2.4.0-rc.0/airship.js"></script>

<script src="/dist/umd/index.min.js"></script>

<script>
async function initialize() {
carto.auth.setDefaultCredentials({ username: 'cartoframes' });

const deckMap = carto.viz.createMap({
view: {
longitude: -122.32527358292457,
latitude: 47.612419309029065,
zoom: 10
}
});
window.deckMap = deckMap;

const countriesLayer = new carto.viz.Layer('seattle_collisions', carto.viz.style.colorCategories('severitydesc'));
await countriesLayer.addTo(deckMap);

const layerAnimation = new carto.viz.Animation(
countriesLayer,
{ column: 'incdate', duration: 30, fade: 1 }
);

setTimeout(async () => {
await countriesLayer.addAnimation(layerAnimation);
layerAnimation.start();
}, 4000);
}

initialize();
</script>
</body>

</html>
5 changes: 4 additions & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import { CategoryDataView, FormulaDataView, HistogramDataView } from './viz/data
// Widgets
import { CategoryWidget, FormulaWidget, HistogramWidget } from './viz/widget';

import { Animation } from './viz/animation/Animation';

/*
* --- Public API ---
*/
Expand Down Expand Up @@ -94,5 +96,6 @@ export const viz = {
widget,
style,
...basemaps,
...basics
...basics,
Animation
};
3 changes: 2 additions & 1 deletion src/lib/maps/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export class Client {
vector_extent: vectorExtent,
vector_simplify_extent: vectorSimplifyExtent,
metadata,
aggregation
aggregation,
dates_as_numbers: true
}
}
]
Expand Down
190 changes: 190 additions & 0 deletions src/lib/viz/animation/Animation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { FeatureCollection } from 'geojson';
import { CartoError } from '@/core/errors/CartoError';
import { Animation } from './Animation';
import { Layer } from '../layer';
import { GeoJSONSource } from '../source';

describe('Animation', () => {
describe('Instantiation', () => {
it('should create instance with defaults', () => {
const animationLayer = createLayer();
spyOn(animationLayer, 'addSourceField');

const animationColumn = 'timestamp';

expect(new Animation(animationLayer, { column: animationColumn })).toMatchObject({
layer: animationLayer,
column: animationColumn,
duration: 10,
fade: 0.15
});

expect(animationLayer.addSourceField).toHaveBeenCalledWith(animationColumn);
});

it('should throw is column is not present or not numeric', async () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'fake_column' });

expect(async () => {
await animation.start();
}).rejects.toEqual(
new CartoError({
type: 'maltype',
message: 'asasdasd'
})
);
});
});

describe('Animation Progress', () => {
it('should start animation and emit N animationStep', async () => {
const animationLayer = createLayer();

const animationStepHandler = jest.fn();

const animation = new Animation(animationLayer, { column: 'timestamp' });
animation.on('animationStep', animationStepHandler);

await animation.start();
expect(animationStepHandler).toHaveBeenCalled();
animation.stop();
});
});

describe('Properties', () => {
describe('isPlaying', () => {
it('should return animation state', () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });
expect(animation.isPlaying).toBe(false);
});
});
});

describe('Methods', () => {
describe('play', () => {
it('should change isAnimationPause to false', () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });

expect(animation.isPlaying).toBe(false);
animation.play();
expect(animation.isPlaying).toBe(true);
});
});

describe('pause', () => {
it('should change isAnimationPause to true', async () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });
await animation.start();

expect(animation.isPlaying).toBe(true);
animation.pause();
expect(animation.isPlaying).toBe(false);
});
});

describe('reset', () => {
it('should reset animationCurrentValue', async () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });

await animation.start();
animation.pause();

animation.setProgressPct(0.8);
let animationProperties = animation.getLayerProperties();
expect(animationProperties.filterSoftRange[0]).toBe(7200);

animation.reset();
animationProperties = animation.getLayerProperties();
expect(animationProperties.filterSoftRange[0]).toBe(0);
});
});

describe('stop', () => {
it('should call pause, reset and emit animationEnd', () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });

spyOn(animation, 'pause');
spyOn(animation, 'reset');
spyOn(animation, 'emit');

animation.stop();

expect(animation.pause).toHaveBeenCalled();
expect(animation.reset).toHaveBeenCalled();
expect(animation.emit).toHaveBeenCalledWith('animationEnd');
});
});

describe('setCurrent', () => {
it('should set animation value', async () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });
await animation.start();
animation.pause();

animation.setCurrent(5000);
const layerProperties = animation.getLayerProperties();

expect(layerProperties).toMatchObject({ filterSoftRange: [4000, 4015] });
});

it('should fail if value is over or below limits', async () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });
await animation.start();
animation.pause();

expect(() => animation.setCurrent(1)).toThrow(
new CartoError({
type: '[Animation]',
message: 'Value should be between 1000 and 10000'
})
);
});
});

describe('setProgressPct', () => {
it('should set animation percentage', async () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });
await animation.start();
animation.pause();

animation.setProgressPct(0.75);
const layerProperties = animation.getLayerProperties();

expect(layerProperties.filterSoftRange[0]).toBe(6750);
});

it('should fail if percentage is over 1 or below 0', () => {
const animationLayer = createLayer();
const animation = new Animation(animationLayer, { column: 'timestamp' });

expect(() => animation.setProgressPct(1.2)).toThrow(
new CartoError({
type: '[Animation]',
message: 'Value should be between 0 and 1'
})
);
});
});
});
});

function createLayer() {
const source = new GeoJSONSource({} as FeatureCollection);
spyOn(source, 'getMetadata').and.returnValue({
stats: [{ name: 'timestamp', type: 'number', min: 1000, max: 10000 }]
});

const layer = new Layer(source);
spyOn(layer, 'isReady').and.returnValue(true);

return layer;
}
Loading