diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index 26ab547fab0cdd5e17f44785dfbea94665e3fa8f..aea9618aa57060e40a6333322834bbadb14220c3 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import {Injectable, NgZone, Optional, Inject, OnDestroy} from "@angular/core"; +import {Injectable, NgZone, Optional, Inject, OnDestroy, InjectionToken} from "@angular/core"; import { select, Store } from "@ngrx/store"; import { Observable, Subject, Subscription, from, race, of, } from "rxjs"; import { distinctUntilChanged, map, filter, startWith, switchMap, catchError, mapTo } from "rxjs/operators"; @@ -446,3 +446,9 @@ export const overrideNehubaClickFactory = (apiService: AtlasViewerAPIServices, g next() } } + +export const API_SERVICE_SET_VIEWER_HANDLE_TOKEN = new InjectionToken<(viewerHandle) => void>('API_SERVICE_SET_VIEWER_HANDLE_TOKEN') + +export const setViewerHandleFactory = (apiService: AtlasViewerAPIServices) => { + return viewerHandle => apiService.interactiveViewer.viewerHandle = viewerHandle +} diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts index 454a2c9338880b08da7262b4fa279d136bb637e4..f435d07bed178da1c42a14bb1af88ca84818a4ed 100644 --- a/src/atlasViewer/atlasViewer.constantService.service.ts +++ b/src/atlasViewer/atlasViewer.constantService.service.ts @@ -1,16 +1,12 @@ import { HttpClient, HttpHeaders } from "@angular/common/http"; import { Injectable, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { merge, Observable, of, Subscription, throwError, fromEvent, forkJoin } from "rxjs"; -import { catchError, map, shareReplay, switchMap, tap, filter, take } from "rxjs/operators"; -import { LoggingService } from "src/logging"; +import { Observable, Subscription } from "rxjs"; +import { map, shareReplay } from "rxjs/operators"; import { SNACKBAR_MESSAGE } from "src/services/state/uiState.store"; import { IavRootStoreInterface } from "../services/stateStore.service"; -import { AtlasWorkerService } from "./atlasViewer.workerService.service"; import { PureContantService } from "src/util"; -const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16) - @Injectable({ providedIn : 'root', }) @@ -30,77 +26,12 @@ export class AtlasViewerConstantsServices implements OnDestroy { // instead of using window.location.href, which includes query param etc public backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}` - private fetchTemplate = (templateUrl) => this.http.get(`${this.backendUrl}${templateUrl}`, { responseType: 'json' }).pipe( - switchMap((template: any) => { - if (template.nehubaConfig) { return of(template) } - if (template.nehubaConfigURL) { return this.http.get(`${this.backendUrl}${template.nehubaConfigURL}`, { responseType: 'json' }).pipe( - map(nehubaConfig => { - return { - ...template, - nehubaConfig, - } - }), - ) - } - throwError('neither nehubaConfig nor nehubaConfigURL defined') - }), - ) - public totalTemplates = null - private workerUpdateParcellation$ = fromEvent(this.workerService.worker, 'message').pipe( - filter((message: MessageEvent) => message && message.data && message.data.type === 'UPDATE_PARCELLATION_REGIONS'), - map(({ data }) => data) - ) - - private processTemplate = template => forkJoin( - ...template.parcellations.map(parcellation => { - - const id = getUniqueId() - - this.workerService.worker.postMessage({ - type: 'PROPAGATE_PARC_REGION_ATTR', - parcellation, - inheritAttrsOpts: { - ngId: (parcellation as any ).ngId, - relatedAreas: [], - fullId: null - }, - id - }) - - return this.workerUpdateParcellation$.pipe( - filter(({ id: returnedId }) => id === returnedId), - take(1), - map(({ parcellation }) => parcellation) - ) - }) - ) - public getTemplateEndpoint$ = this.http.get(`${this.backendUrl}templates`, { responseType: 'json' }).pipe( shareReplay(1) ) - public initFetchTemplate$ = this.getTemplateEndpoint$.pipe( - tap((arr: any[]) => this.totalTemplates = arr.length), - switchMap((templates: string[]) => merge( - ...templates.map(templateName => this.fetchTemplate(templateName).pipe( - switchMap(template => this.processTemplate(template).pipe( - map(parcellations => { - return { - ...template, - parcellations - } - }) - )) - )), - )), - catchError((err) => { - this.log.warn(`fetching templates error`, err) - return of(null) - }), - ) - public templateUrls = Array(100) /* to be provided by KG in future */ @@ -258,8 +189,6 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" constructor( private store$: Store<IavRootStoreInterface>, private http: HttpClient, - private log: LoggingService, - private workerService: AtlasWorkerService, private pureConstantService: PureContantService ) { @@ -291,7 +220,10 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" this.dissmissUserLayerSnackbarMessage = this.dissmissUserLayerSnackbarMessageDesktop } }), - ) + ), + this.pureConstantService.getTemplateEndpoint$.subscribe(arr => { + this.totalTemplates = arr.length + }) } private subscriptions: Subscription[] = [] diff --git a/src/main.module.ts b/src/main.module.ts index c55025199c41b7e7c32a17da846975f26f86fd96..0c274937627d74c3498016649b6aab73c2b5f55c 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -14,7 +14,7 @@ import { GetNamesPipe } from "./util/pipes/getNames.pipe"; import { HttpClientModule } from "@angular/common/http"; import { EffectsModule } from "@ngrx/effects"; -import { AtlasViewerAPIServices, overrideNehubaClickFactory, CANCELLABLE_DIALOG, GET_TOAST_HANDLER_TOKEN } from "./atlasViewer/atlasViewer.apiService.service"; +import { AtlasViewerAPIServices, overrideNehubaClickFactory, CANCELLABLE_DIALOG, GET_TOAST_HANDLER_TOKEN, API_SERVICE_SET_VIEWER_HANDLE_TOKEN, setViewerHandleFactory } from "./atlasViewer/atlasViewer.apiService.service"; import { AtlasWorkerService } from "./atlasViewer/atlasViewer.workerService.service"; import { ModalUnit } from "./atlasViewer/modalUnit/modalUnit.component"; import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe"; @@ -252,12 +252,17 @@ export const GET_STATE_SNAPSHOT_TOKEN = new InjectionToken('GET_STATE_SNAPSHOT_T provide: TOS_OBS_INJECTION_TOKEN, useFactory: (dbService: DatabrowserService) => dbService.kgTos$, deps: [ DatabrowserService ] - } + }, /** * TODO * once nehubacontainer is separated into viewer + overlay, migrate to nehubaContainer module */ + { + provide: API_SERVICE_SET_VIEWER_HANDLE_TOKEN, + useFactory: setViewerHandleFactory, + deps: [ AtlasViewerAPIServices ] + }, ], bootstrap : [ AtlasViewer, diff --git a/src/services/effect/pluginUseEffect.ts b/src/services/effect/pluginUseEffect.ts index 5a28ace68c9c20e5867d70ebff2ad8e46e983e6e..1c60a3d5a99fb8a758aeeaf318308330cf23794f 100644 --- a/src/services/effect/pluginUseEffect.ts +++ b/src/services/effect/pluginUseEffect.ts @@ -6,7 +6,6 @@ import { filter, map, startWith, switchMap } from "rxjs/operators" import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service" import { PluginServices } from "src/atlasViewer/pluginUnit" import { PLUGINSTORE_ACTION_TYPES, PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.store' -import { LoggingService } from "src/logging" import { IavRootStoreInterface } from "../stateStore.service" import { HttpClient } from "@angular/common/http" @@ -23,7 +22,6 @@ export class PluginServiceUseEffect { store$: Store<IavRootStoreInterface>, constantService: AtlasViewerConstantsServices, pluginService: PluginServices, - private log: LoggingService, http: HttpClient ) { this.initManifests$ = store$.pipe( diff --git a/src/services/state/viewerState.store.helper.ts b/src/services/state/viewerState.store.helper.ts index 6405a55b39509fbe3a6315d24ff0d332af115a95..de45f8a073249ff6c2d2de0b7b9505fb02fb58bb 100644 --- a/src/services/state/viewerState.store.helper.ts +++ b/src/services/state/viewerState.store.helper.ts @@ -7,6 +7,7 @@ import { withLatestFrom, map } from "rxjs/operators"; import { Injectable } from "@angular/core"; import { + viewerStateNewViewer, viewerStateHelperSelectParcellationWithId, viewerStateNavigateToRegion, viewerStateRemoveAdditionalLayer, @@ -22,10 +23,14 @@ import { viewerStateSelectRegionWithIdDeprecated, viewerStateDblClickOnViewer, viewerStateAddUserLandmarks, - viewreStateRemoveUserLandmarks + viewreStateRemoveUserLandmarks, + viewerStateMouseOverCustomLandmark, + viewerStateMouseOverCustomLandmarkInPerspectiveView, + viewerStateSelectTemplateWithName, } from './viewerState/actions' export { + viewerStateNewViewer, viewerStateHelperSelectParcellationWithId, viewerStateNavigateToRegion, viewerStateRemoveAdditionalLayer, @@ -41,7 +46,10 @@ export { viewerStateSelectRegionWithIdDeprecated, viewerStateDblClickOnViewer, viewerStateAddUserLandmarks, - viewreStateRemoveUserLandmarks + viewreStateRemoveUserLandmarks, + viewerStateMouseOverCustomLandmark, + viewerStateMouseOverCustomLandmarkInPerspectiveView, + viewerStateSelectTemplateWithName, } import { @@ -50,6 +58,8 @@ import { viewerStateSelectedParcellationSelector, viewerStateGetSelectedAtlas, viewerStateCustomLandmarkSelector, + viewerStateFetchedTemplatesSelector, + viewerStateNavigationStateSelector, } from './viewerState/selectors' export { @@ -57,6 +67,8 @@ export { viewerStateSelectedTemplateSelector, viewerStateSelectedParcellationSelector, viewerStateCustomLandmarkSelector, + viewerStateFetchedTemplatesSelector, + viewerStateNavigationStateSelector, } interface IViewerStateHelperStore{ diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index 0060ef7b39b1644b9d2211648fd7ef85cf88a914..63fefee549b4b6366197ef7e66e19d0a249f1379 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -17,9 +17,14 @@ import { viewerStateSelectParcellation, viewerStateSelectRegionWithIdDeprecated, viewerStateCustomLandmarkSelector, + viewerStateDblClickOnViewer, + viewerStateAddUserLandmarks, + viewreStateRemoveUserLandmarks, + viewerStateMouseOverCustomLandmark, + viewerStateMouseOverCustomLandmarkInPerspectiveView, + viewerStateNewViewer } from './viewerState.store.helper'; - -import { viewerStateDblClickOnViewer, viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks, viewerStateMouseOverCustomLandmark, viewerStateMouseOverCustomLandmarkInPerspectiveView } from './viewerState/actions'; +import { cvtNehubaConfigToNavigationObj } from 'src/ui/viewerStateController/viewerState.useEffect'; export interface StateInterface { fetchedTemplates: any[] @@ -132,17 +137,22 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Part } case NEWVIEWER: { - const { selectParcellation: parcellation } = action + const { + selectParcellation: parcellation, + navigation, + selectTemplate, + } = action + const navigationFromTemplateSelected = cvtNehubaConfigToNavigationObj(selectTemplate?.nehubaConfig?.dataset?.initialNgState) return { ...prevState, - templateSelected : action.selectTemplate, + templateSelected : selectTemplate, parcellationSelected : parcellation, // taken care of by effect.ts // regionsSelected : [], // taken care of by effect.ts // landmarksSelected : [], - navigation : action.navigation, + navigation : navigation || navigationFromTemplateSelected, dedicatedView : null, } } @@ -258,7 +268,7 @@ export function stateStore(state, action) { export const LOAD_DEDICATED_LAYER = 'LOAD_DEDICATED_LAYER' export const UNLOAD_DEDICATED_LAYER = 'UNLOAD_DEDICATED_LAYER' -export const NEWVIEWER = 'NEWVIEWER' +export const NEWVIEWER = viewerStateNewViewer.type export const FETCHED_TEMPLATE = 'FETCHED_TEMPLATE' export const CHANGE_NAVIGATION = 'CHANGE_NAVIGATION' diff --git a/src/services/state/viewerState/actions.ts b/src/services/state/viewerState/actions.ts index 3b19de4b1dd9f725a0b61665be6c641e52729820..4da8446635f964488249447df68329d4b77d1d57 100644 --- a/src/services/state/viewerState/actions.ts +++ b/src/services/state/viewerState/actions.ts @@ -1,6 +1,15 @@ import { createAction, props } from "@ngrx/store" import { IRegion } from './constants' +export const viewerStateNewViewer = createAction( + `[viewerState] newViewer`, + props<{ + selectTemplate: any + selectParcellation: any + navigation?: any + }>() +) + export const viewerStateSetSelectedRegionsWithIds = createAction( `[viewerState] setSelectedRegionsWithIds`, props<{ selectRegionIds: string[] }>() @@ -46,6 +55,11 @@ export const viewerStateSelectParcellation = createAction( props<{ selectParcellation: any }>() ) +export const viewerStateSelectTemplateWithName = createAction( + `[viewerState] selectTemplateWithName`, + props<{ payload: { name: string } }>() +) + export const viewerStateSelectTemplateWithId = createAction( `[viewerState] selectTemplateWithId`, props<{ payload: { ['@id']: string }, config?: { selectParcellation: { ['@id']: string } } }>() @@ -90,3 +104,4 @@ export const viewerStateMouseOverCustomLandmarkInPerspectiveView = createAction( `[viewerState] mouseOverCustomLandmarkInPerspectiveView`, props<{ payload: { label: string } }>() ) + diff --git a/src/services/state/viewerState/selectors.ts b/src/services/state/viewerState/selectors.ts index 8b5b442e3d11d50fd577328099057ac4af89a249..44c4c8db76f62baea9ad3a656d7fcad184ed7ad2 100644 --- a/src/services/state/viewerState/selectors.ts +++ b/src/services/state/viewerState/selectors.ts @@ -22,6 +22,11 @@ const flattenFetchedTemplatesIntoParcellationsReducer = (acc, curr) => { return acc.concat( parcelations ) } +export const viewerStateFetchedTemplatesSelector = createSelector( + state => state['viewerState'], + viewerState => viewerState['fetchedTemplates'] +) + export const viewerStateSelectedTemplateSelector = createSelector( state => state['viewerState'], viewerState => viewerState['templateSelected'] @@ -32,6 +37,11 @@ export const viewerStateSelectedParcellationSelector = createSelector( viewerState => viewerState['parcellationSelected'] ) +export const viewerStateNavigationStateSelector = createSelector( + state => state['viewerState'], + viewerState => viewerState['navigation'] +) + export const viewerStateAllRegionsFlattenedRegionSelector = createSelector( viewerStateSelectedParcellationSelector, parc => { diff --git a/src/ui/nehubaContainer/nehubaContainer.component.spec.ts b/src/ui/nehubaContainer/nehubaContainer.component.spec.ts index 966569fe94484d56131a535dad0d45f0413ea6cc..3e5017b97f3c820dd1637442e736a6505f4edf04 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.spec.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.spec.ts @@ -3,13 +3,12 @@ import { TestBed, async, ComponentFixture, fakeAsync, tick, flush, discardPeriod import { NehubaContainer } from "./nehubaContainer.component" import { provideMockStore, MockStore } from "@ngrx/store/testing" import { defaultRootState } from 'src/services/stateStore.service' -import { ComponentsModule } from "src/components" import { AngularMaterialModule } from "../sharedModules/angularMaterial.module" import { TouchSideClass } from "./touchSideClass.directive" import { MaximmisePanelButton } from "./maximisePanelButton/maximisePanelButton.component" import { LandmarkUnit } from './landmarkUnit/landmarkUnit.component' import { LayoutModule } from 'src/layouts/layout.module' -import { UtilModule } from "src/util" +import { PureContantService, UtilModule } from "src/util" import { AtlasLayerSelector } from "../atlasLayerSelector/atlasLayerSelector.component" import { StatusCardComponent } from './statusCard/statusCard.component' import { NehubaViewerTouchDirective } from './nehubaViewerInterface/nehubaViewerTouch.directive' @@ -28,7 +27,6 @@ import { StateModule } from 'src/state' import { ReactiveFormsModule, FormsModule } from '@angular/forms' import { HttpClientModule } from '@angular/common/http' import { WidgetModule } from 'src/widget' -import { PluginModule } from 'src/atlasViewer/pluginUnit/plugin.module' import { NehubaModule } from './nehuba.module' import { CommonModule } from '@angular/common' import { IMPORT_NEHUBA_INJECT_TOKEN } from './nehubaViewer/nehubaViewer.component' @@ -39,6 +37,8 @@ import { ARIA_LABELS } from 'common/constants' import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { RegionAccordionTooltipTextPipe } from '../util' import { hot } from 'jasmine-marbles' +import { of } from 'rxjs' +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing' import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from 'src/services/state/ngViewerState/selectors' import { PANELS } from 'src/services/state/ngViewerState/constants' @@ -69,9 +69,7 @@ describe('> nehubaContainer.component.ts', () => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, - PluginModule, WidgetModule, - ComponentsModule, AngularMaterialModule, LayoutModule, UtilModule, @@ -82,7 +80,13 @@ describe('> nehubaContainer.component.ts', () => { FormsModule, ReactiveFormsModule, HttpClientModule, - CommonModule + CommonModule, + + /** + * because the change done to pureconstant service, need to intercept http call to avoid crypto error message + * so and so components needs to be compiled first. make sure you call compileComponents + */ + HttpClientTestingModule, ], declarations: [ NehubaContainer, @@ -98,7 +102,7 @@ describe('> nehubaContainer.component.ts', () => { CurrentLayout, RegionDirective, RegionTextSearchAutocomplete, - + // pipes MobileControlNubStylePipe, ReorderPanelIndexPipe, @@ -111,13 +115,15 @@ describe('> nehubaContainer.component.ts', () => { { provide: IMPORT_NEHUBA_INJECT_TOKEN, useValue: importNehubaSpy - } + }, + PureContantService, + ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ], }).compileComponents() - + })) it('> component can be created', () => { @@ -128,7 +134,6 @@ describe('> nehubaContainer.component.ts', () => { }) describe('> on selectedTemplatechange', () => { - it('> calls importNehubaPr', async () => { const fixture = TestBed.createComponent(NehubaContainer) fixture.componentInstance.currentOnHoverObs$ = hot('') diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index fd0c2ba97aacc9b0ecd1d474d93e94173a6a0dac..5e13e5789c093cff93384f47b7dcb34b4291e4f3 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, ViewChild, ChangeDetectorRef, Output, EventEmitter } from "@angular/core"; +import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, ViewChild, ChangeDetectorRef, Output, EventEmitter, Inject, Optional } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { combineLatest, fromEvent, merge, Observable, of, Subscription, timer, asyncScheduler, BehaviorSubject, Subject } from "rxjs"; import { pipe } from "rxjs/internal/util/pipe"; @@ -20,8 +20,9 @@ import { MOUSE_OVER_LANDMARK, NgViewerStateInterface } from "src/services/stateStore.service"; -import { getExportNehuba, isSame } from "src/util/fn"; -import { AtlasViewerAPIServices, IUserLandmark } from "src/atlasViewer/atlasViewer.apiService.service"; + +import { getExportNehuba, isSame, getViewer } from "src/util/fn"; +import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, IUserLandmark } from "src/atlasViewer/atlasViewer.apiService.service"; import { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component"; import { compareLandmarksChanged } from "src/util/constants"; import { PureContantService } from "src/util"; @@ -292,7 +293,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { constructor( private pureConstantService: PureContantService, - private apiService: AtlasViewerAPIServices, + @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) private setViewerHandle: (arg) => void, private store: Store<any>, private elementRef: ElementRef, private log: LoggingService, @@ -920,7 +921,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { * TODO if plugin subscribes to viewerHandle, and then new template is selected, changes willl not be be sent * could be considered as a bug. */ - this.apiService.interactiveViewer.viewerHandle = null + this.setViewerHandle && this.setViewerHandle(null) this.nehubaContainerDirective.clear() this.nehubaViewer = null @@ -937,7 +938,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { } private setupViewerHandleApi() { - this.apiService.interactiveViewer.viewerHandle = { + const viewerHandle = { setNavigationLoc : (coord, realSpace?) => this.nehubaViewer.setNavigationState({ position : coord, positionReal : typeof realSpace !== 'undefined' ? realSpace : true, @@ -1070,6 +1071,8 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { ), getNgHash : this.nehubaViewer.getNgHash, } + + this.setViewerHandle && this.setViewerHandle(viewerHandle) } public setOctantRemoval(octantRemovalFlag: boolean) { diff --git a/src/ui/viewerStateController/viewerState.base.ts b/src/ui/viewerStateController/viewerState.base.ts index ee206f5bd32cad100dc63ade0f6edc1e888619f9..70a67c226bec656ba46429e5d9cd203605a5fcd9 100644 --- a/src/ui/viewerStateController/viewerState.base.ts +++ b/src/ui/viewerStateController/viewerState.base.ts @@ -7,9 +7,9 @@ import { RegionSelection } from "src/services/state/userConfigState.store"; import { IavRootStoreInterface, SELECT_REGIONS, USER_CONFIG_ACTION_TYPES } from "src/services/stateStore.service"; import { MatSelectChange } from "@angular/material/select"; import { MatBottomSheet, MatBottomSheetRef } from "@angular/material/bottom-sheet"; +import { viewerStateSelectTemplateWithName } from "src/services/state/viewerState/actions"; const ACTION_TYPES = { - SELECT_TEMPLATE_WITH_NAME: 'SELECT_TEMPLATE_WITH_NAME', SELECT_PARCELLATION_WITH_NAME: 'SELECT_PARCELLATION_WITH_NAME', } @@ -94,13 +94,11 @@ export class ViewerStateBase implements OnInit { } public handleTemplateChange(event: MatSelectChange) { - - this.store$.dispatch({ - type: ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME, - payload: { - name: event.value, - }, - }) + this.store$.dispatch( + viewerStateSelectTemplateWithName({ + payload: { name: event.value } + }) + ) } public handleParcellationChange(event: MatSelectChange) { diff --git a/src/ui/viewerStateController/viewerState.useEffect.spec.ts b/src/ui/viewerStateController/viewerState.useEffect.spec.ts index 513854f6703aa76d6b5b3c2f79b9fd574c8a13d6..b1eeb09408029608558233406555263d33cf6f73 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.spec.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.spec.ts @@ -1,19 +1,20 @@ -import { ViewerStateControllerUseEffect } from './viewerState.useEffect' +import { cvtNavigationObjToNehubaConfig, cvtNehubaConfigToNavigationObj, ViewerStateControllerUseEffect, defaultNavigationObject, defaultNehubaConfigObject } from './viewerState.useEffect' import { Observable, of } from 'rxjs' import { TestBed, async } from '@angular/core/testing' import { provideMockActions } from '@ngrx/effects/testing' -import { provideMockStore } from '@ngrx/store/testing' +import { MockStore, provideMockStore } from '@ngrx/store/testing' import { defaultRootState, NEWVIEWER } from 'src/services/stateStore.service' import { Injectable } from '@angular/core' import { TemplateCoordinatesTransformation, ITemplateCoordXformResp } from 'src/services/templateCoordinatesTransformation.service' import { hot } from 'jasmine-marbles' -import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from './viewerState.base' import { AngularMaterialModule } from '../sharedModules/angularMaterial.module' import { HttpClientModule } from '@angular/common/http' import { WidgetModule } from 'src/widget' import { PluginModule } from 'src/atlasViewer/pluginUnit/plugin.module' +import { viewerStateNavigationStateSelector, viewerStateNewViewer, viewerStateSelectTemplateWithName } from 'src/services/state/viewerState.store.helper' const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json') +const bigBrainNehubaConfig = require('!json-loader!src/res/ext/bigbrainNehubaConfig.json') const colinJson = require('!json-loader!src/res/ext/colin.json') const colinJsonNehubaConfig = require('!json-loader!src/res/ext/colinNehubaConfig.json') const reconstitutedColin = JSON.parse(JSON.stringify( @@ -22,6 +23,12 @@ const reconstitutedColin = JSON.parse(JSON.stringify( nehubaConfig: colinJsonNehubaConfig } )) +const reconstitutedBigBrain = JSON.parse(JSON.stringify( + { + ...bigbrainJson, + nehubaConfig: bigBrainNehubaConfig + } +)) let returnPosition = null @Injectable() class MockCoordXformService{ @@ -34,7 +41,7 @@ class MockCoordXformService{ const initialState = JSON.parse(JSON.stringify( defaultRootState )) initialState.viewerState.fetchedTemplates = [ - bigbrainJson, + reconstitutedBigBrain, reconstitutedColin ] initialState.viewerState.templateSelected = initialState.viewerState.fetchedTemplates[0] @@ -47,8 +54,8 @@ const currentNavigation = { } initialState.viewerState.navigation = currentNavigation -describe('viewerState.useEffect.ts', () => { - describe('ViewerStateControllerUseEffect', () => { +describe('> viewerState.useEffect.ts', () => { + describe('> ViewerStateControllerUseEffect', () => { let actions$: Observable<any> let spy: any beforeEach(async(() => { @@ -59,10 +66,7 @@ describe('viewerState.useEffect.ts', () => { actions$ = hot( 'a', { - a: { - type: VIEWERSTATE_CONTROLLER_ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME, - payload: reconstitutedColin - } + a: viewerStateSelectTemplateWithName({ payload: reconstitutedColin }) } ) @@ -85,27 +89,94 @@ describe('viewerState.useEffect.ts', () => { }).compileComponents() })) - describe('selectTemplate$', () => { + describe('> selectTemplate$', () => { + describe('> when transiting from template A to template B', () => { + describe('> if the current navigation is correctly formed', () => { + it('> uses current navigation param', () => { - it('if coordXform returns error', () => { - const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) - expect( - viewerStateCtrlEffect.selectTemplate$ - ).toBeObservable( - hot( - 'a', - { - a: { - type: NEWVIEWER, - selectTemplate: reconstitutedColin, - selectParcellation: reconstitutedColin.parcellations[0] - } - } - ) - ) + const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) + expect( + viewerStateCtrlEffect.selectTemplate$ + ).toBeObservable( + hot( + 'a', + { + a: viewerStateNewViewer({ + selectTemplate: reconstitutedColin, + selectParcellation: reconstitutedColin.parcellations[0], + }) + } + ) + ) + expect(spy).toHaveBeenCalledWith( + reconstitutedBigBrain.name, + reconstitutedColin.name, + initialState.viewerState.navigation.position + ) + }) + }) + + describe('> if current navigation is malformed', () => { + it('> if current navigation is undefined, use nehubaConfig of last template', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector(viewerStateNavigationStateSelector, null) + const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) + + expect( + viewerStateCtrlEffect.selectTemplate$ + ).toBeObservable( + hot( + 'a', + { + a: viewerStateNewViewer({ + selectTemplate: reconstitutedColin, + selectParcellation: reconstitutedColin.parcellations[0], + }) + } + ) + ) + const { position } = cvtNehubaConfigToNavigationObj(reconstitutedBigBrain.nehubaConfig.dataset.initialNgState) + + expect(spy).toHaveBeenCalledWith( + reconstitutedBigBrain.name, + reconstitutedColin.name, + position + ) + }) + + it('> if current navigation is empty object, use nehubaConfig of last template', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector(viewerStateNavigationStateSelector, {}) + const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) + + expect( + viewerStateCtrlEffect.selectTemplate$ + ).toBeObservable( + hot( + 'a', + { + a: viewerStateNewViewer({ + selectTemplate: reconstitutedColin, + selectParcellation: reconstitutedColin.parcellations[0], + }) + } + ) + ) + const { position } = cvtNehubaConfigToNavigationObj(reconstitutedBigBrain.nehubaConfig.dataset.initialNgState) + + expect(spy).toHaveBeenCalledWith( + reconstitutedBigBrain.name, + reconstitutedColin.name, + position + ) + }) + }) + }) - it('calls with correct param', () => { + it('> if coordXform returns error', () => { const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) expect( viewerStateCtrlEffect.selectTemplate$ @@ -113,26 +184,21 @@ describe('viewerState.useEffect.ts', () => { hot( 'a', { - a: { - type: NEWVIEWER, - selectTemplate: reconstitutedColin, - selectParcellation: reconstitutedColin.parcellations[0] - } + a: viewerStateNewViewer({ + selectTemplate: reconstitutedColin, + selectParcellation: reconstitutedColin.parcellations[0], + }) } ) ) - expect(spy).toHaveBeenCalledWith( - bigbrainJson.name, - reconstitutedColin.name, - initialState.viewerState.navigation.position - ) }) - it('if coordXform returns complete', () => { + it('> if coordXform complete', () => { returnPosition = [ 1.11e6, 2.22e6, 3.33e6 ] const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) const updatedColin = JSON.parse( JSON.stringify( reconstitutedColin ) ) + const initialNgState = updatedColin.nehubaConfig.dataset.initialNgState const updatedColinNavigation = updatedColin.nehubaConfig.dataset.initialNgState.navigation const { zoom, orientation, perspectiveOrientation, position, perspectiveZoom } = currentNavigation @@ -142,6 +208,8 @@ describe('viewerState.useEffect.ts', () => { } updatedColinNavigation.zoomFactor = zoom updatedColinNavigation.pose.orientation = orientation + initialNgState.perspectiveOrientation = perspectiveOrientation + initialNgState.perspectiveZoom = perspectiveZoom expect( viewerStateCtrlEffect.selectTemplate$ @@ -149,15 +217,136 @@ describe('viewerState.useEffect.ts', () => { hot( 'a', { - a: { - type: NEWVIEWER, - selectTemplate: updatedColin, - selectParcellation: updatedColin.parcellations[0] - } + a: viewerStateNewViewer({ + selectTemplate: updatedColin, + selectParcellation: updatedColin.parcellations[0], + }) } ) ) }) + + }) + }) + + describe('> cvtNehubaConfigToNavigationObj', () => { + describe('> returns default obj when input is malformed', () => { + it('> if no arg is provided', () => { + + const obj = cvtNehubaConfigToNavigationObj() + expect(obj).toEqual({ + orientation: [0, 0, 0, 1], + perspectiveOrientation: [0 , 0, 0, 1], + perspectiveZoom: 1e6, + zoom: 1e6, + position: [0, 0, 0], + positionReal: true + }) + }) + it('> if null or undefined is provided', () => { + + const obj = cvtNehubaConfigToNavigationObj(null) + expect(obj).toEqual(defaultNavigationObject) + + const obj2 = cvtNehubaConfigToNavigationObj(undefined) + expect(obj2).toEqual(defaultNavigationObject) + }) + it('> if malformed', () => { + + const obj = cvtNehubaConfigToNavigationObj(reconstitutedBigBrain) + expect(obj).toEqual(defaultNavigationObject) + + const obj2 = cvtNehubaConfigToNavigationObj({}) + expect(obj2).toEqual(defaultNavigationObject) + }) + }) + it('> converts nehubaConfig object to navigation object', () => { + + const obj = cvtNehubaConfigToNavigationObj(reconstitutedBigBrain.nehubaConfig.dataset.initialNgState) + expect(obj).toEqual({ + orientation: [0, 0, 0, 1], + perspectiveOrientation: [ + 0.3140767216682434, + -0.7418519854545593, + 0.4988985061645508, + -0.3195493221282959 + ], + perspectiveZoom: 1922235.5293810747, + zoom: 350000, + position: [ -463219.89446663484, 325772.3617553711, 601535.3736234978 ], + positionReal: true + }) + }) + }) + describe('> cvtNavigationObjToNehubaConfig', () => { + const validNehubaConfigObj = reconstitutedBigBrain.nehubaConfig.dataset.initialNgState + const validNavigationObj = currentNavigation + describe('> if inputs are malformed', () => { + describe('> if navigation object is malformed, uses navigation default object', () => { + it('> if navigation object is null', () => { + const v1 = cvtNavigationObjToNehubaConfig(null, validNehubaConfigObj) + const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, validNehubaConfigObj) + expect(v1).toEqual(v2) + }) + it('> if navigation object is undefined', () => { + const v1 = cvtNavigationObjToNehubaConfig(undefined, validNehubaConfigObj) + const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, validNehubaConfigObj) + expect(v1).toEqual(v2) + }) + + it('> if navigation object is otherwise malformed', () => { + const v1 = cvtNavigationObjToNehubaConfig(reconstitutedBigBrain, validNehubaConfigObj) + const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, validNehubaConfigObj) + expect(v1).toEqual(v2) + + const v3 = cvtNavigationObjToNehubaConfig({}, validNehubaConfigObj) + const v4 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, validNehubaConfigObj) + expect(v3).toEqual(v4) + }) + }) + + describe('> if nehubaConfig object is malformed, use default nehubaConfig obj', () => { + it('> if nehubaConfig is null', () => { + const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, null) + const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) + expect(v1).toEqual(v2) + }) + + it('> if nehubaConfig is undefined', () => { + const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, undefined) + const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) + expect(v1).toEqual(v2) + }) + + it('> if nehubaConfig is otherwise malformed', () => { + const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, {}) + const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) + expect(v1).toEqual(v2) + + const v3 = cvtNavigationObjToNehubaConfig(validNavigationObj, reconstitutedBigBrain) + const v4 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject) + expect(v3).toEqual(v4) + }) + }) + }) + it('> converts navigation object and reference nehuba config object to navigation object', () => { + const convertedVal = cvtNavigationObjToNehubaConfig(validNavigationObj, validNehubaConfigObj) + const { perspectiveOrientation, orientation, zoom, perspectiveZoom, position } = validNavigationObj + + expect(convertedVal).toEqual({ + navigation: { + pose: { + position: { + voxelSize: validNehubaConfigObj.navigation.pose.position.voxelSize, + voxelCoordinates: [0, 1, 2].map(idx => position[idx] / validNehubaConfigObj.navigation.pose.position.voxelSize[idx]) + }, + orientation + }, + zoomFactor: zoom + }, + perspectiveOrientation: perspectiveOrientation, + perspectiveZoom: perspectiveZoom + }) }) }) -}) \ No newline at end of file +}) diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts index 8edba98ef705137f2a8256a9725e06e5d95e20a9..292d47435e236ea0c07a8f2c8bea00f7b108cf26 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.ts @@ -2,15 +2,96 @@ import { Injectable, OnDestroy } from "@angular/core"; import { Actions, Effect, ofType } from "@ngrx/effects"; import { Action, select, Store } from "@ngrx/store"; import { Observable, Subscription, of, merge } from "rxjs"; -import { distinctUntilChanged, filter, map, shareReplay, withLatestFrom, switchMap, mapTo } from "rxjs/operators"; -import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; +import { distinctUntilChanged, filter, map, shareReplay, withLatestFrom, switchMap, mapTo, startWith } from "rxjs/operators"; import { CHANGE_NAVIGATION, FETCHED_TEMPLATE, IavRootStoreInterface, NEWVIEWER, SELECT_PARCELLATION, SELECT_REGIONS, generalActionError } from "src/services/stateStore.service"; import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "./viewerState.base"; import { TemplateCoordinatesTransformation } from "src/services/templateCoordinatesTransformation.service"; import { CLEAR_STANDALONE_VOLUMES } from "src/services/state/viewerState.store"; -import { viewerStateToggleRegionSelect, viewerStateHelperSelectParcellationWithId, viewerStateSelectTemplateWithId, viewerStateNavigateToRegion, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState.store.helper"; +import { viewerStateToggleRegionSelect, viewerStateHelperSelectParcellationWithId, viewerStateSelectTemplateWithId, viewerStateNavigateToRegion, viewerStateSelectedTemplateSelector, viewerStateFetchedTemplatesSelector, viewerStateNewViewer, viewerStateSelectedParcellationSelector, viewerStateNavigationStateSelector, viewerStateSelectTemplateWithName } from "src/services/state/viewerState.store.helper"; import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState/selectors"; import { ngViewerActionClearView } from "src/services/state/ngViewerState/actions"; +import { PureContantService } from "src/util"; + +const defaultPerspectiveZoom = 1e6 +const defaultZoom = 1e6 + +export const defaultNavigationObject = { + orientation: [0, 0, 0, 1], + perspectiveOrientation: [0 , 0, 0, 1], + perspectiveZoom: defaultPerspectiveZoom, + zoom: defaultZoom, + position: [0, 0, 0], + positionReal: true +} + +export const defaultNehubaConfigObject = { + perspectiveOrientation: [0, 0, 0, 1], + perspectiveZoom: 1e6, + navigation: { + pose: { + position: { + voxelCoordinates: [0, 0, 0], + voxelSize: [1,1,1] + }, + orientation: [0, 0, 0, 1], + }, + zoomFactor: defaultZoom + } +} + +export function cvtNehubaConfigToNavigationObj(nehubaConfig?){ + const { navigation, perspectiveOrientation = [0, 0, 0, 1], perspectiveZoom = 1e6 } = nehubaConfig || {} + const { pose, zoomFactor = 1e6 } = navigation || {} + const { position, orientation = [0, 0, 0, 1] } = pose || {} + const { voxelSize = [1, 1, 1], voxelCoordinates = [0, 0, 0] } = position || {} + + return { + orientation, + perspectiveOrientation: perspectiveOrientation, + perspectiveZoom: perspectiveZoom, + zoom: zoomFactor, + position: [0, 1, 2].map(idx => voxelSize[idx] * voxelCoordinates[idx]), + positionReal: true + } +} + +export function cvtNavigationObjToNehubaConfig(navigationObj, nehubaConfigObj){ + const { + orientation = [0, 0, 0, 1], + perspectiveOrientation = [0, 0, 0, 1], + perspectiveZoom = 1e6, + zoom = 1e6, + position = [0, 0, 0], + positionReal = true, + } = navigationObj || {} + + const voxelSize = (() => { + const { + navigation = {} + } = nehubaConfigObj || {} + const { pose = {}, zoomFactor = 1e6 } = navigation + const { position = {}, orientation = [0, 0, 0, 1] } = pose + const { voxelSize = [1, 1, 1], voxelCoordinates = [0, 0, 0] } = position + return voxelSize + })() + + return { + perspectiveOrientation, + perspectiveZoom, + navigation: { + pose: { + position: { + voxelCoordinates: positionReal + ? [0, 1, 2].map(idx => position[idx] / voxelSize[idx]) + : position, + voxelSize + }, + orientation, + }, + zoomFactor: zoom + } + } +} @Injectable({ providedIn: 'root', @@ -23,7 +104,7 @@ export class ViewerStateControllerUseEffect implements OnDestroy { private selectedRegions$: Observable<any[]> @Effect() - public init$ = this.constantSerivce.initFetchTemplate$.pipe( + public init$ = this.pureService.initFetchTemplate$.pipe( map(fetchedTemplate => { return { type: FETCHED_TEMPLATE, @@ -35,8 +116,151 @@ export class ViewerStateControllerUseEffect implements OnDestroy { @Effect() public selectParcellation$: Observable<any> + private selectTemplateIntent$: Observable<any> = merge( + this.actions$.pipe( + ofType(viewerStateSelectTemplateWithId.type), + map(({ payload, config }) => { + return { + templateId: payload['@id'], + parcellationId: config && config['selectParcellation'] && config['selectParcellation']['@id'] + } + }) + ), + this.actions$.pipe( + ofType(viewerStateSelectTemplateWithName), + withLatestFrom(this.store$.pipe( + select(viewerStateFetchedTemplatesSelector) + )), + map(([ action, fetchedTemplates ]) => { + const templateName = (action as any).payload.name + const foundTemplate = fetchedTemplates.find(t => t.name === templateName) + return foundTemplate && foundTemplate['@id'] + }), + filter(v => !!v), + map(templateId => { + return { templateId, parcellationId: null } + }) + ) + ) + @Effect() - public selectTemplate$: Observable<any> + public selectTemplate$: Observable<any> = this.selectTemplateIntent$.pipe( + withLatestFrom( + this.store$.pipe( + select(viewerStateFetchedTemplatesSelector) + ), + this.store$.pipe( + select(viewerStateSelectedParcellationSelector) + ) + ), + map(([ { templateId, parcellationId }, fetchedTemplates, parcellationSelected ]) => { + /** + * find the correct template & parcellation from their IDs + */ + + /** + * for template, just look for the new id in fetched templates + */ + const newTemplateTobeSelected = fetchedTemplates.find(t => t['@id'] === templateId) + if (!newTemplateTobeSelected) { + return { + selectTemplate: null, + selectParcellation: null, + errorMessage: `Selected templateId ${templateId} not found.` + } + } + + /** + * for parcellation, + * if new parc id is defined, try to find the corresponding parcellation in the new template + * if above fails, try to find the corresponding parcellation of the currently selected parcellation + * if the above fails, select the first parcellation in the new template + */ + const selectParcellationWithTemplate = (parcellationId && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === parcellationId)) + || (parcellationSelected && parcellationSelected['@id'] && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === parcellationSelected['@id'])) + || newTemplateTobeSelected.parcellations[0] + + return { + selectTemplate: newTemplateTobeSelected, + selectParcellation: selectParcellationWithTemplate + } + }), + withLatestFrom( + this.store$.pipe( + select(viewerStateSelectedTemplateSelector), + startWith(null as any), + ), + this.store$.pipe( + select(viewerStateNavigationStateSelector), + startWith(null as any), + ) + ), + switchMap(([{ selectTemplate, selectParcellation, errorMessage }, lastSelectedTemplate, navigation]) => { + /** + * if selectTemplate is undefined (cannot find template with id) + */ + if (errorMessage) { + return of(generalActionError({ + message: errorMessage || 'Switching template error.', + })) + } + /** + * if there were no template selected last + * simply return selectTemplate object + */ + if (!lastSelectedTemplate) { + return of(viewerStateNewViewer({ + selectParcellation, + selectTemplate, + })) + } + + /** + * if there were template selected last, extract navigation info + */ + const previousNavigation = (navigation && Object.keys(navigation).length > 0 && navigation) || cvtNehubaConfigToNavigationObj(lastSelectedTemplate.nehubaConfig?.dataset?.initialNgState) + return this.coordinatesTransformation.getPointCoordinatesForTemplate(lastSelectedTemplate.name, selectTemplate.name, previousNavigation.position).pipe( + map(({ status, result }) => { + + /** + * if getPointCoordinatesForTemplate returns error, simply load the temp/parc + */ + if (status === 'error') { + return viewerStateNewViewer({ + selectParcellation, + selectTemplate, + }) + } + + /** + * otherwise, copy the nav state to templateSelected + * deepclone of json object is required, or it will mutate the fetchedTemplate + * setting navigation sometimes creates a race con, as creating nehubaViewer is not sync + */ + const deepCopiedState = JSON.parse(JSON.stringify(selectTemplate)) + const initialNgState = deepCopiedState.nehubaConfig.dataset.initialNgState + + const newInitialNgState = cvtNavigationObjToNehubaConfig({ + ...previousNavigation, + position: result + }, initialNgState) + + /** + * mutation of initialNgState is expected here + */ + deepCopiedState.nehubaConfig.dataset.initialNgState = { + ...initialNgState, + ...newInitialNgState + } + + return viewerStateNewViewer({ + selectTemplate: deepCopiedState, + selectParcellation, + }) + }) + ) + }) + ) @Effect() public toggleRegionSelection$: Observable<any> @@ -67,7 +291,7 @@ export class ViewerStateControllerUseEffect implements OnDestroy { constructor( private actions$: Actions, private store$: Store<IavRootStoreInterface>, - private constantSerivce: AtlasViewerConstantsServices, + private pureService: PureContantService, private coordinatesTransformation: TemplateCoordinatesTransformation ) { const viewerState$ = this.store$.pipe( @@ -135,121 +359,6 @@ export class ViewerStateControllerUseEffect implements OnDestroy { }) ) - /** - * merge all sources into single stream consisting of template id's - */ - this.selectTemplate$ = merge( - this.actions$.pipe( - ofType(viewerStateSelectTemplateWithId.type), - map(({ payload, config }) => { - return { - templateId: payload['@id'], - parcellationId: config && config['selectParcellation'] && config['selectParcellation']['@id'] - } - }) - ), - this.actions$.pipe( - ofType(VIEWERSTATE_CONTROLLER_ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME), - withLatestFrom(viewerState$.pipe( - select('fetchedTemplates') - )), - map(([ action, fetchedTemplates ]) => { - const templateName = (action as any).payload.name - const foundTemplate = fetchedTemplates.find(t => t.name === templateName) - return foundTemplate && foundTemplate['@id'] - }), - filter(v => !!v), - map(templateId => { - return { templateId, parcellationId: null } - }) - ) - ).pipe( - - withLatestFrom( - viewerState$ - ), - switchMap(([{ templateId: newTemplateId, parcellationId: newParcellationId }, { templateSelected, fetchedTemplates, navigation, parcellationSelected }]) => { - if (!templateSelected) { - return of({ - newTemplateId, - templateSelected: templateSelected, - fetchedTemplates, - translatedCoordinate: null, - navigation, - newParcellationId, - parcellationSelected - }) - } - const position = (navigation && navigation.position) || [0, 0, 0] - if (newTemplateId === templateSelected['@id']) return of(null) - - const newTemplateName = fetchedTemplates.find(t => t['@id'] === newTemplateId).name - - return this.coordinatesTransformation.getPointCoordinatesForTemplate(templateSelected.name, newTemplateName, position).pipe( - map(({ status, statusText, result }) => { - if (status === 'error') { - return { - newTemplateId, - templateSelected: templateSelected, - fetchedTemplates, - translatedCoordinate: null, - navigation, - newParcellationId, - parcellationSelected - } - } - return { - newTemplateId, - templateSelected: templateSelected, - fetchedTemplates, - translatedCoordinate: result, - navigation, - newParcellationId, - parcellationSelected - } - }) - ) - }), - filter(v => !!v), - map(({ newTemplateId, templateSelected, newParcellationId, fetchedTemplates, translatedCoordinate, navigation, parcellationSelected }) => { - const newTemplateTobeSelected = fetchedTemplates.find(t => t['@id'] === newTemplateId) - if (!newTemplateTobeSelected) { - return generalActionError({ - message: 'Selected template not found.' - }) - } - - const selectParcellationWithTemplate = (newParcellationId && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === newParcellationId)) - || (parcellationSelected && parcellationSelected['@id'] && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === parcellationSelected['@id'])) - || newTemplateTobeSelected.parcellations[0] - - if (!translatedCoordinate) { - return { - type: NEWVIEWER, - selectTemplate: newTemplateTobeSelected, - selectParcellation: selectParcellationWithTemplate, - } - } - const deepCopiedState = JSON.parse(JSON.stringify(newTemplateTobeSelected)) - const initNavigation = deepCopiedState.nehubaConfig.dataset.initialNgState.navigation - - const { zoom = null, orientation = null } = navigation || {} - if (zoom) initNavigation.zoomFactor = zoom - if (orientation) initNavigation.pose.orientation = orientation - - for (const idx of [0, 1, 2]) { - initNavigation.pose.position.voxelCoordinates[idx] = translatedCoordinate[idx] / initNavigation.pose.position.voxelSize[idx] - } - - return { - type: NEWVIEWER, - selectTemplate: deepCopiedState, - selectParcellation: selectParcellationWithTemplate, - } - }), - ) - - this.navigateToRegion$ = this.actions$.pipe( ofType(viewerStateNavigateToRegion), map(action => { diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts index 5cdd0b0cffa480e53e6cd811b9a6601493c8e6a1..2f9e6bafd5d980217c4daaeb0893766966d74996 100644 --- a/src/util/pureConstant.service.ts +++ b/src/util/pureConstant.service.ts @@ -1,11 +1,15 @@ import { Injectable, OnDestroy } from "@angular/core"; import { Store, createSelector, select } from "@ngrx/store"; -import { Observable, merge, Subscription, of } from "rxjs"; +import { Observable, merge, Subscription, of, throwError, forkJoin, fromEvent } from "rxjs"; import { VIEWER_CONFIG_FEATURE_KEY, IViewerConfigState } from "src/services/state/viewerConfig.store.helper"; -import { shareReplay, tap, scan, catchError, filter, mergeMap, switchMapTo, switchMap } from "rxjs/operators"; +import { shareReplay, tap, scan, catchError, filter, switchMap, map, take } from "rxjs/operators"; import { HttpClient } from "@angular/common/http"; import { BACKENDURL } from './constants' import { viewerStateSetFetchedAtlases } from "src/services/state/viewerState.store.helper"; +import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; +import { LoggingService } from "src/logging"; + +const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16) @Injectable({ providedIn: 'root' @@ -24,9 +28,85 @@ export class PureContantService implements OnDestroy{ (state: IViewerConfigState) => state.useMobileUI ) + public backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}` + + private workerUpdateParcellation$ = fromEvent(this.workerService.worker, 'message').pipe( + filter((message: MessageEvent) => message && message.data && message.data.type === 'UPDATE_PARCELLATION_REGIONS'), + map(({ data }) => data) + ) + + private fetchTemplate = (templateUrl) => this.http.get(`${this.backendUrl}${templateUrl}`, { responseType: 'json' }).pipe( + switchMap((template: any) => { + if (template.nehubaConfig) { return of(template) } + if (template.nehubaConfigURL) { return this.http.get(`${this.backendUrl}${template.nehubaConfigURL}`, { responseType: 'json' }).pipe( + map(nehubaConfig => { + return { + ...template, + nehubaConfig, + } + }), + ) + } + throwError('neither nehubaConfig nor nehubaConfigURL defined') + }), + ) + + private processTemplate = template => forkJoin( + template.parcellations.map(parcellation => { + + const id = getUniqueId() + + this.workerService.worker.postMessage({ + type: 'PROPAGATE_PARC_REGION_ATTR', + parcellation, + inheritAttrsOpts: { + ngId: (parcellation as any ).ngId, + relatedAreas: [], + fullId: null + }, + id + }) + + return this.workerUpdateParcellation$.pipe( + filter(({ id: returnedId }) => id === returnedId), + take(1), + map(({ parcellation }) => parcellation) + ) + }) + ) + + public getTemplateEndpoint$ = this.http.get<any[]>(`${this.backendUrl}templates`, { responseType: 'json' }).pipe( + catchError(() => { + this.log.warn(`fetching root /tempaltes error`) + return of([]) + }), + shareReplay(), + ) + + public initFetchTemplate$ = this.getTemplateEndpoint$.pipe( + switchMap((templates: string[]) => merge( + ...templates.map(templateName => this.fetchTemplate(templateName).pipe( + switchMap(template => this.processTemplate(template).pipe( + map(parcellations => { + return { + ...template, + parcellations + } + }) + )) + )), + )), + catchError((err) => { + this.log.warn(`fetching templates error`, err) + return of(null) + }), + ) + constructor( private store: Store<any>, private http: HttpClient, + private log: LoggingService, + private workerService: AtlasWorkerService, ){ this.darktheme$ = this.store.pipe( select(state => state?.viewerState?.templateSelected?.useTheme === 'dark')