编者注:
在上篇中,叶子老师介绍了如何使用iModel.js读取CSV数据,在本篇中,我们将介绍如何利用iModel.js定位iModel中的三维模型,以进行下一步的数据挂接。原文版由Roop Saini发布在Medium.com上,叶子老师做了本地化的翻译和整理。
本应用主题分三篇介绍,本文为第二篇。
当iModel.js遇到CSV数据之一:RPC读取(点击浏览)
当iModel.js遇到CSV数据之二:定位三维模型
当iModel.js遇到CSV数据之三:突出显示数据
以下为叶子老师的翻译。
在上一篇文章中,我们讨论了如何从iModel.js通过自定义RPC接口从后端读取CSV文件。现在前端得到了这些数据,现在可以开始做些有趣的事情了,看看我们的数据。
构件ID与三维模型中的元素相关联,我们下一步是找到这些元素的精确坐标。这些信息包含在iModel中,但是我们究竟如何获得它呢?
回答:ECSql----iModel的数据库查询语言。
我们可以在前端使用iModel查询API,传入ECSql语句,结果将为CSV中列出的每个垫片提供坐标。但是我们如何写出查询语句呢?通过iModel Console App!这个基于web的客户端静静地坐在那里几个小时,等待着您登录它,这样您就可以打开iModel,编写ECSql查询语句,帮助您发现数据的确切内容。
让我们关注一下这没人搭理的小东西。首先,我们将登录应用程序并打开我们的目标项目和iModel:
接下来,我们需要运行一些组合的ECSql语句。这应该很简单…我们需要的是具有给定控件ID的垫圈的原点。
为了构造这个查询语句,我们需要这个控件id属性所归属的表(schema)和类(class)。
我知道表AutoPlantPDWPersistenceStrategy的PipingComponent类中有一个component_id(控件Id)属性。我还知道PipingComponent继承自PhysicalElement类并包含PhysicalElement包含的所有属性。其中一个属性是原点,它应该包含我们要查找的元素的精确坐标。
让我们试着从列表以控件ID“AT HRZF7AQ5_4II”找到其中一个组件的原点:
有好消息和坏消息:我们找到了一个元素;ECInstanceId是他的唯一标识。然而,原点居然是Null?
可能管道组件可能是一个单元cell,也就是说,它是一个物理元素,同时它也是一个或多个物理元素的父元素,这些物理元素包含我们需要的实际几何信息。是的,很有可能。如果是这样,管道组件将有一个指向其子元素的ElementOwnsChildElement 关联关系。
(译者备注:关于assembly的详细解释可以参见优先社区对此详细介绍的一篇文章)
https://communities.bentley.com/communities/other_communities/chinafirst/w/chinawiki/51424/cell-itwin
iModel中的关联关系是从SourceECInstanceId(s)指向TargetECInstanceId(s),对于ElementOwnsChildElement ,源ID标识父对象,目标ID标识子对象。
让我们使用上面的ECInstanceId来查看垫圈是否有子元素:
啊哈!是的。我们有子元素的(Target)ECInstanceId,它可能是physicaleElement。
现在,让我们看看是否可以用它来找到我们的原点:
就在这里!我们现在得到了位置信息和ECSql查询语句,也没费什么事儿。
现在我们可以单独查询每个组件的位置。但是,每次从前端调用查询API时,我们都要往返于前后端。我们需要三个单独的查询来从组构件id一直到原点。如果不优化,22个组件就是调用66次前后端接口!
想象一下,在高延迟的网络环境下运行,来回66次仅仅用来填充一个数组!
看看能不能一次搞定。我们需要:
1. 将表PipingComponent和 ElementOwnsChildElements表联查,其中PipingComponent的ECInstanceId等于ElementOwnsChildElements中的SourceECInstanceId(父元素)。
2. 将生成的表和physicalement表联查,其中physicalement的ECInstanceId等于ElementOwnsChildElements中的TargetECInstanceId(子元素)。
3. 查询组合表以获得一个新列表,其中列出了组件的原点,这些原点与我们文件中的component_id一一对应。
薅了一阵头发,在控制台上尝试了一番之后,我最终得出了这样的结论:
让我们把它写进我们的代码里。我们可以在打开iModel之后,也就是App.tsx里的_onIModelSelected函数下加上这些语句:
let componentList = "("; const count = info.length; // prepare list of component ids info.forEach((value: any, index: number) => { componentList += "'" + value.component_id + "'"; componentList += (++index !== count) ? ", " : ")"; }); const query = `SELECT piping.Component_id, physical.Origin FROM AutoPlantPDWPersistenceStrategySchema.PipingComponent piping JOIN Bis.ElementOwnsChildElements link ON piping.ECInstanceId = link.SourceECInstanceId JOIN Bis.PhysicalElement physical ON link.TargetECInstanceId = physical.ECInstanceId WHERE piping.Component_id IN ${componentList}`; const rows = []; for await (const row of imodel.query(query)) rows.push(row);
编译运行的结果是这样的
太好了!我们现在可以从结果中解析出原点,并将它们添加到从CSV获得的原始数组中:
rows.forEach((row) => { const index = info.findIndex((x) => x.component_id === row.cOMPONENT_ID); if (index > 0) info[index].position = row.origin; });
让我们看看最终结果是什么:
很完美!一切数据都集中在一个地方。
有趣的是,我刚刚意识到,当我们从自定义RPC函数中查询CSV数据时,就已经开始往返于前后端了,可以把刚才所有这些逻辑都扔到RPC中,这样就又可以省下一次前后端的接口调用。
让我们把查询代码移到RPC实现类里,并直接从函数fetchInfo中直接调用它:
export class FileReaderRpcImpl extends FileReaderRpcInterface { public static register() { RpcManager.registerImpl(FileReaderRpcInterface, FileReaderRpcImpl); } private _filePath = "assets/testdata.csv"; public async fetchInfo(_token: IModelToken): Promise<any[]> { const data: string = fs.readFileSync(this._filePath, "utf8"); let info = parse(data, {delimiter: ",", columns: ["status", "component_id"]}); info = await this.fetchPositions(info, _token); return info; } private async fetchPositions(info: any[], token: IModelToken) { let componentList = "("; const count = info.length; // prepare list of component ids info.forEach((value: any, index: number) => { componentList += "'" + value.component_id + "'"; componentList += (++index !== count) ? ", " : ")"; }); const query = `SELECT piping.Component_id, physical.Origin FROM AutoPlantPDWPersistenceStrategySchema.PipingComponent piping JOIN Bis.ElementOwnsChildElements link ON piping.ECInstanceId = link.SourceECInstanceId JOIN Bis.PhysicalElement physical ON link.TargetECInstanceId = physical.ECInstanceId WHERE piping.Component_id IN ${componentList}`; const imodel = IModelDb.find(token); // THIS ONE const rows = []; for await (const row of imodel.query(query)) rows.push(row); rows.forEach((row) => { const index = info.findIndex((x) => x.component_id === row.cOMPONENT_ID); if (index > 0) info[index].position = row.origin; }); return info; }}
这里唯一的比较新的地方是imodelDb句柄(第27行)。在前面的例子中,我们使用的是iModelConnection,前端API通过它来访问已经打开的iModel。IModelDb是它的后端对应的名称,传递到RPC接口的token变量允许我们像前端一样对iModel进行查询。
现在前端只需要调用fetchInfo函数就可以了:
砰!一次接口调用还只有一行代码(程序员的成就感你懂的~)。
这就是我们的最终数组的样子,现在用于演示的所有信息都搞到手了,这将(希望)让这些客户惊喜连连。
最后一个任务是在模型上显示这些数据
别担心,咱们歇歇喝口水,下次我们将介绍如何将这些数据显示在三维模型上。
iModel.js相关内容:
5分钟快速部署iModel.js服务
快速使用iModel.js
当iModel.js遇到CSV数据之一:RPC读取
iTwin数字孪生相关内容:
iTwin性能测试报告:平台属性支撑业务拓展
Bentley iTwin数据管理:连接、同步、变更
从WorkSet到iTwin Service: 3/3-Design Review服务
- END -
?