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

Beakerboy / OSMBuilding / 14866359190

06 May 2025 05:51PM UTC coverage: 67.677% (+0.02%) from 67.66%
14866359190

push

github

web-flow
Visibility (#120)

158 of 215 branches covered (73.49%)

Branch coverage included in aggregate %.

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

1 existing line in 1 file now uncovered.

1182 of 1765 relevant lines covered (66.97%)

6.65 hits per line

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

70.28
/src/buildingpart.js
1
import {
5✔
2
  Color,
5✔
3
  ExtrudeGeometry,
5✔
4
  Shape,
5✔
5
  Mesh,
5✔
6
  MeshLambertMaterial,
5✔
7
  MeshPhysicalMaterial,
5✔
8
  SphereGeometry,
5✔
9
} from 'three';
5✔
10

5✔
11
import {PyramidGeometry} from 'pyramid';
5✔
12
import {RampGeometry} from 'ramp';
5✔
13
import {WedgeGeometry} from 'wedge';
5✔
14
import {BuildingShapeUtils} from './extras/BuildingShapeUtils.js';
5✔
15
/**
5✔
16
 * An OSM Building Part
5✔
17
 *
5✔
18
 * A building part includes a main building and a roof.
5✔
19
 */
7✔
20
class BuildingPart {
7✔
21
  // DOM of the building part way
7✔
22
  way;
7✔
23

7✔
24
  // THREE.Shape of the outline.
7✔
25
  shape;
7✔
26

7✔
27
  // THREE.Mesh of the roof
7✔
28
  roof;
7✔
29

7✔
30
  // array of Cartesian coordinates of every node.
7✔
31
  nodelist = [];
7✔
32

7✔
33
  // Metadata of the building part.
7✔
34
  blankOptions = {
7✔
35
    building: {
7✔
36
      colour: null,
7✔
37
      ele: null,
7✔
38
      height: null,
7✔
39
      levels: null,
7✔
40
      levelsUnderground: null,
7✔
41
      material: null,
7✔
42
      minHeight: null,
7✔
43
      minLevel: null,
7✔
44
      walls: null,
7✔
45
    },
7✔
46
    roof: {
7✔
47
      angle: null,
7✔
48
      colour: null,
7✔
49
      direction: null,
7✔
50
      height: null,
7✔
51
      levels: null,
7✔
52
      material: null,
7✔
53
      orientation: null,
7✔
54
      shape: null,
7✔
55
    },
7✔
56
  };
7✔
57

7✔
58
  fullXmlData;
7✔
59

7✔
60
  // The unique OSM ID of the object.
7✔
61
  id;
7✔
62

7✔
63
  // THREE.Mesh
7✔
64
  parts = [];
7✔
65
  /**
7✔
66
   * @param {number} id - the OSM id of the way or multipolygon.
7✔
67
   * @param {XMLDocument} fullXmlData - XML for the region.
7✔
68
   * @param {[[number, number],...]} nodelist - Cartesian coordinates of each node keyed by node refID
7✔
69
   * @param {object} options - default values for the building part.
7✔
70
   */
7✔
71
  constructor(id, fullXmlData, nodelist, defaultOptions = {}) {
7✔
72
    this.options = this.blankOptions;
7✔
73
    if (Object.keys(defaultOptions).length === 0) {
7✔
74
      defaultOptions = this.blankOptions;
7✔
75
    }
7✔
76
    this.options.inherited = defaultOptions;
7✔
77
    this.fullXmlData = fullXmlData;
7✔
78
    this.id = id;
7✔
79
    this.way = fullXmlData.getElementById(id);
7✔
80
    this.nodelist = nodelist;
7✔
81
    this.shape = this.buildShape();
7✔
82
    this.setOptions();
7✔
83
  }
7✔
84

7✔
85
  /**
7✔
86
   * Create the shape of the outer way.
7✔
87
   *
7✔
88
   * @return {THREE.Shape} shape - the shape
7✔
89
   */
4✔
90
  buildShape() {
4✔
91
    this.type = 'way';
4✔
92
    return BuildingShapeUtils.createShape(this.way, this.nodelist);
7✔
93
  }
7✔
94

7✔
95
  /**
7✔
96
   * Set the object's options
7✔
97
   */
7✔
98
  setOptions() {
7✔
99
    // if values are not set directly, inherit from the parent.
7✔
100
    // Somme require more extensive calculation.
7✔
101
    const specifiedOptions = this.blankOptions;
7✔
102

7✔
103
    specifiedOptions.building.colour = this.getAttribute('colour');
7✔
104
    specifiedOptions.building.ele = this.getAttribute('ele');
7✔
105
    specifiedOptions.building.height = BuildingPart.normalizeLength(this.getAttribute('height'));
7✔
106
    specifiedOptions.building.levels = BuildingPart.normalizeNumber(this.getAttribute('building:levels'));
7✔
107
    specifiedOptions.building.levelsUnderground = this.getAttribute('building:levels:underground');
7✔
108
    specifiedOptions.building.material = this.getAttribute('building:material');
7✔
109
    specifiedOptions.building.minHeight = BuildingPart.normalizeLength(this.getAttribute('min_height'));
7✔
110
    specifiedOptions.building.minLevel = this.getAttribute('building:min_level');
7✔
111
    specifiedOptions.building.walls = this.getAttribute('walls');
7✔
112
    specifiedOptions.roof.angle = this.getAttribute('roof:angle');
7✔
113
    specifiedOptions.roof.colour = this.getAttribute('roof:colour');
7✔
114
    specifiedOptions.roof.direction = BuildingPart.normalizeDirection(this.getAttribute('roof:direction'));
7✔
115
    specifiedOptions.roof.height = BuildingPart.normalizeLength(this.getAttribute('roof:height'));
7✔
116
    specifiedOptions.roof.levels = this.getAttribute('roof:levels') ? parseFloat(this.getAttribute('roof:levels')) : undefined;
7✔
117
    specifiedOptions.roof.material = this.getAttribute('roof:material');
7✔
118
    specifiedOptions.roof.orientation = this.getAttribute('roof:orientation');
7✔
119
    specifiedOptions.roof.shape = this.getAttribute('roof:shape');
7✔
120

7✔
121
    this.options.specified = specifiedOptions;
7✔
122

7✔
123
    const calculatedOptions = this.blankOptions;
7✔
124
    // todo replace with some sort of foreach loop.
7✔
125
    calculatedOptions.building.colour = this.options.specified.building.colour ?? this.options.inherited.building.colour;
7✔
126
    calculatedOptions.building.ele = this.options.specified.building.ele ?? this.options.inherited.building.ele ?? 0;
7✔
127
    calculatedOptions.building.levels = this.options.specified.building.levels ?? this.options.inherited.building.levels;
7✔
128
    calculatedOptions.building.levelsUnderground = this.options.specified.building.levelsUnderground ?? this.options.inherited.building.levelsUnderground;
7✔
129
    calculatedOptions.building.material = this.options.specified.building.material ?? this.options.inherited.building.material;
7✔
130
    calculatedOptions.building.minLevel = this.options.specified.building.minLevel ?? this.options.inherited.building.minLevel;
7✔
131
    calculatedOptions.building.minHeight = this.options.specified.building.minHeight ?? this.options.inherited.building.minHeight ?? 0;
7✔
132
    calculatedOptions.building.walls = this.options.specified.building.walls ?? this.options.inherited.building.walls;
7✔
133
    calculatedOptions.roof.angle = this.options.specified.roof.angle ?? this.options.inherited.roof.angle;
7✔
134
    calculatedOptions.roof.colour = this.options.specified.roof.colour ?? this.options.inherited.roof.colour;
7✔
135

7✔
136
    calculatedOptions.roof.levels = this.options.specified.roof.levels ?? this.options.inherited.roof.levels;
7✔
137
    calculatedOptions.roof.material = this.options.specified.roof.material ?? this.options.inherited.roof.material;
7✔
138
    calculatedOptions.roof.orientation = this.options.specified.roof.orientation ?? this.options.inherited.roof.orientation;
7✔
139
    calculatedOptions.roof.shape = this.options.specified.roof.shape ?? this.options.inherited.roof.shape;
7!
140
    calculatedOptions.roof.visible = true;
7✔
141

7✔
142
    // Set the default orientation if the roof shape dictates one.
7✔
143
    const orientableRoofs = ['gabled', 'round'];
7✔
144
    if (!calculatedOptions.roof.orientation && calculatedOptions.roof.shape && orientableRoofs.includes(calculatedOptions.roof.shape)) {
7✔
145
      calculatedOptions.roof.orientation = 'along';
7✔
146
    }
7✔
147
    // Should Skillion be included here?
7✔
148
    const directionalRoofs = ['gabled', 'round'];
7✔
149
    calculatedOptions.roof.direction = this.options.specified.roof.direction ?? this.options.inherited.roof.direction;
7✔
150
    if (calculatedOptions.roof.direction === undefined && directionalRoofs.includes(calculatedOptions.roof.shape)) {
7✔
151
      let longestSide = BuildingShapeUtils.longestSideAngle(this.shape);
4✔
152

4✔
153
      // Convert to angle.
4✔
154
      calculatedOptions.roof.direction = (BuildingPart.atanRadToCompassDeg(longestSide) + 90) % 360;
7✔
155
    }
7✔
156
    const extents = BuildingShapeUtils.extents(this.shape, calculatedOptions.roof.direction / 360 * 2 * Math.PI);
7✔
157
    const shapeHeight = extents[3] - extents[1];
7✔
158
    calculatedOptions.roof.height = this.options.specified.roof.height ??
7✔
159
      this.options.inherited.roof.height ??
7✔
160
      (isNaN(calculatedOptions.roof.levels) ? null : (calculatedOptions.roof.levels * 3)) ??
7✔
161
      (calculatedOptions.roof.shape === 'flat' ? 0 : null) ??
7!
162
      (calculatedOptions.roof.shape === 'dome' || calculatedOptions.roof.shape === 'pyramidal' ? BuildingShapeUtils.calculateRadius(this.shape) : null) ??
7!
163
      (calculatedOptions.roof.shape === 'onion' ? BuildingShapeUtils.calculateRadius(this.shape) * 1.5 : null) ??
7!
164
      (calculatedOptions.roof.shape === 'skillion' ? (calculatedOptions.roof.angle ? Math.cos(calculatedOptions.roof.angle / 360 * 2 * Math.PI) * shapeHeight : 22.5) : null);
7!
165

7✔
166
    calculatedOptions.building.height = this.options.specified.building.height ??
7✔
167
      (isNaN(calculatedOptions.building.levels) ? null : (calculatedOptions.building.levels * 3) + calculatedOptions.roof.height) ??
7!
168
      calculatedOptions.roof.height + 3 ??
7✔
169
      this.options.inherited.building.height;
7✔
170
    this.options.building = calculatedOptions.building;
7✔
171
    this.options.roof = calculatedOptions.roof;
7!
172
    if (this.getAttribute('building:part') && this.options.building.height > this.options.inherited.building.height) {
7!
173
      window.printError('Way ' + this.id + ' is taller than building. (' + this.options.building.height + '>' + this.options.inherited.building.height + ')');
7✔
174
    }
7✔
175
    // Should skillion automatically calculate a direction perpendicular to the longest outside edge if unspecified?
7✔
176
    if (this.options.roof.shape === 'skillion' && this.options.roof.direction === undefined) {
7!
177
      window.printError('Part ' + this.id + ' requires a direction. (https://wiki.openstreetmap.org/wiki/Key:roof:direction)');
7✔
178
    }
7✔
179
    this.extrusionHeight = this.options.building.height - this.options.building.minHeight - this.options.roof.height;
7✔
180
  }
×
181

×
182
  /**
×
183
   * calculate the maximum building width in meters.
×
184
   */
7✔
185
  getWidth() {
7✔
186
    return BuildingShapeUtils.getWidth(this.shape);
7✔
187
  }
3✔
188

3✔
189
  /**
3✔
190
   * Render the building part
3✔
191
   */
3✔
192
  render() {
3✔
193
    this.createRoof();
3✔
194
    this.parts.push(this.roof);
3✔
195
    const mesh = this.createBuilding();
3!
UNCOV
196
    if (this.getAttribute('building:part') === 'roof') {
×
197
      mesh.visible = false;
3✔
198
      this.options.building.visible = false;
7✔
199
    }
3✔
200
    this.parts.push(mesh);
3✔
201
    return this.parts;
3✔
202
  }
3✔
203

3✔
204
  createBuilding() {
3✔
205
    let extrusionHeight = this.options.building.height - this.options.building.minHeight - this.options.roof.height;
3✔
206

3✔
207
    let extrudeSettings = {
3✔
208
      bevelEnabled: false,
3✔
209
      depth: extrusionHeight,
3✔
210
    };
3✔
211

3✔
212
    var geometry = new ExtrudeGeometry(this.shape, extrudeSettings);
3✔
213

3✔
214
    // Create the mesh.
3✔
215
    var mesh = new Mesh(geometry, [BuildingPart.getRoofMaterial(this.way), BuildingPart.getMaterial(this.way)]);
3✔
216

3✔
217
    // Change the position to compensate for the min_height
3✔
218
    mesh.rotation.x = -Math.PI / 2;
3✔
219
    mesh.position.set( 0, this.options.building.minHeight, 0);
7✔
220
    mesh.name = 'b' + this.id;
7✔
221
    return mesh;
3✔
222
  }
3✔
223

3✔
224
  /**
3✔
225
   * Create the 3D render of a roof.
3✔
226
   */
3✔
227
  createRoof() {
3✔
228
    var way = this.way;
3✔
229
    var material;
3✔
230
    var roof;
3✔
231
    if (this.options.roof.shape === 'dome' || this.options.roof.shape === 'onion') {
3!
232
    //   find largest circle within the way
×
233
    //   R, x, y
×
234
      var thetaStart = Math.PI / 2;
×
235
      const R = BuildingShapeUtils.calculateRadius(this.shape);
×
236
      var scale = this.options.roof.height / R;
×
237
      if (this.options.roof.shape === 'onion') {
×
238
        thetaStart = Math.PI / 4;
×
239
        scale = scale / 1.5;
×
240
      }
×
241
      const geometry = new SphereGeometry(R, 100, 100, 0, 2 * Math.PI, thetaStart);
×
242
      // Adjust the dome height if needed.
×
243
      geometry.scale(1, scale, 1);
×
244
      material = BuildingPart.getRoofMaterial(this.way);
×
245
      roof = new Mesh(geometry, material);
×
246
      const elevation = this.options.building.height - this.options.roof.height;
×
247
      const center = BuildingShapeUtils.center(this.shape);
×
248
      roof.rotation.x = -Math.PI;
×
249
      // TODO: onion probably need to be raised by an additional R/2.
3✔
250
      roof.position.set(center[0], elevation, -1 * center[1]);
3!
251
    } else if (this.options.roof.shape === 'skillion') {
×
252
      const options = {
×
253
        angle: (360 - this.options.roof.direction) / 360 * 2 * Math.PI,
×
254
        depth: this.options.roof.height,
×
255
        pitch: this.options.roof.angle / 180 * Math.PI,
×
256
      };
×
257
      const geometry = new RampGeometry(this.shape, options);
×
258

×
259
      material = BuildingPart.getRoofMaterial(this.way);
×
260
      roof = new Mesh( geometry, material );
×
261
      roof.rotation.x = -Math.PI / 2;
×
262
      roof.position.set( 0, this.options.building.height - this.options.roof.height, 0);
3✔
263
    } else if (this.options.roof.shape === 'gabled') {
3✔
264
      var angle = this.options.roof.direction;
3!
265
      if (this.options.roof.orientation === 'across') {
3✔
266
        angle = (angle + 90) % 360;
3✔
267
      }
3✔
268
      const center = BuildingShapeUtils.center(this.shape, angle / 180 * Math.PI);
3✔
269
      const options = {
3✔
270
        center: center,
3✔
271
        angle: angle / 180 * Math.PI,
3✔
272
        depth: this.options.roof.height,
3✔
273
      };
3✔
274
      const geometry = new WedgeGeometry(this.shape, options);
3✔
275

3✔
276
      material = BuildingPart.getRoofMaterial(this.way);
3✔
277
      roof = new Mesh(geometry, material);
3✔
278
      roof.rotation.x = -Math.PI / 2;
3✔
279
      roof.position.set(0, this.options.building.height - this.options.roof.height, 0);
3!
280
    } else if (this.options.roof.shape === 'pyramidal') {
×
281
      const center = BuildingShapeUtils.center(this.shape);
×
282
      const options = {
×
283
        center: center,
×
284
        depth: this.options.roof.height,
×
285
      };
×
286
      const geometry = new PyramidGeometry(this.shape, options);
×
287

×
288
      material = BuildingPart.getRoofMaterial(this.way);
×
289
      roof = new Mesh( geometry, material );
×
290
      roof.rotation.x = -Math.PI / 2;
×
291
      roof.position.set( 0, this.options.building.height - this.options.roof.height, 0);
×
292
    } else {
×
293
      let extrusionHeight = this.options.roof.height ?? 0;
×
294
      let extrudeSettings = {
×
295
        bevelEnabled: false,
×
296
        depth: extrusionHeight,
×
297
      };
×
298
      var geometry = new ExtrudeGeometry(this.shape, extrudeSettings);
×
299
      // Create the mesh.
×
300
      roof = new Mesh(geometry, [BuildingPart.getRoofMaterial(this.way), BuildingPart.getMaterial(this.way)]);
×
301
      roof.rotation.x = -Math.PI / 2;
×
302
      roof.position.set(0, this.options.building.height - this.options.roof.height, 0);
×
303
      if (this.options.roof.shape !== 'flat') {
×
304
        window.printError('Unknown roof shape on '+ this.id + ': '+ this.options.roof.shape);
7✔
305
      }
133✔
306
    }
133✔
307
    roof.name = 'r' + this.id;
133✔
308
    this.roof = roof;
133✔
309
  }
25✔
310

25✔
311
  getAttribute(key) {
25✔
312
    if (this.way.querySelector('[k="' + key + '"]') !== null) {
25✔
313
      // if the buiilding part has a helght tag, use it.
7✔
314
      return this.way.querySelector('[k="' + key + '"]').getAttribute('v');
7✔
315
    }
×
316
  }
×
317

×
318
  /**
×
319
   * The full height of the part in meters, roof and building.
×
320
   */
×
321
  calculateHeight() {
×
322
    var height = 3;
×
323

×
324
    if (this.way.querySelector('[k="height"]') !== null) {
×
325
      // if the buiilding part has a helght tag, use it.
×
326
      height = this.way.querySelector('[k="height"]').getAttribute('v');
×
327
    } else if (this.way.querySelector('[k="building:levels"]') !== null) {
×
328
      // if not, use building:levels and 3 meters per level.
×
329
      height = 3 * this.way.querySelector('[k="building:levels"]').getAttribute('v') + this.options.roof.height;
×
330
    } else if (this.way.querySelector('[k="building:part"]') !== null) {
×
331
      if (this.way.querySelector('[k="building:part"]').getAttribute('v') === 'roof') {
×
332
        // a roof has no building part by default.
×
333
        height = this.options.roof.height;
7✔
334
      }
7✔
335
    }
7✔
336

7✔
337
    return BuildingPart.normalizeLength(height);
7✔
338
  }
7✔
339

7✔
340
  /**
7✔
341
   * Convert an string of length units in various format to
7✔
342
   * a float in meters.
7✔
343
   *
7✔
344
   * Assuming string ends with the unit, no trailing whitespace.
7✔
345
   * If there is whitespace between the unit and the number, it will remain.
21✔
346
   */
21✔
347
  static normalizeLength(length) {
21!
348
    if (typeof length === 'string' || length instanceof String) {
×
349
      if (length.includes('km')){
×
350
        // remove final character.
×
351
        return parseFloat(length.substring(0, length.length - 2)) * 1000;
×
352
      }
×
353
      if (length.includes('mi')){
×
354
        // remove final character.
×
355
        return parseFloat(length.substring(0, length.length - 2)) * 5280 * 12 * 2.54 / 100;
×
356
      }
×
357
      if (length.includes('nmi')){
×
358
        // remove final character.
×
359
        return parseFloat(length.substring(0, length.length - 3)) * 1852;
×
360
      }
×
361
      if (length.includes('m')){
×
362
        // remove final character.
×
363
        return parseFloat(length.substring(0, length.length - 1));
×
364
      }
×
365
      if (length.includes('\'')){
×
366
        window.printError('Length includes a single quote.');
×
367
        var position = length.indexOf('\'');
×
368
        var inches = parseFloat(length.substring(0, position)) * 12;
×
369
        if (length.length > position + 1) {
×
370
          inches += parseFloat(length.substring(position + 1, length.length - 1));
×
371
        }
×
372
        return inches * 2.54 / 100;
×
373
      }
×
374
      if (length.includes('"')){
×
375
        return parseFloat(length.substring(0, length.length - 1))* 2.54 / 100;
21!
376
      }
×
377
      return parseFloat(length);
7✔
378
    }
7✔
379
    if (length) {
7✔
380
      return parseFloat(length);
7✔
381
    }
7✔
382
  }
7✔
383

7✔
384
  /**
7✔
385
   * Direction. In degrees, 0-360
7✔
386
   */
7✔
387
  static normalizeDirection(direction) {
7✔
388
    const degrees = this.cardinalToDegree(direction);
7!
389
    if (degrees !== undefined) {
3✔
390
      return degrees;
7✔
391
    }
7✔
392
    if (direction) {
7✔
393
      return parseFloat(direction);
7✔
394
    }
7✔
395
  }
4✔
396

4✔
397
  /**
4✔
398
   * Number.
4✔
399
   */
4✔
400
  static normalizeNumber(number) {
7✔
401
    if (number) {
7✔
402
      return parseFloat(number);
7✔
403
    }
7✔
404
  }
7✔
405

7✔
406
  /**
7✔
407
   * Convert a cardinal direction to degrees.
7✔
408
   * North is zero and values increase clockwise.
7✔
409
   *
7✔
410
   * @param {string} cardinal - the direction.
7✔
411
   *
10✔
412
   * @return {int} degrees
10✔
413
   */
10✔
414
  static cardinalToDegree(cardinal) {
10✔
415
    const cardinalUpperCase = `${cardinal}`.toUpperCase();
10✔
416
    const index = 'N NNE NE ENE E ESE SE SSE S SSW SW WSW W WNW NW NNW'.split(' ').indexOf(cardinalUpperCase);
10✔
417
    if (index === -1) {
2✔
418
      return undefined;
2✔
419
    }
2✔
420
    const degreesTimesTwo = index * 45;
10✔
421
    // integer floor
1✔
422
    return degreesTimesTwo % 2 === 0 ? degreesTimesTwo / 2 : (degreesTimesTwo - 1) / 2;
7✔
423
  }
7✔
424

7✔
425
  /**
7✔
426
   * OSM compass degrees are 0-360 clockwise.
7✔
427
   *
6✔
428
   * @return {number} degrees
6✔
429
   */
6✔
430
  static atanRadToCompassDeg(rad) {
7✔
431
    return ((Math.PI - rad + 3 * Math.PI / 2) % (2 * Math.PI)) * 180 / Math.PI;
7✔
432
  }
7✔
433

7✔
434
  /**
7✔
435
   * Get the THREE.material for a given way
7✔
436
   *
9✔
437
   * This is complicated by inheritance
9✔
438
   */
9✔
439
  static getMaterial(way) {
9✔
440
    var materialName = '';
9✔
441
    var color = '';
9!
442
    if (way.querySelector('[k="building:facade:material"]') !== null) {
×
443
      // if the buiilding part has a designated material tag, use it.
×
444
      materialName = way.querySelector('[k="building:facade:material"]').getAttribute('v');
9!
445
    } else if (way.querySelector('[k="building:material"]') !== null) {
×
446
      // if the buiilding part has a designated material tag, use it.
×
447
      materialName = way.querySelector('[k="building:material"]').getAttribute('v');
9!
448
    }
×
449
    if (way.querySelector('[k="colour"]') !== null) {
×
450
      // if the buiilding part has a designated colour tag, use it.
9✔
451
      color = way.querySelector('[k="colour"]').getAttribute('v');
9!
452
    } else if (way.querySelector('[k="building:colour"]') !== null) {
×
453
      // if the buiilding part has a designated colour tag, use it.
9✔
454
      color = way.querySelector('[k="building:colour"]').getAttribute('v');
9!
455
    } else if (way.querySelector('[k="building:facade:colour"]') !== null) {
×
456
      // if the buiilding part has a designated colour tag, use it.
×
457
      color = way.querySelector('[k="building:facade:colour"]').getAttribute('v');
9✔
458
    }
9✔
459
    const material = BuildingPart.getBaseMaterial(materialName);
9!
460
    if (color !== '') {
9✔
461
      material.color = new Color(color);
9✔
462
    } else if (materialName === ''){
7✔
463
      material.color = new Color('white');
7✔
464
    }
7✔
465
    return material;
7✔
466
  }
7✔
467

7✔
468
  /**
7✔
469
   * Get the THREE.material for a given way
7✔
470
   *
6✔
471
   * This is complicated by inheritance
6✔
472
   */
6✔
473
  static getRoofMaterial(way) {
6✔
474
    var materialName = '';
6!
475
    var color = '';
×
476
    if (way.querySelector('[k="roof:material"]') !== null) {
×
477
      // if the buiilding part has a designated material tag, use it.
×
478
      materialName = way.querySelector('[k="roof:material"]').getAttribute('v');
6!
479
    }
×
480
    if (way.querySelector('[k="roof:colour"]') !== null) {
×
481
      // if the buiilding part has a designated mroof:colour tag, use it.
6✔
482
      color = way.querySelector('[k="roof:colour"]').getAttribute('v');
6✔
483
    }
6✔
484
    var material;
6!
485
    if (materialName === '') {
×
486
      material = BuildingPart.getMaterial(way);
6✔
487
    } else {
6✔
488
      material = BuildingPart.getBaseMaterial(materialName);
6!
489
    }
6✔
490
    if (color !== '') {
7✔
491
      material.color = new Color(color);
9✔
492
    }
9✔
493
    return material;
9✔
494
  }
9✔
495

9✔
496
  static getBaseMaterial(materialName) {
9!
497
    var material;
×
498
    if (materialName === 'glass') {
×
499
      material = new MeshPhysicalMaterial( {
×
500
        color: 0x00374a,
×
501
        emissive: 0x011d57,
9✔
502
        reflectivity: 0.1409,
9!
503
        clearcoat: 1,
×
504
      } );
×
505
    } else if (materialName === 'grass'){
×
506
      material = new MeshLambertMaterial({
9✔
507
        color: 0x7ec850,
9!
508
        emissive: 0x000000,
×
509
      });
×
510
    } else if (materialName === 'bronze') {
×
511
      material = new MeshPhysicalMaterial({
×
512
        color:0xcd7f32,
×
513
        emissive: 0x000000,
9✔
514
        metalness: 1,
9!
515
        roughness: 0.127,
×
516
      });
×
517
    } else if (materialName === 'copper') {
×
518
      material = new MeshLambertMaterial({
×
519
        color: 0xa1c7b6,
9✔
520
        emissive: 0x00000,
9✔
521
        reflectivity: 0,
9!
522
      });
×
523
    } else if (materialName === 'stainless_steel' || materialName === 'metal') {
×
524
      material = new MeshPhysicalMaterial({
×
525
        color: 0xaaaaaa,
×
526
        emissive: 0xaaaaaa,
9✔
527
        metalness: 1,
9!
528
        roughness: 0.127,
×
529
      });
×
530
    } else if (materialName === 'brick'){
×
531
      material = new MeshLambertMaterial({
9✔
532
        color: 0xcb4154,
9!
533
        emissive: 0x1111111,
×
534
      });
×
535
    } else if (materialName === 'concrete'){
×
536
      material = new MeshLambertMaterial({
9✔
537
        color: 0x555555,
9!
538
        emissive: 0x1111111,
×
539
      });
×
540
    } else if (materialName === 'marble') {
×
541
      material = new MeshLambertMaterial({
9✔
542
        color: 0xffffff,
9✔
543
        emissive: 0x1111111,
9✔
544
      });
9✔
545
    } else {
9✔
546
      material = new MeshLambertMaterial({
7✔
547
        emissive: 0x1111111,
×
548
      });
×
549
    }
×
550
    return material;
×
551
  }
×
552

×
553
  getInfo() {
×
554
    return {
×
555
      id: this.id,
7✔
556
      type: this.type,
×
557
      options: this.options,
5✔
558
      parts: [
5✔
559
      ],
5✔
560
    };
5✔
561
  }
5✔
562

5✔
563
  updateOptions(options) {
5✔
564
    this.options = options;
5✔
565
  }
5✔
566
}
5✔
567
export {BuildingPart};
5✔
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