diff --git a/src/components/src/common/range-slider.tsx b/src/components/src/common/range-slider.tsx
index ad43eede91..36f19ee462 100644
--- a/src/components/src/common/range-slider.tsx
+++ b/src/components/src/common/range-slider.tsx
@@ -250,10 +250,10 @@ export default function RangeSliderFactory(
const hasPlot = plotType?.type;
const value = this.props.plotValue || this.filterValueSelector(this.props);
- const scaledValue = range
- ? // TODO figure out correct types for value and range
- scaleSourceDomainToDestination(value as [number, number], range as [number, number])
- : [0, 0];
+ const scaledValue =
+ timelines?.length && range
+ ? scaleSourceDomainToDestination(value as [number, number], range as [number, number])
+ : [0, 0];
const commonPadding = `${Number(sliderHandleWidth) / 2}px`;
return (
(state: S): S {
};
// reset currentTime based on new domain
- const currentTime = isInRange(state.animationConfig.currentTime, mergedDomain)
+ const syncedFilter = state.filters?.find(f => (f as TimeRangeFilter).syncedWithLayerTimeline) as
+ | TimeRangeFilter
+ | undefined;
+
+ // if synced filter exist wee need to merge animationConfig and filter domains
+ // and validate the current time against the new merged domain
+ const newAnimationDomain = syncedFilter
+ ? mergeTimeDomains([mergedDomain, syncedFilter.domain])
+ : mergedDomain;
+ const currentTime = isInRange(state.animationConfig.currentTime, newAnimationDomain)
? state.animationConfig.currentTime
- : mergedDomain[0];
+ : newAnimationDomain[0];
if (currentTime !== state.animationConfig.currentTime) {
// if currentTime changed, need to call animationTimeUpdater to re call formatLayerData
@@ -3266,8 +3265,12 @@ export function syncTimeFilterWithLayerTimelineUpdater(
mode: getSyncAnimationMode(newFilter)
});
+ newFilter = newState.filters[filterIdx] as TimeRangeFilter;
+
// set the animation config value to match filter value
- return setLayerAnimationTimeUpdater(newState, {value: newState.filters[filterIdx].value[0]});
+ return setLayerAnimationTimeUpdater(newState, {
+ value: newFilter.value[newFilter.syncTimelineMode]
+ });
}
// set domain and step
@@ -3316,9 +3319,14 @@ export function setTimeFilterTimelineModeUpdater(
) {
const {id: filterId, mode: syncTimelineMode} = action;
- const filter = state.filters.find(f => f.id === filterId) as TimeRangeFilter | undefined;
+ const filterIdx = state.filters.findIndex(f => f.id === filterId);
+ if (filterIdx === -1) {
+ return state;
+ }
+
+ const filter = state.filters[filterIdx] as TimeRangeFilter;
- if (!filter || !validateSyncAnimationMode(filter, syncTimelineMode)) {
+ if (!validateSyncAnimationMode(filter, syncTimelineMode)) {
return state;
}
@@ -3327,13 +3335,27 @@ export function setTimeFilterTimelineModeUpdater(
syncTimelineMode
};
- return {
+ const newState = {
...state,
filters: swap_(newFilter)(state.filters)
};
+
+ return adjustAnimationConfigWithFilter(newState, filterIdx);
+}
+
+function adjustAnimationConfigWithFilter(state, filterIdx) {
+ const filter = state.filters[filterIdx];
+ if (filter.syncedWithLayerTimeline) {
+ const timelineValue = getTimelineValueFromFilter(filter);
+ const value = state.animationConfig.timeSteps
+ ? snapToMarks(timelineValue, state.animationConfig.timeSteps)
+ : timelineValue;
+ return setLayerAnimationTimeUpdater(state, {value});
+ }
+ return state;
}
-function getTimelineFromTrip(filter) {
+function getTimelineValueFromFilter(filter) {
return filter.value[filter.syncTimelineMode];
}
diff --git a/src/utils/src/filter-utils.ts b/src/utils/src/filter-utils.ts
index 1e026bcd01..6690bb8b36 100644
--- a/src/utils/src/filter-utils.ts
+++ b/src/utils/src/filter-utils.ts
@@ -1103,7 +1103,7 @@ export function validateFiltersUpdateDatasets<
// TODO Better Typings here
const validated: any[] = [];
const failed: any[] = [];
- const {datasets} = state;
+ const {datasets, layers} = state;
let updatedDatasets = datasets;
// merge filters
@@ -1119,15 +1119,15 @@ export function validateFiltersUpdateDatasets<
applyToDatasets: string[];
augmentedDatasets: {[datasetId: string]: any};
}>(
- (acc, datasetId, idx) => {
+ (acc, datasetId) => {
const dataset = updatedDatasets[datasetId];
- const layers = state.layers.filter(l => l.config.dataId === dataset.id);
+ const datasetLayers = layers.filter(l => l.config.dataId === dataset.id);
const toValidate = acc.validatedFilter || filterToValidate;
const {filter: updatedFilter, dataset: updatedDataset} = validateFilterWithData(
acc.augmentedDatasets[datasetId] || dataset,
toValidate,
- layers
+ datasetLayers
);
if (updatedFilter) {
@@ -1154,7 +1154,20 @@ export function validateFiltersUpdateDatasets<
);
if (validatedFilter && isEqual(datasetIds, applyToDatasets)) {
- validatedFilter.value = adjustValueToFilterDomain(filterToValidate.value, validatedFilter);
+ let domain = validatedFilter.domain;
+ if ((validatedFilter as TimeRangeFilter).syncedWithLayerTimeline) {
+ const animatableLayers = getAnimatableVisibleLayers(layers);
+ domain = mergeTimeDomains([
+ ...animatableLayers.map(l => l.config.animation.domain || [0, 0]),
+ validatedFilter.domain
+ ]);
+ }
+
+ validatedFilter.value = adjustValueToFilterDomain(filterToValidate.value, {
+ ...validatedFilter,
+ domain
+ });
+
validated.push(updateFilterPlot(datasets, validatedFilter));
updatedDatasets = {
...updatedDatasets,
diff --git a/test/fixtures/synced-filter-with-trip-layer.js b/test/fixtures/synced-filter-with-trip-layer.js
new file mode 100644
index 0000000000..730ba55ecb
--- /dev/null
+++ b/test/fixtures/synced-filter-with-trip-layer.js
@@ -0,0 +1,397 @@
+// SPDX-License-Identifier: MIT
+// Copyright contributors to the kepler.gl project
+
+import {processKeplerglJSON} from '@kepler.gl/processors';
+import CloneDeep from 'lodash.clonedeep';
+import {keplerGlReducerCore as coreReducer} from '@kepler.gl/reducers';
+import {addDataToMap} from '@kepler.gl/actions';
+import {InitialState} from '../helpers/mock-state';
+
+export const syncedFilterWithTripLayerMap = {
+ datasets: [
+ {
+ version: 'v1',
+ data: {
+ id: 'ku6sngc2',
+ label: 'un_2573-few-points.csv',
+ color: [143, 47, 191],
+ allData: [
+ [
+ '2019/07/26 21:32:28.23',
+ 34.5585,
+ -117.13116,
+ 5.358,
+ 3.67,
+ 'Md',
+ 13,
+ 255,
+ 65,
+ 0.05,
+ 'NCSN',
+ 330724
+ ],
+ [
+ '2019/07/26 21:41:00.22',
+ 35.99,
+ -120.12033,
+ 12.058,
+ 2.98,
+ 'Md',
+ 41,
+ 127,
+ 8,
+ 0.08,
+ 'NCSN',
+ 330782
+ ],
+ [
+ '2019/07/26 22:04:22.72',
+ 40.50533,
+ -123.50616,
+ 10.046,
+ 2.74,
+ 'Md',
+ 20,
+ 49,
+ 4,
+ 0.14,
+ 'NCSN',
+ 330793
+ ],
+ [
+ '2019/07/26 22:05:35.14',
+ 38.80083,
+ -122.8005,
+ 0.577,
+ 2.66,
+ 'Md',
+ 27,
+ 58,
+ 1,
+ 0.04,
+ 'NCSN',
+ 331469
+ ],
+ [
+ '2019/07/26 23:04:44.55',
+ 40.15017,
+ -123.81734,
+ 19.269,
+ 2.88,
+ 'Md',
+ 21,
+ 160,
+ 6,
+ 0.1,
+ 'NCSN',
+ 331395
+ ],
+ [
+ '2019/07/26 23:30:11.18',
+ 38.03183,
+ -118.73933,
+ 1.896,
+ 2.62,
+ 'Md',
+ 40,
+ 206,
+ 25,
+ 0.07,
+ 'NCSN',
+ 331581
+ ],
+ [
+ '2019/07/26 23:37:25.34',
+ 38.04317,
+ -118.72916,
+ 6.125,
+ 2.89,
+ 'Md',
+ 37,
+ 209,
+ 26,
+ 0.07,
+ 'NCSN',
+ 331585
+ ],
+ [
+ '2019/07/26 23:38:51.29',
+ 37.13117,
+ -121.529,
+ 7.697,
+ 2.7,
+ 'Md',
+ 80,
+ 89,
+ 2,
+ 0.06,
+ 'NCSN',
+ 331584
+ ],
+ [
+ '2019/07/26 23:38:56.37',
+ 36.55633,
+ -121.149,
+ 7.151,
+ 3.36,
+ 'Md',
+ 64,
+ 33,
+ 4,
+ 0.06,
+ 'NCSN',
+ 331636
+ ],
+ [null, null, null, null, null, null, null, null, null, null, null, null]
+ ],
+ fields: [
+ {
+ name: 'DateTime',
+ type: 'timestamp',
+ format: 'YYYY/M/D HH:mm:ss.SSSS',
+ analyzerType: 'DATETIME'
+ },
+ {name: 'Latitude', type: 'real', format: '', analyzerType: 'FLOAT'},
+ {name: 'Longitude', type: 'real', format: '', analyzerType: 'FLOAT'},
+ {name: 'Depth', type: 'real', format: '', analyzerType: 'FLOAT'},
+ {name: 'Magnitude', type: 'real', format: '', analyzerType: 'FLOAT'},
+ {name: 'MagType', type: 'string', format: '', analyzerType: 'STRING'},
+ {name: 'NbStations', type: 'integer', format: '', analyzerType: 'INT'},
+ {name: 'Gap', type: 'integer', format: '', analyzerType: 'INT'},
+ {name: 'Distance', type: 'integer', format: '', analyzerType: 'INT'},
+ {name: 'RMS', type: 'real', format: '', analyzerType: 'FLOAT'},
+ {name: 'Source', type: 'string', format: '', analyzerType: 'STRING'},
+ {name: 'EventID', type: 'integer', format: '', analyzerType: 'INT'}
+ ]
+ }
+ },
+ {
+ version: 'v1',
+ data: {
+ id: '5l94bqp',
+ label: 'un_2573-trips.json',
+ color: [0, 92, 255],
+ allData: [
+ [
+ {
+ type: 'Feature',
+ properties: {vendor: 'A'},
+ geometry: {
+ type: 'LineString',
+ coordinates: [
+ [-74.20986, 40.81743, 0, 1564174363],
+ [-74.20987, 40.81755, 0, 1564174596],
+ [-74.20998, 40.81766, 0, 1564174709],
+ [-74.20986, 40.81773, 0, 1564174963],
+ [-74.20987, 40.81785, 0, 1564175196],
+ [-74.20998, 40.81806, 0, 1564175309],
+ [-74.20986, 40.81813, 0, 1564175563],
+ [-74.20987, 40.81825, 0, 1564175796],
+ [-74.20998, 40.81846, 0, 1564175909],
+ [-74.20986, 40.81853, 0, 1564176163],
+ [-74.20987, 40.81865, 0, 1564176396],
+ [-74.20998, 40.81876, 0, 1564176509],
+ [-74.20986, 40.81883, 0, 1564176763],
+ [-74.20987, 40.81895, 0, 1564176996],
+ [-74.20998, 40.81906, 0, 1564177109],
+ [-74.20986, 40.81913, 0, 1564177363],
+ [-74.20987, 40.81925, 0, 1564177596],
+ [-74.20998, 40.81936, 0, 1564177709],
+ [-74.20986, 40.81943, 0, 1564177963],
+ [-74.20987, 40.81955, 0, 1564178196],
+ [-74.20998, 40.81966, 0, 1564178309],
+ [-74.20986, 40.81973, 0, 1564178563],
+ [-74.20987, 40.81985, 0, 1564178796],
+ [-74.20998, 40.81996, 0, 1564179109]
+ ]
+ }
+ },
+ 'A'
+ ]
+ ],
+ fields: [
+ {name: '_geojson', type: 'geojson', format: '', analyzerType: 'GEOMETRY'},
+ {name: 'vendor', type: 'string', format: '', analyzerType: 'STRING'}
+ ]
+ }
+ }
+ ],
+ config: {
+ version: 'v1',
+ config: {
+ visState: {
+ filters: [
+ {
+ dataId: ['ku6sngc2'],
+ id: 'vxwjjz1sf',
+ name: ['DateTime'],
+ type: 'timeRange',
+ value: [1564176748230, 1564184336370],
+ enlarged: true,
+ plotType: 'histogram',
+ animationWindow: 'free',
+ yAxis: null,
+ speed: 1
+ }
+ ],
+ layers: [
+ {
+ id: 'proypi',
+ type: 'point',
+ config: {
+ dataId: 'ku6sngc2',
+ label: 'Point',
+ color: [255, 203, 153],
+ highlightColor: [252, 242, 26, 255],
+ columns: {lat: 'Latitude', lng: 'Longitude', altitude: null},
+ isVisible: true,
+ visConfig: {
+ radius: 10,
+ fixedRadius: false,
+ opacity: 0.8,
+ outline: false,
+ thickness: 2,
+ strokeColor: null,
+ colorRange: {
+ name: 'Global Warming',
+ type: 'sequential',
+ category: 'Uber',
+ colors: ['#5A1846', '#900C3F', '#C70039', '#E3611C', '#F1920E', '#FFC300']
+ },
+ strokeColorRange: {
+ name: 'Global Warming',
+ type: 'sequential',
+ category: 'Uber',
+ colors: ['#5A1846', '#900C3F', '#C70039', '#E3611C', '#F1920E', '#FFC300']
+ },
+ radiusRange: [0, 50],
+ filled: true
+ },
+ hidden: false,
+ textLabel: [
+ {
+ field: null,
+ color: [255, 255, 255],
+ size: 18,
+ offset: [0, 0],
+ anchor: 'start',
+ alignment: 'center'
+ }
+ ]
+ },
+ visualChannels: {
+ colorField: {name: 'Depth', type: 'real'},
+ colorScale: 'quantile',
+ strokeColorField: null,
+ strokeColorScale: 'quantile',
+ sizeField: null,
+ sizeScale: 'linear'
+ }
+ },
+ {
+ id: 'p4jyzm7',
+ type: 'trip',
+ config: {
+ dataId: '5l94bqp',
+ label: 'un_2573-trips',
+ color: [248, 149, 112],
+ highlightColor: [252, 242, 26, 255],
+ columns: {geojson: '_geojson'},
+ isVisible: true,
+ visConfig: {
+ opacity: 0.8,
+ thickness: 0.5,
+ colorRange: {
+ name: 'Global Warming',
+ type: 'sequential',
+ category: 'Uber',
+ colors: ['#5A1846', '#900C3F', '#C70039', '#E3611C', '#F1920E', '#FFC300']
+ },
+ trailLength: 180,
+ sizeRange: [0, 10]
+ },
+ hidden: false,
+ textLabel: [
+ {
+ field: null,
+ color: [255, 255, 255],
+ size: 18,
+ offset: [0, 0],
+ anchor: 'start',
+ alignment: 'center'
+ }
+ ]
+ },
+ visualChannels: {
+ colorField: null,
+ colorScale: 'quantile',
+ sizeField: null,
+ sizeScale: 'linear'
+ }
+ }
+ ],
+ interactionConfig: {
+ tooltip: {
+ fieldsToShow: {
+ ku6sngc2: [
+ {name: 'DateTime', format: null},
+ {name: 'Latitude', format: null},
+ {name: 'Longitude', format: null},
+ {name: 'Depth', format: null},
+ {name: 'Magnitude', format: null}
+ ],
+ '5l94bqp': [{name: 'vendor', format: null}]
+ },
+ compareMode: false,
+ compareType: 'absolute',
+ enabled: true
+ },
+ brush: {size: 0.5, enabled: false},
+ geocoder: {enabled: false},
+ coordinate: {enabled: false}
+ },
+ layerBlending: 'normal',
+ splitMaps: [],
+ animationConfig: {currentTime: 1564174363000, speed: 1}
+ },
+ mapState: {
+ bearing: 0,
+ dragRotate: false,
+ latitude: 37.68923,
+ longitude: -99.0136,
+ pitch: 0,
+ zoom: 4,
+ isSplit: false
+ },
+ mapStyle: {
+ styleType: 'dark',
+ topLayerGroups: {},
+ visibleLayerGroups: {
+ label: true,
+ road: true,
+ border: false,
+ building: true,
+ water: true,
+ land: true,
+ '3d building': false
+ },
+ threeDBuildingColor: [9.665468314072013, 17.18305478057247, 31.1442867897876],
+ mapStyles: {}
+ }
+ }
+ },
+ info: {
+ app: 'kepler.gl',
+ created_at: 'Wed Oct 20 2021 16:38:55 GMT-0400 (Eastern Daylight Time)',
+ title: 'keplergl_acitcdlh',
+ description: ''
+ }
+};
+
+export const mockStateWithSyncedFilterAndTripLayer = () => {
+ const initialState = CloneDeep(InitialState);
+ const result = processKeplerglJSON(syncedFilterWithTripLayerMap);
+ const newState = coreReducer(initialState, addDataToMap(result));
+
+ return newState;
+};
diff --git a/test/helpers/mock-state.js b/test/helpers/mock-state.js
index 60444452f6..04773661f4 100644
--- a/test/helpers/mock-state.js
+++ b/test/helpers/mock-state.js
@@ -5,7 +5,11 @@ import test from 'tape-catch';
import cloneDeep from 'lodash.clonedeep';
import {drainTasksForTesting} from 'react-palm/tasks';
-import {getInitialInputStyle, keplerGlReducerCore as keplerGlReducer} from '@kepler.gl/reducers';
+import {
+ getInitialInputStyle,
+ keplerGlReducerCore as keplerGlReducer,
+ syncTimeFilterWithLayerTimelineUpdater
+} from '@kepler.gl/reducers';
import {
VizColorPalette,
@@ -374,6 +378,8 @@ export function mockStateWithArcNeighbors(state) {
return updatedState;
}
+// export function mockStateWith
+
export function mockStateWithFilters(state) {
const initialState = state || mockStateWithFileUpload();
@@ -1230,3 +1236,20 @@ export const expectedGeojsonLayerHoverProp = {
mockKeplerProps.visState.interactionConfig.tooltip.config.fieldsToShow[testGeoJsonDataId],
layer: mockKeplerProps.visState.layers[1]
};
+
+export function stateWithTimeFilterAndTripLayer() {
+ const initialState = mockStateWithTripData();
+
+ return mockStateWithTripGeojson(initialState);
+}
+
+export function stateWithTimeFilterSyncedWithTripLayer() {
+ const initialState = stateWithTimeFilterAndTripLayer();
+ return {
+ ...initialState,
+ visState: syncTimeFilterWithLayerTimelineUpdater(initialState.visState, {
+ idx: 0,
+ enable: true
+ })
+ };
+}
diff --git a/test/node/reducers/vis-state-merger-test.js b/test/node/reducers/vis-state-merger-test.js
index 161cca7b76..30ef16a336 100644
--- a/test/node/reducers/vis-state-merger-test.js
+++ b/test/node/reducers/vis-state-merger-test.js
@@ -4,6 +4,7 @@
import test from 'tape';
import cloneDeep from 'lodash.clonedeep';
import Task, {withTask, drainTasksForTesting, succeedTaskInTest} from 'react-palm/tasks';
+import CloneDeep from 'lodash.clonedeep';
import keplerGlReducer, {
mergeFilters,
@@ -18,8 +19,11 @@ import keplerGlReducer, {
visStateReducer,
keplerGlReducerCore as coreReducer,
defaultInteractionConfig,
- getLayerOrderFromLayers
+ getLayerOrderFromLayers,
+ setFilterAnimationTimeUpdater,
+ syncTimeFilterWithLayerTimelineUpdater
} from '@kepler.gl/reducers';
+import {SYNC_TIMELINE_MODES} from '@kepler.gl/constants';
import SchemaManager, {CURRENT_VERSION, visStateSchema} from '@kepler.gl/schemas';
import {processKeplerglJSON} from '@kepler.gl/processors';
@@ -100,8 +104,8 @@ import {
mergedTripFilter,
mergedRateFilter
} from 'test/fixtures/geojson';
-import {mockStateWithPolygonFilter} from '../../fixtures/points-with-polygon-filter-map';
-import CloneDeep from 'lodash.clonedeep';
+import {mockStateWithPolygonFilter} from 'test/fixtures/points-with-polygon-filter-map';
+import {mockStateWithSyncedFilterAndTripLayer} from 'test/fixtures/synced-filter-with-trip-layer';
test('VisStateMerger.v0 -> mergeFilters -> toEmptyState', t => {
const savedConfig = cloneDeep(savedStateV0);
@@ -1965,6 +1969,61 @@ test('VisStateMerger -> load polygon filter map', t => {
t.end();
});
+test('VisStateMerger -> load time filter/trip layer synced map', t => {
+ const oldState = mockStateWithSyncedFilterAndTripLayer();
+ oldState.visState = syncTimeFilterWithLayerTimelineUpdater(oldState.visState, {
+ idx: 0,
+ enable: true
+ });
+
+ let filter = oldState.visState.filters[0];
+
+ oldState.visState = setFilterAnimationTimeUpdater(oldState.visState, {
+ idx: 0,
+ prop: 'value',
+ value: [filter.domain[0], filter.domain[0] + 1000]
+ });
+
+ filter = oldState.visState.filters[0];
+
+ const appStateToSave = SchemaManager.save(oldState);
+ const stateParsed = SchemaManager.load(appStateToSave);
+ const initialState = cloneDeep(InitialState);
+ const initialVisState = initialState.visState;
+
+ const visState = visStateReducer(
+ initialVisState,
+ updateVisData(stateParsed.datasets, {}, stateParsed.config)
+ );
+
+ const newFilter = visState.filters[0];
+
+ t.deepEqual(filter.value, newFilter.value, 'Should have loaded the same filter value');
+
+ // check syncedWithLayerTimeline
+ t.equal(
+ newFilter.syncedWithLayerTimeline,
+ true,
+ 'Should have set syncedWithLayerTimeline to true'
+ );
+
+ // check syncTimelineMode
+ t.equal(
+ newFilter.syncTimelineMode,
+ SYNC_TIMELINE_MODES.end,
+ 'Should have set syncTimelineMode to SYNC_TIMELINE_MODES.end'
+ );
+
+ // check animationConfig value
+ t.equal(
+ visState.animationConfig.currentTime,
+ oldState.visState.animationConfig.currentTime,
+ 'Should have set animationConfig value to filter value[0]'
+ );
+
+ t.end();
+});
+
test('VisStateMerger -> createLayerFromConfig with Parsed Layer', t => {
const oldState = CloneDeep(StateWFiles);
diff --git a/test/node/reducers/vis-state-test.js b/test/node/reducers/vis-state-test.js
index 6ef19ad861..ea7d743656 100644
--- a/test/node/reducers/vis-state-test.js
+++ b/test/node/reducers/vis-state-test.js
@@ -20,7 +20,9 @@ import {
defaultInteractionConfig,
prepareStateForDatasetReplace,
syncTimeFilterWithLayerTimelineUpdater,
- setTimeFilterTimelineModeUpdater
+ setTimeFilterTimelineModeUpdater,
+ setFilterAnimationTimeUpdater,
+ setFilterAnimationWindowUpdater
} from '@kepler.gl/reducers';
import {processCsvData, processGeojson} from '@kepler.gl/processors';
@@ -94,7 +96,7 @@ import {
testCsvDataId,
testGeoJsonDataId,
InitialState,
- mockStateWithTripGeojson
+ stateWithTimeFilterAndTripLayer
} from 'test/helpers/mock-state';
import {getNextColorMakerValue} from 'test/helpers/layer-utils';
import {expectedTripLayerConfig} from '../../fixtures/test-trip-csv-data';
@@ -104,6 +106,7 @@ import {
testCsvDataSlice1Id,
testCsvDataSlice2Id
} from '../../fixtures/test-csv-data';
+import {mockStateWithSyncedFilterAndTripLayer} from '../../fixtures/synced-filter-with-trip-layer';
const mockData = {
fields: [
@@ -5961,14 +5964,87 @@ test('#visStateReducer -> applyFilterFieldName', t => {
t.end();
});
-// sync filter with timeline
-function mockStateWithFilterAndTripLayer() {
- const initialState = CloneDeep(StateWFilters);
- return mockStateWithTripGeojson(initialState).visState;
+function mockStateWithFilterAndIntervalBasedAnimationLayer() {
+ let visState = stateWithTimeFilterAndTripLayer().visState;
+ visState = reducer(visState, VisStateActions.addLayer());
+ const mockedMetaData = {
+ minZoom: 0,
+ maxZoom: 4,
+ fields: [
+ {
+ id: 'Fires',
+ name: 'Fires',
+ type: 'real',
+ analyzerType: 'FLOAT',
+ format: '',
+ filterProps: {
+ fieldType: 'real',
+ domain: [1, 400],
+ domainStops: {
+ z: [0, 1, 2, 3],
+ stops: [
+ [1, 100],
+ [1, 200],
+ [1, 300],
+ [1, 400]
+ ],
+ interpolation: 'interpolate'
+ },
+ domainQuantiles: {
+ z: [0, 1, 2, 3],
+ quantiles: Array.from({length: 4}).map(() => [0, 10])
+ },
+ histogram: null,
+ value: [1, 400],
+ type: 'range',
+ typeOptions: ['range'],
+ gpu: true,
+ step: 1
+ },
+ indexBy: {
+ format: 'x',
+ type: 'timestamp',
+ mappedValue: {
+ 1580515200000: 'Fires|1580515200000',
+ 1583020800000: 'Fires|1583020800000',
+ 1585699200000: 'Fires|1585699200000',
+ 1588291200000: 'Fires|1588291200000'
+ },
+ timeDomain: {
+ domain: [1580515200000, 1588291200000],
+ timeSteps: [1580515200000, 1583020800000, 1585699200000, 1588291200000],
+ duration: 1000
+ }
+ }
+ }
+ ],
+ resolutionOffset: 4,
+ targetTimeInterval: TileTimeInterval.DAY,
+ tilesetIndex: undefined,
+ zipUrl: undefined
+ };
+
+ const lastLayerIndex = visState.layers.length - 1;
+ const layer = visState.layers[lastLayerIndex];
+ layer.meta = mockedMetaData;
+ layer.config = {
+ ...visState.layers[lastLayerIndex].config,
+ animation: {
+ domain: visState.animationConfig.domain,
+ timeSteps: visState.animationConfig.domain,
+ duration: 1000,
+ enabled: true,
+ startTime: visState.animationConfig.domain[0]
+ }
+ };
+
+ visState.layers[lastLayerIndex] = layer;
+
+ return visState;
}
test('#visStateReducer -> sync with time filter with trip layer', t => {
- let visState = mockStateWithFilterAndTripLayer();
+ let visState = mockStateWithSyncedFilterAndTripLayer().visState;
const animatableLayers = getAnimatableVisibleLayers(visState.layers);
t.equal(animatableLayers.length, 1, 'Should find 1 animatable layer');
@@ -6000,7 +6076,7 @@ test('#visStateReducer -> sync with time filter with trip layer', t => {
t.equal(
newFilter.syncTimelineMode,
SYNC_TIMELINE_MODES.end,
- 'Should have set syncTimelineMode to SYNC_TIMELINE_MODES.end (1)'
+ 'Should have set syncTimelineMode to SYNC_TIMELINE_MODES.end'
);
// check filter domains
@@ -6009,17 +6085,30 @@ test('#visStateReducer -> sync with time filter with trip layer', t => {
// check filter value
t.deepEqual(
newFilter.value,
- [1474588800000, 1565578836000],
+ [1564174363000, 1564184336370],
'Should have set filter value by combining filter and animationConfig domains'
);
// check animationConfig value
t.equal(
visState.animationConfig.currentTime,
- 1474588800000,
+ newFilter.value[newFilter.syncTimelineMode],
'Should have set animationConfig value to filter value[0]'
);
+ visState = setFilterAnimationTimeUpdater(visState, {
+ idx: 0,
+ prop: 'value',
+ value: [1474588800000, 1474588800010]
+ });
+
+ newFilter = visState.filters[0];
+ t.equal(
+ visState.animationConfig.currentTime,
+ newFilter.value[newFilter.syncTimelineMode],
+ 'Animation config current time value should match the filter value newFilter.syncTimelineMode=SYNC_TIMELINE_MODES.end'
+ );
+
// update syncTimelineMode
visState = setTimeFilterTimelineModeUpdater(visState, {
id: newFilter.id,
@@ -6034,6 +6123,54 @@ test('#visStateReducer -> sync with time filter with trip layer', t => {
SYNC_TIMELINE_MODES.start,
'Should have set syncTimelineMode to SYNC_TIMELINE_MODES.start'
);
+ // check animation config new value
+ t.equal(
+ visState.animationConfig.currentTime,
+ newFilter.value[newFilter.syncTimelineMode],
+ 'Animation config current time value should match the filter value newFilter.syncTimelineMode=SYNC_TIMELINE_MODES.start'
+ );
+
+ // update filter animation window to INCREMENTAL
+ visState = setFilterAnimationWindowUpdater(visState, {
+ id: newFilter.id,
+ animationWindow: ANIMATION_WINDOW.incremental
+ });
+ newFilter = visState.filters[0];
+
+ // check syncTimelineMode is back to SYNC_TIMELINE_MODES.end
+ t.equal(
+ newFilter.syncTimelineMode,
+ SYNC_TIMELINE_MODES.end,
+ 'SyncTimelineMode should be set to SYNC_TIMELINE_MODES.end when we switch to incremental mode'
+ );
+
+ // check animationConfig currentTime should be set to filter.value[filter.syncTimelineMode]
+ t.equal(
+ visState.animationConfig.currentTime,
+ newFilter.value[newFilter.syncTimelineMode],
+ 'SyncTimelineMode should be set to SYNC_TIMELINE_MODES.end when we switch to incremental mode'
+ );
+
+ // update filter animation window to INCREMENTAL
+ visState = setFilterAnimationWindowUpdater(visState, {
+ id: newFilter.id,
+ animationWindow: ANIMATION_WINDOW.interval
+ });
+ newFilter = visState.filters[0];
+
+ // check syncTimelineMode should stay the same
+ t.equal(
+ newFilter.syncTimelineMode,
+ SYNC_TIMELINE_MODES.end,
+ 'SyncTimelineMode should be set to SYNC_TIMELINE_MODES.end when we switch to incremental mode'
+ );
+
+ // check animationConfig currentTime value should stay the same
+ t.equal(
+ visState.animationConfig.currentTime,
+ newFilter.value[newFilter.syncTimelineMode],
+ 'SyncTimelineMode should remain the same with interval mode'
+ );
// ============
// Disable sync
@@ -6046,6 +6183,12 @@ test('#visStateReducer -> sync with time filter with trip layer', t => {
newFilter = visState.filters[0];
+ t.equal(
+ newFilter.animationWindow,
+ ANIMATION_WINDOW.interval,
+ 'Should keep the same filter animationWindow value'
+ );
+
// check syncedWithLayerTimeline
t.equal(
newFilter.syncedWithLayerTimeline,
@@ -6076,85 +6219,6 @@ test('#visStateReducer -> sync with time filter with trip layer', t => {
t.end();
});
-function mockStateWithFilterAndIntervalBasedAnimationLayer() {
- let visState = mockStateWithFilterAndTripLayer();
- visState = reducer(visState, VisStateActions.addLayer());
- const mockedMetaData = {
- minZoom: 0,
- maxZoom: 4,
- fields: [
- {
- id: 'Fires',
- name: 'Fires',
- type: 'real',
- analyzerType: 'FLOAT',
- format: '',
- filterProps: {
- fieldType: 'real',
- domain: [1, 400],
- domainStops: {
- z: [0, 1, 2, 3],
- stops: [
- [1, 100],
- [1, 200],
- [1, 300],
- [1, 400]
- ],
- interpolation: 'interpolate'
- },
- domainQuantiles: {
- z: [0, 1, 2, 3],
- quantiles: Array.from({length: 4}).map(() => [0, 10])
- },
- histogram: null,
- value: [1, 400],
- type: 'range',
- typeOptions: ['range'],
- gpu: true,
- step: 1
- },
- indexBy: {
- format: 'x',
- type: 'timestamp',
- mappedValue: {
- 1580515200000: 'Fires|1580515200000',
- 1583020800000: 'Fires|1583020800000',
- 1585699200000: 'Fires|1585699200000',
- 1588291200000: 'Fires|1588291200000'
- },
- timeDomain: {
- domain: [1580515200000, 1588291200000],
- timeSteps: [1580515200000, 1583020800000, 1585699200000, 1588291200000],
- duration: 1000
- }
- }
- }
- ],
- resolutionOffset: 4,
- targetTimeInterval: TileTimeInterval.DAY,
- tilesetIndex: undefined,
- zipUrl: undefined
- };
-
- const lastLayerIndex = visState.layers.length - 1;
- const layer = visState.layers[lastLayerIndex];
- layer.meta = mockedMetaData;
- layer.config = {
- ...visState.layers[lastLayerIndex].config,
- animation: {
- domain: visState.animationConfig.domain,
- timeSteps: visState.animationConfig.domain,
- duration: 1000,
- enabled: true,
- startTime: visState.animationConfig.domain[0]
- }
- };
-
- visState.layers[lastLayerIndex] = layer;
-
- return visState;
-}
-
test('#visStateReducer -> sync with time filter with hextile layer', t => {
let visState = mockStateWithFilterAndIntervalBasedAnimationLayer();
const animatableLayers = getAnimatableVisibleLayers(visState.layers);
@@ -6187,7 +6251,7 @@ test('#visStateReducer -> sync with time filter with hextile layer', t => {
'Should have set syncTimelineMode to SYNC_TIMELINE_MODES.end (1)'
);
- // check animation window wasn't updated
+ // check animation window to interval
t.equal(
newFilter.animationWindow,
ANIMATION_WINDOW.interval,
@@ -6207,14 +6271,14 @@ test('#visStateReducer -> sync with time filter with hextile layer', t => {
// check filter value
t.deepEqual(
newFilter.value,
- [1474588800000, 1474588800000],
+ [1421348739000, 1421348739000],
'Should have set filter value to the first interval step'
);
// check animationConfig value
t.equal(
visState.animationConfig.currentTime,
- 1474588800000,
+ newFilter.value[newFilter.syncTimelineMode],
'Should have set animationConfig value to filter value[0]'
);
diff --git a/test/node/utils/filter-utils-test.js b/test/node/utils/filter-utils-test.js
index 8ff4d4a14a..551524817c 100644
--- a/test/node/utils/filter-utils-test.js
+++ b/test/node/utils/filter-utils-test.js
@@ -16,6 +16,8 @@ import {
isInPolygon,
diffFilters,
getTimestampFieldDomain,
+ scaleSourceDomainToDestination,
+ mergeFilterWithTimeline,
createDataContainer
} from '@kepler.gl/utils';
@@ -522,3 +524,148 @@ test('filterUtils -> getTimestampFieldDomain', t => {
t.end();
});
+
+test('filterUtils -> scaleSourceDomainToDestination', t => {
+ const sourceDomain = [1564174363000, 1564179109000];
+ const destinationDomain = [1564174363000, 1564184336370];
+
+ t.deepEqual(scaleSourceDomainToDestination(sourceDomain, destinationDomain), [
+ 0,
+ 47.586723444532794
+ ]);
+
+ t.end();
+});
+
+test('filterUtils -> mergeFilterWithTimeline', t => {
+ const animationConfig = {
+ domain: [1564174363000, 1564179109000],
+ currentTime: 1564178316089.6846,
+ speed: 1,
+ isAnimating: false,
+ timeSteps: null,
+ timeFormat: null,
+ timezone: null,
+ defaultTimeFormat: 'L LTS',
+ duration: null
+ };
+
+ const filter = {
+ dataId: ['fe422b77-b0fd-4c0e-848c-190e7bf94a72'],
+ id: 'daqu9ulv',
+ fixedDomain: true,
+ enlarged: true,
+ isAnimating: false,
+ animationWindow: 'free',
+ speed: 1,
+ name: ['DateTime'],
+ type: 'timeRange',
+ fieldIdx: [0],
+ domain: [1564176748230, 1564184336370],
+ value: [1564176936089.6848, 1564178316089.6846],
+ plotType: {
+ interval: '1-minute',
+ defaultTimeFormat: 'L LT',
+ type: 'histogram',
+ aggregation: 'sum'
+ },
+ yAxis: null,
+ gpu: true,
+ syncedWithLayerTimeline: true,
+ syncTimelineMode: 1,
+ step: 1000,
+ mappedValue: [
+ 1564176748230,
+ 1564177260220,
+ 1564178662720,
+ 1564178735140,
+ 1564182284550,
+ 1564183811180,
+ 1564184245340,
+ 1564184331290,
+ 1564184336370
+ ],
+ defaultTimeFormat: 'L LTS',
+ fieldType: 'timestamp',
+ timeBins: {
+ 'fe422b77-b0fd-4c0e-848c-190e7bf94a72': {
+ '1-minute': [
+ {
+ count: 1,
+ indexes: [0],
+ x0: 1564176720000,
+ x1: 1564176780000
+ },
+ {
+ count: 1,
+ indexes: [1],
+ x0: 1564177260000,
+ x1: 1564177320000
+ },
+ {
+ count: 1,
+ indexes: [2],
+ x0: 1564178640000,
+ x1: 1564178700000
+ },
+ {
+ count: 1,
+ indexes: [3],
+ x0: 1564178700000,
+ x1: 1564178760000
+ },
+ {
+ count: 1,
+ indexes: [4],
+ x0: 1564182240000,
+ x1: 1564182300000
+ },
+ {
+ count: 1,
+ indexes: [5],
+ x0: 1564183800000,
+ x1: 1564183860000
+ },
+ {
+ count: 1,
+ indexes: [6],
+ x0: 1564184220000,
+ x1: 1564184280000
+ },
+ {
+ count: 2,
+ indexes: [7, 8],
+ x0: 1564184280000,
+ x1: 1564184340000
+ }
+ ]
+ }
+ },
+ gpuChannel: [0]
+ };
+
+ const {filter: newFilter, animationConfig: newAnimationConfig} = mergeFilterWithTimeline(
+ filter,
+ animationConfig
+ );
+
+ t.deepEqual(
+ newFilter.domain,
+ [1564174363000, 1564184336370],
+ 'Merged filter should have the same domain'
+ );
+
+ t.deepEqual(
+ newAnimationConfig.domain,
+ [1564174363000, 1564184336370],
+ 'Merged animationConfig should have the same domain'
+ );
+
+ t.deepEqual(
+ newFilter.domain,
+ newAnimationConfig.domain,
+ 'New filter and animationConfig should have the same domain'
+ );
+
+ t.end();
+});