@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/README.md +3 -0
- package/dist/cluster-tile-index.d.ts +41 -6
- package/dist/cluster-tile-index.d.ts.map +1 -1
- package/dist/convert.d.ts +4 -4
- package/dist/convert.d.ts.map +1 -1
- package/dist/deconvert.d.ts +7 -7
- package/dist/deconvert.d.ts.map +1 -1
- package/dist/definitions.d.ts +17 -5
- package/dist/definitions.d.ts.map +1 -1
- package/dist/geojson-vt-dev.js +286 -135
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +285 -135
- package/dist/geojson-vt.mjs.map +1 -1
- package/dist/geojsonvt.d.ts.map +1 -1
- package/dist/tile-index.d.ts +6 -0
- package/dist/tile-index.d.ts.map +1 -1
- package/dist/wrap.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/clip.ts +2 -2
- package/src/cluster-tile-index.test.ts +103 -0
- package/src/cluster-tile-index.ts +242 -97
- package/src/convert.ts +35 -28
- package/src/deconvert.ts +20 -18
- package/src/definitions.ts +17 -5
- package/src/difference.test.ts +71 -0
- package/src/difference.ts +3 -3
- package/src/geojsonvt.test.ts +236 -1
- package/src/geojsonvt.ts +5 -4
- package/src/tile-index.ts +17 -3
- package/src/wrap.ts +2 -0
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
|
}
|
package/src/definitions.ts
CHANGED
|
@@ -54,7 +54,18 @@ export type GeoJSONVTOptions = {
|
|
|
54
54
|
*/
|
|
55
55
|
debug?: number;
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
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;
|
package/src/difference.test.ts
CHANGED
|
@@ -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
|
|
package/src/geojsonvt.test.ts
CHANGED
|
@@ -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
|
+
});
|