diff --git a/.github/workflows/deploy-helm.yml b/.github/workflows/deploy-helm.yml index be72a299c06c12d47079e3cf035c9015d6598c78..59796a18dd55d38db2cd5c8a5b768568d07c2b69 100644 --- a/.github/workflows/deploy-helm.yml +++ b/.github/workflows/deploy-helm.yml @@ -33,6 +33,7 @@ jobs: helm --kubeconfig=$kubecfg_path \ upgrade \ --history-max 3 \ + --reuse-values \ --set image.tag=${{ inputs.IMAGE_TAG }} \ --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ @@ -53,8 +54,6 @@ jobs: upgrade \ --history-max 3 \ --reuse-values \ - --set envObj.HOST_PATHNAME=/viewer-staging \ - --set envObj.OVERWRITE_API_ENDPOINT=https://siibra-api-rc.apps.tc.humanbrainproject.eu/v3_0 \ --set image.tag=${{ inputs.IMAGE_TAG }} \ --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ @@ -75,9 +74,6 @@ jobs: upgrade \ --history-max 3 \ --reuse-values \ - --set envObj.OVERWRITE_EXPERIMENTAL_FLAG_ATTR=1 \ - --set envObj.HOST_PATHNAME=/viewer-exmpt \ - --set envObj.OVERWRITE_API_ENDPOINT=https://siibra-api-rc.apps.tc.humanbrainproject.eu/v3_0 \ --set image.tag=${{ inputs.IMAGE_TAG }} \ --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index ad5df0476fb791736a3fc94c6c3d710d29f88b9f..9fc356880df41ba0efa0a6ee86565e64d1ad0521 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -167,6 +167,19 @@ jobs: secrets: KUBECONFIG: ${{ secrets.KUBECONFIG }} + trigger-deploy-expmt-rancher: + if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'staging' && success() }} + needs: + - build-docker-img + - setting-vars + uses: ./.github/workflows/deploy-helm.yml + with: + DEPLOYMENT_NAME: expmt + IMAGE_TAG: staging + IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.GIT_DIGEST }} + secrets: + KUBECONFIG: ${{ secrets.KUBECONFIG }} + trigger-deploy-master-rancher: if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'master' && success() }} needs: diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts index 322d59bd25739beaf102a3870ffe014366758222..f11a937786a47d695031aca2cad5a6182f65e68e 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -2,7 +2,7 @@ import { Component, Inject, OnDestroy, Optional } from "@angular/core"; import { ModularUserAnnotationToolService } from "../tools/service"; import { ARIA_LABELS } from 'common/constants' import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util"; -import { TContextArg } from "src/viewerModule/viewer.interface"; +import { TViewerEvtCtxData } from "src/viewerModule/viewer.interface"; import { TContextMenuReg } from "src/contextMenuModule"; import { MatSnackBar } from 'src/sharedModules/angularMaterial.exports' @@ -26,7 +26,7 @@ export class AnnotationMode implements OnDestroy{ private modularToolSvc: ModularUserAnnotationToolService, snackbar: MatSnackBar, @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, - @Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu<TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>>> + @Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu<TContextMenuReg<TViewerEvtCtxData<'nehuba' | 'threeSurfer'>>> ) { /** diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 1d36acf63f87a8e269d43f85a449d36fb298d9f6..002fa318906e0728864392f73d567a31360cb300 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -18,6 +18,7 @@ import { atlasSelection } from "src/state"; import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { AnnotationLayer } from "src/atlasComponents/annotations"; import { translateV3Entities } from "src/atlasComponents/sapi/translateV3"; +import { HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; const LOCAL_STORAGE_KEY = 'userAnnotationKey' const ANNOTATION_LAYER_NAME = "modular_tool_layer_name" @@ -276,12 +277,52 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }) ) + #hoverMsgs: THoverConfig[] = [] + + #dismimssHoverMsgs(){ + if (!this.hoverInterceptor) { + return + } + const { remove } = this.hoverInterceptor + for (const msg of this.#hoverMsgs){ + remove(msg) + } + } + #appendHoverMsgs(geometries: IAnnotationGeometry[]){ + if (!this.hoverInterceptor) { + return + } + const { append } = this.hoverInterceptor + this.#hoverMsgs = geometries.map(geom => { + let fontIcon = 'fa-file' + if (geom.annotationType === 'Point') { + fontIcon = 'fa-circle' + } + if (geom.annotationType === 'Line') { + fontIcon = 'fa-slash' + } + if (geom.annotationType === 'Polygon') { + fontIcon = 'fa-draw-polygon' + } + return { + message: geom.name || `Unnamed ${geom.annotationType}`, + fontSet: 'fas', + fontIcon + } + }) + for (const msg of this.#hoverMsgs){ + append(msg) + } + } + constructor( private store: Store<any>, private snackbar: MatSnackBar, @Inject(INJ_ANNOT_TARGET) annotTarget$: Observable<HTMLElement>, @Inject(ANNOTATION_EVENT_INJ_TOKEN) private annotnEvSubj: Subject<TAnnotationEvent<keyof IAnnotationEvents>>, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable<NehubaViewerUnit>, + @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) + private hoverInterceptor: HoverInterceptor, ){ /** @@ -323,7 +364,13 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } } as TAnnotationEvent<'mousedown' | 'mouseup' | 'mousemove'> this.annotnEvSubj.next(payload) - }) + }), + this.hoveringAnnotations$.subscribe(ev => { + this.#dismimssHoverMsgs() + if (ev) { + this.#appendHoverMsgs([ev]) + } + }), ) /** diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index acbcfd87f578637a6c94ac47df9c5e46f24dff7b..e326167d14b816b5723139c7b60d44fa14cec712 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -15,7 +15,6 @@ import { Observable, Subscription, merge, timer, fromEvent } from "rxjs"; import { filter, delay, switchMapTo, take, startWith } from "rxjs/operators"; import { colorAnimation } from "./atlasViewer.animation" -import { MouseHoverDirective } from "src/mouseoverModule"; import { MatSnackBar } from 'src/sharedModules/angularMaterial.exports' import { MatDialog, MatDialogRef } from "src/sharedModules/angularMaterial.exports"; import { CONST } from 'common/constants' @@ -49,8 +48,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @ViewChild('cookieAgreementComponent', {read: TemplateRef}) public cookieAgreementComponent: TemplateRef<any> - @ViewChild(MouseHoverDirective) private mouseOverNehuba: MouseHoverDirective - @ViewChild('idleOverlay', {read: TemplateRef}) idelTmpl: TemplateRef<any> @HostBinding('attr.ismobile') diff --git a/src/contextMenuModule/ctxMenuHost.directive.ts b/src/contextMenuModule/ctxMenuHost.directive.ts index 3f1b2b7b8f82a29bdb28147e3cfd652940fa42d8..71bcff9e308d7779c0c41afd4f8310437dc155ec 100644 --- a/src/contextMenuModule/ctxMenuHost.directive.ts +++ b/src/contextMenuModule/ctxMenuHost.directive.ts @@ -1,6 +1,6 @@ import { AfterViewInit, Directive, HostListener, Input, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core"; import { ContextMenuService } from "./service"; -import { TContextArg } from "src/viewerModule/viewer.interface"; +import { TViewerEvtCtxData } from "src/viewerModule/viewer.interface"; @Directive({ selector: '[ctx-menu-host]' @@ -18,7 +18,7 @@ export class CtxMenuHost implements OnDestroy, AfterViewInit{ constructor( private vcr: ViewContainerRef, - private svc: ContextMenuService<TContextArg<'nehuba' | 'threeSurfer'>>, + private svc: ContextMenuService<TViewerEvtCtxData<'nehuba' | 'threeSurfer'>>, ){ } diff --git a/src/contextMenuModule/dismissCtxMenu.directive.ts b/src/contextMenuModule/dismissCtxMenu.directive.ts index 36576fccce8e8a52f6b7c090d5a56e5bf9ce5d1b..dd57df2ac5b3ba3263f40b9577dac31d52be29a5 100644 --- a/src/contextMenuModule/dismissCtxMenu.directive.ts +++ b/src/contextMenuModule/dismissCtxMenu.directive.ts @@ -1,5 +1,5 @@ import { Directive, HostListener } from "@angular/core"; -import { TContextArg } from "src/viewerModule/viewer.interface"; +import { TViewerEvtCtxData } from "src/viewerModule/viewer.interface"; import { ContextMenuService } from "./service"; @Directive({ @@ -13,7 +13,7 @@ export class DismissCtxMenuDirective{ } constructor( - private svc: ContextMenuService<TContextArg<'threeSurfer' | 'nehuba'>> + private svc: ContextMenuService<TViewerEvtCtxData<'threeSurfer' | 'nehuba'>> ){ } diff --git a/src/features/voi-bbox.directive.ts b/src/features/voi-bbox.directive.ts index f150225b9cf66f5ce2f68f26c7a936f1c30f6576..5408bc08c515814506312ce1643506d027216bcb 100644 --- a/src/features/voi-bbox.directive.ts +++ b/src/features/voi-bbox.directive.ts @@ -1,25 +1,27 @@ -import { Directive, Inject, Input, OnDestroy, Optional } from "@angular/core"; +import { Directive, Inject, Input, Optional, inject } from "@angular/core"; import { Store } from "@ngrx/store"; -import { concat, interval, of, Subject, Subscription } from "rxjs"; -import { debounce, distinctUntilChanged, filter, pairwise, take } from "rxjs/operators"; +import { concat, interval, of, Subject } from "rxjs"; +import { debounce, distinctUntilChanged, filter, pairwise, take, takeUntil } from "rxjs/operators"; import { AnnotationLayer, TNgAnnotationAABBox, TNgAnnotationPoint } from "src/atlasComponents/annotations"; import { Feature, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes"; import { userInteraction } from "src/state"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { arrayEqual } from "src/util/array"; import { isVoiData } from "./guards" +import { DestroyDirective } from "src/util/directives/destroy.directive"; +import { HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; @Directive({ selector: '[voiBbox]', + hostDirectives: [ DestroyDirective ] }) -export class VoiBboxDirective implements OnDestroy { - - #onDestroyCb: (() => void)[] = [] +export class VoiBboxDirective { + + #destory$ = inject(DestroyDirective).destroyed$ static VOI_LAYER_NAME = 'voi-annotation-layer' static VOI_ANNOTATION_COLOR = "#ffff00" - #voiSubs: Subscription[] = [] private _voiBBoxSvc: AnnotationLayer get voiBBoxSvc(): AnnotationLayer { if (this._voiBBoxSvc) return this._voiBBoxSvc @@ -29,14 +31,15 @@ export class VoiBboxDirective implements OnDestroy { VoiBboxDirective.VOI_ANNOTATION_COLOR ) this._voiBBoxSvc = layer - this.#voiSubs.push( - layer.onHover.subscribe(val => this.handleOnHoverFeature(val || {})) - ) - this.#onDestroyCb.push(() => { + layer.onHover.pipe( + takeUntil(this.#destory$) + ).subscribe(val => this.handleOnHoverFeature(val || {})) + + this.#destory$.subscribe(() => { this._voiBBoxSvc.dispose() this._voiBBoxSvc = null }) - return layer + return this._voiBBoxSvc } catch (e) { return null } @@ -54,25 +57,27 @@ export class VoiBboxDirective implements OnDestroy { return this.#voiFeatures } - ngOnDestroy(): void { - while (this.#onDestroyCb.length > 0) this.#onDestroyCb.pop()() - } + #hoverMsgs: THoverConfig[] = [] constructor( private store: Store, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) + clickInterceptor: ClickInterceptor, + @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) + private hoverInterceptor: HoverInterceptor, ){ if (clickInterceptor) { const { register, deregister } = clickInterceptor const handleClick = this.handleClick.bind(this) register(handleClick) - this.#onDestroyCb.push(() => deregister(handleClick)) + this.#destory$.subscribe(() => deregister(handleClick)) } - const sub = concat( + concat( of([] as VoiFeature[]), this.#features$ ).pipe( + takeUntil(this.#destory$), distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)), pairwise(), debounce(() => @@ -109,10 +114,38 @@ export class VoiBboxDirective implements OnDestroy { if (this.voiBBoxSvc) this.voiBBoxSvc.setVisible(true) }) - this.#onDestroyCb.push(() => sub.unsubscribe()) - this.#onDestroyCb.push(() => this.store.dispatch( - userInteraction.actions.setMouseoverVoi({ feature: null }) - )) + this.#destory$.subscribe(() => { + this.store.dispatch( + userInteraction.actions.setMouseoverVoi({ feature: null }) + ) + this.#dismissHoverMsg() + }) + } + + #dismissHoverMsg(){ + if (!this.hoverInterceptor) { + return + } + + const { remove } = this.hoverInterceptor + for (const msg of this.#hoverMsgs){ + remove(msg) + } + } + + #appendHoverMsg(feats: VoiFeature[]){ + if (!this.hoverInterceptor) { + return + } + const { append } = this.hoverInterceptor + this.#hoverMsgs = feats.map(feat => ({ + message: `${feat?.name}`, + fontIcon: 'fa-database', + fontSet: 'fas' + })) + for (const msg of this.#hoverMsgs){ + append(msg) + } } handleClick(){ @@ -135,6 +168,10 @@ export class VoiBboxDirective implements OnDestroy { this.store.dispatch( userInteraction.actions.setMouseoverVoi({ feature }) ) + this.#dismissHoverMsg() + if (feature) { + this.#appendHoverMsg([feature]) + } } #pointsToAABB(pointA: [number, number, number], pointB: [number, number, number]): TNgAnnotationAABBox{ diff --git a/src/mouseoverModule/index.ts b/src/mouseoverModule/index.ts index 8dea7b959fa47feda07047ca34a1fa0d6e204cec..a419a4a25c7904732c185080109b0762a391bfb4 100644 --- a/src/mouseoverModule/index.ts +++ b/src/mouseoverModule/index.ts @@ -1,3 +1,2 @@ -export { MouseHoverDirective } from './mouseover.directive' -export { MouseoverModule } from './mouseover.module' -export { TransformOnhoverSegmentPipe } from './transformOnhoverSegment.pipe' \ No newline at end of file +export { MouseOver } from "./mouseover.component" +export { MouseOverSvc } from "./service" diff --git a/src/mouseoverModule/mouseOverCvt.pipe.ts b/src/mouseoverModule/mouseOverCvt.pipe.ts deleted file mode 100644 index fabd1d4899843579d939eb3ca94162b4ab85cc2e..0000000000000000000000000000000000000000 --- a/src/mouseoverModule/mouseOverCvt.pipe.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { TOnHoverObj } from "./util"; - -function render<T extends keyof TOnHoverObj>(key: T, value: TOnHoverObj[T]){ - if (!value) return [] - switch (key) { - case 'regions': { - return (value as TOnHoverObj['regions']).map(seg => { - return { - icon: { - fontSet: 'fas', - fontIcon: 'fa-brain', - cls: 'fas fa-brain', - }, - text: seg?.name || "Unknown" - } - }) - } - case 'voi': { - const { name } = value as TOnHoverObj['voi'] - return [{ - icon: { - fontSet: 'fas', - fontIcon: 'fa-database', - cls: 'fas fa-database' - }, - text: name - }] - } - case 'annotation': { - const { annotationType, name } = (value as TOnHoverObj['annotation']) - let fontIcon: string - if (annotationType === 'Point') fontIcon = 'fa-circle' - if (annotationType === 'Line') fontIcon = 'fa-slash' - if (annotationType === 'Polygon') fontIcon = 'fa-draw-polygon' - if (!annotationType) fontIcon = 'fa-file' - return [{ - icon: { - fontSet: 'fas', - fontIcon, - cls: `fas ${fontIcon}`, - }, - text: name || `Unnamed ${annotationType}` - }] - } - default: { - return [{ - icon: { - fontSet: 'fas', - fontIcon: 'fa-file', - cls: 'fas fa-file' - }, - text: `Unknown hovered object` - }] - } - } -} - -type TCvtOutput = { - icon: { - fontSet: string - fontIcon: string - cls: string - } - text: string -} - -@Pipe({ - name: 'mouseoverCvt', - pure: true -}) - -export class MouseOverConvertPipe implements PipeTransform{ - - public transform(dict: TOnHoverObj){ - const output: TCvtOutput[] = [] - for (const key in dict) { - output.push( - ...render(key as keyof TOnHoverObj, dict[key]) - ) - } - return output - } -} \ No newline at end of file diff --git a/src/mouseoverModule/mouseover.component.ts b/src/mouseoverModule/mouseover.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c5b58b5e28ca203e16851ce884398d16ef6747b --- /dev/null +++ b/src/mouseoverModule/mouseover.component.ts @@ -0,0 +1,22 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { AngularMaterialModule } from "src/sharedModules"; +import { MouseOverSvc } from "./service"; + +@Component({ + selector: 'mouseover-info', + templateUrl: './mouseover.template.html', + styleUrls: [ + './mouseover.style.css' + ], + standalone: true, + imports: [ + AngularMaterialModule, + CommonModule + ], +}) + +export class MouseOver { + constructor(private svc: MouseOverSvc) {} + messages$ = this.svc.messages$ +} diff --git a/src/mouseoverModule/mouseover.directive.ts b/src/mouseoverModule/mouseover.directive.ts deleted file mode 100644 index a69fccd7630c0be2a35d7ec73d2dd1f0ed7a6d53..0000000000000000000000000000000000000000 --- a/src/mouseoverModule/mouseover.directive.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Directive } from "@angular/core" -import { select, Store } from "@ngrx/store" -import { merge, Observable } from "rxjs" -import { distinctUntilChanged, map, scan } from "rxjs/operators" -import { TOnHoverObj, temporalPositveScanFn } from "./util" -import { ModularUserAnnotationToolService } from "src/atlasComponents/userAnnotations/tools/service"; -import { userInteraction } from "src/state" -import { arrayEqual } from "src/util/array" -import { MouseOverSvc } from "./service" - -@Directive({ - selector: '[iav-mouse-hover]', - exportAs: 'iavMouseHover', -}) - -export class MouseHoverDirective { - - /** - * TODO move - * - mousing over regions - * - hovering annotation - * - hovering voi feature - * to use hover interceptor - */ - public currentOnHoverObs$: Observable<TOnHoverObj> = merge( - this.store$.pipe( - select(userInteraction.selectors.mousingOverRegions), - ).pipe( - distinctUntilChanged(arrayEqual((o, n) => o?.name === n?.name)), - map(regions => { - return { regions } - }), - ), - this.annotSvc.hoveringAnnotations$.pipe( - distinctUntilChanged(), - map(annotation => { - return { annotation } - }), - ), - this.store$.pipe( - select(userInteraction.selectors.mousingOverVoiFeature), - distinctUntilChanged((o, n) => o?.id === n?.id), - map(voi => ({ voi })) - ) - ).pipe( - scan(temporalPositveScanFn, []), - map(arr => { - - let returnObj: TOnHoverObj = { - regions: null, - annotation: null, - voi: null - } - - for (const val of arr) { - returnObj = { - ...returnObj, - ...val - } - } - - return returnObj - }), - ) - - constructor( - private store$: Store<any>, - private annotSvc: ModularUserAnnotationToolService, - private svc: MouseOverSvc, - ) { - } - - messages$ = this.svc.messages$ -} diff --git a/src/mouseoverModule/mouseover.module.ts b/src/mouseoverModule/mouseover.module.ts deleted file mode 100644 index f4c54934842b01a3b656136c63d0706603a178c5..0000000000000000000000000000000000000000 --- a/src/mouseoverModule/mouseover.module.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { TransformOnhoverSegmentPipe } from "./transformOnhoverSegment.pipe"; -import { MouseHoverDirective } from "./mouseover.directive"; -import { MouseOverConvertPipe } from "./mouseOverCvt.pipe"; -import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; -import { MouseOverSvc } from "./service"; - - -@NgModule({ - imports: [ - CommonModule, - ], - declarations: [ - MouseHoverDirective, - TransformOnhoverSegmentPipe, - MouseOverConvertPipe, - ], - exports: [ - MouseHoverDirective, - TransformOnhoverSegmentPipe, - MouseOverConvertPipe, - ], - providers: [ - MouseOverSvc, - { - provide: HOVER_INTERCEPTOR_INJECTOR, - useFactory: (svc: MouseOverSvc) => { - return { - append: svc.append.bind(svc), - remove: svc.remove.bind(svc), - } - }, - deps: [ MouseOverSvc ] - } - ] -}) - -export class MouseoverModule{} diff --git a/src/mouseoverModule/mouseover.style.css b/src/mouseoverModule/mouseover.style.css new file mode 100644 index 0000000000000000000000000000000000000000..15f65ddebd5149c9bafcf843bba786d1e4740009 --- /dev/null +++ b/src/mouseoverModule/mouseover.style.css @@ -0,0 +1,11 @@ +:host +{ + display: inline-block; +} + +.centered +{ + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/mouseoverModule/mouseover.template.html b/src/mouseoverModule/mouseover.template.html new file mode 100644 index 0000000000000000000000000000000000000000..00d649b30be35b8820dc51e1b4eb0d9c1d315101 --- /dev/null +++ b/src/mouseoverModule/mouseover.template.html @@ -0,0 +1,17 @@ +<mat-list> + <ng-template ngFor [ngForOf]="messages$ | async" let-message> + + <ng-template [ngIf]="message.fontIcon && message.fontSet" [ngIfElse]="noIconTmpl"> + <mat-list-item class="h-auto"> + <span [class]="message.fontSet + ' centered ' + message.fontIcon" matListItemIcon></span> + <span matListItemTitle>{{ message.message }}</span> + </mat-list-item> + </ng-template> + + <ng-template #noIconTmpl> + <mat-list-item class="h-auto"> + <span matListItemTitle>{{ message.message }}</span> + </mat-list-item> + </ng-template> + </ng-template> +</mat-list> diff --git a/src/mouseoverModule/service.ts b/src/mouseoverModule/service.ts index eba057bcca83fab20a3e4afebee18de5c8d54efe..284faa28fa0614936ec90b97bb3c698fcc00fcec 100644 --- a/src/mouseoverModule/service.ts +++ b/src/mouseoverModule/service.ts @@ -1,17 +1,24 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject } from "rxjs"; +import { debounceTime, shareReplay } from "rxjs/operators"; import { THoverConfig } from "src/util/injectionTokens"; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class MouseOverSvc { #messages: THoverConfig[] = [] - messages$ = new BehaviorSubject(this.#messages) + #messages$ = new BehaviorSubject(this.#messages) + messages$ = this.#messages$.pipe( + debounceTime(16), + shareReplay(1), + ) set messages(messages: THoverConfig[]){ this.#messages = messages - this.messages$.next(this.#messages) + this.#messages$.next(this.#messages) } get messages(): THoverConfig[]{ diff --git a/src/mouseoverModule/transformOnhoverSegment.pipe.ts b/src/mouseoverModule/transformOnhoverSegment.pipe.ts deleted file mode 100644 index 5199a582a1ba2e5084d7097996652a492211a342..0000000000000000000000000000000000000000 --- a/src/mouseoverModule/transformOnhoverSegment.pipe.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Pipe, PipeTransform, SecurityContext } from "@angular/core"; -import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; - -@Pipe({ - name: 'transformOnhoverSegment', -}) - -export class TransformOnhoverSegmentPipe implements PipeTransform { - constructor(private sanitizer: DomSanitizer) { - - } - - private sanitizeHtml(inc: string): SafeHtml { - return this.sanitizer.sanitize(SecurityContext.HTML, inc) - } - - private getStatus(text: string) { - return ` <span class="text-muted">(${this.sanitizeHtml(text)})</span>` - } - - public transform(segment: any | number): SafeHtml { - return this.sanitizer.bypassSecurityTrustHtml(( - ( this.sanitizeHtml(segment.name) || segment) + - (segment.status - ? this.getStatus(segment.status) - : '') - )) - } -} diff --git a/src/mouseoverModule/util.spec.ts b/src/mouseoverModule/util.spec.ts deleted file mode 100644 index 31920019b049537cadb4df833acc11a8d8faf20c..0000000000000000000000000000000000000000 --- a/src/mouseoverModule/util.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {} from 'jasmine' -import { forkJoin, Subject } from 'rxjs'; -import { scan, skip, take } from 'rxjs/operators'; -import { temporalPositveScanFn } from './util' - -const segmentsPositive = { segments: [{ hello: 'world' }] } as {segments: any} -const segmentsNegative = { segments: [] } - -const userLandmarkPostive = { userLandmark: true } -const userLandmarkNegative = { userLandmark: null } - -describe('temporalPositveScanFn', () => { - const subscriptions = [] - afterAll(() => { - while (subscriptions.length > 0) { subscriptions.pop().unsubscribe() } - }) - - it('should scan obs as expected', (done) => { - - const source = new Subject() - - const testFirstEv = source.pipe( - scan(temporalPositveScanFn, []), - take(1), - ) - - const testSecondEv = source.pipe( - scan(temporalPositveScanFn, []), - skip(1), - take(1), - ) - - const testThirdEv = source.pipe( - scan(temporalPositveScanFn, []), - skip(2), - take(1), - ) - - const testFourthEv = source.pipe( - scan(temporalPositveScanFn, []), - skip(3), - take(1), - ) - - forkJoin([ - testFirstEv, - testSecondEv, - testThirdEv, - testFourthEv, - ]).pipe( - take(1), - ).subscribe(([ arr1, arr2, arr3, arr4 ]) => { - expect(arr1).toEqual([ segmentsPositive ] as any) - expect(arr2).toEqual([ userLandmarkPostive, segmentsPositive ] as any) - expect(arr3).toEqual([ userLandmarkPostive ] as any) - expect(arr4).toEqual([]) - done() - }) - - source.next(segmentsPositive) - source.next(userLandmarkPostive) - source.next(segmentsNegative) - source.next(userLandmarkNegative) - }) -}) diff --git a/src/mouseoverModule/util.ts b/src/mouseoverModule/util.ts deleted file mode 100644 index 208c01ceebea6038619ee5cb781799632c14d2d7..0000000000000000000000000000000000000000 --- a/src/mouseoverModule/util.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { SxplrRegion, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes" -import { IAnnotationGeometry } from "src/atlasComponents/userAnnotations/tools/type" - -export type TOnHoverObj = { - regions: SxplrRegion[] - annotation: IAnnotationGeometry - voi: VoiFeature -} - -/** - * Scan function which prepends newest positive (i.e. defined) value - * - * e.g. const source = new Subject() - * source.pipe( - * scan(temporalPositveScanFn, []) - * ).subscribe(this.log.log) // outputs - * - * - * - */ -export const temporalPositveScanFn = (acc: Array<TOnHoverObj>, curr: Partial<TOnHoverObj>) => { - - const keys = Object.keys(curr) - - // empty array is truthy - const isPositive = keys.some(key => Array.isArray(curr[key]) - ? curr[key].length > 0 - : !!curr[key] - ) - - return isPositive - ? [curr, ...(acc.filter(item => !keys.some(key => !!item[key])))] as Array<TOnHoverObj> - : acc.filter(item => !keys.some(key => !!item[key])) -} diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 81d404525fd4acdf0b91171a9299d4dc0c5c3b56..2ecdfdf8b203769783b1f47623f60f3c3244ee45 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -15,14 +15,14 @@ import { QuickTourModule } from "src/ui/quickTour/module"; import { INJ_ANNOT_TARGET } from "src/atlasComponents/userAnnotations/tools/type"; import { NEHUBA_INSTANCE_INJTKN } from "./nehuba/util"; import { map, switchMap } from "rxjs/operators"; -import { TContextArg } from "./viewer.interface"; +import { TViewerEvtCtxData } from "./viewer.interface"; import { KeyFrameModule } from "src/keyframesModule/module"; import { ViewerInternalStateSvc } from "./viewerInternalState.service"; import { SAPI, SAPIModule } from 'src/atlasComponents/sapi'; import { NehubaVCtxToBbox } from "./pipes/nehubaVCtxToBbox.pipe"; import { SapiViewsModule, SapiViewsUtilModule } from "src/atlasComponents/sapiViews"; import { DialogModule } from "src/ui/dialogInfo/module"; -import { MouseoverModule } from "src/mouseoverModule"; +import { MouseOver, MouseOverSvc } from "src/mouseoverModule"; import { LogoContainer } from "src/ui/logoContainer/logoContainer.component"; import { FloatingMouseContextualContainerDirective } from "src/util/directives/floatingMouseContextualContainer.directive"; import { ShareModule } from "src/share"; @@ -40,6 +40,8 @@ import { Store } from "@ngrx/store"; import { atlasSelection, userPreference } from "src/state"; import { TabComponent } from "src/components/tab/tab.components"; import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; +import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; +import { ViewerWrapper } from "./viewerWrapper/viewerWrapper.component"; @NgModule({ imports: [ @@ -59,7 +61,6 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di SapiViewsModule, SapiViewsUtilModule, DialogModule, - MouseoverModule, ShareModule, ATPSelectorModule, FeatureModule, @@ -69,6 +70,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di BottomMenuModule, TabComponent, + MouseOver, ExperimentalFlagDirective, ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) @@ -78,6 +80,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di NehubaVCtxToBbox, LogoContainer, FloatingMouseContextualContainerDirective, + ViewerWrapper, ], providers: [ { @@ -93,11 +96,11 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di }, { provide: CONTEXT_MENU_ITEM_INJECTOR, - useFactory: (svc: ContextMenuService<TContextArg<'threeSurfer' | 'nehuba'>>) => { + useFactory: (svc: ContextMenuService<TViewerEvtCtxData<'threeSurfer' | 'nehuba'>>) => { return { register: svc.register.bind(svc), deregister: svc.deregister.bind(svc) - } as TContextMenu<TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>>> + } as TContextMenu<TContextMenuReg<TViewerEvtCtxData<'nehuba' | 'threeSurfer'>>> }, deps: [ ContextMenuService ] }, @@ -141,6 +144,17 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di ), deps: [ Store, SAPI ] }, + + { + provide: HOVER_INTERCEPTOR_INJECTOR, + useFactory: (svc: MouseOverSvc) => { + return { + append: svc.append.bind(svc), + remove: svc.remove.bind(svc), + } + }, + deps: [ MouseOverSvc ] + } ], exports: [ ViewerCmp, diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts index f7e7026c186bf4aa75d663587ffc1c25053ed1f6..d6fb5813fc69c786158e2ac560541bd206ceb0a1 100644 --- a/src/viewerModule/nehuba/module.ts +++ b/src/viewerModule/nehuba/module.ts @@ -12,7 +12,6 @@ import { NehubaGlueCmp } from "./nehubaViewerGlue/nehubaViewerGlue.component"; import { UtilModule } from "src/util"; import { ComponentsModule } from "src/components"; import { AngularMaterialModule } from "src/sharedModules"; -import { MouseoverModule } from "src/mouseoverModule"; import { StatusCardComponent } from "./statusCard/statusCard.component"; import { ShareModule } from "src/share"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; @@ -40,7 +39,6 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di UtilModule, AngularMaterialModule, ComponentsModule, - MouseoverModule, ShareModule, WindowResizeModule, NehubaUserLayerModule, diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts index 10d189784c311b6de0c8557e212e86a05774a2ea..263982f2d3c530a3368155b347bd20079b2aba9c 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts @@ -169,85 +169,5 @@ describe('> nehubaViewerGlue.component.ts', () => { expect(fixture.componentInstance).toBeTruthy() }) - describe('> selectHoveredRegion', () => { - let dispatchSpy: jasmine.Spy - let clickIntServ: ClickInterceptorService - beforeEach(() => { - dispatchSpy = spyOn(mockStore, 'dispatch') - clickIntServ = TestBed.inject(ClickInterceptorService) - }) - afterEach(() => { - dispatchSpy.calls.reset() - }) - - describe('> if on hover is empty array', () => { - let fallbackSpy: jasmine.Spy - beforeEach(() => { - fallbackSpy = spyOn(clickIntServ, 'fallback') - TestBed.createComponent(NehubaGlueCmp) - clickIntServ.callRegFns(null) - }) - it('> dispatch not called', () => { - expect(dispatchSpy).not.toHaveBeenCalled() - }) - it('> fallback called', () => { - expect(fallbackSpy).toHaveBeenCalled() - }) - }) - - describe('> if on hover is non object array', () => { - let fallbackSpy: jasmine.Spy - - const testObj0 = { - segment: 'hello world' - } - const testObj1 = 'hello world' - beforeEach(() => { - fallbackSpy = spyOn(clickIntServ, 'fallback') - TestBed.createComponent(NehubaGlueCmp) - clickIntServ.callRegFns(null) - }) - it('> dispatch not called', () => { - expect(dispatchSpy).not.toHaveBeenCalled() - }) - it('> fallback called', () => { - expect(fallbackSpy).toHaveBeenCalled() - }) - }) - - describe('> if on hover array containing at least 1 obj, only dispatch the first obj', () => { - let fallbackSpy: jasmine.Spy - const testObj0 = { - segment: 'hello world' - } - const testObj1 = { - segment: { - foo: 'baz' - } - } - const testObj2 = { - segment: { - hello: 'world' - } - } - beforeEach(() => { - fallbackSpy = spyOn(clickIntServ, 'fallback') - - }) - afterEach(() => { - fallbackSpy.calls.reset() - }) - it('> dispatch called with obj1', () => { - TestBed.createComponent(NehubaGlueCmp) - clickIntServ.callRegFns(null) - const { segment } = testObj1 - }) - it('> fallback called (does not intercept)', () => { - TestBed.createComponent(NehubaGlueCmp) - clickIntServ.callRegFns(null) - expect(fallbackSpy).toHaveBeenCalled() - }) - }) - }) }) diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 1f47b5680c88ac1c21faeca6a89104d4f3e54ce7..a32c784f1ef2af37c0699e5669cec5d2e09ca9d5 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -1,15 +1,10 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Inject, OnDestroy, Optional, Output } from "@angular/core"; -import { select, Store } from "@ngrx/store"; -import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; -import { distinctUntilChanged } from "rxjs/operators"; +import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, Output } from "@angular/core"; import { IViewer, TViewerEvent } from "../../viewer.interface"; import { NehubaMeshService } from "../mesh.service"; import { NehubaLayerControlService, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service"; import { EXTERNAL_LAYER_CONTROL, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util"; -import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"; import { NehubaConfig } from "../config.service"; import { SET_MESHES_TO_LOAD } from "../constants"; -import { atlasSelection, userInteraction } from "src/state"; @Component({ @@ -58,7 +53,6 @@ import { atlasSelection, userInteraction } from "src/state"; export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy { - private onhoverSegments: SxplrRegion[] = [] private onDestroyCb: (() => void)[] = [] public nehubaConfig: NehubaConfig @@ -70,53 +64,4 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy { @Output() public viewerEvent = new EventEmitter<TViewerEvent<'nehuba'>>() - constructor( - private store$: Store<any>, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, - ){ - - /** - * define onclick behaviour - */ - if (clickInterceptor) { - const { deregister, register } = clickInterceptor - const selOnhoverRegion = this.selectHoveredRegion.bind(this) - register(selOnhoverRegion, { last: true }) - this.onDestroyCb.push(() => deregister(selOnhoverRegion)) - } - - /** - * on hover segment - */ - const onhovSegSub = this.store$.pipe( - select(userInteraction.selectors.mousingOverRegions), - distinctUntilChanged(), - ).subscribe(arr => { - this.onhoverSegments = arr - }) - this.onDestroyCb.push(() => onhovSegSub.unsubscribe()) - } - - private selectHoveredRegion(ev: PointerEvent): boolean{ - /** - * If label indicies are not defined by the ontology, it will be a string in the format of `{ngId}#{labelIndex}` - */ - const trueOnhoverSegments = this.onhoverSegments && this.onhoverSegments.filter(v => typeof v === 'object') - if (!trueOnhoverSegments || (trueOnhoverSegments.length === 0)) return true - - if (ev.ctrlKey) { - this.store$.dispatch( - atlasSelection.actions.toggleRegion({ - region: trueOnhoverSegments[0] - }) - ) - } else { - this.store$.dispatch( - atlasSelection.actions.selectRegion({ - region: trueOnhoverSegments[0] - }) - ) - } - return true - } } diff --git a/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts b/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts index e3ceaad4673e198e11eba7ca257ed0a334bb1ac6..37a68c4d3aef4acd4ef3474f2e0e4ad4fae1c5cb 100644 --- a/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts +++ b/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts @@ -1,4 +1,4 @@ -import { TContextArg } from './../viewer.interface'; +import { TViewerEvtCtxData } from './../viewer.interface'; import { Pipe, PipeTransform } from "@angular/core"; type Point = [number, number, number] @@ -12,7 +12,7 @@ const MAGIC_RADIUS = 256 }) export class NehubaVCtxToBbox implements PipeTransform{ - public transform(event: TContextArg<'nehuba' | 'threeSurfer'>, unit: string = "mm"): BBox{ + public transform(event: TViewerEvtCtxData<'nehuba' | 'threeSurfer'>, unit: string = "mm"): BBox{ if (!event) { return null } @@ -23,7 +23,7 @@ export class NehubaVCtxToBbox implements PipeTransform{ if (unit === "mm") { divisor = 1e6 } - const { payload } = event as TContextArg<'nehuba'> + const { payload } = event as TViewerEvtCtxData<'nehuba'> if (!payload.nav) return null diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index 9a44a02e1cd012f11d351e66d8b7b5cc195a4336..4a5c1238064d88be6953b2e65411e7c00e25862d 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -1,12 +1,10 @@ -import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Inject, Optional, ChangeDetectionStrategy } from "@angular/core"; +import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Optional, ChangeDetectionStrategy } from "@angular/core"; import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface"; import { BehaviorSubject, combineLatest, concat, forkJoin, from, merge, NEVER, Observable, of, Subject } from "rxjs"; import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, startWith, switchMap, tap, withLatestFrom } from "rxjs/operators"; import { ComponentStore, LockError } from "src/viewerModule/componentStore"; import { select, Store } from "@ngrx/store"; -import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { MatSnackBar } from "src/sharedModules/angularMaterial.exports" -import { CONST } from 'common/constants' import { getUuid, switchMapWaitFor } from "src/util/fn"; import { AUTO_ROTATE, TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service"; import { atlasAppearance, atlasSelection } from "src/state"; @@ -400,7 +398,6 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit private sapi: SAPI, private snackbar: MatSnackBar, @Optional() intViewerStateSvc: ViewerInternalStateSvc, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, ){ if (intViewerStateSvc) { const { @@ -430,37 +427,6 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit this.onDestroyCb.push(() => done()) } - /** - * intercept click and act - */ - if (clickInterceptor) { - const handleClick = (ev: MouseEvent) => { - - // if does not click inside container, ignore - if (!(el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) { - return true - } - - if (this.mouseoverRegions.length === 0) return true - if (this.mouseoverRegions.length > 1) { - this.snackbar.open(CONST.DOES_NOT_SUPPORT_MULTI_REGION_SELECTION, 'Dismiss', { - duration: 3000 - }) - return true - } - - const regions = this.mouseoverRegions.slice(0, 1) as any[] - this.store$.dispatch( - atlasSelection.actions.setSelectedRegions({ regions }) - ) - return true - } - const { register, deregister } = clickInterceptor - register(handleClick) - this.onDestroyCb.push( - () => { deregister(register) } - ) - } this.domEl = el.nativeElement diff --git a/src/viewerModule/viewer.interface.ts b/src/viewerModule/viewer.interface.ts index cbc85aca0e3e2620a0903b4e332dcb92386e8db0..488f8ee120168bb732a2df71742d1ddfe795990c 100644 --- a/src/viewerModule/viewer.interface.ts +++ b/src/viewerModule/viewer.interface.ts @@ -35,7 +35,9 @@ export interface IViewerCtx { 'threeSurfer': TThreeSurferContextInfo } -export type TContextArg<K extends keyof IViewerCtx> = ({ +export type ViewerType = "nehuba" | "threeSurfer" + +export type TViewerEvtCtxData<K extends ViewerType=ViewerType> = ({ viewerType: K payload: RecursivePartial<IViewerCtx[K]> }) @@ -45,25 +47,40 @@ export enum EnumViewerEvt { VIEWER_CTX, } -type TViewerEventViewerLoaded = { +export type TViewerEventViewerLoaded = { type: EnumViewerEvt.VIEWERLOADED data: boolean } -export type TViewerEvent<T extends keyof IViewerCtx> = TViewerEventViewerLoaded | - { - type: EnumViewerEvt.VIEWER_CTX - data: TContextArg<T> - } +type TViewerEventCtx<T extends ViewerType=ViewerType> = { + type: EnumViewerEvt.VIEWER_CTX + data: TViewerEvtCtxData<T> +} + +export type TViewerEvent< + T extends ViewerType=ViewerType +> = TViewerEventViewerLoaded | TViewerEventCtx<T> + +export function isViewerCtx(ev: TViewerEvent): ev is TViewerEventCtx { + return ev.type === EnumViewerEvt.VIEWER_CTX +} + +export function isNehubaVCtxEvt(ev: TViewerEvent): ev is TViewerEventCtx<"nehuba"> { + return ev.type === EnumViewerEvt.VIEWER_CTX && ev.data.viewerType === "nehuba" +} + +export function isThreeSurferVCtxEvt(ev: TViewerEvent): ev is TViewerEventCtx<"threeSurfer"> { + return ev.type === EnumViewerEvt.VIEWER_CTX && ev.data.viewerType === "threeSurfer" +} -export type TSupportedViewers = keyof IViewerCtx +export type TSupportedViewers = ViewerType -export interface IViewer<K extends keyof IViewerCtx> { +export interface IViewer<K extends ViewerType> { viewerCtrlHandler?: IViewerCtrl viewerEvent: EventEmitter<TViewerEvent<K>> } export interface IGetContextInjArg { - register: (fn: (contextArg: TContextArg<TSupportedViewers>) => void) => void - deregister: (fn: (contextArg: TContextArg<TSupportedViewers>) => void) => void + register: (fn: (contextArg: TViewerEvtCtxData<TSupportedViewers>) => void) => void + deregister: (fn: (contextArg: TViewerEvtCtxData<TSupportedViewers>) => void) => void } diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index fdb4f3c31be246d79ff6aace1d01583633304556..55094ebf67b51781234625188b8b95805de995c2 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,11 +1,11 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, TemplateRef, ViewChild, inject } from "@angular/core"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, TemplateRef, ViewChild, inject } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { BehaviorSubject, combineLatest, Observable, of, Subscription } from "rxjs"; +import { BehaviorSubject, combineLatest, Observable, of } from "rxjs"; import { debounceTime, distinctUntilChanged, map, shareReplay, switchMap, takeUntil } from "rxjs/operators"; import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { animate, state, style, transition, trigger } from "@angular/animations"; import { IQuickTourData } from "src/ui/quickTour"; -import { EnumViewerEvt, TContextArg, TSupportedViewers, TViewerEvent } from "../viewer.interface"; +import { EnumViewerEvt, TViewerEvtCtxData, TSupportedViewers, TViewerEvent } from "../viewer.interface"; import { ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; import { DialogService } from "src/services/dialogService.service"; import { SAPI } from "src/atlasComponents/sapi"; @@ -55,7 +55,7 @@ interface HasName { ] }) -export class ViewerCmp implements OnDestroy { +export class ViewerCmp { public readonly destroy$ = inject(DestroyDirective).destroyed$ @@ -74,8 +74,6 @@ export class ViewerCmp implements OnDestroy { description: QUICKTOUR_DESC.ATLAS_SELECTOR, } - private subscriptions: Subscription[] = [] - private onDestroyCb: (() => void)[] = [] public viewerLoaded: boolean = false private selectedATP = this.store$.pipe( @@ -238,7 +236,7 @@ export class ViewerCmp implements OnDestroy { constructor( private store$: Store<any>, - private ctxMenuSvc: ContextMenuService<TContextArg<'threeSurfer' | 'nehuba'>>, + private ctxMenuSvc: ContextMenuService<TViewerEvtCtxData<'threeSurfer' | 'nehuba'>>, private dialogSvc: DialogService, private cdr: ChangeDetectorRef, private sapi: SAPI, @@ -293,51 +291,51 @@ export class ViewerCmp implements OnDestroy { this.#fullNavBarSwitch$.next(flag) }) - this.subscriptions.push( - this.templateSelected$.subscribe( - t => this.templateSelected = t - ), - combineLatest([ - this.templateSelected$, - this.parcellationSelected$, - this.selectedAtlas$, - ]).pipe( - debounceTime(160) - ).subscribe(async ([tmpl, parc, atlas]) => { - const regex = /pre.?release/i - const checkPrerelease = (obj: any) => { - if (obj?.name) return regex.test(obj.name) - return false - } - const message: string[] = [] - if (checkPrerelease(atlas)) { - message.push(`- _${atlas.name}_`) - } - if (checkPrerelease(tmpl)) { - message.push(`- _${tmpl.name}_`) - } - if (checkPrerelease(parc)) { - message.push(`- _${parc.name}_`) - } - if (message.length > 0) { - message.unshift(`The following have been tagged pre-release, and may be updated frequently:`) - try { - await this.dialogSvc.getUserConfirm({ - title: `Pre-release warning`, - markdown: message.join('\n\n'), - confirmOnly: true - }) - // eslint-disable-next-line no-empty - } catch (e) { - - } - } - }) + this.templateSelected$.pipe( + takeUntil(this.destroy$) + ).subscribe( + t => this.templateSelected = t ) - } - ngAfterViewInit(): void{ - const cb: TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>> = ({ append, context }) => { + combineLatest([ + this.templateSelected$, + this.parcellationSelected$, + this.selectedAtlas$, + ]).pipe( + takeUntil(this.destroy$), + debounceTime(160), + ).subscribe(async ([tmpl, parc, atlas]) => { + const regex = /pre.?release/i + const checkPrerelease = (obj: any) => { + if (obj?.name) return regex.test(obj.name) + return false + } + const message: string[] = [] + if (checkPrerelease(atlas)) { + message.push(`- _${atlas.name}_`) + } + if (checkPrerelease(tmpl)) { + message.push(`- _${tmpl.name}_`) + } + if (checkPrerelease(parc)) { + message.push(`- _${parc.name}_`) + } + if (message.length > 0) { + message.unshift(`The following have been tagged pre-release, and may be updated frequently:`) + try { + await this.dialogSvc.getUserConfirm({ + title: `Pre-release warning`, + markdown: message.join('\n\n'), + confirmOnly: true + }) + // eslint-disable-next-line no-empty + } catch (e) { + + } + } + }) + + const cb: TContextMenuReg<TViewerEvtCtxData<'nehuba' | 'threeSurfer'>> = ({ append, context }) => { if (this.#lastSelectedPoint && this.lastViewedPointTmpl) { const { point, template, face, vertices } = this.#lastSelectedPoint @@ -373,14 +371,14 @@ export class ViewerCmp implements OnDestroy { */ let hoveredRegions = [] if (context.viewerType === 'nehuba') { - hoveredRegions = ((context as TContextArg<'nehuba'>).payload.nehuba || []).reduce( + hoveredRegions = ((context as TViewerEvtCtxData<'nehuba'>).payload.nehuba || []).reduce( (acc, curr) => acc.concat(...curr.regions), [] ) } if (context.viewerType === 'threeSurfer') { - hoveredRegions = (context as TContextArg<'threeSurfer'>).payload.regions + hoveredRegions = (context as TViewerEvtCtxData<'threeSurfer'>).payload.regions } if (hoveredRegions.length > 0) { @@ -397,14 +395,11 @@ export class ViewerCmp implements OnDestroy { return true } this.ctxMenuSvc.register(cb) - this.onDestroyCb.push( - () => this.ctxMenuSvc.deregister(cb) - ) - } - ngOnDestroy(): void { - while (this.subscriptions.length) this.subscriptions.pop().unsubscribe() - while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() + this.destroy$.subscribe(() => { + this.ctxMenuSvc.deregister(cb) + }) + } public clearRoi(): void{ @@ -484,48 +479,6 @@ export class ViewerCmp implements OnDestroy { ) } - public handleViewerEvent(event: TViewerEvent<'nehuba' | 'threeSurfer'>): void{ - switch(event.type) { - case EnumViewerEvt.VIEWERLOADED: - this.viewerLoaded = event.data - this.cdr.detectChanges() - break - case EnumViewerEvt.VIEWER_CTX: - this.ctxMenuSvc.deepMerge(event.data) - if (event.data.viewerType === "nehuba") { - const { nehuba, nav } = (event.data as TContextArg<"nehuba">).payload - if (nehuba) { - const mousingOverRegions = (nehuba || []).reduce((acc, { regions }) => acc.concat(...regions), []) - this.store$.dispatch( - userInteraction.actions.mouseoverRegions({ - regions: mousingOverRegions - }) - ) - } - if (nav) { - this.store$.dispatch( - userInteraction.actions.mouseoverPosition({ - position: { - loc: nav.position as [number, number, number], - space: this.templateSelected, - spaceId: this.templateSelected.id, - } - }) - ) - } - } - if (event.data.viewerType === "threeSurfer") { - const { regions=[] } = (event.data as TContextArg<"threeSurfer">).payload - this.store$.dispatch( - userInteraction.actions.mouseoverRegions({ - regions: regions as SxplrRegion[] - }) - ) - } - break - default: - } - } public disposeCtxMenu(): void{ this.ctxMenuSvc.dismissCtxMenu() @@ -598,4 +551,15 @@ export class ViewerCmp implements OnDestroy { nameEql(a: HasName, b: HasName){ return a.name === b.name } + + handleViewerCtxEvent(event: TViewerEvent) { + if (event.type === EnumViewerEvt.VIEWERLOADED) { + this.viewerLoaded = event.data + this.cdr.detectChanges() + return + } + if (event.type === EnumViewerEvt.VIEWER_CTX) { + this.ctxMenuSvc.deepMerge(event.data) + } + } } diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index d72febea43178d6447b9c113f66ccb4ca905e2e3..05cfce23ca6b00d6251b825dec446b216d2da77a 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -93,13 +93,13 @@ mat-drawer display: block; } -mat-list.contextual-block +.contextual-block { display: inline-block; background-color:rgba(200,200,200,0.8); } -:host-context([darktheme="true"]) mat-list.contextual-block +:host-context([darktheme="true"]) .contextual-block { background-color : rgba(30,30,30,0.8); } @@ -155,13 +155,6 @@ sxplr-sapiviews-core-region-region-list-item align-items: center; } -.centered -{ - display: flex; - justify-content: center; - align-items: center; -} - .leave-me-alone { margin-top: 0.5rem; @@ -194,4 +187,11 @@ sxplr-tab { display: inline-flex; flex-direction: column; -} \ No newline at end of file +} + +viewer-wrapper +{ + width: 100%; + height: 100%; + display: block; +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 9528a948def9fa6eeb3a66124b147409c050af1a..4e15b6b1c9644acb5357bc8ad01376bab9e6e144 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -12,30 +12,9 @@ <div *ngIf="(media.mediaBreakPoint$ | async) < 2" - floatingMouseContextualContainerDirective - iav-mouse-hover - #iavMouseHoverContextualBlock="iavMouseHover"> - - <mat-list class="contextual-block"> - <mat-list-item *ngFor="let cvtOutput of iavMouseHoverContextualBlock.currentOnHoverObs$ | async | mouseoverCvt" - class="h-auto"> - <span class="centered" matListItemIcon [class]="cvtOutput.icon.cls"></span> - <span matListItemTitle>{{ cvtOutput.text }}</span> - </mat-list-item> - - <mat-list-item *ngFor="let message of iavMouseHoverContextualBlock.messages$ | async" - class="h-auto"> - <ng-template [ngIf]="message.fontIcon && message.fontSet"> - <mat-icon matListItemIcon - [fontSet]="message.fontSet" - [fontIcon]="message.fontIcon"> - </mat-icon> - </ng-template> - <span matListItemTitle>{{ message.message }}</span> - </mat-list-item> - </mat-list> + floatingMouseContextualContainerDirective> + <mouseover-info class="contextual-block"></mouseover-info> </div> - </div> </div> @@ -429,44 +408,9 @@ <div class="position-absolute w-100 h-100 z-index-1" ctx-menu-host [ctx-menu-host-tmpl]="viewerCtxMenuTmpl"> - - <ng-container [ngSwitch]="useViewer$ | async"> - - <!-- nehuba viewer --> - <iav-cmp-viewer-nehuba-glue class="d-block w-100 h-100 position-absolute left-0 tosxplr-p-0" - *ngSwitchCase="'nehuba'" - (viewerEvent)="handleViewerEvent($event)" - #iavCmpViewerNehubaGlue="iavCmpViewerNehubaGlue"> - </iav-cmp-viewer-nehuba-glue> - - <!-- three surfer (free surfer viewer) --> - <tmp-threesurfer-lifecycle class="d-block w-100 h-100 position-absolute left-0 tosxplr-p-0" - *ngSwitchCase="'threeSurfer'" - (viewerEvent)="handleViewerEvent($event)"> - </tmp-threesurfer-lifecycle> - - <!-- if not supported, show not supported message --> - <div *ngSwitchCase="'notsupported'">Template not supported by any of the viewers</div> - - <!-- by default, show splash screen --> - <div class="sxplr-h-100" *ngSwitchDefault> - <ng-template [ngIf]="(selectedAtlas$ | async)" [ngIfElse]="splashScreenTmpl"> - <div class="center-a-div"> - <div class="loading-atlas-text-container"> - <spinner-cmp class="fs-200"></spinner-cmp> - <span> - Loading - {{ (selectedAtlas$ | async).name }} - </span> - </div> - </div> - </ng-template> - <ng-template #splashScreenTmpl> - <ui-splashscreen class="position-absolute left-0 tosxplr-p-0"> - </ui-splashscreen> - </ng-template> - </div> - </ng-container> + <viewer-wrapper + (viewer-event)="handleViewerCtxEvent($event)"> + </viewer-wrapper> </div> </ng-template> diff --git a/src/viewerModule/viewerWrapper/viewerWrapper.component.ts b/src/viewerModule/viewerWrapper/viewerWrapper.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e7e7cc5ea32de230faa01a7f60c3c779574972c --- /dev/null +++ b/src/viewerModule/viewerWrapper/viewerWrapper.component.ts @@ -0,0 +1,204 @@ +import { Component, ElementRef, Inject, Optional, Output, inject } from "@angular/core"; +import { Observable, Subject, merge } from "rxjs"; +import { TSupportedViewers, TViewerEvent, isNehubaVCtxEvt, isThreeSurferVCtxEvt, isViewerCtx } from "../viewer.interface"; +import { Store, select } from "@ngrx/store"; +import { MainState, atlasAppearance, atlasSelection, userInteraction } from "src/state"; +import { distinctUntilChanged, filter, finalize, map, shareReplay, takeUntil } from "rxjs/operators"; +import { arrayEqual } from "src/util/array"; +import { DestroyDirective } from "src/util/directives/destroy.directive"; +import { CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor, HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; +import { SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; + +@Component({ + selector: 'viewer-wrapper', + templateUrl: './viewerWrapper.template.html', + styleUrls: [ + './viewerWrapper.style.css' + ], + hostDirectives: [ + DestroyDirective + ] +}) +export class ViewerWrapper { + + #destroy$ = inject(DestroyDirective).destroyed$ + + @Output('viewer-event') + viewerEvent$ = new Subject< + TViewerEvent + >() + + selectedAtlas$ = this.store$.pipe( + select(atlasSelection.selectors.selectedAtlas) + ) + + useViewer$: Observable<TSupportedViewers | 'notsupported'> = this.store$.pipe( + select(atlasAppearance.selectors.useViewer), + map(useviewer => { + if (useviewer === "NEHUBA") return "nehuba" + if (useviewer === "THREESURFER") return "threeSurfer" + if (useviewer === "NOT_SUPPORTED") return "notsupported" + return null + }) + ) + + constructor( + el: ElementRef, + private store$: Store<MainState>, + @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) + hoverInterceptor: HoverInterceptor, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) + clickInterceptor: ClickInterceptor, + ){ + + this.store$.pipe( + select(atlasSelection.selectors.selectedTemplate), + takeUntil(this.#destroy$) + ).subscribe(tmpl => { + this.#selectedTemplate = tmpl + }) + + /** + * handling nehuba event + */ + this.#nehubaViewerCtxEv$.pipe( + takeUntil(this.#destroy$) + ).subscribe(ev => { + const { nehuba, nav } = ev.data.payload + if (nehuba) { + const mousingOverRegions = (nehuba || []).reduce((acc, { regions }) => acc.concat(...regions), []) + this.store$.dispatch( + userInteraction.actions.mouseoverRegions({ + regions: mousingOverRegions + }) + ) + } + if (nav) { + this.store$.dispatch( + userInteraction.actions.mouseoverPosition({ + position: { + loc: nav.position as [number, number, number], + space: this.#selectedTemplate, + spaceId: this.#selectedTemplate.id, + } + }) + ) + } + }) + + /** + * handling threesurfer event + */ + this.#threeSurferViewerCtxEv$.pipe( + takeUntil(this.#destroy$) + ).subscribe(ev => { + const { regions = [] } = ev.data.payload + this.store$.dispatch( + userInteraction.actions.mouseoverRegions({ + regions: regions as SxplrRegion[] + }) + ) + }) + + if (hoverInterceptor) { + let hoverRegionMessages: THoverConfig[] = [] + const { append, remove } = hoverInterceptor + this.#hoveredRegions$.pipe( + takeUntil(this.#destroy$), + finalize(() => { + for (const msg of hoverRegionMessages) { + remove(msg) + } + }) + ).subscribe(regions => { + + for (const msg of hoverRegionMessages) { + remove(msg) + } + + hoverRegionMessages = regions.map(region => ({ + message: region.name || 'Unknown Region', + fontIcon: 'fa-brain', + fontSet: 'fas' + })) + + for (const msg of hoverRegionMessages){ + append(msg) + } + }) + } + + if (clickInterceptor) { + const { register, deregister } = clickInterceptor + let hoveredRegions: SxplrRegion[] + this.#hoveredRegions$.subscribe(reg => { + hoveredRegions = reg as SxplrRegion[] + }) + const handleClick = (ev: PointerEvent) => { + if (!el?.nativeElement?.contains(ev.target)) { + return true + } + if (hoveredRegions.length === 0) { + return true + } + if (ev.ctrlKey) { + this.store$.dispatch( + atlasSelection.actions.toggleRegion({ + region: hoveredRegions[0] + }) + ) + } else { + this.store$.dispatch( + atlasSelection.actions.selectRegion({ + region: hoveredRegions[0] + }) + ) + } + return true + } + register(handleClick, { last: true }) + this.#destroy$.subscribe(() => { + deregister(handleClick) + }) + } + } + + public handleViewerEvent(event: TViewerEvent): void{ + this.viewerEvent$.next(event) + } + + #viewerCtxEvent$ = this.viewerEvent$.pipe( + filter(isViewerCtx), + shareReplay(1), + ) + + #nehubaViewerCtxEv$ = this.#viewerCtxEvent$.pipe( + filter(isNehubaVCtxEvt) + ) + + #threeSurferViewerCtxEv$ = this.#viewerCtxEvent$.pipe( + filter(isThreeSurferVCtxEvt) + ) + + #hoveredRegions$ = merge( + this.#nehubaViewerCtxEv$.pipe( + filter(ev => !!ev.data.payload.nehuba), + map(ev => { + const { nehuba } = ev.data.payload + return nehuba.map(n => n.regions).flatMap(v => v) + }) + ), + this.#threeSurferViewerCtxEv$.pipe( + map(ev => { + const { regions = [] } = ev.data.payload + return regions + }) + ) + ).pipe( + distinctUntilChanged( + arrayEqual((o, n) => o.name === n.name) + ) + ) + + #selectedTemplate: SxplrTemplate = null +} diff --git a/src/viewerModule/viewerWrapper/viewerWrapper.style.css b/src/viewerModule/viewerWrapper/viewerWrapper.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/viewerModule/viewerWrapper/viewerWrapper.template.html b/src/viewerModule/viewerWrapper/viewerWrapper.template.html new file mode 100644 index 0000000000000000000000000000000000000000..5ea7ec0ce2d26719a83a9dd0f1cb175a5f1f286b --- /dev/null +++ b/src/viewerModule/viewerWrapper/viewerWrapper.template.html @@ -0,0 +1,37 @@ +<ng-container [ngSwitch]="useViewer$ | async"> + + <!-- nehuba viewer --> + <iav-cmp-viewer-nehuba-glue class="d-block w-100 h-100 position-absolute left-0 tosxplr-p-0" + *ngSwitchCase="'nehuba'" + (viewerEvent)="handleViewerEvent($event)" + #iavCmpViewerNehubaGlue="iavCmpViewerNehubaGlue"> + </iav-cmp-viewer-nehuba-glue> + + <!-- three surfer (free surfer viewer) --> + <tmp-threesurfer-lifecycle class="d-block w-100 h-100 position-absolute left-0 tosxplr-p-0" + *ngSwitchCase="'threeSurfer'" + (viewerEvent)="handleViewerEvent($event)"> + </tmp-threesurfer-lifecycle> + + <!-- if not supported, show not supported message --> + <div *ngSwitchCase="'notsupported'">Template not supported by any of the viewers</div> + + <!-- by default, show splash screen --> + <div class="sxplr-h-100" *ngSwitchDefault> + <ng-template [ngIf]="(selectedAtlas$ | async)" [ngIfElse]="splashScreenTmpl" let-atlas> + <div class="center-a-div"> + <div class="loading-atlas-text-container"> + <spinner-cmp class="fs-200"></spinner-cmp> + <span> + Loading + {{ atlas.name }} + </span> + </div> + </div> + </ng-template> + <ng-template #splashScreenTmpl> + <ui-splashscreen class="position-absolute left-0 tosxplr-p-0"> + </ui-splashscreen> + </ng-template> + </div> + </ng-container>