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