@maplibre/geojson-vt 6.1.2 → 6.2.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/src/convert.ts CHANGED
@@ -47,30 +47,31 @@ function featureToInternal(features: GeoJSONVTInternalFeature[], geojson: GeoJSO
47
47
 
48
48
  const id = getFeatureId(geojson, options, index);
49
49
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
50
+ const worldCoordinates = options.worldCoordinates === true;
50
51
 
51
52
  switch (geojson.geometry.type) {
52
53
  case 'Point':
53
- convertPointFeature(features, id, geojson.geometry, geojson.properties);
54
+ convertPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
54
55
  return;
55
56
 
56
57
  case 'MultiPoint':
57
- convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
58
+ convertMultiPointFeature(features, id, geojson.geometry, worldCoordinates, geojson.properties);
58
59
  return;
59
60
 
60
61
  case 'LineString':
61
- convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
62
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
62
63
  return;
63
64
 
64
65
  case 'MultiLineString':
65
- convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
66
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinates, options, geojson.properties);
66
67
  return;
67
68
 
68
69
  case 'Polygon':
69
- convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
70
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
70
71
  return;
71
72
 
72
73
  case 'MultiPolygon':
73
- convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
74
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinates, geojson.properties);
74
75
  return;
75
76
 
76
77
  default:
@@ -99,64 +100,68 @@ function convertGeometryCollection(features: GeoJSONVTInternalFeature[], geojson
99
100
  }
100
101
  }
101
102
 
102
- function convertPointFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.Point, properties: GeoJSON.GeoJsonProperties) {
103
+ function convertPointFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.Point, worldCoordinates: boolean, properties: GeoJSON.GeoJsonProperties) {
103
104
  const out: number[] = [];
104
- out.push(projectX(geom.coordinates[0]), projectY(geom.coordinates[1]), 0);
105
+ out.push(projectX(geom.coordinates[0], worldCoordinates), projectY(geom.coordinates[1], worldCoordinates), 0);
105
106
  features.push(createFeature(id, 'Point', out, properties));
106
107
  }
107
108
 
108
- function convertMultiPointFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiPoint, properties: GeoJSON.GeoJsonProperties) {
109
+ function convertMultiPointFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiPoint, worldCoordinates: boolean, properties: GeoJSON.GeoJsonProperties) {
109
110
  const out: number[] = [];
110
111
  for (const coords of geom.coordinates) {
111
- out.push(projectX(coords[0]), projectY(coords[1]), 0);
112
+ out.push(projectX(coords[0], worldCoordinates), projectY(coords[1], worldCoordinates), 0);
112
113
  }
113
114
  features.push(createFeature(id, 'MultiPoint', out, properties));
114
115
  }
115
116
 
116
- function convertLineStringFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.LineString, tolerance: number, properties: GeoJSON.GeoJsonProperties) {
117
+ function convertLineStringFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.LineString, tolerance: number, worldCoordinates: boolean, properties: GeoJSON.GeoJsonProperties) {
117
118
  const out: SliceArray = {points: []};
118
- convertLine(geom.coordinates, out, tolerance, false);
119
+ convertLine(geom.coordinates, out, tolerance, false, worldCoordinates);
119
120
  features.push(createFeature(id, 'LineString', out, properties));
120
121
  }
121
122
 
122
- function convertMultiLineStringFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiLineString, tolerance: number, options: GeoJSONVTOptions, properties: GeoJSON.GeoJsonProperties) {
123
+ function convertMultiLineStringFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiLineString, tolerance: number, worldCoordinates: boolean, options: GeoJSONVTOptions, properties: GeoJSON.GeoJsonProperties) {
123
124
  if (options.lineMetrics) {
124
125
  // explode into linestrings to be able to track metrics
125
126
  for (const line of geom.coordinates) {
126
127
  const out: SliceArray = {points: []};
127
- convertLine(line, out, tolerance, false);
128
+ convertLine(line, out, tolerance, false, worldCoordinates);
128
129
  features.push(createFeature(id, 'LineString', out, properties));
129
130
  }
130
131
  } else {
131
132
  const out: SliceArray[] = [];
132
- convertLines(geom.coordinates, out, tolerance, false);
133
+ convertLines(geom.coordinates, out, tolerance, false, worldCoordinates);
133
134
  features.push(createFeature(id, 'MultiLineString', out, properties));
134
135
  }
135
136
  }
136
137
 
137
- function convertPolygonFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.Polygon, tolerance: number, properties: GeoJSON.GeoJsonProperties) {
138
+ function convertPolygonFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.Polygon, tolerance: number, worldCoordinates: boolean, properties: GeoJSON.GeoJsonProperties) {
138
139
  const out: SliceArray[] = [];
139
- convertLines(geom.coordinates, out, tolerance, true);
140
+ convertLines(geom.coordinates, out, tolerance, true, worldCoordinates);
140
141
  features.push(createFeature(id, 'Polygon', out, properties));
141
142
  }
142
143
 
143
- function convertMultiPolygonFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiPolygon, tolerance: number, properties: GeoJSON.GeoJsonProperties) {
144
+ function convertMultiPolygonFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiPolygon, tolerance: number, worldCoordinates: boolean, properties: GeoJSON.GeoJsonProperties) {
144
145
  const out: SliceArray[][] = [];
145
146
  for (const polygon of geom.coordinates) {
146
147
  const polygonOut: SliceArray[] = [];
147
- convertLines(polygon, polygonOut, tolerance, true);
148
+ convertLines(polygon, polygonOut, tolerance, true, worldCoordinates);
149
+ if (!polygonOut.length) continue;
150
+
148
151
  out.push(polygonOut);
149
152
  }
153
+ if (!out.length) return;
154
+
150
155
  features.push(createFeature(id, 'MultiPolygon', out, properties));
151
156
  }
152
157
 
153
- function convertLine(ring: GeoJSON.Position[], out: SliceArray, tolerance: number, isPolygon: boolean) {
158
+ function convertLine(ring: GeoJSON.Position[], out: SliceArray, tolerance: number, isPolygon: boolean, worldCoordinates: boolean) {
154
159
  let x0, y0;
155
160
  let size = 0;
156
161
 
157
162
  for (let j = 0; j < ring.length; j++) {
158
- const x = projectX(ring[j][0]);
159
- const y = projectY(ring[j][1]);
163
+ const x = projectX(ring[j][0], worldCoordinates);
164
+ const y = projectY(ring[j][1], worldCoordinates);
160
165
 
161
166
  out.points.push(x, y, 0);
162
167
 
@@ -182,25 +187,27 @@ function convertLine(ring: GeoJSON.Position[], out: SliceArray, tolerance: numbe
182
187
  out.end = out.size;
183
188
  }
184
189
 
185
- function convertLines(rings: GeoJSON.Position[][], out: SliceArray[], tolerance: number, isPolygon: boolean) {
190
+ function convertLines(rings: GeoJSON.Position[][], out: SliceArray[], tolerance: number, isPolygon: boolean, worldCoordinates: boolean) {
186
191
  for (let i = 0; i < rings.length; i++) {
187
192
  const geom: SliceArray = {points: []};
188
- convertLine(rings[i], geom, tolerance, isPolygon);
193
+ convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinates);
189
194
  out.push(geom);
190
195
  }
191
196
  }
192
197
 
193
198
  /**
194
- * Convert longitude to spherical mercator in [0..1] range
199
+ * Convert longitude to spherical mercator in [0..1] range, or keep a world coordinate as it is
195
200
  */
196
- export function projectX(x: number) {
201
+ export function projectX(x: number, worldCoordinates = false) {
202
+ if (worldCoordinates) return x;
197
203
  return x / 360 + 0.5;
198
204
  }
199
205
 
200
206
  /**
201
- * Convert latitude to spherical mercator in [0..1] range
207
+ * Convert latitude to spherical mercator in [0..1] range, or keep a world coordinate as it is
202
208
  */
203
- export function projectY(y: number) {
209
+ export function projectY(y: number, worldCoordinates = false) {
210
+ if (worldCoordinates) return y;
204
211
  const sin = Math.sin(y * Math.PI / 180);
205
212
  const y2 = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
206
213
  return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
package/src/deconvert.ts CHANGED
@@ -3,10 +3,10 @@ import type {GeoJSONVTInternalFeature} from './definitions';
3
3
  /**
4
4
  * Converts internal source features back to GeoJSON format.
5
5
  */
6
- export function convertToGeoJSON(source: GeoJSONVTInternalFeature[]): GeoJSON.GeoJSON {
6
+ export function convertToGeoJSON(source: GeoJSONVTInternalFeature[], worldCoordinates = false): GeoJSON.GeoJSON {
7
7
  const geojson: GeoJSON.GeoJSON = {
8
8
  type: 'FeatureCollection',
9
- features: source.map(feature => featureToGeoJSON(feature))
9
+ features: source.map(feature => featureToGeoJSON(feature, worldCoordinates))
10
10
  };
11
11
 
12
12
  return geojson;
@@ -15,10 +15,10 @@ export function convertToGeoJSON(source: GeoJSONVTInternalFeature[]): GeoJSON.Ge
15
15
  /**
16
16
  * Converts a single internal feature to GeoJSON format.
17
17
  */
18
- export function featureToGeoJSON(feature: GeoJSONVTInternalFeature): GeoJSON.Feature {
18
+ export function featureToGeoJSON(feature: GeoJSONVTInternalFeature, worldCoordinates = false): GeoJSON.Feature {
19
19
  const geojsonFeature: GeoJSON.Feature = {
20
20
  type: 'Feature',
21
- geometry: geometryToGeoJSON(feature),
21
+ geometry: geometryToGeoJSON(feature, worldCoordinates),
22
22
  properties: feature.tags
23
23
  };
24
24
  if (feature.id != null) {
@@ -31,66 +31,68 @@ export function featureToGeoJSON(feature: GeoJSONVTInternalFeature): GeoJSON.Fea
31
31
  /**
32
32
  * Converts a single internal feature geometry to GeoJSON format.
33
33
  */
34
- function geometryToGeoJSON(feature: GeoJSONVTInternalFeature): GeoJSON.Geometry {
34
+ function geometryToGeoJSON(feature: GeoJSONVTInternalFeature, worldCoordinates: boolean): GeoJSON.Geometry {
35
35
  const {type, geometry} = feature;
36
36
 
37
37
  switch (type) {
38
38
  case 'Point':
39
39
  return {
40
40
  type: type,
41
- coordinates: unprojectPoint(geometry[0], geometry[1])
41
+ coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinates)
42
42
  };
43
43
  case 'MultiPoint':
44
44
  return {
45
45
  type: type,
46
- coordinates: unprojectPoints(geometry)
46
+ coordinates: unprojectPoints(geometry, worldCoordinates)
47
47
  };
48
48
  case 'LineString':
49
49
  return {
50
50
  type: type,
51
- coordinates: unprojectPoints(geometry.points)
51
+ coordinates: unprojectPoints(geometry.points, worldCoordinates)
52
52
  };
53
53
  case 'MultiLineString':
54
54
  case 'Polygon':
55
55
  return {
56
56
  type: type,
57
- coordinates: geometry.map(ring => unprojectPoints(ring.points))
57
+ coordinates: geometry.map(ring => unprojectPoints(ring.points, worldCoordinates))
58
58
  };
59
59
  case 'MultiPolygon':
60
60
  return {
61
61
  type: type,
62
62
  coordinates: geometry.map(polygon =>
63
- polygon.map(ring => unprojectPoints(ring.points))
63
+ polygon.map(ring => unprojectPoints(ring.points, worldCoordinates))
64
64
  )
65
65
  };
66
66
  }
67
67
  }
68
68
 
69
- export function unprojectPoints(coords: number[] | Float64Array): GeoJSON.Position[] {
69
+ export function unprojectPoints(coords: number[] | Float64Array, worldCoordinates = false): GeoJSON.Position[] {
70
70
  const result: GeoJSON.Position[] = [];
71
71
 
72
72
  for (let i = 0; i < coords.length; i += 3) {
73
- result.push(unprojectPoint(coords[i], coords[i + 1]));
73
+ result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinates));
74
74
  }
75
75
 
76
76
  return result;
77
77
  }
78
78
 
79
- function unprojectPoint(x: number, y: number): GeoJSON.Position {
80
- return [unprojectX(x), unprojectY(y)];
79
+ function unprojectPoint(x: number, y: number, worldCoordinates: boolean): GeoJSON.Position {
80
+ return [unprojectX(x, worldCoordinates), unprojectY(y, worldCoordinates)];
81
81
  }
82
82
 
83
83
  /**
84
- * Convert spherical mercator in [0..1] range to longitude
84
+ * Convert spherical mercator in [0..1] range to longitude, or keep a world coordinate as it is
85
85
  */
86
- export function unprojectX(x: number): number {
86
+ export function unprojectX(x: number, worldCoordinates = false): number {
87
+ if (worldCoordinates) return x;
87
88
  return (x - 0.5) * 360;
88
89
  }
89
90
 
90
91
  /**
91
- * Convert spherical mercator in [0..1] range to latitude
92
+ * Convert spherical mercator in [0..1] range to latitude, or keep a world coordinate as it is
92
93
  */
93
- export function unprojectY(y: number): number {
94
+ export function unprojectY(y: number, worldCoordinates = false): number {
95
+ if (worldCoordinates) return y;
94
96
  const y2 = (180 - y * 360) * Math.PI / 180;
95
97
  return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
96
98
  }
@@ -54,7 +54,18 @@ export type GeoJSONVTOptions = {
54
54
  */
55
55
  debug?: number;
56
56
  /**
57
- * Enable Supercluster for point features.
57
+ * Whether positions are world coordinates instead of lng/lat: x and y from 0 to 1 across the world square, y growing
58
+ * downward, where web mercator would put a lng/lat. The world is then a square that ends at its edges, for data in a
59
+ * world of its own such as a map projection's: positions are neither projected nor wrapped around or clamped,
60
+ * features are cut at the tile buffer around the square, points outside it are left out of the clusters, `getTile`
61
+ * returns `null` outside it, and `getData` and the cluster methods return world coordinates too.
62
+ * @experimental
63
+ * @default false
64
+ */
65
+ worldCoordinates?: boolean;
66
+ /**
67
+ * Enable Supercluster for point features. Each position of a MultiPoint feature is clustered as a point of its own.
68
+ * Other geometry types are left out.
58
69
  * @default false
59
70
  */
60
71
  cluster?: boolean;
@@ -67,7 +78,7 @@ export type GeoJSONVTOptions = {
67
78
 
68
79
  export type GeoJSONToTileOptions = GeoJSONVTOptions & {
69
80
  /**
70
- * Whether to wrap features around the antimeridian
81
+ * Whether to wrap features around the antimeridian. Has no effect with `worldCoordinates`.
71
82
  * @default false
72
83
  */
73
84
  wrap?: boolean;
@@ -214,12 +225,12 @@ export interface GeoJSONVTTileIndex {
214
225
 
215
226
  export type SuperclusterOptions = {
216
227
  /**
217
- * Min zoom to generate clusters on
228
+ * Min zoom to generate clusters on. A value above 30 is limited to 30.
218
229
  * @default 0
219
230
  */
220
231
  minZoom?: number;
221
232
  /**
222
- * Max zoom level to cluster the points on
233
+ * Max zoom level to cluster the points on. A value above 30 is limited to 30.
223
234
  * @default 16
224
235
  */
225
236
  maxZoom?: number;
@@ -249,7 +260,8 @@ export type SuperclusterOptions = {
249
260
  */
250
261
  log?: boolean;
251
262
  /**
252
- * Whether to generate numeric ids for input features (in vector tiles)
263
+ * Whether to generate numeric ids for input features (in vector tiles). Each position of a MultiPoint feature
264
+ * gets an id of its own.
253
265
  * @default false
254
266
  */
255
267
  generateId?: boolean;
@@ -50,6 +50,24 @@ test('applySourceDiff: adds a feature using the promoteId', () => {
50
50
  expect(source[0].id).toBe('point2');
51
51
  });
52
52
 
53
+ test('applySourceDiff: adds a feature without properties when promoteId is set', () => {
54
+ const point = {
55
+ type: 'Feature' as const,
56
+ geometry: {
57
+ type: 'Point' as const,
58
+ coordinates: [0, 0],
59
+ },
60
+ properties: null as GeoJSON.GeoJsonProperties,
61
+ };
62
+
63
+ const {source} = applySourceDiff([], {
64
+ add: [point]
65
+ }, {promoteId: 'promoteId'});
66
+
67
+ expect(source.length).toBe(1);
68
+ expect(source[0].geometry).toEqual([0.5, 0.5, 0]);
69
+ });
70
+
53
71
  test('applySourceDiff: removes a feature by its id', () => {
54
72
  const point = {
55
73
  type: 'Point' as const,
@@ -141,6 +159,59 @@ test('applySourceDiff: updates a feature geometry', () => {
141
159
  expect((source[0].geometry as number[])[1]).toBe(0.5);
142
160
  });
143
161
 
162
+ test('applySourceDiff: keeps a generated id when updating a feature geometry', () => {
163
+ const point = {
164
+ type: 'Point' as const,
165
+ id: 1,
166
+ geometry: [0, 0],
167
+ tags: {},
168
+ minX: 0,
169
+ minY: 0,
170
+ maxX: 0,
171
+ maxY: 0
172
+ };
173
+
174
+ const {source} = applySourceDiff([point], {
175
+ update: [{
176
+ id: 1,
177
+ newGeometry: {
178
+ type: 'Point',
179
+ coordinates: [1, 0]
180
+ }
181
+ }]
182
+ }, {...options, generateId: true});
183
+
184
+ expect(source.length).toBe(1);
185
+ expect(source[0].id).toBe(1);
186
+ });
187
+
188
+ test('applySourceDiff: keeps a promoted id when updating a feature geometry and the promoted property', () => {
189
+ const point = {
190
+ type: 'Point' as const,
191
+ id: 'point',
192
+ geometry: [0, 0],
193
+ tags: {name: 'point'},
194
+ minX: 0,
195
+ minY: 0,
196
+ maxX: 0,
197
+ maxY: 0
198
+ };
199
+
200
+ const {source} = applySourceDiff([point], {
201
+ update: [{
202
+ id: 'point',
203
+ newGeometry: {
204
+ type: 'Point',
205
+ coordinates: [1, 0]
206
+ },
207
+ addOrUpdateProperties: [{key: 'name', value: 'renamed'}]
208
+ }]
209
+ }, {...options, promoteId: 'name'});
210
+
211
+ expect(source.length).toBe(1);
212
+ expect(source[0].id).toBe('point');
213
+ });
214
+
144
215
  test('applySourceDiff: adds properties', () => {
145
216
  const point = {
146
217
  type: 'Point' as const,
package/src/difference.ts CHANGED
@@ -138,7 +138,7 @@ export function applySourceDiff(source: GeoJSONVTInternalFeature[], dataDiff: Ge
138
138
  * @param vtFeatures - the original features
139
139
  * @param update - the update object to apply
140
140
  * @param options - the options to use for the wrap method
141
- * @returns Updated features. If geometry is updated, returns new feature(s) converted from geojson and wrapped. If only properties are updated, returns feature(s) with tags updated.
141
+ * @returns Updated features. If geometry is updated, returns new feature(s) converted from geojson and wrapped, with the id the feature already has. If only properties are updated, returns feature(s) with tags updated.
142
142
  */
143
143
  function getUpdatedFeatures(vtFeatures: GeoJSONVTInternalFeature[], update: GeoJSONVTFeatureDiff, options: GeoJSONVTOptions): GeoJSONVTInternalFeature[] {
144
144
  const changeGeometry = !!update.newGeometry;
@@ -157,7 +157,7 @@ function getUpdatedFeatures(vtFeatures: GeoJSONVTInternalFeature[], update: GeoJ
157
157
  properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
158
158
  };
159
159
 
160
- let features = convertToInternal({type: 'FeatureCollection', features: [geojsonFeature]}, options);
160
+ let features = convertToInternal({type: 'FeatureCollection', features: [geojsonFeature]}, {...options, promoteId: undefined, generateId: false});
161
161
  features = wrap(features, options);
162
162
  return features;
163
163
  }
@@ -213,7 +213,7 @@ export function diffToHashed(diff: GeoJSONVTSourceDiff, options: GeoJSONVTOption
213
213
  const hashed: HashedGeoJSONVTSourceDiff = {
214
214
  removeAll: diff.removeAll,
215
215
  remove: new Set(diff.remove || []),
216
- add: new Map(diff.add?.map(feature => [options.promoteId ? feature.properties[options.promoteId] : feature.id, feature])),
216
+ add: new Map(diff.add?.map(feature => [options.promoteId ? feature.properties?.[options.promoteId] : feature.id, feature])),
217
217
  update: new Map(diff.update?.map(update => [update.id, update]))
218
218
  };
219
219
 
@@ -36,4 +36,239 @@ test('publicly exposed cluster methods: return undefined when clustering is disa
36
36
  expect(index.getClusterExpansionZoom(123)).toBeNull();
37
37
  expect(index.getClusterChildren(123)).toBeNull();
38
38
  expect(index.getClusterLeaves(123, 10, 0)).toBeNull();
39
- });
39
+ });
40
+
41
+ test('worldCoordinates cuts a line at the buffer around the world instead of wrapping or clamping it', () => {
42
+ const fromThreeWorldsNorthToEast = {type: 'LineString' as const, coordinates: [[0.25, -3], [0.75, 0.5], [1.25, 0.5]]};
43
+
44
+ const index = new GeoJSONVT(fromThreeWorldsNorthToEast, {worldCoordinates: true});
45
+
46
+ expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[[2770, -64], [3072, 2048], [4160, 2048]]]]);
47
+ });
48
+
49
+ test('worldCoordinates cuts the top tile at the world again on update, so a feature can move in from outside', () => {
50
+ const movingIn = {type: 'Feature' as const, id: 'moving in', geometry: {type: 'Point' as const, coordinates: [1.25, 0.5]}, properties: {}};
51
+ const stayingOut = {type: 'Feature' as const, id: 'staying out', geometry: {type: 'Point' as const, coordinates: [1.5, 0.5]}, properties: {}};
52
+ const index = new GeoJSONVT({type: 'FeatureCollection', features: [movingIn, stayingOut]}, {worldCoordinates: true, updateable: true});
53
+
54
+ index.updateData({update: [{id: 'moving in', newGeometry: {type: 'Point', coordinates: [0.5, 0.5]}}]});
55
+
56
+ expect(index.getTile(0, 0, 0).features.map(feature => feature.id)).toEqual(['moving in']);
57
+ });
58
+
59
+ test('worldCoordinates returns the data in world coordinates', () => {
60
+ const features: GeoJSON.Feature[] = [
61
+ {type: 'Feature', id: 'point', geometry: {type: 'Point', coordinates: [0.25, 0.75]}, properties: {}},
62
+ {type: 'Feature', id: 'multi point', geometry: {type: 'MultiPoint', coordinates: [[0.25, 0.75], [0.5, 0.5]]}, properties: {}},
63
+ {type: 'Feature', id: 'line', geometry: {type: 'LineString', coordinates: [[0.25, 0.75], [0.5, 0.5]]}, properties: {}},
64
+ {type: 'Feature', id: 'multi line', geometry: {type: 'MultiLineString', coordinates: [[[0.25, 0.75], [0.5, 0.5]], [[0.5, 0.25], [0.75, 0.25]]]}, properties: {}},
65
+ {type: 'Feature', id: 'polygon', geometry: {type: 'Polygon', coordinates: [[[0.25, 0.25], [0.75, 0.25], [0.75, 0.75], [0.25, 0.25]]]}, properties: {}},
66
+ {type: 'Feature', id: 'multi polygon', geometry: {type: 'MultiPolygon', coordinates: [[[[0.25, 0.25], [0.75, 0.25], [0.75, 0.75], [0.25, 0.25]]]]}, properties: {}}
67
+ ];
68
+ const index = new GeoJSONVT({type: 'FeatureCollection', features}, {worldCoordinates: true, updateable: true});
69
+
70
+ expect(index.getData()).toEqual({type: 'FeatureCollection', features});
71
+ });
72
+
73
+ test('worldCoordinates keeps a MultiLineString it splits for lineMetrics in world coordinates', () => {
74
+ const multiLine = {type: 'MultiLineString' as const, coordinates: [[[0.25, 0.75], [0.5, 0.5]], [[0.5, 0.25], [0.75, 0.25]]]};
75
+ const index = new GeoJSONVT(multiLine, {worldCoordinates: true, lineMetrics: true, updateable: true});
76
+
77
+ expect(index.getData()).toEqual({type: 'FeatureCollection', features: [
78
+ {type: 'Feature', geometry: {type: 'LineString', coordinates: [[0.25, 0.75], [0.5, 0.5]]}},
79
+ {type: 'Feature', geometry: {type: 'LineString', coordinates: [[0.5, 0.25], [0.75, 0.25]]}}
80
+ ]});
81
+ });
82
+
83
+ test('worldCoordinates hands the update filter features in world coordinates', () => {
84
+ const point = {type: 'Feature' as const, id: 'p', geometry: {type: 'Point' as const, coordinates: [0.25, 0.75]}, properties: {}};
85
+ const index = new GeoJSONVT({type: 'FeatureCollection', features: [point]}, {worldCoordinates: true, updateable: true});
86
+ const filtered: GeoJSON.Geometry[] = [];
87
+
88
+ index.updateData({add: [{...point, id: 'q'}]}, feature => {
89
+ filtered.push(feature.geometry);
90
+ return true;
91
+ });
92
+
93
+ expect(filtered).toContainEqual({type: 'Point', coordinates: [0.25, 0.75]});
94
+ });
95
+
96
+ test('worldCoordinates leaves points outside the world out of the clusters', () => {
97
+ const data = {
98
+ type: 'FeatureCollection' as const,
99
+ features: [
100
+ {type: 'Feature' as const, id: 'inside', geometry: {type: 'Point' as const, coordinates: [0.99, 0.5]}, properties: {}},
101
+ {type: 'Feature' as const, id: 'east', geometry: {type: 'Point' as const, coordinates: [1.03, 0.5]}, properties: {}},
102
+ {type: 'Feature' as const, id: 'north', geometry: {type: 'Point' as const, coordinates: [0.5, -0.03]}, properties: {}}
103
+ ]
104
+ };
105
+
106
+ const index = new GeoJSONVT(data, {worldCoordinates: true, cluster: true});
107
+
108
+ expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[507, 256]]]);
109
+ });
110
+
111
+ test('worldCoordinates keeps points at either edge out of the clustered tile at the other edge', () => {
112
+ const data = {
113
+ type: 'FeatureCollection' as const,
114
+ features: [
115
+ {type: 'Feature' as const, id: 'west', geometry: {type: 'Point' as const, coordinates: [0.001, 0.5]}, properties: {}},
116
+ {type: 'Feature' as const, id: 'east', geometry: {type: 'Point' as const, coordinates: [0.999, 0.5]}, properties: {}}
117
+ ]
118
+ };
119
+
120
+ const index = new GeoJSONVT(data, {worldCoordinates: true, cluster: true});
121
+
122
+ expect(index.getTile(0, 0, 0).features).toHaveLength(2);
123
+ });
124
+
125
+ test('worldCoordinates returns cluster children and leaves in world coordinates', () => {
126
+ const points = {
127
+ type: 'FeatureCollection' as const,
128
+ features: Array.from({length: 20}, (_, i) => ({
129
+ type: 'Feature' as const,
130
+ geometry: {type: 'Point' as const, coordinates: [0.5 + i * 0.000001, 0.5]},
131
+ properties: {name: `Point ${i}`}
132
+ }))
133
+ };
134
+ const index = new GeoJSONVT(points, {worldCoordinates: true, cluster: true, clusterOptions: {radius: 100}});
135
+ const cluster = index.getTile(0, 0, 0).features.find(f => (f.tags as {cluster?: boolean})?.cluster);
136
+ const clusterId = (cluster.tags as {cluster_id: number}).cluster_id;
137
+
138
+ const childPositions = index.getClusterChildren(clusterId).map(child => (child.geometry as GeoJSON.Point).coordinates);
139
+ const leafPositions = index.getClusterLeaves(clusterId, 20, 0).map(leaf => (leaf.geometry as GeoJSON.Point).coordinates);
140
+
141
+ expect(childPositions.every(([x, y]) => Math.abs(x - 0.50001) < 0.00001 && y === 0.5)).toBe(true);
142
+ expect(leafPositions).toEqual(expect.arrayContaining(points.features.map(point => point.geometry.coordinates)));
143
+ });
144
+
145
+ test('worldCoordinates returns no tile for a column on either side of the world', () => {
146
+ const inTheTwoTopTiles = {type: 'MultiPoint' as const, coordinates: [[0.25, 0.25], [0.75, 0.25]]};
147
+
148
+ const index = new GeoJSONVT(inTheTwoTopTiles, {worldCoordinates: true});
149
+
150
+ expect([index.getTile(1, -1, 0), index.getTile(1, 2, 0)]).toEqual([null, null]);
151
+ });
152
+
153
+ const weightedPoints = {
154
+ type: 'FeatureCollection' as const,
155
+ features: [[0, 1], [0.01, 2], [100, 10], [100.01, 20]].map(([lng, weight]) => ({
156
+ type: 'Feature' as const,
157
+ geometry: {type: 'Point' as const, coordinates: [lng, 0]},
158
+ properties: {weight}
159
+ }))
160
+ };
161
+
162
+ const sumWeights = {
163
+ map: (props: GeoJSON.GeoJsonProperties) => ({sum: (props as {weight: number}).weight}),
164
+ reduce: (a: Record<string, unknown>, b: Record<string, unknown>) => { (a as {sum: number}).sum += (b as {sum: number}).sum; }
165
+ };
166
+
167
+ test('updateClusterOptions aggregates cluster properties when it adds a reduce function', () => {
168
+ const index = new GeoJSONVT(weightedPoints, {
169
+ updateable: true,
170
+ cluster: true,
171
+ clusterOptions: {maxZoom: 2}
172
+ });
173
+
174
+ index.updateClusterOptions(true, {maxZoom: 2, ...sumWeights});
175
+
176
+ expect(index.getTile(0, 0, 0).features.map(f => (f.tags as {sum?: number}).sum)).toEqual([3, 30]);
177
+ });
178
+
179
+ test('updateClusterOptions stops aggregating cluster properties when it removes the reduce function', () => {
180
+ const index = new GeoJSONVT(weightedPoints, {
181
+ updateable: true,
182
+ cluster: true,
183
+ clusterOptions: {maxZoom: 2, ...sumWeights}
184
+ });
185
+
186
+ index.updateClusterOptions(true, {maxZoom: 2});
187
+
188
+ expect(index.getTile(0, 0, 0).features.map(f => (f.tags as {sum?: number}).sum)).toEqual([undefined, undefined]);
189
+ });
190
+
191
+ test('skips an empty polygon of a MultiPolygon', () => {
192
+ const multiPolygon = {
193
+ type: 'MultiPolygon' as const,
194
+ coordinates: [[[[0, 0], [10, 0], [10, 10], [0, 0]]], []]
195
+ };
196
+
197
+ const index = new GeoJSONVT(multiPolygon, {maxZoom: 5});
198
+
199
+ expect(index.getTile(0, 0, 0).features[0].geometry).toEqual([[[2048, 2048], [2162, 1934], [2162, 2048], [2048, 2048]]]);
200
+ });
201
+
202
+ test('leaves out a MultiPolygon that has only empty polygons', () => {
203
+ const multiPolygon = {
204
+ type: 'MultiPolygon' as const,
205
+ coordinates: [[]] as GeoJSON.Position[][][]
206
+ };
207
+
208
+ const index = new GeoJSONVT(multiPolygon, {updateable: true});
209
+
210
+ expect(index.getData()).toEqual({type: 'FeatureCollection', features: []});
211
+ });
212
+
213
+ test('keeps the corner of a polygon whose vertex lies on the left edge of a tile', () => {
214
+ const polygon = {
215
+ type: 'Polygon' as const,
216
+ coordinates: [[[-90, -90], [0, -90], [90, -90], [0, 0], [-90, -90]]]
217
+ };
218
+
219
+ const index = new GeoJSONVT(polygon, {indexMaxZoom: 0, maxZoom: 24, tolerance: 1.5, buffer: 0});
220
+
221
+ expect(index.getTile(1, 1, 1).features[0].geometry).toEqual([[[0, 4096], [0, 0], [0, 0], [2048, 4096], [0, 4096]]]);
222
+ });
223
+
224
+ test('keeps the corner of a polygon whose vertex lies on the right edge of a tile', () => {
225
+ const polygon = {
226
+ type: 'Polygon' as const,
227
+ coordinates: [[[20, 34.365234375], [80, 4.34326171875], [45, 4.34326171875], [20, 4.34326171875], [20, 34.365234375]]]
228
+ };
229
+
230
+ const index = new GeoJSONVT(polygon, {maxZoom: 5, buffer: 0});
231
+
232
+ expect(index.getTile(3, 4, 3).features[0].geometry).toEqual([[[1820, 762], [4096, 1986], [4096, 3700], [1820, 3700], [1820, 762]]]);
233
+ });
234
+
235
+ test('keeps the part of a line that enters a tile exactly at a vertex', () => {
236
+ const line = {
237
+ type: 'LineString' as const,
238
+ coordinates: [[-90, 20], [0, 20], [90, 20]]
239
+ };
240
+
241
+ const index = new GeoJSONVT(line, {lineMetrics: true, buffer: 0});
242
+
243
+ const feature = index.getTile(1, 1, 0).features[0];
244
+ expect(feature.geometry).toEqual([[[0, 3631], [2048, 3631]]]);
245
+ expect(feature.tags).toEqual({geojsonvt_clip_start: 0.5, geojsonvt_clip_end: 1});
246
+ });
247
+
248
+ test('clusters each position of a MultiPoint feature when clustering is on', () => {
249
+ const multiPoint = {
250
+ type: 'Feature' as const,
251
+ id: 7,
252
+ properties: {name: 'multi'},
253
+ geometry: {type: 'MultiPoint' as const, coordinates: [[20, 20], [20.0001, 20], [60, 20]]}
254
+ };
255
+
256
+ const index = new GeoJSONVT(multiPoint, {cluster: true});
257
+
258
+ expect(index.getTile(0, 0, 0).features).toEqual([
259
+ {type: 1, geometry: [[284, 227]], tags: {cluster: true, cluster_id: 20, point_count: 2, point_count_abbreviated: 2}, id: 20},
260
+ {type: 1, geometry: [[341, 227]], tags: {name: 'multi'}, id: 7}
261
+ ]);
262
+ });
263
+
264
+ test('worldCoordinates leaves the positions of a MultiPoint outside the world out of the clusters', () => {
265
+ const multiPoint = {
266
+ type: 'Feature' as const,
267
+ properties: {},
268
+ geometry: {type: 'MultiPoint' as const, coordinates: [[0.99, 0.5], [1.03, 0.5], [0.5, -0.03]]}
269
+ };
270
+
271
+ const index = new GeoJSONVT(multiPoint, {worldCoordinates: true, cluster: true});
272
+
273
+ expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[507, 256]]]);
274
+ });