Skip to content

Commit

Permalink
geosolutions-it#9830: add ifc support
Browse files Browse the repository at this point in the history
Description:
- Handle import ifc model to the 3d map
- Adding ifc model layer to catalog
  • Loading branch information
mahmoudadel54 committed Jan 22, 2024
1 parent 7bd86ec commit 67e3f34
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 104 deletions.
166 changes: 166 additions & 0 deletions web/client/api/Model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright 2023, 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 * as Cesium from 'cesium';

// 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 tileset metadata
function extractCapabilities(ifcApi, modelID, url) {
const version = ifcApi?.GetModelSchema() !== undefined ? ifcApi.GetModelSchema()?.includes("IFC4")? "IFC4" : ifcApi.GetModelSchema() : 'IFC4'; // eslint-disable-line
const format = getFormat(url || '');
const properties = {};
ifcApi.CloseModel(modelID); // eslint-disable-line
return {
version,
format,
properties
};
}

const applyMatrix = (matrix, coords) => {
const result = Cesium.Matrix4.multiplyByPoint(
Cesium.Matrix4.fromArray(matrix),
new Cesium.Cartesian3(...coords),
new Cesium.Cartesian3()
);

return [result.x, result.y, result.z];
};

export const ifcDataToJSON = ({ data, ifcApi }) => {
const settings = {};
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
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, y, z] = applyMatrix(
coordinationMatrix,
applyMatrix(placedGeometry.flatTransformation, [
ifcVertices[j],
ifcVertices[j + 1],
ifcVertices[j + 2]
], Cesium), Cesium
);
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];
normals[j / 2 + 1] = ifcVertices[j + 4];
normals[j / 2 + 2] = ifcVertices[j + 5];
}
geometry.push({
color: placedGeometry.color,
positions,
normals,
indices: Array.from(ifcIndices)
});
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) => {
return {
...acc,
[key]: propertyLines[key]?.value || propertyLines[key]
};
}, {})
});
});
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 => {
window.WebIFC = WebIFC;
const ifcApi = new WebIFC.IfcAPI();
ifcApi.SetWasmPath('./web-ifc/'); // eslint-disable-line
return ifcApi.Init().then(() => ifcApi); // eslint-disable-line
});
/**
* 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 fetch(url)
.then((res) => res.arrayBuffer())
.then((data) => {
return getWebIFC()
.then((ifcApi) => {
window.ifcApi = ifcApi;
let modelID = ifcApi.OpenModel(new Uint8Array(data)); // eslint-disable-line
// const { extent, center } = ifcDataToJSON({ ifcApi, data });
let capabilities = extractCapabilities(ifcApi, modelID, url);
// console.log({extent, center});
// let [minx, miny, maxx, maxy] = extent;
// todo: read IFCProjectedCRS, IFCMapCONVERSION in case of IFC4
let bbox = {
bounds: capabilities.version !== "IFC4" ? {
minx: 0,
miny: 0,
maxx: 0,
maxy: 0
} : {
minx: 0,
miny: 0,
maxx: 0,
maxy: 0},
crs: 'EPSG:4326'
};
return { modelData: data, ...capabilities, ...(bbox && { bbox })};
});
});
};

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

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
108 changes: 108 additions & 0 deletions web/client/api/catalog/Model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2023, 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 { Observable } from 'rxjs';
import { isValidURLTemplate } from '../../utils/URLUtils';
import { preprocess as commonPreprocess } from './common';
import { getCapabilities } from '../Model';

function validateUrl(serviceUrl) {
if (isValidURLTemplate(serviceUrl)) {
const parts = serviceUrl.split(/\./g);
// remove query params
const ext = (parts[parts.length - 1] || '').split(/\?/g)[0];
// from spec: IFC files use the .ifc extension.
return ext === 'ifc'
? true
: false;
}
return false;
}

const recordToLayer = (record) => {
if (!record) {
return null;
}
const { bbox, format, properties } = record;
return {
type: 'model',
url: record.url,
title: record.title,
center: [0, 0, 0],
visibility: true,
...(bbox && { bbox }),
...(format && { format }),
...(properties && { properties })
};
};

function getTitleFromUrl(url) {
const parts = url.split('/');
return parts[parts.length - 2];
}

// todo: need to refactor
const getRecords = (url, startPosition, maxRecords, text, info) => {
return getCapabilities(url)
.then(({ modelData, ...properties }) => {
const records = [{
// current specification does not define the title location
// but there is works related to the metadata in the next version of 3d tiles
// for the moment we set the name assigned to catalog service
// or we can extract the title from the url
title: info?.options?.service?.title || getTitleFromUrl(url),
url: url,
type: 'model',
modelData,
...properties
}];
return {
numberOfRecordsMatched: records.length,
numberOfRecordsReturned: records.length,
records
};
});
};

export const preprocess = commonPreprocess;
export const testService = (service) => Observable.of(service);
export const textSearch = (url, startPosition, maxRecords, text, info) => getRecords(url, startPosition, maxRecords, text, info);
export const getCatalogRecords = (response) => {
return response?.records
? response.records.map(record => {
const { version, bbox, format, properties } = record;
// remove query from identifier
const identifier = (record.url || '').split('?')[0];
return {
serviceType: 'model',
isValid: true,
description: `v. ${version}`,
title: record.title,
identifier,
url: record.url,
thumbnail: null,
...(bbox && { bbox }),
...(format && { format }),
...(properties && { properties }),
references: []
};
})
: null;
};
export const getLayerFromRecord = (record, options, asPromise) => {
const layer = recordToLayer(record, options);
return asPromise ? Promise.resolve(layer) : layer;
};
export const validate = (service) => {
if (service.title && validateUrl(service.url)) {
return Observable.of(service);
}
const error = new Error("catalog.config.notValidURLTemplate");
// insert valid URL;
throw error;
};
5 changes: 3 additions & 2 deletions web/client/api/catalog/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import * as geojson from './GeoJSON';
import * as backgrounds from './backgrounds';
import * as threeDTiles from './ThreeDTiles';
import * as cog from './COG';

import * as model from './Model'; // todo: will change to model
/**
* APIs collection for catalog.
* Each entry must implement:
Expand Down Expand Up @@ -51,5 +51,6 @@ export default {
'geojson': geojson,
'backgrounds': backgrounds,
'3dtiles': threeDTiles,
'cog': cog
'cog': cog,
'model': model
};
4 changes: 2 additions & 2 deletions web/client/components/TOC/DefaultLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class DefaultLayer extends React.Component {

getVisibilityMessage = () => {
if (this.props.node.exclusiveMapType) {
return this.props.node?.type === '3dtiles' ? 'toc.notVisibleSwitchTo3D' : this.props.node?.type === 'cog' ? 'toc.notVisibleSwitchTo2D' : '';
return ['3dtiles', 'model'].includes(this.props.node?.type) ? 'toc.notVisibleSwitchTo3D' : this.props.node?.type === 'cog' ? 'toc.notVisibleSwitchTo2D' : '';
}
const maxResolution = this.props.node.maxResolution || Infinity;
return this.props.resolution >= maxResolution
Expand All @@ -119,7 +119,7 @@ class DefaultLayer extends React.Component {
getSourceCRS = () => this.props.node?.bbox?.crs || this.props.node?.sourceMetadata?.crs;

renderOpacitySlider = (hideOpacityTooltip) => {
return (this.props.activateOpacityTool && this.props.node?.type !== '3dtiles') ? (
return (this.props.activateOpacityTool && !['3dtiles', 'model'].includes(this.props.node?.type)) ? (
<OpacitySlider
opacity={this.props.node.opacity}
disabled={!this.props.node.visibility}
Expand Down
3 changes: 2 additions & 1 deletion web/client/components/catalog/editor/MainFormUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export const defaultPlaceholder = (service) => {
"csw": "e.g. https://mydomain.com/geoserver/csw",
"tms": "e.g. https://mydomain.com/geoserver/gwc/service/tms/1.0.0",
"3dtiles": "e.g. https://mydomain.com/tileset.json",
"cog": "e.g. https://mydomain.com/cog.tif"
"cog": "e.g. https://mydomain.com/cog.tif",
"model": "e.g. https://mydomain.com/filename.ifc"
};
for ( const [key, value] of Object.entries(urlPlaceholder)) {
if ( key === service.type) {
Expand Down
Loading

0 comments on commit 67e3f34

Please sign in to comment.