• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Open-S2 / open-vector-tile / #6

26 Jun 2024 06:18PM UTC coverage: 100.0%. Remained the same
#6

push

Mr Martian
include dependecy graph

2362 of 2362 relevant lines covered (100.0%)

106.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/mapbox/vectorFeature.ts
1
import type { Pbf as Protobuf } from '../pbf';
2
import type {
3
  BBox,
4
  BBox3D,
5
  OldVectorFeatureType,
6
  Point,
7
  Properties,
8
  Value,
9
  VectorGeometry,
10
  VectorLine,
11
  VectorLinesWithOffset,
12
} from '../vectorTile.spec';
13

14
/**
15
 * Mapbox Vector Feature types are all bundled in one class
16
 * to make it easier to read. Primarily contains an id, properties, and geometry.
17
 * The now deprecated S2 model extends this class to include indices and tesselation data.
18
 */
19
export default class MapboxVectorFeature {
20
  id?: number;
21
  version = 5;
22
  properties: Properties = {};
23
  extent: number;
24
  type: OldVectorFeatureType = 1;
25
  isS2: boolean;
26
  #pbf: Protobuf;
27
  #indices = -1;
28
  #geometry = -1;
29
  #tesselation = -1;
30
  #keys: string[];
31
  #values: Value[];
2✔
32
  /**
33
   * @param pbf - the pbf protocol we are reading from
34
   * @param end - the position to stop at
35
   * @param isS2 - whether the layer is a deprecated S2 layer or Mapbox layer.
36
   * @param extent - the extent of the vector tile
37
   * @param version - the version of the vector tile. S2 is 5, Mapbox is 1
38
   * @param keys - the keys in the vector layer to pull from
39
   * @param values - the values in the vector layer to pull from
40
   */
41
  constructor(
26✔
42
    pbf: Protobuf,
20✔
43
    end: number,
20✔
44
    isS2: boolean,
24✔
45
    extent: number,
32✔
46
    version: number,
36✔
47
    keys: string[],
24✔
48
    values: Value[],
32✔
49
  ) {
8✔
50
    this.isS2 = isS2;
84✔
51
    this.extent = extent;
100✔
52
    this.version = version;
108✔
53
    this.#pbf = pbf;
80✔
54
    this.#keys = keys;
88✔
55
    this.#values = values;
104✔
56

57
    pbf.readFields(this.#readFeature, this, end);
208✔
58
  }
59

60
  /**
61
   * @param tag - the tag to know what kind of data to read
62
   * @param feature - the feature to mutate with the new data
63
   * @param pbf - the Protobuf object to read from
64
   */
65
  #readFeature(tag: number, feature: MapboxVectorFeature, pbf: Protobuf): void {
112✔
66
    // old spec
67
    if (feature.isS2) {
90✔
68
      if (tag === 15) feature.id = pbf.readVarint();
288✔
69
      else if (tag === 1) feature.#readTag(pbf, feature);
252✔
70
      else if (tag === 2) feature.type = pbf.readVarint() as OldVectorFeatureType;
256✔
71
      else if (tag === 3) feature.#geometry = pbf.pos;
210✔
72
      else if (tag === 4) feature.#indices = pbf.pos;
120✔
73
      else if (tag === 5) feature.#tesselation = pbf.pos;
114✔
74
    } else {
34✔
75
      if (tag === 1) feature.id = pbf.readVarint();
284✔
76
      else if (tag === 2) feature.#readTag(pbf, feature);
252✔
77
      else if (tag === 3) feature.type = pbf.readVarint() as OldVectorFeatureType;
256✔
78
      else if (tag === 4) feature.#geometry = pbf.pos;
240✔
79
      else if (tag === 5) feature.#indices = pbf.pos;
236✔
80
      else if (tag === 6) feature.#tesselation = pbf.pos;
226✔
81
    }
82
  }
83

84
  /**
85
   * @param pbf - the Protobuf object
86
   * @param feature - the feature to mutate relative to the tag.
87
   */
88
  #readTag(pbf: Protobuf, feature: MapboxVectorFeature): void {
84✔
89
    const end = pbf.readVarint() + pbf.pos;
172✔
90

91
    while (pbf.pos < end) {
106✔
92
      const key = feature.#keys[pbf.readVarint()];
200✔
93
      const value = feature.#values[pbf.readVarint()];
216✔
94

95
      feature.properties[key] = value;
164✔
96
    }
26✔
97
  }
98

99
  /**
100
   * @returns - MapboxVectorTile's do not support m-values so we return false
101
   */
102
  get hasMValues(): boolean {
40✔
103
    return false;
74✔
104
  }
105

106
  /**
107
   * @returns - a default bbox. Since no bbox is present, the default is [0, 0, 0, 0]
108
   * also MapboxVectorTile's do not support 3D, so we only return a 2D bbox
109
   */
110
  bbox(): BBox | BBox3D {
28✔
111
    return [0, 0, 0, 0] as BBox;
102✔
112
  }
113

114
  /**
115
   * @returns - regardless of the type, we return a flattend point array
116
   */
117
  loadPoints(): Point[] {
40✔
118
    let res: Point[] = [];
68✔
119
    const geometry = this.loadGeometry();
164✔
120
    if (this.type === 1) res = geometry as Point[];
224✔
121
    else if (this.type === 2) res = (geometry as Point[][]).flatMap((p) => p);
264✔
122
    else if (this.type === 3 || this.type === 4)
144✔
123
      res = (geometry as Point[][][]).flatMap((p) => {
144✔
124
        return p.flatMap((p) => p);
144✔
125
      });
10✔
126

127
    return res;
66✔
128
  }
129

130
  /**
131
   * @returns - an array of lines. The offsets will be set to 0
132
   */
133
  loadLines(): VectorLinesWithOffset {
38✔
134
    const geometry = this.loadGeometry();
164✔
135
    let res: VectorLinesWithOffset = [];
68✔
136

137
    if (this.type === 2) {
102✔
138
      res = (geometry as VectorLine[]).map((line) => ({ geometry: line, offset: 0 }));
280✔
139
    } else if (this.type === 3 || this.type === 4) {
192✔
140
      res = (geometry as VectorLine[][]).flatMap((poly) => {
158✔
141
        return poly.map((line) => ({ geometry: line, offset: 0 }));
272✔
142
      });
24✔
143
    }
8✔
144

145
    return res;
66✔
146
  }
147

148
  /**
149
   * @returns - [flattened geometry & tesslation if applicable, indices]
150
   */
151
  loadGeometryFlat(): [geometry: number[] | VectorGeometry, indices: number[]] {
52✔
152
    if (!this.isS2) return [this.loadGeometry(), [] as number[]];
226✔
153
    this.#pbf.pos = this.#geometry;
70✔
154
    const { extent } = this;
56✔
155
    const multiplier = 1 / extent;
68✔
156

157
    const geometry = [];
48✔
158
    const end = this.#pbf.readVarint() + this.#pbf.pos;
110✔
159
    let cmd = 1;
32✔
160
    let length = 0;
38✔
161
    let x = 0;
28✔
162
    let y = 0;
28✔
163
    let startX: number | null = null;
44✔
164
    let startY: number | null = null;
44✔
165

166
    while (this.#pbf.pos < end) {
66✔
167
      if (length <= 0) {
48✔
168
        const cmdLen = this.#pbf.readVarint();
92✔
169
        cmd = cmdLen & 0x7;
50✔
170
        length = cmdLen >> 3;
68✔
171
      }
4✔
172

173
      length--;
30✔
174

175
      if (cmd === 1 || cmd === 2) {
70✔
176
        x += this.#pbf.readSVarint();
74✔
177
        y += this.#pbf.readSVarint();
74✔
178
        if (startX === null) startX = x * multiplier;
124✔
179
        if (startY === null) startY = y * multiplier;
124✔
180
        geometry.push(x * multiplier, y * multiplier);
118✔
181
      } else if (cmd === 7) {
48✔
182
        // ClosePath
183
        geometry.push(startX ?? 0, startY ?? 0);
96✔
184
        startX = null;
44✔
185
        startY = null;
54✔
186
      }
10✔
187
    }
4✔
188

189
    // if a poly, check if we should load indices
190
    const indices = this.readIndices();
78✔
191
    // if a poly, check if we should load tesselation
192
    if (this.#tesselation > 0) this.addTesselation(geometry, multiplier);
156✔
193

194
    return [geometry, indices];
72✔
195
  }
196

197
  /**
198
   * @returns - vector geometry relative to feature type.
199
   */
200
  loadGeometry(): VectorGeometry {
44✔
201
    this.#pbf.pos = this.#geometry;
140✔
202

203
    const points: Point[] = [];
88✔
204
    let lines: Point[][] = [];
76✔
205
    let polys: Point[][][] = [];
76✔
206
    const end = this.#pbf.readVarint() + this.#pbf.pos;
220✔
207
    let cmd = 1;
64✔
208
    let length = 0;
76✔
209
    let x = 0;
56✔
210
    let y = 0;
56✔
211
    let input: Point[] = [];
76✔
212

213
    while (this.#pbf.pos < end) {
130✔
214
      if (length <= 0) {
94✔
215
        const cmdLen = this.#pbf.readVarint();
184✔
216
        cmd = cmdLen & 7;
100✔
217
        length = cmdLen >> 3;
136✔
218
      }
6✔
219

220
      length--;
60✔
221

222
      if (cmd === 1 || cmd === 2) {
138✔
223
        x += this.#pbf.readSVarint();
148✔
224
        y += this.#pbf.readSVarint();
148✔
225

226
        if (cmd === 1) {
94✔
227
          // moveTo
228
          if (input.length > 0) {
130✔
229
            if (this.type === 1) points.push(...input);
290✔
230
            else lines.push(input);
162✔
231
          }
6✔
232
          input = [];
112✔
233
        }
6✔
234
        input.push({ x, y });
136✔
235
      } else if (cmd === 7) {
92✔
236
        // ClosePath
237
        if (input.length > 0) {
122✔
238
          input.push({ x: input[0].x, y: input[0].y });
220✔
239
          lines.push(input);
112✔
240
          input = [];
112✔
241
        }
26✔
242
      } else if (cmd === 4) {
92✔
243
        // ClosePolygon
244
        if (input.length > 0) lines.push(input);
190✔
245
        polys.push(lines);
104✔
246
        lines = [];
76✔
247
        input = [];
96✔
248
      } else {
18✔
249
        throw new Error('unknown command ' + String(cmd));
146✔
250
      }
251
    }
6✔
252

253
    if (input.length > 0) {
106✔
254
      if (this.type === 1) points.push(...input);
260✔
255
      else lines.push(input);
114✔
256
    }
6✔
257

258
    // if type is polygon but we are using version 1, we might have a multipolygon
259
    if (this.type === 3 && !this.isS2) {
158✔
260
      polys = classifyRings(lines);
152✔
261
    }
6✔
262

263
    if (this.type === 1) return points;
220✔
264
    else if (polys.length > 0) return polys;
144✔
265
    return lines;
74✔
266
  }
267

268
  /**
269
   * @returns - an array of indices for the geometry
270
   */
271
  readIndices(): number[] {
42✔
272
    if (this.#indices <= 0) return [];
168✔
273
    this.#pbf.pos = this.#indices;
136✔
274

275
    let curr = 0;
68✔
276
    const end = this.#pbf.readVarint() + this.#pbf.pos;
220✔
277
    // build indices
278
    const indices: number[] = [];
92✔
279
    while (this.#pbf.pos < end) {
130✔
280
      curr += this.#pbf.readSVarint();
152✔
281
      indices.push(curr);
112✔
282
    }
6✔
283

284
    return indices;
82✔
285
  }
286

287
  /**
288
   * Add tesselation data to the geometry
289
   * @param geometry - the geometry to add the tesselation data to
290
   * @param multiplier - the multiplier to apply the extent shift
291
   */
292
  addTesselation(geometry: number[], multiplier: number): void {
128✔
293
    if (this.#tesselation <= 0) return;
172✔
294
    this.#pbf.pos = this.#tesselation;
152✔
295
    const end = this.#pbf.readVarint() + this.#pbf.pos;
220✔
296
    let x = 0;
56✔
297
    let y = 0;
56✔
298
    while (this.#pbf.pos < end) {
130✔
299
      x += this.#pbf.readSVarint();
140✔
300
      y += this.#pbf.readSVarint();
140✔
301
      geometry.push(x * multiplier, y * multiplier);
220✔
302
    }
16✔
303
  }
304
}
4✔
305

306
/**
307
 * @param rings - input flattened rings that need to be classified
308
 * @returns - parsed polygons
309
 */
310
function classifyRings(rings: Point[][]): Point[][][] {
130✔
311
  if (rings.length <= 1) return [rings];
168✔
312

313
  const polygons: Point[][][] = [];
88✔
314
  let polygon: Point[][] | undefined;
56✔
315
  let ccw: boolean | undefined;
40✔
316

317
  for (let i = 0, rl = rings.length; i < rl; i++) {
198✔
318
    const area = signedArea(rings[i]);
152✔
319
    if (area === 0) continue;
132✔
320

321
    if (ccw === undefined) ccw = area < 0;
184✔
322

323
    if (ccw === area < 0) {
106✔
324
      if (polygon !== undefined) polygons.push(polygon);
248✔
325
      polygon = [rings[i]];
120✔
326
    } else {
34✔
327
      if (polygon === undefined) polygon = [];
184✔
328
      polygon.push(rings[i]);
138✔
329
    }
330
  }
6✔
331
  if (polygon !== undefined) polygons.push(polygon);
216✔
332

333
  return polygons;
76✔
334
}
335

336
/**
337
 * @param ring - linestring of points to check if it is ccw
338
 * @returns - true if the linestring is ccw
339
 */
340
function signedArea(ring: Point[]): number {
114✔
341
  let sum = 0;
56✔
342
  for (let i = 0, rl = ring.length, j = rl - 1, p1, p2; i < rl; j = i++) {
290✔
343
    p1 = ring[i];
68✔
344
    p2 = ring[j];
68✔
345
    sum += (p2.x - p1.x) * (p1.y + p2.y);
168✔
346
  }
6✔
347
  return sum;
54✔
348
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc