Skip to content

Commit

Permalink
#9830: Support for IFC as a further 3D model managed by MapStore (#9908)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: allyoucanmap <stefano.bovio@geosolutionsgroup.com>
  • Loading branch information
mahmoudadel54 and allyoucanmap committed Feb 19, 2024
1 parent f050775 commit b64ec57
Show file tree
Hide file tree
Showing 32 changed files with 1,403 additions and 18 deletions.
3 changes: 2 additions & 1 deletion build/buildConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ module.exports = (...args) => mapArgumentsToObject(args, ({
{ from: path.join(getCesiumPath({ paths, prod }), 'Workers'), to: path.join(paths.dist, 'cesium', 'Workers') },
{ from: path.join(getCesiumPath({ paths, prod }), 'Assets'), to: path.join(paths.dist, 'cesium', 'Assets') },
{ from: path.join(getCesiumPath({ paths, prod }), 'Widgets'), to: path.join(paths.dist, 'cesium', 'Widgets') },
{ from: path.join(getCesiumPath({ paths, prod }), 'ThirdParty'), to: path.join(paths.dist, 'cesium', 'ThirdParty') }
{ from: path.join(getCesiumPath({ paths, prod }), 'ThirdParty'), to: path.join(paths.dist, 'cesium', 'ThirdParty') },
{ from: path.join(paths.base, 'node_modules', 'web-ifc'), to: path.join(paths.dist, 'web-ifc') }
]),
new ProvidePlugin({
Buffer: ['buffer', 'Buffer']
Expand Down
7 changes: 5 additions & 2 deletions build/testConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ module.exports = ({browsers = [ 'ChromeHeadless' ], files, path, testFile, singl
files: [
...files,
// add all assets needed for Cesium library
{ pattern: './node_modules/cesium/Build/CesiumUnminified/**/*', included: false }
{ pattern: './node_modules/cesium/Build/CesiumUnminified/**/*', included: false },
{ pattern: './node_modules/web-ifc/**/*', included: false }
],

proxies: {
"/web-ifc/": "/base/node_modules/web-ifc/"
},
plugins: [
require('karma-chrome-launcher'),
'karma-webpack',
Expand Down
44 changes: 44 additions & 0 deletions docs/developer-guide/maps-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ In the case of the background the `thumbURL` is used to show a preview of the la
- `3dtiles`: 3d tiles layers
- `terrain`: layers that define the elevation profile of the terrain
- `cog`: Cloud Optimized GeoTIFF layers
- `model`: 3D model layers like: IFC

#### WMS

Expand Down Expand Up @@ -1063,6 +1064,49 @@ i.e.

The style body object for the format 3dtiles accepts rules described in the 3d tiles styling specification version 1.0 available [here](https://github.com/CesiumGS/3d-tiles/tree/1.0/specification/Styling).

#### Model Layer

This type of layer shows ifc models with different versions including referenced and non-georeferenced ifc models inside the Cesium viewer. This layer will not be visible inside 2d map viewer types: openlayer or leaflet.
See specification for more info about IFC [here](https://technical.buildingsmart.org/standards/ifc/ifc-schema-specifications/).

i.e.

```javascript
{
"type": "model",
"url": "http..." // URL of ifc file with ".ifc" format
"title": "IFC Model Layer",
"visibility": true,
features: [
{
type: 'Feature',
id: 'model-origin',
properties: {
heading: 0,
pitch: 0,
roll: 0,
scale: scale
},
geometry: {
type: 'Point',
coordinates: [longitude, latitude, height]
}
}
],
bbox: {
crs: "EPSG:4326",
bounds: {
minx: 0,
miny: 0,
maxx: 0,
maxy: 0
}
},
// optional
properties: {}
}
```

#### Terrain

The `terrain` layer allows the customization of the elevation profile of the globe mesh in the *Cesium 3d viewer*.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@
"uuid": "3.0.1",
"vis": "4.21.0",
"w3c-schemas": "1.3.1",
"web-ifc": "0.0.50",
"webfontloader": "1.6.28",
"wellknown": "0.5.0",
"xml2js": "0.4.17",
Expand Down
242 changes: 242 additions & 0 deletions web/client/api/Model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* Copyright 2024, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import axios from 'axios';
import proj4 from 'proj4';

/**
* get ifc model main info such as: longitude, latitude, height and scale
* and additional info in case of existing a specified crs like: projectedCrs
* @param {object} ifcData includes 2 items: ifcApi, WebIFC
* @param {string} modelVersion the version of ifc file [e.g: IFC2x3, IFC4, IFC4x3]
* @param {number} modelID it is the model if of the ifc model e.g: 0,1,2
* @return {object} return originProperties that includes: latitude, longitude, height, scale and "projectedCrs" in case of georeferenced IFC4 model
* @
*/
// extract model origin point
function getModelOriginCoords(ifcData, modelVersion, modelID) {
const { ifcApi, WebIFC } = ifcData;
let originProperties = {};
if (modelVersion?.includes("IFC4")) {
let projectedCrs;
let mapConversion;
let projectedCrsLineId = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCPROJECTEDCRS); // eslint-disable-line
let mapConversionLineId = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCMAPCONVERSION); // eslint-disable-line
if (projectedCrsLineId.size()) {
let typeID = projectedCrsLineId.get(0);
let projectedCrsObj = ifcApi.GetLine(modelID, typeID); // eslint-disable-line
projectedCrs = projectedCrsObj?.Name?.value;
}
if (mapConversionLineId.size()) {
let typeID = mapConversionLineId.get(0);
let mapConversionObj = ifcApi.GetLine(modelID, typeID); // eslint-disable-line
mapConversion = {
northings: mapConversionObj?.Northings?.value, // x coord
eastings: mapConversionObj?.Eastings?.value, // y coord
orthogonalHeight: mapConversionObj?.OrthogonalHeight?.value, // height (z coord)
xAxisOrdinate: mapConversionObj?.XAxisOrdinate?.value,
xAxisAbscissa: mapConversionObj?.XAxisAbscissa?.value,
rotation: Math.atan2(mapConversionObj?.XAxisOrdinate?.value || 0, mapConversionObj?.XAxisAbscissa?.value || 0) * 180.0 / Math. PI,
scale: mapConversionObj?.Scale?.value
};
if (proj4.defs(projectedCrs)) { // if crs in not defined in MS, model will be locatied at 0,0
let wgs84Origin = proj4(proj4.defs(projectedCrs), proj4.defs('EPSG:4326'), [mapConversion.eastings, mapConversion.northings]);
originProperties = {
projectedCrs,
longitude: wgs84Origin[0] || 0,
latitude: wgs84Origin[1] || 0,
height: mapConversion.orthogonalHeight || 0,
scale: mapConversion.scale || 1,
heading: mapConversion.rotation,
mapConversion
};
} else {
originProperties = {
projectedCrs,
projectedCrsNotSupported: true,
mapConversion
};
}
}
}
// todo: maybe in future enhancement, handling georeferenced ifc models with schema less than version 4 like IFC2x3 by getting projection info ref: (https://medium.com/@stijngoedertier/how-to-georeference-a-bim-model-1905d5154cfd)
return originProperties;
}

// extract the tile format from the uri
function getFormat(uri) {
const parts = uri.split(/\./g);
const format = parts[parts.length - 1];
return format;
}

// extract version, bbox, format and properties from the ifc file
function extractCapabilities(ifcData, modelID, url) {
const { ifcApi } = ifcData;
const version = ifcApi?.GetModelSchema(modelID) !== undefined ? ifcApi.GetModelSchema(modelID)?.includes("IFC4")? "IFC4" : ifcApi.GetModelSchema(modelID) : 'IFC4'; // eslint-disable-line
const format = getFormat(url || '');
return {
version,
format,
properties: {}
};
}

/**
* get ifc response and additional parsed information such as: version, bbox, format and properties
* @param {object} this object includes: data --> ifc raw data, ifcData: is an object includes ifcApi object (from web-ifc) that enable to read ifc content
* @return {object} return json object includes meshes array [geometry data of ifc model], extent of ifc model, center and size
* @
*/
export const ifcDataToJSON = ({ data, ifcModule }) => {
const { ifcApi } = ifcModule;
const settings = {
COORDINATE_TO_ORIGIN: false, // this property change the position for IFC4 with projection if true
USE_FAST_BOOLS: true
};
let rawFileData = new Uint8Array(data);
const modelID = ifcApi.OpenModel(rawFileData, settings); // eslint-disable-line
ifcApi.LoadAllGeometry(modelID); // eslint-disable-line
const coordinationMatrix = ifcApi.GetCoordinationMatrix(modelID); // eslint-disable-line
if (coordinationMatrix) {
ifcApi.SetGeometryTransformation(modelID, coordinationMatrix); // eslint-disable-line
}
let meshes = [];
let minx = Infinity;
let maxx = -Infinity;
let miny = Infinity;
let maxy = -Infinity;
let minz = Infinity;
let maxz = -Infinity;
ifcApi.StreamAllMeshes(modelID, (mesh) => { // eslint-disable-line
const placedGeometries = mesh.geometries;
let geometry = [];
for (let i = 0; i < placedGeometries.size(); i++) {
const placedGeometry = placedGeometries.get(i);
const ifcGeometry = ifcApi.GetGeometry(modelID, placedGeometry.geometryExpressID); // eslint-disable-line
const ifcVertices = ifcApi.GetVertexArray(ifcGeometry.GetVertexData(), ifcGeometry.GetVertexDataSize()); // eslint-disable-line
const ifcIndices = ifcApi.GetIndexArray(ifcGeometry.GetIndexData(), ifcGeometry.GetIndexDataSize()); // eslint-disable-line
const positions = new Float64Array(ifcVertices.length / 2);
const normals = new Float32Array(ifcVertices.length / 2);
for (let j = 0; j < ifcVertices.length; j += 6) {
const x = ifcVertices[j]; // index = 0
const y = ifcVertices[j + 1]; // index = 1
const z = ifcVertices[j + 2]; // index = 2
if (x < minx) { minx = x; }
if (y < miny) { miny = y; }
if (z < minz) { minz = z; }
if (x > maxx) { maxx = x; }
if (y > maxy) { maxy = y; }
if (z > maxz) { maxz = z; }
positions[j / 2] = x;
positions[j / 2 + 1] = y;
positions[j / 2 + 2] = z;
normals[j / 2] = ifcVertices[j + 3]; // index = 3
normals[j / 2 + 1] = ifcVertices[j + 4]; // index = 4
normals[j / 2 + 2] = ifcVertices[j + 5]; // index = 5
}
geometry.push({
color: placedGeometry.color,
positions,
normals,
indices: Array.from(ifcIndices),
flatTransformation: placedGeometry.flatTransformation
});
ifcGeometry.delete();
}
const propertyLines = ifcApi.GetLine(modelID, mesh.expressID); // eslint-disable-line
meshes.push({
geometry,
id: mesh.expressID,
properties: Object.keys(propertyLines).reduce((acc, key) => {
acc[key] = propertyLines[key]?.value || propertyLines[key];
return acc;
}, {})
});
});
ifcApi.CloseModel(modelID); // eslint-disable-line
return {
meshes,
extent: [minx, miny, maxx, maxy, minz, maxz],
center: [minx + (maxx - minx) / 2, miny + (maxy - miny) / 2, minz + (maxz - minz) / 2],
size: [maxx - minx, maxy - miny, maxz - minz]
};
};

export const getWebIFC = () => import('web-ifc')
.then(WebIFC => {
const ifcApi = new WebIFC.IfcAPI();
ifcApi.SetWasmPath('./web-ifc/'); // eslint-disable-line
return ifcApi.Init().then(() => { return { ifcApi, WebIFC } }); // eslint-disable-line
});

let ifcCache = {};
export const getIFCModel = (url) => {
const request = ifcCache[url]
? () => Promise.resolve(ifcCache[url])
: () => axios.get(url, {
responseType: 'arraybuffer'
}).then(({ data }) => {
ifcCache[url] = data;
return data;
});
return request()
.then((data) => {
return getWebIFC()
.then((ifcModule) => {
return { data, ifcModule };
});
});
};
/**
* Common requests to IFC
* @module api.IFC
*/

/**
* get ifc response and additional parsed information such as: version, bbox, format and properties
* @param {string} url URL of the IFC.ifc file
* @
*/
export const getCapabilities = (url) => {
return getIFCModel(url)
.then(({ ifcModule, data }) => {
const { ifcApi } = ifcModule;
const settings = {
COORDINATE_TO_ORIGIN: false,
USE_FAST_BOOLS: true
};
const modelID = ifcApi.OpenModel(new Uint8Array(data), settings); // eslint-disable-line

let capabilities = extractCapabilities(ifcModule, modelID, url);
// extract model origin info by reading IFCProjectedCRS, IFCMapCONVERSION in case of IFC4
const modelOriginProperties = getModelOriginCoords(ifcModule, capabilities.version, modelID);
capabilities.properties = {
...capabilities.properties,
...modelOriginProperties
};
ifcApi.CloseModel(modelID); // eslint-disable-line
let properties = capabilities.properties;
// todo: getting bbox needs to enhance to get the accurate bbox of the ifc model
let bbox = {
bounds: {
minx: properties.longitude || 0 - 0.001,
miny: properties.latitude || 0 - 0.001,
maxx: properties.longitude || 0 + 0.001,
maxy: properties.latitude || 0 + 0.001
},
crs: 'EPSG:4326'
};
return { ...capabilities, ...(bbox && { bbox })};
});
};

/**
* constant of MODEL 'format'
*/
export const MODEL = "MODEL";

37 changes: 37 additions & 0 deletions web/client/api/__tests__/Model-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import { getCapabilities } from '../Model';
import expect from 'expect';

describe('Test Model API for ifc models', () => {
it('should extract capabilities from ifc model', (done) => {
getCapabilities('base/web/client/test-resources/ifcModels/ifcModel.ifc').then(({ bbox, format, version, properties }) => {
try {
expect(format).toBeTruthy();
expect(format).toBe('ifc');
expect(version).toBeTruthy();
expect(version).toBe('IFC4');
expect(properties).toBeTruthy();
expect(properties).toEqual({
projectedCrs: 'EPSG:5834',
projectedCrsNotSupported: true,
mapConversion: { northings: 5334600, eastings: 4468005, orthogonalHeight: 515, xAxisOrdinate: 0, xAxisAbscissa: 1, rotation: 0, scale: 1 }
});
expect(bbox).toBeTruthy();
expect(bbox.crs).toBe('EPSG:4326');
expect(Math.round(bbox.bounds.minx)).toBe(0);
expect(Math.round(bbox.bounds.miny)).toBe(0);
expect(Math.round(bbox.bounds.maxx)).toBe(0);
expect(Math.round(bbox.bounds.maxy)).toBe(0);
} catch (e) {
done(e);
}
done();
});
});
});
4 changes: 3 additions & 1 deletion web/client/api/catalog/CSW.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
preprocess as commonPreprocess
} from './common';
import { THREE_D_TILES } from '../ThreeDTiles';
import { MODEL } from '../Model';
const getBaseCatalogUrl = (url) => {
return url && url.replace(/\/csw$/, "/");
};
Expand Down Expand Up @@ -235,7 +236,8 @@ export const getCatalogRecords = (records, options, locales) => {
let catRecord;
if (dc && dc.format === THREE_D_TILES) {
catRecord = getCatalogRecord3DTiles(record, metadata);

} else if (dc && dc.format === MODEL) {
// todo: handle get catalog record for ifc
} else {
catRecord = {
serviceType: 'csw',
Expand Down
Loading

0 comments on commit b64ec57

Please sign in to comment.