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

Beakerboy / OSMBuilding / 14762393001

30 Apr 2025 07:09PM UTC coverage: 60.042% (+0.8%) from 59.291%
14762393001

push

github

web-flow
Update BuildingShapeUtils.js

120 of 162 branches covered (74.07%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

1019 of 1735 relevant lines covered (58.73%)

4.94 hits per line

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

99.33
/src/extras/BuildingShapeUtils.js
1
import {
7✔
2
  Shape,
7✔
3
  ShapeUtils,
7✔
4
} from 'three';
7✔
5

7✔
6
class BuildingShapeUtils extends ShapeUtils {
7✔
7

7✔
8
  /**
7✔
9
   * Create the shape of this way.
7✔
10
   *
7✔
11
   * @param {DOM.Element} way - OSM XML way element.
7✔
12
   * @param {[number, number]} nodelist - list of all nodes
7✔
13
   *
7✔
14
   * @return {THREE.Shape} shape - the shape
7✔
15
   */
7✔
16
  static createShape(way, nodelist) {
7✔
17
    // Initialize objects
7✔
18
    const shape = new Shape();
7✔
19
    var ref;
7✔
20
    var node = [];
7✔
21

7✔
22
    // Get all the nodes in the way of interest
7✔
23
    const elements = way.getElementsByTagName('nd');
7✔
24

7✔
25
    // Get the coordinates of all the nodes and add them to the shape outline.
7✔
26
    for (let i = 0; i < elements.length; i++) {
7✔
27
      ref = elements[i].getAttribute('ref');
33✔
28
      node = nodelist[ref];
33✔
29
      // The first node requires a differnet function call.
33✔
30
      if (i === 0) {
33✔
31
        shape.moveTo(parseFloat(node[0]), parseFloat(node[1]));
7✔
32
      } else {
26✔
33
        shape.lineTo(parseFloat(node[0]), parseFloat(node[1]));
26✔
34
      }
7✔
35
    }
7✔
36
    return shape;
7✔
37
  }
7✔
38

7✔
39
  /**
7✔
40
   * Check if a way is a closed shape.
7✔
41
   *
7✔
42
   * @param {DOM.Element} way - OSM XML way element.
7✔
43
   *
7✔
44
   * @return {boolean}
7✔
45
   */
7✔
46
  static isClosed(way) {
7✔
47
    // Get all the nodes in the way of interest
77✔
48
    const elements = way.getElementsByTagName('nd');
77✔
49
    return elements[0].getAttribute('ref') === elements[elements.length - 1].getAttribute('ref');
7✔
50
  }
7✔
51

7✔
52
  /**
7✔
53
   * Check if a way is self-intersecting.
7✔
54
   *
7✔
55
   * @param {DOM.Element} way - OSM XML way element.
7✔
56
   *
7✔
57
   * @return {boolean}
7✔
58
   */
7✔
59
  static isSelfIntersecting(way) {
7✔
60
    const nodes = Array.from(way.getElementsByTagName('nd'));
67✔
61
    if (BuildingShapeUtils.isClosed(way)){
67✔
62
      nodes.pop();
25✔
63
    }
67✔
64
    const refs = new Set();
67✔
65
    for (const node of nodes) {
67✔
66
      const ref = node.getAttribute('ref');
195✔
67
      if (refs.has(ref)){
195✔
68
        return true;
5✔
69
      }
195✔
70
      refs.add(ref);
190✔
71
    }
62✔
72
    return false;
7✔
73
  }
7✔
74

7✔
75
  /**
7✔
76
   * Walk through an array and seperate any closed ways.
7✔
77
   * Attempt to find matching open ways to enclose them.
7✔
78
   *
7✔
79
   * @param {[DOM.Element]} ways - array of OSM XML way elements.
7✔
80
   *
7✔
81
   * @return {[DOM.Element]} array of closed ways.
7✔
82
   */
7✔
83
  static combineWays(ways) {
7✔
84
    const validWays = [];
17✔
85

17✔
86
    // Check if the provided array contains any self-intersecting ways.
17✔
87
    // Remove them and notify the user.
17✔
88
    for (const way of ways) {
17✔
89
      if (BuildingShapeUtils.isSelfIntersecting(way)) {
42✔
90
        const id = way.getAttribute('id');
2✔
91
        const msg = 'Way ' + id + ' is self-intersecting';
2✔
92
        window.printError(msg);
2✔
93
      } else {
42✔
94
        validWays.push(way);
40✔
95
      }
42✔
96
    }
17✔
97

17✔
98
    const closedWays = [];
17✔
99
    const wayBegins = {};
17✔
100
    const wayEnds = {};
17✔
101

17✔
102
    // Create lists of the first and last nodes in each way.
17✔
103
    validWays.forEach(w => {
17✔
104
      const firstNodeID = w.querySelector('nd').getAttribute('ref');
40✔
105
      if (wayBegins[firstNodeID]) {
40✔
106
        wayBegins[firstNodeID].push(w);
6✔
107
      } else {
39✔
108
        wayBegins[firstNodeID] = [w];
34✔
109
      }
40✔
110

40✔
111
      const lastNodeID = w.querySelector('nd:last-of-type').getAttribute('ref');
40✔
112
      if (wayEnds[lastNodeID]) {
40✔
113
        wayEnds[lastNodeID].push(w);
7✔
114
      } else {
33✔
115
        wayEnds[lastNodeID] = [w];
33✔
116
      }
17✔
117
    });
17✔
118

17✔
119
    const usedWays = new Set();
17✔
120

17✔
121
    /**
17✔
122
     * Use recursion to attempt to build a ring from ways.
17✔
123
     *
17✔
124
     * @param {[DOM.Element]} currentRingWays - array of OSM XML way elements.
17✔
125
     */
17✔
126
    function tryMakeRing(currentRingWays) {
44✔
127

44✔
128
      // Check if the array contains ways which will together form a ring. Return the array if it does.
44✔
129
      if (currentRingWays[0].querySelector('nd').getAttribute('ref') ===
44✔
130
          currentRingWays[currentRingWays.length - 1].querySelector('nd:last-of-type').getAttribute('ref')) {
44✔
131
        if (BuildingShapeUtils.isSelfIntersecting(BuildingShapeUtils.joinAllWays(currentRingWays))) {
13✔
132
          return [];
13✔
133
        }
12✔
134
        return currentRingWays;
44✔
135
      }
31✔
136

31✔
137
      const lastWay = currentRingWays[currentRingWays.length - 1];
31✔
138
      const lastNodeID = lastWay.querySelector('nd:last-of-type').getAttribute('ref');
31✔
139

31✔
140
      // Check if any of the unused ways can complete a ring as the are.
31✔
141
      for (let way of wayBegins[lastNodeID] ?? []) {
44✔
142
        const wayID = way.getAttribute('id');
20✔
143
        if (usedWays.has(wayID)) {
20✔
144
          continue;
20✔
145
        }
15✔
146
        usedWays.add(wayID);
15✔
147
        currentRingWays.push(way);
15✔
148
        if (tryMakeRing(currentRingWays).length) {
20✔
149
          return currentRingWays;
20✔
150
        }
2✔
151
        currentRingWays.pop();
2✔
152
        usedWays.delete(wayID);
42✔
153
      }
18✔
154

18✔
155
      // Check if any of the unused ways can complete a ring if reversed.
18✔
156
      for (let way of wayEnds[lastNodeID] ?? []) {
44✔
157
        const wayID = way.getAttribute('id');
24✔
158
        if (usedWays.has(wayID)) {
24✔
159
          continue;
23✔
160
        }
6✔
161
        usedWays.add(wayID);
6✔
162
        currentRingWays.push(BuildingShapeUtils.reverseWay(way));
6✔
163
        if (tryMakeRing(currentRingWays).length) {
23✔
164
          return currentRingWays;
24✔
165
        }
2✔
166
        currentRingWays.pop();
2✔
167
        usedWays.delete(wayID);
40✔
168
      }
14✔
169

14✔
170
      return [];
17✔
171
    }
17✔
172

17✔
173
    validWays.forEach(w => {
40✔
174
      const wayID = w.getAttribute('id');
40✔
175
      if (usedWays.has(wayID)){
40✔
176
        return;
38✔
177
      }
23✔
178
      usedWays.add(wayID);
23✔
179
      const result = tryMakeRing([w]);
23✔
180
      if (result.length) {
38✔
181
        const ring = this.joinAllWays(result);
12✔
182
        closedWays.push(ring);
17✔
183
      }
17✔
184
    });
17✔
185

17✔
186
    // Notify the user if there are unused ways.
17✔
187
    // if (validWays.length !== usedWays.length) {
17✔
188
    //   window.printError('Unused ways in relation')
17✔
189
    // }
17✔
190
    return closedWays;
7✔
191
  }
7✔
192

7✔
193
  /**
7✔
194
   * Append the nodes from one way into another.
7✔
195
   *
7✔
196
   * @param {DOM.Element} way1 - an open, non self-intersecring way
7✔
197
   * @param {DOM.Element} way2
7✔
198
   *
7✔
199
   * @return {DOM.Element} way
7✔
200
   */
36✔
201
  static joinWays(way1, way2) {
36✔
202
    const elements = way2.getElementsByTagName('nd');
36✔
203
    const newWay = way1.cloneNode(true);
36✔
204
    for (let i = 1; i < elements.length; i++) {
36✔
205
      let elem = elements[i].cloneNode();
48✔
206
      newWay.appendChild(elem);
36✔
207
    }
36✔
208
    return newWay;
7✔
209
  }
7✔
210

7✔
211
  /**
7✔
212
   * Append the nodes from one way into another.
7✔
213
   *
7✔
214
   * @param {DOM.Element} way1 - an open, non self-intersecring way
7✔
215
   * @param {DOM.Element} way2
7✔
216
   *
7✔
217
   * @return {DOM.Element} way
7✔
218
   */
25✔
219
  static joinAllWays(ways) {
25✔
220
    let way = ways[0];
25✔
221
    ways.slice(1).forEach(w => {
25✔
222
      way = this.joinWays(way, w);
25✔
223
    });
7✔
224
    return way;
7✔
225
  }
7✔
226

7✔
227
  /**
7✔
228
   * Reverse the order of nodes in a way.
7✔
229
   *
7✔
230
   * @param {DOM.Element} way - a way
7✔
231
   *
7✔
232
   * @return {DOM.Element} way
7✔
233
   */
7✔
234
  static reverseWay(way) {
7✔
235
    const elements = way.getElementsByTagName('nd');
7✔
236
    const newWay = way.cloneNode(true);
7✔
237
    newWay.innerHTML = '';
7✔
238
    for (let i = 0; i < elements.length; i++) {
7✔
239
      let elem = elements[elements.length - 1 - i].cloneNode();
16✔
240
      newWay.appendChild(elem);
7✔
241
    }
7✔
242
    return newWay;
7✔
243
  }
7✔
244

7✔
245
  /**
7✔
246
   * Find the center of a closed way
7✔
247
   *
7✔
248
   * @param {THREE.Shape} shape - the shape
7✔
249
   *
7✔
250
   * @return {[number, number]} xy - x/y coordinates of the center
7✔
251
   */
1✔
252
  static center(shape) {
1✔
253
    const extents = BuildingShapeUtils.extents(shape);
1✔
254
    const center = [(extents[0] + extents[2] ) / 2, (extents[1]  + extents[3] ) / 2];
7✔
255
    return center;
7✔
256
  }
7✔
257

7✔
258
  /**
7✔
259
   * Return the longest cardinal side length.
7✔
260
   *
7✔
261
   * @param {THREE.Shape} shape - the shape
7✔
262
   */
1✔
263
  static getWidth(shape) {
1✔
264
    const xy = BuildingShapeUtils.combineCoordinates(shape);
1✔
265
    const x = xy[0];
1✔
266
    const y = xy[1];
1✔
267
    return Math.max(Math.max(...x) - Math.min(...x), Math.max(...y) - Math.min(...y));
7✔
268
  }
7✔
269

7✔
270
  /**
7✔
271
   * can points be an array of shapes?
7✔
272
   */
1✔
273
  static combineCoordinates(shape) {
1✔
274
    //console.log('Shape: ' + JSON.stringify(shape));
1✔
275
    const points = shape.extractPoints().shape;
1✔
276
    var x = [];
1✔
277
    var y = [];
1✔
278
    var vec;
1✔
279
    for (let i = 0; i < points.length; i++) {
1✔
280
      vec = points[i];
3✔
281
      x.push(vec.x);
3✔
282
      y.push(vec.y);
1✔
283
    }
7✔
284
    return [x, y];
7✔
285
  }
7✔
286

7✔
287
  /**
7✔
288
   * Calculate the Cartesian extents of the shape after rotaing couterclockwise by a given angle.
7✔
289
   *
7✔
290
   * @param {THREE.Shape} pts - the shape or Array of shapes.
7✔
291
   * @param {number} angle - angle in radians to rotate shape
7✔
292
   *
7✔
293
   * @return {[number, number, number, number]} the extents of the object.
7✔
294
   */
10✔
295
  static extents(shape, angle = 0) {
10✔
296
    if (!Array.isArray(shape)) {
10✔
297
      shape = [shape];
10✔
298
    }
10✔
299
    var x = [];
10✔
300
    var y = [];
10✔
301
    var vec;
10✔
302
    for (let i = 0; i < shape.length; i++) {
10✔
303
      const points = shape[i].extractPoints().shape;
10✔
304
      for (let i = 0; i < points.length; i++) {
10✔
305
        vec = points[i];
40✔
306
        x.push(vec.x * Math.cos(angle) - vec.y * Math.sin(angle));
40✔
307
        y.push(vec.x * Math.sin(angle) + vec.y * Math.cos(angle));
10✔
308
      }
10✔
309
    }
10✔
310
    const left = Math.min(...x);
10✔
311
    const bottom = Math.min(...y);
10✔
312
    const right = Math.max(...x);
10✔
313
    const top = Math.max(...y);
10✔
314
    return [left, bottom, right, top];
7✔
315
  }
7✔
316

7✔
317
  /**
7✔
318
   * Assuming the shape is all right angles,
7✔
319
   * Find the orientation of the longest edge.
7✔
UNCOV
320
   */
×
UNCOV
321
  static primaryDirection(shape) {
×
322
    const points = shape.extractPoints().shape;
7✔
323
  }
7✔
324

7✔
325
  /**
7✔
326
   * Calculate the length of each of a shape's edge
7✔
327
   *
7✔
328
   * @param {THREE.Shape} shape - the shape
7✔
329
   *
7✔
330
   * @return {[number, ...]} the esge lwngths.
7✔
331
   */
2✔
332
  static edgeLength(shape) {
2✔
333
    const points = shape.extractPoints().shape;
2✔
334
    const lengths = [];
2✔
335
    var p1;
2✔
336
    var p2;
2✔
337
    for (let i = 0; i < points.length - 1; i++) {
2✔
338
      p1 = points[i];
4✔
339
      p2 = points[i + 1];
4✔
340
      lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
2✔
341
    }
2✔
342
    p1 = points[points.length - 1];
2✔
343
    p2 = points[0];
2✔
344
    lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
2✔
345
    return lengths;
7✔
346
  }
7✔
347

7✔
348
  /**
7✔
349
   * Calculate the angle at each of a shape's vertex.
7✔
350
   * The angle will be PI > x >= -PI
7✔
351
   *
7✔
352
   * @param {THREE.Shape} shape - the shape
7✔
353
   *
7✔
354
   * @return {[number, ...]} the angles in radians.
7✔
355
   */
2✔
356
  static vertexAngle(shape) {
2✔
357
    const points = shape.extractPoints().shape;
2✔
358
    const angles = [];
2✔
359
    var p0;
2✔
360
    var p1;
2✔
361
    var p2;
6✔
362

6✔
363
    function calcAngle(p0, p1, p2) {
6✔
364
      let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x) - Math.atan2(p0.y - p1.y, p0.x - p1.x);
6✔
365
      if (angle >= Math.PI) {
6✔
366
        angle -= 2 * Math.PI;
6✔
367
      } else if (angle < -Math.PI) {
5✔
368
        angle += 2 * Math.PI;
6✔
369
      }
6✔
370
      return angle;
2✔
371
    }
2✔
372

2✔
373
    p0 = points[points.length - 1];
2✔
374
    p1 = points[0];
2✔
375
    p2 = points[1];
2✔
376

2✔
377
    angles.push(calcAngle(p0, p1, p2));
2✔
378
    for (let i = 1; i < points.length; i++) {
2✔
379
      p0 = points[i - 1];
4✔
380
      p1 = points[i];
4✔
381
      p2 = points[(i + 1) % points.length];
4✔
382
      angles.push(calcAngle(p0, p1, p2));
2✔
383
    }
7✔
384
    return angles;
7✔
385
  }
7✔
386

7✔
387
  /**
7✔
388
   * Calculate the angle of each of a shape's edge.
7✔
389
   * the angle will be PI > x >= -PI
7✔
390
   *
7✔
391
   * @param {THREE.Shape} shape - the shape
7✔
392
   *
7✔
393
   * @return {[number, ...]} the angles in radians.
7✔
394
   */
2✔
395
  static edgeDirection(shape) {
2✔
396
    const points = shape.extractPoints().shape;
2✔
397
    points.push(points[0]);
2✔
398
    const angles = [];
2✔
399
    var p1;
2✔
400
    var p2;
2✔
401
    for (let i = 0; i < points.length - 1; i++) {
2✔
402
      p1 = points[i];
6✔
403
      p2 = points[i + 1];
6✔
404
      let angle = Math.atan2((p2.y - p1.y), (p2.x - p1.x));
6✔
405
      if (angle >= Math.PI / 2) {
6✔
406
        angle -= Math.PI;
6✔
407
      } else if (angle < -Math.PI / 2) {
4!
408
        angle += Math.PI;
6✔
409
      }
6✔
410
      angles.push(angle);
7✔
411
    }
7✔
412
    return angles;
7✔
413
  }
7✔
414

7✔
415
  /**
7✔
416
   * Is the given point within the given shape?
7✔
417
   *
7✔
418
   * @param {THREE.Shape} shape - the shape
7✔
419
   * @param {[number, number]} point - an x, y pair.
7✔
420
   *
7✔
421
   * @return {boolean}
7✔
422
   */
4✔
423
  static surrounds(shape, point) {
4✔
424
    var count = 0;
4✔
425
    const vecs = shape.extractPoints().shape;
4✔
426
    var vec;
4✔
427
    var nextvec;
4✔
428
    for (let i = 0; i < vecs.length - 1; i++) {
4✔
429
      vec = vecs[i];
6✔
430
      nextvec = vecs[i+1];
6✔
431
      if (vec.x === point[0] && vec.y === point[1]) {
6✔
432
        return true;
5✔
433
      }
5✔
434
      const slope = (nextvec.y - vec.y) / (nextvec.x - vec.x);
5✔
435
      const intercept = vec.y / slope / vec.x;
5✔
436
      const intersection = (point[1] - intercept) / slope;
5✔
437
      if (intersection > point[0]) {
6✔
438
        count++;
3✔
439
      } else if (intersection === point[0]) {
3✔
440
        return true;
4✔
441
      }
1✔
442
    }
1✔
443
    return count % 2 === 1;
7✔
444
  }
7✔
445

7✔
446
  /**
7✔
447
   * Calculate the radius of a circle that can fit within a shape.
7✔
448
   *
7✔
449
   * @param {THREE.Shape} shape - the shape
7✔
450
   */
1✔
451
  static calculateRadius(shape) {
1✔
452
    const extents = BuildingShapeUtils.extents(shape);
1✔
453
    // return half of the shorter side-length.
1✔
454
    return Math.min(extents[2] - extents[0], extents[3] - extents[1]) / 2;
7✔
455
  }
7✔
456

7✔
457
  /**
7✔
458
   * Calculate the angle of the longest side of a shape with 90° vertices.
7✔
459
   * is begining / end duplicated?
7✔
460
   *
7✔
461
   * @param {THREE.Shape} shape - the shape
7✔
462
   * @return {number}
7✔
463
   */
1✔
464
  static longestSideAngle(shape) {
1✔
465
    const vecs = shape.extractPoints().shape;
1✔
466
    const lengths = BuildingShapeUtils.edgeLength(shape);
1✔
467
    const directions = BuildingShapeUtils.edgeDirection(shape);
1✔
468
    var index;
1✔
469
    var maxLength = 0;
1✔
470
    for (let i = 0; i < lengths.length; i++) {
1✔
471
      if (lengths[i] > maxLength) {
3✔
472
        index = i;
2✔
473
        maxLength = lengths[i];
1✔
474
      }
1✔
475
    }
1✔
476
    var angle = directions[index];
1✔
477
    const extents = BuildingShapeUtils.extents(shape, -angle);
1✔
478
    // If the shape is taller than it is wide after rotation, we are off by 90 degrees.
1✔
479
    if ((extents[3] - extents[1]) > (extents[2] - extents[0])) {
1!
480
      angle = angle > 0 ? angle - Math.PI / 2 : angle + Math.PI / 2;
7✔
481
    }
7✔
482
    return angle;
7✔
483
  }
7✔
484

7✔
485
  /**
7✔
486
   * Rotate lat/lon to reposition the home point onto 0,0.
7✔
487
   *
7✔
488
   * @param {[number, number]} lonLat - The longitute and latitude of a point.
7✔
489
   *
7✔
490
   * @return {[number, number]} x, y in meters
7✔
491
   */
4✔
492
  static repositionPoint(lonLat, home) {
4✔
493
    const R = 6371 * 1000;   // Earth radius in m
4✔
494
    const circ = 2 * Math.PI * R;  // Circumference
4✔
495
    const phi = 90 - lonLat[1];
4✔
496
    const theta = lonLat[0] - home[0];
4✔
497
    const thetaPrime = home[1] / 180 * Math.PI;
4✔
498
    const x = R * Math.sin(theta / 180 * Math.PI) * Math.sin(phi / 180 * Math.PI);
4✔
499
    const y = R * Math.cos(phi / 180 * Math.PI);
4✔
500
    const z = R * Math.sin(phi / 180 * Math.PI) * Math.cos(theta / 180 * Math.PI);
4✔
501
    const abs = Math.sqrt(z**2 + y**2);
4✔
502
    const arg = Math.atan(y / z) - thetaPrime;
4✔
503

4✔
504
    return [x, Math.sin(arg) * abs];
7✔
505
  }
7✔
506
}
7✔
507
export {BuildingShapeUtils};
7✔
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

© 2026 Coveralls, Inc