@sqlrooms/kepler 0.29.0-rc.1 → 0.29.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +2 -1
- package/README.md +133 -6
- package/dist/KeplerSlice.d.ts +85 -11
- package/dist/KeplerSlice.d.ts.map +1 -1
- package/dist/KeplerSlice.js +391 -123
- package/dist/KeplerSlice.js.map +1 -1
- package/dist/components/CustomDndContext.d.ts.map +1 -1
- package/dist/components/CustomDndContext.js +5 -2
- package/dist/components/CustomDndContext.js.map +1 -1
- package/dist/components/CustomFilterManager.d.ts.map +1 -1
- package/dist/components/CustomFilterManager.js +20 -5
- package/dist/components/CustomFilterManager.js.map +1 -1
- package/dist/components/CustomFilterPanelHeader.d.ts.map +1 -1
- package/dist/components/CustomFilterPanelHeader.js +5 -5
- package/dist/components/CustomFilterPanelHeader.js.map +1 -1
- package/dist/components/CustomInteractionManager.d.ts.map +1 -1
- package/dist/components/CustomInteractionManager.js +4 -1
- package/dist/components/CustomInteractionManager.js.map +1 -1
- package/dist/components/CustomLayerManager.d.ts.map +1 -1
- package/dist/components/CustomLayerManager.js +51 -17
- package/dist/components/CustomLayerManager.js.map +1 -1
- package/dist/components/CustomMapDrawPanel.d.ts +8 -0
- package/dist/components/CustomMapDrawPanel.d.ts.map +1 -0
- package/dist/components/CustomMapDrawPanel.js +28 -0
- package/dist/components/CustomMapDrawPanel.js.map +1 -0
- package/dist/components/CustomMapLegend.d.ts +4 -1
- package/dist/components/CustomMapLegend.d.ts.map +1 -1
- package/dist/components/CustomMapLegend.js +39 -12
- package/dist/components/CustomMapLegend.js.map +1 -1
- package/dist/components/CustomMapLegendPanel.d.ts +3 -3
- package/dist/components/CustomMapLegendPanel.d.ts.map +1 -1
- package/dist/components/CustomMapLegendPanel.js +27 -4
- package/dist/components/CustomMapLegendPanel.js.map +1 -1
- package/dist/components/CustomSourceDataSelector.d.ts +18 -0
- package/dist/components/CustomSourceDataSelector.d.ts.map +1 -0
- package/dist/components/CustomSourceDataSelector.js +130 -0
- package/dist/components/CustomSourceDataSelector.js.map +1 -0
- package/dist/components/KeplerAddDataDialog.d.ts +2 -3
- package/dist/components/KeplerAddDataDialog.d.ts.map +1 -1
- package/dist/components/KeplerAddDataDialog.js +19 -8
- package/dist/components/KeplerAddDataDialog.js.map +1 -1
- package/dist/components/KeplerImageExport.d.ts.map +1 -1
- package/dist/components/KeplerImageExport.js +26 -8
- package/dist/components/KeplerImageExport.js.map +1 -1
- package/dist/components/KeplerInjector.d.ts.map +1 -1
- package/dist/components/KeplerInjector.js +5 -1
- package/dist/components/KeplerInjector.js.map +1 -1
- package/dist/components/KeplerMapContainer.d.ts.map +1 -1
- package/dist/components/KeplerMapContainer.js +45 -6
- package/dist/components/KeplerMapContainer.js.map +1 -1
- package/dist/components/KeplerPlotContainer.d.ts +1 -1
- package/dist/components/KeplerPlotContainer.d.ts.map +1 -1
- package/dist/components/KeplerPlotContainer.js +3 -1
- package/dist/components/KeplerPlotContainer.js.map +1 -1
- package/dist/components/KeplerProvider.d.ts.map +1 -1
- package/dist/components/KeplerProvider.js +3 -1
- package/dist/components/KeplerProvider.js.map +1 -1
- package/dist/components/KeplerTableSourceSelector.d.ts +20 -0
- package/dist/components/KeplerTableSourceSelector.d.ts.map +1 -0
- package/dist/components/KeplerTableSourceSelector.js +21 -0
- package/dist/components/KeplerTableSourceSelector.js.map +1 -0
- package/dist/components/SplitMapIndexContext.d.ts +2 -0
- package/dist/components/SplitMapIndexContext.d.ts.map +1 -0
- package/dist/components/SplitMapIndexContext.js +3 -0
- package/dist/components/SplitMapIndexContext.js.map +1 -0
- package/dist/hooks/useDndEffects.d.ts +1 -1
- package/dist/hooks/useDndEffects.d.ts.map +1 -1
- package/dist/hooks/useDndEffects.js +4 -0
- package/dist/hooks/useDndEffects.js.map +1 -1
- package/dist/hooks/useDndLayers.d.ts +1 -1
- package/dist/hooks/useDndLayers.d.ts.map +1 -1
- package/dist/hooks/useDndLayers.js +4 -0
- package/dist/hooks/useDndLayers.js.map +1 -1
- package/dist/hooks/useKeplerStateActions.d.ts +7 -6
- package/dist/hooks/useKeplerStateActions.d.ts.map +1 -1
- package/dist/hooks/useKeplerStateActions.js +1 -1
- package/dist/hooks/useKeplerStateActions.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/keplerTableSelection.d.ts +142 -0
- package/dist/keplerTableSelection.d.ts.map +1 -0
- package/dist/keplerTableSelection.js +232 -0
- package/dist/keplerTableSelection.js.map +1 -0
- package/dist/styles/theme.d.ts +12 -0
- package/dist/styles/theme.d.ts.map +1 -1
- package/dist/styles/theme.js +12 -0
- package/dist/styles/theme.js.map +1 -1
- package/package.json +43 -37
package/dist/KeplerSlice.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addDataToMap, addLayer as addLayerAction, deleteEntry, ActionTypes as KeplerActionTypes, registerEntry, removeDataset, requestMapStyles, wrapTo, } from '@kepler.gl/actions';
|
|
1
|
+
import { addDataToMap, addFilter as addFilterAction, addLayer as addLayerAction, deleteEntry, interactionConfigChange, ActionTypes as KeplerActionTypes, registerEntry, removeDataset, requestMapStyles, setFilter as setFilterAction, setFilterAnimationTime as setFilterAnimationTimeAction, setFilterAnimationWindow as setFilterAnimationWindowAction, setFilterView as setFilterViewAction, toggleLayerForMap as toggleLayerForMapAction, toggleSplitMap as toggleSplitMapAction, wrapTo, } from '@kepler.gl/actions';
|
|
2
2
|
import { ALL_FIELD_TYPES } from '@kepler.gl/constants';
|
|
3
3
|
import { castDuckDBTypesForKepler, getDuckDBColumnTypes, getDuckDBColumnTypesMap, restoreGeoarrowMetadata, setGeoArrowWKBExtension, } from '@kepler.gl/duckdb';
|
|
4
4
|
import { arrowSchemaToFields } from '@kepler.gl/processors';
|
|
@@ -7,11 +7,15 @@ import { KeplerGLSchemaClass } from '@kepler.gl/schemas';
|
|
|
7
7
|
import { KeplerTable } from '@kepler.gl/table';
|
|
8
8
|
import { initApplicationConfig, } from '@kepler.gl/utils';
|
|
9
9
|
import { createId } from '@paralleldrive/cuid2';
|
|
10
|
+
import { KeplerSliceConfig } from '@sqlrooms/kepler-config';
|
|
10
11
|
import { createSlice, useBaseRoomShellStore, } from '@sqlrooms/room-shell';
|
|
12
|
+
import { getTheme } from '@sqlrooms/ui';
|
|
11
13
|
import { produce, setAutoFreeze } from 'immer';
|
|
12
14
|
import { taskMiddleware } from 'react-palm/tasks';
|
|
13
15
|
import { compose } from 'redux';
|
|
14
16
|
import { createLogger } from 'redux-logger';
|
|
17
|
+
import { getTableIdentity, getUnqualifiedSqlIdentifier, } from '@sqlrooms/duckdb-core';
|
|
18
|
+
import { findKeplerTableForDatasetId, getKeplerDatasetIdForTable, getKeplerTableLabel, } from './keplerTableSelection';
|
|
15
19
|
setAutoFreeze(false); // Kepler attempts to mutate redux state, so we need to disable immer's auto freeze to avoid errors
|
|
16
20
|
const KeplerGLSchemaManager = new KeplerGLSchemaClass();
|
|
17
21
|
class DesktopKeplerTable extends KeplerTable {
|
|
@@ -20,9 +24,40 @@ class DesktopKeplerTable extends KeplerTable {
|
|
|
20
24
|
return (d) => d;
|
|
21
25
|
};
|
|
22
26
|
}
|
|
27
|
+
function createDefaultMapKeplerState(resolvedTheme) {
|
|
28
|
+
return {
|
|
29
|
+
mapStyle: {
|
|
30
|
+
styleType: resolvedTheme === 'dark' ? 'dark-matter' : 'positron',
|
|
31
|
+
},
|
|
32
|
+
uiState: {
|
|
33
|
+
...INITIAL_UI_STATE,
|
|
34
|
+
activeSidePanel: null,
|
|
35
|
+
currentModal: null,
|
|
36
|
+
mapControls: {
|
|
37
|
+
visibleLayers: INITIAL_UI_STATE.mapControls.visibleLayers,
|
|
38
|
+
splitMap: {
|
|
39
|
+
show: true,
|
|
40
|
+
active: false,
|
|
41
|
+
},
|
|
42
|
+
mapLegend: {
|
|
43
|
+
show: true,
|
|
44
|
+
active: false,
|
|
45
|
+
},
|
|
46
|
+
toggle3d: {
|
|
47
|
+
show: true,
|
|
48
|
+
active: false,
|
|
49
|
+
},
|
|
50
|
+
mapDraw: {
|
|
51
|
+
show: true,
|
|
52
|
+
active: false,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
23
58
|
export function createDefaultKeplerConfig(props) {
|
|
24
59
|
const mapId = createId();
|
|
25
|
-
|
|
60
|
+
const config = {
|
|
26
61
|
maps: [
|
|
27
62
|
{
|
|
28
63
|
id: mapId,
|
|
@@ -31,10 +66,9 @@ export function createDefaultKeplerConfig(props) {
|
|
|
31
66
|
lastOpenedAt: Date.now(),
|
|
32
67
|
},
|
|
33
68
|
],
|
|
34
|
-
currentMapId: mapId,
|
|
35
|
-
openTabs: [mapId],
|
|
36
69
|
...props,
|
|
37
70
|
};
|
|
71
|
+
return KeplerSliceConfig.parse(config);
|
|
38
72
|
}
|
|
39
73
|
export function hasMapId(action) {
|
|
40
74
|
return (typeof action.payload === 'object' &&
|
|
@@ -44,6 +78,25 @@ export function hasMapId(action) {
|
|
|
44
78
|
action.payload.meta !== null &&
|
|
45
79
|
'_id_' in action.payload.meta);
|
|
46
80
|
}
|
|
81
|
+
function normalizeAddTableToMapParams(paramsOrMapId, tableName, options = {}, config = {}, loadOptions = {}) {
|
|
82
|
+
if (typeof paramsOrMapId === 'object') {
|
|
83
|
+
return {
|
|
84
|
+
options: {},
|
|
85
|
+
config: {},
|
|
86
|
+
...paramsOrMapId,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (!tableName) {
|
|
90
|
+
throw new Error('addTableToMap requires a tableName.');
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
mapId: paramsOrMapId,
|
|
94
|
+
tableName,
|
|
95
|
+
options,
|
|
96
|
+
config,
|
|
97
|
+
datasetId: loadOptions.datasetId,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
47
100
|
// Auto save will be triggered in middleware on every kepler action
|
|
48
101
|
// skip these actions to avoid unnecessary save
|
|
49
102
|
const SKIP_AUTO_SAVE_ACTIONS = [
|
|
@@ -51,37 +104,60 @@ const SKIP_AUTO_SAVE_ACTIONS = [
|
|
|
51
104
|
KeplerActionTypes.UPDATE_MAP,
|
|
52
105
|
KeplerActionTypes.MOUSE_MOVE,
|
|
53
106
|
];
|
|
54
|
-
export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfigProps,
|
|
55
|
-
mapStyle: { styleType: 'positron' },
|
|
56
|
-
uiState: {
|
|
57
|
-
...INITIAL_UI_STATE,
|
|
58
|
-
currentModal: null,
|
|
59
|
-
mapControls: {
|
|
60
|
-
visibleLayers: INITIAL_UI_STATE.mapControls.visibleLayers,
|
|
61
|
-
mapLegend: {
|
|
62
|
-
show: true,
|
|
63
|
-
active: false,
|
|
64
|
-
},
|
|
65
|
-
toggle3d: {
|
|
66
|
-
show: true,
|
|
67
|
-
active: false,
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
}, actionLogging = false, middlewares: additionalMiddlewares = [], applicationConfig, onAction, } = {}) {
|
|
107
|
+
export function createKeplerSlice({ basicKeplerProps = {}, keplerTheme, modalPortalTarget = 'container', config: initialConfigProps, createInitialMapKeplerState, actionLogging = false, middlewares: additionalMiddlewares = [], applicationConfig, tableSelection = {}, onAction, } = {}) {
|
|
72
108
|
const initialConfig = createDefaultKeplerConfig(initialConfigProps);
|
|
73
109
|
initApplicationConfig({
|
|
74
110
|
table: DesktopKeplerTable,
|
|
75
111
|
...applicationConfig,
|
|
76
112
|
});
|
|
113
|
+
let syncKeplerPromise = null;
|
|
114
|
+
let configRestoreDepth = 0;
|
|
115
|
+
const configRestorePromises = new Set();
|
|
116
|
+
// When a caller arrives while a sync is already in-flight, the in-flight run
|
|
117
|
+
// may have captured a stale snapshot of db.tables/kepler.map (missing maps or
|
|
118
|
+
// tables added by createMap, duplicateMap, etc.). Setting this flag tells the
|
|
119
|
+
// active run to loop once more with a fresh snapshot after it finishes, so
|
|
120
|
+
// late mutations are never silently dropped.
|
|
121
|
+
let pendingKeplerSync = false;
|
|
77
122
|
const updateMapLastOpenedAt = (maps, mapId, now = Date.now()) => {
|
|
78
123
|
const map = maps.find((item) => item.id === mapId);
|
|
79
124
|
if (map) {
|
|
80
125
|
map.lastOpenedAt = now;
|
|
81
126
|
}
|
|
82
127
|
};
|
|
83
|
-
return createSlice((set, get) => {
|
|
84
|
-
|
|
128
|
+
return createSlice((set, get, _store) => {
|
|
129
|
+
function getDefaultDbSchema() {
|
|
130
|
+
const currentDatabase = get().db.currentDatabase;
|
|
131
|
+
const currentSchema = get().db.currentSchema;
|
|
132
|
+
if (!currentDatabase || !currentSchema) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
database: currentDatabase,
|
|
137
|
+
schema: currentSchema,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const resolvedTableSelection = {
|
|
141
|
+
...tableSelection,
|
|
142
|
+
};
|
|
143
|
+
if (!resolvedTableSelection.defaultDbSchema) {
|
|
144
|
+
resolvedTableSelection.defaultDbSchema = getDefaultDbSchema;
|
|
145
|
+
}
|
|
146
|
+
function resolveInitialMapKeplerState(context) {
|
|
147
|
+
const resolvedTheme = getTheme();
|
|
148
|
+
const defaultInitialMapKeplerState = createDefaultMapKeplerState(resolvedTheme);
|
|
149
|
+
return (createInitialMapKeplerState?.({
|
|
150
|
+
...context,
|
|
151
|
+
defaultInitialMapKeplerState,
|
|
152
|
+
}) ?? defaultInitialMapKeplerState);
|
|
153
|
+
}
|
|
154
|
+
function createKeplerReducer(context) {
|
|
155
|
+
return keplerGlReducer.initialState(resolveInitialMapKeplerState(context));
|
|
156
|
+
}
|
|
157
|
+
function registerMapEntry(id) {
|
|
158
|
+
return registerEntry({ id, ...basicKeplerProps });
|
|
159
|
+
}
|
|
160
|
+
const keplerReducer = createKeplerReducer({ reason: 'initialize' });
|
|
85
161
|
const middlewares = [
|
|
86
162
|
taskMiddleware,
|
|
87
163
|
saveKeplerConfigMiddleware,
|
|
@@ -111,23 +187,37 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
|
|
|
111
187
|
kepler: {
|
|
112
188
|
config: initialConfig,
|
|
113
189
|
basicKeplerProps,
|
|
190
|
+
keplerTheme,
|
|
191
|
+
modalPortalTarget,
|
|
192
|
+
tableSelection: resolvedTableSelection,
|
|
114
193
|
map: {},
|
|
194
|
+
isRestoringConfig: false,
|
|
115
195
|
dispatchAction: () => { },
|
|
116
196
|
__reduxProviderStore: undefined,
|
|
117
197
|
forwardDispatch: {},
|
|
118
198
|
setConfig: (config) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
199
|
+
void get()
|
|
200
|
+
.kepler.withConfigPersistencePaused(async () => {
|
|
201
|
+
const nextConfig = KeplerSliceConfig.parse(config);
|
|
202
|
+
set((state) => produce(state, (draft) => {
|
|
203
|
+
draft.kepler.config = nextConfig;
|
|
204
|
+
}));
|
|
205
|
+
updateMapReduxStates();
|
|
206
|
+
updateForwardDispatch();
|
|
207
|
+
updateMapConfigs();
|
|
208
|
+
await get().kepler.syncKeplerDatasets();
|
|
209
|
+
})
|
|
210
|
+
.catch((error) => {
|
|
211
|
+
console.error('setConfig: failed to restore Kepler config', error);
|
|
212
|
+
});
|
|
126
213
|
},
|
|
127
214
|
async initialize() {
|
|
128
215
|
const config = get().kepler.config;
|
|
129
|
-
const
|
|
130
|
-
|
|
216
|
+
const keplerInitialState = config.maps.reduce((mapState, map) => createKeplerReducer({
|
|
217
|
+
reason: 'initialize',
|
|
218
|
+
mapId: map.id,
|
|
219
|
+
name: map.name,
|
|
220
|
+
})(mapState, registerMapEntry(map.id)), {});
|
|
131
221
|
set({
|
|
132
222
|
kepler: {
|
|
133
223
|
...get().kepler,
|
|
@@ -150,24 +240,101 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
|
|
|
150
240
|
// @ts-expect-error - Symbol.observable is not defined in the Redux type definitions
|
|
151
241
|
[Symbol.observable]: () => { },
|
|
152
242
|
},
|
|
243
|
+
isRestoringConfig: get().kepler.isRestoringConfig,
|
|
153
244
|
},
|
|
154
245
|
});
|
|
155
246
|
updateForwardDispatch();
|
|
156
|
-
updateMapConfigs();
|
|
157
247
|
await get().kepler.syncKeplerDatasets();
|
|
158
|
-
|
|
248
|
+
updateMapConfigs();
|
|
249
|
+
for (const mapId of Object.keys(get().kepler.map)) {
|
|
250
|
+
requestMapStyle(mapId);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
async destroy() {
|
|
254
|
+
// no-op
|
|
159
255
|
},
|
|
160
256
|
addLayer: (mapId, layer, datasetId) => {
|
|
161
257
|
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
162
258
|
get().kepler.dispatchAction(mapId, addLayerAction(layer, datasetId));
|
|
163
259
|
},
|
|
164
|
-
|
|
260
|
+
addFilter: (mapId, datasetId) => {
|
|
261
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
262
|
+
get().kepler.dispatchAction(mapId, addFilterAction(datasetId));
|
|
263
|
+
},
|
|
264
|
+
setFilter: (mapId, filterIdx, prop, value, valueIndex) => {
|
|
265
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
266
|
+
get().kepler.dispatchAction(mapId, setFilterAction(filterIdx, prop, value, valueIndex));
|
|
267
|
+
},
|
|
268
|
+
setFilterAnimationTime: (mapId, filterIdx, prop, value) => {
|
|
269
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
270
|
+
get().kepler.dispatchAction(mapId, setFilterAnimationTimeAction(filterIdx, prop, value));
|
|
271
|
+
},
|
|
272
|
+
setFilterAnimationWindow: (mapId, filterId, animationWindow) => {
|
|
273
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
274
|
+
get().kepler.dispatchAction(mapId, setFilterAnimationWindowAction({ id: filterId, animationWindow }));
|
|
275
|
+
},
|
|
276
|
+
setFilterView: (mapId, filterIdx, view) => {
|
|
277
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
278
|
+
get().kepler.dispatchAction(mapId, setFilterViewAction(filterIdx, view));
|
|
279
|
+
},
|
|
280
|
+
updateTooltipFields: (mapId, datasetId, fieldNames) => {
|
|
281
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
282
|
+
const mapState = get().kepler.map[mapId];
|
|
283
|
+
const tooltipConfig = mapState?.visState?.interactionConfig?.tooltip;
|
|
284
|
+
if (!tooltipConfig)
|
|
285
|
+
return;
|
|
286
|
+
const currentFieldsToShow = tooltipConfig.config?.fieldsToShow || {};
|
|
287
|
+
const existingFields = currentFieldsToShow[datasetId] || [];
|
|
288
|
+
const existingByName = new Map(existingFields.map((field) => [
|
|
289
|
+
field.name,
|
|
290
|
+
field,
|
|
291
|
+
]));
|
|
292
|
+
const nextFields = Array.from(new Set(fieldNames)).map((name) => existingByName.get(name) ?? { name, format: null });
|
|
293
|
+
if (nextFields.length === existingFields.length &&
|
|
294
|
+
nextFields.every((field, index) => field.name === existingFields[index]?.name &&
|
|
295
|
+
field.format === existingFields[index]?.format)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
get().kepler.dispatchAction(mapId, interactionConfigChange({
|
|
299
|
+
...tooltipConfig,
|
|
300
|
+
config: {
|
|
301
|
+
...tooltipConfig.config,
|
|
302
|
+
fieldsToShow: {
|
|
303
|
+
...currentFieldsToShow,
|
|
304
|
+
[datasetId]: nextFields,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
}));
|
|
308
|
+
},
|
|
309
|
+
toggleSplitMap: (mapId, index) => {
|
|
310
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
311
|
+
get().kepler.dispatchAction(mapId, toggleSplitMapAction(index));
|
|
312
|
+
},
|
|
313
|
+
toggleLayerForMap: (mapId, mapIndex, layerId) => {
|
|
314
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
315
|
+
get().kepler.dispatchAction(mapId, toggleLayerForMapAction(mapIndex, layerId));
|
|
316
|
+
},
|
|
317
|
+
addTableToMap: (async (paramsOrMapId, legacyTableName, legacyOptions, legacyConfig, legacyLoadOptions) => {
|
|
318
|
+
const { mapId, tableName, options, config, datasetId: explicitDatasetId, } = normalizeAddTableToMapParams(paramsOrMapId, legacyTableName, legacyOptions, legacyConfig, legacyLoadOptions);
|
|
319
|
+
const tableSelection = get().kepler.tableSelection;
|
|
320
|
+
const table = findKeplerTableForDatasetId(get().db.tables, tableName, tableSelection);
|
|
321
|
+
const sqlTableName = table
|
|
322
|
+
? getTableIdentity(table.table)
|
|
323
|
+
: tableName;
|
|
324
|
+
let datasetId = explicitDatasetId;
|
|
325
|
+
if (!datasetId && table) {
|
|
326
|
+
datasetId = getKeplerDatasetIdForTable(table, tableSelection);
|
|
327
|
+
}
|
|
328
|
+
if (!datasetId) {
|
|
329
|
+
datasetId =
|
|
330
|
+
getUnqualifiedSqlIdentifier(String(sqlTableName)) ?? tableName;
|
|
331
|
+
}
|
|
165
332
|
const connector = await get().db.getConnector();
|
|
166
333
|
const duckDbColumns = await getDuckDBColumnTypes({
|
|
167
334
|
query: (query) => connector.query(query).result,
|
|
168
|
-
},
|
|
335
|
+
}, sqlTableName);
|
|
169
336
|
const tableDuckDBTypes = getDuckDBColumnTypesMap(duckDbColumns);
|
|
170
|
-
const adjustedQuery = castDuckDBTypesForKepler(
|
|
337
|
+
const adjustedQuery = castDuckDBTypesForKepler(sqlTableName, duckDbColumns);
|
|
171
338
|
const arrowResult = await connector.query(adjustedQuery).result;
|
|
172
339
|
setGeoArrowWKBExtension(arrowResult, duckDbColumns);
|
|
173
340
|
// TODO remove once DuckDB doesn't drop geoarrow metadata
|
|
@@ -175,98 +342,169 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
|
|
|
175
342
|
const fields = arrowSchemaToFields(arrowResult, tableDuckDBTypes);
|
|
176
343
|
const cols = Array.from({ length: arrowResult.numCols }, (_, i) => arrowResult.getChildAt(i)).filter((col) => col);
|
|
177
344
|
if (fields && cols) {
|
|
345
|
+
let label = tableName;
|
|
346
|
+
if (table) {
|
|
347
|
+
label = getKeplerTableLabel(table, tableSelection);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
label =
|
|
351
|
+
getUnqualifiedSqlIdentifier(String(sqlTableName)) ?? tableName;
|
|
352
|
+
}
|
|
178
353
|
const datasets = {
|
|
179
354
|
data: { fields, cols, rows: [], arrowTable: arrowResult },
|
|
180
|
-
info: { label
|
|
181
|
-
metadata: { tableName },
|
|
355
|
+
info: { label, id: datasetId },
|
|
356
|
+
metadata: { tableName: sqlTableName },
|
|
182
357
|
};
|
|
183
|
-
get().kepler.dispatchAction(mapId, addDataToMap({ datasets, options }));
|
|
358
|
+
get().kepler.dispatchAction(mapId, addDataToMap({ datasets, options, config }));
|
|
184
359
|
}
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
return get().kepler.config.maps.find((map) => map.id === get().kepler.config.currentMapId);
|
|
188
|
-
},
|
|
189
|
-
setCurrentMapId: (mapId) => {
|
|
190
|
-
return set((state) => produce(state, (draft) => {
|
|
191
|
-
const now = Date.now();
|
|
192
|
-
draft.kepler.config.currentMapId = mapId;
|
|
193
|
-
updateMapLastOpenedAt(draft.kepler.config.maps, mapId, now);
|
|
194
|
-
}));
|
|
195
|
-
},
|
|
196
|
-
createMap: (name) => {
|
|
197
|
-
const mapId = createId();
|
|
360
|
+
}),
|
|
361
|
+
ensureMap: (mapId, name) => {
|
|
198
362
|
const now = Date.now();
|
|
199
363
|
set((state) => produce(state, (draft) => {
|
|
364
|
+
const existing = draft.kepler.config.maps.find((map) => map.id === mapId);
|
|
365
|
+
if (existing) {
|
|
366
|
+
if (name && existing.name !== name) {
|
|
367
|
+
existing.name = name;
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
200
371
|
draft.kepler.config.maps.push({
|
|
201
372
|
id: mapId,
|
|
202
373
|
name: name ?? 'Untitled Map',
|
|
203
374
|
lastOpenedAt: now,
|
|
204
375
|
});
|
|
205
|
-
draft.kepler.
|
|
206
|
-
|
|
376
|
+
draft.kepler.map = createKeplerReducer({
|
|
377
|
+
reason: 'create-map',
|
|
378
|
+
mapId,
|
|
379
|
+
name,
|
|
380
|
+
})(draft.kepler.map, registerMapEntry(mapId));
|
|
207
381
|
draft.kepler.forwardDispatch[mapId] = getForwardDispatch(mapId);
|
|
208
382
|
}));
|
|
383
|
+
if (!get().kepler.map[mapId]) {
|
|
384
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
385
|
+
}
|
|
209
386
|
requestMapStyle(mapId);
|
|
210
387
|
get().kepler.syncKeplerDatasets();
|
|
388
|
+
},
|
|
389
|
+
createMap: (name, options) => {
|
|
390
|
+
const mapId = options?.id ?? createId();
|
|
391
|
+
get().kepler.ensureMap(mapId, name);
|
|
211
392
|
return mapId;
|
|
212
393
|
},
|
|
213
394
|
async syncKeplerDatasets() {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
395
|
+
if (syncKeplerPromise) {
|
|
396
|
+
pendingKeplerSync = true;
|
|
397
|
+
return syncKeplerPromise;
|
|
398
|
+
}
|
|
399
|
+
syncKeplerPromise = (async () => {
|
|
400
|
+
// Loop until no new sync was requested during the current pass.
|
|
401
|
+
// Each iteration reads a fresh snapshot of db.tables and kepler.map,
|
|
402
|
+
// so tables/maps added concurrently are picked up on the next pass.
|
|
403
|
+
do {
|
|
404
|
+
pendingKeplerSync = false;
|
|
405
|
+
for (const mapId of Object.keys(get().kepler.map)) {
|
|
406
|
+
const mapState = get().kepler.map[mapId];
|
|
407
|
+
if (!mapState)
|
|
408
|
+
continue;
|
|
409
|
+
const keplerDatasets = mapState.visState.datasets;
|
|
410
|
+
// Only sync tables that are referenced by existing layers or filters
|
|
411
|
+
const referencedDataIds = new Set();
|
|
412
|
+
for (const layer of mapState.visState.layers ?? []) {
|
|
413
|
+
if (layer.config.dataId) {
|
|
414
|
+
referencedDataIds.add(layer.config.dataId);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const filter of mapState.visState.filters ?? []) {
|
|
418
|
+
for (const dataId of filter.dataId ?? []) {
|
|
419
|
+
referencedDataIds.add(dataId);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const availableTables = get().db.tables;
|
|
423
|
+
for (const dataId of referencedDataIds) {
|
|
424
|
+
if (keplerDatasets?.[dataId]) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const table = findKeplerTableForDatasetId(availableTables, dataId, get().kepler.tableSelection);
|
|
428
|
+
if (!table) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await get().kepler.addTableToMap({
|
|
433
|
+
mapId,
|
|
434
|
+
tableName: dataId,
|
|
435
|
+
options: {
|
|
436
|
+
autoCreateLayers: false,
|
|
437
|
+
centerMap: false,
|
|
438
|
+
},
|
|
439
|
+
datasetId: dataId,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
console.error('syncKeplerDatasets: addTableToMap failed', {
|
|
444
|
+
dataId,
|
|
445
|
+
e,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
228
449
|
}
|
|
229
|
-
}
|
|
450
|
+
} while (pendingKeplerSync);
|
|
451
|
+
})();
|
|
452
|
+
try {
|
|
453
|
+
await syncKeplerPromise;
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
syncKeplerPromise = null;
|
|
230
457
|
}
|
|
231
458
|
},
|
|
232
459
|
deleteMap: (mapId) => {
|
|
233
460
|
set((state) => produce(state, (draft) => {
|
|
234
|
-
const openTabs = draft.kepler.config.openTabs;
|
|
235
461
|
const maps = draft.kepler.config.maps;
|
|
236
|
-
const wasCurrentMap = draft.kepler.config.currentMapId === mapId;
|
|
237
|
-
const deletingIndex = openTabs.indexOf(mapId);
|
|
238
|
-
// Remove from maps and openTabs
|
|
239
462
|
draft.kepler.config.maps = maps.filter((map) => map.id !== mapId);
|
|
240
|
-
draft.kepler.config.openTabs = openTabs.filter((id) => id !== mapId);
|
|
241
|
-
// If we deleted the current map, select another one
|
|
242
|
-
if (wasCurrentMap) {
|
|
243
|
-
const newOpenTabs = draft.kepler.config.openTabs;
|
|
244
|
-
const remainingMaps = draft.kepler.config.maps;
|
|
245
|
-
if (newOpenTabs.length > 0) {
|
|
246
|
-
// Select from remaining open tabs
|
|
247
|
-
const newIndex = deletingIndex === 0
|
|
248
|
-
? 0
|
|
249
|
-
: Math.min(deletingIndex - 1, newOpenTabs.length - 1);
|
|
250
|
-
const newSelectedId = newOpenTabs[newIndex];
|
|
251
|
-
if (newSelectedId) {
|
|
252
|
-
draft.kepler.config.currentMapId = newSelectedId;
|
|
253
|
-
updateMapLastOpenedAt(draft.kepler.config.maps, newSelectedId, Date.now());
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else if (remainingMaps.length > 0) {
|
|
257
|
-
// No open tabs left, open a closed map
|
|
258
|
-
const mapToOpen = remainingMaps[0];
|
|
259
|
-
if (mapToOpen) {
|
|
260
|
-
draft.kepler.config.openTabs.push(mapToOpen.id);
|
|
261
|
-
draft.kepler.config.currentMapId = mapToOpen.id;
|
|
262
|
-
updateMapLastOpenedAt(draft.kepler.config.maps, mapToOpen.id, Date.now());
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
463
|
draft.kepler.map = keplerReducer(draft.kepler.map, deleteEntry(mapId));
|
|
267
464
|
delete draft.kepler.forwardDispatch[mapId];
|
|
268
465
|
}));
|
|
269
466
|
},
|
|
467
|
+
duplicateMap: async (mapId) => {
|
|
468
|
+
// Ensure the map's redux state is registered, consistent with other kepler actions
|
|
469
|
+
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
470
|
+
const sourceMap = get().kepler.config.maps.find((m) => m.id === mapId);
|
|
471
|
+
const sourceMapState = get().kepler.map[mapId];
|
|
472
|
+
if (!sourceMap || !sourceMapState) {
|
|
473
|
+
return {
|
|
474
|
+
success: false,
|
|
475
|
+
message: 'Unable to duplicate map: source map or state not found',
|
|
476
|
+
code: 'source-map-not-found',
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const newMapId = createId();
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
// Save the source map state using Kepler's schema manager
|
|
482
|
+
const savedConfig = KeplerGLSchemaManager.getConfigToSave(sourceMapState);
|
|
483
|
+
set((state) => produce(state, (draft) => {
|
|
484
|
+
draft.kepler.config.maps.push({
|
|
485
|
+
id: newMapId,
|
|
486
|
+
name: `Copy of ${sourceMap.name}`,
|
|
487
|
+
config: savedConfig,
|
|
488
|
+
lastOpenedAt: now,
|
|
489
|
+
});
|
|
490
|
+
// Register the new map with empty state, then load the config
|
|
491
|
+
draft.kepler.map = createKeplerReducer({
|
|
492
|
+
reason: 'duplicate-map',
|
|
493
|
+
mapId: newMapId,
|
|
494
|
+
name: `Copy of ${sourceMap.name}`,
|
|
495
|
+
})(draft.kepler.map, registerMapEntry(newMapId));
|
|
496
|
+
draft.kepler.forwardDispatch[newMapId] =
|
|
497
|
+
getForwardDispatch(newMapId);
|
|
498
|
+
}));
|
|
499
|
+
// Load the saved config into the new map
|
|
500
|
+
get().kepler.addConfigToMap(newMapId, savedConfig);
|
|
501
|
+
requestMapStyle(newMapId);
|
|
502
|
+
get().kepler.syncKeplerDatasets();
|
|
503
|
+
return {
|
|
504
|
+
success: true,
|
|
505
|
+
message: 'Map duplicated successfully',
|
|
506
|
+
};
|
|
507
|
+
},
|
|
270
508
|
renameMap: (mapId, name) => {
|
|
271
509
|
set((state) => produce(state, (draft) => {
|
|
272
510
|
const map = draft.kepler.config.maps.find((map) => map.id === mapId);
|
|
@@ -275,21 +513,6 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
|
|
|
275
513
|
}
|
|
276
514
|
}));
|
|
277
515
|
},
|
|
278
|
-
closeMap: (mapId) => {
|
|
279
|
-
set((state) => produce(state, (draft) => {
|
|
280
|
-
const openTabs = draft.kepler.config.openTabs;
|
|
281
|
-
// Don't close if it's the last open tab (defensive check, TabStrip also prevents this)
|
|
282
|
-
if (openTabs.length <= 1)
|
|
283
|
-
return;
|
|
284
|
-
// Just remove from openTabs - TabStrip handles selection via onSelect before calling onClose
|
|
285
|
-
draft.kepler.config.openTabs = openTabs.filter((id) => id !== mapId);
|
|
286
|
-
}));
|
|
287
|
-
},
|
|
288
|
-
setOpenTabs: (tabIds) => {
|
|
289
|
-
set((state) => produce(state, (draft) => {
|
|
290
|
-
draft.kepler.config.openTabs = tabIds;
|
|
291
|
-
}));
|
|
292
|
-
},
|
|
293
516
|
addDataToMap: (mapId, data) => {
|
|
294
517
|
get().kepler.registerKeplerMapIfNotExists(mapId);
|
|
295
518
|
get().kepler.dispatchAction(mapId, addDataToMap(data));
|
|
@@ -338,12 +561,53 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
|
|
|
338
561
|
}
|
|
339
562
|
get().kepler.dispatchAction(mapId, addDataToMap({ config: parsedConfig, datasets: [] }));
|
|
340
563
|
},
|
|
564
|
+
withConfigPersistencePaused: async (fn) => {
|
|
565
|
+
configRestoreDepth += 1;
|
|
566
|
+
if (configRestoreDepth === 1) {
|
|
567
|
+
set((state) => produce(state, (draft) => {
|
|
568
|
+
draft.kepler.isRestoringConfig = true;
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
const restorePromise = (async () => {
|
|
572
|
+
try {
|
|
573
|
+
return await fn();
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
// Kepler restore actions can enqueue follow-up work through
|
|
577
|
+
// react-palm; let those actions settle before persisted config
|
|
578
|
+
// writes are allowed again.
|
|
579
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
580
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
581
|
+
configRestoreDepth -= 1;
|
|
582
|
+
if (configRestoreDepth === 0) {
|
|
583
|
+
set((state) => produce(state, (draft) => {
|
|
584
|
+
draft.kepler.isRestoringConfig = false;
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
})();
|
|
589
|
+
configRestorePromises.add(restorePromise);
|
|
590
|
+
try {
|
|
591
|
+
return await restorePromise;
|
|
592
|
+
}
|
|
593
|
+
finally {
|
|
594
|
+
configRestorePromises.delete(restorePromise);
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
waitForConfigRestore: async () => {
|
|
598
|
+
while (configRestorePromises.size > 0) {
|
|
599
|
+
await Promise.allSettled(Array.from(configRestorePromises));
|
|
600
|
+
}
|
|
601
|
+
},
|
|
341
602
|
registerKeplerMapIfNotExists(mapId) {
|
|
342
603
|
if (!get().kepler.map[mapId]) {
|
|
343
604
|
set({
|
|
344
605
|
kepler: {
|
|
345
606
|
...get().kepler,
|
|
346
|
-
map:
|
|
607
|
+
map: createKeplerReducer({
|
|
608
|
+
reason: 'register-map',
|
|
609
|
+
mapId,
|
|
610
|
+
})(get().kepler.map, registerMapEntry(mapId)),
|
|
347
611
|
forwardDispatch: {
|
|
348
612
|
...get().kepler.forwardDispatch,
|
|
349
613
|
[mapId]: getForwardDispatch(mapId),
|
|
@@ -377,7 +641,10 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
|
|
|
377
641
|
if (!mapId)
|
|
378
642
|
throw new Error('Map ID not found in action payload');
|
|
379
643
|
const result = next(action);
|
|
380
|
-
if (
|
|
644
|
+
if (configRestoreDepth === 0 &&
|
|
645
|
+
!get().kepler.isRestoringConfig &&
|
|
646
|
+
!SKIP_AUTO_SAVE_ACTIONS.includes(action.type) &&
|
|
647
|
+
mapId) {
|
|
381
648
|
// save kepler config to store
|
|
382
649
|
set((state) => produce(state, (draft) => {
|
|
383
650
|
const mapToSave = draft.kepler.config.maps.find((map) => map.id === mapId);
|
|
@@ -410,7 +677,11 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
|
|
|
410
677
|
// Register redux state of maps that are not in the config
|
|
411
678
|
for (const map of draft.kepler.config.maps) {
|
|
412
679
|
if (!draft.kepler.map[map.id]) {
|
|
413
|
-
draft.kepler.map =
|
|
680
|
+
draft.kepler.map = createKeplerReducer({
|
|
681
|
+
reason: 'sync-config',
|
|
682
|
+
mapId: map.id,
|
|
683
|
+
name: map.name,
|
|
684
|
+
})(draft.kepler.map, registerMapEntry(map.id));
|
|
414
685
|
draft.kepler.forwardDispatch[map.id] = getForwardDispatch(map.id);
|
|
415
686
|
}
|
|
416
687
|
}
|
|
@@ -418,10 +689,7 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
|
|
|
418
689
|
}
|
|
419
690
|
function updateForwardDispatch() {
|
|
420
691
|
const config = get().kepler.config;
|
|
421
|
-
const
|
|
422
|
-
const forwardDispatch = {
|
|
423
|
-
[currentMapId]: getForwardDispatch(currentMapId),
|
|
424
|
-
};
|
|
692
|
+
const forwardDispatch = {};
|
|
425
693
|
if (config) {
|
|
426
694
|
for (const { id } of config.maps) {
|
|
427
695
|
forwardDispatch[id] = getForwardDispatch(id);
|