From 9e30a493103c44f63d43f5646143847d94bfcca8 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:01:48 +0200 Subject: [PATCH 01/14] Get dates as numbers from Maps API --- src/lib/maps/Client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/maps/Client.ts b/src/lib/maps/Client.ts index 582874cb..731852ae 100644 --- a/src/lib/maps/Client.ts +++ b/src/lib/maps/Client.ts @@ -46,7 +46,8 @@ export class Client { vector_extent: vectorExtent, vector_simplify_extent: vectorSimplifyExtent, metadata, - aggregation + aggregation, + dates_as_numbers: true } } ] From 6655d670164eb85542d1bd1206826cda0592e64c Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:02:20 +0200 Subject: [PATCH 02/14] Add Animation class --- src/lib/viz/animation/Animation.ts | 179 +++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/lib/viz/animation/Animation.ts diff --git a/src/lib/viz/animation/Animation.ts b/src/lib/viz/animation/Animation.ts new file mode 100644 index 00000000..7e571231 --- /dev/null +++ b/src/lib/viz/animation/Animation.ts @@ -0,0 +1,179 @@ +import { DataFilterExtension } from '@deck.gl/extensions'; +import { CartoError } from '@/core/errors/CartoError'; +import { WithEvents } from '@/core/mixins/WithEvents'; +import type { Layer } from '../layer/Layer'; +import { NumericFieldStats } from '../source'; + +const SCREEN_HZ = 60; + +export class Animation extends WithEvents { + private layer: Layer; + private column: string; + private duration: number; + private fade: number; + + private isAnimationPaused = true; + private animationRange: AnimationRange = { min: Infinity, max: -Infinity }; + private animationCurrentValue = 0; + private animationStep = 0; + private animationFadeDuration = 0; + + constructor(layer: Layer, options: AnimationOptions) { + super(); + + const { + column, + duration = DEFAULT_ANIMATION_OPTIONS.duration, + fade = DEFAULT_ANIMATION_OPTIONS.fade + } = options; + + this.layer = layer; + this.column = column; + this.duration = duration; + this.fade = fade; + + this.layer.addSourceField(this.column); + this.registerAvailableEvents(['animationStart', 'animationEnd', 'animationStep']); + } + + async start() { + await this.init(); + this.play(); + this.onAnimationFrame(); + } + + play() { + this.isAnimationPaused = false; + } + + pause() { + this.isAnimationPaused = true; + } + + reset() { + this.animationCurrentValue = this.animationRange.min; + } + + stop() { + this.pause(); + this.reset(); + } + + setCurrent(value: number) { + if (value > this.animationRange.max || value < this.animationRange.min) { + throw new CartoError({ + type: '', + message: '' + }); + } + + this.animationCurrentValue = value; + } + + setProgressPct(progress: number) { + if (progress > 1 || progress < 0) { + throw new CartoError({ + type: '', + message: '' + }); + } + + const progressValue = progress * (this.animationRange.max - this.animationRange.min); + this.animationCurrentValue = this.animationRange.min + progressValue; + } + + getLayerProperties() { + if (this.animationCurrentValue > this.animationRange.max) { + this.reset(); + } + + const animationRangeStart = this.animationCurrentValue; + const animationRangeEnd = Math.min( + this.animationCurrentValue + this.animationStep, + this.animationRange.max + ); + + // Range defines timestamp range for + // visible features (some features may be fading in/out) + const filterRange = [ + animationRangeStart - this.animationFadeDuration, + animationRangeEnd + this.animationFadeDuration + ]; + + // Soft Range defines the timestamp range when + // features are at max opacity and size + const filterSoftRange = [animationRangeStart, animationRangeEnd]; + + const layerProperties = { + extensions: [new DataFilterExtension({ filterSize: 1 })], + getFilterValue: (feature: GeoJSON.Feature) => { + if (!feature) { + return null; + } + + return (feature.properties || {})[this.column]; + }, + filterRange, + filterSoftRange + }; + + this.animationCurrentValue += this.animationStep; + return layerProperties; + } + + private async init() { + this.animationRange = this.getAnimationRange(); + this.animationCurrentValue = this.animationRange.min; + + const animationRange = this.animationRange.max - this.animationRange.min; + this.animationStep = animationRange / (SCREEN_HZ * this.duration); + this.animationFadeDuration = this.fade * this.animationStep * SCREEN_HZ; + } + + private onAnimationFrame() { + if (this.isAnimationPaused) { + return; + } + + requestAnimationFrame(() => { + this.onAnimationFrame(); + }); + + this.emit('animationStep'); + } + + private getAnimationRange() { + const layerMetadata = this.layer.source.getMetadata(); + const columnStats = layerMetadata.stats.find( + column => column.name === this.column + ) as NumericFieldStats; + + if (!columnStats || columnStats.type !== 'number') { + throw new CartoError({ + message: '', + type: '' + }); + } + + return { + min: columnStats.min, + max: columnStats.max + }; + } +} + +export interface AnimationOptions { + column: string; + duration?: number; + fade?: number; +} + +export interface AnimationRange { + min: number; + max: number; +} + +const DEFAULT_ANIMATION_OPTIONS = { + duration: 10, + fade: 0.15 +}; From 1fbc440f43b67b4cb4953781bb13c8632cdd1278 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:03:03 +0200 Subject: [PATCH 03/14] Add Animation to layer --- src/lib/viz/layer/Layer.ts | 41 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/lib/viz/layer/Layer.ts b/src/lib/viz/layer/Layer.ts index 826ef14d..8e830853 100644 --- a/src/lib/viz/layer/Layer.ts +++ b/src/lib/viz/layer/Layer.ts @@ -4,6 +4,7 @@ import { GeoJsonLayer, IconLayer } from '@deck.gl/layers'; import { MVTLayer } from '@deck.gl/geo-layers'; import ViewState from '@deck.gl/core/controllers/view-state'; import mitt from 'mitt'; +import { DataFilterExtension } from '@deck.gl/extensions'; import deepmerge from 'deepmerge'; import { GeoJSON } from 'geojson'; import { uuidv4 } from '@/core/utils/uuid'; @@ -22,6 +23,7 @@ import { FiltersCollection } from '../filters/FiltersCollection'; import { FunctionFilterApplicator } from '../filters/FunctionFilterApplicator'; import { ColumnFilters } from '../filters/types'; import { basicStyle } from '../style/helpers/basic-style'; +import { Animation } from '../animation/Animation'; export enum LayerEvent { DATA_READY = 'dataReady', @@ -63,6 +65,8 @@ export class Layer extends WithEvents implements StyledLayer { ); private dataState: DATA_STATES = DATA_STATES.STARTING; + private animationTest: Animation | undefined; + constructor( source: string | Source, style: Style | StyleProperties = basicStyle(), @@ -314,6 +318,19 @@ export class Layer extends WithEvents implements StyledLayer { return this._source.getFeatures(excludedFilters); } + async addAnimation(animationInstance: Animation) { + this.animationTest = animationInstance; + this.animationTest.on('animationStep', () => { + if (this._deckLayer) { + this.replaceDeckGLLayer(); + } + }); + + if (this._deckLayer) { + await this.replaceDeckGLLayer(); + } + } + private _getLayerProperties() { const props = this._source.getProps(); const styleProps = this.getStyle().getLayerProps(this); @@ -336,12 +353,28 @@ export class Layer extends WithEvents implements StyledLayer { onHover: this._interactivity.onHover.bind(this._interactivity) }; + filters.getOptions(); const layerProps = { ...this._options, ...props, ...styleProps, ...events, - ...filters.getOptions() + ...(this.animationTest + ? this.animationTest.getLayerProperties() + : { + filterEnabled: false, + extensions: [new DataFilterExtension({ filterSize: 1 })], + getFilterValue(feature: GeoJSON.Feature) { + if (!feature) { + return 0; + } + + const { properties = {} } = feature; + return (properties as Record).incdate; + }, + filterRange: [0, 0], + filterSoftRange: [0, 0] + }) }; // Merge Update Triggers to avoid overriding @@ -349,7 +382,11 @@ export class Layer extends WithEvents implements StyledLayer { // updateTriggers layerProps.updateTriggers = deepmerge.all([ layerProps.updateTriggers || {}, - this.filtersCollection.getUpdateTriggers() + { + getFilterValue: [Math.trunc(Math.random() * 1000)], + filterRange: [Math.trunc(Math.random() * 1000)], + filterSoftRange: [Math.trunc(Math.random() * 1000)] + } ]); return ensureRelatedStyleProps(layerProps); From e208a90518e16d7212d65254ef996f4b6eb6a43c Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:03:26 +0200 Subject: [PATCH 04/14] Add whole NumericFieldStats properties --- src/lib/viz/source/GeoJSONSource.ts | 1 + src/lib/viz/source/Source.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/lib/viz/source/GeoJSONSource.ts b/src/lib/viz/source/GeoJSONSource.ts index e34a4d4d..0c338b08 100644 --- a/src/lib/viz/source/GeoJSONSource.ts +++ b/src/lib/viz/source/GeoJSONSource.ts @@ -196,6 +196,7 @@ export class GeoJSONSource extends Source { const sample = createSample(values); numericStats.push({ + type: 'number', name: propName, min, max, diff --git a/src/lib/viz/source/Source.ts b/src/lib/viz/source/Source.ts index d106c276..5f3d687c 100644 --- a/src/lib/viz/source/Source.ts +++ b/src/lib/viz/source/Source.ts @@ -22,6 +22,12 @@ export interface Stats { export interface NumericFieldStats extends Stats { name: string; + sample: number[]; + type: string; + min: number; + max: number; + avg: number; + sum: number; } export interface Category { From bf18648e24d7dfee03277869ba1ef734be31d4ff Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:04:03 +0200 Subject: [PATCH 05/14] Export animation in public API --- src/lib/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 9c923282..18f5d785 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -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 --- */ @@ -94,5 +96,6 @@ export const viz = { widget, style, ...basemaps, - ...basics + ...basics, + Animation }; From cd2846904ccd64d0505cef01fd67d8cda1776610 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:04:14 +0200 Subject: [PATCH 06/14] Add tests --- src/lib/viz/animation/Animation.spec.ts | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/lib/viz/animation/Animation.spec.ts diff --git a/src/lib/viz/animation/Animation.spec.ts b/src/lib/viz/animation/Animation.spec.ts new file mode 100644 index 00000000..9de097c7 --- /dev/null +++ b/src/lib/viz/animation/Animation.spec.ts @@ -0,0 +1,37 @@ +import { Animation } from './Animation'; +import { Layer } from '../layer'; + +describe('Animation', () => { + describe('Instantiation', () => { + it('should create instance with defaults', () => { + const animationLayer = new Layer('fake_source'); + 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); + }); + }); + + describe('Animation Progress', () => { + it.skip('should start animation and emit N animationStep', async () => { + const animationLayer = new Layer('fake_source'); + + const animationStepHandler = jest.fn(); + + const animation = new Animation(animationLayer, { column: 'timestamp' }); + animation.on('animationStep', animationStepHandler); + + await animation.start(); + + expect(animationStepHandler).toHaveBeenCalledTimes(10); + }); + }); +}); From 5d71058e5cfd65f5dbd62fefc359b145c10277f2 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:07:20 +0200 Subject: [PATCH 07/14] Add example --- examples/_debug/animation.html | 77 ++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 examples/_debug/animation.html diff --git a/examples/_debug/animation.html b/examples/_debug/animation.html new file mode 100644 index 00000000..22b0ea01 --- /dev/null +++ b/examples/_debug/animation.html @@ -0,0 +1,77 @@ + + + + + + + Animation Example + + + + + + + + + +
+
+
+ +
+
+
+
+ + + + + + + + + + + From 6ba06ff45344341e1005f3a7ba500551f7f44fa1 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:20:26 +0200 Subject: [PATCH 08/14] Transform animation range from 0 to max --- src/lib/viz/animation/Animation.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/viz/animation/Animation.ts b/src/lib/viz/animation/Animation.ts index 7e571231..6a6dd0e9 100644 --- a/src/lib/viz/animation/Animation.ts +++ b/src/lib/viz/animation/Animation.ts @@ -14,6 +14,7 @@ export class Animation extends WithEvents { private isAnimationPaused = true; private animationRange: AnimationRange = { min: Infinity, max: -Infinity }; + private originalAnimationRange: AnimationRange = { min: Infinity, max: -Infinity }; private animationCurrentValue = 0; private animationStep = 0; private animationFadeDuration = 0; @@ -60,14 +61,14 @@ export class Animation extends WithEvents { } setCurrent(value: number) { - if (value > this.animationRange.max || value < this.animationRange.min) { + if (value > this.originalAnimationRange.max || value < this.originalAnimationRange.min) { throw new CartoError({ type: '', message: '' }); } - this.animationCurrentValue = value; + this.animationCurrentValue = value - this.originalAnimationRange.min; } setProgressPct(progress: number) { @@ -111,7 +112,7 @@ export class Animation extends WithEvents { return null; } - return (feature.properties || {})[this.column]; + return (feature.properties || {})[this.column] - this.originalAnimationRange.min; }, filterRange, filterSoftRange @@ -122,7 +123,9 @@ export class Animation extends WithEvents { } private async init() { - this.animationRange = this.getAnimationRange(); + const ranges = this.getAnimationRange(); + this.animationRange = ranges.transformedRange; + this.originalAnimationRange = ranges.originalRange; this.animationCurrentValue = this.animationRange.min; const animationRange = this.animationRange.max - this.animationRange.min; @@ -156,8 +159,8 @@ export class Animation extends WithEvents { } return { - min: columnStats.min, - max: columnStats.max + originalRange: { min: columnStats.min, max: columnStats.max }, + transformedRange: { min: 0, max: columnStats.max - columnStats.min } }; } } From 6518228e9677df58c60d822d05e3692bd53fc0c4 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 11:36:56 +0200 Subject: [PATCH 09/14] Change fade animation timing in example --- examples/_debug/animation.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/_debug/animation.html b/examples/_debug/animation.html index 22b0ea01..8db45568 100644 --- a/examples/_debug/animation.html +++ b/examples/_debug/animation.html @@ -61,7 +61,7 @@ const layerAnimation = new carto.viz.Animation( countriesLayer, - { column: 'incdate', duration: 30 } + { column: 'incdate', duration: 30, fade: 1 } ); setTimeout(async () => { From 18fcf30afa5b7830efa148a1d96429e75efcba53 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 12:33:09 +0200 Subject: [PATCH 10/14] Add untested tests --- src/lib/viz/animation/Animation.spec.ts | 82 ++++++++++++++++++++++++- src/lib/viz/animation/Animation.ts | 6 ++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/lib/viz/animation/Animation.spec.ts b/src/lib/viz/animation/Animation.spec.ts index 9de097c7..2b6055f7 100644 --- a/src/lib/viz/animation/Animation.spec.ts +++ b/src/lib/viz/animation/Animation.spec.ts @@ -1,7 +1,7 @@ import { Animation } from './Animation'; import { Layer } from '../layer'; -describe('Animation', () => { +describe.skip('Animation', () => { describe('Instantiation', () => { it('should create instance with defaults', () => { const animationLayer = new Layer('fake_source'); @@ -34,4 +34,84 @@ describe('Animation', () => { expect(animationStepHandler).toHaveBeenCalledTimes(10); }); }); + + describe('Methods', () => { + describe('play', () => { + it('should change isAnimationPause to false', () => { + const animationLayer = new Layer('fake_source'); + 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', () => { + const animationLayer = new Layer('fake_source'); + const animation = new Animation(animationLayer, { column: 'timestamp' }); + + expect(animation.isPlaying).toBe(true); + animation.play(); + expect(animation.isPlaying).toBe(false); + }); + }); + + describe('reset', () => { + it('should reset animationCurrentValue', () => { + const animationLayer = new Layer('fake_source'); + const animation = new Animation(animationLayer, { column: 'timestamp' }); + + animation.setProgressPct(0.8); + let animationProperties = animation.getLayerProperties(); + expect(animationProperties.filterRange[0]).toBe(0); + + animation.reset(); + animationProperties = animation.getLayerProperties(); + expect(animationProperties); + }); + }); + + describe('stop', () => { + it('should call pause, reset and emit animationEnd', () => { + const animationLayer = new Layer('fake_source'); + 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', () => { + const animationLayer = new Layer('fake_source'); + const animation = new Animation(animationLayer, { column: 'timestamp' }); + + animation.setCurrent(10); + const layerProperties = animation.getLayerProperties(); + + expect(layerProperties).toMatchObject({ filterSoftRange: [] }); + }); + }); + + describe('setProgressPct', () => { + it('should set animation value', () => { + const animationLayer = new Layer('fake_source'); + const animation = new Animation(animationLayer, { column: 'timestamp' }); + + animation.setProgressPct(0.75); + const layerProperties = animation.getLayerProperties(); + + expect(layerProperties).toMatchObject({ filterSoftRange: [] }); + }); + }); + }); }); diff --git a/src/lib/viz/animation/Animation.ts b/src/lib/viz/animation/Animation.ts index 6a6dd0e9..cd9304c2 100644 --- a/src/lib/viz/animation/Animation.ts +++ b/src/lib/viz/animation/Animation.ts @@ -40,9 +40,14 @@ export class Animation extends WithEvents { async start() { await this.init(); this.play(); + this.emit('animationStart'); this.onAnimationFrame(); } + public get isPlaying() { + return !this.isAnimationPaused; + } + play() { this.isAnimationPaused = false; } @@ -58,6 +63,7 @@ export class Animation extends WithEvents { stop() { this.pause(); this.reset(); + this.emit('animationEnd'); } setCurrent(value: number) { From abd5200513e6e997f688d4dcf74dcd758ae0acc6 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 20:36:16 +0200 Subject: [PATCH 11/14] Move step change out of getLayerProperties --- src/lib/viz/animation/Animation.ts | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/lib/viz/animation/Animation.ts b/src/lib/viz/animation/Animation.ts index cd9304c2..776ebad6 100644 --- a/src/lib/viz/animation/Animation.ts +++ b/src/lib/viz/animation/Animation.ts @@ -38,6 +38,12 @@ export class Animation extends WithEvents { } async start() { + if (!this.layer.isReady()) { + this.layer.on('tilesLoaded', () => this.start()); + return; + } + + await this.layer.addAnimation(this); await this.init(); this.play(); this.emit('animationStart'); @@ -69,8 +75,8 @@ export class Animation extends WithEvents { setCurrent(value: number) { if (value > this.originalAnimationRange.max || value < this.originalAnimationRange.min) { throw new CartoError({ - type: '', - message: '' + type: '[Animation]', + message: `Value should be between ${this.originalAnimationRange.min} and ${this.originalAnimationRange.max}` }); } @@ -80,8 +86,8 @@ export class Animation extends WithEvents { setProgressPct(progress: number) { if (progress > 1 || progress < 0) { throw new CartoError({ - type: '', - message: '' + type: '[Animation]', + message: `Value should be between 0 and 1` }); } @@ -90,10 +96,6 @@ export class Animation extends WithEvents { } getLayerProperties() { - if (this.animationCurrentValue > this.animationRange.max) { - this.reset(); - } - const animationRangeStart = this.animationCurrentValue; const animationRangeEnd = Math.min( this.animationCurrentValue + this.animationStep, @@ -111,7 +113,7 @@ export class Animation extends WithEvents { // features are at max opacity and size const filterSoftRange = [animationRangeStart, animationRangeEnd]; - const layerProperties = { + return { extensions: [new DataFilterExtension({ filterSize: 1 })], getFilterValue: (feature: GeoJSON.Feature) => { if (!feature) { @@ -123,9 +125,6 @@ export class Animation extends WithEvents { filterRange, filterSoftRange }; - - this.animationCurrentValue += this.animationStep; - return layerProperties; } private async init() { @@ -144,7 +143,12 @@ export class Animation extends WithEvents { return; } + if (this.animationCurrentValue > this.animationRange.max) { + this.reset(); + } + requestAnimationFrame(() => { + this.animationCurrentValue += this.animationStep; this.onAnimationFrame(); }); @@ -159,8 +163,8 @@ export class Animation extends WithEvents { if (!columnStats || columnStats.type !== 'number') { throw new CartoError({ - message: '', - type: '' + message: 'Specified column is not present or does not contain timestamps or dates', + type: '[Animation]' }); } From b3ba91e441f559ae5b563bac8f1e842109c7f352 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 30 Jul 2020 20:36:36 +0200 Subject: [PATCH 12/14] Add tests --- src/lib/viz/animation/Animation.spec.ts | 115 ++++++++++++++---- .../source/__tests__/GeoJSONSource.spec.ts | 2 + 2 files changed, 96 insertions(+), 21 deletions(-) diff --git a/src/lib/viz/animation/Animation.spec.ts b/src/lib/viz/animation/Animation.spec.ts index 2b6055f7..4cf2b248 100644 --- a/src/lib/viz/animation/Animation.spec.ts +++ b/src/lib/viz/animation/Animation.spec.ts @@ -1,10 +1,13 @@ +import { FeatureCollection } from 'geojson'; +import { CartoError } from '@/core/errors/CartoError'; import { Animation } from './Animation'; import { Layer } from '../layer'; +import { GeoJSONSource } from '../source'; -describe.skip('Animation', () => { +describe('Animation', () => { describe('Instantiation', () => { it('should create instance with defaults', () => { - const animationLayer = new Layer('fake_source'); + const animationLayer = createLayer(); spyOn(animationLayer, 'addSourceField'); const animationColumn = 'timestamp'; @@ -18,11 +21,25 @@ describe.skip('Animation', () => { expect(animationLayer.addSourceField).toHaveBeenCalledWith(animationColumn); }); + + it.skip('should throw is column is not present or not numeric', () => { + const animationLayer = createLayer(); + const animation = new Animation(animationLayer, { column: 'fake_column' }); + + expect(async () => { + await animation.start(); + }).toThrow( + new CartoError({ + type: 'maltype', + message: 'asasdasd' + }) + ); + }); }); describe('Animation Progress', () => { - it.skip('should start animation and emit N animationStep', async () => { - const animationLayer = new Layer('fake_source'); + it('should start animation and emit N animationStep', async () => { + const animationLayer = createLayer(); const animationStepHandler = jest.fn(); @@ -30,15 +47,25 @@ describe.skip('Animation', () => { animation.on('animationStep', animationStepHandler); await animation.start(); + expect(animationStepHandler).toHaveBeenCalled(); + animation.stop(); + }); + }); - expect(animationStepHandler).toHaveBeenCalledTimes(10); + 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 = new Layer('fake_source'); + const animationLayer = createLayer(); const animation = new Animation(animationLayer, { column: 'timestamp' }); expect(animation.isPlaying).toBe(false); @@ -48,34 +75,38 @@ describe.skip('Animation', () => { }); describe('pause', () => { - it('should change isAnimationPause to true', () => { - const animationLayer = new Layer('fake_source'); + 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.play(); + animation.pause(); expect(animation.isPlaying).toBe(false); }); }); describe('reset', () => { - it('should reset animationCurrentValue', () => { - const animationLayer = new Layer('fake_source'); + 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.filterRange[0]).toBe(0); + expect(animationProperties.filterSoftRange[0]).toBe(7200); animation.reset(); animationProperties = animation.getLayerProperties(); - expect(animationProperties); + expect(animationProperties.filterSoftRange[0]).toBe(0); }); }); describe('stop', () => { it('should call pause, reset and emit animationEnd', () => { - const animationLayer = new Layer('fake_source'); + const animationLayer = createLayer(); const animation = new Animation(animationLayer, { column: 'timestamp' }); spyOn(animation, 'pause'); @@ -91,27 +122,69 @@ describe.skip('Animation', () => { }); describe('setCurrent', () => { - it('should set animation value', () => { - const animationLayer = new Layer('fake_source'); + it('should set animation value', async () => { + const animationLayer = createLayer(); const animation = new Animation(animationLayer, { column: 'timestamp' }); + await animation.start(); + animation.pause(); - animation.setCurrent(10); + animation.setCurrent(5000); const layerProperties = animation.getLayerProperties(); - expect(layerProperties).toMatchObject({ filterSoftRange: [] }); + 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 value', () => { - const animationLayer = new Layer('fake_source'); + 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).toMatchObject({ filterSoftRange: [] }); + 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; +} diff --git a/src/lib/viz/source/__tests__/GeoJSONSource.spec.ts b/src/lib/viz/source/__tests__/GeoJSONSource.spec.ts index 7776ad0b..3e18b34a 100644 --- a/src/lib/viz/source/__tests__/GeoJSONSource.spec.ts +++ b/src/lib/viz/source/__tests__/GeoJSONSource.spec.ts @@ -156,6 +156,7 @@ describe('SourceMetadata', () => { stats: [ { name: 'number', + type: 'number', min: 10, max: 70, avg: 100 / 3, @@ -190,6 +191,7 @@ describe('SourceMetadata', () => { stats: [ { name: 'number', + type: 'number', min: 10, max: 70, avg: 100 / 3, From c6011f7ab23c785a7a4b5e80172cbbcc7569cb13 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Fri, 31 Jul 2020 11:25:29 +0200 Subject: [PATCH 13/14] Fix tests --- src/lib/viz/animation/Animation.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/viz/animation/Animation.spec.ts b/src/lib/viz/animation/Animation.spec.ts index 4cf2b248..65a9d35c 100644 --- a/src/lib/viz/animation/Animation.spec.ts +++ b/src/lib/viz/animation/Animation.spec.ts @@ -22,13 +22,13 @@ describe('Animation', () => { expect(animationLayer.addSourceField).toHaveBeenCalledWith(animationColumn); }); - it.skip('should throw is column is not present or not numeric', () => { + 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(); - }).toThrow( + }).rejects.toEqual( new CartoError({ type: 'maltype', message: 'asasdasd' From eb94afdd564f31e205632e0b077565254dbe69cd Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Fri, 31 Jul 2020 11:25:47 +0200 Subject: [PATCH 14/14] Move animationStep event emitter --- src/lib/viz/animation/Animation.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/viz/animation/Animation.ts b/src/lib/viz/animation/Animation.ts index 776ebad6..35cf0a44 100644 --- a/src/lib/viz/animation/Animation.ts +++ b/src/lib/viz/animation/Animation.ts @@ -147,12 +147,13 @@ export class Animation extends WithEvents { this.reset(); } + + this.emit('animationStep'); + requestAnimationFrame(() => { this.animationCurrentValue += this.animationStep; this.onAnimationFrame(); }); - - this.emit('animationStep'); } private getAnimationRange() {