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

Beakerboy / OSMBuilding / 14758654467

30 Apr 2025 03:44PM UTC coverage: 59.194% (+0.5%) from 58.709%
14758654467

push

github

web-flow
Do not allow self intersecting ways to be included in ring formation. (#104)

118 of 160 branches covered (73.75%)

Branch coverage included in aggregate %.

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

1 existing line in 1 file now uncovered.

983 of 1700 relevant lines covered (57.82%)

4.4 hits per line

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

99.11
/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
62✔
48
    const elements = way.getElementsByTagName('nd');
62✔
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'));
52✔
61
    if (BuildingShapeUtils.isClosed(way)){
52✔
62
      nodes.pop();
12✔
63
    }
52✔
64
    const refs = new Set();
52✔
65
    for (const node of nodes) {
52✔
66
      const ref = node.getAttribute('ref');
140✔
67
      if (refs.has(ref)){
140✔
68
        return true;
4✔
69
      }
140✔
70
      refs.add(ref);
136✔
71
    }
48✔
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
    const validWays = [];
16✔
85

16✔
86
    for (const way of ways) {
16✔
87
      if (BuildingShapeUtils.isSelfIntersecting(way)) {
40✔
88
        const id = way.getAttribute('id');
2✔
89
        const msg = 'Way ' + id + ' is self-intersecting';
2✔
90
        window.printError(msg);
2✔
91
      } else {
40✔
92
        const i = 3 + 'q';
38✔
93
        validWays.push(way);
38✔
94
      }
40✔
95
    }
16✔
96

16✔
97
    const closedWays = [];
16✔
98
    const wayBegins = {};
16✔
99
    const wayEnds = {};
16✔
100

16✔
101
    validWays.forEach(w => {
16✔
102
      const firstNodeID = w.querySelector('nd').getAttribute('ref');
38✔
103
      if (wayBegins[firstNodeID]) {
38✔
104
        wayBegins[firstNodeID].push(w);
6✔
105
      } else {
32✔
106
        wayBegins[firstNodeID] = [w];
32✔
107
      }
38✔
108

38✔
109
      const lastNodeID = w.querySelector('nd:last-of-type').getAttribute('ref');
38✔
110
      if (wayEnds[lastNodeID]) {
38✔
111
        wayEnds[lastNodeID].push(w);
37✔
112
      } else {
31✔
113
        wayEnds[lastNodeID] = [w];
38✔
114
      }
16✔
115
    });
16✔
116

16✔
117
    const usedWays = new Set();
16✔
118

41✔
119
    function tryMakeRing(currentRingWays) {
41✔
120
      if (currentRingWays[0].querySelector('nd').getAttribute('ref') ===
41✔
121
          currentRingWays[currentRingWays.length - 1].querySelector('nd:last-of-type').getAttribute('ref')) {
41✔
122
        return currentRingWays;
39✔
123
      }
29✔
124

29✔
125
      const lastWay = currentRingWays[currentRingWays.length - 1];
29✔
126
      const lastNodeID = lastWay.querySelector('nd:last-of-type').getAttribute('ref');
29✔
127
      for (let way of wayBegins[lastNodeID] ?? []) {
41✔
128
        const wayID = way.getAttribute('id');
18✔
129
        if (usedWays.has(wayID)) {
18✔
130
          continue;
18✔
131
        }
14✔
132
        usedWays.add(wayID);
14✔
133
        currentRingWays.push(way);
14✔
134
        if (tryMakeRing(currentRingWays).length) {
18✔
135
          return currentRingWays;
18✔
136
        }
1✔
137
        currentRingWays.pop();
1✔
138
        usedWays.delete(wayID);
39✔
139
      }
16✔
140

16✔
141
      for (let way of wayEnds[lastNodeID] ?? []) {
41✔
142
        const wayID = way.getAttribute('id');
22✔
143
        if (usedWays.has(wayID)) {
22✔
144
          continue;
21✔
145
        }
6✔
146
        usedWays.add(wayID);
6✔
147
        currentRingWays.push(BuildingShapeUtils.reverseWay(way));
6✔
148
        if (tryMakeRing(currentRingWays).length) {
21✔
149
          return currentRingWays;
22✔
150
        }
2✔
151
        currentRingWays.pop();
2✔
152
        usedWays.delete(wayID);
37✔
153
      }
41✔
154

16✔
155
      return [];
16✔
156
    }
16✔
157

38✔
158
    validWays.forEach(w => {
38✔
159
      const wayID = w.getAttribute('id');
38✔
160
      if (usedWays.has(wayID)){
38✔
161
        return;
21✔
162
      }
21✔
163
      usedWays.add(wayID);
21✔
164
      const result = tryMakeRing([w]);
21✔
165
      if (result.length) {
36✔
166
        let ring = result[0];
12✔
167
        result.slice(1).forEach(w => {
12✔
168
          ring = this.joinWays(ring, w);
12✔
169
        });
12✔
170
        closedWays.push(ring);
16✔
171
      }
16✔
172
    });
16✔
173

16✔
174
    return closedWays;
7✔
175
  }
7✔
176

7✔
177
  /**
7✔
178
   * Append the nodes from one way into another.
7✔
179
   *
7✔
180
   * @param {DOM.Element} way1 - an open, non self-intersecring way
7✔
181
   * @param {DOM.Element} way2
7✔
182
   *
7✔
183
   * @return {DOM.Element} way
7✔
184
   */
18✔
185
  static joinWays(way1, way2) {
18✔
186
    const elements = way2.getElementsByTagName('nd');
18✔
187
    for (let i = 1; i < elements.length; i++) {
18✔
188
      let elem = elements[i].cloneNode();
23✔
189
      way1.appendChild(elem);
18✔
190
    }
7✔
191
    return way1;
7✔
192
  }
7✔
193

7✔
194
  /**
7✔
195
   * Reverse the order of nodes in a way.
7✔
196
   *
7✔
197
   * @param {DOM.Element} way - a way
7✔
198
   *
7✔
199
   * @return {DOM.Element} way
7✔
200
   */
7✔
201
  static reverseWay(way) {
7✔
202
    const elements = way.getElementsByTagName('nd');
7✔
203
    const newWay = way.cloneNode(true);
7✔
204
    newWay.innerHTML = '';
7✔
205
    for (let i = 0; i < elements.length; i++) {
7✔
206
      let elem = elements[elements.length - 1 - i].cloneNode();
16✔
207
      newWay.appendChild(elem);
7✔
208
    }
7✔
209
    return newWay;
7✔
210
  }
7✔
211

7✔
212
  /**
7✔
213
   * Find the center of a closed way
7✔
214
   *
7✔
215
   * @param {THREE.Shape} shape - the shape
7✔
216
   *
7✔
217
   * @return {[number, number]} xy - x/y coordinates of the center
7✔
218
   */
1✔
219
  static center(shape) {
1✔
220
    const extents = BuildingShapeUtils.extents(shape);
1✔
221
    const center = [(extents[0] + extents[2] ) / 2, (extents[1]  + extents[3] ) / 2];
7✔
222
    return center;
7✔
223
  }
7✔
224

7✔
225
  /**
7✔
226
   * Return the longest cardinal side length.
7✔
227
   *
7✔
228
   * @param {THREE.Shape} shape - the shape
7✔
229
   */
1✔
230
  static getWidth(shape) {
1✔
231
    const xy = BuildingShapeUtils.combineCoordinates(shape);
1✔
232
    const x = xy[0];
1✔
233
    const y = xy[1];
1✔
234
    return Math.max(Math.max(...x) - Math.min(...x), Math.max(...y) - Math.min(...y));
7✔
235
  }
7✔
236

7✔
237
  /**
7✔
238
   * can points be an array of shapes?
7✔
239
   */
1✔
240
  static combineCoordinates(shape) {
1✔
241
    //console.log('Shape: ' + JSON.stringify(shape));
1✔
242
    const points = shape.extractPoints().shape;
1✔
243
    var x = [];
1✔
244
    var y = [];
1✔
245
    var vec;
1✔
246
    for (let i = 0; i < points.length; i++) {
1✔
247
      vec = points[i];
3✔
248
      x.push(vec.x);
1✔
249
      y.push(vec.y);
1✔
250
    }
7✔
251
    return [x, y];
7✔
252
  }
7✔
253

7✔
254
  /**
7✔
255
   * Calculate the Cartesian extents of the shape after rotaing couterclockwise by a given angle.
7✔
256
   *
7✔
257
   * @param {THREE.Shape} pts - the shape or Array of shapes.
7✔
258
   * @param {number} angle - angle in radians to rotate shape
7✔
259
   *
7✔
260
   * @return {[number, number, number, number]} the extents of the object.
7✔
261
   */
10✔
262
  static extents(shape, angle = 0) {
10✔
263
    if (!Array.isArray(shape)) {
10✔
264
      shape = [shape];
10✔
265
    }
10✔
266
    var x = [];
10✔
267
    var y = [];
10✔
268
    var vec;
10✔
269
    for (let i = 0; i < shape.length; i++) {
10✔
270
      const points = shape[i].extractPoints().shape;
10✔
271
      for (let i = 0; i < points.length; i++) {
10✔
272
        vec = points[i];
40✔
273
        x.push(vec.x * Math.cos(angle) - vec.y * Math.sin(angle));
40✔
274
        y.push(vec.x * Math.sin(angle) + vec.y * Math.cos(angle));
10✔
275
      }
10✔
276
    }
10✔
277
    const left = Math.min(...x);
10✔
278
    const bottom = Math.min(...y);
10✔
279
    const right = Math.max(...x);
10✔
280
    const top = Math.max(...y);
10✔
281
    return [left, bottom, right, top];
7✔
282
  }
7✔
283

7✔
284
  /**
7✔
285
   * Assuming the shape is all right angles,
7✔
286
   * Find the orientation of the longest edge.
7✔
287
   */
×
288
  static primaryDirection(shape) {
×
289
    const points = shape.extractPoints().shape;
7✔
290
  }
7✔
291

7✔
292
  /**
7✔
293
   * Calculate the length of each of a shape's edge
7✔
294
   *
7✔
295
   * @param {THREE.Shape} shape - the shape
7✔
296
   *
7✔
297
   * @return {[number, ...]} the esge lwngths.
7✔
298
   */
2✔
299
  static edgeLength(shape) {
2✔
300
    const points = shape.extractPoints().shape;
2✔
301
    const lengths = [];
2✔
302
    var p1;
2✔
303
    var p2;
2✔
304
    for (let i = 0; i < points.length - 1; i++) {
2✔
305
      p1 = points[i];
4✔
306
      p2 = points[i + 1];
4✔
307
      lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
2✔
308
    }
2✔
309
    p1 = points[points.length - 1];
2✔
310
    p2 = points[0];
2✔
311
    lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
7✔
312
    return lengths;
7✔
313
  }
7✔
314

7✔
315
  /**
7✔
316
   * Calculate the angle at each of a shape's vertex.
7✔
317
   * The angle will be PI > x >= -PI
7✔
318
   *
7✔
319
   * @param {THREE.Shape} shape - the shape
7✔
320
   *
7✔
321
   * @return {[number, ...]} the angles in radians.
7✔
322
   */
2✔
323
  static vertexAngle(shape) {
2✔
324
    const points = shape.extractPoints().shape;
2✔
325
    const angles = [];
2✔
326
    var p0;
2✔
327
    var p1;
6✔
328
    var p2;
6✔
329

6✔
330
    function calcAngle(p0, p1, p2) {
6✔
331
      let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x) - Math.atan2(p0.y - p1.y, p0.x - p1.x);
6✔
332
      if (angle >= Math.PI) {
1✔
333
        angle -= 2 * Math.PI;
6✔
334
      } else if (angle < -Math.PI) {
5✔
335
        angle += 2 * Math.PI;
6✔
336
      }
2✔
337
      return angle;
2✔
338
    }
2✔
339

2✔
340
    p0 = points[points.length - 1];
2✔
341
    p1 = points[0];
2✔
342
    p2 = points[1];
2✔
343

2✔
344
    angles.push(calcAngle(p0, p1, p2));
2✔
345
    for (let i = 1; i < points.length; i++) {
2✔
346
      p0 = points[i - 1];
4✔
347
      p1 = points[i];
4✔
348
      p2 = points[(i + 1) % points.length];
4✔
349
      angles.push(calcAngle(p0, p1, p2));
7✔
350
    }
7✔
351
    return angles;
7✔
352
  }
7✔
353

7✔
354
  /**
7✔
355
   * Calculate the angle of each of a shape's edge.
7✔
356
   * the angle will be PI > x >= -PI
7✔
357
   *
7✔
358
   * @param {THREE.Shape} shape - the shape
7✔
359
   *
7✔
360
   * @return {[number, ...]} the angles in radians.
7✔
361
   */
3✔
362
  static edgeDirection(shape) {
3✔
363
    const points = shape.extractPoints().shape;
3✔
364
    points.push(points[0]);
3✔
365
    const angles = [];
3✔
366
    var p1;
3✔
367
    var p2;
3✔
368
    for (let i = 0; i < points.length - 1; i++) {
3✔
369
      p1 = points[i];
9✔
370
      p2 = points[i + 1];
9✔
371
      let angle = Math.atan2((p2.y - p1.y), (p2.x - p1.x));
9✔
372
      if (angle >= Math.PI) {
1✔
373
        angle -= 2 * Math.PI;
9✔
UNCOV
374
      } else if (angle < -Math.PI) {
×
375
        angle += 2* Math.PI;
9✔
376
      }
3✔
377
      angles.push(angle);
7✔
378
    }
7✔
379
    return angles;
7✔
380
  }
7✔
381

7✔
382
  /**
7✔
383
   * Is the given point within the given shape?
7✔
384
   *
7✔
385
   * @param {THREE.Shape} shape - the shape
7✔
386
   * @param {[number, number]} point - an x, y pair.
7✔
387
   *
7✔
388
   * @return {boolean}
7✔
389
   */
4✔
390
  static surrounds(shape, point) {
4✔
391
    var count = 0;
4✔
392
    const vecs = shape.extractPoints().shape;
4✔
393
    var vec;
4✔
394
    var nextvec;
4✔
395
    for (let i = 0; i < vecs.length - 1; i++) {
4✔
396
      vec = vecs[i];
6✔
397
      nextvec = vecs[i+1];
6✔
398
      if (vec.x === point[0] && vec.y === point[1]) {
6✔
399
        return true;
5✔
400
      }
5✔
401
      const slope = (nextvec.y - vec.y) / (nextvec.x - vec.x);
5✔
402
      const intercept = vec.y / slope / vec.x;
5✔
403
      const intersection = (point[1] - intercept) / slope;
5✔
404
      if (intersection > point[0]) {
6✔
405
        count++;
3✔
406
      } else if (intersection === point[0]) {
6✔
407
        return true;
4✔
408
      }
1✔
409
    }
4✔
410
    return count % 2 === 1;
7✔
411
  }
7✔
412

7✔
413
  /**
7✔
414
   * Calculate the radius of a circle that can fit within a shape.
7✔
415
   *
7✔
416
   * @param {THREE.Shape} shape - the shape
7✔
417
   */
1✔
418
  static calculateRadius(shape) {
1✔
419
    const extents = BuildingShapeUtils.extents(shape);
1✔
420
    // return half of the shorter side-length.
1✔
421
    return Math.min(extents[2] - extents[0], extents[3] - extents[1]) / 2;
7✔
422
  }
7✔
423

7✔
424
  /**
7✔
425
   * Return the angle of the longest side of a shape with 90° vertices.
7✔
426
   *
7✔
427
   * @param {THREE.Shape} shape - the shape
7✔
428
   * @return {number}
7✔
429
   */
1✔
430
  static longestSideAngle(shape) {
1✔
431
    const lengths = BuildingShapeUtils.edgeLength(shape);
1✔
432
    const directions = BuildingShapeUtils.edgeDirection(shape);
1✔
433
    var index;
1✔
434
    var maxLength = 0;
1✔
435
    for (let i = 0; i < lengths.length; i++) {
1✔
436
      if (lengths[i] > maxLength) {
3✔
437
        index = i;
2✔
438
        maxLength = lengths[i];
1✔
439
      }
1✔
440
    }
1✔
441
    var angle = directions[index];
1✔
442
    const extents = BuildingShapeUtils.extents(shape, -angle);
1✔
443
    // If the shape is taller than it is wide after rotation, we are off by 90 degrees.
1✔
444
    if ((extents[3] - extents[1]) > (extents[2] - extents[0])) {
1!
445
      angle = angle > 0 ? angle - Math.PI / 2 : angle + Math.PI / 2;
7✔
446
    }
7✔
447
    return angle;
7✔
448
  }
7✔
449

7✔
450
  /**
7✔
451
   * Rotate lat/lon to reposition the home point onto 0,0.
7✔
452
   *
7✔
453
   * @param {[number, number]} lonLat - The longitute and latitude of a point.
7✔
454
   *
7✔
455
   * @return {[number, number]} x, y in meters
7✔
456
   */
4✔
457
  static repositionPoint(lonLat, home) {
4✔
458
    const R = 6371 * 1000;   // Earth radius in m
4✔
459
    const circ = 2 * Math.PI * R;  // Circumference
4✔
460
    const phi = 90 - lonLat[1];
4✔
461
    const theta = lonLat[0] - home[0];
4✔
462
    const thetaPrime = home[1] / 180 * Math.PI;
4✔
463
    const x = R * Math.sin(theta / 180 * Math.PI) * Math.sin(phi / 180 * Math.PI);
4✔
464
    const y = R * Math.cos(phi / 180 * Math.PI);
4✔
465
    const z = R * Math.sin(phi / 180 * Math.PI) * Math.cos(theta / 180 * Math.PI);
4✔
466
    const abs = Math.sqrt(z**2 + y**2);
4✔
467
    const arg = Math.atan(y / z) - thetaPrime;
4✔
468

4✔
469
    return [x, Math.sin(arg) * abs];
7✔
470
  }
7✔
471
}
7✔
472
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

© 2025 Coveralls, Inc