Skip to content

Commit 7e36c57

Browse files
Beakerboydeevroman
andauthored
Update from main (#133)
* Split data downloading and building creating (#118) * Outer building visibility (#119) * Visibility (#120) * Update building.js (#121) * Tests for API (#122) * check HTTP code status and show alert with error * tests for API errors * Show validation errors (#123) * show validation errors + tests * fix typo * Skip incompleted ways, skip non-way members, prevent global modification of way object (#100) * skip non-way members, skip incompleted ways, prevent global modification of Document with way * add test * better colors for MeshPhysicalMaterial (#126) * Fix crash when processing type=building with outline being a multipolygon (#124) * #88 initial support type=building with multipolygon outline * support multiple ways in inner rings * add test * Hipped roof (#128) * Update BuildingShapeUtils.js (#129) * Update BuildingShapeUtils.js (#130) * Update utils.test.js (#132) --------- Co-authored-by: Roman Deev <roman.deev06@gmail.com>
1 parent bd59df6 commit 7e36c57

14 files changed

+434
-106
lines changed

.github/workflows/main.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ jobs:
1010
mkdir ../pyramid
1111
mkdir ../ramp
1212
mkdir ../wedge
13+
mkdir ../hipped
1314
wget -P ../pyramid https://beakerboy.github.io/Threejs-Geometries/src/PyramidGeometry.js
1415
wget -P ../ramp https://beakerboy.github.io/Threejs-Geometries/src/RampGeometry.js
1516
wget -P ../wedge https://beakerboy.github.io/Threejs-Geometries/src/WedgeGeometry.js
17+
wget -P ../hipped https://beakerboy.github.io/Threejs-Geometries/src/HippedGeometry.js
1618
cd ../pyramid
1719
echo '{"name":"pyramid","type":"module","private":true,"scripts":{"test":"npx jest"}}' > "./package.json" && npm init -y
1820
cd ../ramp
1921
echo '{"name":"ramp","type":"module","private":true,"scripts":{"test":"npx jest"}}' > "./package.json" && npm init -y
2022
cd ../wedge
2123
echo '{"name":"wedge","type":"module","private":true,"scripts":{"test":"npx jest"}}' > "./package.json" && npm init -y
24+
cd ../hipped
25+
echo '{"name":"hipped","type":"module","private":true,"scripts":{"test":"npx jest"}}' > "./package.json" && npm init -y
2226
cd ../OSMBuilding
2327
yarn --prod=false
2428
- name: Lint

index.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
{
1717
"imports": {
1818
"three": "https://unpkg.com/three/build/three.module.js",
19+
"straight-skeleton": "https://cdn.skypack.dev/straight-skeleton@1.1.0",
1920
"pyramid": "https://beakerboy.github.io/Threejs-Geometries/src/PyramidGeometry.js",
2021
"ramp": "https://beakerboy.github.io/Threejs-Geometries/src/RampGeometry.js",
21-
"wedge": "https://beakerboy.github.io/Threejs-Geometries/src/WedgeGeometry.js"
22+
"wedge": "https://beakerboy.github.io/Threejs-Geometries/src/WedgeGeometry.js",
23+
"hipped": "https://beakerboy.github.io/Threejs-Geometries/src/HippedGeometry.js"
2224
}
2325
}
2426
</script>
25-
<script src="./src/apis.js"></script>
27+
<script type="module" src="./src/apis.js"></script>
2628
<script type="module" src="./src/index.js"></script>
2729
<div id="errorBox" style="position:absolute; top:10px; display:block; z-index:100; background-color: #ffffff; white-space:pre-line"></div>
2830
</body>

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
"jest-fetch-mock": "*",
2020
"jest-matcher-deep-close-to": "*",
2121
"jsdom": "*",
22+
"hipped": "file:../hipped",
2223
"pyramid": "file:../pyramid",
2324
"ramp": "file:../ramp",
24-
"wedge": "file:../wedge"
25+
"wedge": "file:../wedge",
26+
"straight-skeleton": "1.1.0"
2527
},
2628
"scripts": {
2729
"test": "c8 jest"

src/apis.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const osmApiUrl = new URLSearchParams(location.search).get('osmApiUrl') || 'https://api.openstreetmap.org/api/0.6';
2-
const apis = {
2+
export const apis = {
33
bounding: {
44
api: osmApiUrl + '/map?bbox=',
55
url: (left, bottom, right, top) => {

src/building.js

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {apis} from './apis.js';
12
import {BuildingShapeUtils} from './extras/BuildingShapeUtils.js';
23
import {BuildingPart} from './buildingpart.js';
34
import {MultiBuildingPart} from './multibuildingpart.js';
@@ -8,16 +9,28 @@ import {MultiBuildingPart} from './multibuildingpart.js';
89
* XML data from the API.
910
*/
1011
class Building {
11-
// Latitude and longitude that transitioned to (0, 0)
12+
/**
13+
* Latitude and longitude that transitions to (0, 0)
14+
* @type {number[2]}
15+
*/
1216
home = [];
1317

14-
// the parts
18+
/**
19+
* The parts.
20+
* @type {BuildingPart[]}
21+
*/
1522
parts = [];
1623

17-
// the BuildingPart of the outer building parimeter
24+
/**
25+
* The building part of the outer parimeter.
26+
* @type {BuildingPart}
27+
*/
1828
outerElement;
1929

20-
// DOM Tree of all elements to render
30+
/**
31+
* DOM Tree of all elements to render
32+
* @type {DOM.Element}
33+
*/
2134
fullXmlData;
2235

2336
id = '0';
@@ -29,21 +42,38 @@ class Building {
2942
type;
3043
options;
3144

45+
static async getRelationDataWithChildRelations(id) {
46+
const xmlData = new window.DOMParser().parseFromString(await Building.getRelationData(id), 'text/xml');
47+
await Promise.all(Array.from(xmlData.querySelectorAll('member[type=relation]')).map(async r => {
48+
const childId = r.getAttribute('ref');
49+
if (r.getAttribute('id') === childId) {
50+
return;
51+
}
52+
const childData = new window.DOMParser().parseFromString(await Building.getRelationData(childId), 'text/xml');
53+
childData.querySelectorAll('node, way, relation').forEach(i => {
54+
if (xmlData.querySelector(`${i.tagName}[id="${i.getAttribute('id')}"]`)) {
55+
return;
56+
}
57+
xmlData.querySelector('osm').appendChild(i);
58+
});
59+
}));
60+
return new XMLSerializer().serializeToString(xmlData);
61+
}
62+
3263
/**
33-
* Create new building
64+
* Download data for new building
3465
*/
35-
static async create(type, id) {
36-
var data;
66+
static async downloadDataAroundBuilding(type, id) {
67+
let data;
3768
if (type === 'way') {
3869
data = await Building.getWayData(id);
3970
} else {
40-
data = await Building.getRelationData(id);
71+
data = await Building.getRelationDataWithChildRelations(id);
4172
}
4273
let xmlData = new window.DOMParser().parseFromString(data, 'text/xml');
4374
const nodelist = Building.buildNodeList(xmlData);
4475
const extents = Building.getExtents(id, xmlData, nodelist);
45-
const innerData = await Building.getInnerData(...extents);
46-
return new Building(id, innerData);
76+
return await Building.getInnerData(...extents);
4777
}
4878

4979
/**
@@ -63,29 +93,30 @@ class Building {
6393
} else {
6494
this.type = 'relation';
6595
}
66-
if (this.isValidData(outerElementXml)) {
67-
this.nodelist = Building.buildNodeList(this.fullXmlData);
68-
this.setHome();
69-
this.repositionNodes();
70-
if (this.type === 'way') {
96+
try {
97+
this.validateData(outerElementXml);
98+
} catch (e) {
99+
throw new Error(`Rendering of ${outerElementXml.tagName.toLowerCase()} ${id} is not possible. ${e}`);
100+
}
101+
102+
this.nodelist = Building.buildNodeList(this.fullXmlData);
103+
this.setHome();
104+
this.repositionNodes();
105+
if (this.type === 'way') {
106+
this.outerElement = new BuildingPart(id, this.fullXmlData, this.nodelist);
107+
} else if (this.type === 'multipolygon') {
108+
this.outerElement = new MultiBuildingPart(id, this.fullXmlData, this.nodelist);
109+
} else {
110+
const outlineRef = outerElementXml.querySelector('member[role="outline"]').getAttribute('ref');
111+
const outline = this.fullXmlData.getElementById(outlineRef);
112+
const outlineType = outline.tagName.toLowerCase();
113+
if (outlineType === 'way') {
71114
this.outerElement = new BuildingPart(id, this.fullXmlData, this.nodelist);
72-
} else if (this.type === 'multipolygon') {
73-
this.outerElement = new MultiBuildingPart(id, this.fullXmlData, this.nodelist);
74115
} else {
75-
const outlineRef = outerElementXml.querySelector('member[role="outline"]').getAttribute('ref');
76-
const outline = this.fullXmlData.getElementById(outlineRef);
77-
const outlineType = outline.tagName.toLowerCase();
78-
if (outlineType === 'way') {
79-
this.outerElement = new BuildingPart(id, this.fullXmlData, this.nodelist);
80-
} else {
81-
this.outerElement = new MultiBuildingPart(outlineRef, this.fullXmlData, this.nodelist);
82-
}
116+
this.outerElement = new MultiBuildingPart(outlineRef, this.fullXmlData, this.nodelist);
83117
}
84-
this.addParts();
85-
} else {
86-
window.printError('XML Not Valid');
87-
throw new Error('invalid XML');
88118
}
119+
this.addParts();
89120
}
90121

91122
/**
@@ -140,7 +171,12 @@ class Building {
140171
const mesh = [];
141172
if (this.parts.length > 0) {
142173
this.outerElement.options.building.visible = false;
143-
mesh.push(...this.outerElement.render());
174+
const outerMeshes = this.outerElement.render();
175+
outerMeshes[0].visible = false;
176+
this.outerElement.options.roof.visible = false;
177+
outerMeshes[1].visible = false;
178+
this.outerElement.options.building.visible = false;
179+
mesh.push(...outerMeshes);
144180
for (let i = 0; i < this.parts.length; i++) {
145181
mesh.push(...this.parts[i].render());
146182
}
@@ -181,7 +217,11 @@ class Building {
181217
for (let i = 0; i < parts.length; i++) {
182218
if (parts[i].querySelector('[k="building:part"]')) {
183219
const id = parts[i].getAttribute('id');
184-
this.parts.push(new MultiBuildingPart(id, this.fullXmlData, this.nodelist, this.outerElement.options));
220+
try {
221+
this.parts.push(new MultiBuildingPart(id, this.fullXmlData, this.nodelist, this.outerElement.options));
222+
} catch (e) {
223+
window.printError(e);
224+
}
185225
}
186226
}
187227
}
@@ -193,38 +233,53 @@ class Building {
193233
static async getWayData(id) {
194234
let restPath = apis.getWay.url(id);
195235
let response = await fetch(restPath);
196-
let text = await response.text();
197-
return text;
236+
if (response.status === 404) {
237+
throw `The way ${id} was not found on the server.\nURL: ${restPath}`;
238+
} else if (response.status === 410) {
239+
throw `The way ${id} was deleted.\nURL: ${restPath}`;
240+
} else if (response.status !== 200) {
241+
throw `HTTP ${response.status}.\nURL: ${restPath}`;
242+
}
243+
return await response.text();
198244
}
199245

200246
static async getRelationData(id) {
201247
let restPath = apis.getRelation.url(id);
202248
let response = await fetch(restPath);
203-
let text = await response.text();
204-
return text;
249+
if (response.status === 404) {
250+
throw `The relation ${id} was not found on the server.\nURL: ${restPath}`;
251+
} else if (response.status === 410) {
252+
throw `The relation ${id} was deleted.\nURL: ${restPath}`;
253+
} else if (response.status !== 200) {
254+
throw `HTTP ${response.status}.\nURL: ${restPath}`;
255+
}
256+
return await response.text();
205257
}
206258

207259
/**
208-
* Fetch way data from OSM
260+
* Fetch map data data from OSM
209261
*/
210262
static async getInnerData(left, bottom, right, top) {
211-
let response = await fetch(apis.bounding.url(left, bottom, right, top));
212-
let res = await response.text();
213-
return res;
263+
let url = apis.bounding.url(left, bottom, right, top);
264+
let response = await fetch(url);
265+
if (response.status !== 200) {
266+
throw `HTTP ${response.status}.\nURL: ${url}`;
267+
}
268+
return await response.text();
214269
}
215270

216271
/**
217272
* validate that we have the ID of a building way.
218273
*/
219-
isValidData(xmlData) {
274+
validateData(xmlData) {
220275
// Check that it is a building (<tag k="building" v="*"/> exists)
221276
const buildingType = xmlData.querySelector('[k="building"]');
222277
const ways = [];
223278
if (xmlData.tagName === 'relation') {
224279
// get all building relation parts
225280
// todo: multipolygon inner and outer roles.
226281
let parts = xmlData.querySelectorAll('member[role="part"]');
227-
var ref = 0;
282+
let ref = 0;
228283
for (let i = 0; i < parts.length; i++) {
229284
ref = parts[i].getAttribute('ref');
230285
const part = this.fullXmlData.getElementById(ref);
@@ -236,8 +291,7 @@ class Building {
236291
}
237292
} else {
238293
if (!buildingType) {
239-
window.printError('Outer way is not a building');
240-
return false;
294+
throw new Error('Outer way is not a building');
241295
}
242296
ways.push(xmlData);
243297
}
@@ -250,16 +304,14 @@ class Building {
250304
const firstRef = nodes[0].getAttribute('ref');
251305
const lastRef = nodes[nodes.length - 1].getAttribute('ref');
252306
if (firstRef !== lastRef) {
253-
window.printError('Way ' + way.getAttribute('id') + ' is not a closed way. ' + firstRef + ' !== ' + lastRef + '.');
254-
return false;
307+
throw new Error('Way ' + way.getAttribute('id') + ' is not a closed way. ' + firstRef + ' !== ' + lastRef + '.');
255308
}
256309
} else {
257-
window.printError('Way ' + way.getAttribute('id') + ' has no nodes.');
258-
return false;
310+
throw new Error('Way ' + way.getAttribute('id') + ' has no nodes.');
259311
}
260312
} else {
261313
let parts = way.querySelectorAll('member[role="part"]');
262-
var ref = 0;
314+
let ref = 0;
263315
for (let i = 0; i < parts.length; i++) {
264316
ref = parts[i].getAttribute('ref');
265317
const part = this.fullXmlData.getElementById(ref);

src/buildingpart.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import {PyramidGeometry} from 'pyramid';
1212
import {RampGeometry} from 'ramp';
1313
import {WedgeGeometry} from 'wedge';
14+
import {HippedGeometry} from 'hipped';
1415
import {BuildingShapeUtils} from './extras/BuildingShapeUtils.js';
1516
/**
1617
* An OSM Building Part
@@ -193,7 +194,6 @@ class BuildingPart {
193194
this.createRoof();
194195
this.parts.push(this.roof);
195196
const mesh = this.createBuilding();
196-
this.options.building.visible = true;
197197
if (this.getAttribute('building:part') === 'roof') {
198198
mesh.visible = false;
199199
this.options.building.visible = false;
@@ -286,6 +286,15 @@ class BuildingPart {
286286
};
287287
const geometry = new PyramidGeometry(this.shape, options);
288288

289+
material = BuildingPart.getRoofMaterial(this.way);
290+
roof = new Mesh( geometry, material );
291+
roof.rotation.x = -Math.PI / 2;
292+
roof.position.set( 0, this.options.building.height - this.options.roof.height, 0);
293+
} else if (this.options.roof.shape === 'hipped') {
294+
const options = {
295+
depth: this.options.roof.height,
296+
};
297+
const geometry = new HippedGeometry(this.shape, options);
289298
material = BuildingPart.getRoofMaterial(this.way);
290299
roof = new Mesh( geometry, material );
291300
roof.rotation.x = -Math.PI / 2;
@@ -459,7 +468,13 @@ class BuildingPart {
459468
}
460469
const material = BuildingPart.getBaseMaterial(materialName);
461470
if (color !== '') {
462-
material.color = new Color(color);
471+
if (material instanceof MeshPhysicalMaterial) {
472+
material.emissive = new Color(color);
473+
material.emissiveIntensity = 0.5;
474+
material.roughness = 0.5;
475+
} else {
476+
material.color = new Color(color);
477+
}
463478
} else if (materialName === ''){
464479
material.color = new Color('white');
465480
}
@@ -489,7 +504,11 @@ class BuildingPart {
489504
material = BuildingPart.getBaseMaterial(materialName);
490505
}
491506
if (color !== '') {
492-
material.color = new Color(color);
507+
if (material instanceof MeshPhysicalMaterial) {
508+
material.emissive = new Color(color);
509+
} else {
510+
material.color = new Color(color);
511+
}
493512
}
494513
return material;
495514
}

0 commit comments

Comments
 (0)