diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 861aae4b718480d9afca1d856ef5d53eef153664..13248f4a0d39349ba8f58b45cbc309246b1c9788 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -1,8 +1,8 @@ -import { Component, HostBinding, ViewChild, ViewContainerRef, OnDestroy, OnInit, TemplateRef, AfterViewInit, ElementRef, Renderer2 } from "@angular/core"; +import { Component, HostBinding, ViewChild, ViewContainerRef, OnDestroy, OnInit, TemplateRef, AfterViewInit, Renderer2 } from "@angular/core"; import { Store, select, ActionsSubject } from "@ngrx/store"; -import { ViewerStateInterface, isDefined, FETCHED_SPATIAL_DATA, UPDATE_SPATIAL_DATA, TOGGLE_SIDE_PANEL, safeFilter, OPEN_SIDE_PANEL, CLOSE_SIDE_PANEL } from "../services/stateStore.service"; -import { Observable, Subscription, combineLatest, interval, merge, of, fromEvent } from "rxjs"; -import { map, filter, distinctUntilChanged, delay, concatMap, debounceTime, withLatestFrom, switchMap, takeUntil, scan, takeLast } from "rxjs/operators"; +import { ViewerStateInterface, isDefined, FETCHED_SPATIAL_DATA, UPDATE_SPATIAL_DATA, safeFilter } from "../services/stateStore.service"; +import { Observable, Subscription, combineLatest, interval, merge, of } from "rxjs"; +import { map, filter, distinctUntilChanged, delay, concatMap, withLatestFrom } from "rxjs/operators"; import { AtlasViewerDataService } from "./atlasViewer.dataService.service"; import { WidgetServices } from "./widgetUnit/widgetService.service"; import { LayoutMainSide } from "../layouts/mainside/mainside.component"; @@ -13,7 +13,6 @@ import { AtlasViewerAPIServices } from "./atlasViewer.apiService.service"; import { NehubaContainer } from "../ui/nehubaContainer/nehubaContainer.component"; import { colorAnimation } from "./atlasViewer.animation" import { FixedMouseContextualContainerDirective } from "src/util/directives/FixedMouseContextualContainerDirective.directive"; -import { DatabrowserService } from "src/ui/databrowserModule/databrowser.service"; import { AGREE_COOKIE, AGREE_KG_TOS, SHOW_KG_TOS, SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; import { TabsetComponent } from "ngx-bootstrap/tabs"; import { LocalFileService } from "src/services/localFile.service"; @@ -37,8 +36,6 @@ const filterFn = (segment) => typeof segment.segment !== 'string' }) export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { - - @ViewChild('floatingMouseContextualContainer', { read: ViewContainerRef }) floatingMouseContextualContainer: ViewContainerRef @ViewChild('cookieAgreementComponent', {read: TemplateRef}) cookieAgreementComponent : TemplateRef<any> @ViewChild('kgToS', {read: TemplateRef}) kgTosComponent: TemplateRef<any> @@ -49,7 +46,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @ViewChild(FixedMouseContextualContainerDirective) rClContextualMenu: FixedMouseContextualContainerDirective @ViewChild('mobileMenuTabs') mobileMenuTabs: TabsetComponent - @ViewChild('sidenav', { read: ElementRef} ) mobileSideNav: ElementRef /** * required for styling of all child components @@ -76,7 +72,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public dedicatedView$: Observable<string | null> public onhoverSegments$: Observable<string[]> public onhoverSegmentsForFixed$: Observable<string[]> - public onhoverLandmarksForFixed$: Observable<any> + public onhoverLandmark$ : Observable<{landmarkName: string, datasets: any} | null> private subscriptions: Subscription[] = [] @@ -104,7 +100,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public urlService: AtlasViewerURLService, public apiService: AtlasViewerAPIServices, private matDialog: MatDialog, - private databrowserService: DatabrowserService, private dispatcher$: ActionsSubject, private rd: Renderer2, public localFileService: LocalFileService, @@ -194,13 +189,16 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { if(landmark === null) return landmark const idx = Number(landmark.replace('label=','')) - if(isNaN(idx)) - return `Landmark index could not be parsed as a number: ${landmark}` - return { - landmarkName: spatialDatas[idx].name, - datasets: (spatialDatas[idx].dataset - && spatialDatas[idx].dataset.length)? spatialDatas[idx].dataset : null - } + if(isNaN(idx)) { + console.warn(`Landmark index could not be parsed as a number: ${landmark}`) + return { + landmarkName: idx + } + } else { + return { + landmarkName: spatialDatas[idx].name + } + } }) ) @@ -218,8 +216,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { ).pipe( map(([segments, onhoverLandmark]) => onhoverLandmark ? null : segments ), map(segments => { - if (!segments) - return null + if (!segments) return null const filteredSeg = segments.filter(filterFn) return filteredSeg.length > 0 ? segments.map(s => s.segment) @@ -234,11 +231,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { distinctUntilChanged(), ) - - this.subscriptions.push( - this.newViewer$.subscribe(template => this.selectedTemplate = template) - ) - this.subscriptions.push( this.selectedParcellation$.subscribe(parcellation => { this.selectedParcellation = parcellation @@ -408,17 +400,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { withLatestFrom(this.onhoverSegments$), map(([_flag, onhoverSegments]) => onhoverSegments || []) ) - - this.onhoverLandmarksForFixed$ = this.rClContextualMenu.onShow.pipe( - withLatestFrom(this.onhoverLandmark$), - map(([_flag, onhoverLandmark]) => onhoverLandmark || []) - ) - - /** - * TODO clean up code - * do not do this imperatively - */ - this.closeMenuWithSwipe(this.mobileSideNav) } /** @@ -486,8 +467,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.rClContextualMenu.show() } - private selectedTemplate: any - openLandmarkUrl(dataset) { this.rClContextualMenu.hide() window.open(dataset.externalLink, "_blank") @@ -495,48 +474,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @HostBinding('attr.version') public _version : string = VERSION - - /** - * TODO deprecated - */ - changeMenuState({open, close}:{open?:boolean, close?:boolean} = {}) { - if (open) { - return this.store.dispatch({ - type: OPEN_SIDE_PANEL - }) - } - if (close) { - return this.store.dispatch({ - type: CLOSE_SIDE_PANEL - }) - } - this.store.dispatch({ - type: TOGGLE_SIDE_PANEL - }) - } - - closeMenuWithSwipe(documentToSwipe: ElementRef) { - if (documentToSwipe && documentToSwipe.nativeElement) { - const swipeDistance = 150; // swipe distance - const swipeLeft$ = fromEvent(documentToSwipe.nativeElement, 'touchstart') - .pipe( - switchMap(startEvent => - fromEvent(documentToSwipe.nativeElement, 'touchmove') - .pipe( - takeUntil(fromEvent(documentToSwipe.nativeElement, 'touchend')), - map(event => event['touches'][0].pageX), - scan((acc, pageX) => Math.round(startEvent['touches'][0].pageX - pageX), 0), - takeLast(1), - filter(difference => difference >= swipeDistance) - ))) - this.subscriptions.push( - swipeLeft$.subscribe(() => { - this.changeMenuState({close: true}) - }) - ) - } - } - } export interface NgLayerInterface{ diff --git a/src/atlasViewer/atlasViewer.style.css b/src/atlasViewer/atlasViewer.style.css index 23c6702a2296eba0c7e0c9b2bfa8fd7d653b1e0c..d8dd38d79c50bdde5e9a19342486d0be3fc0449c 100644 --- a/src/atlasViewer/atlasViewer.style.css +++ b/src/atlasViewer/atlasViewer.style.css @@ -50,29 +50,14 @@ layout-floating-container margin: 0.8em 0.4em; } -div[floatingMouseContextualContainer] +[contextualBlock] { - position : absolute; - left: 0; - top : 0; - width : 0px; - height: 0px; -} - -div[contextualBlock] -{ - margin-top: 1px; - white-space: nowrap; - display:inline-block; - width:auto; - padding: 0.3em 0.5em; background-color:rgba(200,200,200,0.8); } -:host-context([darktheme="true"]) div[contextualBlock] +:host-context([darktheme="true"]) [contextualBlock] { background-color : rgba(30,30,30,0.8); - color : rgba(250,250,250,0.8); } diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 6a67f8a11f9113e3b7ddce2e6ba20c949913a26e..65bd7abfbf9c02838ad6b5704cbcaaf88ef966e1 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -46,7 +46,12 @@ <ng-template #viewerBody> <div class="atlas-container" (drag-drop)="localFileService.handleFileDrop($event)"> - <ui-nehuba-container (contextmenu)="$event.stopPropagation(); $event.preventDefault();"> <!--nehubaClickHandler($event)--> + <ui-nehuba-container + iav-mouse-hover + #iavMouseHoverEl="iavMouseHover" + [currentOnHoverObs$]="iavMouseHoverEl.currentOnHoverObs$" + [currentOnHover]="iavMouseHoverEl.currentOnHoverObs$ | async" + (contextmenu)="$event.stopPropagation(); $event.preventDefault();"> </ui-nehuba-container> <div class="z-index-10 position-absolute pe-none w-100 h-100"> @@ -104,7 +109,7 @@ </div> <div class="d-flex flex-row justify-content-end z-index-10 position-absolute pe-none w-100 h-100"> - <signin-banner signinWrapper [ngStyle]="{'margin-right': !selectedTemplate? '20px': ''}"> + <signin-banner signinWrapper> </signin-banner> </div> @@ -115,7 +120,7 @@ </div> - <!-- TODO deprecate --> + <!-- TODO document fixedMouseContextualContainerDirective , then deprecate this --> <!-- TODO move to nehuba overlay container --> <panel-component class="shadow" fixedMouseContextualContainerDirective #rClContextMenu> <div heading> @@ -130,23 +135,6 @@ class="p-2"> Search for data relating to: </div> - - <div *ngIf="(onhoverLandmarksForFixed$ | async) as onhoverLandmarksForFixed"> - <div *ngIf="onhoverLandmarksForFixed && onhoverLandmarksForFixed.datasets && onhoverLandmarksForFixed.datasets.length > 0"> - <div class="p-2"> - Explore datasets for {{onhoverLandmarksForFixed.landmarkName}} - </div> - <div - *ngFor="let dataset of onhoverLandmarksForFixed.datasets" - class="ws-no-wrap text-left pe-all btn btn-sm btn-secondary btn-block mt-0" - data-toggle="tooltip" - data-placement="top" - (click)="openLandmarkUrl(dataset)" - [title]="dataset.name"> - {{ dataset.name }} <i class="fas fa-external-link-alt"></i> - </div> - </div> - </div> <div *ngFor="let onhoverSegmentFixed of (onhoverSegmentsForFixed$ | async)" @@ -187,20 +175,32 @@ </div> </panel-component> - <div floatingMouseContextualContainer floatingMouseContextualContainerDirective> - <div - *ngIf="onhoverLandmark$ | async" - contextualBlock> - {{ (onhoverLandmark$ | async)?.landmarkName }} <i><small class = "mute-text">{{ toggleMessage }}</small></i> - </div> - <div - *ngIf="onhoverSegments$ | async; let onhoverSegments" + <div floatingMouseContextualContainerDirective> + + <div class="d-inline-block" + iav-mouse-hover + #iavMouseHoverConetxtualBlock="iavMouseHover" contextualBlock> - <div - *ngFor="let segment of onhoverSegments" - [innerHtml]="segment | transformOnhoverSegment"> - </div> - <i><small class = "mute-text">{{ toggleMessage }}</small></i> + + <ng-container *ngFor="let labelText of iavMouseHoverConetxtualBlock.currentOnHoverObs$ | async | mouseOverTextPipe"> + + <mat-list dense> + + <mat-list-item> + + <mat-icon [fontSet]="(labelText.label | mouseOverIconPipe).fontSet" + [fontIcon]="(labelText.label | mouseOverIconPipe).fontIcon" + mat-list-icon> + + </mat-icon> + + <div matLine + *ngFor="let text of labelText.text" + [innerHTML]="text"> + </div> + </mat-list-item> + </mat-list> + </ng-container> </div> <!-- TODO Potentially implementing plugin contextual info --> </div> diff --git a/src/atlasViewer/onhoverSegment.pipe.ts b/src/atlasViewer/onhoverSegment.pipe.ts index baff7b9d5d1de04b0d6c5327aabf2a41a532ff4f..9119b115f7595ac1d412d6c9728803cd79c0745f 100644 --- a/src/atlasViewer/onhoverSegment.pipe.ts +++ b/src/atlasViewer/onhoverSegment.pipe.ts @@ -1,5 +1,5 @@ import { PipeTransform, Pipe, SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; +import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; @Pipe({ name: 'transformOnhoverSegment' @@ -14,7 +14,7 @@ export class TransformOnhoverSegmentPipe implements PipeTransform{ return ` <span class="text-muted">(${this.sanitizer.sanitize(SecurityContext.HTML, text)})</span>` } - public transform(segment: any | number){ + public transform(segment: any | number): SafeHtml{ return this.sanitizer.bypassSecurityTrustHtml(( (segment.name || segment) + (segment.status diff --git a/src/main.module.ts b/src/main.module.ts index 50ba1a22d5d3c4f5fbe8af4980272da10827aad3..6dd663fae585edfc1048ea2c5c8d5dc5beece6f9 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -4,7 +4,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop' import { UIModule } from "./ui/ui.module"; import { LayoutModule } from "./layouts/layout.module"; import { AtlasViewer } from "./atlasViewer/atlasViewer.component"; -import { StoreModule, Store, select } from "@ngrx/store"; +import { StoreModule } from "@ngrx/store"; import { viewerState, dataStore,spatialSearchState,uiState, ngViewerState, pluginState, viewerConfigState, userConfigState, UserConfigStateUseEffect } from "./services/stateStore.service"; import { GetNamesPipe } from "./util/pipes/getNames.pipe"; import { CommonModule } from "@angular/common"; @@ -46,9 +46,10 @@ import { ConfirmDialogComponent } from "./components/confirmDialog/confirmDialog import { ViewerStateUseEffect } from "./services/state/viewerState.store"; import { NgViewerUseEffect } from "./services/state/ngViewerState.store"; import { DatabrowserModule } from "./ui/databrowserModule/databrowser.module"; +import { UIService } from "./services/uiService.service"; +import { UtilModule } from "./util/util.module"; import 'hammerjs' -import { UIService } from "./services/uiService.service"; import 'src/res/css/version.css' import 'src/theme.scss' @@ -64,6 +65,7 @@ import 'src/res/css/extra_styles.css' UIModule, DatabrowserModule, AngularMaterialModule, + UtilModule, TooltipModule.forRoot(), TabsModule.forRoot(), diff --git a/src/services/state/uiState.store.ts b/src/services/state/uiState.store.ts index b58f86c33349151322501584766e2cc3984f9ddb..396ebfe8ea69d2ccce217f87f6ace0de7536421a 100644 --- a/src/services/state/uiState.store.ts +++ b/src/services/state/uiState.store.ts @@ -6,7 +6,10 @@ import { LOCAL_STORAGE_CONST, COOKIE_VERSION, KG_TOS_VERSION } from 'src/util/co const defaultState : UIStateInterface = { mouseOverSegments: [], mouseOverSegment: null, + mouseOverLandmark: null, + mouseOverUserLandmark: null, + focusedSidePanel: null, sidePanelOpen: false, @@ -34,6 +37,13 @@ export function uiState(state:UIStateInterface = defaultState,action:UIAction){ ...state, mouseOverSegment : action.segment } + case MOUSEOVER_USER_LANDMARK: + const { payload = {} } = action + const { userLandmark: mouseOverUserLandmark = null } = payload + return { + ...state, + mouseOverUserLandmark + } case MOUSE_OVER_LANDMARK: return { ...state, @@ -103,10 +113,13 @@ export interface UIStateInterface{ } segment: any | null }[] - sidePanelOpen : boolean - mouseOverSegment : any | number - mouseOverLandmark : any - focusedSidePanel : string | null + sidePanelOpen: boolean + mouseOverSegment: any | number + + mouseOverLandmark: any + mouseOverUserLandmark: any + + focusedSidePanel: string | null snackbarMessage: Symbol @@ -119,7 +132,7 @@ export interface UIStateInterface{ export interface UIAction extends Action{ segment: any | number landmark: any - focusedSidePanel? : string + focusedSidePanel?: string segments?:{ layer: { name: string @@ -129,11 +142,14 @@ export interface UIAction extends Action{ snackbarMessage: string bottomSheetTemplate: TemplateRef<any> + + payload: any } export const MOUSE_OVER_SEGMENT = `MOUSE_OVER_SEGMENT` export const MOUSE_OVER_SEGMENTS = `MOUSE_OVER_SEGMENTS` export const MOUSE_OVER_LANDMARK = `MOUSE_OVER_LANDMARK` +export const MOUSEOVER_USER_LANDMARK = `MOUSEOVER_USER_LANDMARK` export const TOGGLE_SIDE_PANEL = 'TOGGLE_SIDE_PANEL' export const CLOSE_SIDE_PANEL = `CLOSE_SIDE_PANEL` diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index 359cf2be6547ee998f73cf05b36b9909ed1e1ae4..5bf19bbae79fdf73b7a0fdc30aa66c6a6fbd873c 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -3,8 +3,10 @@ import { UserLandmark } from 'src/atlasViewer/atlasViewer.apiService.service'; import { NgLayerInterface } from 'src/atlasViewer/atlasViewer.component'; import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; -import { withLatestFrom, map, shareReplay, startWith, tap } from 'rxjs/operators'; +import { withLatestFrom, map, shareReplay, startWith, filter, distinctUntilChanged } from 'rxjs/operators'; import { Observable } from 'rxjs'; +import { MOUSEOVER_USER_LANDMARK } from './uiState.store'; +import { generateLabelIndexId } from '../stateStore.service'; export interface ViewerStateInterface{ fetchedTemplates : any[] @@ -250,20 +252,132 @@ export class ViewerStateUseEffect{ } }) ) + + this.mouseoverUserLandmarks = this.actions$.pipe( + ofType(ACTION_TYPES.MOUSEOVER_USER_LANDMARK_LABEL), + withLatestFrom(this.currentLandmarks$), + map(([ action, currentLandmarks ]) => { + const { payload } = action as any + const { label } = payload + if (!label) return { + type: MOUSEOVER_USER_LANDMARK, + payload: { + userLandmark: null + } + } + + const idx = Number(label.replace('label=', '')) + if (isNaN(idx)) { + console.warn(`Landmark index could not be parsed as a number: ${idx}`) + return { + type: MOUSEOVER_USER_LANDMARK, + payload: { + userLandmark: null + } + } + } + + return { + type: MOUSEOVER_USER_LANDMARK, + payload: { + userLandmark: currentLandmarks[idx] + } + } + }) + + ) + + const doubleClickOnViewer$ = this.actions$.pipe( + ofType(ACTION_TYPES.DOUBLE_CLICK_ON_VIEWER), + map(action => { + const { payload } = action as any + const { segments, landmark, userLandmark } = payload + return { segments, landmark, userLandmark } + }), + shareReplay(1) + ) + + this.doubleClickOnViewerToggleRegions$ = doubleClickOnViewer$.pipe( + filter(({ segments }) => segments && segments.length > 0), + withLatestFrom(this.store$.pipe( + select('viewerState'), + select('regionsSelected'), + distinctUntilChanged(), + startWith([]) + )), + map(([{ segments }, regionsSelected]) => { + const selectedSet = new Set(regionsSelected.map(generateLabelIndexId)) + const toggleArr = segments.map(({ segment, layer }) => generateLabelIndexId({ ngId: layer.name, ...segment })) + + const deleteFlag = toggleArr.some(id => selectedSet.has(id)) + + for (const id of toggleArr){ + if (deleteFlag) selectedSet.delete(id) + else selectedSet.add(id) + } + + return { + type: SELECT_REGIONS_WITH_ID, + selectRegionIds: [...selectedSet] + } + }) + ) + + this.doubleClickOnViewerToggleLandmark$ = doubleClickOnViewer$.pipe( + filter(({ landmark }) => !!landmark), + withLatestFrom(this.store$.pipe( + select('viewerState'), + select('landmarksSelected'), + startWith([]) + )), + map(([{ landmark }, selectedSpatialDatas]) => { + + const selectedIdx = selectedSpatialDatas.findIndex(data => data.name === landmark.name) + + const newSelectedSpatialDatas = selectedIdx >= 0 + ? selectedSpatialDatas.filter((_, idx) => idx !== selectedIdx) + : selectedSpatialDatas.concat(landmark) + + return { + type: SELECT_LANDMARKS, + landmarks: newSelectedSpatialDatas + } + }) + ) + + this.doubleClickOnViewerToogleUserLandmark$ = doubleClickOnViewer$.pipe( + filter(({ userLandmark }) => userLandmark) + ) } private currentLandmarks$: Observable<any[]> + @Effect() + mouseoverUserLandmarks: Observable<any> + @Effect() removeUserLandmarks: Observable<any> @Effect() addUserLandmarks$: Observable<any> + + @Effect() + doubleClickOnViewerToggleRegions$: Observable<any> + + @Effect() + doubleClickOnViewerToggleLandmark$: Observable<any> + + // @Effect() + doubleClickOnViewerToogleUserLandmark$: Observable<any> } const ACTION_TYPES = { ADD_USERLANDMARKS: `ADD_USERLANDMARKS`, - REMOVE_USER_LANDMARKS: 'REMOVE_USER_LANDMARKS' + REMOVE_USER_LANDMARKS: 'REMOVE_USER_LANDMARKS', + MOUSEOVER_USER_LANDMARK_LABEL: 'MOUSEOVER_USER_LANDMARK_LABEL', + + SINGLE_CLICK_ON_VIEWER: 'SINGLE_CLICK_ON_VIEWER', + DOUBLE_CLICK_ON_VIEWER: 'DOUBLE_CLICK_ON_VIEWER' } export const VIEWERSTATE_ACTION_TYPES = ACTION_TYPES \ No newline at end of file diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index c0fe9adee278ef8c31425492f5e03dae84017cac..35899d499480c8887c7923c2f7bd72a91b14610f 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -1,9 +1,9 @@ -import { Component, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentFactory, ComponentRef, OnInit, OnDestroy, ElementRef } from "@angular/core"; +import { Component, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentFactory, ComponentRef, OnInit, OnDestroy, ElementRef, Input, OnChanges } from "@angular/core"; import { NehubaViewerUnit, computeDistance } from "./nehubaViewer/nehubaViewer.component"; import { Store, select } from "@ngrx/store"; -import { ViewerStateInterface, safeFilter, CHANGE_NAVIGATION, isDefined, ADD_NG_LAYER, REMOVE_NG_LAYER, NgViewerStateInterface, MOUSE_OVER_LANDMARK, SELECT_LANDMARKS, Landmark, PointLandmarkGeometry, PlaneLandmarkGeometry, OtherLandmarkGeometry, getNgIds, getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId, DataEntry } from "src/services/stateStore.service"; +import { ViewerStateInterface, safeFilter, CHANGE_NAVIGATION, isDefined, ADD_NG_LAYER, REMOVE_NG_LAYER, NgViewerStateInterface, MOUSE_OVER_LANDMARK, Landmark, PointLandmarkGeometry, PlaneLandmarkGeometry, OtherLandmarkGeometry, getNgIds, getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId, DataEntry } from "src/services/stateStore.service"; import { Observable, Subscription, fromEvent, combineLatest, merge, of } from "rxjs"; -import { filter,map, take, scan, debounceTime, distinctUntilChanged, switchMap, skip, withLatestFrom, buffer, tap, switchMapTo, shareReplay, mapTo, takeUntil } from "rxjs/operators"; +import { filter,map, take, scan, debounceTime, distinctUntilChanged, switchMap, skip, buffer, tap, switchMapTo, shareReplay, mapTo, takeUntil, throttleTime } from "rxjs/operators"; import { AtlasViewerAPIServices, UserLandmark } from "../../atlasViewer/atlasViewer.apiService.service"; import { timedValues } from "../../util/generator"; import { AtlasViewerConstantsServices } from "../../atlasViewer/atlasViewer.constantService.service"; @@ -87,7 +87,7 @@ const scanFn : (acc:[boolean, boolean, boolean], curr: CustomEvent) => [boolean, ] }) -export class NehubaContainer implements OnInit, OnDestroy{ +export class NehubaContainer implements OnInit, OnChanges, OnDestroy{ @ViewChild('container',{read:ViewContainerRef}) container : ViewContainerRef @@ -112,10 +112,16 @@ export class NehubaContainer implements OnInit, OnDestroy{ private fetchedSpatialDatasets$ : Observable<Landmark[]> private userLandmarks$ : Observable<UserLandmark[]> - public onHoverSegmentName$ : Observable<string> + public onHoverSegment$ : Observable<any> + + @Input() + private currentOnHover: {segments:any, landmark:any, userLandmark: any} + + @Input() + private currentOnHoverObs$: Observable<{segments:any, landmark:any, userLandmark: any}> + public onHoverSegments$: Observable<any[]> - private onHoverLandmark$ : Observable<any|null> private navigationChanges$ : Observable<any> public spatialResultsVisible$ : Observable<boolean> @@ -272,111 +278,6 @@ export class NehubaContainer implements OnInit, OnDestroy{ distinctUntilChanged() ) - this.onHoverSegments$ = this.store.pipe( - select('uiState'), - select('mouseOverSegments'), - filter(v => !!v), - distinctUntilChanged((o, n) => o.length === n.length - && n.every(segment => - o.find(oSegment => oSegment.layer.name === segment.layer.name - && oSegment.segment === segment.segment))) - ) - - const sortByFreshness: (acc: any[], curr: any[]) => any[] = (acc, curr) => { - - const getLayerName = ({layer} = {layer:{}}) => { - const { name } = <any>layer - return name - } - - const newEntries = curr.filter(entry => { - const name = getLayerName(entry) - return acc.map(getLayerName).indexOf(name) < 0 - }) - - const entryChanged: (itemPrevState, newArr) => boolean = (itemPrevState, newArr) => { - const layerName = getLayerName(itemPrevState) - const { segment } = itemPrevState - const foundItem = newArr.find((_item) => - getLayerName(_item) === layerName) - - if (foundItem) { - const { segment:foundSegment } = foundItem - return segment !== foundSegment - } else { - /** - * if item was not found in the new array, meaning hovering nothing - */ - return segment !== null - } - } - - const getItemFromLayerName = (item, arr) => { - const foundItem = arr.find(i => getLayerName(i) === getLayerName(item)) - return foundItem - ? foundItem - : { - layer: item.layer, - segment: null - } - } - - const getReduceExistingLayers = (newArr) => ([changed, unchanged], _curr) => { - const changedFlag = entryChanged(_curr, newArr) - return changedFlag - ? [ changed.concat( getItemFromLayerName(_curr, newArr) ), unchanged ] - : [ changed, unchanged.concat(_curr) ] - } - - /** - * now, for all the previous layers, separate into changed and unchanged layers - */ - const [changed, unchanged] = acc.reduce(getReduceExistingLayers(curr), [[], []]) - return [...newEntries, ...changed, ...unchanged] - } - - this.onHoverSegment$ = this.onHoverSegments$.pipe( - scan(sortByFreshness, []), - /** - * take the first element after sort by freshness - */ - map(arr => arr[0]), - /** - * map to the older interface - */ - filter(v => !!v), - map(({ segment }) => { - - return { - labelIndex: (isNaN(segment) && Number(segment.labelIndex)) || null, - foundRegion: (isNaN(segment) && segment) || null - } - }) - ) - - this.onHoverLandmark$ = this.store.pipe( - select('uiState'), - filter(state => isDefined(state)), - map(state => state.mouseOverLandmark) - ) - - // TODO hack, even though octant is hidden, it seems with VTK, one can highlight - this.onHoverSegmentName$ = combineLatest( - this.store.pipe( - select('uiState'), - filter(state=>isDefined(state)), - map(state=>state.mouseOverSegment ? - state.mouseOverSegment.constructor === Number ? - state.mouseOverSegment.toString() : - state.mouseOverSegment.name : - '' ), - distinctUntilChanged() - ), - this.onHoverLandmark$ - ).pipe( - map(results => results[1] === null ? results[0] : '') - ) - this.sliceViewLoadingMain$ = fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent').pipe( scan(scanFn, [null, null, null]), shareReplay(1) @@ -552,6 +453,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ shareReplay(1) ) + // TODO deprecate /* each time a new viewer is initialised, take the first event to get the translation function */ this.subscriptions.push( this.newViewer$.pipe( @@ -833,11 +735,11 @@ export class NehubaContainer implements OnInit, OnDestroy{ this.navigationChanges$, this.selectedRegions$, ).subscribe(([navigation,regions])=>{ - this.nehubaViewer.initNav = - Object.assign({},navigation,{ - positionReal : true - }) - this.nehubaViewer.initRegions = regions.map(({ ngId, labelIndex }) =>generateLabelIndexId({ ngId, labelIndex })) + this.nehubaViewer.initNav = { + ...navigation, + positionReal: true + } + this.nehubaViewer.initRegions = regions.map(({ ngId, labelIndex }) => generateLabelIndexId({ ngId, labelIndex })) }) this.subscriptions.push( @@ -845,18 +747,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ ) /* handler to open/select landmark */ - const clickObs$ = fromEvent(this.elementRef.nativeElement, 'click').pipe( - withLatestFrom(this.onHoverLandmark$), - filter(results => results[1] !== null), - map(results => results[1]), - withLatestFrom( - this.store.pipe( - select('dataStore'), - safeFilter('fetchedSpatialData'), - map(state => state.fetchedSpatialData) - ) - ) - ) + const clickObs$ = fromEvent(this.elementRef.nativeElement, 'click') this.subscriptions.push( clickObs$.pipe( @@ -865,39 +756,14 @@ export class NehubaContainer implements OnInit, OnDestroy{ debounceTime(200) ) ), - filter(arr => arr.length >= 2), - map(arr => [...arr].reverse()[0]), - withLatestFrom(this.selectedLandmarks$) + filter(arr => arr.length >= 2) ) - .subscribe(([clickObs, selectedSpatialDatas]) => { - const [landmark, spatialDatas] = clickObs - const idx = Number(landmark.replace('label=','')) - if(isNaN(idx)){ - console.warn(`Landmark index could not be parsed as a number: ${landmark}`) - return - } - - const newSelectedSpatialDatas = selectedSpatialDatas.findIndex(data => data.name === spatialDatas[idx].name) >= 0 - ? selectedSpatialDatas.filter(v => v.name !== spatialDatas[idx].name) - : selectedSpatialDatas.concat(Object.assign({}, spatialDatas[idx], {_label: landmark}) ) - + .subscribe(() => { + const { currentOnHover } = this this.store.dispatch({ - type : SELECT_LANDMARKS, - landmarks : newSelectedSpatialDatas + type : VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_VIEWER, + payload: { ...currentOnHover } }) - // if(this.datasetViewerRegistry.has(spatialDatas[idx].name)){ - // return - // } - // this.datasetViewerRegistry.add(spatialDatas[idx].name) - // const comp = this.datasetViewerFactory.create(this.injector) - // comp.instance.dataset = spatialDatas[idx] - // comp.onDestroy(() => this.datasetViewerRegistry.delete(spatialDatas[idx].name)) - // this.widgetServices.addNewWidget(comp, { - // exitable : true, - // persistency : false, - // state : 'floating', - // title : `Spatial Dataset - ${spatialDatas[idx].name}` - // }) }) ) @@ -919,7 +785,85 @@ export class NehubaContainer implements OnInit, OnDestroy{ public showObliqueSelection$ : Observable<boolean> public showObliqueRotate$ : Observable<boolean> - ngAfterViewInit(){ + ngOnChanges(){ + if (this.currentOnHoverObs$) { + this.onHoverSegments$ = this.currentOnHoverObs$.pipe( + map(({ segments }) => segments) + ) + + const sortByFreshness: (acc: any[], curr: any[]) => any[] = (acc, curr) => { + + const getLayerName = ({layer} = {layer:{}}) => { + const { name } = <any>layer + return name + } + + const newEntries = curr.filter(entry => { + const name = getLayerName(entry) + return acc.map(getLayerName).indexOf(name) < 0 + }) + + const entryChanged: (itemPrevState, newArr) => boolean = (itemPrevState, newArr) => { + const layerName = getLayerName(itemPrevState) + const { segment } = itemPrevState + const foundItem = newArr.find((_item) => + getLayerName(_item) === layerName) + + if (foundItem) { + const { segment:foundSegment } = foundItem + return segment !== foundSegment + } else { + /** + * if item was not found in the new array, meaning hovering nothing + */ + return segment !== null + } + } + + const getItemFromLayerName = (item, arr) => { + const foundItem = arr.find(i => getLayerName(i) === getLayerName(item)) + return foundItem + ? foundItem + : { + layer: item.layer, + segment: null + } + } + + const getReduceExistingLayers = (newArr) => ([changed, unchanged], _curr) => { + const changedFlag = entryChanged(_curr, newArr) + return changedFlag + ? [ changed.concat( getItemFromLayerName(_curr, newArr) ), unchanged ] + : [ changed, unchanged.concat(_curr) ] + } + + /** + * now, for all the previous layers, separate into changed and unchanged layers + */ + const [changed, unchanged] = acc.reduce(getReduceExistingLayers(curr), [[], []]) + return [...newEntries, ...changed, ...unchanged] + } + + // TODO to be deprected soon + + this.onHoverSegment$ = this.onHoverSegments$.pipe( + scan(sortByFreshness, []), + /** + * take the first element after sort by freshness + */ + map(arr => arr[0]), + /** + * map to the older interface + */ + filter(v => !!v), + map(({ segment }) => { + return { + labelIndex: (isNaN(segment) && Number(segment.labelIndex)) || null, + foundRegion: (isNaN(segment) && segment) || null + } + }) + ) + } } ngOnDestroy(){ @@ -971,6 +915,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ return this.returnTruePos(quadrant,data)[2] } + // handles mouse enter/leave landmarks in 2D handleMouseEnterLandmark(spatialData:any){ spatialData.highlight = true this.store.dispatch({ @@ -1111,7 +1056,9 @@ export class NehubaContainer implements OnInit, OnDestroy{ ) this.nehubaViewerSubscriptions.push( - this.nehubaViewer.mouseoverLandmarkEmitter.subscribe(label => { + this.nehubaViewer.mouseoverLandmarkEmitter.pipe( + throttleTime(100) + ).subscribe(label => { this.store.dispatch({ type : MOUSE_OVER_LANDMARK, landmark : label @@ -1119,33 +1066,15 @@ export class NehubaContainer implements OnInit, OnDestroy{ }) ) - // TODO hack, even though octant is hidden, it seems with vtk one can mouse on hover this.nehubaViewerSubscriptions.push( - this.nehubaViewer.regionSelectionEmitter.pipe( - withLatestFrom(this.onHoverLandmark$), - filter(results => results[1] === null), - withLatestFrom(this.onHoverSegments$), - map(results => results[1]), - filter(arr => arr.length > 0), - map(arr => { - return arr.map(({ layer, segment }) => { - const ngId = segment.ngId || layer.name - const labelIndex = segment.labelIndex - return generateLabelIndexId({ ngId, labelIndex }) - }) - }) - ).subscribe((ids:string[]) => { - const deselectFlag = ids.some(id => this.selectedRegionIndexSet.has(id)) - - const set = new Set(this.selectedRegionIndexSet) - if (deselectFlag) { - ids.forEach(id => set.delete(id)) - } else { - ids.forEach(id => set.add(id)) - } + this.nehubaViewer.mouseoverUserlandmarkEmitter.pipe( + throttleTime(160) + ).subscribe(label => { this.store.dispatch({ - type: SELECT_REGIONS_WITH_ID, - selectRegionIds: [...set] + type: VIEWERSTATE_ACTION_TYPES.MOUSEOVER_USER_LANDMARK_LABEL, + payload: { + label + } }) }) ) @@ -1271,6 +1200,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ } } + // TODO deprecate handleNavigationPositionAndNavigationZoomChange(navigation){ if(!navigation.position){ return diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 32cf2f9b7c437e9e9317e7cc05d214e2eebd6f06..d2c23e12870634e5402ebd87272dfa7869281a2d 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -44,7 +44,6 @@ *ngIf="!(useMobileUI$ | async)" [selectedTemplate]="selectedTemplate" [isMobile]="useMobileUI$ | async" - [onHoverSegmentName]="onHoverSegmentName$ | async" [nehubaViewer]="nehubaViewer"> </ui-status-card> </layout-floating-container> @@ -190,7 +189,7 @@ <!-- place holder when no fav data is available --> <mat-card *ngIf="(!(favDataEntries$ | async)) || (favDataEntries$ | async).length === 0"> <mat-card-content class="muted"> - No pinned dataset ... yet. + No pinned datasets. </mat-card-content> </mat-card> diff --git a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts index f4131f84bc6473b0efec458eb409a5afc5e4ad49..3d86c2cfe5915ad9959032433c08099d164f6fbf 100644 --- a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts +++ b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts @@ -60,6 +60,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy{ } }> = new EventEmitter() @Output() mouseoverLandmarkEmitter : EventEmitter<number | null> = new EventEmitter() + @Output() mouseoverUserlandmarkEmitter: EventEmitter<number | null> = new EventEmitter() @Output() regionSelectionEmitter : EventEmitter<{segment:number, layer:{name?: string, url?: string}}> = new EventEmitter() @Output() errorEmitter : EventEmitter<any> = new EventEmitter() @@ -368,9 +369,9 @@ export class NehubaViewerUnit implements OnInit, OnDestroy{ // [0,1,2].forEach(idx => this.viewportToDatas[idx] = events[idx].detail.viewportToData) // }) pipeFromArray([...takeOnePipe])(fromEvent(this.elementRef.nativeElement, 'viewportToData')) - .subscribe((events:CustomEvent[]) => { - [0,1,2].forEach(idx => this.viewportToDatas[idx] = events[idx].detail.viewportToData) - }) + .subscribe((events:CustomEvent[]) => { + [0,1,2].forEach(idx => this.viewportToDatas[idx] = events[idx].detail.viewportToData) + }) ) } @@ -770,6 +771,12 @@ export class NehubaViewerUnit implements OnInit, OnDestroy{ .subscribe(obj => this.mouseoverLandmarkEmitter.emit(obj.value)) ) + this.ondestroySubscriptions.push( + this.nehubaViewer.mouseOver.layer + .filter(obj => obj.layer.name === this.constantService.ngUserLandmarkLayerName) + .subscribe(obj => this.mouseoverUserlandmarkEmitter.emit(obj.value)) + ) + this._s4$ = this.nehubaViewer.navigationState.position.inRealSpace .filter(v=>typeof v !== 'undefined' && v !== null) .subscribe(v=>this.navPosReal=v) diff --git a/src/ui/searchSideNav/searchSideNav.component.ts b/src/ui/searchSideNav/searchSideNav.component.ts index 21ee2294c33fc2db0ceefac48dd27e7e5d6db6f3..fe072947e18e765d9d9da4664bf0dcf65fc45a66 100644 --- a/src/ui/searchSideNav/searchSideNav.component.ts +++ b/src/ui/searchSideNav/searchSideNav.component.ts @@ -5,7 +5,7 @@ import { LayerBrowser } from "../layerbrowser/layerbrowser.component"; import { Observable, Subscription } from "rxjs"; import { Store, select } from "@ngrx/store"; import { map, startWith, scan, filter, mapTo } from "rxjs/operators"; -import { VIEWERSTATE_ACTION_TYPES } from "../viewerStateController/viewerState.base"; +import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "../viewerStateController/viewerState.base"; import { trackRegionBy } from '../viewerStateController/regionHierachy/regionHierarchy.component' @Component({ @@ -79,7 +79,7 @@ export class SearchSideNav implements OnInit, OnDestroy { removeRegion(region: any){ this.store$.dispatch({ - type: VIEWERSTATE_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY, + type: VIEWERSTATE_CONTROLLER_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY, payload: { region } }) } diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts index e32fa8cb4e4e58760e373c2857d37b454f423584..e34a1fd34a17713358efeff465b7ea1028bf47a0 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts +++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts @@ -6,7 +6,7 @@ import { getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId } from "src/ser import { FormControl } from "@angular/forms"; import { MatAutocompleteSelectedEvent, MatDialog } from "@angular/material"; import { ADD_TO_REGIONS_SELECTION_WITH_IDS, SELECT_REGIONS } from "src/services/state/viewerState.store"; -import { VIEWERSTATE_ACTION_TYPES } from "../viewerState.base"; +import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "../viewerState.base"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; const filterRegionBasedOnText = searchTerm => region => region.name.toLowerCase().includes(searchTerm.toLowerCase()) @@ -127,9 +127,9 @@ export class RegionTextSearchAutocomplete{ // TODO handle mobile handleRegionClick({ mode = null, region = null } = {}){ const type = mode === 'single' - ? VIEWERSTATE_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY + ? VIEWERSTATE_CONTROLLER_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY : mode === 'double' - ? VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY + ? VIEWERSTATE_CONTROLLER_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY : '' this.store$.dispatch({ type, diff --git a/src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component.ts b/src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component.ts index 7b02be507c2ad53a7c7ddfeea986bdef57aab2c7..15dd459928e4e730535da6831a0dc06bc173818d 100644 --- a/src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component.ts +++ b/src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component.ts @@ -3,7 +3,7 @@ import { Store, select } from "@ngrx/store"; import { Observable } from "rxjs"; import { distinctUntilChanged, startWith } from "rxjs/operators"; import { DESELECT_REGIONS } from "src/services/state/viewerState.store"; -import { VIEWERSTATE_ACTION_TYPES } from "src/ui/viewerStateController/viewerState.base"; +import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "src/ui/viewerStateController/viewerState.base"; @Component({ selector: 'currently-selected-regions', @@ -39,7 +39,7 @@ export class CurrentlySelectedRegions { public gotoRegion(event: MouseEvent, region:any){ this.store$.dispatch({ - type: VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY, + type: VIEWERSTATE_CONTROLLER_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY, payload: { region } }) } diff --git a/src/ui/viewerStateController/viewerState.base.ts b/src/ui/viewerStateController/viewerState.base.ts index 72d3b99c7c4302e525d7258781ce50307f8f7581..957be297a44f2f030fa0c0e84d7a697043239b2f 100644 --- a/src/ui/viewerStateController/viewerState.base.ts +++ b/src/ui/viewerStateController/viewerState.base.ts @@ -209,6 +209,7 @@ const ACTION_TYPES = { DOUBLE_CLICK_ON_REGIONHIERARCHY: 'DOUBLE_CLICK_ON_REGIONHIERARCHY', SELECT_TEMPLATE_WITH_NAME: 'SELECT_TEMPLATE_WITH_NAME', SELECT_PARCELLATION_WITH_NAME: 'SELECT_PARCELLATION_WITH_NAME', + } -export const VIEWERSTATE_ACTION_TYPES = ACTION_TYPES +export const VIEWERSTATE_CONTROLLER_ACTION_TYPES = ACTION_TYPES diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts index 3b5e4a625304e0335eec195ee4696730e3d47d3d..4dc0831faf978faf05d2ea2a3152a9714c2541bf 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.ts @@ -3,7 +3,7 @@ import { Injectable, OnInit, OnDestroy } from "@angular/core"; import { Actions, ofType, Effect } from "@ngrx/effects"; import { Store, select, Action } from "@ngrx/store"; import { shareReplay, distinctUntilChanged, map, withLatestFrom, filter } from "rxjs/operators"; -import { VIEWERSTATE_ACTION_TYPES } from "./viewerState.base"; +import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "./viewerState.base"; import { CHANGE_NAVIGATION, SELECT_REGIONS, NEWVIEWER, GENERAL_ACTION_TYPES, SELECT_PARCELLATION, isDefined } from "src/services/stateStore.service"; import { regionFlattener } from "src/util/regionFlattener"; import { UIService } from "src/services/uiService.service"; @@ -46,7 +46,7 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{ ) this.selectParcellationWithName$ = this.actions$.pipe( - ofType(VIEWERSTATE_ACTION_TYPES.SELECT_PARCELLATION_WITH_NAME), + ofType(VIEWERSTATE_CONTROLLER_ACTION_TYPES.SELECT_PARCELLATION_WITH_NAME), map(action => { const { payload = {} } = action as ViewerStateAction const { name } = payload @@ -84,7 +84,7 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{ ) this.selectTemplateWithName$ = this.actions$.pipe( - ofType(VIEWERSTATE_ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME), + ofType(VIEWERSTATE_CONTROLLER_ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME), map(action => { const { payload = {} } = action as ViewerStateAction const { name } = payload @@ -121,7 +121,7 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{ ) this.doubleClickOnHierarchy$ = this.actions$.pipe( - ofType(VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY), + ofType(VIEWERSTATE_CONTROLLER_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY), map(action => { const { payload = {} } = action as ViewerStateAction const { region } = payload @@ -155,7 +155,7 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{ ) this.singleClickOnHierarchy$ = this.actions$.pipe( - ofType(VIEWERSTATE_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY), + ofType(VIEWERSTATE_CONTROLLER_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY), withLatestFrom(this.selectedRegions$), map(([action, regionsSelected]) => { diff --git a/src/util/directives/floatingMouseContextualContainer.directive.ts b/src/util/directives/floatingMouseContextualContainer.directive.ts index 2f62b11aac7311055c9023ba1c32e83077807bbb..556052c751cc5953c797ed7bdbf3883dcdd744ae 100644 --- a/src/util/directives/floatingMouseContextualContainer.directive.ts +++ b/src/util/directives/floatingMouseContextualContainer.directive.ts @@ -1,4 +1,5 @@ import { Directive, HostListener, HostBinding } from "@angular/core"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; @Directive({ selector: '[floatingMouseContextualContainerDirective]' @@ -8,13 +9,21 @@ export class FloatingMouseContextualContainerDirective{ private mousePos: [number, number] = [0, 0] + constructor(private sanitizer: DomSanitizer){ + + } + @HostListener('document:mousemove', ['$event']) mousemove(event:MouseEvent){ this.mousePos = [event.clientX, event.clientY] + + this.transform = `translate(${this.mousePos[0]}px,${this.mousePos[1]}px)` } + @HostBinding('style') + style: SafeUrl = this.sanitizer.bypassSecurityTrustStyle('position: absolute; width: 0; height: 0; top: 0; left: 0;') + + @HostBinding('style.transform') - get transform(){ - return `translate(${this.mousePos[0]}px,${this.mousePos[1]}px)` - } + transform: string = `translate(${this.mousePos[0]}px,${this.mousePos[1]}px)` } \ No newline at end of file diff --git a/src/util/mouseOver.directive.spec.ts b/src/util/mouseOver.directive.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..656931eecdb8e8a0dc05c3df70b752f21da79db9 --- /dev/null +++ b/src/util/mouseOver.directive.spec.ts @@ -0,0 +1,50 @@ +import { temporalPositveScanFn } from './mouseOver.directive' +import { Subject } from 'rxjs'; +import {} from 'jasmine' +import { scan, take, skip } from 'rxjs/operators'; + +const segmentsPositive = { segments: [{ hello: 'world' }] } as {segments:any} +const segmentsNegative = { segments: null } + +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) + ) + subscriptions.push( + testFirstEv.subscribe( + arr => expect(arr).toBe([ segmentsPositive ]), + null, + () => done() + ) + ) + + source.next(segmentsPositive) + source.next(userLandmarkPostive) + source.next(segmentsNegative) + }) +}) diff --git a/src/util/mouseOver.directive.ts b/src/util/mouseOver.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..46333a540d835a74efee3ff1eba98635a833db09 --- /dev/null +++ b/src/util/mouseOver.directive.ts @@ -0,0 +1,207 @@ +import { Directive, Pipe, PipeTransform, SecurityContext } from "@angular/core"; +import { Store, select } from "@ngrx/store"; +import { filter, distinctUntilChanged, map, shareReplay, scan, startWith } from "rxjs/operators"; +import { merge, Observable, combineLatest } from "rxjs"; +import { TransformOnhoverSegmentPipe } from "src/atlasViewer/onhoverSegment.pipe"; +import { SafeHtml, DomSanitizer } from "@angular/platform-browser"; + + +/** + * Scan function which prepends newest positive (i.e. defined) value + * + * e.g. const source = new Subject() + * source.pipe( + * scan(temporalPositveScanFn, []) + * ).subscribe(console.log) // outputs + * + * + * + */ +export const temporalPositveScanFn = (acc: {segments:any, landmark:any, userLandmark: any}[], curr: {segments:any, landmark:any, userLandmark: any}) => { + + const keys = Object.keys(curr) + const isPositive = keys.some(key => !!curr[key]) + + return isPositive + ? [curr, ...(acc.filter(item => !keys.some(key => !!item[key])))] as {segments?:any, landmark?:any, userLandmark?: any}[] + : acc.filter(item => !keys.some(key => !!item[key])) +} + +@Directive({ + selector: '[iav-mouse-hover]', + exportAs: 'iavMouseHover' +}) + +export class MouseHoverDirective{ + + public onHoverObs$: Observable<{segments:any, landmark:any, userLandmark: any}> + public currentOnHoverObs$: Observable<{segments:any, landmark:any, userLandmark: any}> + + constructor(private store$: Store<any>){ + + const onHoverUserLandmark$ = this.store$.pipe( + select('uiState'), + map(state => state.mouseOverUserLandmark) + ) + + const onHoverLandmark$ = combineLatest( + this.store$.pipe( + select('uiState'), + map(state => state.mouseOverLandmark) + ), + this.store$.pipe( + select('dataStore'), + select('fetchedSpatialData'), + startWith([]) + ) + ).pipe( + map(([landmark, spatialDatas]) => { + if(landmark === null) return landmark + const idx = Number(landmark.replace('label=','')) + if(isNaN(idx)) { + console.warn(`Landmark index could not be parsed as a number: ${landmark}`) + return { + landmarkName: idx + } + } else { + return { + ...spatialDatas[idx], + landmarkName: spatialDatas[idx].name + } + } + }) + ) + + const onHoverSegments$ = this.store$.pipe( + select('uiState'), + select('mouseOverSegments'), + filter(v => !!v), + distinctUntilChanged((o, n) => o.length === n.length + && n.every(segment => + o.find(oSegment => oSegment.layer.name === segment.layer.name + && oSegment.segment === segment.segment))) + ) + + const mergeObs = merge( + onHoverSegments$.pipe( + distinctUntilChanged(), + map(segments => { + return { segments } + }) + ), + onHoverLandmark$.pipe( + distinctUntilChanged(), + map(landmark => { + return { landmark } + }) + ), + onHoverUserLandmark$.pipe( + distinctUntilChanged(), + map(userLandmark => { + return { userLandmark } + }) + ) + ).pipe( + shareReplay(1) + ) + + this.onHoverObs$ = mergeObs.pipe( + scan((acc, curr) => { + return { + ...acc, + ...curr + } + }, { segments: null, landmark: null, userLandmark: null }), + shareReplay(1) + ) + + this.currentOnHoverObs$ = mergeObs.pipe( + scan(temporalPositveScanFn, []), + map(arr => arr[0]), + map(val => { + return { + segments: null, + landmark: null, + userLandmark: null, + ...val + } + }), + shareReplay(1) + ) + } +} + + +@Pipe({ + name: 'mouseOverTextPipe' +}) + +export class MouseOverTextPipe implements PipeTransform{ + + private transformOnHoverSegmentPipe: TransformOnhoverSegmentPipe + constructor(private sanitizer: DomSanitizer){ + this.transformOnHoverSegmentPipe = new TransformOnhoverSegmentPipe(this.sanitizer) + } + + private renderText = ({ label, obj }): SafeHtml[] => { + switch(label) { + case 'landmark': + return [this.sanitizer.sanitize(SecurityContext.HTML, obj.landmarkName)] + case 'segments': + return obj.map(({ segment }) => this.transformOnHoverSegmentPipe.transform(segment)) + case 'userLandmark': + return [this.sanitizer.sanitize(SecurityContext.HTML, obj.id)] + default: + console.log(obj) + return [this.sanitizer.bypassSecurityTrustHtml(`Cannot be displayed: label: ${label}`)] + } + } + + public transform(inc: {segments:any, landmark:any, userLandmark: any}): {label: string, text: SafeHtml[]} [] { + const keys = Object.keys(inc) + return keys + // if is segments, filter out if lengtth === 0 + .filter(key => Array.isArray(inc[key]) ? inc[key].length > 0 : true ) + // for other properties, check if value is defined + .filter(key => !!inc[key]) + .map(key => { + return { + label: key, + text: this.renderText({ label: key, obj: inc[key] }) + } + }) + } +} + +@Pipe({ + name: 'mouseOverIconPipe' +}) + +export class MouseOverIconPipe implements PipeTransform{ + + public transform(type: string): {fontSet:string, fontIcon:string}{ + + switch(type) { + case 'landmark': + return { + fontSet:'fas', + fontIcon: 'fa-map-marker-alt' + } + case 'segments': + return { + fontSet: 'fas', + fontIcon: 'fa-brain' + } + case 'userLandmark': + return { + fontSet:'fas', + fontIcon: 'fa-map-marker-alt' + } + default: + return { + fontSet: 'fas', + fontIcon: 'fa-file' + } + } + } +} \ No newline at end of file diff --git a/src/util/util.module.ts b/src/util/util.module.ts index 7baaf43fbe1c60c10d8fb34c417d71abb5251074..239488050a1507df1091d7fb5264ef14e576c27c 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -3,19 +3,26 @@ import { FilterNullPipe } from "./pipes/filterNull.pipe"; import { FilterRowsByVisbilityPipe } from "src/components/flatTree/filterRowsByVisibility.pipe"; import { StopPropagationDirective } from "./directives/stopPropagation.directive"; import { DelayEventDirective } from "./directives/delayEvent.directive"; +import { MouseHoverDirective, MouseOverTextPipe, MouseOverIconPipe } from "./mouseOver.directive"; @NgModule({ declarations: [ FilterNullPipe, FilterRowsByVisbilityPipe, StopPropagationDirective, - DelayEventDirective + DelayEventDirective, + MouseHoverDirective, + MouseOverTextPipe, + MouseOverIconPipe ], exports: [ FilterNullPipe, FilterRowsByVisbilityPipe, StopPropagationDirective, - DelayEventDirective + DelayEventDirective, + MouseHoverDirective, + MouseOverTextPipe, + MouseOverIconPipe ] })