@maplibre/geojson-vt 6.1.2 → 6.3.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 (41) hide show
  1. package/README.md +8 -0
  2. package/dist/cluster-tile-index.d.ts +43 -5
  3. package/dist/cluster-tile-index.d.ts.map +1 -1
  4. package/dist/convert.d.ts +2 -9
  5. package/dist/convert.d.ts.map +1 -1
  6. package/dist/deconvert.d.ts +4 -11
  7. package/dist/deconvert.d.ts.map +1 -1
  8. package/dist/definitions.d.ts +47 -7
  9. package/dist/definitions.d.ts.map +1 -1
  10. package/dist/difference.d.ts +2 -1
  11. package/dist/difference.d.ts.map +1 -1
  12. package/dist/geojson-vt-dev.js +462 -181
  13. package/dist/geojson-vt.js +1 -1
  14. package/dist/geojson-vt.mjs +459 -180
  15. package/dist/geojson-vt.mjs.map +1 -1
  16. package/dist/geojsonvt.d.ts +8 -4
  17. package/dist/geojsonvt.d.ts.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/tile-index.d.ts +7 -0
  21. package/dist/tile-index.d.ts.map +1 -1
  22. package/dist/world-coordinate-helper.d.ts +13 -0
  23. package/dist/world-coordinate-helper.d.ts.map +1 -0
  24. package/dist/wrap.d.ts.map +1 -1
  25. package/package.json +5 -5
  26. package/src/clip.ts +2 -2
  27. package/src/cluster-tile-index.test.ts +123 -0
  28. package/src/cluster-tile-index.ts +270 -99
  29. package/src/convert.ts +49 -44
  30. package/src/deconvert.test.ts +4 -4
  31. package/src/deconvert.ts +15 -29
  32. package/src/definitions.ts +48 -7
  33. package/src/difference.test.ts +71 -0
  34. package/src/difference.ts +50 -21
  35. package/src/geojson-to-tile.ts +2 -2
  36. package/src/geojsonvt.test.ts +387 -1
  37. package/src/geojsonvt.ts +25 -10
  38. package/src/index.ts +2 -1
  39. package/src/tile-index.ts +19 -3
  40. package/src/world-coordinate-helper.ts +63 -0
  41. package/src/wrap.ts +2 -0
package/src/convert.ts CHANGED
@@ -1,6 +1,7 @@
1
1
 
2
2
  import {simplify} from './simplify';
3
3
  import {createFeature, optimizeLineMemory} from './feature';
4
+ import {getWorldCoordinateHelper, type WorldCoordinateHelper} from './world-coordinate-helper';
4
5
  import type {GeoJSONVTInternalFeature, GeoJSONVTOptions, SliceArray} from './definitions';
5
6
 
6
7
  const MAX_GEOMETRY_COLLECTION_DEPTH = 1024;
@@ -9,27 +10,42 @@ const MAX_GEOMETRY_COLLECTION_DEPTH = 1024;
9
10
  * converts GeoJSON to internal source features (an intermediate projected JSON vector format with simplification data)
10
11
  * @param data
11
12
  * @param options
13
+ * @param unlocated - the features without coordinates, by id: a feature with an id that converts to nothing is set in it, and one that converts to something is deleted from it
12
14
  * @returns
13
15
  */
14
- export function convertToInternal(data: GeoJSON.GeoJSON, options: GeoJSONVTOptions): GeoJSONVTInternalFeature[] {
16
+ export function convertToInternal(data: GeoJSON.GeoJSON, options: GeoJSONVTOptions, unlocated?: Map<string | number, GeoJSON.Feature>): GeoJSONVTInternalFeature[] {
15
17
  const features: GeoJSONVTInternalFeature[] = [];
16
18
 
17
19
  switch (data.type) {
18
20
  case 'FeatureCollection':
19
21
  for (let i = 0; i < data.features.length; i++) {
20
- featureToInternal(features, data.features[i], options, i);
22
+ convertFeature(features, data.features[i], options, unlocated, i);
21
23
  }
22
24
  break;
23
25
  case 'Feature':
24
- featureToInternal(features, data, options);
26
+ convertFeature(features, data, options, unlocated);
25
27
  break;
26
28
  default:
27
- featureToInternal(features, {type: "Feature" as const, geometry: data, properties: undefined}, options);
29
+ convertFeature(features, {type: "Feature" as const, geometry: data, properties: undefined}, options, unlocated);
28
30
  }
29
31
 
30
32
  return features;
31
33
  }
32
34
 
35
+ function convertFeature(features: GeoJSONVTInternalFeature[], geojson: GeoJSON.Feature, options: GeoJSONVTOptions, unlocated?: Map<string | number, GeoJSON.Feature>, index?: number) {
36
+ const count = features.length;
37
+ featureToInternal(features, geojson, options, index);
38
+ if (!unlocated) return;
39
+
40
+ const id = getFeatureId(geojson, options, index);
41
+ if (id == null) return;
42
+ if (features.length > count) {
43
+ unlocated.delete(id);
44
+ } else {
45
+ unlocated.set(id, {...geojson, id});
46
+ }
47
+ }
48
+
33
49
  function featureToInternal(features: GeoJSONVTInternalFeature[], geojson: GeoJSON.Feature, options: GeoJSONVTOptions, index?: number, depth = 0) {
34
50
  if (!geojson.geometry) return;
35
51
 
@@ -47,30 +63,31 @@ function featureToInternal(features: GeoJSONVTInternalFeature[], geojson: GeoJSO
47
63
 
48
64
  const id = getFeatureId(geojson, options, index);
49
65
  const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
66
+ const worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
50
67
 
51
68
  switch (geojson.geometry.type) {
52
69
  case 'Point':
53
- convertPointFeature(features, id, geojson.geometry, geojson.properties);
70
+ convertPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
54
71
  return;
55
72
 
56
73
  case 'MultiPoint':
57
- convertMultiPointFeature(features, id, geojson.geometry, geojson.properties);
74
+ convertMultiPointFeature(features, id, geojson.geometry, worldCoordinateHelper, geojson.properties);
58
75
  return;
59
76
 
60
77
  case 'LineString':
61
- convertLineStringFeature(features, id, geojson.geometry, tolerance, geojson.properties);
78
+ convertLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
62
79
  return;
63
80
 
64
81
  case 'MultiLineString':
65
- convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, options, geojson.properties);
82
+ convertMultiLineStringFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, options, geojson.properties);
66
83
  return;
67
84
 
68
85
  case 'Polygon':
69
- convertPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
86
+ convertPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
70
87
  return;
71
88
 
72
89
  case 'MultiPolygon':
73
- convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, geojson.properties);
90
+ convertMultiPolygonFeature(features, id, geojson.geometry, tolerance, worldCoordinateHelper, geojson.properties);
74
91
  return;
75
92
 
76
93
  default:
@@ -99,64 +116,68 @@ function convertGeometryCollection(features: GeoJSONVTInternalFeature[], geojson
99
116
  }
100
117
  }
101
118
 
102
- function convertPointFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.Point, properties: GeoJSON.GeoJsonProperties) {
119
+ function convertPointFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.Point, worldCoordinateHelper: WorldCoordinateHelper, properties: GeoJSON.GeoJsonProperties) {
103
120
  const out: number[] = [];
104
- out.push(projectX(geom.coordinates[0]), projectY(geom.coordinates[1]), 0);
121
+ out.push(worldCoordinateHelper.projectX(geom.coordinates[0]), worldCoordinateHelper.projectY(geom.coordinates[1]), 0);
105
122
  features.push(createFeature(id, 'Point', out, properties));
106
123
  }
107
124
 
108
- function convertMultiPointFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiPoint, properties: GeoJSON.GeoJsonProperties) {
125
+ function convertMultiPointFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiPoint, worldCoordinateHelper: WorldCoordinateHelper, properties: GeoJSON.GeoJsonProperties) {
109
126
  const out: number[] = [];
110
127
  for (const coords of geom.coordinates) {
111
- out.push(projectX(coords[0]), projectY(coords[1]), 0);
128
+ out.push(worldCoordinateHelper.projectX(coords[0]), worldCoordinateHelper.projectY(coords[1]), 0);
112
129
  }
113
130
  features.push(createFeature(id, 'MultiPoint', out, properties));
114
131
  }
115
132
 
116
- function convertLineStringFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.LineString, tolerance: number, properties: GeoJSON.GeoJsonProperties) {
133
+ function convertLineStringFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.LineString, tolerance: number, worldCoordinateHelper: WorldCoordinateHelper, properties: GeoJSON.GeoJsonProperties) {
117
134
  const out: SliceArray = {points: []};
118
- convertLine(geom.coordinates, out, tolerance, false);
135
+ convertLine(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
119
136
  features.push(createFeature(id, 'LineString', out, properties));
120
137
  }
121
138
 
122
- function convertMultiLineStringFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiLineString, tolerance: number, options: GeoJSONVTOptions, properties: GeoJSON.GeoJsonProperties) {
139
+ function convertMultiLineStringFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiLineString, tolerance: number, worldCoordinateHelper: WorldCoordinateHelper, options: GeoJSONVTOptions, properties: GeoJSON.GeoJsonProperties) {
123
140
  if (options.lineMetrics) {
124
141
  // explode into linestrings to be able to track metrics
125
142
  for (const line of geom.coordinates) {
126
143
  const out: SliceArray = {points: []};
127
- convertLine(line, out, tolerance, false);
144
+ convertLine(line, out, tolerance, false, worldCoordinateHelper);
128
145
  features.push(createFeature(id, 'LineString', out, properties));
129
146
  }
130
147
  } else {
131
148
  const out: SliceArray[] = [];
132
- convertLines(geom.coordinates, out, tolerance, false);
149
+ convertLines(geom.coordinates, out, tolerance, false, worldCoordinateHelper);
133
150
  features.push(createFeature(id, 'MultiLineString', out, properties));
134
151
  }
135
152
  }
136
153
 
137
- function convertPolygonFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.Polygon, tolerance: number, properties: GeoJSON.GeoJsonProperties) {
154
+ function convertPolygonFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.Polygon, tolerance: number, worldCoordinateHelper: WorldCoordinateHelper, properties: GeoJSON.GeoJsonProperties) {
138
155
  const out: SliceArray[] = [];
139
- convertLines(geom.coordinates, out, tolerance, true);
156
+ convertLines(geom.coordinates, out, tolerance, true, worldCoordinateHelper);
140
157
  features.push(createFeature(id, 'Polygon', out, properties));
141
158
  }
142
159
 
143
- function convertMultiPolygonFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiPolygon, tolerance: number, properties: GeoJSON.GeoJsonProperties) {
160
+ function convertMultiPolygonFeature(features: GeoJSONVTInternalFeature[], id: number | string | undefined, geom: GeoJSON.MultiPolygon, tolerance: number, worldCoordinateHelper: WorldCoordinateHelper, properties: GeoJSON.GeoJsonProperties) {
144
161
  const out: SliceArray[][] = [];
145
162
  for (const polygon of geom.coordinates) {
146
163
  const polygonOut: SliceArray[] = [];
147
- convertLines(polygon, polygonOut, tolerance, true);
164
+ convertLines(polygon, polygonOut, tolerance, true, worldCoordinateHelper);
165
+ if (!polygonOut.length) continue;
166
+
148
167
  out.push(polygonOut);
149
168
  }
169
+ if (!out.length) return;
170
+
150
171
  features.push(createFeature(id, 'MultiPolygon', out, properties));
151
172
  }
152
173
 
153
- function convertLine(ring: GeoJSON.Position[], out: SliceArray, tolerance: number, isPolygon: boolean) {
174
+ function convertLine(ring: GeoJSON.Position[], out: SliceArray, tolerance: number, isPolygon: boolean, worldCoordinateHelper: WorldCoordinateHelper) {
154
175
  let x0, y0;
155
176
  let size = 0;
156
177
 
157
178
  for (let j = 0; j < ring.length; j++) {
158
- const x = projectX(ring[j][0]);
159
- const y = projectY(ring[j][1]);
179
+ const x = worldCoordinateHelper.projectX(ring[j][0]);
180
+ const y = worldCoordinateHelper.projectY(ring[j][1]);
160
181
 
161
182
  out.points.push(x, y, 0);
162
183
 
@@ -182,26 +203,10 @@ function convertLine(ring: GeoJSON.Position[], out: SliceArray, tolerance: numbe
182
203
  out.end = out.size;
183
204
  }
184
205
 
185
- function convertLines(rings: GeoJSON.Position[][], out: SliceArray[], tolerance: number, isPolygon: boolean) {
206
+ function convertLines(rings: GeoJSON.Position[][], out: SliceArray[], tolerance: number, isPolygon: boolean, worldCoordinateHelper: WorldCoordinateHelper) {
186
207
  for (let i = 0; i < rings.length; i++) {
187
208
  const geom: SliceArray = {points: []};
188
- convertLine(rings[i], geom, tolerance, isPolygon);
209
+ convertLine(rings[i], geom, tolerance, isPolygon, worldCoordinateHelper);
189
210
  out.push(geom);
190
211
  }
191
212
  }
192
-
193
- /**
194
- * Convert longitude to spherical mercator in [0..1] range
195
- */
196
- export function projectX(x: number) {
197
- return x / 360 + 0.5;
198
- }
199
-
200
- /**
201
- * Convert latitude to spherical mercator in [0..1] range
202
- */
203
- export function projectY(y: number) {
204
- const sin = Math.sin(y * Math.PI / 180);
205
- const y2 = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
206
- return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
207
- }
@@ -1,7 +1,7 @@
1
1
 
2
2
  import {test, expect} from 'vitest';
3
- import {featureToGeoJSON, unprojectX, unprojectY} from './deconvert';
4
- import {projectX, projectY} from './convert';
3
+ import {featureToGeoJSON} from './deconvert';
4
+ import {mercatorWorldCoordinateHelper} from './world-coordinate-helper';
5
5
  import type {GeoJSONVTInternalFeature} from './definitions';
6
6
 
7
7
  test('project/unproject roundtrip retains precision', () => {
@@ -19,8 +19,8 @@ test('project/unproject roundtrip retains precision', () => {
19
19
  ];
20
20
 
21
21
  for (const {lng, lat} of coords) {
22
- expect(unprojectX(projectX(lng))).toBeCloseTo(lng, 10);
23
- expect(unprojectY(projectY(lat))).toBeCloseTo(lat, 10);
22
+ expect(mercatorWorldCoordinateHelper.unprojectX(mercatorWorldCoordinateHelper.projectX(lng))).toBeCloseTo(lng, 10);
23
+ expect(mercatorWorldCoordinateHelper.unprojectY(mercatorWorldCoordinateHelper.projectY(lat))).toBeCloseTo(lat, 10);
24
24
  }
25
25
  });
26
26
 
package/src/deconvert.ts CHANGED
@@ -1,12 +1,13 @@
1
+ import {mercatorWorldCoordinateHelper, type WorldCoordinateHelper} from './world-coordinate-helper';
1
2
  import type {GeoJSONVTInternalFeature} from './definitions';
2
3
 
3
4
  /**
4
5
  * Converts internal source features back to GeoJSON format.
5
6
  */
6
- export function convertToGeoJSON(source: GeoJSONVTInternalFeature[]): GeoJSON.GeoJSON {
7
+ export function convertToGeoJSON(source: GeoJSONVTInternalFeature[], worldCoordinateHelper: WorldCoordinateHelper = mercatorWorldCoordinateHelper): GeoJSON.GeoJSON {
7
8
  const geojson: GeoJSON.GeoJSON = {
8
9
  type: 'FeatureCollection',
9
- features: source.map(feature => featureToGeoJSON(feature))
10
+ features: source.map(feature => featureToGeoJSON(feature, worldCoordinateHelper))
10
11
  };
11
12
 
12
13
  return geojson;
@@ -15,10 +16,10 @@ export function convertToGeoJSON(source: GeoJSONVTInternalFeature[]): GeoJSON.Ge
15
16
  /**
16
17
  * Converts a single internal feature to GeoJSON format.
17
18
  */
18
- export function featureToGeoJSON(feature: GeoJSONVTInternalFeature): GeoJSON.Feature {
19
+ export function featureToGeoJSON(feature: GeoJSONVTInternalFeature, worldCoordinateHelper: WorldCoordinateHelper = mercatorWorldCoordinateHelper): GeoJSON.Feature {
19
20
  const geojsonFeature: GeoJSON.Feature = {
20
21
  type: 'Feature',
21
- geometry: geometryToGeoJSON(feature),
22
+ geometry: geometryToGeoJSON(feature, worldCoordinateHelper),
22
23
  properties: feature.tags
23
24
  };
24
25
  if (feature.id != null) {
@@ -31,66 +32,51 @@ export function featureToGeoJSON(feature: GeoJSONVTInternalFeature): GeoJSON.Fea
31
32
  /**
32
33
  * Converts a single internal feature geometry to GeoJSON format.
33
34
  */
34
- function geometryToGeoJSON(feature: GeoJSONVTInternalFeature): GeoJSON.Geometry {
35
+ function geometryToGeoJSON(feature: GeoJSONVTInternalFeature, worldCoordinateHelper: WorldCoordinateHelper): GeoJSON.Geometry {
35
36
  const {type, geometry} = feature;
36
37
 
37
38
  switch (type) {
38
39
  case 'Point':
39
40
  return {
40
41
  type: type,
41
- coordinates: unprojectPoint(geometry[0], geometry[1])
42
+ coordinates: unprojectPoint(geometry[0], geometry[1], worldCoordinateHelper)
42
43
  };
43
44
  case 'MultiPoint':
44
45
  return {
45
46
  type: type,
46
- coordinates: unprojectPoints(geometry)
47
+ coordinates: unprojectPoints(geometry, worldCoordinateHelper)
47
48
  };
48
49
  case 'LineString':
49
50
  return {
50
51
  type: type,
51
- coordinates: unprojectPoints(geometry.points)
52
+ coordinates: unprojectPoints(geometry.points, worldCoordinateHelper)
52
53
  };
53
54
  case 'MultiLineString':
54
55
  case 'Polygon':
55
56
  return {
56
57
  type: type,
57
- coordinates: geometry.map(ring => unprojectPoints(ring.points))
58
+ coordinates: geometry.map(ring => unprojectPoints(ring.points, worldCoordinateHelper))
58
59
  };
59
60
  case 'MultiPolygon':
60
61
  return {
61
62
  type: type,
62
63
  coordinates: geometry.map(polygon =>
63
- polygon.map(ring => unprojectPoints(ring.points))
64
+ polygon.map(ring => unprojectPoints(ring.points, worldCoordinateHelper))
64
65
  )
65
66
  };
66
67
  }
67
68
  }
68
69
 
69
- export function unprojectPoints(coords: number[] | Float64Array): GeoJSON.Position[] {
70
+ export function unprojectPoints(coords: number[] | Float64Array, worldCoordinateHelper: WorldCoordinateHelper = mercatorWorldCoordinateHelper): GeoJSON.Position[] {
70
71
  const result: GeoJSON.Position[] = [];
71
72
 
72
73
  for (let i = 0; i < coords.length; i += 3) {
73
- result.push(unprojectPoint(coords[i], coords[i + 1]));
74
+ result.push(unprojectPoint(coords[i], coords[i + 1], worldCoordinateHelper));
74
75
  }
75
76
 
76
77
  return result;
77
78
  }
78
79
 
79
- function unprojectPoint(x: number, y: number): GeoJSON.Position {
80
- return [unprojectX(x), unprojectY(y)];
81
- }
82
-
83
- /**
84
- * Convert spherical mercator in [0..1] range to longitude
85
- */
86
- export function unprojectX(x: number): number {
87
- return (x - 0.5) * 360;
88
- }
89
-
90
- /**
91
- * Convert spherical mercator in [0..1] range to latitude
92
- */
93
- export function unprojectY(y: number): number {
94
- const y2 = (180 - y * 360) * Math.PI / 180;
95
- return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
80
+ function unprojectPoint(x: number, y: number, worldCoordinateHelper: WorldCoordinateHelper): GeoJSON.Position {
81
+ return [worldCoordinateHelper.unprojectX(x), worldCoordinateHelper.unprojectY(y)];
96
82
  }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The space positions are given in, see `coordinates` in {@link GeoJSONVTOptions}.
3
+ * @experimental
4
+ */
5
+ export type GeoJSONVTCoordinatesType = 'lngLat' | 'unitSquare';
6
+
1
7
  export type GeoJSONVTOptions = {
2
8
  /**
3
9
  * Max zoom to preserve detail on
@@ -54,20 +60,39 @@ export type GeoJSONVTOptions = {
54
60
  */
55
61
  debug?: number;
56
62
  /**
57
- * Enable Supercluster for point features.
63
+ * The space the positions are in. `'lngLat'` is longitude and latitude, which web mercator puts in the square from 0
64
+ * to 1 that tile 0/0/0 covers, with latitudes beyond ~85.05 degrees clamped onto its top and bottom. `'unitSquare'`
65
+ * is x and y in that square already, y growing downward, taken as they are, for data in another projection.
66
+ * `getData` and the cluster methods return positions in the same space.
67
+ * @experimental
68
+ * @default 'lngLat'
69
+ */
70
+ coordinates?: GeoJSONVTCoordinatesType;
71
+ /**
72
+ * Whether the world wraps around the antimeridian. With `true`, features near the left or right edge are copied
73
+ * across it, and the tiles at either edge include what lies past it. With `false`, the world is the square and ends
74
+ * at its edges. Features are cut at the tile buffer around it, points outside it are left out of the clusters, and
75
+ * `getTile` returns `null` outside it. `geoJSONToTile` doesn't wrap unless this is `true`.
76
+ * @experimental
77
+ * @default true
78
+ */
79
+ wrap?: boolean;
80
+ /**
81
+ * Enable Supercluster for point features. Each position of a MultiPoint feature is clustered as a point of its own.
82
+ * Other geometry types are left out.
58
83
  * @default false
59
84
  */
60
85
  cluster?: boolean;
61
86
  /**
62
- * Options for the Supercluster point clustering algorithm.
87
+ * Options for the Supercluster point clustering algorithm. `coordinates` and `wrap` come from the options above.
63
88
  * @see {@link SuperclusterOptions}
64
89
  */
65
- clusterOptions?: SuperclusterOptions;
90
+ clusterOptions?: Omit<SuperclusterOptions, 'coordinates' | 'wrap'>;
66
91
  };
67
92
 
68
93
  export type GeoJSONToTileOptions = GeoJSONVTOptions & {
69
94
  /**
70
- * Whether to wrap features around the antimeridian
95
+ * Whether to wrap features around the antimeridian, see `wrap` in {@link GeoJSONVTOptions}.
71
96
  * @default false
72
97
  */
73
98
  wrap?: boolean;
@@ -214,12 +239,12 @@ export interface GeoJSONVTTileIndex {
214
239
 
215
240
  export type SuperclusterOptions = {
216
241
  /**
217
- * Min zoom to generate clusters on
242
+ * Min zoom to generate clusters on. A value above 30 is limited to 30.
218
243
  * @default 0
219
244
  */
220
245
  minZoom?: number;
221
246
  /**
222
- * Max zoom level to cluster the points on
247
+ * Max zoom level to cluster the points on. A value above 30 is limited to 30.
223
248
  * @default 16
224
249
  */
225
250
  maxZoom?: number;
@@ -249,7 +274,8 @@ export type SuperclusterOptions = {
249
274
  */
250
275
  log?: boolean;
251
276
  /**
252
- * Whether to generate numeric ids for input features (in vector tiles)
277
+ * Whether to generate numeric ids for input features (in vector tiles). Each position of a MultiPoint feature
278
+ * gets an id of its own.
253
279
  * @default false
254
280
  */
255
281
  generateId?: boolean;
@@ -263,4 +289,19 @@ export type SuperclusterOptions = {
263
289
  * @default props => props
264
290
  */
265
291
  map?: (props: GeoJSON.GeoJsonProperties) => Record<string, unknown>;
292
+ /**
293
+ * The space the points are in, see `coordinates` in {@link GeoJSONVTOptions}. The bounding box of `getClusters` is
294
+ * in the same space.
295
+ * @experimental
296
+ * @default 'lngLat'
297
+ */
298
+ coordinates?: GeoJSONVTCoordinatesType;
299
+ /**
300
+ * Whether the world wraps around the antimeridian, see `wrap` in {@link GeoJSONVTOptions}. With `false`, points
301
+ * outside the square are left out of the clusters, the tiles at an edge get no points from the opposite one, and
302
+ * `getTile` returns `null` outside the square.
303
+ * @experimental
304
+ * @default true
305
+ */
306
+ wrap?: boolean;
266
307
  };
@@ -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
@@ -70,9 +70,10 @@ type HashedGeoJSONVTSourceDiff = {
70
70
  * @param source
71
71
  * @param dataDiff
72
72
  * @param options
73
+ * @param unlocated - the features without coordinates, by id, so that an update can give them some
73
74
  * @returns
74
75
  */
75
- export function applySourceDiff(source: GeoJSONVTInternalFeature[], dataDiff: GeoJSONVTSourceDiff, options: GeoJSONVTOptions): ApplySourceDiffResult {
76
+ export function applySourceDiff(source: GeoJSONVTInternalFeature[], dataDiff: GeoJSONVTSourceDiff, options: GeoJSONVTOptions, unlocated?: Map<string | number, GeoJSON.Feature>): ApplySourceDiffResult {
76
77
  // convert diff to sets/maps for o(1) lookups
77
78
  const diff = diffToHashed(dataDiff, options);
78
79
 
@@ -82,9 +83,12 @@ export function applySourceDiff(source: GeoJSONVTInternalFeature[], dataDiff: Ge
82
83
  if (diff.removeAll) {
83
84
  affected = source;
84
85
  source = [];
86
+ unlocated?.clear();
85
87
  }
86
88
 
87
89
  if (diff.remove.size || diff.add.size) {
90
+ for (const id of diff.remove) unlocated?.delete(id);
91
+
88
92
  const removeFeatures = [];
89
93
 
90
94
  // Collect features to remove (explicit removals + replacements via add)
@@ -101,7 +105,7 @@ export function applySourceDiff(source: GeoJSONVTInternalFeature[], dataDiff: Ge
101
105
  }
102
106
 
103
107
  if (diff.add.size) {
104
- let addFeatures = convertToInternal({type: 'FeatureCollection', features: Array.from(diff.add.values())}, options);
108
+ let addFeatures = convertToInternal({type: 'FeatureCollection', features: Array.from(diff.add.values())}, options, unlocated);
105
109
  addFeatures = wrap(addFeatures, options);
106
110
  affected = affected.concat(addFeatures);
107
111
  source = source.concat(addFeatures);
@@ -121,8 +125,14 @@ export function applySourceDiff(source: GeoJSONVTInternalFeature[], dataDiff: Ge
121
125
  }
122
126
  for (const [id, update] of diff.update) {
123
127
  const oldFeatures = oldFeaturesMap.get(id);
124
- if (!oldFeatures || oldFeatures.length === 0) continue;
125
- const updatedFeatures = getUpdatedFeatures(oldFeatures, update, options);
128
+ if (!oldFeatures || oldFeatures.length === 0) {
129
+ if (!unlocated?.has(id)) continue;
130
+ const locatedFeatures = getUpdatedUnlocatedFeatures(id, update, options, unlocated);
131
+ affected = affected.concat(locatedFeatures);
132
+ keepFeatures = keepFeatures.concat(locatedFeatures);
133
+ continue;
134
+ }
135
+ const updatedFeatures = getUpdatedFeatures(oldFeatures, update, options, unlocated);
126
136
 
127
137
  affected = affected.concat(oldFeatures, updatedFeatures);
128
138
  keepFeatures = keepFeatures.concat(updatedFeatures);
@@ -138,28 +148,18 @@ export function applySourceDiff(source: GeoJSONVTInternalFeature[], dataDiff: Ge
138
148
  * @param vtFeatures - the original features
139
149
  * @param update - the update object to apply
140
150
  * @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.
151
+ * @param unlocated - the features without coordinates, by id, which a new geometry without coordinates adds the feature to
152
+ * @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
153
  */
143
- function getUpdatedFeatures(vtFeatures: GeoJSONVTInternalFeature[], update: GeoJSONVTFeatureDiff, options: GeoJSONVTOptions): GeoJSONVTInternalFeature[] {
154
+ function getUpdatedFeatures(vtFeatures: GeoJSONVTInternalFeature[], update: GeoJSONVTFeatureDiff, options: GeoJSONVTOptions, unlocated?: Map<string | number, GeoJSON.Feature>): GeoJSONVTInternalFeature[] {
144
155
  const changeGeometry = !!update.newGeometry;
145
- const changeProps =
146
- update.removeAllProperties ||
147
- update.removeProperties?.length > 0 ||
148
- update.addOrUpdateProperties?.length > 0;
156
+ const changeProps = hasPropertyChanges(update);
149
157
 
150
158
  // if geometry changed, need to create a new geojson feature and convert to internal format
151
159
  if (changeGeometry) {
152
160
  const vtFeature = vtFeatures[0];
153
- const geojsonFeature = {
154
- type: 'Feature' as const,
155
- id: vtFeature.id,
156
- geometry: update.newGeometry,
157
- properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
158
- };
159
-
160
- let features = convertToInternal({type: 'FeatureCollection', features: [geojsonFeature]}, options);
161
- features = wrap(features, options);
162
- return features;
161
+ const properties = changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags;
162
+ return convertNewGeometry(vtFeature.id, update.newGeometry, properties, options, unlocated);
163
163
  }
164
164
 
165
165
  if (changeProps) {
@@ -175,6 +175,35 @@ function getUpdatedFeatures(vtFeatures: GeoJSONVTInternalFeature[], update: GeoJ
175
175
  return vtFeatures;
176
176
  }
177
177
 
178
+ /**
179
+ * Gets the features an update gives a feature without coordinates: none, unless its new geometry has some.
180
+ * Its property changes apply either way.
181
+ */
182
+ function getUpdatedUnlocatedFeatures(id: string | number, update: GeoJSONVTFeatureDiff, options: GeoJSONVTOptions, unlocated: Map<string | number, GeoJSON.Feature>): GeoJSONVTInternalFeature[] {
183
+ const feature = unlocated.get(id);
184
+ const properties = hasPropertyChanges(update) ? applyPropertyUpdates(feature.properties, update) : feature.properties;
185
+ if (!update.newGeometry) {
186
+ unlocated.set(id, {...feature, properties});
187
+ return [];
188
+ }
189
+ return convertNewGeometry(id, update.newGeometry, properties, options, unlocated);
190
+ }
191
+
192
+ /**
193
+ * Converts the new geometry of a feature to internal format, keeping the id the feature already has
194
+ */
195
+ function convertNewGeometry(id: GeoJSONVTInternalFeature['id'], geometry: GeoJSON.Geometry, properties: GeoJSON.GeoJsonProperties, options: GeoJSONVTOptions, unlocated?: Map<string | number, GeoJSON.Feature>): GeoJSONVTInternalFeature[] {
196
+ const geojsonFeature = {type: 'Feature' as const, id, geometry, properties};
197
+ const features = convertToInternal({type: 'FeatureCollection', features: [geojsonFeature]}, {...options, promoteId: undefined, generateId: false}, unlocated);
198
+ return wrap(features, options);
199
+ }
200
+
201
+ function hasPropertyChanges(update: GeoJSONVTFeatureDiff): boolean {
202
+ return update.removeAllProperties ||
203
+ update.removeProperties?.length > 0 ||
204
+ update.addOrUpdateProperties?.length > 0;
205
+ }
206
+
178
207
  /**
179
208
  * helper to apply property updates from a diff update object to a properties object
180
209
  */
@@ -213,7 +242,7 @@ export function diffToHashed(diff: GeoJSONVTSourceDiff, options: GeoJSONVTOption
213
242
  const hashed: HashedGeoJSONVTSourceDiff = {
214
243
  removeAll: diff.removeAll,
215
244
  remove: new Set(diff.remove || []),
216
- add: new Map(diff.add?.map(feature => [options.promoteId ? feature.properties[options.promoteId] : feature.id, feature])),
245
+ add: new Map(diff.add?.map(feature => [options.promoteId ? feature.properties?.[options.promoteId] : feature.id, feature])),
217
246
  update: new Map(diff.update?.map(update => [update.id, update]))
218
247
  };
219
248
 
@@ -40,8 +40,8 @@ import type {GeoJSONToTileOptions, GeoJSONVTTile} from './definitions';
40
40
  */
41
41
 
42
42
  export function geoJSONToTile(data: GeoJSON.GeoJSON, z: number, x: number, y: number, options: GeoJSONToTileOptions = {}): GeoJSONVTTile {
43
- options = {...defaultOptions, ...options};
44
- const {wrap: shouldWrap = false, clip: shouldClip = false} = options;
43
+ options = {...defaultOptions, wrap: false, ...options};
44
+ const {wrap: shouldWrap, clip: shouldClip = false} = options;
45
45
 
46
46
  let features = convertToInternal(data, options);
47
47
  if (shouldWrap) {