@deck.gl-community/infovis-layers 9.3.5 → 9.3.7
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/dist/index.cjs +2998 -114
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/layers/animation-layer/animation-layer.d.ts +30 -0
- package/dist/layers/animation-layer/animation-layer.d.ts.map +1 -0
- package/dist/layers/animation-layer/animation-layer.js +53 -0
- package/dist/layers/animation-layer/animation-layer.js.map +1 -0
- package/dist/layers/animation-layer/animation.d.ts +86 -0
- package/dist/layers/animation-layer/animation.d.ts.map +1 -0
- package/dist/layers/animation-layer/animation.js +214 -0
- package/dist/layers/animation-layer/animation.js.map +1 -0
- package/dist/layers/block-layer/block-layer-fragment.glsl.d.ts +3 -0
- package/dist/layers/block-layer/block-layer-fragment.glsl.d.ts.map +1 -0
- package/dist/layers/block-layer/block-layer-fragment.glsl.js +41 -0
- package/dist/layers/block-layer/block-layer-fragment.glsl.js.map +1 -0
- package/dist/layers/block-layer/block-layer-uniforms.d.ts +27 -0
- package/dist/layers/block-layer/block-layer-uniforms.d.ts.map +1 -0
- package/dist/layers/block-layer/block-layer-uniforms.js +26 -0
- package/dist/layers/block-layer/block-layer-uniforms.js.map +1 -0
- package/dist/layers/block-layer/block-layer-vertex.glsl.d.ts +3 -0
- package/dist/layers/block-layer/block-layer-vertex.glsl.d.ts.map +1 -0
- package/dist/layers/block-layer/block-layer-vertex.glsl.js +67 -0
- package/dist/layers/block-layer/block-layer-vertex.glsl.js.map +1 -0
- package/dist/layers/block-layer/block-layer.d.ts +71 -0
- package/dist/layers/block-layer/block-layer.d.ts.map +1 -0
- package/dist/layers/block-layer/block-layer.js +108 -0
- package/dist/layers/block-layer/block-layer.js.map +1 -0
- package/dist/layers/fast-text-layer/fast-text-layer.d.ts +148 -0
- package/dist/layers/fast-text-layer/fast-text-layer.d.ts.map +1 -0
- package/dist/layers/fast-text-layer/fast-text-layer.js +862 -0
- package/dist/layers/fast-text-layer/fast-text-layer.js.map +1 -0
- package/dist/layers/fast-text-layer/fast-text-layer.test.d.ts +2 -0
- package/dist/layers/fast-text-layer/fast-text-layer.test.d.ts.map +1 -0
- package/dist/layers/fast-text-layer/fast-text-layer.test.js +300 -0
- package/dist/layers/fast-text-layer/fast-text-layer.test.js.map +1 -0
- package/dist/layers/fast-text-layer/fast-text-layout.d.ts +179 -0
- package/dist/layers/fast-text-layer/fast-text-layout.d.ts.map +1 -0
- package/dist/layers/fast-text-layer/fast-text-layout.js +665 -0
- package/dist/layers/fast-text-layer/fast-text-layout.js.map +1 -0
- package/dist/layers/fast-text-layer/font-atlas.d.ts +98 -0
- package/dist/layers/fast-text-layer/font-atlas.d.ts.map +1 -0
- package/dist/layers/fast-text-layer/font-atlas.js +337 -0
- package/dist/layers/fast-text-layer/font-atlas.js.map +1 -0
- package/dist/layers/fast-text-layer/index.d.ts +4 -0
- package/dist/layers/fast-text-layer/index.d.ts.map +1 -0
- package/dist/layers/fast-text-layer/index.js +4 -0
- package/dist/layers/fast-text-layer/index.js.map +1 -0
- package/dist/layers/time-delta-layer.d.ts +22 -60
- package/dist/layers/time-delta-layer.d.ts.map +1 -1
- package/dist/layers/time-delta-layer.js +23 -45
- package/dist/layers/time-delta-layer.js.map +1 -1
- package/dist/synchronized-views/synchronized-views.js +1 -1
- package/dist/synchronized-views/synchronized-views.js.map +1 -1
- package/dist/utils/format-utils.d.ts +27 -3
- package/dist/utils/format-utils.d.ts.map +1 -1
- package/dist/utils/format-utils.js +72 -40
- package/dist/utils/format-utils.js.map +1 -1
- package/dist/utils/utf8-string-view.d.ts +53 -0
- package/dist/utils/utf8-string-view.d.ts.map +1 -0
- package/dist/utils/utf8-string-view.js +169 -0
- package/dist/utils/utf8-string-view.js.map +1 -0
- package/dist/utils/utf8-string-view.test.d.ts +2 -0
- package/dist/utils/utf8-string-view.test.d.ts.map +1 -0
- package/dist/utils/utf8-string-view.test.js +88 -0
- package/dist/utils/utf8-string-view.test.js.map +1 -0
- package/dist/views/bounds-utils.d.ts +23 -0
- package/dist/views/bounds-utils.d.ts.map +1 -0
- package/dist/views/bounds-utils.js +65 -0
- package/dist/views/bounds-utils.js.map +1 -0
- package/dist/views/layer-bounds-filter.d.ts +48 -0
- package/dist/views/layer-bounds-filter.d.ts.map +1 -0
- package/dist/views/layer-bounds-filter.js +99 -0
- package/dist/views/layer-bounds-filter.js.map +1 -0
- package/dist/views/orthographic-utils.d.ts +30 -2
- package/dist/views/orthographic-utils.d.ts.map +1 -1
- package/dist/views/orthographic-utils.js +33 -14
- package/dist/views/orthographic-utils.js.map +1 -1
- package/dist/views/view-layout/build-views-from-view-layout.d.ts +37 -0
- package/dist/views/view-layout/build-views-from-view-layout.d.ts.map +1 -0
- package/dist/views/view-layout/build-views-from-view-layout.js +413 -0
- package/dist/views/view-layout/build-views-from-view-layout.js.map +1 -0
- package/dist/views/view-layout/index.d.ts +3 -0
- package/dist/views/view-layout/index.d.ts.map +1 -0
- package/dist/views/view-layout/index.js +3 -0
- package/dist/views/view-layout/index.js.map +1 -0
- package/dist/views/view-layout/view-layout-item.d.ts +74 -0
- package/dist/views/view-layout/view-layout-item.d.ts.map +1 -0
- package/dist/views/view-layout/view-layout-item.js +48 -0
- package/dist/views/view-layout/view-layout-item.js.map +1 -0
- package/dist/views/view-layout/view-layout.test.d.ts +2 -0
- package/dist/views/view-layout/view-layout.test.d.ts.map +1 -0
- package/dist/views/view-layout/view-layout.test.js +112 -0
- package/dist/views/view-layout/view-layout.test.js.map +1 -0
- package/dist/views/viewport-bounds-utils.d.ts +55 -0
- package/dist/views/viewport-bounds-utils.d.ts.map +1 -0
- package/dist/views/viewport-bounds-utils.js +70 -0
- package/dist/views/viewport-bounds-utils.js.map +1 -0
- package/package.json +11 -4
- package/src/index.ts +91 -2
- package/src/layers/animation-layer/animation-layer.ts +92 -0
- package/src/layers/animation-layer/animation.ts +328 -0
- package/src/layers/block-layer/block-layer-fragment.glsl.ts +41 -0
- package/src/layers/block-layer/block-layer-uniforms.ts +43 -0
- package/src/layers/block-layer/block-layer-vertex.glsl.ts +67 -0
- package/src/layers/block-layer/block-layer.ts +199 -0
- package/src/layers/fast-text-layer/fast-text-layer.test.ts +359 -0
- package/src/layers/fast-text-layer/fast-text-layer.ts +1208 -0
- package/src/layers/fast-text-layer/fast-text-layout.ts +1124 -0
- package/src/layers/fast-text-layer/font-atlas.ts +574 -0
- package/src/layers/fast-text-layer/index.ts +37 -0
- package/src/layers/time-delta-layer.ts +47 -62
- package/src/synchronized-views/synchronized-views.ts +1 -1
- package/src/utils/format-utils.ts +90 -40
- package/src/utils/utf8-string-view.test.ts +119 -0
- package/src/utils/utf8-string-view.ts +273 -0
- package/src/views/bounds-utils.ts +88 -0
- package/src/views/layer-bounds-filter.ts +160 -0
- package/src/views/orthographic-utils.ts +65 -18
- package/src/views/view-layout/build-views-from-view-layout.ts +547 -0
- package/src/views/view-layout/index.ts +18 -0
- package/src/views/view-layout/view-layout-item.ts +125 -0
- package/src/views/view-layout/view-layout.test.ts +129 -0
- package/src/views/viewport-bounds-utils.ts +104 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type * as arrow from 'apache-arrow';
|
|
2
|
+
|
|
3
|
+
/** Byte-range view for one UTF-8 encoded string. */
|
|
4
|
+
export type Utf8StringView = {
|
|
5
|
+
/** Backing UTF-8 bytes. */
|
|
6
|
+
data: Uint8Array;
|
|
7
|
+
/** Inclusive byte offset where the string starts. */
|
|
8
|
+
start: number;
|
|
9
|
+
/** Exclusive byte offset where the string ends. */
|
|
10
|
+
end: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Normalized direct-buffer source for one Arrow Utf8 vector. */
|
|
14
|
+
export type Utf8ColumnSource = {
|
|
15
|
+
/** Number of logical rows in the column. */
|
|
16
|
+
readonly rowCount: number;
|
|
17
|
+
/** Direct-buffer chunks that make up the column. */
|
|
18
|
+
readonly chunks: readonly Utf8ColumnSourceChunk[];
|
|
19
|
+
/** Returns whether one logical row is valid. */
|
|
20
|
+
readonly isValid: (rowIndex: number) => boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** One direct-buffer chunk in a normalized Arrow Utf8 column source. */
|
|
24
|
+
export type Utf8ColumnSourceChunk = {
|
|
25
|
+
/** Logical row index where this chunk starts. */
|
|
26
|
+
readonly rowOffset: number;
|
|
27
|
+
/** Number of logical rows in this chunk. */
|
|
28
|
+
readonly rowCount: number;
|
|
29
|
+
/** Row offset inside the chunk's value-offset buffer. */
|
|
30
|
+
readonly valueOffsetIndex: number;
|
|
31
|
+
/** UTF-8 value-offset buffer. */
|
|
32
|
+
readonly valueOffsets: Int32Array | Uint32Array;
|
|
33
|
+
/** Contiguous UTF-8 value bytes. */
|
|
34
|
+
readonly values: Uint8Array;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const textEncoder = new TextEncoder();
|
|
38
|
+
const textDecoder = new TextDecoder();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Encodes one JavaScript string into a reusable UTF-8 byte view.
|
|
42
|
+
*/
|
|
43
|
+
export function makeUtf8StringView(value: string): Utf8StringView {
|
|
44
|
+
const data = textEncoder.encode(value);
|
|
45
|
+
return {data, start: 0, end: data.length};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Builds a direct-buffer source from an Arrow Utf8 vector without copying UTF-8 bytes.
|
|
50
|
+
*/
|
|
51
|
+
export function getArrowUtf8ColumnSource(
|
|
52
|
+
utf8Column: arrow.Vector<arrow.Utf8>
|
|
53
|
+
): Utf8ColumnSource | null {
|
|
54
|
+
const chunks = getArrowUtf8DataChunks(utf8Column);
|
|
55
|
+
if (!chunks || chunks.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sourceChunks: Utf8ColumnSourceChunk[] = [];
|
|
60
|
+
let rowOffset = 0;
|
|
61
|
+
for (const chunk of chunks) {
|
|
62
|
+
const valueOffsets = chunk.valueOffsets;
|
|
63
|
+
const values = chunk.values;
|
|
64
|
+
if (!valueOffsets || !values) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
sourceChunks.push({
|
|
68
|
+
rowOffset,
|
|
69
|
+
rowCount: chunk.length,
|
|
70
|
+
valueOffsetIndex: 0,
|
|
71
|
+
valueOffsets,
|
|
72
|
+
values
|
|
73
|
+
});
|
|
74
|
+
rowOffset += chunk.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
rowCount: utf8Column.length,
|
|
79
|
+
chunks: sourceChunks,
|
|
80
|
+
isValid: rowIndex => utf8Column.isValid(rowIndex)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fills a reusable UTF-8 view for one Arrow Utf8 row without decoding the row to a string.
|
|
86
|
+
*/
|
|
87
|
+
export function getArrowUtf8RowView(
|
|
88
|
+
utf8Column: arrow.Vector<arrow.Utf8>,
|
|
89
|
+
rowIndex: number,
|
|
90
|
+
out: Utf8StringView
|
|
91
|
+
): boolean {
|
|
92
|
+
if (!Number.isInteger(rowIndex) || rowIndex < 0 || rowIndex >= utf8Column.length) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (!utf8Column.isValid(rowIndex)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const chunks = getArrowUtf8DataChunks(utf8Column);
|
|
100
|
+
if (!chunks || chunks.length === 0) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let rowOffset = 0;
|
|
105
|
+
for (const chunk of chunks) {
|
|
106
|
+
const chunkEnd = rowOffset + chunk.length;
|
|
107
|
+
if (rowIndex >= rowOffset && rowIndex < chunkEnd) {
|
|
108
|
+
const valueOffsets = chunk.valueOffsets;
|
|
109
|
+
const values = chunk.values;
|
|
110
|
+
if (!valueOffsets || !values) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return fillUtf8StringView(valueOffsets, values, rowIndex - rowOffset, out);
|
|
114
|
+
}
|
|
115
|
+
rowOffset = chunkEnd;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fills a reusable UTF-8 view for one normalized Utf8 column row.
|
|
123
|
+
*/
|
|
124
|
+
export function getUtf8ColumnSourceRowView(
|
|
125
|
+
source: Utf8ColumnSource,
|
|
126
|
+
rowIndex: number,
|
|
127
|
+
out: Utf8StringView
|
|
128
|
+
): boolean {
|
|
129
|
+
if (!Number.isInteger(rowIndex) || rowIndex < 0 || rowIndex >= source.rowCount) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (!source.isValid(rowIndex)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const chunk of source.chunks) {
|
|
137
|
+
const chunkEnd = chunk.rowOffset + chunk.rowCount;
|
|
138
|
+
if (rowIndex < chunk.rowOffset || rowIndex >= chunkEnd) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return fillUtf8StringView(
|
|
143
|
+
chunk.valueOffsets,
|
|
144
|
+
chunk.values,
|
|
145
|
+
chunk.valueOffsetIndex + rowIndex - chunk.rowOffset,
|
|
146
|
+
out
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Finds the first row in an Arrow Utf8 column whose bytes equal the requested string view.
|
|
155
|
+
*/
|
|
156
|
+
export function arrowFindUtf8(
|
|
157
|
+
utf8Column: arrow.Vector<arrow.Utf8>,
|
|
158
|
+
value: Utf8StringView,
|
|
159
|
+
startRow = 0
|
|
160
|
+
): number {
|
|
161
|
+
const normalizedStartRow = Math.max(0, startRow);
|
|
162
|
+
const fastResult = arrowFindUtf8InDataChunks(utf8Column, value, normalizedStartRow);
|
|
163
|
+
if (fastResult !== null) {
|
|
164
|
+
return fastResult;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const expected = decodeUtf8StringView(value);
|
|
168
|
+
for (let rowIndex = normalizedStartRow; rowIndex < utf8Column.length; rowIndex += 1) {
|
|
169
|
+
if (utf8Column.get(rowIndex) === expected) {
|
|
170
|
+
return rowIndex;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return -1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
type ArrowUtf8DataChunk = {
|
|
177
|
+
/** Number of rows in this Arrow data chunk. */
|
|
178
|
+
readonly length: number;
|
|
179
|
+
/** Row offset inside the chunk's value-offset buffer. */
|
|
180
|
+
readonly offset?: number;
|
|
181
|
+
/** UTF-8 value-offset buffer. */
|
|
182
|
+
readonly valueOffsets?: Int32Array | Uint32Array;
|
|
183
|
+
/** Contiguous UTF-8 value bytes. */
|
|
184
|
+
readonly values?: Uint8Array;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Scans Arrow's contiguous Utf8 buffers directly when the vector exposes them.
|
|
189
|
+
*/
|
|
190
|
+
function arrowFindUtf8InDataChunks(
|
|
191
|
+
utf8Column: arrow.Vector<arrow.Utf8>,
|
|
192
|
+
value: Utf8StringView,
|
|
193
|
+
startRow: number
|
|
194
|
+
): number | null {
|
|
195
|
+
const chunks = getArrowUtf8DataChunks(utf8Column);
|
|
196
|
+
if (!chunks || chunks.length === 0) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let globalRowOffset = 0;
|
|
201
|
+
for (const chunk of chunks) {
|
|
202
|
+
const valueOffsets = chunk.valueOffsets;
|
|
203
|
+
const values = chunk.values;
|
|
204
|
+
if (!valueOffsets || !values) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const chunkStartRow = Math.max(0, startRow - globalRowOffset);
|
|
209
|
+
for (let chunkRow = chunkStartRow; chunkRow < chunk.length; chunkRow += 1) {
|
|
210
|
+
const globalRow = globalRowOffset + chunkRow;
|
|
211
|
+
if (!utf8Column.isValid(globalRow)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const start = valueOffsets[chunkRow];
|
|
216
|
+
const end = valueOffsets[chunkRow + 1];
|
|
217
|
+
if (start !== undefined && end !== undefined && utf8BytesEqual(values, start, end, value)) {
|
|
218
|
+
return globalRow;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
globalRowOffset += chunk.length;
|
|
222
|
+
}
|
|
223
|
+
return -1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getArrowUtf8DataChunks(
|
|
227
|
+
utf8Column: arrow.Vector<arrow.Utf8>
|
|
228
|
+
): readonly ArrowUtf8DataChunk[] | undefined {
|
|
229
|
+
return (utf8Column as unknown as {data?: readonly ArrowUtf8DataChunk[]}).data;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fillUtf8StringView(
|
|
233
|
+
valueOffsets: Int32Array | Uint32Array,
|
|
234
|
+
values: Uint8Array,
|
|
235
|
+
offsetIndex: number,
|
|
236
|
+
out: Utf8StringView
|
|
237
|
+
): boolean {
|
|
238
|
+
const start = valueOffsets[offsetIndex];
|
|
239
|
+
const end = valueOffsets[offsetIndex + 1];
|
|
240
|
+
if (start === undefined || end === undefined) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
out.data = values;
|
|
245
|
+
out.start = start;
|
|
246
|
+
out.end = end;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Returns whether a UTF-8 byte range equals one target UTF-8 view.
|
|
252
|
+
*/
|
|
253
|
+
function utf8BytesEqual(
|
|
254
|
+
data: Uint8Array,
|
|
255
|
+
start: number,
|
|
256
|
+
end: number,
|
|
257
|
+
value: Utf8StringView
|
|
258
|
+
): boolean {
|
|
259
|
+
const valueLength = value.end - value.start;
|
|
260
|
+
if (end - start !== valueLength) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
for (let index = 0; index < valueLength; index += 1) {
|
|
264
|
+
if (data[start + index] !== value.data[value.start + index]) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function decodeUtf8StringView(value: Utf8StringView): string {
|
|
272
|
+
return textDecoder.decode(value.data.subarray(value.start, value.end));
|
|
273
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
/** Two-dimensional world-space bounds represented by minimum and maximum corners. */
|
|
6
|
+
export type Bounds = [[number, number], [number, number]];
|
|
7
|
+
/** Interleaved X/Y block geometry values accepted by {@link getPaddedBlockBounds}. */
|
|
8
|
+
export type Geometry = ArrayLike<number> | null | undefined;
|
|
9
|
+
|
|
10
|
+
/** Padding configuration for {@link getPaddedBlockBounds}. */
|
|
11
|
+
export type PaddedBlockBoundsOptions = {
|
|
12
|
+
/** Padding added as a fraction of geometry width and height. @defaultValue 0.1 */
|
|
13
|
+
paddingFraction?: number;
|
|
14
|
+
/** Minimum world-space padding added on each axis. @defaultValue 1 */
|
|
15
|
+
minimumPadding?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Computes padded bounds for a block geometry represented as an interleaved array of X/Y
|
|
20
|
+
* coordinates.
|
|
21
|
+
*
|
|
22
|
+
* @param geometry - The geometry describing the block perimeter, or `null`/`undefined` when the
|
|
23
|
+
* block has not been rendered.
|
|
24
|
+
* @param options - Optional padding configuration. When omitted, a small percentage padding is
|
|
25
|
+
* applied so that the block does not touch the viewport edges.
|
|
26
|
+
* @returns The padded bounds for the geometry or `null` if the input does not contain any points.
|
|
27
|
+
*/
|
|
28
|
+
export function getPaddedBlockBounds(
|
|
29
|
+
geometry: Geometry,
|
|
30
|
+
options?: PaddedBlockBoundsOptions
|
|
31
|
+
): Bounds | null {
|
|
32
|
+
const bounds = getGeometryBounds(geometry);
|
|
33
|
+
if (!bounds) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const paddingFraction = options?.paddingFraction ?? 0.1;
|
|
38
|
+
const minimumPadding = options?.minimumPadding ?? 1;
|
|
39
|
+
return padBounds(bounds, paddingFraction, minimumPadding);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getGeometryBounds(geometry: Geometry): Bounds | null {
|
|
43
|
+
if (!geometry || geometry.length < 2) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let minX = Infinity;
|
|
48
|
+
let minY = Infinity;
|
|
49
|
+
let maxX = -Infinity;
|
|
50
|
+
let maxY = -Infinity;
|
|
51
|
+
|
|
52
|
+
for (let index = 0; index < geometry.length; index += 2) {
|
|
53
|
+
const x = geometry[index]!;
|
|
54
|
+
const y = geometry[index + 1]!;
|
|
55
|
+
|
|
56
|
+
if (x < minX) minX = x;
|
|
57
|
+
if (y < minY) minY = y;
|
|
58
|
+
if (x > maxX) maxX = x;
|
|
59
|
+
if (y > maxY) maxY = y;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
!Number.isFinite(minX) ||
|
|
64
|
+
!Number.isFinite(minY) ||
|
|
65
|
+
!Number.isFinite(maxX) ||
|
|
66
|
+
!Number.isFinite(maxY)
|
|
67
|
+
) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [
|
|
72
|
+
[minX, minY],
|
|
73
|
+
[maxX, maxY]
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function padBounds(bounds: Bounds, paddingFraction: number, minimumPadding: number): Bounds {
|
|
78
|
+
const [[minX, minY], [maxX, maxY]] = bounds;
|
|
79
|
+
const width = maxX - minX;
|
|
80
|
+
const height = maxY - minY;
|
|
81
|
+
const paddingX = Math.max(width * paddingFraction, minimumPadding);
|
|
82
|
+
const paddingY = Math.max(height * paddingFraction, minimumPadding);
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
[minX - paddingX, minY - paddingY],
|
|
86
|
+
[maxX + paddingX, maxY + paddingY]
|
|
87
|
+
];
|
|
88
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {FilterContext, Layer, Viewport} from '@deck.gl/core';
|
|
6
|
+
|
|
7
|
+
/** Deck.gl layer filter callback or `null` when no filter should be applied. */
|
|
8
|
+
export type LayerFilter = ((context: FilterContext) => boolean) | null;
|
|
9
|
+
|
|
10
|
+
/** Flat two-dimensional bounds represented as `[minX, minY, maxX, maxY]`. */
|
|
11
|
+
export type Bounds = [number, number, number, number];
|
|
12
|
+
|
|
13
|
+
/** Reason emitted when the viewport bounds filter makes or skips a filter decision. */
|
|
14
|
+
export type LayerBoundsFilterDecision =
|
|
15
|
+
| {
|
|
16
|
+
/** Indicates that the layer was filtered because it missed the viewport. */
|
|
17
|
+
decision: 'filter';
|
|
18
|
+
/** Deck.gl filter context evaluated by the filter. */
|
|
19
|
+
context: FilterContext;
|
|
20
|
+
/** Layer bounds evaluated by the filter. */
|
|
21
|
+
layerBounds: Bounds;
|
|
22
|
+
/** Viewport bounds evaluated by the filter. */
|
|
23
|
+
viewportBounds: Bounds;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
/** Indicates that the layer did not expose usable bounds. */
|
|
27
|
+
decision: 'missing-layer-bounds';
|
|
28
|
+
/** Deck.gl filter context evaluated by the filter. */
|
|
29
|
+
context: FilterContext;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
/** Indicates that the viewport did not expose usable bounds. */
|
|
33
|
+
decision: 'missing-viewport-bounds';
|
|
34
|
+
/** Deck.gl filter context evaluated by the filter. */
|
|
35
|
+
context: FilterContext;
|
|
36
|
+
/** Layer bounds evaluated before viewport bounds were found missing. */
|
|
37
|
+
layerBounds: Bounds;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Configuration for {@link createViewportBoundsFilter}. */
|
|
41
|
+
export type LayerBoundsFilterOptions = {
|
|
42
|
+
/** Callback invoked when the filter skips or applies a bounds decision. */
|
|
43
|
+
onDecision?: (decision: LayerBoundsFilterDecision) => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type BoundsCache<Id extends WeakKey> = WeakMap<Id, Bounds | null>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Combine multiple layer filters.
|
|
50
|
+
* Returns null when none of the supplied filters are defined.
|
|
51
|
+
* @param filters - Layer filters to evaluate in order.
|
|
52
|
+
* @returns One filter that requires every defined filter to pass, or `null`.
|
|
53
|
+
*/
|
|
54
|
+
export function combineLayerFilters(filters: (LayerFilter | undefined | null)[]): LayerFilter {
|
|
55
|
+
const definedFilters = filters.filter(Boolean) as ((context: FilterContext) => boolean)[];
|
|
56
|
+
if (!definedFilters.length) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (definedFilters.length === 1) {
|
|
60
|
+
return definedFilters[0]!;
|
|
61
|
+
}
|
|
62
|
+
return context => definedFilters.every(filter => filter(context));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a layer filter that compares layer and viewport bounds.
|
|
67
|
+
* Layers that do not intersect the viewport bounds are filtered out.
|
|
68
|
+
* @param options - Optional decision callback configuration.
|
|
69
|
+
* @returns Layer filter that keeps only layers intersecting the current viewport.
|
|
70
|
+
*/
|
|
71
|
+
export function createViewportBoundsFilter(options?: LayerBoundsFilterOptions): LayerFilter {
|
|
72
|
+
const layerBoundsCache: BoundsCache<Layer> = new WeakMap();
|
|
73
|
+
const viewportBoundsCache: BoundsCache<Viewport> = new WeakMap();
|
|
74
|
+
|
|
75
|
+
return (context: FilterContext) => {
|
|
76
|
+
const {layer, viewport} = context;
|
|
77
|
+
|
|
78
|
+
let layerBounds = layerBoundsCache.get(layer);
|
|
79
|
+
if (layerBounds === undefined) {
|
|
80
|
+
layerBounds = getLayerBounds(layer);
|
|
81
|
+
layerBoundsCache.set(layer, layerBounds);
|
|
82
|
+
}
|
|
83
|
+
if (!layerBounds) {
|
|
84
|
+
options?.onDecision?.({decision: 'missing-layer-bounds', context});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let viewportBounds = viewportBoundsCache.get(viewport);
|
|
89
|
+
if (viewportBounds === undefined) {
|
|
90
|
+
viewportBounds = getViewportBounds(viewport);
|
|
91
|
+
viewportBoundsCache.set(viewport, viewportBounds);
|
|
92
|
+
}
|
|
93
|
+
if (!viewportBounds) {
|
|
94
|
+
options?.onDecision?.({decision: 'missing-viewport-bounds', context, layerBounds});
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const shouldRender = boundsOverlap(layerBounds, viewportBounds);
|
|
99
|
+
if (!shouldRender) {
|
|
100
|
+
options?.onDecision?.({decision: 'filter', context, layerBounds, viewportBounds});
|
|
101
|
+
}
|
|
102
|
+
return shouldRender;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getLayerBounds(layer: Layer): Bounds | null {
|
|
107
|
+
const bounds = layer.getBounds();
|
|
108
|
+
if (!bounds || bounds.length < 2) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return normalizeBounds(bounds[0], bounds[1]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getViewportBounds(viewport: Viewport): Bounds | null {
|
|
115
|
+
const bounds = viewport.getBounds();
|
|
116
|
+
if (!bounds || bounds.length < 4) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const [minX, minY, maxX, maxY] = bounds;
|
|
121
|
+
if (
|
|
122
|
+
!isFiniteNumber(minX) ||
|
|
123
|
+
!isFiniteNumber(minY) ||
|
|
124
|
+
!isFiniteNumber(maxX) ||
|
|
125
|
+
!isFiniteNumber(maxY)
|
|
126
|
+
) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return normalizeBounds([minX, minY], [maxX, maxY]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeBounds(min: number[] | undefined, max: number[] | undefined): Bounds | null {
|
|
133
|
+
if (!min || !max || min.length < 2 || max.length < 2) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const minX = Math.min(min[0]!, max[0]!);
|
|
138
|
+
const minY = Math.min(min[1]!, max[1]!);
|
|
139
|
+
const maxX = Math.max(min[0]!, max[0]!);
|
|
140
|
+
const maxY = Math.max(min[1]!, max[1]!);
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
!isFiniteNumber(minX) ||
|
|
144
|
+
!isFiniteNumber(minY) ||
|
|
145
|
+
!isFiniteNumber(maxX) ||
|
|
146
|
+
!isFiniteNumber(maxY)
|
|
147
|
+
) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [minX, minY, maxX, maxY];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function boundsOverlap(a: Bounds, b: Bounds): boolean {
|
|
155
|
+
return !(a[2] < b[0] || b[2] < a[0] || a[3] < b[1] || b[3] < a[1]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isFiniteNumber(value: number): boolean {
|
|
159
|
+
return Number.isFinite(value);
|
|
160
|
+
}
|
|
@@ -2,38 +2,57 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
-
type Bounds
|
|
5
|
+
import type {Bounds} from './bounds-utils';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Fits two-dimensional bounds into an orthographic viewport using one shared zoom value.
|
|
9
|
+
* @param width - Viewport width in pixels.
|
|
10
|
+
* @param height - Viewport height in pixels.
|
|
11
|
+
* @param bounds - World-space bounds to fit.
|
|
12
|
+
* @param zoomMode - Shared-zoom mode selector.
|
|
13
|
+
* @returns View target and shared/per-axis zoom values.
|
|
14
|
+
*/
|
|
7
15
|
export function fitBoundsOrthographic(
|
|
8
16
|
width: number,
|
|
9
17
|
height: number,
|
|
10
18
|
bounds: Readonly<Bounds>,
|
|
11
19
|
zoomMode: 'single'
|
|
12
|
-
): {target: [number, number]; zoom: number};
|
|
20
|
+
): {target: [number, number]; zoom: number; zoomX: number; zoomY: number};
|
|
13
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Fits two-dimensional bounds into an orthographic viewport using one zoom per axis.
|
|
24
|
+
* @param width - Viewport width in pixels.
|
|
25
|
+
* @param height - Viewport height in pixels.
|
|
26
|
+
* @param bounds - World-space bounds to fit.
|
|
27
|
+
* @param zoomMode - Per-axis zoom mode selector.
|
|
28
|
+
* @returns View target and per-axis zoom values.
|
|
29
|
+
*/
|
|
14
30
|
export function fitBoundsOrthographic(
|
|
15
31
|
width: number,
|
|
16
32
|
height: number,
|
|
17
33
|
bounds: Readonly<Bounds>,
|
|
18
34
|
zoomMode: 'per-axis'
|
|
19
|
-
): {target: [number, number]; zoom: [number, number]};
|
|
35
|
+
): {target: [number, number]; zoom: [number, number]; zoomX: number; zoomY: number};
|
|
20
36
|
|
|
21
37
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @param width
|
|
24
|
-
* @param height
|
|
25
|
-
* @param bounds
|
|
26
|
-
* @
|
|
27
|
-
*
|
|
28
|
-
* zoom: deck.gl orthographic zoom level (log2(scale))
|
|
29
|
-
* (deck.gl orthographic zoom is the log2 of the scale factor)
|
|
38
|
+
* Fits two-dimensional bounds into an orthographic viewport.
|
|
39
|
+
* @param width - Viewport width in pixels.
|
|
40
|
+
* @param height - Viewport height in pixels.
|
|
41
|
+
* @param bounds - World-space bounds to fit.
|
|
42
|
+
* @param zoomMode - Whether to use one shared zoom or one zoom per axis.
|
|
43
|
+
* @returns View target plus deck.gl orthographic zoom values.
|
|
30
44
|
*/
|
|
31
45
|
export function fitBoundsOrthographic(
|
|
32
46
|
width: number,
|
|
33
47
|
height: number,
|
|
34
48
|
bounds: Readonly<Bounds>,
|
|
35
49
|
zoomMode: 'single' | 'per-axis' = 'per-axis'
|
|
36
|
-
): {
|
|
50
|
+
): {
|
|
51
|
+
target: [number, number];
|
|
52
|
+
zoom: number | [number, number];
|
|
53
|
+
zoomX: number;
|
|
54
|
+
zoomY: number;
|
|
55
|
+
} {
|
|
37
56
|
const [[minX, minY], [maxX, maxY]] = bounds;
|
|
38
57
|
|
|
39
58
|
// center of the box
|
|
@@ -41,12 +60,12 @@ export function fitBoundsOrthographic(
|
|
|
41
60
|
const centerY = (minY + maxY) / 2;
|
|
42
61
|
|
|
43
62
|
// size of the box
|
|
44
|
-
const boxW = maxX - minX;
|
|
45
|
-
const boxH = maxY - minY;
|
|
63
|
+
const boxW = Math.max(0, maxX - minX);
|
|
64
|
+
const boxH = Math.max(0, maxY - minY);
|
|
46
65
|
|
|
47
66
|
// scale (world units → screen pixels)
|
|
48
|
-
const scaleX = width / boxW;
|
|
49
|
-
const scaleY = height / boxH;
|
|
67
|
+
const scaleX = boxW ? Math.max(1, width) / boxW : 1;
|
|
68
|
+
const scaleY = boxH ? Math.max(1, height) / boxH : 1;
|
|
50
69
|
|
|
51
70
|
// pick the smaller scale so the whole box fits
|
|
52
71
|
const scale = Math.min(scaleX, scaleY);
|
|
@@ -58,13 +77,41 @@ export function fitBoundsOrthographic(
|
|
|
58
77
|
const zoomX = Math.log2(scaleX);
|
|
59
78
|
const zoomY = Math.log2(scaleY);
|
|
60
79
|
|
|
61
|
-
if (Number.isNaN(zoom) || Number.isNaN(zoomX) || Number.isNaN(
|
|
80
|
+
if (Number.isNaN(zoom) || Number.isNaN(zoomX) || Number.isNaN(zoomY)) {
|
|
62
81
|
// eslint-disable-next-line no-console
|
|
63
82
|
console.warn('Invalid zoom values:', {zoom, zoomX, zoomY});
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
return {
|
|
67
86
|
target: [centerX, centerY],
|
|
68
|
-
zoom: zoomMode === 'single' ? zoom : [zoomX, zoomY]
|
|
87
|
+
zoom: zoomMode === 'single' ? zoom : [zoomX, zoomY],
|
|
88
|
+
zoomX: zoomMode === 'single' ? zoom : zoomX,
|
|
89
|
+
zoomY: zoomMode === 'single' ? zoom : zoomY
|
|
69
90
|
};
|
|
70
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Computes world-space bounds covered by an orthographic viewport.
|
|
95
|
+
* @param width - Viewport width in pixels.
|
|
96
|
+
* @param height - Viewport height in pixels.
|
|
97
|
+
* @param zoom - Deck.gl orthographic zoom level, as one value or `[zoomX, zoomY]`.
|
|
98
|
+
* @param target - Deck.gl orthographic target `[centerX, centerY]`.
|
|
99
|
+
* @returns World-space bounds covered by the viewport.
|
|
100
|
+
*/
|
|
101
|
+
export function getBoundsOrthographic(
|
|
102
|
+
width: number,
|
|
103
|
+
height: number,
|
|
104
|
+
zoom: number | [number, number],
|
|
105
|
+
target: [number, number]
|
|
106
|
+
): Bounds {
|
|
107
|
+
const [centerX, centerY] = target;
|
|
108
|
+
const scaleX = Array.isArray(zoom) ? 2 ** zoom[0] : 2 ** zoom;
|
|
109
|
+
const scaleY = Array.isArray(zoom) ? 2 ** zoom[1] : 2 ** zoom;
|
|
110
|
+
const worldW = width / scaleX;
|
|
111
|
+
const worldH = height / scaleY;
|
|
112
|
+
|
|
113
|
+
return [
|
|
114
|
+
[centerX - worldW / 2, centerY - worldH / 2],
|
|
115
|
+
[centerX + worldW / 2, centerY + worldH / 2]
|
|
116
|
+
];
|
|
117
|
+
}
|