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

Beakerboy / OSMBuilding / 14683887594

26 Apr 2025 06:18PM UTC coverage: 52.649% (+0.4%) from 52.202%
14683887594

Pull #84

github

web-flow
Merge 7a63348b9 into 6e46da80e
Pull Request #84: Make data preprocessing more error-tolerant, rewrite combineWays

78 of 129 branches covered (60.47%)

Branch coverage included in aggregate %.

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

17 existing lines in 2 files now uncovered.

886 of 1702 relevant lines covered (52.06%)

2.31 hits per line

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

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

6✔
6
class BuildingShapeUtils extends ShapeUtils {
6✔
7

6✔
8
  /**
6✔
9
   * Create the shape of this way.
6✔
10
   *
6✔
11
   * @param {DOM.Element} way - OSM XML way element.
6✔
12
   * @param {[number, number]} nodelist - list of all nodes
6✔
13
   * @param augmentedNodelist - list of nodes outside bbox
6✔
14
   *
6✔
15
   * @return {THREE.Shape} shape - the shape
6✔
16
   */
6✔
17
  static createShape(way, nodelist, augmentedNodelist = {}) {
6✔
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;
6✔
38
  }
6✔
39

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

6✔
53
  /**
6✔
54
   * Walk through an array and seperate any closed ways.
6✔
55
   * Attempt to find matching open ways to enclose them.
6✔
56
   *
6✔
57
   * @param {[DOM.Element]} array - list of OSM XML way elements.
6✔
58
   *
6✔
59
   * @return {DOM.Element}
6✔
60
   */
6✔
61
  static combineWays(ways) {
6✔
62
    const closedWays = [];
4✔
63
    const wayBegins = {};
4✔
64
    const wayEnds = {};
4✔
65

4✔
66
    ways.forEach(w => {
4✔
67
      const firstNodeID = w.querySelector('nd').getAttribute('ref');
5✔
68
      if (wayBegins[firstNodeID]) {
5✔
69
        wayBegins[firstNodeID].push(w);
1✔
70
      } else {
4✔
71
        wayBegins[firstNodeID] = [w];
4✔
72
      }
5✔
73

5✔
74
      const lastNodeID = w.querySelector('nd:last-of-type').getAttribute('ref');
5✔
75
      if (wayEnds[lastNodeID]) {
5✔
76
        wayEnds[lastNodeID].push(w);
4✔
77
      } else {
4✔
78
        wayEnds[lastNodeID] = [w];
5✔
79
      }
4✔
80
    });
4✔
81

4✔
82
    const usedWays = new Set();
4✔
83

5✔
84
    function tryMakeRing(currentRingWays) {
5✔
85
      if (currentRingWays[0].querySelector('nd').getAttribute('ref') ===
5✔
86
          currentRingWays[currentRingWays.length - 1].querySelector('nd:last-of-type').getAttribute('ref')) {
5✔
87
        return currentRingWays;
3✔
88
      }
2✔
89

2✔
90
      const lastWay = currentRingWays[currentRingWays.length - 1];
2✔
91
      const lastNodeID = lastWay.querySelector('nd:last-of-type').getAttribute('ref');
2✔
92
      for (let way of wayBegins[lastNodeID] ?? []) {
5✔
93
        const wayID = way.getAttribute('id');
1✔
94
        if (usedWays.has(wayID)) {
1!
95
          continue;
1✔
96
        }
1✔
97
        usedWays.add(wayID);
1✔
98
        currentRingWays.push(way);
1✔
99
        if (tryMakeRing(currentRingWays).length) {
1✔
100
          return currentRingWays;
1!
NEW
101
        }
×
NEW
102
        currentRingWays.pop();
×
103
        usedWays.delete(wayID);
4✔
104
      }
1✔
105

1✔
106
      for (let way of wayEnds[lastNodeID] ?? []) {
5!
107
        const wayID = way.getAttribute('id');
1✔
108
        if (usedWays.has(wayID)) {
1!
109
          continue;
1✔
110
        }
1✔
111
        usedWays.add(wayID);
1✔
112
        currentRingWays.push(BuildingShapeUtils.reverseWay(way));
1✔
113
        if (tryMakeRing(currentRingWays).length) {
1✔
114
          return currentRingWays;
1!
NEW
115
        }
×
NEW
116
        currentRingWays.pop();
×
117
        usedWays.delete(wayID);
2!
118
      }
5✔
119

4✔
120
      return [];
4✔
121
    }
5✔
122

5✔
123
    ways.forEach(w => {
5✔
124
      const wayID = w.getAttribute('ref');
5✔
125
      if (usedWays.has(wayID)){
5✔
126
        return;
3✔
127
      }
3✔
128
      usedWays.add(wayID);
3✔
129
      const result = tryMakeRing([w]);
3✔
130
      if (result.length) {
3✔
131
        let ring = result[0];
3✔
132
        result.slice(1).forEach(w => {
3✔
133
          ring = this.joinWays(ring, w);
3✔
134
        });
3✔
135
        closedWays.push(ring);
4✔
136
      }
4✔
137
    });
4✔
138

4✔
139
    return closedWays;
6✔
140
  }
6✔
141

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

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

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

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

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

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

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

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

6✔
280
  /**
6✔
281
   * Calculate the angle at each of a shape's vertex
6✔
UNCOV
282
   */
×
283
  static vertexAngle(shape) {
×
284
    const points = shape.extractPoints().shape;
×
285
    const angles = [];
×
286
    var p0;
×
287
    var p1;
×
288
    var p2;
×
289
    p0 = points[points.length];
×
290
    p1 = points[0];
×
291
    p2 = points[1];
×
292
    angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
293
    for (let i = 1; i < points.length - 1; i++) {
×
294
      p0 = points[i-1];
×
295
      p1 = points[i];
×
296
      p2 = points[i + 1];
×
297
      angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
298
    }
×
299
    p0 = points[points.length-1];
×
300
    p1 = points[points.length];
×
301
    p2 = points[0];
×
302
    angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
6✔
303
    return angles;
6✔
304
  }
6✔
305

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

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

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

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

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

4✔
417
    return [x, Math.sin(arg) * abs];
6✔
418
  }
6✔
419
}
6✔
420
export {BuildingShapeUtils};
6✔
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