red-ui/apps/red-ui/src/app/models/file/annotation.wrapper.ts

610 lines
24 KiB
TypeScript

import { annotationTypesTranslations, SuggestionAddFalsePositive } from '@translations/annotation-types-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import {
annotationDefaultColorConfig,
annotationEntityColorConfig,
AnnotationIconType,
ChangeTypes,
DefaultColors,
Dictionary,
Earmark,
FalsePositiveSuperTypes,
IComment,
IManualChange,
IPoint,
IRectangle,
LogEntryStatuses,
LowLevelFilterTypes,
ManualRedactionTypes,
SuggestionAddSuperTypes,
SuggestionRemoveSuperTypes,
SuggestionsSuperTypes,
SuperType,
SuperTypes,
} from '@red/domain';
import { RedactionLogEntry } from '@models/file/redaction-log.entry';
import { IListable, List } from '@iqser/common-ui';
import { chronologicallyBy, timestampOf } from '../../modules/file-preview/services/file-data.service';
export class AnnotationWrapper implements IListable, Record<string, unknown> {
[x: string]: unknown;
superType: SuperType;
typeValue: string;
recategorizationType: string;
color: string;
entity: Dictionary;
comments: IComment[] = [];
firstTopLeftPoint: IPoint;
annotationId: string;
shortContent: string;
content: string;
value: string;
typeLabel: string;
pageNumber: number;
hint: boolean;
redaction: boolean;
status: string;
dictionaryOperation: boolean;
positions: IRectangle[];
recommendationType: string;
legalBasisValue: string;
legalBasisChangeValue?: string;
rectangle?: boolean;
section?: string;
reference: List;
imported?: boolean;
image?: boolean;
manual?: boolean;
pending = false;
hintDictionary = false;
textAfter?: string;
textBefore?: string;
isChangeLogEntry = false;
changeLogType?: 'ADDED' | 'REMOVED' | 'CHANGED';
engines?: string[];
hasBeenResized: boolean;
hasBeenRecategorized: boolean;
hasLegalBasisChanged: boolean;
hasBeenForcedHint: boolean;
hasBeenForcedRedaction: boolean;
hasBeenRemovedByManualOverride: boolean;
get searchKey(): string {
return this.id;
}
get isChangeLogRemoved() {
return this.changeLogType === 'REMOVED';
}
get descriptor() {
return this.isModifyDictionary ? _('dictionary') : _('type');
}
get hasTextAfter() {
return this.textAfter && this.textAfter.trim().length > 0;
}
get hasTextBefore() {
return this.textBefore && this.textBefore.trim().length > 0;
}
get canBeMarkedAsFalsePositive() {
return (
(this.isRecommendation || this.superType === SuperTypes.Redaction) &&
!this.isImage &&
!this.imported &&
!this.pending &&
!this.hasBeenResized
);
}
get isSuperTypeBasedColor() {
return this.isSuggestion || this.isDeclinedSuggestion;
}
get isSkipped() {
return this.superType === SuperTypes.Skipped;
}
get isImage() {
return this.type?.toLowerCase() === 'image' || this.image;
}
get isNotSignatureImage() {
return this.isImage && this.recategorizationType === 'signature';
}
get isOCR() {
return this.type?.toLowerCase() === 'ocr';
}
get type() {
return this.recategorizationType || this.typeValue;
}
get topLevelFilter() {
return !LowLevelFilterTypes[this.superType];
}
get filterKey() {
if (this.isEarmark) {
return this.color;
}
return this.topLevelFilter ? this.superType : this.superType + this.type;
}
get isFalsePositive() {
return this.type?.toLowerCase() === 'false_positive' && !!FalsePositiveSuperTypes[this.superType];
}
get isSuggestionAddToFalsePositive() {
return this.typeLabel === annotationTypesTranslations[SuggestionAddFalsePositive];
}
get isDeclinedSuggestion() {
return this.superType === SuperTypes.DeclinedSuggestion;
}
get isApproved() {
return this.status === 'APPROVED';
}
get isHint() {
return this.superType === SuperTypes.Hint;
}
get isEarmark() {
return this.superType === SuperTypes.TextHighlight;
}
get iconShape(): AnnotationIconType {
if (this.isRecommendation) {
return 'hexagon';
}
if (this.isHint || this.isIgnoredHint) {
return 'circle';
}
if (this.isSuggestion || this.isDeclinedSuggestion) {
return 'rhombus';
}
return 'square';
}
get isIgnoredHint() {
return this.superType === SuperTypes.IgnoredHint;
}
get isRedacted() {
return this.superType === SuperTypes.Redaction || this.superType === SuperTypes.ManualRedaction;
}
get isSuggestion() {
return !!SuggestionsSuperTypes[this.superType];
}
get isSuggestionResize() {
return this.superType === SuperTypes.SuggestionResize;
}
get isSuggestionRecategorizeImage() {
return this.superType === SuperTypes.SuggestionRecategorizeImage;
}
get isSuggestionForceHint() {
return this.superType === SuperTypes.SuggestionForceHint;
}
get isSuggestionAdd() {
return !!SuggestionAddSuperTypes[this.superType];
}
get isSuggestionAddDictionary() {
return this.superType === SuperTypes.SuggestionAddDictionary;
}
get isSuggestionRemove() {
return !!SuggestionRemoveSuperTypes[this.superType];
}
get isSuggestionRemoveDictionary() {
return this.superType === SuperTypes.SuggestionRemoveDictionary;
}
get isSuggestionLegalBasisChange() {
return this.superType === SuperTypes.SuggestionChangeLegalBasis;
}
get isModifyDictionary() {
return this.dictionaryOperation;
}
get hasRedactionChanges(): boolean {
return (
this.hasBeenResized ||
this.hasBeenRecategorized ||
this.hasLegalBasisChanged ||
this.hasBeenForcedHint ||
this.hasBeenForcedRedaction ||
this.hasBeenRemovedByManualOverride
);
}
get isRecommendation() {
return this.superType === SuperTypes.Recommendation;
}
get id() {
return this.annotationId;
}
get x() {
return this.firstTopLeftPoint.x;
}
get y() {
return this.firstTopLeftPoint.y;
}
get legalBasis() {
return this.legalBasisChangeValue || this.legalBasisValue;
}
get width(): number {
return Math.floor(this.positions[0].width);
}
get height(): number {
return Math.floor(this.positions[0].height);
}
get previewAnnotation() {
return (
this.isRedacted ||
this.isSuggestionAdd ||
this.isSuggestionRemove ||
this.isSuggestionResize ||
this.isSuggestionLegalBasisChange ||
this.isSuggestionRecategorizeImage
);
}
static fromEarmark(earmark: Earmark) {
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.annotationId = earmark.id;
annotationWrapper.pageNumber = earmark.positions[0].page;
annotationWrapper.superType = SuperTypes.TextHighlight;
annotationWrapper.typeValue = SuperTypes.TextHighlight;
annotationWrapper.value = 'Imported';
annotationWrapper.color = earmark.hexColor;
annotationWrapper.positions = earmark.positions;
annotationWrapper.firstTopLeftPoint = earmark.positions[0]?.topLeft;
annotationWrapper.typeLabel = annotationTypesTranslations[annotationWrapper.superType];
return annotationWrapper;
}
static fromData(redactionLogEntry: RedactionLogEntry, dictionaries: Dictionary[], defaultColors: DefaultColors) {
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.annotationId = redactionLogEntry.id;
annotationWrapper.isChangeLogEntry = redactionLogEntry.isChangeLogEntry;
annotationWrapper.changeLogType = redactionLogEntry.changeLogType;
annotationWrapper.redaction = redactionLogEntry.redacted;
annotationWrapper.hint = redactionLogEntry.hint;
annotationWrapper.typeValue = redactionLogEntry.type;
annotationWrapper.value = redactionLogEntry.value;
annotationWrapper.firstTopLeftPoint = redactionLogEntry.positions[0]?.topLeft;
annotationWrapper.pageNumber = redactionLogEntry.positions[0]?.page;
annotationWrapper.positions = redactionLogEntry.positions;
annotationWrapper.textBefore = redactionLogEntry.textBefore;
annotationWrapper.textAfter = redactionLogEntry.textAfter;
annotationWrapper.dictionaryOperation = redactionLogEntry.dictionaryEntry;
annotationWrapper.image = redactionLogEntry.image;
annotationWrapper.imported = redactionLogEntry.imported;
annotationWrapper.legalBasisValue = redactionLogEntry.legalBasis;
annotationWrapper.comments = redactionLogEntry.comments || [];
annotationWrapper.manual = redactionLogEntry.manualChanges?.length > 0;
annotationWrapper.engines = redactionLogEntry.engines;
annotationWrapper.section = redactionLogEntry.section;
annotationWrapper.reference = redactionLogEntry.reference || [];
annotationWrapper.rectangle = redactionLogEntry.rectangle;
annotationWrapper.hintDictionary = redactionLogEntry.hintDictionary;
annotationWrapper.hasBeenResized = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.RESIZE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenRecategorized = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.RECATEGORIZE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasLegalBasisChanged = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenForcedHint = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.FORCE_HINT && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenForcedRedaction = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.FORCE_REDACT && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenRemovedByManualOverride = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.REMOVE_LOCALLY && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.legalBasisChangeValue = redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE && c.annotationStatus === LogEntryStatuses.REQUESTED,
)?.propertyChanges.legalBasis;
this.#createContent(annotationWrapper, redactionLogEntry);
this.#setSuperType(annotationWrapper, redactionLogEntry);
this.#handleRecommendations(annotationWrapper, redactionLogEntry);
annotationWrapper.typeLabel = this.#getTypeLabel(redactionLogEntry, annotationWrapper);
const entity = dictionaries.find(d => d.type === annotationWrapper.typeValue);
annotationWrapper.entity = entity?.virtual ? null : entity;
let colorKey = annotationWrapper.isSuperTypeBasedColor
? annotationDefaultColorConfig[annotationWrapper.superType]
: annotationEntityColorConfig[annotationWrapper.superType];
if (annotationWrapper.isSuperTypeBasedColor && annotationWrapper.isSuggestionResize && !annotationWrapper.isModifyDictionary) {
colorKey = annotationDefaultColorConfig[SuperTypes.SuggestionAdd];
}
annotationWrapper.color = annotationWrapper.isSuperTypeBasedColor ? defaultColors[colorKey] : (entity[colorKey] as string);
return annotationWrapper;
}
static #getTypeLabel(redactionLogEntry: RedactionLogEntry, annotation: AnnotationWrapper): string {
if (redactionLogEntry.reason?.toLowerCase() === 'false positive') {
return annotationTypesTranslations[SuggestionAddFalsePositive];
}
return annotationTypesTranslations[annotation.superType];
}
static #handleRecommendations(annotationWrapper: AnnotationWrapper, redactionLogEntry: RedactionLogEntry) {
if (annotationWrapper.superType === SuperTypes.Recommendation) {
annotationWrapper.recommendationType = redactionLogEntry.type;
}
}
static #getLastRelevantManualChange(manualChanges: IManualChange[]) {
return manualChanges[manualChanges.length - 1];
}
static #setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntry) {
if (redactionLogEntryWrapper.manualChanges?.length) {
const lastRelevantManualChange = this.#getLastRelevantManualChange(redactionLogEntryWrapper.manualChanges);
const viableChanges = redactionLogEntryWrapper.changes.filter(c => c.analysisNumber > 1);
const lastChange = viableChanges.sort(chronologicallyBy(x => x.dateTime)).at(-1);
const lastChangeOccurredAfterLastManualChange =
lastChange && timestampOf(lastChange.dateTime) > timestampOf(lastRelevantManualChange.processedDate);
if (lastChangeOccurredAfterLastManualChange && lastChange.type === ChangeTypes.ADDED && redactionLogEntryWrapper.redacted) {
annotationWrapper.superType = SuperTypes.Redaction;
return;
}
annotationWrapper.pending = !lastRelevantManualChange.processed;
annotationWrapper.superType = AnnotationWrapper.#selectSuperType(
redactionLogEntryWrapper,
lastRelevantManualChange,
annotationWrapper.hintDictionary,
);
if (lastRelevantManualChange.annotationStatus === LogEntryStatuses.REQUESTED) {
annotationWrapper.recategorizationType = lastRelevantManualChange.propertyChanges.type;
}
} else {
if (redactionLogEntryWrapper.recommendation) {
annotationWrapper.superType = SuperTypes.Recommendation;
} else if (redactionLogEntryWrapper.redacted) {
annotationWrapper.superType = SuperTypes.Redaction;
} else if (redactionLogEntryWrapper.hint) {
annotationWrapper.superType = SuperTypes.Hint;
} else {
annotationWrapper.superType = SuperTypes.Skipped;
}
}
}
static #createContent(annotationWrapper: AnnotationWrapper, entry: RedactionLogEntry) {
let content = '';
if (entry.matchedRule) {
content += `Rule ${entry.matchedRule} matched \n\n`;
}
if (entry.reason) {
content += entry.reason + '\n\n';
//remove leading and trailing commas and whitespaces
content = content.replace(/(^[, ]*)|([, ]*$)/g, '');
content = content.substring(0, 1).toUpperCase() + content.substring(1);
}
if (annotationWrapper.legalBasis) {
content += 'Legal basis: ' + annotationWrapper.legalBasis + '\n\n';
}
if (annotationWrapper.hasBeenRemovedByManualOverride) {
content += 'Removed by manual override';
}
if (entry.section) {
let prefix = 'In section: ';
if (content.length) {
prefix = ` ${prefix.toLowerCase()}`;
}
content += `${prefix} "${entry.section}"`;
}
annotationWrapper.shortContent = this.#getShortContent(annotationWrapper, entry) || content;
annotationWrapper.content = content;
}
static #getShortContent(annotationWrapper: AnnotationWrapper, entry: RedactionLogEntry) {
if (annotationWrapper.legalBasis) {
const lb = entry.legalBasisList?.find(lbm => lbm.reason?.toLowerCase().includes(annotationWrapper.legalBasis.toLowerCase()));
if (lb) {
return lb.name;
}
}
return annotationWrapper.legalBasis;
}
static #selectSuperType(redactionLogEntry: RedactionLogEntry, lastManualChange: IManualChange, isHintDictionary: boolean): SuperType {
switch (lastManualChange.manualRedactionType) {
case ManualRedactionTypes.ADD_LOCALLY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return SuperTypes.ManualRedaction;
case LogEntryStatuses.DECLINED:
return SuperTypes.DeclinedSuggestion;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionAdd;
}
break;
case ManualRedactionTypes.ADD_TO_DICTIONARY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Redaction;
case LogEntryStatuses.DECLINED:
return SuperTypes.DeclinedSuggestion;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionAddDictionary;
}
break;
case ManualRedactionTypes.REMOVE_LOCALLY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatuses.DECLINED: {
if (isHintDictionary) {
return SuperTypes.Hint;
}
if (redactionLogEntry.redacted) {
return SuperTypes.Redaction;
}
return SuperTypes.Skipped;
}
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemove;
}
break;
case ManualRedactionTypes.REMOVE_FROM_DICTIONARY:
if (redactionLogEntry.redacted) {
if (lastManualChange.processed) {
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return SuperTypes.Skipped;
case LogEntryStatuses.DECLINED:
return SuperTypes.Redaction;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
} else {
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
return SuperTypes.Redaction;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
}
} else {
if (lastManualChange.processed) {
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return redactionLogEntry.recommendation ? SuperTypes.Recommendation : SuperTypes.Skipped;
case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
} else {
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
}
}
break;
case ManualRedactionTypes.FORCE_REDACT:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return SuperTypes.Redaction;
case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionForceRedaction;
}
break;
case ManualRedactionTypes.FORCE_HINT:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return SuperTypes.Hint;
case LogEntryStatuses.DECLINED:
return SuperTypes.IgnoredHint;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionForceHint;
}
break;
case ManualRedactionTypes.RECATEGORIZE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED: {
if (redactionLogEntry.recommendation) {
return SuperTypes.Recommendation;
} else if (redactionLogEntry.redacted) {
return SuperTypes.Redaction;
} else if (redactionLogEntry.hint) {
return SuperTypes.Hint;
}
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
}
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRecategorizeImage;
}
break;
case ManualRedactionTypes.LEGAL_BASIS_CHANGE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
return redactionLogEntry.type === SuperTypes.ManualRedaction ? SuperTypes.ManualRedaction : SuperTypes.Redaction;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionChangeLegalBasis;
}
break;
case ManualRedactionTypes.RESIZE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
if (redactionLogEntry.recommendation) {
return SuperTypes.Recommendation;
} else if (redactionLogEntry.redacted) {
return redactionLogEntry.type === SuperTypes.ManualRedaction
? SuperTypes.ManualRedaction
: SuperTypes.Redaction;
} else if (redactionLogEntry.hint) {
return SuperTypes.Hint;
}
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionResize;
}
break;
}
}
}