@carto/api-client 0.5.16 → 0.5.17
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/CHANGELOG.md +6 -0
- package/build/api-client.cjs +978 -153
- package/build/api-client.cjs.map +1 -1
- package/build/api-client.d.cts +152 -29
- package/build/api-client.d.ts +152 -29
- package/build/api-client.js +964 -151
- package/build/api-client.js.map +1 -1
- package/package.json +2 -1
- package/src/fetch-map/basemap-styles.ts +1 -1
- package/src/fetch-map/index.ts +6 -1
- package/src/fetch-map/layer-map.ts +130 -40
- package/src/fetch-map/parse-map.ts +284 -165
- package/src/fetch-map/raster-layer.ts +536 -0
- package/src/fetch-map/types.ts +21 -7
- package/src/fetch-map/utils.ts +56 -0
- package/src/fetch-map/vec-expr-evaluator.ts +374 -0
- package/src/index.ts +7 -1
- package/src/sources/types.ts +52 -11
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import type {RasterMetadata, RasterMetadataBand} from '../sources/types.js';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createVecExprEvaluator,
|
|
5
|
+
type VecExprResult,
|
|
6
|
+
type VecExprVecLike,
|
|
7
|
+
} from './vec-expr-evaluator.js';
|
|
8
|
+
import type {
|
|
9
|
+
ColorBand,
|
|
10
|
+
ColorRange,
|
|
11
|
+
MapLayerConfig,
|
|
12
|
+
RasterLayerConfigColorBand,
|
|
13
|
+
VisualChannels,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
import {createColorScale, type ScaleType} from './layer-map.js';
|
|
16
|
+
import {getLog10ScaleSteps} from './utils.js';
|
|
17
|
+
|
|
18
|
+
const UNKNOWN_COLOR = [134, 141, 145];
|
|
19
|
+
|
|
20
|
+
const RASTER_COLOR_BANDS = ['red', 'green', 'blue'] as const;
|
|
21
|
+
|
|
22
|
+
function getHasDataPredicate(noData: number | string | undefined) {
|
|
23
|
+
if (noData === 'nan') {
|
|
24
|
+
return (v: number) => !isNaN(v);
|
|
25
|
+
}
|
|
26
|
+
if (typeof noData === 'string') {
|
|
27
|
+
noData = parseFloat(noData);
|
|
28
|
+
}
|
|
29
|
+
if (typeof noData === 'number') {
|
|
30
|
+
return (v: number) => v !== noData && !isNaN(v);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return () => true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// this is data as seen in RasterLayer
|
|
37
|
+
type RasterLayerData = {
|
|
38
|
+
blockSize: number;
|
|
39
|
+
cells: BinaryDataCells;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// this is data as seen in RasterColumnLayer - the one that actually renders pixels
|
|
43
|
+
// (binary data is just wrapped)
|
|
44
|
+
type RasterColumnLayerData = {
|
|
45
|
+
length: number; // number of pixels
|
|
46
|
+
data: RasterLayerData;
|
|
47
|
+
|
|
48
|
+
// used to store buffers directly to be used by deck.gl, so we can skip accessors
|
|
49
|
+
attributes?: Record<string, ArrayLike<number>>;
|
|
50
|
+
|
|
51
|
+
// temporary storage for expression results filled in dataTransform
|
|
52
|
+
expressionEvalContext?: Record<string, ArrayLike<number>>;
|
|
53
|
+
customExpressionResults?: Record<string, VecExprResult>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type BinaryDataCells = {
|
|
57
|
+
numericProps: Record<string, {value: VecExprVecLike}>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function createRasterColumnLayerDataTransform(
|
|
61
|
+
transform: (dataWrapped: RasterColumnLayerData) => RasterColumnLayerData
|
|
62
|
+
) {
|
|
63
|
+
return (data: RasterLayerData | RasterColumnLayerData) => {
|
|
64
|
+
if (!data || !('data' in data) || !data?.data?.cells?.numericProps) {
|
|
65
|
+
// we're in RasterLayer, or we've got invalid data
|
|
66
|
+
return data as RasterLayerData;
|
|
67
|
+
}
|
|
68
|
+
// we only transform data when in RasterColumnLayer
|
|
69
|
+
return transform(data);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createEvaluationContext(
|
|
74
|
+
numericProps: Record<string, {value: VecExprVecLike}>,
|
|
75
|
+
noData: number | string | undefined
|
|
76
|
+
) {
|
|
77
|
+
// Optimization note
|
|
78
|
+
// Seems like Array.from(256k+typed array takes even 15ms), so _we_ can afford to copy the array if we really don't need it to
|
|
79
|
+
// only copy values to array only if we really see nodata in all bands
|
|
80
|
+
const hasData = getHasDataPredicate(noData);
|
|
81
|
+
const bands = Object.entries(numericProps).map(([bandName, {value}]) => ({
|
|
82
|
+
bandName,
|
|
83
|
+
values: value,
|
|
84
|
+
copied: false,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const length = bands[0].values.length;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < length; i++) {
|
|
90
|
+
let hasSomeData = false;
|
|
91
|
+
for (let j = 0; j < bands.length; j++) {
|
|
92
|
+
hasSomeData = hasSomeData || hasData(bands[j].values[i]);
|
|
93
|
+
}
|
|
94
|
+
if (!hasSomeData) {
|
|
95
|
+
for (let j = 0; j < bands.length; j++) {
|
|
96
|
+
if (!bands[j].copied) {
|
|
97
|
+
bands[j].copied = true;
|
|
98
|
+
bands[j].values = Array.from(bands[j].values);
|
|
99
|
+
}
|
|
100
|
+
bands[j].values[i] = NaN;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const context = bands.reduce(
|
|
106
|
+
(agg, {bandName, values}) => {
|
|
107
|
+
agg[bandName] = values;
|
|
108
|
+
return agg;
|
|
109
|
+
},
|
|
110
|
+
{} as Record<string, ArrayLike<number>>
|
|
111
|
+
);
|
|
112
|
+
return context;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createExprDataTransform({
|
|
116
|
+
colorBand,
|
|
117
|
+
rasterMetadata,
|
|
118
|
+
usedSymbols,
|
|
119
|
+
}: {
|
|
120
|
+
colorBand: RasterLayerConfigColorBand | null | undefined;
|
|
121
|
+
rasterMetadata: RasterMetadata;
|
|
122
|
+
usedSymbols: string[];
|
|
123
|
+
}) {
|
|
124
|
+
if (!colorBand || !colorBand.type || colorBand.type === 'none') {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const expr = colorBand?.type === 'expression' ? colorBand.value : undefined;
|
|
129
|
+
const vecExprEvaluator = expr ? createVecExprEvaluator(expr) : undefined;
|
|
130
|
+
|
|
131
|
+
const dataTransform = createRasterColumnLayerDataTransform(
|
|
132
|
+
(dataWrapped: RasterColumnLayerData) => {
|
|
133
|
+
const data = dataWrapped.data;
|
|
134
|
+
if (expr) {
|
|
135
|
+
const cachedResult = dataWrapped.customExpressionResults?.[expr];
|
|
136
|
+
if (cachedResult) {
|
|
137
|
+
return dataWrapped;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let context = dataWrapped.expressionEvalContext;
|
|
142
|
+
if (!context) {
|
|
143
|
+
const usedNumericProps = usedSymbols.reduce(
|
|
144
|
+
(acc, symbol) => {
|
|
145
|
+
acc[symbol] = data.cells.numericProps[symbol];
|
|
146
|
+
return acc;
|
|
147
|
+
},
|
|
148
|
+
{} as Record<string, {value: VecExprVecLike}>
|
|
149
|
+
);
|
|
150
|
+
context = createEvaluationContext(
|
|
151
|
+
usedNumericProps,
|
|
152
|
+
rasterMetadata.nodata
|
|
153
|
+
);
|
|
154
|
+
dataWrapped = {
|
|
155
|
+
...dataWrapped,
|
|
156
|
+
expressionEvalContext: context,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!vecExprEvaluator || !expr) return dataWrapped;
|
|
161
|
+
|
|
162
|
+
const evalResult = vecExprEvaluator(context);
|
|
163
|
+
return {
|
|
164
|
+
...dataWrapped,
|
|
165
|
+
customExpressionResults: {
|
|
166
|
+
...dataWrapped.customExpressionResults,
|
|
167
|
+
[expr]: evalResult,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
return dataTransform;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function combineDataTransforms<T>(
|
|
176
|
+
dataTransforms: (((data: T) => T) | undefined)[]
|
|
177
|
+
): ((data: T) => T) | undefined {
|
|
178
|
+
const actualTransforms = dataTransforms.filter((v) => v) as ((
|
|
179
|
+
data: T
|
|
180
|
+
) => T)[];
|
|
181
|
+
|
|
182
|
+
if (actualTransforms.length === 0) return undefined;
|
|
183
|
+
if (actualTransforms.length === 1) return actualTransforms[0];
|
|
184
|
+
|
|
185
|
+
return (data: T) =>
|
|
186
|
+
actualTransforms.reduce(
|
|
187
|
+
(aggData, transformFun) => transformFun(aggData),
|
|
188
|
+
data
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createRgbToColorBufferDataTransform({
|
|
193
|
+
bandDefs,
|
|
194
|
+
attribute,
|
|
195
|
+
}: {
|
|
196
|
+
bandDefs: Partial<Record<ColorBand, RasterLayerConfigColorBand>>;
|
|
197
|
+
attribute: string;
|
|
198
|
+
}) {
|
|
199
|
+
return createRasterColumnLayerDataTransform(
|
|
200
|
+
(dataWrapped: RasterColumnLayerData) => {
|
|
201
|
+
const length = dataWrapped.length;
|
|
202
|
+
|
|
203
|
+
const getBandBufferOrValue = (colorBand?: RasterLayerConfigColorBand) => {
|
|
204
|
+
if (colorBand?.type === 'expression') {
|
|
205
|
+
return dataWrapped.customExpressionResults?.[colorBand.value];
|
|
206
|
+
}
|
|
207
|
+
if (colorBand?.type === 'band') {
|
|
208
|
+
// we could use original band, but this one is already cleared from nodata if needed
|
|
209
|
+
return dataWrapped.expressionEvalContext?.[colorBand.value];
|
|
210
|
+
}
|
|
211
|
+
return 0;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const red = getBandBufferOrValue(bandDefs.red);
|
|
215
|
+
const green = getBandBufferOrValue(bandDefs.green);
|
|
216
|
+
const blue = getBandBufferOrValue(bandDefs.blue);
|
|
217
|
+
|
|
218
|
+
const colorBuffer = new Uint8Array(length * 4);
|
|
219
|
+
for (
|
|
220
|
+
let inputIndex = 0, outputIndex = 0;
|
|
221
|
+
inputIndex < length;
|
|
222
|
+
inputIndex++, outputIndex += 4
|
|
223
|
+
) {
|
|
224
|
+
const redRaw =
|
|
225
|
+
typeof red === 'number' ? red : red ? red[inputIndex] : NaN;
|
|
226
|
+
const greenRaw =
|
|
227
|
+
typeof green === 'number' ? green : green ? green[inputIndex] : NaN;
|
|
228
|
+
const blueRaw =
|
|
229
|
+
typeof blue === 'number' ? blue : blue ? blue[inputIndex] : NaN;
|
|
230
|
+
|
|
231
|
+
if (isNaN(redRaw) && isNaN(greenRaw) && isNaN(blueRaw)) {
|
|
232
|
+
// skip pixel
|
|
233
|
+
bufferSetRgba(colorBuffer, outputIndex, 0, 0, 0, 0);
|
|
234
|
+
} else {
|
|
235
|
+
bufferSetRgba(
|
|
236
|
+
colorBuffer,
|
|
237
|
+
outputIndex,
|
|
238
|
+
redRaw,
|
|
239
|
+
greenRaw,
|
|
240
|
+
blueRaw,
|
|
241
|
+
255
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// clear cached buffers
|
|
247
|
+
// This transform is applied last -after expression & band evaluators which store and
|
|
248
|
+
// cache values for _this_ transform.
|
|
249
|
+
// Now, _assuming_ this is last transform we can clear those buffers.
|
|
250
|
+
dataWrapped.customExpressionResults = undefined;
|
|
251
|
+
dataWrapped.expressionEvalContext = undefined;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...dataWrapped,
|
|
255
|
+
attributes: {
|
|
256
|
+
[attribute]: colorBuffer,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getUsedSymbols(colorBands: RasterLayerConfigColorBand[]) {
|
|
264
|
+
return Array.from(
|
|
265
|
+
colorBands.reduce((symbols, band) => {
|
|
266
|
+
if (band.type === 'expression') {
|
|
267
|
+
const expressionSymbols =
|
|
268
|
+
createVecExprEvaluator(band.value)?.symbols || [];
|
|
269
|
+
expressionSymbols.forEach((symbol) => symbols.add(symbol));
|
|
270
|
+
}
|
|
271
|
+
if (band.type === 'band') {
|
|
272
|
+
symbols.add(band.value);
|
|
273
|
+
}
|
|
274
|
+
return symbols;
|
|
275
|
+
}, new Set<string>())
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function getRasterTileLayerStylePropsRgb({
|
|
280
|
+
layerConfig,
|
|
281
|
+
rasterMetadata,
|
|
282
|
+
visualChannels,
|
|
283
|
+
}: {
|
|
284
|
+
layerConfig: MapLayerConfig;
|
|
285
|
+
rasterMetadata: RasterMetadata;
|
|
286
|
+
visualChannels: VisualChannels;
|
|
287
|
+
}) {
|
|
288
|
+
const {visConfig} = layerConfig;
|
|
289
|
+
const {colorBands} = visConfig;
|
|
290
|
+
|
|
291
|
+
const bandDefs = {
|
|
292
|
+
red: colorBands?.find((band) => band.band === 'red'),
|
|
293
|
+
green: colorBands?.find((band) => band.band === 'green'),
|
|
294
|
+
blue: colorBands?.find((band) => band.band === 'blue'),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const rgbToInstanceFillColorsDataTransform =
|
|
298
|
+
createRgbToColorBufferDataTransform({
|
|
299
|
+
bandDefs,
|
|
300
|
+
attribute: 'instanceFillColors',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const usedSymbols = colorBands ? getUsedSymbols(colorBands) : [];
|
|
304
|
+
const bandTransforms = RASTER_COLOR_BANDS.map((band) =>
|
|
305
|
+
createExprDataTransform({
|
|
306
|
+
colorBand: bandDefs[band],
|
|
307
|
+
rasterMetadata,
|
|
308
|
+
usedSymbols,
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
const combinedDataTransform = combineDataTransforms([
|
|
312
|
+
...bandTransforms,
|
|
313
|
+
rgbToInstanceFillColorsDataTransform,
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
dataTransform: combinedDataTransform as () => any,
|
|
318
|
+
updateTriggers: getRasterTileLayerUpdateTriggers({
|
|
319
|
+
layerConfig,
|
|
320
|
+
visualChannels,
|
|
321
|
+
}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function createBandColorScaleDataTransform({
|
|
326
|
+
bandName,
|
|
327
|
+
scaleFun,
|
|
328
|
+
nodata,
|
|
329
|
+
attribute,
|
|
330
|
+
}: {
|
|
331
|
+
bandName: string;
|
|
332
|
+
scaleFun: (v: number) => number[];
|
|
333
|
+
nodata: number | string | undefined;
|
|
334
|
+
attribute: string;
|
|
335
|
+
}) {
|
|
336
|
+
const hasData = getHasDataPredicate(nodata);
|
|
337
|
+
|
|
338
|
+
return createRasterColumnLayerDataTransform(
|
|
339
|
+
(dataWrapped: RasterColumnLayerData) => {
|
|
340
|
+
const length = dataWrapped.length;
|
|
341
|
+
const bandBuffer = dataWrapped.data.cells.numericProps[bandName].value;
|
|
342
|
+
const colorBuffer = new Uint8Array(length * 4);
|
|
343
|
+
|
|
344
|
+
for (let i = 0; i < length; i++) {
|
|
345
|
+
const rawValue = bandBuffer[i];
|
|
346
|
+
if (!hasData(rawValue)) {
|
|
347
|
+
// skip pixel
|
|
348
|
+
bufferSetRgba(colorBuffer, i * 4, 0, 0, 0, 0);
|
|
349
|
+
} else {
|
|
350
|
+
const colorRgb = scaleFun(rawValue);
|
|
351
|
+
bufferSetRgba(
|
|
352
|
+
colorBuffer,
|
|
353
|
+
i * 4,
|
|
354
|
+
colorRgb[0],
|
|
355
|
+
colorRgb[1],
|
|
356
|
+
colorRgb[2],
|
|
357
|
+
255
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
...dataWrapped,
|
|
363
|
+
attributes: {
|
|
364
|
+
[attribute]: colorBuffer,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function domainFromRasterMetadataBand(
|
|
372
|
+
band: RasterMetadataBand,
|
|
373
|
+
scaleType: ScaleType,
|
|
374
|
+
colorRange: ColorRange
|
|
375
|
+
) {
|
|
376
|
+
if (scaleType === 'ordinal') {
|
|
377
|
+
return colorRange.colorMap?.map(([value]) => value) || [];
|
|
378
|
+
}
|
|
379
|
+
if (scaleType === 'custom') {
|
|
380
|
+
if (colorRange.uiCustomScaleType === 'logarithmic') {
|
|
381
|
+
return getLog10ScaleSteps({
|
|
382
|
+
min: band.stats.min,
|
|
383
|
+
max: band.stats.max,
|
|
384
|
+
steps: colorRange.colors.length,
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
// actually custom, read colorMap
|
|
388
|
+
return colorRange.colorMap?.map(([value]) => value) || [];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const scaleLength = colorRange.colors.length;
|
|
392
|
+
if (scaleType === 'quantile') {
|
|
393
|
+
const quantiles = band.stats.quantiles?.[scaleLength];
|
|
394
|
+
if (!quantiles) {
|
|
395
|
+
return [0, 1];
|
|
396
|
+
}
|
|
397
|
+
return [band.stats.min, ...quantiles, band.stats.max];
|
|
398
|
+
}
|
|
399
|
+
return [band.stats.min, band.stats.max];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get RasterLayerStyle props for ColorRange and UniqueValues modes
|
|
404
|
+
*
|
|
405
|
+
* Effectively, applies selected color scale applied to one band.
|
|
406
|
+
*/
|
|
407
|
+
export function getRasterTileLayerStylePropsScaledBand({
|
|
408
|
+
layerConfig,
|
|
409
|
+
rasterMetadata,
|
|
410
|
+
visualChannels,
|
|
411
|
+
}: {
|
|
412
|
+
layerConfig: MapLayerConfig;
|
|
413
|
+
visualChannels: VisualChannels;
|
|
414
|
+
rasterMetadata: RasterMetadata;
|
|
415
|
+
}) {
|
|
416
|
+
const {visConfig} = layerConfig;
|
|
417
|
+
const {colorField} = visualChannels;
|
|
418
|
+
const {rasterStyleType} = visConfig;
|
|
419
|
+
|
|
420
|
+
const colorRange =
|
|
421
|
+
rasterStyleType === 'ColorRange'
|
|
422
|
+
? visConfig.colorRange
|
|
423
|
+
: visConfig.uniqueValuesColorRange;
|
|
424
|
+
const scaleType =
|
|
425
|
+
rasterStyleType === 'ColorRange' ? visualChannels.colorScale : 'ordinal';
|
|
426
|
+
const bandInfo = rasterMetadata.bands.find(
|
|
427
|
+
(band) => band.name === colorField?.name
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
if (!colorField?.name || !scaleType || !colorRange || !bandInfo) {
|
|
431
|
+
return {};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const domain = domainFromRasterMetadataBand(bandInfo, scaleType, colorRange);
|
|
435
|
+
|
|
436
|
+
const scaleFun = createColorScale(
|
|
437
|
+
scaleType,
|
|
438
|
+
domain,
|
|
439
|
+
colorRange.colors.map(hexToRGB),
|
|
440
|
+
UNKNOWN_COLOR
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const bandColorScaleDataTransform = createBandColorScaleDataTransform({
|
|
444
|
+
bandName: bandInfo.name,
|
|
445
|
+
scaleFun,
|
|
446
|
+
nodata: bandInfo?.nodata ?? rasterMetadata.nodata,
|
|
447
|
+
attribute: 'instanceFillColors',
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
dataTransform: bandColorScaleDataTransform as () => any,
|
|
452
|
+
updateTriggers: getRasterTileLayerUpdateTriggers({
|
|
453
|
+
layerConfig,
|
|
454
|
+
visualChannels,
|
|
455
|
+
}),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function getRasterTileLayerStyleProps({
|
|
460
|
+
layerConfig,
|
|
461
|
+
visualChannels,
|
|
462
|
+
rasterMetadata,
|
|
463
|
+
}: {
|
|
464
|
+
layerConfig: MapLayerConfig;
|
|
465
|
+
visualChannels: VisualChannels;
|
|
466
|
+
rasterMetadata: RasterMetadata;
|
|
467
|
+
}) {
|
|
468
|
+
const {visConfig} = layerConfig;
|
|
469
|
+
const {rasterStyleType} = visConfig;
|
|
470
|
+
|
|
471
|
+
if (rasterStyleType === 'Rgb') {
|
|
472
|
+
return getRasterTileLayerStylePropsRgb({
|
|
473
|
+
layerConfig,
|
|
474
|
+
rasterMetadata,
|
|
475
|
+
visualChannels,
|
|
476
|
+
});
|
|
477
|
+
} else {
|
|
478
|
+
return getRasterTileLayerStylePropsScaledBand({
|
|
479
|
+
layerConfig,
|
|
480
|
+
rasterMetadata,
|
|
481
|
+
visualChannels,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function getRasterTileLayerUpdateTriggers({
|
|
487
|
+
layerConfig,
|
|
488
|
+
visualChannels,
|
|
489
|
+
}: {
|
|
490
|
+
layerConfig: MapLayerConfig;
|
|
491
|
+
visualChannels: VisualChannels;
|
|
492
|
+
}) {
|
|
493
|
+
const {visConfig} = layerConfig;
|
|
494
|
+
const {rasterStyleType} = visConfig;
|
|
495
|
+
const getFillColorUpdateTriggers: Record<string, unknown> = {
|
|
496
|
+
rasterStyleType,
|
|
497
|
+
};
|
|
498
|
+
if (rasterStyleType === 'ColorRange') {
|
|
499
|
+
getFillColorUpdateTriggers.colorRange = visConfig.colorRange?.colors;
|
|
500
|
+
getFillColorUpdateTriggers.colorMap = visConfig.colorRange?.colorMap;
|
|
501
|
+
getFillColorUpdateTriggers.colorScale = visualChannels.colorScale;
|
|
502
|
+
getFillColorUpdateTriggers.colorFieldId = visualChannels.colorField?.name;
|
|
503
|
+
} else if (rasterStyleType === 'UniqueValues') {
|
|
504
|
+
getFillColorUpdateTriggers.colorMap =
|
|
505
|
+
visConfig.uniqueValuesColorRange?.colorMap;
|
|
506
|
+
getFillColorUpdateTriggers.colorFieldId = visualChannels.colorField?.name;
|
|
507
|
+
} else if (rasterStyleType === 'Rgb') {
|
|
508
|
+
getFillColorUpdateTriggers.colorBands = visConfig.colorBands;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
getFillColor: getFillColorUpdateTriggers,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function bufferSetRgba(
|
|
517
|
+
target: VecExprVecLike,
|
|
518
|
+
index: number,
|
|
519
|
+
r: number,
|
|
520
|
+
g: number,
|
|
521
|
+
b: number,
|
|
522
|
+
a: number
|
|
523
|
+
) {
|
|
524
|
+
target[index + 0] = r;
|
|
525
|
+
target[index + 1] = g;
|
|
526
|
+
target[index + 2] = b;
|
|
527
|
+
target[index + 3] = a;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function hexToRGB(hexColor: string): [number, number, number] {
|
|
531
|
+
const r = parseInt(hexColor.slice(1, 3), 16);
|
|
532
|
+
const g = parseInt(hexColor.slice(3, 5), 16);
|
|
533
|
+
const b = parseInt(hexColor.slice(5, 7), 16);
|
|
534
|
+
|
|
535
|
+
return [r, g, b];
|
|
536
|
+
}
|
package/src/fetch-map/types.ts
CHANGED
|
@@ -39,6 +39,7 @@ export type ColorRange = {
|
|
|
39
39
|
colorMap: string[][] | undefined;
|
|
40
40
|
name: string;
|
|
41
41
|
type: string;
|
|
42
|
+
uiCustomScaleType?: 'logarithmic';
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export type CustomMarkersRange = {
|
|
@@ -49,12 +50,20 @@ export type CustomMarkersRange = {
|
|
|
49
50
|
othersMarker?: string;
|
|
50
51
|
};
|
|
51
52
|
|
|
53
|
+
export type ColorBand = 'red' | 'green' | 'blue' | 'alpha';
|
|
54
|
+
|
|
55
|
+
export type RasterLayerConfigColorBand = {
|
|
56
|
+
band: ColorBand;
|
|
57
|
+
type: 'none' | 'band' | 'expression';
|
|
58
|
+
value: string; // band name or expression
|
|
59
|
+
};
|
|
60
|
+
|
|
52
61
|
export type VisConfig = {
|
|
53
62
|
filled?: boolean;
|
|
54
63
|
opacity?: number;
|
|
55
64
|
enable3d?: boolean;
|
|
56
65
|
|
|
57
|
-
colorAggregation?:
|
|
66
|
+
colorAggregation?: string;
|
|
58
67
|
colorRange: ColorRange;
|
|
59
68
|
|
|
60
69
|
customMarkers?: boolean;
|
|
@@ -64,21 +73,26 @@ export type VisConfig = {
|
|
|
64
73
|
radius: number;
|
|
65
74
|
radiusRange?: number[];
|
|
66
75
|
|
|
67
|
-
sizeAggregation?:
|
|
68
|
-
sizeRange?:
|
|
76
|
+
sizeAggregation?: string;
|
|
77
|
+
sizeRange?: number[];
|
|
69
78
|
|
|
70
|
-
strokeColorAggregation?:
|
|
79
|
+
strokeColorAggregation?: string;
|
|
71
80
|
strokeOpacity?: number;
|
|
72
81
|
strokeColorRange?: ColorRange;
|
|
73
82
|
|
|
74
|
-
heightRange?:
|
|
75
|
-
heightAggregation?:
|
|
83
|
+
heightRange?: number[];
|
|
84
|
+
heightAggregation?: string;
|
|
76
85
|
|
|
77
|
-
weightAggregation?:
|
|
86
|
+
weightAggregation?: string;
|
|
78
87
|
|
|
79
88
|
// type = clusterTile
|
|
80
89
|
clusterLevel?: number;
|
|
81
90
|
isTextVisible?: boolean;
|
|
91
|
+
|
|
92
|
+
rasterStyleType?: 'Rgb' | 'ColorRange' | 'UniqueValues';
|
|
93
|
+
colorBands?: RasterLayerConfigColorBand[];
|
|
94
|
+
|
|
95
|
+
uniqueValuesColorRange?: ColorRange;
|
|
82
96
|
};
|
|
83
97
|
|
|
84
98
|
export type TextLabel = {
|
package/src/fetch-map/utils.ts
CHANGED
|
@@ -85,3 +85,59 @@ export function formatDate(value: string | number | Date): string {
|
|
|
85
85
|
export function formatTimestamp(value: string | number | Date): string {
|
|
86
86
|
return String(Math.floor(new Date(value).getTime() / 1000));
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
function roundedPow10(exp: number) {
|
|
90
|
+
// Math.pow(10, less than 4) generates "0.0...009999999999999999" instead of "0.0...01"
|
|
91
|
+
// round it ...
|
|
92
|
+
const raw = Math.pow(10, exp);
|
|
93
|
+
if (exp < 0) {
|
|
94
|
+
const shift = Math.pow(10, -exp);
|
|
95
|
+
return Math.round(raw * shift) / shift;
|
|
96
|
+
}
|
|
97
|
+
return raw;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create domain for D3 threshold scale with logarithmic steps.
|
|
102
|
+
*
|
|
103
|
+
* If min is 0, it starts with max and goes down to fill color scale.
|
|
104
|
+
* If max is Infinity, it starts with 10 and goes up to fill color scale.
|
|
105
|
+
* Othersise it starts on first power of 10 that is greater than min.
|
|
106
|
+
*
|
|
107
|
+
* Generates `steps-1` entries, as this is what d3 threshold scale expects
|
|
108
|
+
*
|
|
109
|
+
* @see https://d3js.org/d3-scale/threshold
|
|
110
|
+
*/
|
|
111
|
+
export function getLog10ScaleSteps({
|
|
112
|
+
min,
|
|
113
|
+
max,
|
|
114
|
+
steps,
|
|
115
|
+
}: {
|
|
116
|
+
min: number;
|
|
117
|
+
max: number;
|
|
118
|
+
steps: number;
|
|
119
|
+
}): number[] {
|
|
120
|
+
if (min === 0) {
|
|
121
|
+
if (max === Infinity) {
|
|
122
|
+
// count aggregations have [0, Infinity]
|
|
123
|
+
// that will yield [10, 100, 1000, ...]
|
|
124
|
+
return [...Array(steps - 1)].map((_v, i) => roundedPow10(i + 1));
|
|
125
|
+
}
|
|
126
|
+
// if stats.min = 0, we only can attempt to start from max and decrease powers until
|
|
127
|
+
// we use all color buckets ...
|
|
128
|
+
const maxLog = Math.log10(max);
|
|
129
|
+
const endExponent = Math.ceil(maxLog);
|
|
130
|
+
const startExponent = endExponent - steps + 1;
|
|
131
|
+
return [...Array(steps - 1)].map((_v, i) =>
|
|
132
|
+
roundedPow10(startExponent + i)
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
const minLog = Math.log10(min);
|
|
136
|
+
const startExponent =
|
|
137
|
+
Math.ceil(minLog) === minLog ? minLog + 1 : Math.ceil(minLog);
|
|
138
|
+
|
|
139
|
+
return [...Array(steps - 1)].map((_v, i) =>
|
|
140
|
+
roundedPow10(startExponent + i)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|