@maplibre/geojson-vt 6.2.0 → 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.
- package/README.md +7 -2
- package/dist/cluster-tile-index.d.ts +12 -9
- package/dist/cluster-tile-index.d.ts.map +1 -1
- package/dist/convert.d.ts +2 -9
- package/dist/convert.d.ts.map +1 -1
- package/dist/deconvert.d.ts +4 -11
- package/dist/deconvert.d.ts.map +1 -1
- package/dist/definitions.d.ts +38 -10
- package/dist/definitions.d.ts.map +1 -1
- package/dist/difference.d.ts +2 -1
- package/dist/difference.d.ts.map +1 -1
- package/dist/geojson-vt-dev.js +263 -133
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +261 -132
- package/dist/geojson-vt.mjs.map +1 -1
- package/dist/geojsonvt.d.ts +8 -4
- package/dist/geojsonvt.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/tile-index.d.ts +4 -3
- package/dist/tile-index.d.ts.map +1 -1
- package/dist/world-coordinate-helper.d.ts +13 -0
- package/dist/world-coordinate-helper.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cluster-tile-index.test.ts +28 -8
- package/src/cluster-tile-index.ts +53 -27
- package/src/convert.ts +45 -47
- package/src/deconvert.test.ts +4 -4
- package/src/deconvert.ts +15 -31
- package/src/definitions.ts +39 -10
- package/src/difference.ts +48 -19
- package/src/geojson-to-tile.ts +2 -2
- package/src/geojsonvt.test.ts +171 -20
- package/src/geojsonvt.ts +24 -10
- package/src/index.ts +2 -1
- package/src/tile-index.ts +8 -6
- package/src/world-coordinate-helper.ts +63 -0
- package/src/wrap.ts +1 -1
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[],
|
|
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[], worldCoordi
|
|
|
15
16
|
/**
|
|
16
17
|
* Converts a single internal feature to GeoJSON format.
|
|
17
18
|
*/
|
|
18
|
-
export function featureToGeoJSON(feature: GeoJSONVTInternalFeature,
|
|
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,68 +32,51 @@ export function featureToGeoJSON(feature: GeoJSONVTInternalFeature, worldCoordin
|
|
|
31
32
|
/**
|
|
32
33
|
* Converts a single internal feature geometry to GeoJSON format.
|
|
33
34
|
*/
|
|
34
|
-
function geometryToGeoJSON(feature: GeoJSONVTInternalFeature,
|
|
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,
|
|
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,
|
|
80
|
-
return [unprojectX(x
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Convert spherical mercator in [0..1] range to longitude, or keep a world coordinate as it is
|
|
85
|
-
*/
|
|
86
|
-
export function unprojectX(x: number, worldCoordinates = false): number {
|
|
87
|
-
if (worldCoordinates) return x;
|
|
88
|
-
return (x - 0.5) * 360;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Convert spherical mercator in [0..1] range to latitude, or keep a world coordinate as it is
|
|
93
|
-
*/
|
|
94
|
-
export function unprojectY(y: number, worldCoordinates = false): number {
|
|
95
|
-
if (worldCoordinates) return y;
|
|
96
|
-
const y2 = (180 - y * 360) * Math.PI / 180;
|
|
97
|
-
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)];
|
|
98
82
|
}
|
package/src/definitions.ts
CHANGED
|
@@ -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,15 +60,23 @@ export type GeoJSONVTOptions = {
|
|
|
54
60
|
*/
|
|
55
61
|
debug?: number;
|
|
56
62
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* returns `null` outside it, and `getData` and the cluster methods return world coordinates too.
|
|
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.
|
|
62
67
|
* @experimental
|
|
63
|
-
* @default
|
|
68
|
+
* @default 'lngLat'
|
|
64
69
|
*/
|
|
65
|
-
|
|
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;
|
|
66
80
|
/**
|
|
67
81
|
* Enable Supercluster for point features. Each position of a MultiPoint feature is clustered as a point of its own.
|
|
68
82
|
* Other geometry types are left out.
|
|
@@ -70,15 +84,15 @@ export type GeoJSONVTOptions = {
|
|
|
70
84
|
*/
|
|
71
85
|
cluster?: boolean;
|
|
72
86
|
/**
|
|
73
|
-
* Options for the Supercluster point clustering algorithm.
|
|
87
|
+
* Options for the Supercluster point clustering algorithm. `coordinates` and `wrap` come from the options above.
|
|
74
88
|
* @see {@link SuperclusterOptions}
|
|
75
89
|
*/
|
|
76
|
-
clusterOptions?: SuperclusterOptions
|
|
90
|
+
clusterOptions?: Omit<SuperclusterOptions, 'coordinates' | 'wrap'>;
|
|
77
91
|
};
|
|
78
92
|
|
|
79
93
|
export type GeoJSONToTileOptions = GeoJSONVTOptions & {
|
|
80
94
|
/**
|
|
81
|
-
* Whether to wrap features around the antimeridian
|
|
95
|
+
* Whether to wrap features around the antimeridian, see `wrap` in {@link GeoJSONVTOptions}.
|
|
82
96
|
* @default false
|
|
83
97
|
*/
|
|
84
98
|
wrap?: boolean;
|
|
@@ -275,4 +289,19 @@ export type SuperclusterOptions = {
|
|
|
275
289
|
* @default props => props
|
|
276
290
|
*/
|
|
277
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;
|
|
278
307
|
};
|
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)
|
|
125
|
-
|
|
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
|
|
151
|
+
* @param unlocated - the features without coordinates, by id, which a new geometry without coordinates adds the feature to
|
|
141
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
|
|
154
|
-
|
|
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, promoteId: undefined, generateId: false});
|
|
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
|
*/
|
package/src/geojson-to-tile.ts
CHANGED
|
@@ -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
|
|
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) {
|
package/src/geojsonvt.test.ts
CHANGED
|
@@ -38,25 +38,25 @@ test('publicly exposed cluster methods: return undefined when clustering is disa
|
|
|
38
38
|
expect(index.getClusterLeaves(123, 10, 0)).toBeNull();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test('
|
|
41
|
+
test('a unit square without wrap cuts a line at the buffer around the world instead of wrapping or clamping it', () => {
|
|
42
42
|
const fromThreeWorldsNorthToEast = {type: 'LineString' as const, coordinates: [[0.25, -3], [0.75, 0.5], [1.25, 0.5]]};
|
|
43
43
|
|
|
44
|
-
const index = new GeoJSONVT(fromThreeWorldsNorthToEast, {
|
|
44
|
+
const index = new GeoJSONVT(fromThreeWorldsNorthToEast, {coordinates: 'unitSquare', wrap: false});
|
|
45
45
|
|
|
46
46
|
expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[[2770, -64], [3072, 2048], [4160, 2048]]]]);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
test('
|
|
49
|
+
test('a unit square without wrap cuts the top tile at the world again on update, so a feature can move in from outside', () => {
|
|
50
50
|
const movingIn = {type: 'Feature' as const, id: 'moving in', geometry: {type: 'Point' as const, coordinates: [1.25, 0.5]}, properties: {}};
|
|
51
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]}, {
|
|
52
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [movingIn, stayingOut]}, {coordinates: 'unitSquare', wrap: false, updateable: true});
|
|
53
53
|
|
|
54
54
|
index.updateData({update: [{id: 'moving in', newGeometry: {type: 'Point', coordinates: [0.5, 0.5]}}]});
|
|
55
55
|
|
|
56
56
|
expect(index.getTile(0, 0, 0).features.map(feature => feature.id)).toEqual(['moving in']);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
test('
|
|
59
|
+
test('a unit square without wrap returns the data in the unit square', () => {
|
|
60
60
|
const features: GeoJSON.Feature[] = [
|
|
61
61
|
{type: 'Feature', id: 'point', geometry: {type: 'Point', coordinates: [0.25, 0.75]}, properties: {}},
|
|
62
62
|
{type: 'Feature', id: 'multi point', geometry: {type: 'MultiPoint', coordinates: [[0.25, 0.75], [0.5, 0.5]]}, properties: {}},
|
|
@@ -65,14 +65,14 @@ test('worldCoordinates returns the data in world coordinates', () => {
|
|
|
65
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
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
67
|
];
|
|
68
|
-
const index = new GeoJSONVT({type: 'FeatureCollection', features}, {
|
|
68
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features}, {coordinates: 'unitSquare', wrap: false, updateable: true});
|
|
69
69
|
|
|
70
70
|
expect(index.getData()).toEqual({type: 'FeatureCollection', features});
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
test('
|
|
73
|
+
test('a unit square without wrap keeps a MultiLineString it splits for lineMetrics in the unit square', () => {
|
|
74
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, {
|
|
75
|
+
const index = new GeoJSONVT(multiLine, {coordinates: 'unitSquare', wrap: false, lineMetrics: true, updateable: true});
|
|
76
76
|
|
|
77
77
|
expect(index.getData()).toEqual({type: 'FeatureCollection', features: [
|
|
78
78
|
{type: 'Feature', geometry: {type: 'LineString', coordinates: [[0.25, 0.75], [0.5, 0.5]]}},
|
|
@@ -80,9 +80,9 @@ test('worldCoordinates keeps a MultiLineString it splits for lineMetrics in worl
|
|
|
80
80
|
]});
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
test('
|
|
83
|
+
test('a unit square without wrap hands the update filter features in the unit square', () => {
|
|
84
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]}, {
|
|
85
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [point]}, {coordinates: 'unitSquare', wrap: false, updateable: true});
|
|
86
86
|
const filtered: GeoJSON.Geometry[] = [];
|
|
87
87
|
|
|
88
88
|
index.updateData({add: [{...point, id: 'q'}]}, feature => {
|
|
@@ -93,7 +93,7 @@ test('worldCoordinates hands the update filter features in world coordinates', (
|
|
|
93
93
|
expect(filtered).toContainEqual({type: 'Point', coordinates: [0.25, 0.75]});
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test('
|
|
96
|
+
test('a unit square without wrap leaves points outside the world out of the clusters', () => {
|
|
97
97
|
const data = {
|
|
98
98
|
type: 'FeatureCollection' as const,
|
|
99
99
|
features: [
|
|
@@ -103,12 +103,12 @@ test('worldCoordinates leaves points outside the world out of the clusters', ()
|
|
|
103
103
|
]
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
const index = new GeoJSONVT(data, {
|
|
106
|
+
const index = new GeoJSONVT(data, {coordinates: 'unitSquare', wrap: false, cluster: true});
|
|
107
107
|
|
|
108
108
|
expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[507, 256]]]);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
test('
|
|
111
|
+
test('a unit square without wrap keeps points at either edge out of the clustered tile at the other edge', () => {
|
|
112
112
|
const data = {
|
|
113
113
|
type: 'FeatureCollection' as const,
|
|
114
114
|
features: [
|
|
@@ -117,12 +117,12 @@ test('worldCoordinates keeps points at either edge out of the clustered tile at
|
|
|
117
117
|
]
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
const index = new GeoJSONVT(data, {
|
|
120
|
+
const index = new GeoJSONVT(data, {coordinates: 'unitSquare', wrap: false, cluster: true});
|
|
121
121
|
|
|
122
122
|
expect(index.getTile(0, 0, 0).features).toHaveLength(2);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
test('
|
|
125
|
+
test('a unit square without wrap returns cluster children and leaves in the unit square', () => {
|
|
126
126
|
const points = {
|
|
127
127
|
type: 'FeatureCollection' as const,
|
|
128
128
|
features: Array.from({length: 20}, (_, i) => ({
|
|
@@ -131,7 +131,7 @@ test('worldCoordinates returns cluster children and leaves in world coordinates'
|
|
|
131
131
|
properties: {name: `Point ${i}`}
|
|
132
132
|
}))
|
|
133
133
|
};
|
|
134
|
-
const index = new GeoJSONVT(points, {
|
|
134
|
+
const index = new GeoJSONVT(points, {coordinates: 'unitSquare', wrap: false, cluster: true, clusterOptions: {radius: 100}});
|
|
135
135
|
const cluster = index.getTile(0, 0, 0).features.find(f => (f.tags as {cluster?: boolean})?.cluster);
|
|
136
136
|
const clusterId = (cluster.tags as {cluster_id: number}).cluster_id;
|
|
137
137
|
|
|
@@ -142,10 +142,41 @@ test('worldCoordinates returns cluster children and leaves in world coordinates'
|
|
|
142
142
|
expect(leafPositions).toEqual(expect.arrayContaining(points.features.map(point => point.geometry.coordinates)));
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
test('
|
|
145
|
+
test('a unit square without wrap stays without wrap for clusters after updateData', () => {
|
|
146
|
+
const point = (id: string, x: number) => ({type: 'Feature' as const, id, properties: {}, geometry: {type: 'Point' as const, coordinates: [x, 0.5]}});
|
|
147
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [point('a', 0.01)]}, {coordinates: 'unitSquare', wrap: false, cluster: true, updateable: true});
|
|
148
|
+
|
|
149
|
+
index.updateData({add: [point('b', 0.99)]});
|
|
150
|
+
|
|
151
|
+
expect(index.getTile(1, -1, 0)).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('wraps positions in the unit square around the antimeridian by default', () => {
|
|
155
|
+
const nearTheEastEdge = {type: 'Feature' as const, properties: {}, geometry: {type: 'Point' as const, coordinates: [0.999, 0.25]}};
|
|
156
|
+
const index = new GeoJSONVT(nearTheEastEdge, {coordinates: 'unitSquare'});
|
|
157
|
+
|
|
158
|
+
expect(index.getTile(1, 0, 0).features.map(feature => feature.geometry)).toEqual([[[-8, 2048]]]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('cuts the top tile above and below for positions in a unit square that wraps', () => {
|
|
162
|
+
const fromThreeBelowToThreeAbove = {type: 'Feature' as const, properties: {}, geometry: {type: 'LineString' as const, coordinates: [[0.25, -3], [0.75, 3]]}};
|
|
163
|
+
const index = new GeoJSONVT(fromThreeBelowToThreeAbove, {coordinates: 'unitSquare'});
|
|
164
|
+
|
|
165
|
+
expect(index.getTile(0, 0, 0).features[0].geometry).toEqual([[[2043, -64], [2395, 4160]]]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('copies no lng/lat features across the antimeridian without wrap', () => {
|
|
169
|
+
const nearTheAntimeridian = {type: 'Feature' as const, properties: {}, geometry: {type: 'Point' as const, coordinates: [179.9, -10]}};
|
|
170
|
+
const index = new GeoJSONVT(nearTheAntimeridian, {wrap: false});
|
|
171
|
+
|
|
172
|
+
expect(index.getTile(1, 0, 1)?.features ?? []).toEqual([]);
|
|
173
|
+
expect(index.getTile(1, 1, 1).features).toHaveLength(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('a unit square without wrap returns no tile for a column on either side of the world', () => {
|
|
146
177
|
const inTheTwoTopTiles = {type: 'MultiPoint' as const, coordinates: [[0.25, 0.25], [0.75, 0.25]]};
|
|
147
178
|
|
|
148
|
-
const index = new GeoJSONVT(inTheTwoTopTiles, {
|
|
179
|
+
const index = new GeoJSONVT(inTheTwoTopTiles, {coordinates: 'unitSquare', wrap: false});
|
|
149
180
|
|
|
150
181
|
expect([index.getTile(1, -1, 0), index.getTile(1, 2, 0)]).toEqual([null, null]);
|
|
151
182
|
});
|
|
@@ -210,6 +241,126 @@ test('leaves out a MultiPolygon that has only empty polygons', () => {
|
|
|
210
241
|
expect(index.getData()).toEqual({type: 'FeatureCollection', features: []});
|
|
211
242
|
});
|
|
212
243
|
|
|
244
|
+
test('updateData draws a feature again when an update gives back the coordinates an earlier one took away', () => {
|
|
245
|
+
const line = {type: 'Feature' as const, id: 'line', properties: {name: 'line'}, geometry: {type: 'LineString' as const, coordinates: [[0, 0], [10, 10]]}};
|
|
246
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [line]}, {updateable: true});
|
|
247
|
+
|
|
248
|
+
index.updateData({update: [{id: 'line', newGeometry: {type: 'LineString', coordinates: []}}]});
|
|
249
|
+
index.updateData({update: [{id: 'line', newGeometry: line.geometry}]});
|
|
250
|
+
|
|
251
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.tags)).toEqual([{name: 'line'}]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('updateData draws a feature loaded without a geometry once an update gives it one', () => {
|
|
255
|
+
const later: GeoJSON.Feature = {type: 'Feature', id: 'later', properties: {name: 'later'}, geometry: null};
|
|
256
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [later]}, {updateable: true});
|
|
257
|
+
|
|
258
|
+
index.updateData({update: [{id: 'later', newGeometry: {type: 'Point', coordinates: [10, 10]}}]});
|
|
259
|
+
|
|
260
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.tags)).toEqual([{name: 'later'}]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('updateData draws a feature added without coordinates once an update gives it some', () => {
|
|
264
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: []}, {updateable: true});
|
|
265
|
+
|
|
266
|
+
index.updateData({add: [{type: 'Feature', id: 'added', properties: {name: 'added'}, geometry: {type: 'Polygon', coordinates: []}}]});
|
|
267
|
+
index.updateData({update: [{id: 'added', newGeometry: {type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 0]]]}}]});
|
|
268
|
+
|
|
269
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.tags)).toEqual([{name: 'added'}]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('updateData leaves a feature without a geometry removed when an update gives it one', () => {
|
|
273
|
+
const kept = {type: 'Feature' as const, id: 'kept', properties: {}, geometry: {type: 'Point' as const, coordinates: [0, 0]}};
|
|
274
|
+
const removed: GeoJSON.Feature = {type: 'Feature', id: 'removed', properties: {}, geometry: null};
|
|
275
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [kept, removed]}, {updateable: true});
|
|
276
|
+
|
|
277
|
+
index.updateData({remove: ['removed']});
|
|
278
|
+
index.updateData({update: [{id: 'removed', newGeometry: {type: 'Point', coordinates: [10, 10]}}]});
|
|
279
|
+
|
|
280
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.id)).toEqual(['kept']);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('updateData leaves a feature without a geometry removed by removeAll when an update gives it one', () => {
|
|
284
|
+
const kept = {type: 'Feature' as const, id: 'kept', properties: {}, geometry: {type: 'Point' as const, coordinates: [0, 0]}};
|
|
285
|
+
const removed: GeoJSON.Feature = {type: 'Feature', id: 'removed', properties: {}, geometry: null};
|
|
286
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [removed]}, {updateable: true});
|
|
287
|
+
|
|
288
|
+
index.updateData({removeAll: true, add: [kept]});
|
|
289
|
+
index.updateData({update: [{id: 'removed', newGeometry: {type: 'Point', coordinates: [10, 10]}}]});
|
|
290
|
+
|
|
291
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.id)).toEqual(['kept']);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('updateData keeps the property changes made to a feature while it has no geometry', () => {
|
|
295
|
+
const renamed: GeoJSON.Feature = {type: 'Feature', id: 'renamed', properties: {name: 'before'}, geometry: null};
|
|
296
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [renamed]}, {updateable: true});
|
|
297
|
+
|
|
298
|
+
index.updateData({update: [{id: 'renamed', addOrUpdateProperties: [{key: 'name', value: 'after'}]}]});
|
|
299
|
+
index.updateData({update: [{id: 'renamed', newGeometry: {type: 'Point', coordinates: [10, 10]}}]});
|
|
300
|
+
|
|
301
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.tags)).toEqual([{name: 'after'}]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('updateData does not bring back a feature the update filter took out after an update gave it a geometry', () => {
|
|
305
|
+
const kept = {type: 'Feature' as const, id: 'kept', properties: {}, geometry: {type: 'Point' as const, coordinates: [0, 0]}};
|
|
306
|
+
const hidden: GeoJSON.Feature = {type: 'Feature', id: 'hidden', properties: {hidden: true}, geometry: null};
|
|
307
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [kept, hidden]}, {updateable: true});
|
|
308
|
+
|
|
309
|
+
index.updateData({update: [{id: 'hidden', newGeometry: {type: 'Point', coordinates: [10, 10]}}]}, feature => !feature.properties.hidden);
|
|
310
|
+
index.updateData({update: [{id: 'hidden', newGeometry: {type: 'Point', coordinates: [20, 20]}}]});
|
|
311
|
+
|
|
312
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.id)).toEqual(['kept']);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('updateData does not bring back a feature the update filter took out while it had no coordinates', () => {
|
|
316
|
+
const point = {type: 'Feature' as const, id: 'point', properties: {keep: true}, geometry: {type: 'Point' as const, coordinates: [0, 0]}};
|
|
317
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [point]}, {updateable: true});
|
|
318
|
+
const keepIfMarked = (feature: GeoJSON.Feature) => feature.properties?.keep === true;
|
|
319
|
+
|
|
320
|
+
index.updateData({update: [{id: 'point', newGeometry: {type: 'Point', coordinates: []}, addOrUpdateProperties: [{key: 'keep', value: false}]}]}, keepIfMarked);
|
|
321
|
+
index.updateData({update: [{id: 'point', newGeometry: {type: 'Point', coordinates: [10, 10]}, addOrUpdateProperties: [{key: 'keep', value: true}]}]}, keepIfMarked);
|
|
322
|
+
|
|
323
|
+
expect(index.getData()).toEqual({type: 'FeatureCollection', features: []});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('updateData draws a bare geometry loaded without coordinates once an update gives it some', () => {
|
|
327
|
+
const index = new GeoJSONVT({type: 'LineString', coordinates: []}, {updateable: true, generateId: true});
|
|
328
|
+
|
|
329
|
+
index.updateData({update: [{id: 0, newGeometry: {type: 'LineString', coordinates: [[0, 0], [10, 10]]}}]});
|
|
330
|
+
|
|
331
|
+
expect(index.getTile(0, 0, 0)?.features.map(feature => feature.id)).toEqual([0]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('updateData keeps a feature loaded without a geometry away from the update filter', () => {
|
|
335
|
+
const point = {type: 'Feature' as const, id: 'point', properties: {}, geometry: {type: 'Point' as const, coordinates: [0, 0]}};
|
|
336
|
+
const later: GeoJSON.Feature = {type: 'Feature', id: 'later', properties: {}, geometry: null};
|
|
337
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [point, later]}, {updateable: true});
|
|
338
|
+
const pointsOnly = (feature: GeoJSON.Feature) => feature.geometry.type === 'Point';
|
|
339
|
+
|
|
340
|
+
index.updateData({update: [{id: 'point', addOrUpdateProperties: [{key: 'name', value: 'p'}]}]}, pointsOnly);
|
|
341
|
+
index.updateData({update: [{id: 'later', newGeometry: {type: 'Point', coordinates: [10, 10]}}]}, pointsOnly);
|
|
342
|
+
|
|
343
|
+
expect(index.getTile(0, 0, 0)?.features.map(feature => feature.id).sort()).toEqual(['later', 'point']);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('updateData hands the filter a feature without coordinates with its promoted id', () => {
|
|
347
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [{type: 'Feature', id: 'raw', properties: {key: 'promoted'}, geometry: {type: 'Point', coordinates: []}}]}, {updateable: true, promoteId: 'key'});
|
|
348
|
+
|
|
349
|
+
index.updateData({}, feature => feature.id === 'promoted');
|
|
350
|
+
index.updateData({update: [{id: 'promoted', newGeometry: {type: 'Point', coordinates: [10, 10]}}]});
|
|
351
|
+
|
|
352
|
+
expect(index.getTile(0, 0, 0)?.features.map(feature => feature.id)).toEqual(['promoted']);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('updateData hands the filter a feature without coordinates with its generated id', () => {
|
|
356
|
+
const index = new GeoJSONVT({type: 'FeatureCollection', features: [{type: 'Feature', properties: {}, geometry: {type: 'Point', coordinates: []}}]}, {updateable: true, generateId: true});
|
|
357
|
+
|
|
358
|
+
index.updateData({}, feature => feature.id === 0);
|
|
359
|
+
index.updateData({update: [{id: 0, newGeometry: {type: 'Point', coordinates: [10, 10]}}]});
|
|
360
|
+
|
|
361
|
+
expect(index.getTile(0, 0, 0)?.features.map(feature => feature.id)).toEqual([0]);
|
|
362
|
+
});
|
|
363
|
+
|
|
213
364
|
test('keeps the corner of a polygon whose vertex lies on the left edge of a tile', () => {
|
|
214
365
|
const polygon = {
|
|
215
366
|
type: 'Polygon' as const,
|
|
@@ -261,14 +412,14 @@ test('clusters each position of a MultiPoint feature when clustering is on', ()
|
|
|
261
412
|
]);
|
|
262
413
|
});
|
|
263
414
|
|
|
264
|
-
test('
|
|
415
|
+
test('a unit square without wrap leaves the positions of a MultiPoint outside the world out of the clusters', () => {
|
|
265
416
|
const multiPoint = {
|
|
266
417
|
type: 'Feature' as const,
|
|
267
418
|
properties: {},
|
|
268
419
|
geometry: {type: 'MultiPoint' as const, coordinates: [[0.99, 0.5], [1.03, 0.5], [0.5, -0.03]]}
|
|
269
420
|
};
|
|
270
421
|
|
|
271
|
-
const index = new GeoJSONVT(multiPoint, {
|
|
422
|
+
const index = new GeoJSONVT(multiPoint, {coordinates: 'unitSquare', wrap: false, cluster: true});
|
|
272
423
|
|
|
273
424
|
expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[507, 256]]]);
|
|
274
425
|
});
|