diff --git a/web/client/api/Model.js b/web/client/api/Model.js new file mode 100644 index 0000000000..b603417efe --- /dev/null +++ b/web/client/api/Model.js @@ -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"; + diff --git a/web/client/api/catalog/CSW.js b/web/client/api/catalog/CSW.js index e9cccaede7..9e00062de2 100644 --- a/web/client/api/catalog/CSW.js +++ b/web/client/api/catalog/CSW.js @@ -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$/, "/"); }; @@ -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', diff --git a/web/client/api/catalog/Model.js b/web/client/api/catalog/Model.js new file mode 100644 index 0000000000..c5724441a1 --- /dev/null +++ b/web/client/api/catalog/Model.js @@ -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; +}; diff --git a/web/client/api/catalog/index.js b/web/client/api/catalog/index.js index 3e72a62391..db1bb88aec 100644 --- a/web/client/api/catalog/index.js +++ b/web/client/api/catalog/index.js @@ -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: @@ -51,5 +51,6 @@ export default { 'geojson': geojson, 'backgrounds': backgrounds, '3dtiles': threeDTiles, - 'cog': cog + 'cog': cog, + 'model': model }; diff --git a/web/client/components/TOC/DefaultLayer.jsx b/web/client/components/TOC/DefaultLayer.jsx index 2fb8f61ada..d2fd21f5b1 100644 --- a/web/client/components/TOC/DefaultLayer.jsx +++ b/web/client/components/TOC/DefaultLayer.jsx @@ -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 @@ -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)) ? ( { "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) { diff --git a/web/client/components/map/cesium/plugins/ModelLayer.js b/web/client/components/map/cesium/plugins/ModelLayer.js index 99b660900f..9c900fc33d 100644 --- a/web/client/components/map/cesium/plugins/ModelLayer.js +++ b/web/client/components/map/cesium/plugins/ModelLayer.js @@ -6,99 +6,10 @@ * LICENSE file in the root directory of this source tree. */ -import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; +import Layers from '../../../../utils/cesium/Layers'; +import { ifcDataToJSON, getWebIFC } from '../../../../api/Model'; // todo: change path to MODEL -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]; -}; - -const dataToJSON = ({ 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] - ]) - ); - 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] - }; -}; - -const getWebIFC = () => import('web-ifc') - .then(WebIFC => { - const ifcApi = new WebIFC.IfcAPI(); - ifcApi.SetWasmPath('./web-ifc/'); // eslint-disable-line - return ifcApi.Init().then(() => ifcApi); // eslint-disable-line - }); const transform = (positions, coords, matrix) => { let transformed = []; @@ -130,8 +41,8 @@ const getGeometryInstances = ({ indices }) => { const rotationMatrix = Cesium.Matrix4.fromTranslationQuaternionRotationScale( - new Cesium.Cartesian3(0.0, 0.0, 0.0), - Cesium.Quaternion.fromAxisAngle( + new Cesium.Cartesian3(0.0, 0.0, 0.0), // 0,0 + Cesium.Quaternion.fromAxisAngle( // 90 deg new Cesium.Cartesian3(1.0, 0.0, 0.0), Math.PI / 2 ), @@ -195,7 +106,13 @@ const getGeometryInstances = ({ }; const createLayer = (options, map) => { - + if (!options.visibility) { + return { + detached: true, + primitives: () => undefined, + remove: () => {} + }; + } let primitives = new Cesium.PrimitiveCollection({ destroyPrimitives: true }); fetch(options.url) @@ -203,13 +120,14 @@ const createLayer = (options, map) => { .then((data) => { return getWebIFC() .then((ifcApi) => { - const { meshes, center } = dataToJSON({ ifcApi, data }); + const { meshes, center } = ifcDataToJSON({ ifcApi, data }); const translucentPrimitive = new Cesium.Primitive({ geometryInstances: getGeometryInstances({ meshes: meshes.filter(mesh => !mesh.geometry.every(({ color }) => color.w === 1)), center, options }), + releaseGeometryInstances: false, appearance: new Cesium.PerInstanceColorAppearance({ translucent: true }), @@ -219,6 +137,7 @@ const createLayer = (options, map) => { // see https://github.com/geosolutions-it/MapStore2/blob/9f6f9d498796180ff59679887d300ce51e72a289/web/client/components/map/cesium/Map.jsx#L354-L393 translucentPrimitive._msGetFeatureById = (id) => meshes.find((_mesh) => _mesh.id === id)?.properties || {}; translucentPrimitive.msId = options.id; + translucentPrimitive.id = 'translucentPrimitive'; primitives.add(translucentPrimitive); const opaquePrimitive = new Cesium.Primitive({ geometryInstances: getGeometryInstances({ @@ -226,6 +145,7 @@ const createLayer = (options, map) => { center, options }), + releaseGeometryInstances: false, appearance: new Cesium.PerInstanceColorAppearance({ // flat: true translucent: false @@ -235,10 +155,12 @@ const createLayer = (options, map) => { }); opaquePrimitive._msGetFeatureById = (id) => meshes.find((_mesh) => _mesh.id === id)?.properties || {}; opaquePrimitive.msId = options.id; + opaquePrimitive.id = 'opaquePrimitive'; primitives.add(opaquePrimitive); }); }); map.scene.primitives.add(primitives); + window.MapScene = map.scene; return { detached: true, primitives, @@ -247,6 +169,11 @@ const createLayer = (options, map) => { map.scene.primitives.remove(primitives); primitives = undefined; } + }, + setVisible: ( + // newVisiblity + ) => { + // todo: add the logic of setting visibility } }; }; @@ -254,6 +181,7 @@ const createLayer = (options, map) => { Layers.registerType('model', { create: createLayer, update: (/* layer, newOptions, oldOptions, map */) => { + // todo: here we can put change opacity logic return null; } }); diff --git a/web/client/configs/new.json b/web/client/configs/new.json index ac5f20fdba..13f4280279 100644 --- a/web/client/configs/new.json +++ b/web/client/configs/new.json @@ -125,6 +125,25 @@ "fixed": true, "type": "empty", "visibility": false + }, + { + "id": "model-01", + "type": "model", + "url": " https://threejs.org/examples/models/ifc/rac_advanced_sample_project.ifc", + "name": "Model", + "title": "Model", + "center": [0, 0, 0], + "visibility": true + }, + { + "id": "model-02", + "type": "model", + "url": "https://www.steptools.com/docs/stpfiles/ifc/AC20-Institute-Var-2.ifc", + "name": "Model2", + "title": "Model2", + "center": [30.044583340501617, + 31.30455288820029, 0], + "visibility": true } ] } diff --git a/web/client/plugins/MetadataExplorer.jsx b/web/client/plugins/MetadataExplorer.jsx index 84194d13af..58e51d870b 100644 --- a/web/client/plugins/MetadataExplorer.jsx +++ b/web/client/plugins/MetadataExplorer.jsx @@ -179,7 +179,7 @@ class MetadataExplorerComponent extends React.Component { static defaultProps = { id: "mapstore-metadata-explorer", - serviceTypes: [{ name: "csw", label: "CSW" }, { name: "wms", label: "WMS" }, { name: "wmts", label: "WMTS" }, { name: "tms", label: "TMS", allowedProviders: DEFAULT_ALLOWED_PROVIDERS }, { name: "wfs", label: "WFS" }, { name: "3dtiles", label: "3D Tiles" }], + serviceTypes: [{ name: "csw", label: "CSW" }, { name: "wms", label: "WMS" }, { name: "wmts", label: "WMTS" }, { name: "tms", label: "TMS", allowedProviders: DEFAULT_ALLOWED_PROVIDERS }, { name: "wfs", label: "WFS" }, { name: "3dtiles", label: "3D Tiles" }, {name: "model", label: "IFC Model"}], active: false, wrap: false, modal: true, diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index b4adbb8f7e..ac888e5b18 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -155,7 +155,7 @@ const tocSelector = createShallowSelectorCreator(isEqual)( }, { options: { exclusiveMapType: true }, - func: (node) => (node.type === "3dtiles" && !isCesiumActive) || (node.type === "cog" && isCesiumActive) + func: (node) => (["3dtiles", 'model'].includes(node.type) && !isCesiumActive) || (node.type === "cog" && isCesiumActive) } ]), catalogActive, diff --git a/web/client/plugins/widgetbuilder/CatalogServiceEditor.jsx b/web/client/plugins/widgetbuilder/CatalogServiceEditor.jsx index 6a206850c2..6f2f33424b 100644 --- a/web/client/plugins/widgetbuilder/CatalogServiceEditor.jsx +++ b/web/client/plugins/widgetbuilder/CatalogServiceEditor.jsx @@ -43,7 +43,7 @@ export default ({service: defaultService, catalogServices, const handleChangeServiceProperty = (property, value) => { setService({ ...service, [property]: value }); }; - + // todo: add ifc to serviceTypes return (
setService({...service, url})}