@sqlrooms/kepler 0.29.0-rc.1 → 0.29.0-rc.3

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.
Files changed (66) hide show
  1. package/README.md +12 -2
  2. package/dist/KeplerSlice.d.ts +29 -11
  3. package/dist/KeplerSlice.d.ts.map +1 -1
  4. package/dist/KeplerSlice.js +247 -112
  5. package/dist/KeplerSlice.js.map +1 -1
  6. package/dist/components/CustomDndContext.d.ts.map +1 -1
  7. package/dist/components/CustomDndContext.js +5 -2
  8. package/dist/components/CustomDndContext.js.map +1 -1
  9. package/dist/components/CustomFilterManager.d.ts.map +1 -1
  10. package/dist/components/CustomFilterManager.js +4 -1
  11. package/dist/components/CustomFilterManager.js.map +1 -1
  12. package/dist/components/CustomFilterPanelHeader.d.ts.map +1 -1
  13. package/dist/components/CustomFilterPanelHeader.js +5 -5
  14. package/dist/components/CustomFilterPanelHeader.js.map +1 -1
  15. package/dist/components/CustomInteractionManager.d.ts.map +1 -1
  16. package/dist/components/CustomInteractionManager.js +4 -1
  17. package/dist/components/CustomInteractionManager.js.map +1 -1
  18. package/dist/components/CustomLayerManager.d.ts.map +1 -1
  19. package/dist/components/CustomLayerManager.js +138 -17
  20. package/dist/components/CustomLayerManager.js.map +1 -1
  21. package/dist/components/CustomMapDrawPanel.d.ts +8 -0
  22. package/dist/components/CustomMapDrawPanel.d.ts.map +1 -0
  23. package/dist/components/CustomMapDrawPanel.js +28 -0
  24. package/dist/components/CustomMapDrawPanel.js.map +1 -0
  25. package/dist/components/CustomMapLegend.d.ts +4 -1
  26. package/dist/components/CustomMapLegend.d.ts.map +1 -1
  27. package/dist/components/CustomMapLegend.js +15 -5
  28. package/dist/components/CustomMapLegend.js.map +1 -1
  29. package/dist/components/CustomMapLegendPanel.d.ts +3 -3
  30. package/dist/components/CustomMapLegendPanel.d.ts.map +1 -1
  31. package/dist/components/CustomMapLegendPanel.js +19 -4
  32. package/dist/components/CustomMapLegendPanel.js.map +1 -1
  33. package/dist/components/KeplerAddDataDialog.d.ts +2 -3
  34. package/dist/components/KeplerAddDataDialog.d.ts.map +1 -1
  35. package/dist/components/KeplerAddDataDialog.js +18 -8
  36. package/dist/components/KeplerAddDataDialog.js.map +1 -1
  37. package/dist/components/KeplerImageExport.d.ts.map +1 -1
  38. package/dist/components/KeplerImageExport.js +26 -8
  39. package/dist/components/KeplerImageExport.js.map +1 -1
  40. package/dist/components/KeplerInjector.d.ts.map +1 -1
  41. package/dist/components/KeplerInjector.js +3 -1
  42. package/dist/components/KeplerInjector.js.map +1 -1
  43. package/dist/components/KeplerMapContainer.d.ts.map +1 -1
  44. package/dist/components/KeplerMapContainer.js +36 -4
  45. package/dist/components/KeplerMapContainer.js.map +1 -1
  46. package/dist/components/SplitMapIndexContext.d.ts +2 -0
  47. package/dist/components/SplitMapIndexContext.d.ts.map +1 -0
  48. package/dist/components/SplitMapIndexContext.js +3 -0
  49. package/dist/components/SplitMapIndexContext.js.map +1 -0
  50. package/dist/hooks/useDndEffects.d.ts +1 -1
  51. package/dist/hooks/useDndEffects.d.ts.map +1 -1
  52. package/dist/hooks/useDndEffects.js +4 -0
  53. package/dist/hooks/useDndEffects.js.map +1 -1
  54. package/dist/hooks/useDndLayers.d.ts +1 -1
  55. package/dist/hooks/useDndLayers.d.ts.map +1 -1
  56. package/dist/hooks/useDndLayers.js +4 -0
  57. package/dist/hooks/useDndLayers.js.map +1 -1
  58. package/dist/hooks/useKeplerStateActions.d.ts +7 -6
  59. package/dist/hooks/useKeplerStateActions.d.ts.map +1 -1
  60. package/dist/hooks/useKeplerStateActions.js +1 -1
  61. package/dist/hooks/useKeplerStateActions.js.map +1 -1
  62. package/dist/index.d.ts +5 -3
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +3 -2
  65. package/dist/index.js.map +1 -1
  66. package/package.json +20 -20
package/README.md CHANGED
@@ -7,7 +7,16 @@ Use this package when you want a **map-first analytics experience** in a SQLRoom
7
7
  - `createKeplerSlice()` to add Kepler state/actions to your Room store
8
8
  - `KeplerMapContainer` and `KeplerPlotContainer` for rendering maps/overlays
9
9
  - `KeplerSidePanels` for layer/filter/interaction UI
10
- - utilities for map config persistence and dataset synchronization
10
+ - utilities for map config persistence, dataset synchronization, and migration
11
+ from legacy Kepler-owned tabs to artifact-backed tabs
12
+
13
+ ## Selection model
14
+
15
+ - `createKeplerSlice()` manages Kepler map documents and runtime state keyed by
16
+ map id, but it does not own host-level map selection.
17
+ - Render maps with explicit ids, for example `<KeplerMapContainer mapId={id} />`.
18
+ - Use `@sqlrooms/artifacts` when an app needs multiple user-managed map tabs;
19
+ artifact state should own the selected map artifact.
11
20
 
12
21
  ## Installation
13
22
 
@@ -51,7 +60,7 @@ export const {roomStore, useRoomStore} = createRoomStore<RoomState>(
51
60
  );
52
61
 
53
62
  function MapPanel() {
54
- const mapId = useRoomStore((state) => state.kepler.config.currentMapId);
63
+ const mapId = useRoomStore((state) => state.kepler.config.maps[0]?.id);
55
64
  const addTableToMap = useRoomStore((state) => state.kepler.addTableToMap);
56
65
  const isTableReady = useRoomStore((state) =>
57
66
  Boolean(state.db.findTableByName('earthquakes')),
@@ -94,6 +103,7 @@ createKeplerSlice({
94
103
 
95
104
  ## Related packages
96
105
 
106
+ - `@sqlrooms/artifacts` for artifact-backed map tabs
97
107
  - `@sqlrooms/kepler-config` for Zod schemas used by persisted Kepler config
98
108
  - `@sqlrooms/room-shell` for Room store composition and UI shell
99
109
  - `@sqlrooms/duckdb` for DuckDB-backed table loading/querying
@@ -1,7 +1,6 @@
1
1
  import { VectorTileDatasetMetadata } from '@kepler.gl/constants';
2
- import { Layer } from '@kepler.gl/layers';
3
2
  import { KeplerGlState } from '@kepler.gl/reducers';
4
- import { AddDataToMapPayload } from '@kepler.gl/types';
3
+ import { AddDataToMapPayload, MinSavedLayer, ParsedLayer } from '@kepler.gl/types';
5
4
  import { KeplerApplicationConfig } from '@kepler.gl/utils';
6
5
  import { KeplerMapSchema, KeplerSliceConfig } from '@sqlrooms/kepler-config';
7
6
  import { RoomShellSliceState, type StateCreator } from '@sqlrooms/room-shell';
@@ -11,9 +10,15 @@ import { ReduxLoggerOptions } from 'redux-logger';
11
10
  export type KeplerGLBasicProps = {
12
11
  mapboxApiAccessToken?: string;
13
12
  };
13
+ export type CreateInitialMapKeplerStateContext = {
14
+ reason: 'initialize' | 'create-map' | 'duplicate-map' | 'register-map' | 'sync-config';
15
+ defaultInitialMapKeplerState: Partial<KeplerGlState>;
16
+ mapId?: string;
17
+ name?: string;
18
+ };
14
19
  export type CreateKeplerSliceOptions = {
15
20
  config?: Partial<KeplerSliceConfig>;
16
- initialKeplerState?: Partial<KeplerGlState>;
21
+ createInitialMapKeplerState?: (context: CreateInitialMapKeplerStateContext) => Partial<KeplerGlState>;
17
22
  basicKeplerProps?: Partial<KeplerGLBasicProps>;
18
23
  actionLogging?: boolean | ReduxLoggerOptions;
19
24
  middlewares?: Middleware[];
@@ -49,14 +54,23 @@ export type KeplerSliceState = {
49
54
  [mapId: string]: Dispatch;
50
55
  };
51
56
  initialize: () => Promise<void>;
57
+ destroy: () => Promise<void>;
52
58
  setConfig: (config: KeplerSliceConfig) => void;
53
59
  /**
54
60
  * Update the datasets in all the kepler map so that they correspond to
55
61
  * the latest table schemas in the database
56
62
  */
57
63
  syncKeplerDatasets: () => Promise<void>;
58
- addLayer: (mapId: string, layer: Layer, datasetId: string) => void;
59
- addTableToMap: (mapId: string, tableName: string, options?: AddDataToMapPayload['options']) => Promise<void>;
64
+ addLayer: (mapId: string, layer: ParsedLayer | MinSavedLayer | undefined, datasetId: string) => void;
65
+ addFilter: (mapId: string, datasetId: string) => void;
66
+ setFilter: (mapId: string, filterIdx: number, prop: string | string[], value: any, valueIndex?: number) => void;
67
+ setFilterAnimationTime: (mapId: string, filterIdx: number, prop: string, value: any) => void;
68
+ setFilterAnimationWindow: (mapId: string, filterId: string, animationWindow: string) => void;
69
+ setFilterView: (mapId: string, filterIdx: number, view: 'side' | 'enlarged' | 'minified') => void;
70
+ updateTooltipFields: (mapId: string, datasetId: string, fieldNames: string[]) => void;
71
+ toggleSplitMap: (mapId: string, index?: number) => void;
72
+ toggleLayerForMap: (mapId: string, mapIndex: number, layerId: string) => void;
73
+ addTableToMap: (mapId: string, tableName: string, options?: AddDataToMapPayload['options'], config?: AddDataToMapPayload['config']) => Promise<void>;
60
74
  addTileSetToMap: (mapId: string, tableName: string, tileset: {
61
75
  name: string;
62
76
  type: string;
@@ -65,23 +79,27 @@ export type KeplerSliceState = {
65
79
  addConfigToMap: (mapId: string, config: KeplerMapSchema) => void;
66
80
  removeDatasetFromMaps: (datasetId: string) => void;
67
81
  dispatchAction: (mapId: string, action: KeplerAction) => void;
68
- setCurrentMapId: (mapId: string) => void;
82
+ ensureMap: (mapId: string, name?: string) => void;
69
83
  /**
70
84
  * Create a new map and return the map id
71
85
  * @param name - The name of the map
72
86
  * @returns The map id
73
87
  */
74
- createMap: (name?: string) => string;
88
+ createMap: (name?: string, options?: {
89
+ id?: string;
90
+ }) => string;
75
91
  deleteMap: (mapId: string) => void;
92
+ duplicateMap: (mapId: string) => Promise<{
93
+ success: boolean;
94
+ message?: string;
95
+ code?: string;
96
+ }>;
76
97
  renameMap: (mapId: string, name: string) => void;
77
- closeMap: (mapId: string) => void;
78
- setOpenTabs: (tabIds: string[]) => void;
79
- getCurrentMap: () => KeplerMapSchema | undefined;
80
98
  registerKeplerMapIfNotExists: (mapId: string) => void;
81
99
  __reduxProviderStore: ReduxStore<KeplerGlReduxState, AnyAction> | undefined;
82
100
  };
83
101
  };
84
- export declare function createKeplerSlice({ basicKeplerProps, config: initialConfigProps, initialKeplerState, actionLogging, middlewares: additionalMiddlewares, applicationConfig, onAction, }?: CreateKeplerSliceOptions): StateCreator<KeplerSliceState>;
102
+ export declare function createKeplerSlice({ basicKeplerProps, config: initialConfigProps, createInitialMapKeplerState, actionLogging, middlewares: additionalMiddlewares, applicationConfig, onAction, }?: CreateKeplerSliceOptions): StateCreator<KeplerSliceState>;
85
103
  type RoomStateWithDiscuss = RoomShellSliceState & KeplerSliceState;
86
104
  export declare function useStoreWithKepler<T>(selector: (state: RoomStateWithDiscuss) => T): T;
87
105
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"KeplerSlice.d.ts","sourceRoot":"","sources":["../src/KeplerSlice.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"KeplerSlice.d.ts","sourceRoot":"","sources":["../src/KeplerSlice.ts"],"names":[],"mappings":"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"}
@@ -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,7 +7,9 @@ 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';
@@ -20,9 +22,39 @@ class DesktopKeplerTable extends KeplerTable {
20
22
  return (d) => d;
21
23
  };
22
24
  }
25
+ function createDefaultMapKeplerState(resolvedTheme) {
26
+ return {
27
+ mapStyle: {
28
+ styleType: resolvedTheme === 'dark' ? 'dark-matter' : 'positron',
29
+ },
30
+ uiState: {
31
+ ...INITIAL_UI_STATE,
32
+ currentModal: null,
33
+ mapControls: {
34
+ visibleLayers: INITIAL_UI_STATE.mapControls.visibleLayers,
35
+ splitMap: {
36
+ show: true,
37
+ active: false,
38
+ },
39
+ mapLegend: {
40
+ show: true,
41
+ active: false,
42
+ },
43
+ toggle3d: {
44
+ show: true,
45
+ active: false,
46
+ },
47
+ mapDraw: {
48
+ show: true,
49
+ active: false,
50
+ },
51
+ },
52
+ },
53
+ };
54
+ }
23
55
  export function createDefaultKeplerConfig(props) {
24
56
  const mapId = createId();
25
- return {
57
+ const config = {
26
58
  maps: [
27
59
  {
28
60
  id: mapId,
@@ -31,10 +63,9 @@ export function createDefaultKeplerConfig(props) {
31
63
  lastOpenedAt: Date.now(),
32
64
  },
33
65
  ],
34
- currentMapId: mapId,
35
- openTabs: [mapId],
36
66
  ...props,
37
67
  };
68
+ return KeplerSliceConfig.parse(config);
38
69
  }
39
70
  export function hasMapId(action) {
40
71
  return (typeof action.payload === 'object' &&
@@ -51,37 +82,38 @@ const SKIP_AUTO_SAVE_ACTIONS = [
51
82
  KeplerActionTypes.UPDATE_MAP,
52
83
  KeplerActionTypes.MOUSE_MOVE,
53
84
  ];
54
- export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfigProps, initialKeplerState = {
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, } = {}) {
85
+ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfigProps, createInitialMapKeplerState, actionLogging = false, middlewares: additionalMiddlewares = [], applicationConfig, onAction, } = {}) {
72
86
  const initialConfig = createDefaultKeplerConfig(initialConfigProps);
73
87
  initApplicationConfig({
74
88
  table: DesktopKeplerTable,
75
89
  ...applicationConfig,
76
90
  });
91
+ let syncKeplerPromise = null;
92
+ // When a caller arrives while a sync is already in-flight, the in-flight run
93
+ // may have captured a stale snapshot of db.tables/kepler.map (missing maps or
94
+ // tables added by createMap, duplicateMap, etc.). Setting this flag tells the
95
+ // active run to loop once more with a fresh snapshot after it finishes, so
96
+ // late mutations are never silently dropped.
97
+ let pendingKeplerSync = false;
77
98
  const updateMapLastOpenedAt = (maps, mapId, now = Date.now()) => {
78
99
  const map = maps.find((item) => item.id === mapId);
79
100
  if (map) {
80
101
  map.lastOpenedAt = now;
81
102
  }
82
103
  };
83
- return createSlice((set, get) => {
84
- const keplerReducer = keplerGlReducer.initialState(initialKeplerState);
104
+ return createSlice((set, get, _store) => {
105
+ function resolveInitialMapKeplerState(context) {
106
+ const resolvedTheme = getTheme();
107
+ const defaultInitialMapKeplerState = createDefaultMapKeplerState(resolvedTheme);
108
+ return (createInitialMapKeplerState?.({
109
+ ...context,
110
+ defaultInitialMapKeplerState,
111
+ }) ?? defaultInitialMapKeplerState);
112
+ }
113
+ function createKeplerReducer(context) {
114
+ return keplerGlReducer.initialState(resolveInitialMapKeplerState(context));
115
+ }
116
+ const keplerReducer = createKeplerReducer({ reason: 'initialize' });
85
117
  const middlewares = [
86
118
  taskMiddleware,
87
119
  saveKeplerConfigMiddleware,
@@ -116,18 +148,22 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
116
148
  __reduxProviderStore: undefined,
117
149
  forwardDispatch: {},
118
150
  setConfig: (config) => {
151
+ const nextConfig = KeplerSliceConfig.parse(config);
119
152
  set((state) => produce(state, (draft) => {
120
- draft.kepler.config = config;
153
+ draft.kepler.config = nextConfig;
121
154
  }));
122
155
  updateMapReduxStates();
123
156
  updateForwardDispatch();
124
- updateMapConfigs();
125
157
  get().kepler.syncKeplerDatasets();
158
+ updateMapConfigs();
126
159
  },
127
160
  async initialize() {
128
161
  const config = get().kepler.config;
129
- const currentMapId = config.currentMapId;
130
- const keplerInitialState = keplerReducer(undefined, registerEntry({ id: currentMapId }));
162
+ const keplerInitialState = config.maps.reduce((mapState, map) => createKeplerReducer({
163
+ reason: 'initialize',
164
+ mapId: map.id,
165
+ name: map.name,
166
+ })(mapState, registerEntry({ id: map.id })), {});
131
167
  set({
132
168
  kepler: {
133
169
  ...get().kepler,
@@ -153,15 +189,77 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
153
189
  },
154
190
  });
155
191
  updateForwardDispatch();
156
- updateMapConfigs();
157
192
  await get().kepler.syncKeplerDatasets();
158
- requestMapStyle(config.currentMapId);
193
+ updateMapConfigs();
194
+ for (const mapId of Object.keys(get().kepler.map)) {
195
+ requestMapStyle(mapId);
196
+ }
197
+ },
198
+ async destroy() {
199
+ // no-op
159
200
  },
160
201
  addLayer: (mapId, layer, datasetId) => {
161
202
  get().kepler.registerKeplerMapIfNotExists(mapId);
162
203
  get().kepler.dispatchAction(mapId, addLayerAction(layer, datasetId));
163
204
  },
164
- addTableToMap: async (mapId, tableName, options = {}) => {
205
+ addFilter: (mapId, datasetId) => {
206
+ get().kepler.registerKeplerMapIfNotExists(mapId);
207
+ get().kepler.dispatchAction(mapId, addFilterAction(datasetId));
208
+ },
209
+ setFilter: (mapId, filterIdx, prop, value, valueIndex) => {
210
+ get().kepler.registerKeplerMapIfNotExists(mapId);
211
+ get().kepler.dispatchAction(mapId, setFilterAction(filterIdx, prop, value, valueIndex));
212
+ },
213
+ setFilterAnimationTime: (mapId, filterIdx, prop, value) => {
214
+ get().kepler.registerKeplerMapIfNotExists(mapId);
215
+ get().kepler.dispatchAction(mapId, setFilterAnimationTimeAction(filterIdx, prop, value));
216
+ },
217
+ setFilterAnimationWindow: (mapId, filterId, animationWindow) => {
218
+ get().kepler.registerKeplerMapIfNotExists(mapId);
219
+ get().kepler.dispatchAction(mapId, setFilterAnimationWindowAction({ id: filterId, animationWindow }));
220
+ },
221
+ setFilterView: (mapId, filterIdx, view) => {
222
+ get().kepler.registerKeplerMapIfNotExists(mapId);
223
+ get().kepler.dispatchAction(mapId, setFilterViewAction(filterIdx, view));
224
+ },
225
+ updateTooltipFields: (mapId, datasetId, fieldNames) => {
226
+ get().kepler.registerKeplerMapIfNotExists(mapId);
227
+ const mapState = get().kepler.map[mapId];
228
+ const tooltipConfig = mapState?.visState?.interactionConfig?.tooltip;
229
+ if (!tooltipConfig)
230
+ return;
231
+ const currentFieldsToShow = tooltipConfig.config?.fieldsToShow || {};
232
+ const existingFields = currentFieldsToShow[datasetId] || [];
233
+ const existingByName = new Map(existingFields.map((field) => [
234
+ field.name,
235
+ field,
236
+ ]));
237
+ const nextFields = Array.from(new Set(fieldNames)).map((name) => existingByName.get(name) ?? { name, format: null });
238
+ if (nextFields.length === existingFields.length &&
239
+ nextFields.every((field, index) => field.name === existingFields[index]?.name &&
240
+ field.format === existingFields[index]?.format)) {
241
+ return;
242
+ }
243
+ get().kepler.dispatchAction(mapId, interactionConfigChange({
244
+ ...tooltipConfig,
245
+ config: {
246
+ ...tooltipConfig.config,
247
+ fieldsToShow: {
248
+ ...currentFieldsToShow,
249
+ [datasetId]: nextFields,
250
+ },
251
+ },
252
+ }));
253
+ },
254
+ toggleSplitMap: (mapId, index) => {
255
+ get().kepler.registerKeplerMapIfNotExists(mapId);
256
+ get().kepler.dispatchAction(mapId, toggleSplitMapAction(index));
257
+ },
258
+ toggleLayerForMap: (mapId, mapIndex, layerId) => {
259
+ get().kepler.registerKeplerMapIfNotExists(mapId);
260
+ get().kepler.dispatchAction(mapId, toggleLayerForMapAction(mapIndex, layerId));
261
+ },
262
+ addTableToMap: async (mapId, tableName, options = {}, config = {}) => {
165
263
  const connector = await get().db.getConnector();
166
264
  const duckDbColumns = await getDuckDBColumnTypes({
167
265
  query: (query) => connector.query(query).result,
@@ -180,93 +278,141 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
180
278
  info: { label: tableName, id: tableName },
181
279
  metadata: { tableName },
182
280
  };
183
- get().kepler.dispatchAction(mapId, addDataToMap({ datasets, options }));
281
+ get().kepler.dispatchAction(mapId, addDataToMap({ datasets, options, config }));
184
282
  }
185
283
  },
186
- getCurrentMap: () => {
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();
284
+ ensureMap: (mapId, name) => {
198
285
  const now = Date.now();
199
286
  set((state) => produce(state, (draft) => {
287
+ const existing = draft.kepler.config.maps.find((map) => map.id === mapId);
288
+ if (existing) {
289
+ if (name && existing.name !== name) {
290
+ existing.name = name;
291
+ }
292
+ return;
293
+ }
200
294
  draft.kepler.config.maps.push({
201
295
  id: mapId,
202
296
  name: name ?? 'Untitled Map',
203
297
  lastOpenedAt: now,
204
298
  });
205
- draft.kepler.config.openTabs.push(mapId);
206
- draft.kepler.map = keplerReducer(draft.kepler.map, registerEntry({ id: mapId }));
299
+ draft.kepler.map = createKeplerReducer({
300
+ reason: 'create-map',
301
+ mapId,
302
+ name,
303
+ })(draft.kepler.map, registerEntry({ id: mapId }));
207
304
  draft.kepler.forwardDispatch[mapId] = getForwardDispatch(mapId);
208
305
  }));
306
+ if (!get().kepler.map[mapId]) {
307
+ get().kepler.registerKeplerMapIfNotExists(mapId);
308
+ }
209
309
  requestMapStyle(mapId);
210
310
  get().kepler.syncKeplerDatasets();
311
+ },
312
+ createMap: (name, options) => {
313
+ const mapId = options?.id ?? createId();
314
+ get().kepler.ensureMap(mapId, name);
211
315
  return mapId;
212
316
  },
213
317
  async syncKeplerDatasets() {
214
- for (const mapId of Object.keys(get().kepler.map)) {
215
- const keplerDatasets = get().kepler.map[mapId]?.visState.datasets;
216
- for (const { table } of get().db.tables) {
217
- // TODO: remove this once getDuckDBColumnTypesMap can handle qualified table names
218
- // const qualifiedTable = table.toString();
219
- if (
220
- // !table.schema?.startsWith('__') && // skip internal schemas
221
- // !keplerDatasets?.[qualifiedTable]
222
- table.schema === 'main' &&
223
- !keplerDatasets?.[table.table]) {
224
- await get().kepler.addTableToMap(mapId, table.table, {
225
- autoCreateLayers: false,
226
- centerMap: false,
227
- });
318
+ if (syncKeplerPromise) {
319
+ pendingKeplerSync = true;
320
+ return syncKeplerPromise;
321
+ }
322
+ syncKeplerPromise = (async () => {
323
+ // Loop until no new sync was requested during the current pass.
324
+ // Each iteration reads a fresh snapshot of db.tables and kepler.map,
325
+ // so tables/maps added concurrently are picked up on the next pass.
326
+ do {
327
+ pendingKeplerSync = false;
328
+ for (const mapId of Object.keys(get().kepler.map)) {
329
+ const mapState = get().kepler.map[mapId];
330
+ if (!mapState)
331
+ continue;
332
+ const keplerDatasets = mapState.visState.datasets;
333
+ // Only sync tables that are referenced by existing layers or filters
334
+ const referencedDataIds = new Set();
335
+ for (const layer of mapState.visState.layers ?? []) {
336
+ if (layer.config.dataId) {
337
+ referencedDataIds.add(layer.config.dataId);
338
+ }
339
+ }
340
+ for (const filter of mapState.visState.filters ?? []) {
341
+ for (const dataId of filter.dataId ?? []) {
342
+ referencedDataIds.add(dataId);
343
+ }
344
+ }
345
+ const availableTables = new Set(get()
346
+ .db.tables.filter((t) => t.table.schema === 'main')
347
+ .map((t) => t.table.table));
348
+ for (const dataId of referencedDataIds) {
349
+ if (!keplerDatasets?.[dataId] &&
350
+ availableTables.has(dataId)) {
351
+ await get().kepler.addTableToMap(mapId, dataId, {
352
+ autoCreateLayers: false,
353
+ centerMap: false,
354
+ });
355
+ }
356
+ }
228
357
  }
229
- }
358
+ } while (pendingKeplerSync);
359
+ })();
360
+ try {
361
+ await syncKeplerPromise;
362
+ }
363
+ finally {
364
+ syncKeplerPromise = null;
230
365
  }
231
366
  },
232
367
  deleteMap: (mapId) => {
233
368
  set((state) => produce(state, (draft) => {
234
- const openTabs = draft.kepler.config.openTabs;
235
369
  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
370
  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
371
  draft.kepler.map = keplerReducer(draft.kepler.map, deleteEntry(mapId));
267
372
  delete draft.kepler.forwardDispatch[mapId];
268
373
  }));
269
374
  },
375
+ duplicateMap: async (mapId) => {
376
+ // Ensure the map's redux state is registered, consistent with other kepler actions
377
+ get().kepler.registerKeplerMapIfNotExists(mapId);
378
+ const sourceMap = get().kepler.config.maps.find((m) => m.id === mapId);
379
+ const sourceMapState = get().kepler.map[mapId];
380
+ if (!sourceMap || !sourceMapState) {
381
+ return {
382
+ success: false,
383
+ message: 'Unable to duplicate map: source map or state not found',
384
+ code: 'source-map-not-found',
385
+ };
386
+ }
387
+ const newMapId = createId();
388
+ const now = Date.now();
389
+ // Save the source map state using Kepler's schema manager
390
+ const savedConfig = KeplerGLSchemaManager.getConfigToSave(sourceMapState);
391
+ set((state) => produce(state, (draft) => {
392
+ draft.kepler.config.maps.push({
393
+ id: newMapId,
394
+ name: `Copy of ${sourceMap.name}`,
395
+ config: savedConfig,
396
+ lastOpenedAt: now,
397
+ });
398
+ // Register the new map with empty state, then load the config
399
+ draft.kepler.map = createKeplerReducer({
400
+ reason: 'duplicate-map',
401
+ mapId: newMapId,
402
+ name: `Copy of ${sourceMap.name}`,
403
+ })(draft.kepler.map, registerEntry({ id: newMapId }));
404
+ draft.kepler.forwardDispatch[newMapId] =
405
+ getForwardDispatch(newMapId);
406
+ }));
407
+ // Load the saved config into the new map
408
+ get().kepler.addConfigToMap(newMapId, savedConfig);
409
+ requestMapStyle(newMapId);
410
+ get().kepler.syncKeplerDatasets();
411
+ return {
412
+ success: true,
413
+ message: 'Map duplicated successfully',
414
+ };
415
+ },
270
416
  renameMap: (mapId, name) => {
271
417
  set((state) => produce(state, (draft) => {
272
418
  const map = draft.kepler.config.maps.find((map) => map.id === mapId);
@@ -275,21 +421,6 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
275
421
  }
276
422
  }));
277
423
  },
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
424
  addDataToMap: (mapId, data) => {
294
425
  get().kepler.registerKeplerMapIfNotExists(mapId);
295
426
  get().kepler.dispatchAction(mapId, addDataToMap(data));
@@ -343,7 +474,10 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
343
474
  set({
344
475
  kepler: {
345
476
  ...get().kepler,
346
- map: keplerReducer(get().kepler.map, registerEntry({ id: mapId })),
477
+ map: createKeplerReducer({
478
+ reason: 'register-map',
479
+ mapId,
480
+ })(get().kepler.map, registerEntry({ id: mapId })),
347
481
  forwardDispatch: {
348
482
  ...get().kepler.forwardDispatch,
349
483
  [mapId]: getForwardDispatch(mapId),
@@ -410,7 +544,11 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
410
544
  // Register redux state of maps that are not in the config
411
545
  for (const map of draft.kepler.config.maps) {
412
546
  if (!draft.kepler.map[map.id]) {
413
- draft.kepler.map = keplerReducer(draft.kepler.map, registerEntry({ id: map.id }));
547
+ draft.kepler.map = createKeplerReducer({
548
+ reason: 'sync-config',
549
+ mapId: map.id,
550
+ name: map.name,
551
+ })(draft.kepler.map, registerEntry({ id: map.id }));
414
552
  draft.kepler.forwardDispatch[map.id] = getForwardDispatch(map.id);
415
553
  }
416
554
  }
@@ -418,10 +556,7 @@ export function createKeplerSlice({ basicKeplerProps = {}, config: initialConfig
418
556
  }
419
557
  function updateForwardDispatch() {
420
558
  const config = get().kepler.config;
421
- const currentMapId = config.currentMapId;
422
- const forwardDispatch = {
423
- [currentMapId]: getForwardDispatch(currentMapId),
424
- };
559
+ const forwardDispatch = {};
425
560
  if (config) {
426
561
  for (const { id } of config.maps) {
427
562
  forwardDispatch[id] = getForwardDispatch(id);