diff --git a/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.ts b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.ts index 472dd20..3b538d2 100644 --- a/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.ts +++ b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.ts @@ -1,194 +1,198 @@ import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { externalAssignStyle, Continuation, Identifier, Line, LineStub, 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 SELECTED_STATUS: string = HIGHTLIGHT_CASES.SELECTED_LINE /** * the currently hovered line * */ hoveredLine?: Line; /** * the currently hovered reference line * */ hoveredReferenceLine?: LineStub; /** * 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; /** * specifies reference type that should be displayed **/ @Input() showReference: string = "to" /** * 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; /** * initial maximum width of margin field. **/ @Input() max_width: number = -1; /** * identifiers of selected lines that should be highlighted. **/ @Input() selectedLines: Identifier[] = []; /** * global zoom factor **/ @Input() zoomFactor: number = 1; /** * 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() { + ngOnInit() { if (this.max_height == -1 && this.max_width == -1){ this.max_height = screen.availHeight; } this.viewBox = 0 + ' ' + this.margin_top + ' ' + this.margin_width + ' ' + this.margin_height; if (this.text_field != null) { this.updateViewBox() } this.lineservice.onHoveredLine.subscribe( (changedLine: Line) => { this.hoveredLine = changedLine;} ); this.lineservice.onHoveredContinuation.subscribe( (changedContinuation: Continuation) => { this.hoveredReferenceLine = changedContinuation.reference.line;} ); this.lineservice.offHoveredContinuation.subscribe( (changedContinuation: Continuation) => { this.hoveredReferenceLine = null} ); 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) { + **/ + 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(){ + **/ + private updateViewBox(){ this.lines.forEach(line =>line.datatype = "Line"); if (this.showReference == 'to'){ this.margin_width = (this.lines.some(line =>line.continuesTo != null || line.continuesTo != undefined)) ? this.line_length*2+30 : 30; } else { let hasReference = this.lines.some(line =>line.continuesFrom != null || line.continuesFrom != undefined) this.margin_width = (this.showReference == 'from' && hasReference) ? this.line_length*2+30 : 30; this.line_x = (this.showReference == 'from' && hasReference) ? 2*this.line_length : 5; } this.margin_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 != -1 && this.max_width == -1) ? this.max_height/this.text_field.height : this.max_width/this.text_field.width; - } - /** + if (this.max_width == -1 && this.text_field.height < this.text_field.width){ + this.local_zoom = (window.innerWidth/2-100)/this.text_field.width; + } + + } + /** * 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 { + private getHoverStatus(line: Line): string { if ( (this.hoveredLine != undefined && this.hoveredLine != null && line.id == this.hoveredLine.id) || (this.hoveredReferenceLine != undefined && this.hoveredReferenceLine != null && line.id == this.hoveredReferenceLine.id) || (this.hoveredWord != undefined && this.hoveredWord != null && line.id == this.hoveredWord.line)) { return HIGHTLIGHT_CASES.LINE_HOVERED; } else if (this.selectedLines.length > 0 && this.selectedLines.indexOf(line.id) > -1) { return HIGHTLIGHT_CASES.SELECTED_LINE } else { return HIGHTLIGHT_CASES.DEFAULT; } - } + } /** * 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/nietzsche-beta-app/src/app/page-view/page-view.component.html b/nietzsche-beta-app/src/app/page-view/page-view.component.html index 1bdcd5c..0beeafb 100644 --- a/nietzsche-beta-app/src/app/page-view/page-view.component.html +++ b/nietzsche-beta-app/src/app/page-view/page-view.component.html @@ -1,50 +1,50 @@
diff --git a/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.ts b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.ts index 863a53e..d091a4a 100644 --- a/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.ts +++ b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.ts @@ -1,345 +1,348 @@ -import { Component, ElementRef, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, ElementRef, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { externalAssignClass, externalAssignStyle, Configuration, Continuation, Identifier, Image, Line, Position, 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'; import { Matrix } from './matrix'; /** * This component displays an image with word hovers. **/ @Component({ selector: 'text-field', templateUrl: './textfield.component.html', styleUrls: ['./textfield.component.css'] }) export class TextFieldComponent extends ConfigurableComponent implements OnInit, OnChanges { /** * scrollable HTML-container of this textfield **/ @Input() container: HTMLElement; /** * the currently clicked word * */ clickedWord?: Word; /** * Debug mode. **/ debug: boolean = false; /** * 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; /** * image properties for the svg-image. * */ imageSpec = { x: 0, y: 0, height: 973.91998, width: 2038.5601, URL: null, secondaryURL: null, transform: 'matrix(1 0 0 1 0 0)' }; /** * 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; /** * the (initial) maximum width of the image. **/ @Input() max_width: 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; /** * 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_width == -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; } this.pageViewService.onClickedWord.subscribe( (changedWord: Word ) => this.clickedWord = changedWord ); this.pageViewService.onHoveredWord.subscribe( (changedWord: Word) => this.hoveredWord = changedWord ); this.pageViewService.offHoveredWord.subscribe( (changedWord: Word) => { this.hoveredWord = null; } ); this.pageViewService.onHoveredLine.subscribe( (changedLine: Line) => { this.hoveredLine = changedLine} ); 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() { super.ngOnChanges() if (this.image.text_field != null) { this.updateImageProperties(); } if(this.debug && this.findText != null && this.findText != ''){ let words = this.words.filter(word =>word.text == this.findText) if (words.length > 0){ this.pageViewService.onHoverService(words[0], {visible: true, clientX: 100, clientY: 100, layerX: -1, layerY: -1 }) } } } /** * 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 != -1 && this.max_width == -1) ? this.max_height/this.image.text_field.height : this.max_width/this.image.text_field.width; + if (this.max_width == -1 && this.image_height < this.image_width){ + this.local_zoom = (window.innerWidth/2-100)/this.image.text_field.width; + } 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; } if(this.image.transform != null){ this.local_zoom = this.max_height/this.image.text_field.width; let matrix = new Matrix(this.image.transform, this.local_zoom*this.zoomFactor); this.imageSpec.transform = matrix.toString() } this.viewBox = image_left + ' ' + image_top + ' ' + this.image_width + ' ' + this.image_height; } /** * Return the position (i.e. '{ x: x, y: y }') for the copyright symbol. * @param dimension dimension of the copyright symbol. **/ private getCopyrightPosition(dimension: number): Object { if (this.image.text_field != null && this.image.text_field != undefined){ let positions: Position[] = [ { x: Number(this.image.text_field.left) +10/this.zoomFactor, y: Number(this.image.text_field.top) +10/this.zoomFactor }, { x: Number(this.image.text_field.left), y: Number(this.image.text_field.top)}, { x: Number(this.image.text_field.width) + Number(this.image.text_field.left) -15/this.zoomFactor - dimension, y: Number(this.image.text_field.height) + Number(this.image.text_field.top) -15/this.zoomFactor - dimension }, { x: Number(this.image.text_field.width) + Number(this.image.text_field.left) - dimension, y: Number(this.image.text_field.height) + Number(this.image.text_field.top) - dimension }, { x: Number(this.image.text_field.left) +10/this.zoomFactor, y: Number(this.image.text_field.height) + Number(this.image.text_field.top) -10/this.zoomFactor - dimension }, { x: Number(this.image.text_field.width) + Number(this.image.text_field.left) -10/this.zoomFactor - dimension, y: Number(this.image.text_field.top) +10/this.zoomFactor}, { x: Number(this.image.text_field.width) + Number(this.image.text_field.left) - dimension, y: Number(this.image.text_field.top)} ] let default_index = 1 let index = 0; let position_found = false; while (!position_found && index < positions.length){ let left = positions[index].x let top = positions[index].y if(!this.doesPositionConflict(left, top, dimension, this.words) && !this.doesPositionConflict(left, top, dimension, this.foreign_texts)){ position_found = true; } else { index++ } } let left = (index < positions.length) ? positions[index].x : positions[default_index].x; let top = (index < positions.length) ? positions[index].y : positions[default_index].y; return { x: `${left}px`, y: `${top}px` } } else { return { x: '0px', y: '0px' } } } /** * Return whether position specified by left, top and dimension does conflict with one of the positional objects' position. * * @param left left of position * @param top top of position * @param dimension dimension of position * @param positionalObjects Array of positions **/ private doesPositionConflict(left: number, top: number, dimension: number, positionalObjects: PositionalObject[]): boolean { let conflicts = positionalObjects.filter(positionalObject => !(Number(positionalObject.left) + Number(positionalObject.width) < left || Number(positionalObject.left) > left + dimension || Number(positionalObject.top) > top + dimension || Number(positionalObject.top) + Number(positionalObject.height) < top) ) return conflicts.length > 0 } /** * Get the hover status of the word as one of the {@link /miscellaneous/enumerations.html#HIGHTLIGHT_CASES|HIGHTLIGHT_CASES}. **/ private getHoverStatus(word: Word, skipFindText: boolean = false): string { if (this.selectedWords.indexOf(word.id) > -1 || this.selectedLines.indexOf(word.line) > -1){ return HIGHTLIGHT_CASES.SELECTED_WORD; } if (!skipFindText && this.findText != null && this.findText != ''){ let findRegex = '^[^\\w]*(' + this.findText.split(' ').join('|') + ')' return (word.text.match(findRegex) || (word.edited_text != null && word.edited_text.match(findRegex)) ) ? HIGHTLIGHT_CASES.SEARCHED_WORD : this.getHoverStatus(word, true); } if (typeof this.hoveredLine !== 'undefined' && this.hoveredLine !== null) { return (this.hoveredLine.id == word.line || (this.hoveredLine.continuesTo != undefined && this.hoveredLine.continuesTo != null && this.hoveredLine.continuesTo.line.id == word.line) || (this.hoveredLine.continuesFrom != undefined && this.hoveredLine.continuesFrom != null && this.hoveredLine.continuesFrom.line.id == word.line)) ? HIGHTLIGHT_CASES.LINE_HOVERED : HIGHTLIGHT_CASES.DEFAULT; } else if (typeof this.hoveredWord !== 'undefined' && this.hoveredWord !== null){ return (this.hoveredWord.id == word.id) ? HIGHTLIGHT_CASES.WORD_HOVERED : HIGHTLIGHT_CASES.DEFAULT; } return HIGHTLIGHT_CASES.DEFAULT; } /** * 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 (word.deleted) ? 'textfield deleted' : 'textfield highlight_yellow'; } case HIGHTLIGHT_CASES.WORD_HOVERED: { 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) : {}; } private msg(URL: string){ if(this.preferPrimaryUrl){ console.log(URL + ' TODO: show smaller image during loading'); } } }