@maplibre/geojson-vt 5.0.1 → 5.0.2
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/dist/geojson-vt-dev.js +511 -534
- package/dist/geojson-vt.js +1 -1
- package/dist/geojson-vt.mjs +1155 -0
- package/dist/geojson-vt.mjs.map +1 -0
- package/package.json +19 -7
- package/src/clip.test.ts +79 -0
- package/src/clip.ts +232 -0
- package/src/convert.ts +178 -0
- package/src/definitions.ts +86 -0
- package/src/difference.test.ts +270 -0
- package/src/{difference.js → difference.ts} +87 -49
- package/src/feature.ts +64 -0
- package/src/{index.js → index.ts} +107 -52
- package/src/simplify.test.ts +73 -0
- package/src/{simplify.js → simplify.ts} +22 -9
- package/src/tile.ts +166 -0
- package/src/transform.ts +55 -0
- package/src/wrap.ts +81 -0
- package/src/clip.js +0 -200
- package/src/convert.js +0 -139
- package/src/feature.js +0 -43
- package/src/tile.js +0 -123
- package/src/transform.js +0 -41
- package/src/wrap.js +0 -68
package/src/convert.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
|
|
2
|
+
import {simplify} from './simplify';
|
|
3
|
+
import {createFeature} from './feature';
|
|
4
|
+
import type { GeoJSONVTFeature, GeoJSONVTOptions, StartEndSizeArray } from './definitions';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* converts GeoJSON feature into an intermediate projected JSON vector format with simplification data
|
|
8
|
+
* @param data
|
|
9
|
+
* @param options
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
export function convert(data: GeoJSON.GeoJSON, options: GeoJSONVTOptions): GeoJSONVTFeature[] {
|
|
13
|
+
const features: GeoJSONVTFeature[] = [];
|
|
14
|
+
|
|
15
|
+
switch (data.type) {
|
|
16
|
+
case 'FeatureCollection':
|
|
17
|
+
for (let i = 0; i < data.features.length; i++) {
|
|
18
|
+
convertFeature(features, data.features[i], options, i);
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
case 'Feature':
|
|
22
|
+
convertFeature(features, data, options);
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
convertFeature(features, {type: "Feature" as const, geometry: data, properties: undefined}, options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return features;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function convertFeature(features: GeoJSONVTFeature[], geojson: GeoJSON.Feature, options: GeoJSONVTOptions, index?: number) {
|
|
32
|
+
if (!geojson.geometry) return;
|
|
33
|
+
|
|
34
|
+
if (geojson.geometry.type === 'GeometryCollection') {
|
|
35
|
+
for (const singleGeometry of geojson.geometry.geometries) {
|
|
36
|
+
convertFeature(features, {
|
|
37
|
+
id: geojson.id,
|
|
38
|
+
type: 'Feature',
|
|
39
|
+
geometry: singleGeometry,
|
|
40
|
+
properties: geojson.properties
|
|
41
|
+
}, options, index);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const coords = geojson.geometry.coordinates;
|
|
47
|
+
if (!coords?.length) return;
|
|
48
|
+
|
|
49
|
+
const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
|
|
50
|
+
let id = geojson.id;
|
|
51
|
+
if (options.promoteId) {
|
|
52
|
+
id = geojson.properties?.[options.promoteId];
|
|
53
|
+
} else if (options.generateId) {
|
|
54
|
+
id = index || 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (geojson.geometry.type) {
|
|
58
|
+
case 'Point': {
|
|
59
|
+
const pointGeometry: StartEndSizeArray = [];
|
|
60
|
+
convertPoint(geojson.geometry.coordinates, pointGeometry);
|
|
61
|
+
|
|
62
|
+
features.push(createFeature(id, geojson.geometry.type, pointGeometry, geojson.properties));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'MultiPoint': {
|
|
67
|
+
const multiPointGeometry: StartEndSizeArray = [];
|
|
68
|
+
for (const p of geojson.geometry.coordinates) {
|
|
69
|
+
convertPoint(p, multiPointGeometry);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
features.push(createFeature(id, geojson.geometry.type, multiPointGeometry, geojson.properties));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case 'LineString': {
|
|
77
|
+
const lineGeometry: StartEndSizeArray = [];
|
|
78
|
+
convertLine(geojson.geometry.coordinates, lineGeometry, tolerance, false);
|
|
79
|
+
|
|
80
|
+
features.push(createFeature(id, geojson.geometry.type, lineGeometry, geojson.properties));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case 'MultiLineString': {
|
|
85
|
+
if (options.lineMetrics) {
|
|
86
|
+
// explode into linestrings in order to track metrics
|
|
87
|
+
for (const line of geojson.geometry.coordinates) {
|
|
88
|
+
const lineGeometry: StartEndSizeArray = [];
|
|
89
|
+
convertLine(line, lineGeometry, tolerance, false);
|
|
90
|
+
features.push(createFeature(id, 'LineString', lineGeometry, geojson.properties));
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const multiLineGeometry: StartEndSizeArray[] = [];
|
|
96
|
+
convertLines(geojson.geometry.coordinates, multiLineGeometry, tolerance, false);
|
|
97
|
+
|
|
98
|
+
features.push(createFeature(id, geojson.geometry.type, multiLineGeometry, geojson.properties));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'Polygon': {
|
|
103
|
+
const polygonGeometry: StartEndSizeArray[] = [];
|
|
104
|
+
convertLines(geojson.geometry.coordinates, polygonGeometry, tolerance, true);
|
|
105
|
+
|
|
106
|
+
features.push(createFeature(id, geojson.geometry.type, polygonGeometry, geojson.properties));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'MultiPolygon': {
|
|
111
|
+
const multiPolygonGeometry: StartEndSizeArray[][] = [];
|
|
112
|
+
for (const polygon of geojson.geometry.coordinates) {
|
|
113
|
+
const newPolygon: StartEndSizeArray[] = [];
|
|
114
|
+
convertLines(polygon, newPolygon, tolerance, true);
|
|
115
|
+
multiPolygonGeometry.push(newPolygon);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
features.push(createFeature(id, geojson.geometry.type, multiPolygonGeometry, geojson.properties));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
throw new Error('Input data is not a valid GeoJSON object.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function convertPoint(coords: GeoJSON.Position, out: number[]) {
|
|
128
|
+
out.push(projectX(coords[0]), projectY(coords[1]), 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function convertLine(ring: GeoJSON.Position[], out: StartEndSizeArray, tolerance: number, isPolygon: boolean) {
|
|
132
|
+
let x0, y0;
|
|
133
|
+
let size = 0;
|
|
134
|
+
|
|
135
|
+
for (let j = 0; j < ring.length; j++) {
|
|
136
|
+
const x = projectX(ring[j][0]);
|
|
137
|
+
const y = projectY(ring[j][1]);
|
|
138
|
+
|
|
139
|
+
out.push(x, y, 0);
|
|
140
|
+
|
|
141
|
+
if (j > 0) {
|
|
142
|
+
if (isPolygon) {
|
|
143
|
+
size += (x0 * y - x * y0) / 2; // area
|
|
144
|
+
} else {
|
|
145
|
+
size += Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2)); // length
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
x0 = x;
|
|
149
|
+
y0 = y;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const last = out.length - 3;
|
|
153
|
+
out[2] = 1;
|
|
154
|
+
if (tolerance > 0) simplify(out, 0, last, tolerance);
|
|
155
|
+
out[last + 2] = 1;
|
|
156
|
+
|
|
157
|
+
out.size = Math.abs(size);
|
|
158
|
+
out.start = 0;
|
|
159
|
+
out.end = out.size;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function convertLines(rings: GeoJSON.Position[][], out: StartEndSizeArray[], tolerance: number, isPolygon: boolean) {
|
|
163
|
+
for (let i = 0; i < rings.length; i++) {
|
|
164
|
+
const geom: StartEndSizeArray = [];
|
|
165
|
+
convertLine(rings[i], geom, tolerance, isPolygon);
|
|
166
|
+
out.push(geom);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function projectX(x: number) {
|
|
171
|
+
return x / 360 + 0.5;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function projectY(y: number) {
|
|
175
|
+
const sin = Math.sin(y * Math.PI / 180);
|
|
176
|
+
const y2 = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
|
|
177
|
+
return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
|
|
178
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type GeoJSONVTOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Max zoom to preserve detail on
|
|
4
|
+
* @default 14
|
|
5
|
+
*/
|
|
6
|
+
maxZoom?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Max zoom in the tile index
|
|
9
|
+
* @default 5
|
|
10
|
+
*/
|
|
11
|
+
indexMaxZoom?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Max number of points per tile in the tile index
|
|
14
|
+
* @default 100000
|
|
15
|
+
*/
|
|
16
|
+
indexMaxPoints?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Simplification tolerance (higher means simpler)
|
|
19
|
+
* @default 3
|
|
20
|
+
*/
|
|
21
|
+
tolerance?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Tile extent
|
|
24
|
+
* @default 4096
|
|
25
|
+
*/
|
|
26
|
+
extent?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Tile buffer on each side
|
|
29
|
+
* @default 64
|
|
30
|
+
*/
|
|
31
|
+
buffer?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Whether to calculate line metrics
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
lineMetrics?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Name of a feature property to be promoted to feature.id
|
|
39
|
+
*/
|
|
40
|
+
promoteId?: string | null;
|
|
41
|
+
/**
|
|
42
|
+
* Whether to generate feature ids. Cannot be used with promoteId
|
|
43
|
+
* @default false
|
|
44
|
+
*/
|
|
45
|
+
generateId?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether geojson can be updated (with caveat of a stored simplified copy)
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
updateable?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Logging level (0, 1 or 2)
|
|
53
|
+
* @default 0
|
|
54
|
+
*/
|
|
55
|
+
debug?: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
export type StartEndSizeArray = number[] & { start?: number; end?: number; size?: number };
|
|
60
|
+
|
|
61
|
+
export type PartialGeoJSONVTFeature = {
|
|
62
|
+
id?: number | string | undefined;
|
|
63
|
+
tags: GeoJSON.GeoJsonProperties;
|
|
64
|
+
minX: number;
|
|
65
|
+
minY: number;
|
|
66
|
+
maxX: number;
|
|
67
|
+
maxY: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type GeometryTypeMap = {
|
|
71
|
+
Point: number[];
|
|
72
|
+
MultiPoint: number[];
|
|
73
|
+
LineString: StartEndSizeArray;
|
|
74
|
+
MultiLineString: StartEndSizeArray[];
|
|
75
|
+
Polygon: StartEndSizeArray[];
|
|
76
|
+
MultiPolygon: StartEndSizeArray[][];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type GeometryType = "Point" | "MultiPoint" | "LineString" | "MultiLineString" | "Polygon" | "MultiPolygon";
|
|
80
|
+
|
|
81
|
+
export type GeoJSONVTFeature = {
|
|
82
|
+
[K in GeometryType]: PartialGeoJSONVTFeature & {
|
|
83
|
+
type: K;
|
|
84
|
+
geometry: GeometryTypeMap[K];
|
|
85
|
+
}
|
|
86
|
+
}[GeometryType];
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import {test, expect} from 'vitest';
|
|
2
|
+
import {applySourceDiff} from './difference';
|
|
3
|
+
|
|
4
|
+
const options = {
|
|
5
|
+
maxZoom: 14,
|
|
6
|
+
indexMaxZoom: 5,
|
|
7
|
+
indexMaxPoints: 100000,
|
|
8
|
+
tolerance: 3,
|
|
9
|
+
extent: 4096,
|
|
10
|
+
buffer: 64,
|
|
11
|
+
updateable: true
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
test('applySourceDiff: adds a feature using the feature id', () => {
|
|
15
|
+
const point = {
|
|
16
|
+
type: 'Feature' as const,
|
|
17
|
+
id: 'point',
|
|
18
|
+
geometry: {
|
|
19
|
+
type: 'Point' as const,
|
|
20
|
+
coordinates: [0, 0]
|
|
21
|
+
},
|
|
22
|
+
properties: {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const {source} = applySourceDiff([], {
|
|
26
|
+
add: [point]
|
|
27
|
+
}, options);
|
|
28
|
+
|
|
29
|
+
expect(source.length).toBe(1);
|
|
30
|
+
expect(source[0].id).toBe('point');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('applySourceDiff: adds a feature using the promoteId', () => {
|
|
34
|
+
const point2 = {
|
|
35
|
+
type: 'Feature' as const,
|
|
36
|
+
geometry: {
|
|
37
|
+
type: 'Point' as const,
|
|
38
|
+
coordinates: [0, 0],
|
|
39
|
+
},
|
|
40
|
+
properties: {
|
|
41
|
+
promoteId: 'point2'
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const {source} = applySourceDiff([], {
|
|
46
|
+
add: [point2]
|
|
47
|
+
}, {promoteId: 'promoteId'});
|
|
48
|
+
|
|
49
|
+
expect(source.length).toBe(1);
|
|
50
|
+
expect(source[0].id).toBe('point2');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('applySourceDiff: removes a feature by its id', () => {
|
|
54
|
+
const point = {
|
|
55
|
+
type: 'Point' as const,
|
|
56
|
+
id: 'point',
|
|
57
|
+
geometry: [0, 0],
|
|
58
|
+
tags: {},
|
|
59
|
+
minX: 0,
|
|
60
|
+
minY: 0,
|
|
61
|
+
maxX: 0,
|
|
62
|
+
maxY: 0
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const point2 = {
|
|
66
|
+
type: 'Point' as const,
|
|
67
|
+
id: 'point2',
|
|
68
|
+
geometry: [0, 0],
|
|
69
|
+
tags: {},
|
|
70
|
+
minX: 0,
|
|
71
|
+
minY: 0,
|
|
72
|
+
maxX: 0,
|
|
73
|
+
maxY: 0
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const {source} = applySourceDiff([point, point2], {
|
|
77
|
+
remove: ['point2'],
|
|
78
|
+
}, options);
|
|
79
|
+
|
|
80
|
+
expect(source.length).toBe(1);
|
|
81
|
+
expect(source[0].id).toBe('point');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('applySourceDiff: removeAll clears all features', () => {
|
|
85
|
+
const point = {
|
|
86
|
+
type: 'Point' as const,
|
|
87
|
+
id: 'point',
|
|
88
|
+
geometry: [0, 0],
|
|
89
|
+
tags: {},
|
|
90
|
+
minX: 0,
|
|
91
|
+
minY: 0,
|
|
92
|
+
maxX: 0,
|
|
93
|
+
maxY: 0
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const point2 = {
|
|
97
|
+
type: 'Point' as const,
|
|
98
|
+
id: 'point2',
|
|
99
|
+
geometry: [0, 0],
|
|
100
|
+
tags: {},
|
|
101
|
+
minX: 0,
|
|
102
|
+
minY: 0,
|
|
103
|
+
maxX: 0,
|
|
104
|
+
maxY: 0
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const source = [point, point2];
|
|
108
|
+
const result = applySourceDiff(source, {
|
|
109
|
+
removeAll: true
|
|
110
|
+
}, options);
|
|
111
|
+
|
|
112
|
+
expect(source).toEqual(result.affected);
|
|
113
|
+
expect(result.source).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('applySourceDiff: updates a feature geometry', () => {
|
|
117
|
+
const point = {
|
|
118
|
+
type: 'Point' as const,
|
|
119
|
+
id: 'point',
|
|
120
|
+
geometry: [0, 0],
|
|
121
|
+
tags: {},
|
|
122
|
+
minX: 0,
|
|
123
|
+
minY: 0,
|
|
124
|
+
maxX: 0,
|
|
125
|
+
maxY: 0
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const {source} = applySourceDiff([point], {
|
|
129
|
+
update: [{
|
|
130
|
+
id: 'point',
|
|
131
|
+
newGeometry: {
|
|
132
|
+
type: 'Point',
|
|
133
|
+
coordinates: [1, 0]
|
|
134
|
+
}
|
|
135
|
+
}]
|
|
136
|
+
}, options);
|
|
137
|
+
|
|
138
|
+
expect(source.length).toBe(1);
|
|
139
|
+
expect(source[0].id).toBe('point');
|
|
140
|
+
expect(source[0].geometry[0]).toBe(0.5027777777777778);
|
|
141
|
+
expect(source[0].geometry[1]).toBe(0.5);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('applySourceDiff: adds properties', () => {
|
|
145
|
+
const point = {
|
|
146
|
+
type: 'Point' as const,
|
|
147
|
+
id: 'point',
|
|
148
|
+
geometry: [0, 0],
|
|
149
|
+
tags: {},
|
|
150
|
+
minX: 0,
|
|
151
|
+
minY: 0,
|
|
152
|
+
maxX: 0,
|
|
153
|
+
maxY: 0
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const {source} = applySourceDiff([point], {
|
|
157
|
+
update: [{
|
|
158
|
+
id: 'point',
|
|
159
|
+
addOrUpdateProperties: [
|
|
160
|
+
{key: 'prop', value: 'value'},
|
|
161
|
+
{key: 'prop2', value: 'value2'}
|
|
162
|
+
]
|
|
163
|
+
}]
|
|
164
|
+
}, options);
|
|
165
|
+
|
|
166
|
+
expect(source.length).toBe(1);
|
|
167
|
+
const tags = source[0].tags;
|
|
168
|
+
expect(Object.keys(tags).length).toBe(2);
|
|
169
|
+
expect(tags.prop).toBe('value');
|
|
170
|
+
expect(tags.prop2).toBe('value2');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('applySourceDiff: updates properties', () => {
|
|
174
|
+
const point = {
|
|
175
|
+
type: 'Point' as const,
|
|
176
|
+
id: 'point',
|
|
177
|
+
geometry: [0, 0],
|
|
178
|
+
tags: {prop: 'value', prop2: 'value2'},
|
|
179
|
+
minX: 0,
|
|
180
|
+
minY: 0,
|
|
181
|
+
maxX: 0,
|
|
182
|
+
maxY: 0
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const {source} = applySourceDiff([point], {
|
|
186
|
+
update: [{
|
|
187
|
+
id: 'point',
|
|
188
|
+
addOrUpdateProperties: [
|
|
189
|
+
{key: 'prop2', value: 'value3'}
|
|
190
|
+
]
|
|
191
|
+
}]
|
|
192
|
+
}, options);
|
|
193
|
+
expect(source.length).toBe(1);
|
|
194
|
+
|
|
195
|
+
const tags2 = source[0].tags;
|
|
196
|
+
expect(Object.keys(tags2).length).toBe(2);
|
|
197
|
+
expect(tags2.prop).toBe('value');
|
|
198
|
+
expect(tags2.prop2).toBe('value3');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('applySourceDiff: removes properties', () => {
|
|
202
|
+
const point = {
|
|
203
|
+
type: 'Point' as const,
|
|
204
|
+
id: 'point',
|
|
205
|
+
geometry: [0, 0],
|
|
206
|
+
tags: {prop: 'value', prop2: 'value2'},
|
|
207
|
+
minX: 0,
|
|
208
|
+
minY: 0,
|
|
209
|
+
maxX: 0,
|
|
210
|
+
maxY: 0
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const {source} = applySourceDiff([point], {
|
|
214
|
+
update: [{
|
|
215
|
+
id: 'point',
|
|
216
|
+
removeProperties: ['prop2']
|
|
217
|
+
}]
|
|
218
|
+
}, options);
|
|
219
|
+
|
|
220
|
+
expect(source.length).toBe(1);
|
|
221
|
+
const tags3 = source[0].tags;
|
|
222
|
+
expect(Object.keys(tags3).length).toBe(1);
|
|
223
|
+
expect(tags3.prop).toBe('value');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('applySourceDiff: removes all properties', () => {
|
|
227
|
+
const point = {
|
|
228
|
+
type: 'Point' as const,
|
|
229
|
+
id: 'point',
|
|
230
|
+
geometry: [0, 0],
|
|
231
|
+
tags: {prop: 'value', prop2: 'value2'},
|
|
232
|
+
minX: 0,
|
|
233
|
+
minY: 0,
|
|
234
|
+
maxX: 0,
|
|
235
|
+
maxY: 0
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const {source} = applySourceDiff([point], {
|
|
239
|
+
update: [{
|
|
240
|
+
id: 'point',
|
|
241
|
+
removeAllProperties: true,
|
|
242
|
+
}]
|
|
243
|
+
}, options);
|
|
244
|
+
|
|
245
|
+
expect(source.length).toBe(1);
|
|
246
|
+
expect(Object.keys(source[0].tags).length).toBe(0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('applySourceDiff: empty update preserves properties', () => {
|
|
250
|
+
const point = {
|
|
251
|
+
type: 'Point' as const,
|
|
252
|
+
id: 'point',
|
|
253
|
+
geometry: [0, 0],
|
|
254
|
+
tags: {prop: 'value', prop2: 'value2'},
|
|
255
|
+
minX: 0,
|
|
256
|
+
minY: 0,
|
|
257
|
+
maxX: 0,
|
|
258
|
+
maxY: 0
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const {source} = applySourceDiff([point], {
|
|
262
|
+
update: [{id: 'point'}]
|
|
263
|
+
}, options);
|
|
264
|
+
|
|
265
|
+
expect(source.length).toBe(1);
|
|
266
|
+
const tags2 = source[0].tags;
|
|
267
|
+
expect(Object.keys(tags2).length).toBe(2);
|
|
268
|
+
expect(tags2.prop).toBe('value');
|
|
269
|
+
expect(tags2.prop2).toBe('value2');
|
|
270
|
+
});
|
|
@@ -1,40 +1,73 @@
|
|
|
1
|
-
import convert from './convert
|
|
2
|
-
import wrap from './wrap
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
import {convert} from './convert';
|
|
2
|
+
import {wrap} from './wrap';
|
|
3
|
+
import type { GeoJSONVTFeature, GeoJSONVTOptions } from './definitions';
|
|
4
|
+
|
|
5
|
+
export type GeoJSONVTSourceDiff = {
|
|
6
|
+
/**
|
|
7
|
+
* If true, clear all existing features
|
|
8
|
+
*/
|
|
9
|
+
removeAll?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Array of feature IDs to remove
|
|
12
|
+
*/
|
|
13
|
+
remove?: (string | number)[];
|
|
14
|
+
/**
|
|
15
|
+
* Array of GeoJSON features to add
|
|
16
|
+
*/
|
|
17
|
+
add?: GeoJSON.Feature[];
|
|
18
|
+
/**
|
|
19
|
+
* Array of per-feature updates
|
|
20
|
+
*/
|
|
21
|
+
update?: GeoJSONVTFeatureDiff[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type GeoJSONVTFeatureDiff = {
|
|
25
|
+
/**
|
|
26
|
+
* ID of the feature being updated
|
|
27
|
+
*/
|
|
28
|
+
id: string | number;
|
|
29
|
+
/**
|
|
30
|
+
* Optional new geometry
|
|
31
|
+
*/
|
|
32
|
+
newGeometry?: GeoJSON.Geometry;
|
|
33
|
+
/**
|
|
34
|
+
* Remove all properties if true
|
|
35
|
+
*/
|
|
36
|
+
removeAllProperties?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Specific properties to delete
|
|
39
|
+
*/
|
|
40
|
+
removeProperties?: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Properties to add or update
|
|
43
|
+
*/
|
|
44
|
+
addOrUpdateProperties?: {
|
|
45
|
+
key: string;
|
|
46
|
+
value: unknown;
|
|
47
|
+
}[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type HashedGeoJSONVTSourceDiff = {
|
|
51
|
+
removeAll?: boolean | undefined;
|
|
52
|
+
remove: Set<string | number>;
|
|
53
|
+
add: Map<string | number | undefined, GeoJSON.Feature>;
|
|
54
|
+
update: Map<string | number, GeoJSONVTFeatureDiff>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Applies a GeoJSON Source Diff to an existing set of simplified features
|
|
59
|
+
* @param source
|
|
60
|
+
* @param dataDiff
|
|
61
|
+
* @param options
|
|
62
|
+
* @returns
|
|
63
|
+
*/
|
|
64
|
+
export function applySourceDiff(source: GeoJSONVTFeature[], dataDiff: GeoJSONVTSourceDiff, options: GeoJSONVTOptions) {
|
|
32
65
|
|
|
33
66
|
// convert diff to sets/maps for o(1) lookups
|
|
34
67
|
const diff = diffToHashed(dataDiff);
|
|
35
68
|
|
|
36
69
|
// collection for features that will be affected by this update
|
|
37
|
-
let affected = [];
|
|
70
|
+
let affected: GeoJSONVTFeature[] = [];
|
|
38
71
|
|
|
39
72
|
// full removal - clear everything before applying diff
|
|
40
73
|
if (diff.removeAll) {
|
|
@@ -103,7 +136,7 @@ export function applySourceDiff(source, dataDiff, options) {
|
|
|
103
136
|
}
|
|
104
137
|
|
|
105
138
|
// return an updated geojsonvt simplified feature
|
|
106
|
-
function getUpdatedFeature(vtFeature, update, options) {
|
|
139
|
+
function getUpdatedFeature(vtFeature: GeoJSONVTFeature, update: GeoJSONVTFeatureDiff, options: GeoJSONVTOptions): GeoJSONVTFeature | null {
|
|
107
140
|
const changeGeometry = !!update.newGeometry;
|
|
108
141
|
|
|
109
142
|
const changeProps =
|
|
@@ -111,13 +144,10 @@ function getUpdatedFeature(vtFeature, update, options) {
|
|
|
111
144
|
update.removeProperties?.length > 0 ||
|
|
112
145
|
update.addOrUpdateProperties?.length > 0;
|
|
113
146
|
|
|
114
|
-
// nothing to do
|
|
115
|
-
if (!changeGeometry && !changeProps) return null;
|
|
116
|
-
|
|
117
147
|
// if geometry changed, need to create new geojson feature and convert to simplified format
|
|
118
148
|
if (changeGeometry) {
|
|
119
149
|
const geojsonFeature = {
|
|
120
|
-
type: 'Feature',
|
|
150
|
+
type: 'Feature' as const,
|
|
121
151
|
id: vtFeature.id,
|
|
122
152
|
geometry: update.newGeometry,
|
|
123
153
|
properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
|
|
@@ -142,8 +172,10 @@ function getUpdatedFeature(vtFeature, update, options) {
|
|
|
142
172
|
return null;
|
|
143
173
|
}
|
|
144
174
|
|
|
145
|
-
|
|
146
|
-
|
|
175
|
+
/**
|
|
176
|
+
* helper to apply property updates from a diff update object to a properties object
|
|
177
|
+
*/
|
|
178
|
+
function applyPropertyUpdates(tags: GeoJSON.GeoJsonProperties, update: GeoJSONVTFeatureDiff): GeoJSON.GeoJsonProperties {
|
|
147
179
|
if (update.removeAllProperties) {
|
|
148
180
|
return {};
|
|
149
181
|
}
|
|
@@ -165,16 +197,22 @@ function applyPropertyUpdates(tags, update) {
|
|
|
165
197
|
return properties;
|
|
166
198
|
}
|
|
167
199
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Convert a GeoJSON Source Diff to an idempotent hashed representation using Sets and Maps
|
|
202
|
+
*/
|
|
203
|
+
export function diffToHashed(diff: GeoJSONVTSourceDiff): HashedGeoJSONVTSourceDiff {
|
|
204
|
+
if (!diff) return {
|
|
205
|
+
remove: new Set(),
|
|
206
|
+
add: new Map(),
|
|
207
|
+
update: new Map()
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const hashed: HashedGeoJSONVTSourceDiff = {
|
|
211
|
+
removeAll: diff.removeAll,
|
|
212
|
+
remove: new Set(diff.remove || []),
|
|
213
|
+
add: new Map(diff.add?.map(feature => [feature.id, feature])),
|
|
214
|
+
update: new Map(diff.update?.map(update => [update.id, update]))
|
|
215
|
+
};
|
|
178
216
|
|
|
179
217
|
return hashed;
|
|
180
218
|
}
|