Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ jobs:
mkdir ../pyramid
mkdir ../ramp
mkdir ../wedge
mkdir ../hipped
wget -P ../pyramid https://beakerboy.github.io/Threejs-Geometries/src/PyramidGeometry.js
wget -P ../ramp https://beakerboy.github.io/Threejs-Geometries/src/RampGeometry.js
wget -P ../wedge https://beakerboy.github.io/Threejs-Geometries/src/WedgeGeometry.js
wget -P ../hipped https://beakerboy.github.io/Threejs-Geometries/src/HippedGeometry.js
cd ../pyramid
echo '{"name":"pyramid","type":"module","private":true,"scripts":{"test":"npx jest"}}' > "./package.json" && npm init -y
cd ../ramp
echo '{"name":"ramp","type":"module","private":true,"scripts":{"test":"npx jest"}}' > "./package.json" && npm init -y
cd ../wedge
echo '{"name":"wedge","type":"module","private":true,"scripts":{"test":"npx jest"}}' > "./package.json" && npm init -y
cd ../hipped
echo '{"name":"hipped","type":"module","private":true,"scripts":{"test":"npx jest"}}' > "./package.json" && npm init -y
cd ../OSMBuilding
yarn --prod=false
- name: Lint
Expand Down
6 changes: 4 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
{
"imports": {
"three": "https://unpkg.com/three/build/three.module.js",
"straight-skeleton": "https://cdn.skypack.dev/straight-skeleton@1.1.0",
"pyramid": "https://beakerboy.github.io/Threejs-Geometries/src/PyramidGeometry.js",
"ramp": "https://beakerboy.github.io/Threejs-Geometries/src/RampGeometry.js",
"wedge": "https://beakerboy.github.io/Threejs-Geometries/src/WedgeGeometry.js"
"wedge": "https://beakerboy.github.io/Threejs-Geometries/src/WedgeGeometry.js",
"hipped": "https://beakerboy.github.io/Threejs-Geometries/src/HippedGeometry.js"
}
}
</script>
<script src="./src/apis.js"></script>
<script type="module" src="./src/apis.js"></script>
<script type="module" src="./src/index.js"></script>
<div id="errorBox" style="position:absolute; top:10px; display:block; z-index:100; background-color: #ffffff; white-space:pre-line"></div>
</body>
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
"jest-fetch-mock": "*",
"jest-matcher-deep-close-to": "*",
"jsdom": "*",
"hipped": "file:../hipped",
"pyramid": "file:../pyramid",
"ramp": "file:../ramp",
"wedge": "file:../wedge"
"wedge": "file:../wedge",
"straight-skeleton": "1.1.0"
},
"scripts": {
"test": "c8 jest"
Expand Down
2 changes: 1 addition & 1 deletion src/apis.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const osmApiUrl = new URLSearchParams(location.search).get('osmApiUrl') || 'https://api.openstreetmap.org/api/0.6';
const apis = {
export const apis = {
bounding: {
api: osmApiUrl + '/map?bbox=',
url: (left, bottom, right, top) => {
Expand Down
148 changes: 100 additions & 48 deletions src/building.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {apis} from './apis.js';
import {BuildingShapeUtils} from './extras/BuildingShapeUtils.js';
import {BuildingPart} from './buildingpart.js';
import {MultiBuildingPart} from './multibuildingpart.js';
Expand All @@ -8,16 +9,28 @@ import {MultiBuildingPart} from './multibuildingpart.js';
* XML data from the API.
*/
class Building {
// Latitude and longitude that transitioned to (0, 0)
/**
* Latitude and longitude that transitions to (0, 0)
* @type {number[2]}
*/
home = [];

// the parts
/**
* The parts.
* @type {BuildingPart[]}
*/
parts = [];

// the BuildingPart of the outer building parimeter
/**
* The building part of the outer parimeter.
* @type {BuildingPart}
*/
outerElement;

// DOM Tree of all elements to render
/**
* DOM Tree of all elements to render
* @type {DOM.Element}
*/
fullXmlData;

id = '0';
Expand All @@ -29,21 +42,38 @@ class Building {
type;
options;

static async getRelationDataWithChildRelations(id) {
const xmlData = new window.DOMParser().parseFromString(await Building.getRelationData(id), 'text/xml');
await Promise.all(Array.from(xmlData.querySelectorAll('member[type=relation]')).map(async r => {
const childId = r.getAttribute('ref');
if (r.getAttribute('id') === childId) {
return;
}
const childData = new window.DOMParser().parseFromString(await Building.getRelationData(childId), 'text/xml');
childData.querySelectorAll('node, way, relation').forEach(i => {
if (xmlData.querySelector(`${i.tagName}[id="${i.getAttribute('id')}"]`)) {
return;
}
xmlData.querySelector('osm').appendChild(i);
});
}));
return new XMLSerializer().serializeToString(xmlData);
}

/**
* Create new building
* Download data for new building
*/
static async create(type, id) {
var data;
static async downloadDataAroundBuilding(type, id) {
let data;
if (type === 'way') {
data = await Building.getWayData(id);
} else {
data = await Building.getRelationData(id);
data = await Building.getRelationDataWithChildRelations(id);
}
let xmlData = new window.DOMParser().parseFromString(data, 'text/xml');
const nodelist = Building.buildNodeList(xmlData);
const extents = Building.getExtents(id, xmlData, nodelist);
const innerData = await Building.getInnerData(...extents);
return new Building(id, innerData);
return await Building.getInnerData(...extents);
}

/**
Expand All @@ -63,29 +93,30 @@ class Building {
} else {
this.type = 'relation';
}
if (this.isValidData(outerElementXml)) {
this.nodelist = Building.buildNodeList(this.fullXmlData);
this.setHome();
this.repositionNodes();
if (this.type === 'way') {
try {
this.validateData(outerElementXml);
} catch (e) {
throw new Error(`Rendering of ${outerElementXml.tagName.toLowerCase()} ${id} is not possible. ${e}`);
}

this.nodelist = Building.buildNodeList(this.fullXmlData);
this.setHome();
this.repositionNodes();
if (this.type === 'way') {
this.outerElement = new BuildingPart(id, this.fullXmlData, this.nodelist);
} else if (this.type === 'multipolygon') {
this.outerElement = new MultiBuildingPart(id, this.fullXmlData, this.nodelist);
} else {
const outlineRef = outerElementXml.querySelector('member[role="outline"]').getAttribute('ref');
const outline = this.fullXmlData.getElementById(outlineRef);
const outlineType = outline.tagName.toLowerCase();
if (outlineType === 'way') {
this.outerElement = new BuildingPart(id, this.fullXmlData, this.nodelist);
} else if (this.type === 'multipolygon') {
this.outerElement = new MultiBuildingPart(id, this.fullXmlData, this.nodelist);
} else {
const outlineRef = outerElementXml.querySelector('member[role="outline"]').getAttribute('ref');
const outline = this.fullXmlData.getElementById(outlineRef);
const outlineType = outline.tagName.toLowerCase();
if (outlineType === 'way') {
this.outerElement = new BuildingPart(id, this.fullXmlData, this.nodelist);
} else {
this.outerElement = new MultiBuildingPart(outlineRef, this.fullXmlData, this.nodelist);
}
this.outerElement = new MultiBuildingPart(outlineRef, this.fullXmlData, this.nodelist);
}
this.addParts();
} else {
window.printError('XML Not Valid');
throw new Error('invalid XML');
}
this.addParts();
}

/**
Expand Down Expand Up @@ -140,7 +171,12 @@ class Building {
const mesh = [];
if (this.parts.length > 0) {
this.outerElement.options.building.visible = false;
mesh.push(...this.outerElement.render());
const outerMeshes = this.outerElement.render();
outerMeshes[0].visible = false;
this.outerElement.options.roof.visible = false;
outerMeshes[1].visible = false;
this.outerElement.options.building.visible = false;
mesh.push(...outerMeshes);
for (let i = 0; i < this.parts.length; i++) {
mesh.push(...this.parts[i].render());
}
Expand Down Expand Up @@ -181,7 +217,11 @@ class Building {
for (let i = 0; i < parts.length; i++) {
if (parts[i].querySelector('[k="building:part"]')) {
const id = parts[i].getAttribute('id');
this.parts.push(new MultiBuildingPart(id, this.fullXmlData, this.nodelist, this.outerElement.options));
try {
this.parts.push(new MultiBuildingPart(id, this.fullXmlData, this.nodelist, this.outerElement.options));
} catch (e) {
window.printError(e);
}
}
}
}
Expand All @@ -193,38 +233,53 @@ class Building {
static async getWayData(id) {
let restPath = apis.getWay.url(id);
let response = await fetch(restPath);
let text = await response.text();
return text;
if (response.status === 404) {
throw `The way ${id} was not found on the server.\nURL: ${restPath}`;
} else if (response.status === 410) {
throw `The way ${id} was deleted.\nURL: ${restPath}`;
} else if (response.status !== 200) {
throw `HTTP ${response.status}.\nURL: ${restPath}`;
}
return await response.text();
}

static async getRelationData(id) {
let restPath = apis.getRelation.url(id);
let response = await fetch(restPath);
let text = await response.text();
return text;
if (response.status === 404) {
throw `The relation ${id} was not found on the server.\nURL: ${restPath}`;
} else if (response.status === 410) {
throw `The relation ${id} was deleted.\nURL: ${restPath}`;
} else if (response.status !== 200) {
throw `HTTP ${response.status}.\nURL: ${restPath}`;
}
return await response.text();
}

/**
* Fetch way data from OSM
* Fetch map data data from OSM
*/
static async getInnerData(left, bottom, right, top) {
let response = await fetch(apis.bounding.url(left, bottom, right, top));
let res = await response.text();
return res;
let url = apis.bounding.url(left, bottom, right, top);
let response = await fetch(url);
if (response.status !== 200) {
throw `HTTP ${response.status}.\nURL: ${url}`;
}
return await response.text();
}

/**
* validate that we have the ID of a building way.
*/
isValidData(xmlData) {
validateData(xmlData) {
// Check that it is a building (<tag k="building" v="*"/> exists)
const buildingType = xmlData.querySelector('[k="building"]');
const ways = [];
if (xmlData.tagName === 'relation') {
// get all building relation parts
// todo: multipolygon inner and outer roles.
let parts = xmlData.querySelectorAll('member[role="part"]');
var ref = 0;
let ref = 0;
for (let i = 0; i < parts.length; i++) {
ref = parts[i].getAttribute('ref');
const part = this.fullXmlData.getElementById(ref);
Expand All @@ -236,8 +291,7 @@ class Building {
}
} else {
if (!buildingType) {
window.printError('Outer way is not a building');
return false;
throw new Error('Outer way is not a building');
}
ways.push(xmlData);
}
Expand All @@ -250,16 +304,14 @@ class Building {
const firstRef = nodes[0].getAttribute('ref');
const lastRef = nodes[nodes.length - 1].getAttribute('ref');
if (firstRef !== lastRef) {
window.printError('Way ' + way.getAttribute('id') + ' is not a closed way. ' + firstRef + ' !== ' + lastRef + '.');
return false;
throw new Error('Way ' + way.getAttribute('id') + ' is not a closed way. ' + firstRef + ' !== ' + lastRef + '.');
}
} else {
window.printError('Way ' + way.getAttribute('id') + ' has no nodes.');
return false;
throw new Error('Way ' + way.getAttribute('id') + ' has no nodes.');
}
} else {
let parts = way.querySelectorAll('member[role="part"]');
var ref = 0;
let ref = 0;
for (let i = 0; i < parts.length; i++) {
ref = parts[i].getAttribute('ref');
const part = this.fullXmlData.getElementById(ref);
Expand Down
25 changes: 22 additions & 3 deletions src/buildingpart.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {PyramidGeometry} from 'pyramid';
import {RampGeometry} from 'ramp';
import {WedgeGeometry} from 'wedge';
import {HippedGeometry} from 'hipped';
import {BuildingShapeUtils} from './extras/BuildingShapeUtils.js';
/**
* An OSM Building Part
Expand Down Expand Up @@ -193,7 +194,6 @@ class BuildingPart {
this.createRoof();
this.parts.push(this.roof);
const mesh = this.createBuilding();
this.options.building.visible = true;
if (this.getAttribute('building:part') === 'roof') {
mesh.visible = false;
this.options.building.visible = false;
Expand Down Expand Up @@ -286,6 +286,15 @@ class BuildingPart {
};
const geometry = new PyramidGeometry(this.shape, options);

material = BuildingPart.getRoofMaterial(this.way);
roof = new Mesh( geometry, material );
roof.rotation.x = -Math.PI / 2;
roof.position.set( 0, this.options.building.height - this.options.roof.height, 0);
} else if (this.options.roof.shape === 'hipped') {
const options = {
depth: this.options.roof.height,
};
const geometry = new HippedGeometry(this.shape, options);
material = BuildingPart.getRoofMaterial(this.way);
roof = new Mesh( geometry, material );
roof.rotation.x = -Math.PI / 2;
Expand Down Expand Up @@ -459,7 +468,13 @@ class BuildingPart {
}
const material = BuildingPart.getBaseMaterial(materialName);
if (color !== '') {
material.color = new Color(color);
if (material instanceof MeshPhysicalMaterial) {
material.emissive = new Color(color);
material.emissiveIntensity = 0.5;
material.roughness = 0.5;
} else {
material.color = new Color(color);
}
} else if (materialName === ''){
material.color = new Color('white');
}
Expand Down Expand Up @@ -489,7 +504,11 @@ class BuildingPart {
material = BuildingPart.getBaseMaterial(materialName);
}
if (color !== '') {
material.color = new Color(color);
if (material instanceof MeshPhysicalMaterial) {
material.emissive = new Color(color);
} else {
material.color = new Color(color);
}
}
return material;
}
Expand Down
Loading
Loading