From c8475737b6ddfd5825e6acae027b8304e36a510d Mon Sep 17 00:00:00 2001 From: Igor Dykhta Date: Tue, 4 Apr 2023 20:14:37 +0300 Subject: [PATCH] [feat] Create layer correctly from saved layer config (#2179) --- src/components/src/common/data-table/grid.tsx | 1 + src/reducers/src/vis-state-merger.ts | 30 +++- src/reducers/src/vis-state-updaters.ts | 8 +- test/helpers/mock-state.js | 4 +- test/node/reducers/vis-state-merger-test.js | 27 ++- test/node/reducers/vis-state-test.js | 169 ++++++++++++++++++ 6 files changed, 226 insertions(+), 13 deletions(-) diff --git a/src/components/src/common/data-table/grid.tsx b/src/components/src/common/data-table/grid.tsx index 7d66a20997..52bb3bba7f 100644 --- a/src/components/src/common/data-table/grid.tsx +++ b/src/components/src/common/data-table/grid.tsx @@ -36,6 +36,7 @@ export default class GridHack extends PureComponent { } return; }; + _updateRef = x => { if (!this.grid && x) { this.grid = x; diff --git a/src/reducers/src/vis-state-merger.ts b/src/reducers/src/vis-state-merger.ts index d3e804bf3d..d6ed4b47cb 100644 --- a/src/reducers/src/vis-state-merger.ts +++ b/src/reducers/src/vis-state-merger.ts @@ -79,10 +79,29 @@ export function mergeFilters( }; } +export function isSavedLayerConfigV1(layerConfig: any): boolean { + // exported layer configuration contains visualChannels property + return layerConfig.visualChannels; +} + +export function parseLayerConfig(layerConfig: any): Layer | undefined { + // @ts-expect-error ParsedLayer vs Layer + return visStateSchema[CURRENT_VERSION].load({ + layers: [layerConfig], + // @ts-expect-error layerOrder not in SavedVisState + layerOrder: [0] + }).visState?.layers?.[0]; +} + export function createLayerFromConfig(state: VisState, layerConfig: any): Layer | null { + // check if the layer config is parsed + const parsedLayerConfig = isSavedLayerConfigV1(layerConfig) + ? parseLayerConfig(layerConfig) + : layerConfig; + // first validate config against dataset const {validated, failed} = validateLayersByDatasets(state.datasets, state.layerClasses, [ - layerConfig + parsedLayerConfig ]); if (failed?.length || !validated.length) { @@ -95,17 +114,16 @@ export function createLayerFromConfig(state: VisState, layerConfig: any): Layer return newLayer; } -export function serializeLayer(newLayer): ParsedLayer { +export function serializeLayer(newLayer): ParsedLayer | undefined { const savedVisState = visStateSchema[CURRENT_VERSION].save( - // @ts-expect-error not all expected properties are provided + // @ts-expect-error consider MinSavedVisState instead of SavedVisState { layers: [newLayer], layerOrder: [0] } ).visState; - const loadedLayer = visStateSchema[CURRENT_VERSION].load(savedVisState).visState?.layers?.[0]; - // @ts-expect-error - return loadedLayer; + + return visStateSchema[CURRENT_VERSION].load(savedVisState).visState?.layers?.[0]; } /** diff --git a/src/reducers/src/vis-state-updaters.ts b/src/reducers/src/vis-state-updaters.ts index 8c3075bff8..2030a0c1e2 100644 --- a/src/reducers/src/vis-state-updaters.ts +++ b/src/reducers/src/vis-state-updaters.ts @@ -469,9 +469,11 @@ export function layerTextLabelChangeUpdater( function validateExistingLayerWithData(dataset, layerClasses, layer) { const loadedLayer = serializeLayer(layer); - return validateLayerWithData(dataset, loadedLayer, layerClasses, { - allowEmptyColumn: true - }); + return loadedLayer + ? validateLayerWithData(dataset, loadedLayer, layerClasses, { + allowEmptyColumn: true + }) + : null; } /** diff --git a/test/helpers/mock-state.js b/test/helpers/mock-state.js index 70a5fe76ea..4c9ff389fc 100644 --- a/test/helpers/mock-state.js +++ b/test/helpers/mock-state.js @@ -94,7 +94,7 @@ export const InitialState = keplerGlReducer(undefined, {}); * Mock app state with uploaded geojson and csv file * @returns {Immutable} appState */ -function mockStateWithFileUpload() { +export function mockStateWithFileUpload() { const initialState = cloneDeep(InitialState); // load csv and geojson @@ -348,7 +348,7 @@ function mockStateWithTripData() { ); } -function mockStateWithLayerDimensions(state) { +export function mockStateWithLayerDimensions(state) { const initialState = state || mockStateWithFileUpload(); const layer0 = initialState.visState.layers.find( diff --git a/test/node/reducers/vis-state-merger-test.js b/test/node/reducers/vis-state-merger-test.js index 062447c598..1f8c5f0d3c 100644 --- a/test/node/reducers/vis-state-merger-test.js +++ b/test/node/reducers/vis-state-merger-test.js @@ -30,13 +30,14 @@ import keplerGlReducer, { mergeSplitMaps, insertLayerAtRightOrder, VIS_STATE_MERGERS, + createLayerFromConfig, applyMergersUpdater, visStateReducer, keplerGlReducerCore as coreReducer, defaultInteractionConfig } from '@kepler.gl/reducers'; -import SchemaManager from '@kepler.gl/schemas'; +import SchemaManager, {CURRENT_VERSION, visStateSchema} from '@kepler.gl/schemas'; import {processKeplerglJSON} from '@kepler.gl/processors'; import {updateVisData, receiveMapConfig, addDataToMap, registerEntry} from '@kepler.gl/actions'; @@ -90,7 +91,8 @@ import { StateWFilesFiltersLayerColor, StateWSplitMaps, testCsvDataId, - testGeoJsonDataId + testGeoJsonDataId, + StateWFiles } from 'test/helpers/mock-state'; import { @@ -113,6 +115,7 @@ import { mergedRateFilter } from 'test/fixtures/geojson'; import {mockStateWithPolygonFilter} from '../../fixtures/points-with-polygon-filter-map'; +import CloneDeep from 'lodash.clonedeep'; test('VisStateMerger.v0 -> mergeFilters -> toEmptyState', t => { const savedConfig = cloneDeep(savedStateV0); @@ -1928,6 +1931,26 @@ test('VisStateMerger -> load polygon filter map', t => { t.end(); }); +test('VisStateMerger -> createLayerFromConfig with Parsed Layer', t => { + const oldState = CloneDeep(StateWFiles); + + t.equal(oldState.visState.layers.length, 2, 'Should have layers'); + + // mock an exported layer config with visual channels + const savedLayer = visStateSchema[CURRENT_VERSION].save({ + layers: [oldState.visState.layers[0]], + layerOrder: [0] + }).visState.layers[0]; + + t.ok(savedLayer.visualChannels, 'Should have visualChannels'); + + const addedLayer = createLayerFromConfig(oldState.visState, savedLayer); + + t.ok(addedLayer.visConfigSettings, 'Should have visConfig settings loaded correctly'); + + t.end(); +}); + const MOCK_MERGE_TASK = Task.fromPromise( time => new Promise(resolve => window.setTimeout(resolve, time)), 'MOCK_MERGE_TASK' diff --git a/test/node/reducers/vis-state-test.js b/test/node/reducers/vis-state-test.js index 2bfc2dea34..48e32d39ec 100644 --- a/test/node/reducers/vis-state-test.js +++ b/test/node/reducers/vis-state-test.js @@ -1186,6 +1186,175 @@ test('#visStateReducer -> DUPLICATE_LAYER', t => { t.end(); }); +// TODO: not sure if this test is correct, it basically checks if the loaded visState is empty +// if we load configuration through the schema manager all layers will be changing from having visualChannels to visualConfig +// test('#visStateReducer -> SERIALIZE', t => { +// const configuration = { +// version: 'v1', +// config: { +// visState: { +// filters: [ +// { +// dataId: ['earthquakes'], +// id: 'vo18yorx', +// name: ['DateTime'], +// type: 'timeRange', +// value: [663046722470, 1301519405470], +// enlarged: true, +// plotType: 'histogram', +// animationWindow: 'free', +// yAxis: null, +// speed: 1 +// } +// ], +// layers: [ +// { +// id: 'hty62yd', +// type: 'point', +// config: { +// dataId: 'earthquakes', +// label: 'Point', +// color: [23, 184, 190], +// highlightColor: [252, 242, 26, 255], +// columns: { +// lat: 'Latitude', +// lng: 'Longitude', +// altitude: null +// }, +// isVisible: true, +// visConfig: { +// radius: 10, +// fixedRadius: false, +// opacity: 0.39, +// outline: false, +// thickness: 2, +// strokeColor: [23, 184, 190], +// colorRange: { +// name: 'ColorBrewer PRGn-6', +// type: 'diverging', +// category: 'ColorBrewer', +// colors: ['#762a83', '#af8dc3', '#e7d4e8', '#d9f0d3', '#7fbf7b', '#1b7837'], +// reversed: false +// }, +// strokeColorRange: { +// name: 'ColorBrewer PRGn-6', +// type: 'diverging', +// category: 'ColorBrewer', +// colors: ['#762a83', '#af8dc3', '#e7d4e8', '#d9f0d3', '#7fbf7b', '#1b7837'], +// reversed: false +// }, +// radiusRange: [4.2, 96.2], +// filled: true +// }, +// hidden: false, +// textLabel: [ +// { +// field: null, +// color: [255, 255, 255], +// size: 18, +// offset: [0, 0], +// anchor: 'start', +// alignment: 'center' +// } +// ] +// }, +// visualChannels: { +// colorField: { +// name: 'Magnitude', +// type: 'real' +// }, +// colorScale: 'quantize', +// strokeColorField: null, +// strokeColorScale: 'quantile', +// sizeField: { +// name: 'Magnitude', +// type: 'real' +// }, +// sizeScale: 'sqrt' +// } +// } +// ], +// interactionConfig: { +// tooltip: { +// fieldsToShow: { +// earthquakes: [ +// { +// name: 'DateTime', +// format: null +// }, +// { +// name: 'Latitude', +// format: null +// }, +// { +// name: 'Longitude', +// format: null +// }, +// { +// name: 'Depth', +// format: null +// }, +// { +// name: 'Magnitude', +// 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: null, +// speed: 1 +// } +// }, +// mapState: { +// bearing: 0, +// dragRotate: false, +// latitude: 37.05881309947238, +// longitude: -122.80009283836715, +// pitch: 0, +// zoom: 5.740491857794806, +// isSplit: false +// }, +// mapStyle: { +// styleType: 'light', +// topLayerGroups: {}, +// visibleLayerGroups: { +// border: false, +// building: true, +// label: true, +// land: true, +// road: true, +// water: true +// }, +// threeDBuildingColor: [9.665468314072013, 17.18305478057247, 31.1442867897876], +// mapStyles: {} +// } +// } +// }; +// +// const loadedVisState = visStateSchema[CURRENT_VERSION].load(configuration.config.visState); +// +// t.deepEqual(loadedVisState, {}, 'Should have the same configuration'); +// +// t.end(); +// }); + test('#visStateReducer -> UPDATE_VIS_DATA.1 -> No data', t => { const oldState = CloneDeep(InitialState).visState; const nextState1 = reducer(oldState, VisStateActions.updateVisData([{info: null, data: null}]));