Skip to content

Commit 810046c

Browse files
authored
Allow Performing Transforms with additionalTransforms (#265)
If src and target imodel's have differing local Helmert transforms, transform target elements positions to maintain proper local positions. The target imodel "additionalTransform" will always be maintained, even if it is null. The source imodel when copied into the target will have a combination of both helmert transforms applied to its elements placements. This way the result will place the elements in a position that when the targets "additionalTransform" is applied it will be in the correct geographic position. If the target's "additionalTransform" is null, then the we are simply applying the src's helmert transform permanently to each elements position instead of just at render time. If the src and target have different non null transforms, then the resulting transform applied to the src elements will also negate the helmert transform applied by the target model. --------- Co-authored-by: Daniel Rodriguez <DanRod1999@users.noreply.github.com>
1 parent 74a408f commit 810046c

4 files changed

Lines changed: 921 additions & 138 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 transforming imodels with matching CRS, but differing local transforms",
4+
"packageName": "@itwin/imodel-transformer",
5+
"email": "'DanRod1999@users.noreply.github.com'",
6+
"dependentChangeType": "patch"
7+
}

common/api/imodel-transformer.api.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { EntityReference } from '@itwin/core-common';
2222
import { ExternalSourceAspect } from '@itwin/core-backend';
2323
import { ExternalSourceAspectProps } from '@itwin/core-common';
2424
import { FontProps } from '@itwin/core-common';
25+
import { Helmert2DWithZOffset } from '@itwin/core-common';
2526
import { Id64Arg } from '@itwin/core-bentley';
2627
import { Id64Array } from '@itwin/core-bentley';
2728
import { Id64Set } from '@itwin/core-bentley';
@@ -244,21 +245,24 @@ export interface IModelImportOptions {
244245
export class IModelTransformer extends IModelExportHandler {
245246
constructor(source: IModelDb | IModelExporter, target: IModelDb | IModelImporter, options?: IModelTransformOptions);
246247
protected addCustomChanges(_sourceDbChanges: ChangedInstanceIds): Promise<void>;
247-
calculateEcefTransform(srcDb: IModelDb, targetDb: IModelDb): Transform;
248+
calculateEcefTransform(): Transform | undefined;
249+
// (undocumented)
250+
calculateTransformFromHelmertTransforms(): Transform | undefined;
248251
combineElements(sourceElementIds: Id64Array, targetElementId: Id64String): void;
249252
// (undocumented)
250253
protected completePartiallyCommittedAspects(): void;
251254
// (undocumented)
252255
protected completePartiallyCommittedElements(): void;
253256
readonly context: IModelCloneContext;
257+
// (undocumented)
258+
static convertHelmertToTransform(helmert: Helmert2DWithZOffset | undefined): Transform;
254259
// @deprecated
255260
detectElementDeletes(): Promise<void>;
256261
// @deprecated
257262
detectRelationshipDeletes(): Promise<void>;
258263
static determineSyncType(sourceDb: IModelDb, targetDb: IModelDb,
259264
targetScopeElementId: Id64String): "forward" | "reverse";
260265
dispose(): void;
261-
ecefTransform?: Transform;
262266
protected _elementsWithExplicitlyTrackedProvenance: Set<string>;
263267
readonly exporter: IModelExporter;
264268
static forEachTrackedElement(args: {
@@ -357,7 +361,6 @@ export class IModelTransformer extends IModelExportHandler {
357361

358362
// @beta
359363
export interface IModelTransformOptions {
360-
alignECEFLocations?: boolean;
361364
argsForProcessChanges?: ProcessChangesOptions;
362365
branchRelationshipDataBehavior?: "unsafe-migrate" | "reject";
363366
cloneUsingBinaryGeometry?: boolean;
@@ -371,6 +374,7 @@ export interface IModelTransformOptions {
371374
preserveElementIdsForFiltering?: boolean;
372375
skipPropagateChangesToRootElements?: boolean;
373376
targetScopeElementId?: Id64String;
377+
tryAlignGeolocation?: boolean;
374378
wasSourceIModelCopiedToTarget?: boolean;
375379
}
376380

packages/transformer/src/IModelTransformer.ts

Lines changed: 166 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ import {
2323
YieldManager,
2424
} from "@itwin/core-bentley";
2525
import * as ECSchemaMetaData from "@itwin/ecschema-metadata";
26-
import { Point3d, Transform } from "@itwin/core-geometry";
26+
import {
27+
Angle,
28+
AxisIndex,
29+
Matrix3d,
30+
Point3d,
31+
Transform,
32+
Vector3d,
33+
} from "@itwin/core-geometry";
2734
import * as coreBackendPkgJson from "@itwin/core-backend/package.json";
2835
import {
2936
BriefcaseManager,
@@ -82,6 +89,7 @@ import {
8289
FontProps,
8390
GeometricElement3dProps,
8491
GeometricElementProps,
92+
Helmert2DWithZOffset,
8593
IModel,
8694
IModelError,
8795
ModelProps,
@@ -240,11 +248,14 @@ export interface IModelTransformOptions {
240248
* @default undefined
241249
*/
242250
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
251+
252+
/**
253+
* A flag that determines if spatial elements from the source db should be transformed if:
254+
* source and target iModel GCS/CRS data is the same, but they have differing additional transforms
255+
* source and target iModel ECEF locations differ
245256
* @default false
246257
*/
247-
alignECEFLocations?: boolean;
258+
tryAlignGeolocation?: boolean;
248259
}
249260

250261
/**
@@ -385,10 +396,15 @@ export class IModelTransformer extends IModelExportHandler {
385396
public readonly targetDb: IModelDb;
386397
/** The IModelTransformContext for this IModelTransformer. */
387398
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
399+
/** The transform to be applied to the placement of spatial elements
400+
* This transform should be applied when:
401+
* - source and target db have different ECEF locations
402+
* - source and target db have matching GCS/CRS data, but differing `geographicCoordinateSystem.additionalTransform.helmert2DWithZOffset`
403+
* @note for ECEF transforms, this can only be used when source and target are linearly located imodels
404+
* @note for non linearly located imodels, this transform will be a linear transform derived from Helmert Transforms from the src and target iModels.
405+
* @beta
390406
*/
391-
public ecefTransform?: Transform;
407+
private _linearSpatialTransform?: Transform;
392408
private _syncType?: SyncType;
393409

394410
/** The Id of the Element in the **target** iModel that represents the **source** repository as a whole and scopes its [ExternalSourceAspect]($backend) instances. */
@@ -599,7 +615,7 @@ export class IModelTransformer extends IModelExportHandler {
599615
options?.branchRelationshipDataBehavior ?? "reject",
600616
skipPropagateChangesToRootElements:
601617
options?.skipPropagateChangesToRootElements ?? true,
602-
alignECEFLocations: options?.alignECEFLocations ?? false,
618+
tryAlignGeolocation: options?.tryAlignGeolocation ?? false,
603619
};
604620
// check if authorization client is defined
605621
if (IModelHost.authorizationClient === undefined) {
@@ -667,9 +683,29 @@ export class IModelTransformer extends IModelExportHandler {
667683
(this.targetDb as any).codeValueBehavior = "exact";
668684
}
669685
/* eslint-enable @itwin/no-internal */
670-
this.ecefTransform = this._options.alignECEFLocations
671-
? this.calculateEcefTransform(this.sourceDb, this.targetDb)
672-
: undefined;
686+
if (this._options.tryAlignGeolocation) {
687+
if (
688+
this.sourceDb.geographicCoordinateSystem ||
689+
this.targetDb.geographicCoordinateSystem
690+
) {
691+
Logger.logTrace(
692+
loggerCategory,
693+
"Aligning Additional transforms between imodels due to imodels containing GeographicCoordinateSystem data"
694+
);
695+
this._linearSpatialTransform =
696+
this.calculateTransformFromHelmertTransforms();
697+
} else if (this.sourceDb.ecefLocation && this.targetDb.ecefLocation) {
698+
Logger.logTrace(
699+
loggerCategory,
700+
"Aligning ECEF Location's between imodels due to imodels not containing GeographicCoordinateSystem data"
701+
);
702+
this._linearSpatialTransform = this.calculateEcefTransform();
703+
} else
704+
Logger.logTrace(
705+
loggerCategory,
706+
"No Geolcation data to align, both GCS and ECEF are undefined"
707+
);
708+
}
673709
}
674710

675711
/** validates that the importer set on the transformer has the same values for its shared options as the transformer.
@@ -1587,7 +1623,7 @@ export class IModelTransformer extends IModelExportHandler {
15871623
}
15881624

15891625
if (
1590-
this.ecefTransform !== undefined &&
1626+
this._linearSpatialTransform !== undefined &&
15911627
sourceElement instanceof GeometricElement3d
15921628
) {
15931629
// can check the sourceElement since this IModelTransformer does not remap classes
@@ -1596,7 +1632,7 @@ export class IModelTransformer extends IModelExportHandler {
15961632
);
15971633

15981634
if (placement.isValid) {
1599-
placement.multiplyTransform(this.ecefTransform);
1635+
placement.multiplyTransform(this._linearSpatialTransform);
16001636
(targetElementProps as GeometricElement3dProps).placement = placement;
16011637
}
16021638
}
@@ -1610,45 +1646,137 @@ export class IModelTransformer extends IModelExportHandler {
16101646
* @returns Transform that converts relative coordinates in the source iModel to relative coordinates in the target iModel.
16111647
* @note This can only be used if both source and target iModels are linearly located
16121648
*/
1613-
public calculateEcefTransform(
1614-
srcDb: IModelDb,
1615-
targetDb: IModelDb
1649+
public calculateEcefTransform(): Transform | undefined {
1650+
const srcEcefLoc = this.sourceDb.ecefLocation;
1651+
const targetEcefLoc = this.targetDb.ecefLocation;
1652+
1653+
if (srcEcefLoc === undefined || targetEcefLoc === undefined) {
1654+
throw new IModelError(
1655+
IModelStatus.NoGeoLocation,
1656+
"Both source and target ECEF locations must be defined to calculate the transform."
1657+
);
1658+
}
1659+
if (srcEcefLoc.getTransform().isAlmostEqual(targetEcefLoc.getTransform())) {
1660+
Logger.logTrace(
1661+
loggerCategory,
1662+
"ECEF data is already aligned. No spatial transforms needed."
1663+
);
1664+
return undefined;
1665+
}
1666+
1667+
const srcSpatialToECEF = srcEcefLoc.getTransform(); // converts relative to ECEF in relation to source
1668+
const targetECEFToSpatial = targetEcefLoc.getTransform().inverse(); // converts ECEF to relative in relation to target
1669+
if (!targetECEFToSpatial) {
1670+
throw new IModelError(
1671+
IModelStatus.NoGeoLocation,
1672+
"Failed to invert target ECEF transform."
1673+
);
1674+
}
1675+
const ecefTransform =
1676+
targetECEFToSpatial.multiplyTransformTransform(srcSpatialToECEF); // chain both transforms
1677+
1678+
return ecefTransform;
1679+
}
1680+
1681+
public static convertHelmertToTransform(
1682+
helmert: Helmert2DWithZOffset | undefined
16161683
): Transform {
1617-
const srcEcefLoc = srcDb.ecefLocation;
1618-
const targetEcefLoc = targetDb.ecefLocation;
1684+
if (!helmert) {
1685+
return Transform.createIdentity();
1686+
}
1687+
1688+
const rotationXY = Matrix3d.createRotationAroundAxisIndex(
1689+
AxisIndex.Z,
1690+
Angle.createDegrees(helmert?.rotDeg)
1691+
);
1692+
rotationXY.scaleColumnsInPlace(helmert.scale, helmert.scale, 1.0);
1693+
const translation = Vector3d.create(
1694+
helmert.translationX,
1695+
helmert.translationY,
1696+
helmert.translationZ
1697+
);
1698+
const helmertTransform = Transform.createRefs(translation, rotationXY);
16191699

1700+
return helmertTransform;
1701+
}
1702+
1703+
public calculateTransformFromHelmertTransforms(): Transform | undefined {
1704+
if (
1705+
this.sourceDb.geographicCoordinateSystem?.horizontalCRS === undefined ||
1706+
this.sourceDb.geographicCoordinateSystem?.verticalCRS === undefined
1707+
) {
1708+
throw new IModelError(
1709+
IModelStatus.BadRequest,
1710+
"Source iModel does not have a geographic coordinate system defined."
1711+
);
1712+
}
16201713
if (
1621-
srcDb.geographicCoordinateSystem !== undefined ||
1622-
targetDb.geographicCoordinateSystem !== undefined
1714+
this.targetDb.geographicCoordinateSystem?.horizontalCRS === undefined ||
1715+
this.targetDb.geographicCoordinateSystem.verticalCRS === undefined
1716+
) {
1717+
throw new IModelError(
1718+
IModelStatus.BadRequest,
1719+
"Target iModel does not have a geographic coordinate system defined."
1720+
);
1721+
}
1722+
if (
1723+
!this.sourceDb.geographicCoordinateSystem.horizontalCRS.equals(
1724+
this.targetDb.geographicCoordinateSystem.horizontalCRS
1725+
) ||
1726+
!this.sourceDb.geographicCoordinateSystem.verticalCRS.equals(
1727+
this.targetDb.geographicCoordinateSystem.verticalCRS
1728+
)
16231729
) {
16241730
throw new IModelError(
16251731
IModelStatus.MismatchGcs,
1626-
"Both source and target geographic coordinate systems must not be defined to calculate the linear ecef transform."
1732+
"Source and target geographic coordinate systems must match to calculate the spatial transform."
16271733
);
16281734
}
1735+
if (
1736+
this.sourceDb.geographicCoordinateSystem.additionalTransform ===
1737+
this.targetDb.geographicCoordinateSystem.additionalTransform
1738+
) {
1739+
Logger.logTrace(
1740+
loggerCategory,
1741+
"Geolocation data is already aligned. No spatial transforms needed."
1742+
);
1743+
return undefined;
1744+
}
16291745

1630-
if (srcEcefLoc === undefined || targetEcefLoc === undefined) {
1746+
const srcScale =
1747+
this.sourceDb.geographicCoordinateSystem.additionalTransform
1748+
?.helmert2DWithZOffset?.scale ?? 1;
1749+
const targetScale =
1750+
this.targetDb.geographicCoordinateSystem.additionalTransform
1751+
?.helmert2DWithZOffset?.scale ?? 1;
1752+
1753+
if (srcScale !== targetScale) {
16311754
throw new IModelError(
1632-
IModelStatus.NoGeoLocation,
1633-
"Both source and target ECEF locations must be defined to calculate the transform."
1755+
IModelStatus.MismatchGcs,
1756+
"Spatial transform is non rigid. Source and target Helmert transforms must have the same scale to calculate a rigid spatial transform."
16341757
);
1635-
} else {
1636-
if (srcEcefLoc.getTransform().isAlmostEqual(targetEcefLoc.getTransform()))
1637-
return Transform.createIdentity();
1758+
}
16381759

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
1760+
const srcTransform = IModelTransformer.convertHelmertToTransform(
1761+
this.sourceDb.geographicCoordinateSystem.additionalTransform
1762+
?.helmert2DWithZOffset
1763+
); // moves elements to where src helmert transform would move them at render time
1764+
const targetTransformInv = IModelTransformer.convertHelmertToTransform(
1765+
this.targetDb.geographicCoordinateSystem.additionalTransform
1766+
?.helmert2DWithZOffset
1767+
).inverse(); // negates target helmert transform that is applied at render time
16491768

1650-
return ecefTransform;
1769+
if (!targetTransformInv) {
1770+
throw new IModelError(
1771+
IModelStatus.NoGeoLocation,
1772+
"Failed to invert target Helmert transform."
1773+
);
16511774
}
1775+
1776+
const combinedTransform =
1777+
targetTransformInv.multiplyTransformTransform(srcTransform);
1778+
1779+
return combinedTransform;
16521780
}
16531781

16541782
// if undefined, it can be initialized by calling [[this.processChangesets]]

0 commit comments

Comments
 (0)