diff --git a/src/app/action/action.component.html b/src/app/action/action.component.html
index 6a45f4d..b938c50 100644
--- a/src/app/action/action.component.html
+++ b/src/app/action/action.component.html
@@ -1,52 +1,62 @@
-
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 9458f69..7bd260c 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,158 +1,239 @@
import { Component, OnInit, HostListener } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import wordData from '../assets/test.json';
-import {MyData, Response, EditableWord} from './models/models';
+import {MyData, Response, ResponseHandler, EditableWord} from './models/models';
import { PageViewService } from './page-view/page-view.service';
import { HIGHTLIGHT_CASES} from './page-view/highlight_status';
-import { externalAssignClass, externalAssignStyle, Image, TextField, Line, Word } from './page-view/models';
+import { externalAssignClass, externalAssignStyle, Image, Identifier, TextField, Line, Word } from './page-view/models';
import { DataService } from './services/data.service';
+const WORD_CORRESPONDANCE: string = 'faksimile/transkription word correspondance'
+
+
class Point {
left: number;
top: number;
constructor(left: number, top: number){
this.left = left;
this.top = top;
}
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [PageViewService ]
})
export class AppComponent implements OnInit {
app_title = 'Nietzsche Online Tools';
actions: any;
findText: string = ''
max_page_height: number = 700;
numWords: string = "0";
positionWords: boolean = false;
svg_lines: Line[];
svg: Image;
faksimile: Image;
+ faksimile_words: EditableWord[];
+ faksimile_lines: Line[];
+ potentialDoubleLines: Line[];
+ potentialDoubleLineId: Identifier;
+ potentialDoubleWords: EditableWord[];
+ potentialDoubleWordId: Identifier;
selectedWords: EditableWord[] = [];
title = this.app_title
showActions: boolean = true;
showFaksimile: boolean = false;
savedTextfield: TextField = null;
words: EditableWord[] = [];
- faksimile_words: EditableWord[];
+ current_task: string;
tmpWords: EditableWord[] = [];
tmpFaksimileWords: EditableWord[] = [];
- zoomFactor: number = 1;
+ highlightSelected: Identifier[] = [];
+ zoomFactor: number = 2;
offset: number = 10;
KEY_CODE = { 'ArrowRight': new Point(this.offset, 0),
'ArrowUp': new Point(0, this.offset*-1),
'ArrowDown': new Point(0, this.offset ),
'ArrowLeft': new Point(this.offset*-1, 0)};
constructor(private dataService: DataService, private wordservice: PageViewService, private route: ActivatedRoute ) {}
ngOnInit() {
this.max_page_height = screen.availHeight - 400;
this.dataService.getHttpJSON().subscribe(mydata => {
this.updateData(mydata);
})
this.route.queryParams.subscribe(params => {
this.findText = params['find'];
});
this.wordservice.onClickedWord.subscribe(
(changedWord: EditableWord) => { this.addWord(changedWord); }
);
}
private addWord(word: EditableWord){
if (this.selectedWords.indexOf(word) == -1){
this.selectedWords.push(word);
}
}
assignClass: externalAssignClass = (currentWord: EditableWord, hoveredWord: EditableWord, hoveredLine: Line): string => {
+ if (this.potentialDoubleWordId != null && this.potentialDoubleWordId != '' && currentWord.id == this.potentialDoubleWordId){
+ return 'text_field highlight_magenta'
+ }
if (this.showFaksimile || hoveredWord != null || hoveredLine != null) {
return 'textfield unhighlighted'
}
return (currentWord.id % 2 == 0) ? 'textfield highlight_magenta' : 'textfield highlight_yellow';
}
private assignHiddenClass() {
return (this.selectedWords.length > 0 && this.positionWords) ? 'hidden': 'auto' ;
}
removeWord = (word: EditableWord): void => {
this.selectedWords = this.selectedWords.filter(item =>item !== word)
}
private addWords(){
if (this.words.length < 50){
for(var i = 0; i < this.words.length; i++){
this.addWord(this.words[i]);
}
}
}
private removeWords(){
this.selectedWords = [];
}
send = (response: Response): void => {
this.selectedWords = [];
this.dataService.send(response).subscribe(mydata =>{
this.updateData(mydata);
});
}
+ disposeOldResultMessage = (timeout: number = 0): void => {
+ setTimeout(()=>{this.actions.result = null}, timeout);
+ }
+ private resetFindText() {
+ this.findText = null;
+ }
+ private resetPoints() {
+ this.KEY_CODE = { 'ArrowRight': new Point(this.offset, 0),
+ 'ArrowUp': new Point(0, this.offset*-1),
+ 'ArrowDown': new Point(0, this.offset ),
+ 'ArrowLeft': new Point(this.offset*-1, 0)};
+ }
private updateData(mydata: MyData) {
this.actions = mydata.actions
this.svg = mydata.svg;
this.svg_lines = mydata.lines;
this.words = mydata.words;
this.faksimile = mydata.faksimile;
this.faksimile_words = mydata.faksimile_positions
+ this.faksimile_lines = mydata.faksimile_lines
this.tmpWords = this.words.slice();
this.tmpFaksimileWords = this.faksimile_words.slice();
this.title = this.app_title + ": " + mydata.title + ", " + mydata.number;
- }
- private resetFindText() {
- this.findText = null;
- }
- private resetPoints() {
- this.KEY_CODE = { 'ArrowRight': new Point(this.offset, 0),
- 'ArrowUp': new Point(0, this.offset*-1),
- 'ArrowDown': new Point(0, this.offset ),
- 'ArrowLeft': new Point(this.offset*-1, 0)};
+ if (this.actions.result != null && this.actions.result != undefined
+ && this.actions.result.includes('succeeded!') && !this.actions.result.includes('WARNING')){
+ this.disposeOldResultMessage(2000);
+ }
+ if (!this.current_task && this.actions && this.actions.tasks.length > 0){
+ this.current_task = this.actions.tasks[0]
+ }
+ if (this.current_task != null){
+ this.showFaksimile = (this.current_task == WORD_CORRESPONDANCE)
+ if (this.current_task == WORD_CORRESPONDANCE){
+ this.updatePotentialDoubles()
+ }
+ }
}
private updateWords() {
if (this.findText != null && this.findText != ''){
this.words = [];
this.words = this.tmpWords.filter(word => word.text.match(this.findText) || (word.edited_text != null && word.edited_text.match(this.findText)));
this.faksimile_words = [];
this.faksimile_words = this.tmpFaksimileWords.filter(word => word.text.match(this.findText) || (word.edited_text != null && word.edited_text.match(this.findText)));
} else if (this.tmpWords.length > 0) {
this.words = this.tmpWords.slice();
this.faksimile_words = this.tmpFaksimileWords.slice();
}
}
+ private updatePotentialDoubles() {
+ this.potentialDoubleLines = [];
+ let uneven_lines = this.faksimile_lines.filter(line =>
(line.id) % 2 == 1);
+ for (const line of uneven_lines.values()){
+ let words_on_line = this.faksimile_words.filter(word =>word.line == line.id)
+ let unique_words = [...new Set(Array.from(words_on_line, word=>word.text))]
+ if (words_on_line.length > unique_words.length){
+ this.potentialDoubleLines.push(line)
+ }
+ }
+ if (this.potentialDoubleLines.length > 0){
+ this.updatePotentialDoubleWords(this.potentialDoubleLines[0].id);
+ }
+ }
+ private updatePotentialDoubleWords(line: Identifier) {
+ this.potentialDoubleWords = [];
+ this.potentialDoubleWordId = '';
+ this.potentialDoubleLineId = line;
+ let words_on_line = this.faksimile_words.filter(word =>word.line == line)
+ let unique_words = [...new Set(Array.from(words_on_line, word=>word.text))]
+ if (words_on_line.length > unique_words.length){
+ for(const text of unique_words.values()){
+ let words_with_text = words_on_line.filter(word =>word.text == text)
+ if (words_with_text.length > 1){
+ words_with_text.forEach(word =>this.potentialDoubleWords.push(word));
+ }
+ }
+ }
+ }
+ private showPotentialDoubles(id: Identifier){
+ this.potentialDoubleWordId = id;
+ if (this.potentialDoubleWordId != null && this.potentialDoubleWordId != ''){
+ this.words.filter(word =>word.id == this.potentialDoubleWordId).concat(
+ this.faksimile_words.filter(word =>word.id == this.potentialDoubleWordId)).forEach(
+ word => {this.wordservice.onHoverService(word, { visible: true, layerX: -1, layerY: -1, clientX: -1, clientY: -1});
+ this.wordservice.offHoverService(word, { visible: true, layerX: -1, layerY: -1, clientX: -1, clientY: -1})});
+
+ }
+ }
+ private setTask(task: string){
+ this.current_task = task;
+ }
+ private taskDone(){
+ if (this.current_task){
+ let response_handler: ResponseHandler = { action_name: 'set task done', description: 'set task done'};
+ let response: Response = { 'target_file': this.actions.target_file, 'date_stamp': this.actions.date_stamp,
+ 'response_handler': response_handler, 'task': this.current_task, 'words': [] }
+ this.current_task = null;
+ this.send(response);
+ }
+ }
private setZoomFactor(newZoomFactor: number) {
this.zoomFactor = newZoomFactor;
}
private toggleShowTextFieldOnly() {
if (this.savedTextfield != null) {
this.svg.text_field = this.savedTextfield;
this.savedTextfield = null;
} else {
this.savedTextfield = this.svg.text_field;
this.svg.text_field = { 'top': 0, 'left': 0,
'width': Number(this.svg.text_field.left)*2 + Number(this.svg.text_field.width),
'height': Number(this.svg.text_field.top)*2 + Number(this.svg.text_field.height) }
}
this.svg = { 'x': this.svg.x, 'y': this.svg.y, 'filename': this.svg.filename, 'URL': this.svg.URL,
'secondaryURL': this.svg.secondaryURL, 'text_field': this.svg.text_field,
'width': this.svg.width,
'height': this.svg.height
}
}
@HostListener('window:keydown', ['$event']) keyEvent(event: KeyboardEvent) {
if (this.positionWords) {
if (this.KEY_CODE[event.key] != null){
this.moveWords(this.KEY_CODE[event.key]);
}
}
}
private moveWords(add2Position: Point){
this.selectedWords.forEach(word =>{word.left += add2Position.left; word.top += add2Position.top});
}
}
diff --git a/src/app/models/models.ts b/src/app/models/models.ts
index 93bf544..091171c 100644
--- a/src/app/models/models.ts
+++ b/src/app/models/models.ts
@@ -1,60 +1,66 @@
-import { Image, Line, IdWord } from '../page-view/models';
+import { Image, Line, Word } from '../page-view/models';
export interface Actions {
target_file: string;
date_stamp: number;
response_handlers: ResponseHandler[];
+ tasks?: string[];
+ pages?: string[];
result?: string;
}
export interface MyData {
title: string;
number: string;
svg: Image;
words: EditableWord[];
lines: Line[];
faksimile?: Image;
faksimile_positions?: EditableWord[];
+ faksimile_lines?: Line[];
actions: Actions;
}
export interface Response {
target_file: string;
date_stamp: number;
response_handler: ResponseHandler;
words: EditableWord[];
+ task?: string;
}
export interface ResponseHandler{
action_name: string;
description: string;
requirements?: Requirement[];
}
export interface Requirement {
name: string;
type: any;
input?: any;
}
export interface TextField {
width: number;
height: number;
left: number;
top: number;
bottom: number;
}
-export interface EditableWord extends IdWord {
+export interface EditableWord extends Word {
id: number;
text: string;
edited_text?: string;
left: number;
top: number;
width: number;
height: number;
line: number;
tp_id?: string;
fp_id?: string;
old_id?: number;
deleted: boolean;
+ deletion_path?: string;
+ paths_near_word?: string[];
transform?: string;
earlier_version?: string;
overwrites_word?: string;
part_text?: string;
}
diff --git a/src/app/page-view/README.md b/src/app/page-view/README.md
new file mode 100644
index 0000000..66c5560
--- /dev/null
+++ b/src/app/page-view/README.md
@@ -0,0 +1,133 @@
+# PageViewModule
+
+## How to use PageViewComponent
+
+This component displays one or two images with word hovers and corresponding lines in `TextFieldComponent(s)` and `MarginFieldComponent(s)`.
+
+On more information about this module see the documentation.
+
+### Import Module
+
+In your Angular module file, e.g. `app.module.ts`:
+
+```
+import { PageViewModule} from './page-view/page-view.module';
+import { PageViewService } from './page-view/page-view.service';
+
+@NgModule({
+ declarations: [ AppComponent ],
+ imports: [ PageViewModule ],
+ providers: [ PageViewService ],
+ .
+ .
+ .
+```
+
+### In your template:
+
+```
+
+
+```
+
+List of inputs:
+
+- `assignClass`:
+
+ An OPTIONAL function that will be passed to `TextFieldComponent`
+ in order to return a further highlight class
+ to the word rects when the internal function would return 'textfield unhighlighted'
+
+- `assignStyle`:
+
+ An OPTIONAL function that will be passed to `TextFieldComponent` and `MarginFieldComponent`
+ in order to return a (svg-)style object
+ to the word and line rects. This function allows the user to extend the style of this component.
+ E.g. by returning { fill: blue } the function overwrites the default behaviour and sets
+ the default highlight color to blue.
+
+- `configuration`: OPTIONAL configuration in the form `{'ComponentName|*': { 'PropertyName': value }}`
+
+- `findText`: OPTIONAL the search text of words that should be highlighted.
+
+- `first_foreign_texts`: text by foreign hand belonging to first image
+
+- `first_image`: the first image that will be displayed
+
+- `first_lines`: the Array of lines of the first image that will be displayed
+
+- `first_words`: the Array of words of the first image that will be displayed
+
+- `max_height`: OPTIONAL the (initial) maximum height of the image(s)
+
+- `preferPrimaryUrl`: OPTIONAL should primary Url be used for image. Use secondary Url if false. Default: true.
+
+- `second_foreign_texts`: text by foreign hand belonging to second image
+
+- `second_image`: OPTIONAL the second image that will be displayed
+
+- `second_lines`: OPTIONAL the Array of lines of the second image that will be displayed
+
+- `second_words`: OPTIONAL the Array of words of the second image that will be displayed
+
+- `selectedWords`: OPTIONAL identifiers of selected words that should be highlighted (i.e. list of IRIs or Ids).
+
+- `selectedLines`: OPTIONAL identifiers of selected words that should be highlighted (i.e. list of IRIs or Ids).
+
+- `zoomFactor`: OPTIONAL global zoom factor
+
+### For your Data
+
+Use the interfaces from `pageView/models.ts` for your data:
+
+```
+import { externalAssignClass, externalAssignStyle, Image, PositionalObject, TextField, TextByForeignHand, Line, Word } from './page-view/models';
+```
+
+### For mouse event interaction
+
+Use the `PageViewService` in order to react on mouse events.
+
+Import:
+
+```
+import { PageViewService } from './page-view/page-view.service';
+```
+
+Inject service:
+
+```
+constructor(private pageViewService: PageViewService) {}
+```
+
+Subscribe to mouse events on words and lines:
+
+```
+ngOnInit() {
+ this.pageViewService.onClickedWord.subscribe(
+ (clickedWord: Word) => { this.doSomething(clickedWord); }
+ );
+ this.pageViewService.onClickedLine.subscribe(
+ (clickedLine: Line) => { this.doSomething(clickedLine); }
+ );
+ this.pageViewService.onHoveredWord.subscribe(
+ (hoveredWord: Word) => { this.doSomething(hoveredWord); }
+ );
+ this.pageViewService.onHoveredLine.subscribe(
+ (hoveredLine: Line) => { this.doSomething(hoveredLine); }
+ );
+ this.pageViewService.offHoveredWord.subscribe(
+ (unhoveredWord: Word) => { this.doSomething(unhoveredWord); }
+ );
+ this.pageViewService.offHoveredLine.subscribe(
+ (unhoveredLine: Line) => { this.doSomething(unhoveredLine); }
+ );
+}
+```
+
diff --git a/src/app/page-view/configurable-component.spec.ts b/src/app/page-view/configurable-component.spec.ts
new file mode 100644
index 0000000..1302bbd
--- /dev/null
+++ b/src/app/page-view/configurable-component.spec.ts
@@ -0,0 +1,45 @@
+import { ComponentFixture, TestBed, async } from '@angular/core/testing';
+import { Configuration} from './models';
+import { ConfigurableComponent } from './configurable-component';
+
+const conf: Configuration = { '*': { 'success': true }}
+
+class Test extends ConfigurableComponent {
+ success: boolean = false;
+ configuration: Configuration = conf;
+
+ constructor(){ super() }
+ testConfig(){
+ this.processConfiguration();
+ }
+}
+
+describe('ConfigurableComponent', () => {
+ it('should create the Object', ()=>{
+ expect(new ConfigurableComponent()).toBeTruthy();
+ });
+ it('should have a proper name', ()=>{
+ let test = new Test();
+ expect(test.getConfigurationName()).toEqual('Test');
+ });
+ it('should process * config', ()=>{
+ let test = new Test();
+ test.testConfig()
+ expect(test.success).toBeTruthy();
+ });
+ it('should update configuration', ()=>{
+ let a: Configuration = { 'a': { 'b': true, 'c': false}}
+ let b: Configuration = { 'a': { 'b': false}}
+ a = ConfigurableComponent.updateConfiguration(a, b)
+ expect(a).toEqual({ 'a': { 'b': false, 'c': false}});
+ a = { 'a': { 'b': true, 'c': false}}
+ b = { 'a': true}
+ a = ConfigurableComponent.updateConfiguration(a, b)
+ expect(a).toEqual({ 'a': true});
+ a = { 'a': { 'b': true, 'c': false}}
+ b = { 'a': { 'd': true}}
+ a = ConfigurableComponent.updateConfiguration(a, b)
+ expect(a).toEqual({ 'a': { 'b': true, 'c': false, 'd': true}});
+ });
+
+});
diff --git a/src/app/page-view/configurable-component.ts b/src/app/page-view/configurable-component.ts
new file mode 100644
index 0000000..e6d35ec
--- /dev/null
+++ b/src/app/page-view/configurable-component.ts
@@ -0,0 +1,79 @@
+import { Input, OnChanges } from '@angular/core';
+import { Configuration } from './models';
+/**
+ * This is a super class of components that can be configured by passing
+ * a configuration to their inputs and running 'processConfiguration' in
+ * 'ngOnChanges'.
+ *
+ * E.g. given a configuration '{"ComponentName": { "ComponentProperty": value }}'
+ * if "ComponentName" is the name of the subclass component then
+ * 'processConfiguration' will update its property with name
+ * "ComponentProperty" to this value. If configuration uses wildcard "*" then
+ * all components that have a property with name "ComponentProperty" will
+ * update to this value.
+ **/
+export class ConfigurableComponent implements OnChanges {
+ /**
+ * the configuration
+ **/
+ @Input() configuration: Configuration;
+ /**
+ * index of configuration_listeners pointing to primary name of component
+ **/
+ private readonly primary_name_index: number = 1;
+ /**
+ * list of configuration keys
+ **/
+ protected configuration_listeners: string[] = [ '*', this.constructor.name ];
+
+ /**
+ * Process configuration by updating properties to given values if the keys
+ * in configuration_listeners are part of the configuration.
+ **/
+ protected processConfiguration(){
+ this.configuration_listeners.forEach(key =>{
+ if (key in this.configuration){
+ Object.getOwnPropertyNames(this.configuration[key]).forEach(conf_key =>{
+ if (conf_key in this){
+ this[conf_key] = this.configuration[key][conf_key]
+ }
+ });
+ }
+ });
+ }
+ /**
+ * Add a further configuration key
+ **/
+ public addConfigurationName(configuration_listener: string){
+ if (this.configuration_listeners.indexOf(configuration_listener) == -1){
+ this.configuration_listeners.push(configuration_listener);
+ }
+ }
+ /**
+ * Get the primary name of the component.
+ **/
+ public getConfigurationName(): string {
+ return this.configuration_listeners[this.primary_name_index];
+ }
+ /**
+ * update configuration
+ **/
+ ngOnChanges (){
+ if (this.configuration != null){
+ this.processConfiguration();
+ }
+ }
+ public static updateConfiguration(oldConfiguration: Configuration, newConfiguration: Configuration): Configuration {
+ if (oldConfiguration != null){
+ Object.getOwnPropertyNames(newConfiguration).forEach(key =>{
+ if (key in oldConfiguration && typeof oldConfiguration[key] == 'object' && typeof newConfiguration[key] == 'object') {
+ oldConfiguration[key] = this.updateConfiguration(oldConfiguration[key], newConfiguration[key]);
+ } else {
+ oldConfiguration[key] = newConfiguration[key]
+ }
+ }); return oldConfiguration;
+ } else {
+ return newConfiguration;
+ }
+ }
+}
diff --git a/src/app/page-view/highlight_status.ts b/src/app/page-view/highlight_status.ts
index 112fef1..22a9175 100644
--- a/src/app/page-view/highlight_status.ts
+++ b/src/app/page-view/highlight_status.ts
@@ -1,7 +1,8 @@
export enum HIGHTLIGHT_CASES {
DEFAULT = 'default',
LINE_HOVERED = 'hovered line',
- SEARCHED_WORD = 'text of word == findText',
+ SELECTED_WORD = 'text of word == findText',
+ SEARCHED_WORD = 'selected word',
WORD_HOVERED = 'hovered word'
}
diff --git a/src/app/page-view/interacted.directive.ts b/src/app/page-view/interacted.directive.ts
new file mode 100644
index 0000000..9c3585f
--- /dev/null
+++ b/src/app/page-view/interacted.directive.ts
@@ -0,0 +1,97 @@
+import { Directive, HostListener, Input, ElementRef, OnInit} from '@angular/core';
+import { PageViewService } from './page-view.service';
+import { Interactable, Word, Line } from './models';
+/**
+ * This directive informs the {@link /injectables/PageViewService.html|PageViewService} about
+ * mouse events on interactable objects and scrolls interactable objects in view if they are
+ * invisible.
+ **/
+@Directive({
+ selector: '[interactedObject]'
+})
+export class InteractedDirective implements OnInit {
+ /**
+ * the object of this rect
+ **/
+ @Input('interactedObject') interactedObject: Interactable;
+ /**
+ * the identification string of this Interactable's textfield (e.g. 'first textfield' or 'second textfield')
+ **/
+ @Input() identity: string = 'first textfield';
+ /**
+ * the scrollable HTML-container of this Interactable's textfield.
+ **/
+ @Input() container: HTMLElement;
+
+ constructor(private pageViewService: PageViewService, private el: ElementRef) {}
+
+ /**
+ * Subscribe to onHover methods of the {@link /injectables/PageViewService.html|PageViewService}
+ * and scroll hovered object in view if it is invisible.
+ **/
+ ngOnInit(){
+ this.interactedObject.textfield_identity = this.identity;
+ this.pageViewService.onHoveredWord.subscribe(
+ (hoveredWord: Word) => { this.scrollIntoViewIfNeeded(hoveredWord, 'Word')
+ });
+ this.pageViewService.onHoveredLine.subscribe(
+ (hoveredLine: Line) => { this.scrollIntoViewIfNeeded(hoveredLine, 'Line')
+ });
+ }
+ /**
+ * Scroll interactable object in view if it is invisible.
+ * @param hoveredItem interactable object that is hovered
+ * @param hoveredType string representation of object's type (i.e. 'Word' | 'Line')
+ **/
+ private scrollIntoViewIfNeeded(hoveredItem: Interactable, hoveredType: String){
+ if (hoveredType == 'Word' && this.interactedObject.datatype == 'Word' && this.identity != hoveredItem.textfield_identity){
+ let hoveredWord = hoveredItem
+ let currentWord = this.interactedObject
+ if (currentWord.id == hoveredWord.id && currentWord.is_top_object && this.isElementInvisible()){
+ this.el.nativeElement.scrollIntoView(true);
+ }
+ } else if (hoveredType =='Line' && this.interactedObject.datatype == 'Line'){
+ let hoveredLine = hoveredItem
+ let currentLine = this.interactedObject
+ if (currentLine !== hoveredLine && currentLine.id == hoveredLine.id && this.isElementInvisible()){
+ this.el.nativeElement.scrollIntoView(true);
+ }
+ }
+ }
+ /**
+ * Return whether interactable object is invisible, i.e. whether it is outside of
+ * its scrollable container's viewport.
+ **/
+ private isElementInvisible(): boolean {
+ if (this.container == null || this.container == undefined || this.container.getAttribute('class') == 'inline'){
+ return false;
+ }
+ let myRect: DOMRect = this.el.nativeElement.getBoundingClientRect();
+ let containerRect: DOMRect = this.container.getBoundingClientRect();
+ return myRect.top < containerRect.top
+ || myRect.bottom > containerRect.bottom
+ || myRect.left < containerRect.left
+ || myRect.right > containerRect.right;
+ }
+ /**
+ * informs the {@link /injectables/PageViewService.html|PageViewService} about
+ * click events on {@link #interactedObject|interactedObject}.
+ **/
+ @HostListener('click', ['$event']) onMouseClick( e: MouseEvent) {
+ this.pageViewService.onClickService(this.interactedObject, { visible: true, layerX: e.layerX, layerY: e.layerY, clientX: e.clientX, clientY: e.clientY });
+ }
+ /**
+ * informs the {@link /injectables/PageViewService.html|PageViewService} about
+ * mouse enter events on {@link #interactedObject|interactedObject}.
+ **/
+ @HostListener('mouseenter', ['$event']) onMouseEnter( e: MouseEvent) {
+ this.pageViewService.onHoverService(this.interactedObject, { visible: true, layerX: e.layerX, layerY: e.layerY, clientX: e.clientX, clientY: e.clientY });
+ }
+ /**
+ * informs the {@link /injectables/PageViewService.html|PageViewService} about
+ * mouse leave events on {@link #interactedObject|interactedObject}.
+ **/
+ @HostListener('mouseleave') onMouseLeave() {
+ this.pageViewService.offHoverService(this.interactedObject);
+ }
+}
diff --git a/src/app/page-view/margin-field/line.directive.spec.ts b/src/app/page-view/margin-field/line.directive.spec.ts
deleted file mode 100644
index b37eaaa..0000000
--- a/src/app/page-view/margin-field/line.directive.spec.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { LineDirective } from './line.directive';
-
-
diff --git a/src/app/page-view/margin-field/line.directive.ts b/src/app/page-view/margin-field/line.directive.ts
deleted file mode 100644
index b1b140d..0000000
--- a/src/app/page-view/margin-field/line.directive.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Directive, HostListener, Input } from '@angular/core';
-import { Line } from '../models';
-import { PageViewService } from '../page-view.service';
-
-
-@Directive({
- selector: '[interactedLine]'
-})
-export class LineDirective {
- @Input('interactedLine') interactedLine: Line;
-
- constructor(private lineservice: PageViewService) { }
-
- @HostListener('click') onMouseClick() {
- // alert(this.word);
- this.lineservice.updateInfo(this.interactedLine);
- this.lineservice.onLineClickService(this.interactedLine);
- }
-
- @HostListener('mouseenter') onMouseEnter() {
- this.lineservice.mouseEnterLineService(this.interactedLine);
- }
-
- @HostListener('mouseleave') onMouseLeave() {
- this.lineservice.mouseLeaveLineService(this.interactedLine);
- }
-}
diff --git a/src/app/page-view/margin-field/margin-field.component.html b/src/app/page-view/margin-field/margin-field.component.html
index 22b24d3..f036a1b 100644
--- a/src/app/page-view/margin-field/margin-field.component.html
+++ b/src/app/page-view/margin-field/margin-field.component.html
@@ -1,18 +1,18 @@
diff --git a/src/app/page-view/margin-field/margin-field.component.ts b/src/app/page-view/margin-field/margin-field.component.ts
index fc471e7..0d77c95 100644
--- a/src/app/page-view/margin-field/margin-field.component.ts
+++ b/src/app/page-view/margin-field/margin-field.component.ts
@@ -1,77 +1,160 @@
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { externalAssignStyle, Line, TextField, Word} from '../models';
import { PageViewService } from '../page-view.service';
import { HIGHTLIGHT_CASES } from '../highlight_status';
-
+/**
+ * This component displays an Array of lines.
+ **/
@Component({
selector: 'margin-field',
templateUrl: './margin-field.component.html',
styleUrls: ['./margin-field.component.css']
})
export class MarginFieldComponent implements OnInit, OnChanges {
+ /**
+ * scrollable HTML-container of this textfield
+ **/
+ @Input() container: HTMLElement;
+ /**
+ * the hovered status for a line
+ **/
+ HOVERED_STATUS: string = HIGHTLIGHT_CASES.LINE_HOVERED
+ /**
+ * the currently hovered line
+ * */
hoveredLine?: Line;
+ /**
+ * the currently hovered word
+ * */
hoveredWord?: Word;
+ /**
+ * an Array of lines that will be displayed.
+ **/
@Input() lines: Line[];
+ /**
+ * the height of a line rect.
+ **/
line_height: number = 8;
+ /**
+ * the length of the line rect.
+ **/
line_length: number = 10;
+ /**
+ * x coordinate of the line rect.
+ **/
line_x: number = 5;
+ /**
+ * the height of the margin field.
+ **/
margin_height: number = 973.91998;
+ /**
+ * geometrical top position of the margin field.
+ **/
margin_top: number = 0;
+ /**
+ * the width of the margin field.
+ **/
margin_width: number = 30;
+ /**
+ * The area of the image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ * The height of the text_field determines {@link #margin_height|margin_height}, while its top position
+ * determines {@link #margin_top|margin_top}.
+ **/
@Input() text_field: TextField;
+ /**
+ * The viewbox of this svg component.
+ **/
viewBox: string = '';
+ /**
+ * initial maximum height of margin field.
+ **/
@Input() max_height: number = -1;
+ /**
+ * global zoom factor
+ **/
@Input() zoomFactor: number = 1;
- @Input('assignStyle') assignStyle: externalAssignStyle = (line: Line, hoveredWord: Word, hoveredLine: Line, hoverStatus: string): Object =>{ return {} };
- identification_key: string = 'iri';
+ /**
+ * An optional function that can be passed to this component in order to return a (svg-)style object
+ * to the line rects. This function allows the user to extend the style of this component.
+ * E.g. by returning { fill: blue } the function overwrites the default behaviour and sets
+ * the default highlight color to blue.
+ **/
+ @Input('assignStyle') extAssignStyle?: externalAssignStyle;
+ /**
+ * local zoom factor
+ **/
local_zoom: number = 1;
+ /**
+ * @param lineservice an information source about (un-)hovered and clicked Lines/Words.
+ **/
constructor( private lineservice: PageViewService) { }
+ /**
+ * Initialize geometrical information and subscribe to {@link /injectables/PageViewService.html|PageViewService}.
+ **/
ngOnInit() {
if (this.max_height == -1){
this.max_height = screen.availHeight;
}
this.viewBox = 0 + ' ' + this.margin_top + ' ' + this.margin_width + ' ' + this.margin_height;
- if (this.lines.length > 0 && this.lines[0]['iri'] === undefined){
- this.identification_key = 'id';
- }
if (this.text_field != null) {
this.updateViewBox()
}
this.lineservice.onHoveredLine.subscribe(
(changedLine: Line) => { this.hoveredLine = changedLine;}
);
this.lineservice.offHoveredLine.subscribe(
(changedLine: Line) => { this.hoveredLine = null; }
);
this.lineservice.onHoveredWord.subscribe(
(changedWord: Word) => { this.hoveredWord = changedWord;}
);
this.lineservice.offHoveredWord.subscribe(
(changedWord: Word) => { this.hoveredWord = null; }
);
}
+ /**
+ * Update viewBox if there is a change.
+ **/
ngOnChanges(changes: SimpleChanges) {
if (this.text_field != null) {
this.updateViewBox()
}
}
+ /**
+ * Update viewBox: set
+ * {@link #margin_height|margin_height},
+ * {@link #margin_top|margin_top},
+ * {@link #viewBox|viewBox}
+ * and {@link #local_zoom|local_zoom} according to
+ * {@link #text_field|text_field}.
+ **/
private updateViewBox(){
+ this.lines.forEach(line =>line.datatype = "Line");
this.margin_height = this.text_field.height;
- this.local_zoom = this.max_height/this.text_field.height;
this.margin_top = this.text_field.top;
this.viewBox = 0 + ' ' + this.margin_top + ' ' + this.margin_width + ' ' + this.margin_height;
+ this.local_zoom = this.max_height/this.text_field.height;
}
+ /**
+ * Get the hover status of a line, i.e. whether it is hovered
+ * ({@link /miscellaneous/enumerations.html#HIGHTLIGHT_CASES|HIGHTLIGHT_CASES.LINE_HOVERED})
+ * or not ({@link /miscellaneous/enumerations.html#HIGHTLIGHT_CASES|HIGHTLIGHT_CASES.DEFAULT}).
+ **/
private getHoverStatus(line: Line): string {
if ( (typeof this.hoveredLine !== 'undefined' && this.hoveredLine !== null && line === this.hoveredLine)
- || (typeof this.hoveredWord !== 'undefined' && this.hoveredWord !== null && line[this.identification_key] == this.hoveredWord.line)) {
+ || (typeof this.hoveredWord !== 'undefined' && this.hoveredWord !== null && line.id == this.hoveredWord.line)) {
return HIGHTLIGHT_CASES.LINE_HOVERED;
} else {
return HIGHTLIGHT_CASES.DEFAULT;
}
}
- private highlight(line: Line){
- return this.getHoverStatus(line) == HIGHTLIGHT_CASES.LINE_HOVERED;
- }
+ /**
+ * Assign a style to the rects of a line.
+ **/
+ private assignStyle(line: Line, hoveredWord: Word, hoveredLine: Line, hoverStatus: string): Object {
+ return (this.extAssignStyle != null) ? this.extAssignStyle(line, hoveredWord, hoveredLine, hoverStatus) : {};
+ }
+
}
diff --git a/src/app/page-view/models.ts b/src/app/page-view/models.ts
index 7d3f57a..c5d4fcd 100644
--- a/src/app/page-view/models.ts
+++ b/src/app/page-view/models.ts
@@ -1,48 +1,186 @@
+/**
+ * This interface specifies a function that returns a style class string (e.g. 'textfield unhighlighted')
+ * that can be passed to [ngClass].
+ **/
export interface externalAssignClass {
(currentWord: Word, hoveredWord: Word, hoveredLine: Line): string;
}
+/**
+ * This interface specifies a function that returns a style Object (e.g. { fill: red })
+ * that can be passed to [ngStyle].
+ **/
export interface externalAssignStyle {
(currentItem: Line | Word, hoveredWord: Word, hoveredLine: Line, hoverStatus: string): Object;
}
+/**
+ * This interface specifies a configuration
+ * */
+export interface Configuration {
+ [name: string]: any;
+}
+/**
+ * This interface specifies an object that can interact with {@link /injectables/PageViewService.html|PageViewService}.
+ **/
+export interface Interactable {
+ /**
+ * the string representation of the Interactable's interface type
+ * ({@link /interfaces/Word.html|Word}|{@link /interfaces/Line.html|Line}|{@link /interfaces/TextByForeignHand.html|TextByForeignHand}).
+ **/
+ datatype?: string;
+ /**
+ * the identity of the textfield to which this Interactable belongs.
+ **/
+ textfield_identity?: string;
+ /**
+ * is Interactable top object
+ **/
+ is_top_object?: boolean;
+}
+/**
+ * This interface specifies the image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ **/
export interface Image {
+ /** x coordinate of image
+ **/
x: number;
+ /** y coordinate of image
+ **/
y: number;
+ /** width of image
+ **/
width: number;
+ /** height of image
+ **/
height: number;
+ /** filename of image
+ **/
filename: string;
+ /** primary URL of image
+ **/
URL: string;
+ /** secondary URL of image
+ **/
secondaryURL?: string;
+ /** displayable area of image
+ **/
text_field: TextField;
+ /** matrix transformation string
+ **/
transform?: string;
}
-export interface Line {
- id: number;
- iri?: string;
+/**
+ * This interface specifies a line that will be displayed by {@link /components/MarginFieldComponent.html|MarginFieldComponent}.
+ **/
+export interface Line extends Interactable {
+ /** the line number
+ **/
+ number: number;
+ /** the (optional) IRI of this line
+ **/
+ id: Identifier;
+ /** geometrical bottom position of this line
+ **/
bottom: number;
+ /** geometrical top position of this line
+ **/
top: number;
}
+/**
+ * This interface specifies the area of an image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ **/
export interface TextField {
+ /** the width of this textfield
+ **/
width: number;
+ /** the height of this textfield
+ **/
height: number;
+ /** the geometrical left position of this textfield
+ **/
left: number;
+ /** the geometrical top position of this textfield
+ **/
top: number;
}
-export interface BaseWord {
- text: string;
- edited_text?: string;
+/**
+ * This type specifies an identifier for words/lines (by its IRI string or its id number)
+ **/
+export type Identifier = string | number;
+/**
+ * Any svg path with an optional type.
+ **/
+export interface Path {
+ id: Identifier;
+ d: string;
+ type?: string;
+}
+/**
+ * geometrical Point
+ **/
+export interface Point {
+ visible: boolean
+ clientX: number;
+ clientY: number;
+ layerX: number;
+ layerY: number;
+}
+/**
+ * This interface specifies a postional object that can be displayed as a rect on the image by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ **/
+export interface PositionalObject extends Interactable {
+ /** the identifier of a positional object (i.e. 'IRI' (string) or 'id' (number))
+ **/
+ id: Identifier;
+ /** the geometrical left position of this word's rect.
+ **/
left: number;
+ /** the geometrical top position of this word's rect.
+ **/
top: number;
+ /** the width of this word's rect.
+ **/
width: number;
+ /** the height of this word's rect.
+ **/
height: number;
- line: string | number;
- line_number: number;
- deleted: boolean;
+ /** the matrix transformation string of the geometrical position of this word's rect.
+ **/
transform?: string;
}
-export interface IRIWord extends BaseWord {
- iri: string;
+/**
+ * This interface specifies a text written by a foreign hand.
+ **/
+export interface TextByForeignHand extends PositionalObject {
+ /**
+ * pen used for writing text
+ **/
+ pen: string;
+ /**
+ * text by foreign hand
+ **/
+ text: string;
}
-export interface IdWord extends BaseWord {
- id: number;
+/**
+ * This interface specifies a word that can be displayed as a rect on the image by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ **/
+export interface Word extends PositionalObject {
+ /** the (raw) text of this word.
+ **/
+ text: string;
+ /** the text of this word as it has been edited by the editors.
+ **/
+ edited_text?: string;
+ /** the identification of the line to which this word belongs (iri or id).
+ **/
+ line: string | number;
+ /** the number of the line to which this word belongs.
+ **/
+ line_number: number;
+ /** is this word deleted.
+ **/
+ deleted: boolean;
+ /** a deletion path
+ **/
+ deletion_path?: string;
}
-export type Word = IRIWord | IdWord
+export const USE_EXTERNAL_TOOLTIP: string = 'UseExternalTooltip';
diff --git a/src/app/page-view/page-view.component.css b/src/app/page-view/page-view.component.css
index c97d26f..2151653 100644
--- a/src/app/page-view/page-view.component.css
+++ b/src/app/page-view/page-view.component.css
@@ -1,27 +1,31 @@
#page {
width: 100%;
- /*height: 700px;*/
- margin: 0;
- padding: 0;
- white-space: nowrap;
- /*overflow-y: hidden;
- overflow-x: auto;*/
- overflow: auto;
+ margin: 0;
+ padding: 0;
+ white-space: nowrap;
+}
+.inline {
+ display: inline-block;
+}
+.breakline {
+ display: block;
+ height: 50%;
+ overflow: scroll;
}
-#gap {
+.gap {
display: inline-block;
width: 1px;
height: 100%;
margin: 0;
}
#margin {
display: inline-block;
height: 100%;
margin: 0;
}
#textfield {
display: inline-block;
/*width: 95%;*/
height: 100%;
margin: 0;
}
diff --git a/src/app/page-view/page-view.component.html b/src/app/page-view/page-view.component.html
index 439587f..abee801 100644
--- a/src/app/page-view/page-view.component.html
+++ b/src/app/page-view/page-view.component.html
@@ -1,20 +1,37 @@
-
-
-
+
+
-
-
-
-
-
-
-
-
-
diff --git a/src/app/page-view/page-view.component.ts b/src/app/page-view/page-view.component.ts
index ffbf970..d85a383 100644
--- a/src/app/page-view/page-view.component.ts
+++ b/src/app/page-view/page-view.component.ts
@@ -1,29 +1,115 @@
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
-import { externalAssignClass, externalAssignStyle, Image, Line, Word} from './models';
-
+import { externalAssignClass, externalAssignStyle, Configuration, Identifier, Image, Line, TextByForeignHand, Word} from './models';
+/**
+ * This component displays one or two {@link /components/TextFieldComponent.html|TextFieldComponent(s)}
+ * and its or their {@link /components/MarginFieldComponent.html|MarginFieldComponent(s)}.
+ **/
@Component({
selector: 'page-view',
templateUrl: './page-view.component.html',
styleUrls: ['./page-view.component.css']
})
export class PageViewComponent implements OnInit {
+ @Input() configuration: Configuration;
+ /**
+ * the search text of words that should be highlighted as {@link /miscellaneous/enumerations.html#HIGHTLIGHT_CASES|HIGHTLIGHT_CASES.SEARCHED_WORD}.
+ **/
@Input() findText: string;
+ /**
+ * first texts written by foreign hand
+ **/
+ @Input() first_foreign_texts: TextByForeignHand[] = [];
+ /**
+ * the first image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ **/
@Input() first_image: Image;
+ /**
+ * the Array of lines of the first image that will be displayed by {@link /components/MarginFieldComponent.html|MarginFieldComponent}.
+ **/
@Input() first_lines: Line[];
+ /**
+ * Identification of first textfield.
+ **/
+ first_textfield_id: string = 'first textfield'
+ /**
+ * the Array of words of the first image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ **/
@Input() first_words: Word[];
+ /**
+ * the (initial) maximum height of the image(s).
+ **/
+ @Input() max_height: number = -1;
+ /**
+ * should primary Url be used for image. Use secondary Url if false.
+ **/
+ @Input() preferPrimaryUrl: boolean = true;
+ /**
+ * second texts written by foreign hand
+ **/
+ @Input() second_foreign_texts: TextByForeignHand[] = [];
+ /**
+ * the second image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ **/
@Input() second_image: Image;
+ /**
+ * the Array of lines of the second image that will be displayed by {@link /components/MarginFieldComponent.html|MarginFieldComponent}.
+ **/
@Input() second_lines: Line[];
+ /**
+ * Identification of second textfield.
+ **/
+ second_textfield_id: string = 'second textfield'
+ /**
+ * the Array of words of the second image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}.
+ **/
@Input() second_words: Word[];
+ /**
+ * An optional function that will be passed to {@link /components/TextFieldComponent.html|TextFieldComponent}
+ * in order to return a further highlight class
+ * to the word rects when the internal function would return 'textfield unhighlighted'.
+ **/
+ @Input('assignClass') assignClass?: externalAssignClass;
+ /**
+ * An optional function that will be passed to {@link /components/TextFieldComponent.html|TextFieldComponent}
+ * and {@link /components/MarginFieldComponent.html|MarginFieldComponent}
+ * in order to return a (svg-)style object
+ * to the word and line rects. This function allows the user to extend the style of this component.
+ * E.g. by returning { fill: blue } the function overwrites the default behaviour and sets
+ * the default highlight color to blue.
+ **/
+ @Input('assignStyle') assignStyle?: externalAssignStyle;
+ /**
+ * global zoom factor.
+ **/
@Input() zoomFactor: number = 1;
- @Input() max_height: number = -1;
- @Input('assignClass') assignClass: externalAssignClass;
- @Input('assignStyle') assignStyle: externalAssignStyle;
-
- constructor() { }
+ /**
+ * identifiers of selected words that should be highlighted.
+ **/
+ @Input() selectedWords: Identifier[] = [];
+ /**
+ * identifiers of selected lines that should be highlighted.
+ **/
+ @Input() selectedLines: Identifier[] = [];
+ constructor() {}
+
+ /**
+ * sets {@link /components/PageViewComponent.html#max_height|max_height} if it is unset.
+ **/
ngOnInit() {
if (this.max_height == -1){
this.max_height = screen.availHeight;
}
}
+ /**
+ * Returns whether the two images can be displayed as columns.
+ **/
+ private hasColumnStyle(): boolean {
+ if (this.zoomFactor <= 1 || this.first_image == null || this.second_image == null){
+ return true
+ }
+ let newLeftWidth = this.max_height/this.first_image.text_field.height*this.zoomFactor*this.first_image.text_field.width;
+ let newRightWidth = this.max_height/this.second_image.text_field.height*this.zoomFactor*this.second_image.text_field.width;
+ return newLeftWidth + newRightWidth < screen.availWidth;
+ }
}
diff --git a/src/app/page-view/page-view.module.ts b/src/app/page-view/page-view.module.ts
index f72bb6f..3499622 100644
--- a/src/app/page-view/page-view.module.ts
+++ b/src/app/page-view/page-view.module.ts
@@ -1,27 +1,29 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { LineDirective } from './margin-field/line.directive';
import { MarginFieldComponent } from './margin-field/margin-field.component';
import { TextFieldComponent} from './textfield-component/textfield.component';
-import { WordPositionDirective } from './textfield-component/word-position.directive';
+import { InteractedDirective } from './interacted.directive';
import { PageViewComponent } from './page-view.component';
+import { PageViewService } from './page-view.service';
@NgModule({
declarations: [
- LineDirective,
- WordPositionDirective,
+ InteractedDirective,
MarginFieldComponent,
TextFieldComponent,
PageViewComponent
],
imports: [
CommonModule
],
+ providers: [
+ PageViewService
+ ],
exports: [
- MarginFieldComponent,
+ MarginFieldComponent,
TextFieldComponent,
PageViewComponent
]
})
export class PageViewModule { }
diff --git a/src/app/page-view/page-view.service.ts b/src/app/page-view/page-view.service.ts
index ee16d68..a1a2211 100644
--- a/src/app/page-view/page-view.service.ts
+++ b/src/app/page-view/page-view.service.ts
@@ -1,52 +1,81 @@
-import { Line, Word } from './models';
import {EventEmitter, Injectable} from '@angular/core';
-import {Subject} from 'rxjs';
-
+import { Configuration, Interactable, Line, Point, TextByForeignHand, Word } from './models';
+/**
+ * This is an information service about clicked and (un-)hovered
+ * {@link /interfaces/Line.html|Lines},
+ * {@link /miscellaneous/typealiases.html#Word|Words}.
+ * and {@link /interfaces/TextByForeignHand.html|TextByForeignHands}.
+ * */
@Injectable()
export class PageViewService {
+ /**
+ * hovered line emitter
+ **/
onHoveredLine = new EventEmitter
();
+ /**
+ * off hovered line emitter
+ **/
offHoveredLine = new EventEmitter();
- oldClickedLine: Line;
+ /**
+ * clicked line emitter
+ **/
onClickedLine = new EventEmitter();
- onClickedWord = new EventEmitter();
+ /**
+ * hovered word emitter
+ **/
onHoveredWord = new EventEmitter();
+ /**
+ * off hovered word emitter
+ **/
offHoveredWord = new EventEmitter();
- oldClickedWord: Word;
-
- private informationSource = new Subject(); // any clicked/interacted thing
- lineChange$ = this.informationSource.asObservable();
- private wordInformationSource = new Subject(); // any clicked/interacted thing
- wordChange$ = this.wordInformationSource.asObservable();
+ /**
+ * clicked word emitter
+ **/
+ onClickedWord = new EventEmitter();
+ /**
+ * clicked text by foreign hand emitter
+ **/
+ onClickedTextByForeignHand = new EventEmitter();
+ /**
+ * hovered text by foreign hand emitter
+ **/
+ onHoveredTextByForeignHand = new EventEmitter();
+ /**
+ * off hovered text by foreign hand emitter
+ **/
+ offHoveredTextByForeignHand = new EventEmitter();
+ /**
+ * point where mouse hovered/clicked
+ **/
+ mousePosition = new EventEmitter();
+ /**
+ * configuration change emitter
+ * */
+ configuration = new EventEmitter();
- updateInfo(information: Line) {
- this.informationSource.next(information);
- }
- updateWordInfo(information: Word) {
- this.wordInformationSource.next(information);
+ /**
+ * emit an event on 'onClicked' + interactable.datatype
+ **/
+ public onClickService(interactable: Interactable, point: Point){
+ this['onClicked' + interactable.datatype].emit(interactable);
+ if (interactable.datatype != 'Line'){
+ this.mousePosition.emit(point);
+ }
}
- public onWordClickService(word: Word) {
- if ( word !== this.oldClickedWord) {
- this.onClickedWord.emit(word);
- this.oldClickedWord = word;
- }
+ /**
+ * emit an event on 'onHover' + interactable.datatype
+ **/
+ public onHoverService(interactable: Interactable, point: Point){
+ this['onHovered' + interactable.datatype].emit(interactable);
+ if (interactable.datatype != 'Line'){
+ this.mousePosition.emit(point);
+ }
}
- public onLineClickService(line: Line) {
- if ( line !== this.oldClickedLine) {
- this.onClickedLine.emit(line);
- this.oldClickedLine = line;
- }
+ /**
+ * emit an event on 'offHover' + interactable.datatype
+ **/
+ public offHoverService(interactable: Interactable){
+ this['offHovered' + interactable.datatype].emit(interactable);
+ this.mousePosition.emit({visible: false, clientX: -1, clientY: -1, layerX: -1, layerY: -1 });
}
- public mouseEnterLineService(line: Line) {
- this.onHoveredLine.emit(line);
- }
- public mouseLeaveLineService(line: Line) {
- this.offHoveredLine.emit(line);
- }
- public mouseEnterWordService(word: Word) {
- this.onHoveredWord.emit(word);
- }
- public mouseLeaveWordService(word: Word) {
- this.offHoveredWord.emit(word);
- }
-
}
diff --git a/src/app/page-view/textfield-component/textfield.component.css b/src/app/page-view/textfield-component/textfield.component.css
index fbba924..824eeda 100644
--- a/src/app/page-view/textfield-component/textfield.component.css
+++ b/src/app/page-view/textfield-component/textfield.component.css
@@ -1,44 +1,41 @@
.textfield {
background-color: #DADADA;
}
.textfield .deleted {
fill: grey;
opacity: 0.3;
}
.textfield .highlight_red {
fill: #e20000;
opacity: 0.3;
}
.textfield .highlight_yellow {
fill: #e2fa00;
opacity: 0.3;
}
.textfield .highlight_magenta {
fill: #FF00FF;
opacity: 0.3;
}
-.textfield .border {
- stroke: #e2fa00;
- stroke-width:5;
- opacity: 0.3;
+.textfield .highlight_foreign_text {
+ fill: blue;
+ opacity: 0.5;
}
.textfield .unhighlighted {
opacity: 0.0;
}
-.textfield .howered_line {
- fill: #e2fa00;
- opacity: 0.3;
-}
-
-.textfield .same_word {
- fill: #c9fac5;
- opacity: 0.3;
+.textfield .highlight_path {
+ stroke: red;
+ fill: none;
+ stroke-width: 0.2;
}
-
-.text_fadeout {
- fill: #a4a4a4;
+.textfield .unhighlighted_path {
+ stroke: none;
+ fill: none;
+ opacity: 0.0;
}
-
-.hover {
- transform: scale(1.5);
+.textfield .border {
+ stroke: #e2fa00;
+ stroke-width:5;
+ opacity: 0.3;
}
diff --git a/src/app/page-view/textfield-component/textfield.component.html b/src/app/page-view/textfield-component/textfield.component.html
index 5a9c53b..641a561 100644
--- a/src/app/page-view/textfield-component/textfield.component.html
+++ b/src/app/page-view/textfield-component/textfield.component.html
@@ -1,17 +1,33 @@
diff --git a/src/app/page-view/textfield-component/textfield.component.ts b/src/app/page-view/textfield-component/textfield.component.ts
index 70851bb..596d1bf 100644
--- a/src/app/page-view/textfield-component/textfield.component.ts
+++ b/src/app/page-view/textfield-component/textfield.component.ts
@@ -1,114 +1,257 @@
-import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
-import { externalAssignClass, externalAssignStyle, Image, Line, Word} from '../models';
+import { Component, ElementRef, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
+import { externalAssignClass, externalAssignStyle, Configuration, Identifier, Image, Line, PositionalObject, TextByForeignHand, Word, USE_EXTERNAL_TOOLTIP} from '../models';
import { PageViewService } from '../page-view.service';
import { HIGHTLIGHT_CASES } from '../highlight_status';
-
-
+import { ConfigurableComponent } from '../configurable-component';
+/**
+ * This component displays an image with word hovers.
+ **/
@Component({
selector: 'text-field',
templateUrl: './textfield.component.html',
styleUrls: ['./textfield.component.css']
})
-/**
- * Textfield component
- */
-export class TextFieldComponent implements OnInit, OnChanges {
+export class TextFieldComponent extends ConfigurableComponent implements OnInit, OnChanges {
+ /**
+ * scrollable HTML-container of this textfield
+ **/
+ @Input() container: HTMLElement;
+ /**
+ * the currently clicked word
+ * */
clickedWord?: Word;
+ /**
+ * the search text of words that should be highlighted as {@link /miscellaneous/enumerations.html#HIGHTLIGHT_CASES|HIGHTLIGHT_CASES.SEARCHED_WORD}.
+ **/
@Input() findText: string;
+ /**
+ * texts written by foreign hand
+ **/
+ @Input() foreign_texts: TextByForeignHand[] = [];
+ /**
+ * the currently hovered line
+ * */
hoveredLine?: Line;
+ /**
+ * the currently hovered text by foreign hand
+ * */
+ hoveredTextByForeignHand?: TextByForeignHand;
+ /**
+ * the currently hovered word
+ * */
hoveredWord?: Word;
+ /**
+ * the image that will be displayed.
+ **/
@Input() image: Image;
+ /**
+ * textfield's identity.
+ **/
+ @Input() identity: string = 'first textfield';
+ /**
+ * The (unzoomed) height of the root svg.
+ *
+ * (The actual height is 'image_height*local_zoom*zoomFactor'
+ * */
image_height: number = 400;
- imageSpec = { x: 0, y: 0, height: 973.91998, width: 2038.5601 };
+ /**
+ * image properties for the svg-image.
+ * */
+ imageSpec = { x: 0, y: 0, height: 973.91998, width: 2038.5601, URL: null, secondaryURL: null };
+ /**
+ * The (unzoomed) width of the root svg.
+ *
+ * (The actual width is 'image_width*local_zoom*zoomFactor'
+ * */
image_width: number = 300;
+ /**
+ * the viewBox of the root svg specifying the area of the svg that will be shown.
+ * */
viewBox: string = '';
+ /**
+ * the (initial) maximum height of the image.
+ **/
@Input() max_height: number = -1;
+ /**
+ * should primary Url be used for image. Use secondary Url if false.
+ **/
+ @Input() preferPrimaryUrl: boolean = true;
+ /**
+ * Use extended tooltip.
+ **/
+ @Input() useExtendedTooltip: boolean = false;
+ /**
+ * the words that will be displayed as rects on the image.
+ **/
@Input() words: Word[];
+ /**
+ * global zoom factor.
+ **/
@Input() zoomFactor: number = 1;
+ /**
+ * local zoom factor that sets the height and width of the image according to {@link #max_height|max_height}.
+ * */
local_zoom: number = 1;
- identification_key: string = 'iri';
- @Input('assignClass') externalAssignClassAfter: externalAssignClass;
- @Input('assignStyle') assignStyle: externalAssignStyle = (word: Word, hoveredWord: Word, hoveredLine: Line, hoverStatus: string): Object =>{ return {} };
-
- constructor( private wordservice: PageViewService) { }
-
+ /**
+ * An optional function that can be passed to this component in order to return a further highlight class
+ * to the word rects when the internal function would return 'textfield unhighlighted'.
+ **/
+ @Input('assignClass') externalAssignClassAfter?: externalAssignClass;
+ /**
+ * An optional function that can be passed to this component in order to return a (svg-)style object
+ * to the word rects. This function allows the user to extend the style of this component.
+ * E.g. by returning { fill: blue } the function overwrites the default behaviour and sets
+ * the default highlight color to blue.
+ **/
+ @Input('assignStyle') extAssignStyle?: externalAssignStyle;
+ /**
+ * identifiers of selected words that should be highlighted.
+ **/
+ @Input() selectedWords: Identifier[] = [];
+ /**
+ * identifiers of selected lines that should be highlighted.
+ **/
+ @Input() selectedLines: Identifier[] = [];
+ /**
+ * @param pageViewService an information source about (un-)hovered and clicked Lines/Words.
+ * */
+ constructor( protected pageViewService: PageViewService) {
+ super()
+ }
ngOnInit() {
if (this.max_height == -1){
this.max_height = screen.availHeight;
}
if (this.image.text_field != null) {
this.updateImageProperties();
} else if (this.imageSpec != null) {
this.image_width = this.imageSpec.width;
this.image_height = this.imageSpec.height;
this.viewBox = '0 0 ' + this.image_width + ' ' + this.image_height;
}
- if (this.words.length > 0 && this.words[0]['iri'] === undefined){
- this.identification_key = 'id';
- }
- this.wordservice.onClickedWord.subscribe(
+ this.pageViewService.onClickedWord.subscribe(
(changedWord: Word ) => this.clickedWord = changedWord
);
-
- this.wordservice.onHoveredWord.subscribe(
+ this.pageViewService.onHoveredWord.subscribe(
(changedWord: Word) => this.hoveredWord = changedWord
);
- this.wordservice.offHoveredWord.subscribe(
+ this.pageViewService.offHoveredWord.subscribe(
(changedWord: Word) => { this.hoveredWord = null; }
);
-
- this.wordservice.onHoveredLine.subscribe(
+ this.pageViewService.onHoveredLine.subscribe(
(changedLine: Line) => { this.hoveredLine = changedLine;}
);
-
- this.wordservice.offHoveredLine.subscribe(
+ this.pageViewService.offHoveredLine.subscribe(
(changedLine: Line) => { this.hoveredLine = null; }
);
+ this.pageViewService.onHoveredTextByForeignHand.subscribe(
+ (changedForeignText: TextByForeignHand) => { this.hoveredTextByForeignHand = changedForeignText;}
+ );
+ this.pageViewService.offHoveredTextByForeignHand.subscribe(
+ (changedForeignText: TextByForeignHand) => { this.hoveredTextByForeignHand = null; }
+ );
}
- ngOnChanges(changes: SimpleChanges) {
+ ngOnChanges() {
+ super.ngOnChanges()
if (this.image.text_field != null) {
this.updateImageProperties();
}
}
- private updateImageProperties(){
+ /**
+ * Update image properties: use textfield in order to specify the area of the image that will be shown.
+ *
+ * @param URL set alternative image url. This will be used on image load error (see Template)
+ **/
+ private updateImageProperties(URL?: string){
+ let previous_word: Word = null;
+ for (var i = 0; i < this.words.length; i++){
+ this.words[i].datatype = "Word";
+ if (previous_word == null || previous_word.id != this.words[i].id){
+ previous_word = this.words[i]
+ previous_word.is_top_object = true;
+ } else if (previous_word.top > this.words[i].top){
+ previous_word.is_top_object = false;
+ previous_word = this.words[i]
+ previous_word.is_top_object = true;
+ } else {
+ this.words[i].is_top_object = false;
+ }
+ }
+ this.foreign_texts.forEach(foreignText =>foreignText.datatype = "TextByForeignHand");
let image_left = this.image.text_field.left;
let image_top = this.image.text_field.top;
this.image_width = this.image.text_field.width;
this.image_height = this.image.text_field.height;
this.local_zoom = this.max_height/this.image.text_field.height;
this.imageSpec.x = this.image.x;
this.imageSpec.y = this.image.y;
this.imageSpec.height = this.image.height;
this.imageSpec.width = this.image.width;
+ this.imageSpec.URL = (this.preferPrimaryUrl) ? this.image.URL : this.image.secondaryURL;
+ this.imageSpec.secondaryURL = (this.preferPrimaryUrl) ? this.image.URL : this.image.URL;
+ if (URL != null){
+ this.imageSpec.secondaryURL = this.imageSpec.URL
+ this.imageSpec.URL = URL;
+ }
this.viewBox = image_left + ' ' + image_top + ' ' + this.image_width + ' ' + this.image_height;
}
+ /**
+ * Get the hover status of the word as one of the {@link /miscellaneous/enumerations.html#HIGHTLIGHT_CASES|HIGHTLIGHT_CASES}.
+ **/
private getHoverStatus(word: Word): string {
+ if (this.selectedWords.indexOf(word.id) > -1
+ || this.selectedLines.indexOf(word.line) > -1){
+ return HIGHTLIGHT_CASES.SELECTED_WORD;
+ }
if (this.findText != null && this.findText != ''){
return (word.text.match(this.findText)
|| (word.edited_text != null && word.edited_text.match(this.findText))
) ? HIGHTLIGHT_CASES.SEARCHED_WORD : HIGHTLIGHT_CASES.DEFAULT
}
if (typeof this.hoveredLine !== 'undefined' && this.hoveredLine !== null) {
- return (this.hoveredLine[this.identification_key] == word.line) ? HIGHTLIGHT_CASES.LINE_HOVERED : HIGHTLIGHT_CASES.DEFAULT;
+ return (this.hoveredLine.id == word.line) ? HIGHTLIGHT_CASES.LINE_HOVERED : HIGHTLIGHT_CASES.DEFAULT;
} else if (typeof this.hoveredWord !== 'undefined' && this.hoveredWord !== null){
- return (this.hoveredWord[this.identification_key] == word[this.identification_key]) ? HIGHTLIGHT_CASES.WORD_HOVERED : HIGHTLIGHT_CASES.DEFAULT;
+ return (this.hoveredWord.id == word.id) ? HIGHTLIGHT_CASES.WORD_HOVERED : HIGHTLIGHT_CASES.DEFAULT;
}
return HIGHTLIGHT_CASES.DEFAULT;
}
- private assignClass(word: Word): string {
+ /**
+ * Return a css class for word that will be used with [ngClass] in order to (un-)highlight the word's rect.
+ *
+ * If a function has been passed to Input {@link #assignClass|assignClass},
+ * this function will call it if {@link #getHoverStatus|getHoverStatus(word)} == {@link /miscellaneous/enumerations.html#HIGHTLIGHT_CASES|HIGHTLIGHT_CASES.DEFAULT}.
+ **/
+ private assignClass(positionalObject: PositionalObject, elementName?: string): string {
+ if (positionalObject.datatype == 'TextByForeignHand'){
+ return (this.hoveredTextByForeignHand != null && this.hoveredTextByForeignHand.id == positionalObject.id) ?
+ 'text_field highlight_foreign_text' : 'text_field unhighlighted'
+ }
+ let word = positionalObject;
+ if (elementName != null) {
+ return (this.getHoverStatus(word) == HIGHTLIGHT_CASES.DEFAULT) ? `text_field unhighlighted_${elementName}` : `text_field highlight_${elementName}`;
+ }
switch(this.getHoverStatus(word)) {
+ case HIGHTLIGHT_CASES.SELECTED_WORD: {
+ return 'textfield highlight_magenta';
+ }
case HIGHTLIGHT_CASES.SEARCHED_WORD: {
return 'textfield highlight_red';
}
case HIGHTLIGHT_CASES.LINE_HOVERED: {
- return 'textfield highlight_yellow'
+ return (word.deleted) ? 'textfield deleted' : 'textfield highlight_yellow';
}
case HIGHTLIGHT_CASES.WORD_HOVERED: {
- return 'textfield highlight_yellow'
+ return (word.deleted) ? 'textfield deleted' : 'textfield highlight_yellow';
}
case HIGHTLIGHT_CASES.DEFAULT: {
return (this.externalAssignClassAfter != null) ? this.externalAssignClassAfter(word, this.hoveredWord, this.hoveredLine) : 'textfield unhighlighted';
}
}
}
+ /**
+ * Assign a style to the rects of a line.
+ **/
+ private assignStyle(word: Word, hoveredWord: Word, hoveredLine: Line, hoverStatus: string): Object {
+ return (this.extAssignStyle != null) ? this.extAssignStyle(word, hoveredWord, hoveredLine, hoverStatus) : {};
+ }
}
diff --git a/src/app/page-view/textfield-component/word-position.directive.ts b/src/app/page-view/textfield-component/word-position.directive.ts
deleted file mode 100644
index 54c9a2f..0000000
--- a/src/app/page-view/textfield-component/word-position.directive.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Directive, HostListener, Input } from '@angular/core';
-import { PageViewService } from '../page-view.service';
-import { Word } from '../models';
-
-@Directive({
- selector: '[interactedWord]'
-})
-export class WordPositionDirective {
- @Input('interactedWord') interactedWord: Word;
-
- constructor(private wordservice: PageViewService) {}
-
- @HostListener('click') onMouseClick() {
- // alert(this.word);
- this.wordservice.updateWordInfo(this.interactedWord);
- this.wordservice.onWordClickService(this.interactedWord);
- }
- @HostListener('mouseenter') onMouseEnter() {
- this.wordservice.mouseEnterWordService(this.interactedWord);
- }
-
- @HostListener('mouseleave') onMouseLeave() {
- this.wordservice.mouseLeaveWordService(this.interactedWord);
- }
-
-}