wip: draw only changed annotations

This commit is contained in:
Dan Percic 2022-03-17 15:34:45 +02:00
parent c8a2e0b580
commit 7399f44881
12 changed files with 109 additions and 113 deletions

View File

@ -10,8 +10,11 @@ import {
SuperType,
SuperTypes,
} from '@models/file/super-types';
import { List } from '@iqser/common-ui';
export class AnnotationWrapper implements Record<string, unknown> {
[x: string]: unknown;
export class AnnotationWrapper {
superType: SuperType;
typeValue: string;
@ -33,10 +36,10 @@ export class AnnotationWrapper {
recommendationType: string;
legalBasisValue: string;
legalBasisChangeValue?: string;
resizing?: boolean;
resizing = false;
rectangle?: boolean;
section?: string;
reference: Array<string>;
reference: List;
imported?: boolean;
image?: boolean;
@ -58,7 +61,6 @@ export class AnnotationWrapper {
hasBeenForcedHint: boolean;
hasBeenForcedRedaction: boolean;
hasBeenRemovedByManualOverride: boolean;
redactionLogEntry: any;
get isChangeLogRemoved() {
return this.changeLogType === 'REMOVED';
@ -287,7 +289,6 @@ export class AnnotationWrapper {
this._handleRecommendations(annotationWrapper, redactionLogEntry);
annotationWrapper.typeLabel = annotationTypesTranslations[annotationWrapper.superType];
annotationWrapper.redactionLogEntry = redactionLogEntry;
return annotationWrapper;
}

View File

@ -7,7 +7,7 @@ import { UserPreferenceService } from '@services/user-preference.service';
import { ViewModeService } from '../../services/view-mode.service';
import { BehaviorSubject } from 'rxjs';
import { TextHighlightsGroup } from '@red/domain';
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
import { PdfViewer } from '../../services/pdf-viewer.service';
@Component({
selector: 'redaction-annotations-list',
@ -23,7 +23,6 @@ export class AnnotationsListComponent implements OnChanges {
@Output() readonly pagesPanelActive = new EventEmitter<boolean>();
@Output() readonly selectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
highlightGroups$ = new BehaviorSubject<TextHighlightsGroup[]>([]);
@ -33,7 +32,7 @@ export class AnnotationsListComponent implements OnChanges {
private readonly _filterService: FilterService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _viewModeService: ViewModeService,
private readonly _dialogService: FilePreviewDialogService,
private readonly _pdf: PdfViewer,
) {}
ngOnChanges(changes: SimpleChanges): void {
@ -58,7 +57,7 @@ export class AnnotationsListComponent implements OnChanges {
this.pagesPanelActive.emit(false);
if (this.isSelected(annotation.annotationId)) {
this.deselectAnnotations.emit([annotation]);
this._pdf.deselectAnnotations([annotation]);
} else {
const canMultiSelect = this.multiSelectService.isEnabled;
if (canMultiSelect && ($event?.ctrlKey || $event?.metaKey) && this.selectedAnnotations.length > 0) {

View File

@ -209,7 +209,6 @@
</ng-container>
<redaction-annotations-list
(deselectAnnotations)="deselectAnnotations.emit($event)"
(pagesPanelActive)="pagesPanelActive = $event"
(selectAnnotations)="selectAnnotations.emit($event)"
[activeViewerPage]="activeViewerPage"

View File

@ -35,6 +35,7 @@ import { FilePreviewStateService } from '../../services/file-preview-state.servi
import { ViewModeService } from '../../services/view-mode.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FileDataService } from '../../services/file-data.service';
import { PdfViewer } from '../../services/pdf-viewer.service';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -56,7 +57,6 @@ export class FileWorkloadComponent {
@Input() file!: File;
@Input() annotationActionsTemplate: TemplateRef<unknown>;
@Output() readonly selectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly selectPage = new EventEmitter<number>();
@Output() readonly annotationsChanged = new EventEmitter<AnnotationWrapper>();
displayedPages: number[] = [];
@ -78,6 +78,7 @@ export class FileWorkloadComponent {
readonly excludedPagesService: ExcludedPagesService,
readonly fileDataService: FileDataService,
private readonly _viewModeService: ViewModeService,
private readonly _pdf: PdfViewer,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _annotationProcessingService: AnnotationProcessingService,
) {
@ -170,7 +171,7 @@ export class FileWorkloadComponent {
}
deselectAllOnActivePage(): void {
this.deselectAnnotations.emit(this.activeAnnotations);
this._pdf.deselectAnnotations(this.activeAnnotations);
}
@HostListener('window:keyup', ['$event'])

View File

@ -66,7 +66,6 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
@Output() readonly pageChanged = new EventEmitter<number>();
@Output() readonly keyUp = new EventEmitter<KeyboardEvent>();
@Output() readonly viewerReady = new EventEmitter<WebViewerInstance>();
@Output() readonly annotationsChanged = new EventEmitter<AnnotationWrapper>();
@ViewChild('viewer', { static: true }) viewer: ElementRef;
@ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef;
@ -238,7 +237,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
const visibleAnnotations = await this._fileDataService.visibleAnnotations;
this.pdf.deselectAnnotations(visibleAnnotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)));
}
this.#configureAnnotationSpecificActions(annotations);
await this.#configureAnnotationSpecificActions(annotations);
this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly);
});
@ -694,7 +693,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private _setReadyAndInitialState(): void {
this._ngZone.run(() => {
this.viewerReady.emit(this.instance);
this.pdf.documentLoaded$.next(true);
const routePageNumber: number = this._activatedRoute.snapshot.queryParams.page;
this.pageChanged.emit(routePageNumber || 1);
this._setInitialDisplayMode();

View File

@ -63,11 +63,10 @@
<div class="content-container">
<redaction-pdf-viewer
(annotationSelected)="handleAnnotationSelected($event)"
(annotationsChanged)="annotationsChangedByReviewAction($event)"
(annotationsChanged)="annotationsChangedByReviewAction()"
(keyUp)="handleKeyEvent($event); handleArrowEvent($event)"
(manualAnnotationRequested)="openManualAnnotationDialog($event)"
(pageChanged)="viewerPageChanged($event)"
(viewerReady)="viewerReady()"
*ngIf="displayPdfViewer"
[canPerformActions]="canPerformAnnotationActions$ | async"
[class.hidden]="!ready"
@ -87,8 +86,7 @@
<redaction-file-workload
#fileWorkloadComponent
(annotationsChanged)="annotationsChangedByReviewAction($event)"
(deselectAnnotations)="deselectAnnotations($event)"
(annotationsChanged)="annotationsChangedByReviewAction()"
(selectAnnotations)="selectAnnotations($event)"
(selectPage)="selectPage($event)"
*ngIf="!file.excluded"
@ -105,7 +103,7 @@
<ng-template #annotationActionsTemplate let-annotation="annotation">
<redaction-annotation-actions
(annotationsChanged)="annotationsChangedByReviewAction($event)"
(annotationsChanged)="annotationsChangedByReviewAction()"
[annotations]="[annotation]"
[canPerformAnnotationActions]="canPerformAnnotationActions$ | async"
></redaction-annotation-actions>

View File

@ -18,19 +18,18 @@ import {
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from '../dossier/services/annotation-processing.service';
import { File, ViewMode } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { combineLatest, firstValueFrom, Observable, of, timer } from 'rxjs';
import { combineLatest, firstValueFrom, Observable, of, pairwise, timer } from 'rxjs';
import { UserPreferenceService } from '@services/user-preference.service';
import { clearStamps, download, handleFilterDelta, stampPDFPage } from '../../utils';
import { FileWorkloadComponent } from './components/file-workload/file-workload.component';
import { TranslateService } from '@ngx-translate/core';
import { FilesService } from '@services/entity-services/files.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { catchError, filter, map, startWith, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { WatermarkService } from '@shared/services/watermark.service';
import { ExcludedPagesService } from './services/excluded-pages.service';
@ -69,7 +68,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
readonly canPerformAnnotationActions$: Observable<boolean>;
readonly fileId = this.stateService.fileId;
readonly dossierId = this.stateService.dossierId;
readonly file$ = this.stateService.file$.pipe(tap(file => this._fileUpdated(file)));
readonly file$ = this.stateService.file$.pipe(tap(file => this._loadFileData(file)));
ready = false;
private _lastPage: string;
@ -112,6 +111,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
readonly documentInfoService: DocumentInfoService,
) {
super();
this.bla();
this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
document.documentElement.addEventListener('fullscreenchange', () => {
@ -283,10 +283,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
}
deselectAnnotations(annotations: AnnotationWrapper[]) {
this._pdf.deselectAnnotations(annotations);
}
selectPage(pageNumber: number) {
this._pdf.navigateToPage(pageNumber);
this._workloadComponent?.scrollAnnotationsToPage(pageNumber, 'always');
@ -299,23 +295,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
'manualAnnotation',
null,
{ manualRedactionEntryWrapper, dossierId: this.dossierId },
async (entryWrapper: ManualRedactionEntryWrapper) => {
const addAnnotation$ = this._manualAnnotationService
.addAnnotation(entryWrapper.manualRedactionEntry, this.dossierId, this.fileId)
.pipe(catchError(() => of(undefined)));
const addAnnotationResponse = await firstValueFrom(addAnnotation$);
const response = new ManualAnnotationResponse(entryWrapper, addAnnotationResponse);
if (response?.annotationId) {
this._pdf.deleteAnnotations([response.manualRedactionEntryWrapper.rectId]);
const distinctPages = manualRedactionEntryWrapper.manualRedactionEntry.positions
.map(p => p.page)
.filter((item, pos, self) => self.indexOf(item) === pos);
for (const page of distinctPages) {
await this._reloadAnnotationsForPage(page);
}
await this.updateViewMode();
}
async ({ manualRedactionEntry }: ManualRedactionEntryWrapper) => {
const addAnnotation$ = this._manualAnnotationService.addAnnotation(manualRedactionEntry, this.dossierId, this.fileId);
await firstValueFrom(addAnnotation$.pipe(catchError(() => of(undefined))));
await this._fileDataService.load(await this.stateService.file);
},
);
});
@ -384,12 +367,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._changeDetectorRef.markForCheck();
}
@Debounce()
async viewerReady() {
viewerReady() {
this.ready = true;
this._pdf.ready = true;
await this._reloadAnnotations();
this._setExcludedPageStyles();
this._pdf.documentViewer.addEventListener('pageComplete', () => {
@ -412,13 +393,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._changeDetectorRef.markForCheck();
}
async annotationsChangedByReviewAction(annotation?: AnnotationWrapper) {
async annotationsChangedByReviewAction() {
this.multiSelectService.deactivate();
const file = await this.stateService.file;
const fileReloaded = await firstValueFrom(this._filesService.reload(this.dossierId, file));
if (!fileReloaded) {
await this._reloadAnnotationsForPage(annotation?.pageNumber ?? this.activeViewerPage);
}
await firstValueFrom(this._filesService.reload(this.dossierId, file));
}
closeFullScreen() {
@ -443,6 +421,69 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
download(await firstValueFrom(originalFile), file.filename);
}
bla() {
const documentLoaded$ = this._pdf.documentLoaded$.pipe(tap(() => this.viewerReady()));
let start;
combineLatest([documentLoaded$, this._fileDataService.annotations$])
.pipe(
withLatestFrom(this.stateService.file$),
filter(([, file]) => !file.isProcessing),
tap(() => (start = new Date().getTime())),
map(([[, annotations]]) => annotations),
startWith([] as AnnotationWrapper[]),
pairwise(),
tap(annotations => this.deleteAnnotations(...annotations)),
switchMap(annotations => this.drawChangedAnnotations(...annotations)),
tap(() => console.log(`%c [ANNOTATIONS] Processing time: ${new Date().getTime() - start}`, 'color: aqua')),
tap(() => this.updateViewMode()),
)
.subscribe();
}
deleteAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
const annotationsToDelete = oldAnnotations.filter(
oldAnnotation => !newAnnotations.some(newAnnotation => newAnnotation.id === oldAnnotation.id),
);
if (annotationsToDelete.length === 0) {
return;
}
console.log('%c [ANNOTATIONS] To delete: ', 'color: aqua', annotationsToDelete);
this._pdf.deleteAnnotations(annotationsToDelete.map(annotation => annotation.id));
}
drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
const annotationsToDraw = newAnnotations.filter(newAnnotation => {
const oldAnnotation = oldAnnotations.find(annotation => annotation.id === newAnnotation.id);
if (!oldAnnotation) {
return true;
}
const changed = JSON.stringify(oldAnnotation) !== JSON.stringify(newAnnotation);
if (changed && this.userPreferenceService.areDevFeaturesEnabled) {
import('@iqser/common-ui').then(commonUi => {
console.log('%c [ANNOTATIONS] Changed annotation: ', 'color: aqua', {
value: oldAnnotation.value,
before: commonUi.deepDiffObj(newAnnotation, oldAnnotation),
after: commonUi.deepDiffObj(oldAnnotation, newAnnotation),
});
});
}
return changed;
});
if (annotationsToDraw.length === 0) {
return firstValueFrom(of({}));
}
console.log('%c [ANNOTATIONS] To draw: ', 'color: aqua', annotationsToDraw);
const annotationsToDrawIds = annotationsToDraw.map(a => a.annotationId);
this._pdf.deleteAnnotations(annotationsToDrawIds);
return this._cleanupAndRedrawAnnotations(annotation => annotationsToDrawIds.includes(annotation.annotationId));
}
async #deactivateMultiSelect() {
this.multiSelectService.deactivate();
this._pdf.deselectAllAnnotations();
@ -524,11 +565,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
}
private async _fileUpdated(file: File): Promise<void> {
await this._loadFileData(file);
await this._reloadAnnotations();
}
private _subscribeToFileUpdates(): void {
this.addActiveScreenSubscription = timer(0, 5000)
.pipe(
@ -583,31 +619,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._workloadComponent?.scrollAnnotations();
}
private async _reloadAnnotations() {
if (!this._fileDataService.shouldUpdateAnnotations) {
console.log('skip reloading annotations');
return;
}
this._pdf.deleteAnnotations();
await this._cleanupAndRedrawAnnotations();
await this.updateViewMode();
}
private async _reloadAnnotationsForPage(page: number) {
const file = await this.stateService.file;
// if this action triggered a re-processing,
// we don't want to redraw for this page since they will get redrawn as soon as processing ends;
if (file.isProcessing) {
return;
}
const currentPageAnnotations = (await this._fileDataService.visibleAnnotations).filter(a => a.pageNumber === page);
await this._fileDataService.loadRedactionLog();
this._pdf.deleteAnnotations(currentPageAnnotations.map(a => a.id));
await this._cleanupAndRedrawAnnotations(annotation => annotation.pageNumber === page);
}
private async _cleanupAndRedrawAnnotations(newAnnotationsFilter?: (annotation: AnnotationWrapper) => boolean) {
if (!this._pdf.ready) {
return;
@ -626,7 +637,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
await this._annotationDrawService.drawAnnotations(newAnnotations);
console.log(`[REDACTION] Annotations redraw time: ${new Date().getTime() - startTime} ms for ${newAnnotations.length} annotations`);
console.log(
`%c [ANNOTATIONS] Redraw time: ${new Date().getTime() - startTime} ms for ${newAnnotations.length} annotations`,
'color: aqua',
);
}
private _handleDeltaAnnotationFilters(currentFilters: NestedFilter[], newAnnotations: AnnotationWrapper[]) {

View File

@ -96,8 +96,8 @@ export class AnnotationDrawService {
await document.lock();
const annotations = annotationWrappers.map(annotation => this._computeAnnotation(annotation)).filter(a => !!a);
this._pdf.instance.Core.annotationManager.addAnnotations(annotations, { imported: true });
await this._pdf.instance.Core.annotationManager.drawAnnotationsFromList(annotations);
this._pdf.annotationManager.addAnnotations(annotations, { imported: true });
await this._pdf.annotationManager.drawAnnotationsFromList(annotations);
if (this._userPreferenceService.areDevFeaturesEnabled) {
const { dossierId, fileId } = this._state;

View File

@ -22,7 +22,7 @@ import { DictionariesMapService } from '../../../services/entity-services/dictio
import { catchError, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { PermissionsService } from '../../../services/permissions.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { log, shareLast, Toaster } from '../../../../../../../libs/common-ui/src';
import { shareLast, Toaster } from '../../../../../../../libs/common-ui/src';
import { RedactionLogService } from '../../dossier/services/redaction-log.service';
import { TextHighlightService } from '../../dossier/services/text-highlight.service';
import { ViewModeService } from './view-mode.service';
@ -41,8 +41,6 @@ export class FileDataService {
readonly visibleAnnotations$: Observable<AnnotationWrapper[]>;
readonly hiddenAnnotations = new Set<string>();
#redactionLog: IRedactionLog;
#redactionLogHash = '';
readonly #redactionLog$ = new BehaviorSubject<IRedactionLog>({});
readonly #textHighlights$ = new BehaviorSubject<AnnotationWrapper[]>([]);
@ -66,7 +64,6 @@ export class FileDataService {
this.annotations$.pipe(map(annotations => this.getVisibleAnnotations(annotations, viewMode))),
),
),
log('visible annotations: '),
shareLast(),
);
}
@ -87,6 +84,7 @@ export class FileDataService {
return this.#redactionLog$.pipe(
withLatestFrom(this._state.file$),
map(([redactionLog, file]) => this.#buildAnnotations(redactionLog, file)),
tap(() => this.checkMissingTypes()),
map(annotations =>
this._userPreferenceService.areDevFeaturesEnabled ? annotations : annotations.filter(a => !a.isFalsePositive),
),
@ -94,30 +92,18 @@ export class FileDataService {
);
}
setRedactionLog(redactionLog: IRedactionLog) {
this.#redactionLog = redactionLog;
const hash = require('object-hash');
const newRedactionLogHash = hash(redactionLog.redactionLogEntry ?? []);
if (newRedactionLogHash === this.#redactionLogHash) {
this.shouldUpdateAnnotations = false;
return;
}
this.shouldUpdateAnnotations = true;
this.#redactionLogHash = newRedactionLogHash;
this.missingTypes.clear();
}
async load(file: File) {
this.viewedPages = await firstValueFrom(this.getViewedPagesFor(file));
await this.loadRedactionLog();
}
checkMissingTypes() {
if (this.missingTypes.size > 0) {
this._toaster.error(_('error.missing-types'), {
disableTimeOut: true,
params: { missingTypes: Array.from(this.missingTypes).join(', ') },
});
this.missingTypes.clear();
}
}
@ -137,14 +123,14 @@ export class FileDataService {
return of([] as IViewedPage[]);
}
async loadRedactionLog() {
loadRedactionLog() {
const redactionLog$ = this._redactionLogService.getRedactionLog(this._state.dossierId, this._state.fileId).pipe(
tap(redactionLog => redactionLog.redactionLogEntry.sort((a, b) => a.positions[0].page - b.positions[0].page)),
catchError(() => of({})),
tap(redactionLog => this.#redactionLog$.next(redactionLog)),
);
this.setRedactionLog(await firstValueFrom(redactionLog$));
return firstValueFrom(redactionLog$);
}
getVisibleAnnotations(annotations: AnnotationWrapper[], viewMode: ViewMode) {

View File

@ -1,4 +1,4 @@
import { translateQuads } from '../../../utils/pdf-coordinates';
import { translateQuads } from '../../../utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import WebViewer, { Core, WebViewerInstance } from '@pdftron/webviewer';
import { ViewModeService } from './view-mode.service';
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@angular/core';
import { BASE_HREF } from '../../../tokens';
import { environment } from '@environments/environment';
import { DISABLED_HOTKEYS } from '../shared/constants';
import { Subject } from 'rxjs';
import Annotation = Core.Annotations.Annotation;
import DocumentViewer = Core.DocumentViewer;
import AnnotationManager = Core.AnnotationManager;
@ -17,6 +18,7 @@ export class PdfViewer {
instance: WebViewerInstance;
documentViewer: DocumentViewer;
annotationManager: AnnotationManager;
readonly documentLoaded$ = new Subject();
constructor(@Inject(BASE_HREF) private readonly _baseHref: string, readonly viewModeService: ViewModeService) {}

View File

@ -1,7 +1,6 @@
import { Injectable, Injector } from '@angular/core';
import { Injectable } from '@angular/core';
import { UserService } from './user.service';
import { Dossier, File, IComment, IDossier } from '@red/domain';
import { ActivatedRoute } from '@angular/router';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FeaturesService } from '@services/features.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
@ -10,8 +9,6 @@ import { DOSSIERS_ARCHIVE } from '@utils/constants';
export class PermissionsService {
constructor(
private readonly _userService: UserService,
private readonly _route: ActivatedRoute,
private readonly _injector: Injector,
private readonly _filesMapService: FilesMapService,
private readonly _featuresService: FeaturesService,
) {}

@ -1 +1 @@
Subproject commit 8a992aa440ff24d1244e24edea3ce75fdadbebd5
Subproject commit 3e1b124bd9439fabf1eaa67e265c212ad9cb09e7