diff --git a/nietzsche-beta-app/src/app/app.module.ts b/nietzsche-beta-app/src/app/app.module.ts index d4f5d26..0e78345 100644 --- a/nietzsche-beta-app/src/app/app.module.ts +++ b/nietzsche-beta-app/src/app/app.module.ts @@ -1,62 +1,64 @@ import { AppComponent } from './app.component'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule} from '@angular/forms'; import { NgModule } from '@angular/core'; import {MatCardModule} from '@angular/material/card'; import { MatToolbarModule, MatButtonModule} from '@angular/material'; import { MatExpansionModule } from '@angular/material/expansion'; import {MatIconModule} from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule} from '@angular/material/menu'; import { MatSidenavModule } from '@angular/material'; import { MatSelectModule } from '@angular/material/select'; import {MatTabsModule} from '@angular/material/tabs'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { QueryService } from './services/query.service'; import {routing} from './app.routing'; import {HomeComponent} from './home.component'; import { ManuscriptViewComponentComponent } from './manuscript-view-component/manuscript-view-component.component'; import { ContentViewTabComponentComponent } from './content-view-tab-component/content-view-tab-component.component'; import { RhizomeViewComponentComponent } from './rhizome-view-component/rhizome-view-component.component'; import { MainMenuComponentComponent } from './main-menu-component/main-menu-component.component'; import { PageViewComponentComponent } from './page-view-component/page-view-component.component'; import { NavigationListComponentComponent } from './navigation-list-component/navigation-list-component.component'; import {NavigationServiceService} from './services/navigation-service.service'; +import { TlnEditionModule} from './tln-edition/tln-edition.module'; @NgModule({ declarations: [ AppComponent, HomeComponent, ManuscriptViewComponentComponent, ContentViewTabComponentComponent, RhizomeViewComponentComponent, MainMenuComponentComponent, PageViewComponentComponent, NavigationListComponentComponent ], imports: [ routing, BrowserModule, BrowserAnimationsModule, CommonModule, HttpClientModule, MatButtonModule, MatCardModule, MatExpansionModule, MatIconModule, MatListModule, MatMenuModule, MatSelectModule, // for themes selection MatSidenavModule, MatTabsModule, MatToolbarModule, FormsModule, + TlnEditionModule, ReactiveFormsModule ], providers: [ NavigationServiceService, QueryService ], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.html b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.html new file mode 100644 index 0000000..cc1f5b7 --- /dev/null +++ b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.html @@ -0,0 +1,13 @@ + + + + + {{segment.sText}} + + + diff --git a/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.spec.ts b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.spec.ts new file mode 100644 index 0000000..3b8a7a2 --- /dev/null +++ b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NgxMatStandoffMarkupComponent } from './ngx-mat-standoff-markup.component'; + +describe('NgxMatStandoffMarkupComponent', () => { + let component: NgxMatStandoffMarkupComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NgxMatStandoffMarkupComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NgxMatStandoffMarkupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.ts b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.ts new file mode 100644 index 0000000..b2c3f3d --- /dev/null +++ b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.component.ts @@ -0,0 +1,350 @@ +import {Component, Input, OnChanges} from '@angular/core'; +import {DomSanitizer, SafeResourceUrl, SafeStyle} from '@angular/platform-browser' +import {NgxMatStandoffMarkupService} from "./ngx-mat-standoff-markup.service"; +import {Style} from "@angular/cli/lib/config/schema"; + + +@Component({ + selector: 'NgxMat-NgxMatStandoffMarkup', + templateUrl: './ngx-mat-standoff-markup.component.html' +}) +/** +* Simple and leightweight component marking up/styling a text input textToStyle with standoff markupDataMerged. +* @param standoffData: the standoff data for style markup/semantic markup. +* @param textToStyle: the text which will be styled acc. to the standoffData. +* @param startIndices: the final startindices to segment the text for markup. Does NOT simply equal all startindices of standoffData. It +* also contains all startindices of segments which are not styled/marked up. +* @param templateSegments: the resulting text segments with their individual markup information. Overlaps are supported +*/ +export class NgxMatStandoffMarkupComponent implements OnChanges { + @Input() textToStyle: string; + @Input() markupSettings: StandoffMarkupSettings; + @Input() standoffData: (SemanticPositonalAnnotation | PositionalStyleMarkup )[]; // both combined in one array + @Input() posStyleMarkup?: PositionalStyleMarkup[]; + @Input() semPosAnnotation?: SemanticPositonalAnnotation[]; + @Input() cssDef?: StyleSheetDef; + @Input() parentId?: string; + @Input() parentClass?: string; + cssUrl: SafeResourceUrl; // the url if a file path is passed acc. to the cssDef + markupDataMerged: (SemanticPositonalAnnotation | PositionalStyleMarkup )[]; + + startIndices: Array = []; // the start indices of all style segments + templateSegments: Segment[]; // the resulting style segments with their css styles + + selectionStartIndex: number; + + constructor( public sanitizer: DomSanitizer, private standoffService: NgxMatStandoffMarkupService) { + } + + ngOnChanges() { + if (this.cssDef.type === 'url') { + this.cssUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.cssDef.css); // the actual url of the css for embetting. + } + this.mergeInputData(); + this.startIndices = this.getStartIndices(this.markupDataMerged); + this.segmentText(); + } + + /** + * Merging the different data inputs to one data array if multiple inputs + */ + private mergeInputData() { + if (this.standoffData && this.standoffData.length) { + this.markupDataMerged = this.standoffData; + if ((this.posStyleMarkup && this.posStyleMarkup.length) || (this.semPosAnnotation && this.semPosAnnotation.length)) { + console.log('WARNING: posStyleMarkup or semPosAnnotation input ignored.') + } + } else { + if ((this.posStyleMarkup && this.posStyleMarkup.length) && (this.semPosAnnotation && this.semPosAnnotation.length)) { + this.markupDataMerged.concat(this.posStyleMarkup, this.semPosAnnotation); + } else { + if (this.posStyleMarkup && this.posStyleMarkup.length) { this.markupDataMerged = this.posStyleMarkup } + if (this.semPosAnnotation && this.semPosAnnotation.length) { this.markupDataMerged = this.semPosAnnotation } + } + } + } + + + /** + * Creates the final text segments and styles (this.templateSegments) which will + * be *ngFored as and styled with *ngStyle in the template. + * Iterates through all startindices defined, pushes the corresponding substring + * of our text to style together with its matching styles to this.templateSegments + * + */ + private segmentText() { + this.templateSegments = []; + let c = 1; // counter for getting the end index of the substring() via this.startIndices[c] + for (const startIndex of this.startIndices) { + // the text from startIndex to the next startIndex, i.e. the endIndex ... + const segmentText = this.textToStyle.substring(startIndex, this.startIndices[c]); + // all segmentDefs within the given range ... + const segmentDefsInRange = this.markupDataMerged.filter((def) => { + return def.startIndex <= startIndex && startIndex <= def.endIndex}); + let segment2 = new Segment(segmentText, startIndex, this.markupSettings, segmentDefsInRange); + this.templateSegments.push(segment2); + c += 1; + } + } + + /** + * Creates an array of startindices defining the final text/style segments. + * Hence every end index can be described as a startindex minus one (an end + * index equals a start index of sth. new -1), end indices are also covered implicitly. + * So every endIndex +1 is also pushed to the startIndices array if not yet existing. + * If a defined range is ending at the very last character, there is obviously not a new + * style starting and an additive startIndex is wrong. Therefore the last startIndex + * is simply popped from the array if its value exceeds textToStyle.length. + * + * @return startIndices: Array of all distinct startIndices of the final text segments. + */ + private getStartIndices(standoffData: StandoffDef[]) { + + const startIndices: Array = []; + // Push every startindex to startIndices if not yet there to get a distinct array of startindices for segmenting the text. + // Hence every every end means a start of sth new, every endIndex equals a startIndex -1. Therefore we simply add also + // every endIndex +1 to the Array of startIndices (if there is not yet a start defined for that character). + + standoffData.forEach(defEntry => { + if (startIndices.indexOf(defEntry.startIndex) === -1) { // if the startIndex of the defEntry is not yet in startIndices + startIndices.push(defEntry.startIndex); + } + if (startIndices.indexOf(defEntry.endIndex + 1) === -1) { + startIndices.push(defEntry.endIndex + 1); + } + }); + // if there is no startindex of 0, we have to add it to define the first text segment. + if (startIndices.indexOf(0) === -1) { + startIndices.push(0); } + // sort it + startIndices.sort((n1, n2) => n1 - n2); + // If the last endIndex is the very last character of the string to style, we must not generate a startIndex at endIndex+1. + // In any other case we have to start a new style. + // If the last generated startIndex is bigger than this.textToStyle.length + // we have to pop that last startIndex (from the right) from our startIndices. + // If the textToStyle.length is bigger or equals the last startIndex, that last startIndex simply means the end of the style + // one character before. In these cases a last style range with font style undefined (style:normal) has to start, + // so the last end ist also defined. + if (this.textToStyle.length < startIndices[startIndices.length - 1]) { startIndices.pop(); } + return startIndices; + } + + private onSegmentClick(segment: Segment){ + const sel = window.getSelection(); // needed for preventing onclicks when selecting text + if (sel.type != 'Range' && segment.interact) { + let seg = new InteractedSegment(this.parentId, this.parentClass, segment); + this.standoffService.interactedSegmentEmitter.emit(seg); + } + } + + // Text selection only works within a parent element (div). If you have several ngx-mat-standoff-components, e.g. + // in an ngFor loop, you have to read the window data out yourself in your component. + private setSelectedText(selectionEndstartIndex: number){ + if (window.getSelection) { + let sel = window.getSelection(); + if (!sel.isCollapsed) { + let totalRangeStartIndex = this.selectionStartIndex + sel.anchorOffset; + let totalRangeEndIndexFocus = selectionEndstartIndex + sel.focusOffset; + // all standoff definitions in that range ... + const segmentDefsInRange = this.markupDataMerged.filter((def) => { + return def.endIndex >= totalRangeStartIndex && def.startIndex <= totalRangeEndIndexFocus + }); + let selectedText = this.textToStyle.substring(totalRangeStartIndex, totalRangeEndIndexFocus); + this.standoffService.selectedTextEmitter.emit( + new TextSelection(selectedText, totalRangeStartIndex, totalRangeEndIndexFocus, this.parentId, segmentDefsInRange)); + } + } + } + + private setSelectionStartIndex(startIndex) { + this.selectionStartIndex = startIndex; + } +} + +export class StandoffDef { // extendended by both types + sType: number; // 0 === markup via css tag; 1 === apply css class + startIndex: number; // character where the style/class starts + endIndex: number; // character where the style/class ends + + + constructor( sType: number, startIndex: number, endIndex: number ) { + this.sType = sType; + this.startIndex = startIndex; + this.endIndex = endIndex; + } +} + +export class PositionalStyleMarkup extends StandoffDef { + cssStyleTag: string; // one or multiple css style tags like 'font-weight: bold;' or 'font-weight: bold; font-style: italic' + constructor( sType: number, startIndex: number, endIndex: number, cssStyleTag: string) { + super( sType, startIndex, endIndex); + this.cssStyleTag = cssStyleTag; + } +} + +export class SemanticPositonalAnnotation extends StandoffDef { + cssClass?: string; // class name or subclass path with or without dots like '.animal.dog' or 'animal dog' + iri?: string; // a desired iri or id + description?: string; + custom?: any; + constructor( sType: number, + startIndex: number, + endIndex: number, + cssClass?: string, + iri?: string, + description?: string, + custom?: any) { + super(sType, startIndex, endIndex); + this.cssClass = cssClass; + this.iri = iri; + this.description = description; + this.custom = custom; } +} + +export class Segment { + sType: number; // 0 = markup only, 1 = semAnnotation only + sText: string; + sStartIndex: number; + interact: boolean; + originalDefs?: OriginalStandoffDef; // all originalDefs no matter whatsoever + cssStyleTag?: StyleDef; + cssClass?: StyleDef; + + constructor( text, + sStartIndex: number, + settings: StandoffMarkupSettings, + standoffDef?: (SemanticPositonalAnnotation[] | PositionalStyleMarkup[])) { + this.sText = text; //the string (=== subString) of the segment + this.sStartIndex = sStartIndex; // the startIndex of the segment within the whole text + this.originalDefs = new OriginalStandoffDef(standoffDef); + this.setSegment(standoffDef, settings); + + } + + /** + * sets the sType, interact and sStyleDef + * + */ + private setSegment(defs, settings) { + if (defs && defs.length > 0) { // If there are definitions ... + // check for markup type overlaps, e.g. definitions for type 0 and type 1 + // by checking the length of a Set (= distinct) of all sTypes in defs. + if (Array.from(new Set(defs.map((item: any) => item.sType))).length > 1) { // If >1 there is an overlap + // set the sType depending on the markupPreference ... + this.sType = settings.markupPreference; + this.sType === 0? this.setSyle(defs): this.setClass(defs, settings.polysemanticWrapper); + + } else { this.sType = defs[0].sType } // if there is only one sType within defs we set that + // setting the interact according to the sType. + this.sType === 0? this.interact = settings.interactWithStyles : this.interact = settings.interactWithAnnotations; + let filteredDefs = defs.filter((def) => { return def.sType === this.sType; }); // subset of all defs which type === sType + this.sType === 0? this.setSyle(filteredDefs): this.setClass(filteredDefs, settings.polysemanticWrapper); + } else { + // If there is nothing defined for the text segment at all ... + this.setUndefinedSegmentParts(settings.interactWithUndefined) } + } + + setSyle(defs) { + defs.forEach( def => { + const posStyle = def as PositionalStyleMarkup; + // Adds every css property/value to the markup object + // splitting multiple styles at ';' + // removes leading and ending spaces, deletes ";" and assigns it as an object as css property:"value" + this.cssStyleTag = {}; + posStyle.cssStyleTag.split(';').forEach( style => { + if (style.trim().length > 0) { this.cssStyleTag[style.split(':')[0].trim()] = style.split(':')[1].trim()} + } + ); + }); + } + + setClass(defs, polysemanticWrapper?: string) { + this.cssClass = {}; + if (polysemanticWrapper && defs.length > 1) { this.cssClass[polysemanticWrapper]= true;} else { + defs.forEach( def => { + const posSem = def as SemanticPositonalAnnotation; + // adding a class + this.cssClass[posSem.cssClass.trim().replace('.', ' ' )]= true; + }); + } + } + + setUndefinedSegmentParts(interact: boolean) { + this.sType = 0; + this.cssStyleTag = []; // {'font-style': 'normal'} + this.interact = interact; + } +} + +export class InteractedSegment{ + parentId: string; + parentClass: string; + segment; + constructor(parentId: string, parentClass:string, segment: Segment) { + this.parentClass = parentClass; + this.parentId = parentId; + this.segment = segment; + } +} + +export class OriginalStandoffDef { + positionalMarkup: PositionalStyleMarkup[]; + semanticAnnotation: SemanticPositonalAnnotation[]; + + constructor(standOffDef: any[]) { + this.positionalMarkup = standOffDef.filter((def) => { return def.sType === 0; }); // subset of all defs which type === 0 + this.semanticAnnotation = standOffDef.filter((def) => { return def.sType === 1; }); // subset of all defs which type === 1 + } +} + +export interface StyleDef { + [klass: string]: any; // font-weight: bold; myCssClass = true; ... +} + +export class StyleSheetDef { + css: string; // the url to the file or the css iteself as plain text + type: string; // 'url' for a path to the css file or 'text' for plain css text; + + constructor(css: string, type: string) { + this.css = css; + this.type = type; + } +} + +export class StandoffMarkupSettings { + + interactWithStyles?: boolean; // clickable style segments && emitting the style definition of the segment to service + interactWithAnnotations?: boolean; // clickable anotation segments && emitting the definition back to service + interactWithUndefined?: boolean; // clickable segments for which nothing is defined + markupPreference?: number; // The preference on overlaps of styles and classes. Defines the winner: 0 === styles win; 1 === classes win; + polysemanticWrapper?: string; // optional class name for all polysemantics. In case of semantic overlaps/polysemantics this css class will be applied + + constructor(interactWithStyles: boolean = false, + interactWithAnnotations: boolean = true, + interactWithUndefined: boolean = false, + markupPreference: number = 1, + polysemanticWrapper?: string) { + + this.interactWithStyles = interactWithStyles; + this.interactWithAnnotations = interactWithAnnotations; + this.interactWithUndefined = interactWithUndefined; + this.markupPreference = markupPreference; + this.polysemanticWrapper = polysemanticWrapper; + } +} + +export class TextSelection { + text: string; + startIndex: number; + endIndex: number; + parentId: string; + standoffDefsInRange?: (SemanticPositonalAnnotation | PositionalStyleMarkup )[]; + + constructor(text: string, startIndex: number, endIndex: number, parentId?: string, standoffDefsInRange? ) { + this.text = text; + this.startIndex = startIndex; + this.endIndex = endIndex; + this.parentId = parentId; + this.standoffDefsInRange = standoffDefsInRange; + } +} diff --git a/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.module.ts b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.module.ts new file mode 100644 index 0000000..b0af432 --- /dev/null +++ b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { NgxMatStandoffMarkupComponent } from './ngx-mat-standoff-markup.component'; +import { MatTooltipModule } from '@angular/material'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgxMatStandoffMarkupService} from "./ngx-mat-standoff-markup.service"; + +@NgModule({ + declarations: [ + NgxMatStandoffMarkupComponent], + imports: [ + MatTooltipModule, + BrowserModule, + BrowserAnimationsModule + ], + exports: [NgxMatStandoffMarkupComponent], + providers: [NgxMatStandoffMarkupService] +}) +export class NgxMatStandoffMarkupModule { } diff --git a/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.service.spec.ts b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.service.spec.ts new file mode 100644 index 0000000..10b97c9 --- /dev/null +++ b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { NgxMatStandoffMarkupService } from './ngx-mat-standoff-markup.service'; + +describe('NgxMatStandoffMarkupService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: NgxMatStandoffMarkupService = TestBed.get(NgxMatStandoffMarkupService); + expect(service).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.service.ts b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.service.ts new file mode 100644 index 0000000..6738d41 --- /dev/null +++ b/nietzsche-beta-app/src/app/lib/ngx-mat-standoff-markup.service.ts @@ -0,0 +1,18 @@ +import { Injectable, EventEmitter } from '@angular/core'; +import { + PositionalStyleMarkup, SemanticPositonalAnnotation, Segment, + TextSelection, InteractedSegment +} from "./ngx-mat-standoff-markup.component"; + +@Injectable({ + providedIn: 'root' +}) +export class NgxMatStandoffMarkupService { + + // subscribe here to the selected things. For editing your standoff markup/semantic annotations + public interactedSegmentEmitter = new EventEmitter(); + public selectedTextEmitter = new EventEmitter(); // multiple spaces removed + + constructor() { + } +} diff --git a/nietzsche-beta-app/src/app/page-view-component/page-view-component.component.css b/nietzsche-beta-app/src/app/page-view-component/page-view-component.component.css new file mode 100644 index 0000000..53a354a --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view-component/page-view-component.component.css @@ -0,0 +1,10 @@ +#content { + width: 100%; + /*height: 700px;*/ + margin: 0; + padding: 0; + white-space: nowrap; + /*overflow-y: hidden; + overflow-x: auto;*/ + position: fixed; +} diff --git a/nietzsche-beta-app/src/app/page-view/README.md b/nietzsche-beta-app/src/app/page-view/README.md new file mode 100644 index 0000000..66c5560 --- /dev/null +++ b/nietzsche-beta-app/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/nietzsche-beta-app/src/app/page-view/configurable-component.spec.ts b/nietzsche-beta-app/src/app/page-view/configurable-component.spec.ts new file mode 100644 index 0000000..1302bbd --- /dev/null +++ b/nietzsche-beta-app/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/nietzsche-beta-app/src/app/page-view/configurable-component.ts b/nietzsche-beta-app/src/app/page-view/configurable-component.ts new file mode 100644 index 0000000..e6d35ec --- /dev/null +++ b/nietzsche-beta-app/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/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.css b/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.css new file mode 100644 index 0000000..58c4f31 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.css @@ -0,0 +1,8 @@ +#circle { + fill: none; +} +#cc { + fill: grey; + opacity: 0.2; +} + diff --git a/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.html b/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.html new file mode 100644 index 0000000..fa3b60e --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.html @@ -0,0 +1,24 @@ + + + + + diff --git a/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.spec.ts b/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.spec.ts new file mode 100644 index 0000000..274ad8d --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CopyrightComponent } from './copyright.component'; +/* +describe('CopyrightComponent', () => { + let component: CopyrightComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CopyrightComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CopyrightComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + */ diff --git a/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.ts b/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.ts new file mode 100644 index 0000000..e6e4775 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/copyright/copyright.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit, Input, ElementRef } from '@angular/core'; +import { PageViewService } from '../page-view.service'; +import { Copyright, Point } from '../models'; + + +@Component({ + selector: 'copyright', + templateUrl: './copyright.component.html', + styleUrls: ['./copyright.component.css'] +}) +export class CopyrightComponent implements OnInit { + @Input() dimension: number; + highlight: boolean = false; + @Input() copyright: Copyright; + + constructor(private pageViewService: PageViewService) { } + + ngOnInit() { + } + private setHighlight(highlight: boolean){ + this.highlight = highlight; + } + private showCopyrightInformation(e: MouseEvent){ + let point: Point = { visible: true, clientX: e.clientX, clientY: e.clientY, layerX: e.layerX, layerY: e.layerY } + this.pageViewService.copyrightService(this.copyright, point); + } +} diff --git a/nietzsche-beta-app/src/app/page-view/highlight_status.ts b/nietzsche-beta-app/src/app/page-view/highlight_status.ts new file mode 100644 index 0000000..896701c --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/highlight_status.ts @@ -0,0 +1,9 @@ +export enum HIGHTLIGHT_CASES { + DEFAULT = 'default', + LINE_HOVERED = 'hovered line', + SEARCHED_WORD = 'text of word == findText', + SELECTED_LINE = 'selected line', + SELECTED_WORD = 'selected word', + WORD_HOVERED = 'hovered word' +} + diff --git a/nietzsche-beta-app/src/app/page-view/interacted.directive.ts b/nietzsche-beta-app/src/app/page-view/interacted.directive.ts new file mode 100644 index 0000000..ce242bb --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/interacted.directive.ts @@ -0,0 +1,145 @@ +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; + /** + * The time (in milliseconds) the timer should wait before + * the element is scrolled in view. + **/ + delay: number = 500; + /** + * The ID of the timeout set by {@link /directives/InteractedDirective.html#timeoutScroll|timeoutScroll}. + **/ + timeoutID: number = -1; + /** + * Whether or not the element should scroll into view when the timer expires. + **/ + doScroll: boolean = false; + + constructor(private pageViewService: PageViewService, private el: ElementRef) {} + + /** + * Subscribe to on/offHovered and onClicked 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.onClickedWord.subscribe( + (clickedWord: Word) => { this.scrollIntoViewIfNeeded(clickedWord, 'Word', 0) + }); + this.pageViewService.onHoveredWord.subscribe( + (hoveredWord: Word) => { this.scrollIntoViewIfNeeded(hoveredWord, 'Word') + }); + this.pageViewService.offHoveredWord.subscribe( + (hoveredWord: Word) => { + this.clearTimeout() + }); + this.pageViewService.offHoveredLine.subscribe( + (hoveredLine: Line) => { this.clearTimeout() + }); + this.pageViewService.onHoveredLine.subscribe( + (hoveredLine: Line) => { this.scrollIntoViewIfNeeded(hoveredLine, 'Line') + }); + this.pageViewService.onClickedLine.subscribe( + (clickedLine: Line) => { this.scrollIntoViewIfNeeded(clickedLine, 'Line', 0) + }); + } + /** + * Clear timeout and prevent element from scrolling into view. + **/ + private clearTimeout(){ + if(this.timeoutID != -1){ + this.doScroll = false; + clearTimeout(this.timeoutID); + this.timeoutID = -1; + } + } + /** + * 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, delay: number= this.delay){ + 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.timeoutScroll(delay); + } + } 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.timeoutScroll(delay) + } + } + } + /** + * Scroll element in view if timeout has not been canceled during its countdown. + **/ + private timeoutScroll(delay: number) { + let behavior = (delay == 0) ? "instant" : "smooth"; + this.doScroll = true; + this.timeoutID = window.setTimeout(()=>{ + if (this.doScroll){ + this.el.nativeElement.scrollIntoView({ 'behavior': behavior}); + } + }, delay); + } + /** + * 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/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.css b/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.css new file mode 100644 index 0000000..8e724bf --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.css @@ -0,0 +1,8 @@ +.unhighlighted { + fill: white; + opacity: 0.0; +} +.highlighted { + fill: yellow; + opacity: 0.3; +} diff --git a/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.html b/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.html new file mode 100644 index 0000000..e2c6c91 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.html @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.spec.ts b/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.spec.ts new file mode 100644 index 0000000..602c8f0 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LineReferenceComponent } from './line-reference.component'; + +describe('LineReferenceComponent', () => { + let component: LineReferenceComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LineReferenceComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LineReferenceComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.ts b/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.ts new file mode 100644 index 0000000..fb01dca --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/margin-field/line-reference/line-reference.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Line, Continuation, Point, Reference } from '../../models'; +import { PageViewService } from '../../page-view.service'; + +@Component({ + selector: 'line-reference', + templateUrl: './line-reference.component.html', + styleUrls: ['./line-reference.component.css'] +}) +export class LineReferenceComponent implements OnInit { + @Input() line: Line; + @Input('showReference') referenceName: string; + highlighted: boolean = false; + continuation: Continuation; + reference: Reference; + + constructor(private pageViewService: PageViewService) { } + + ngOnInit() { + if (this.line != null && this.referenceName != '' && ((this.referenceName == 'to' && this.line.continuesTo != null) + || (this.referenceName == 'from' && this.line.continuesFrom != null))){ + this.reference = (this.referenceName == 'to') ? this.line.continuesTo : this.line.continuesFrom; + this.continuation = { source: this.line.source, reference: this.reference, datatype: 'Continuation', show: this.referenceName } + } + } + + private showReference(show: boolean, e: MouseEvent){ + this.highlighted = show; + let point: Point = { visible: show, clientX: e.clientX-10, clientY: e.clientY-80, layerX: e.layerX, layerY: e.layerY } + if (show){ + this.pageViewService.onHoverService(this.continuation, point); + this.pageViewService.onHoverService(this.line) + } else { + this.pageViewService.offHoverService(this.continuation); + this.pageViewService.offHoverService(this.line) + } + } + private changeLocation(){ + this.pageViewService.referenceService(this.reference); + } +} diff --git a/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.css b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.css new file mode 100644 index 0000000..8429a40 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.css @@ -0,0 +1,38 @@ +.marginfield { + background-color: white; +} + +.marginfield .text { + font-size: 8px; +} + +.marginfield .unhighlighted { + fill: white; + opacity: 0.0; +} +.marginfield .highlight { + fill: yellow; + opacity: 0.3; +} +.marginfield .border { + stroke: #e2fa00; + stroke-width:5; + opacity: 0.3; +} +.marginfield .howered_line { + fill: #fa301c; + opacity: 0.3; +} + +.marginfield .same_word { + fill: #c9fac5; + opacity: 0.3; +} + +.text_fadeout { + fill: #a4a4a4; +} + +.hover { + transform: scale(1.5); +} diff --git a/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.html b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.html new file mode 100644 index 0000000..87c3d86 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.html @@ -0,0 +1,27 @@ + + + + + + + + {{line.number}} + + + + + + diff --git a/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.spec.ts b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.spec.ts new file mode 100644 index 0000000..1fc9818 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; + +import { MarginFieldComponent } from './margin-field.component'; + +describe('MarginFieldComponent', () => { + let component: MarginFieldComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ FormsModule ], + declarations: [ MarginFieldComponent ] + }) + .compileComponents(); + })); + + /*beforeEach(() => { + fixture = TestBed.createComponent(MarginFieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + });*/ +}); 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 new file mode 100644 index 0000000..472dd20 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/margin-field/margin-field.component.ts @@ -0,0 +1,194 @@ +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() { + 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) { + 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"); + 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; + } + /** + * 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 ( (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/models.ts b/nietzsche-beta-app/src/app/page-view/models.ts new file mode 100644 index 0000000..c40ed4d --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/models.ts @@ -0,0 +1,282 @@ +/** + * 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; +} +/** + * a text continuation + **/ +export interface Continuation extends Interactable { + reference: Reference; + source: Reference; + show?: string; +} +/** + * Copyright information + **/ +export interface Copyright { + text: string; + licenseTextUrl?: string; + license: string; + originalUrl?: string; +} +/** + * 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; + /** + * copyright information + **/ + copyright?: Copyright; +} +/** + * This interface specifies a line that will be displayed by {@link /components/MarginFieldComponent.html|MarginFieldComponent}. + **/ +export interface LineStub extends Interactable { + /** the line number + **/ + number: number; + /** the (optional) IRI of this line + **/ + id: Identifier; +} +/** + * This interface specifies a line that will be displayed by {@link /components/MarginFieldComponent.html|MarginFieldComponent}. + **/ +export interface Line extends LineStub { + /** geometrical bottom position of this line + **/ + bottom: number; + /** geometrical top position of this line + **/ + top: number; + /** + * reference to the line from which this line continues + **/ + continuesFrom?: Reference; + /** + * reference to the line on which this line continues + **/ + continuesTo?: Reference; + source?: Reference; +} +/** + * 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; +} +/** + * This type specifies an identifier for words/lines (by its IRI string or its id number) + **/ +export type Identifier = string | number; +/** + * This interface specifies a page. + **/ +export interface Page { + id: Identifier; + number: string; +} +/** + * 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; +} +/** + * simple x,y-position + **/ +export interface Position { + x: number; + y: 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; + /** the matrix transformation string of the geometrical position of this word's rect. + **/ + transform?: string; +} +/** + * This interface specifies a Line Reference that can be routed to. + **/ +export interface LineReference { + /** + * the title of the reference + **/ + manuscript?: Manuscript; + /** + * the page number of the reference + **/ + page?: string; + /** + * the line number of the reference + **/ + line_number?: number; + /** + * the line identifiaction of the reference + **/ + id: Identifier; +} +export interface Reference { + /** + * reference to the manuscript + **/ + manuscript?: Manuscript; + /** + * reference to the page + **/ + page?: Page; + /** + * reference to the line + **/ + line?: LineStub; + /** + * reference to the word + **/ + //word?: WordStub; +} +/** + * This interface specifies a manuscript + **/ +export interface Manuscript { + id: Identifier; + title: string; + type?: 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; +} +/** + * 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 const USE_EXTERNAL_TOOLTIP: string = 'UseExternalTooltip'; diff --git a/nietzsche-beta-app/src/app/page-view/page-view.component.css b/nietzsche-beta-app/src/app/page-view/page-view.component.css new file mode 100644 index 0000000..2151653 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/page-view.component.css @@ -0,0 +1,31 @@ +#page { + width: 100%; + margin: 0; + padding: 0; + white-space: nowrap; +} +.inline { + display: inline-block; +} +.breakline { + display: block; + height: 50%; + overflow: scroll; +} +.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/nietzsche-beta-app/src/app/page-view/page-view.component.html b/nietzsche-beta-app/src/app/page-view/page-view.component.html new file mode 100644 index 0000000..1bdcd5c --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/page-view.component.html @@ -0,0 +1,50 @@ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+ +
+
+
diff --git a/nietzsche-beta-app/src/app/page-view/page-view.component.spec.ts b/nietzsche-beta-app/src/app/page-view/page-view.component.spec.ts new file mode 100644 index 0000000..0a00c5c --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/page-view.component.spec.ts @@ -0,0 +1,32 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormsModule } from '@angular/forms'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { PageViewComponent } from './page-view.component'; +import { PageViewModule } from './page-view.module'; + +describe('PageViewComponent', () => { + let component: PageViewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports:[ + FormsModule, + HttpClientTestingModule, + PageViewModule + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PageViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/page-view/page-view.component.ts b/nietzsche-beta-app/src/app/page-view/page-view.component.ts new file mode 100644 index 0000000..6c82b72 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/page-view.component.ts @@ -0,0 +1,170 @@ +import { Component, Input, OnInit, OnChanges} from '@angular/core'; +import { externalAssignClass, externalAssignStyle, Configuration, Identifier, Image, Line, TextField, 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, OnChanges { + @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; + /** + * the (initial) maximum width of the image(s). + **/ + @Input() max_width: 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; + /** + * identifiers of selected words that should be highlighted. + **/ + @Input() selectedWords: Identifier[] = []; + /** + * identifiers of selected lines that should be highlighted. + **/ + @Input() selectedLines: Identifier[] = []; + @Input('startLine') startLineId: Identifier; + @Input('endLine') endLineId: Identifier; + @Input() dontShowReference: boolean; + showReferenceLeft: string = 'from'; + showReferenceRight: string = 'to'; + + constructor() {} + + /** + * sets {@link /components/PageViewComponent.html#max_height|max_height} if it is unset. + **/ + ngOnInit() { + if (this.max_height == -1 && this.max_width == -1){ + this.max_height = screen.availHeight; + } + if (this.dontShowReference != undefined && this.dontShowReference != null && this.dontShowReference){ + this.showReferenceLeft = ''; + this.showReferenceRight = ''; + } + this.checkImages(); + } + ngOnChanges(){ + this.checkImages(); + if (this.first_image != null && this.first_image != undefined && this.first_image.transform != null){ + this.updateLines(this.first_words, this.first_lines) + } + if (this.second_image != null && this.second_image != undefined && this.second_image.transform != null){ + this.updateLines(this.second_words, this.second_lines) + } + } + private checkImages(){ + if (this.first_image != null && this.first_image != undefined && this.startLineId != null && this.startLineId != undefined){ + if(this.first_lines != null && this.first_lines != undefined && this.first_lines.length > 0){ + this.first_image = this.updateTextField(this.first_image, this.first_lines); + } + if(this.second_lines != null && this.second_lines != undefined && this.second_lines.length > 0){ + this.second_image = this.updateTextField(this.second_image, this.second_lines); + } + } + } + private updateLines(words: Word[], lines: Line[]) { + for (var i = 0; i < lines.length; i++){ + if (words.filter(word =>word.line == lines[i].id).length > 0){ + lines[i].top = words.filter(word =>word.line == lines[i].id).map(word =>Number(word.top)).sort(function(a,b){ return a-b; })[0] + lines[i].bottom = words.filter(word =>word.line == lines[i].id).map(word =>Number(word.top)+Number(word.height)).sort(function(a,b){ return b-a; })[0] + } + } + } + private updateTextField(image: Image, lines: Line[]): Image { + let endLineId = (this.endLineId != null && this.endLineId != undefined) ? this.endLineId : this.startLineId; + let startLines = lines.filter(line =>line.id == this.startLineId) + let endLines = lines.filter(line =>line.id == endLineId) + if (startLines.length > 0 && endLines.length > 0){ + let top = (startLines[0].top > 10) ? startLines[0].top-10 : startLines[0].top; + let height = (endLines[0].bottom-top)+10; + let text_field: TextField = { top: top, left: image.text_field.left, width: image.text_field.width, height: height } + return { x: image.x, y: image.y, width: image.width, height: image.height, filename: image.filename, + URL: image.URL, secondaryURL: image.secondaryURL, text_field: text_field, transform: image.transform, + copyright: image.copyright } + } + return image; + } + /** + * 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/nietzsche-beta-app/src/app/page-view/page-view.module.ts b/nietzsche-beta-app/src/app/page-view/page-view.module.ts new file mode 100644 index 0000000..5cc1d32 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/page-view.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { MarginFieldComponent } from './margin-field/margin-field.component'; +import { TextFieldComponent} from './textfield-component/textfield.component'; +import { InteractedDirective } from './interacted.directive'; +import { PageViewComponent } from './page-view.component'; +import { PageViewService } from './page-view.service'; +import { CopyrightComponent } from './copyright/copyright.component'; +import { LineReferenceComponent } from './margin-field/line-reference/line-reference.component'; + +@NgModule({ + declarations: [ + InteractedDirective, + MarginFieldComponent, + TextFieldComponent, + PageViewComponent, + CopyrightComponent, + LineReferenceComponent + ], + imports: [ + CommonModule + ], + providers: [ + PageViewService + ], + exports: [ + MarginFieldComponent, + TextFieldComponent, + PageViewComponent, + CopyrightComponent + ] +}) +export class PageViewModule { } diff --git a/nietzsche-beta-app/src/app/page-view/page-view.service.ts b/nietzsche-beta-app/src/app/page-view/page-view.service.ts new file mode 100644 index 0000000..9908424 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/page-view.service.ts @@ -0,0 +1,102 @@ +import {EventEmitter, Injectable} from '@angular/core'; +import { Configuration, Continuation, Copyright, Interactable, Line, Point, Reference, 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(); + /** + * clicked line emitter + **/ + onClickedLine = new EventEmitter(); + /** + * hovered word emitter + **/ + onHoveredWord = new EventEmitter(); + /** + * off hovered word emitter + **/ + offHoveredWord = new EventEmitter(); + /** + * 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(); + /** + * hovered line continuation + **/ + onHoveredContinuation = new EventEmitter(); + /** + * off hovered line continuation + **/ + offHoveredContinuation = new EventEmitter(); + /** + * point where mouse hovered/clicked + **/ + mousePosition = new EventEmitter(); + /** + * Reference change emitter, subscribe in order to navigate to reference. + **/ + reference = new EventEmitter(); + /** + * copyright change emitter + * */ + copyright = new EventEmitter(); + + public referenceService(reference: Reference){ + this.reference.emit(reference); + } + public copyrightService(copyright: Copyright, point?: Point){ + this.copyright.emit(copyright); + if (point != null && point != undefined){ + this.mousePosition.emit(point); + } + } + /** + * emit an event on 'onClicked' + interactable.datatype + **/ + public onClickService(interactable: Interactable, point?: Point){ + this['onClicked' + interactable.datatype].emit(interactable); + if (interactable.datatype != 'Line' && point != null && point != undefined){ + this.mousePosition.emit(point); + } + } + /** + * emit an event on 'onHover' + interactable.datatype + **/ + public onHoverService(interactable: Interactable, point?: Point){ + this['onHovered' + interactable.datatype].emit(interactable); + if (interactable.datatype != 'Line' && point != null && point != undefined){ + this.mousePosition.emit(point); + } + } + /** + * 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 }); + } +} diff --git a/nietzsche-beta-app/src/app/page-view/textfield-component/matrix.spec.ts b/nietzsche-beta-app/src/app/page-view/textfield-component/matrix.spec.ts new file mode 100644 index 0000000..9f19788 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/textfield-component/matrix.spec.ts @@ -0,0 +1,15 @@ +import { Component, Input, DebugElement, NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { Matrix } from './matrix'; + + +describe('Matrix', () => { + it('should create the Object', ()=>{ + expect(new Matrix('rotation(90)')).toBeTruthy(); + }); + it('should create the Object', ()=>{ + let matrix = new Matrix('matrix(1 0 0 1 0 0)', 1.5) + //console.log(matrix.toString()); + expect(matrix.toString()).toEqual('matrix(1 0 0 1 -210 -210)'); + }); +}); diff --git a/nietzsche-beta-app/src/app/page-view/textfield-component/matrix.ts b/nietzsche-beta-app/src/app/page-view/textfield-component/matrix.ts new file mode 100644 index 0000000..73a3518 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/textfield-component/matrix.ts @@ -0,0 +1,31 @@ +export class Matrix { + private readonly offset: number = -140; + private readonly A: number = 0 + private readonly B: number = 1 + private readonly C: number = 2 + private readonly D: number = 3 + private readonly E: number = 4 + private readonly F: number = 5 + matrix: number[] = [ 1, 0, 0, 1, 0, 0 ] + + constructor(matrix_string: string, zoomFactor?: number){ + if (matrix_string.startsWith('matrix')){ + this.matrix = matrix_string.replace('matrix(','').replace(')','').split(' ').map(n =>Number(n)) + } else if (matrix_string.startsWith('rotation')){ + let angle = Number(matrix_string.replace('rotation(','').replace(')','')) + let radians = Math.PI/180*angle + this.matrix[this.A] = Math.round(Math.cos(radians)) + this.matrix[this.B] = Math.sin(radians) + this.matrix[this.C] = Math.sin(radians)*-1 + this.matrix[this.D] = Math.round(Math.cos(radians)) + } + if (zoomFactor != null){ + this.matrix[this.E] = zoomFactor*this.offset + this.matrix[this.F] = zoomFactor*this.offset + } + } + + public toString(): string { + return 'matrix(' + this.matrix.map(n =>String(n)).join(' ') + ')' + } +} diff --git a/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.css b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.css new file mode 100644 index 0000000..824eeda --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.css @@ -0,0 +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 .highlight_foreign_text { + fill: blue; + opacity: 0.5; +} +.textfield .unhighlighted { + opacity: 0.0; +} +.textfield .highlight_path { + stroke: red; + fill: none; + stroke-width: 0.2; +} +.textfield .unhighlighted_path { + stroke: none; + fill: none; + opacity: 0.0; +} +.textfield .border { + stroke: #e2fa00; + stroke-width:5; + opacity: 0.3; +} diff --git a/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.html b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.html new file mode 100644 index 0000000..11c74cf --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.html @@ -0,0 +1,38 @@ + + + + + + + + + + + {{ word.edited_text != null ? '> ' + word.edited_text : word.text }} + + + + + + + + {{foreignText.text}}, {{ foreignText.pen }} + + + diff --git a/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.spec.ts b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.spec.ts new file mode 100644 index 0000000..5732eb9 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.spec.ts @@ -0,0 +1,30 @@ +import { TestBed, async } from '@angular/core/testing'; + +/*describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'svg-test-app'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('svg-test-app'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('.content span').textContent).toContain('svg-test-app app is running!'); + }); +});*/ 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 new file mode 100644 index 0000000..4524a70 --- /dev/null +++ b/nietzsche-beta-app/src/app/page-view/textfield-component/textfield.component.ts @@ -0,0 +1,344 @@ +import { Component, ElementRef, Input, 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; + 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 != ''){ + return (word.text.match(this.findText) + || (word.edited_text != null && word.edited_text.match(this.findText)) + ) ? 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'); + } + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/README.md b/nietzsche-beta-app/src/app/tln-edition/README.md new file mode 100644 index 0000000..8473dbc --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/README.md @@ -0,0 +1,143 @@ +# TlnEditionModule + +## How to use TlnPageViewComponent + +Given a page IRI, this component will request all relevant information and display the data with `PageViewComponent`. + +On more information about this module see the documentation. + +### Import Module + +In your Angular module file, e.g. `app.module.ts`: + +``` +import { TlnEditionModule} from './tln-edition/tln-edition.module'; + +@NgModule({ + declarations: [ AppComponent ], + imports: [ TlnEditionModule ], + . + . + . +``` + +### 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. + +- `page`: the IRI of the page that should be displayed + +- `preferPrimaryUrl`: OPTIONAL should primary Url be used for image. Use secondary Url if false. Default: true. + +- `queryService`: OPTIONAL pass a queryService that implements `TlnQueryServiceInterface` (see `tln-edition/models.ts`) + +- `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). + +- `selectedViewOption`: OPTIONAL selected view option (Transkription, Faksimile or Synopse), use `VIEW_OPTIONS` from `tln-edition/constants.ts`. + +- `zoomFactor`: OPTIONAL global zoom factor + +### For your Data + +Use the interfaces from `tln-edition/models.ts` for your data: + +``` +import { externalAssignClass, externalAssignStyle, Identifier, Image, Line, PositionalObject, TextByForeignHand, Word } from './tln-edition/models'; +``` + +### For Data retrieval + +Use the service `TlnQueryService` for your data revtrieval. + +Import: + +``` +import { TlnQueryService } from './tln-edition/services'; +``` + +Inject service: + +``` +constructor(private queryService: TlnQueryService) { } +``` + +Query and subscribe to results: + +``` +ngOnInit() { + this.queryService.getData(query).subscribe(results => { this.doSomething(results) }); +} +``` + +Alternatively, pass your own query service that implements `TlnQueryServiceInterface` via input `[queryService]` to ``. + +### For mouse event interaction + +Use the `PageViewService` in order to react on mouse events. + +Import: + +``` +import { PageViewService, TlnQueryService } from './tln-edition/services'; +``` + +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/nietzsche-beta-app/src/app/tln-edition/constants.ts b/nietzsche-beta-app/src/app/tln-edition/constants.ts new file mode 100644 index 0000000..7e431be --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/constants.ts @@ -0,0 +1,50 @@ +export {HIGHTLIGHT_CASES} from '../page-view/highlight_status'; + +export enum VIEW_OPTIONS { + TRANSKRIPTION = 'Transkription', + FAKSIMILE = 'Faksimile', + SYNOPSIS = 'Transkription/Faksimile', + SYNOPSIS_B = 'Faksimile/Transkription' +} + +export const ONTOLOTY_PREFIX: string = 'http://www.nie.org/ontology/nietzsche#' +/** + * Route for TlnCrossrefComponent + **/ +export const TLN_CROSSREF_ROUTE: string = 'tln-crossref'; +/** + * Route for TlnViewerComponent + **/ +export const TLN_VIEWER_ROUTE: string = 'tln-viewer'; +/** + * Param for find text in page. + **/ +export const TLN_FIND_PARAM: string = 'find'; +/** + * Param for page iri. + **/ +export const TLN_PAGE_PARAM: string = 'page'; +/** + * Param for manuscript iri. + **/ +export const TLN_MANUSCRIPT_PARAM: string = 'manuscript'; +/** + * Param for selected lines. + **/ +export const TLN_SELECTED_LINES_PARAM: string = 'selected-lines'; +/** + * Param for selected lines. + **/ +export const TLN_SELECTED_WORDS_PARAM: string = 'selected-words'; +/** + * Param for iri of a genetic order of text versions. + **/ +export const TLN_TEXT_GENETIC_ORDER_PARAM: string = 'genetic-order'; +/** + * Param for selected view option, e.g. 'Transkription', 'Faksimile', etc. + **/ +export const TLN_VIEW_OPTION_PARAM: string = 'view-option'; +/** + * Param for zoom. + **/ +export const TLN_ZOOM_PARAM: string = 'zoom'; diff --git a/nietzsche-beta-app/src/app/tln-edition/data_handler.ts b/nietzsche-beta-app/src/app/tln-edition/data_handler.ts new file mode 100644 index 0000000..d0adca3 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/data_handler.ts @@ -0,0 +1,157 @@ +import { OnInit, EventEmitter} from '@angular/core'; +import { first, takeUntil } from 'rxjs/operators'; +import { BasicResultBindingElement, AskResult} from './datatypes/basic_datatype'; +import { TlnQueryServiceInterface } from './models'; + +export interface KeyIriMapping { + key: string; + iri: string; +} +export interface ComplexKeyIriMapping { + idIndex: number; + mapping: KeyIriMapping[]; +} + +/** + * This interface can be used in order to handle data + * of type {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export interface Handler { + /** + * a class that instantiates data of type {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement} + **/ + handler: typeof BasicResultBindingElement; + /** + * data handler's next key for retrieving and instantiating data. + **/ + next_key?: string; + /** + * a service that informs its listeners about its handler's data. + **/ + service?: any; +} +/** + * This class retrieves data from a query service and instantiates it using + * corresponding handlers. + **/ +export class DataHandler { + /** + * the query services with which data is retrieved + **/ + queryService: TlnQueryServiceInterface; + debug: boolean = false; + /** + * whether or not DataHandler is ready to retrieve data + **/ + ready: boolean = false; + stop_processing = new EventEmitter(); + start_processing = new EventEmitter(); + processing_finished = new EventEmitter(); + /** + * @param component the component that uses this data handler + **/ + constructor(private component: OnInit){} + /** + * add a {@link /interfaces/Handler.html|Handler} + * or an Array of handler keys to DataHandler. + **/ + public addHandler(key: string, handler: Handler | string[]) { + this[key] = handler; + } + /** + * Retrieve and instantiate data + * @param key data handler key + * @param iri iri that should be passed to query + * @param next_iri use next_iri instead of the iri of the first item in the current data array. + **/ + public getData(key: string, iri?: string, next_iri?: string) { + if (Array.isArray(this[key])){ + this[key].forEach(value =>this.getData(value, iri)); + if (next_iri != null && this[key]['next_key'] != null){ + this.getData(this[key]['next_key'], next_iri); + } + } else { + this.start_processing.emit(true); + let handler = this[key]['handler']; + if (this.debug && key == 'geneticOrders' ) { + //console.log(this[key]['handler'], key, iri); + console.log(handler.getQuery(iri, handler.query_key)) + } + let is_target_array = Array.isArray(this.component[key]); + if (!is_target_array){ + this.queryService.getData(handler.getQuery(iri, handler.query_key)).pipe(takeUntil(this.stop_processing) || first()).subscribe(results => { + this.component[key] = handler.convertData(results, iri, this[key]['service'])[0]; + if (next_iri != null && this[key]['next_key'] != null){ + this.getData(this[key]['next_key'], next_iri); + } + }); + } else { + this.queryService.getData(handler.getQuery(iri, handler.query_key)).pipe(takeUntil(this.stop_processing)).subscribe(results => { + this.component[key] = (handler.use_id) ? handler.convertData(results, iri, this[key]['service']) : handler.convertData(results, this[key]['service']); + if (this.component[key].length > 0 && this[key]['next_key'] != null){ + let use_next_iri = (next_iri != null) ? next_iri : this.component[key][0].id; + this.getData(this[key]['next_key'], use_next_iri); + } + }); + } + this.processing_finished.emit(true); + } + } + public getData4Keys(key: string, datatypeKeyIriMapping: ComplexKeyIriMapping) { + if (Array.isArray(this[key])){ + this[key].forEach(value =>this.getData4Keys(value, datatypeKeyIriMapping)); + } else { + let handler = this[key]['handler']; + if (this.debug) { + console.log(this[key]['handler'], datatypeKeyIriMapping); + } + let is_target_array = Array.isArray(this.component[key]); + let iri = datatypeKeyIriMapping.mapping[datatypeKeyIriMapping.idIndex]; + if (!is_target_array){ + this.queryService.getData(handler.getComplexQuery(datatypeKeyIriMapping.mapping)).pipe(takeUntil(this.stop_processing) || first()).subscribe(results => { + this.component[key] = handler.convertData(results, iri, this[key]['service'])[0]; + }); + } else { + this.queryService.getData(handler.getQuery(datatypeKeyIriMapping.mapping)).pipe(takeUntil(this.stop_processing)).subscribe(results => { + this.component[key] = (handler.use_id) ? handler.convertData(results, iri) : handler.convertData(results); + if (this.component[key].length > 0 && this[key]['next_key'] != null){ + datatypeKeyIriMapping.mapping[datatypeKeyIriMapping.idIndex] = this.component[key][0].id; + this.getData4Keys(this[key]['next_key'], datatypeKeyIriMapping); + } + }); + } + } + } + public getDataWithNewHandlerIf(key: string, subjectIri: string, typeIri: string, handlerTrue: Handler, handlerFalse: Handler, iri?: string, next_iri?: string) { + let complexMapping: KeyIriMapping[] = [ { key: 'id', iri: subjectIri }, { key: 'type', iri: typeIri } ] + this.queryService.getData(AskResult.getComplexQuery(complexMapping)).pipe(first()).subscribe(result => { + this[key] = (AskResult.getAnswer(result)) ? handlerTrue : handlerFalse; + console.log(key, subjectIri, typeIri, result, this[key]); + this.getData(key, iri, next_iri); + }); + } + public conditionalAddHandler(askQuery: string, key: string, handlerTrue: Handler, handlerFalse: Handler) { + this.queryService.getData(askQuery).pipe(first()).subscribe(result => { + this[key] = (AskResult.getAnswer(result)) ? handlerTrue : handlerFalse; + }); + } + + /** + * reset all data belonging to key + **/ + public resetData(key){ + this.queryService.resetData(key) + if (Array.isArray(this[key])){ + this[key].forEach(value =>this.resetData(value)); + } else { + this.component[key] = (Array.isArray(this.component[key])) ? [] : null; + } + } + /** + * set a query service to DataHandler and switch status ready to true. + **/ + public setQueryService(queryService: TlnQueryServiceInterface){ + this.queryService = queryService; + this.ready = true; + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/basic_datatype.spec.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/basic_datatype.spec.ts new file mode 100644 index 0000000..811b698 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/basic_datatype.spec.ts @@ -0,0 +1,38 @@ +import { Component, Input, DebugElement, NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { BasicResultBindingElement, FusekiResults} from './basic_datatype'; + +const data = {'iri': 'asdf'}; +const validData: FusekiResults = { 'head': { 'vars': [ 'id' ] }, 'results': { 'bindings': null }} + +class A { + class: string = 'A'; + static do(this: T): InstanceType { + return new this() as InstanceType; + } + } + +class B extends A { + class: string = 'B'; + } + +describe('BasicResultBindingElement', () => { + it('should create the Object', ()=>{ + expect(new BasicResultBindingElement(data)).toBeTruthy(); + }); + it('should return its query if there is not input', ()=>{ + expect(BasicResultBindingElement.getQuery()).toEqual(`SELECT ?id ?p ?o WHERE { ?id ?p ?o. }`); + }); + it('should return a subject changed query', ()=>{ + expect(BasicResultBindingElement.getQuery('http://example.com#test')).toEqual(`SELECT ?id ?p ?o WHERE { ?p ?o. }`); + }); + it('should return a object changed query', ()=>{ + expect(BasicResultBindingElement.getQuery('http://example.com#test', 'o')).toEqual(`SELECT ?id ?p ?o WHERE { ?id ?p . }`); + }); + it('should return a predicate changed query', ()=>{ + expect(BasicResultBindingElement.getQuery('http://example.com#test', 'p')).toEqual(`SELECT ?id ?p ?o WHERE { ?id ?o. }`); + }); + it('should return subclass', ()=>{ + expect(B.do().class == "B").toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/basic_datatype.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/basic_datatype.ts new file mode 100644 index 0000000..c3a6b88 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/basic_datatype.ts @@ -0,0 +1,217 @@ +import { Parser, Generator } from 'sparqljs'; +import { KeyIriMapping } from '../data_handler'; +/** + * this interface specifies the head of {@link /interfaces/FusekiResults.html|FusekiResults}. + **/ +interface FusekiVars { + vars: string[]; +} +/** + * this interface specifies the bindings of {@link /interfaces/FusekiResults.html|FusekiResults}. + **/ +interface FusekiBindings { + bindings: []; +} +/** + * this interface specifies the results as they are retrieved from an Apache Jena Fuseki server. + **/ +export interface FusekiResults { + results: FusekiBindings; + head: FusekiVars +} +export interface FusekiBoolean { + head: any; + boolean: boolean; +} +/** + * This is the basic datatype that instantiates an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * + * All datatypes can be subclassed from this type in order to create SPARQL-queries, retrieve data and convert it + * to the corresponding datatypes. + **/ +export class BasicResultBindingElement { + /** + * the internal default key for replacing {@link /classes/BasicResultBindingElement.html#query|query} by "id" + * in {@link /classes/BasicResultBindingElement.html#getQuery|getQuery} if "key" is omitted. + **/ + protected static readonly default_key: string = 'id'; + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = `SELECT ?id ?p ?o WHERE { ?id ?p ?o. }`; + /** + * the public key for replacing {@link /classes/BasicResultBindingElement.html#query|query} by "id". + **/ + public static readonly query_key: string = null; + /** + * the id of this datatype. + **/ + public id: string; + /** + * the raw data of this datatype, i.e. a singular bindings element of {@link /interfaces/FusekiBindings.html|FusekiBindings}. + **/ + protected data: any; + /** + * whether or not to pass the id used for the query to the constructor and + * use it as the value of the property specified by query_key. + **/ + public static readonly use_id: boolean = false; + /** + * a service that this datatype can use in order to communicate with its data holder. + **/ + protected service: any; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor(data: any, id?: string, service?: any){ + this.data = data; + this.service = service; + if (id != undefined && id != null && id != ''){ + let key = (Object.getPrototypeOf(this).constructor.use_id + && Object.getPrototypeOf(this).constructor.query_key != null) + ? Object.getPrototypeOf(this).constructor.query_key : 'id'; + this[key] = id; + } + if (this.id == null){ + this.id = this.getData4Key('id'); + } + } + /** + * This function returns the value of the content specified by "key" from {@link /classes/BasicResultBindingElement.html#data|data}. + * + * @param key the key that specifies the content + * + * @returns {any} the value of the content if key exists else null + **/ + protected getData4Key(key: string): any { + if (!this.data.hasOwnProperty(key)) { + return null; + } + if (this.data[key].datatype == 'http://www.w3.org/2001/XMLSchema#boolean'){ + return JSON.parse(this.data[key].value); + } else if (this.data[key].datatype == 'http://www.w3.org/2001/XMLSchema#integer'){ + return Number(this.data[key].value); + } + return this.data[key].value; + } + /** + * This method returns the SPARQL query of this BasicResultBindingElement. + * The query can be modified by providing an "id" and "key" such that every "key" in + * the query will be replaced by "id". + * + * If "key" is omitted {@link /classes/BasicResultBindingElement.html#default_key|default_key} will be used. + * + * @param id will replace key in query + * @param key will be replaced by id. + **/ + public static getQuery(id?: string, key?: string): string { + if (typeof(id) === 'undefined' || id === null || id == ''){ + return this.query; + } else { + if (key == null || key == ''){ + key = this.default_key; + } + let parser = new Parser(); + let sparqlGenerator = new Generator({}); + let parsedQuery = parser.parse(this.query) + if (parsedQuery.where[0].patterns != undefined){ + for (var j = 0; j < parsedQuery.where[0].patterns.length; j++){ + for (var i = 0; i < parsedQuery.where[0].patterns[j].triples.length; i++){ + if(parsedQuery.where[0].patterns[j].triples[i]['subject']['value'] == key){ + parsedQuery.where[0].patterns[j].triples[i]['subject'] = { termType: "NamedNode", value: id }; + } else if(parsedQuery.where[0].patterns[j].triples[i]['object']['value'] == key){ + parsedQuery.where[0].patterns[j].triples[i]['object'] = { termType: "NamedNode", value: id }; + } else if(parsedQuery.where[0].patterns[j].triples[i]['predicate']['value'] == key){ + parsedQuery.where[0].patterns[j].triples[i]['predicate'] = { termType: "NamedNode", value: id }; + } + } + } + return sparqlGenerator.stringify(parsedQuery); + } else if (parsedQuery.where[0].triples == undefined){ + console.log('ERROR', parsedQuery); + return '' + } + for (var i = 0; i < parsedQuery.where[0].triples.length; i++){ + if(parsedQuery.where[0].triples[i]['subject']['value'] == key){ + parsedQuery.where[0].triples[i]['subject'] = { termType: "NamedNode", value: id }; + } else if (parsedQuery.where[0].triples[i]['object']['value'] == key){ + parsedQuery.where[0].triples[i]['object'] = { termType: "NamedNode", value: id }; + } else if (parsedQuery.where[0].triples[i]['predicate']['value'] == key){ + parsedQuery.where[0].triples[i]['predicate'] = { termType: "NamedNode", value: id }; + } + } + return sparqlGenerator.stringify(parsedQuery); + } + } + public static getComplexQuery(keyIriMapping: KeyIriMapping[]): string { + let parser = new Parser(); + let sparqlGenerator = new Generator({}); + let parsedQuery = parser.parse(this.query) + for (let mapping of keyIriMapping){ + let key = mapping.key; + let id = mapping.iri; + for (var i = 0; i < parsedQuery.where[0].triples.length; i++){ + if(parsedQuery.where[0].triples[i]['subject']['value'] == key){ + parsedQuery.where[0].triples[i]['subject'] = { termType: "NamedNode", value: id }; + } else if (parsedQuery.where[0].triples[i]['object']['value'] == key){ + parsedQuery.where[0].triples[i]['object'] = { termType: "NamedNode", value: id }; + } else if (parsedQuery.where[0].triples[i]['predicate']['value'] == key){ + parsedQuery.where[0].triples[i]['predicate'] = { termType: "NamedNode", value: id }; + } + } + } + return sparqlGenerator.stringify(parsedQuery); + } + + /** + * This function returns 'results.bindings' of {@link /interfaces/FusekiResults.html|FusekiResults}. + **/ + public static getContent(data: FusekiResults): [] { + return data['results']['bindings']; + } + /** + * This static function instantiates the subclasses of {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement} from + * the data retrieved by executing the query that is provided by {@link /classes/BasicResultBindingElement.html#getQuery|getQuery}. + * + * @param this a subclass of BasicResultBindingElement + * @param data the fuseki result json + * @param id the id that has been used in order to retrieve the data and that will identify the instantiation of the subclass. + * @param service a means to communicate with the data holder. + * + * @returns Array of subclass instantiations + **/ + public static convertData(this: T, data: FusekiResults, id?: string, service?: any): Array> { + let elements = []; + let content = this.getContent(data); + for (var i = 0; i < content.length; i++){ + let element = new this(content[i], id, service) as InstanceType; + elements.push(element); + } + return elements; + } +} +export class AskResult extends BasicResultBindingElement { + static readonly query: string = ` + PREFIX tln: + ASK { + ?id a ?type. + }`; + + public static getAnswer(answer: FusekiBoolean): boolean { + return answer.boolean; + } +} +export class IsReconstructedKonvolut extends AskResult { + static readonly query: string = ` + PREFIX tln: + ASK { + ?id a tln:ReconstructedKonvolut. + }`; + + public static readonly query_key: string = 'id'; +} + + diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/earlier_version.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/earlier_version.ts new file mode 100644 index 0000000..8a27dd7 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/earlier_version.ts @@ -0,0 +1,54 @@ +import { BasicResultBindingElement } from './basic_datatype'; +/** + * This is the 'earlier version' stub instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class TlnEarlierVersionStub extends BasicResultBindingElement { + /** + * the internal default key for replacing {@link /classes/TlnWord.html#query|query} by "id" + * in {@link /classes/TlnWord.html#getQuery|getQuery} if "key" is omitted. + **/ + static readonly default_key: string = 'word'; + /** + * the public key for replacing {@link /classes/TlnWord.html#query|query} by "id". + **/ + static readonly query_key: string = 'word'; + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?text WHERE { + ?word tln:wordHasEarlierVersion ?id. + ?id tln:hasText ?text. + }`; + /** + * text of earlier version + **/ + text: string; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor(data: any, id?: string, service?: any){ + super(data, id, service) + this.text = this.getData4Key('text'); + } +} +export class TlnOverwrittenStub extends TlnEarlierVersionStub { + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?text WHERE { + ?word (tln:wordHasWordParts/rdf:rest*/rdf:first/tln:overwritesWord|tln:overwritesWord) ?id. + ?id tln:hasText ?text. + }`; +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/faksimile_image.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/faksimile_image.ts new file mode 100644 index 0000000..ffad067 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/faksimile_image.ts @@ -0,0 +1,43 @@ +import { TlnImage } from './image'; +import { Copyright, Image } from '../models'; +/** + * This is the faksimile image instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/TlnImage.html|TlnImage}. + **/ +export class FaksimileImage extends TlnImage { + /** + * the SPARQL-query of this datatype + **/ + static readonly query: string = ` + PREFIX tln: + + SELECT ?id ?filename ?URL ?secondaryURL ?width ?height ?t_left ?t_top ?t_width ?t_height ?page ?transform WHERE { + ?id a tln:FaksimileImage; + tln:hasFileName ?filename; + tln:hasPrimaryurl ?URL; + tln:hasSecondaryurl ?secondaryURL; + tln:hasWidth ?width; + tln:hasHeight ?height; + tln:hasTextField ?textfield. + ?textfield tln:hasLeft ?t_left; + tln:hasTop ?t_top; + tln:hasWidth ?t_width; + tln:hasHeight ?t_height; + ^tln:pageIsOnFaksimileTextField ?page. + OPTIONAL { ?id tln:hasTransform ?transform. } + }`; + /** + * Copyright information of image + **/ + copyright: Copyright; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.copyright = { + originalUrl: 'http://www.nietzschesource.org/DFGA/' + this.filename.replace('.jpg', ''), + licenseTextUrl: 'http://creativecommons.org/licenses/by-nc-nd/4.0/deed.de', + license: 'CC BY-NC-ND 4.0', + text: 'Dieses Faksimile wird von nietzschesource.org gehostet und wird hier unverändert dargestellt.' + } + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/foreign_text.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/foreign_text.ts new file mode 100644 index 0000000..bf3fb1f --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/foreign_text.ts @@ -0,0 +1,63 @@ +import { TlnPositionalObject } from './positional_object'; +import { TextByForeignHand } from '../models'; +/** + * This is the 'text by foreign hand' instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class TlnTextByForeignHand extends TlnPositionalObject implements TextByForeignHand { + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?text ?pen ?left ?top ?width ?height ?transform WHERE { + ?page tln:hasMarkForeignHands/rdf:rest*/rdf:first ?id. + ?id tln:textOfForeignHands/tln:textHasContent ?text; + tln:penOfForeignHands ?pen; + tln:hasTranskriptionPosition ?tp. + ?tp tln:hasLeft ?left; tln:hasTop ?top; tln:hasWidth ?width; tln:hasHeight ?height. + OPTIONAl { ?tp tln:hasTransform ?transform.} + } `; + /** + * the text of this word + **/ + text: string; + /** + * the text of this word as it has been edited by the editors. + **/ + pen: string; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor(data: any, id?: string, service?: any){ + super(data, id, service) + this.text = this.getData4Key('text'); + this.pen = this.getData4Key('pen'); + } +} +/** + * This is the faksimile text by foreign hand instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class FaksimileTextByForeignHand extends TlnTextByForeignHand { + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?text ?pen ?left ?top ?width ?height ?transform WHERE { + ?page tln:hasMarkForeignHands/rdf:rest*/rdf:first ?id. + ?id tln:textOfForeignHands/tln:textHasContent ?text; + tln:penOfForeignHands ?pen; + tln:hasFaksimilePosition ?fp. + ?fp tln:hasLeft ?left; tln:hasTop ?top; tln:hasWidth ?width; tln:hasHeight ?height. + OPTIONAl { ?fp tln:hasTransform ?transform.} + } `; +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/image.spec.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/image.spec.ts new file mode 100644 index 0000000..bb74e90 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/image.spec.ts @@ -0,0 +1,15 @@ +import { TlnImage} from './image'; +const data = {'id': { 'value': 'asdf'}, 'filename': { 'value': './svg/asdf.svg'}, 'width': { 'value': 20}, 'height': { 'value': 20}, 'URL': { 'value': 'http://asdf.com'}, 't_left': { 'value': 0}, 't_top': { 'value': 0}, 't_width': { 'value': 20}, 't_height': { 'value': 20 }}; +const rawData = { 'results': { 'bindings': [ data ], 'head': { vars: [] }}} + +describe('Image', () => { + /* + it('should create the Object', ()=>{ + expect(new TlnImage(data)).toBeTruthy(); + }); + it('should return TlnImage[]', ()=>{ + let results = TlnImage.convertData(rawData); + expect(results[0].filename == './svg/asdf.svg').toBeTruthy(); + }); + */ +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/image.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/image.ts new file mode 100644 index 0000000..c454480 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/image.ts @@ -0,0 +1,64 @@ +import { BasicResultBindingElement } from './basic_datatype'; +import { Image, TextField} from '../models'; + +export class TlnImage extends BasicResultBindingElement implements Image { + static readonly default_key: string = 'page'; + static readonly query: string = ` + PREFIX tln: + + SELECT ?id ?filename ?width ?height ?URL ?secondaryURL ?t_left ?t_top ?t_width ?t_height ?page ?transform WHERE { + ?id a tln:Image; + tln:hasPrimaryurl ?URL; + tln:hasSecondaryurl ?secondaryURL; + tln:hasFileName ?filename; + tln:hasWidth ?width; + tln:hasHeight ?height; + tln:hasTextField ?textfield. + ?textfield tln:hasLeft ?t_left; + tln:hasTop ?t_top; + tln:hasWidth ?t_width; + tln:hasHeight ?t_height; + ^tln:pageIsOnTextField ?page. + OPTIONAL { ?id tln:hasTransform ?transform. } + }`; + filename: string; + x: number; + y: number; + width: number; + height: number; + text_field: TextField; + URL: string; + secondaryURL: string; + transform: string; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.x = 0; + this.y = 0; + this.filename = this.getData4Key('filename'); + this.width = this.getData4Key('width'); + this.height = this.getData4Key('height'); + this.URL = this.getData4Key('URL'); + this.secondaryURL = this.getData4Key('secondaryURL'); + this.transform = this.getData4Key('transform') + this.text_field = new TlnTextField(this.getData4Key('t_left'), + this.getData4Key('t_top'), + this.getData4Key('t_width'), + this.getData4Key('t_height')); + } +} + +export class TlnTextField implements TextField { + left: number; + top: number; + width: number; + height: number; + bottom: number; + + constructor(left: number, top: number, width: number, height: number){ + this.left = left; + this.top = top; + this.width = width; + this.height = height; + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/line.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/line.ts new file mode 100644 index 0000000..f64215d --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/line.ts @@ -0,0 +1,141 @@ +import { BasicResultBindingElement } from './basic_datatype'; +import { Identifier, Line, LineStub, Manuscript, Page, Reference } from '../models'; +/** + * This is the line instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class TlnLine extends BasicResultBindingElement implements Line { + /** + * the internal default key for replacing {@link /classes/TlnLine.html#query|query} by "id" + * in {@link /classes/TlnLine.html#getQuery|getQuery} if "key" is omitted. + **/ + static readonly default_key: string = 'page'; + static readonly use_id: boolean = true; + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?number ?top ?bottom + ?pageNumber ?manuscriptId ?manuscriptTitle + ?toLine ?toLineNumber ?toPage ?toPageNumber ?toManuscript ?toManuscriptTitle + ?fromLine ?fromLineNumber ?fromPage ?fromPageNumber ?fromManuscript ?fromManuscriptTitle + WHERE { + ?page tln:hasLines/rdf:rest*/rdf:first ?id; + tln:hasNumber ?pageNumber. + ?id a tln:Line; tln:lineHasNumber ?number; + tln:lineHasTopValueOnTranskription ?top; + tln:lineHasBottomValueOnTranskription ?bottom. + ?manuscriptId tln:hasPages/rdf:rest*/rdf:first ?page; + tln:hasTitle ?manuscriptTitle. + OPTIONAL { + ?id tln:lineContinuesOn ?toLine. + ?toLine tln:lineHasNumber ?toLineNumber. + ?toPage tln:hasLines/rdf:rest*/rdf:first ?toLine; tln:hasNumber ?toPageNumber. + ?toManuscript tln:hasPages/rdf:rest*/rdf:first ?toPage; tln:hasTitle ?toManuscriptTitle. + } + OPTIONAL { + ?id ^tln:lineContinuesOn ?fromLine. + ?fromLine tln:lineHasNumber ?fromLineNumber. + ?fromPage tln:hasLines/rdf:rest*/rdf:first ?fromLine; tln:hasNumber ?fromPageNumber. + ?fromManuscript tln:hasPages/rdf:rest*/rdf:first ?fromPage; tln:hasTitle ?fromManuscriptTitle. + } + } `; + /** + * the number of this line. + **/ + number: number; + /** + * the public key for replacing {@link /classes/TlnLine.html#query|query} by "id". + **/ + static readonly query_key: string = 'page'; + /** + * the geometrical top position of this line. + **/ + top: number; + /** + * the geometrical bottom position of this line. + **/ + bottom: number; + continuesFrom?: Reference; + continuesTo?: Reference; + source?: Reference; + page?: string; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.number = this.getData4Key('number'); + this.top = this.getData4Key('top'); + this.bottom = this.getData4Key('bottom'); + let pageNumber = this.getData4Key('pageNumber') + let manuscriptTitle = this.getData4Key('manuscriptTitle') + let continuesTo = this.getData4Key('toLine') + let continuesFrom = this.getData4Key('fromLine') + if (continuesTo != null){ + let manuscript: Manuscript = { id: this.getData4Key('toManuscript'), title: this.getData4Key('toManuscriptTitle') } + let page: Page = { id: this.getData4Key('toPage'), number: this.getData4Key('toPageNumber') } + let line: LineStub = { id: continuesTo, number: this.getData4Key('toLineNumber') } + this.continuesTo = { manuscript: manuscript, page: page, line: line } + } + if (continuesFrom != null){ + let manuscript: Manuscript = { id: this.getData4Key('fromManuscript'), title: this.getData4Key('fromManuscriptTitle') } + let page: Page = { id: this.getData4Key('fromPage'), number: this.getData4Key('fromPageNumber') } + let line: LineStub = { id: continuesFrom, number: this.getData4Key('fromLineNumber') } + this.continuesFrom = { manuscript: manuscript, page: page, line: line } + } + if (this.continuesFrom != null || this.continuesTo != null){ + let reference = (this.continuesTo != null) ? this.continuesTo : this.continuesFrom; + let manuscript: Manuscript = (this.getData4Key('manuscriptId') == reference.manuscript.id) ? null : + { id: this.getData4Key('manuscriptId'), title: this.getData4Key('manuscriptTitle') } + let page: Page = (this.page == reference.page.id) ? null: + { id: this.page, number: this.getData4Key('pageNumber') } + this.source = { manuscript: manuscript, page: page, line: { id: this.id, number: this.number } } + } + } +} +/** + * This is the faksimile line instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/TlnLine.html|TlnLine}. + **/ +export class FaksimileLine extends TlnLine implements Line { + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?number ?top ?bottom + ?pageNumber ?manuscriptId ?manuscriptTitle + ?toLine ?toLineNumber ?toPage ?toPageNumber ?toManuscript ?toManuscriptTitle + ?fromLine ?fromLineNumber ?fromPage ?fromPageNumber ?fromManuscript ?fromManuscriptTitle + WHERE { + ?page tln:hasLines/rdf:rest*/rdf:first ?id; + tln:hasNumber ?pageNumber. + ?id a tln:Line; tln:lineHasNumber ?number; + tln:lineHasInnerTopValueOnFaksimile ?top; + tln:lineHasInnerBottomValueOnFaksimile ?i_bottom. + ?manuscriptId tln:hasPages/rdf:rest*/rdf:first ?page; + tln:hasTitle ?manuscriptTitle. + BIND( (?top+?i_bottom)/2 as ?bottom). + OPTIONAL { + ?id tln:lineContinuesOn ?toLine. + ?toLine tln:lineHasNumber ?toLineNumber. + ?toPage tln:hasLines/rdf:rest*/rdf:first ?toLine; tln:hasNumber ?toPageNumber. + ?toManuscript tln:hasPages/rdf:rest*/rdf:first ?toPage; tln:hasTitle ?toManuscriptTitle. + } + OPTIONAL { + ?id ^tln:lineContinuesOn ?fromLine. + ?fromLine tln:lineHasNumber ?fromLineNumber. + ?fromPage tln:hasLines/rdf:rest*/rdf:first ?fromLine; tln:hasNumber ?fromPageNumber. + ?fromManuscript tln:hasPages/rdf:rest*/rdf:first ?fromPage; tln:hasTitle ?fromManuscriptTitle. + } + } `; +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/line_reference.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/line_reference.ts new file mode 100644 index 0000000..d65e99e --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/line_reference.ts @@ -0,0 +1,51 @@ +import { BasicResultBindingElement } from './basic_datatype'; +import { Identifier, LineStub, Manuscript, Page, Reference} from '../models'; + +export class TlnLineReference extends BasicResultBindingElement implements Reference { + /** + * the internal default key for replacing {@link /classes/TlnLine.html#query|query} by "id" + * in {@link /classes/TlnLine.html#getQuery|getQuery} if "key" is omitted. + **/ + static readonly default_key: string = 'page'; + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?page ?page_id ?manuscript ?title ?line_number WHERE { + ?id a tln:Line; tln:lineHasNumber ?line_number. + ?page_id tln:hasLines/rdf:rest*/rdf:first ?id; tln:hasNumber ?page. + ?manuscript tln:hasPages/rdf:rest*/rdf:first ?page_id; tln:hasTitle ?title. + } `; + /** + * the public key for replacing {@link /classes/TlnLine.html#query|query} by "id". + **/ + static readonly query_key: string = 'id'; + /** + * the manuscript. + **/ + manuscript: Manuscript; + /** + * the line. + **/ + line: LineStub; + /** + * the page. + **/ + page: Page; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.line = { id: this.id, number: this.getData4Key('line_number') } + this.page = { id: this.getData4Key('page_id'), number: this.getData4Key('page') } + this.manuscript = { id: this.getData4Key('manuscript'), title: this.getData4Key('title') } + } +} + diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/manuscript.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/manuscript.ts new file mode 100644 index 0000000..6b5d9a7 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/manuscript.ts @@ -0,0 +1,22 @@ +import { BasicResultBindingElement } from './basic_datatype'; +import { Manuscript } from '../models'; + +export class ManuscriptStub extends BasicResultBindingElement implements Manuscript { + static readonly query: string = ` + PREFIX data: + PREFIX tln: + + SELECT ?id ?title ?type WHERE { + ?id a tln:ArchivalManuscriptUnity ; + tln:hasTitle ?title; + tln:hasManuscriptType ?type. + }`; + title: string; + type: string; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.title = this.getData4Key('title'); + this.type = this.getData4Key('type'); + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/navigation.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/navigation.ts new file mode 100644 index 0000000..a84d483 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/navigation.ts @@ -0,0 +1,79 @@ +import { BasicResultBindingElement } from './basic_datatype'; +import { ManuscriptUnity, NavigationPage } from '../models'; + +export class TlnManuscriptUnity extends BasicResultBindingElement implements ManuscriptUnity { + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + PREFIX list: + + SELECT ?title ?numberOfPages ?firstItem ?firstNumber ?lastItem ?lastNumber WHERE { + ?id a tln:ManuscriptUnity; + tln:hasTitle ?title; + tln:hasPages ?list. + ?list list:length ?numberOfPages; + rdf:first ?firstItem; + rdf:rest* ?lastNode. + ?lastNode rdf:first ?lastItem; + rdf:rest rdf:nil. + ?firstItem tln:hasNumber ?firstNumber. + ?lastItem tln:hasNumber ?lastNumber. + }`; + static readonly query_key: string = 'id'; + title: string; + numberOfPages: number; + firstPage: NavigationPage; + lastPage: NavigationPage; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.title = this.getData4Key('title'); + this.numberOfPages = this.getData4Key('numberOfPages') + this.firstPage = { id: this.getData4Key('firstItem'), number: this.getData4Key('firstNumber'), title: this.title, index: 1 }; + this.lastPage = { id: this.getData4Key('lastItem'), number: this.getData4Key('lastNumber'), title: this.title, index: this.numberOfPages }; + } +} + +export class TlnNavigationPage extends BasicResultBindingElement { + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + PREFIX list: + + SELECT ?title ?number ?previous ?previousNumber ?next ?nextNumber ?previousIndex ?nextIndex WHERE { + ?id tln:hasNumber ?number. + ?unity a tln:ArchivalManuscriptUnity; + tln:hasTitle ?title; + tln:hasPages ?list. + ?list rdf:rest* ?node. + ?node rdf:first ?id. + OPTIONAL { ?node ^rdf:rest/rdf:first ?previous. + ?previous tln:hasNumber ?previousNumber. + ?list list:index (?previousIndex ?previous); + } + OPTIONAL { ?node rdf:rest/rdf:first ?next. + ?next tln:hasNumber ?nextNumber. + ?list list:index (?nextIndex ?next); + } + }`; + static readonly query_key: string = 'id'; + title: string; + number: string; + previous?: NavigationPage; + next?: NavigationPage; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.title = this.getData4Key('title'); + this.number = this.getData4Key('number'); + let previous = this.getData4Key('previous'); + let next = this.getData4Key('next'); + if (previous != null){ + this.previous = { id: previous, number: this.getData4Key('previousNumber'), title: this.title, index: this.getData4Key('previousIndex')+1 }; + } + if (next != null){ + this.next = { id: next, number: this.getData4Key('nextNumber'), title: this.title, index: this.getData4Key('nextIndex')+1 }; + } + } +} + diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/page.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/page.ts new file mode 100644 index 0000000..a9caf15 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/page.ts @@ -0,0 +1,48 @@ +import { BasicResultBindingElement } from './basic_datatype'; +import { Page} from '../models'; + +export class PageStub extends BasicResultBindingElement implements Page { + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?number WHERE { + ?manuscript a tln:ArchivalManuscriptUnity; + tln:hasPages/rdf:rest*/rdf:first ?id. + ?id tln:hasNumber ?number. + FILTER EXISTS { ?id tln:hasImage ?image } + }`; + static readonly query_key: string = 'manuscript'; + number: string; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.number = this.getData4Key('number'); + } +} +export class TlnPage extends PageStub { + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?title ?number ?manuscript WHERE { + ?manuscript a tln:ArchivalManuscriptUnity; + tln:hasTitle ?title; + tln:hasPages/rdf:rest*/rdf:first ?id. + ?id a tln:Page; tln:hasNumber ?number. + }`; + static readonly query_key: string = 'id'; + title: string; + manuscript: string; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.title = this.getData4Key('title'); + this.manuscript = this.getData4Key('manuscript'); + if (this.service != null) { + this.service.setCurrentIri(this.id, this.manuscript); + //this.service.setTitle(this.title + ", " + this.number); + } + } +} + diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/positional-markup.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/positional-markup.ts new file mode 100644 index 0000000..617f20f --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/positional-markup.ts @@ -0,0 +1,65 @@ +import { BasicResultBindingElement } from './basic_datatype'; +/** + * This is the line instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class TlnPositionalStyleMarkup extends BasicResultBindingElement { + /** + * the internal default key for replacing {@link /classes/TlnLine.html#query|query} by "id" + * in {@link /classes/TlnLine.html#getQuery|getQuery} if "key" is omitted. + **/ + static readonly default_key: string = 'word'; + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX homotypic: + PREFIX rdf: + PREFIX stoff: + PREFIX tln: + + SELECT ?id ?cssStyleTag ?startIndex ?endIndex WHERE { + ?word homotypic:hasMarkup ?id. + ?id stoff:hasCSS ?cssStyleTag; + stoff:standoffMarkupHasStartIndex ?startIndex; + stoff:standoffMarkupHasEndIndex ?endIndex. + } `; + /** + * one or multiple css style tags like 'font-weight: bold;' or 'font-weight: bold; font-style: italic' + **/ + cssStyleTag: string; + /** + * character where the style ends. + **/ + endIndex: number; // character where the style/class ends + /** + * standoff type: 0 === markup via css tag; 1 === apply css class + **/ + sType: number = 0; + /** + * character where the style starts. + **/ + startIndex: number; + static readonly query_key: string = 'word'; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.cssStyleTag = this.getData4Key('cssStyleTag'); + this.endIndex = this.getData4Key('endIndex'); + this.startIndex = this.getData4Key('startIndex'); + } +} + +export class TlnFilteredPositionalStyleMarkup extends TlnPositionalStyleMarkup { + protected readonly regex: RegExp = new RegExp('font-size:[0-9]\+%[;]*'); + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.cssStyleTag = this.cssStyleTag.replace(this.regex,'') + 'filter: invert(1);'; + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/positional_object.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/positional_object.ts new file mode 100644 index 0000000..f244219 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/positional_object.ts @@ -0,0 +1,51 @@ +import { BasicResultBindingElement } from './basic_datatype'; +import { PositionalObject } from '../models'; +/** + * This is the 'text by foreign hand' instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class TlnPositionalObject extends BasicResultBindingElement implements PositionalObject { + /** + * the internal default key for replacing {@link /classes/TlnWord.html#query|query} by "id" + * in {@link /classes/TlnWord.html#getQuery|getQuery} if "key" is omitted. + **/ + static readonly default_key: string = 'page'; + /** + * the public key for replacing {@link /classes/TlnWord.html#query|query} by "id". + **/ + static readonly query_key: string = 'page'; + /** + * 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; + /** + * the matrix transformation string of the geometrical position of this word's rect. + * */ + transform?: string; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor(data: any, id?: string, service?: any){ + super(data, id, service) + this.left = this.getData4Key('left'); + this.top = this.getData4Key('top'); + this.width = this.getData4Key('width'); + this.height = this.getData4Key('height'); + this.transform = this.getData4Key('transform'); + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/preview_data.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/preview_data.ts new file mode 100644 index 0000000..0d48494 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/preview_data.ts @@ -0,0 +1,71 @@ +import { BasicResultBindingElement, FusekiResults } from './basic_datatype'; +import { TlnWord } from './word'; +import { TlnPositionalStyleMarkup, TlnFilteredPositionalStyleMarkup } from './positional-markup'; + +export class TlnPreviewWord extends TlnWord { + /** + * the internal default key for replacing {@link /classes/TlnLine.html#query|query} by "id" + * in {@link /classes/TlnLine.html#getQuery|getQuery} if "key" is omitted. + **/ + static readonly default_key: string = 'line'; + static readonly query_key: string = 'line'; + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX homotypic: + PREFIX rdf: + PREFIX stoff: + PREFIX tln: + + SELECT DISTINCT ?id ?text ?markup_id ?cssStyleTag ?startIndex ?endIndex ?left ?top ?earlier_version WHERE { + ?id a tln:Word; + tln:wordBelongsToLine ?line; + tln:hasTranskriptionPosition/tln:hasLeft ?left; + tln:hasTranskriptionPosition/tln:hasTop ?top; + tln:hasText ?text; + homotypic:hasMarkup ?markup_id. + ?markup_id stoff:hasCSS ?cssStyleTag; + stoff:standoffMarkupHasStartIndex ?startIndex; + stoff:standoffMarkupHasEndIndex ?endIndex. + + FILTER(NOT EXISTS {?word tln:wordHasWordParts/rdf:rest*/rdf:first ?id}) + FILTER(NOT EXISTS {?word (tln:wordHasEarlierVersion|tln:overwritesWord) ?id}) + OPTIONAL { ?id (tln:wordHasEarlierVersion|tln:overwritesWord)/tln:hasText ?earlier_version} + }`; + earlier_version?: string; + left: number; + top: number; + markups: TlnPositionalStyleMarkup[] = []; + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor(data: any, id?: string, service?: any){ + super(data, id, service) + this.earlier_version = this.getData4Key('earlier_version') + this.left = this.getData4Key('left') + this.top = this.getData4Key('top') + } + public static convertData(this: T, data: FusekiResults, id?: string, service?: any): Array> { + let elements = []; + let content = this.getContent(data); + for (var i = 0; i < content.length; i++){ + let element = new TlnPreviewWord(content[i], id, service); + element.markups = TlnFilteredPositionalStyleMarkup.convertData({ head: { vars: []}, results: { bindings: [ content[i] ] } }, content[i]['markup_id']) + if (elements.length > 0 && element.id == elements[elements.length-1].id){ + elements[elements.length-1].markups.push(element.markups[0]) + if (element.top < elements[elements.length-1].top && element.left < elements[elements.length-1].left){ + elements[elements.length-1].left = element.left; + } + } else { + elements.push(element); + } + } + elements.sort((w0: TlnPreviewWord, w1: TlnPreviewWord)=>{ + return w0.left - w1.left + }) + return elements; + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/svg_image.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/svg_image.ts new file mode 100644 index 0000000..1791b46 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/svg_image.ts @@ -0,0 +1,67 @@ +import { TlnImage } from './image'; +import { Image } from '../models'; + +export class SVGImage extends TlnImage { + static readonly query: string = ` + PREFIX tln: + + SELECT ?id ?filename ?width ?height ?URL ?secondaryURL ?t_left ?t_top ?t_width ?t_height ?page WHERE { + ?id a tln:SVGImage; + tln:hasPrimaryurl ?URL; + tln:hasSecondaryurl ?secondaryURL; + tln:hasFileName ?filename; + tln:hasWidth ?width; + tln:hasHeight ?height; + tln:hasTextField ?textfield. + ?textfield tln:hasLeft ?t_left; + tln:hasTop ?t_top; + tln:hasWidth ?t_width; + tln:hasHeight ?t_height; + ^tln:pageIsOnTextField ?page. + }`; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.x = this.text_field.left; + this.y = this.text_field.top; + } +} +export class SVGImageClip extends SVGImage { + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?filename ?width ?height ?URL ?secondaryURL ?t_left ?t_top ?t_width ?t_height ?page ?y ?end_bottom WHERE { + ?id a tln:SVGImage; + tln:hasPrimaryurl ?URL; + tln:hasSecondaryurl ?secondaryURL; + tln:hasFileName ?filename; + tln:hasWidth ?width; + tln:hasHeight ?height; + tln:hasTextField ?textfield. + ?textfield tln:hasLeft ?t_left; + tln:hasTop ?y; + tln:hasWidth ?t_width; + ^tln:pageIsOnTextField ?page. + ?page tln:hasLines ?lines. + ?lines rdf:rest* ?start_node; + rdf:rest* ?end_node. + ?start_node rdf:first ?startLine;. + ?end_node rdf:first ?endLine;. + ?startLine tln:lineHasTopValueOnTranskription ?start_top. + ?endLine tln:lineHasBottomValueOnTranskription ?end_bottom. + OPTIONAL { ?start_node ^rdf:rest/rdf:first ?previousLine. + ?previousLine tln:lineHasBottomValueOnTranskription ?previous_bottom. + } + OPTIONAL { ?end_node rdf:rest/rdf:first/rdf:rest/rdf:first ?nextLine. + ?nextLine tln:lineHasTopValueOnTranskription ?next_top. + } + BIND(IF(bound(?previous_bottom), ?previous_bottom+1, ?start_top - 5) as ?t_top) + BIND(IF(bound(?next_top), ?next_top-?t_top, ?end_bottom - ?t_top + 10) as ?t_height) + }`; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.y = this.getData4Key('y'); + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/text_version.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/text_version.ts new file mode 100644 index 0000000..ec9a7a2 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/text_version.ts @@ -0,0 +1,189 @@ +import { FusekiResults, BasicResultBindingElement } from './basic_datatype'; +import { TextGeneticOrder, TextUnity, TextVersion } from '../models'; +export class TlnTextVersion extends BasicResultBindingElement implements TextVersion { + id: string; + title: string; + extUrl?: string; + textUnities: TlnTextUnity[] = []; + + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.title = this.getData4Key('title'); + this.extUrl = this.getData4Key('extUrl'); + } +} +export class TlnTextUnity extends BasicResultBindingElement implements TextUnity { + number: string; + startLine?: number; + endLine?: number; + belongsToPage?: string; + endLineIri?: string; + startLineIri? : string; + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + this.number = this.getData4Key('number'); + this.startLine = this.getData4Key('startLine'); + this.endLine = this.getData4Key('endLine'); + this.belongsToPage = this.getData4Key('text_page'); + this.startLineIri = this.getData4Key('startLineIri'); + this.endLineIri = this.getData4Key('endLineIri'); + } +} +/** + * This is the text version instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class TlnTextGeneticOrder extends BasicResultBindingElement implements TextGeneticOrder { + /** + * the internal default key for replacing {@link /classes/TlnLine.html#query|query} by "id" + * in {@link /classes/TlnLine.html#getQuery|getQuery} if "key" is omitted. + **/ + static readonly default_key: string = 'page'; + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT distinct ?id ?text_version ?text_unity ?text_page ?number ?startLine ?endLine ?title WHERE { + {?version a tln:IdentifiedTextVersion; + tln:identifiesAsVersion/rdf:rest*/rdf:first ?page. + } UNION { + ?version a tln:IdentifiedTextVersion; + tln:identifiesAsVersion/rdf:rest*/rdf:first/tln:belongsToPage ?page. + } + ?id tln:hasGeneticOrder/rdf:rest*/rdf:first ?version; + tln:hasGeneticOrder/rdf:rest*/rdf:first ?text_version. + + OPTIONAL { ?text_version tln:identifiesAsVersion/rdf:rest*/rdf:first ?text_unity. + OPTIONAL { ?text_unity tln:hasNumber ?number. + ?manuscript a tln:ArchivalManuscriptUnity; + tln:hasTitle ?title; + tln:hasPages/rdf:rest*/rdf:first ?text_unity. + } + OPTIONAL { ?text_unity tln:belongsToPage ?text_page; + tln:startLine/tln:lineHasNumber?startLine; + tln:endLine/tln:lineHasNumber ?endLine. + ?text_page tln:hasNumber ?number. + ?manuscript a tln:ArchivalManuscriptUnity; + tln:hasTitle ?title; + tln:hasPages/rdf:rest*/rdf:first ?text_page. + } + } + OPTIONAL { ?text_version tln:textUnitHasTitle ?title.} + }`; + /** + * the public key for replacing {@link /classes/TlnLine.html#query|query} by "id". + **/ + static readonly query_key: string = 'page'; + textVersions: TlnTextVersion[] = []; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor (data: any, id?: string, service?: any) { + super(data, id, service); + } + public static convertData(this: T, data: FusekiResults, id?: string, service?: any): Array> { + let elements = []; + let content = this.getContent(data); + for (var i = 0; i < content.length; i++){ + let element = new TlnTextGeneticOrder(content[i], service); + let versions = TlnTextVersion.convertData({ head: { vars: []}, results: { bindings: [ content[i] ] } }, content[i]['text_version']['value']) + let text_unities = (content[i]['text_unity'] != undefined) ? + TlnTextUnity.convertData({ head: { vars: []}, results: { bindings: [ content[i] ] } }, content[i]['text_unity']['value']) : []; + /*if (text_unities.length > 0){ + console.log(text_unities[0]); + }*/ + if (elements.length > 0 && elements[elements.length-1].id == element.id){ + if (elements[elements.length-1].textVersions.length > 0 + && elements[elements.length-1].textVersions[elements[elements.length-1].textVersions.length-1].id == versions[0].id){ + if (text_unities.length > 0){ + elements[elements.length-1].textVersions[elements[elements.length-1].textVersions.length-1].textUnities.push(text_unities[0]); + } + } else { + if (text_unities.length > 0){ + versions[0].textUnities.push(text_unities[0]) + } + elements[elements.length-1].textVersions.push(versions[0]); + } + } else { + if (text_unities.length > 0){ + versions[0].textUnities.push(text_unities[0]) + } + element.textVersions.push(versions[0]) + elements.push(element) + } + } + //console.log(elements) + return elements; + } + +} +export class TlnStandaloneTextVersion extends TlnTextVersion { + /** + * the internal default key for replacing {@link /classes/TlnLine.html#query|query} by "id" + * in {@link /classes/TlnLine.html#getQuery|getQuery} if "key" is omitted. + **/ + static readonly default_key: string = 'geneticOrder'; + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT distinct ?id ?text_unity ?text_page ?number ?startLine ?startLineIri ?endLine ?endLineIri ?title ?extUrl WHERE { + ?geneticOrder a tln:TextGenesis; + tln:hasGeneticOrder/rdf:rest*/rdf:first ?id. + + OPTIONAL { ?id tln:identifiesAsVersion/rdf:rest*/rdf:first ?text_unity. + OPTIONAL { ?text_unity tln:hasNumber ?number. + ?manuscript a tln:ArchivalManuscriptUnity; + tln:hasTitle ?title; + tln:hasPages/rdf:rest*/rdf:first ?text_unity. + } + OPTIONAL { ?text_unity tln:belongsToPage ?text_page; + tln:startLine ?startLineIri; + tln:endLine ?endLineIri. + ?text_page tln:hasNumber ?number. + ?startLineIri tln:lineHasNumber?startLine. + ?endLineIri tln:lineHasNumber ?endLine. + ?manuscript a tln:ArchivalManuscriptUnity; + tln:hasTitle ?title; + tln:hasPages/rdf:rest*/rdf:first ?text_page. + } + } + OPTIONAL { ?id tln:textUnitHasTitle ?title; tln:textUnitHasUrl ?extUrl.} + }`; + /** + * the public key for replacing {@link /classes/TlnLine.html#query|query} by "id". + **/ + static readonly query_key: string = 'geneticOrder'; + + public static convertData(this: T, data: FusekiResults, id?: string, service?: any): Array> { + let elements = []; + let content = this.getContent(data); + for (var i = 0; i < content.length; i++){ + let element = new TlnStandaloneTextVersion(content[i], service); + let text_unities = (content[i]['text_unity'] != undefined) ? + TlnTextUnity.convertData({ head: { vars: []}, results: { bindings: [ content[i] ] } }, content[i]['text_unity']['value']) : []; + if (elements.length > 0 && elements[elements.length-1].id == element.id){ + if (text_unities.length > 0){ + elements[elements.length-1].textUnities.push(text_unities[0]); + } + } else { + if (text_unities.length > 0){ + element.textUnities.push(text_unities[0]); + } + elements.push(element) + } + } + //console.log(elements) + return elements; + } + +} diff --git a/nietzsche-beta-app/src/app/tln-edition/datatypes/word.ts b/nietzsche-beta-app/src/app/tln-edition/datatypes/word.ts new file mode 100644 index 0000000..77d0247 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/datatypes/word.ts @@ -0,0 +1,98 @@ +import { TlnPositionalObject } from './positional_object'; +import { Word } from '../models'; +/** + * This is the word instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class TlnWord extends TlnPositionalObject implements Word { + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?text ?edited_text ?left ?top ?width ?height ?transform ?line ?line_number ?deleted ?deletion_path WHERE { + ?page tln:hasWords/rdf:rest*/rdf:first ?id. + ?id tln:wordBelongsToLine ?line; + tln:hasText ?text; + tln:hasTranskriptionPosition ?tp. + ?tp tln:hasLeft ?left; tln:hasTop ?top; tln:hasWidth ?width; tln:hasHeight ?height. + ?line tln:lineHasNumber ?line_number. + BIND(exists{ + {?id tln:wordIsDeletedByPath ?path} + UNION{ ?id tln:wordHasWordParts/rdf:rest*/rdf:first ?word_part. + ?word_part tln:hasTranskriptionPosition ?tp; tln:wordIsDeletedByPath ?path} + } as ?deleted) + + OPTIONAl { ?tp tln:hasTransform ?transform.} + OPTIONAl { ?id tln:hasEditedText ?edited_text.} + OPTIONAl { ?id tln:wordIsDeletedByPath/tln:hasDAttribute ?deletion_path.} + OPTIONAl { ?id tln:wordHasWordParts/rdf:rest*/rdf:first ?word_part. + ?word_part tln:hasTranskriptionPosition ?tp; tln:wordIsDeletedByPath/tln:hasDAttribute ?deletion_path} + } `; + /** + * the text of this word + **/ + text: string; + /** + * the text of this word as it has been edited by the editors. + **/ + edited_text?: string; + /** + * the id of the line to which this word belongs. + **/ + line: string; + /** + * 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; + + /** + * The constructor creates a datatype from the data. + * + * @param id if omitted the id will be retrieved from data + **/ + constructor(data: any, id?: string, service?: any){ + super(data, id, service) + this.text = this.getData4Key('text'); + this.edited_text = this.getData4Key('edited_text'); + this.line = this.getData4Key('line'); + this.line_number = this.getData4Key('line_number'); + this.deleted = this.getData4Key('deleted'); + this.deletion_path = this.getData4Key('deletion_path'); + } +} +/** + * This is the faksimile word instantiation of an element of {@link /interfaces/FusekiResults.html|FusekiResults}. + * It extends {@link /classes/BasicResultBindingElement.html|BasicResultBindingElement}. + **/ +export class FaksimileWord extends TlnWord { + /** + * the SPARQL-query of this datatype. + **/ + static readonly query: string = ` + PREFIX tln: + PREFIX rdf: + + SELECT ?id ?text ?edited_text ?left ?top ?width ?height ?transform ?line ?line_number ?deleted WHERE { + ?page tln:hasWords/rdf:rest*/rdf:first ?id. + ?id tln:wordBelongsToLine ?line; + tln:hasText ?text; + tln:hasFaksimilePosition ?fp. + ?fp tln:hasLeft ?left; tln:hasTop ?top; tln:hasWidth ?width; tln:hasHeight ?height. + ?line tln:lineHasNumber ?line_number. + BIND(exists{{?id tln:wordIsDeletedByPath ?path} UNION { ?id tln:wordHasWordParts/rdf:rest*/rdf:first/tln:wordIsDeletedByPath ?path} + } as ?deleted) + OPTIONAl { ?fp tln:hasTransform ?transform.} + OPTIONAl { ?id tln:hasEditedText ?edited_text.} + } `; +} diff --git a/nietzsche-beta-app/src/app/tln-edition/models.ts b/nietzsche-beta-app/src/app/tln-edition/models.ts new file mode 100644 index 0000000..7546326 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/models.ts @@ -0,0 +1,61 @@ +import { Observable } from 'rxjs'; +import {EventEmitter} from '@angular/core'; +import { Page } from '../page-view/models'; +export { externalAssignClass, + externalAssignStyle, + Configuration, + Continuation, + Copyright, + Identifier, + Image, + Line, + LineStub, + Reference, + Manuscript, + Page, + Point, + PositionalObject, + TextField, + TextByForeignHand, + Word, + USE_EXTERNAL_TOOLTIP } from '../page-view/models'; +export interface ManuscriptUnity { + numberOfPages: number; + title: string; +} +export interface NavigationPage extends Page { + title: string; + index?: number; +} +export interface TextVersion { + id: string; + title: string; + textUnities: TextUnity[]; +} +export interface TextUnity { + id: string; + number: string; + startLine?: number; + endLine?: number; + belongsToPage?: string; +} +/** + * This interface specifies a query service that returns + * the response from a HttpClient.post as an Observable. + * */ +export interface TlnQueryServiceInterface { + reset_data: EventEmitter; + /** + * @param query: The query to run. + * @returns response: the response as an Observable. + * */ + getData(query: string): Observable; + resetData(key: string): void; +} +export interface TextGeneticOrder { + id: string; + textVersions: TextVersion[]; +} +/** + * Set zoom factor interface + **/ diff --git a/nietzsche-beta-app/src/app/tln-edition/route-reader.ts b/nietzsche-beta-app/src/app/tln-edition/route-reader.ts new file mode 100644 index 0000000..a6109d8 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/route-reader.ts @@ -0,0 +1,50 @@ +import { OnInit } from '@angular/core'; +import { Router, ActivatedRoute, Params } from '@angular/router'; + +export interface ParamContent { + param: string; + isNumber: boolean; + debug?: string; +} +export interface Mapping { + [name: string]: ParamContent; +} + +export class RouteReader implements OnInit { + protected mapping: Mapping; + protected routerParams: Params; + + constructor(protected router: Router, protected activatedRoute: ActivatedRoute ) { } + + ngOnInit() { + this.activatedRoute.queryParams.subscribe(params => { + this.readParams(params) + }); + } + protected readParams(params: Params) { + this.routerParams = params; + for(let key of Object.keys(this.mapping)){ + let paramsKey = this.mapping[key]['param']; + if (this.routerParams[paramsKey] != null){ + if (this.mapping[key]['isNumber']){ + this[key] = Number(this.routerParams[paramsKey]) + } else if (Array.isArray(this[key]) && !Array.isArray(this.routerParams[paramsKey])) { + let items = JSON.parse(this.routerParams[paramsKey]) + this[key] = items.map(item =>{ + if (this.routerParams['prefix'] != null && String(item).startsWith('#')){ + return this.routerParams['prefix'] + item; + } else { + return item; + } + }); + } else { + this[key] = (this.routerParams['prefix'] != null && String(this.routerParams[paramsKey]).startsWith('#')) + ? this.routerParams['prefix'] + this.routerParams[paramsKey] : this.routerParams[paramsKey]; + } + if (this.mapping[key]['debug'] != null){ + console.log(this.mapping[key]['debug'], this[key]); + } + } + } + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/route-updater.ts b/nietzsche-beta-app/src/app/tln-edition/route-updater.ts new file mode 100644 index 0000000..cf0b506 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/route-updater.ts @@ -0,0 +1,55 @@ +import { Router, ActivatedRoute, Params } from '@angular/router'; +import { Mapping, RouteReader } from './route-reader'; + +export class RouteUpdater extends RouteReader { + protected mapping: Mapping; + protected routerParams: Params; + protected currentRoute: string; + + constructor(protected router: Router, protected activatedRoute: ActivatedRoute ) { + super(router, activatedRoute); + } + private createPrefix(): string { + let iris = Object.entries(this).filter(([key, value]) =>this.mapping[key] != null && String(value).startsWith('http')) + let uniquePrefixes = Array.from(new Set(iris.map(([key, value]) =>String(value).substring(0, String(value).indexOf('#'))))) + if (uniquePrefixes.length == 0){ + return '' + } else if (uniquePrefixes.length == 1){ + return uniquePrefixes[0]; + } else { + let prefix: string = ''; + for(let uniquePrefix of uniquePrefixes){ + if (prefix == ''){ + prefix = uniquePrefix; + } else if(iris.filter(iri =>String(iri).startsWith(uniquePrefix)).length > iris.filter(iri =>String(iri).startsWith(prefix)).length) { + prefix = uniquePrefix; + } + } + return prefix; + } + } + protected updateParams() { + let prefix = (this.routerParams['prefix'] != null) ? this.routerParams['prefix'] : this.createPrefix(); + let newRouterParam = { prefix: prefix }; + for(let key of Object.keys(this.mapping)){ + let paramsKey = this.mapping[key]['param']; + if(this[key] != null){ + if (Array.isArray(this[key]) && this[key].length > 0){ + let items = this[key].map(item =>{ + return String(item).replace(newRouterParam['prefix'], ''); + }) + newRouterParam[paramsKey] = JSON.stringify(items) + } else { + newRouterParam[paramsKey] = + (String(this[key]).startsWith('http')) ? String(this[key]).replace(newRouterParam['prefix'], '') : this[key]; + } + } + } + for(let key of Object.keys(this.routerParams)){ + if(newRouterParam[key] == null){ + newRouterParam[key] = this.routerParams[key]; + } + } + this.router.navigate([ 'contentView/' + this.currentRoute], { queryParams: newRouterParam }); + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/services.ts b/nietzsche-beta-app/src/app/tln-edition/services.ts new file mode 100644 index 0000000..5cd2f3d --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/services.ts @@ -0,0 +1,3 @@ +export { TlnQueryService } from './tln-query.service'; +export { PageViewService } from '../page-view/page-view.service'; +export { ConfigurableComponent } from '../page-view/configurable-component'; diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.css b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.css new file mode 100644 index 0000000..e69de29 diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.html b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.html new file mode 100644 index 0000000..4c33fc7 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.html @@ -0,0 +1 @@ +

page-version-view works!

diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.spec.ts b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.spec.ts new file mode 100644 index 0000000..7ac0ef7 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PageVersionViewComponent } from './page-version-view.component'; + +describe('PageVersionViewComponent', () => { + let component: PageVersionViewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PageVersionViewComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PageVersionViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.ts b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.ts new file mode 100644 index 0000000..2ff693d --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/page-version-view/page-version-view.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-page-version-view', + templateUrl: './page-version-view.component.html', + styleUrls: ['./page-version-view.component.css'] +}) +export class PageVersionViewComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.css b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.css new file mode 100644 index 0000000..3b35664 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.css @@ -0,0 +1,43 @@ +#page { + width: 100%; + position: relative; + top: 0px; + left: 0px; +} + +.preview { + position: absolute; + top: 0px; + width: 400px; + height: 100%; + text-align: center; +} +.centered { + margin: auto; +} +.text { + position: absolute; + top: 0px; + left: 400px; +} +.page-view { + overflow: auto; +} +.external { + background-color: lightblue; + height: 100px; + position: relative; + left: 80px; +} +.highlight { + background-color: lightblue; +} +.text-navi { + position: absolute; + right: 0px; + background-color: lightblue; + z-index: 1; +} +.page-navi { + float: left; +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.html b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.html new file mode 100644 index 0000000..ac0ba34 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.html @@ -0,0 +1,52 @@ +
+
+

Querverweise

+
+ +
+ south +
+
+
+
+
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+
diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.spec.ts b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.spec.ts new file mode 100644 index 0000000..dbee56d --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TlnCrossrefComponent } from './tln-crossref.component'; + +describe('TlnCrossrefComponent', () => { + let component: TlnCrossrefComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TlnCrossrefComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TlnCrossrefComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.ts b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.ts new file mode 100644 index 0000000..ce2ce19 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/tln-crossref.component.ts @@ -0,0 +1,115 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Router, ActivatedRoute, Params } from '@angular/router'; +import { DomSanitizer } from '@angular/platform-browser'; +import { TlnQueryServiceInterface, Reference, TextVersion} from '../models'; +import { TlnManuscriptUnity, TlnNavigationPage} from '../datatypes/navigation'; +import { TLN_CROSSREF_ROUTE, TLN_FIND_PARAM, TLN_PAGE_PARAM, TLN_MANUSCRIPT_PARAM, + TLN_SELECTED_LINES_PARAM, TLN_TEXT_GENETIC_ORDER_PARAM, TLN_VIEW_OPTION_PARAM, TLN_ZOOM_PARAM, VIEW_OPTIONS, ONTOLOTY_PREFIX } from '../constants'; +import { IsReconstructedKonvolut } from '../datatypes/basic_datatype'; +import { TlnLine} from '../datatypes/line'; +import { TlnStandaloneTextVersion} from '../datatypes/text_version'; +import { Mapping } from '../route-reader'; +import { RouteUpdater } from '../route-updater'; +import { ComplexKeyIriMapping, DataHandler, KeyIriMapping } from '../data_handler'; +import { PageViewService, TlnQueryService } from '../services'; +import { TlnInformationComponent, ParentInformation } from '../tln-information/tln-information.component'; +import { PageInformation } from '../tln-information/page-information'; + +@Component({ + selector: 'tln-crossref', + templateUrl: './tln-crossref.component.html', + styleUrls: ['./tln-crossref.component.css'] +}) +export class TlnCrossrefComponent extends RouteUpdater { + /** + * OPTIONAL pass a queryService with method + * {@link /interfaces/TlnQueryServiceInterface.html#getData|getData} + * to TlnPageViewComponent. + **/ + @Input() queryService: TlnQueryServiceInterface; + zoomFactor: number = 1; + findText: string; + dataHandler: DataHandler = new DataHandler(this); + textVersions: TextVersion[] = []; + current_genetic_order_iri: string; + max_width: number = -1; + max_height: number = -1; + each_version_height: number = -1; + selectedLines: string[] = []; + selectedTextVersions: TextVersion[] = [] + private readonly increment: number = 0.333; + private readonly decrement: number = this.increment*-1; + private readonly margin_width: number = 280; + protected currentRoute: string = TLN_CROSSREF_ROUTE; + protected mapping: Mapping = { findText: { param: TLN_FIND_PARAM, isNumber: false }, + current_genetic_order_iri: { param: TLN_TEXT_GENETIC_ORDER_PARAM, isNumber: false }, + selectedLines: { param: TLN_SELECTED_LINES_PARAM, isNumber: false }, + zoomFactor: { param: TLN_ZOOM_PARAM, isNumber: true } } + routerParams: Params; + selectedViewOption: string = VIEW_OPTIONS.TRANSKRIPTION + updating: boolean = false; + viewOptions: string[] = [ VIEW_OPTIONS.TRANSKRIPTION, VIEW_OPTIONS.FAKSIMILE ]; + + constructor(private pageViewService: PageViewService, private localQueryService: TlnQueryService, protected router: Router, protected activatedRoute: ActivatedRoute ) { + super(router, activatedRoute); + //this.localQueryService.baseUrl = 'http://localhost:3030/nietzsche/query'; + } + ngOnInit() { + this.max_width = screen.availWidth - 400 - this.margin_width; + this.max_height = screen.availHeight - 200; + let tlnQueryService = (this.queryService != null) ? this.queryService : this.localQueryService; + this.dataHandler.addHandler('textVersions', { 'handler': TlnStandaloneTextVersion}); + this.dataHandler.setQueryService(tlnQueryService); + this.dataHandler.start_processing.subscribe( + (started: boolean) =>{ this.updating = true; + }); + this.dataHandler.processing_finished.subscribe( + (finished: boolean) =>{ this.updating = false; + }); + super.ngOnInit(); + this.pageViewService.onClickedLine.subscribe( + (clickedLine: TlnLine) => { + let index = this.selectedLines.indexOf(clickedLine.id) + if (index > -1){ + this.selectedLines.splice(index, 1); + } else { + this.selectedLines.push(clickedLine.id); + } + this.updateParams(); + }); + + } + private addOrRemove(textVersion: TextVersion) { + let index = this.selectedTextVersions.indexOf(textVersion); + if (index == -1){ + this.selectedTextVersions.push(textVersion); + } else { + this.selectedTextVersions.splice(index, 1); + } + this.each_version_height = (this.selectedTextVersions.length > 0) ? this.max_height/this.selectedTextVersions.length : this.max_height; + } + private clearFindText() { + this.findText = ''; + this.updateParams(); + } + private getButtonTitle(textVersion: TextVersion): string { + return (this.selectedTextVersions.indexOf(textVersion) == -1) ? 'Textstelle anzeigen' : 'Textstelle ausblenden'; + } + protected readParams(params: Params){ + let old_genetic_order_iri = this.current_genetic_order_iri + super.readParams(params); + if (this.dataHandler.ready && this.current_genetic_order_iri != null + && (this.textVersions.length == 0 || this.current_genetic_order_iri != old_genetic_order_iri)){ + this.dataHandler.resetData('textVersions') + this.dataHandler.getData('textVersions', this.current_genetic_order_iri); + } + } + private setZoomFactor(newZoomFactor: number){ + if (newZoomFactor > 0){ + this.zoomFactor = Math.round(newZoomFactor*100)/100; + } else { + this.zoomFactor = this.zoomFactor/2 + } + this.updateParams(); + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.css b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.css new file mode 100644 index 0000000..b6948f1 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.css @@ -0,0 +1,3 @@ +.highlight { + text-decoration: underline; +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.html b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.html new file mode 100644 index 0000000..53b5d4e --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.html @@ -0,0 +1,9 @@ + + {{version.title}}, + + {{textUnity.number}}, {{textUnity.startLine}}-{{textUnity.endLine}} + ; + , + + diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.spec.ts b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.spec.ts new file mode 100644 index 0000000..2cbb928 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VersionViewComponent } from './version-view.component'; + +describe('VersionViewComponent', () => { + let component: VersionViewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ VersionViewComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(VersionViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.ts b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.ts new file mode 100644 index 0000000..9d99aec --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-crossref/version-view/version-view.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { TextVersion} from '../../models'; + +@Component({ + selector: 'version-view', + templateUrl: './version-view.component.html', + styleUrls: ['./version-view.component.css'] +}) +export class VersionViewComponent implements OnInit { + @Input() version: TextVersion; + @Input() current_iri: string; + + constructor() { } + + ngOnInit() { + } + +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-edition.module.ts b/nietzsche-beta-app/src/app/tln-edition/tln-edition.module.ts new file mode 100644 index 0000000..601d9b4 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-edition.module.ts @@ -0,0 +1,61 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { HttpClientModule } from "@angular/common/http"; +import { NgModule } from '@angular/core'; +import { MatBottomSheetModule,MatButtonModule,MatCheckboxModule,MatDialogModule,MatExpansionModule,MatFormFieldModule,MatInputModule,MatListModule,MatPaginatorModule,MatRadioModule,MatSelectModule,MatSidenavModule,MatSortModule,MatTableModule,MatToolbarModule,MatButtonToggleModule,MatCardModule,MatIconModule,MatMenuModule,MatTabsModule,MatTooltipModule +} from '@angular/material'; +import { NgxMatStandoffMarkupModule } from '../lib/ngx-mat-standoff-markup.module'; +import { PageViewService } from '../page-view/page-view.service'; +import { PageViewModule } from '../page-view/page-view.module'; +import { TlnPageViewComponent } from './tln-page-view.component'; +import { TlnQueryService } from './tln-query.service'; +import { ToolTipComponent } from './tooltip/tool-tip.component'; +import { TlnViewerNavigation } from './tln-viewer-navigation/tln-viewer-navigation.component'; +import { TlnInformationComponent } from './tln-information/tln-information.component'; +import { TlnCrossrefComponent } from './tln-crossref/tln-crossref.component'; +import { VersionViewComponent } from './tln-crossref/version-view/version-view.component'; +import { PageVersionViewComponent } from './tln-crossref/page-version-view/page-version-view.component'; + + +@NgModule({ + declarations: [TlnPageViewComponent, ToolTipComponent, TlnViewerNavigation, TlnInformationComponent, TlnCrossrefComponent, VersionViewComponent, PageVersionViewComponent], + imports: [ + MatBottomSheetModule, + MatButtonModule, + MatButtonToggleModule, + MatCardModule, + MatCheckboxModule, + MatDialogModule, + MatExpansionModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatListModule, + MatMenuModule, + MatPaginatorModule, + MatRadioModule, + MatSelectModule, + MatSidenavModule, + MatSortModule, + MatTableModule, + MatTabsModule, + MatToolbarModule, + MatTooltipModule, + BrowserModule, + CommonModule, + FormsModule, + NgxMatStandoffMarkupModule, + PageViewModule + ], + exports: [ + TlnPageViewComponent, + TlnViewerNavigation + ], + providers: [ + PageViewService, + TlnQueryService + ], + entryComponents: [TlnInformationComponent] +}) +export class TlnEditionModule { } diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-information/page-information.ts b/nietzsche-beta-app/src/app/tln-edition/tln-information/page-information.ts new file mode 100644 index 0000000..a0c3e48 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-information/page-information.ts @@ -0,0 +1,27 @@ +import { OnInit } from '@angular/core'; +import { TlnQueryServiceInterface, ManuscriptUnity, NavigationPage} from '../models'; +import { DataHandler } from '../data_handler'; +import { TlnTextGeneticOrder} from '../datatypes/text_version'; +import { TLN_PAGE_PARAM } from '../constants'; +import { TlnPositionalStyleMarkup, TlnFilteredPositionalStyleMarkup } from '../datatypes/positional-markup'; + +export class PageInformation implements OnInit { + /** + * the data handler of this component that retrieves + * data and instantiates it according to their proper + * datatypes. + **/ + dataHandler: DataHandler = new DataHandler(this); + geneticOrders: TlnTextGeneticOrder[] = []; + + constructor(private queryService: TlnQueryServiceInterface, public current_page: NavigationPage){ } + + ngOnInit() { + this.dataHandler.addHandler('geneticOrders', { 'handler': TlnTextGeneticOrder}); + this.dataHandler.setQueryService(this.queryService); + this.dataHandler.getData('geneticOrders', this.current_page.id) + } + public hasInformation(): boolean { + return this.geneticOrders.length > 0; + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.css b/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.css new file mode 100644 index 0000000..b6948f1 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.css @@ -0,0 +1,3 @@ +.highlight { + text-decoration: underline; +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.html b/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.html new file mode 100644 index 0000000..c5e595c --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.html @@ -0,0 +1,14 @@ +
+

{{current_page.title}} {{current_page.number}}:

+ +

Querverweise:

+ + + +
+
diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.spec.ts b/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.spec.ts new file mode 100644 index 0000000..2203001 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TlnInformationComponent } from './tln-information.component'; + +describe('TlnInformationComponent', () => { + let component: TlnInformationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TlnInformationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TlnInformationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.ts b/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.ts new file mode 100644 index 0000000..b0a8edc --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-information/tln-information.component.ts @@ -0,0 +1,63 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { Router, ActivatedRoute, Params } from '@angular/router'; +import { MAT_BOTTOM_SHEET_DATA, MatBottomSheetRef} from '@angular/material/bottom-sheet'; +import { TlnQueryServiceInterface, ManuscriptUnity, NavigationPage} from '../models'; +import { TlnQueryService } from '../tln-query.service'; +import { Mapping, RouteReader } from '../route-reader'; +import { DataHandler } from '../data_handler'; +import { TlnTextGeneticOrder} from '../datatypes/text_version'; +import { TLN_PAGE_PARAM } from '../constants'; +import { TlnViewerNavigation } from '../tln-viewer-navigation/tln-viewer-navigation.component'; +import { PositionalStyleMarkup, StyleSheetDef, StandoffMarkupSettings} from '../../lib/ngx-mat-standoff-markup.component'; +import { TlnPositionalStyleMarkup, TlnFilteredPositionalStyleMarkup } from '../datatypes/positional-markup'; + +export interface ParentInformation { + geneticOrders: TlnTextGeneticOrder[]; + page: NavigationPage; + manuscript: ManuscriptUnity; +} + +@Component({ + selector: 'app-tln-information', + templateUrl: './tln-information.component.html', + styleUrls: ['./tln-information.component.css'] +}) +export class TlnInformationComponent implements OnInit { + /** + * IRI of the current page + **/ + current_iri: string; + /** + * the current page + **/ + current_page: NavigationPage; + /** + * the data handler of this component that retrieves + * data and instantiates it according to their proper + * datatypes. + **/ + dataHandler: DataHandler = new DataHandler(this); + geneticOrders: TlnTextGeneticOrder[] = []; + + constructor(@Inject(MAT_BOTTOM_SHEET_DATA) private data: ParentInformation, private bottomSheetRef: MatBottomSheetRef){ + this.current_page = data.page; + this.geneticOrders = data.geneticOrders; + } + + ngOnInit() { + /* + this.dataHandler.addHandler('geneticOrders', { 'handler': TlnTextGeneticOrder}); + this.dataHandler.setQueryService(this.queryService); + this.dataHandler.getData('geneticOrders', this.current_page.id) + */ + } + + openLink(event: MouseEvent): void { + console.log(event) + /*this.bottomSheetRef.dismiss(); + event.preventDefault();*/ + } + test(){ + console.log(this.geneticOrders); + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.css b/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.css new file mode 100644 index 0000000..e69de29 diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.html b/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.html new file mode 100644 index 0000000..cdbeb07 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.html @@ -0,0 +1,13 @@ +
+ + +
diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.spec.ts b/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.spec.ts new file mode 100644 index 0000000..3b3f718 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TlnPageViewComponent } from './tln-page-view.component'; + +describe('TlnPageViewComponent', () => { + let component: TlnPageViewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TlnPageViewComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TlnPageViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + /* + it('should create', () => { + expect(component).toBeTruthy(); + }); + */ +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.ts b/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.ts new file mode 100644 index 0000000..43ddbf3 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-page-view.component.ts @@ -0,0 +1,226 @@ +import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Router, ActivatedRoute, Params } from '@angular/router'; +import { TlnQueryServiceInterface } from './models'; +import { TlnQueryService } from './tln-query.service'; +import { TlnPage} from './datatypes/page'; +import { FaksimileImage } from './datatypes/faksimile_image'; +import { SVGImage, SVGImageClip } from './datatypes/svg_image'; +import { TlnImage } from './datatypes/image'; +import { TlnLine, FaksimileLine } from './datatypes/line'; +import { TlnWord, FaksimileWord } from './datatypes/word'; +import { TlnTextByForeignHand, FaksimileTextByForeignHand } from './datatypes/foreign_text'; +import { externalAssignClass, externalAssignStyle, Configuration, Identifier, Image, Line, Word } from './models'; +import { ConfigurableComponent } from './services'; +import { ComplexKeyIriMapping, DataHandler } from './data_handler'; +import { VIEW_OPTIONS, TLN_FIND_PARAM, TLN_PAGE_PARAM, TLN_SELECTED_LINES_PARAM, TLN_VIEW_OPTION_PARAM, TLN_ZOOM_PARAM } from './constants'; +import { Mapping, RouteReader } from './route-reader'; + +/** + * Given a page IRI, this component will request all relevant information and + * display the data with {@link /components/PageViewComponent.html|PageViewComponent}. + **/ +@Component({ + selector: 'tln-page-view', + templateUrl: './tln-page-view.component.html', + styleUrls: ['./tln-page-view.component.css'] +}) +export class TlnPageViewComponent extends RouteReader implements OnInit, OnChanges { + @Input() configuration: Configuration; + /** + * IRI of the current page, the component will ignore page iri's from params if set. + **/ + @Input('page') current_iri: string; + /** + * the search text of words that should be highlighted as {@link /miscellaneous/enumerations.html#HIGHTLIGHT_CASES|HIGHTLIGHT_CASES.SEARCHED_WORD}. + **/ + @Input() findText: string; + /** + * whether or not to ignore the params + **/ + private ignoreParams: boolean = false; + /** + * OPTIONAL pass a queryService with method + * {@link /interfaces/TlnQueryServiceInterface.html#getData|getData} + * to TlnPageViewComponent. + **/ + @Input() queryService: TlnQueryServiceInterface; + /** + * global zoom factor. + **/ + @Input() zoomFactor: number = 1; + /** + * identifiers of selected words that should be highlighted. + **/ + @Input() selectedWords: Identifier[] = []; + /** + * identifiers of selected lines that should be highlighted. + **/ + @Input() selectedLines: Identifier[] = []; + /** + * the (initial) maximum height of the image. + **/ + @Input() dontShowReference: boolean = false; + @Input() startLine: Identifier; + @Input() endLine: Identifier; + @Input() max_height: number = -1; + @Input() max_width: number = -1; + /** + * should primary Url be used for image. Use secondary Url if false. + **/ + @Input() preferPrimaryUrl: boolean = true; + /** + * selected view option, i.e. one of the following + * {@link /miscellaneous/enumerations.html#VIEW_OPTIONS|VIEW_OPTIONS}. + * */ + @Input() selectedViewOption: string = VIEW_OPTIONS.SYNOPSIS + /** + * 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; + /** + * the data handler of this component that retrieves + * data and instantiates it according to their proper + * datatypes. + **/ + dataHandler: DataHandler = new DataHandler(this); + /** + * texts written by foreign hand + **/ + foreignTexts: TlnTextByForeignHand[] = []; + /** + * the (first) image to be displayed by + * {@link /components/PageViewComponent.html|PageViewComponent}. + **/ + image: Image; + /** + * the Array of lines of the first image that will be displayed by {@link /components/MarginFieldComponent.html|MarginFieldComponent}. + **/ + lines: Line[] = []; + /** + * texts written by foreign hand + **/ + second_foreignTexts: FaksimileTextByForeignHand[] = []; + /** + * the second image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}. + **/ + second_image: Image; + /** + * the Array of lines of the second image that will be displayed by {@link /components/MarginFieldComponent.html|MarginFieldComponent}. + **/ + second_lines: Line[] = []; + /** + * the Array of words of the second image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}. + **/ + second_words: Word[] = []; + /** + * the Array of words of the first image that will be displayed by {@link /components/TextFieldComponent.html|TextFieldComponent}. + **/ + words: Word[] = []; + protected mapping: Mapping = { + findText: { param: TLN_FIND_PARAM, isNumber: false }, + selectedViewOption: { param: TLN_VIEW_OPTION_PARAM, isNumber: false }, + selectedLines: { param: TLN_SELECTED_LINES_PARAM, isNumber: false }, + current_iri: { param: TLN_PAGE_PARAM, isNumber: false }, + zoomFactor: { param: TLN_ZOOM_PARAM, isNumber: true } + } + + /** + * @param localQueryService internal query service that will be used if no external queryService is passed + * to input. + **/ + constructor(private localQueryService: TlnQueryService, protected router: Router, protected activatedRoute: ActivatedRoute){ + super(router, activatedRoute); + } + + ngOnInit() { + this.ignoreParams = (this.current_iri != null); + if (!this.ignoreParams){ + super.ngOnInit(); + } + let tlnQueryService = (this.queryService != null) ? this.queryService : this.localQueryService; + let queryConfiguration: Configuration = { '*': { 'tlnQueryService': tlnQueryService }} + this.configuration = ConfigurableComponent.updateConfiguration(this.configuration, queryConfiguration) + this.dataHandler.addHandler('image', { 'handler': SVGImage }); + this.dataHandler.addHandler('words', { 'handler': TlnWord }); + this.dataHandler.addHandler('foreignTexts', { 'handler': TlnTextByForeignHand}); + this.dataHandler.addHandler('lines', { 'handler': TlnLine }); + this.dataHandler.addHandler('second_image',{ 'handler': FaksimileImage }); + this.dataHandler.addHandler('second_words',{ 'handler': FaksimileWord }); + this.dataHandler.addHandler('second_lines',{ 'handler': FaksimileLine }); + this.dataHandler.addHandler('second_foreignTexts', { 'handler': FaksimileTextByForeignHand}); + this.dataHandler.addHandler('page_content',[ 'image', 'lines', 'words', 'foreignTexts' ]); + this.dataHandler.addHandler('second_page_content', [ 'second_image', 'second_lines', 'second_words', 'second_foreignTexts' ] ); + this.dataHandler.setQueryService(tlnQueryService); + this.updatePageData(); + } + ngOnChanges(change: SimpleChanges) { + if (this.dataHandler.ready + && (change.current_iri != undefined && change.current_iri != null && !change.current_iri.firstChange) + || (change.selectedViewOption != undefined && change.selectedViewOption != null && !change.selectedViewOption.firstChange)) { + this.updatePageData(); + } + } + protected readParams(params: Params){ + if (!this.ignoreParams){ + let old_page_iri = this.current_iri; + let old_selectedViewOption = this.selectedViewOption; + super.readParams(params); + if (this.dataHandler.ready + && (old_page_iri != this.current_iri || old_selectedViewOption != this.selectedViewOption)){ + this.updatePageData(); + } + } + } + /** + * This function updates the page data by setting the handlers for the current + * {@link #selectedViewOption|selectedViewOption} and by retrieving the data. + **/ + private updatePageData(){ + this.dataHandler['image']['handler'] = (this.selectedViewOption != VIEW_OPTIONS.FAKSIMILE && this.selectedViewOption != VIEW_OPTIONS.SYNOPSIS_B) ? SVGImage : FaksimileImage ; + this.dataHandler['words']['handler'] = (this.selectedViewOption != VIEW_OPTIONS.FAKSIMILE && this.selectedViewOption != VIEW_OPTIONS.SYNOPSIS_B) ? TlnWord : FaksimileWord ; + this.dataHandler['lines']['handler'] = (this.selectedViewOption != VIEW_OPTIONS.FAKSIMILE && this.selectedViewOption != VIEW_OPTIONS.SYNOPSIS_B) ? TlnLine : FaksimileLine ; + this.dataHandler['foreignTexts']['handler'] = (this.selectedViewOption != VIEW_OPTIONS.FAKSIMILE && this.selectedViewOption != VIEW_OPTIONS.SYNOPSIS_B) + ? TlnTextByForeignHand : FaksimileTextByForeignHand ; + if (this.dataHandler['page_content'][this.dataHandler['page_content'].length-1] == 'second_page_content'){ + this.dataHandler['page_content'].pop(); + this.second_image = null; + this.second_foreignTexts = []; + this.second_words = []; + this.second_lines = []; + } + if (this.selectedViewOption == VIEW_OPTIONS.SYNOPSIS || this.selectedViewOption == VIEW_OPTIONS.SYNOPSIS_B){ + this.dataHandler['second_image']['handler'] = (this.selectedViewOption == VIEW_OPTIONS.SYNOPSIS_B) ? SVGImage : FaksimileImage ; + this.dataHandler['second_words']['handler'] = (this.selectedViewOption == VIEW_OPTIONS.SYNOPSIS_B) ? TlnWord : FaksimileWord ; + this.dataHandler['second_lines']['handler'] = (this.selectedViewOption == VIEW_OPTIONS.SYNOPSIS_B) ? TlnLine : FaksimileLine ; + this.dataHandler['second_foreignTexts']['handler'] = (this.selectedViewOption == VIEW_OPTIONS.SYNOPSIS_B) ? TlnTextByForeignHand : FaksimileTextByForeignHand ; + this.dataHandler['page_content'].push('second_page_content'); + } + if (this.current_iri != null){ + this.dataHandler.resetData('page_content'); + if (this.startLine != null && this.startLine != undefined){ + this.dataHandler['image']['handler'] = SVGImageClip ; + let endLine = (this.endLine != null && this.endLine != undefined) ? this.endLine : this.startLine; + let complex: ComplexKeyIriMapping = { idIndex: 0, + mapping: [ { key: 'page', iri: this.current_iri}, { key: 'startLine', iri: this.startLine}, {key: 'endLine', iri: endLine} ] + } + this.dataHandler['page_content'] = this.dataHandler['page_content'].filter(key =>key != 'image') + this.dataHandler.getData4Keys('image', complex); + } else if (this.dataHandler['page_content'].indexOf('image') == -1){ + this.dataHandler['page_content'].splice(0, 0, 'image') + } + this.dataHandler.getData('page_content', this.current_iri); + } + } + +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-query.service.ts b/nietzsche-beta-app/src/app/tln-edition/tln-query.service.ts new file mode 100644 index 0000000..c557b66 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-query.service.ts @@ -0,0 +1,38 @@ +import { Injectable, EventEmitter } from '@angular/core'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { TlnQueryServiceInterface } from './models'; +/** + * This is the internal query service + * that communicates with the SPARQL-endpoint. + * */ +@Injectable() +export class TlnQueryService implements TlnQueryServiceInterface { + //baseUrl = 'http://localhost:3030/nietzsche/query'; + baseUrl = 'https://nietzsche.fuseki.services.dasch.swiss/nietzsche' //'http://fuseki.nie-ine.ch/nietzsche/query'; + reset_data = new EventEmitter(); + + constructor(private http: HttpClient) { } + + public resetData(key: string){ + this.reset_data.emit(key); + } + /** + * Gets the data from an endpoint via http post + * + * @param query: The query to run. + * @returns response + */ + public getData(query: string): Observable { + let httpOptions = { + headers: new HttpHeaders( + { 'Content-Type': 'application/sparql-query', + 'Accept': 'application/sparql-results+json; charset=UTF-8'} + ) + }; + return this.http.post(this.baseUrl, query, httpOptions); + } + +} + + diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.css b/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.css new file mode 100644 index 0000000..ef61dda --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.css @@ -0,0 +1,14 @@ +#navi { + width: 100%; + height: 50px; + margin: 0; + padding: 0; + white-space: nowrap; +} +.zoom { + max-width: 10px; +} +.mat-button.min-width { + min-width: 120px; + max-width: 120px; +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.html b/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.html new file mode 100644 index 0000000..c507519 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.html @@ -0,0 +1,46 @@ + + diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.spec.ts b/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.spec.ts new file mode 100644 index 0000000..223011c --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TlnViewerNavigation.ComponentComponent } from './tln-viewer-navigation.component.component'; + +describe('TlnViewerNavigation.ComponentComponent', () => { + let component: TlnViewerNavigation.ComponentComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TlnViewerNavigation.ComponentComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TlnViewerNavigation.ComponentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.ts b/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.ts new file mode 100644 index 0000000..1130c9d --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tln-viewer-navigation/tln-viewer-navigation.component.ts @@ -0,0 +1,152 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Router, ActivatedRoute, Params } from '@angular/router'; +import {MatBottomSheet, MatBottomSheetRef} from '@angular/material/bottom-sheet'; +import { TlnQueryServiceInterface, Reference, ManuscriptUnity, NavigationPage } from '../models'; +import { TlnManuscriptUnity, TlnNavigationPage} from '../datatypes/navigation'; +import { TLN_VIEWER_ROUTE,TLN_FIND_PARAM, TLN_PAGE_PARAM, TLN_MANUSCRIPT_PARAM, + TLN_SELECTED_LINES_PARAM, TLN_VIEW_OPTION_PARAM, TLN_ZOOM_PARAM, VIEW_OPTIONS, ONTOLOTY_PREFIX } from '../constants'; +import { IsReconstructedKonvolut } from '../datatypes/basic_datatype'; +import { TlnLine} from '../datatypes/line'; +import { TlnTextGeneticOrder} from '../datatypes/text_version'; +import { Mapping } from '../route-reader'; +import { RouteUpdater } from '../route-updater'; +import { ComplexKeyIriMapping, DataHandler, KeyIriMapping } from '../data_handler'; +import { PageViewService, TlnQueryService } from '../services'; +import { TlnInformationComponent, ParentInformation } from '../tln-information/tln-information.component'; +import { PageInformation } from '../tln-information/page-information'; + +@Component({ + selector: 'tln-viewer-navigation', + templateUrl: './tln-viewer-navigation.component.html', + styleUrls: ['./tln-viewer-navigation.component.css'] +}) +export class TlnViewerNavigation extends RouteUpdater { + /** + * OPTIONAL pass a queryService with method + * {@link /interfaces/TlnQueryServiceInterface.html#getData|getData} + * to TlnPageViewComponent. + **/ + @Input() queryService: TlnQueryServiceInterface; + zoomFactor: number = 1; + findText: string; + current_iri: string; + current_manuscript_iri: string; + manuscript_unity: ManuscriptUnity; + current_page: NavigationPage; + pageInformation: PageInformation; + previous_page: NavigationPage; + next_page: NavigationPage; + showArchivalManuscriptUnity: boolean = false; + dataHandler: DataHandler = new DataHandler(this); + geneticOrders: TlnTextGeneticOrder[] = []; + selectedLines: string[] = []; + private readonly increment: number = 0.333; + private readonly decrement: number = this.increment*-1; + protected currentRoute: string = TLN_VIEWER_ROUTE; + protected mapping: Mapping = { findText: { param: TLN_FIND_PARAM, isNumber: false }, + current_iri: { param: TLN_PAGE_PARAM, isNumber: false }, + current_manuscript_iri: { param: TLN_MANUSCRIPT_PARAM, isNumber: false }, + selectedViewOption: { param: TLN_VIEW_OPTION_PARAM, isNumber: false }, + selectedLines: { param: TLN_SELECTED_LINES_PARAM, isNumber: false }, + zoomFactor: { param: TLN_ZOOM_PARAM, isNumber: true } } + routerParams: Params; + selectedViewOption: string = VIEW_OPTIONS.TRANSKRIPTION + updating: boolean = false; + viewOptions: string[] = [ VIEW_OPTIONS.TRANSKRIPTION, VIEW_OPTIONS.FAKSIMILE, VIEW_OPTIONS.SYNOPSIS, VIEW_OPTIONS.SYNOPSIS_B ]; + + constructor(private bottomSheet: MatBottomSheet, private pageViewService: PageViewService, private localQueryService: TlnQueryService, protected router: Router, protected activatedRoute: ActivatedRoute ) { + super(router, activatedRoute); + } + ngOnInit() { + let tlnQueryService = (this.queryService != null) ? this.queryService : this.localQueryService; + this.dataHandler.addHandler('manuscript_unity', { 'handler': TlnManuscriptUnity, 'next_key': 'navigation_page'}); + this.dataHandler.addHandler('navigation_page', ['current_page', 'geneticOrders'] ); + this.dataHandler.addHandler('current_page', { 'handler': TlnNavigationPage }); + this.dataHandler.addHandler('geneticOrders', { 'handler': TlnTextGeneticOrder}); + this.dataHandler.setQueryService(tlnQueryService); + this.dataHandler.start_processing.subscribe( + (started: boolean) =>{ this.updating = true; + }); + this.dataHandler.processing_finished.subscribe( + (finished: boolean) =>{ this.updating = false; + }); + super.ngOnInit(); + this.pageViewService.reference.subscribe( + (newReference: Reference) => { + this.updatePageToReference(newReference) + }) + this.pageViewService.onClickedLine.subscribe( + (clickedLine: TlnLine) => { + let index = this.selectedLines.indexOf(clickedLine.id) + if (index > -1){ + this.selectedLines.splice(index, 1); + } else { + this.selectedLines.push(clickedLine.id); + } + this.updateParams(); + }); + } + private clearFindText() { + this.findText = ''; + this.updateParams(); + } + private getPageTitle(page?: NavigationPage, numPages?: number): string { + if (page == null){ + return ''; + } + let indexPrefix = (numPages != null) ? page.index + '/' + numPages : page.index; + return indexPrefix + ': ' + page.title + ' ' + page.number; + } + private getZoomTitle(changeValue: number): string { + if (this.zoomFactor+changeValue < 0){ + return Math.round(this.zoomFactor*50) + '%'; + } + return Math.round((this.zoomFactor+changeValue)*100) + '%'; + } + protected readParams(params: Params){ + super.readParams(params); + if (this.dataHandler.ready && (this.current_page == null || this.current_page.id != this.current_iri)){ + this.dataHandler.resetData('navigation_page') + if(this.current_manuscript_iri != null){ + //this.dataHandler.debug = true; + this.dataHandler.conditionalAddHandler(IsReconstructedKonvolut.getQuery(this.current_manuscript_iri), + 'current_page',{ handler: TlnNavigationPage}, { handler: TlnNavigationPage}); + this.dataHandler.getData('manuscript_unity', this.current_manuscript_iri, this.current_iri); + } else { + this.dataHandler.getData('current_page', this.current_iri); + } + } + } + private setZoomFactor(newZoomFactor: number){ + if (newZoomFactor > 0){ + this.zoomFactor = Math.round(newZoomFactor*100)/100; + } else { + this.zoomFactor = this.zoomFactor/2 + } + this.updateParams(); + } + private setCurrentIri(pageIri: string, manuscriptIir?: string){ + this.dataHandler.stop_processing.emit(true); + this.current_iri = pageIri; + this.updateParams(); + } + private showInformation() { + let parentData: ParentInformation = { + geneticOrders: this.geneticOrders, + page: this.current_page, + manuscript: this.manuscript_unity + } + this.bottomSheet.open(TlnInformationComponent, { + data: parentData + }); + } + private updatePageToReference(reference: Reference){ + this.current_iri = reference.page.id; + this.selectedLines = [ reference.line.id ] + this.updateParams(); + } + public test(iri?: string){ + this.bottomSheet.open(TlnInformationComponent); + //this.dataHandler.isOfType('showArchivalManuscriptUnity', 'http://rdfh.ch/projects/0068#_Mp_XIV', 'http://www.nie.org/ontology/nietzsche#ArchivalManuscriptUnity') + } +} diff --git a/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.css b/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.css new file mode 100644 index 0000000..34e7d0d --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.css @@ -0,0 +1,43 @@ +.tooltip { + position: absolute; + /*color: white; + background-color: black;*/ + border-radius: 10px; + padding: 8px; + font-size: 0.8em; + opacity: 0.65; +} +div .text, .edited_text, .overwritten { + display: block; +} +span { + display: inline; +} +.deleted { + text-decoration: line-through; + text-decoration-color: red; +} +.blackfg { + color: black; +} +.word { + opacity: 0.7; + background-color: black; + color: white; +} +.foreignHand { + opacity: 0.5; + background-color: blue; + color: white; +} +.continuation { + opacity: 1.0; + background-color: black; + color: white; +} +.copyright { + opacity: 1.0; + background-color: white; + color: black; +} + diff --git a/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.html b/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.html new file mode 100644 index 0000000..8a5c0fd --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.html @@ -0,0 +1,49 @@ +
+
+
+
+
{{markup.startIndex}} {{markup.endIndex}} {{markup.cssStyleTag}}
+
+
+
0: {{earlier_version.text}}
+
+ 1:  + {{word.text}} + +
+ >{{word.edited_text}} +
überschreibt: {{overwrittenWord.text}}
+
+
+ {{foreignHand.text}}, {{foreignHand.pen}} +
+
+
+ +
Lizenz: + {{copyright.license}} + {{copyright.license}} +
+
+
+ {{continuation.source.line.number}} nach  + + {{continuation.reference.manuscript.title}} + {{continuation.reference.page.number}}, + {{continuation.reference.line.number}} + +  nach {{continuation.source.line.number}} +
+
+ + + {{ (word.earlier_version) ? '[0:' + word.earlier_version + '|1:' : ''}} + + {{ (word.earlier_version) ? ']' : ''}} + +
+
diff --git a/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.spec.ts b/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.spec.ts new file mode 100644 index 0000000..8e107e5 --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToolTipComponent } from './tool-tip.component'; + +describe('ToolTipComponent', () => { + let component: ToolTipComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ToolTipComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ToolTipComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + /* + it('should create', () => { + expect(component).toBeTruthy(); + }); + */ +}); diff --git a/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.ts b/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.ts new file mode 100644 index 0000000..09418ff --- /dev/null +++ b/nietzsche-beta-app/src/app/tln-edition/tooltip/tool-tip.component.ts @@ -0,0 +1,197 @@ +import { Component, Input, OnInit, OnChanges, ElementRef, ViewChild} from '@angular/core'; +import { ConfigurableComponent, PageViewService, TlnQueryService } from '../services'; +import { Configuration, Continuation, Copyright, Point, PositionalObject, Reference, TextByForeignHand, TlnQueryServiceInterface, Word } from '../models'; +import { TlnEarlierVersionStub, TlnOverwrittenStub } from '../datatypes/earlier_version'; +import { TlnLineReference } from '../datatypes/line_reference'; +import { TlnPreviewWord } from '../datatypes/preview_data'; +import { TlnPositionalStyleMarkup, TlnFilteredPositionalStyleMarkup } from '../datatypes/positional-markup'; +import { DataHandler } from '../data_handler'; +import { PositionalStyleMarkup, StyleSheetDef, StandoffMarkupSettings} from '../../lib/ngx-mat-standoff-markup.component'; + +/** + * This component can be used to show extended tooltips, i.e. with deleted words and distinguished + * words if they are by foreign hand. + **/ +@Component({ + selector: 'tool-tip', + templateUrl: './tool-tip.component.html', + styleUrls: ['./tool-tip.component.css'] +}) +export class ToolTipComponent extends ConfigurableComponent implements OnInit, OnChanges { + @Input() container: HTMLElement; + /** + * content keys + **/ + private readonly contentKeys: string[] = [ 'word', 'foreignHand', 'copyright', 'continuation' ] + /** + * copyright information to be shown in the tooltip. + **/ + copyright: Copyright; + /** + * current key of contentKeys + **/ + currentKey: string; + debug: boolean = false; + /** + * the data handler of this component that retrieves + * data and instantiates it according to their proper + * datatypes. + **/ + dataHandler: DataHandler = new DataHandler(this); + /** + * text by foreign hand to be shown in tooltip + **/ + foreignHand: TextByForeignHand; + /** + * earlier version of word + **/ + earlier_version: TlnEarlierVersionStub; + /** + * overwritten word + **/ + overwrittenWord: TlnOverwrittenStub; + /** + * a line continuation + **/ + continuation: Continuation; + /** + * a list of positional style markups. + **/ + positionalStyleMarkups: PositionalStyleMarkup[] = []; + continuation_words: TlnPreviewWord[] = []; + cssDef: StyleSheetDef = { type: 'text', css: '' } + mySettings = new StandoffMarkupSettings(true, true, false, 1); + width: number = 300; + + + /** + * line reference from + **/ + sourceLineReference: TlnLineReference; + /** + * actual tooltip position + **/ + tooltipPosition: Point = { visible: false, clientX: -1, clientY: -1, layerX: -1, layerY: -1 }; + /** + * whether or not to show extended tooltips + **/ + useExtendedTooltip: boolean = false; + /** + * word to be shown in tooltip + **/ + word: Word; + /** + * y offset for tooltip position relative to mouse position + **/ + private readonly yOffset: number = 30; + constructor(protected pageViewService: PageViewService, private tlnQueryService: TlnQueryService) { + super() + } + ngOnChanges(){ + super.ngOnChanges(); + if (this.tlnQueryService != null && !this.dataHandler.ready){ + this.dataHandler.addHandler('earlier_version', { 'handler': TlnEarlierVersionStub }); + this.dataHandler.addHandler('overwrittenWord', { 'handler': TlnOverwrittenStub}); + this.dataHandler.addHandler('positionalStyleMarkups', { 'handler': TlnFilteredPositionalStyleMarkup }); + this.dataHandler.addHandler('sourceLineReference', { 'handler': TlnLineReference}); + this.dataHandler.addHandler('continuation_words', { 'handler': TlnPreviewWord}); + this.dataHandler.addHandler('wordStubs', ['earlier_version', 'overwrittenWord', 'positionalStyleMarkups' ]); + this.dataHandler.setQueryService(this.tlnQueryService); + this.tlnQueryService.reset_data.subscribe( + (data_key: string) =>{ + if (data_key == 'page_content'){ + this.resetData() + } + }); + } + } + /** + * listen on pageViewService + **/ + ngOnInit() { + this.pageViewService.mousePosition.subscribe( + (newPoint: Point) => this.tooltipPosition = newPoint + ); + this.pageViewService.onHoveredWord.subscribe( + (newWord: Word) => { this.setContent('word', newWord);this.updateEarlierVersion() } + ); + this.pageViewService.offHoveredWord.subscribe( + (newWord: Word) => this.word = null + ); + this.pageViewService.onHoveredTextByForeignHand.subscribe( + (newTextByForeignHand: TextByForeignHand) => { this.setContent('foreignHand',newTextByForeignHand) } + ); + this.pageViewService.offHoveredTextByForeignHand.subscribe( + (newTextByForeignHand: TextByForeignHand) => this.foreignHand = null + ); + this.pageViewService.copyright.subscribe( + (copyright: Copyright) =>{ + this.setContent('copyright', (this.copyright == null) ? copyright : null) + }); + this.pageViewService.onHoveredContinuation.subscribe( + (newContinuation: Continuation) => { + if(this.continuation == null){ + this.updateLineContinuation(newContinuation) + } + }); + this.pageViewService.offHoveredContinuation.subscribe( + (newContinuation: Continuation) => { this.dataHandler.stop_processing.emit(true);this.continuation = null;this.updateLineContinuation(); } + ); + } + /** + * reset data + **/ + private resetData(){ + this.dataHandler.resetData('wordStubs') + this.contentKeys.forEach(key=>this[key] = null); + } + /** + * update earlier version of word + **/ + private updateEarlierVersion(){ + if (this.word != null && this.dataHandler.ready) { + this.dataHandler.resetData('wordStubs') + this.dataHandler.getData('wordStubs', this.word.id); + } + } + private updateLineContinuation(continuation?: Continuation){ + this.setContent('continuation', continuation); + if(continuation != null && this.dataHandler.ready){ + this.dataHandler.resetData('continuation_words') + if (continuation.source.page != null){ + this.dataHandler.getData('continuation_words', this.continuation.reference.line.id) + } + } + } + private getTop(key: string): number { + return (key == 'copyright') ? this.tooltipPosition.layerY : this.tooltipPosition.clientY - this.yOffset; + } + private getLeft(width: number): number { + if (!this.tooltipPosition.visible + || this.continuation == null + || this.continuation.show == null + || this.continuation.show != 'to' + || this.container == null || this.container == undefined ){ + return this.tooltipPosition.clientX; + } + let containerRect: DOMRect = this.container.getBoundingClientRect(); + let left = this.tooltipPosition.clientX; + return (left + width <= containerRect.right) ? left : left-(left+width-containerRect.right)-5; + } + /** + * Set tooltip's content and remove prior content. + * @param key key of content + * @param content new content + **/ + private setContent(key: string, content: Continuation|Copyright|PositionalObject){ + this.contentKeys.forEach(key=>this[key] = null); + this[key] = content; + this.currentKey = key; + } + /** + * whether tooltip has any content (i.e. any content of {@link /components/ToolTipComponent.html#contentKeys|contentKeys}) + **/ + private hasAnyContent(): boolean { + return this.contentKeys.filter(key =>this[key] != null).length > 0 + } +}