@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.
@@ -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
+ }
@@ -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?: any;
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?: any;
68
- sizeRange?: any;
76
+ sizeAggregation?: string;
77
+ sizeRange?: number[];
69
78
 
70
- strokeColorAggregation?: any;
79
+ strokeColorAggregation?: string;
71
80
  strokeOpacity?: number;
72
81
  strokeColorRange?: ColorRange;
73
82
 
74
- heightRange?: any;
75
- heightAggregation?: any;
83
+ heightRange?: number[];
84
+ heightAggregation?: string;
76
85
 
77
- weightAggregation?: any;
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 = {
@@ -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
+ }