@maplibre/geojson-vt 5.0.4 → 6.0.0
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 +3 -13
- package/dist/clip.d.ts +20 -1
- package/dist/clip.d.ts.map +1 -1
- package/dist/cluster-tile-index.d.ts +76 -0
- package/dist/cluster-tile-index.d.ts.map +1 -0
- package/dist/cluster-tile-index.test.d.ts +2 -0
- package/dist/cluster-tile-index.test.d.ts.map +1 -0
- package/dist/convert.d.ts +10 -2
- package/dist/convert.d.ts.map +1 -1
- package/dist/deconvert.d.ts +19 -0
- package/dist/deconvert.d.ts.map +1 -0
- package/dist/deconvert.test.d.ts +2 -0
- package/dist/deconvert.test.d.ts.map +1 -0
- package/dist/definitions.d.ts +176 -20
- package/dist/definitions.d.ts.map +1 -1
- package/dist/feature.d.ts +11 -3
- package/dist/feature.d.ts.map +1 -1
- package/dist/geojson-to-tile.d.ts +35 -0
- package/dist/geojson-to-tile.d.ts.map +1 -0
- package/dist/geojson-vt-dev.js +1582 -478
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +1250 -473
- package/dist/geojson-vt.mjs.map +1 -1
- package/dist/geojsonvt.d.ts +76 -0
- package/dist/geojsonvt.d.ts.map +1 -0
- package/dist/geojsonvt.test.d.ts +2 -0
- package/dist/geojsonvt.test.d.ts.map +1 -0
- package/dist/index.d.ts +8 -62
- package/dist/index.d.ts.map +1 -1
- package/dist/tile-index.d.ts +51 -0
- package/dist/tile-index.d.ts.map +1 -0
- package/dist/tile.d.ts +1 -29
- package/dist/tile.d.ts.map +1 -1
- package/dist/transform.d.ts +1 -18
- package/dist/transform.d.ts.map +1 -1
- package/package.json +18 -10
- package/src/clip.ts +119 -81
- package/src/cluster-tile-index.test.ts +205 -0
- package/src/cluster-tile-index.ts +513 -0
- package/src/convert.ts +97 -75
- package/src/deconvert.test.ts +153 -0
- package/src/deconvert.ts +92 -0
- package/src/definitions.ts +196 -18
- package/src/difference.ts +3 -3
- package/src/feature.ts +11 -4
- package/src/geojson-to-tile.ts +58 -0
- package/src/geojsonvt.test.ts +39 -0
- package/src/geojsonvt.ts +209 -0
- package/src/index.ts +27 -378
- package/src/tile-index.ts +310 -0
- package/src/tile.ts +92 -103
- package/src/transform.ts +41 -39
- package/src/wrap.ts +4 -4
package/src/clip.ts
CHANGED
|
@@ -1,110 +1,78 @@
|
|
|
1
1
|
|
|
2
2
|
import {createFeature} from './feature';
|
|
3
|
-
import type { GeoJSONVTInternalFeature, GeoJSONVTOptions, StartEndSizeArray } from './definitions';
|
|
3
|
+
import type { GeoJSONVTInternalFeature, GeoJSONVTInternalLineStringFeature, GeoJSONVTInternalMultiLineStringFeature, GeoJSONVTInternalMultiPointFeature, GeoJSONVTInternalMultiPolygonFeature, GeoJSONVTInternalPointFeature, GeoJSONVTInternalPolygonFeature, GeoJSONVTOptions, StartEndSizeArray } from './definitions';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export const enum AxisType {
|
|
6
|
+
X = 0,
|
|
7
|
+
Y = 1
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* clip features between two vertical or horizontal axis-parallel lines:
|
|
6
12
|
* | |
|
|
7
13
|
* ___|___ | /
|
|
8
14
|
* / | \____|____/
|
|
9
15
|
* | |
|
|
10
16
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
17
|
+
* @param features - the features to clip
|
|
18
|
+
* @param scale - the scale to divide start and end inputs
|
|
19
|
+
* @param start - the start of the clip range
|
|
20
|
+
* @param end - the end of the clip range
|
|
21
|
+
* @param axis - which axis to clip against
|
|
22
|
+
* @param minAll - the minimum for all features in the relevant axis
|
|
23
|
+
* @param maxAll - the maximum for all features in the relevant axis
|
|
14
24
|
*/
|
|
15
|
-
export function clip(features: GeoJSONVTInternalFeature[], scale: number,
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
export function clip(features: GeoJSONVTInternalFeature[], scale: number, start: number, end: number, axis: AxisType, minAll: number, maxAll: number, options: GeoJSONVTOptions): GeoJSONVTInternalFeature[] | null {
|
|
26
|
+
start /= scale;
|
|
27
|
+
end /= scale;
|
|
18
28
|
|
|
19
|
-
if (minAll >=
|
|
29
|
+
if (minAll >= start && maxAll < end) { // trivial accept
|
|
20
30
|
return features;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
|
-
if (maxAll <
|
|
33
|
+
if (maxAll < start || minAll >= end) { // trivial reject
|
|
24
34
|
return null;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
const clipped: GeoJSONVTInternalFeature[] = [];
|
|
28
38
|
|
|
29
39
|
for (const feature of features) {
|
|
30
|
-
const min = axis ===
|
|
31
|
-
const max = axis ===
|
|
40
|
+
const min = axis === AxisType.X ? feature.minX : feature.minY;
|
|
41
|
+
const max = axis === AxisType.X ? feature.maxX : feature.maxY;
|
|
32
42
|
|
|
33
|
-
if (min >=
|
|
43
|
+
if (min >= start && max < end) { // trivial accept
|
|
34
44
|
clipped.push(feature);
|
|
35
45
|
continue;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
if (max <
|
|
48
|
+
if (max < start || min >= end) { // trivial reject
|
|
39
49
|
continue;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
switch (feature.type) {
|
|
43
53
|
case 'Point':
|
|
44
54
|
case 'MultiPoint': {
|
|
45
|
-
|
|
46
|
-
clipPoints(feature.geometry, pointGeometry, k1, k2, axis);
|
|
47
|
-
if (!pointGeometry.length) continue;
|
|
48
|
-
|
|
49
|
-
const type = pointGeometry.length === 3 ? 'Point' : 'MultiPoint';
|
|
50
|
-
clipped.push(createFeature(feature.id, type, pointGeometry, feature.tags));
|
|
55
|
+
clipPointFeature(feature, clipped, start, end, axis);
|
|
51
56
|
continue;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
case 'LineString': {
|
|
55
|
-
|
|
56
|
-
clipLine(feature.geometry, lineGeometry, k1, k2, axis, false, options.lineMetrics);
|
|
57
|
-
if (!lineGeometry.length) continue;
|
|
58
|
-
|
|
59
|
-
if (options.lineMetrics) {
|
|
60
|
-
for (const line of lineGeometry) {
|
|
61
|
-
clipped.push(createFeature(feature.id, feature.type, line, feature.tags));
|
|
62
|
-
}
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (lineGeometry.length > 1) {
|
|
67
|
-
clipped.push(createFeature(feature.id, "MultiLineString", lineGeometry, feature.tags));
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
clipped.push(createFeature(feature.id, feature.type, lineGeometry[0], feature.tags));
|
|
60
|
+
clipLineStringFeature(feature, clipped, start, end, axis, options);
|
|
72
61
|
continue;
|
|
73
62
|
}
|
|
74
63
|
|
|
75
64
|
case 'MultiLineString': {
|
|
76
|
-
|
|
77
|
-
clipLines(feature.geometry, multiLineGeometry, k1, k2, axis, false);
|
|
78
|
-
if (!multiLineGeometry.length) continue;
|
|
79
|
-
|
|
80
|
-
if (multiLineGeometry.length === 1) {
|
|
81
|
-
clipped.push(createFeature(feature.id, "LineString", multiLineGeometry[0], feature.tags));
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
clipped.push(createFeature(feature.id, feature.type, multiLineGeometry, feature.tags));
|
|
65
|
+
clipMultiLineStringFeature(feature, clipped, start, end, axis);
|
|
86
66
|
continue;
|
|
87
67
|
}
|
|
88
68
|
|
|
89
69
|
case 'Polygon': {
|
|
90
|
-
|
|
91
|
-
clipLines(feature.geometry, polygonGeometry, k1, k2, axis, true);
|
|
92
|
-
if (!polygonGeometry.length) continue;
|
|
93
|
-
|
|
94
|
-
clipped.push(createFeature(feature.id, feature.type, polygonGeometry, feature.tags));
|
|
70
|
+
clipPolygonFeature(feature, clipped, start, end, axis);
|
|
95
71
|
continue;
|
|
96
72
|
}
|
|
97
73
|
|
|
98
74
|
case 'MultiPolygon': {
|
|
99
|
-
|
|
100
|
-
for (const polygon of feature.geometry) {
|
|
101
|
-
const newPolygon: StartEndSizeArray[] = [];
|
|
102
|
-
clipLines(polygon, newPolygon, k1, k2, axis, true);
|
|
103
|
-
if (newPolygon.length) multiPolygonGeometry.push(newPolygon);
|
|
104
|
-
}
|
|
105
|
-
if (!multiPolygonGeometry.length) continue;
|
|
106
|
-
|
|
107
|
-
clipped.push(createFeature(feature.id, feature.type, multiPolygonGeometry, feature.tags));
|
|
75
|
+
clipMultiPolygonFeature(feature, clipped, start, end, axis);
|
|
108
76
|
continue;
|
|
109
77
|
}
|
|
110
78
|
}
|
|
@@ -115,20 +83,90 @@ export function clip(features: GeoJSONVTInternalFeature[], scale: number, k1: nu
|
|
|
115
83
|
return clipped;
|
|
116
84
|
}
|
|
117
85
|
|
|
118
|
-
function
|
|
86
|
+
function clipPointFeature(feature: GeoJSONVTInternalPointFeature | GeoJSONVTInternalMultiPointFeature, clipped: GeoJSONVTInternalFeature[], start: number, end: number, axis: AxisType) {
|
|
87
|
+
const geom: number[] = [];
|
|
88
|
+
|
|
89
|
+
clipPoints(feature.geometry, geom, start, end, axis);
|
|
90
|
+
if (!geom.length) return;
|
|
91
|
+
|
|
92
|
+
const type = geom.length === 3 ? 'Point' : 'MultiPoint';
|
|
93
|
+
clipped.push(createFeature(feature.id, type, geom, feature.tags));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clipLineStringFeature(feature: GeoJSONVTInternalLineStringFeature, clipped: GeoJSONVTInternalFeature[], start: number, end: number, axis: AxisType, options: GeoJSONVTOptions) {
|
|
97
|
+
const geom: StartEndSizeArray[] = [];
|
|
98
|
+
|
|
99
|
+
clipLine(feature.geometry, geom, start, end, axis, false, options.lineMetrics);
|
|
100
|
+
if (!geom.length) return;
|
|
101
|
+
|
|
102
|
+
if (options.lineMetrics) {
|
|
103
|
+
for (const line of geom) {
|
|
104
|
+
clipped.push(createFeature(feature.id, 'LineString', line, feature.tags));
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (geom.length > 1) {
|
|
110
|
+
clipped.push(createFeature(feature.id, 'MultiLineString', geom, feature.tags));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
clipped.push(createFeature(feature.id, 'LineString', geom[0], feature.tags));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function clipMultiLineStringFeature(feature: GeoJSONVTInternalMultiLineStringFeature, clipped: GeoJSONVTInternalFeature[], start: number, end: number, axis: AxisType) {
|
|
118
|
+
const geom: StartEndSizeArray[] = [];
|
|
119
|
+
|
|
120
|
+
clipLines(feature.geometry, geom, start, end, axis, false);
|
|
121
|
+
if (!geom.length) return;
|
|
122
|
+
|
|
123
|
+
if (geom.length === 1) {
|
|
124
|
+
clipped.push(createFeature(feature.id, 'LineString', geom[0], feature.tags));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
clipped.push(createFeature(feature.id,'MultiLineString', geom, feature.tags));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function clipPolygonFeature(feature: GeoJSONVTInternalPolygonFeature, clipped: GeoJSONVTInternalFeature[], start: number, end: number, axis: AxisType) {
|
|
132
|
+
const geom: StartEndSizeArray[] = [];
|
|
133
|
+
|
|
134
|
+
clipLines(feature.geometry, geom, start, end, axis, true);
|
|
135
|
+
if (!geom.length) return;
|
|
136
|
+
|
|
137
|
+
clipped.push(createFeature(feature.id, 'Polygon', geom, feature.tags));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function clipMultiPolygonFeature(feature: GeoJSONVTInternalMultiPolygonFeature, clipped: GeoJSONVTInternalFeature[], start: number, end: number, axis: AxisType) {
|
|
141
|
+
const geom: StartEndSizeArray[][] = [];
|
|
142
|
+
|
|
143
|
+
for (const polygon of feature.geometry) {
|
|
144
|
+
const newPolygon: StartEndSizeArray[] = [];
|
|
145
|
+
|
|
146
|
+
clipLines(polygon, newPolygon, start, end, axis, true);
|
|
147
|
+
if (!newPolygon.length) continue;
|
|
148
|
+
|
|
149
|
+
geom.push(newPolygon);
|
|
150
|
+
}
|
|
151
|
+
if (!geom.length) return;
|
|
152
|
+
|
|
153
|
+
clipped.push(createFeature(feature.id, 'MultiPolygon', geom, feature.tags));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function clipPoints(geom: number[], newGeom: number[], start: number, end: number, axis: AxisType) {
|
|
119
157
|
for (let i = 0; i < geom.length; i += 3) {
|
|
120
158
|
const a = geom[i + axis];
|
|
121
159
|
|
|
122
|
-
if (a >=
|
|
160
|
+
if (a >= start && a <= end) {
|
|
123
161
|
addPoint(newGeom, geom[i], geom[i + 1], geom[i + 2]);
|
|
124
162
|
}
|
|
125
163
|
}
|
|
126
164
|
}
|
|
127
165
|
|
|
128
|
-
function clipLine(geom: StartEndSizeArray, newGeom: StartEndSizeArray[],
|
|
166
|
+
function clipLine(geom: StartEndSizeArray, newGeom: StartEndSizeArray[], start: number, end: number, axis: AxisType, isPolygon: boolean, trackMetrics: boolean) {
|
|
129
167
|
|
|
130
168
|
let slice = newSlice(geom);
|
|
131
|
-
const intersect = axis ===
|
|
169
|
+
const intersect = axis === AxisType.X ? intersectX : intersectY;
|
|
132
170
|
let len = geom.start;
|
|
133
171
|
let segLen, t;
|
|
134
172
|
|
|
@@ -138,37 +176,37 @@ function clipLine(geom: StartEndSizeArray, newGeom: StartEndSizeArray[], k1: num
|
|
|
138
176
|
const az = geom[i + 2];
|
|
139
177
|
const bx = geom[i + 3];
|
|
140
178
|
const by = geom[i + 4];
|
|
141
|
-
const a = axis ===
|
|
142
|
-
const b = axis ===
|
|
179
|
+
const a = axis === AxisType.X ? ax : ay;
|
|
180
|
+
const b = axis === AxisType.X ? bx : by;
|
|
143
181
|
let exited = false;
|
|
144
182
|
|
|
145
183
|
if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
|
|
146
184
|
|
|
147
|
-
if (a <
|
|
185
|
+
if (a < start) {
|
|
148
186
|
// ---|--> | (line enters the clip region from the left)
|
|
149
|
-
if (b >
|
|
150
|
-
t = intersect(slice, ax, ay, bx, by,
|
|
187
|
+
if (b > start) {
|
|
188
|
+
t = intersect(slice, ax, ay, bx, by, start);
|
|
151
189
|
if (trackMetrics) slice.start = len + segLen * t;
|
|
152
190
|
}
|
|
153
|
-
} else if (a >
|
|
191
|
+
} else if (a > end) {
|
|
154
192
|
// | <--|--- (line enters the clip region from the right)
|
|
155
|
-
if (b <
|
|
156
|
-
t = intersect(slice, ax, ay, bx, by,
|
|
193
|
+
if (b < end) {
|
|
194
|
+
t = intersect(slice, ax, ay, bx, by, end);
|
|
157
195
|
if (trackMetrics) slice.start = len + segLen * t;
|
|
158
196
|
}
|
|
159
197
|
} else {
|
|
160
198
|
addPoint(slice, ax, ay, az);
|
|
161
199
|
}
|
|
162
200
|
|
|
163
|
-
if (b <
|
|
201
|
+
if (b < start && a >= start) {
|
|
164
202
|
// <--|--- | or <--|-----|--- (line exits the clip region on the left)
|
|
165
|
-
t = intersect(slice, ax, ay, bx, by,
|
|
203
|
+
t = intersect(slice, ax, ay, bx, by, start);
|
|
166
204
|
exited = true;
|
|
167
205
|
}
|
|
168
206
|
|
|
169
|
-
if (b >
|
|
207
|
+
if (b > end && a <= end) {
|
|
170
208
|
// | ---|--> or ---|-----|--> (line exits the clip region on the right)
|
|
171
|
-
t = intersect(slice, ax, ay, bx, by,
|
|
209
|
+
t = intersect(slice, ax, ay, bx, by, end);
|
|
172
210
|
exited = true;
|
|
173
211
|
}
|
|
174
212
|
|
|
@@ -186,8 +224,8 @@ function clipLine(geom: StartEndSizeArray, newGeom: StartEndSizeArray[], k1: num
|
|
|
186
224
|
const ax = geom[last];
|
|
187
225
|
const ay = geom[last + 1];
|
|
188
226
|
const az = geom[last + 2];
|
|
189
|
-
const a = axis ===
|
|
190
|
-
if (a >=
|
|
227
|
+
const a = axis === AxisType.X ? ax : ay;
|
|
228
|
+
if (a >= start && a <= end) addPoint(slice, ax, ay, az);
|
|
191
229
|
|
|
192
230
|
// close the polygon if its endpoints are not the same after clipping
|
|
193
231
|
last = slice.length - 3;
|
|
@@ -209,9 +247,9 @@ function newSlice(line: StartEndSizeArray): StartEndSizeArray {
|
|
|
209
247
|
return slice;
|
|
210
248
|
}
|
|
211
249
|
|
|
212
|
-
function clipLines(geom: StartEndSizeArray[], newGeom: StartEndSizeArray[],
|
|
250
|
+
function clipLines(geom: StartEndSizeArray[], newGeom: StartEndSizeArray[], start: number, end: number, axis: AxisType, isPolygon: boolean) {
|
|
213
251
|
for (const line of geom) {
|
|
214
|
-
clipLine(line, newGeom,
|
|
252
|
+
clipLine(line, newGeom, start, end, axis, isPolygon, false);
|
|
215
253
|
}
|
|
216
254
|
}
|
|
217
255
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {test, expect} from 'vitest';
|
|
2
|
+
import {readFileSync} from 'fs';
|
|
3
|
+
import {ClusterTileIndex} from './cluster-tile-index';
|
|
4
|
+
import type {ClusterProperties, GeoJSONVTTile} from './definitions';
|
|
5
|
+
|
|
6
|
+
const places = JSON.parse(readFileSync(new URL('../test/fixtures/places.json', import.meta.url), 'utf-8')) as GeoJSON.FeatureCollection<GeoJSON.Point>;
|
|
7
|
+
const placesTile = JSON.parse(readFileSync(new URL('../test/fixtures/places-z0-0-0.json', import.meta.url), 'utf-8')) as GeoJSONVTTile;
|
|
8
|
+
const placesTileMin5 = JSON.parse(readFileSync(new URL('../test/fixtures/places-z0-0-0-min5.json', import.meta.url), 'utf-8')) as GeoJSONVTTile;
|
|
9
|
+
|
|
10
|
+
test('generates clusters properly', () => {
|
|
11
|
+
const index = new ClusterTileIndex();
|
|
12
|
+
index.load(places.features);
|
|
13
|
+
const tile = index.getTile(0, 0, 0);
|
|
14
|
+
expect(tile?.features).toEqual(placesTile.features);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('supports minPoints option', () => {
|
|
18
|
+
const index = new ClusterTileIndex({minPoints: 5});
|
|
19
|
+
index.load(places.features);
|
|
20
|
+
const tile = index.getTile(0, 0, 0);
|
|
21
|
+
expect(tile?.features).toEqual(placesTileMin5.features);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('returns children of a cluster', () => {
|
|
25
|
+
const index = new ClusterTileIndex();
|
|
26
|
+
index.load(places.features);
|
|
27
|
+
const childCounts = index.getChildren(163).map(p => (p.properties as ClusterProperties)?.point_count || 1);
|
|
28
|
+
expect(childCounts).toEqual([6, 7, 2, 1]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('returns leaves of a cluster', () => {
|
|
32
|
+
const index = new ClusterTileIndex();
|
|
33
|
+
index.load(places.features);
|
|
34
|
+
const leafNames = index.getLeaves(163, 10, 5).map(p => (p.properties as {name: string} | null)?.name);
|
|
35
|
+
expect(leafNames).toEqual([
|
|
36
|
+
'Niagara Falls',
|
|
37
|
+
'Cape San Blas',
|
|
38
|
+
'Cape Sable',
|
|
39
|
+
'Cape Canaveral',
|
|
40
|
+
'San Salvador',
|
|
41
|
+
'Cabo Gracias a Dios',
|
|
42
|
+
'I. de Cozumel',
|
|
43
|
+
'Grand Cayman',
|
|
44
|
+
'Miquelon',
|
|
45
|
+
'Cape Bauld'
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('generates unique ids with generateId option', () => {
|
|
50
|
+
const index = new ClusterTileIndex({generateId: true});
|
|
51
|
+
index.load(places.features);
|
|
52
|
+
const tile = index.getTile(0, 0, 0)!;
|
|
53
|
+
const ids = tile.features.filter(f => !(f.tags as ClusterProperties)?.cluster).map(f => f.id);
|
|
54
|
+
expect(ids).toEqual([12, 20, 21, 22, 24, 28, 30, 62, 81, 118, 119, 125, 81, 118]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('getLeaves handles null-property features', () => {
|
|
58
|
+
const index = new ClusterTileIndex();
|
|
59
|
+
index.load(places.features.concat([{
|
|
60
|
+
type: 'Feature',
|
|
61
|
+
properties: null,
|
|
62
|
+
geometry: {
|
|
63
|
+
type: 'Point',
|
|
64
|
+
coordinates: [-79.04411780507252, 43.08771393436908]
|
|
65
|
+
}
|
|
66
|
+
}]));
|
|
67
|
+
const leaves = index.getLeaves(164, 1, 6);
|
|
68
|
+
expect(leaves[0].properties).toBe(null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('returns cluster expansion zoom', () => {
|
|
72
|
+
const index = new ClusterTileIndex();
|
|
73
|
+
index.load(places.features);
|
|
74
|
+
expect(index.getClusterExpansionZoom(163)).toBe(1);
|
|
75
|
+
expect(index.getClusterExpansionZoom(195)).toBe(1);
|
|
76
|
+
expect(index.getClusterExpansionZoom(580)).toBe(2);
|
|
77
|
+
expect(index.getClusterExpansionZoom(1156)).toBe(2);
|
|
78
|
+
expect(index.getClusterExpansionZoom(4133)).toBe(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('returns cluster expansion zoom for maxZoom', () => {
|
|
82
|
+
const index = new ClusterTileIndex({
|
|
83
|
+
radius: 60,
|
|
84
|
+
extent: 256,
|
|
85
|
+
maxZoom: 4,
|
|
86
|
+
});
|
|
87
|
+
index.load(places.features);
|
|
88
|
+
|
|
89
|
+
expect(index.getClusterExpansionZoom(2503)).toBe(5);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('aggregates cluster properties with reduce', () => {
|
|
93
|
+
const index = new ClusterTileIndex({
|
|
94
|
+
map: (props) => ({sum: (props as {scalerank: number})?.scalerank}),
|
|
95
|
+
reduce: (a, b) => { (a as {sum: number}).sum += (b as {sum: number}).sum; },
|
|
96
|
+
radius: 100
|
|
97
|
+
});
|
|
98
|
+
index.load(places.features);
|
|
99
|
+
|
|
100
|
+
expect(index.getTile(1, 0, 0)!.features.map(f => (f.tags as {sum?: number})?.sum).filter(Boolean)).toEqual(
|
|
101
|
+
[146, 84, 63, 23, 34, 12, 19, 29, 8, 8, 80, 35]);
|
|
102
|
+
expect(index.getTile(0, 0, 0)!.features.map(f => (f.tags as {sum?: number})?.sum).filter(Boolean)).toEqual(
|
|
103
|
+
[298, 122, 12, 36, 98, 7, 24, 8, 125, 98, 125, 12, 36, 8]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('uses default map function with reduce', () => {
|
|
107
|
+
const index = new ClusterTileIndex({
|
|
108
|
+
reduce: () => {},
|
|
109
|
+
radius: 100
|
|
110
|
+
});
|
|
111
|
+
index.load(places.features);
|
|
112
|
+
|
|
113
|
+
expect(index.getTile(0, 0, 0)).toBeTruthy();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('returns clusters when query crosses international dateline', () => {
|
|
117
|
+
const index = new ClusterTileIndex();
|
|
118
|
+
index.load([
|
|
119
|
+
{
|
|
120
|
+
type: 'Feature',
|
|
121
|
+
properties: null,
|
|
122
|
+
geometry: {
|
|
123
|
+
type: 'Point',
|
|
124
|
+
coordinates: [-178.989, 0]
|
|
125
|
+
}
|
|
126
|
+
}, {
|
|
127
|
+
type: 'Feature',
|
|
128
|
+
properties: null,
|
|
129
|
+
geometry: {
|
|
130
|
+
type: 'Point',
|
|
131
|
+
coordinates: [-178.990, 0]
|
|
132
|
+
}
|
|
133
|
+
}, {
|
|
134
|
+
type: 'Feature',
|
|
135
|
+
properties: null,
|
|
136
|
+
geometry: {
|
|
137
|
+
type: 'Point',
|
|
138
|
+
coordinates: [-178.991, 0]
|
|
139
|
+
}
|
|
140
|
+
}, {
|
|
141
|
+
type: 'Feature',
|
|
142
|
+
properties: null,
|
|
143
|
+
geometry: {
|
|
144
|
+
type: 'Point',
|
|
145
|
+
coordinates: [-178.992, 0]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const nonCrossing = index.getClusters([-179, -10, -177, 10], 1);
|
|
151
|
+
const crossing = index.getClusters([179, -10, -177, 10], 1);
|
|
152
|
+
|
|
153
|
+
expect(nonCrossing.length).toBeGreaterThan(0);
|
|
154
|
+
expect(crossing.length).toBeGreaterThan(0);
|
|
155
|
+
expect(nonCrossing.length).toBe(crossing.length);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('does not crash on weird bbox values', () => {
|
|
159
|
+
const index = new ClusterTileIndex();
|
|
160
|
+
index.load(places.features);
|
|
161
|
+
expect(index.getClusters([129.426390, -103.720017, -445.930843, 114.518236], 1).length).toBe(26);
|
|
162
|
+
expect(index.getClusters([112.207836, -84.578666, -463.149397, 120.169159], 1).length).toBe(27);
|
|
163
|
+
expect(index.getClusters([129.886277, -82.332680, -445.470956, 120.390930], 1).length).toBe(26);
|
|
164
|
+
expect(index.getClusters([458.220043, -84.239039, -117.137190, 120.206585], 1).length).toBe(25);
|
|
165
|
+
expect(index.getClusters([456.713058, -80.354196, -118.644175, 120.539148], 1).length).toBe(25);
|
|
166
|
+
expect(index.getClusters([453.105328, -75.857422, -122.251904, 120.732760], 1).length).toBe(25);
|
|
167
|
+
expect(index.getClusters([-180, -90, 180, 90], 1).length).toBe(61);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('does not crash on non-integer zoom values', () => {
|
|
171
|
+
const index = new ClusterTileIndex();
|
|
172
|
+
index.load(places.features);
|
|
173
|
+
expect(index.getClusters([179, -10, -177, 10], 1.25)).toBeTruthy();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('makes sure same-location points are clustered', () => {
|
|
177
|
+
const index = new ClusterTileIndex({
|
|
178
|
+
maxZoom: 20,
|
|
179
|
+
extent: 8192,
|
|
180
|
+
radius: 16
|
|
181
|
+
});
|
|
182
|
+
index.load([
|
|
183
|
+
{type: 'Feature', properties: null, geometry: {type: 'Point', coordinates: [-1.426798, 53.943034]}},
|
|
184
|
+
{type: 'Feature', properties: null, geometry: {type: 'Point', coordinates: [-1.426798, 53.943034]}}
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
expect(index.trees[20].ids.length).toBe(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('makes sure unclustered point coords are not rounded', () => {
|
|
191
|
+
const index = new ClusterTileIndex({maxZoom: 19});
|
|
192
|
+
index.load([
|
|
193
|
+
{type: 'Feature', properties: null, geometry: {type: 'Point', coordinates: [173.19150559062456, -41.340357424709275]}}
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
expect(index.getTile(20, 1028744, 656754)!.features[0].geometry[0]).toEqual([421, 281]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('does not throw on zero items', () => {
|
|
200
|
+
expect(() => {
|
|
201
|
+
const index = new ClusterTileIndex();
|
|
202
|
+
index.load([]);
|
|
203
|
+
expect(index.getClusters([-180, -85, 180, 85], 0)).toEqual([]);
|
|
204
|
+
}).not.toThrow();
|
|
205
|
+
});
|