import { destroy, getParentOfType, getRoot, isAlive, types } from "mobx-state-tree";
|
|
import { guidGenerator } from "../core/Helpers";
|
import Tree, { TRAVERSE_SKIP } from "../core/Tree";
|
import Area from "../regions/Area";
|
import { isDefined } from "../utils/utilities";
|
|
const localStorageKeys = {
|
order: "relations:order",
|
};
|
|
/**
|
* Relation between two different nodes
|
*/
|
const Relation = types
|
.model("Relation", {
|
id: types.optional(types.identifier, guidGenerator),
|
|
node1: types.reference(Area),
|
node2: types.reference(Area),
|
|
direction: types.optional(types.enumeration(["left", "right", "bi"]), "right"),
|
|
// labels
|
labels: types.maybeNull(types.array(types.string)),
|
})
|
.volatile(() => ({
|
showMeta: false,
|
visible: true,
|
}))
|
.views((self) => ({
|
get parent() {
|
return getParentOfType(self, RelationStore);
|
},
|
|
get control() {
|
return self.parent.control;
|
},
|
|
get selectedValues() {
|
return self.labels?.filter((relationLabel) => {
|
return self.control?.values.includes(relationLabel);
|
});
|
},
|
|
get hasRelations() {
|
return self.control?.children?.length > 0;
|
},
|
|
get shouldRender() {
|
if (!isAlive(self)) return false;
|
const { node1: start, node2: end } = self;
|
const [sIdx, eIdx] = [start.item_index, end.item_index];
|
|
// as we don't currently have a unified solution for multi-object segmentation
|
// and the Image tag is the only one to support it, we rely on its API
|
// TODO: make multi-object solution more generic
|
if (isDefined(sIdx) && start.object.multiImage && sIdx !== start.object.currentImage) return false;
|
|
if (isDefined(eIdx) && end.object.multiImage && eIdx !== end.object.currentImage) return false;
|
|
return true;
|
},
|
}))
|
.actions((self) => ({
|
rotateDirection() {
|
const d = ["left", "right", "bi"];
|
let idx = d.findIndex((item) => item === self.direction);
|
|
idx = idx + 1;
|
if (idx >= d.length) idx = 0;
|
|
self.direction = d[idx];
|
},
|
|
toggleHighlight() {
|
if (self.node1 === self.node2) {
|
self.node1.toggleHighlight();
|
} else {
|
self.node1.toggleHighlight();
|
self.node2.toggleHighlight();
|
}
|
},
|
|
toggleMeta() {
|
self.showMeta = !self.showMeta;
|
},
|
|
setSelfHighlight(highlighted = false) {
|
if (highlighted) {
|
self.parent.setHighlight(self);
|
} else {
|
self.parent.removeHighlight();
|
}
|
},
|
|
toggleVisibility() {
|
self.visible = !self.visible;
|
},
|
|
setRelations(values) {
|
self.labels = values;
|
},
|
}));
|
|
const RelationStore = types
|
.model("RelationStore", {
|
relations: types.array(Relation),
|
order: types.optional(
|
types.enumeration(["asc", "desc"]),
|
window.localStorage.getItem(localStorageKeys.order) ?? "asc",
|
),
|
})
|
.volatile(() => ({
|
showConnections: true,
|
_highlighted: null,
|
control: null,
|
}))
|
.views((self) => ({
|
get highlighted() {
|
return self.relations.find((r) => r.id === self._highlighted);
|
},
|
get size() {
|
return self.relations.length;
|
},
|
get orderedRelations() {
|
if (!self.relations) return [];
|
if (self.order === "asc") {
|
return self.relations.slice();
|
}
|
return self.relations.slice().reverse();
|
},
|
get isAllHidden() {
|
return !self.relations.find((rl) => !rl.visible);
|
},
|
get values() {
|
return self.control?.values ?? [];
|
},
|
}))
|
.actions((self) => ({
|
afterAttach() {
|
const appStore = getRoot(self);
|
|
// find <Relations> tag in the tree
|
let relationsTag = null;
|
|
Tree.traverseTree(appStore.annotationStore.root, (node) => {
|
if (node.type === "relations") {
|
relationsTag = node;
|
return TRAVERSE_SKIP;
|
}
|
});
|
self.setControl(relationsTag);
|
},
|
setControl(relationsTag) {
|
self.control = relationsTag;
|
},
|
findRelations(node1, node2) {
|
const id1 = node1.id || node1;
|
const id2 = node2?.id || node2;
|
|
if (!id2) {
|
return self.relations.filter((rl) => {
|
return rl.node1.id === id1 || rl.node2.id === id1;
|
});
|
}
|
|
return self.relations.filter((rl) => {
|
return rl.node1.id === id1 && rl.node2.id === id2;
|
});
|
},
|
|
nodesRelated(node1, node2) {
|
return self.findRelations(node1, node2).length > 0;
|
},
|
|
addRelation(node1, node2) {
|
if (self.nodesRelated(node1, node2)) return;
|
|
const rl = Relation.create({ node1, node2 });
|
|
// self.relations.unshift(rl);
|
self.relations.push(rl);
|
|
return rl;
|
},
|
|
deleteRelation(rl) {
|
self.relations = self.relations.filter((r) => r.id !== rl.id);
|
destroy(rl);
|
},
|
|
deleteNodeRelation(node) {
|
// lookup $node and delete it's relation
|
const rl = self.findRelations(node);
|
|
rl.length && rl.forEach(self.deleteRelation);
|
},
|
|
deleteAllRelations() {
|
self.relations.forEach((rl) => destroy(rl));
|
self.relations = [];
|
},
|
|
serialize() {
|
return self.relations.map((r) => {
|
const s = {
|
from_id: r.node1.cleanId,
|
to_id: r.node2.cleanId,
|
type: "relation",
|
direction: r.direction,
|
};
|
|
if (r.selectedValues) s.labels = r.selectedValues;
|
|
return s;
|
});
|
},
|
|
deserializeRelation(node1, node2, direction, labels) {
|
const rl = self.addRelation(node1, node2);
|
|
if (!rl) return; // duplicated relation
|
|
rl.direction = direction;
|
rl.labels = labels;
|
},
|
|
toggleConnections() {
|
self.showConnections = !self.showConnections;
|
},
|
|
toggleOrder() {
|
self.order = self.order === "asc" ? "desc" : "asc";
|
window.localStorage.setItem(localStorageKeys.order, self.order);
|
},
|
|
toggleAllVisibility() {
|
const shouldBeHidden = !self.isAllHidden;
|
|
self.relations.forEach((rl) => {
|
if (rl.visible !== shouldBeHidden) {
|
rl.toggleVisibility();
|
}
|
});
|
},
|
|
setHighlight(relation) {
|
self._highlighted = relation.id;
|
},
|
|
removeHighlight() {
|
self._highlighted = null;
|
},
|
}));
|
|
export default RelationStore;
|