• 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

51.07
/src/building.js
1
import {BuildingShapeUtils} from './extras/BuildingShapeUtils.js';
4✔
2
import {BuildingPart} from './buildingpart.js';
4✔
3
import {MultiBuildingPart} from './multibuildingpart.js';
4✔
4
/**
4✔
5
 * A class representing an OSM building
4✔
6
 *
4✔
7
 * The static factory is responsible for pulling all required
4✔
8
 * XML data from the API.
4✔
9
 */
4✔
10
class Building {
4✔
11
  // Latitude and longitude that transitioned to (0, 0)
1✔
12
  home = [];
1✔
13

1✔
14
  // the parts
1✔
15
  parts = [];
1✔
16

1✔
17
  // the BuildingPart of the outer building parimeter
1✔
18
  outerElement;
1✔
19

1✔
20
  // DOM Tree of all elements to render
1✔
21
  fullXmlData;
1✔
22

1✔
23
  id = '0';
1✔
24

1✔
25
  // the list of all nodes with lat/lon coordinates.
1✔
26
  nodelist = [];
1✔
27

1✔
28
  // The type of building
1✔
29
  type;
1✔
30
  options;
1✔
31

1✔
32
  /**
1✔
33
   * Create new building
1✔
34
   */
1✔
35
  static async create(type, id) {
1✔
36
    var data;
×
37
    if (type === 'way') {
×
38
      data = await Building.getWayData(id);
×
39
    } else {
×
40
      data = await Building.getRelationData(id);
×
41
    }
×
42
    let xmlData = new window.DOMParser().parseFromString(data, 'text/xml');
×
43
    const nodelist = Building.buildNodeList(xmlData);
×
44
    const extents = Building.getExtents(id, xmlData, nodelist);
×
45
    const innerData = await Building.getInnerData(...extents);
×
NEW
46
    const [augmentedNodelist, augmentedWays] = await Building.buildAugmentedData(innerData);
×
NEW
47
    return new Building(id, innerData, augmentedNodelist, augmentedWays);
×
48
  }
×
49

×
50
  /**
1✔
51
   * build an object
1✔
52
   *
1✔
53
   * @param {string} id - the unique XML id of the object.
1✔
54
   * @param {string} FullXmlData - XML data.
1✔
55
   */
1✔
56
  constructor(id, FullXmlData, augmentedNodelist, augmentedWays) {
1✔
57
    this.id = id;
1✔
58
    this.fullXmlData = new window.DOMParser().parseFromString(FullXmlData, 'text/xml');
1✔
59
    this.augmentedNodelist = augmentedNodelist;
1✔
60
    this.augmentedWays = augmentedWays;
1✔
61
    const outerElementXml = this.fullXmlData.getElementById(id);
1✔
62
    if (outerElementXml.tagName.toLowerCase() === 'way') {
1✔
63
      this.type = 'way';
1!
64
    } else if (outerElementXml.querySelector('[k="type"]').getAttribute('v') === 'multipolygon') {
1✔
65
      this.type = 'multipolygon';
1✔
66
    } else {
1!
67
      this.type = 'relation';
×
68
    }
×
69
    if (this.isValidData(outerElementXml)) {
1✔
70
      this.nodelist = Building.buildNodeList(this.fullXmlData);
1✔
71
      this.setHome();
1✔
72
      this.repositionNodes();
1✔
73
      if (this.type === 'way') {
1✔
74
        this.outerElement = new BuildingPart(id, this.fullXmlData, this.nodelist, this.augmentedNodelist, this.augmentedWays);
1!
75
      } else if (this.type === 'multipolygon') {
1✔
76
        this.outerElement = new MultiBuildingPart(id, this.fullXmlData, this.nodelist, this.augmentedNodelist, this.augmentedWays);
1✔
77
      } else {
1!
78
        const outlineRef = outerElementXml.querySelector('member[role="outline"]').getAttribute('ref');
×
79
        const outline = this.fullXmlData.getElementById(outlineRef);
×
80
        const outlineType = outline.tagName.toLowerCase();
×
81
        if (outlineType === 'way') {
×
NEW
82
          this.outerElement = new BuildingPart(id, this.fullXmlData, this.nodelist, this.augmentedNodelist, this.augmentedWays);
×
83
        } else {
×
NEW
84
          this.outerElement = new MultiBuildingPart(outlineRef, this.fullXmlData, this.nodelist, this.augmentedNodelist, this.augmentedWays);
×
85
        }
×
86
      }
×
87
      this.addParts();
1✔
88
    } else {
1!
89
      window.printError('XML Not Valid');
×
90
      throw new Error('invalid XML');
×
91
    }
×
92
  }
×
93

×
94
  /**
1✔
95
   * the Home point is the center of the outer shape
1✔
96
   */
1✔
97
  setHome() {
1✔
98
    const extents = Building.getExtents(this.id, this.fullXmlData, this.nodelist);
1✔
99
    // Set the "home point", the lat lon to center the structure.
1✔
100
    const homeLon = (extents[0] + extents[2]) / 2;
1✔
101
    const homeLat = (extents[1] + extents[3]) / 2;
1✔
102
    this.home = [homeLon, homeLat];
1✔
103
  }
1✔
104

1✔
105
  /**
1✔
106
   * Extract all nodes from an XML file.
1✔
107
   *
1✔
108
   * @param {DOM.Element} fullXmlData - OSM XML with nodes
1✔
109
   *
1✔
110
   * @return {Object} dictionary of nodes
1✔
111
   */
1✔
112
  static buildNodeList(fullXmlData) {
1✔
113
    const nodeElements = fullXmlData.getElementsByTagName('node');
5✔
114
    let id = 0;
5✔
115
    var node;
5✔
116
    let coordinates = [];
5✔
117
    const nodeList = {};
5✔
118
    for (let j = 0; j < nodeElements.length; j++) {
5✔
119
      node = nodeElements[j];
5✔
120
      id = node.getAttribute('id');
26✔
121
      coordinates = [node.getAttribute('lon'), node.getAttribute('lat')];
26✔
122
      nodeList[id] = coordinates;
26✔
123
    }
26✔
124
    return nodeList;
5✔
125
  }
5✔
126

5✔
127

5✔
128
  /**
1✔
129
   * @param {DOM.Element} fullXmlData - OSM XML with nodes
1✔
130
   * @return {Promise<({}|*)[]>}
1✔
131
   */
1✔
132
  static async buildAugmentedData(fullXmlData) {
1✔
NEW
133
    const xmlData = new DOMParser().parseFromString(fullXmlData, 'text/xml');
×
NEW
134
    const completedWays = new Set(Array.from(xmlData.getElementsByTagName('way')).map(i => i.getAttribute('id')));
×
NEW
135
    const memberWays = xmlData.querySelectorAll('member[type="way"]');
×
NEW
136
    const nodeList = {};
×
NEW
137
    const waysList = {};
×
NEW
138
    await Promise.all(Array.from(memberWays).map(async currentWay => {
×
NEW
139
      const wayID = currentWay.getAttribute('ref');
×
NEW
140
      if (completedWays.has(wayID)) {
×
NEW
141
        return;
×
NEW
142
      }
×
NEW
143
      window.printError('Additional downloading way ' + wayID);
×
NEW
144
      const wayData = new DOMParser().parseFromString(await Building.getWayData(wayID), 'text/xml');
×
NEW
145
      window.printError(`Way ${wayID} was downloaded`);
×
NEW
146
      waysList[wayID] = wayData.querySelector('way');
×
NEW
147
      wayData.querySelectorAll('node').forEach(i => {
×
NEW
148
        nodeList[i.getAttribute('id')] = [i.getAttribute('lon'), i.getAttribute('lat')];
×
NEW
149
      });
×
NEW
150
    }));
×
NEW
151
    return [nodeList, waysList];
×
NEW
152
  }
×
NEW
153

×
NEW
154

×
155
  /**
1✔
156
   * convert all the longitude latitude values
1✔
157
   * to meters from the home point.
1✔
158
   */
1✔
159
  repositionNodes() {
1✔
160
    for (const key in this.nodelist) {
1✔
161
      this.nodelist[key] = BuildingShapeUtils.repositionPoint(this.nodelist[key], this.home);
1✔
162
    }
4✔
163
    for (const key in this.augmentedNodelist) {
1✔
164
      this.augmentedNodelist[key] = BuildingShapeUtils.repositionPoint(this.augmentedNodelist[key], this.home);
1!
NEW
165
    }
×
UNCOV
166
  }
×
UNCOV
167

×
168
  /**
1✔
169
   * Create the array of building parts.
1✔
170
   *
1✔
171
   * @return {array} mesh - an array or Three.Mesh objects
1✔
172
   */
1✔
173
  render() {
1✔
174
    const mesh = [];
×
175
    if (this.parts.length > 0) {
×
176
      this.outerElement.options.building.visible = false;
×
177
      mesh.push(...this.outerElement.render());
×
178
      for (let i = 0; i < this.parts.length; i++) {
×
179
        mesh.push(...this.parts[i].render());
×
180
      }
×
181
    } else {
×
182
      const parts = this.outerElement.render();
×
183
      mesh.push(parts[0], parts[1]);
×
184
    }
×
185
    return mesh;
×
186
  }
×
187

×
188
  addParts() {
1✔
189
    if (this.type === 'relation') {
1✔
UNCOV
190
      let parts = this.fullXmlData.getElementById(this.id).querySelectorAll('member[role="part"]');
×
191
      for (let i = 0; i < parts.length; i++) {
×
192
        const ref = parts[i].getAttribute('ref');
×
193
        const part = this.fullXmlData.getElementById(ref);
×
194
        if (part.tagName.toLowerCase() === 'way') {
×
NEW
195
          this.parts.push(new BuildingPart(ref, this.fullXmlData, this.nodelist, this.augmentedNodelist, this.augmentedWays, this.outerElement.options));
×
196
        } else {
×
NEW
197
          this.parts.push(new MultiBuildingPart(ref, this.fullXmlData, this.nodelist, this.augmentedNodelist, this.augmentedWays, this.outerElement.options));
×
198
        }
×
199
      }
×
200
    } else {
1✔
201
      // Filter to all ways
1✔
202
      var parts = this.fullXmlData.getElementsByTagName('way');
1✔
203
      for (let j = 0; j < parts.length; j++) {
1✔
204
        if (parts[j].querySelector('[k="building:part"]')) {
1✔
UNCOV
205
          const id = parts[j].getAttribute('id');
×
NEW
206
          this.parts.push(new BuildingPart(id, this.fullXmlData, this.nodelist, this.augmentedNodelist, this.augmentedWays, this.outerElement.options));
×
207
        }
×
208
      }
1✔
209
      // Filter all relations
1✔
210
      parts = this.fullXmlData.getElementsByTagName('relation');
1✔
211
      for (let i = 0; i < parts.length; i++) {
1✔
212
        if (parts[i].querySelector('[k="building:part"]')) {
1✔
UNCOV
213
          const id = parts[i].getAttribute('id');
×
NEW
214
          try {
×
NEW
215
            this.parts.push(new MultiBuildingPart(id, this.fullXmlData, this.nodelist, this.augmentedNodelist, this.augmentedWays, this.outerElement.options));
×
NEW
216
          } catch (e) {
×
NEW
217
            window.printError(e);
×
NEW
218
          }
×
UNCOV
219
        }
×
220
      }
1✔
221
    }
1✔
222
  }
1✔
223

1✔
224
  /**
1✔
225
   * Fetch way data from OSM
1✔
226
   */
1✔
227
  static async getWayData(id) {
1✔
228
    let restPath = apis.getWay.url(id);
×
229
    let response = await fetch(restPath);
×
230
    let text = await response.text();
×
231
    return text;
×
232
  }
×
233

×
234
  static async getRelationData(id) {
1✔
235
    let restPath = apis.getRelation.url(id);
×
236
    let response = await fetch(restPath);
×
237
    let text = await response.text();
×
238
    return text;
×
239
  }
×
240

×
241
  /**
1✔
242
   * Fetch way data from OSM
1✔
243
   */
1✔
244
  static async getInnerData(left, bottom, right, top) {
1✔
245
    let response = await fetch(apis.bounding.url(left, bottom, right, top));
×
246
    let res = await response.text();
×
247
    return res;
×
248
  }
×
249

×
250
  /**
1✔
251
   * validate that we have the ID of a building way.
1✔
252
   */
1✔
253
  isValidData(xmlData) {
1✔
254
    // Check that it is a building (<tag k="building" v="*"/> exists)
1✔
255
    const buildingType = xmlData.querySelector('[k="building"]');
1✔
256
    const ways = [];
1✔
257
    if (xmlData.tagName === 'relation') {
1✔
258
      // get all building relation parts
1✔
259
      // todo: multipolygon inner and outer roles.
1✔
260
      let parts = xmlData.querySelectorAll('member[role="part"]');
1✔
261
      var ref = 0;
1✔
262
      for (let i = 0; i < parts.length; i++) {
1✔
UNCOV
263
        ref = parts[i].getAttribute('ref');
×
264
        const part = this.fullXmlData.getElementById(ref);
×
265
        if (part) {
×
266
          ways.push(this.fullXmlData.getElementById(ref));
×
267
        } else {
×
268
          window.printError('Part #' + i + '(' + ref + ') is null.');
×
269
        }
×
270
      }
×
271
    } else {
1!
272
      if (!buildingType) {
×
273
        window.printError('Outer way is not a building');
×
274
        return false;
×
275
      }
×
276
      ways.push(xmlData);
×
277
    }
×
278
    for (let i = 0; i < ways.length; i++) {
1✔
UNCOV
279
      const way = ways[i];
×
280
      if (way.tagName.toLowerCase() === 'way') {
×
281
        const nodes = way.getElementsByTagName('nd');
×
282
        if (nodes.length > 0) {
×
283
          // Check that it is a closed way
×
284
          const firstRef = nodes[0].getAttribute('ref');
×
285
          const lastRef = nodes[nodes.length - 1].getAttribute('ref');
×
286
          if (firstRef !== lastRef) {
×
287
            window.printError('Way ' + way.getAttribute('id') + ' is not a closed way. ' + firstRef + ' !== ' + lastRef + '.');
×
288
            return false;
×
289
          }
×
290
        } else {
×
291
          window.printError('Way ' + way.getAttribute('id') + ' has no nodes.');
×
292
          return false;
×
293
        }
×
294
      } else {
×
295
        let parts = way.querySelectorAll('member[role="part"]');
×
296
        var ref = 0;
×
297
        for (let i = 0; i < parts.length; i++) {
×
298
          ref = parts[i].getAttribute('ref');
×
299
          const part = this.fullXmlData.getElementById(ref);
×
300
          if (part) {
×
301
            ways.push(this.fullXmlData.getElementById(ref));
×
302
          } else {
×
303
            window.printError('Part ' + ref + ' is null.');
×
304
          }
×
305
        }
×
306
      }
×
307
    }
×
308
    return true;
1✔
309
  }
1✔
310

1✔
311
  /**
1✔
312
   * Get the extents of the top level building.
1✔
313
   *
1✔
314
   * @param {number} id - The id of the relation or way
1✔
315
   * @param {XML} fulXmlData - A complete <osm> XML file.
1✔
316
   * @param {[number => [number, number]]} nodelist - x/y or lon/lat coordinated keyed by id
1✔
317
   *
1✔
318
   * @param {[number, number, number, number]} extents - [left, bottom, right, top] of the entire building.
1✔
319
   */
1✔
320
  static getExtents(id, fullXmlData, nodelist) {
1✔
321
    const xmlElement = fullXmlData.getElementById(id);
1✔
322
    const buildingType = xmlElement.tagName.toLowerCase();
1✔
323
    var shape;
1✔
324
    var extents = [];
1✔
325
    if (buildingType === 'way') {
1✔
UNCOV
326
      shape = BuildingShapeUtils.createShape(xmlElement, nodelist);
×
327
      extents = BuildingShapeUtils.extents(shape);
×
328
    } else if (buildingType === 'relation'){
1✔
329
      const relationType = xmlElement.querySelector('[k="type"]').getAttribute('v');
1✔
330
      if (relationType === 'multipolygon') {
1✔
331
        let outerMembers = xmlElement.querySelectorAll('member[role="outer"]');
1✔
332
        var shape;
1✔
333
        var way;
1✔
334
        for (let i = 0; i < outerMembers.length; i++) {
1✔
335
          way = fullXmlData.getElementById(outerMembers[i].getAttribute('ref'));
1✔
336
          shape = BuildingShapeUtils.createShape(way, nodelist);
1✔
337
          const wayExtents = BuildingShapeUtils.extents(shape);
1✔
338
          if (i === 0) {
1✔
339
            extents = wayExtents;
1✔
340
          } else {
1!
341
            extents[0] = Math.min(extents[0], wayExtents[0]);
×
342
            extents[1] = Math.min(extents[1], wayExtents[1]);
×
343
            extents[2] = Math.max(extents[2], wayExtents[2]);
×
344
            extents[3] = Math.max(extents[3], wayExtents[3]);
×
345
          }
×
346
        }
1✔
347
      } else {
1!
348
        // In a relation, the overall extents may be larger than the outline.
×
349
        // use the extents of all the provided nodes.
×
350
        extents[0] = 180;
×
351
        extents[1] = 90;
×
352
        extents[2] = -180;
×
353
        extents[3] = -90;
×
354
        for (const key in nodelist) {
×
355
          extents[0] = Math.min(extents[0], nodelist[key][0]);
×
356
          extents[1] = Math.min(extents[1], nodelist[key][1]);
×
357
          extents[2] = Math.max(extents[2], nodelist[key][0]);
×
358
          extents[3] = Math.max(extents[3], nodelist[key][1]);
×
359
        }
×
360
      }
×
361
    } else {
1!
362
      window.printError('"' + buildingType + '" is neither "way" nor "relation". Check that the id is correct.');
×
363
    }
×
364
    return extents;
1✔
365
  }
1✔
366

1✔
367
  getInfo() {
1✔
368
    var partsInfo = [];
×
369
    for (let i = 0; i < this.parts.length; i++) {
×
370
      partsInfo.push(this.parts[i].getInfo());
×
371
    }
×
372
    return {
×
373
      id: this.id,
×
374
      type: this.type,
×
375
      options: this.outerElement.options,
×
376
      parts: partsInfo,
×
377
    };
×
378
  }
×
379

1✔
380
  /**
1✔
381
   * Use the provided options to update and return the geometry
1✔
382
   * of a part.
1✔
383
   */
1✔
384
  getPartGeometry(options) {
1✔
385
    for (let i = 0; i < this.parts.length; i++) {
×
386
      const part = this.parts[i];
×
387
      if (part.id === options.id) {
×
388
        part.updateOptions(options);
×
389
        return part.render();
×
390
      }
×
391
    }
×
392
  }
×
393
}
1✔
394
export {Building};
4✔
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