import { destroy, isAlive, types } from "mobx-state-tree";
|
import { defaultStyle } from "../core/Constants";
|
import { guidGenerator } from "../core/Helpers";
|
import Result from "../regions/Result";
|
import { PER_REGION_MODES } from "./PerRegion";
|
import { ReadOnlyRegionMixin } from "./ReadOnlyMixin";
|
import { FF_LSDV_4930, FF_TAXONOMY_LABELING, isFF } from "../utils/feature-flags";
|
|
let ouid = 1;
|
|
export const AreaMixinBase = types
|
.model({
|
id: types.optional(types.identifier, guidGenerator),
|
ouid: types.optional(types.number, () => ouid++),
|
results: types.array(Result),
|
parentID: types.maybeNull(types.string),
|
})
|
.views((self) => ({
|
// self id without annotation id added to uniquiness across all the tree
|
get cleanId() {
|
return self.id.replace(/#.*/, "");
|
},
|
|
/**
|
* @return {Result[]} all results with labeling (created by *Labels control)
|
*/
|
get labelings() {
|
return self.results.filter((r) => r.from_name.isLabeling);
|
},
|
|
/**
|
* @return {Result?} first result with labels (usually it's the only one, but not always)
|
*/
|
get labeling() {
|
if (!isAlive(self)) {
|
return undefined;
|
}
|
return self.results.find((r) => r.from_name.isLabeling && r.hasValue);
|
},
|
|
get emptyLabel() {
|
return self.results.find((r) => r.from_name?.emptyLabel)?.from_name?.emptyLabel;
|
},
|
|
get texting() {
|
return isAlive(self) && self.results.find((r) => r.type === "textarea" && r.hasValue);
|
},
|
|
get tag() {
|
return self.labeling?.from_name;
|
},
|
|
hasLabel(value) {
|
const labels = self.labeling?.mainValue;
|
|
if (!labels || !value) return false;
|
// label can contain comma, so check for full match first
|
if (labels.includes(value)) return true;
|
if (value.includes(",")) {
|
return value.split(",").some((v) => labels.includes(v));
|
}
|
return false;
|
},
|
|
get perRegionTags() {
|
return self.annotation.toNames.get(self.object.name)?.filter((tag) => tag.perregion) || [];
|
},
|
|
// special tags that can be used for labeling (only <Taxonomy isLabeling/> for now)
|
get labelingTags() {
|
if (!isFF(FF_TAXONOMY_LABELING)) return [];
|
|
return self.annotation.toNames.get(self.object.name)?.filter((tag) => tag.classification && tag.isLabeling) || [];
|
},
|
|
get perRegionDescControls() {
|
return self.perRegionTags.filter((tag) => tag.displaymode === PER_REGION_MODES.REGION_LIST);
|
},
|
|
get perRegionFocusTarget() {
|
return self.perRegionTags.find((tag) => tag.isVisible !== false && tag.focusable);
|
},
|
|
get labelName() {
|
if (!isAlive(self)) {
|
return void 0;
|
}
|
return self.labeling?.mainValue?.[0] || self.emptyLabel?._value;
|
},
|
|
get labels() {
|
return Array.from(self.labeling?.mainValue ?? []);
|
},
|
|
// used only in labels on regions for Image and Video tags
|
getLabelText(joinstr) {
|
const index = self.region_index;
|
const label = self.labeling;
|
const text = self.texting?.mainValue?.[0]?.replace(/\n\r|\n/, " ");
|
const labelNames = label?.getSelectedString(joinstr);
|
const labelText = [];
|
|
if (index) labelText.push(String(index));
|
if (labelNames) labelText.push(labelNames);
|
if (text) labelText.push(text);
|
return labelText.join(": ");
|
},
|
|
get parent() {
|
if (!isAlive(self)) {
|
return void 0;
|
}
|
return self.object;
|
},
|
|
get style() {
|
if (!isAlive(self)) {
|
return void 0;
|
}
|
|
const styled = self.results.find((r) => r.style);
|
|
if (styled && styled.style) {
|
return styled.style;
|
}
|
const emptyStyled = self.results.find((r) => r.emptyStyle);
|
|
if (emptyStyled && emptyStyled.emptyStyle) {
|
return emptyStyled.emptyStyle;
|
}
|
|
const controlStyled = self.results.find((r) => self.type.startsWith(r.type));
|
|
return controlStyled && controlStyled.controlStyle;
|
},
|
|
// @todo may be slow, consider to add some code to annotation (un)select* methods
|
get selected() {
|
return self.annotation?.highlightedNode === self;
|
},
|
|
getOneColor() {
|
return (self.style || defaultStyle).fillcolor;
|
},
|
|
get highlighted() {
|
return self.parent?.selectionArea?.isActive ? self.isInSelectionArea : self._highlighted;
|
},
|
|
get isInSelectionArea() {
|
return (!isFF(FF_LSDV_4930) || !self.hidden) && self.parent?.selectionArea?.isActive
|
? self.parent.selectionArea.intersectsBbox(self.bboxCoords)
|
: false;
|
},
|
|
get supportSuggestions() {
|
return self.object.supportSuggestions;
|
},
|
|
// index of the region in the regions tree (Outliner); will be updated on any order change
|
get region_index() {
|
if (!self.isRealRegion) {
|
return null;
|
}
|
return self.annotation?.regionStore.regionIndexMap[self.id] || null;
|
},
|
}))
|
.actions((self) => ({
|
beforeDestroy() {
|
self.results.forEach((r) => destroy(r));
|
|
// Some region indexes have to be recalculated after destroying regions
|
self.annotation?.updateAppearenceFromState?.();
|
},
|
|
setSelected(value) {
|
self.selected = value;
|
},
|
|
/**
|
* Remove region
|
*/
|
deleteRegion() {
|
if (self.annotation.isReadOnly()) return;
|
if (self.isReadOnly()) return;
|
if (self.selected) self.annotation.unselectAll(true);
|
if (self.destroyRegion) self.destroyRegion();
|
self.annotation.deleteRegion(self);
|
},
|
|
addResult(r) {
|
self.results.push(r);
|
},
|
|
/**
|
* Applies additional data from the given result.
|
* In the results we have almost all data meaningful stored in value but in regions we have two places for it:
|
* - region itself (fields in model)
|
* - related results (in results array)
|
* so for some fields we should control more if we want to apply fields that could be in both places into the region.
|
* This method also helps to avoid region type detection at the deserialization stage.
|
*
|
* @param {Object} result - The result object containing additional data.
|
* @returns {void}
|
*/
|
applyAdditionalDataFromResult(_result) {
|
// This method should be overridden if we need to get some additional data from result on deserialize
|
},
|
|
removeResult(r) {
|
const index = self.results.indexOf(r);
|
|
if (index < 0) return;
|
self.results.splice(index, 1);
|
destroy(r);
|
if (!self.results.length) self.annotation.deleteArea(self);
|
},
|
|
setValue(tag) {
|
const result = self.results.find((r) => r.from_name === tag);
|
const values = tag.selectedValues();
|
|
if (result) {
|
if (tag.holdsState) result.setValue(values);
|
else self.removeResult(result);
|
} else {
|
self.results.push({
|
area: self,
|
from_name: tag,
|
to_name: self.object,
|
type: tag.resultType,
|
value: {
|
[tag.valueType]: values,
|
},
|
});
|
}
|
self.updateAppearenceFromState && self.updateAppearenceFromState();
|
},
|
}));
|
|
export const AreaMixin = types.compose("AreaMixin", AreaMixinBase, ReadOnlyRegionMixin);
|