terriajs
Version:
Geospatial data visualization platform.
160 lines (143 loc) • 5.13 kB
text/typescript
import { computed } from "mobx";
import isDefined from "../../Core/isDefined";
import Result from "../../Core/Result";
import TerriaError from "../../Core/TerriaError";
import createStubCatalogItem from "../../Models/Catalog/createStubCatalogItem";
import { BaseModel } from "../../Models/Definition/Model";
import ModelFactory from "../../Models/Definition/ModelFactory";
import upsertModelFromJson from "../../Models/Definition/upsertModelFromJson";
import ModelReference from "../ModelReference";
import Trait, { TraitOptions } from "../Trait";
export interface ModelArrayTraitOptions extends TraitOptions {
factory?: ModelFactory;
}
export default function modelReferenceArrayTrait<T>(
options: ModelArrayTraitOptions
) {
return function (target: any, propertyKey: string) {
const constructor = target.constructor;
if (!constructor.traits) {
constructor.traits = {};
}
constructor.traits[propertyKey] = new ModelReferenceArrayTrait(
propertyKey,
options,
constructor
);
};
}
export class ModelReferenceArrayTrait extends Trait {
readonly decoratorForFlattened = computed.struct;
private factory: ModelFactory | undefined;
constructor(id: string, options: ModelArrayTraitOptions, parent: any) {
super(id, options, parent);
this.factory = options.factory;
}
// This can probably be converted to a general array handler.
// It takes an optional idProperty. If not specified, the values are themselves IDs.
// It ensures that each ID is unique and that the topmost stratum wins for a given ID.
// There can even be properties to control relative ordering of items in different strata.
getValue(model: BaseModel): ReadonlyArray<ModelReference> {
const strataTopToBottom = model.strataTopToBottom;
const result: ModelReference[] = [];
type IdToBool = { [key: string]: boolean };
const idMap: IdToBool = {};
const removedIds: IdToBool = {};
// Create a single array with all the unique model IDs.
for (let stratum of <IterableIterator<any>>strataTopToBottom.values()) {
const modelIdArray: ModelReference[] = stratum[this.id];
if (modelIdArray) {
modelIdArray.forEach((modelId) => {
if (ModelReference.isRemoved(modelId)) {
// This ID is removed in this stratum.
removedIds[modelId.removed] = true;
} else if (removedIds[modelId]) {
// This ID was removed by a stratum above this one, so ignore it.
return;
} else if (!idMap[modelId]) {
// This is the first time we've seen this ID, so add it
idMap[modelId] = true;
result.push(modelId);
}
});
}
}
// TODO: only freeze in debug builds?
// TODO: can we instead react to modifications of the array?
return Object.freeze(result);
}
fromJson(
model: BaseModel,
stratumName: string,
jsonValue: any
): Result<ReadonlyArray<string> | undefined> {
// TODO: support removals
if (!Array.isArray(jsonValue)) {
return Result.error(
new TerriaError({
title: "Invalid property",
message: `Property ${
this.id
} is expected to be an array but instead it is of type ${typeof jsonValue}.`
})
);
}
const errors: TerriaError[] = [];
const result = jsonValue
.map((jsonElement) => {
if (typeof jsonElement === "string") {
return jsonElement;
} else if (typeof jsonElement === "object") {
if (this.factory === undefined) {
errors.push(
new TerriaError({
title: "Cannot create Model",
message:
"A modelReferenceArrayTrait does not have a factory but it contains an embedded model that does not yet exist."
})
);
return;
}
const nestedModel = upsertModelFromJson(
this.factory,
model.terria,
model.uniqueId === undefined ? "/" : model.uniqueId,
stratumName,
jsonElement,
{}
).pushErrorTo(errors);
// Maybe this should throw if undefined?
return (
nestedModel?.uniqueId ??
createStubCatalogItem(model.terria).uniqueId!
);
} else {
errors.push(
new TerriaError({
title: "Invalid property",
message: `Elements of ${
this.id
} are expected to be strings or objects but instead are of type ${typeof jsonElement}.`
})
);
}
})
.filter(isDefined);
return new Result(
result,
TerriaError.combine(
errors,
`Error updating modelReferenceArrayTrait model "${model.uniqueId}" from JSON`
)
);
}
toJson(value: ReadonlyArray<ModelReference> | undefined): any {
return value;
}
isSameType(trait: Trait): boolean {
return (
trait instanceof ModelReferenceArrayTrait &&
trait.factory === this.factory
);
}
}