@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.
- package/README.md +8 -0
- package/dist/cluster-tile-index.d.ts +43 -5
- 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 +47 -7
- 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 +462 -181
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +459 -180
- 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 +7 -0
- 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/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 +123 -0
- package/src/cluster-tile-index.ts +270 -99
- package/src/convert.ts +49 -44
- package/src/deconvert.test.ts +4 -4
- package/src/deconvert.ts +15 -29
- package/src/definitions.ts +48 -7
- package/src/difference.test.ts +71 -0
- package/src/difference.ts +50 -21
- package/src/geojson-to-tile.ts +2 -2
- package/src/geojsonvt.test.ts +387 -1
- package/src/geojsonvt.ts +25 -10
- package/src/index.ts +2 -1
- package/src/tile-index.ts +19 -3
- package/src/world-coordinate-helper.ts +63 -0
- package/src/wrap.ts +2 -0
package/src/geojsonvt.test.ts
CHANGED
|
@@ -36,4 +36,390 @@ 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('a unit square without wrap 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, {coordinates: 'unitSquare', wrap: false});
|
|
45
|
+
|
|
46
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[[2770, -64], [3072, 2048], [4160, 2048]]]]);
|
|
47
|
+
});
|
|
48
|
+
|
|
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
|
+
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]}, {coordinates: 'unitSquare', wrap: false, 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('a unit square without wrap returns the data in the unit square', () => {
|
|
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}, {coordinates: 'unitSquare', wrap: false, updateable: true});
|
|
69
|
+
|
|
70
|
+
expect(index.getData()).toEqual({type: 'FeatureCollection', features});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a unit square without wrap keeps a MultiLineString it splits for lineMetrics in the unit square', () => {
|
|
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, {coordinates: 'unitSquare', wrap: false, 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('a unit square without wrap hands the update filter features in the unit square', () => {
|
|
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]}, {coordinates: 'unitSquare', wrap: false, 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('a unit square without wrap 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, {coordinates: 'unitSquare', wrap: false, cluster: true});
|
|
107
|
+
|
|
108
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[507, 256]]]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('a unit square without wrap 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, {coordinates: 'unitSquare', wrap: false, cluster: true});
|
|
121
|
+
|
|
122
|
+
expect(index.getTile(0, 0, 0).features).toHaveLength(2);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('a unit square without wrap returns cluster children and leaves in the unit square', () => {
|
|
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, {coordinates: 'unitSquare', wrap: false, 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('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', () => {
|
|
177
|
+
const inTheTwoTopTiles = {type: 'MultiPoint' as const, coordinates: [[0.25, 0.25], [0.75, 0.25]]};
|
|
178
|
+
|
|
179
|
+
const index = new GeoJSONVT(inTheTwoTopTiles, {coordinates: 'unitSquare', wrap: false});
|
|
180
|
+
|
|
181
|
+
expect([index.getTile(1, -1, 0), index.getTile(1, 2, 0)]).toEqual([null, null]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const weightedPoints = {
|
|
185
|
+
type: 'FeatureCollection' as const,
|
|
186
|
+
features: [[0, 1], [0.01, 2], [100, 10], [100.01, 20]].map(([lng, weight]) => ({
|
|
187
|
+
type: 'Feature' as const,
|
|
188
|
+
geometry: {type: 'Point' as const, coordinates: [lng, 0]},
|
|
189
|
+
properties: {weight}
|
|
190
|
+
}))
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const sumWeights = {
|
|
194
|
+
map: (props: GeoJSON.GeoJsonProperties) => ({sum: (props as {weight: number}).weight}),
|
|
195
|
+
reduce: (a: Record<string, unknown>, b: Record<string, unknown>) => { (a as {sum: number}).sum += (b as {sum: number}).sum; }
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
test('updateClusterOptions aggregates cluster properties when it adds a reduce function', () => {
|
|
199
|
+
const index = new GeoJSONVT(weightedPoints, {
|
|
200
|
+
updateable: true,
|
|
201
|
+
cluster: true,
|
|
202
|
+
clusterOptions: {maxZoom: 2}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
index.updateClusterOptions(true, {maxZoom: 2, ...sumWeights});
|
|
206
|
+
|
|
207
|
+
expect(index.getTile(0, 0, 0).features.map(f => (f.tags as {sum?: number}).sum)).toEqual([3, 30]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('updateClusterOptions stops aggregating cluster properties when it removes the reduce function', () => {
|
|
211
|
+
const index = new GeoJSONVT(weightedPoints, {
|
|
212
|
+
updateable: true,
|
|
213
|
+
cluster: true,
|
|
214
|
+
clusterOptions: {maxZoom: 2, ...sumWeights}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
index.updateClusterOptions(true, {maxZoom: 2});
|
|
218
|
+
|
|
219
|
+
expect(index.getTile(0, 0, 0).features.map(f => (f.tags as {sum?: number}).sum)).toEqual([undefined, undefined]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('skips an empty polygon of a MultiPolygon', () => {
|
|
223
|
+
const multiPolygon = {
|
|
224
|
+
type: 'MultiPolygon' as const,
|
|
225
|
+
coordinates: [[[[0, 0], [10, 0], [10, 10], [0, 0]]], []]
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const index = new GeoJSONVT(multiPolygon, {maxZoom: 5});
|
|
229
|
+
|
|
230
|
+
expect(index.getTile(0, 0, 0).features[0].geometry).toEqual([[[2048, 2048], [2162, 1934], [2162, 2048], [2048, 2048]]]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('leaves out a MultiPolygon that has only empty polygons', () => {
|
|
234
|
+
const multiPolygon = {
|
|
235
|
+
type: 'MultiPolygon' as const,
|
|
236
|
+
coordinates: [[]] as GeoJSON.Position[][][]
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const index = new GeoJSONVT(multiPolygon, {updateable: true});
|
|
240
|
+
|
|
241
|
+
expect(index.getData()).toEqual({type: 'FeatureCollection', features: []});
|
|
242
|
+
});
|
|
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
|
+
|
|
364
|
+
test('keeps the corner of a polygon whose vertex lies on the left edge of a tile', () => {
|
|
365
|
+
const polygon = {
|
|
366
|
+
type: 'Polygon' as const,
|
|
367
|
+
coordinates: [[[-90, -90], [0, -90], [90, -90], [0, 0], [-90, -90]]]
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const index = new GeoJSONVT(polygon, {indexMaxZoom: 0, maxZoom: 24, tolerance: 1.5, buffer: 0});
|
|
371
|
+
|
|
372
|
+
expect(index.getTile(1, 1, 1).features[0].geometry).toEqual([[[0, 4096], [0, 0], [0, 0], [2048, 4096], [0, 4096]]]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('keeps the corner of a polygon whose vertex lies on the right edge of a tile', () => {
|
|
376
|
+
const polygon = {
|
|
377
|
+
type: 'Polygon' as const,
|
|
378
|
+
coordinates: [[[20, 34.365234375], [80, 4.34326171875], [45, 4.34326171875], [20, 4.34326171875], [20, 34.365234375]]]
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const index = new GeoJSONVT(polygon, {maxZoom: 5, buffer: 0});
|
|
382
|
+
|
|
383
|
+
expect(index.getTile(3, 4, 3).features[0].geometry).toEqual([[[1820, 762], [4096, 1986], [4096, 3700], [1820, 3700], [1820, 762]]]);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('keeps the part of a line that enters a tile exactly at a vertex', () => {
|
|
387
|
+
const line = {
|
|
388
|
+
type: 'LineString' as const,
|
|
389
|
+
coordinates: [[-90, 20], [0, 20], [90, 20]]
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const index = new GeoJSONVT(line, {lineMetrics: true, buffer: 0});
|
|
393
|
+
|
|
394
|
+
const feature = index.getTile(1, 1, 0).features[0];
|
|
395
|
+
expect(feature.geometry).toEqual([[[0, 3631], [2048, 3631]]]);
|
|
396
|
+
expect(feature.tags).toEqual({geojsonvt_clip_start: 0.5, geojsonvt_clip_end: 1});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('clusters each position of a MultiPoint feature when clustering is on', () => {
|
|
400
|
+
const multiPoint = {
|
|
401
|
+
type: 'Feature' as const,
|
|
402
|
+
id: 7,
|
|
403
|
+
properties: {name: 'multi'},
|
|
404
|
+
geometry: {type: 'MultiPoint' as const, coordinates: [[20, 20], [20.0001, 20], [60, 20]]}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const index = new GeoJSONVT(multiPoint, {cluster: true});
|
|
408
|
+
|
|
409
|
+
expect(index.getTile(0, 0, 0).features).toEqual([
|
|
410
|
+
{type: 1, geometry: [[284, 227]], tags: {cluster: true, cluster_id: 20, point_count: 2, point_count_abbreviated: 2}, id: 20},
|
|
411
|
+
{type: 1, geometry: [[341, 227]], tags: {name: 'multi'}, id: 7}
|
|
412
|
+
]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('a unit square without wrap leaves the positions of a MultiPoint outside the world out of the clusters', () => {
|
|
416
|
+
const multiPoint = {
|
|
417
|
+
type: 'Feature' as const,
|
|
418
|
+
properties: {},
|
|
419
|
+
geometry: {type: 'MultiPoint' as const, coordinates: [[0.99, 0.5], [1.03, 0.5], [0.5, -0.03]]}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const index = new GeoJSONVT(multiPoint, {coordinates: 'unitSquare', wrap: false, cluster: true});
|
|
423
|
+
|
|
424
|
+
expect(index.getTile(0, 0, 0).features.map(feature => feature.geometry)).toEqual([[[507, 256]]]);
|
|
425
|
+
});
|
package/src/geojsonvt.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {convertToInternal} from './convert';
|
|
2
2
|
import {convertToGeoJSON, featureToGeoJSON} from './deconvert';
|
|
3
|
+
import {getWorldCoordinateHelper, type WorldCoordinateHelper} from './world-coordinate-helper';
|
|
3
4
|
import {wrap} from './wrap';
|
|
4
5
|
import {applySourceDiff, type GeoJSONVTSourceDiff} from './difference';
|
|
5
6
|
import {ClusterTileIndex, defaultClusterOptions} from './cluster-tile-index';
|
|
6
7
|
import {TileIndex} from './tile-index';
|
|
7
|
-
import type {ClusterOrPointFeature, GeoJSONVTTileIndex, GeoJSONVTInternalFeature, GeoJSONVTOptions, GeoJSONVTTile
|
|
8
|
+
import type {ClusterOrPointFeature, GeoJSONVTTileIndex, GeoJSONVTInternalFeature, GeoJSONVTOptions, GeoJSONVTTile} from './definitions';
|
|
8
9
|
|
|
9
10
|
export const defaultOptions: GeoJSONVTOptions = {
|
|
10
11
|
maxZoom: 14,
|
|
@@ -19,7 +20,9 @@ export const defaultOptions: GeoJSONVTOptions = {
|
|
|
19
20
|
updateable: false,
|
|
20
21
|
cluster: false,
|
|
21
22
|
clusterOptions: defaultClusterOptions,
|
|
22
|
-
debug: 0
|
|
23
|
+
debug: 0,
|
|
24
|
+
coordinates: 'lngLat',
|
|
25
|
+
wrap: true
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -27,12 +30,15 @@ export const defaultOptions: GeoJSONVTOptions = {
|
|
|
27
30
|
*/
|
|
28
31
|
export class GeoJSONVT {
|
|
29
32
|
private options: GeoJSONVTOptions;
|
|
33
|
+
private worldCoordinateHelper: WorldCoordinateHelper;
|
|
30
34
|
|
|
31
35
|
private source?: GeoJSONVTInternalFeature[];
|
|
36
|
+
private unlocated?: Map<string | number, GeoJSON.Feature>;
|
|
32
37
|
private tileIndex: GeoJSONVTTileIndex;
|
|
33
38
|
|
|
34
39
|
constructor(data: GeoJSON.GeoJSON, options?: GeoJSONVTOptions) {
|
|
35
40
|
options = this.options = Object.assign({}, defaultOptions, options);
|
|
41
|
+
this.worldCoordinateHelper = getWorldCoordinateHelper(options.coordinates);
|
|
36
42
|
|
|
37
43
|
const debug = options.debug;
|
|
38
44
|
|
|
@@ -41,8 +47,11 @@ export class GeoJSONVT {
|
|
|
41
47
|
if (options.maxZoom < 0 || options.maxZoom > 24) throw new Error('maxZoom should be in the 0-24 range');
|
|
42
48
|
if (options.promoteId && options.generateId) throw new Error('promoteId and generateId cannot be used together.');
|
|
43
49
|
|
|
50
|
+
// for updateable indexes, keep the properties of the features without coordinates, so that an update can give them some
|
|
51
|
+
if (options.updateable) this.unlocated = new Map();
|
|
52
|
+
|
|
44
53
|
// projects and adds simplification info
|
|
45
|
-
let features = convertToInternal(data, options);
|
|
54
|
+
let features = convertToInternal(data, options, this.unlocated);
|
|
46
55
|
|
|
47
56
|
if (debug) {
|
|
48
57
|
console.timeEnd('preprocess data');
|
|
@@ -62,7 +71,7 @@ export class GeoJSONVT {
|
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
private initializeIndex(features: GeoJSONVTInternalFeature[], options: GeoJSONVTOptions) {
|
|
65
|
-
this.tileIndex = options.cluster ? new ClusterTileIndex(options.clusterOptions) : new TileIndex(options);
|
|
74
|
+
this.tileIndex = options.cluster ? new ClusterTileIndex({...options.clusterOptions, coordinates: options.coordinates, wrap: options.wrap}) : new TileIndex(options);
|
|
66
75
|
if (!features.length) return;
|
|
67
76
|
this.tileIndex.initialize(features);
|
|
68
77
|
}
|
|
@@ -94,7 +103,7 @@ export class GeoJSONVT {
|
|
|
94
103
|
if (!options.updateable) throw new Error('to update tile geojson `updateable` option must be set to true');
|
|
95
104
|
|
|
96
105
|
// apply diff and collect affected features and updated source that will be used to invalidate tiles
|
|
97
|
-
let {affected, source} = applySourceDiff(this.source, diff, options);
|
|
106
|
+
let {affected, source} = applySourceDiff(this.source, diff, options, this.unlocated);
|
|
98
107
|
|
|
99
108
|
if (filter) {
|
|
100
109
|
({affected, source} = this.filterUpdate(source, affected, filter));
|
|
@@ -110,19 +119,25 @@ export class GeoJSONVT {
|
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
/**
|
|
113
|
-
* Filter an update using a predicate function
|
|
122
|
+
* Filter an update using a predicate function, the features an update emptied included, so that one the filter
|
|
123
|
+
* rejects can't come back with a later update. A feature without a geometry at all isn't passed to the predicate.
|
|
124
|
+
* Returns the affected and updated source features.
|
|
114
125
|
*/
|
|
115
126
|
private filterUpdate(source: GeoJSONVTInternalFeature[], affected: GeoJSONVTInternalFeature[], predicate: (feature: GeoJSON.Feature) => boolean) {
|
|
116
127
|
const removeIds = new Set();
|
|
117
128
|
|
|
118
129
|
for (const feature of source) {
|
|
119
130
|
if (feature.id == undefined) continue;
|
|
120
|
-
if (predicate(featureToGeoJSON(feature))) continue;
|
|
131
|
+
if (predicate(featureToGeoJSON(feature, this.worldCoordinateHelper))) continue;
|
|
121
132
|
affected.push(feature);
|
|
122
133
|
removeIds.add(feature.id);
|
|
123
134
|
}
|
|
124
135
|
source = source.filter(feature => !removeIds.has(feature.id));
|
|
125
136
|
|
|
137
|
+
for (const [id, feature] of this.unlocated ?? []) {
|
|
138
|
+
if (feature.geometry && !predicate(feature)) this.unlocated.delete(id);
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
return {affected, source};
|
|
127
142
|
}
|
|
128
143
|
|
|
@@ -131,15 +146,15 @@ export class GeoJSONVT {
|
|
|
131
146
|
*/
|
|
132
147
|
getData(): GeoJSON.GeoJSON {
|
|
133
148
|
if (!this.options.updateable) throw new Error('to retrieve data the `updateable` option must be set to true');
|
|
134
|
-
return convertToGeoJSON(this.source);
|
|
149
|
+
return convertToGeoJSON(this.source, this.worldCoordinateHelper);
|
|
135
150
|
}
|
|
136
151
|
|
|
137
152
|
/**
|
|
138
153
|
* Update supercluster options and regenerate the index.
|
|
139
154
|
* @param cluster - whether to enable clustering
|
|
140
|
-
* @param clusterOptions - {@link SuperclusterOptions}
|
|
155
|
+
* @param clusterOptions - {@link SuperclusterOptions} without `coordinates` and `wrap`, which come from the index options
|
|
141
156
|
*/
|
|
142
|
-
updateClusterOptions(cluster: boolean, clusterOptions:
|
|
157
|
+
updateClusterOptions(cluster: boolean, clusterOptions: GeoJSONVTOptions['clusterOptions']) {
|
|
143
158
|
const wasCluster = this.options.cluster;
|
|
144
159
|
this.options.cluster = cluster;
|
|
145
160
|
this.options.clusterOptions = clusterOptions;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import type {GeoJSONVTFeatureDiff, GeoJSONVTSourceDiff} from './difference';
|
|
3
|
-
import type {GeoJSONVTInternalFeature, GeoJSONVTInternalLineStringFeature, GeoJSONVTInternalMultiLineStringFeature, GeoJSONVTInternalMultiPointFeature, GeoJSONVTInternalMultiPolygonFeature, GeoJSONVTInternalPointFeature, GeoJSONVTInternalPolygonFeature, GeoJSONVTOptions, GeoJSONToTileOptions, PartialGeoJSONVTFeature, SliceArray, SliceFixedArray, ClusterProperties, ClusterFeature, ClusterOrPointFeature, GeoJSONVTTile, GeoJSONVTFeature, GeoJSONVTFeaturePoint, GeoJSONVTFeatureNonPoint, GeoJSONVTInternalTileFeaturePoint, GeoJSONVTInternalTileFeatureNonPoint, GeoJSONVTInternalTile, GeoJSONVTInternalTileFeature, GeoJSONVTTileIndex, SuperclusterOptions} from './definitions';
|
|
3
|
+
import type {GeoJSONVTInternalFeature, GeoJSONVTInternalLineStringFeature, GeoJSONVTInternalMultiLineStringFeature, GeoJSONVTInternalMultiPointFeature, GeoJSONVTInternalMultiPolygonFeature, GeoJSONVTInternalPointFeature, GeoJSONVTInternalPolygonFeature, GeoJSONVTOptions, GeoJSONToTileOptions, PartialGeoJSONVTFeature, SliceArray, SliceFixedArray, ClusterProperties, ClusterFeature, ClusterOrPointFeature, GeoJSONVTTile, GeoJSONVTFeature, GeoJSONVTFeaturePoint, GeoJSONVTFeatureNonPoint, GeoJSONVTInternalTileFeaturePoint, GeoJSONVTInternalTileFeatureNonPoint, GeoJSONVTInternalTile, GeoJSONVTInternalTileFeature, GeoJSONVTTileIndex, SuperclusterOptions, GeoJSONVTCoordinatesType} from './definitions';
|
|
4
4
|
import type {KDBushWithData} from './cluster-tile-index';
|
|
5
5
|
import {GeoJSONVT} from './geojsonvt';
|
|
6
6
|
import {ClusterTileIndex} from './cluster-tile-index';
|
|
@@ -39,6 +39,7 @@ export type {
|
|
|
39
39
|
GeoJSONVTInternalPolygonFeature,
|
|
40
40
|
GeoJSONVTInternalMultiPolygonFeature,
|
|
41
41
|
SuperclusterOptions,
|
|
42
|
+
GeoJSONVTCoordinatesType,
|
|
42
43
|
ClusterProperties,
|
|
43
44
|
ClusterFeature,
|
|
44
45
|
ClusterOrPointFeature,
|
package/src/tile-index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export class TileIndex implements GeoJSONVTTileIndex {
|
|
|
23
23
|
|
|
24
24
|
initialize(features: GeoJSONVTInternalFeature[]): void {
|
|
25
25
|
// start slicing from the top tile down
|
|
26
|
-
this.splitTile(features, 0, 0, 0);
|
|
26
|
+
this.splitTile(this.clipToWorld(features), 0, 0, 0);
|
|
27
27
|
|
|
28
28
|
if (this.options.debug) {
|
|
29
29
|
if (features.length) console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints);
|
|
@@ -45,8 +45,9 @@ export class TileIndex implements GeoJSONVTTileIndex {
|
|
|
45
45
|
|
|
46
46
|
// re-generate root tile with updated feature set
|
|
47
47
|
const [z, x, y] = [0, 0, 0];
|
|
48
|
-
const
|
|
49
|
-
rootTile
|
|
48
|
+
const rootSource = this.clipToWorld(source);
|
|
49
|
+
const rootTile = createTile(rootSource, z, x, y, options);
|
|
50
|
+
rootTile.source = rootSource;
|
|
50
51
|
|
|
51
52
|
// update tile index with new root tile - ready for getTile calls
|
|
52
53
|
const id = toID(z, x, y);
|
|
@@ -83,6 +84,7 @@ export class TileIndex implements GeoJSONVTTileIndex {
|
|
|
83
84
|
const {extent, debug} = this.options;
|
|
84
85
|
|
|
85
86
|
const z2 = 1 << z;
|
|
87
|
+
if (!this.options.wrap && (x < 0 || x >= z2)) return null; // a row outside the world has no tile either
|
|
86
88
|
x = (x + z2) & (z2 - 1); // wrap tile x coordinate
|
|
87
89
|
|
|
88
90
|
const id = toID(z, x, y);
|
|
@@ -119,6 +121,20 @@ export class TileIndex implements GeoJSONVTTileIndex {
|
|
|
119
121
|
return transformTile(this.tiles[id], extent);
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The features the top tile starts from. A world that wraps keeps them all left and right, where wrapping already
|
|
126
|
+
* put them inside, and one that doesn't only what lies inside the world square and its buffer. Positions in the
|
|
127
|
+
* unit square are also cut above and below in a world that wraps, where lng/lat is clamped already. The index keeps
|
|
128
|
+
* the rest, so an update can still move a feature in from outside.
|
|
129
|
+
*/
|
|
130
|
+
private clipToWorld(features: GeoJSONVTInternalFeature[]): GeoJSONVTInternalFeature[] {
|
|
131
|
+
const {wrap, coordinates} = this.options;
|
|
132
|
+
if (wrap && coordinates !== 'unitSquare') return features;
|
|
133
|
+
const buffer = this.options.buffer / this.options.extent;
|
|
134
|
+
const inX = wrap ? features : clip(features, 1, -buffer, 1 + buffer, AxisType.X, -Infinity, Infinity, this.options) || [];
|
|
135
|
+
return clip(inX, 1, -buffer, 1 + buffer, AxisType.Y, -Infinity, Infinity, this.options) || [];
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
/**
|
|
123
139
|
* splits features from a parent tile to sub-tiles.
|
|
124
140
|
* z, x, and y are the coordinates of the parent tile
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {GeoJSONVTCoordinatesType} from './definitions';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How the positions of the input relate to the world square the tiles cover, in which x and y run from 0 to 1.
|
|
5
|
+
*/
|
|
6
|
+
export interface WorldCoordinateHelper {
|
|
7
|
+
projectX(x: number): number;
|
|
8
|
+
projectY(y: number): number;
|
|
9
|
+
unprojectX(x: number): number;
|
|
10
|
+
unprojectY(y: number): number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Longitude and latitude, which spherical mercator puts in the world square.
|
|
15
|
+
*/
|
|
16
|
+
class MercatorWorldCoordinateHelper implements WorldCoordinateHelper {
|
|
17
|
+
projectX(lng: number): number {
|
|
18
|
+
return lng / 360 + 0.5;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
projectY(lat: number): number {
|
|
22
|
+
const sin = Math.sin(lat * Math.PI / 180);
|
|
23
|
+
const y = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
|
|
24
|
+
return y < 0 ? 0 : y > 1 ? 1 : y;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
unprojectX(x: number): number {
|
|
28
|
+
return (x - 0.5) * 360;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
unprojectY(y: number): number {
|
|
32
|
+
const y2 = (180 - y * 360) * Math.PI / 180;
|
|
33
|
+
return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Positions in the unit square already, see `coordinates` in {@link GeoJSONVTOptions}.
|
|
39
|
+
*/
|
|
40
|
+
class IdentityWorldCoordinateHelper implements WorldCoordinateHelper {
|
|
41
|
+
projectX(x: number): number {
|
|
42
|
+
return x;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
projectY(y: number): number {
|
|
46
|
+
return y;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
unprojectX(x: number): number {
|
|
50
|
+
return x;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
unprojectY(y: number): number {
|
|
54
|
+
return y;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const mercatorWorldCoordinateHelper: WorldCoordinateHelper = new MercatorWorldCoordinateHelper();
|
|
59
|
+
const identityWorldCoordinateHelper: WorldCoordinateHelper = new IdentityWorldCoordinateHelper();
|
|
60
|
+
|
|
61
|
+
export function getWorldCoordinateHelper(coordinates?: GeoJSONVTCoordinatesType): WorldCoordinateHelper {
|
|
62
|
+
return coordinates === 'unitSquare' ? identityWorldCoordinateHelper : mercatorWorldCoordinateHelper;
|
|
63
|
+
}
|
package/src/wrap.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { GeoJSONVTInternalFeature, GeoJSONVTOptions, SliceArray, SliceFixed
|
|
|
4
4
|
import {createFeature, optimizeLineMemory} from './feature';
|
|
5
5
|
|
|
6
6
|
export function wrap(features: GeoJSONVTInternalFeature[], options: GeoJSONVTOptions): GeoJSONVTInternalFeature[] {
|
|
7
|
+
if (!options.wrap) return features;
|
|
8
|
+
|
|
7
9
|
const buffer = options.buffer / options.extent;
|
|
8
10
|
let merged = features;
|
|
9
11
|
|