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

Beakerboy / OSMBuilding / 14731797867

29 Apr 2025 12:56PM UTC coverage: 54.091% (+1.9%) from 52.202%
14731797867

push

github

web-flow
Increased coverage and added isSelfIntersecting (#90)

72 of 114 branches covered (63.16%)

Branch coverage included in aggregate %.

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

2 existing lines in 1 file now uncovered.

880 of 1646 relevant lines covered (53.46%)

3.05 hits per line

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

88.31
/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}
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);
×
184
    const center = [(extents[0] + extents[2] ) / 2, (extents[1]  + extents[3] ) / 2];
×
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);
×
195
    const x = xy[0];
×
196
    const y = xy[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));
×
205
    const points = shape.extractPoints().shape;
×
206
    var x = [];
×
207
    var y = [];
×
208
    var vec;
×
209
    for (let i = 0; i < points.length; i++) {
×
210
      vec = points[i];
×
211
      x.push(vec.x);
×
212
      y.push(vec.y);
×
213
    }
×
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)) {
8✔
227
      shape = [shape];
8✔
228
    }
8✔
229
    var x = [];
8✔
230
    var y = [];
8✔
231
    var vec;
8✔
232
    for (let i = 0; i < shape.length; i++) {
8✔
233
      const points = shape[i].extractPoints().shape;
8✔
234
      for (let i = 0; i < points.length; i++) {
8✔
235
        vec = points[i];
34✔
236
        x.push(vec.x * Math.cos(angle) - vec.y * Math.sin(angle));
34✔
237
        y.push(vec.x * Math.sin(angle) + vec.y * Math.cos(angle));
34✔
238
      }
8✔
239
    }
8✔
240
    const left = Math.min(...x);
8✔
241
    const bottom = Math.min(...y);
8✔
242
    const right = Math.max(...x);
8✔
243
    const top = Math.max(...y);
8✔
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
   */
7✔
281
  static vertexAngle(shape) {
7✔
282
    const points = shape.extractPoints().shape;
×
283
    const angles = [];
×
284
    var p0;
×
285
    var p1;
×
286
    var p2;
×
287
    p0 = points[points.length];
×
288
    p1 = points[0];
×
289
    p2 = points[1];
×
290
    angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
291
    for (let i = 1; i < points.length - 1; i++) {
×
292
      p0 = points[i-1];
×
293
      p1 = points[i];
×
294
      p2 = points[i + 1];
×
295
      angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
296
    }
×
297
    p0 = points[points.length-1];
×
298
    p1 = points[points.length];
×
299
    p2 = points[0];
×
300
    angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
UNCOV
301
    return angles;
×
302
  }
7✔
303

7✔
304
  /**
7✔
305
   * Calculate the angle of each of a shape's edge.
7✔
306
   * the angle will be PI > x >= -PI
7✔
307
   *
7✔
308
   * @param {THREE.Shape} shape - the shape
7✔
309
   *
7✔
310
   * @return {[number, ...]} the angles in radians.
7✔
311
   */
7✔
312
  static edgeDirection(shape) {
7✔
313
    const points = shape.extractPoints().shape;
2✔
314
    points.push(points[0]);
2✔
315
    const angles = [];
2✔
316
    var p1;
2✔
317
    var p2;
2✔
318
    for (let i = 0; i < points.length - 1; i++) {
2✔
319
      p1 = points[i];
6✔
320
      p2 = points[i + 1];
6✔
321
      let angle = Math.atan2((p2.y - p1.y), (p2.x - p1.x));
6✔
322
      if (angle >= Math.PI / 2) {
6✔
323
        angle -= Math.PI;
2✔
324
      } else if (angle < -Math.PI / 2) {
6!
UNCOV
325
        angle += Math.PI;
×
326
      }
6✔
327
      angles.push(angle);
6✔
328
    }
2✔
329
    return angles;
7✔
330
  }
7✔
331

7✔
332
  /**
7✔
333
   * Count the number of times that a line horizontal from point intersects shape
7✔
334
   *
7✔
335
   * if an odd number are crossed, it is inside.
7✔
336
   * todo, test holes
7✔
337
   * Test edge conditions.
7✔
338
   */
7✔
339
  static surrounds(shape, point) {
7✔
340
    var count = 0;
×
341
    const vecs = shape.extractPoints().shape;
×
342
    var vec;
×
343
    var nextvec;
×
344
    for (let i = 0; i < vecs.length - 1; i++) {
×
345
      vec = vecs[i];
×
346
      nextvec = vecs[i+1];
×
347
      if (vec.x === point[0] && vec.y === point[1]) {
×
348
        return true;
×
349
      }
×
350
      if ((vec.x >= point[0] || nextvec.x >= point[0]) && (vec.y >= point[1] !== nextvec.y >= point[1])) {
×
351
        count++;
×
352
      }
×
353
    }
×
354
    return count % 2 === 1;
7✔
355
  }
7✔
356

7✔
357
  /**
7✔
358
   * Calculate the radius of a circle that can fit within a shape.
7✔
359
   *
7✔
360
   * @param {THREE.Shape} shape - the shape
7✔
361
   */
7✔
362
  static calculateRadius(shape) {
7✔
363
    const extents = BuildingShapeUtils.extents(shape);
×
364
    // return half of the shorter side-length.
×
365
    return Math.min(extents[2] - extents[0], extents[3] - extents[1]) / 2;
7✔
366
  }
7✔
367

7✔
368
  /**
7✔
369
   * Calculate the angle of the longest side of a shape with 90° vertices.
7✔
370
   * is begining / end duplicated?
7✔
371
   *
7✔
372
   * @param {THREE.Shape} shape - the shape
7✔
373
   * @return {number}
7✔
374
   */
7✔
375
  static longestSideAngle(shape) {
7✔
376
    const vecs = shape.extractPoints().shape;
1✔
377
    const lengths = BuildingShapeUtils.edgeLength(shape);
1✔
378
    const directions = BuildingShapeUtils.edgeDirection(shape);
1✔
379
    var index;
1✔
380
    var maxLength = 0;
1✔
381
    for (let i = 0; i < lengths.length; i++) {
1✔
382
      if (lengths[i] > maxLength) {
3✔
383
        index = i;
2✔
384
        maxLength = lengths[i];
2✔
385
      }
3✔
386
    }
1✔
387
    var angle = directions[index];
1✔
388
    const extents = BuildingShapeUtils.extents(shape, -angle);
1✔
389
    // If the shape is taller than it is wide after rotation, we are off by 90 degrees.
1✔
390
    if ((extents[3] - extents[1]) > (extents[2] - extents[0])) {
1!
391
      angle = angle > 0 ? angle - Math.PI / 2 : angle + Math.PI / 2;
1✔
392
    }
1✔
393
    return angle;
7✔
394
  }
7✔
395

7✔
396
  /**
7✔
397
   * Rotate lat/lon to reposition the home point onto 0,0.
7✔
398
   *
7✔
399
   * @param {[number, number]} lonLat - The longitute and latitude of a point.
7✔
400
   *
7✔
401
   * @return {[number, number]} x, y in meters
7✔
402
   */
7✔
403
  static repositionPoint(lonLat, home) {
4✔
404
    const R = 6371 * 1000;   // Earth radius in m
4✔
405
    const circ = 2 * Math.PI * R;  // Circumference
4✔
406
    const phi = 90 - lonLat[1];
4✔
407
    const theta = lonLat[0] - home[0];
4✔
408
    const thetaPrime = home[1] / 180 * Math.PI;
4✔
409
    const x = R * Math.sin(theta / 180 * Math.PI) * Math.sin(phi / 180 * Math.PI);
4✔
410
    const y = R * Math.cos(phi / 180 * Math.PI);
4✔
411
    const z = R * Math.sin(phi / 180 * Math.PI) * Math.cos(theta / 180 * Math.PI);
4✔
412
    const abs = Math.sqrt(z**2 + y**2);
4✔
413
    const arg = Math.atan(y / z) - thetaPrime;
4✔
414

4✔
415
    return [x, Math.sin(arg) * abs];
7✔
416
  }
7✔
417
}
7✔
418
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