diff --git a/common/constants.js b/common/constants.js index 00fd20a33a36df6b725bbc3735c4fc0d9d52dd32..ceeb3d027330bb5167bd469abe812569d8be6da0 100644 --- a/common/constants.js +++ b/common/constants.js @@ -60,6 +60,7 @@ //Viewer mode VIEWER_MODE_ANNOTATING: 'annotating', + VIEWER_MODE_KEYFRAME: 'key frame', // Annotations USER_ANNOTATION_LIST: 'user annotations footer', diff --git a/src/keyframesModule/constants.ts b/src/keyframesModule/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d716c05f4abaa7a9a6a98909a806c5ff03c63f2 --- /dev/null +++ b/src/keyframesModule/constants.ts @@ -0,0 +1,2 @@ +import { ARIA_LABELS } from 'common/constants' +export const KEYFRAME_VIEWMODE = ARIA_LABELS.VIEWER_MODE_KEYFRAME \ No newline at end of file diff --git a/src/keyframesModule/index.ts b/src/keyframesModule/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/keyframesModule/keyframe.directive.ts b/src/keyframesModule/keyframe.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..50708e85062ba075edf28ca9a41506d492694419 --- /dev/null +++ b/src/keyframesModule/keyframe.directive.ts @@ -0,0 +1,42 @@ +import { Directive, HostListener, Input } from "@angular/core"; +import { KeyFrameService } from "./service"; + +@Directive({ + selector: '[key-frame-play-now]' +}) + +export class KeyFrameDirective{ + @HostListener('click') + onClick(){ + if (this._mode === 'on') { + this.svc.startKeyFrameSession() + return + } + if (this._mode === 'off') { + this.svc.endKeyFrameSession() + return + } + if (this.svc.inSession) { + this.svc.endKeyFrameSession() + } else { + this.svc.startKeyFrameSession() + } + } + + private _mode: 'toggle' | 'off' | 'on' = 'on' + @Input('key-frame-play-now') + set mode(val: string){ + if (val === 'off') { + this._mode = val + return + } + if (val === 'toggle') { + this._mode = val + return + } + this._mode = 'on' + } + + constructor(private svc: KeyFrameService){ + } +} diff --git a/src/keyframesModule/keyframeCtrl/keyframeCtrl.component.ts b/src/keyframesModule/keyframeCtrl/keyframeCtrl.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..95f4f1a95e7885dfa378dd8bfe36f3b3ba6b4847 --- /dev/null +++ b/src/keyframesModule/keyframeCtrl/keyframeCtrl.component.ts @@ -0,0 +1,209 @@ +import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop"; +import { Component, OnDestroy, Optional } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { Subscription } from "rxjs"; +import { getUuid } from "src/util/fn"; +import { timedValues } from "src/util/generator"; +import { TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service"; + +type TStoredState = { + name: string + duration: number + viewerType: string + payload: any +} + +@Component({ + selector: 'key-frame-controller', + templateUrl: './keyframeCtrl.template.html', + styleUrls: [ + './keyframeCtrl.style.css' + ] +}) + +export class KeyFrameCtrlCmp implements OnDestroy { + + public loopFlag = false + public linearFlag = false + public currState: any + private currViewerType: string + public internalStates: TStoredState[] = [] + + private subs: Subscription[] = [] + + ngOnDestroy(){ + while(this.subs.length) this.subs.pop().unsubscribe() + } + + constructor( + private snackbar: MatSnackBar, + @Optional() private viewerInternalSvc: ViewerInternalStateSvc + ){ + if (!viewerInternalSvc) { + this.snackbar.open(`error: ViewerInternalStateSvc not injected.`) + return + } + + this.subs.push( + viewerInternalSvc.viewerInternalState$.pipe( + ).subscribe(state => { + this.currState = state.payload + this.currViewerType = state.viewerType + }) + ) + } + + addKeyFrame(){ + this.internalStates = [ + ...this.internalStates, + { + name: `Frame ${this.internalStates.length + 1}`, + duration: 1000, + viewerType: this.currViewerType, + payload: this.currState + } + ] + } + + private raf: number + + private isPlaying = false + async togglePlay(){ + if (this.isPlaying) { + this.isPlaying = false + return + } + this.isPlaying = true + + let idx = 0 + this.gotoFrame(this.internalStates[0]) + while (true) { + try { + await this.animateFrame( + this.internalStates[idx % this.internalStates.length], + this.internalStates[(idx + 1) % this.internalStates.length] + ) + idx ++ + if (idx >= this.internalStates.length-1 && !this.loopFlag) break + } catch (e) { + // user interrupted + console.log(e) + } + } + this.isPlaying = false + } + private async animateFrame(fromFrame: TStoredState, toFrame: TStoredState) { + const toPayloadCamera = (toFrame.payload as any).camera + const fromPayloadCamera = (fromFrame.payload as any).camera + + const delta = { + x: toPayloadCamera.x - fromPayloadCamera.x, + y: toPayloadCamera.y - fromPayloadCamera.y, + z: toPayloadCamera.z - fromPayloadCamera.z, + } + + const applyDelta = (() => { + if (this.linearFlag) { + return (d: number) => { + return { + x: delta.x * d + fromPayloadCamera.x, + y: delta.y * d + fromPayloadCamera.y, + z: delta.z * d + fromPayloadCamera.z, + } + } + } else { + const THREE = (window as any).ThreeSurfer.THREE + const idQ = new THREE.Quaternion() + const targetQ = new THREE.Quaternion() + const vec1 = new THREE.Vector3( + fromPayloadCamera.x, + fromPayloadCamera.y, + fromPayloadCamera.z, + ) + + const startVec = vec1.clone() + const vec1Length = vec1.length() + const vec2 = new THREE.Vector3( + toPayloadCamera.x, + toPayloadCamera.y, + toPayloadCamera.z, + ) + const vec2Length = vec2.length() + vec1.normalize() + vec2.normalize() + + targetQ.setFromUnitVectors(vec1, vec2) + + return (d: number) => { + const deltaQ = idQ.clone() + deltaQ.slerp(targetQ, d) + const v = startVec.clone() + v.applyQuaternion(deltaQ) + return { + x: v.x, + y: v.y, + z: v.z + } + } + } + })() + + const gen = timedValues(toFrame.duration) + + return new Promise((rs, rj) => { + + const animate = () => { + if (!this.isPlaying) { + this.raf = null + return rj('User interrupted') + } + const next = gen.next() + const d = next.value + + if (this.viewerInternalSvc) { + const camera = applyDelta(d) + this.viewerInternalSvc.applyInternalState({ + "@id": getUuid(), + "@type": "TViewerInternalStateEmitterEvent", + viewerType: fromFrame.viewerType, + payload: { + camera + } + }) + } + + if (next.done) { + this.raf = null + + rs('') + } else { + this.raf = requestAnimationFrame(() => { + animate() + }) + } + } + this.raf = requestAnimationFrame(() => { + animate() + }) + }) + } + + gotoFrame(item: TStoredState) { + if (this.viewerInternalSvc) { + this.viewerInternalSvc.applyInternalState({ + "@id": getUuid(), + "@type": "TViewerInternalStateEmitterEvent", + viewerType: item.viewerType, + payload: item.payload + }) + } + } + + removeFrame(item: TStoredState){ + this.internalStates = this.internalStates.filter(v => item !== v) + } + + drop(event: CdkDragDrop<string[]>) { + moveItemInArray(this.internalStates, event.previousIndex, event.currentIndex); + } +} diff --git a/src/keyframesModule/keyframeCtrl/keyframeCtrl.style.css b/src/keyframesModule/keyframeCtrl/keyframeCtrl.style.css new file mode 100644 index 0000000000000000000000000000000000000000..5a57375ec0d032b46d0553cc82e86ccbcfca162b --- /dev/null +++ b/src/keyframesModule/keyframeCtrl/keyframeCtrl.style.css @@ -0,0 +1,21 @@ +.cdk-drop-list-dragging :not(.cdk-drag-placeholder) +{ + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} +.cdk-drag-placeholder { + opacity: 0.2; +} +[cdkDragHandle] +{ + cursor:ns-resize; +} + +mat-form-field +{ + margin-right: 0.25rem; +} + +.duration-container +{ + width: 3.5rem; +} \ No newline at end of file diff --git a/src/keyframesModule/keyframeCtrl/keyframeCtrl.template.html b/src/keyframesModule/keyframeCtrl/keyframeCtrl.template.html new file mode 100644 index 0000000000000000000000000000000000000000..2fecef02aa1eff14b9936582e42ec5e2f88fbc57 --- /dev/null +++ b/src/keyframesModule/keyframeCtrl/keyframeCtrl.template.html @@ -0,0 +1,61 @@ +<ng-template #noCurrStateTmpl> + No current state recorded +</ng-template> + +<ng-template + [ngIf]="currState" + [ngIfElse]="noCurrStateTmpl"> + + <button + (click)="addKeyFrame()" + matTooltip="Add key frame. Shortcut: [a]" + mat-button + color="primary"> + Add Key Frame + </button> + + <mat-slide-toggle [(ngModel)]="loopFlag">Loop</mat-slide-toggle> + <mat-slide-toggle [(ngModel)]="linearFlag">Linear Camera</mat-slide-toggle> + + <button mat-icon-button + (click)="togglePlay()"> + <i class="fas fa-play"></i> + </button> + + <span *ngIf="false"> + <span *ngFor="let obj of currState | keyvalue"> + {{ obj['key'] }} + </span> + </span> +</ng-template> + +<mat-list cdkDropList (cdkDropListDropped)="drop($event)"> + <mat-list-item *ngFor="let state of internalStates" cdkDrag> + <button mat-icon-button + cdkDragHandle> + <i class="fas fa-grip-vertical"></i> + </button> + + <mat-form-field> + <mat-label>name</mat-label> + <input type="text" matInput [(ngModel)]="state.name"> + </mat-form-field> + + <mat-form-field class="duration-container"> + <mat-label>ms</mat-label> + <input type="number" matInput [(ngModel)]="state.duration"> + </mat-form-field> + + <button mat-icon-button + (click)="gotoFrame(state)"> + <i class="fas fa-map-marker-alt"></i> + </button> + + <button mat-icon-button + color="warn" + (click)="removeFrame(state)"> + <i class="fas fa-trash"></i> + </button> + + </mat-list-item> +</mat-list> \ No newline at end of file diff --git a/src/keyframesModule/module.ts b/src/keyframesModule/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..0042fdbf4a8dd60efc0f048897950236fba2e3b8 --- /dev/null +++ b/src/keyframesModule/module.ts @@ -0,0 +1,30 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { ComponentsModule } from "src/components"; +import { AngularMaterialModule } from "src/sharedModules"; +import { KeyFrameDirective } from "./keyframe.directive"; +import { KeyFrameCtrlCmp } from "./keyframeCtrl/keyframeCtrl.component"; +import { KeyFrameService } from "./service"; + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + ComponentsModule, + FormsModule, + ], + declarations: [ + KeyFrameCtrlCmp, + KeyFrameDirective, + ], + exports: [ + KeyFrameCtrlCmp, + KeyFrameDirective, + ], + providers: [ + KeyFrameService, + ] +}) + +export class KeyFrameModule{} \ No newline at end of file diff --git a/src/keyframesModule/service.ts b/src/keyframesModule/service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b807aa15ba8414c823bf48ab132fb01c29c19590 --- /dev/null +++ b/src/keyframesModule/service.ts @@ -0,0 +1,51 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { Store } from "@ngrx/store"; +import { BehaviorSubject, Subscription } from "rxjs"; +import { distinctUntilChanged } from "rxjs/operators"; +import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper"; +import { KEYFRAME_VIEWMODE } from "./constants"; +import { KeyFrameCtrlCmp } from "./keyframeCtrl/keyframeCtrl.component"; + +@Injectable() +export class KeyFrameService implements OnDestroy { + + inSession$ = new BehaviorSubject(false) + private subs: Subscription[] = [] + private _inSession = false + get inSession(){ + return this._inSession + } + set inSession(val){ + this._inSession = val + this.inSession$.next(val) + } + + ngOnDestroy(){ + while(this.subs.length) this.subs.pop().unsubscribe() + } + + startKeyFrameSession(){ + this.inSession = true + } + endKeyFrameSession(){ + this.inSession = false + } + + constructor( + // private dialog: MatDialog, + private store: Store<any> + ){ + this.inSession$.pipe( + distinctUntilChanged() + ).subscribe(flag => { + + // TODO enable side bar when ready + this.store.dispatch( + viewerStateSetViewerMode({ + payload: flag && KEYFRAME_VIEWMODE + }) + ) + }) + } +} \ No newline at end of file diff --git a/src/main.module.ts b/src/main.module.ts index bfe4020d475c53317d98064505ca26cd3d79ef57..088d031e28b98de0e59c2be73a2c6e548c806bdd 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -234,7 +234,7 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { }, { provide: BS_ENDPOINT, - useValue: (environment.BS_REST_URL || `https://siibra-api-latest.apps-dev.hbp.eu/v1_0`).replace(/\/$/, '') + useValue: ('https://siibra-api-edge.apps-dev.hbp.eu/v1_0' || environment.BS_REST_URL || `https://siibra-api-latest.apps-dev.hbp.eu/v1_0`).replace(/\/$/, '') }, ], bootstrap : [ diff --git a/src/ui/topMenu/module.ts b/src/ui/topMenu/module.ts index 9b4d781bb97bc6e21fa27a4453025e327a72727a..241a0e3d27543f3968c81d4964834b3f7dce2fa8 100644 --- a/src/ui/topMenu/module.ts +++ b/src/ui/topMenu/module.ts @@ -14,6 +14,7 @@ import { AngularMaterialModule } from "src/sharedModules"; import { TopMenuCmp } from "./topMenuCmp/topMenu.components"; import { UserAnnotationsModule } from "src/atlasComponents/userAnnotations"; import { QuickTourModule } from "src/ui/quickTour/module"; +import { KeyFrameModule } from "src/keyframesModule/module"; @NgModule({ imports: [ @@ -30,6 +31,7 @@ import { QuickTourModule } from "src/ui/quickTour/module"; AuthModule, ScreenshotModule, UserAnnotationsModule, + KeyFrameModule, QuickTourModule, ], declarations: [ diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts index d9adf9d8a823e893329e57832c411cc652464f1d..716a66662e49dbd00a17c97f1284aac92a6e616e 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts +++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts @@ -49,7 +49,8 @@ export class TopMenuCmp { public pluginTooltipText: string = `Plugins and Tools` public screenshotTooltipText: string = 'Take screenshot' public annotateTooltipText: string = 'Start annotating' - + public keyFrameText = `Start KeyFrames` + public quickTourData: IQuickTourData = { description: QUICKTOUR_DESC.TOP_MENU, order: 8, diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index 6982b1ad149a20377d938d8f0b9ebe5a752aac7d..c090670d792dccab9a0754e79e1d1197ef0a1ddc 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -163,6 +163,17 @@ Annotation mode </span> </button> + + <button mat-menu-item + [disabled]="!viewerLoaded" + key-frame-play-now + [matTooltip]="keyFrameText"> + <mat-icon fontSet="fas" fontIcon="fa-play"></mat-icon> + <span> + KeyFrames + </span> + </button> + <plugin-banner></plugin-banner> </mat-menu> diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts index 16258cba419a3bef23721f6a7143737a946dbbbd..b7962a9fe0ed9622e7237cb2b7cd7245ac9539ad 100644 --- a/src/util/pureConstant.service.ts +++ b/src/util/pureConstant.service.ts @@ -448,6 +448,17 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" return forkJoin( templateSpaces.map( tmpl => { + // hardcode + // see https://github.com/FZJ-INM1-BDA/siibra-python/issues/98 + if ( + tmpl.id === 'minds/core/referencespace/v1.0.0/tmp-fsaverage' + && !tmpl.availableParcellations.find(p => p.id === 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290') + ) { + tmpl.availableParcellations.push({ + id: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290', + name: 'Julich-Brain Probabilistic Cytoarchitectonic Maps (v2.9)' + }) + } ngLayerObj[tmpl.id] = {} return tmpl.availableParcellations.map( parc => this.getRegions(atlas['@id'], parc.id, tmpl.id).pipe( diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index eb6302174da652e6328def8c94531b160894e9aa..53d48a2ecc114144ba740b3f25231f2030882df9 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -26,6 +26,8 @@ import { ViewerStateBreadCrumbModule } from "./viewerStateBreadCrumb/module"; import { KgRegionalFeatureModule } from "src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature"; import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, AtlasViewerAPIServices, setViewerHandleFactory } from "src/atlasViewer/atlasViewer.apiService.service"; import { ILoadMesh, LOAD_MESH_TOKEN } from "src/messaging/types"; +import { KeyFrameModule } from "src/keyframesModule/module"; +import { ViewerInternalStateSvc } from "./viewerInternalState.service"; @NgModule({ imports: [ @@ -47,6 +49,7 @@ import { ILoadMesh, LOAD_MESH_TOKEN } from "src/messaging/types"; ContextMenuModule, ViewerStateBreadCrumbModule, KgRegionalFeatureModule, + KeyFrameModule, ], declarations: [ ViewerCmp, @@ -95,6 +98,7 @@ import { ILoadMesh, LOAD_MESH_TOKEN } from "src/messaging/types"; ] }, AtlasViewerAPIServices, + ViewerInternalStateSvc, ], exports: [ ViewerCmp, diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index 852a10faa56829c7ab8da8b87cbb749a19e04cc4..c4a9fb323bebe16410bb186e1a0e978bdcdfbbd3 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -14,8 +14,19 @@ import { REGION_OF_INTEREST } from "src/util/interfaces"; import { MatSnackBar } from "@angular/material/snack-bar"; import { CONST } from 'common/constants' import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service"; -import { switchMapWaitFor } from "src/util/fn"; - +import { getUuid, switchMapWaitFor } from "src/util/fn"; +import { TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service"; + +const viewerType = 'ThreeSurfer' +type TInternalState = { + camera: { + x: number + y: number + z: number + } + mode: string + hemisphere: 'left' | 'right' | 'both' +} const pZoomFactor = 5e3 type THandlingCustomEv = { @@ -88,6 +99,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af private localCameraNav: TCameraOrientation = null public allKeys: {name: string, checked: boolean}[] = [] + private internalStateNext: (arg: TInteralStatePayload<TInternalState>) => void private regionMap: Map<string, Map<number, any>> = new Map() private mouseoverRegions = [] @@ -96,10 +108,29 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af private store$: Store<any>, private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>, private snackbar: MatSnackBar, + @Optional() intViewerStateSvc: ViewerInternalStateSvc, @Optional() @Inject(REGION_OF_INTEREST) private roi$: Observable<any>, @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle, ){ + if (intViewerStateSvc) { + const { + done, + next, + } = intViewerStateSvc.registerEmitter({ + "@type": 'TViewerInternalStateEmitter', + viewerType, + applyState: arg => { + // type check + if (arg.viewerType !== viewerType) return + this.toTsRef(tsRef => { + tsRef.camera.position.copy((arg.payload as any).camera) + }) + } + }) + this.internalStateNext = next + this.onDestroyCb.push(() => done()) + } // set viewer handle // the API won't be 100% compatible with ngviewer @@ -474,7 +505,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af this.tsRef.dispose() this.tsRef = null } - ); + ) this.tsRef.control.enablePan = false while (this.tsRefInitCb.length > 0) this.tsRefInitCb.pop()(this.tsRef) } @@ -574,6 +605,18 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af private cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>() private handleCustomCameraEvent(detail: any){ + if (this.internalStateNext) { + this.internalStateNext({ + "@id": getUuid(), + "@type": 'TViewerInternalStateEmitterEvent', + viewerType, + payload: { + mode: this.selectedMode, + camera: detail.position, + hemisphere: 'both' + } + }) + } this.cameraEv$.next(detail) } diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index 735eba1520dca238a3688a96a4e35bbe68e14332..19b64d6b88116e5954dd0fb2226e6fc79b3dcc01 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -20,6 +20,10 @@ margin-top: 1.5rem; } +.tab-toggle-container > * +{ + display: block; +} .tab-toggle { diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index aa2004d86a9df45e61b54719dbd291f627df569e..30d2c0411ce039d73beba463374fe6a3e2bed884 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -6,34 +6,42 @@ <layout-floating-container [zIndex]="10"> <!-- Annotation mode --> - <div *ngIf="(viewerMode$ | async) === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> - <mat-drawer-container class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" + <mat-drawer-container *ngIf="viewerMode$ | async as viewerMode" + class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" [hasBackdrop]="false"> - <mat-drawer #annotationDrawer="matDrawer" + + <mat-drawer #viewerModeDrawer="matDrawer" mode="side" - (annotation-event-directive)="annotationDrawer.open()" + (annotation-event-directive)="viewerModeDrawer.open()" [annotation-event-directive-filter]="['showList']" [autoFocus]="false" [disableClose]="true" class="p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2"> - <annotation-list></annotation-list> - </mat-drawer> - <mat-drawer-content class="visible position-relative pe-none"> + <!-- annotation --> + <ng-template [ngIf]="viewerMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> + <annotation-list></annotation-list> + </ng-template> - <iav-layout-fourcorners> + <ng-template [ngIf]="viewerMode === ARIA_LABELS.VIEWER_MODE_KEYFRAME"> + <key-frame-controller></key-frame-controller> + </ng-template> + + </mat-drawer> + <mat-drawer-content class="visible position-relative pe-none"> + <!-- annotation specific --> + <iav-layout-fourcorners *ngIf="viewerMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> <!-- pullable tab top right corner --> <div iavLayoutFourCornersTopLeft class="tab-toggle-container"> - <div> - <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { - matColor: 'primary', - fontIcon: 'fa-list', - tooltip: 'Annotation list', - click: annotationDrawer.toggle.bind(annotationDrawer) - }"> - </ng-container> - </div> + + <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { + matColor: 'primary', + fontIcon: 'fa-list', + tooltip: 'Annotation list', + click: viewerModeDrawer.toggle.bind(viewerModeDrawer) + }"> + </ng-container> <annotating-tools-panel class="z-index-10"> </annotating-tools-panel> @@ -53,13 +61,42 @@ </button> </mat-card> </div> + </iav-layout-fourcorners> + <!-- key frame specific --> + <iav-layout-fourcorners *ngIf="viewerMode === ARIA_LABELS.VIEWER_MODE_KEYFRAME"> + <!-- pullable tab top right corner --> + <div iavLayoutFourCornersTopLeft class="tab-toggle-container"> + + <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { + matColor: 'primary', + fontIcon: 'fa-play', + tooltip: 'Annotation list', + click: viewerModeDrawer.toggle.bind(viewerModeDrawer) + }"> + </ng-container> + </div> + + <div iavLayoutFourCornersTopRight> + <mat-card class="mat-card-sm pe-all m-4"> + <span> + Key Frame + </span> + <button mat-icon-button + color="warn" + key-frame-play-now="off"> + <i class="fas fa-times"></i> + </button> + </mat-card> + </div> + + </iav-layout-fourcorners> </mat-drawer-content> + </mat-drawer-container> - <!-- <annotation-message></annotation-message> --> - </div> + <!-- top drawer --> <mat-drawer-container diff --git a/src/viewerModule/viewerInternalState.service.ts b/src/viewerModule/viewerInternalState.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..02600a20bdfe491fede2620b8566db3eb930a225 --- /dev/null +++ b/src/viewerModule/viewerInternalState.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; + +export type TInteralStatePayload<TPayloadShape> = { + '@type': 'TViewerInternalStateEmitterEvent' + '@id': string + viewerType: string + payload: TPayloadShape +} + +type TViewerInternalStateEmitter<TPayloadShape> = { + '@type': 'TViewerInternalStateEmitter' + viewerType: string + applyState: (arg: TInteralStatePayload<TPayloadShape>) => void +} + +type TEmitterCallbacks<TPayloadShape> = { + next: (arg: TInteralStatePayload<TPayloadShape>) => void + done: () => void +} + +@Injectable() +export class ViewerInternalStateSvc{ + + public viewerInternalState$ = new BehaviorSubject<TInteralStatePayload<any>>(null) + + private registeredEmitter: TViewerInternalStateEmitter<any> + + applyInternalState(arg: TInteralStatePayload<any>){ + if (!this.registeredEmitter) { + throw new Error(`No emitter registered. Aborting.`) + } + this.registeredEmitter.applyState(arg) + } + + registerEmitter<T>(emitter: TViewerInternalStateEmitter<T>): TEmitterCallbacks<T>{ + this.registeredEmitter = emitter + return { + next: arg => { + this.viewerInternalState$.next(arg) + }, + done: () => this.deregisterEmitter(emitter) + } + } + deregisterEmitter(emitter: TViewerInternalStateEmitter<any>){ + if (emitter === this.registeredEmitter) { + this.viewerInternalState$.next(null) + this.registeredEmitter = null + } + } +} \ No newline at end of file