@maplibre/geojson-vt 5.0.0 → 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/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.js'; // GeoJSON conversion and preprocessing
2
- import wrap from './wrap.js'; // date line processing
3
-
4
- // This file provides a set of helper functions for managing "diffs" (changes)
5
- // to GeoJSON data structures. These diffs describe additions, removals,
6
- // and updates of features in a GeoJSON source in an efficient way.
7
-
8
- // GeoJSON Source Diff:
9
- // {
10
- // removeAll: true, // If true, clear all existing features
11
- // remove: [featureId, ...], // Array of feature IDs to remove
12
- // add: [feature, ...], // Array of GeoJSON features to add
13
- // update: [GeoJSON Feature Diff, ...] // Array of per-feature updates
14
- // }
15
-
16
- // GeoJSON Feature Diff:
17
- // {
18
- // id: featureId, // ID of the feature being updated
19
- // newGeometry: GeoJSON.Geometry, // Optional new geometry
20
- // removeAllProperties: true, // Remove all properties if true
21
- // removeProperties: [key, ...], // Specific properties to delete
22
- // addOrUpdateProperties: [ // Properties to add or update
23
- // { key: "name", value: "New name" }
24
- // ]
25
- // }
26
-
27
- /* eslint @stylistic/comma-spacing: 0, no-shadow: 0 */
28
-
29
- // applies a diff to the geojsonvt source simplified features array
30
- // returns an object with the affected features and new source array for invalidation
31
- export function applySourceDiff(source, dataDiff, options) {
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
- // helper to apply property updates from a diff update object to a properties object
146
- function applyPropertyUpdates(tags, update) {
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
- // Convert a GeoJSON Source Diff to an idempotent hashed representation using Sets and Maps
169
- export function diffToHashed(diff) {
170
- if (!diff) return {};
171
-
172
- const hashed = {};
173
-
174
- hashed.removeAll = diff.removeAll;
175
- hashed.remove = new Set(diff.remove || []);
176
- hashed.add = new Map(diff.add?.map(feature => [feature.id, feature]));
177
- hashed.update = new Map(diff.update?.map(update => [update.id, update]));
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
  }