Skip to content

Commit 7971863

Browse files
Transform Linearly Located Imodels (#260)
Creates transform that can be applied to spatial elements when cloning a src imodel into target with different ECEF locations. --------- Co-authored-by: Daniel Rodriguez <DanRod1999@users.noreply.github.com> Co-authored-by: Ben Polinsky <78756012+ben-polinsky@users.noreply.github.com>
1 parent 6a2dabe commit 7971863

4 files changed

Lines changed: 281 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "allow option to transform spatial elements of linearly located imodels",
4+
"packageName": "@itwin/imodel-transformer",
5+
"email": "'DanRod1999@users.noreply.github.com'",
6+
"dependentChangeType": "patch"
7+
}

common/api/imodel-transformer.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { RelationshipProps } from '@itwin/core-backend';
3838
import { Schema } from '@itwin/ecschema-metadata';
3939
import { SchemaKey } from '@itwin/ecschema-metadata';
4040
import { SqliteChangeOp } from '@itwin/core-backend';
41+
import { Transform } from '@itwin/core-geometry';
4142

4243
// @public
4344
export class ChangedInstanceIds {
@@ -243,6 +244,7 @@ export interface IModelImportOptions {
243244
export class IModelTransformer extends IModelExportHandler {
244245
constructor(source: IModelDb | IModelExporter, target: IModelDb | IModelImporter, options?: IModelTransformOptions);
245246
protected addCustomChanges(_sourceDbChanges: ChangedInstanceIds): Promise<void>;
247+
calculateEcefTransform(srcDb: IModelDb, targetDb: IModelDb): Transform;
246248
combineElements(sourceElementIds: Id64Array, targetElementId: Id64String): void;
247249
// (undocumented)
248250
protected completePartiallyCommittedAspects(): void;
@@ -256,6 +258,7 @@ export class IModelTransformer extends IModelExportHandler {
256258
static determineSyncType(sourceDb: IModelDb, targetDb: IModelDb,
257259
targetScopeElementId: Id64String): "forward" | "reverse";
258260
dispose(): void;
261+
ecefTransform?: Transform;
259262
protected _elementsWithExplicitlyTrackedProvenance: Set<string>;
260263
readonly exporter: IModelExporter;
261264
static forEachTrackedElement(args: {
@@ -354,6 +357,7 @@ export class IModelTransformer extends IModelExportHandler {
354357

355358
// @beta
356359
export interface IModelTransformOptions {
360+
alignECEFLocations?: boolean;
357361
argsForProcessChanges?: ProcessChangesOptions;
358362
branchRelationshipDataBehavior?: "unsafe-migrate" | "reject";
359363
cloneUsingBinaryGeometry?: boolean;

packages/transformer/src/IModelTransformer.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import {
8080
EntityReference,
8181
ExternalSourceAspectProps,
8282
FontProps,
83+
GeometricElement3dProps,
8384
GeometricElementProps,
8485
IModel,
8586
IModelError,
@@ -239,6 +240,11 @@ export interface IModelTransformOptions {
239240
* @default undefined
240241
*/
241242
argsForProcessChanges?: ProcessChangesOptions;
243+
/** A flag that determines if spatial elements from the source db should be transformed if source and target iModel ECEF locations differ.
244+
* @note This flag should only be used if imodels are linearly located
245+
* @default false
246+
*/
247+
alignECEFLocations?: boolean;
242248
}
243249

244250
/**
@@ -379,6 +385,10 @@ export class IModelTransformer extends IModelExportHandler {
379385
public readonly targetDb: IModelDb;
380386
/** The IModelTransformContext for this IModelTransformer. */
381387
public readonly context: IModelCloneContext;
388+
/** The transform to be applied to the placement of spatial elements when source and target db have different ECEF locations
389+
* @note transform can only be used when source and target are linearly located imodels
390+
*/
391+
public ecefTransform?: Transform;
382392
private _syncType?: SyncType;
383393

384394
/** The Id of the Element in the **target** iModel that represents the **source** repository as a whole and scopes its [ExternalSourceAspect]($backend) instances. */
@@ -589,6 +599,7 @@ export class IModelTransformer extends IModelExportHandler {
589599
options?.branchRelationshipDataBehavior ?? "reject",
590600
skipPropagateChangesToRootElements:
591601
options?.skipPropagateChangesToRootElements ?? true,
602+
alignECEFLocations: options?.alignECEFLocations ?? false,
592603
};
593604
// check if authorization client is defined
594605
if (IModelHost.authorizationClient === undefined) {
@@ -656,6 +667,9 @@ export class IModelTransformer extends IModelExportHandler {
656667
(this.targetDb as any).codeValueBehavior = "exact";
657668
}
658669
/* eslint-enable @itwin/no-internal */
670+
this.ecefTransform = this._options.alignECEFLocations
671+
? this.calculateEcefTransform(this.sourceDb, this.targetDb)
672+
: undefined;
659673
}
660674

661675
/** validates that the importer set on the transformer has the same values for its shared options as the transformer.
@@ -1571,9 +1585,72 @@ export class IModelTransformer extends IModelExportHandler {
15711585
targetElementProps.jsonProperties.Subject.Job = undefined;
15721586
}
15731587
}
1588+
1589+
if (
1590+
this.ecefTransform !== undefined &&
1591+
sourceElement instanceof GeometricElement3d
1592+
) {
1593+
// can check the sourceElement since this IModelTransformer does not remap classes
1594+
const placement = Placement3d.fromJSON(
1595+
(targetElementProps as GeometricElement3dProps).placement
1596+
);
1597+
1598+
if (placement.isValid) {
1599+
placement.multiplyTransform(this.ecefTransform);
1600+
(targetElementProps as GeometricElement3dProps).placement = placement;
1601+
}
1602+
}
15741603
return targetElementProps;
15751604
}
15761605

1606+
/**
1607+
* Calculate the transform between two ECEF locations
1608+
* @param srcDb
1609+
* @param targetDb
1610+
* @returns Transform that converts relative coordinates in the source iModel to relative coordinates in the target iModel.
1611+
* @note This can only be used if both source and target iModels are linearly located
1612+
*/
1613+
public calculateEcefTransform(
1614+
srcDb: IModelDb,
1615+
targetDb: IModelDb
1616+
): Transform {
1617+
const srcEcefLoc = srcDb.ecefLocation;
1618+
const targetEcefLoc = targetDb.ecefLocation;
1619+
1620+
if (
1621+
srcDb.geographicCoordinateSystem !== undefined ||
1622+
targetDb.geographicCoordinateSystem !== undefined
1623+
) {
1624+
throw new IModelError(
1625+
IModelStatus.MismatchGcs,
1626+
"Both source and target geographic coordinate systems must not be defined to calculate the linear ecef transform."
1627+
);
1628+
}
1629+
1630+
if (srcEcefLoc === undefined || targetEcefLoc === undefined) {
1631+
throw new IModelError(
1632+
IModelStatus.NoGeoLocation,
1633+
"Both source and target ECEF locations must be defined to calculate the transform."
1634+
);
1635+
} else {
1636+
if (srcEcefLoc.getTransform().isAlmostEqual(targetEcefLoc.getTransform()))
1637+
return Transform.createIdentity();
1638+
1639+
const srcSpatialToECEF = srcEcefLoc.getTransform(); // converts relative to ECEF in relation to source
1640+
const targetECEFToSpatial = targetEcefLoc.getTransform().inverse(); // converts ECEF to relative in relation to target
1641+
if (!targetECEFToSpatial) {
1642+
throw new IModelError(
1643+
IModelStatus.NoGeoLocation,
1644+
"Failed to invert target ECEF transform."
1645+
);
1646+
}
1647+
const ecefTransform =
1648+
targetECEFToSpatial.multiplyTransformTransform(srcSpatialToECEF); // chain both transforms
1649+
1650+
return ecefTransform;
1651+
}
1652+
}
1653+
15771654
// if undefined, it can be initialized by calling [[this.processChangesets]]
15781655
private _deletedSourceRelationshipData?: Map<
15791656
Id64String,
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3+
* See LICENSE.md in the project root for license terms and full copyright notice.
4+
*--------------------------------------------------------------------------------------------*/
5+
import * as path from "path";
6+
import {
7+
DefinitionModel,
8+
GeometricElement3d,
9+
IModelDb,
10+
IModelJsFs,
11+
PhysicalModel,
12+
PhysicalObject,
13+
SnapshotDb,
14+
SpatialCategory,
15+
Subject,
16+
} from "@itwin/core-backend";
17+
import {
18+
Cartographic,
19+
Code,
20+
ColorDef,
21+
EcefLocation,
22+
GeometryStreamBuilder,
23+
PhysicalElementProps,
24+
} from "@itwin/core-common";
25+
import { Point3d, Sphere, YawPitchRollAngles } from "@itwin/core-geometry";
26+
import { assert } from "console";
27+
import {
28+
IModelTransformer,
29+
IModelTransformOptions,
30+
} from "../../IModelTransformer";
31+
import { expect } from "chai";
32+
33+
describe("Linear Geolocation Transformations", () => {
34+
function initOutputFile(fileBaseName: string) {
35+
const outputDirName = path.join(__dirname, "output");
36+
if (!IModelJsFs.existsSync(outputDirName)) {
37+
IModelJsFs.mkdirSync(outputDirName);
38+
}
39+
const outputFileName = path.join(outputDirName, fileBaseName);
40+
if (IModelJsFs.existsSync(outputFileName)) {
41+
IModelJsFs.removeSync(outputFileName);
42+
}
43+
return outputFileName;
44+
}
45+
46+
function convertLatLongToEcef(lat: number, long: number): EcefLocation {
47+
const cartographic = Cartographic.fromDegrees({
48+
longitude: long,
49+
latitude: lat,
50+
height: 0,
51+
});
52+
const ecef = EcefLocation.createFromCartographicOrigin(cartographic);
53+
54+
return ecef;
55+
}
56+
57+
// Create a test iModel with a specified ECEF location and number of spherical elements
58+
// Elements are of radius 1, placed in 2 by 2 by x grids 5 meters apart, and first eleement is inserted at origin
59+
// which is the specified ECEF location
60+
function createTestSnapshotDb(
61+
ecef: EcefLocation,
62+
dbName: string,
63+
numElements: number = 1,
64+
color: string = "red"
65+
): SnapshotDb {
66+
const dbFileName = initOutputFile(`${dbName}.bim`);
67+
const imodelDb = SnapshotDb.createEmpty(dbFileName, {
68+
rootSubject: { name: dbName },
69+
ecefLocation: ecef,
70+
});
71+
// bug in SnapshotEb.createEmpty does not properly set ecefLocation, this was corrected in itwinjs-core 5.1, and will be fixed when transformer repo is updated
72+
imodelDb.setEcefLocation(ecef);
73+
74+
const subjectId = Subject.insert(
75+
imodelDb,
76+
IModelDb.rootSubjectId,
77+
"Test Subject"
78+
);
79+
const defintionModelId = DefinitionModel.insert(
80+
imodelDb,
81+
subjectId,
82+
"DefinitionModel"
83+
);
84+
85+
const categoryId = SpatialCategory.insert(
86+
imodelDb,
87+
defintionModelId,
88+
`${color} Category`,
89+
{ color: ColorDef.fromString(color).toJSON() }
90+
);
91+
92+
const modelId = PhysicalModel.insert(imodelDb, subjectId, "Test Model");
93+
94+
const builder = new GeometryStreamBuilder();
95+
builder.appendGeometry(Sphere.createCenterRadius(Point3d.createZero(), 1));
96+
for (let i = 0; i < numElements; i++) {
97+
// Arrange elements in a 2x2 grid, incrementing z every 4 elements
98+
const x = (i % 2) * 5;
99+
const y = (Math.floor(i / 2) % 2) * 5;
100+
const z = Math.floor(i / 4) * 5;
101+
102+
const elementProps: PhysicalElementProps = {
103+
classFullName: PhysicalObject.classFullName,
104+
model: modelId,
105+
category: categoryId,
106+
code: Code.createEmpty(),
107+
geom: builder.geometryStream,
108+
placement: {
109+
origin: Point3d.create(x, y, z),
110+
angles: YawPitchRollAngles.createDegrees(0, 0, 0),
111+
},
112+
};
113+
imodelDb.elements.insertElement(elementProps);
114+
}
115+
imodelDb.saveChanges("Created test elements");
116+
117+
return imodelDb;
118+
}
119+
120+
// Get all GeometricElement3d elements from the iModel
121+
// Used to find and compare placement of elements before and after transform
122+
async function getGeometric3dElements(
123+
iModelDb: IModelDb
124+
): Promise<GeometricElement3d[]> {
125+
const elements: GeometricElement3d[] = [];
126+
const query = "SELECT ECInstanceId FROM bis.GeometricElement3d";
127+
for await (const row of iModelDb.createQueryReader(query)) {
128+
const element = iModelDb.elements.getElement<GeometricElement3d>(row.id);
129+
elements.push(element);
130+
}
131+
return elements;
132+
}
133+
134+
it("should transform placement of src elements using core transfromer", async function () {
135+
const srcEcef = convertLatLongToEcef(
136+
39.952959446468206,
137+
-75.16349515933572
138+
); // City Hall
139+
const targetEcef = convertLatLongToEcef(
140+
39.95595450339434,
141+
-75.16697176954752
142+
); // Bentley Cherry Street
143+
144+
// generate imodels with ecef locations specified above, and number of spherical elements inserted
145+
const sourceDb = createTestSnapshotDb(
146+
srcEcef,
147+
"Source-ECEF-core-Transform",
148+
12,
149+
"red"
150+
);
151+
const targetDb = createTestSnapshotDb(
152+
targetEcef,
153+
"Target-ECEF-core-Transform",
154+
12,
155+
"blue"
156+
);
157+
assert(sourceDb.geographicCoordinateSystem === undefined);
158+
assert(targetDb.geographicCoordinateSystem === undefined);
159+
assert(sourceDb.ecefLocation !== undefined);
160+
assert(targetDb.ecefLocation !== undefined);
161+
162+
// get Fed Guid of one geomentric element in srcDb so we can compare the transfromed element in targetDb
163+
const srcElements = await getGeometric3dElements(sourceDb);
164+
const srcElemFedGuid = srcElements[0].federationGuid;
165+
166+
const transformerOptions: IModelTransformOptions = {
167+
alignECEFLocations: true,
168+
};
169+
const transfrom = new IModelTransformer(
170+
sourceDb,
171+
targetDb,
172+
transformerOptions
173+
);
174+
175+
await transfrom.process();
176+
targetDb.saveChanges("clone contents from source");
177+
178+
const srcElemPositionPostTransform =
179+
targetDb.elements.getElement<GeometricElement3d>(
180+
srcElemFedGuid!
181+
).placement;
182+
srcElemPositionPostTransform.multiplyTransform(targetEcef.getTransform());
183+
// assert that the element at the origin of sourceDb still has the same ecef location when transformed to targetDb
184+
expect(
185+
srcEcef.origin.isAlmostEqual(srcElemPositionPostTransform.origin),
186+
"Source element position's ecef location does not match target element position's ecef location after transform"
187+
).to.be.true;
188+
189+
targetDb.close();
190+
sourceDb.close();
191+
transfrom.dispose();
192+
});
193+
});

0 commit comments

Comments
 (0)