@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.
Files changed (53) hide show
  1. package/README.md +3 -13
  2. package/dist/clip.d.ts +20 -1
  3. package/dist/clip.d.ts.map +1 -1
  4. package/dist/cluster-tile-index.d.ts +76 -0
  5. package/dist/cluster-tile-index.d.ts.map +1 -0
  6. package/dist/cluster-tile-index.test.d.ts +2 -0
  7. package/dist/cluster-tile-index.test.d.ts.map +1 -0
  8. package/dist/convert.d.ts +10 -2
  9. package/dist/convert.d.ts.map +1 -1
  10. package/dist/deconvert.d.ts +19 -0
  11. package/dist/deconvert.d.ts.map +1 -0
  12. package/dist/deconvert.test.d.ts +2 -0
  13. package/dist/deconvert.test.d.ts.map +1 -0
  14. package/dist/definitions.d.ts +176 -20
  15. package/dist/definitions.d.ts.map +1 -1
  16. package/dist/feature.d.ts +11 -3
  17. package/dist/feature.d.ts.map +1 -1
  18. package/dist/geojson-to-tile.d.ts +35 -0
  19. package/dist/geojson-to-tile.d.ts.map +1 -0
  20. package/dist/geojson-vt-dev.js +1582 -478
  21. package/dist/geojson-vt.js +1 -1
  22. package/dist/geojson-vt.mjs +1250 -473
  23. package/dist/geojson-vt.mjs.map +1 -1
  24. package/dist/geojsonvt.d.ts +76 -0
  25. package/dist/geojsonvt.d.ts.map +1 -0
  26. package/dist/geojsonvt.test.d.ts +2 -0
  27. package/dist/geojsonvt.test.d.ts.map +1 -0
  28. package/dist/index.d.ts +8 -62
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/tile-index.d.ts +51 -0
  31. package/dist/tile-index.d.ts.map +1 -0
  32. package/dist/tile.d.ts +1 -29
  33. package/dist/tile.d.ts.map +1 -1
  34. package/dist/transform.d.ts +1 -18
  35. package/dist/transform.d.ts.map +1 -1
  36. package/package.json +18 -10
  37. package/src/clip.ts +119 -81
  38. package/src/cluster-tile-index.test.ts +205 -0
  39. package/src/cluster-tile-index.ts +513 -0
  40. package/src/convert.ts +97 -75
  41. package/src/deconvert.test.ts +153 -0
  42. package/src/deconvert.ts +92 -0
  43. package/src/definitions.ts +196 -18
  44. package/src/difference.ts +3 -3
  45. package/src/feature.ts +11 -4
  46. package/src/geojson-to-tile.ts +58 -0
  47. package/src/geojsonvt.test.ts +39 -0
  48. package/src/geojsonvt.ts +209 -0
  49. package/src/index.ts +27 -378
  50. package/src/tile-index.ts +310 -0
  51. package/src/tile.ts +92 -103
  52. package/src/transform.ts +41 -39
  53. 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
- /* clip features between two vertical or horizontal axis-parallel lines:
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
- * k1 and k2 are the line coordinates
12
- * axis: 0 for x, 1 for y
13
- * minAll and maxAll: minimum and maximum coordinate value for all features
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, k1: number, k2: number, axis: number, minAll: number, maxAll: number, options: GeoJSONVTOptions): GeoJSONVTInternalFeature[] | null {
16
- k1 /= scale;
17
- k2 /= scale;
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 >= k1 && maxAll < k2) { // trivial accept
29
+ if (minAll >= start && maxAll < end) { // trivial accept
20
30
  return features;
21
31
  }
22
32
 
23
- if (maxAll < k1 || minAll >= k2) { // trivial reject
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 === 0 ? feature.minX : feature.minY;
31
- const max = axis === 0 ? feature.maxX : feature.maxY;
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 >= k1 && max < k2) { // trivial accept
43
+ if (min >= start && max < end) { // trivial accept
34
44
  clipped.push(feature);
35
45
  continue;
36
46
  }
37
47
 
38
- if (max < k1 || min >= k2) { // trivial reject
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
- const pointGeometry: number[] = [];
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
- const lineGeometry: StartEndSizeArray[] = [];
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
- const multiLineGeometry: StartEndSizeArray[] = [];
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
- const polygonGeometry: StartEndSizeArray[] = [];
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
- const multiPolygonGeometry: StartEndSizeArray[][] = [];
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 clipPoints(geom: number[], newGeom: number[], k1: number, k2: number, axis: number) {
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 >= k1 && a <= k2) {
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[], k1: number, k2: number, axis: number, isPolygon: boolean, trackMetrics: boolean) {
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 === 0 ? intersectX : intersectY;
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 === 0 ? ax : ay;
142
- const b = axis === 0 ? bx : by;
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 < k1) {
185
+ if (a < start) {
148
186
  // ---|--> | (line enters the clip region from the left)
149
- if (b > k1) {
150
- t = intersect(slice, ax, ay, bx, by, k1);
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 > k2) {
191
+ } else if (a > end) {
154
192
  // | <--|--- (line enters the clip region from the right)
155
- if (b < k2) {
156
- t = intersect(slice, ax, ay, bx, by, k2);
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 < k1 && a >= k1) {
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, k1);
203
+ t = intersect(slice, ax, ay, bx, by, start);
166
204
  exited = true;
167
205
  }
168
206
 
169
- if (b > k2 && a <= k2) {
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, k2);
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 === 0 ? ax : ay;
190
- if (a >= k1 && a <= k2) addPoint(slice, ax, ay, az);
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[], k1: number, k2: number, axis: number, isPolygon: boolean) {
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, k1, k2, axis, isPolygon, false);
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
+ });