diff --git a/package.json b/package.json index f40943d..1fa7be7 100644 --- a/package.json +++ b/package.json @@ -1,64 +1,63 @@ { "name": "svg-test-app", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", "compodoc": "./node_modules/.bin/compodoc -p tsconfig.app.json", "sparqljs": "./node_modules/sparqljs/sparql.js" }, "private": true, "dependencies": { "@angular/animations": "~8.2.14", "@angular/cdk": "~8.2.3", "@angular/common": "~8.2.14", "@angular/compiler": "~8.2.14", "@angular/core": "~8.2.14", "@angular/forms": "~8.2.14", "@angular/material": "^8.2.3", "@angular/platform-browser": "~8.2.14", "@angular/platform-browser-dynamic": "~8.2.14", "@angular/router": "~8.2.14", "@types/jquery": "^3.3.33", "@types/rdf-js": "^2.0.11", "buffer": "^5.5.0", "fast-deep-equal": "^3.1.1", "hammerjs": "^2.0.8", "minimist": "^1.2.5", "n3": "^1.3.5", "ngx-csv": "^0.3.2", "process": "^0.11.10", "rdfjs": "^0.0.1", "rxjs": "~6.4.0", "sparqlalgebrajs": "^2.2.0", "sparqljs": "^3.0.1", "tslib": "^1.10.0", "zone.js": "~0.9.1" }, "devDependencies": { "@angular-devkit/build-angular": "^0.803.25", "@angular/cli": "~8.3.24", "@angular/compiler-cli": "~8.2.14", "@angular/language-service": "~8.2.14", "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", "codelyzer": "^5.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^1.4.0", "protractor": "~5.4.0", - "sparqler": "^0.8.0", "ts-node": "~7.0.0", "tslint": "~5.15.0", "typescript": "~3.5.3" } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 6e31a71..0d46b1d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,27 +1,27 @@ import { Component } from '@angular/core'; -import { DataService } from './services/resources.service'; +import { QueryService } from './services/query.service'; import { InfoService } from './services/info.service'; import { WordService } from './services/field-interaction.service'; import { OptionService } from './services/options.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], - providers: [DataService, WordService, OptionService, InfoService] + providers: [QueryService, WordService, OptionService, InfoService] }) export class AppComponent { title = 'svg-test-app'; image = { height: 973.91998, width: 2038.5601, file_name: "/assets/W-II-1,131et132.jpg" }; text_field = { left: 358.805, top: 78.051, width: 662.761, height: 831.879 }; manuscript = { title: 'W II 1', pages: [ { number: '131', metadata: [ { head: { description: 'Stellenkommentar'}, content: [ { reference: '2', quote: 'Leben Thomas Carlyle’s', text: 'vgl. Froude, Das Leben Thomas Carlyles'} ]}, { head: { description: 'Druckorte' }, content: [ { reference: '24-34', text: 'KGW VIII 9[11]'}, { reference: '34-40', text:'KGW VIII 9[12]'} ]} ] } ] }; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0925dd2..6bc33e9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,77 +1,77 @@ import { AppComponent } from './app.component'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { CdkTableModule } from '@angular/cdk/table'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { FormsModule, ReactiveFormsModule} from '@angular/forms'; import { NgModule } from '@angular/core'; import { MatDialogModule, MatToolbarModule} from '@angular/material'; import { MatExpansionModule } from '@angular/material/expansion'; import {MatIconModule} from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatListModule } from '@angular/material/list'; import {MatMenuModule} from '@angular/material/menu'; import { MatPaginatorModule, MatSortModule, MatSidenavModule, MatCheckboxModule } from '@angular/material'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import {MatTabsModule} from '@angular/material/tabs'; import { DataListViewComponent } from './data-list-view/data-list-view.component'; import { DataListViewTableComponent, HighlightPipe } from './data-list-view/data-list-view-table/data-list-view-table.component'; import { DialogComponent } from './dialog-component/dialog.component'; import { InfoBoxComponent } from './info-box-component/info-box.component'; import { TextFieldComponent} from './textfield-component/textfield.component'; import { WordPositionDirective } from './textfield-component/word-position.directive'; import { TextfieldOptionsComponentComponent } from './textfield-options-component/textfield-options-component.component'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { RdfDataBrowserComponentComponent } from './rdf-data-browser-component/rdf-data-browser-component.component'; -import { DataService } from './services/resources.service'; import { QueryService } from './services/query.service'; import { TextStyleService } from './services/text-style.service'; import { DisplayedCollumnsService } from './data-list-view/data-list-view-services/table-data.service'; +import {DataListViewSettings} from './data-list-view/data-list-view-settings/data-list-view-settings.service'; @NgModule({ declarations: [ AppComponent, TextFieldComponent, WordPositionDirective, InfoBoxComponent, TextfieldOptionsComponentComponent, DataListViewComponent, DataListViewTableComponent, DialogComponent, HighlightPipe, RdfDataBrowserComponentComponent ], imports: [ BrowserModule, BrowserAnimationsModule, CdkTableModule, CommonModule, DragDropModule, HttpClientModule, MatCheckboxModule, MatDialogModule, MatExpansionModule, MatIconModule, MatInputModule, MatListModule, MatMenuModule, MatPaginatorModule, MatSelectModule, MatSidenavModule, MatSortModule, MatTableModule, MatTabsModule, MatToolbarModule, FormsModule, ReactiveFormsModule ], - providers: [DataService, QueryService, DisplayedCollumnsService, TextStyleService ], + providers: [ QueryService, DisplayedCollumnsService, DataListViewSettings, TextStyleService ], bootstrap: [AppComponent], entryComponents: [DialogComponent] }) export class AppModule { } diff --git a/src/app/data-list-view/data-list-view-settings/data-list-view-settings.service.ts b/src/app/data-list-view/data-list-view-settings/data-list-view-settings.service.ts new file mode 100644 index 0000000..858e450 --- /dev/null +++ b/src/app/data-list-view/data-list-view-settings/data-list-view-settings.service.ts @@ -0,0 +1,226 @@ +import {Injectable} from '@angular/core'; +import fallbackSettings from '../assets/settings.json'; +import {CellActions, ExportOptions, SortingOptions, Styles } from './models'; + + +// queryResponse --> table takes an array; query --> table is loading content via a passed SPARQL query string +// sparql --> +@Injectable() +export class Filter { + constructor( private _showFilter: boolean, // show/hide filter input field + private _caseSensitive: boolean // how filter input is validated + ) {} + get showFilter() { + return this._showFilter; + } + set showFilter(val: boolean) { + this._showFilter = val; + } + get caseSensitive() { + return this._caseSensitive; + } + set caseSensitive(val: boolean) { + this._caseSensitive = val; + } +} + +@Injectable() +export class Paginator { + constructor( private _paginate: boolean, // show/hide paginator + private _pageIndex: number, // displayed page on startup/refresh + private _pageSize: number, // offset/number of displayed entries on one page + private _pageSizeOptions: Array // the displayed page numbers to schoose from e.g. "0, 5, 10, 15" + ) {} + + get paginate() { + return this._paginate; + } + set paginate(val: boolean) { + this._paginate = val; + } + get pageIndex() { + return this._pageIndex; + } + set pageIndex(val: number) { + this._pageIndex = val; + } + get pageSize() { + return this._pageSize; + } + set pageSize(val: number) { + this._pageSize = val; + } + get pageSizeOptions() { + return this._pageSizeOptions; + } + set pageSizeOptions(val: Array) { + this._pageSizeOptions = val; + } +} + +@Injectable() +export class ColumnMapping { + constructor( private _name: string, // displayed comlumn name or name for reuse + private _path: Array, // the path within the passed JSON response from endpoint + private _filtered: boolean, // if the entries of that column apply to filter input + private _displayed: boolean, // if the column header is displayed or not + ) {} + + get name() { + return this._name; + } + set name(val: string) { + this._name = val; + } + get path() { + return this._path; + } + set path(val: Array) { + this._path = val; + } + get filtered() { + return this._filtered; + } + set filtered(val: boolean) { + this._filtered = val; + } + get displayed() { + return this._displayed; + } + set displayed(val: boolean) { + this._displayed = val; + } +} + +@Injectable() +export class ColumnDefinition { + constructor( private _genericColumns: boolean, // false if columns are manually defined by column mapping + private _stickyColumn: number, // the first line which should be sticky and not scroll + private _nestedDatasource: boolean, // if a JSON response is nested and should be flattened + private _columnMappings?: Array) {} + + get genericColumns() { + return this._genericColumns; + } + set genericColumns(val: boolean) { + this._genericColumns = val; + } + get stickyColumn() { + return this._stickyColumn; + } + set stickyColumn(val: number) { + this._stickyColumn = val; + } + get nestedDatasource() { + return this._nestedDatasource; + } + set nestedDatasource(val: boolean) { + this._nestedDatasource = val; + } + get columnMappings() { + return this._columnMappings; + } + set columnMappings(val: Array) { + this._columnMappings = val; + } +} + + +@Injectable() +export class DataListViewSettings { + constructor(private _inputMode: string, + private _jsonType: string, + private _collumnDefinition: ColumnDefinition, + private _filter: Filter, + private _paginator: Paginator + /* + private _exportOptions: ExportOptions, + private _sortingOptions: SortingOptions, + private _styles: Styles, + private _cellActions: CellActions, + */ + ) { + } + + get inputMode() { + return this._inputMode; + } + + set inputMode(val: string) { + this._inputMode = val; + } + + get jsonType() { + return this._jsonType; + } + + set jsonType(val: string) { + this._jsonType = val; + } + + get collumnDefinition() { + return this._collumnDefinition; + } + + set collumnDefinition(val: ColumnDefinition) { + this._collumnDefinition = val; + } + get filter() { + return this._filter; + } + + set filter(val: Filter) { + this._filter = val; + } + get paginator() { + return this._paginator; + } + + set paginator(val: Paginator) { + this._paginator = val; + } +/* + get exportOptions() { + return this._exportOptions; + } + + set exportOptions(val: ExportOptions) { + this._exportOptions = val; + } + + get sortingOptions() { + return this._sortingOptions; + } + + set sortingOptions(val: SortingOptions) { + this._sortingOptions = val; + } + + get styles() { + return this._styles; + } + + set styles(val: Styles) { + this._styles = val; + } + + get cellActions() { + return this._cellActions; + } + + set cellActions(val: CellActions) { + this._cellActions = val; + }*/ + + public generateDataListViewSettings(settingsJSON?) { + // console.log(settingsJSON); + let sets: DataListViewSettings; + console.log(sets); + /*if (settingsJSON) { + Object.assign(sets, settingsJSON); + } else { Object.assign(sets, fallbackSettings ); + }*/ + return sets; + } +} + diff --git a/src/app/data-list-view/data-list-view-settings/models.ts b/src/app/data-list-view/data-list-view-settings/models.ts new file mode 100644 index 0000000..7cf9aee --- /dev/null +++ b/src/app/data-list-view/data-list-view-settings/models.ts @@ -0,0 +1,26 @@ + + + + +export interface ExportOptions { + showExport: boolean; +} + +export interface SortingOptions { + disallowSorting: boolean; +} + +export interface Styles { + cellStyle: CellStyle; +} + +export interface CellStyle { + curser: string; +} + +export interface CellActions { + actions: boolean; + actionMode: string; + actionType: string; + actionRange: string; +} diff --git a/src/app/data-list-view/data-list-view-table/data-list-view-table.component.html b/src/app/data-list-view/data-list-view-table/data-list-view-table.component.html index 36c67e0..9977822 100644 --- a/src/app/data-list-view/data-list-view-table/data-list-view-table.component.html +++ b/src/app/data-list-view/data-list-view-table/data-list-view-table.component.html @@ -1,52 +1,52 @@
- Showing {{paginator.length}} of {{dataSource.data.length}} entries in total + Displaying {{getSumOfDisplayedEntries()}} of {{dataSource.data.length}} entries in total
{{col}} - +
Export options Displayed data only Complete export
diff --git a/src/app/data-list-view/data-list-view-table/data-list-view-table.component.ts b/src/app/data-list-view/data-list-view-table/data-list-view-table.component.ts index 639252b..a2e8872 100644 --- a/src/app/data-list-view/data-list-view-table/data-list-view-table.component.ts +++ b/src/app/data-list-view/data-list-view-table/data-list-view-table.component.ts @@ -1,234 +1,243 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { MatPaginator, MatSort, MatTable, MatTableDataSource } from '@angular/material'; import { MatDialog } from '@angular/material'; import { DialogComponent } from '../../dialog-component/dialog.component'; import { PipeTransform, Pipe } from '@angular/core'; import { ngxCsv } from 'ngx-csv/ngx-csv'; import { TextStyleService } from '../../services/text-style.service'; +import { QueryService} from '../../services/query.service'; + +// import { DataListViewSettings } from '../data-list-view-settings/data-list-view-settings.service'; @Component({ selector: 'data-list-view-table', templateUrl: './data-list-view-table.component.html' }) export class DataListViewTableComponent implements OnInit { @Input() dataListSettings: any; @Input() dataToDisplay: any; @Input() displayedColumns?: any; @ViewChild(MatTable, { static: true } ) table: MatTable; @ViewChild(MatPaginator, { static: true } ) paginator: MatPaginator; @ViewChild(MatSort, { static: true } ) sort: MatSort; // cssUrl: string; dataSource: MatTableDataSource ; dataSourceForExport: MatTableDataSource ; // TODO: highlight filter results in table cells by pipe toHighlightByFilter: string = ''; // For highlighting Filter results // Export variables renderedData: any; renderedDisplayedData: any; exportSelection = 'displayed'; UMLAUT_REPLACEMENTS = '{[{ "Ä", "Ae" }, { "Ü", "Ue" }, { "Ö", "Oe" }, { "ä", "ae" }, { "ü", "ue" }, { "ö", "oe" }]}'; - constructor(private dialog: MatDialog, private textStyleService: TextStyleService) { + constructor(private dialog: MatDialog, private textStyleService: TextStyleService, private queryService: QueryService) { } ngOnInit() { // console.log('this.dataToDisplay: ' + this.dataToDisplay ); // console.log('displayed columns:' + this.displayedColumns); this.populateByDatastream(); this.setFilter(); } // // DATA STREAM // private populateByDatastream() { // INSTANTIATE the datasource of the table this.dataSource = new MatTableDataSource(this.dataToDisplay); this.dataSource.connect().subscribe(data => { this.renderedDisplayedData = data; } ); if (this.dataListSettings.paginator.paginate) { this.dataSource.paginator = this.paginator; } // SUBSCRIBE to the tabledata for exporting this rendered data this.dataSourceForExport = new MatTableDataSource(this.dataToDisplay); this.dataSourceForExport.connect().subscribe(data => this.renderedData = data); if (this.dataSource) { if (this.dataListSettings.columns.nestedDatasource) { // IF the dataSource is nested sort must sort the table for subproperties (item.poperty.value) // and not for properties (standard sort). Therefore changing the sortingDataAccessor. this.dataSource.sortingDataAccessor = (item, property) => { switch (property) { default: return item[property].value; }}; } /*else { this.dataSource.sortingDataAccessor = (item, property) => { switch (property) { default: return this.replaceUmlaute(item[property]); }};*/ this.dataSource.sort = this.sort; } // } private getStyle() { return this.textStyleService.setStyles(); } public replaceUmlaute(input) { console.log(input); for (const i of this.UMLAUT_REPLACEMENTS) { console.log(i[0], i[1]); input = input.replace(i[0], i[1]); } // console.log(input); return input; } // FILTERING THE datasource acc to settings private doFilter(value: string) { if (this.dataListSettings.filter.caseSensitive) { this.dataSource.filter = value; // TODO: highlighting this.toHighlightByFilter = value; } else { this.dataSource.filter = value.toLowerCase(); // TODO: highlighting this.toHighlightByFilter = value; } } // // FILTER // private setFilter() { // setting Filter predicate acc. to settings this.dataSource.filterPredicate = (data, filter) => { // console.log("resetting filter predicate for Filter term " + filter); const dataStr: string = this.joinFilteredColumns(data); // applying case sensitivity/insensitivity from settings if (this.dataListSettings.filter.caseSensitive) { return dataStr.indexOf(filter) !== -1; } else { return dataStr.toLowerCase().indexOf(filter) !== -1; } }; } private joinFilteredColumns(data) { let dataStr = ''; if ( this.dataListSettings.columns.genericColumns === false ) { // JOINING all columns to be searched by filter (defined in the settings) together. // NOTE: If the datasource would be nested we have to set filtered data from data to sth like data.[column].value // so the object property value is compared by filtering and not the object itself. for (const column of this.dataListSettings.columns.columnMapping) { if (column.filtered) { dataStr = dataStr + data[column.name]; } } } else {for (const column of this.displayedColumns) { dataStr = dataStr + data[column]; } } return dataStr; } - private onThisClick(val, object) { + private onThisClick(val, index) { // SIMPLE METHOD TO DO SOMETHING WITH THE clicked cell/object like passing it to somewhere if (this.dataListSettings.actions.actions && this.dataListSettings.actions.actionMode === 'object') { if (this.dataListSettings.actions.actionType === 'dialog') { + const shrunkTitle = this.queryService.shrink_iri(val); console.log('opening detail dialog with object with property value ' + val); - this.openDetailsDialog(val); + this.openDetailsDialog(val, shrunkTitle); } else { console.log('actions disabled or no action defined'); } } } // TODO: maybe outsource this - private openDetailsDialog(msg) { + private openDetailsDialog(msg, shrunkTitle) { this.dialog.open(DialogComponent, { data: { - message: msg, buttonText: {cancel: 'close' - } + message: msg, buttonText: {cancel: 'close'}, title: shrunkTitle }, }); } // TODO: maybe implement features from events by hostlistener ... /* @HostListener('click', ['$event']) onClick(event) { if (this.dataListSettings.actions.actions && this.dataListSettings.actions.actionMode === 'host' && event.target.parentElement.classList[0] === 'fuuws') { // HERE THINGS CAN BE ADDED § console.log('opening detail dialog with ' + event.target.firstChild.data ); console.log( event.target ); this.openDetailsDialog(event.target.firstChild.data); } // else {console.log('actions on cells disabled or no action defined')} }*/ // // EXPORT TO CSV public exportToCsv() { var options = { fieldSeparator: ',', quoteStrings: '"', decimalseparator: '.', showLabels: true, showTitle: true, title: 'data export', useBom: true, noDownload: false, headers: this.displayedColumns }; let exportData = this.getExportData(); new ngxCsv(exportData, options.title, options); } public getExportData() { if (this.exportSelection === 'displayed') { return this.renderedDisplayedData; } else { return this.renderedData; } } /*public flatten(data) { // FLATTENS the data so the actual values of the nested objects are exported - not whole objects. let flattenedData = []; for (let obj in data) { let flattenobject = []; if (obj < data.length) { for (let property in data[obj]) { const prop = data[obj][property].value; flattenobject.push(prop); } flattenedData.push(flattenobject); } } return flattenedData; }*/ // // Display / Design stuff // private isColumnSticky(column: number): boolean { // Returns for each column whether/which column should be sticky when scrolling horizontally // (this.dataListSettings.columns.stickyColumn ? true : false) return !!this.dataListSettings.columns.stickyColumn; } + getSumOfDisplayedEntries() { + if (this.paginator.pageSize > this.dataSource.data.length) { + return this.dataSource.data.length; + } else { return this.paginator.pageSize; } + } } // TODO: highlighting filter results in cells by pipe @Pipe({ name: 'highlight' }) export class HighlightPipe implements PipeTransform { transform(text: string, search): string { const pattern = search .replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") .split(' ') .filter(t => t.length > 0) .join('|'); const regex = new RegExp(pattern, 'gi'); return search ? text.replace(regex, match => `${match}`) : text; } + } diff --git a/src/app/data-list-view/data-list-view.component.ts b/src/app/data-list-view/data-list-view.component.ts index 2e02376..b0fc524 100644 --- a/src/app/data-list-view/data-list-view.component.ts +++ b/src/app/data-list-view/data-list-view.component.ts @@ -1,136 +1,138 @@ import { Component, Input, Output, OnInit, OnChanges } from '@angular/core'; -import { DataService } from '../services/resources.service'; +import { QueryService } from '../services/query.service'; import { DisplayedCollumnsService } from './data-list-view-services/table-data.service'; -import fallbackSettings from './assets/settings.json'; +// import { DataListViewSettings } from './data-list-view-settings/data-list-view-settings.service'; @Component({ selector: 'app-data-list-view', templateUrl: './data-list-view.component.html' }) export class DataListViewComponent implements OnChanges { @Input() dataToDisplay?: Array; - @Input() settings?: any; + @Input() settings?: any; // DataListViewSettings; @Input() query?: string; @Output() displayedColumns: Array = []; @Output() dataListSettings: any; @Output() resData: any; tableData: Array = []; - constructor(private dataService: DataService, - private displayedCollumnsService: DisplayedCollumnsService) { + constructor(private dataService: QueryService, + private displayedCollumnsService: DisplayedCollumnsService, + // private datalistViewSettings: DataListViewSettings + ) { } ngOnChanges() { this.getSettings(); this.onGetData(); } generateTableData(responseData: Array, depth: number) { if (this.dataListSettings.columns.genericColumns) { let length = 0; for (const entry of responseData) { this.flattenObjects(entry, length); length += 1; } this.resData = this.tableData; // console.log(this.resData); } else { let length = -1; for (const responseEntry of responseData) { length += 1; this.appendEntryToTabledata(responseEntry, 0, length); } } } appendEntryToTabledata(ResponseEntry: any, depth: number, length: number, pathCompare?: Array) { // recursive method for getting the actual values from nested jsons // and appending them to the tabledata. Allowed values are strings, // numbers, symbols and booleans (so no objects allowed here). for (const column of this.dataListSettings.columns.columnMapping) { if (typeof ResponseEntry[column.path[depth]] === 'string' || typeof ResponseEntry[column.path[depth]] === 'number' || typeof ResponseEntry[column.path[depth]] === 'symbol' || typeof ResponseEntry[column.path[depth]] === 'boolean') { // checks if the path of a recursive function call is the same as the column // generated by the for loop, this is necessary if more than one column has in the same depths the same segment names if (pathCompare === undefined || pathCompare === column.path) { this.append(ResponseEntry[column.path[depth]], column.name, length); } } else if (typeof ResponseEntry[column.path[depth]] === 'object') { this.appendEntryToTabledata(ResponseEntry[column.path[depth]], depth + 1, length, column.path); } } } append(entry: string, name: string, length: number) { // is appending the collected values to the tabledata if (this.tableData[length] === undefined) { this.tableData[length] = {}; } this.tableData[length][name] = entry; if (this.tableData.length === this.dataToDisplay.length) { this.resData = this.tableData; } } flattenObjects(input, length, reference?, output?) { // FLATTENS the response completely and assigns the result to tableData. output = output || {}; for (let key of Object.keys(input)) { const value = input[key]; if (reference) { key = reference + '.' + key; } if (typeof value === 'object' && value !== null) { this.flattenObjects(value, length, key, output); } else { output[key] = value; // renesting with name and types // output[key]['value'] = value; // if (reference['type'] || reference['whatever']) { // output[key]['type'] = reference['type']; // } } } this.tableData[length] = output; } // GET the data - either by a passed input from another app/service or by a passed query private onGetData() { if ( this.dataListSettings.inputMode === 'query' ) { console.log('getting data by running a SPARQL query.'); - // console.log('this.dataListSettings): ' + this.dataListSettings) ; + console.log('this.dataListSettings): ' + this.dataListSettings) ; this.getTableDataFromQuery(this.query); } else if ( this.dataListSettings.inputMode === 'input') { console.log('getting data by input.'); if (this.dataToDisplay ) { this.generateTableData(this.dataToDisplay, 0); - this.displayedColumns = this.displayedCollumnsService.getDisplayedColumns(this.dataListSettings, this.dataToDisplay); + // this.displayedColumns = this.displayedCollumnsService.getDisplayedColumns(this.dataListSettings, this.dataToDisplay); } else { console.log('No dataToDisplay passed by input as defined in dataListSettings.inputMode: ' + this.dataListSettings.inputMode); } } else { console.log('Wrong settings definition for --> \"inputmode: ' + this.dataListSettings.inputMode + '\" allowed are: input, query'); } } private getTableDataFromQuery(query) { this.dataService.getData( this.query ).subscribe(data => { const responseData: any = data; this.dataToDisplay = responseData.results.bindings; this.generateTableData(this.dataToDisplay, 0); this.displayedColumns = this.displayedCollumnsService.getDisplayedColumns(this.dataListSettings, this.dataToDisplay); }); } private getSettings() { if ( this.settings ) { this.dataListSettings = this.settings; - console.log('Got settings by input.'); + console.log('Got settings by input: ', this.dataListSettings); } else { - console.log('No settings input. Loading default settings.'); - this.dataListSettings = fallbackSettings; + console.log('No settings input. Loading fallback settings.'); + // this.dataListSettings = this.datalistViewSettings.generateDataListViewSettings(); } } } diff --git a/src/app/dialog-component/dialog.component.ts b/src/app/dialog-component/dialog.component.ts index 2e880d8..dc9a9c6 100644 --- a/src/app/dialog-component/dialog.component.ts +++ b/src/app/dialog-component/dialog.component.ts @@ -1,38 +1,39 @@ import {Component, Inject, Injectable} from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; -import {DataService} from '../services/resources.service'; +import {QueryService} from '../services/query.service'; @Component({ selector: 'dialog-component', templateUrl: './dialog.component.html', styleUrls: ['./dialog.component.css'] }) @Injectable() export class DialogComponent { // DEFAULT values in case there is no initialContent or button text passed from app initialContent = 'loading resource'; cancelButtonText = 'close'; title: string; constructor( @Inject(MAT_DIALOG_DATA) private data: any, - private dialogRef: MatDialogRef, private dataService: DataService ) { + private dialogRef: MatDialogRef, + private dataService: QueryService ) { this.dialogRef.updateSize('1200vw', '800vw'); if (this.dataService.shrink_iri(data.message)) { this.title = this.dataService.shrink_iri(data.message); - } else { this.title = data.message; } - // + } else { this.title = data.message; } // in case there is no data/initialContent passed the default this.initialContent will be displayed- Same for button text. // if (data) { this.initialContent = data.message || this.initialContent; + this.title = data.title; if (data.buttonText) { this.cancelButtonText = data.buttonText.cancel || this.cancelButtonText; } } } onConfirmClick(): void { this.dialogRef.close(true); } } diff --git a/src/app/info-box-component/info-box.component.html b/src/app/info-box-component/info-box.component.html index d39e21a..1a002e9 100644 --- a/src/app/info-box-component/info-box.component.html +++ b/src/app/info-box-component/info-box.component.html @@ -1,49 +1,49 @@
{{word.text}} + (click)="openInBrowser(word, this.queryService.shrink_iri(word.text))">open resource in data browser
{{manuscript.title}}
{{word.text}}
{{word.id}}
Informationen zu Seite {{page.number}} {{metadata.head.description}}
{{content.reference}}:
{{content.quote}}]
{{content.text}}
diff --git a/src/app/info-box-component/info-box.component.ts b/src/app/info-box-component/info-box.component.ts index a2eed62..42cd65c 100644 --- a/src/app/info-box-component/info-box.component.ts +++ b/src/app/info-box-component/info-box.component.ts @@ -1,72 +1,73 @@ import {Component, Input, OnInit, Output} from '@angular/core'; import { MatExpansionModule } from '@angular/material/expansion'; import { Subscription } from 'rxjs'; import { WordService} from '../services/field-interaction.service'; import { Word } from '../models/models'; import { QueryService } from '../services/query.service'; -import { DataService } from '../services/resources.service'; -import settings from '../../assets/infobox_datalist_settings.json'; import {DialogComponent} from '../dialog-component/dialog.component'; import {MatDialog} from '@angular/material'; @Component({ selector: 'app-info-box', templateUrl: './info-box.component.html', styleUrls: ['./info-box.component.css'] }) export class InfoBoxComponent implements OnInit { @Input() manuscript: any; @Output() queryResponse: any; - @Output() dataListSettings = settings; query: string; word: Word; subscription: Subscription; showInfo: boolean = false; expansion: boolean = true; constructor(private infoService: WordService, private matExpansionModule: MatExpansionModule, private queryService: QueryService, - private dataService: DataService, private dialog: MatDialog ) { this.subscription = infoService.wordChange$.subscribe( word => { if (this.word === word) { this.toggleShowInfo(); this.word = null; } else { this.word = word; this.updateRDFData(word); if (!this.showInfo) { this.toggleShowInfo(); } } }); } ngOnInit() { } private toggleShowInfo() { this.showInfo = !this.showInfo; } public updateRDFData(word) { - this.query = this.queryService.assemble_resource_data( encodeURI('http://rdfh.ch/projects/0068#_W_II_1_Page131_Word206'), 'subject'); - this.dataService.getData(this.query).subscribe(data => { + this.query = this.queryService.getQueryforResourceData( encodeURI('http://rdfh.ch/projects/0068#_W_II_1_Page131_Word206'), 'subject'); + this.queryService.getData(this.query).subscribe(data => { this.queryResponse = data; - console.log('response for new ' + word + ': ' + data); + console.log('response for ' + word + ': ', data); }); + const myRDF = this.queryService.getAnnotationMarkup(word); + console.log('myRDF: ', myRDF); + + let fuu = this.queryService.parseQueryFromFile('annotationMarkup.rq'); + console.log('fuu: ', fuu); } - private openInBrowser(word) { + private openInBrowser(word, shrunkTitle) { + // TODO: Change this to actual iri word = encodeURI('http://rdfh.ch/projects/0068#_W_II_1_Page131_Word206'); this.dialog.open(DialogComponent, { data: { - message: word, buttonText: {cancel: 'close' - } + message: word, buttonText: {cancel: 'close'}, title: shrunkTitle }, }); } } diff --git a/src/app/rdf-data-browser-component/rdf-data-browser-component.component.html b/src/app/rdf-data-browser-component/rdf-data-browser-component.component.html index 96d6d7a..6e0c9cc 100644 --- a/src/app/rdf-data-browser-component/rdf-data-browser-component.component.html +++ b/src/app/rdf-data-browser-component/rdf-data-browser-component.component.html @@ -1,15 +1,15 @@

browse resource as ...

- + - + - +
diff --git a/src/app/rdf-data-browser-component/rdf-data-browser-component.component.ts b/src/app/rdf-data-browser-component/rdf-data-browser-component.component.ts index 1c2ce0e..15cf448 100644 --- a/src/app/rdf-data-browser-component/rdf-data-browser-component.component.ts +++ b/src/app/rdf-data-browser-component/rdf-data-browser-component.component.ts @@ -1,83 +1,46 @@ -import {Component, Input, OnInit, Output} from '@angular/core'; +import {Component, Input, OnChanges, OnInit, Output} from '@angular/core'; import {QueryService} from '../services/query.service'; -import { DataService } from '../services/resources.service'; +import rdfDataBrowserSettings from './rdf-data-browser-settings.json'; @Component({ selector: 'app-rdf-data-browser-component', templateUrl: './rdf-data-browser-component.component.html', styleUrls: ['./rdf-data-browser-component.component.scss'] }) export class RdfDataBrowserComponentComponent implements OnInit { - @Input() resourceOfInterest?: string; - @Output() queryResponse: any; - queryForSubject: string; - @Output() dataListSettings = { - "inputMode": "query", - "jsonType": "sparql", - "columns": { - "genericColumns": false, - "columnMapping" : [ - { - name: 'subject', - path: ["s", "value" ], - displayed: false, - filtered: false - }, - { - name: 'predicate', - path: [ "p", "value" ], - displayed: true, - filtered: true - }, - { - name: 'object', - path: [ "o", "value" ], - displayed: true, - filtered: true - }], - "stickyColumn": 0, - "nestedDatasource": false - }, - "filter": { - "showFilter": true, - "caseSensitive": true - }, - "paginator": { - "paginate": true, - "pageIndex": "0", - "pageSize": "25", - "pageSizeOptions": [5, 10, 25, 50, 100, 250] - }, - "export": { - "showExport": true - }, - "sort": { - "disallowSorting": false - }, - "styles": { - "cellStyle": { - "cursor": "pointer" - } - }, - "actions": { - "actions": true, - "actionMode": "object", - "actionType": "dialog", - "actionRange": "cell" - } - }; + @Input() resourceOfInterest?: string; + @Output() subjectQuery: string; + @Output() subjectSettings; + @Output() predicateQuery: string; + @Output() predicateSettings; - constructor( private queryservice: QueryService, private dataService: DataService ) { } + @Output() objectQuery: string; + @Output() objectSettings; + constructor( private queryservice: QueryService ) { + } ngOnInit() { if ( this.resourceOfInterest ) { - this.queryForSubject = this.queryservice.assemble_resource_data(encodeURI(this.resourceOfInterest), 'subject'); + this.subjectSettings = this.getRdfDataSettings(0); + this.subjectQuery = this.queryservice.getQueryforResourceData(encodeURI(this.resourceOfInterest), 'subject'); + + this.predicateSettings = this.getRdfDataSettings(1); + this.predicateQuery = this.queryservice.getQueryforResourceData(encodeURI(this.resourceOfInterest), 'predicate'); + + this.objectSettings = this.getRdfDataSettings(2); + this.objectQuery = this.queryservice.getQueryforResourceData(encodeURI(this.resourceOfInterest), 'object'); - this.dataService.getData( this.queryForSubject ).subscribe(data => { - this.queryResponse = data; - console.log('response: ' + data); }); } else { console.log('No resource of interest passed.'); } } + + private getRdfDataSettings(type: number) { + // must parse stringify hence each call should use it's own new JSON and not the changed one from last call + const settings = JSON.parse(JSON.stringify(rdfDataBrowserSettings)); + settings.columns.columnMapping[type].displayed = false; + settings.columns.columnMapping[type].filtered = false; + return settings; + } + } diff --git a/src/assets/infobox_datalist_settings.json b/src/app/rdf-data-browser-component/rdf-data-browser-settings.json similarity index 62% rename from src/assets/infobox_datalist_settings.json rename to src/app/rdf-data-browser-component/rdf-data-browser-settings.json index 1543853..359dfbb 100644 --- a/src/assets/infobox_datalist_settings.json +++ b/src/app/rdf-data-browser-component/rdf-data-browser-settings.json @@ -1,59 +1,55 @@ { - "inputMode": "input", + "inputMode": "query", "jsonType": "sparql", "columns": { "genericColumns": false, "columnMapping" : [ + { + "name": "subject", + "path": ["s", "value" ], + "displayed": true, + "filtered": true + }, { "name": "predicate", "path": [ "p", "value" ], "displayed": true, "filtered": true }, { "name": "object", "path": [ "o", "value" ], "displayed": true, "filtered": true }], "stickyColumn": 0, "nestedDatasource": false }, "filter": { - "showFilter": false, + "showFilter": true, "caseSensitive": true }, "paginator": { "paginate": true, "pageIndex": "0", - "pageSize": "5", + "pageSize": "25", "pageSizeOptions": [5, 10, 25, 50, 100, 250] }, "export": { - "showExport": false + "showExport": true }, "sort": { - "disallowSorting": true + "disallowSorting": false }, "styles": { "cellStyle": { "cursor": "pointer" } }, "actions": { "actions": true, - "_comment": { - "cursorstyle": "use custom css properties", - "intLink": "opens another app", - "extLink": "opens another webpage" - }, "actionMode": "object", "actionType": "dialog", - "actionRange": "cell", - "baseUrl": "http://localhost:4200/page?actionID=5c8a6300b4438759d237b246", - "urlParams": { - "label": "label.value", - "highlight": "authorsname.value" - } + "actionRange": "cell" } } diff --git a/src/app/services/query.service.ts b/src/app/services/query.service.ts index d37220f..ecd9d05 100644 --- a/src/app/services/query.service.ts +++ b/src/app/services/query.service.ts @@ -1,67 +1,148 @@ import { Injectable } from '@angular/core'; -import {Algebra, toSparql, translate} from 'sparqlalgebrajs'; +import { Parser, Generator, Wildcard } from 'sparqljs'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {Word} from '../models/models'; -@Injectable() +@Injectable() export class QueryService { - query: string; - select = 'SELECT ?p ?o '; - whereSubject: string; - whereObject: string; - wherePredicate: string; - Optional = ''; - queryLimit = ' LIMIT 200'; - - constructor() { + constructor(private http: HttpClient ) { } - public assemble_resource_data2(resourceValue: string, resourceOwlClass?: string) { - let sparqlAlgebra: Algebra.Operation; - sparqlAlgebra = { type: 'project', - input: { type: 'bgp', patterns: [ - { type: 'pattern', subject: '?x', predicate: '?y', object: '?z' } ] }, - variables: [ '?x', '?y', '?z' ] }; + parser = new Parser(); + sparqlGenerator = new Generator({}); + + // prefixes and namespaces for shrinking iri's to the enduser + namespaces = { + data: 'http://rdfh.ch/projects/0068#', + owl: 'http://www.w3.org/2002/07/owl#', + rdfs: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + tln: 'http://www.nie.org/ontology/nietzsche#', + }; + + baseUrl = 'http://localhost:3030/nietzsche-rw/query'; + + /** + * Gets the data from an endpoint via http post + * + * @param query: The query to run. + * @returns the response. + */ + public getData(query: string, queryType?: string ) { + + let httpOptions; + if (queryType === 'CONSTRUCT') { + // A construct does contain a text as response, not a json, so responseType must be 'text' to avoid parse errors + httpOptions = { headers: new HttpHeaders({'Content-Type': 'application/sparql-query', 'Accept': 'text/turtle'}), + responseType: 'text'}; + return this.http.post(this.baseUrl, query, httpOptions); + } else { httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/sparql-query', + 'Accept': 'application/sparql-results+json; charset=UTF-8'})}; + } + + if (this.baseUrl && httpOptions.headers && (query === undefined || query === null || query === '') ) { + console.log('fallback to static query as there is no query passed'); + this.getQueryfromFilename('fallbackQuery.rq' ).subscribe(fallbackQuery => { + console.log('fallbackQuery: ', fallbackQuery); + return this.http.post(this.baseUrl, query, httpOptions); + }); + } else { return this.http.post(this.baseUrl, query, httpOptions); } } - public assemble_resource_data(resourceValue: string, resourceOwlClass?: string) { - switch (resourceOwlClass) { - case 'subject': { - this.whereSubject = ' <' + decodeURI(resourceValue) + '> '; - this.wherePredicate = ' ?p '; - this.whereObject = ' ?o '; - break; - } - case 'object': { - this.whereObject = ' <' + decodeURI(resourceValue) + '> '; - this.wherePredicate = ' ?p '; - this.whereSubject = ' ?s '; - break; + /** + * Shrinks an iri according to the defined prefixes/namespaces. + * + * @param iri The iri to be shrunken. + * @returns shrunkIri: the shrunken iri if it can be shrunken. + */ + public shrink_iri(iri) { + let shrunkIri: string; + Object.keys(this.namespaces).forEach((ns, index) => { + // console.log(this.namespaces[ns] + ' key ' + ns ) + if (iri.includes(this.namespaces[ns])) { + shrunkIri = iri.replace(this.namespaces[ns], ns + ':' ); } + }); + + if (shrunkIri) { return shrunkIri; } else {return iri; } + } + + /** + * Gets all the properties and connected resources of one thing. Gets it depending on its type or role: + * if the thing is a property, it gets all connected ?s and ?o; + * if it's usage as subject is of interest, it gets all ?p and ?o; + * if it's usage as object is of interest, it gets all ?s ?p; + * + * @param iri: The iri of the selected resource + * @param resourceType: the type of usage i.e. as ?s, ?p or ?o + * @returns the query for the resource. + */ + public getQueryforResourceData(iri: string, resourceType?: string) { + const parsedQuery = this.parser.parse('SELECT ?s ?p ?o WHERE { ?s ?p ?o }'); + // reset the subject iri to the word's iri we like to query for + const resource = {'termType': 'NamedNode', 'value': decodeURI(iri) }; + switch (resourceType) { + case 'subject': { + parsedQuery.where[0].triples[0].subject = resource; + break; } case 'predicate': { - this.wherePredicate = ' <' + decodeURI(resourceValue) + '> '; - this.whereObject = ' ?o '; - this.whereSubject = ' ?s '; - break; - } + parsedQuery.where[0].triples[0].predicate = resource; + break; } + case 'object': { + parsedQuery.where[0].triples[0].object = resource; + break; } } - this.query = this.select + 'WHERE { ' + this.whereSubject + this.wherePredicate + this.whereObject + this.Optional + ' } ' + this.queryLimit; - - return this.query; + // generate the new query string + return this.sparqlGenerator.stringify(parsedQuery); } - public sparqlToJSON(sparqlString?: string) { - - if (!sparqlString ) { - sparqlString = 'SELECT * WHERE { ?x ?y ?z }'; - } + /** + * Gets a text file by its name from the directory assets/queries. + * + * @param filename The name of the file + file name extension. + * @returns the text of the file. + */ + private getQueryfromFilename(filename) { + return this.http.get('../assets/queries/' + filename, {responseType: 'text'}); + } - let sparqlAlgebra: Algebra.Operation; - sparqlAlgebra = translate(sparqlString); - console.log('sparqlAlgebra: ' + sparqlAlgebra); + /** + * Gets a query string from a given file in the directory assets/queries. + * + * @param filename The name of the file + file name extension. + * @returns The JSON equivalence of the parsed query. + */ + public parseQueryFromFile(filename) { + return this.getQueryfromFilename(filename ) + .subscribe(query => { + this.parser.parse(query); + } ); + } - const q = toSparql(sparqlAlgebra); - console.log('my query: ' + q); - return sparqlAlgebra; + /** + * Gets the Markup definitions of a word by redefining a query and running it. + * + * @param word: the word for which the markup information should be queried. + * @returns Observable of the resulting json response containing all markups. + */ + public getAnnotationMarkup(word: Word) { + // get the pre made query + return this.getQueryfromFilename('annotationMarkup.rq' ) + .subscribe(query => { + // parse it to sparqljs object. + const parsedQuery = this.parser.parse(query); + // reset the subject iri to the word's iri we like to query for + // TODO: Change static to the passed word.iri + parsedQuery.where[0].patterns[0].triples[0].subject.id = decodeURI('http://rdfh.ch/resources#word'); + // generate the new query string + const generatedQuery = this.sparqlGenerator.stringify(parsedQuery); + // get and return the data from the endpoint + return this.getData(generatedQuery, 'CONSTRUCT').subscribe(data => { + console.log('got markup/standoff for new ', word , ': ', data); + }); + } ); } } + + diff --git a/src/app/services/resources.service.ts b/src/app/services/resources.service.ts deleted file mode 100644 index 3a258ca..0000000 --- a/src/app/services/resources.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import namespaces from '../../assets/namespaces.json'; -import {QueryService} from './query.service'; - -@Injectable() - -export class DataService { - constructor(private http: HttpClient, private queryService: QueryService) { - } - - readonly httpOptions = { - headers: new HttpHeaders({ - 'Content-Type': 'application/sparql-query', - 'Accept': 'application/sparql-results+json; charset=UTF-8' - }) - }; - - baseUrl = 'http://localhost:3030/nietzsche/query'; - prefix = 'http://www.nie.org/ontology/nietzsche#'; - prefix2 = 'http://rdfh.ch/projects/0068#'; - fallBackQuery = 'prefix tln: <' + this.prefix + '> prefix data: <' + this.prefix2 + '> ' + ' SELECT ?s ?p ?o WHERE' + - ' { ?s a tln:Word . ?s tln:hasText ?o ' + - ' OPTIONAL { ?s ?p ?o } ' + - ' } LIMIT 50'; - - - public getData(query: string) { - if (this.baseUrl && query && this.httpOptions.headers) { - return this.http.post(this.baseUrl, query, this.httpOptions); - } - if (this.baseUrl && this.httpOptions.headers && (query === undefined || query === null || query === '') ) { - console.log('fallback to static query as there is no query passed: ' + this.fallBackQuery); - const fuu = this.queryService.sparqlToJSON(this.fallBackQuery); - console.log(fuu); - return this.http.post(this.baseUrl, this.fallBackQuery, this.httpOptions); - } - } - - public shrink_iri(iri) { - let shrunkIri: string; - Object.keys(namespaces).forEach((ns, index) => { - // console.log(this.namespaces[ns] + ' key ' + ns ) - if (iri.includes(namespaces[ns])) { - shrunkIri = iri.replace(namespaces[ns], ns + ':' ); - } - }); - if (shrunkIri) { return shrunkIri; } - } -} diff --git a/src/assets/queries/annotationMarkup.rq b/src/assets/queries/annotationMarkup.rq new file mode 100644 index 0000000..7080db7 --- /dev/null +++ b/src/assets/queries/annotationMarkup.rq @@ -0,0 +1,7 @@ +prefix tln: +prefix data: + +CONSTRUCT { ?markup ?p ?o } + WHERE { { data:wordx tln:wordHasStandoffTag ?markup } UNION { data:word tln:wordHasStyle ?markup } + OPTIONAL { ?markup ?p ?o } +} diff --git a/src/assets/queries/fallbackQuery.rq b/src/assets/queries/fallbackQuery.rq new file mode 100644 index 0000000..3e608a3 --- /dev/null +++ b/src/assets/queries/fallbackQuery.rq @@ -0,0 +1,7 @@ +prefix tln: +prefix data: + +SELECT ?s ?p ?o WHERE + { ?s a tln:Word . ?s tln:hasText ?o + OPTIONAL { ?s ?p ?o } + } LIMIT 50 diff --git a/tsconfig.json b/tsconfig.json index 0c0577e..e0f169c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,28 +1,29 @@ { "compileOnSave": false, "compilerOptions": { + "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "esModuleInterop": true, "baseUrl": "./", "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "module": "esnext", "moduleResolution": "node", "importHelpers": true, "target": "es2015", "typeRoots": [ "node_modules/@types" ], "lib": [ "es2018", "dom" ] }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, "strictInjectionParameters": true } }