Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(resource): draw regions (DSP-1845) #524

Merged
merged 15 commits into from Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10,975 changes: 8,125 additions & 2,850 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/app/app.module.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -242,6 +243,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
VisualizerComponent,
AudioComponent,
IntermediateComponent,
AddRegionFormComponent,
ResourceLinkFormComponent,
ConfirmationDialogComponent,
ConfirmationMessageComponent,
Expand Down
5 changes: 5 additions & 0 deletions src/app/main/dialog/dialog.component.html
Expand Up @@ -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>
Expand Down
@@ -0,0 +1,42 @@
<form [formGroup]="regionForm">
<mat-form-field>
<mat-label>Comment</mat-label>
EricSommerhalder marked this conversation as resolved.
Show resolved Hide resolved
<input matInput formControlName="comment">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment field should be a textarea. But I think the form style could also be done in a separate, style refactoring PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to a textarea for now, but I agree that a style refactoring PR will be needed.

</mat-form-field>
<mat-form-field>
<mat-label>Label</mat-label>
EricSommerhalder marked this conversation as resolved.
Show resolved Hide resolved
<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],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is a comment actually required to submit the region?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, otherwise the api is not happy. Not sure why though.

label: [null, Validators.required]
});
}

}
Expand Up @@ -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>

Expand Down
@@ -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
Expand Down Expand Up @@ -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();
Expand Down
@@ -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

Expand Down Expand Up @@ -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;
Expand All @@ -91,12 +114,14 @@ export class StillImageComponent implements OnChanges, OnDestroy {

@Output() regionClicked = 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 _mouseTracker; // stores the MouseTracker that allows drawing regions
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this used anywhere? My IDE says it's unused so it can probably be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point this got the whole thing running, but apparently it is not needed anymore. I have removed it.

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', '');
Expand Down Expand Up @@ -175,6 +200,143 @@ 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) => {
if (data) { // data is null if the cancel button was clicked
this._uploadRegion(startPoint, endPoint, imageSize, data.color, data.comment, data.label);
// maybe we should remove the artificial overlay (drawing) also on success and refresh the regions from the api.
} else {
this._viewer.removeOverlay(overlay);
}
});
}
/**
* 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) => {
// should we add some confirmation message?
},
(error) => {
this._errorHandler.showMessage(error);
}
);
}

/**
* set up function for the region drawer
*/
private _addRegionDrawer(){
this._mouseTracker = 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.
*
Expand Down Expand Up @@ -267,6 +429,7 @@ export class StillImageComponent implements OnChanges, OnDestroy {
this._viewer.addHandler('resize', (args) => {
args.eventSource.svgOverlay().resize();
});
this._addRegionDrawer();
}

/**
Expand Down