Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a possibility to re-add deleted elements with preserved ids #209

Merged
merged 33 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ecdea59
first commit
derbynn Sep 25, 2024
bf1e63d
remove unused imports
derbynn Sep 25, 2024
ea420ad
add validation to onExportElement
derbynn Sep 26, 2024
60f2f20
remove unnecessary check, update comment
derbynn Sep 26, 2024
ff7a432
update to check targetElementId instead of calling
derbynn Sep 27, 2024
13aa914
Change files
derbynn Sep 27, 2024
ad3bdbd
Merge branch 'main' into daniel/preserved-ids-for-filtering
derbynn Sep 27, 2024
9c0705e
update test to cover additional case for possible duplicate
derbynn Sep 30, 2024
b76adfa
Merge branch 'daniel/preserved-ids-for-filtering' of https://github.c…
derbynn Sep 30, 2024
7759b91
update description instead - updating code will not give duplicate error
derbynn Sep 30, 2024
08217bd
Improve error description
derbynn Sep 30, 2024
17917dd
fix id property
derbynn Sep 30, 2024
d8e60c8
optimize call for tryGetElementProps
derbynn Sep 30, 2024
4cc2611
add function to catch potential errors
derbynn Sep 30, 2024
29c1ef3
add markElementAsUpdate function and fromTransformation option in imp…
derbynn Sep 30, 2024
e82009c
update comments and make elementsToUpdate private
derbynn Oct 1, 2024
030db68
Improve elementToUpdate set comments
derbynn Oct 3, 2024
41ac9a5
Improve elementToUpdate set comments
derbynn Oct 3, 2024
d284496
Improve fromTransformation option description
derbynn Oct 3, 2024
1bb52a8
Update elementsToUpdate set description
derbynn Oct 3, 2024
77f0a5d
add 2 additional test cases for preserveElementIdsForFiltering
derbynn Oct 4, 2024
94996d7
add additional check
derbynn Oct 4, 2024
e6a4fc6
add eslint-disable-next-line for nativeDb line
derbynn Oct 4, 2024
f324365
use GetElement instead of tryGetElement
derbynn Oct 7, 2024
6aa4599
replace double negative variable
derbynn Oct 7, 2024
2fda8ac
add test for updating elem props if elem exists
derbynn Oct 7, 2024
0212295
remove fromTransformation and preserveElementIdsForFiltering from imp…
derbynn Oct 8, 2024
8249907
preserveElementIdsForFiltering now transform options
derbynn Oct 8, 2024
df48dd6
Make set private, add getter, update names
derbynn Oct 8, 2024
e5bc1c2
update comment
derbynn Oct 8, 2024
554f14f
updated tests
derbynn Oct 9, 2024
ff99a0f
re-add preserveElementIdsForFiltering option
derbynn Oct 9, 2024
59bd2e2
Merge branch 'main' into daniel/preserved-ids-for-filtering
derbynn Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "add more cases to support preserveElementIdsForFiltering options",
"packageName": "@itwin/imodel-transformer",
"email": "Daniel.Erbynn@bentley.com",
"dependentChangeType": "patch"
}
84 changes: 64 additions & 20 deletions packages/transformer/src/IModelImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export interface IModelImportOptions {
* @see [IModelImporter Options]($docs/learning/transformer/index.md#IModelImporter)
*/
autoExtendProjectExtents?: boolean | { excludeOutliers: boolean };
/**
* Indicates whether the importer is used in a transformation process.
* If `true`, the importer was passed into the transformer, or the transformer constructed this instance of the importer
*/
fromTransformation?: boolean;
/** See [IModelTransformOptions]($transformer) */
preserveElementIdsForFiltering?: boolean;
/** If `true`, simplify the element geometry for visualization purposes. For example, convert b-reps into meshes.
Expand Down Expand Up @@ -103,6 +108,20 @@ export class IModelImporter {
*/
private _duplicateCodeValueMap: Map<Id64String, string>;

/**
* A set of elementIds that the transformer adds to while exporting elements to indicate that the element already exists in the target.
* Defaults to an empty set.
* @note
*
* This is used as an optimization when `preserveElementIdsForFiltering` is set to `true`
* In normal cases where this option set to `false`,
* the importer determines whether to insert or update based off of whether the ID is defined on the `elementProps` passed to `importElement`.
* However, with `preserveElementIdsForFiltering` set to `true`, IDs are always set, so we can't determine insert/update like the normal case.
* The transformer already knows if an element exists or not by the time `importElement` is called and pushes to this set with `markElementAsUpdate`.
* @note This set should stay small, as right after the transformer pushes to it, the importer will remove from the set.
*/
private _elementsToUpdate = new Set<Id64String>([]);

/** The set of elements that should not be updated by this IModelImporter.
* Defaults to an empty set.
* @note Adding an element to this set is typically necessary when remapping a source element to one that already exists in the target and already has the desired properties.
Expand Down Expand Up @@ -133,6 +152,7 @@ export class IModelImporter {
this.targetDb = targetDb;
this.options = {
autoExtendProjectExtents: options?.autoExtendProjectExtents ?? true,
fromTransformation: options?.fromTransformation ?? false,
preserveElementIdsForFiltering:
options?.preserveElementIdsForFiltering ?? false,
simplifyElementGeometry: options?.simplifyElementGeometry ?? false,
Expand All @@ -155,6 +175,16 @@ export class IModelImporter {
return false;
}

/**
* Marks an element so that it can be updated during import when [[IModelImportOptions.preserveElementIdsForFiltering]] is set to true
*/
public markElementAsUpdate(elementId: Id64String) {
if (this._rootElementIds.has(elementId)) {
return;
}
this._elementsToUpdate.add(elementId);
}

/** Import the specified ModelProps (either as an insert or an update) into the target iModel. */
public importModel(modelProps: ModelProps): void {
if (undefined === modelProps.id || !Id64.isValidId64(modelProps.id))
Expand Down Expand Up @@ -229,6 +259,25 @@ export class IModelImporter {

/** Import the specified ElementProps (either as an insert or an update) into the target iModel. */
public importElement(elementProps: ElementProps): Id64String {
const tryUpdateElement = (elemProps: ElementProps): void => {
try {
this.onUpdateElement(elemProps);
} catch (err) {
if ((err as IModelError).errorNumber === IModelStatus.DuplicateCode) {
assert(
elemProps.code.value !== undefined,
"NULL code values are always considered unique and cannot clash"
);
this._duplicateCodeValueMap.set(elemProps.id!, elemProps.code.value);
// Using NULL code values as an alternative is not valid because definition elements cannot have NULL code values.
elemProps.code.value = Guid.createValue();
this.onUpdateElement(elemProps);
} else {
throw err;
}
}
};

if (
undefined !== elementProps.id &&
this.doNotUpdateElement(elementProps.id)
Expand All @@ -246,6 +295,7 @@ export class IModelImporter {
"elementProps.id must be defined during a preserveIds operation"
);
}

// Categories are the only element that onInserted will immediately insert a new element (their default subcategory)
// since default subcategories always exist and always will be inserted after their categories, we treat them as an update
// to prevent duplicate inserts.
Expand All @@ -257,29 +307,23 @@ export class IModelImporter {
) {
this.onUpdateElement(elementProps);
} else {
this.onInsertElement(elementProps);
// if [[IModelImportOptions.fromTransformation]] is false, try and get the element (update if it exists)
const shouldUpdateElement =
!this.options.fromTransformation &&
this.targetDb.elements.tryGetElement(elementProps.id);
if (
this._elementsToUpdate.has(elementProps.id) ||
shouldUpdateElement
) {
tryUpdateElement(elementProps);
this._elementsToUpdate.delete(elementProps.id);
} else {
this.onInsertElement(elementProps);
}
}
} else {
if (undefined !== elementProps.id) {
try {
this.onUpdateElement(elementProps);
} catch (err) {
if ((err as IModelError).errorNumber === IModelStatus.DuplicateCode) {
assert(
elementProps.code.value !== undefined,
"NULL code values are always considered unique and cannot clash"
);
this._duplicateCodeValueMap.set(
elementProps.id,
elementProps.code.value
);
// Using NULL code values as an alternative is not valid because definition elements cannot have NULL code values.
elementProps.code.value = Guid.createValue();
this.onUpdateElement(elementProps);
} else {
throw err;
}
}
tryUpdateElement(elementProps);
} else {
this.onInsertElement(elementProps); // targetElementProps.id assigned by insertElement
}
Expand Down
34 changes: 30 additions & 4 deletions packages/transformer/src/IModelTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@ export class IModelTransformer extends IModelExportHandler {
this.validateSharedOptionsMatch();
}
this.targetDb = this.importer.targetDb;
this.importer.options.fromTransformation = true;
// create the IModelCloneContext, it must be initialized later
this.context = new IModelCloneContext(this.sourceDb, this.targetDb);

Expand Down Expand Up @@ -1882,10 +1883,7 @@ export class IModelTransformer extends IModelExportHandler {
public override onExportElement(sourceElement: Element): void {
let targetElementId: Id64String;
let targetElementProps: ElementProps;
if (this._options.preserveElementIdsForFiltering) {
targetElementId = sourceElement.id;
targetElementProps = this.onTransformElement(sourceElement);
} else if (this._options.wasSourceIModelCopiedToTarget) {
if (this._options.wasSourceIModelCopiedToTarget) {
targetElementId = sourceElement.id;
targetElementProps =
this.targetDb.elements.getElementProps(targetElementId);
Expand Down Expand Up @@ -1944,6 +1942,34 @@ export class IModelTransformer extends IModelExportHandler {
? targetElementId
: undefined;

if (this._options.preserveElementIdsForFiltering) {
const isInvalid = !Id64.isValid(targetElementId);

if (!isInvalid && targetElementId !== sourceElement.id) {
// Element found with different id
throw new Error(
`Element id(${sourceElement.id}) cannot be preserved. Found a different mapping(${targetElementId}) from source element`
);
} else if (!isInvalid && targetElementId === sourceElement.id) {
// targetElementId is valid (indicating update)
this.importer.markElementAsUpdate(sourceElement.id);
} else if (isInvalid) {
const sourceInTargetElemProps =
this.targetDb.elements.tryGetElementProps(sourceElement.id);

// if we don't find mapping for source element in target(invalid) but another element with source id exists in target
if (sourceInTargetElemProps) {
// Element id is already taken by another element
throw new Error(
`Element id(${sourceElement.id}) cannot be preserved. An unrelated element in the target already uses id: ${sourceElement.id}`
);
} else {
// Element id in target is available to be remapped
targetElementProps.id = sourceElement.id;
}
}
}

if (!this._options.wasSourceIModelCopiedToTarget) {
this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
}
Expand Down
Loading
Loading