@carto/api-client 0.5.7-alpha.3 → 0.5.7-alpha.6
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/build/api-client.cjs +82 -40
- package/build/api-client.cjs.map +1 -1
- package/build/api-client.d.cts +22 -14
- package/build/api-client.d.ts +22 -14
- package/build/api-client.js +81 -36
- package/build/api-client.js.map +1 -1
- package/build/worker-compat.js +3458 -2308
- package/build/worker-compat.js.map +1 -1
- package/build/worker.js +22 -32
- package/build/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/filters/tileFeaturesRaster.ts +25 -10
- package/src/index.ts +1 -0
- package/src/operations/groupBy.ts +6 -24
- package/src/utils/CellSet.ts +90 -0
- package/src/widget-sources/index.ts +0 -1
- package/src/widget-sources/types.ts +1 -3
- package/src/widget-sources/widget-remote-source.ts +1 -3
- package/src/widget-sources/widget-tileset-source-impl.ts +0 -2
- package/src/widget-sources/constants.ts +0 -6
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"homepage": "https://github.com/CartoDB/carto-api-client#readme",
|
|
9
9
|
"author": "Don McCurdy <donmccurdy@carto.com>",
|
|
10
10
|
"packageManager": "yarn@4.3.1",
|
|
11
|
-
"version": "0.5.7-alpha.
|
|
11
|
+
"version": "0.5.7-alpha.6",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"access": "public"
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"typescript": "~5.8.2",
|
|
127
127
|
"typescript-eslint": "^8.26.1",
|
|
128
128
|
"vite": "^6.2.2",
|
|
129
|
-
"vitest": "3.1.
|
|
129
|
+
"vitest": "3.1.4"
|
|
130
130
|
},
|
|
131
131
|
"resolutions": {
|
|
132
132
|
"@carto/api-client": "portal:./",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cellToChildren as _cellToChildren,
|
|
3
|
+
cellToBoundary,
|
|
3
4
|
cellToTile,
|
|
4
5
|
geometryToCells,
|
|
5
6
|
getResolution,
|
|
@@ -11,6 +12,9 @@ import type {
|
|
|
11
12
|
RasterMetadataBand,
|
|
12
13
|
SpatialDataType,
|
|
13
14
|
} from '../sources/types.js';
|
|
15
|
+
import booleanWithin from '@turf/boolean-within';
|
|
16
|
+
import intersect from '@turf/intersect';
|
|
17
|
+
import {feature, featureCollection} from '@turf/helpers';
|
|
14
18
|
|
|
15
19
|
export type TileFeaturesRasterOptions = {
|
|
16
20
|
tiles: RasterTile[];
|
|
@@ -43,23 +47,34 @@ export function tileFeaturesRaster({
|
|
|
43
47
|
const tileBlockSize = tiles[0].data!.blockSize;
|
|
44
48
|
const cellResolution = tileResolution + BigInt(Math.log2(tileBlockSize));
|
|
45
49
|
|
|
46
|
-
// Compute covering cells for the spatial filter, at same resolution as the
|
|
47
|
-
// raster pixels, to be used as a mask.
|
|
48
|
-
const spatialFilterCells = new Set(
|
|
49
|
-
geometryToCells(options.spatialFilter, cellResolution)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
50
|
const data = new Map<bigint, FeatureData>();
|
|
53
51
|
|
|
54
52
|
for (const tile of tiles as Required<RasterTile>[]) {
|
|
55
53
|
const parent = tile.index.q;
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
// If tile is partially overlapping with the spatial filter, compute a quadbin covering for the
|
|
56
|
+
// spatial filter + tile intersection, at cell resolution. If tile is fully inside or outside
|
|
57
|
+
// the spatial filter, computing a covering can be skipped. Avoid computing a quadbin covering
|
|
58
|
+
// for the _entire_ spatial filter, which may contain far too many cells (e.g. at zoom=3 and
|
|
59
|
+
// resolution=14, we have ~18,000,000 cells in the viewport.)
|
|
60
|
+
const tilePolygon = cellToBoundary(parent);
|
|
61
|
+
const tileFilter = intersect(
|
|
62
|
+
featureCollection([feature(tilePolygon), feature(options.spatialFilter)])
|
|
63
|
+
);
|
|
64
|
+
const needsFilter = tileFilter
|
|
65
|
+
? !booleanWithin(tilePolygon, options.spatialFilter)
|
|
66
|
+
: false;
|
|
67
|
+
const tileFilterCells = needsFilter
|
|
68
|
+
? new Set(geometryToCells(tileFilter!.geometry, cellResolution))
|
|
69
|
+
: null;
|
|
70
|
+
const tileSortedCells = cellToChildrenSorted(parent, cellResolution);
|
|
58
71
|
|
|
59
72
|
// For each pixel/cell within the spatial filter, create a FeatureData.
|
|
60
73
|
// Order is row-major, starting from NW and ending at SE.
|
|
61
|
-
for (let i = 0; i <
|
|
62
|
-
if (!
|
|
74
|
+
for (let i = 0; i < tileSortedCells.length; i++) {
|
|
75
|
+
if (needsFilter && !tileFilterCells!.has(tileSortedCells[i])) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
63
78
|
|
|
64
79
|
const cellData: FeatureData = {};
|
|
65
80
|
let cellDataExists = false;
|
|
@@ -75,7 +90,7 @@ export function tileFeaturesRaster({
|
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
if (cellDataExists) {
|
|
78
|
-
data.set(
|
|
93
|
+
data.set(tileSortedCells[i], cellData);
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {aggregationFunctions, aggregate} from './aggregation.js';
|
|
2
|
-
import {OTHERS_CATEGORY_NAME} from '../widget-sources/constants.js';
|
|
3
2
|
import type {AggregationType} from '../types.js';
|
|
4
3
|
import type {FeatureData} from '../types-internal.js';
|
|
5
4
|
|
|
@@ -16,14 +15,12 @@ export function groupValuesByColumn({
|
|
|
16
15
|
joinOperation,
|
|
17
16
|
keysColumn,
|
|
18
17
|
operation,
|
|
19
|
-
othersThreshold,
|
|
20
18
|
}: {
|
|
21
19
|
data: FeatureData[];
|
|
22
20
|
valuesColumns?: string[];
|
|
23
21
|
joinOperation?: AggregationType;
|
|
24
22
|
keysColumn: string;
|
|
25
23
|
operation: AggregationType;
|
|
26
|
-
othersThreshold?: number;
|
|
27
24
|
}): GroupByFeature | null {
|
|
28
25
|
if (Array.isArray(data) && data.length === 0) {
|
|
29
26
|
return null;
|
|
@@ -51,27 +48,12 @@ export function groupValuesByColumn({
|
|
|
51
48
|
const targetOperation =
|
|
52
49
|
aggregationFunctions[operation as Exclude<AggregationType, 'custom'>];
|
|
53
50
|
|
|
54
|
-
if (
|
|
55
|
-
return []
|
|
51
|
+
if (targetOperation) {
|
|
52
|
+
return Array.from(groups).map(([name, value]) => ({
|
|
53
|
+
name,
|
|
54
|
+
value: targetOperation(value),
|
|
55
|
+
}));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
name,
|
|
60
|
-
value: targetOperation(value),
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
allCategories.sort((a, b) => b.value - a.value);
|
|
64
|
-
|
|
65
|
-
if (othersThreshold && allCategories.length > othersThreshold) {
|
|
66
|
-
const otherNames = allCategories
|
|
67
|
-
.map((entry) => entry.name)
|
|
68
|
-
.slice(othersThreshold);
|
|
69
|
-
const otherValue = otherNames.flatMap((name) => groups.get(name));
|
|
70
|
-
allCategories.push({
|
|
71
|
-
name: OTHERS_CATEGORY_NAME,
|
|
72
|
-
value: targetOperation(otherValue),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return allCategories;
|
|
58
|
+
return [];
|
|
77
59
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Flags 'empty' values in a Uint32Array index. */
|
|
2
|
+
const EMPTY_U32 = 2 ** 32 - 1;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom Set-like interface optimized for BigUint64 cell IDs. Unlike Set,
|
|
6
|
+
* limited in most JavaScript runtimes to ~16M entries, this implementation
|
|
7
|
+
* can support up to `n = 2^32 - 1` (4 billion) entries, with lookups in
|
|
8
|
+
* amortized O(1) time.
|
|
9
|
+
*/
|
|
10
|
+
export class CellSet {
|
|
11
|
+
/** List of cells stored by the set. Stored by reference, without copying. */
|
|
12
|
+
private cells: bigint[];
|
|
13
|
+
|
|
14
|
+
/** DataView representing a single cell ID. Pre-allocated to reduce memory during queries. */
|
|
15
|
+
private cellView = new DataView(new ArrayBuffer(8));
|
|
16
|
+
|
|
17
|
+
/** Hash table, mapping a hash index (computed) to an index in the 'cells' array. */
|
|
18
|
+
private hashTable: Uint32Array;
|
|
19
|
+
|
|
20
|
+
constructor(cells: bigint[]) {
|
|
21
|
+
this.cells = cells;
|
|
22
|
+
|
|
23
|
+
// Pre-allocate hash table for queries.
|
|
24
|
+
this.hashTable = new Uint32Array(hashBuckets(cells.length)).fill(EMPTY_U32);
|
|
25
|
+
for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
|
|
26
|
+
this.hashTable[this.hashLookup(cells[cellIndex])] = cellIndex;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
has(cell: bigint): boolean {
|
|
31
|
+
const hashIndex = this.hashLookup(cell);
|
|
32
|
+
return this.hashTable[hashIndex] !== EMPTY_U32;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private hashLookup(cell: bigint): number {
|
|
36
|
+
// Hash implementation operates on 32-bit chunks, so write the cell ID
|
|
37
|
+
// into a pre-allocated DataView for easier iteration.
|
|
38
|
+
this.cellView.setBigUint64(0, cell);
|
|
39
|
+
const hashval = hash(this.cellView);
|
|
40
|
+
const hashmod = this.hashTable.length - 1;
|
|
41
|
+
let bucket = hashval & hashmod;
|
|
42
|
+
|
|
43
|
+
// Find the first bucket in the hash table where either (a) no cell
|
|
44
|
+
// is yet stored, or (b) the stored cell and the query cell are equal.
|
|
45
|
+
for (let probe = 0; probe <= hashmod; probe++) {
|
|
46
|
+
const cellIndex = this.hashTable[bucket];
|
|
47
|
+
|
|
48
|
+
if (cellIndex === EMPTY_U32 || cell === this.cells[cellIndex]) {
|
|
49
|
+
return bucket;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
bucket = (bucket + probe + 1) & hashmod; // Hash collision; quadratic probing.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error('Hash table full.'); // Unreachable.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* MurmurHash2
|
|
61
|
+
*
|
|
62
|
+
* References:
|
|
63
|
+
* - https://github.com/mikolalysenko/murmurhash-js/blob/f19136e9f9c17f8cddc216ca3d44ec7c5c502f60/murmurhash2_gc.js#L14
|
|
64
|
+
* - https://github.com/zeux/meshoptimizer/blob/e47e1be6d3d9513153188216455bdbed40a206ef/src/indexgenerator.cpp#L12
|
|
65
|
+
*/
|
|
66
|
+
function hash(view: DataView, h = 0): number {
|
|
67
|
+
const m = 0x5bd1e995;
|
|
68
|
+
const r = 24;
|
|
69
|
+
|
|
70
|
+
for (let i = 0, il = view.byteLength / 4; i < il; i++) {
|
|
71
|
+
let k = view.getUint32(i * 4);
|
|
72
|
+
|
|
73
|
+
k = Math.imul(k, m) >>> 0;
|
|
74
|
+
k = (k ^ (k >> r)) >>> 0;
|
|
75
|
+
k = Math.imul(k, m) >>> 0;
|
|
76
|
+
|
|
77
|
+
h = Math.imul(h, m) >>> 0;
|
|
78
|
+
h = (h ^ k) >>> 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return h;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hashBuckets(initialCount: number) {
|
|
85
|
+
let buckets = 1;
|
|
86
|
+
while (buckets < initialCount + initialCount / 4) {
|
|
87
|
+
buckets *= 2;
|
|
88
|
+
}
|
|
89
|
+
return buckets;
|
|
90
|
+
}
|
|
@@ -59,8 +59,6 @@ export interface CategoryRequestOptions extends BaseRequestOptions {
|
|
|
59
59
|
operationColumn?: string;
|
|
60
60
|
/** Local only. */
|
|
61
61
|
joinOperation?: 'count' | 'avg' | 'min' | 'max' | 'sum';
|
|
62
|
-
/** Calculate `_carto_others` category for all categories after first N (N is threshold). */
|
|
63
|
-
othersThreshold?: number;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
/**
|
|
@@ -208,7 +206,7 @@ export type FeaturesResponse = {rows: Record<string, unknown>[]};
|
|
|
208
206
|
export type FormulaResponse = {value: number | null};
|
|
209
207
|
|
|
210
208
|
/** Response from {@link WidgetRemoteSource#getCategories}. */
|
|
211
|
-
export type CategoryResponse = {name: string
|
|
209
|
+
export type CategoryResponse = {name: string; value: number}[];
|
|
212
210
|
|
|
213
211
|
/** Response from {@link WidgetRemoteSource#getRange}. */
|
|
214
212
|
export type RangeResponse = {min: number; max: number} | null;
|
|
@@ -74,8 +74,7 @@ export abstract class WidgetRemoteSource<
|
|
|
74
74
|
spatialFiltersMode,
|
|
75
75
|
...params
|
|
76
76
|
} = options;
|
|
77
|
-
const {column, operation, operationColumn, operationExp
|
|
78
|
-
params;
|
|
77
|
+
const {column, operation, operationColumn, operationExp} = params;
|
|
79
78
|
|
|
80
79
|
if (operation === AggregationTypes.Custom) {
|
|
81
80
|
assert(operationExp, 'operationExp is required for custom operation');
|
|
@@ -95,7 +94,6 @@ export abstract class WidgetRemoteSource<
|
|
|
95
94
|
operation,
|
|
96
95
|
operationExp,
|
|
97
96
|
operationColumn: operationColumn || column,
|
|
98
|
-
othersThreshold,
|
|
99
97
|
},
|
|
100
98
|
opts: {signal, headers: this.props.headers},
|
|
101
99
|
}).then((res: CategoriesModelResponse) => normalizeObjectKeys(res.rows));
|
|
@@ -190,7 +190,6 @@ export class WidgetTilesetSourceImpl extends WidgetSource<WidgetTilesetSourcePro
|
|
|
190
190
|
filters,
|
|
191
191
|
filterOwner,
|
|
192
192
|
spatialFilter,
|
|
193
|
-
othersThreshold,
|
|
194
193
|
}: CategoryRequestOptions): Promise<CategoryResponse> {
|
|
195
194
|
const filteredFeatures = this._getFilteredFeatures(
|
|
196
195
|
spatialFilter,
|
|
@@ -210,7 +209,6 @@ export class WidgetTilesetSourceImpl extends WidgetSource<WidgetTilesetSourcePro
|
|
|
210
209
|
joinOperation,
|
|
211
210
|
keysColumn: column,
|
|
212
211
|
operation,
|
|
213
|
-
othersThreshold,
|
|
214
212
|
});
|
|
215
213
|
|
|
216
214
|
return groups || [];
|