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(); +});