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

Beakerboy / OSMBuilding / 14666537864

25 Apr 2025 02:07PM UTC coverage: 52.199%. First build
14666537864

push

github

web-flow
Update BuildingShapeUtils.js

60 of 104 branches covered (57.69%)

Branch coverage included in aggregate %.

1 of 6 new or added lines in 1 file covered. (16.67%)

842 of 1624 relevant lines covered (51.85%)

2.51 hits per line

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

83.45
/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
   *
6✔
14
   * @return {THREE.Shape} shape - the shape
6✔
15
   */
6✔
16
  static createShape(way, nodelist) {
6✔
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;
6✔
37
  }
6✔
38

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

6✔
52
  /**
6✔
53
   * Walk through an array and seperate any closed ways.
6✔
54
   * Attempt to find matching open ways to enclose them.
6✔
55
   *
6✔
56
   * @param {[DOM.Element]} array - list of OSM XML way elements.
6✔
57
   *
6✔
58
   * @return {DOM.Element}
6✔
59
   */
6✔
60
  static combineWays(ways) {
6✔
61
    var closedWays = [];
7✔
62
    var openWays = [];
7✔
63
    var changed = true;
7✔
64
    while (changed) {
7✔
65
      changed = false;
12✔
66
      for (let i = 0; i < ways.length - 1; i++) {
12✔
67
        if (BuildingShapeUtils.isClosed(ways[i])) {
10✔
68
          closedWays.push(ways[i]);
5✔
69
        } else {
5✔
70
          // These are HTMLCollections of nodes, not ways.
5✔
71
          const way1 = ways[i].getElementsByTagName('nd');
5✔
72
          const way2 = ways[i + 1].getElementsByTagName('nd');
5✔
73

5✔
74
          // If the first node of way2 is the same as the last in way one, they can be combined
5✔
75
          // Or if the first node of way1 is the same as the last in way2
5✔
76
          // Need to extend this to tip-to-tip connections as well.
5✔
77
          // Need to add a "reverse way" function somewhere.
5✔
78
          if (way2[0].getAttribute('ref') === way1[way1.length - 1].getAttribute('ref')) {
5✔
79
            const result = BuildingShapeUtils.joinWays(ways[i], ways[i + 1]);
3✔
80
            openWays.push(result);
3✔
81
            i++;
3✔
82
            changed = true;
3✔
83
          } else if (way1[0].getAttribute('ref') === way2[way2.length - 1].getAttribute('ref')) {
5!
84
            const result = BuildingShapeUtils.joinWays(ways[i + 1], ways[i]);
×
85
            openWays.push(result);
×
86
            i++;
×
87
            changed = true;
×
88
          } else if (way1[way1.length - 1].getAttribute('ref') === way2[way2.length - 1].getAttribute('ref')) {
2✔
89
            const tempway = BuildingShapeUtils.reverseWay(ways[i + 1]);
2✔
90
            const result = BuildingShapeUtils.joinWays(ways[i], tempway);
2✔
91
            openWays.push(result);
2✔
92
            i++;
2✔
93
            changed = true;
2✔
94
          } else if (way1[0].getAttribute('ref') === way2[0].getAttribute('ref')) {
2!
NEW
95
            const tempway = BuildingShapeUtils.reverseWay(ways[i]);
×
NEW
96
            const result = BuildingShapeUtils.joinWays(tempway, ways[i + 1]);
×
NEW
97
            openWays.push(result);
×
NEW
98
            i++;
×
NEW
99
            changed = true;
×
100
          } else {
×
101
            openWays.push(ways[i]);
×
102
          }
5✔
103
        }
10✔
104
      }
12✔
105
      const lastWay = ways[ways.length - 1];
12✔
106
      if (BuildingShapeUtils.isClosed(lastWay)) {
12✔
107
        closedWays.push(lastWay);
2✔
108
      } else {
10✔
109
        openWays.push(lastWay);
10✔
110
      }
12✔
111
      ways = openWays;
12✔
112
      openWays = [];
7✔
113
    }
7✔
114
    return closedWays;
6✔
115
  }
6✔
116

6✔
117
  /**
6✔
118
   * Append the nodes from one way into another.
6✔
119
   *
6✔
120
   * @param {DOM.Element} way1 - an open, non self-intersecring way
6✔
121
   * @param {DOM.Element} way2
6✔
122
   *
6✔
123
   * @return {DOM.Element} way
6✔
124
   */
6✔
125
  static joinWays(way1, way2) {
6✔
126
    const elements = way2.getElementsByTagName('nd');
6✔
127
    for (let i = 1; i < elements.length; i++) {
6✔
128
      let elem = elements[i].cloneNode();
12✔
129
      way1.appendChild(elem);
6✔
130
    }
6✔
131
    return way1;
6✔
132
  }
6✔
133

6✔
134
  /**
6✔
135
   * Reverse the order of nodes in a way.
6✔
136
   *
6✔
137
   * @param {DOM.Element} way - a way
6✔
138
   *
6✔
139
   * @return {DOM.Element} way
6✔
140
   */
6✔
141
  static reverseWay(way) {
6✔
142
    const elements = way.getElementsByTagName('nd');
3✔
143
    const newWay = way.cloneNode(true);
3✔
144
    newWay.innerHTML = '';
3✔
145
    for (let i = 0; i < elements.length; i++) {
3✔
146
      let elem = elements[elements.length - 1 - i].cloneNode();
9✔
147
      newWay.appendChild(elem);
3✔
148
    }
3✔
149
    return newWay;
6✔
150
  }
6✔
151

6✔
152
  /**
6✔
153
   * Find the center of a closed way
6✔
154
   *
6✔
155
   * @param {THREE.Shape} shape - the shape
6✔
156
   *
6✔
157
   * @return {[number, number]} xy - x/y coordinates of the center
6✔
158
   */
6✔
159
  static center(shape) {
6✔
160
    const extents = BuildingShapeUtils.extents(shape);
×
161
    const center = [(extents[0] + extents[2] ) / 2, (extents[1]  + extents[3] ) / 2];
×
162
    return center;
6✔
163
  }
6✔
164

6✔
165
  /**
6✔
166
   * Return the longest cardinal side length.
6✔
167
   *
6✔
168
   * @param {THREE.Shape} shape - the shape
6✔
169
   */
6✔
170
  static getWidth(shape) {
×
171
    const xy = BuildingShapeUtils.combineCoordinates(shape);
×
172
    const x = xy[0];
×
173
    const y = xy[1];
×
174
    return Math.max(Math.max(...x) - Math.min(...x), Math.max(...y) - Math.min(...y));
6✔
175
  }
6✔
176

6✔
177
  /**
6✔
178
   * can points be an array of shapes?
6✔
179
   */
6✔
180
  static combineCoordinates(shape) {
×
181
    //console.log('Shape: ' + JSON.stringify(shape));
×
182
    const points = shape.extractPoints().shape;
×
183
    var x = [];
×
184
    var y = [];
×
185
    var vec;
×
186
    for (let i = 0; i < points.length; i++) {
×
187
      vec = points[i];
×
188
      x.push(vec.x);
×
189
      y.push(vec.y);
×
190
    }
×
191
    return [x, y];
6✔
192
  }
6✔
193

6✔
194
  /**
6✔
195
   * Calculate the Cartesian extents of the shape after rotaing couterclockwise by a given angle.
6✔
196
   *
6✔
197
   * @param {THREE.Shape} pts - the shape or Array of shapes.
6✔
198
   * @param {number} angle - angle in radians to rotate shape
6✔
199
   *
6✔
200
   * @return {[number, number, number, number]} the extents of the object.
6✔
201
   */
6✔
202
  static extents(shape, angle = 0) {
8✔
203
    if (!Array.isArray(shape)) {
8✔
204
      shape = [shape];
8✔
205
    }
8✔
206
    var x = [];
8✔
207
    var y = [];
8✔
208
    var vec;
8✔
209
    for (let i = 0; i < shape.length; i++) {
8✔
210
      const points = shape[i].extractPoints().shape;
8✔
211
      for (let i = 0; i < points.length; i++) {
8✔
212
        vec = points[i];
34✔
213
        x.push(vec.x * Math.cos(angle) - vec.y * Math.sin(angle));
34✔
214
        y.push(vec.x * Math.sin(angle) + vec.y * Math.cos(angle));
8✔
215
      }
8✔
216
    }
8✔
217
    const left = Math.min(...x);
8✔
218
    const bottom = Math.min(...y);
8✔
219
    const right = Math.max(...x);
8✔
220
    const top = Math.max(...y);
8✔
221
    return [left, bottom, right, top];
6✔
222
  }
6✔
223

6✔
224
  /**
6✔
225
   * Assuming the shape is all right angles,
6✔
226
   * Find the orientation of the longest edge.
6✔
227
   */
6✔
228
  static primaryDirection(shape) {
×
229
    const points = shape.extractPoints().shape;
6✔
230
  }
6✔
231

6✔
232
  /**
6✔
233
   * Calculate the length of each of a shape's edge
6✔
234
   *
6✔
235
   * @param {THREE.Shape} shape - the shape
6✔
236
   *
6✔
237
   * @return {[number, ...]} the esge lwngths.
6✔
238
   */
6✔
239
  static edgeLength(shape) {
2✔
240
    const points = shape.extractPoints().shape;
2✔
241
    const lengths = [];
2✔
242
    var p1;
2✔
243
    var p2;
2✔
244
    for (let i = 0; i < points.length - 1; i++) {
2✔
245
      p1 = points[i];
4✔
246
      p2 = points[i + 1];
4✔
247
      lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
2✔
248
    }
2✔
249
    p1 = points[points.length - 1];
2✔
250
    p2 = points[0];
2✔
251
    lengths.push(Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2));
2✔
252
    return lengths;
6✔
253
  }
6✔
254

6✔
255
  /**
6✔
256
   * Calculate the angle at each of a shape's vertex
6✔
257
   */
6✔
258
  static vertexAngle(shape) {
×
259
    const points = shape.extractPoints().shape;
×
260
    const angles = [];
×
261
    var p0;
×
262
    var p1;
×
263
    var p2;
×
264
    p0 = points[points.length];
×
265
    p1 = points[0];
×
266
    p2 = points[1];
×
267
    angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
268
    for (let i = 1; i < points.length - 1; i++) {
×
269
      p0 = points[i-1];
×
270
      p1 = points[i];
×
271
      p2 = points[i + 1];
×
272
      angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
273
    }
×
274
    p0 = points[points.length-1];
×
275
    p1 = points[points.length];
×
276
    p2 = points[0];
×
277
    angles.push(Math.atan((p2.y - p1.y) / (p2.x - p1.x)) - Math.atan((p0.y - p1.y) / (p0.x - p1.x)));
×
278
    return angles;
6✔
279
  }
6✔
280

6✔
281
  /**
6✔
282
   * Calculate the angle of each of a shape's edge.
6✔
283
   * the angle will be PI > x >= -PI
6✔
284
   *
6✔
285
   * @param {THREE.Shape} shape - the shape
6✔
286
   *
6✔
287
   * @return {[number, ...]} the angles in radians.
6✔
288
   */
6✔
289
  static edgeDirection(shape) {
6✔
290
    const points = shape.extractPoints().shape;
2✔
291
    points.push(points[0]);
2✔
292
    const angles = [];
2✔
293
    var p1;
2✔
294
    var p2;
2✔
295
    for (let i = 0; i < points.length - 1; i++) {
2✔
296
      p1 = points[i];
6✔
297
      p2 = points[i + 1];
6✔
298
      let angle = Math.atan2((p2.y - p1.y), (p2.x - p1.x));
6✔
299
      if (angle >= Math.PI / 2) {
6✔
300
        angle -= Math.PI;
6✔
301
      } else if (angle < -Math.PI / 2) {
4!
302
        angle += Math.PI;
6✔
303
      }
6✔
304
      angles.push(angle);
2✔
305
    }
2✔
306
    return angles;
6✔
307
  }
6✔
308

6✔
309
  /**
6✔
310
   * Count the number of times that a line horizontal from point intersects shape
6✔
311
   *
6✔
312
   * if an odd number are crossed, it is inside.
6✔
313
   * todo, test holes
6✔
314
   * Test edge conditions.
6✔
315
   */
6✔
316
  static surrounds(shape, point) {
×
317
    var count = 0;
×
318
    const vecs = shape.extractPoints().shape;
×
319
    var vec;
×
320
    var nextvec;
×
321
    for (let i = 0; i < vecs.length - 1; i++) {
×
322
      vec = vecs[i];
×
323
      nextvec = vecs[i+1];
×
324
      if (vec.x === point[0] && vec.y === point[1]) {
×
325
        return true;
×
326
      }
×
327
      if ((vec.x >= point[0] || nextvec.x >= point[0]) && (vec.y >= point[1] !== nextvec.y >= point[1])) {
×
328
        count++;
×
329
      }
×
330
    }
×
331
    return count % 2 === 1;
6✔
332
  }
6✔
333

6✔
334
  /**
6✔
335
   * Calculate the radius of a circle that can fit within a shape.
6✔
336
   *
6✔
337
   * @param {THREE.Shape} shape - the shape
6✔
338
   */
6✔
339
  static calculateRadius(shape) {
×
340
    const extents = BuildingShapeUtils.extents(shape);
×
341
    // return half of the shorter side-length.
×
342
    return Math.min(extents[2] - extents[0], extents[3] - extents[1]) / 2;
6✔
343
  }
6✔
344

6✔
345
  /**
6✔
346
   * Calculate the angle of the longest side of a shape with 90° vertices.
6✔
347
   * is begining / end duplicated?
6✔
348
   *
6✔
349
   * @param {THREE.Shape} shape - the shape
6✔
350
   * @return {number}
6✔
351
   */
6✔
352
  static longestSideAngle(shape) {
1✔
353
    const vecs = shape.extractPoints().shape;
1✔
354
    const lengths = BuildingShapeUtils.edgeLength(shape);
1✔
355
    const directions = BuildingShapeUtils.edgeDirection(shape);
1✔
356
    var index;
1✔
357
    var maxLength = 0;
1✔
358
    for (let i = 0; i < lengths.length; i++) {
1✔
359
      if (lengths[i] > maxLength) {
3✔
360
        index = i;
2✔
361
        maxLength = lengths[i];
3✔
362
      }
1✔
363
    }
1✔
364
    var angle = directions[index];
1✔
365
    const extents = BuildingShapeUtils.extents(shape, -angle);
1✔
366
    // If the shape is taller than it is wide after rotation, we are off by 90 degrees.
1✔
367
    if ((extents[3] - extents[1]) > (extents[2] - extents[0])) {
1!
368
      angle = angle > 0 ? angle - Math.PI / 2 : angle + Math.PI / 2;
1✔
369
    }
1✔
370
    return angle;
6✔
371
  }
6✔
372

6✔
373
  /**
6✔
374
   * Rotate lat/lon to reposition the home point onto 0,0.
6✔
375
   *
6✔
376
   * @param {[number, number]} lonLat - The longitute and latitude of a point.
6✔
377
   *
6✔
378
   * @return {[number, number]} x, y in meters
6✔
379
   */
6✔
380
  static repositionPoint(lonLat, home) {
4✔
381
    const R = 6371 * 1000;   // Earth radius in m
4✔
382
    const circ = 2 * Math.PI * R;  // Circumference
4✔
383
    const phi = 90 - lonLat[1];
4✔
384
    const theta = lonLat[0] - home[0];
4✔
385
    const thetaPrime = home[1] / 180 * Math.PI;
4✔
386
    const x = R * Math.sin(theta / 180 * Math.PI) * Math.sin(phi / 180 * Math.PI);
4✔
387
    const y = R * Math.cos(phi / 180 * Math.PI);
4✔
388
    const z = R * Math.sin(phi / 180 * Math.PI) * Math.cos(theta / 180 * Math.PI);
4✔
389
    const abs = Math.sqrt(z**2 + y**2);
4✔
390
    const arg = Math.atan(y / z) - thetaPrime;
4✔
391

4✔
392
    return [x, Math.sin(arg) * abs];
6✔
393
  }
6✔
394
}
6✔
395
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