@sqlrooms/deck 0.29.0-rc.10 → 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/README.md +42 -6
- package/dist/DeckJsonMap.d.ts.map +1 -1
- package/dist/DeckJsonMap.js +2 -6
- package/dist/DeckJsonMap.js.map +1 -1
- package/dist/DeckJsonMapSpec.d.ts +875 -53
- package/dist/DeckJsonMapSpec.d.ts.map +1 -1
- package/dist/DeckJsonMapSpec.js +7 -0
- package/dist/DeckJsonMapSpec.js.map +1 -1
- package/dist/DeckMapDefaultStylesProvider.d.ts +3 -4
- package/dist/DeckMapDefaultStylesProvider.d.ts.map +1 -1
- package/dist/DeckMapDefaultStylesProvider.js +13 -7
- package/dist/DeckMapDefaultStylesProvider.js.map +1 -1
- package/dist/MapSettings.d.ts.map +1 -1
- package/dist/MapSettings.js +25 -11
- package/dist/MapSettings.js.map +1 -1
- package/dist/ai.d.ts +19 -5
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +60 -112
- package/dist/ai.js.map +1 -1
- package/dist/aiNormalize.d.ts +31 -0
- package/dist/aiNormalize.d.ts.map +1 -0
- package/dist/aiNormalize.js +629 -0
- package/dist/aiNormalize.js.map +1 -0
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +72 -8
- package/dist/dashboard.js.map +1 -1
- package/dist/dashboardConfig.d.ts +10 -1
- package/dist/dashboardConfig.d.ts.map +1 -1
- package/dist/dashboardConfig.js +55 -22
- package/dist/dashboardConfig.js.map +1 -1
- package/dist/datasets/PreparedDatasetStore.d.ts.map +1 -1
- package/dist/datasets/PreparedDatasetStore.js +31 -2
- package/dist/datasets/PreparedDatasetStore.js.map +1 -1
- package/dist/datasets/wrapGeometryAsWkb.d.ts +20 -0
- package/dist/datasets/wrapGeometryAsWkb.d.ts.map +1 -0
- package/dist/datasets/wrapGeometryAsWkb.js +65 -0
- package/dist/datasets/wrapGeometryAsWkb.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/compileGeoArrowAccessor.d.ts +5 -0
- package/dist/json/compileGeoArrowAccessor.d.ts.map +1 -1
- package/dist/json/compileGeoArrowAccessor.js +17 -3
- package/dist/json/compileGeoArrowAccessor.js.map +1 -1
- package/dist/json/createDeckJsonConfiguration.d.ts.map +1 -1
- package/dist/json/createDeckJsonConfiguration.js +85 -18
- package/dist/json/createDeckJsonConfiguration.js.map +1 -1
- package/dist/json/extractColorScaleLegends.d.ts.map +1 -1
- package/dist/json/extractColorScaleLegends.js +21 -7
- package/dist/json/extractColorScaleLegends.js.map +1 -1
- package/dist/json/layers/DeckH3HexagonLayer.d.ts.map +1 -1
- package/dist/json/layers/DeckH3HexagonLayer.js +9 -14
- package/dist/json/layers/DeckH3HexagonLayer.js.map +1 -1
- package/dist/json/layers/h3IndexFormat.d.ts +6 -0
- package/dist/json/layers/h3IndexFormat.d.ts.map +1 -0
- package/dist/json/layers/h3IndexFormat.js +17 -0
- package/dist/json/layers/h3IndexFormat.js.map +1 -0
- package/dist/json/layers/index.d.ts +1 -0
- package/dist/json/layers/index.d.ts.map +1 -1
- package/dist/json/layers/index.js +1 -0
- package/dist/json/layers/index.js.map +1 -1
- package/dist/json/rewriteGeoArrowAccessors.d.ts.map +1 -1
- package/dist/json/rewriteGeoArrowAccessors.js +9 -0
- package/dist/json/rewriteGeoArrowAccessors.js.map +1 -1
- package/dist/json/scaleFunction.d.ts +30 -0
- package/dist/json/scaleFunction.d.ts.map +1 -0
- package/dist/json/scaleFunction.js +126 -0
- package/dist/json/scaleFunction.js.map +1 -0
- package/dist/mapAiConfig.d.ts +2 -0
- package/dist/mapAiConfig.d.ts.map +1 -1
- package/dist/mapAiConfig.js +2 -1
- package/dist/mapAiConfig.js.map +1 -1
- package/dist/mapAiSharedInstructions.d.ts +3 -0
- package/dist/mapAiSharedInstructions.d.ts.map +1 -0
- package/dist/mapAiSharedInstructions.js +16 -0
- package/dist/mapAiSharedInstructions.js.map +1 -0
- package/dist/mapConfig.d.ts +6 -0
- package/dist/mapConfig.d.ts.map +1 -1
- package/dist/mapConfig.js.map +1 -1
- package/dist/mapConfigUtils.d.ts.map +1 -1
- package/dist/mapConfigUtils.js +2 -0
- package/dist/mapConfigUtils.js.map +1 -1
- package/dist/mapFit.d.ts +16 -0
- package/dist/mapFit.d.ts.map +1 -1
- package/dist/mapFit.js +208 -30
- package/dist/mapFit.js.map +1 -1
- package/dist/mapLayerConfigUtils.d.ts +9 -1
- package/dist/mapLayerConfigUtils.d.ts.map +1 -1
- package/dist/mapLayerConfigUtils.js +62 -12
- package/dist/mapLayerConfigUtils.js.map +1 -1
- package/dist/mapResourceAuthoring.d.ts +2 -0
- package/dist/mapResourceAuthoring.d.ts.map +1 -1
- package/dist/mapResourceAuthoring.js +402 -16
- package/dist/mapResourceAuthoring.js.map +1 -1
- package/dist/mosaic.d.ts +1 -1
- package/dist/mosaic.d.ts.map +1 -1
- package/dist/mosaic.js.map +1 -1
- package/dist/prepare/geoarrow.js.map +1 -1
- package/dist/prepare/wkbDecoder.d.ts +10 -0
- package/dist/prepare/wkbDecoder.d.ts.map +1 -1
- package/dist/prepare/wkbDecoder.js +93 -18
- package/dist/prepare/wkbDecoder.js.map +1 -1
- package/dist/prepare/wkbParser.d.ts +7 -0
- package/dist/prepare/wkbParser.d.ts.map +1 -1
- package/dist/prepare/wkbParser.js +21 -0
- package/dist/prepare/wkbParser.js.map +1 -1
- package/dist/selectStarAsWkbCollision.d.ts +3 -0
- package/dist/selectStarAsWkbCollision.d.ts.map +1 -0
- package/dist/selectStarAsWkbCollision.js +78 -0
- package/dist/selectStarAsWkbCollision.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useDeckMapFitToBounds.d.ts.map +1 -1
- package/dist/useDeckMapFitToBounds.js +1 -1
- package/dist/useDeckMapFitToBounds.js.map +1 -1
- package/package.json +15 -15
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { allKnownColorSchemeNames } from '@sqlrooms/color-scales/colorSchemeNames';
|
|
2
|
+
import { deckMapColumnLayerHasRadiusConflicts, stripDeckMapColumnLayerRadiusConflicts, } from './mapLayerConfigUtils';
|
|
3
|
+
// Avoid importing duckdb/mosaic here — keeps Jest light.
|
|
4
|
+
const DECK_TABLE_DATASET_SOURCE_RELATION = '__sqlrooms_source';
|
|
5
|
+
function quoteDeckMapSqlIdentifier(identifier) {
|
|
6
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
7
|
+
}
|
|
8
|
+
const SCHEME_NAME_BY_LOWER = new Map(allKnownColorSchemeNames.map((s) => [s.toLowerCase(), s]));
|
|
9
|
+
const DEFAULT_AI_GEOMETRY_COLUMN = '__sqlrooms_geom';
|
|
10
|
+
const DEFAULT_AI_POINT_RADIUS = 4;
|
|
11
|
+
const DEFAULT_AI_HEATMAP_RADIUS_PIXELS = 30; // deck.gl default
|
|
12
|
+
const DEFAULT_AI_COLUMN_RADIUS_METERS = 20;
|
|
13
|
+
const DEFAULT_FILL_COLOR = [56, 189, 248, 180];
|
|
14
|
+
/** Basic-mode numeric size clamps; string accessors are validator-only. */
|
|
15
|
+
function normalizeAiMapConfigRadius(config) {
|
|
16
|
+
if (config.configMode === 'custom')
|
|
17
|
+
return config;
|
|
18
|
+
const spec = config.spec;
|
|
19
|
+
if (!spec || !Array.isArray(spec.layers))
|
|
20
|
+
return config;
|
|
21
|
+
let changed = false;
|
|
22
|
+
const layers = spec.layers.map((layer) => {
|
|
23
|
+
if (!layer || typeof layer !== 'object' || Array.isArray(layer)) {
|
|
24
|
+
return layer;
|
|
25
|
+
}
|
|
26
|
+
let l = layer;
|
|
27
|
+
let layerChanged = false;
|
|
28
|
+
if (l['@@type'] === 'GeoArrowScatterplotLayer' &&
|
|
29
|
+
typeof l.getRadius === 'number' &&
|
|
30
|
+
l.getRadius <= 0) {
|
|
31
|
+
// Pair with pixel units/clamps so 4 is not meters / capped by a stale max.
|
|
32
|
+
l = {
|
|
33
|
+
...l,
|
|
34
|
+
getRadius: DEFAULT_AI_POINT_RADIUS,
|
|
35
|
+
radiusUnits: 'pixels',
|
|
36
|
+
radiusMinPixels: DEFAULT_AI_POINT_RADIUS,
|
|
37
|
+
radiusMaxPixels: DEFAULT_AI_POINT_RADIUS,
|
|
38
|
+
};
|
|
39
|
+
layerChanged = true;
|
|
40
|
+
}
|
|
41
|
+
const LINE_LAYER_TYPES = new Set([
|
|
42
|
+
'GeoArrowPathLayer',
|
|
43
|
+
'GeoArrowArcLayer',
|
|
44
|
+
'GeoArrowTripsLayer',
|
|
45
|
+
]);
|
|
46
|
+
if (typeof l['@@type'] === 'string' &&
|
|
47
|
+
LINE_LAYER_TYPES.has(l['@@type']) &&
|
|
48
|
+
typeof l.getWidth === 'number') {
|
|
49
|
+
if (l.widthUnits !== 'pixels') {
|
|
50
|
+
l = { ...l, widthUnits: 'pixels' };
|
|
51
|
+
layerChanged = true;
|
|
52
|
+
}
|
|
53
|
+
if (typeof l.widthMinPixels === 'number' &&
|
|
54
|
+
typeof l.widthMaxPixels === 'number' &&
|
|
55
|
+
l.widthMaxPixels < l.widthMinPixels) {
|
|
56
|
+
const value = Math.max(l.widthMinPixels, l.widthMaxPixels, typeof l.getWidth === 'number' ? l.getWidth : 0);
|
|
57
|
+
l = {
|
|
58
|
+
...l,
|
|
59
|
+
widthUnits: 'pixels',
|
|
60
|
+
widthMinPixels: value,
|
|
61
|
+
widthMaxPixels: value,
|
|
62
|
+
getWidth: value,
|
|
63
|
+
};
|
|
64
|
+
layerChanged = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (l['@@type'] === 'GeoArrowHeatmapLayer') {
|
|
68
|
+
const rp = l.radiusPixels;
|
|
69
|
+
if (typeof rp === 'number' && rp <= 0) {
|
|
70
|
+
l = { ...l, radiusPixels: DEFAULT_AI_HEATMAP_RADIUS_PIXELS };
|
|
71
|
+
layerChanged = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Column radius: default meters; strip scatter/heatmap radius leftovers.
|
|
75
|
+
if (l['@@type'] === 'GeoArrowColumnLayer') {
|
|
76
|
+
const r = l.radius;
|
|
77
|
+
const needsDefaultRadius = typeof r !== 'string' &&
|
|
78
|
+
(typeof r !== 'number' || !Number.isFinite(r) || r <= 0);
|
|
79
|
+
const hasConflicts = deckMapColumnLayerHasRadiusConflicts(l);
|
|
80
|
+
if (needsDefaultRadius || hasConflicts) {
|
|
81
|
+
const next = stripDeckMapColumnLayerRadiusConflicts(l);
|
|
82
|
+
// Stripping getRadius without setting radius leaves deck.gl's 1000m default.
|
|
83
|
+
if (needsDefaultRadius) {
|
|
84
|
+
next.radius = DEFAULT_AI_COLUMN_RADIUS_METERS;
|
|
85
|
+
}
|
|
86
|
+
l = next;
|
|
87
|
+
layerChanged = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (layerChanged) {
|
|
91
|
+
changed = true;
|
|
92
|
+
return l;
|
|
93
|
+
}
|
|
94
|
+
return layer;
|
|
95
|
+
});
|
|
96
|
+
if (!changed)
|
|
97
|
+
return config;
|
|
98
|
+
return { ...config, spec: { ...spec, layers } };
|
|
99
|
+
}
|
|
100
|
+
const COLOR_ACCESSOR_PROPS = [
|
|
101
|
+
'getFillColor',
|
|
102
|
+
'getLineColor',
|
|
103
|
+
'getColor',
|
|
104
|
+
'getSourceColor',
|
|
105
|
+
'getTargetColor',
|
|
106
|
+
];
|
|
107
|
+
/** Safe layer defaults; invalid shapes stay for getDeckMapResourceConfigIssues. */
|
|
108
|
+
function normalizeAiMapConfigLayers(config) {
|
|
109
|
+
const spec = config.spec;
|
|
110
|
+
if (!spec || !Array.isArray(spec.layers))
|
|
111
|
+
return config;
|
|
112
|
+
const datasetIds = config.datasets ? Object.keys(config.datasets) : [];
|
|
113
|
+
const soloDatasetId = datasetIds.length === 1 ? datasetIds[0] : undefined;
|
|
114
|
+
let changed = false;
|
|
115
|
+
const layers = spec.layers.map((layer) => {
|
|
116
|
+
if (!layer || typeof layer !== 'object' || Array.isArray(layer)) {
|
|
117
|
+
return layer;
|
|
118
|
+
}
|
|
119
|
+
let l = layer;
|
|
120
|
+
let layerChanged = false;
|
|
121
|
+
// scaleLinear → scale only for getElevation (radius/width scales are invalid).
|
|
122
|
+
{
|
|
123
|
+
const value = l.getElevation;
|
|
124
|
+
if (value &&
|
|
125
|
+
typeof value === 'object' &&
|
|
126
|
+
!Array.isArray(value) &&
|
|
127
|
+
value['@@function'] === 'scaleLinear') {
|
|
128
|
+
l = {
|
|
129
|
+
...l,
|
|
130
|
+
getElevation: { ...value, '@@function': 'scale' },
|
|
131
|
+
};
|
|
132
|
+
layerChanged = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Canonicalize scheme casing / trim; type×scheme mismatches stay for validation.
|
|
136
|
+
for (const prop of COLOR_ACCESSOR_PROPS) {
|
|
137
|
+
const value = l[prop];
|
|
138
|
+
if (value &&
|
|
139
|
+
typeof value === 'object' &&
|
|
140
|
+
!Array.isArray(value) &&
|
|
141
|
+
'@@function' in value &&
|
|
142
|
+
value['@@function'] === 'colorScale') {
|
|
143
|
+
const v = value;
|
|
144
|
+
let next = v;
|
|
145
|
+
const rawScheme = v.scheme;
|
|
146
|
+
if (typeof rawScheme === 'string') {
|
|
147
|
+
const trimmed = rawScheme.trim();
|
|
148
|
+
const canonical = SCHEME_NAME_BY_LOWER.get(trimmed.toLowerCase()) ?? trimmed;
|
|
149
|
+
if (canonical !== rawScheme) {
|
|
150
|
+
next = { ...next, scheme: canonical };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (typeof v.type === 'string' && v.type !== v.type.trim()) {
|
|
154
|
+
next = { ...next, type: v.type.trim() };
|
|
155
|
+
}
|
|
156
|
+
if (next !== v) {
|
|
157
|
+
l = { ...l, [prop]: next };
|
|
158
|
+
layerChanged = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Solo dataset: inject missing binding or replace unknown dataset ids.
|
|
163
|
+
if (soloDatasetId) {
|
|
164
|
+
const binding = l._sqlroomsBinding;
|
|
165
|
+
const boundId = binding && typeof binding.dataset === 'string' && binding.dataset.trim()
|
|
166
|
+
? binding.dataset.trim()
|
|
167
|
+
: undefined;
|
|
168
|
+
const needsBinding = !boundId ||
|
|
169
|
+
!(config.datasets && boundId in config.datasets);
|
|
170
|
+
if (needsBinding) {
|
|
171
|
+
l = {
|
|
172
|
+
...l,
|
|
173
|
+
_sqlroomsBinding: { ...(binding ?? {}), dataset: soloDatasetId },
|
|
174
|
+
};
|
|
175
|
+
layerChanged = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// AI-only strip: UI may keep colorRange; do not reject in the resource validator.
|
|
179
|
+
if (l['@@type'] === 'GeoArrowHeatmapLayer' && 'colorRange' in l) {
|
|
180
|
+
const { colorRange: _cr, ...rest } = l;
|
|
181
|
+
l = rest;
|
|
182
|
+
layerChanged = true;
|
|
183
|
+
}
|
|
184
|
+
// Lift simple @@=column into hexagonColumn for fit-to-bounds.
|
|
185
|
+
if (l['@@type'] === 'GeoArrowH3HexagonLayer') {
|
|
186
|
+
const hexAccessor = l.getHexagon;
|
|
187
|
+
let hexColumn;
|
|
188
|
+
if (typeof hexAccessor === 'string') {
|
|
189
|
+
const m = hexAccessor.trim().match(/^@@=([A-Za-z_][\w]*)$/);
|
|
190
|
+
hexColumn = m?.[1];
|
|
191
|
+
}
|
|
192
|
+
const binding = l._sqlroomsBinding &&
|
|
193
|
+
typeof l._sqlroomsBinding === 'object' &&
|
|
194
|
+
!Array.isArray(l._sqlroomsBinding)
|
|
195
|
+
? l._sqlroomsBinding
|
|
196
|
+
: undefined;
|
|
197
|
+
if (hexColumn &&
|
|
198
|
+
(!binding?.hexagonColumn ||
|
|
199
|
+
typeof binding.hexagonColumn !== 'string' ||
|
|
200
|
+
!binding.hexagonColumn.trim())) {
|
|
201
|
+
l = {
|
|
202
|
+
...l,
|
|
203
|
+
_sqlroomsBinding: {
|
|
204
|
+
...(binding ?? {}),
|
|
205
|
+
hexagonColumn: hexColumn,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
layerChanged = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (l['@@type'] === 'GeoArrowColumnLayer' &&
|
|
212
|
+
l.getElevation !== undefined &&
|
|
213
|
+
l.extruded === undefined) {
|
|
214
|
+
l = { ...l, extruded: true };
|
|
215
|
+
layerChanged = true;
|
|
216
|
+
}
|
|
217
|
+
// deck.gl defaults missing fill to opaque black.
|
|
218
|
+
const layerType = l['@@type'];
|
|
219
|
+
const needsFillDefault = typeof layerType === 'string' &&
|
|
220
|
+
(layerType === 'GeoArrowScatterplotLayer' ||
|
|
221
|
+
layerType === 'GeoArrowPolygonLayer' ||
|
|
222
|
+
layerType === 'GeoArrowSolidPolygonLayer') &&
|
|
223
|
+
!l.getFillColor;
|
|
224
|
+
if (needsFillDefault) {
|
|
225
|
+
l = { ...l, getFillColor: [...DEFAULT_FILL_COLOR] };
|
|
226
|
+
layerChanged = true;
|
|
227
|
+
}
|
|
228
|
+
// filled:false with no stroke renders nothing.
|
|
229
|
+
if (l['@@type'] === 'GeoArrowScatterplotLayer' &&
|
|
230
|
+
l.filled === false &&
|
|
231
|
+
!l.stroked) {
|
|
232
|
+
l = { ...l, filled: true };
|
|
233
|
+
layerChanged = true;
|
|
234
|
+
}
|
|
235
|
+
if (layerChanged)
|
|
236
|
+
changed = true;
|
|
237
|
+
return layerChanged ? l : layer;
|
|
238
|
+
});
|
|
239
|
+
// Keep visible:false — legitimate hide state (type switches use replaceLayers).
|
|
240
|
+
if (!changed)
|
|
241
|
+
return config;
|
|
242
|
+
return { ...config, spec: { ...spec, layers } };
|
|
243
|
+
}
|
|
244
|
+
/** Split table refs on dots outside double quotes. */
|
|
245
|
+
function splitTableRef(ref) {
|
|
246
|
+
const parts = [];
|
|
247
|
+
let current = '';
|
|
248
|
+
let inQuotes = false;
|
|
249
|
+
for (const ch of ref) {
|
|
250
|
+
if (ch === '"') {
|
|
251
|
+
inQuotes = !inQuotes;
|
|
252
|
+
current += ch;
|
|
253
|
+
}
|
|
254
|
+
else if (ch === '.' && !inQuotes) {
|
|
255
|
+
parts.push(current);
|
|
256
|
+
current = '';
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
current += ch;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
parts.push(current);
|
|
263
|
+
return parts;
|
|
264
|
+
}
|
|
265
|
+
function unquoteTableRefPart(part) {
|
|
266
|
+
const trimmed = part.trim();
|
|
267
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
268
|
+
return trimmed.slice(1, -1).replace(/""/g, '"');
|
|
269
|
+
}
|
|
270
|
+
return trimmed;
|
|
271
|
+
}
|
|
272
|
+
function normalizeAiMapConfigDatasetSources(config, stripCatalogNames) {
|
|
273
|
+
const datasets = config.datasets;
|
|
274
|
+
if (!datasets || typeof datasets !== 'object')
|
|
275
|
+
return config;
|
|
276
|
+
// Arc-bound geom columns that need WKB decode hints.
|
|
277
|
+
const arcGeomColsByDataset = new Map();
|
|
278
|
+
const spec = config.spec;
|
|
279
|
+
if (spec && Array.isArray(spec.layers)) {
|
|
280
|
+
for (const layer of spec.layers) {
|
|
281
|
+
if (!layer || typeof layer !== 'object')
|
|
282
|
+
continue;
|
|
283
|
+
const l = layer;
|
|
284
|
+
if (l['@@type'] !== 'GeoArrowArcLayer')
|
|
285
|
+
continue;
|
|
286
|
+
const binding = l._sqlroomsBinding;
|
|
287
|
+
const datasetId = typeof binding?.dataset === 'string' ? binding.dataset : undefined;
|
|
288
|
+
if (!datasetId)
|
|
289
|
+
continue;
|
|
290
|
+
const cols = arcGeomColsByDataset.get(datasetId) ?? new Set();
|
|
291
|
+
if (typeof binding?.sourceGeometryColumn === 'string') {
|
|
292
|
+
cols.add(binding.sourceGeometryColumn);
|
|
293
|
+
}
|
|
294
|
+
if (typeof binding?.targetGeometryColumn === 'string') {
|
|
295
|
+
cols.add(binding.targetGeometryColumn);
|
|
296
|
+
}
|
|
297
|
+
if (cols.size > 0)
|
|
298
|
+
arcGeomColsByDataset.set(datasetId, cols);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
let changed = false;
|
|
302
|
+
const nextDatasets = {};
|
|
303
|
+
for (const [id, dataset] of Object.entries(datasets)) {
|
|
304
|
+
const d = dataset;
|
|
305
|
+
const source = d?.source;
|
|
306
|
+
const tableName = source?.tableName;
|
|
307
|
+
let next = d;
|
|
308
|
+
let nextSource = source;
|
|
309
|
+
if (typeof tableName === 'string' && stripCatalogNames.size > 0) {
|
|
310
|
+
const parts = splitTableRef(tableName);
|
|
311
|
+
if (parts.length >= 3) {
|
|
312
|
+
const catalog = unquoteTableRefPart(parts[0]).toLowerCase();
|
|
313
|
+
if (stripCatalogNames.has(catalog)) {
|
|
314
|
+
const stripped = parts.slice(1).join('.');
|
|
315
|
+
nextSource = { ...nextSource, tableName: stripped };
|
|
316
|
+
next = { ...next, source: nextSource };
|
|
317
|
+
changed = true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Scope WKB hints to this dataset's arc columns only.
|
|
322
|
+
const transformSql = next?.source
|
|
323
|
+
?.transformSql;
|
|
324
|
+
const arcCols = arcGeomColsByDataset.get(id);
|
|
325
|
+
if (arcCols &&
|
|
326
|
+
arcCols.size > 0 &&
|
|
327
|
+
typeof transformSql === 'string' &&
|
|
328
|
+
!next?.geometryEncodingHint) {
|
|
329
|
+
const mentionsArcCol = [...arcCols].some((col) => transformSql.includes(col));
|
|
330
|
+
if (mentionsArcCol) {
|
|
331
|
+
next = { ...next, geometryEncodingHint: 'wkb' };
|
|
332
|
+
changed = true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
nextDatasets[id] = next ?? dataset;
|
|
336
|
+
}
|
|
337
|
+
if (!changed)
|
|
338
|
+
return config;
|
|
339
|
+
return { ...config, datasets: nextDatasets };
|
|
340
|
+
}
|
|
341
|
+
/** Inject lon/lat → `__sqlrooms_geom` transform when fitToData needs it. */
|
|
342
|
+
function normalizeAiMapConfig(config) {
|
|
343
|
+
// mapbox:// mapStyle: leave for validator (do not strip silently).
|
|
344
|
+
const datasets = config.datasets;
|
|
345
|
+
let fitToData = config.fitToData;
|
|
346
|
+
// Unwrap { datasetId: { dataset, … } } → flat fitToData.
|
|
347
|
+
if (fitToData && !fitToData.dataset && typeof fitToData === 'object') {
|
|
348
|
+
const keys = Object.keys(fitToData);
|
|
349
|
+
if (keys.length === 1) {
|
|
350
|
+
const nested = fitToData[keys[0]];
|
|
351
|
+
if (nested && typeof nested === 'object' && nested.dataset) {
|
|
352
|
+
fitToData = nested;
|
|
353
|
+
config = { ...config, fitToData: fitToData };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Default fitToData to sole dataset / first layer binding.
|
|
358
|
+
if (!fitToData?.dataset && datasets && typeof datasets === 'object') {
|
|
359
|
+
const datasetIds = Object.keys(datasets);
|
|
360
|
+
let datasetId = datasetIds.length === 1 ? datasetIds[0] : undefined;
|
|
361
|
+
if (!datasetId) {
|
|
362
|
+
const spec = config.spec;
|
|
363
|
+
const layers = Array.isArray(spec?.layers) ? spec.layers : [];
|
|
364
|
+
for (const layer of layers) {
|
|
365
|
+
if (!layer || typeof layer !== 'object')
|
|
366
|
+
continue;
|
|
367
|
+
const binding = layer._sqlroomsBinding;
|
|
368
|
+
if (typeof binding?.dataset === 'string' && binding.dataset.trim()) {
|
|
369
|
+
datasetId = binding.dataset;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (datasetId && datasets[datasetId]) {
|
|
375
|
+
fitToData = { dataset: datasetId };
|
|
376
|
+
config = { ...config, fitToData: fitToData };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (!datasets || typeof datasets !== 'object' || !fitToData) {
|
|
380
|
+
return config;
|
|
381
|
+
}
|
|
382
|
+
const lonCol = fitToData.longitudeColumn;
|
|
383
|
+
const latCol = fitToData.latitudeColumn;
|
|
384
|
+
if (!lonCol || !latCol) {
|
|
385
|
+
return config;
|
|
386
|
+
}
|
|
387
|
+
const targetDatasetId = fitToData.dataset;
|
|
388
|
+
if (!targetDatasetId) {
|
|
389
|
+
return config;
|
|
390
|
+
}
|
|
391
|
+
const targetDataset = datasets[targetDatasetId];
|
|
392
|
+
if (!targetDataset) {
|
|
393
|
+
return config;
|
|
394
|
+
}
|
|
395
|
+
const source = targetDataset.source;
|
|
396
|
+
// Inject ST_Point WKB as `__sqlrooms_geom` for point layers only (never AS geom).
|
|
397
|
+
if (!source?.tableName || source.sqlQuery || source.transformSql) {
|
|
398
|
+
return config;
|
|
399
|
+
}
|
|
400
|
+
const POINT_LONLAT_LAYER_TYPES = new Set([
|
|
401
|
+
'GeoArrowScatterplotLayer',
|
|
402
|
+
'GeoArrowHeatmapLayer',
|
|
403
|
+
'GeoArrowColumnLayer',
|
|
404
|
+
'GeoJsonLayer',
|
|
405
|
+
// Unprefixed aliases may appear before validator repair.
|
|
406
|
+
'ScatterplotLayer',
|
|
407
|
+
'HeatmapLayer',
|
|
408
|
+
'ColumnLayer',
|
|
409
|
+
]);
|
|
410
|
+
const datasetIds = Object.keys(datasets);
|
|
411
|
+
const soleDatasetId = datasetIds.length === 1 ? datasetIds[0] : undefined;
|
|
412
|
+
const layerTargetsDataset = (binding) => {
|
|
413
|
+
if (binding?.dataset === targetDatasetId)
|
|
414
|
+
return true;
|
|
415
|
+
// Match solo-dataset binding injection in normalizeAiMapConfigLayers.
|
|
416
|
+
return ((!binding || typeof binding.dataset !== 'string' || !binding.dataset) &&
|
|
417
|
+
soleDatasetId === targetDatasetId);
|
|
418
|
+
};
|
|
419
|
+
const spec = config.spec;
|
|
420
|
+
const layers = Array.isArray(spec?.layers) ? spec.layers : [];
|
|
421
|
+
const pointLayersForDataset = layers.filter((layer) => {
|
|
422
|
+
if (!layer || typeof layer !== 'object')
|
|
423
|
+
return false;
|
|
424
|
+
const rec = layer;
|
|
425
|
+
if (rec.visible === false)
|
|
426
|
+
return false;
|
|
427
|
+
if (typeof rec['@@type'] !== 'string' ||
|
|
428
|
+
!POINT_LONLAT_LAYER_TYPES.has(rec['@@type'])) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
const binding = rec._sqlroomsBinding;
|
|
432
|
+
return layerTargetsDataset(binding);
|
|
433
|
+
});
|
|
434
|
+
if (pointLayersForDataset.length === 0) {
|
|
435
|
+
return config;
|
|
436
|
+
}
|
|
437
|
+
const geometryColumn = DEFAULT_AI_GEOMETRY_COLUMN;
|
|
438
|
+
const quotedLon = quoteDeckMapSqlIdentifier(lonCol);
|
|
439
|
+
const quotedLat = quoteDeckMapSqlIdentifier(latCol);
|
|
440
|
+
const quotedGeom = quoteDeckMapSqlIdentifier(geometryColumn);
|
|
441
|
+
const transformSql = [
|
|
442
|
+
`SELECT *, ST_AsWKB(ST_Point(${quotedLon}, ${quotedLat})) AS ${quotedGeom}`,
|
|
443
|
+
`FROM ${DECK_TABLE_DATASET_SOURCE_RELATION}`,
|
|
444
|
+
`WHERE ${quotedLon} IS NOT NULL AND ${quotedLat} IS NOT NULL`,
|
|
445
|
+
].join(' ');
|
|
446
|
+
const nextLayers = layers.map((layer) => {
|
|
447
|
+
if (!layer || typeof layer !== 'object')
|
|
448
|
+
return layer;
|
|
449
|
+
const rec = layer;
|
|
450
|
+
if (typeof rec['@@type'] !== 'string' ||
|
|
451
|
+
!POINT_LONLAT_LAYER_TYPES.has(rec['@@type'])) {
|
|
452
|
+
return layer;
|
|
453
|
+
}
|
|
454
|
+
const binding = rec._sqlroomsBinding;
|
|
455
|
+
if (!layerTargetsDataset(binding))
|
|
456
|
+
return layer;
|
|
457
|
+
const nextBinding = {
|
|
458
|
+
...(binding ?? {}),
|
|
459
|
+
dataset: binding?.dataset ?? targetDatasetId,
|
|
460
|
+
geometryColumn,
|
|
461
|
+
};
|
|
462
|
+
if (binding?.dataset === nextBinding.dataset &&
|
|
463
|
+
binding?.geometryColumn === geometryColumn) {
|
|
464
|
+
return layer;
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
...rec,
|
|
468
|
+
_sqlroomsBinding: nextBinding,
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
const layersChanged = nextLayers.some((layer, i) => layer !== layers[i]);
|
|
472
|
+
return {
|
|
473
|
+
...config,
|
|
474
|
+
...(layersChanged && spec
|
|
475
|
+
? { spec: { ...spec, layers: nextLayers } }
|
|
476
|
+
: {}),
|
|
477
|
+
datasets: {
|
|
478
|
+
...datasets,
|
|
479
|
+
[targetDatasetId]: {
|
|
480
|
+
...targetDataset,
|
|
481
|
+
source: { tableName: source.tableName, transformSql },
|
|
482
|
+
geometryColumn,
|
|
483
|
+
geometryEncodingHint: 'wkb',
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
export function normalizeAiDeckMapConfig(config, options) {
|
|
489
|
+
const stripCatalogNames = new Set((options?.stripCatalogNames ?? [])
|
|
490
|
+
.map((name) => name.trim().toLowerCase())
|
|
491
|
+
.filter((name) => name.length > 0));
|
|
492
|
+
return normalizeAiMapConfigRadius(normalizeAiMapConfigLayers(normalizeAiMapConfigDatasetSources(normalizeAiMapConfig(config), stripCatalogNames)));
|
|
493
|
+
}
|
|
494
|
+
const COLOR_SCALE_ACCESSOR_PROPS = [
|
|
495
|
+
'getFillColor',
|
|
496
|
+
'getLineColor',
|
|
497
|
+
'getColor',
|
|
498
|
+
'getSourceColor',
|
|
499
|
+
'getTargetColor',
|
|
500
|
+
];
|
|
501
|
+
/**
|
|
502
|
+
* Fix colorScale `field` casing; reject unknown fields on bare `{tableName}` sources.
|
|
503
|
+
* Call before normalize so lon/lat `transformSql` inject does not disable rejection.
|
|
504
|
+
*/
|
|
505
|
+
export function validateAndFixColorScaleFields(config, resolveTable) {
|
|
506
|
+
const rawSpec = config.spec;
|
|
507
|
+
if (!rawSpec)
|
|
508
|
+
return config;
|
|
509
|
+
let spec;
|
|
510
|
+
if (typeof rawSpec === 'string') {
|
|
511
|
+
try {
|
|
512
|
+
const parsed = JSON.parse(rawSpec);
|
|
513
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
514
|
+
return config;
|
|
515
|
+
}
|
|
516
|
+
spec = parsed;
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return config;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else if (typeof rawSpec === 'object' && !Array.isArray(rawSpec)) {
|
|
523
|
+
spec = rawSpec;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
return config;
|
|
527
|
+
}
|
|
528
|
+
const layers = Array.isArray(spec.layers)
|
|
529
|
+
? spec.layers
|
|
530
|
+
: [];
|
|
531
|
+
const columnsByDataset = new Map();
|
|
532
|
+
for (const [datasetId, dataset] of Object.entries(config.datasets ?? {})) {
|
|
533
|
+
const source = dataset?.source;
|
|
534
|
+
if (!source)
|
|
535
|
+
continue;
|
|
536
|
+
const tableName = typeof source.tableName === 'string' ? source.tableName : undefined;
|
|
537
|
+
if (!tableName)
|
|
538
|
+
continue;
|
|
539
|
+
const table = resolveTable(tableName);
|
|
540
|
+
if (!table?.columns?.length)
|
|
541
|
+
continue;
|
|
542
|
+
const transformSql = typeof source.transformSql === 'string' ? source.transformSql : '';
|
|
543
|
+
const sqlQuery = typeof source.sqlQuery === 'string' ? source.sqlQuery : '';
|
|
544
|
+
const derivedSql = `${transformSql} ${sqlQuery}`.trim();
|
|
545
|
+
const hasDerivedSql = derivedSql.length > 0;
|
|
546
|
+
const byLower = new Map();
|
|
547
|
+
for (const col of table.columns) {
|
|
548
|
+
byLower.set(col.name.toLowerCase(), col.name);
|
|
549
|
+
}
|
|
550
|
+
columnsByDataset.set(datasetId, {
|
|
551
|
+
byLower,
|
|
552
|
+
rejectUnknown: !hasDerivedSql,
|
|
553
|
+
derivedSql,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (columnsByDataset.size === 0)
|
|
557
|
+
return config;
|
|
558
|
+
const datasetIds = Object.keys(config.datasets ?? {});
|
|
559
|
+
const soloDatasetId = datasetIds.length === 1 ? datasetIds[0] : undefined;
|
|
560
|
+
const errors = [];
|
|
561
|
+
let changed = false;
|
|
562
|
+
const nextLayers = layers.map((layer, i) => {
|
|
563
|
+
const binding = layer._sqlroomsBinding;
|
|
564
|
+
const boundDataset = typeof binding?.dataset === 'string' && binding.dataset.trim()
|
|
565
|
+
? binding.dataset
|
|
566
|
+
: undefined;
|
|
567
|
+
// Mirror solo-dataset binding repair in normalize.
|
|
568
|
+
const datasetId = boundDataset && columnsByDataset.has(boundDataset)
|
|
569
|
+
? boundDataset
|
|
570
|
+
: (soloDatasetId ?? boundDataset);
|
|
571
|
+
const cols = datasetId ? columnsByDataset.get(datasetId) : undefined;
|
|
572
|
+
if (!cols || !datasetId)
|
|
573
|
+
return layer;
|
|
574
|
+
let nextLayer;
|
|
575
|
+
for (const prop of COLOR_SCALE_ACCESSOR_PROPS) {
|
|
576
|
+
const accessor = (nextLayer ?? layer)[prop];
|
|
577
|
+
if (!accessor || typeof accessor !== 'object' || Array.isArray(accessor))
|
|
578
|
+
continue;
|
|
579
|
+
const acc = accessor;
|
|
580
|
+
if (acc['@@function'] !== 'colorScale')
|
|
581
|
+
continue;
|
|
582
|
+
const field = acc.field;
|
|
583
|
+
if (typeof field !== 'string')
|
|
584
|
+
continue;
|
|
585
|
+
const canonical = cols.byLower.get(field.toLowerCase());
|
|
586
|
+
if (canonical === field) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (canonical !== undefined) {
|
|
590
|
+
nextLayer = {
|
|
591
|
+
...(nextLayer ?? layer),
|
|
592
|
+
[prop]: { ...acc, field: canonical },
|
|
593
|
+
};
|
|
594
|
+
changed = true;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (!cols.rejectUnknown) {
|
|
598
|
+
// Allow SQL aliases; reject typos absent from both schema and SQL text.
|
|
599
|
+
if (cols.derivedSql.toLowerCase().includes(field.toLowerCase())) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const available = [...cols.byLower.values()].join(', ');
|
|
604
|
+
errors.push(`spec.layers.${i}.${prop}: colorScale field "${field}" is not a column in dataset "${datasetId}". Available columns: ${available}`);
|
|
605
|
+
}
|
|
606
|
+
return nextLayer ?? layer;
|
|
607
|
+
});
|
|
608
|
+
if (errors.length > 0) {
|
|
609
|
+
throw new Error(errors.join('; '));
|
|
610
|
+
}
|
|
611
|
+
if (!changed && typeof rawSpec !== 'string')
|
|
612
|
+
return config;
|
|
613
|
+
return {
|
|
614
|
+
...config,
|
|
615
|
+
spec: {
|
|
616
|
+
...spec,
|
|
617
|
+
layers: nextLayers,
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
export function prepareAiDeckMapConfig(config, options) {
|
|
622
|
+
const withFields = options?.resolveTable
|
|
623
|
+
? validateAndFixColorScaleFields(config, options.resolveTable)
|
|
624
|
+
: config;
|
|
625
|
+
return normalizeAiDeckMapConfig(withFields, {
|
|
626
|
+
stripCatalogNames: options?.stripCatalogNames,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
//# sourceMappingURL=aiNormalize.js.map
|