@osmix/vt 0.0.2 → 0.0.7

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/encode.ts CHANGED
@@ -1,8 +1,20 @@
1
- import type { Osmix } from "@osmix/core"
2
- import { wayIsArea } from "@osmix/json"
1
+ /**
2
+ * Vector tile encoding for OSM data.
3
+ *
4
+ * The OsmixVtEncoder class converts Osmix datasets into Mapbox Vector Tiles,
5
+ * handling geometry projection, clipping, area detection, and proper
6
+ * MVT encoding for nodes, ways, and relations.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { Osm } from "@osmix/core"
12
+ import { bboxContainsOrIntersects } from "@osmix/shared/bbox-intersects"
13
+ import { normalizeHexColor } from "@osmix/shared/color"
3
14
  import { clipPolygon, clipPolyline } from "@osmix/shared/lineclip"
4
- import SphericalMercatorTile from "@osmix/shared/spherical-mercator"
15
+ import { llToTilePx, tileToBbox } from "@osmix/shared/tile"
5
16
  import type { GeoBbox2D, LonLat, Tile, XY } from "@osmix/shared/types"
17
+ import { wayIsArea } from "@osmix/shared/way-is-area"
6
18
  import type {
7
19
  VtSimpleFeature,
8
20
  VtSimpleFeatureGeometry,
@@ -10,7 +22,9 @@ import type {
10
22
  } from "./types"
11
23
  import writeVtPbf from "./write-vt-pbf"
12
24
 
25
+ /** Default tile extent (coordinate resolution). */
13
26
  const DEFAULT_EXTENT = 4096
27
+ /** Default buffer around tile in extent units. */
14
28
  const DEFAULT_BUFFER = 64
15
29
 
16
30
  const SF_TYPE: VtSimpleFeatureType = {
@@ -34,47 +48,79 @@ function dedupePoints(points: XY[]): XY[] {
34
48
  return result
35
49
  }
36
50
 
51
+ /**
52
+ * Returns a projection function that converts [lon, lat] to [x, y] pixel coordinates
53
+ * relative to the given tile. The extent determines the resolution of the tile
54
+ * (e.g. 4096 means coordinates range from 0 to 4096).
55
+ */
37
56
  export function projectToTile(
38
57
  tile: Tile,
39
58
  extent = DEFAULT_EXTENT,
40
59
  ): (ll: LonLat) => XY {
41
- const sm = new SphericalMercatorTile({ size: extent, tile })
42
- return (lonLat) => sm.llToTilePx(lonLat)
60
+ return (lonLat) => llToTilePx(lonLat, tile, extent)
43
61
  }
44
62
 
63
+ /**
64
+ * Encode an Osm instance into a Mapbox Vector Tile PBF.
65
+ */
45
66
  export class OsmixVtEncoder {
46
67
  readonly nodeLayerName: string
47
68
  readonly wayLayerName: string
48
- private readonly osmix: Osmix
69
+ readonly relationLayerName: string
70
+ private readonly osm: Osm
49
71
  private readonly extent: number
50
72
  private readonly extentBbox: [number, number, number, number]
51
73
 
52
- constructor(osmix: Osmix, extent = DEFAULT_EXTENT, buffer = DEFAULT_BUFFER) {
53
- this.osmix = osmix
74
+ static layerNames(id: string) {
75
+ return {
76
+ nodeLayerName: `@osmix:${id}:nodes`,
77
+ wayLayerName: `@osmix:${id}:ways`,
78
+ relationLayerName: `@osmix:${id}:relations`,
79
+ }
80
+ }
81
+
82
+ constructor(osm: Osm, extent = DEFAULT_EXTENT, buffer = DEFAULT_BUFFER) {
83
+ this.osm = osm
54
84
 
55
85
  const min = -buffer
56
86
  const max = extent + buffer
57
87
  this.extent = extent
58
88
  this.extentBbox = [min, min, max, max]
59
89
 
60
- const layerName = `@osmix:${osmix.id}`
90
+ const layerName = `@osmix:${osm.id}`
61
91
  this.nodeLayerName = `${layerName}:nodes`
62
92
  this.wayLayerName = `${layerName}:ways`
93
+ this.relationLayerName = `${layerName}:relations`
63
94
  }
64
95
 
96
+ /**
97
+ * Get a vector tile PBF for a specific tile coordinate.
98
+ * Returns an empty buffer if the tile does not intersect with the OSM dataset.
99
+ */
65
100
  getTile(tile: Tile): ArrayBuffer {
66
- const sm = new SphericalMercatorTile({ size: this.extent, tile })
67
- const bbox = sm.bbox(tile[0], tile[1], tile[2]) as GeoBbox2D
68
- return this.getTileForBbox(bbox, (ll) => sm.llToTilePx(ll))
101
+ const bbox = tileToBbox(tile)
102
+ const osmBbox = this.osm.bbox()
103
+ if (!bboxContainsOrIntersects(bbox, osmBbox)) {
104
+ return new ArrayBuffer(0)
105
+ }
106
+ return this.getTileForBbox(bbox, (ll) => llToTilePx(ll, tile, this.extent))
69
107
  }
70
108
 
109
+ /**
110
+ * Get a vector tile PBF for a specific geographic bounding box.
111
+ * @param bbox The bounding box to include features from.
112
+ * @param proj A function to project [lon, lat] to [x, y] within the tile extent.
113
+ */
71
114
  getTileForBbox(bbox: GeoBbox2D, proj: (ll: LonLat) => XY): ArrayBuffer {
72
- const layer = writeVtPbf([
115
+ // Get way IDs that are part of relations (to exclude from individual rendering)
116
+ const relationWayIds = this.osm.relations.getWayMemberIds()
117
+
118
+ const layers = [
73
119
  {
74
120
  name: this.wayLayerName,
75
121
  version: 2,
76
122
  extent: this.extent,
77
- features: this.wayFeatures(bbox, proj),
123
+ features: this.wayFeatures(bbox, proj, relationWayIds),
78
124
  },
79
125
  {
80
126
  name: this.nodeLayerName,
@@ -82,26 +128,32 @@ export class OsmixVtEncoder {
82
128
  extent: this.extent,
83
129
  features: this.nodeFeatures(bbox, proj),
84
130
  },
85
- ])
86
- return layer
131
+ {
132
+ name: this.relationLayerName,
133
+ version: 2,
134
+ extent: this.extent,
135
+ features: this.relationFeatures(bbox, proj),
136
+ },
137
+ ]
138
+ return writeVtPbf(layers)
87
139
  }
88
140
 
89
141
  *nodeFeatures(
90
142
  bbox: GeoBbox2D,
91
143
  proj: (ll: LonLat) => XY,
92
144
  ): Generator<VtSimpleFeature> {
93
- const nodeIndexes = this.osmix.nodes.withinBbox(bbox)
145
+ const nodeIndexes = this.osm.nodes.findIndexesWithinBbox(bbox)
94
146
  for (let i = 0; i < nodeIndexes.length; i++) {
95
147
  const nodeIndex = nodeIndexes[i]
96
148
  if (nodeIndex === undefined) continue
97
- const tags = this.osmix.nodes.tags.getTags(nodeIndex)
149
+ const tags = this.osm.nodes.tags.getTags(nodeIndex)
98
150
  if (!tags || Object.keys(tags).length === 0) continue
99
- const id = this.osmix.nodes.ids.at(nodeIndex)
100
- const ll = this.osmix.nodes.getNodeLonLat({ index: nodeIndex })
151
+ const id = this.osm.nodes.ids.at(nodeIndex)
152
+ const ll = this.osm.nodes.getNodeLonLat({ index: nodeIndex })
101
153
  yield {
102
154
  id,
103
155
  type: SF_TYPE.POINT,
104
- properties: { type: "node", ...tags },
156
+ properties: { ...tags, type: "node" },
105
157
  geometry: [[proj(ll)]],
106
158
  }
107
159
  }
@@ -110,36 +162,49 @@ export class OsmixVtEncoder {
110
162
  *wayFeatures(
111
163
  bbox: GeoBbox2D,
112
164
  proj: (ll: LonLat) => XY,
165
+ relationWayIds?: Set<number>,
113
166
  ): Generator<VtSimpleFeature> {
114
- const wayIndexes = this.osmix.ways.intersects(bbox)
167
+ const wayIndexes = this.osm.ways.intersects(bbox)
115
168
  for (let i = 0; i < wayIndexes.length; i++) {
116
169
  const wayIndex = wayIndexes[i]
117
170
  if (wayIndex === undefined) continue
118
- const id = this.osmix.ways.ids.at(wayIndex)
119
- const tags = this.osmix.ways.tags.getTags(wayIndex)
120
- const count = this.osmix.ways.refCount.at(wayIndex)
121
- const start = this.osmix.ways.refStart.at(wayIndex)
122
- const points: XY[] = new Array(count)
123
- for (let i = 0; i < count; i++) {
124
- const ref = this.osmix.ways.refs.at(start + i)
125
- const ll = this.osmix.nodes.getNodeLonLat({ id: ref })
126
- points[i] = proj(ll)
127
- }
128
- const isArea = wayIsArea({ id, refs: new Array(count).fill(0), tags })
171
+ const id = this.osm.ways.ids.at(wayIndex)
172
+ // Skip ways that are part of relations (they will be rendered via relations)
173
+ if (id !== undefined && relationWayIds?.has(id)) continue
174
+ const tags = this.osm.ways.tags.getTags(wayIndex)
175
+ // Skip ways without tags (they are likely only for relations)
176
+ if (!tags || Object.keys(tags).length === 0) continue
177
+ const normalizedColor = normalizeHexColor(tags["color"] ?? tags["colour"])
178
+ const wayLine = this.osm.ways.getCoordinates(wayIndex)
179
+ const points: XY[] = wayLine.map((ll) => proj(ll))
180
+
181
+ const isArea = wayIsArea({
182
+ id,
183
+ refs: this.osm.ways.getRefIds(wayIndex),
184
+ tags,
185
+ })
129
186
  const geometry: VtSimpleFeatureGeometry = []
130
187
  if (isArea) {
131
- // 1. clip polygon in tile coords
132
- const clippedPoly = this.clipProjectedPolygon(points)
133
- // clipProjectedPolygon currently returns XY[], not XY[][]
134
- // i.e. assumes single ring. We'll treat it as one ring.
188
+ // 1. clip polygon in tile coords (returns array of rings)
189
+ const clippedRings = this.clipProjectedPolygon(points)
135
190
 
136
- // 2. round/clamp, dedupe, close, orient
137
- const processedRing = this.processClippedPolygonRing(clippedPoly)
191
+ // 2. process each ring (first is outer, rest would be holes if from relations)
192
+ for (let ringIndex = 0; ringIndex < clippedRings.length; ringIndex++) {
193
+ const clippedRing = clippedRings[ringIndex]
194
+ if (!clippedRing) continue
138
195
 
139
- // TODO handle MultiPolygons with holes
196
+ // Normalize winding order using rewind before processing
197
+ // GeoJSON: outer counterclockwise, inner clockwise
198
+ // MVT: outer clockwise, inner counterclockwise
199
+ const isOuter = ringIndex === 0
200
+ const processedRing = this.processClippedPolygonRing(
201
+ clippedRing,
202
+ isOuter,
203
+ )
140
204
 
141
- if (processedRing.length > 0) {
142
- geometry.push(processedRing)
205
+ if (processedRing.length > 0) {
206
+ geometry.push(processedRing)
207
+ }
143
208
  }
144
209
  } else {
145
210
  const clippedSegmentsRaw = this.clipProjectedPolyline(points)
@@ -155,7 +220,11 @@ export class OsmixVtEncoder {
155
220
  yield {
156
221
  id,
157
222
  type: isArea ? SF_TYPE.POLYGON : SF_TYPE.LINE,
158
- properties: { type: "way", ...tags },
223
+ properties: {
224
+ ...tags,
225
+ ...(normalizedColor ? { color: normalizedColor } : {}),
226
+ type: "way",
227
+ },
159
228
  geometry,
160
229
  }
161
230
  }
@@ -165,11 +234,14 @@ export class OsmixVtEncoder {
165
234
  return clipPolyline(points, this.extentBbox)
166
235
  }
167
236
 
168
- clipProjectedPolygon(points: XY[]): XY[] {
169
- return clipPolygon(points, this.extentBbox)
237
+ clipProjectedPolygon(points: XY[]): XY[][] {
238
+ // clipPolygon returns a single ring, but we return as array for consistency
239
+ // with multi-ring support (e.g., from relations)
240
+ const clipped = clipPolygon(points, this.extentBbox)
241
+ return [clipped]
170
242
  }
171
243
 
172
- processClippedPolygonRing(rawRing: XY[]): XY[] {
244
+ processClippedPolygonRing(rawRing: XY[], isOuter: boolean): XY[] {
173
245
  // 1. round & clamp EVERY point
174
246
  const snapped = rawRing.map((xy) => this.clampAndRoundPoint(xy))
175
247
 
@@ -177,12 +249,130 @@ export class OsmixVtEncoder {
177
249
  const cleaned = cleanRing(snapped)
178
250
  if (cleaned.length === 0) return []
179
251
 
180
- // 3. enforce clockwise for outer ring
181
- const oriented = ensureClockwise(cleaned)
252
+ // 3. enforce winding order per MVT spec:
253
+ // - Outer rings: clockwise
254
+ // - Inner rings (holes): counterclockwise
255
+ const oriented = isOuter
256
+ ? ensureClockwise(cleaned)
257
+ : ensureCounterclockwise(cleaned)
182
258
 
183
259
  return oriented
184
260
  }
185
261
 
262
+ /**
263
+ * Super relations and logical relations are not directly rendered; they would need recursive expansion.
264
+ */
265
+ *relationFeatures(
266
+ bbox: GeoBbox2D,
267
+ proj: (ll: LonLat) => XY,
268
+ ): Generator<VtSimpleFeature> {
269
+ const relationIndexes = this.osm.relations.intersects(bbox)
270
+
271
+ for (const relIndex of relationIndexes) {
272
+ const relation = this.osm.relations.getByIndex(relIndex)
273
+ const relationGeometry = this.osm.relations.getRelationGeometry(relIndex)
274
+ if (
275
+ !relation ||
276
+ (!relationGeometry.lineStrings &&
277
+ !relationGeometry.rings &&
278
+ !relationGeometry.points)
279
+ )
280
+ continue
281
+
282
+ const id = this.osm.relations.ids.at(relIndex)
283
+ const tags = this.osm.relations.tags.getTags(relIndex)
284
+
285
+ if (relationGeometry.rings) {
286
+ // Area relations (multipolygon, boundary)
287
+ const { rings } = relationGeometry
288
+ if (rings.length === 0) continue
289
+
290
+ // Process each polygon in the relation
291
+ for (const polygon of rings) {
292
+ const geometry: VtSimpleFeatureGeometry = []
293
+
294
+ // Process outer ring and inner rings (holes)
295
+ for (let ringIndex = 0; ringIndex < polygon.length; ringIndex++) {
296
+ const ring = polygon[ringIndex]
297
+ if (!ring || ring.length < 3) continue
298
+
299
+ // Project ring to tile coordinates
300
+ const projectedRing: XY[] = ring.map((ll: LonLat) => proj(ll))
301
+
302
+ // Clip polygon ring
303
+ const clipped = clipPolygon(projectedRing, this.extentBbox)
304
+ if (clipped.length < 3) continue
305
+
306
+ // Process ring (round/clamp, dedupe, close, orient)
307
+ const isOuter = ringIndex === 0
308
+ const processedRing = this.processClippedPolygonRing(
309
+ clipped,
310
+ isOuter,
311
+ )
312
+
313
+ if (processedRing.length > 0) {
314
+ geometry.push(processedRing)
315
+ }
316
+ }
317
+
318
+ if (geometry.length === 0) continue
319
+
320
+ yield {
321
+ id: id ?? 0,
322
+ type: SF_TYPE.POLYGON,
323
+ properties: { ...tags, type: "relation" },
324
+ geometry,
325
+ }
326
+ }
327
+ } else if (relationGeometry.lineStrings) {
328
+ // Line relations (route, multilinestring)
329
+ const { lineStrings } = relationGeometry
330
+ if (lineStrings.length === 0) continue
331
+
332
+ for (const lineString of lineStrings) {
333
+ const geometry: VtSimpleFeatureGeometry = []
334
+ const points: XY[] = lineString.map((ll) => proj(ll))
335
+ const clippedSegmentsRaw = this.clipProjectedPolyline(points)
336
+ for (const segment of clippedSegmentsRaw) {
337
+ const rounded = segment.map((xy) => this.clampAndRoundPoint(xy))
338
+ const deduped = dedupePoints(rounded)
339
+ if (deduped.length >= 2) {
340
+ geometry.push(deduped)
341
+ }
342
+ }
343
+ if (geometry.length === 0) continue
344
+
345
+ yield {
346
+ id: id ?? 0,
347
+ type: SF_TYPE.LINE,
348
+ properties: { ...tags, type: "relation" },
349
+ geometry,
350
+ }
351
+ }
352
+ } else if (relationGeometry.points) {
353
+ // Point relations (multipoint)
354
+ const { points } = relationGeometry
355
+ if (points.length === 0) continue
356
+
357
+ const geometry: VtSimpleFeatureGeometry = []
358
+ for (const point of points) {
359
+ const projected = proj(point)
360
+ const clamped = this.clampAndRoundPoint(projected)
361
+ geometry.push([clamped])
362
+ }
363
+
364
+ if (geometry.length === 0) continue
365
+
366
+ yield {
367
+ id: id ?? 0,
368
+ type: SF_TYPE.POINT,
369
+ properties: { ...tags, type: "relation" },
370
+ geometry,
371
+ }
372
+ }
373
+ }
374
+ }
375
+
186
376
  clampAndRoundPoint(xy: XY): XY {
187
377
  const clampedX = Math.round(
188
378
  clamp(xy[0], this.extentBbox[0], this.extentBbox[2]),
@@ -194,6 +384,10 @@ export class OsmixVtEncoder {
194
384
  }
195
385
  }
196
386
 
387
+ /**
388
+ * Ensures the ring is closed (first and last points are identical).
389
+ * If not, appends the first point to the end.
390
+ */
197
391
  function closeRing(ring: XY[]): XY[] {
198
392
  const first = ring[0]
199
393
  const last = ring[ring.length - 1]
@@ -204,8 +398,10 @@ function closeRing(ring: XY[]): XY[] {
204
398
  return ring
205
399
  }
206
400
 
207
- // Signed area via shoelace formula.
208
- // Positive area => CCW, Negative => CW.
401
+ /**
402
+ * Signed area via shoelace formula.
403
+ * Positive area => CCW, Negative => CW.
404
+ */
209
405
  function ringArea(ring: XY[]): number {
210
406
  let sum = 0
211
407
  for (let i = 0; i < ring.length - 1; i++) {
@@ -220,15 +416,14 @@ function ensureClockwise(ring: XY[]): XY[] {
220
416
  return ringArea(ring) < 0 ? ring : [...ring].reverse()
221
417
  }
222
418
 
223
- /**
224
- * TODO handle MultiPolygons with holes
225
- *
226
- function _ensureCounterClockwise(ring: XY[]): XY[] {
419
+ function ensureCounterclockwise(ring: XY[]): XY[] {
227
420
  return ringArea(ring) > 0 ? ring : [...ring].reverse()
228
421
  }
229
- */
230
422
 
231
- // Remove consecutive duplicates *after* rounding
423
+ /**
424
+ * Clean a polygon ring by removing consecutive duplicates, ensuring it's closed,
425
+ * and checking that it has at least 4 coordinates (3 unique points).
426
+ */
232
427
  function cleanRing(ring: XY[]): XY[] {
233
428
  const deduped = dedupePoints(ring)
234
429
  // After dedupe, we still must ensure closure, and a polygon
package/src/index.ts CHANGED
@@ -1 +1,33 @@
1
- export { OsmixVtEncoder } from "./encode"
1
+ /**
2
+ * @osmix/vt - Mapbox Vector Tile encoding for OSM data.
3
+ *
4
+ * Converts `@osmix/core` OSM datasets into Mapbox Vector Tiles (MVT) format.
5
+ * Generates PBF-encoded tiles with separate layers for nodes, ways, and relations.
6
+ *
7
+ * Key features:
8
+ * - **Per-entity layers**: Separate layers for nodes, ways, and relations.
9
+ * - **Geometry conversion**: Ways become lines or polygons based on area heuristics.
10
+ * - **Multipolygon support**: Renders multipolygon relations as proper polygons with holes.
11
+ * - **Clipping**: Geometry is clipped to tile bounds with configurable buffer.
12
+ * - **Winding order**: Automatically enforces MVT spec (CW outer, CCW inner rings).
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { OsmixVtEncoder } from "@osmix/vt"
17
+ *
18
+ * const encoder = new OsmixVtEncoder(osm)
19
+ * const pbfBuffer = encoder.getTile([9372, 12535, 15])
20
+ *
21
+ * // Use with MapLibre or other vector tile renderers
22
+ * map.addSource("osmix", {
23
+ * type: "vector",
24
+ * tiles: [/* ... generate tile URLs ... *\/]
25
+ * })
26
+ * ```
27
+ *
28
+ * @module @osmix/vt
29
+ */
30
+
31
+ export { OsmixVtEncoder, projectToTile } from "./encode"
32
+ export * from "./types"
33
+ export { default as writeVtPbf } from "./write-vt-pbf"
package/src/types.ts CHANGED
@@ -1,20 +1,43 @@
1
- import type { OsmEntityType, OsmTags } from "@osmix/json"
2
- import type { XY } from "@osmix/shared/types"
1
+ /**
2
+ * Type definitions for vector tile encoding.
3
+ *
4
+ * These types represent the intermediate format used when converting
5
+ * OSM entities to Mapbox Vector Tile features.
6
+ *
7
+ * @module
8
+ */
3
9
 
10
+ import type { OsmTags, XY } from "@osmix/shared/types"
11
+
12
+ /**
13
+ * Geometry for a vector tile feature.
14
+ * Array of rings/segments, where each ring/segment is an array of [x, y] coordinates.
15
+ */
4
16
  export type VtSimpleFeatureGeometry = XY[][]
5
17
 
18
+ /**
19
+ * Properties attached to a vector tile feature.
20
+ * Includes OSM tags plus optional source metadata.
21
+ */
6
22
  export type VtSimpleFeatureProperties = {
7
23
  sourceId?: string
8
- type: OsmEntityType
9
24
  tileKey?: string
10
25
  } & OsmTags
11
26
 
27
+ /**
28
+ * MVT geometry type constants.
29
+ * Point = 1, Line = 2, Polygon = 3.
30
+ */
12
31
  export type VtSimpleFeatureType = {
13
32
  POINT: 1
14
33
  LINE: 2
15
34
  POLYGON: 3
16
35
  }
17
36
 
37
+ /**
38
+ * A simplified vector tile feature ready for encoding.
39
+ * Geometry coordinates are in tile extent units (typically 0-4096).
40
+ */
18
41
  export interface VtSimpleFeature {
19
42
  id: number
20
43
  type: VtSimpleFeatureType[keyof VtSimpleFeatureType]
@@ -22,6 +45,10 @@ export interface VtSimpleFeature {
22
45
  geometry: VtSimpleFeatureGeometry
23
46
  }
24
47
 
48
+ /**
49
+ * A layer to be written to the vector tile PBF.
50
+ * Features are provided via a generator to support lazy evaluation.
51
+ */
25
52
  export type VtPbfLayer = {
26
53
  name: string
27
54
  version: number
@@ -1,6 +1,20 @@
1
+ /**
2
+ * Vector tile PBF writer.
3
+ *
4
+ * Encodes vector tile layers and features into the Mapbox Vector Tile
5
+ * binary format (PBF/protobuf). Handles key/value deduplication,
6
+ * geometry encoding with delta compression, and proper command sequences.
7
+ *
8
+ * @see https://github.com/mapbox/vector-tile-spec
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import { zigzag, zigzag32 } from "@osmix/shared/zigzag"
1
14
  import Pbf from "pbf"
2
15
  import type { VtPbfLayer, VtSimpleFeature } from "./types"
3
16
 
17
+ /** Internal context for encoding a layer's features. */
4
18
  type VtLayerContext = {
5
19
  feature: VtSimpleFeature
6
20
  keys: string[]
@@ -10,7 +24,10 @@ type VtLayerContext = {
10
24
  }
11
25
 
12
26
  /**
13
- * Write Vector Tile Layers to a PBF buffer
27
+ * Write vector tile layers to a PBF buffer.
28
+ *
29
+ * @param layers - Array of layers to encode.
30
+ * @returns ArrayBuffer containing the encoded vector tile.
14
31
  */
15
32
  export default function writeVtPbf(layers: VtPbfLayer[]) {
16
33
  const pbf = new Pbf()
@@ -53,7 +70,12 @@ function writeLayer(layer: VtPbfLayer, pbf: Pbf) {
53
70
 
54
71
  function writeFeature(ctx: VtLayerContext, pbf: Pbf) {
55
72
  if (ctx.feature.id !== undefined) {
56
- pbf.writeVarintField(1, ctx.feature.id)
73
+ const id = ctx.feature.id
74
+
75
+ // Use zigzag encoding for IDs to convert negative IDs to positive numbers
76
+ // that can be properly decoded. Uses arithmetic-based encoding to support
77
+ // the full safe integer range.
78
+ pbf.writeVarintField(1, zigzag(id))
57
79
  }
58
80
 
59
81
  pbf.writeMessage(2, writeProperties, ctx)
@@ -93,10 +115,6 @@ function command(cmd: number, length: number) {
93
115
  return (length << 3) + (cmd & 0x7)
94
116
  }
95
117
 
96
- function zigzag(num: number) {
97
- return (num << 1) ^ (num >> 31)
98
- }
99
-
100
118
  function writeGeometry(feature: VtSimpleFeature, pbf: Pbf) {
101
119
  const type = feature.type
102
120
  let x = 0
@@ -116,8 +134,9 @@ function writeGeometry(feature: VtSimpleFeature, pbf: Pbf) {
116
134
  }
117
135
  const dx = xy[0] - x
118
136
  const dy = xy[1] - y
119
- pbf.writeVarint(zigzag(dx))
120
- pbf.writeVarint(zigzag(dy))
137
+ // Use bitwise zigzag for geometry deltas (small values, 32-bit is sufficient)
138
+ pbf.writeVarint(zigzag32(dx))
139
+ pbf.writeVarint(zigzag32(dy))
121
140
  x += dx
122
141
  y += dy
123
142
  })
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "./tsconfig.json",
4
+ "include": ["src"]
5
+ }