@sqlrooms/deck 0.29.0-rc.8 → 0.29.0-rc.9

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 (107) hide show
  1. package/README.md +31 -6
  2. package/dist/DeckJsonMap.d.ts +2 -2
  3. package/dist/DeckJsonMap.d.ts.map +1 -1
  4. package/dist/DeckJsonMap.js +202 -19
  5. package/dist/DeckJsonMap.js.map +1 -1
  6. package/dist/DeckJsonMapSpec.d.ts +36 -36
  7. package/dist/DeckJsonMapSpec.d.ts.map +1 -1
  8. package/dist/DeckJsonMapSpec.js +1 -0
  9. package/dist/DeckJsonMapSpec.js.map +1 -1
  10. package/dist/MapSettings.d.ts.map +1 -1
  11. package/dist/MapSettings.js +202 -25
  12. package/dist/MapSettings.js.map +1 -1
  13. package/dist/ai.d.ts +55 -8
  14. package/dist/ai.d.ts.map +1 -1
  15. package/dist/ai.js +190 -41
  16. package/dist/ai.js.map +1 -1
  17. package/dist/block.d.ts +20 -0
  18. package/dist/block.d.ts.map +1 -0
  19. package/dist/block.js +119 -0
  20. package/dist/block.js.map +1 -0
  21. package/dist/dashboard.d.ts +2 -8
  22. package/dist/dashboard.d.ts.map +1 -1
  23. package/dist/dashboard.js +194 -289
  24. package/dist/dashboard.js.map +1 -1
  25. package/dist/dashboardConfig.d.ts +11 -3
  26. package/dist/dashboardConfig.d.ts.map +1 -1
  27. package/dist/dashboardConfig.js +82 -7
  28. package/dist/dashboardConfig.js.map +1 -1
  29. package/dist/index.d.ts +4 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/json/aggregateWaypoints.d.ts +14 -0
  34. package/dist/json/aggregateWaypoints.d.ts.map +1 -0
  35. package/dist/json/aggregateWaypoints.js +190 -0
  36. package/dist/json/aggregateWaypoints.js.map +1 -0
  37. package/dist/json/compileColorScale.d.ts +1 -1
  38. package/dist/json/compileColorScale.d.ts.map +1 -1
  39. package/dist/json/compileColorScale.js +42 -20
  40. package/dist/json/compileColorScale.js.map +1 -1
  41. package/dist/json/createDeckJsonConfiguration.d.ts.map +1 -1
  42. package/dist/json/createDeckJsonConfiguration.js +106 -8
  43. package/dist/json/createDeckJsonConfiguration.js.map +1 -1
  44. package/dist/json/defaultClasses.d.ts +5 -4
  45. package/dist/json/defaultClasses.d.ts.map +1 -1
  46. package/dist/json/defaultClasses.js +9 -4
  47. package/dist/json/defaultClasses.js.map +1 -1
  48. package/dist/json/extractColorScaleLegends.d.ts.map +1 -1
  49. package/dist/json/extractColorScaleLegends.js +43 -5
  50. package/dist/json/extractColorScaleLegends.js.map +1 -1
  51. package/dist/json/heatmapDefaults.d.ts +8 -0
  52. package/dist/json/heatmapDefaults.d.ts.map +1 -0
  53. package/dist/json/heatmapDefaults.js +14 -0
  54. package/dist/json/heatmapDefaults.js.map +1 -0
  55. package/dist/json/layerCompatibility.d.ts.map +1 -1
  56. package/dist/json/layerCompatibility.js +5 -1
  57. package/dist/json/layerCompatibility.js.map +1 -1
  58. package/dist/json/layers/DeckColumnLayer.d.ts +12 -0
  59. package/dist/json/layers/DeckColumnLayer.d.ts.map +1 -0
  60. package/dist/json/layers/DeckColumnLayer.js +26 -0
  61. package/dist/json/layers/DeckColumnLayer.js.map +1 -0
  62. package/dist/json/layers/DeckH3HexagonLayer.d.ts +37 -0
  63. package/dist/json/layers/DeckH3HexagonLayer.d.ts.map +1 -0
  64. package/dist/json/layers/DeckH3HexagonLayer.js +96 -0
  65. package/dist/json/layers/DeckH3HexagonLayer.js.map +1 -0
  66. package/dist/json/layers/DeckTripsLayer.d.ts +45 -0
  67. package/dist/json/layers/DeckTripsLayer.d.ts.map +1 -0
  68. package/dist/json/layers/DeckTripsLayer.js +169 -0
  69. package/dist/json/layers/DeckTripsLayer.js.map +1 -0
  70. package/dist/json/layers/index.d.ts +4 -0
  71. package/dist/json/layers/index.d.ts.map +1 -0
  72. package/dist/json/layers/index.js +4 -0
  73. package/dist/json/layers/index.js.map +1 -0
  74. package/dist/json/rewriteGeoArrowAccessors.d.ts.map +1 -1
  75. package/dist/json/rewriteGeoArrowAccessors.js +5 -0
  76. package/dist/json/rewriteGeoArrowAccessors.js.map +1 -1
  77. package/dist/mapConfigUtils.d.ts +102 -1
  78. package/dist/mapConfigUtils.d.ts.map +1 -1
  79. package/dist/mapConfigUtils.js +21 -3
  80. package/dist/mapConfigUtils.js.map +1 -1
  81. package/dist/mapLayerConfigUtils.d.ts +15 -0
  82. package/dist/mapLayerConfigUtils.d.ts.map +1 -1
  83. package/dist/mapLayerConfigUtils.js +109 -7
  84. package/dist/mapLayerConfigUtils.js.map +1 -1
  85. package/dist/prepare/detectGeometryColumn.d.ts +10 -1
  86. package/dist/prepare/detectGeometryColumn.d.ts.map +1 -1
  87. package/dist/prepare/detectGeometryColumn.js +48 -6
  88. package/dist/prepare/detectGeometryColumn.js.map +1 -1
  89. package/dist/prepare/wkbDecoder.d.ts.map +1 -1
  90. package/dist/prepare/wkbDecoder.js +88 -1
  91. package/dist/prepare/wkbDecoder.js.map +1 -1
  92. package/dist/prepare/wkbParser.d.ts +5 -0
  93. package/dist/prepare/wkbParser.d.ts.map +1 -1
  94. package/dist/prepare/wkbParser.js +24 -0
  95. package/dist/prepare/wkbParser.js.map +1 -1
  96. package/dist/types.d.ts +14 -4
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/types.js.map +1 -1
  99. package/dist/useDeckMapDatasets.d.ts +17 -0
  100. package/dist/useDeckMapDatasets.d.ts.map +1 -0
  101. package/dist/useDeckMapDatasets.js +69 -0
  102. package/dist/useDeckMapDatasets.js.map +1 -0
  103. package/dist/useDeckMapFitToBounds.d.ts +29 -0
  104. package/dist/useDeckMapFitToBounds.d.ts.map +1 -0
  105. package/dist/useDeckMapFitToBounds.js +333 -0
  106. package/dist/useDeckMapFitToBounds.js.map +1 -0
  107. package/package.json +7 -6
@@ -0,0 +1,333 @@
1
+ import { WebMercatorViewport } from '@deck.gl/core';
2
+ import { escapeId, getColValAsNumber, quoteTableReference, useStoreWithDuckDb, } from '@sqlrooms/duckdb';
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { asDeckJsonMapConfig, resolveDeckMapDashboardDatasetSource, } from './dashboardConfig';
5
+ function createInitialFitState(key) {
6
+ return {
7
+ key,
8
+ didAutoFit: false,
9
+ fitRequestVersion: 0,
10
+ handledFitRequestVersion: 0,
11
+ };
12
+ }
13
+ function isDeckMapFitToDataValid(fitToData) {
14
+ return Boolean(fitToData?.dataset);
15
+ }
16
+ function resolveFitToData(fitToDataRaw, datasets, interaction, spec) {
17
+ if (!fitToDataRaw)
18
+ return null;
19
+ const dataset = datasets?.[fitToDataRaw.dataset];
20
+ if (fitToDataRaw.longitudeColumn && fitToDataRaw.latitudeColumn) {
21
+ return fitToDataRaw;
22
+ }
23
+ const geomCol = fitToDataRaw.geometryColumn ?? dataset?.geometryColumn;
24
+ if (geomCol) {
25
+ return { ...fitToDataRaw, geometryColumn: geomCol };
26
+ }
27
+ const inter = interaction;
28
+ if (inter?.longitudeColumn && inter?.latitudeColumn) {
29
+ return {
30
+ ...fitToDataRaw,
31
+ geometryColumn: undefined,
32
+ longitudeColumn: inter.longitudeColumn,
33
+ latitudeColumn: inter.latitudeColumn,
34
+ };
35
+ }
36
+ // Detect H3 layer as last resort: check layers bound to this dataset
37
+ // for hexagonColumn when no other bounds method is available.
38
+ const specObj = spec;
39
+ const specLayers = Array.isArray(specObj?.layers) ? specObj.layers : [];
40
+ for (const layer of specLayers) {
41
+ if (!layer || typeof layer !== 'object')
42
+ continue;
43
+ const binding = layer._sqlroomsBinding;
44
+ if (binding?.dataset === fitToDataRaw.dataset && binding?.hexagonColumn) {
45
+ return {
46
+ ...fitToDataRaw,
47
+ h3Column: binding.hexagonColumn,
48
+ };
49
+ }
50
+ }
51
+ // Strip unverifiable geometryColumn; the bounds query will use
52
+ // DuckDB case-insensitive column resolution as a fallback.
53
+ const { geometryColumn: _geom, ...safeConfig } = fitToDataRaw;
54
+ void _geom;
55
+ return safeConfig;
56
+ }
57
+ export function createDeckMapBoundsQuery(options) {
58
+ const { source, fitToData } = options;
59
+ if (!isDeckMapFitToDataValid(fitToData)) {
60
+ return null;
61
+ }
62
+ const baseSourceSql = source.sqlQuery
63
+ ? `SELECT * FROM (${source.sqlQuery}) AS "__sqlrooms_dashboard_map_source"`
64
+ : `SELECT * FROM ${quoteTableReference(source.tableName ?? '')}`;
65
+ if (fitToData.h3Column) {
66
+ const h3Col = escapeId(fitToData.h3Column);
67
+ return `
68
+ SELECT
69
+ MIN(h3_cell_to_lng(${h3Col})) AS min_longitude,
70
+ MIN(h3_cell_to_lat(${h3Col})) AS min_latitude,
71
+ MAX(h3_cell_to_lng(${h3Col})) AS max_longitude,
72
+ MAX(h3_cell_to_lat(${h3Col})) AS max_latitude
73
+ FROM (${baseSourceSql}) AS "__sqlrooms_dashboard_map_h3"
74
+ WHERE ${h3Col} IS NOT NULL
75
+ `;
76
+ }
77
+ if (fitToData.geometryColumn) {
78
+ const geometryCol = escapeId(fitToData.geometryColumn);
79
+ const isWkb = source.sqlQuery && source.sqlQuery.toLowerCase().includes('st_aswkb');
80
+ const geomExpr = isWkb
81
+ ? `ST_GeomFromWKB(${geometryCol})`
82
+ : `${geometryCol}::GEOMETRY`;
83
+ return `
84
+ SELECT
85
+ ST_XMin(extent) AS min_longitude,
86
+ ST_YMin(extent) AS min_latitude,
87
+ ST_XMax(extent) AS max_longitude,
88
+ ST_YMax(extent) AS max_latitude
89
+ FROM (
90
+ SELECT ST_Extent_Agg(${geomExpr}) AS extent
91
+ FROM (${baseSourceSql}) AS "__sqlrooms_dashboard_map_geom"
92
+ WHERE ${geometryCol} IS NOT NULL
93
+ ) AS "__sqlrooms_dashboard_map_extent"
94
+ WHERE extent IS NOT NULL
95
+ `;
96
+ }
97
+ if (fitToData.longitudeColumn && fitToData.latitudeColumn) {
98
+ const longitudeColumn = escapeId(fitToData.longitudeColumn);
99
+ const latitudeColumn = escapeId(fitToData.latitudeColumn);
100
+ return `
101
+ SELECT
102
+ ST_XMin(extent) AS min_longitude,
103
+ ST_YMin(extent) AS min_latitude,
104
+ ST_XMax(extent) AS max_longitude,
105
+ ST_YMax(extent) AS max_latitude
106
+ FROM (
107
+ SELECT ST_Extent_Agg(ST_Point(${longitudeColumn}, ${latitudeColumn})) AS extent
108
+ FROM (${baseSourceSql}) AS "__sqlrooms_dashboard_map_points"
109
+ WHERE ${longitudeColumn} IS NOT NULL AND ${latitudeColumn} IS NOT NULL
110
+ ) AS "__sqlrooms_dashboard_map_extent"
111
+ WHERE extent IS NOT NULL
112
+ `;
113
+ }
114
+ // Fallback: use DuckDB unquoted identifiers for case-insensitive resolution
115
+ // of common coordinate column names (longitude, lat, etc.)
116
+ return `
117
+ SELECT
118
+ ST_XMin(extent) AS min_longitude,
119
+ ST_YMin(extent) AS min_latitude,
120
+ ST_XMax(extent) AS max_longitude,
121
+ ST_YMax(extent) AS max_latitude
122
+ FROM (
123
+ SELECT ST_Extent_Agg(ST_Point(Longitude, Latitude)) AS extent
124
+ FROM (${baseSourceSql}) AS "__sqlrooms_dashboard_map_points"
125
+ WHERE Longitude IS NOT NULL AND Latitude IS NOT NULL
126
+ ) AS "__sqlrooms_dashboard_map_extent"
127
+ WHERE extent IS NOT NULL
128
+ `;
129
+ }
130
+ function readBoundsFromExtentResult(result) {
131
+ const minLongitude = getColValAsNumber(result, 'min_longitude');
132
+ const minLatitude = getColValAsNumber(result, 'min_latitude');
133
+ const maxLongitude = getColValAsNumber(result, 'max_longitude');
134
+ const maxLatitude = getColValAsNumber(result, 'max_latitude');
135
+ if (!Number.isFinite(minLongitude) ||
136
+ !Number.isFinite(minLatitude) ||
137
+ !Number.isFinite(maxLongitude) ||
138
+ !Number.isFinite(maxLatitude)) {
139
+ return null;
140
+ }
141
+ return [
142
+ [
143
+ minLongitude === maxLongitude ? minLongitude - 0.01 : minLongitude,
144
+ minLatitude === maxLatitude ? minLatitude - 0.01 : minLatitude,
145
+ ],
146
+ [
147
+ minLongitude === maxLongitude ? maxLongitude + 0.01 : maxLongitude,
148
+ minLatitude === maxLatitude ? maxLatitude + 0.01 : maxLatitude,
149
+ ],
150
+ ];
151
+ }
152
+ function fitViewStateToBounds(options) {
153
+ const { bounds, width, height, padding = 40, maxZoom = 18 } = options;
154
+ const viewport = new WebMercatorViewport({
155
+ width: Math.max(width, 1),
156
+ height: Math.max(height, 1),
157
+ });
158
+ const fitted = viewport.fitBounds([
159
+ [bounds[0][0], bounds[0][1]],
160
+ [bounds[1][0], bounds[1][1]],
161
+ ], { padding });
162
+ return {
163
+ longitude: fitted.longitude,
164
+ latitude: fitted.latitude,
165
+ zoom: Math.min(fitted.zoom, maxZoom),
166
+ };
167
+ }
168
+ const deckMapDashboardFitRequestTarget = new EventTarget();
169
+ export function emitDeckMapDashboardFitRequest(panelId) {
170
+ deckMapDashboardFitRequestTarget.dispatchEvent(new CustomEvent('fit-view', { detail: { panelId } }));
171
+ }
172
+ /**
173
+ * Manages the "fit to bounds" state machine for a deck map panel.
174
+ * Handles auto-fit on mount, manual fit requests, and bounds query execution.
175
+ */
176
+ export function useDeckMapFitToBounds(options) {
177
+ const { panelId, dashboard, panel, containerSize, deckMapRef } = options;
178
+ const mapConfig = asDeckJsonMapConfig(panel.config);
179
+ const executeSql = useStoreWithDuckDb((state) => state.db.executeSql);
180
+ const fitToDataRaw = mapConfig?.fitToData ?? null;
181
+ const fitToData = useMemo(() => resolveFitToData(fitToDataRaw, mapConfig?.datasets, mapConfig?.interaction, mapConfig?.spec), [
182
+ fitToDataRaw,
183
+ mapConfig?.datasets,
184
+ mapConfig?.interaction,
185
+ mapConfig?.spec,
186
+ ]);
187
+ const fitToDataSource = useMemo(() => fitToData
188
+ ? resolveDeckMapDashboardDatasetSource({
189
+ dashboard,
190
+ panel,
191
+ dataset: mapConfig?.datasets[fitToData.dataset],
192
+ })
193
+ : undefined, [dashboard, fitToData, mapConfig?.datasets, panel]);
194
+ const fitToDataKey = useMemo(() => fitToData && fitToDataSource
195
+ ? JSON.stringify({
196
+ source: fitToDataSource,
197
+ fitToData,
198
+ selectedTable: dashboard.selectedTable,
199
+ })
200
+ : null, [dashboard.selectedTable, fitToData, fitToDataSource]);
201
+ const fitStateKey = useMemo(() => JSON.stringify({ panelId, fitToDataKey }), [fitToDataKey, panelId]);
202
+ const [fitState, setFitState] = useState(() => createInitialFitState(fitStateKey));
203
+ const activeFitState = fitState.key === fitStateKey
204
+ ? fitState
205
+ : createInitialFitState(fitStateKey);
206
+ const { didAutoFit, fitRequestVersion, handledFitRequestVersion } = activeFitState;
207
+ // Listen for manual fit requests
208
+ useEffect(() => {
209
+ const handleFitRequest = (event) => {
210
+ const detail = event.detail;
211
+ if (detail?.panelId === panelId) {
212
+ setFitState((current) => {
213
+ const scoped = current.key === fitStateKey
214
+ ? current
215
+ : createInitialFitState(fitStateKey);
216
+ return {
217
+ ...scoped,
218
+ fitRequestVersion: scoped.fitRequestVersion + 1,
219
+ };
220
+ });
221
+ }
222
+ };
223
+ deckMapDashboardFitRequestTarget.addEventListener('fit-view', handleFitRequest);
224
+ return () => {
225
+ deckMapDashboardFitRequestTarget.removeEventListener('fit-view', handleFitRequest);
226
+ };
227
+ }, [fitStateKey, panelId]);
228
+ // Execute fit-to-bounds query
229
+ useEffect(() => {
230
+ const hasManualFitRequest = fitRequestVersion > handledFitRequestVersion;
231
+ if (!isDeckMapFitToDataValid(fitToData) ||
232
+ !fitToDataSource ||
233
+ containerSize.width <= 0 ||
234
+ containerSize.height <= 0 ||
235
+ (!hasManualFitRequest && didAutoFit)) {
236
+ return;
237
+ }
238
+ let isCancelled = false;
239
+ const fitToDataBounds = async () => {
240
+ try {
241
+ if (fitToData.h3Column) {
242
+ try {
243
+ await executeSql('INSTALL h3 FROM community');
244
+ }
245
+ catch {
246
+ // May already be installed
247
+ }
248
+ try {
249
+ await executeSql('LOAD h3');
250
+ }
251
+ catch {
252
+ // May already be loaded
253
+ }
254
+ }
255
+ const boundsQuery = createDeckMapBoundsQuery({
256
+ source: fitToDataSource,
257
+ fitToData,
258
+ });
259
+ if (!boundsQuery)
260
+ return;
261
+ const handle = await executeSql(boundsQuery);
262
+ const result = handle ? await handle : null;
263
+ if (isCancelled || !result)
264
+ return;
265
+ const bounds = readBoundsFromExtentResult(result);
266
+ if (!bounds) {
267
+ setFitState((current) => {
268
+ const scoped = current.key === fitStateKey
269
+ ? current
270
+ : createInitialFitState(fitStateKey);
271
+ return {
272
+ ...scoped,
273
+ didAutoFit: true,
274
+ handledFitRequestVersion: fitRequestVersion,
275
+ };
276
+ });
277
+ return;
278
+ }
279
+ const nextViewState = fitViewStateToBounds({
280
+ bounds,
281
+ width: containerSize.width,
282
+ height: containerSize.height,
283
+ padding: fitToData.padding,
284
+ maxZoom: fitToData.maxZoom,
285
+ });
286
+ if (!isCancelled && deckMapRef.current) {
287
+ deckMapRef.current.jumpTo(nextViewState);
288
+ }
289
+ setFitState((current) => {
290
+ const scoped = current.key === fitStateKey
291
+ ? current
292
+ : createInitialFitState(fitStateKey);
293
+ return {
294
+ ...scoped,
295
+ didAutoFit: true,
296
+ handledFitRequestVersion: fitRequestVersion,
297
+ };
298
+ });
299
+ }
300
+ catch {
301
+ if (!isCancelled) {
302
+ setFitState((current) => {
303
+ const scoped = current.key === fitStateKey
304
+ ? current
305
+ : createInitialFitState(fitStateKey);
306
+ return {
307
+ ...scoped,
308
+ didAutoFit: true,
309
+ handledFitRequestVersion: fitRequestVersion,
310
+ };
311
+ });
312
+ }
313
+ }
314
+ };
315
+ void fitToDataBounds();
316
+ return () => {
317
+ isCancelled = true;
318
+ };
319
+ }, [
320
+ containerSize.height,
321
+ containerSize.width,
322
+ deckMapRef,
323
+ didAutoFit,
324
+ executeSql,
325
+ fitRequestVersion,
326
+ fitStateKey,
327
+ fitToData,
328
+ fitToDataSource,
329
+ handledFitRequestVersion,
330
+ ]);
331
+ return { fitToData };
332
+ }
333
+ //# sourceMappingURL=useDeckMapFitToBounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDeckMapFitToBounds.js","sourceRoot":"","sources":["../src/useDeckMapFitToBounds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,mBAAmB,EAAC,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,QAAQ,EACR,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAM1B,OAAO,EAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAiB,MAAM,OAAO,CAAC;AACnE,OAAO,EACL,mBAAmB,EACnB,oCAAoC,GAErC,MAAM,mBAAmB,CAAC;AAU3B,SAAS,qBAAqB,CAAC,GAAW;IACxC,OAAO;QACL,GAAG;QACH,UAAU,EAAE,KAAK;QACjB,iBAAiB,EAAE,CAAC;QACpB,wBAAwB,EAAE,CAAC;KAC5B,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAC9B,SAA6D;IAE7D,OAAO,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,gBAAgB,CACvB,YAAoD,EACpD,QAA6C,EAC7C,WAAgD,EAChD,IAAa;IAEb,IAAI,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,OAAO,GACX,QAMD,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAE1B,IAAI,YAAY,CAAC,eAAe,IAAI,YAAY,CAAC,cAAc,EAAE,CAAC;QAChE,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,cAAc,IAAI,OAAO,EAAE,cAAc,CAAC;IAEvE,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,EAAC,GAAG,YAAY,EAAE,cAAc,EAAE,OAAO,EAAC,CAAC;IACpD,CAAC;IAED,MAAM,KAAK,GAAG,WAED,CAAC;IACd,IAAI,KAAK,EAAE,eAAe,IAAI,KAAK,EAAE,cAAc,EAAE,CAAC;QACpD,OAAO;YACL,GAAG,YAAY;YACf,cAAc,EAAE,SAAS;YACzB,eAAe,EAAE,KAAK,CAAC,eAAe;YACtC,cAAc,EAAE,KAAK,CAAC,cAAc;SACrC,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,8DAA8D;IAC9D,MAAM,OAAO,GAAG,IAA2C,CAAC;IAC5D,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;QAC/B,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,SAAS;QAClD,MAAM,OAAO,GAAI,KAAiC,CAAC,gBAEtC,CAAC;QACd,IAAI,OAAO,EAAE,OAAO,KAAK,YAAY,CAAC,OAAO,IAAI,OAAO,EAAE,aAAa,EAAE,CAAC;YACxE,OAAO;gBACL,GAAG,YAAY;gBACf,QAAQ,EAAE,OAAO,CAAC,aAAuB;aAC1C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,+DAA+D;IAC/D,2DAA2D;IAC3D,MAAM,EAAC,cAAc,EAAE,KAAK,EAAE,GAAG,UAAU,EAAC,GAAG,YAAY,CAAC;IAC5D,KAAK,KAAK,CAAC;IACX,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,OAGxC;IACC,MAAM,EAAC,MAAM,EAAE,SAAS,EAAC,GAAG,OAAO,CAAC;IACpC,IAAI,CAAC,uBAAuB,CAAC,SAAS,CAAC,EAAE,CAAC;QACxC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ;QACnC,CAAC,CAAC,kBAAkB,MAAM,CAAC,QAAQ,wCAAwC;QAC3E,CAAC,CAAC,iBAAiB,mBAAmB,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,CAAC;IAEnE,IAAI,SAAS,CAAC,QAAQ,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC3C,OAAO;;6BAEkB,KAAK;6BACL,KAAK;6BACL,KAAK;6BACL,KAAK;cACpB,aAAa;cACb,KAAK;KACd,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,CAAC,cAAc,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACvD,MAAM,KAAK,GACT,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACxE,MAAM,QAAQ,GAAG,KAAK;YACpB,CAAC,CAAC,kBAAkB,WAAW,GAAG;YAClC,CAAC,CAAC,GAAG,WAAW,YAAY,CAAC;QAC/B,OAAO;;;;;;;+BAOoB,QAAQ;gBACvB,aAAa;gBACb,WAAW;;;KAGtB,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,CAAC,eAAe,IAAI,SAAS,CAAC,cAAc,EAAE,CAAC;QAC1D,MAAM,eAAe,GAAG,QAAQ,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QAC5D,MAAM,cAAc,GAAG,QAAQ,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC1D,OAAO;;;;;;;wCAO6B,eAAe,KAAK,cAAc;gBAC1D,aAAa;gBACb,eAAe,oBAAoB,cAAc;;;KAG5D,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,2DAA2D;IAC3D,OAAO;;;;;;;;cAQK,aAAa;;;;GAIxB,CAAC;AACJ,CAAC;AAED,SAAS,0BAA0B,CAAC,MAAkB;IACpD,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAChE,MAAM,WAAW,GAAG,iBAAiB,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC9D,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAChE,MAAM,WAAW,GAAG,iBAAiB,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC9D,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;QAC9B,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC7B,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;QAC9B,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,EAC7B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL;YACE,YAAY,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY;YAClE,WAAW,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC,WAAW;SAC/D;QACD;YACE,YAAY,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY;YAClE,WAAW,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC,WAAW;SAC/D;KACO,CAAC;AACb,CAAC;AAED,SAAS,oBAAoB,CAAC,OAM7B;IACC,MAAM,EAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAC,GAAG,OAAO,CAAC;IACpE,MAAM,QAAQ,GAAG,IAAI,mBAAmB,CAAC;QACvC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;QACzB,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;KAC5B,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,CAC/B;QACE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC7B,EACD,EAAC,OAAO,EAAC,CAKV,CAAC;IAEF,OAAO;QACL,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC;KACrC,CAAC;AACJ,CAAC;AAED,MAAM,gCAAgC,GAAG,IAAI,WAAW,EAAE,CAAC;AAE3D,MAAM,UAAU,8BAA8B,CAAC,OAAe;IAC5D,gCAAgC,CAAC,aAAa,CAC5C,IAAI,WAAW,CAAC,UAAU,EAAE,EAAC,MAAM,EAAE,EAAC,OAAO,EAAC,EAAC,CAAC,CACjD,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAMrC;IACC,MAAM,EAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAC,GAAG,OAAO,CAAC;IACvE,MAAM,SAAS,GAAG,mBAAmB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,kBAAkB,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;IAEtE,MAAM,YAAY,GAAG,SAAS,EAAE,SAAS,IAAI,IAAI,CAAC;IAClD,MAAM,SAAS,GAA2C,OAAO,CAC/D,GAAG,EAAE,CACH,gBAAgB,CACd,YAAY,EACZ,SAAS,EAAE,QAAQ,EACnB,SAAS,EAAE,WAAW,EACtB,SAAS,EAAE,IAAI,CAChB,EACH;QACE,YAAY;QACZ,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,WAAW;QACtB,SAAS,EAAE,IAAI;KAChB,CACF,CAAC;IAEF,MAAM,eAAe,GAAG,OAAO,CAC7B,GAAG,EAAE,CACH,SAAS;QACP,CAAC,CAAC,oCAAoC,CAAC;YACnC,SAAS;YACT,KAAK;YACL,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC;SAChD,CAAC;QACJ,CAAC,CAAC,SAAS,EACf,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CACnD,CAAC;IAEF,MAAM,YAAY,GAAG,OAAO,CAC1B,GAAG,EAAE,CACH,SAAS,IAAI,eAAe;QAC1B,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;YACb,MAAM,EAAE,eAAe;YACvB,SAAS;YACT,aAAa,EAAE,SAAS,CAAC,aAAa;SACvC,CAAC;QACJ,CAAC,CAAC,IAAI,EACV,CAAC,SAAS,CAAC,aAAa,EAAE,SAAS,EAAE,eAAe,CAAC,CACtD,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CACzB,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAC,OAAO,EAAE,YAAY,EAAC,CAAC,EAC7C,CAAC,YAAY,EAAE,OAAO,CAAC,CACxB,CAAC;IAEF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAA2B,GAAG,EAAE,CACtE,qBAAqB,CAAC,WAAW,CAAC,CACnC,CAAC;IAEF,MAAM,cAAc,GAClB,QAAQ,CAAC,GAAG,KAAK,WAAW;QAC1B,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,EAAC,UAAU,EAAE,iBAAiB,EAAE,wBAAwB,EAAC,GAC7D,cAAc,CAAC;IAEjB,iCAAiC;IACjC,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,gBAAgB,GAAG,CAAC,KAAY,EAAE,EAAE;YACxC,MAAM,MAAM,GAAI,KAAyC,CAAC,MAAM,CAAC;YACjE,IAAI,MAAM,EAAE,OAAO,KAAK,OAAO,EAAE,CAAC;gBAChC,WAAW,CAAC,CAAC,OAAO,EAAE,EAAE;oBACtB,MAAM,MAAM,GACV,OAAO,CAAC,GAAG,KAAK,WAAW;wBACzB,CAAC,CAAC,OAAO;wBACT,CAAC,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC;oBACzC,OAAO;wBACL,GAAG,MAAM;wBACT,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,GAAG,CAAC;qBAChD,CAAC;gBACJ,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC;QAEF,gCAAgC,CAAC,gBAAgB,CAC/C,UAAU,EACV,gBAAgB,CACjB,CAAC;QACF,OAAO,GAAG,EAAE;YACV,gCAAgC,CAAC,mBAAmB,CAClD,UAAU,EACV,gBAAgB,CACjB,CAAC;QACJ,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;IAE3B,8BAA8B;IAC9B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,mBAAmB,GAAG,iBAAiB,GAAG,wBAAwB,CAAC;QACzE,IACE,CAAC,uBAAuB,CAAC,SAAS,CAAC;YACnC,CAAC,eAAe;YAChB,aAAa,CAAC,KAAK,IAAI,CAAC;YACxB,aAAa,CAAC,MAAM,IAAI,CAAC;YACzB,CAAC,CAAC,mBAAmB,IAAI,UAAU,CAAC,EACpC,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,WAAW,GAAG,KAAK,CAAC;QAExB,MAAM,eAAe,GAAG,KAAK,IAAI,EAAE;YACjC,IAAI,CAAC;gBACH,IAAI,SAAS,CAAC,QAAQ,EAAE,CAAC;oBACvB,IAAI,CAAC;wBACH,MAAM,UAAU,CAAC,2BAA2B,CAAC,CAAC;oBAChD,CAAC;oBAAC,MAAM,CAAC;wBACP,2BAA2B;oBAC7B,CAAC;oBACD,IAAI,CAAC;wBACH,MAAM,UAAU,CAAC,SAAS,CAAC,CAAC;oBAC9B,CAAC;oBAAC,MAAM,CAAC;wBACP,wBAAwB;oBAC1B,CAAC;gBACH,CAAC;gBACD,MAAM,WAAW,GAAG,wBAAwB,CAAC;oBAC3C,MAAM,EAAE,eAAe;oBACvB,SAAS;iBACV,CAAC,CAAC;gBACH,IAAI,CAAC,WAAW;oBAAE,OAAO;gBAEzB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,WAAW,CAAC,CAAC;gBAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC5C,IAAI,WAAW,IAAI,CAAC,MAAM;oBAAE,OAAO;gBAEnC,MAAM,MAAM,GAAG,0BAA0B,CAAC,MAAM,CAAC,CAAC;gBAClD,IAAI,CAAC,MAAM,EAAE,CAAC;oBACZ,WAAW,CAAC,CAAC,OAAO,EAAE,EAAE;wBACtB,MAAM,MAAM,GACV,OAAO,CAAC,GAAG,KAAK,WAAW;4BACzB,CAAC,CAAC,OAAO;4BACT,CAAC,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC;wBACzC,OAAO;4BACL,GAAG,MAAM;4BACT,UAAU,EAAE,IAAI;4BAChB,wBAAwB,EAAE,iBAAiB;yBAC5C,CAAC;oBACJ,CAAC,CAAC,CAAC;oBACH,OAAO;gBACT,CAAC;gBAED,MAAM,aAAa,GAAG,oBAAoB,CAAC;oBACzC,MAAM;oBACN,KAAK,EAAE,aAAa,CAAC,KAAK;oBAC1B,MAAM,EAAE,aAAa,CAAC,MAAM;oBAC5B,OAAO,EAAE,SAAS,CAAC,OAAO;oBAC1B,OAAO,EAAE,SAAS,CAAC,OAAO;iBAC3B,CAAC,CAAC;gBACH,IAAI,CAAC,WAAW,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC;oBACvC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;gBAC3C,CAAC;gBACD,WAAW,CAAC,CAAC,OAAO,EAAE,EAAE;oBACtB,MAAM,MAAM,GACV,OAAO,CAAC,GAAG,KAAK,WAAW;wBACzB,CAAC,CAAC,OAAO;wBACT,CAAC,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC;oBACzC,OAAO;wBACL,GAAG,MAAM;wBACT,UAAU,EAAE,IAAI;wBAChB,wBAAwB,EAAE,iBAAiB;qBAC5C,CAAC;gBACJ,CAAC,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,WAAW,EAAE,CAAC;oBACjB,WAAW,CAAC,CAAC,OAAO,EAAE,EAAE;wBACtB,MAAM,MAAM,GACV,OAAO,CAAC,GAAG,KAAK,WAAW;4BACzB,CAAC,CAAC,OAAO;4BACT,CAAC,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC;wBACzC,OAAO;4BACL,GAAG,MAAM;4BACT,UAAU,EAAE,IAAI;4BAChB,wBAAwB,EAAE,iBAAiB;yBAC5C,CAAC;oBACJ,CAAC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC,CAAC;QAEF,KAAK,eAAe,EAAE,CAAC;QAEvB,OAAO,GAAG,EAAE;YACV,WAAW,GAAG,IAAI,CAAC;QACrB,CAAC,CAAC;IACJ,CAAC,EAAE;QACD,aAAa,CAAC,MAAM;QACpB,aAAa,CAAC,KAAK;QACnB,UAAU;QACV,UAAU;QACV,UAAU;QACV,iBAAiB;QACjB,WAAW;QACX,SAAS;QACT,eAAe;QACf,wBAAwB;KACzB,CAAC,CAAC;IAEH,OAAO,EAAC,SAAS,EAAC,CAAC;AACrB,CAAC","sourcesContent":["import {WebMercatorViewport} from '@deck.gl/core';\nimport {\n escapeId,\n getColValAsNumber,\n quoteTableReference,\n useStoreWithDuckDb,\n} from '@sqlrooms/duckdb';\nimport type {\n MosaicDashboardEntryType,\n MosaicDashboardPanelConfigType,\n} from '@sqlrooms/mosaic';\nimport type {Table as ArrowTable} from 'apache-arrow';\nimport {useEffect, useMemo, useState, type RefObject} from 'react';\nimport {\n asDeckJsonMapConfig,\n resolveDeckMapDashboardDatasetSource,\n type DeckMapDashboardFitToDataConfig,\n} from './dashboardConfig';\nimport type {DeckJsonMapHandle} from './types';\n\ntype DeckMapDashboardFitState = {\n key: string;\n didAutoFit: boolean;\n fitRequestVersion: number;\n handledFitRequestVersion: number;\n};\n\nfunction createInitialFitState(key: string): DeckMapDashboardFitState {\n return {\n key,\n didAutoFit: false,\n fitRequestVersion: 0,\n handledFitRequestVersion: 0,\n };\n}\n\nfunction isDeckMapFitToDataValid(\n fitToData: DeckMapDashboardFitToDataConfig | null | undefined,\n): fitToData is DeckMapDashboardFitToDataConfig {\n return Boolean(fitToData?.dataset);\n}\n\nfunction resolveFitToData(\n fitToDataRaw: DeckMapDashboardFitToDataConfig | null,\n datasets: Record<string, unknown> | undefined,\n interaction: Record<string, unknown> | undefined,\n spec: unknown,\n): DeckMapDashboardFitToDataConfig | null {\n if (!fitToDataRaw) return null;\n const dataset = (\n datasets as\n | Record<\n string,\n {geometryColumn?: string; source?: {sqlQuery?: string}} | undefined\n >\n | undefined\n )?.[fitToDataRaw.dataset];\n\n if (fitToDataRaw.longitudeColumn && fitToDataRaw.latitudeColumn) {\n return fitToDataRaw;\n }\n\n const geomCol = fitToDataRaw.geometryColumn ?? dataset?.geometryColumn;\n\n if (geomCol) {\n return {...fitToDataRaw, geometryColumn: geomCol};\n }\n\n const inter = interaction as\n | {longitudeColumn?: string; latitudeColumn?: string}\n | undefined;\n if (inter?.longitudeColumn && inter?.latitudeColumn) {\n return {\n ...fitToDataRaw,\n geometryColumn: undefined,\n longitudeColumn: inter.longitudeColumn,\n latitudeColumn: inter.latitudeColumn,\n };\n }\n\n // Detect H3 layer as last resort: check layers bound to this dataset\n // for hexagonColumn when no other bounds method is available.\n const specObj = spec as Record<string, unknown> | undefined;\n const specLayers = Array.isArray(specObj?.layers) ? specObj.layers : [];\n for (const layer of specLayers) {\n if (!layer || typeof layer !== 'object') continue;\n const binding = (layer as Record<string, unknown>)._sqlroomsBinding as\n | Record<string, unknown>\n | undefined;\n if (binding?.dataset === fitToDataRaw.dataset && binding?.hexagonColumn) {\n return {\n ...fitToDataRaw,\n h3Column: binding.hexagonColumn as string,\n };\n }\n }\n\n // Strip unverifiable geometryColumn; the bounds query will use\n // DuckDB case-insensitive column resolution as a fallback.\n const {geometryColumn: _geom, ...safeConfig} = fitToDataRaw;\n void _geom;\n return safeConfig;\n}\n\nexport function createDeckMapBoundsQuery(options: {\n source: {tableName?: string; sqlQuery?: string};\n fitToData: DeckMapDashboardFitToDataConfig;\n}) {\n const {source, fitToData} = options;\n if (!isDeckMapFitToDataValid(fitToData)) {\n return null;\n }\n const baseSourceSql = source.sqlQuery\n ? `SELECT * FROM (${source.sqlQuery}) AS \"__sqlrooms_dashboard_map_source\"`\n : `SELECT * FROM ${quoteTableReference(source.tableName ?? '')}`;\n\n if (fitToData.h3Column) {\n const h3Col = escapeId(fitToData.h3Column);\n return `\n SELECT\n MIN(h3_cell_to_lng(${h3Col})) AS min_longitude,\n MIN(h3_cell_to_lat(${h3Col})) AS min_latitude,\n MAX(h3_cell_to_lng(${h3Col})) AS max_longitude,\n MAX(h3_cell_to_lat(${h3Col})) AS max_latitude\n FROM (${baseSourceSql}) AS \"__sqlrooms_dashboard_map_h3\"\n WHERE ${h3Col} IS NOT NULL\n `;\n }\n\n if (fitToData.geometryColumn) {\n const geometryCol = escapeId(fitToData.geometryColumn);\n const isWkb =\n source.sqlQuery && source.sqlQuery.toLowerCase().includes('st_aswkb');\n const geomExpr = isWkb\n ? `ST_GeomFromWKB(${geometryCol})`\n : `${geometryCol}::GEOMETRY`;\n return `\n SELECT\n ST_XMin(extent) AS min_longitude,\n ST_YMin(extent) AS min_latitude,\n ST_XMax(extent) AS max_longitude,\n ST_YMax(extent) AS max_latitude\n FROM (\n SELECT ST_Extent_Agg(${geomExpr}) AS extent\n FROM (${baseSourceSql}) AS \"__sqlrooms_dashboard_map_geom\"\n WHERE ${geometryCol} IS NOT NULL\n ) AS \"__sqlrooms_dashboard_map_extent\"\n WHERE extent IS NOT NULL\n `;\n }\n\n if (fitToData.longitudeColumn && fitToData.latitudeColumn) {\n const longitudeColumn = escapeId(fitToData.longitudeColumn);\n const latitudeColumn = escapeId(fitToData.latitudeColumn);\n return `\n SELECT\n ST_XMin(extent) AS min_longitude,\n ST_YMin(extent) AS min_latitude,\n ST_XMax(extent) AS max_longitude,\n ST_YMax(extent) AS max_latitude\n FROM (\n SELECT ST_Extent_Agg(ST_Point(${longitudeColumn}, ${latitudeColumn})) AS extent\n FROM (${baseSourceSql}) AS \"__sqlrooms_dashboard_map_points\"\n WHERE ${longitudeColumn} IS NOT NULL AND ${latitudeColumn} IS NOT NULL\n ) AS \"__sqlrooms_dashboard_map_extent\"\n WHERE extent IS NOT NULL\n `;\n }\n\n // Fallback: use DuckDB unquoted identifiers for case-insensitive resolution\n // of common coordinate column names (longitude, lat, etc.)\n return `\n SELECT\n ST_XMin(extent) AS min_longitude,\n ST_YMin(extent) AS min_latitude,\n ST_XMax(extent) AS max_longitude,\n ST_YMax(extent) AS max_latitude\n FROM (\n SELECT ST_Extent_Agg(ST_Point(Longitude, Latitude)) AS extent\n FROM (${baseSourceSql}) AS \"__sqlrooms_dashboard_map_points\"\n WHERE Longitude IS NOT NULL AND Latitude IS NOT NULL\n ) AS \"__sqlrooms_dashboard_map_extent\"\n WHERE extent IS NOT NULL\n `;\n}\n\nfunction readBoundsFromExtentResult(result: ArrowTable) {\n const minLongitude = getColValAsNumber(result, 'min_longitude');\n const minLatitude = getColValAsNumber(result, 'min_latitude');\n const maxLongitude = getColValAsNumber(result, 'max_longitude');\n const maxLatitude = getColValAsNumber(result, 'max_latitude');\n if (\n !Number.isFinite(minLongitude) ||\n !Number.isFinite(minLatitude) ||\n !Number.isFinite(maxLongitude) ||\n !Number.isFinite(maxLatitude)\n ) {\n return null;\n }\n\n return [\n [\n minLongitude === maxLongitude ? minLongitude - 0.01 : minLongitude,\n minLatitude === maxLatitude ? minLatitude - 0.01 : minLatitude,\n ],\n [\n minLongitude === maxLongitude ? maxLongitude + 0.01 : maxLongitude,\n minLatitude === maxLatitude ? maxLatitude + 0.01 : maxLatitude,\n ],\n ] as const;\n}\n\nfunction fitViewStateToBounds(options: {\n bounds: readonly [readonly [number, number], readonly [number, number]];\n width: number;\n height: number;\n padding?: number;\n maxZoom?: number;\n}) {\n const {bounds, width, height, padding = 40, maxZoom = 18} = options;\n const viewport = new WebMercatorViewport({\n width: Math.max(width, 1),\n height: Math.max(height, 1),\n });\n const fitted = viewport.fitBounds(\n [\n [bounds[0][0], bounds[0][1]],\n [bounds[1][0], bounds[1][1]],\n ],\n {padding},\n ) as WebMercatorViewport & {\n longitude: number;\n latitude: number;\n zoom: number;\n };\n\n return {\n longitude: fitted.longitude,\n latitude: fitted.latitude,\n zoom: Math.min(fitted.zoom, maxZoom),\n };\n}\n\nconst deckMapDashboardFitRequestTarget = new EventTarget();\n\nexport function emitDeckMapDashboardFitRequest(panelId: string) {\n deckMapDashboardFitRequestTarget.dispatchEvent(\n new CustomEvent('fit-view', {detail: {panelId}}),\n );\n}\n\n/**\n * Manages the \"fit to bounds\" state machine for a deck map panel.\n * Handles auto-fit on mount, manual fit requests, and bounds query execution.\n */\nexport function useDeckMapFitToBounds(options: {\n panelId: string;\n dashboard: MosaicDashboardEntryType;\n panel: MosaicDashboardPanelConfigType;\n containerSize: {width: number; height: number};\n deckMapRef: RefObject<DeckJsonMapHandle | null>;\n}) {\n const {panelId, dashboard, panel, containerSize, deckMapRef} = options;\n const mapConfig = asDeckJsonMapConfig(panel.config);\n const executeSql = useStoreWithDuckDb((state) => state.db.executeSql);\n\n const fitToDataRaw = mapConfig?.fitToData ?? null;\n const fitToData: DeckMapDashboardFitToDataConfig | null = useMemo(\n () =>\n resolveFitToData(\n fitToDataRaw,\n mapConfig?.datasets,\n mapConfig?.interaction,\n mapConfig?.spec,\n ),\n [\n fitToDataRaw,\n mapConfig?.datasets,\n mapConfig?.interaction,\n mapConfig?.spec,\n ],\n );\n\n const fitToDataSource = useMemo(\n () =>\n fitToData\n ? resolveDeckMapDashboardDatasetSource({\n dashboard,\n panel,\n dataset: mapConfig?.datasets[fitToData.dataset],\n })\n : undefined,\n [dashboard, fitToData, mapConfig?.datasets, panel],\n );\n\n const fitToDataKey = useMemo(\n () =>\n fitToData && fitToDataSource\n ? JSON.stringify({\n source: fitToDataSource,\n fitToData,\n selectedTable: dashboard.selectedTable,\n })\n : null,\n [dashboard.selectedTable, fitToData, fitToDataSource],\n );\n\n const fitStateKey = useMemo(\n () => JSON.stringify({panelId, fitToDataKey}),\n [fitToDataKey, panelId],\n );\n\n const [fitState, setFitState] = useState<DeckMapDashboardFitState>(() =>\n createInitialFitState(fitStateKey),\n );\n\n const activeFitState =\n fitState.key === fitStateKey\n ? fitState\n : createInitialFitState(fitStateKey);\n const {didAutoFit, fitRequestVersion, handledFitRequestVersion} =\n activeFitState;\n\n // Listen for manual fit requests\n useEffect(() => {\n const handleFitRequest = (event: Event) => {\n const detail = (event as CustomEvent<{panelId?: string}>).detail;\n if (detail?.panelId === panelId) {\n setFitState((current) => {\n const scoped =\n current.key === fitStateKey\n ? current\n : createInitialFitState(fitStateKey);\n return {\n ...scoped,\n fitRequestVersion: scoped.fitRequestVersion + 1,\n };\n });\n }\n };\n\n deckMapDashboardFitRequestTarget.addEventListener(\n 'fit-view',\n handleFitRequest,\n );\n return () => {\n deckMapDashboardFitRequestTarget.removeEventListener(\n 'fit-view',\n handleFitRequest,\n );\n };\n }, [fitStateKey, panelId]);\n\n // Execute fit-to-bounds query\n useEffect(() => {\n const hasManualFitRequest = fitRequestVersion > handledFitRequestVersion;\n if (\n !isDeckMapFitToDataValid(fitToData) ||\n !fitToDataSource ||\n containerSize.width <= 0 ||\n containerSize.height <= 0 ||\n (!hasManualFitRequest && didAutoFit)\n ) {\n return;\n }\n\n let isCancelled = false;\n\n const fitToDataBounds = async () => {\n try {\n if (fitToData.h3Column) {\n try {\n await executeSql('INSTALL h3 FROM community');\n } catch {\n // May already be installed\n }\n try {\n await executeSql('LOAD h3');\n } catch {\n // May already be loaded\n }\n }\n const boundsQuery = createDeckMapBoundsQuery({\n source: fitToDataSource,\n fitToData,\n });\n if (!boundsQuery) return;\n\n const handle = await executeSql(boundsQuery);\n const result = handle ? await handle : null;\n if (isCancelled || !result) return;\n\n const bounds = readBoundsFromExtentResult(result);\n if (!bounds) {\n setFitState((current) => {\n const scoped =\n current.key === fitStateKey\n ? current\n : createInitialFitState(fitStateKey);\n return {\n ...scoped,\n didAutoFit: true,\n handledFitRequestVersion: fitRequestVersion,\n };\n });\n return;\n }\n\n const nextViewState = fitViewStateToBounds({\n bounds,\n width: containerSize.width,\n height: containerSize.height,\n padding: fitToData.padding,\n maxZoom: fitToData.maxZoom,\n });\n if (!isCancelled && deckMapRef.current) {\n deckMapRef.current.jumpTo(nextViewState);\n }\n setFitState((current) => {\n const scoped =\n current.key === fitStateKey\n ? current\n : createInitialFitState(fitStateKey);\n return {\n ...scoped,\n didAutoFit: true,\n handledFitRequestVersion: fitRequestVersion,\n };\n });\n } catch {\n if (!isCancelled) {\n setFitState((current) => {\n const scoped =\n current.key === fitStateKey\n ? current\n : createInitialFitState(fitStateKey);\n return {\n ...scoped,\n didAutoFit: true,\n handledFitRequestVersion: fitRequestVersion,\n };\n });\n }\n }\n };\n\n void fitToDataBounds();\n\n return () => {\n isCancelled = true;\n };\n }, [\n containerSize.height,\n containerSize.width,\n deckMapRef,\n didAutoFit,\n executeSql,\n fitRequestVersion,\n fitStateKey,\n fitToData,\n fitToDataSource,\n handledFitRequestVersion,\n ]);\n\n return {fitToData};\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlrooms/deck",
3
- "version": "0.29.0-rc.8",
3
+ "version": "0.29.0-rc.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/sqlrooms/sqlrooms.git"
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@deck.gl/core": "^9.3.1",
31
+ "@deck.gl/geo-layers": "^9.3.1",
31
32
  "@deck.gl/json": "^9.3.1",
32
33
  "@deck.gl/layers": "^9.3.1",
33
34
  "@deck.gl/mapbox": "^9.3.1",
@@ -37,10 +38,10 @@
37
38
  "@loaders.gl/gis": "^4.4.1",
38
39
  "@loaders.gl/wkt": "^4.4.1",
39
40
  "@paralleldrive/cuid2": "^3.0.0",
40
- "@sqlrooms/color-scales": "0.29.0-rc.8",
41
- "@sqlrooms/duckdb": "0.29.0-rc.8",
42
- "@sqlrooms/mosaic": "0.29.0-rc.8",
43
- "@sqlrooms/ui": "0.29.0-rc.8",
41
+ "@sqlrooms/color-scales": "0.29.0-rc.9",
42
+ "@sqlrooms/duckdb": "0.29.0-rc.9",
43
+ "@sqlrooms/mosaic": "0.29.0-rc.9",
44
+ "@sqlrooms/ui": "0.29.0-rc.9",
44
45
  "@uwdata/mosaic-core": "^0.21.0",
45
46
  "@uwdata/mosaic-sql": "^0.21.0",
46
47
  "ai": "^6.0.159",
@@ -65,5 +66,5 @@
65
66
  "publishConfig": {
66
67
  "access": "public"
67
68
  },
68
- "gitHead": "dcb1a51631c5fa87b49d3e78ae6c0023eccd4189"
69
+ "gitHead": "e9e4ed45ebb0ceb3e1e647674a7039b8d9325583"
69
70
  }