diff --git a/libs/vre/shared/app-representations/src/lib/region.service.ts b/libs/vre/shared/app-representations/src/lib/region.service.ts index c9f22382af..0591b234d7 100644 --- a/libs/vre/shared/app-representations/src/lib/region.service.ts +++ b/libs/vre/shared/app-representations/src/lib/region.service.ts @@ -2,15 +2,16 @@ import { Injectable } from '@angular/core'; import { ReadResourceSequence } from '@dasch-swiss/dsp-js'; import { DspResource, GenerateProperty } from '@dasch-swiss/vre/shared/app-common'; import { IncomingService } from '@dasch-swiss/vre/shared/app-common-to-move'; -import { BehaviorSubject, of } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { map, takeUntil, tap } from 'rxjs/operators'; /** * Regions, also called annotations, are used to mark specific areas on an image. + * This service handles the loading, display and selection of a resource's regions. */ @Injectable() export class RegionService { - private _resource!: DspResource; + private _resourceId!: string; private _regionsSubject = new BehaviorSubject([]); regions$ = this._regionsSubject.asObservable(); @@ -22,47 +23,45 @@ export class RegionService { private _showRegions = new BehaviorSubject(false); showRegions$ = this._showRegions.asObservable(); - private _highlightRegion = new BehaviorSubject(null); - highlightRegion$ = this.showRegions$.pipe( - switchMap(value => (value ? this._highlightRegion.asObservable() : of(null))) - ); + private _selectedRegion = new BehaviorSubject(null); + selectedRegion$ = this._selectedRegion.asObservable(); - private _imageIsLoadedSubject = new BehaviorSubject(false); - imageIsLoaded$ = this._imageIsLoadedSubject.asObservable(); + private _ngUnsubscribe = new Subject(); constructor(private _incomingService: IncomingService) {} - onInit(resource: DspResource) { - this._resource = resource; - this.updateRegions(); + /** This method acts as a constructor. */ + initialize(resourceId: string) { + this._resourceId = resourceId; + this.updateRegions$().pipe(takeUntil(this._ngUnsubscribe)).subscribe(); } - showRegions(value: boolean) { - this._showRegions.next(value); + updateRegions$() { + return this._getIncomingRegions(this._resourceId).pipe( + tap(res => { + this._regionsSubject.next(res); + }) + ); } - updateRegions() { - this._incomingService - .getIncomingRegions(this._resource.res.id, 0) - .pipe( - map(regions => - (regions as ReadResourceSequence).resources.map(_resource => { - const z = new DspResource(_resource); - z.resProps = GenerateProperty.regionProperty(_resource); - return z; - }) - ) - ) - .subscribe(res => { - this._regionsSubject.next(res); - }); + showRegions(value: boolean) { + this._showRegions.next(value); } - highlightRegion(regionIri: string) { - this._highlightRegion.next(regionIri); + selectRegion(regionIri: string) { + this._selectedRegion.next(regionIri); } - imageIsLoaded() { - this._imageIsLoadedSubject.next(true); + private _getIncomingRegions(resourceId: string) { + const offset = 0; + return this._incomingService.getIncomingRegions(resourceId, offset).pipe( + map(regions => + (regions as ReadResourceSequence).resources.map(_resource => { + const z = new DspResource(_resource); + z.resProps = GenerateProperty.regionProperty(_resource); + return z; + }) + ) + ); } } diff --git a/libs/vre/shared/app-representations/src/lib/still-image/open-sea-dragon.service.ts b/libs/vre/shared/app-representations/src/lib/still-image/open-sea-dragon.service.ts index 8f15fd6b63..12e61ad8f3 100644 --- a/libs/vre/shared/app-representations/src/lib/still-image/open-sea-dragon.service.ts +++ b/libs/vre/shared/app-representations/src/lib/still-image/open-sea-dragon.service.ts @@ -1,25 +1,48 @@ import { Injectable } from '@angular/core'; +import { Point2D } from '@dasch-swiss/dsp-js'; +import { AppError } from '@dasch-swiss/vre/shared/app-error-handler'; import { AccessTokenService } from '@dasch-swiss/vre/shared/app-session'; -import * as OpenSeadragon from 'openseadragon'; +import OpenSeadragon from 'openseadragon'; +import { Subject } from 'rxjs'; import { osdViewerConfig } from './osd-viewer.config'; -@Injectable() -export class OpenSeaDragonService { - private _viewer!: OpenSeadragon.Viewer; +interface Overlay { + startPoint: Point2D; + endPoint: Point2D; + imageSize: Point2D; + overlay: Element; +} - get viewer(): OpenSeadragon.Viewer { +@Injectable({ providedIn: 'root' }) +export class OpenSeaDragonService { + get viewer() { return this._viewer; } - set viewer(htmlElement: HTMLElement) { - const accessToken = this._accessToken.getAccessToken(); + private readonly _OVERLAY_COLOR = 'rgba(255,0,0,0.3)'; + private readonly ZOOM_FACTOR = 0.2; + + private _viewer!: OpenSeadragon.Viewer; + + private _rectangleInDrawing: { + overlayElement: HTMLElement; + startPos: OpenSeadragon.Point; + endPos?: OpenSeadragon.Point; + } | null = null; + + private _createdRectangleSubject = new Subject(); + createdRectangle$ = this._createdRectangleSubject.asObservable(); + constructor(private _accessToken: AccessTokenService) {} + + onInit(htmlElement: HTMLElement) { const viewerConfig: OpenSeadragon.Options = { ...osdViewerConfig, element: htmlElement, loadTilesWithAjax: true, }; + const accessToken = this._accessToken.getAccessToken(); if (accessToken) { viewerConfig.ajaxHeaders = { Authorization: `Bearer ${accessToken}`, @@ -27,7 +50,77 @@ export class OpenSeaDragonService { } this._viewer = new OpenSeadragon.Viewer(viewerConfig); + this._trackClickEvents(this._viewer); } - constructor(private _accessToken: AccessTokenService) {} + zoom(direction: 1 | -1) { + this._viewer.viewport.zoomBy(1 + direction * this.ZOOM_FACTOR); + } + + private _trackClickEvents(viewer: OpenSeadragon.Viewer) { + return new OpenSeadragon.MouseTracker({ + element: viewer.canvas, + pressHandler: event => { + if (viewer.isMouseNavEnabled()) { + return; + } + + const overlayElement: HTMLElement = document.createElement('div'); + overlayElement.style.background = this._OVERLAY_COLOR; + const viewportPos = viewer.viewport.pointFromPixel((event as OpenSeadragon.ViewerEvent).position!); + viewer.addOverlay(overlayElement, new OpenSeadragon.Rect(viewportPos.x, viewportPos.y, 0, 0)); + this._rectangleInDrawing = { + overlayElement, + startPos: viewportPos, + }; + }, + dragHandler: event => { + if (viewer.isMouseNavEnabled()) { + return; + } + + if (!this._rectangleInDrawing) { + throw new AppError('Rectangle is not set'); + } + + const viewPortPos = viewer.viewport.pointFromPixel((event as OpenSeadragon.ViewerEvent).position!); + const diffX = viewPortPos.x - this._rectangleInDrawing.startPos.x; + const diffY = viewPortPos.y - this._rectangleInDrawing.startPos.y; + const location = new OpenSeadragon.Rect( + Math.min(this._rectangleInDrawing.startPos.x, this._rectangleInDrawing.startPos.x + diffX), + Math.min(this._rectangleInDrawing.startPos.y, this._rectangleInDrawing.startPos.y + diffY), + Math.abs(diffX), + Math.abs(diffY) + ); + + viewer.updateOverlay(this._rectangleInDrawing.overlayElement, location); + this._rectangleInDrawing.endPos = viewPortPos; + }, + releaseHandler: () => { + if (viewer.isMouseNavEnabled()) { + return; + } + + if (!this._rectangleInDrawing) { + throw new AppError('Rectangle is not set'); + } + + if (!this._rectangleInDrawing.endPos) { + return; + } + + const imageSize = viewer.world.getItemAt(0).getContentSize(); + const startPoint = viewer.viewport.viewportToImageCoordinates(this._rectangleInDrawing.startPos); + const endPoint = viewer.viewport.viewportToImageCoordinates(this._rectangleInDrawing.endPos); + this._createdRectangleSubject.next({ + startPoint, + endPoint, + imageSize, + overlay: this._rectangleInDrawing.overlayElement, + }); + + this._rectangleInDrawing = null; + }, + }); + } } diff --git a/libs/vre/shared/app-representations/src/lib/still-image/osd-drawer.service.ts b/libs/vre/shared/app-representations/src/lib/still-image/osd-drawer.service.ts index 116b4b40dd..3b6ad5f742 100644 --- a/libs/vre/shared/app-representations/src/lib/still-image/osd-drawer.service.ts +++ b/libs/vre/shared/app-representations/src/lib/still-image/osd-drawer.service.ts @@ -1,41 +1,33 @@ -import { ChangeDetectorRef, Inject, Injectable, Renderer2 } from '@angular/core'; +import { ChangeDetectorRef, Inject, Injectable, OnDestroy } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Constants, KnoraApiConnection, - Point2D, ReadColorValue, ReadResource, ReadStillImageFileValue, RegionGeometry, } from '@dasch-swiss/dsp-js'; +import { DspResource } from '@dasch-swiss/vre/shared/app-common'; import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppError } from '@dasch-swiss/vre/shared/app-error-handler'; import * as OpenSeadragon from 'openseadragon'; -import { filter, switchMap } from 'rxjs/operators'; +import { combineLatest, of, Subject } from 'rxjs'; +import { filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { AddRegionFormDialogComponent, AddRegionFormDialogProps } from '../add-region-form-dialog.component'; import { RegionService } from '../region.service'; +import { OpenSeaDragonService } from './open-sea-dragon.service'; import { PolygonsForRegion } from './polygons-for-region.interface'; import { StillImageHelper } from './still-image-helper'; @Injectable() -export class OsdDrawerService { +export class OsdDrawerService implements OnDestroy { resource!: ReadResource; - private _regionDragInfo: { - overlayElement: HTMLElement; - startPos: OpenSeadragon.Point; - endPos?: OpenSeadragon.Point; - } | null = null; // stores the information of the first click for drawing a region - private _regions: PolygonsForRegion = {}; - - public viewer!: OpenSeadragon.Viewer; - - public readonly ZOOM_FACTOR = 0.2; - private readonly _REGION_COLOR = 'rgba(255,0,0,0.3)'; + private _paintedPolygons: PolygonsForRegion = {}; + private _ngUnsubscribe = new Subject(); constructor( - private _renderer: Renderer2, + private _osd: OpenSeaDragonService, private _regionService: RegionService, @Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, @@ -43,98 +35,120 @@ export class OsdDrawerService { private _cdr: ChangeDetectorRef ) {} - onInit(viewer: OpenSeadragon.Viewer, resource: ReadResource): void { + onInit(resource: ReadResource): void { this.resource = resource; - this.viewer = viewer; - - this._regionService.imageIsLoaded$ - .pipe( - filter(loaded => loaded), - switchMap(() => this._regionService.regions$), - switchMap(() => this._regionService.showRegions$) - ) - .subscribe(showRegion => { - this._regions = {}; - if (showRegion) { - this._renderRegions(); - } - }); + this._subscribeToRegions(); + this._subscribeToSelectedRegion(); + this._subscribeToCreatedRectangle(); + } - this._regionService.highlightRegion$.subscribe(region => { + private _subscribeToSelectedRegion() { + this._regionService.selectedRegion$.pipe(takeUntil(this._ngUnsubscribe)).subscribe(region => { + this._unhighlightAllRegions(); if (region === null) { - this._unhighlightAllRegions(); return; } this._highlightRegion(region); }); } - public trackClickEvents() { - const viewer = this.viewer; - return new OpenSeadragon.MouseTracker({ - element: viewer.canvas, - pressHandler: event => { - if (viewer.isMouseNavEnabled()) { - return; + private _subscribeToRegions() { + combineLatest([this._regionService.showRegions$, this._regionService.regions$]) + .pipe(takeUntil(this._ngUnsubscribe)) + .subscribe(([showRegions, regions]) => { + if (!showRegions) { + this._removeOverlays(); } - const overlayElement: HTMLElement = this._renderer.createElement('div'); - overlayElement.style.background = this._REGION_COLOR; - const viewportPos = viewer.viewport.pointFromPixel((event as OpenSeadragon.ViewerEvent).position!); - viewer.addOverlay(overlayElement, new OpenSeadragon.Rect(viewportPos.x, viewportPos.y, 0, 0)); - this._regionDragInfo = { - overlayElement, - startPos: viewportPos, - }; - }, - dragHandler: event => { - if (viewer.isMouseNavEnabled()) { - return; - } - - if (!this._regionDragInfo) { - throw new AppError('Region drag info is not set'); + if (showRegions) { + this._removeOverlays(regions); + this._renderRegions(); } + }); + } - const viewPortPos = viewer.viewport.pointFromPixel((event as OpenSeadragon.ViewerEvent).position!); - const diffX = viewPortPos.x - this._regionDragInfo.startPos.x; - const diffY = viewPortPos.y - this._regionDragInfo.startPos.y; - const location = new OpenSeadragon.Rect( - Math.min(this._regionDragInfo.startPos.x, this._regionDragInfo.startPos.x + diffX), - Math.min(this._regionDragInfo.startPos.y, this._regionDragInfo.startPos.y + diffY), - Math.abs(diffX), - Math.abs(diffY) - ); - - viewer.updateOverlay(this._regionDragInfo.overlayElement, location); - this._regionDragInfo.endPos = viewPortPos; - }, - releaseHandler: () => { - if (viewer.isMouseNavEnabled()) { - return; - } + private _subscribeToCreatedRectangle() { + this._osd.createdRectangle$ + .pipe(takeUntil(this._ngUnsubscribe)) + .pipe( + switchMap(overlay => + this._dialog + .open(AddRegionFormDialogComponent, { + data: { + resourceIri: this.resource.id, + }, + }) + .afterClosed() + .pipe(map(data => ({ data, overlay }))) + ) + ) + .pipe( + switchMap(({ data, overlay }) => { + this._osd.viewer.setMouseNavEnabled(true); + this._osd.viewer.removeOverlay(overlay.overlay); + this._cdr.detectChanges(); + + if (!data) { + // User pressed on cancel + return of(null); + } + + return this._dspApiConnection.v2.res.createResource( + StillImageHelper.getPayloadUploadRegion( + this.resource.id, + this.resource.attachedToProject, + overlay.startPoint, + overlay.endPoint, + overlay.imageSize, + data.color, + data.comment, + data.label + ) + ); + }), + filter(data => !!data), + takeUntil(this._ngUnsubscribe), + switchMap(res => + this._regionService.updateRegions$().pipe( + map(() => { + return res; + }) + ) + ) + ) + .subscribe(res => { + const regionId = (res as ReadResource).id; + this._regionService.selectRegion(regionId); + }); + } - if (!this._regionDragInfo) { - throw new AppError('Region drag info and draw mode are not set'); - } - const imageSize = viewer.world.getItemAt(0).getContentSize(); - const startPoint = viewer.viewport.viewportToImageCoordinates(this._regionDragInfo.startPos); - const endPoint = viewer.viewport.viewportToImageCoordinates(this._regionDragInfo.endPos!); - this._openRegionDialog(startPoint, endPoint, imageSize, this._regionDragInfo.overlayElement); - this._regionDragInfo = null; - viewer.setMouseNavEnabled(false); - }, + private _removeOverlays(keep: DspResource[] = []): void { + const elementsToRemove = this._getPolygonsToRemove(keep.map(r => r.res.id)); + elementsToRemove.forEach(r => { + const e = this._osd.viewer.getOverlayById(r); + if (e) { + delete this._paintedPolygons[r]; + this._osd.viewer.clearOverlays(); + this._cdr.detectChanges(); + } }); } + private _getPolygonsToRemove(keep: string[] = []): string[] { + if (!keep.length) { + return Object.keys(this._paintedPolygons); + } + return Object.keys(this._paintedPolygons).filter(el => !keep.includes(el)); + } + private _renderRegions() { let imageXOffset = 0; // see documentation in this.openImages() for the usage of imageXOffset const stillImage = this.resource.properties[Constants.HasStillImageFileValue][0] as ReadStillImageFileValue; const aspectRatio = stillImage.dimY / stillImage.dimX; - const geometries = StillImageHelper.collectAndSortGeometries(this._regionService.regions, this._regions); + const geometries = StillImageHelper.collectAndSortGeometries(this._regionService.regions, this._paintedPolygons); // render all geometries for this page for (const geom of geometries) { @@ -164,71 +178,23 @@ export class OsdDrawerService { regionLabel: string, regionComment: string ): void { - const viewer = this.viewer; - - const { regEle, loc } = this._createRectangle(geometry, aspectRatio); - viewer + const { regEle, loc } = this._createRectangle(regionIri, geometry, aspectRatio); + this._osd.viewer .addOverlay({ + id: regionIri, element: regEle, location: loc, }) .addHandler('canvas-click', event => { - this._regionService.highlightRegion((event).originalTarget.dataset.regionIri); + this._regionService.selectRegion((event).originalTarget.dataset.regionIri); }); - this._regions[regionIri].push(regEle); + this._paintedPolygons[regionIri].push(regEle); this._createTooltip(regionLabel, regionComment, regEle, regionIri); } - private _openRegionDialog(startPoint: Point2D, endPoint: Point2D, imageSize: Point2D, overlay: Element): void { - this._dialog - .open(AddRegionFormDialogComponent, { - data: { - resourceIri: this.resource.id, - }, - }) - .afterClosed() - .subscribe(data => { - this.viewer.setMouseNavEnabled(true); - this._cdr.detectChanges(); - this.viewer.removeOverlay(overlay); - if (data) { - this._uploadRegion(startPoint, endPoint, imageSize, data.color, data.comment, data.label); - } - }); - } - - private _uploadRegion( - startPoint: Point2D, - endPoint: Point2D, - imageSize: Point2D, - color: string, - comment: string, - label: string - ) { - this._dspApiConnection.v2.res - .createResource( - StillImageHelper.getPayloadUploadRegion( - this.resource.id, - this.resource.attachedToProject, - startPoint, - endPoint, - imageSize, - color, - comment, - label - ) - ) - .subscribe(res => { - const regionId = (res as ReadResource).id; - this._regionService.showRegions(true); - this._regionService.updateRegions(); - this._regionService.highlightRegion(regionId); - }); - } - private _highlightRegion(regionIri: string) { - const activeRegions: HTMLElement[] = this._regions[regionIri]; + const activeRegions: HTMLElement[] = this._paintedPolygons[regionIri]; if (activeRegions !== undefined) { for (const pol of activeRegions) { @@ -238,9 +204,9 @@ export class OsdDrawerService { } private _unhighlightAllRegions() { - for (const reg in this._regions) { - if (reg in this._regions) { - for (const pol of this._regions[reg]) { + for (const reg in this._paintedPolygons) { + if (reg in this._paintedPolygons) { + for (const pol of this._paintedPolygons[reg]) { pol.setAttribute('class', 'region'); } } @@ -248,6 +214,7 @@ export class OsdDrawerService { } private _createRectangle( + regionIri: string, geometry: RegionGeometry, aspectRatio: number ): { @@ -257,8 +224,8 @@ export class OsdDrawerService { const lineColor = geometry.lineColor; const lineWidth = geometry.lineWidth; - const regEle: HTMLElement = this._renderer.createElement('div'); - regEle.id = `region-overlay-${Math.random() * 10000}`; + const regEle: HTMLElement = document.createElement('div'); + regEle.id = regionIri; regEle.className = 'region'; regEle.setAttribute('style', `outline: solid ${lineColor} ${lineWidth}px;`); regEle.setAttribute('data-cy', 'annotation-rectangle'); @@ -278,7 +245,7 @@ export class OsdDrawerService { } private _createTooltip(regionLabel: string, regionComment: string, regEle: HTMLElement, regionIri: string): void { - const comEle: HTMLElement = this._renderer.createElement('div'); + const comEle: HTMLElement = document.createElement('div'); comEle.className = 'annotation-tooltip'; comEle.innerHTML = `${regionLabel}
${regionComment}`; regEle.append(comEle); @@ -291,4 +258,8 @@ export class OsdDrawerService { }); regEle.dataset['regionIri'] = regionIri; } + + ngOnDestroy() { + this._ngUnsubscribe.complete(); + } } diff --git a/libs/vre/shared/app-representations/src/lib/still-image/still-image-toolbar.component.html b/libs/vre/shared/app-representations/src/lib/still-image/still-image-toolbar.component.html index 33d8e52352..0a47a59652 100644 --- a/libs/vre/shared/app-representations/src/lib/still-image/still-image-toolbar.component.html +++ b/libs/vre/shared/app-representations/src/lib/still-image/still-image-toolbar.component.html @@ -49,18 +49,10 @@ [disabled]="this.isReadStillImageExternalFileValue"> settings - -