Skip to content

Commit 913ba1c

Browse files
authored
[feat] support for table plugin in demo examples and privately stored datasets (#2923)
- support for custom table plugin + demo examples - support for custom table plugin + privately stored datasets - isAppleDevice util Signed-off-by: Ihor Dykhta <[email protected]>
1 parent 6307281 commit 913ba1c

File tree

8 files changed

+204
-45
lines changed

8 files changed

+204
-45
lines changed

examples/demo-app/src/actions.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
// CONSTANTS
2222
export const INIT = 'INIT';
2323
export const LOAD_REMOTE_RESOURCE_SUCCESS = 'LOAD_REMOTE_RESOURCE_SUCCESS';
24+
export const LOAD_REMOTE_DATASET_PROCESSED_SUCCESS = 'LOAD_REMOTE_DATASET_PROCESSED_SUCCESS';
2425
export const LOAD_REMOTE_RESOURCE_ERROR = 'LOAD_REMOTE_RESOURCE_ERROR';
2526
export const LOAD_MAP_SAMPLE_FILE = 'LOAD_MAP_SAMPLE_FILE';
2627
export const SET_SAMPLE_LOADING_STATUS = 'SET_SAMPLE_LOADING_STATUS';
@@ -46,6 +47,13 @@ export function loadRemoteResourceSuccess(response, config, options, remoteDatas
4647
};
4748
}
4849

50+
export function loadRemoteDatasetProcessedSuccessAction(result) {
51+
return {
52+
type: LOAD_REMOTE_DATASET_PROCESSED_SUCCESS,
53+
payload: result
54+
};
55+
}
56+
4957
export function loadRemoteResourceError(error, url) {
5058
return {
5159
type: LOAD_REMOTE_RESOURCE_ERROR,

examples/demo-app/src/reducers/index.js

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@
33

44
import {combineReducers} from 'redux';
55
import {handleActions} from 'redux-actions';
6+
import Task, {withTask} from 'react-palm/tasks';
67

7-
import keplerGlReducer, {combinedUpdaters, uiStateUpdaters} from '@kepler.gl/reducers';
8+
import {aiAssistantReducer} from '@kepler.gl/ai-assistant';
9+
import {EXPORT_MAP_FORMATS} from '@kepler.gl/constants';
810
import {processGeojson, processRowObject, processArrowTable} from '@kepler.gl/processors';
11+
import keplerGlReducer, {combinedUpdaters, uiStateUpdaters} from '@kepler.gl/reducers';
912
import KeplerGlSchema from '@kepler.gl/schemas';
10-
import {EXPORT_MAP_FORMATS} from '@kepler.gl/constants';
11-
import {aiAssistantReducer} from '@kepler.gl/ai-assistant';
13+
import {KeplerTable} from '@kepler.gl/table';
14+
import {getApplicationConfig} from '@kepler.gl/utils';
1215

1316
import {
1417
INIT,
1518
LOAD_MAP_SAMPLE_FILE,
1619
LOAD_REMOTE_RESOURCE_SUCCESS,
20+
LOAD_REMOTE_DATASET_PROCESSED_SUCCESS,
1721
LOAD_REMOTE_RESOURCE_ERROR,
18-
SET_SAMPLE_LOADING_STATUS
22+
SET_SAMPLE_LOADING_STATUS,
23+
loadRemoteDatasetProcessedSuccessAction
1924
} from '../actions';
2025

2126
import {CLOUD_PROVIDERS_CONFIGURATION} from '../constants/default-settings';
@@ -81,9 +86,35 @@ const demoReducer = combineReducers({
8186
aiAssistant: aiAssistantReducer
8287
});
8388

89+
async function loadRemoteResourceSuccessTask({
90+
dataUrl,
91+
datasetId,
92+
processorMethod,
93+
remoteDatasetConfig,
94+
unprocessedData
95+
}) {
96+
if (dataUrl) {
97+
const data = await processorMethod(unprocessedData);
98+
return {
99+
info: {
100+
id: datasetId
101+
},
102+
data
103+
};
104+
}
105+
106+
// remote datasets like vector tile datasets
107+
return remoteDatasetConfig;
108+
}
109+
110+
const LOAD_REMOTE_RESOURCE_SUCCESS_TASK = Task.fromPromise(
111+
loadRemoteResourceSuccessTask,
112+
'LOAD_REMOTE_RESOURCE_SUCCESS_TASK'
113+
);
114+
84115
// this can be moved into a action and call kepler.gl action
85116
/**
86-
*
117+
* Used to load Kepler.gl demo examples
87118
* @param state
88119
* @param action {map: resultset, config, map}
89120
* @returns {{app: {isMapLoading: boolean}, keplerGl: {map: (state|*)}}}
@@ -96,40 +127,62 @@ export const loadRemoteResourceSuccess = (state, action) => {
96127
const {shape} = dataUrl ? action.response : {};
97128
let processorMethod = processRowObject;
98129
let unprocessedData = action.response;
130+
unprocessedData = shape === 'object-row-table' ? action.response.data : unprocessedData;
99131

100132
if (dataUrl) {
101-
if (shape === 'arrow-table') {
102-
processorMethod = processArrowTable;
103-
} else if (shape === 'object-row-table') {
104-
processorMethod = processRowObject;
105-
unprocessedData = action.response.data;
106-
} else if (dataUrl.includes('.json') || dataUrl.includes('.geojson')) {
107-
processorMethod = processGeojson;
133+
const table = getApplicationConfig().table ?? KeplerTable;
134+
if (typeof table.getFileProcessor === 'function') {
135+
if (shape === 'arrow-table') {
136+
// arrow processor from table plugin expects batches
137+
unprocessedData = action.response.data.batches;
138+
}
139+
// use custom processors from table class
140+
const processorResult = table.getFileProcessor(unprocessedData);
141+
// TODO save processorResult.format here with the dataset
142+
processorMethod = processorResult.processor;
108143
} else {
109-
throw new Error('Failed to select data processor');
144+
if (shape === 'arrow-table') {
145+
processorMethod = processArrowTable;
146+
} else if (shape === 'object-row-table') {
147+
processorMethod = processRowObject;
148+
} else if (dataUrl.includes('.json') || dataUrl.includes('.geojson')) {
149+
processorMethod = processGeojson;
150+
} else {
151+
throw new Error('Failed to select data processor');
152+
}
110153
}
111154
}
112155

113-
const datasets = dataUrl
114-
? {
115-
info: {
116-
id: datasetId
117-
},
118-
data: processorMethod(unprocessedData)
119-
}
120-
: // remote datasets like vector tile datasets
121-
action.remoteDatasetConfig;
156+
// processorMethod can be async so create a task
157+
const task = LOAD_REMOTE_RESOURCE_SUCCESS_TASK({
158+
dataUrl,
159+
datasetId,
160+
processorMethod,
161+
remoteDatasetConfig: action.remoteDatasetConfig,
162+
unprocessedData
163+
}).bimap(
164+
datasets => loadRemoteDatasetProcessedSuccessAction({...action, datasets}),
165+
() => {
166+
throw new Error('loadRemoteResource data processor failed');
167+
}
168+
);
169+
170+
return withTask(state, task);
171+
};
172+
173+
const loadRemoteDatasetProcessedSuccess = (state, action) => {
174+
const {config, datasets, options} = action.payload;
122175

123-
const config = action.config ? KeplerGlSchema.parseSavedConfig(action.config) : null;
176+
const parsedConfig = config ? KeplerGlSchema.parseSavedConfig(config) : null;
124177

125178
const keplerGlInstance = combinedUpdaters.addDataToMapUpdater(
126179
state.keplerGl.map, // "map" is the id of your kepler.gl instance
127180
{
128181
payload: {
129182
datasets,
130-
config,
183+
config: parsedConfig,
131184
options: {
132-
centerMap: Boolean(!action.config)
185+
centerMap: Boolean(!config)
133186
}
134187
}
135188
}
@@ -139,7 +192,7 @@ export const loadRemoteResourceSuccess = (state, action) => {
139192
...state,
140193
app: {
141194
...state.app,
142-
currentSample: action.options,
195+
currentSample: options,
143196
isMapLoading: false // we turn off the spinner
144197
},
145198
keplerGl: {
@@ -177,6 +230,7 @@ export const loadRemoteResourceError = (state, action) => {
177230

178231
const composedUpdaters = {
179232
[LOAD_REMOTE_RESOURCE_SUCCESS]: loadRemoteResourceSuccess,
233+
[LOAD_REMOTE_DATASET_PROCESSED_SUCCESS]: loadRemoteDatasetProcessedSuccess,
180234
[LOAD_REMOTE_RESOURCE_ERROR]: loadRemoteResourceError
181235
};
182236

src/actions/src/provider-actions.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
import {Provider} from '@kepler.gl/cloud-providers';
1313

1414
// eslint-disable-next-line prettier/prettier
15-
const assignType = <T>(obj: T): { [K in keyof T]: `${typeof ACTION_PREFIX}${string & K}`; } => obj as any
15+
const assignType = <T>(obj: T): {[K in keyof T]: `${typeof ACTION_PREFIX}${string & K}`} =>
16+
obj as any;
1617
export const ActionTypes = assignType({
1718
EXPORT_FILE_TO_CLOUD: `${ACTION_PREFIX}EXPORT_FILE_TO_CLOUD`,
1819
EXPORT_FILE_SUCCESS: `${ACTION_PREFIX}EXPORT_FILE_SUCCESS`,
@@ -21,6 +22,7 @@ export const ActionTypes = assignType({
2122
POST_SAVE_LOAD_SUCCESS: `${ACTION_PREFIX}POST_SAVE_LOAD_SUCCESS`,
2223
LOAD_CLOUD_MAP: `${ACTION_PREFIX}LOAD_CLOUD_MAP`,
2324
LOAD_CLOUD_MAP_SUCCESS: `${ACTION_PREFIX}LOAD_CLOUD_MAP_SUCCESS`,
25+
LOAD_CLOUD_MAP_SUCCESS_2: `${ACTION_PREFIX}LOAD_CLOUD_MAP_SUCCESS_2`,
2426
LOAD_CLOUD_MAP_ERROR: `${ACTION_PREFIX}LOAD_CLOUD_MAP_ERROR`
2527
});
2628

@@ -110,6 +112,17 @@ export const loadCloudMapSuccess: (p: LoadCloudMapSuccessPayload) => {
110112
payload
111113
}));
112114

115+
/** LOAD_CLOUD_MAP_SUCCESS_2 */
116+
export type LoadCloudMapSuccess2Payload = LoadCloudMapSuccessPayload & {
117+
datasetsPayload: any;
118+
};
119+
export const loadCloudMapSuccess2: (p: LoadCloudMapSuccess2Payload) => {
120+
type: typeof ActionTypes.LOAD_CLOUD_MAP_SUCCESS_2;
121+
payload: LoadCloudMapSuccess2Payload;
122+
} = createAction(ActionTypes.LOAD_CLOUD_MAP_SUCCESS_2, (payload: LoadCloudMapSuccess2Payload) => ({
123+
payload
124+
}));
125+
113126
/** LOAD_CLOUD_MAP_ERROR */
114127
export type LoadCloudMapErrorPayload = {
115128
error: any;

src/reducers/src/provider-state-updaters.ts

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright contributors to the kepler.gl project
33

4-
import {withTask} from 'react-palm/tasks';
4+
import Task, {withTask} from 'react-palm/tasks';
55
import Console from 'global/console';
6-
import {getError, isPlainObject} from '@kepler.gl/utils';
6+
import {getApplicationConfig, getError, isPlainObject} from '@kepler.gl/utils';
77
import {generateHashId, toArray} from '@kepler.gl/common-utils';
88
import {
99
EXPORT_FILE_TO_CLOUD_TASK,
@@ -16,6 +16,7 @@ import {
1616
exportFileError,
1717
postSaveLoadSuccess,
1818
loadCloudMapSuccess,
19+
loadCloudMapSuccess2,
1920
loadCloudMapError,
2021
resetProviderStatus,
2122
removeNotification,
@@ -30,10 +31,11 @@ import {
3031
DATASET_FORMATS,
3132
OVERWRITE_MAP_ID
3233
} from '@kepler.gl/constants';
33-
import {ExportFileToCloudPayload} from '@kepler.gl/types';
34+
import {AddDataToMapPayload, ExportFileToCloudPayload} from '@kepler.gl/types';
3435

3536
import {FILE_CONFLICT_MSG, MapListItem} from '@kepler.gl/cloud-providers';
3637
import {DATASET_HANDLERS} from '@kepler.gl/processors';
38+
import {KeplerTable} from '@kepler.gl/table';
3739

3840
type ActionPayload<P> = {
3941
type?: string;
@@ -263,6 +265,17 @@ function getDatasetHandler(format) {
263265
return defaultHandler;
264266
}
265267

268+
// use custom processors from table class
269+
const TableClass = getApplicationConfig().table ?? KeplerTable;
270+
if (typeof TableClass.getFileProcessor === 'function') {
271+
const processorResult = TableClass.getFileProcessor(null, format);
272+
if (!processorResult.processor) {
273+
Console.warn(`No processor found for format ${format}, will use csv by default`);
274+
return defaultHandler;
275+
}
276+
return processorResult.processor;
277+
}
278+
266279
if (!DATASET_HANDLERS[format]) {
267280
const supportedFormat = Object.keys(DATASET_FORMATS)
268281
.map(k => `'${k}'`)
@@ -276,19 +289,46 @@ function getDatasetHandler(format) {
276289
return DATASET_HANDLERS[format];
277290
}
278291

279-
function parseLoadMapResponse(response, loadParams, provider) {
292+
/**
293+
* A task to handle async processorMethod
294+
* @param param0
295+
* @returns
296+
*/
297+
async function parseLoadMapResponseTask({
298+
response,
299+
loadParams,
300+
provider
301+
}: {
302+
response: ProviderActions.LoadCloudMapSuccessPayload['response'];
303+
loadParams: ProviderActions.LoadCloudMapSuccessPayload['loadParams'];
304+
provider: ProviderActions.LoadCloudMapSuccessPayload['provider'];
305+
}) {
280306
const {map, format} = response;
281307
const processorMethod = getDatasetHandler(format);
282308

283-
const parsedDatasets = toArray(map.datasets).map(ds => {
284-
if (format === DATASET_FORMATS.keplergl) {
285-
// no need to obtain id, directly pass them in
286-
return processorMethod(ds);
287-
}
288-
const info = (ds && ds.info) || {id: generateHashId(6)};
289-
const data = processorMethod(ds.data || ds);
290-
return {info, data};
291-
});
309+
let parsedDatasets: AddDataToMapPayload['datasets'] = [];
310+
311+
if (
312+
format === DATASET_FORMATS.keplergl &&
313+
processorMethod !== DATASET_HANDLERS[DATASET_FORMATS.keplergl]
314+
) {
315+
// plugin table provides processor for keplergl map, not single dataset with allData
316+
const parsedMap = await processorMethod(map);
317+
parsedDatasets = parsedMap.datasets;
318+
} else {
319+
const datasets = toArray(map.datasets);
320+
parsedDatasets = await Promise.all(
321+
datasets.map(async ds => {
322+
if (format === DATASET_FORMATS.keplergl) {
323+
// no need to obtain id, directly pass them in
324+
return await processorMethod(ds);
325+
}
326+
const info = (ds && ds.info) || {id: generateHashId(6)};
327+
const data = await processorMethod(ds.data || ds);
328+
return {info, data};
329+
})
330+
);
331+
}
292332

293333
const info = {
294334
...map.info,
@@ -302,11 +342,19 @@ function parseLoadMapResponse(response, loadParams, provider) {
302342
};
303343
}
304344

345+
const PARSE_LOAD_MAP_RESPONSE_TASK = Task.fromPromise(
346+
parseLoadMapResponseTask,
347+
'PARSE_LOAD_MAP_RESPONSE_TASK'
348+
);
349+
350+
/**
351+
* Used to load resources stored in a private storage.
352+
*/
305353
export const loadCloudMapSuccessUpdater = (
306354
state: ProviderState,
307355
action: ActionPayload<ProviderActions.LoadCloudMapSuccessPayload>
308356
): ProviderState => {
309-
const {response, loadParams, provider, onSuccess, onError} = action.payload;
357+
const {response, loadParams, provider, onError} = action.payload;
310358

311359
const formatError = checkLoadMapResponseError(response);
312360
if (formatError) {
@@ -316,6 +364,30 @@ export const loadCloudMapSuccessUpdater = (
316364
});
317365
}
318366

367+
// processorMethod can be async so create a task
368+
const parseLoadMapResponseTask = PARSE_LOAD_MAP_RESPONSE_TASK({
369+
response,
370+
loadParams,
371+
provider
372+
}).bimap(
373+
(datasetsPayload: AddDataToMapPayload) => {
374+
return loadCloudMapSuccess2({...action.payload, datasetsPayload});
375+
},
376+
error =>
377+
exportFileErrorUpdater(state, {
378+
payload: {error, provider, onError}
379+
})
380+
);
381+
382+
return withTask(state, parseLoadMapResponseTask);
383+
};
384+
385+
export const loadCloudMapSuccess2Updater = (
386+
state: ProviderState,
387+
action: ActionPayload<ProviderActions.LoadCloudMapSuccess2Payload>
388+
): ProviderState => {
389+
const {datasetsPayload, response, loadParams, provider, onSuccess} = action.payload;
390+
319391
const newState = {
320392
...state,
321393
mapSaved: provider.name,
@@ -324,10 +396,8 @@ export const loadCloudMapSuccessUpdater = (
324396
isProviderLoading: false
325397
};
326398

327-
const payload = parseLoadMapResponse(response, loadParams, provider);
328-
329399
const tasks = [
330-
ACTION_TASK().map(() => addDataToMap(payload)),
400+
ACTION_TASK().map(() => addDataToMap(datasetsPayload)),
331401
createActionTask(onSuccess, {response, loadParams, provider}),
332402
ACTION_TASK().map(() => postSaveLoadSuccess(`Map from ${provider.name} loaded`))
333403
].filter(d => d);

0 commit comments

Comments
 (0)