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

Beakerboy / OSMBuilding / 14734484374

29 Apr 2025 02:58PM UTC coverage: 53.644% (-0.4%) from 54.091%
14734484374

Pull #84

github

web-flow
Merge 5a219ba1e into 2eb5d502d
Pull Request #84: Make data preprocessing more error-tolerant, rewrite combineWays

93 of 141 branches covered (65.96%)

Branch coverage included in aggregate %.

105 of 150 new or added lines in 4 files covered. (70.0%)

23 existing lines in 2 files now uncovered.

908 of 1725 relevant lines covered (52.64%)

2.99 hits per line

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

85.85
/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
   * @param augmentedNodelist - list of nodes outside bbox
7✔
14
   *
7✔
15
   * @return {THREE.Shape} shape - the shape
7✔
16
   */
7✔
17
  static createShape(way, nodelist, augmentedNodelist = {}) {
7✔
18
    // Initialize objects
6✔
19
    const shape = new Shape();
6✔
20
    var ref;
6✔
21
    var node = [];
6✔
22

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

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

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

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

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

10✔
89
    ways.forEach(w => {
10✔
90
      const firstNodeID = w.querySelector('nd').getAttribute('ref');
22✔
91
      if (wayBegins[firstNodeID]) {
22✔
92
        wayBegins[firstNodeID].push(w);
3✔
93
      } else {
21✔
94
        wayBegins[firstNodeID] = [w];
19✔
95
      }
22✔
96

22✔
97
      const lastNodeID = w.querySelector('nd:last-of-type').getAttribute('ref');
22✔
98
      if (wayEnds[lastNodeID]) {
22✔
99
        wayEnds[lastNodeID].push(w);
3✔
100
      } else {
19✔
101
        wayEnds[lastNodeID] = [w];
19✔
102
      }
10✔
103
    });
10✔
104

10✔
105
    const usedWays = new Set();
10✔
106

22✔
107
    function tryMakeRing(currentRingWays) {
22✔
108
      if (currentRingWays[0].querySelector('nd').getAttribute('ref') ===
22✔
109
          currentRingWays[currentRingWays.length - 1].querySelector('nd:last-of-type').getAttribute('ref')) {
22✔
110
        return currentRingWays;
20✔
111
      }
14✔
112

14✔
113
      const lastWay = currentRingWays[currentRingWays.length - 1];
14✔
114
      const lastNodeID = lastWay.querySelector('nd:last-of-type').getAttribute('ref');
14✔
115
      for (let way of wayBegins[lastNodeID] ?? []) {
22✔
116
        const wayID = way.getAttribute('id');
11✔
117
        if (usedWays.has(wayID)) {
11✔
118
          continue;
11✔
119
        }
10✔
120
        usedWays.add(wayID);
10✔
121
        currentRingWays.push(way);
10✔
122
        if (tryMakeRing(currentRingWays).length) {
10✔
123
          return currentRingWays;
11!
NEW
124
        }
×
NEW
125
        currentRingWays.pop();
×
126
        usedWays.delete(wayID);
21✔
127
      }
4✔
128

4✔
129
      for (let way of wayEnds[lastNodeID] ?? []) {
22!
130
        const wayID = way.getAttribute('id');
6✔
131
        if (usedWays.has(wayID)) {
6✔
132
          continue;
3✔
133
        }
3✔
134
        usedWays.add(wayID);
3✔
135
        currentRingWays.push(BuildingShapeUtils.reverseWay(way));
3✔
136
        if (tryMakeRing(currentRingWays).length) {
3✔
137
          return currentRingWays;
6!
NEW
138
        }
×
NEW
139
        currentRingWays.pop();
×
140
        usedWays.delete(wayID);
19✔
141
      }
1✔
142

1✔
143
      return [];
10✔
144
    }
10✔
145

22✔
146
    ways.forEach(w => {
22✔
147
      const wayID = w.getAttribute('id');
22✔
148
      if (usedWays.has(wayID)){
22✔
149
        return;
20✔
150
      }
9✔
151
      usedWays.add(wayID);
9✔
152
      const result = tryMakeRing([w]);
9✔
153
      if (result.length) {
20✔
154
        let ring = result[0];
8✔
155
        result.slice(1).forEach(w => {
8✔
156
          ring = this.joinWays(ring, w);
8✔
157
        });
8✔
158
        closedWays.push(ring);
10✔
159
      }
10✔
160
    });
10✔
161

10✔
162
    return closedWays;
7✔
163
  }
7✔
164

7✔
165
  /**
7✔
166
   * Append the nodes from one way into another.
7✔
167
   *
7✔
168
   * @param {DOM.Element} way1 - an open, non self-intersecring way
7✔
169
   * @param {DOM.Element} way2
7✔
170
   *
7✔
171
   * @return {DOM.Element} way
7✔
172
   */
14✔
173
  static joinWays(way1, way2) {
14✔
174
    const elements = way2.getElementsByTagName('nd');
14✔
175
    for (let i = 1; i < elements.length; i++) {
14✔
176
      let elem = elements[i].cloneNode();
17✔
177
      way1.appendChild(elem);
14✔
178
    }
7✔
179
    return way1;
7✔
180
  }
7✔
181

7✔
182
  /**
7✔
183
   * Reverse the order of nodes in a way.
7✔
184
   *
7✔
185
   * @param {DOM.Element} way - a way
7✔
186
   *
7✔
187
   * @return {DOM.Element} way
7✔
188
   */
4✔
189
  static reverseWay(way) {
4✔
190
    const elements = way.getElementsByTagName('nd');
4✔
191
    const newWay = way.cloneNode(true);
4✔
192
    newWay.innerHTML = '';
4✔
193
    for (let i = 0; i < elements.length; i++) {
4✔
194
      let elem = elements[elements.length - 1 - i].cloneNode();
10✔
195
      newWay.appendChild(elem);
4✔
196
    }
4✔
197
    return newWay;
7✔
198
  }
7✔
199

7✔
200
  /**
7✔
201
   * Find the center of a closed way
7✔
202
   *
7✔
203
   * @param {THREE.Shape} shape - the shape
7✔
204
   *
7✔
205
   * @return {[number, number]} xy - x/y coordinates of the center
7✔
UNCOV
206
   */
×
UNCOV
207
  static center(shape) {
×
208
    const extents = BuildingShapeUtils.extents(shape);
×
209
    const center = [(extents[0] + extents[2] ) / 2, (extents[1]  + extents[3] ) / 2];
7✔
210
    return center;
7✔
211
  }
7✔
212

7✔
213
  /**
7✔
214
   * Return the longest cardinal side length.
7✔
215
   *
7✔
216
   * @param {THREE.Shape} shape - the shape
7✔
UNCOV
217
   */
×
UNCOV
218
  static getWidth(shape) {
×
219
    const xy = BuildingShapeUtils.combineCoordinates(shape);
×
220
    const x = xy[0];
×
221
    const y = xy[1];
×
222
    return Math.max(Math.max(...x) - Math.min(...x), Math.max(...y) - Math.min(...y));
7✔
223
  }
7✔
224

7✔
225
  /**
7✔
226
   * can points be an array of shapes?
7✔
UNCOV
227
   */
×
UNCOV
228
  static combineCoordinates(shape) {
×
229
    //console.log('Shape: ' + JSON.stringify(shape));
×
230
    const points = shape.extractPoints().shape;
×
231
    var x = [];
×
232
    var y = [];
×
233
    var vec;
×
234
    for (let i = 0; i < points.length; i++) {
×
235
      vec = points[i];
×
236
      x.push(vec.x);
×
237
      y.push(vec.y);
×
238
    }
7✔
239
    return [x, y];
7✔
240
  }
7✔
241

7✔
242
  /**
7✔
243
   * Calculate the Cartesian extents of the shape after rotaing couterclockwise by a given angle.
7✔
244
   *
7✔
245
   * @param {THREE.Shape} pts - the shape or Array of shapes.
7✔
246
   * @param {number} angle - angle in radians to rotate shape
7✔
247
   *
7✔
248
   * @return {[number, number, number, number]} the extents of the object.
7✔
249
   */
8✔
250
  static extents(shape, angle = 0) {
8✔
251
    if (!Array.isArray(shape)) {
8✔
252
      shape = [shape];
8✔
253
    }
8✔
254
    var x = [];
8✔
255
    var y = [];
8✔
256
    var vec;
8✔
257
    for (let i = 0; i < shape.length; i++) {
8✔
258
      const points = shape[i].extractPoints().shape;
7✔
259
      for (let i = 0; i < points.length; i++) {
7✔
260
        vec = points[i];
29✔
261
        x.push(vec.x * Math.cos(angle) - vec.y * Math.sin(angle));
29✔
262
        y.push(vec.x * Math.sin(angle) + vec.y * Math.cos(angle));
8✔
263
      }
8✔
264
    }
8✔
265
    const left = Math.min(...x);
8✔
266
    const bottom = Math.min(...y);
8✔
267
    const right = Math.max(...x);
8✔
268
    const top = Math.max(...y);
8✔
269
    return [left, bottom, right, top];
7✔
270
  }
7✔
271

7✔
272
  /**
7✔
273
   * Assuming the shape is all right angles,
7✔
274
   * Find the orientation of the longest edge.
7✔
UNCOV
275
   */
×
UNCOV
276
  static primaryDirection(shape) {
×
277
    const points = shape.extractPoints().shape;
7✔
278
  }
7✔
279

7✔
280
  /**
7✔
281
   * Calculate the length of each of a shape's edge
7✔
282
   *
7✔
283
   * @param {THREE.Shape} shape - the shape
7✔
284
   *
7✔
285
   * @return {[number, ...]} the esge lwngths.
7✔
286
   */
2✔
287
  static edgeLength(shape) {
2✔
288
    const points = shape.extractPoints().shape;
2✔
289
    const lengths = [];
2✔
290
    var p1;
2✔
291
    var p2;
2✔
292
    for (let i = 0; i < points.length - 1; i++) {
2✔
293
      p1 = points[i];
4✔
294
      p2 = points[i + 1];
4✔
295
      lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
2✔
296
    }
2✔
297
    p1 = points[points.length - 1];
2✔
298
    p2 = points[0];
2✔
299
    lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
7✔
300
    return lengths;
7✔
301
  }
7✔
302

7✔
303
  /**
7✔
304
   * Calculate the angle at each of a shape's vertex
7✔
UNCOV
305
   */
×
UNCOV
306
  static vertexAngle(shape) {
×
307
    const points = shape.extractPoints().shape;
×
308
    const angles = [];
×
309
    var p0;
×
310
    var p1;
×
311
    var p2;
×
312
    p0 = points[points.length];
×
313
    p1 = points[0];
×
314
    p2 = points[1];
×
315
    angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
316
    for (let i = 1; i < points.length - 1; i++) {
×
317
      p0 = points[i-1];
×
318
      p1 = points[i];
×
319
      p2 = points[i + 1];
×
320
      angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
321
    }
×
322
    p0 = points[points.length-1];
×
323
    p1 = points[points.length];
×
324
    p2 = points[0];
×
325
    angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
326
    return angles;
7✔
327
  }
7✔
328

7✔
329
  /**
7✔
330
   * Calculate the angle of each of a shape's edge.
7✔
331
   * the angle will be PI > x >= -PI
7✔
332
   *
7✔
333
   * @param {THREE.Shape} shape - the shape
7✔
334
   *
7✔
335
   * @return {[number, ...]} the angles in radians.
7✔
336
   */
2✔
337
  static edgeDirection(shape) {
2✔
338
    const points = shape.extractPoints().shape;
2✔
339
    points.push(points[0]);
2✔
340
    const angles = [];
2✔
341
    var p1;
2✔
342
    var p2;
2✔
343
    for (let i = 0; i < points.length - 1; i++) {
2✔
344
      p1 = points[i];
6✔
345
      p2 = points[i + 1];
6✔
346
      let angle = Math.atan2((p2.y - p1.y), (p2.x - p1.x));
6✔
347
      if (angle >= Math.PI / 2) {
6✔
348
        angle -= Math.PI;
6✔
349
      } else if (angle < -Math.PI / 2) {
4!
350
        angle += Math.PI;
6✔
351
      }
6✔
352
      angles.push(angle);
2✔
353
    }
7✔
354
    return angles;
7✔
355
  }
7✔
356

7✔
357
  /**
7✔
358
   * Count the number of times that a line horizontal from point intersects shape
7✔
359
   *
7✔
360
   * if an odd number are crossed, it is inside.
7✔
361
   * todo, test holes
7✔
362
   * Test edge conditions.
7✔
UNCOV
363
   */
×
UNCOV
364
  static surrounds(shape, point) {
×
365
    var count = 0;
×
366
    const vecs = shape.extractPoints().shape;
×
367
    var vec;
×
368
    var nextvec;
×
369
    for (let i = 0; i < vecs.length - 1; i++) {
×
370
      vec = vecs[i];
×
371
      nextvec = vecs[i+1];
×
372
      if (vec.x === point[0] && vec.y === point[1]) {
×
373
        return true;
×
374
      }
×
375
      if ((vec.x >= point[0] || nextvec.x >= point[0]) && (vec.y >= point[1] !== nextvec.y >= point[1])) {
×
376
        count++;
×
377
      }
×
378
    }
×
379
    return count % 2 === 1;
7✔
380
  }
7✔
381

7✔
382
  /**
7✔
383
   * Calculate the radius of a circle that can fit within a shape.
7✔
384
   *
7✔
385
   * @param {THREE.Shape} shape - the shape
7✔
UNCOV
386
   */
×
UNCOV
387
  static calculateRadius(shape) {
×
388
    const extents = BuildingShapeUtils.extents(shape);
×
389
    // return half of the shorter side-length.
×
390
    return Math.min(extents[2] - extents[0], extents[3] - extents[1]) / 2;
7✔
391
  }
7✔
392

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

7✔
421
  /**
7✔
422
   * Rotate lat/lon to reposition the home point onto 0,0.
7✔
423
   *
7✔
424
   * @param {[number, number]} lonLat - The longitute and latitude of a point.
7✔
425
   *
7✔
426
   * @return {[number, number]} x, y in meters
7✔
427
   */
4✔
428
  static repositionPoint(lonLat, home) {
4✔
429
    const R = 6371 * 1000;   // Earth radius in m
4✔
430
    const circ = 2 * Math.PI * R;  // Circumference
4✔
431
    const phi = 90 - lonLat[1];
4✔
432
    const theta = lonLat[0] - home[0];
4✔
433
    const thetaPrime = home[1] / 180 * Math.PI;
4✔
434
    const x = R * Math.sin(theta / 180 * Math.PI) * Math.sin(phi / 180 * Math.PI);
4✔
435
    const y = R * Math.cos(phi / 180 * Math.PI);
4✔
436
    const z = R * Math.sin(phi / 180 * Math.PI) * Math.cos(theta / 180 * Math.PI);
4✔
437
    const abs = Math.sqrt(z**2 + y**2);
4✔
438
    const arg = Math.atan(y / z) - thetaPrime;
4✔
439

4✔
440
    return [x, Math.sin(arg) * abs];
7✔
441
  }
7✔
442
}
7✔
443
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