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

Beakerboy / OSMBuilding / 14737139692

29 Apr 2025 05:06PM UTC coverage: 56.59% (+2.5%) from 54.091%
14737139692

push

github

web-flow
Additional coverage. Formalize vertexAngle return range. (#95)

81 of 123 branches covered (65.85%)

Branch coverage included in aggregate %.

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

2 existing lines in 1 file now uncovered.

928 of 1660 relevant lines covered (55.9%)

3.11 hits per line

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

95.88
/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
60✔
48
    const elements = way.getElementsByTagName('nd');
60✔
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'));
9✔
61
    if (BuildingShapeUtils.isClosed(way)){
9✔
62
      nodes.pop();
7✔
63
    }
9✔
64
    const refs = new Set();
9✔
65
    for (const node of nodes) {
9✔
66
      const ref = node.getAttribute('ref');
30✔
67
      if (refs.has(ref)){
30✔
68
        return true;
2✔
69
      }
30✔
70
      refs.add(ref);
28✔
71
    }
7✔
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]} array - list 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
    var closedWays = [];
10✔
85
    var openWays = [];
10✔
86
    var changed = true;
10✔
87
    while (changed) {
10✔
88
      changed = false;
23✔
89
      for (let i = 0; i < ways.length - 1; i++) {
23✔
90
        if (BuildingShapeUtils.isClosed(ways[i])) {
21✔
91
          closedWays.push(ways[i]);
7✔
92
        } else {
19✔
93
          // These are HTMLCollections of nodes, not ways.
14✔
94
          const way1 = ways[i].getElementsByTagName('nd');
14✔
95
          const way2 = ways[i + 1].getElementsByTagName('nd');
14✔
96

14✔
97
          // If the first node of way2 is the same as the last in way one, they can be combined
14✔
98
          // Or if the first node of way1 is the same as the last in way2
14✔
99
          // Need to extend this to tip-to-tip connections as well.
14✔
100
          // Need to add a "reverse way" function somewhere.
14✔
101
          if (way2[0].getAttribute('ref') === way1[way1.length - 1].getAttribute('ref')) {
14✔
102
            const result = BuildingShapeUtils.joinWays(ways[i], ways[i + 1]);
8✔
103
            openWays.push(result);
8✔
104
            i++;
8✔
105
            changed = true;
8✔
106
          } else if (way1[0].getAttribute('ref') === way2[way2.length - 1].getAttribute('ref')) {
14✔
107
            const result = BuildingShapeUtils.joinWays(ways[i + 1], ways[i]);
2✔
108
            openWays.push(result);
2✔
109
            i++;
2✔
110
            changed = true;
2✔
111
          } else if (way1[way1.length - 1].getAttribute('ref') === way2[way2.length - 1].getAttribute('ref')) {
6✔
112
            const tempway = BuildingShapeUtils.reverseWay(ways[i + 1]);
2✔
113
            const result = BuildingShapeUtils.joinWays(ways[i], tempway);
2✔
114
            openWays.push(result);
2✔
115
            i++;
2✔
116
            changed = true;
2✔
117
          } else if (way1[0].getAttribute('ref') === way2[0].getAttribute('ref')) {
2✔
118
            const tempway = BuildingShapeUtils.reverseWay(ways[i+1]);
1✔
119
            const result = BuildingShapeUtils.joinWays(tempway, ways[i]);
1✔
120
            openWays.push(result);
1✔
121
            i++;
1✔
122
            changed = true;
1✔
123
          } else {
1✔
124
            openWays.push(ways[i]);
1✔
125
          }
14✔
126
        }
21✔
127
      }
23✔
128
      const lastWay = ways[ways.length - 1];
23✔
129
      if (BuildingShapeUtils.isClosed(lastWay)) {
23✔
130
        closedWays.push(lastWay);
2✔
131
      } else {
23✔
132
        openWays.push(lastWay);
21✔
133
      }
23✔
134
      ways = openWays;
23✔
135
      openWays = [];
23✔
136
    }
10✔
137
    return closedWays;
10✔
138
  }
7✔
139

7✔
140
  /**
7✔
141
   * Append the nodes from one way into another.
7✔
142
   *
7✔
143
   * @param {DOM.Element} way1 - an open, non self-intersecring way
7✔
144
   * @param {DOM.Element} way2
7✔
145
   *
7✔
146
   * @return {DOM.Element} way
7✔
147
   */
7✔
148
  static joinWays(way1, way2) {
7✔
149
    const elements = way2.getElementsByTagName('nd');
14✔
150
    for (let i = 1; i < elements.length; i++) {
14✔
151
      let elem = elements[i].cloneNode();
17✔
152
      way1.appendChild(elem);
17✔
153
    }
14✔
154
    return way1;
14✔
155
  }
7✔
156

7✔
157
  /**
7✔
158
   * Reverse the order of nodes in a way.
7✔
159
   *
7✔
160
   * @param {DOM.Element} way - a way
7✔
161
   *
7✔
162
   * @return {DOM.Element} way
7✔
163
   */
7✔
164
  static reverseWay(way) {
7✔
165
    const elements = way.getElementsByTagName('nd');
4✔
166
    const newWay = way.cloneNode(true);
4✔
167
    newWay.innerHTML = '';
4✔
168
    for (let i = 0; i < elements.length; i++) {
4✔
169
      let elem = elements[elements.length - 1 - i].cloneNode();
10✔
170
      newWay.appendChild(elem);
10✔
171
    }
4✔
172
    return newWay;
4✔
173
  }
7✔
174

7✔
175
  /**
7✔
176
   * Find the center of a closed way
7✔
177
   *
7✔
178
   * @param {THREE.Shape} shape - the shape
7✔
179
   *
7✔
180
   * @return {[number, number]} xy - x/y coordinates of the center
7✔
181
   */
7✔
182
  static center(shape) {
7✔
183
    const extents = BuildingShapeUtils.extents(shape);
1✔
184
    const center = [(extents[0] + extents[2] ) / 2, (extents[1]  + extents[3] ) / 2];
1✔
185
    return center;
7✔
186
  }
7✔
187

7✔
188
  /**
7✔
189
   * Return the longest cardinal side length.
7✔
190
   *
7✔
191
   * @param {THREE.Shape} shape - the shape
7✔
192
   */
7✔
193
  static getWidth(shape) {
7✔
194
    const xy = BuildingShapeUtils.combineCoordinates(shape);
1✔
195
    const x = xy[0];
1✔
196
    const y = xy[1];
1✔
197
    return Math.max(Math.max(...x) - Math.min(...x), Math.max(...y) - Math.min(...y));
7✔
198
  }
7✔
199

7✔
200
  /**
7✔
201
   * can points be an array of shapes?
7✔
202
   */
7✔
203
  static combineCoordinates(shape) {
7✔
204
    //console.log('Shape: ' + JSON.stringify(shape));
1✔
205
    const points = shape.extractPoints().shape;
1✔
206
    var x = [];
1✔
207
    var y = [];
1✔
208
    var vec;
1✔
209
    for (let i = 0; i < points.length; i++) {
1✔
210
      vec = points[i];
3✔
211
      x.push(vec.x);
3✔
212
      y.push(vec.y);
3✔
213
    }
1✔
214
    return [x, y];
7✔
215
  }
7✔
216

7✔
217
  /**
7✔
218
   * Calculate the Cartesian extents of the shape after rotaing couterclockwise by a given angle.
7✔
219
   *
7✔
220
   * @param {THREE.Shape} pts - the shape or Array of shapes.
7✔
221
   * @param {number} angle - angle in radians to rotate shape
7✔
222
   *
7✔
223
   * @return {[number, number, number, number]} the extents of the object.
7✔
224
   */
7✔
225
  static extents(shape, angle = 0) {
7✔
226
    if (!Array.isArray(shape)) {
9✔
227
      shape = [shape];
9✔
228
    }
9✔
229
    var x = [];
9✔
230
    var y = [];
9✔
231
    var vec;
9✔
232
    for (let i = 0; i < shape.length; i++) {
9✔
233
      const points = shape[i].extractPoints().shape;
9✔
234
      for (let i = 0; i < points.length; i++) {
9✔
235
        vec = points[i];
37✔
236
        x.push(vec.x * Math.cos(angle) - vec.y * Math.sin(angle));
37✔
237
        y.push(vec.x * Math.sin(angle) + vec.y * Math.cos(angle));
37✔
238
      }
9✔
239
    }
9✔
240
    const left = Math.min(...x);
9✔
241
    const bottom = Math.min(...y);
9✔
242
    const right = Math.max(...x);
9✔
243
    const top = Math.max(...y);
9✔
244
    return [left, bottom, right, top];
7✔
245
  }
7✔
246

7✔
247
  /**
7✔
248
   * Assuming the shape is all right angles,
7✔
249
   * Find the orientation of the longest edge.
7✔
250
   */
7✔
251
  static primaryDirection(shape) {
7✔
252
    const points = shape.extractPoints().shape;
7✔
253
  }
7✔
254

7✔
255
  /**
7✔
256
   * Calculate the length of each of a shape's edge
7✔
257
   *
7✔
258
   * @param {THREE.Shape} shape - the shape
7✔
259
   *
7✔
260
   * @return {[number, ...]} the esge lwngths.
7✔
261
   */
7✔
262
  static edgeLength(shape) {
7✔
263
    const points = shape.extractPoints().shape;
2✔
264
    const lengths = [];
2✔
265
    var p1;
2✔
266
    var p2;
2✔
267
    for (let i = 0; i < points.length - 1; i++) {
2✔
268
      p1 = points[i];
4✔
269
      p2 = points[i + 1];
4✔
270
      lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
4✔
271
    }
2✔
272
    p1 = points[points.length - 1];
2✔
273
    p2 = points[0];
2✔
274
    lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
2✔
275
    return lengths;
7✔
276
  }
7✔
277

7✔
278
  /**
7✔
279
   * Calculate the angle at each of a shape's vertex.
7✔
280
   * The angle will be PI > x >= -PI
7✔
281
   *
7✔
282
   * @param {THREE.Shape} shape - the shape
7✔
283
   *
7✔
284
   * @return {[number, ...]} the angles in radians.
7✔
285
   */
7✔
286
  static vertexAngle(shape) {
7✔
287
    const points = shape.extractPoints().shape;
2✔
288
    const angles = [];
2✔
289
    var p0;
2✔
290
    var p1;
2✔
291
    var p2;
2✔
292

6✔
293
    function calcAngle(p0, p1, p2) {
6✔
294
      let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x) - Math.atan2(p0.y - p1.y, p0.x - p1.x);
6✔
295
      if (angle >= Math.PI) {
6✔
296
        angle -= 2 * Math.PI;
1✔
297
      } else if (angle < -Math.PI) {
5✔
298
        angle += 2 * Math.PI;
1✔
299
      }
6✔
300
      return angle;
2✔
301
    }
2✔
302

2✔
303
    p0 = points[points.length - 1];
2✔
304
    p1 = points[0];
2✔
305
    p2 = points[1];
2✔
306

2✔
307
    angles.push(calcAngle(p0, p1, p2));
2✔
308
    for (let i = 1; i < points.length - 1; i++) {
2✔
309
      p0 = points[i - 1];
2✔
310
      p1 = points[i];
2✔
311
      p2 = points[i + 1];
2✔
312
      angles.push(calcAngle(p0, p1, p2));
2✔
313
    }
2✔
314
    angles.push(calcAngle(p0, p1, p2));
2✔
315
    return angles;
7✔
316
  }
7✔
317

7✔
318
  /**
7✔
319
   * Calculate the angle of each of a shape's edge.
7✔
320
   * the angle will be PI > x >= -PI
7✔
321
   *
7✔
322
   * @param {THREE.Shape} shape - the shape
7✔
323
   *
7✔
324
   * @return {[number, ...]} the angles in radians.
7✔
325
   */
7✔
326
  static edgeDirection(shape) {
2✔
327
    const points = shape.extractPoints().shape;
2✔
328
    points.push(points[0]);
2✔
329
    const angles = [];
2✔
330
    var p1;
2✔
331
    var p2;
2✔
332
    for (let i = 0; i < points.length - 1; i++) {
2✔
333
      p1 = points[i];
6✔
334
      p2 = points[i + 1];
6✔
335
      let angle = Math.atan2((p2.y - p1.y), (p2.x - p1.x));
6✔
336
      if (angle >= Math.PI / 2) {
6✔
337
        angle -= Math.PI;
6✔
338
      } else if (angle < -Math.PI / 2) {
4!
339
        angle += Math.PI;
6✔
340
      }
6✔
341
      angles.push(angle);
2✔
342
    }
2✔
343
    return angles;
7✔
344
  }
7✔
345

7✔
346
  /**
7✔
347
   * Count the number of times that a line horizontal from point intersects shape
7✔
348
   *
7✔
349
   * if an odd number are crossed, it is inside.
7✔
350
   * todo, test holes
7✔
351
   * Test edge conditions.
7✔
352
   */
7✔
UNCOV
353
  static surrounds(shape, point) {
×
354
    var count = 0;
×
355
    const vecs = shape.extractPoints().shape;
×
356
    var vec;
×
357
    var nextvec;
×
358
    for (let i = 0; i < vecs.length - 1; i++) {
×
359
      vec = vecs[i];
×
360
      nextvec = vecs[i+1];
×
361
      if (vec.x === point[0] && vec.y === point[1]) {
×
362
        return true;
×
363
      }
×
364
      if ((vec.x >= point[0] || nextvec.x >= point[0]) && (vec.y >= point[1] !== nextvec.y >= point[1])) {
×
365
        count++;
×
366
      }
×
367
    }
×
368
    return count % 2 === 1;
7✔
369
  }
7✔
370

7✔
371
  /**
7✔
372
   * Calculate the radius of a circle that can fit within a shape.
7✔
373
   *
7✔
374
   * @param {THREE.Shape} shape - the shape
7✔
375
   */
7✔
UNCOV
376
  static calculateRadius(shape) {
×
377
    const extents = BuildingShapeUtils.extents(shape);
×
378
    // return half of the shorter side-length.
×
379
    return Math.min(extents[2] - extents[0], extents[3] - extents[1]) / 2;
7✔
380
  }
7✔
381

7✔
382
  /**
7✔
383
   * Calculate the angle of the longest side of a shape with 90° vertices.
7✔
384
   * is begining / end duplicated?
7✔
385
   *
7✔
386
   * @param {THREE.Shape} shape - the shape
7✔
387
   * @return {number}
7✔
388
   */
7✔
389
  static longestSideAngle(shape) {
1✔
390
    const vecs = shape.extractPoints().shape;
1✔
391
    const lengths = BuildingShapeUtils.edgeLength(shape);
1✔
392
    const directions = BuildingShapeUtils.edgeDirection(shape);
1✔
393
    var index;
1✔
394
    var maxLength = 0;
1✔
395
    for (let i = 0; i < lengths.length; i++) {
1✔
396
      if (lengths[i] > maxLength) {
3✔
397
        index = i;
2✔
398
        maxLength = lengths[i];
3✔
399
      }
1✔
400
    }
1✔
401
    var angle = directions[index];
1✔
402
    const extents = BuildingShapeUtils.extents(shape, -angle);
1✔
403
    // If the shape is taller than it is wide after rotation, we are off by 90 degrees.
1✔
404
    if ((extents[3] - extents[1]) > (extents[2] - extents[0])) {
1!
405
      angle = angle > 0 ? angle - Math.PI / 2 : angle + Math.PI / 2;
1✔
406
    }
1✔
407
    return angle;
7✔
408
  }
7✔
409

7✔
410
  /**
7✔
411
   * Rotate lat/lon to reposition the home point onto 0,0.
7✔
412
   *
7✔
413
   * @param {[number, number]} lonLat - The longitute and latitude of a point.
7✔
414
   *
7✔
415
   * @return {[number, number]} x, y in meters
7✔
416
   */
4✔
417
  static repositionPoint(lonLat, home) {
4✔
418
    const R = 6371 * 1000;   // Earth radius in m
4✔
419
    const circ = 2 * Math.PI * R;  // Circumference
4✔
420
    const phi = 90 - lonLat[1];
4✔
421
    const theta = lonLat[0] - home[0];
4✔
422
    const thetaPrime = home[1] / 180 * Math.PI;
4✔
423
    const x = R * Math.sin(theta / 180 * Math.PI) * Math.sin(phi / 180 * Math.PI);
4✔
424
    const y = R * Math.cos(phi / 180 * Math.PI);
4✔
425
    const z = R * Math.sin(phi / 180 * Math.PI) * Math.cos(theta / 180 * Math.PI);
4✔
426
    const abs = Math.sqrt(z**2 + y**2);
4✔
427
    const arg = Math.atan(y / z) - thetaPrime;
4✔
428

4✔
429
    return [x, Math.sin(arg) * abs];
7✔
430
  }
7✔
431
}
7✔
432
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