Skip to content
Permalink
Browse files
feat(resource): draw regions (DSP-1845) (#524)
* Implemented drawing of regions

* get coordinates for region

* implemented form

* Compiled search

* Finalized request

* fix(resource): correct dspApiConnectionToken

* feat(resource): add color picker to region form

* refactor: fix formatting

* test: fixes unit tests

* feat(resource): regions update automatically after a new one is submitted

* feat(resource): Update regions and highlight new region on submit

* refactor(region): make lint happy

Co-authored-by: André Kilchenmann <github@milchkannen.ch>
Co-authored-by: mdelez <60604010+mdelez@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 15, 2021
1 parent b07ec63 commit f08706ba2e4e6c26022371c4345646884d19acca

Large diffs are not rendered by default.

@@ -90,6 +90,7 @@ import { ResourceComponent } from './workspace/resource/resource.component';
import { ResultsComponent } from './workspace/results/results.component';
import { AudioComponent } from './workspace/resource/representation/audio/audio.component';
import { IntermediateComponent } from './workspace/intermediate/intermediate.component';
import { AddRegionFormComponent } from './workspace/resource/representation/add-region-form/add-region-form.component';
import { ResourceLinkFormComponent } from './workspace/resource/resource-link-form/resource-link-form.component';
import { ConfirmationDialogComponent } from './main/action/confirmation-dialog/confirmation-dialog.component';
import { ConfirmationMessageComponent } from './main/action/confirmation-dialog/confirmation-message/confirmation-message.component';
@@ -242,6 +243,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
VisualizerComponent,
AudioComponent,
IntermediateComponent,
AddRegionFormComponent,
ResourceLinkFormComponent,
ConfirmationDialogComponent,
ConfirmationMessageComponent,
@@ -387,6 +387,11 @@
</mat-dialog-actions>
</div>

<div *ngSwitchCase="'addRegion'">
<app-dialog-header [title]="data.title" [subtitle]="data.subtitle"></app-dialog-header>
<app-add-region-form [resourceIri]="data.id"></app-add-region-form>
</div>

<div *ngSwitchCase="'linkResources'">
<app-dialog-header [title]="data.title" [subtitle]="'Link resources'"></app-dialog-header>
<mat-dialog-content>
@@ -0,0 +1,42 @@
<form [formGroup]="regionForm">
<mat-form-field>
<mat-label>Comment*</mat-label>
<textarea matInput formControlName="comment"></textarea>
</mat-form-field>
<mat-form-field>
<mat-label>Label*</mat-label>
<input matInput formControlName="label">
</mat-form-field>
<mat-form-field>
<mat-label>Color*</mat-label>
<app-color-picker
#colorInput
[formControlName]="'color'"
class="value">
</app-color-picker>
</mat-form-field>
<mat-form-field>
<mat-label>Is Region Of</mat-label>
<input matInput disabled [value]="resourceIri">
</mat-form-field>
<!-- Further inputs would be status, lineColor and lineWidth if we want to have these options-->
<div class="form-panel large-field">
<span>
<button mat-button type="button" mat-dialog-close>
{{ 'appLabels.form.action.cancel' | translate }}
</button>
</span>
<span class="fill-remaining-space"></span>
<span>
<button
mat-raised-button
[mat-dialog-close]="regionForm.value"
type="button"
color="primary"
[disabled]="!regionForm.valid"
class="form-submit">
Submit
</button>
</span>
</div>
</form>
@@ -0,0 +1,35 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';

import { AddRegionFormComponent } from './add-region-form.component';

describe('AddRegionFormComponent', () => {
let component: AddRegionFormComponent;
let fixture: ComponentFixture<AddRegionFormComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AddRegionFormComponent
],
imports: [
TranslateModule.forRoot()
],
providers:[
FormBuilder
]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(AddRegionFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,24 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
selector: 'app-add-region-form',
templateUrl: './add-region-form.component.html',
styleUrls: ['./add-region-form.component.scss']
})
export class AddRegionFormComponent implements OnInit {
@Input() resourceIri: string;
regionForm: FormGroup;
colorPattern = '^#[a-f0-9]{6}$';
constructor(private _fb: FormBuilder) {
}

ngOnInit(): void {
this.regionForm = this._fb.group({
color: ['#ff3333', [Validators.required, Validators.pattern(this.colorPattern)]],
comment: [null, Validators.required],
label: [null, Validators.required]
});
}

}
@@ -20,6 +20,9 @@
<button mat-icon-button id="DSP_OSD_FULL_PAGE" matTooltip="Open in fullscreen">
<mat-icon>fullscreen</mat-icon>
</button>
<button (click)="drawButtonClicked()" mat-icon-button matTooltip="Draw Region">
<mat-icon>edit</mat-icon>
</button>
</span>
</div>

@@ -1,11 +1,15 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import { By } from '@angular/platform-browser';
import { Constants, ReadGeomValue, ReadResource, ReadValue } from '@dasch-swiss/dsp-js';
import { AppInitService } from 'src/app/app-init.service';
import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens';
import { FileRepresentation } from '../file-representation';
import { Region, StillImageComponent, StillImageRepresentation } from './still-image.component';
import { Region, StillImageComponent } from './still-image.component';

// --> TODO: get test data from dsp-js
// --> TODO: get this from dsp-js: https://dasch.myjetbrains.com/youtrack/issue/DSP-506
@@ -182,11 +186,27 @@ describe('StillImageComponent', () => {
let testHostFixture: ComponentFixture<TestHostComponent>;

beforeEach(waitForAsync(() => {

const adminSpyObj = {
v2: {
res: jasmine.createSpyObj('res', ['createResource'])
}
};

TestBed.configureTestingModule({
declarations: [StillImageComponent, TestHostComponent],
imports: [
MatDialogModule,
MatIconModule,
MatSnackBarModule,
MatToolbarModule
],
providers: [
AppInitService,
{
provide: DspApiConnectionToken,
useValue: adminSpyObj
},
]
})
.compileComponents();
@@ -1,9 +1,30 @@
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { Constants, Point2D, ReadFileValue, ReadGeomValue, ReadResource, ReadStillImageFileValue, RegionGeometry } from '@dasch-swiss/dsp-js';
import {
Component,
ElementRef,
EventEmitter,
Inject,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges
} from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import {
Constants, CreateColorValue, CreateGeomValue, CreateLinkValue,
CreateResource, CreateTextValueAsString, KnoraApiConnection,
Point2D, ReadFileValue,
ReadGeomValue,
ReadResource,
ReadStillImageFileValue,
RegionGeometry
} from '@dasch-swiss/dsp-js';
import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens';
import { DialogComponent } from 'src/app/main/dialog/dialog.component';
import { ErrorHandlerService } from 'src/app/main/error/error-handler.service';
import { DspCompoundPosition } from '../../dsp-resource';
import { FileRepresentation } from '../file-representation';


// this component needs the openseadragon library itself, as well as the openseadragon plugin openseadragon-svg-overlay
// both libraries are installed via package.json, and loaded globally via the script tag in .angular-cli.json

@@ -83,6 +104,8 @@ export class StillImageComponent implements OnChanges, OnDestroy {

@Input() images: FileRepresentation[];
@Input() imageCaption?: string;
@Input() resourceIri: string;
@Input() project: string;
@Input() activateRegion?: string; // highlight a region

@Input() compoundNavigation?: DspCompoundPosition;
@@ -91,12 +114,15 @@ export class StillImageComponent implements OnChanges, OnDestroy {

@Output() regionClicked = new EventEmitter<string>();

@Output() regionAdded = new EventEmitter<string>();

private _regionDrawMode: Boolean = false; // stores whether viewer is currently drawing a region
private _regionDragInfo; // stores the information of the first click for drawing a region
private _viewer;
private _regions: PolygonsForRegion = {};


constructor(
private _elementRef: ElementRef
@Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection, private _elementRef: ElementRef, private _dialog: MatDialog, private _errorHandler: ErrorHandlerService
) {
OpenSeadragon.setString('Tooltips.Home', '');
OpenSeadragon.setString('Tooltips.ZoomIn', '');
@@ -175,6 +201,142 @@ export class StillImageComponent implements OnChanges, OnDestroy {
this._renderRegions();
}

/**
* when the draw region button is clicked, this method is called from the html. It sets the draw mode to true and
* prevents navigation by mouse (so that the region can be accurately drawn).
*/
drawButtonClicked(): void {
this._regionDrawMode = true;
this._viewer.setMouseNavEnabled(false);
}

/**
* opens the dialog to enter further properties for the region after it has been drawn and calls the function to upload the region after confirmation
* @param startPoint the start point of the drawing
* @param endPoint the end point of the drawing
* @param imageSize the image size for calculations
*/
private _openRegionDialog(startPoint, endPoint, imageSize, overlay): void{
const dialogConfig: MatDialogConfig = {
width: '840px',
maxHeight: '80vh',
position: {
top: '112px'
},
data: { mode: 'addRegion', title: 'Create a region', subtitle: 'Add further properties', id: this.resourceIri },
disableClose: true
};
const dialogRef = this._dialog.open(
DialogComponent,
dialogConfig
);

dialogRef.afterClosed().subscribe((data) => {
// remove the drawn rectangle as either the cancel button was clicked or the region will be displayed
this._viewer.removeOverlay(overlay);
if (data) { // data is null if the cancel button was clicked
this._uploadRegion(startPoint, endPoint, imageSize, data.color, data.comment, data.label);
}
});
}
/**
* uploads the region after being prepared by the dialog
* @param startPoint the start point of the drawing
* @param endPoint the end point of the drawing
* @param imageSize the image size for calculations
* @param color the value for the color entered in the form
* @param comment the value for the comment entered in the form
* @param label the value for the label entered in the form
*/
private _uploadRegion(startPoint, endPoint, imageSize, color, comment, label){
const x1 = Math.max(Math.min(startPoint.x, imageSize.x), 0)/imageSize.x;
const x2 = Math.max(Math.min(endPoint.x, imageSize.x), 0)/imageSize.x;
const y1 = Math.max(Math.min(startPoint.y, imageSize.y), 0)/imageSize.y;
const y2 = Math.max(Math.min(endPoint.y, imageSize.y), 0)/imageSize.y;
const geomStr = '{"status":"active","lineColor":"' + color + '","lineWidth":2,"points":[{"x":' + x1.toString() +
',"y":' + y1.toString() + '},{"x":' + x2.toString() + ',"y":' + y2.toString()+ '}],"type":"rectangle"}';
const createResource = new CreateResource();
createResource.label = label;
createResource.type = Constants.KnoraApiV2 + Constants.HashDelimiter + 'Region';
createResource.attachedToProject = this.project;
const geomVal = new CreateGeomValue();
geomVal.type = Constants.GeomValue;
geomVal.geometryString = geomStr;
const colorVal = new CreateColorValue();
colorVal.type = Constants.ColorValue;
colorVal.color = color;
const linkVal = new CreateLinkValue();
linkVal.type = Constants.LinkValue;
linkVal.linkedResourceIri = this.resourceIri;
const commentVal = new CreateTextValueAsString();
commentVal.type = Constants.TextValue;
commentVal.text = comment;

createResource.properties = {
[Constants.KnoraApiV2 + Constants.HashDelimiter + 'hasComment']: [commentVal],
[Constants.KnoraApiV2 + Constants.HashDelimiter + 'hasColor'] : [colorVal],
[Constants.KnoraApiV2 + Constants.HashDelimiter + 'isRegionOfValue'] : [linkVal],
[Constants.KnoraApiV2 + Constants.HashDelimiter + 'hasGeometry'] : [geomVal]
};
this._dspApiConnection.v2.res.createResource(createResource).subscribe(
(res: ReadResource) => {
this.regionAdded.emit(res.id);
},
(error) => {
this._errorHandler.showMessage(error);
}
);
}

/**
* set up function for the region drawer
*/
private _addRegionDrawer(){
new OpenSeadragon.MouseTracker({
element: this._viewer.canvas,
pressHandler: (event) => {
if (!this._regionDrawMode){
return;
}
const overlayElement = document.createElement('div');
overlayElement.style.background = 'rgba(255,0,0,0.3)';
const viewportPos = this._viewer.viewport.pointFromPixel(event.position);
this._viewer.addOverlay(overlayElement, new OpenSeadragon.Rect(viewportPos.x, viewportPos.y, 0, 0));
this._regionDragInfo = {
overlayElement: overlayElement,
startPos: viewportPos
};
},
dragHandler: (event) => {
if (!this._regionDragInfo){
return;
}
const viewPortPos = this._viewer.viewport.pointFromPixel(event.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)
);
this._viewer.updateOverlay(this._regionDragInfo.overlayElement, location);
this._regionDragInfo.endPos = viewPortPos;
},
releaseHandler: () => {
if (this._regionDrawMode) {
const imageSize = this._viewer.world.getItemAt(0).getContentSize();
const startPoint = this._viewer.viewport.viewportToImageCoordinates(this._regionDragInfo.startPos);
const endPoint = this._viewer.viewport.viewportToImageCoordinates(this._regionDragInfo.endPos);
this._openRegionDialog(startPoint, endPoint, imageSize, this._regionDragInfo.overlayElement);
this._regionDragInfo = null;
this._regionDrawMode = false;
this._viewer.setMouseNavEnabled(true);
}
}
});
}

/**
* highlights the polygon elements associated with the given region.
*
@@ -266,6 +428,7 @@ export class StillImageComponent implements OnChanges, OnDestroy {
this._viewer.addHandler('resize', (args) => {
args.eventSource.svgOverlay().resize();
});
this._addRegionDrawer();
}

/**
Loading

0 comments on commit f08706b

Please sign in to comment.