diff --git a/package.json b/package.json index ca4fd9bfed7c008c897935272ac7c59bbeb42c3b..606c5a8660664fd4388fe152db45482345e010d7 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "license": "ISC", "devDependencies": { "@angular/animations": "^7.2.15", + "@angular/cdk": "^7.3.7", "@angular/common": "^7.2.15", "@angular/compiler": "^7.2.15", "@angular/compiler-cli": "^7.2.15", @@ -31,8 +32,10 @@ "@angular/forms": "^7.2.15", "@angular/http": "^7.2.15", "@angular/language-service": "^7.2.15", + "@angular/material": "^7.3.7", "@angular/platform-browser": "^7.2.15", "@angular/platform-browser-dynamic": "^7.2.15", + "@angular/router": "^7.2.15", "@ngrx/effects": "^7.4.0", "@ngrx/store": "^6.0.1", "@ngtools/webpack": "^6.0.5", @@ -46,6 +49,7 @@ "codelyzer": "^5.0.1", "core-js": "^3.0.1", "file-loader": "^1.1.11", + "hammerjs": "^2.0.8", "html-webpack-plugin": "^3.2.0", "jasmine": "^3.1.0", "jasmine-core": "^3.4.0", @@ -74,11 +78,7 @@ "webpack-closure-compiler": "^2.1.6", "webpack-dev-server": "^3.1.4", "webpack-merge": "^4.1.2", - "@angular/cdk": "^7.3.7", - "@angular/material": "^7.3.7", - "@angular/router": "^7.2.15", "zone.js": "^0.9.1" }, - "dependencies": { - } + "dependencies": {} } diff --git a/src/atlasViewer/atlasViewer.pluginService.service.ts b/src/atlasViewer/atlasViewer.pluginService.service.ts index 7adc864521c39d581871bd6be3b1a8172e065ed4..46da82439242c06a5a491c78b1a8b639fe28de3c 100644 --- a/src/atlasViewer/atlasViewer.pluginService.service.ts +++ b/src/atlasViewer/atlasViewer.pluginService.service.ts @@ -1,5 +1,5 @@ import { Injectable, ViewContainerRef, ComponentFactoryResolver, ComponentFactory } from "@angular/core"; -import { PluginInitManifestInterface, ACTION_TYPES } from "src/services/state/pluginState.store"; +import { PluginInitManifestInterface, PLUGIN_STATE_ACTION_TYPES } from "src/services/state/pluginState.store"; import { isDefined } from 'src/services/stateStore.service' import { AtlasViewerAPIServices } from "./atlasViewer.apiService.service"; import { PluginUnit } from "./pluginUnit/pluginUnit.component"; @@ -154,7 +154,7 @@ export class PluginServices{ : null handler.setInitManifestUrl = (url) => this.store.dispatch({ - type : ACTION_TYPES.SET_INIT_PLUGIN, + type : PLUGIN_STATE_ACTION_TYPES.SET_INIT_PLUGIN, manifest : { name : plugin.name, initManifestUrl : url diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 1a4876f532732f6415988deb76849f926ab3377a..f8d3522472ea1c736ea56850db0091c8b536d69a 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -78,10 +78,6 @@ <help-component> </help-component> </mat-tab> - <mat-tab label="Settings"> - <config-component> - </config-component> - </mat-tab> <mat-tab label="Privacy Policy"> <!-- TODO make tab container scrollable --> <cookie-agreement> diff --git a/src/main.module.ts b/src/main.module.ts index 7c0b4504e7ac41bf2e358fea9c0f30777e595880..d2dc2b916e9ed72f0ea110b6d049b942db581be6 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -43,6 +43,8 @@ import { EffectsModule } from "@ngrx/effects"; import { UseEffects } from "./services/effect/effect"; import { MatDialogModule, MatTabsModule } from "@angular/material"; +import 'hammerjs' + @NgModule({ imports : [ FormsModule, diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index 19da12b63740924a679c68fea67f8f27b07eefed..61564e98a21122542f1bdad2118b7d0ca1374992 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -282,6 +282,19 @@ markdown-dom pre code white-space: initial!important; } +.w-5em +{ + width: 5em!important; +} +.w-10em +{ + width: 10em!important; +} +.w-20em +{ + width: 20em!important; +} + .mw-100 { max-width: 100%!important; @@ -322,9 +335,26 @@ markdown-dom pre code pointer-events: none; } -.h-100 +.h-5em { - height:100%; + height: 5em!important; +} + +.h-7em +{ + height:7em!important; +} +.h-10em +{ + height:10em!important; +} +.h-15em +{ + height:15em!important; +} +.h-20em +{ + height:20em!important; } .overflow-x-hidden diff --git a/src/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts index d1dd8b27a79daa1b2a73ad56df959cac71f87636..016911efc30984b8d25284ac1c511c97d3c7ed51 100644 --- a/src/services/state/ngViewerState.store.ts +++ b/src/services/state/ngViewerState.store.ts @@ -1,32 +1,50 @@ import { Action } from '@ngrx/store' +export const FOUR_PANEL = 'FOUR_PANEL' +export const V_ONE_THREE = 'V_ONE_THREE' +export const H_ONE_THREE = 'H_ONE_THREE' +export const SINGLE_PANEL = 'SINGLE_PANEL' + export interface NgViewerStateInterface{ layers : NgLayerInterface[] forceShowSegment : boolean | null nehubaReady: boolean + panelMode: string + panelOrder: string } export interface NgViewerAction extends Action{ layer : NgLayerInterface forceShowSegment : boolean nehubaReady: boolean + payload: any } -const defaultState:NgViewerStateInterface = {layers:[], forceShowSegment:null, nehubaReady: false} +const defaultState:NgViewerStateInterface = { + layers:[], + forceShowSegment:null, + nehubaReady: false, + panelMode: FOUR_PANEL, + panelOrder: `0123` +} export function ngViewerState(prevState:NgViewerStateInterface = defaultState, action:NgViewerAction):NgViewerStateInterface{ switch(action.type){ + case ACTION_TYPES.SWITCH_PANEL_MODE: { + const { payload } = action + const { panelMode } = payload + if (SUPPORTED_PANEL_MODES.indexOf(panelMode) < 0) return prevState + return { + ...prevState, + panelMode + } + } case ADD_NG_LAYER: - return Object.assign({}, prevState, { + return { + ...prevState, + /* this configration hides the layer if a non mixable layer already present */ - layers : action.layer.constructor === Array - ? prevState.layers.concat(action.layer) - : prevState.layers.concat( - Object.assign({}, action.layer, - action.layer.mixability === 'nonmixable' && prevState.layers.findIndex(l => l.mixability === 'nonmixable') >= 0 - ? {visible: false} - : {})) - + /* this configuration does not the addition of multiple non mixable layers */ // layers : action.layer.mixability === 'nonmixable' && prevState.layers.findIndex(l => l.mixability === 'nonmixable') >= 0 // ? prevState.layers @@ -34,39 +52,47 @@ export function ngViewerState(prevState:NgViewerStateInterface = defaultState, a /* this configuration allows the addition of multiple non mixables */ // layers : prevState.layers.map(l => mapLayer(l, action.layer)).concat(action.layer) - }) + layers : action.layer.constructor === Array + ? prevState.layers.concat(action.layer) + : prevState.layers.concat({ + ...action.layer, + ...( action.layer.mixability === 'nonmixable' && prevState.layers.findIndex(l => l.mixability === 'nonmixable') >= 0 + ? {visible: false} + : {}) + }) + } case REMOVE_NG_LAYER: - return Object.assign({}, prevState, { + return { + ...prevState, layers : prevState.layers.filter(l => l.name !== action.layer.name) - } as NgViewerStateInterface) + } case SHOW_NG_LAYER: - return Object.assign({}, prevState, { + return { + ...prevState, layers : prevState.layers.map(l => l.name === action.layer.name - ? Object.assign({}, l, { - visible : true - } as NgLayerInterface) + ? { ...l, visible: true } : l) - }) + } case HIDE_NG_LAYER: - return Object.assign({}, prevState, { + return { + ...prevState, + layers : prevState.layers.map(l => l.name === action.layer.name - ? Object.assign({}, l, { - visible : false - } as NgLayerInterface) + ? { ...l, visible: false } : l) - }) + } case FORCE_SHOW_SEGMENT: - return Object.assign({}, prevState, { + return { + ...prevState, forceShowSegment : action.forceShowSegment - }) as NgViewerStateInterface + } case NEHUBA_READY: const { nehubaReady } = action return { ...prevState, nehubaReady } - default: - return prevState + default: return prevState } } @@ -84,4 +110,18 @@ interface NgLayerInterface{ visible : boolean shader? : string transform? : any -} \ No newline at end of file +} + +const ACTION_TYPES = { + SWITCH_PANEL_MODE: 'SWITCH_PANEL_MODE' +} + +export const SUPPORTED_PANEL_MODES = [ + FOUR_PANEL, + H_ONE_THREE, + V_ONE_THREE, + SINGLE_PANEL, +] + + +export const NG_VIEWER_ACTION_TYPES = ACTION_TYPES \ No newline at end of file diff --git a/src/services/state/pluginState.store.ts b/src/services/state/pluginState.store.ts index 93d6b1e9678128b624fbe146c26afb08f494306b..e9a80cc8d3b180b847ec8e6b86294093e8a7f903 100644 --- a/src/services/state/pluginState.store.ts +++ b/src/services/state/pluginState.store.ts @@ -12,10 +12,12 @@ export interface PluginInitManifestActionInterface extends Action{ } } -export const ACTION_TYPES = { +const ACTION_TYPES = { SET_INIT_PLUGIN: `SET_INIT_PLUGIN` } +export const PLUGIN_STATE_ACTION_TYPES = ACTION_TYPES + export function pluginState(prevState:PluginInitManifestInterface = {initManifests : new Map()}, action:PluginInitManifestActionInterface):PluginInitManifestInterface{ switch(action.type){ case ACTION_TYPES.SET_INIT_PLUGIN: diff --git a/src/ui/config/config.component.ts b/src/ui/config/config.component.ts index 12a3a700a6f84bdf71fa77699c0be0b772756385..e60d64ae778cef8e473fce2dbee28e455d319175 100644 --- a/src/ui/config/config.component.ts +++ b/src/ui/config/config.component.ts @@ -1,12 +1,14 @@ import { Component, OnInit, OnDestroy } from '@angular/core' import { Store, select } from '@ngrx/store'; import { ViewerConfiguration, ACTION_TYPES } from 'src/services/state/viewerConfig.store' -import { Observable, Subject, Subscription } from 'rxjs'; -import { map, distinctUntilChanged, debounceTime } from 'rxjs/operators'; -import { MatSlideToggleChange } from '@angular/material'; +import { Observable, Subscription } from 'rxjs'; +import { map, distinctUntilChanged, startWith, shareReplay } from 'rxjs/operators'; +import { MatSlideToggleChange, MatSliderChange } from '@angular/material'; +import { NG_VIEWER_ACTION_TYPES, SUPPORTED_PANEL_MODES } from 'src/services/state/ngViewerState.store'; const GPU_TOOLTIP = `GPU TOOLTIP` const ANIMATION_TOOLTIP = `ANIMATION_TOOLTIP` +const ROOT_TEXT_ORDER = ['Coronal', 'Sagittal', 'Axial', '3D'] @Component({ selector: 'config-component', @@ -20,6 +22,7 @@ export class ConfigComponent implements OnInit, OnDestroy{ public GPU_TOOLTIP = GPU_TOOLTIP public ANIMATION_TOOLTIP = ANIMATION_TOOLTIP + public supportedPanelModes = SUPPORTED_PANEL_MODES /** * in MB @@ -27,12 +30,15 @@ export class ConfigComponent implements OnInit, OnDestroy{ public gpuLimit$: Observable<number> public animationFlag$: Observable<boolean> - public keydown$: Subject<Event> = new Subject() private subscriptions: Subscription[] = [] public gpuMin : number = 100 public gpuMax : number = 1000 - + + public panelMode$: Observable<string> + public panelOrder$: Observable<[number, number, number, number]> + public panelTexts$: Observable<[string, string, string, string]> + constructor(private store: Store<ViewerConfiguration>) { this.gpuLimit$ = this.store.pipe( select('viewerConfigState'), @@ -45,39 +51,31 @@ export class ConfigComponent implements OnInit, OnDestroy{ select('viewerConfigState'), map((config:ViewerConfiguration) => config.animation), ) + + this.panelMode$ = this.store.pipe( + select('ngViewerState'), + select('panelMode'), + startWith(SUPPORTED_PANEL_MODES[0]) + ) + + this.panelOrder$ = this.store.pipe( + select('ngViewerState'), + select('panelOrder'), + map(string => string.split('').map(s => Number(s))) + ) + + this.panelTexts$ = this.panelOrder$.pipe( + map(arr => arr.map(idx => ROOT_TEXT_ORDER[idx]) as [string, string, string, string]) + ) } ngOnInit(){ - this.subscriptions.push( - this.keydown$.pipe( - debounceTime(250) - ).subscribe(ev => { - /** - * maybe greak in FF. ev.srcElement is IE non standard property - */ - const val = (<HTMLInputElement>ev.srcElement).value - const numVal = val && Number(val) - if (isNaN(numVal) || numVal < this.gpuMin || numVal > this.gpuMax ) - return - this.setGpuPreset({ - value: numVal - }) - }) - ) } ngOnDestroy(){ this.subscriptions.forEach(s => s.unsubscribe()) } - public wheelEvent(ev:WheelEvent) { - const delta = ev.deltaY * -1e5 - this.store.dispatch({ - type: ACTION_TYPES.CHANGE_GPU_LIMIT, - payload: { delta } - }) - } - public toggleAnimationFlag(ev: MatSlideToggleChange ){ const { checked } = ev this.store.dispatch({ @@ -88,12 +86,33 @@ export class ConfigComponent implements OnInit, OnDestroy{ }) } - public setGpuPreset({value}: {value: number}) { + public handleMatSliderChange(ev:MatSliderChange){ this.store.dispatch({ type: ACTION_TYPES.UPDATE_CONFIG, config: { - gpuLimit: value * 1e6 + gpuLimit: ev.value * 1e6 } }) } + usePanelMode(panelMode: string){ + this.store.dispatch({ + type: NG_VIEWER_ACTION_TYPES.SWITCH_PANEL_MODE, + payload: { panelMode } + }) + } + + handleDrop(event:DragEvent){ + const droppedAttri = (event.target as HTMLElement).getAttribute('panel-order') + const draggedAttri = event.dataTransfer.getData('text/plain') + console.log({droppedAttri, draggedAttri}) + } + handleDragOver(event){ + event.preventDefault() + } + handleDragStart(event:DragEvent){ + const attri = (event.target as HTMLElement).getAttribute('panel-order') + event.dataTransfer.setData('text/plain', attri) + } + + public stepSize: number = 10 } \ No newline at end of file diff --git a/src/ui/config/config.style.css b/src/ui/config/config.style.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6ba7757691d6d946412acae13f13b1285f8f33c9 100644 --- a/src/ui/config/config.style.css +++ b/src/ui/config/config.style.css @@ -0,0 +1,4 @@ +[cell-i],[cell-ii],[cell-iii],[cell-iv] +{ + background-color: rgba(128,128,128,0.1); +} \ No newline at end of file diff --git a/src/ui/config/config.template.html b/src/ui/config/config.template.html index dca3b164d0fbe6bd4235e50e856073835acb13d0..6d34bc0da7a2c7c58e2382d57b15b2386e2092f4 100644 --- a/src/ui/config/config.template.html +++ b/src/ui/config/config.template.html @@ -1,42 +1,182 @@ -<div class="input-group mb-2"> - <span class="input-group-prepend"> - <span class="input-group-text"> - GPU Limit - <small tooltipClass="z-1060" [matTooltip]="GPU_TOOLTIP" class="ml-2 fas fa-question"></small> - </span> - </span> - <input - (wheel)="wheelEvent($event)" - type="number" - [min]="100" - [max]="1000" - [step]="0.1" - class="form-control" - (input)="keydown$.next($event)" - [value]="gpuLimit$ | async "> - - <div class="input-group-append"> - - <div (click)="setGpuPreset({ value: 100 })" class="btn btn-outline-secondary"> - 100 +<mat-tab-group> + + <!-- viewer preference --> + <mat-tab label="Viewer Preference"> + + <div [ngSwitch]="panelMode$ | async" class="d-flex flex-row p-2"> + <layout-four-panel + *ngSwitchCase="supportedPanelModes[0]" + class="d-block w-20em h-15em cdk-drag-boundary"> + <div + matRipple + (dragstart)="handleDragStart($event)" + (dragover)="handleDragOver($event)" + (drop)="handleDrop($event)" + class="w-100 h-100" + cell-i> + <div [attr.panel-order]="0" class="w-100 h-100 d-flex align-items-center justify-content-center border" draggable="true"> + {{ (panelTexts$ | async)[0] }} + </div> + </div> + <div + matRipple + (dragstart)="handleDragStart($event)" + (dragover)="handleDragOver($event)" + (drop)="handleDrop($event)" + class="w-100 h-100" + cell-ii> + <div [attr.panel-order]="1" class="w-100 h-100 d-flex align-items-center justify-content-center border" draggable="true"> + {{ (panelTexts$ | async)[1] }} + </div> + </div> + <div + matRipple + (dragstart)="handleDragStart($event)" + (dragover)="handleDragOver($event)" + (drop)="handleDrop($event)" + class="w-100 h-100" + cell-iii> + <div [attr.panel-order]="2" class="w-100 h-100 d-flex align-items-center justify-content-center border" draggable="true"> + {{ (panelTexts$ | async)[2] }} + </div> + </div> + <div + matRipple + (dragstart)="handleDragStart($event)" + (dragover)="handleDragOver($event)" + (drop)="handleDrop($event)" + class="w-100 h-100" + cell-iv> + <div [attr.panel-order]="3" class="w-100 h-100 d-flex align-items-center justify-content-center border" draggable="true"> + {{ (panelTexts$ | async)[3] }} + </div> + </div> + </layout-four-panel> + <layout-horizontal-one-three + *ngSwitchCase="supportedPanelModes[1]" + class="d-block w-20em h-15em"> + <div cell-i>{{ (panelTexts$ | async)[0] }}</div> + <div cell-ii>{{ (panelTexts$ | async)[1] }}</div> + <div cell-iii>{{ (panelTexts$ | async)[2] }}</div> + <div cell-iv>{{ (panelTexts$ | async)[3] }}</div> + </layout-horizontal-one-three> + <layout-vertical-one-three + *ngSwitchCase="supportedPanelModes[2]" + class="d-block w-20em h-15em"> + <div cell-i>{{ (panelTexts$ | async)[0] }}</div> + <div cell-ii>{{ (panelTexts$ | async)[1] }}</div> + <div cell-iii>{{ (panelTexts$ | async)[2] }}</div> + <div cell-iv>{{ (panelTexts$ | async)[3] }}</div> + </layout-vertical-one-three> + <layout-single-panel + *ngSwitchCase="supportedPanelModes[3]" + class="d-block w-20em h-15em"> + <div cell-i>{{ (panelTexts$ | async)[0] }}</div> + <div cell-ii>{{ (panelTexts$ | async)[1] }}</div> + <div cell-iii>{{ (panelTexts$ | async)[2] }}</div> + <div cell-iv>{{ (panelTexts$ | async)[3] }}</div> + </layout-single-panel> + <div *ngSwitchDefault> + A panel mode which I have never seen before ... + </div> + </div> + + <!-- scroll window --> + <div class="d-flex flex-row flex-nowrap p-2"> + + <!-- Four Panel Card --> + <button + class="m-2 p-2" + mat-flat-button + (click)="usePanelMode(supportedPanelModes[0])" + [color]="(panelMode$ | async) === supportedPanelModes[0] ? 'primary' : null"> + <layout-four-panel class="d-block w-10em h-7em"> + <div cell-i></div> + <div cell-ii></div> + <div cell-iii></div> + <div cell-iv></div> + </layout-four-panel> + </button> + + <!-- horizontal 1 3 card --> + <button + class="m-2 p-2" + mat-flat-button + (click)="usePanelMode(supportedPanelModes[1])" + [color]="(panelMode$ | async) === supportedPanelModes[1] ? 'primary' : null"> + <layout-horizontal-one-three class="d-block w-10em h-7em"> + <div cell-i></div> + <div cell-ii></div> + <div cell-iii></div> + <div cell-iv></div> + </layout-horizontal-one-three> + </button> + + <!-- vertical 1 3 card --> + <button + class="m-2 p-2" + mat-flat-button + (click)="usePanelMode(supportedPanelModes[2])" + [color]="(panelMode$ | async) === supportedPanelModes[2] ? 'primary' : null"> + <layout-vertical-one-three class="d-block w-10em h-7em"> + <div cell-i></div> + <div cell-ii></div> + <div cell-iii></div> + <div cell-iv></div> + </layout-vertical-one-three> + </button> + + <!-- single --> + <button + class="m-2 p-2" + mat-flat-button + (click)="usePanelMode(supportedPanelModes[3])" + [color]="(panelMode$ | async) === supportedPanelModes[3] ? 'primary' : null"> + <layout-single-panel class="d-block w-10em h-7em"> + <div cell-i></div> + <div cell-ii></div> + <div cell-iii></div> + <div cell-iv></div> + </layout-single-panel> + </button> </div> - <div (click)="setGpuPreset({ value: 500 })" class="btn btn-outline-secondary"> - 500 + </mat-tab> + + <!-- hard ware --> + <mat-tab label="Hardware"> + <div class="d-flex align-items-center"> + <mat-slide-toggle + [checked]="animationFlag$ | async" + (change)="toggleAnimationFlag($event)"> + Enable Animation + </mat-slide-toggle> + <small [matTooltip]="ANIMATION_TOOLTIP" class="ml-2 fas fa-question"></small> </div> - <div (click)="setGpuPreset({ value: 1000 })" class="btn btn-outline-secondary"> - 1000 + <div class="d-flex flex-row align-items-center justify-content start"> + <label + class="m-0 d-inline-block flex-grow-0 flex-shrink-0" + for="gpuLimitSlider"> + GPU Limit + <small [matTooltip]="GPU_TOOLTIP" class="ml-2 fas fa-question"></small> + </label> + <mat-slider + class="flex-grow-1 flex-shrink-1 ml-2 mr-2" + id="gpuLimitSlider" + name="gpuLimitSlider" + thumbLabel="true" + min="100" + max="1000" + [step]="stepSize" + (change)="handleMatSliderChange($event)" + [value]="gpuLimit$ | async"> + </mat-slider> + <span class="d-inline-block flex-grow-0 flex-shrink-0 w-10em"> + {{ gpuLimit$ | async }} MB + </span> </div> - <span class="input-group-text"> - MB - </span> - </div> -</div> - -<div class="d-flex align-items-center"> - <mat-slide-toggle - [checked]="animationFlag$ | async" - (change)="toggleAnimationFlag($event)"> - Enable Animation - </mat-slide-toggle> - <small [matTooltip]="ANIMATION_TOOLTIP" class="ml-2 fas fa-question"></small> -</div> \ No newline at end of file + + + </mat-tab> + +</mat-tab-group> + diff --git a/src/ui/config/layouts/fourPanel/fourPanel.component.ts b/src/ui/config/layouts/fourPanel/fourPanel.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7a22a241e08a3862385b7e7704bf20f0adfded7 --- /dev/null +++ b/src/ui/config/layouts/fourPanel/fourPanel.component.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: 'layout-four-panel', + templateUrl: './fourPanel.template.html', + styleUrls: [ + './fourPanel.style.css' + ] +}) + +export class FourPanelLayout{ + +} \ No newline at end of file diff --git a/src/ui/config/layouts/fourPanel/fourPanel.style.css b/src/ui/config/layouts/fourPanel/fourPanel.style.css new file mode 100644 index 0000000000000000000000000000000000000000..03169dfb4b9564f62c86ca9250303c5337927861 --- /dev/null +++ b/src/ui/config/layouts/fourPanel/fourPanel.style.css @@ -0,0 +1,4 @@ +.four-panel-cell +{ + flex: 0 0 50%; +} \ No newline at end of file diff --git a/src/ui/config/layouts/fourPanel/fourPanel.template.html b/src/ui/config/layouts/fourPanel/fourPanel.template.html new file mode 100644 index 0000000000000000000000000000000000000000..37cd6732a39f4e06aceb21adab0ead302fddedfd --- /dev/null +++ b/src/ui/config/layouts/fourPanel/fourPanel.template.html @@ -0,0 +1,18 @@ +<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-stretch"> + <div class="d-flex flex-row flex-grow-1 flex-shrink-1"> + <div class="d-flex flex-row four-panel-cell border align-items-center justify-content-center"> + <ng-content select="[cell-i]"></ng-content> + </div> + <div class="d-flex flex-row four-panel-cell border align-items-center justify-content-center"> + <ng-content select="[cell-ii]"></ng-content> + </div> + </div> + <div class="d-flex flex-row flex-grow-1 flex-shrink-1"> + <div class="d-flex flex-row four-panel-cell border align-items-center justify-content-center"> + <ng-content select="[cell-iii]"></ng-content> + </div> + <div class="d-flex flex-row four-panel-cell border align-items-center justify-content-center"> + <ng-content select="[cell-iv]"></ng-content> + </div> + </div> +</div> diff --git a/src/ui/config/layouts/h13/h13.component.ts b/src/ui/config/layouts/h13/h13.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..eccf98f96c6e49993db2a2ac609bfbc05e9a6bc7 --- /dev/null +++ b/src/ui/config/layouts/h13/h13.component.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: 'layout-horizontal-one-three', + templateUrl: './h13.template.html', + styleUrls: [ + './h13.style.css' + ] +}) + +export class HorizontalOneThree{ + +} \ No newline at end of file diff --git a/src/ui/config/layouts/h13/h13.style.css b/src/ui/config/layouts/h13/h13.style.css new file mode 100644 index 0000000000000000000000000000000000000000..be83c538297487ff9c330ba551ac85e2badfd309 --- /dev/null +++ b/src/ui/config/layouts/h13/h13.style.css @@ -0,0 +1,12 @@ +.major-column +{ + flex: 0 0 67%; +} +.minor-column +{ + flex: 0 0 33%; +} +.layout-31-cell +{ + flex: 0 0 33.33%; +} \ No newline at end of file diff --git a/src/ui/config/layouts/h13/h13.template.html b/src/ui/config/layouts/h13/h13.template.html new file mode 100644 index 0000000000000000000000000000000000000000..c18778839283cf818be54d25955e3bd9ed02dae8 --- /dev/null +++ b/src/ui/config/layouts/h13/h13.template.html @@ -0,0 +1,18 @@ +<div class="w-100 h-100 d-flex flex-row justify-content-center align-items-stretch"> + <div class="d-flex flex-column major-column"> + <div class="overflow-hidden border flex-grow-1 d-flex align-items-center justify-content-center"> + <ng-content select="[cell-i]"></ng-content> + </div> + </div> + <div class="d-flex flex-column minor-column"> + <div class="overflow-hidden border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-ii]"></ng-content> + </div> + <div class="overflow-hidden border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-iii]"></ng-content> + </div> + <div class="overflow-hidden border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-iv]"></ng-content> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/ui/config/layouts/single/single.component.ts b/src/ui/config/layouts/single/single.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..25101d27a9462b37e4072a912bd281126fdb7500 --- /dev/null +++ b/src/ui/config/layouts/single/single.component.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: 'layout-single-panel', + templateUrl: './single.template.html', + styleUrls: [ + './single.style.css' + ] +}) + +export class SinglePanel{ + +} \ No newline at end of file diff --git a/src/ui/config/layouts/single/single.style.css b/src/ui/config/layouts/single/single.style.css new file mode 100644 index 0000000000000000000000000000000000000000..19615b37060256eef0194131b901b11e89b01b49 --- /dev/null +++ b/src/ui/config/layouts/single/single.style.css @@ -0,0 +1,12 @@ +.major-column +{ + flex: 0 0 100%; +} +.minor-column +{ + flex: 0 0 0%; +} +.layout-31-cell +{ + flex: 0 0 33%; +} \ No newline at end of file diff --git a/src/ui/config/layouts/single/single.template.html b/src/ui/config/layouts/single/single.template.html new file mode 100644 index 0000000000000000000000000000000000000000..f7511175205e48ba3b0aecab473d26f6f6d46148 --- /dev/null +++ b/src/ui/config/layouts/single/single.template.html @@ -0,0 +1,18 @@ +<div class="w-100 h-100 d-flex flex-row justify-content-center align-items-stretch"> + <div class="d-flex flex-column major-column"> + <div class="overflow-hidden border flex-grow-1 d-flex align-items-center justify-content-center"> + <ng-content select="[cell-i]"></ng-content> + </div> + </div> + <div class="d-flex flex-column minor-column"> + <div class="overflow-hidden border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-ii]"></ng-content> + </div> + <div class="overflow-hidden border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-iii]"></ng-content> + </div> + <div class="overflow-hidden border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-iv]"></ng-content> + </div> + </div> + </div> \ No newline at end of file diff --git a/src/ui/config/layouts/v13/v13.component.ts b/src/ui/config/layouts/v13/v13.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c650ac701de3fff14118a9487403410ae89e72ad --- /dev/null +++ b/src/ui/config/layouts/v13/v13.component.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: 'layout-vertical-one-three', + templateUrl: './v13.template.html', + styleUrls: [ + './v13.style.css' + ] +}) + +export class VerticalOneThree{ + +} \ No newline at end of file diff --git a/src/ui/config/layouts/v13/v13.style.css b/src/ui/config/layouts/v13/v13.style.css new file mode 100644 index 0000000000000000000000000000000000000000..be83c538297487ff9c330ba551ac85e2badfd309 --- /dev/null +++ b/src/ui/config/layouts/v13/v13.style.css @@ -0,0 +1,12 @@ +.major-column +{ + flex: 0 0 67%; +} +.minor-column +{ + flex: 0 0 33%; +} +.layout-31-cell +{ + flex: 0 0 33.33%; +} \ No newline at end of file diff --git a/src/ui/config/layouts/v13/v13.template.html b/src/ui/config/layouts/v13/v13.template.html new file mode 100644 index 0000000000000000000000000000000000000000..6c4059a7ce36bb036d881ea3c537ce062c4b42ad --- /dev/null +++ b/src/ui/config/layouts/v13/v13.template.html @@ -0,0 +1,18 @@ +<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-stretch"> + <div class="d-flex flex-column major-column"> + <div class="border flex-grow-1 d-flex align-items-center justify-content-center"> + <ng-content select="[cell-i]"></ng-content> + </div> + </div> + <div class="d-flex flex-row minor-column"> + <div class="border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-ii]"></ng-content> + </div> + <div class="border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-iii]"></ng-content> + </div> + <div class="border layout-31-cell d-flex align-items-center justify-content-center"> + <ng-content select="[cell-iv]"></ng-content> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 44dfc715979eabe3d800db61441c919171ecb669..090bad598038810c0d9e97d09eb42eddea3be396 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -3,15 +3,16 @@ import { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component"; import { Store, select } from "@ngrx/store"; import { ViewerStateInterface, safeFilter, CHANGE_NAVIGATION, isDefined, USER_LANDMARKS, ADD_NG_LAYER, REMOVE_NG_LAYER, NgViewerStateInterface, MOUSE_OVER_LANDMARK, SELECT_LANDMARKS, Landmark, PointLandmarkGeometry, PlaneLandmarkGeometry, OtherLandmarkGeometry, getNgIds, getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId } from "../../services/stateStore.service"; import { Observable, Subscription, fromEvent, combineLatest, merge } from "rxjs"; -import { filter,map, take, scan, debounceTime, distinctUntilChanged, switchMap, skip, withLatestFrom, buffer, tap, throttleTime, bufferTime } from "rxjs/operators"; +import { filter,map, take, scan, debounceTime, distinctUntilChanged, switchMap, skip, withLatestFrom, buffer, tap, throttleTime, bufferTime, switchMapTo } from "rxjs/operators"; import { AtlasViewerAPIServices, UserLandmark } from "../../atlasViewer/atlasViewer.apiService.service"; import { timedValues } from "../../util/generator"; import { AtlasViewerConstantsServices } from "../../atlasViewer/atlasViewer.constantService.service"; import { ViewerConfiguration } from "src/services/state/viewerConfig.store"; import { pipeFromArray } from "rxjs/internal/util/pipe"; -import { NEHUBA_READY } from "src/services/state/ngViewerState.store"; +import { NEHUBA_READY, H_ONE_THREE, V_ONE_THREE, FOUR_PANEL, SINGLE_PANEL } from "src/services/state/ngViewerState.store"; import { MOUSE_OVER_SEGMENTS } from "src/services/state/uiState.store"; import { SELECT_REGIONS_WITH_ID, NEHUBA_LAYER_CHANGED } from "src/services/state/viewerState.store"; +import { getHorizontalOneThree, getVerticalOneThree, getFourPanel, getSinglePanel } from "./util"; const getProxyUrl = (ngUrl) => `nifti://${BACKEND_URL}preview/file?fileUrl=${encodeURIComponent(ngUrl.replace(/^nifti:\/\//,''))}` const getProxyOther = ({source}) => /AUTH_227176556f3c4bb38df9feea4b91200c/.test(source) @@ -143,6 +144,8 @@ export class NehubaContainer implements OnInit, OnDestroy{ public nanometersToOffsetPixelsFn : Function[] = [] private viewerConfig : Partial<ViewerConfiguration> = {} + private viewPanels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement] = [null, null, null, null] + constructor( private constantService : AtlasViewerConstantsServices, private apiService :AtlasViewerAPIServices, @@ -335,19 +338,25 @@ export class NehubaContainer implements OnInit, OnDestroy{ ) /* each time a new viewer is initialised, take the first event to get the translation function */ - this.newViewer$.pipe( - // switchMap(() => fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent') - // .pipe( - // ...takeOnePipe - // ) - // ) - - switchMap(() => pipeFromArray([...takeOnePipe])(fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent'))) - + this.subscriptions.push( + this.newViewer$.pipe( + switchMap(() => pipeFromArray([...takeOnePipe])(fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent'))) + ).subscribe((events)=>{ + for (const idx in [0,1,2]) { + const ev = events[idx] as CustomEvent + this.viewPanels[idx] = ev.target as HTMLElement + this.nanometersToOffsetPixelsFn[idx] = ev.detail.nanometersToOffsetPixels + } + }) + ) - ).subscribe((events)=>{ - [0,1,2].forEach(idx=>this.nanometersToOffsetPixelsFn[idx] = (events[idx] as any).detail.nanometersToOffsetPixels) - }) + this.subscriptions.push( + this.newViewer$.pipe( + switchMapTo(fromEvent(this.elementRef.nativeElement, 'perpspectiveRenderEvent').pipe( + take(1) + )), + ).subscribe(ev => this.viewPanels[3] = ((ev as CustomEvent).target) as HTMLElement) + ) this.sliceViewLoadingMain$ = fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent').pipe( scan(scanFn, [null, null, null]), @@ -407,8 +416,58 @@ export class NehubaContainer implements OnInit, OnDestroy{ return this.constantService.mobile } + private removeExistingPanels() { + const element = this.nehubaViewer.nehubaViewer.ngviewer.layout.container.componentValue.element as HTMLElement + while (element.childElementCount > 0) { + element.removeChild(element.firstElementChild) + } + return element + } + ngOnInit(){ + this.subscriptions.push( + this.store.pipe( + select('ngViewerState'), + select('panelMode'), + distinctUntilChanged() + ).subscribe(mode => { + + /** + * TODO be smarter with event stream + */ + if (!this.nehubaViewer) return + + switch (mode) { + case H_ONE_THREE:{ + const element = this.removeExistingPanels() + const newEl = getHorizontalOneThree(this.viewPanels) + element.appendChild(newEl) + break; + } + case V_ONE_THREE:{ + const element = this.removeExistingPanels() + const newEl = getVerticalOneThree(this.viewPanels) + element.appendChild(newEl) + break; + } + case FOUR_PANEL: { + const element = this.removeExistingPanels() + const newEl = getFourPanel(this.viewPanels) + element.appendChild(newEl) + break; + } + case SINGLE_PANEL: { + const element = this.removeExistingPanels() + const newEl = getSinglePanel(this.viewPanels) + element.appendChild(newEl) + break; + } + default: + } + }) + ) + this.subscriptions.push( this.viewerPerformanceConfig$.subscribe(config => { this.nehubaViewer.applyPerformanceConfig(config) @@ -1181,7 +1240,6 @@ export const takeOnePipe = [ * 4 ??? */ const key = identifySrcElement(target) - const _ = {} _[key] = event return Object.assign({},acc,_) diff --git a/src/ui/nehubaContainer/util.ts b/src/ui/nehubaContainer/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..edd3a98f8fea9b48e40fe7cd16702f469a95ca93 --- /dev/null +++ b/src/ui/nehubaContainer/util.ts @@ -0,0 +1,74 @@ +const flexContCmnCls = ['w-100', 'h-100', 'd-flex', 'justify-content-center', 'align-items-stretch'] + +const makeRow = (...els:HTMLElement[]) => { + const container = document.createElement('div') + container.classList.add(...flexContCmnCls, 'flex-row') + for (const el of els){ + container.appendChild(el) + } + return container +} + +const makeCol = (...els:HTMLElement[]) => { + const container = document.createElement('div') + container.classList.add(...flexContCmnCls, 'flex-column') + for (const el of els){ + container.appendChild(el) + } + return container +} + +export const getHorizontalOneThree = (panels:[HTMLElement, HTMLElement, HTMLElement, HTMLElement]) => { + for (let panel of panels){ + panel.className = '' + } + const majorContainer = makeCol(panels[0]) + const minorContainer = makeCol(panels[1], panels[2], panels[3]) + + majorContainer.style.flexBasis = '67%' + minorContainer.style.flexBasis = '33%' + + return makeRow(majorContainer, minorContainer) +} + +export const getVerticalOneThree = (panels:[HTMLElement, HTMLElement, HTMLElement, HTMLElement]) => { + for (let panel of panels){ + panel.className = '' + } + const majorContainer = makeRow(panels[0]) + const minorContainer = makeRow(panels[1], panels[2], panels[3]) + + majorContainer.style.flexBasis = '67%' + minorContainer.style.flexBasis = '33%' + + return makeCol(majorContainer, minorContainer) +} + + +export const getFourPanel = (panels:[HTMLElement, HTMLElement, HTMLElement, HTMLElement]) => { + for (let panel of panels){ + panel.className = '' + } + const majorContainer = makeRow(panels[0], panels[1]) + const minorContainer = makeRow(panels[2], panels[3]) + + majorContainer.style.flexBasis = '50%' + minorContainer.style.flexBasis = '50%' + + return makeCol(majorContainer, minorContainer) +} + +export const getSinglePanel = (panels:[HTMLElement, HTMLElement, HTMLElement, HTMLElement]) => { + for (let panel of panels){ + panel.className = '' + } + const majorContainer = makeRow(panels[0]) + const minorContainer = makeRow(panels[1], panels[2], panels[3]) + + majorContainer.style.flexBasis = '100%' + minorContainer.style.flexBasis = '0%' + + minorContainer.className = '' + minorContainer.style.height = '0px' + return makeCol(majorContainer, minorContainer) +} \ No newline at end of file diff --git a/src/ui/sharedModules/angularMaterial.module.ts b/src/ui/sharedModules/angularMaterial.module.ts index fe3b30ad0b03a82f351a0d90fab08e89d821f06d..7c501dee8f70df574ef14e530791f88694130683 100644 --- a/src/ui/sharedModules/angularMaterial.module.ts +++ b/src/ui/sharedModules/angularMaterial.module.ts @@ -5,12 +5,13 @@ import { MatCardModule, MatTabsModule, MatTooltipModule, - MatSlideToggleModule + MatSlideToggleModule, + MatDialogModule, } from '@angular/material'; import { NgModule } from '@angular/core'; @NgModule({ - imports: [MatSlideToggleModule, MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule], - exports: [MatSlideToggleModule, MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule], + imports: [MatDialogModule, MatSlideToggleModule, MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule], + exports: [MatDialogModule, MatSlideToggleModule, MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule], }) export class AngularMaterialModule { } \ No newline at end of file diff --git a/src/ui/signinBanner/signinBanner.components.ts b/src/ui/signinBanner/signinBanner.components.ts index 592e3101d2f6bc897fdee15efdabbefd948011c5..4d50a745a5a9f83e682655f0089c6bb3213d35fa 100644 --- a/src/ui/signinBanner/signinBanner.components.ts +++ b/src/ui/signinBanner/signinBanner.components.ts @@ -1,4 +1,4 @@ -import {Component, ChangeDetectionStrategy, OnDestroy, OnInit, Input, ViewChild, TemplateRef } from "@angular/core"; +import {Component, ChangeDetectionStrategy, OnDestroy, OnInit, Input, ViewChild, TemplateRef, ElementRef } from "@angular/core"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; import { AuthService, User } from "src/services/auth.service"; import { Store, select } from "@ngrx/store"; @@ -9,6 +9,7 @@ import { map, filter, distinctUntilChanged, bufferTime, delay, share, tap, withL import { regionFlattener } from "src/util/regionFlattener"; import { ToastService } from "src/services/toastService.service"; import { getSchemaIdFromName } from "src/util/pipes/templateParcellationDecoration.pipe"; +import { MatDialog } from "@angular/material"; const compareParcellation = (o, n) => !o || !n ? false @@ -40,6 +41,7 @@ export class SigninBanner implements OnInit, OnDestroy{ @Input() darktheme: boolean @ViewChild('publicationTemplate', {read:TemplateRef}) publicationTemplate: TemplateRef<any> + @ViewChild('settingBtn', {read: ElementRef}) settingBtn: ElementRef public focusedDatasets$: Observable<any[]> private userFocusedDataset$: Subject<any> = new Subject() @@ -50,7 +52,8 @@ export class SigninBanner implements OnInit, OnDestroy{ private constantService: AtlasViewerConstantsServices, private authService: AuthService, private store: Store<ViewerConfiguration>, - private toastService: ToastService + private toastService: ToastService, + private dialog: MatDialog ){ this.loadedTemplates$ = this.store.pipe( select('viewerState'), @@ -137,6 +140,10 @@ export class SigninBanner implements OnInit, OnDestroy{ ) } + ngAfterViewInit(){ + this.settingBtn.nativeElement.click() + } + ngOnDestroy(){ this.subscriptions.forEach(s => s.unsubscribe()) } @@ -216,10 +223,25 @@ export class SigninBanner implements OnInit, OnDestroy{ return `<div class="d-flex"><small>Template</small> <small class = "flex-grow-1 mute-text">${template ? '(' + template.name + ')' : ''}</small> <span class = "fas fa-caret-down"></span></div>` } + /** + * move the templates to signin banner when pluginprettify is merged + */ showHelp() { this.constantService.showHelpSubject$.next() } + /** + * move the templates to signin banner when pluginprettify is merged + */ + showSetting(settingTemplate:TemplateRef<any>){ + this.dialog.open(settingTemplate, { + autoFocus: false + }) + } + + /** + * move the templates to signin banner when pluginprettify is merged + */ showSignin() { this.constantService.showSigninSubject$.next(this.user) } diff --git a/src/ui/signinBanner/signinBanner.template.html b/src/ui/signinBanner/signinBanner.template.html index 0193227feeb5f6eebb95e05cbaaaf32df5195bc8..00cdd6cf385fab29210487a5868c1fcd85ea7324 100644 --- a/src/ui/signinBanner/signinBanner.template.html +++ b/src/ui/signinBanner/signinBanner.template.html @@ -56,6 +56,19 @@ </button> </div> + <!-- setting btn --> + <div class="btnWrapper"> + <button + #settingBtn + matTooltip="Settings" + matTooltipPosition="below" + (click)="showSetting(settingTemplate)" + mat-icon-button + color="basic"> + <i class="fas fa-cog"></i> + </button> + </div> + <!-- signin --> <div class="btnWrapper"> <button @@ -84,4 +97,14 @@ [kgId]="focusedDataset.kgId"> </single-dataset-view> +</ng-template> + +<ng-template #settingTemplate> + <h2 mat-dialog-title>Settings</h2> + <mat-dialog-content> + <!-- required to avoid showing an ugly vertical scroll bar --> + <!-- TODO investigate why, then remove the friller class --> + <config-component class="mb-4 d-block"> + </config-component> + </mat-dialog-content> </ng-template> \ No newline at end of file diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index a7c08e93c200ab6454d88efa8a618687ca049dec..f14ba244d7f14ab5f4ebf094378f5cc7b27fcfa3 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -48,6 +48,12 @@ import { KGToS } from "./kgtos/kgtos.component"; import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' import { TemplateParcellationsDecorationPipe } from "src/util/pipes/templateParcellationDecoration.pipe"; import { AppendtooltipTextPipe } from "src/util/pipes/appendTooltipText.pipe"; +import { MatSliderModule, MatRippleModule } from "@angular/material"; +import { FourPanelLayout } from "./config/layouts/fourPanel/fourPanel.component"; +import { HorizontalOneThree } from "./config/layouts/h13/h13.component"; +import { VerticalOneThree } from "./config/layouts/v13/v13.component"; +import { SinglePanel } from "./config/layouts/single/single.component"; +import { DragDropModule } from "@angular/cdk/drag-drop"; @NgModule({ @@ -59,6 +65,13 @@ import { AppendtooltipTextPipe } from "src/util/pipes/appendTooltipText.pipe"; UtilModule, AngularMaterialModule, + /** + * move to angular material module + */ + MatSliderModule, + DragDropModule, + MatRippleModule, + PopoverModule.forRoot(), TooltipModule.forRoot() ], @@ -84,6 +97,10 @@ import { AppendtooltipTextPipe } from "src/util/pipes/appendTooltipText.pipe"; StatusCardComponent, CookieAgreement, KGToS, + FourPanelLayout, + HorizontalOneThree, + VerticalOneThree, + SinglePanel, /* pipes */ GroupDatasetByRegion,