diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3c07d114df83ae2c2dc663da720cc5451914f31..78d9a7e7b59248e5b280835a47c7860b579061e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 14.x for lint + - name: Use Node.js 16.x for lint uses: actions/setup-node@v1 with: - node-version: '14.x' + node-version: '16.x' - run: npm i - run: npm run lint @@ -28,7 +28,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [16.x] env: NODE_ENV: test @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [16.x] env: NODE_ENV: test diff --git a/common/constants.js b/common/constants.js index cf56106555a3b26559711671e22fcc0e06494e86..94a42b81abf59142a3d50f5a175d64208e02fbbd 100644 --- a/common/constants.js +++ b/common/constants.js @@ -108,6 +108,7 @@ If you do not accept the Terms & Conditions you are not permitted to access or u CANNOT_DECIPHER_HEMISPHERE: 'Cannot decipher region hemisphere.', DOES_NOT_SUPPORT_MULTI_REGION_SELECTION: `Please only select a single region.`, MULTI_REGION_SELECTION: `Multi region selection`, + DESCRIPTION: 'Description', REGIONAL_FEATURES: 'Regional features', CONNECTIVITY: 'Connectivity', NO_ADDIONTAL_INFO_AVAIL: `Currently, no additional information is linked to this region.`, diff --git a/deploy/bkwdCompat/urlState.js b/deploy/bkwdCompat/urlState.js index 66f92c3dc4f6b375fcfe0c70f16a685b43c30e0d..64093f9e035e19fc95c9ee8eea8b2caa2303912b 100644 --- a/deploy/bkwdCompat/urlState.js +++ b/deploy/bkwdCompat/urlState.js @@ -114,8 +114,12 @@ const WARNING_STRINGS = { REGION_SELECT_ERROR: 'Region selected cannot be processed properly.', TEMPLATE_ERROR: 'Template not found.', } - +const pliPreviewUrl = `/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:a1655b99-82f1-420f-a3c2-fe80fd4c8588/p:juelich:iav:atlas:v1.0.0:4/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIy..1qI1a.D31U~.i-Os~..HRE/f:siibra:features:voi:19c437087299dd62e7c507200f69aea6` module.exports = (query, _warningCb) => { + + const HOST_PATHNAME = process.env.HOST_PATHNAME || '' + let redirectUrl = `${HOST_PATHNAME}/#` + const { standaloneVolumes, niftiLayers, // deprecating - check if anyone calls this url @@ -163,7 +167,17 @@ module.exports = (query, _warningCb) => { if (Array.isArray(parsedDsp)) { if (parsedDsp.length === 1) { const { datasetId, filename } = parsedDsp[0] - dsp = `/dsp:${encodeId(datasetId)}::${encodeURI(filename)}` + if (datasetId === "minds/core/dataset/v1.0.0/b08a7dbc-7c75-4ce7-905b-690b2b1e8957") { + /** + * if preview pli link, return hardcoded link + */ + return `${HOST_PATHNAME}/#${pliPreviewUrl}` + } else { + /** + * TODO deprecate dsp + */ + dsp = `/dsp:${encodeId(datasetId)}::${encodeURI(filename)}` + } } else { searchParam.set(`previewingDatasetFiles`, previewingDatasetFiles) } @@ -206,8 +220,6 @@ module.exports = (query, _warningCb) => { // ignore region selected and move on } } - const HOST_PATHNAME = process.env.HOST_PATHNAME || '' - let redirectUrl = `${HOST_PATHNAME}/#` if (standaloneVolumes) { searchParam.set('standaloneVolumes', standaloneVolumes) if (nav) redirectUrl += nav diff --git a/deploy/csp/index.js b/deploy/csp/index.js index 7273c69b4e841bc5e0be5aa85d68fb8c10b15e09..898228ce845b95c747d027704c08ab96f8196c30 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -54,7 +54,9 @@ const connectSrc = [ 'object.cscs.ch', // required for dataset previews - 'hbp-kg-dataset-previewer.apps.hbp.eu/v2/', + + // spatial transform + "hbp-spatial-backend.apps.hbp.eu", // injected by env var ...CSP_CONNECT_SRC @@ -102,7 +104,6 @@ module.exports = { ], imgSrc: [ "'self'", - "hbp-kg-dataset-previewer.apps.hbp.eu/v2/" ], scriptSrc:[ "'self'", @@ -118,6 +119,9 @@ module.exports = { ...WHITE_LIST_SRC, ...defaultAllowedSites ], + frameSrc: [ + "*" + ], reportUri: CSP_REPORT_URI || '/report-violation' }, reportOnly diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index ec352e5fbd3707b63b4da42ca92e9157c36bfb0c..fd67601ee087cf32ddd230d7e42b3ca9fdaea421 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -146,7 +146,8 @@ export class AnnotationLayer { } } updateAnnotation(spec: AnnotationSpec) { - const localAnnotations = this.nglayer.layer.localAnnotations + const localAnnotations = this.nglayer?.layer?.localAnnotations + if (!localAnnotations) return const ref = localAnnotations.references.get(spec.id) const _spec = this.parseNgSpecType(spec) if (ref) { diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts index 08bdb9d55bd85a616914cae076797142f3ff1026..88614d6194880e18d3e802de65c1660f75557b13 100644 --- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts +++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts @@ -1,5 +1,8 @@ -import { Component, Input } from "@angular/core"; +import { Component, Input, OnChanges, SimpleChanges } from "@angular/core"; import { SapiDatasetModel } from "src/atlasComponents/sapi"; +import { CONST } from "common/constants" + +const RESTRICTED_ACCESS_ID = "https://nexus.humanbrainproject.org/v0/data/minds/core/embargostatus/v1.0.0/3054f80d-96a8-4dce-9b92-55c68a8b5efd" @Component({ selector: `sxplr-sapiviews-core-datasets-dataset`, @@ -9,7 +12,17 @@ import { SapiDatasetModel } from "src/atlasComponents/sapi"; ] }) -export class DatasetView { +export class DatasetView implements OnChanges{ @Input('sxplr-sapiviews-core-datasets-dataset-input') dataset: SapiDatasetModel + + public isRestricted = false + public CONST = CONST + + ngOnChanges(changes: SimpleChanges): void { + const { dataset } = changes + if (dataset) { + this.isRestricted = (dataset.currentValue as SapiDatasetModel)?.metadata?.accessibility?.["@id"] === RESTRICTED_ACCESS_ID + } + } } diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html index 68ce4f9a8540b0c1154408328260ed2e00a07b5a..f9c37f9907d70750b2de1db334ac2c15949db7ca 100644 --- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html +++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html @@ -24,6 +24,13 @@ <span class="sxplr-m-a"> EBRAINS dataset </span> + + <button *ngIf="isRestricted" + [matTooltip]="CONST.GDPR_TEXT" + mat-icon-button color="warn"> + <i class="fas fa-exclamation-triangle"></i> + </button> + <mat-divider class="sxplr-pl-1" [vertical]="true"></mat-divider> <a mat-icon-button *ngFor="let url of dataset.urls" [href]="url.doi | parseDoi" target="_blank"> diff --git a/src/atlasComponents/sapiViews/core/index.ts b/src/atlasComponents/sapiViews/core/index.ts index 6db4628176e571f0a50adca091dc02c5d51801de..cb3d0ffce18ea93d534e44866392f0fc819b0ae8 100644 --- a/src/atlasComponents/sapiViews/core/index.ts +++ b/src/atlasComponents/sapiViews/core/index.ts @@ -1,3 +1,7 @@ export { SapiViewsCoreModule -} from "./module" \ No newline at end of file +} from "./module" + +export { + SapiViewsCoreSpaceBoundingBox +} from "./space" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts index 541b428be133981a956008ca640e2b2a67a52cf1..27d4a53cc941a95164407b86f61825c01df1527f 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts @@ -1,5 +1,5 @@ -import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core"; -import { concat, Observable, of, timer } from "rxjs"; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges } from "@angular/core"; +import { BehaviorSubject, concat, Observable, of, timer } from "rxjs"; import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; import { ParcellationVisibilityService } from "../parcellationVis.service"; import { ARIA_LABELS } from "common/constants" @@ -38,7 +38,11 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges otherVersions: SapiParcellationModel[] - ngOnChanges() { + ngOnChanges(changes: SimpleChanges) { + const { parcellation } = changes + if (parcellation) { + this.onDismissClicked$.next(false) + } this.otherVersions = [] if (!this.parcellation) { return @@ -82,6 +86,8 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges } dismiss(){ + if (this.onDismissClicked$.value) return + this.onDismissClicked$.next(true) this.onDismiss.emit(this.parcellation) } @@ -93,4 +99,6 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges trackByFn(parc: SapiParcellationModel){ return parc["@id"] } + + onDismissClicked$ = new BehaviorSubject<boolean>(false) } diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css index 93caeb8c21b229a9d3a4efb5a9219e9f604163d3..e8d0ca357ccbfe577e65a2f9d78c1589227cbf33 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css @@ -5,20 +5,6 @@ margin: 0.5rem; } -.otherversion-wrapper.loading > sxplr-sapiviews-core-parcellation-chip -{ - animation: blink 500ms ease-in-out infinite alternate; -} - -@keyframes blink { - 0% { - opacity: 0.8; - } - 100% { - opacity: 0.5; - } -} - .otherversion-wrapper.loading > .spinner-container { position: absolute; diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html index b21af3c775b330b100c4aa8c3e877fdc88237dfd..e56b6b69b9fc9405c4efb29900f9a448921839cf 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html @@ -10,7 +10,10 @@ }"> - <sxplr-sapiviews-core-parcellation-chip + <sxplr-sapiviews-core-parcellation-chip + [ngClass]="{ + 'sxplr-blink': (loadingParc$ | async) === parc + }" [sxplr-sapiviews-core-parcellation-chip-parcellation]="parc" [sxplr-sapiviews-core-parcellation-chip-color]="(parcellation | equality : parc : trackByFn) ? 'primary' : 'default'" (sxplr-sapiviews-core-parcellation-chip-onclick)="selectParcellation(parc)"> @@ -28,7 +31,8 @@ <sxplr-sapiviews-core-parcellation-chip [ngClass]="{ - 'sxplr-muted': !(parcellationVisibility$ | async) + 'sxplr-muted': !(parcellationVisibility$ | async), + 'sxplr-blink': onDismissClicked$ | async }" class="sxplr-d-inline-block" [sxplr-sapiviews-core-parcellation-chip-parcellation]="parcellation" @@ -63,7 +67,12 @@ color="primary" iav-stop="mousedown click" (click)="dismiss()"> - <i class="fas fa-times"></i> + + <spinner-cmp class="sxplr-w-100 sxplr-h-100" *ngIf="onDismissClicked$ | async; else defaultDismissIcon"></spinner-cmp> + <ng-template #defaultDismissIcon> + <i class="fas fa-times"></i> + </ng-template> + </button> </div> </sxplr-sapiviews-core-parcellation-chip> diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts index f0e19a9bc9a82c83dc8e3501ce3f2b99ff51b7bd..6c30611aa3b4ce8494f21bc60501da87089b9bb6 100644 --- a/src/atlasComponents/sapiViews/core/region/module.ts +++ b/src/atlasComponents/sapiViews/core/region/module.ts @@ -1,5 +1,6 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { MarkdownModule } from "src/components/markdown"; import { SpinnerModule } from "src/components/spinner"; import { AngularMaterialModule } from "src/sharedModules"; import { SapiViewsFeaturesModule } from "../../features"; @@ -17,6 +18,7 @@ import { SapiViewsCoreRegionRegionRich } from "./region/rich/region.rich.compone SapiViewsUtilModule, SapiViewsFeaturesModule, SpinnerModule, + MarkdownModule, ], declarations: [ SapiViewsCoreRegionRegionListItem, diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html index 8884f0786d55340b4e389cd1b47ddaaec1d08f85..d4a9a79d17698288b49f115c83d895459327bd20 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html @@ -68,7 +68,7 @@ <spinner-cmp *ngIf="rfDir.busy$ | async"></spinner-cmp> <sxplr-sapiviews-features-entry-list-item - *ngFor="let feat of rfDir.listOfFeatures$ | async" + *ngFor="let feat of rfDir.listOfFeatures$ | async | orderFilterFeatures" [sxplr-sapiviews-features-entry-list-item-feature]="feat" (click)="handleRegionalFeatureClicked(feat)"> </sxplr-sapiviews-features-entry-list-item> @@ -76,10 +76,23 @@ </ng-template> + <ng-template #regionDesc> + <markdown-dom class="sxplr-muted" [markdown]="region?.versionInnovation || 'No description provided.'"> + </markdown-dom> + </ng-template> + <mat-accordion class="d-block mt-2"> + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: CONST.DESCRIPTION, + iconClass: 'fas fa-info', + content: regionDesc, + desc: '', + iconTooltip: 'Description', + iavNgIf: !!region?.versionInnovation + }"> - <mat-accordion class="d-block mt-2"> + </ng-container> <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { title: CONST.REGIONAL_FEATURES, @@ -91,10 +104,6 @@ }"> </ng-container> - </mat-accordion> - - <mat-accordion class="d-block mt-2"> - <!-- connectivity --> <ng-template #sxplrSapiviewsFeaturesConnectivityBrowser> <sxplr-sapiviews-features-connectivity-browser diff --git a/src/atlasComponents/sapiViews/core/space/index.ts b/src/atlasComponents/sapiViews/core/space/index.ts index 46f783b69e03bdae2ef01144ba731e0b264c1c12..26c7eed07b1e454d4eac3bcbdf0c77bb9fe4f162 100644 --- a/src/atlasComponents/sapiViews/core/space/index.ts +++ b/src/atlasComponents/sapiViews/core/space/index.ts @@ -1 +1,4 @@ -export { SapiViewsCoreSpaceModule } from "./module" \ No newline at end of file +export { SapiViewsCoreSpaceModule } from "./module" +export { + SapiViewsCoreSpaceBoundingBox +} from "./boundingBox.directive" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts index 3b915ff248a21b0cf19d3dc5fbac8131b2d21643..8008affe34a6b783fd1b93ed14ee527279e36d04 100644 --- a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts +++ b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; import { SapiFeatureModel } from "src/atlasComponents/sapi"; import { CleanedIeegDataset, CLEANED_IEEG_DATASET_TYPE, SapiDatasetModel, SapiParcellationFeatureMatrixModel, SapiRegionalFeatureReceptorModel, SapiSerializationErrorModel, SapiVOIDataResponse, SxplrCleanedFeatureModel } from "src/atlasComponents/sapi/type"; @@ -7,7 +7,8 @@ import { CleanedIeegDataset, CLEANED_IEEG_DATASET_TYPE, SapiDatasetModel, SapiPa templateUrl: `./entryListItem.template.html`, styleUrls: [ `./entryListItem.style.css` - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SapiViewsFeaturesEntryListItem{ diff --git a/src/atlasComponents/sapiViews/features/index.ts b/src/atlasComponents/sapiViews/features/index.ts index 19e30dcb1873aa37868be5e8944eaad3a9b0ce05..89e1fafbad8108576c708d401326b2d5f4137f6d 100644 --- a/src/atlasComponents/sapiViews/features/index.ts +++ b/src/atlasComponents/sapiViews/features/index.ts @@ -1,3 +1,7 @@ export { SapiViewsFeaturesModule -} from "./module" \ No newline at end of file +} from "./module" + +export { + SapiViewsFeaturesVoiQuery +} from "./voi" diff --git a/src/atlasComponents/sapiViews/features/module.ts b/src/atlasComponents/sapiViews/features/module.ts index cbf7aeacaeccaf721bb6ca55108b01366f3fa685..3df348c72f79be4b53c3050e30627c896e8b93f2 100644 --- a/src/atlasComponents/sapiViews/features/module.ts +++ b/src/atlasComponents/sapiViews/features/module.ts @@ -11,6 +11,7 @@ import * as ieeg from "./ieeg" import * as receptor from "./receptors" import {SapiViewsFeatureConnectivityModule} from "src/atlasComponents/sapiViews/features/connectivity"; import * as voi from "./voi" +import { OrderFilterFeaturesPipe } from "./orderFilterFeatureList.pipe" const { SxplrSapiViewsFeaturesIeegModule @@ -35,6 +36,7 @@ const { SapiViewsFeaturesVoiModule } = voi FeatureBadgeColourPipe, FeatureBadgeFlagPipe, SapiViewsFeaturesEntryListItem, + OrderFilterFeaturesPipe, ], providers: [ { @@ -48,6 +50,7 @@ const { SapiViewsFeaturesVoiModule } = voi SapiViewsFeaturesEntryListItem, SapiViewsFeaturesVoiModule, SapiViewsFeatureConnectivityModule, + OrderFilterFeaturesPipe, ] }) export class SapiViewsFeaturesModule{} diff --git a/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts b/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..382862b0300f4bdaffadce20aeb4e28274a8bef6 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts @@ -0,0 +1,37 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { CLEANED_IEEG_DATASET_TYPE, SapiFeatureModel, SxplrCleanedFeatureModel } from "src/atlasComponents/sapi/type"; +import { environment } from "src/environments/environment" + +type PipableFeatureType = SapiFeatureModel | SxplrCleanedFeatureModel + +type ArrayOperation<T extends boolean | number> = (input: PipableFeatureType) => T + +const FILTER_FN: ArrayOperation<boolean> = feature => { + return feature["@type"] !== "siibra/features/cells" +} + +const ORDER_LIST: ArrayOperation<number> = feature => { + if (feature["@type"] === "siibra/features/receptor") return -4 + if (feature["@type"] === CLEANED_IEEG_DATASET_TYPE) return -3 + if (feature['@type'] === "https://openminds.ebrains.eu/core/DatasetVersion") return 2 + return 0 +} + +@Pipe({ + name: 'orderFilterFeatures', + pure: true +}) + +export class OrderFilterFeaturesPipe implements PipeTransform{ + public transform(inputFeatures: PipableFeatureType[]): PipableFeatureType[] { + return inputFeatures + .filter(f => { + /** + * if experimental flag is set, do not filter out anything + */ + if (environment.EXPERIMENTAL_FEATURE_FLAG) return true + return FILTER_FN(f) + }) + .sort((a, b) => ORDER_LIST(a) - ORDER_LIST(b)) + } +} diff --git a/src/overwrite.scss b/src/overwrite.scss index a6b3e9f55255d3080d58a94e7be6aedce47fc890..790e436d35acc4e970127548ec060fa796b6a650 100644 --- a/src/overwrite.scss +++ b/src/overwrite.scss @@ -264,4 +264,18 @@ $flex-directions: row,column; .#{$nsp}-flex-static { flex: 0 0 auto; -} \ No newline at end of file +} + +.#{$nsp}-blink +{ + animation: blink 500ms ease-in-out infinite alternate; +} + +@keyframes blink { + 0% { + opacity: 0.8; + } + 100% { + opacity: 0.5; + } +} diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts index 6537a7aef4ea762d6d38c85ac335011551b5767e..ca5ae4be810af998f93318a6348f751555fabff3 100644 --- a/src/viewerModule/nehuba/constants.ts +++ b/src/viewerModule/nehuba/constants.ts @@ -64,3 +64,5 @@ export interface IMeshesToLoad { } export const SET_MESHES_TO_LOAD = new InjectionToken<Observable<IMeshesToLoad>>('SET_MESHES_TO_LOAD') + +export const PMAP_LAYER_NAME = 'regional-pmap' diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts index 6f6d708876773bd84a484d73226fab01066f731a..579f7fb79ab3e65547cc420391f2097587eeff18 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -11,7 +11,7 @@ import { EnumColorMapName } from "src/util/colorMaps"; import { getShader } from "src/util/constants"; import { getNgLayersFromVolumesATP, getRegionLabelIndex } from "../config.service"; import { ParcVolumeSpec } from "../store/util"; -import { NehubaLayerControlService } from "./layerCtrl.service"; +import { PMAP_LAYER_NAME } from "../constants"; @Injectable() export class LayerCtrlEffects { @@ -22,7 +22,7 @@ export class LayerCtrlEffects { ), mapTo( atlasAppearance.actions.removeCustomLayer({ - id: NehubaLayerControlService.PMAP_LAYER_NAME + id: PMAP_LAYER_NAME }) ) )) @@ -42,7 +42,7 @@ export class LayerCtrlEffects { atlasAppearance.actions.addCustomLayer({ customLayer: { clType: "customlayer/nglayer", - id: NehubaLayerControlService.PMAP_LAYER_NAME, + id: PMAP_LAYER_NAME, source: `nifti://${sapiRegion.getMapUrl(template["@id"])}`, shader: getShader({ colormap: EnumColorMapName.VIRIDIS, @@ -55,7 +55,7 @@ export class LayerCtrlEffects { ), catchError(() => of( atlasAppearance.actions.removeCustomLayer({ - id: NehubaLayerControlService.PMAP_LAYER_NAME + id: PMAP_LAYER_NAME }) )) ) diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index ba8b1d0adbe40c1c00fb794cac66a0b1890c25ae..5980c76d634265b8d20acac43b548da7d0f293e6 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -13,6 +13,7 @@ import { arrayEqual } from "src/util/array"; import { ColorMapCustomLayer } from "src/state/atlasAppearance"; import { SapiRegionModel } from "src/atlasComponents/sapi"; import { AnnotationLayer } from "src/atlasComponents/annotations"; +import { PMAP_LAYER_NAME } from "../constants" export const BACKUP_COLOR = { red: 255, @@ -25,8 +26,6 @@ export const BACKUP_COLOR = { }) export class NehubaLayerControlService implements OnDestroy{ - static PMAP_LAYER_NAME = 'regional-pmap' - private selectedRegion$ = this.store$.pipe( select(atlasSelection.selectors.selectedRegions), shareReplay(1), @@ -367,7 +366,7 @@ export class NehubaLayerControlService implements OnDestroy{ */ return customLayers .map(l => l.id) - .filter(name => name !== NehubaLayerControlService.PMAP_LAYER_NAME) + .filter(name => name !== PMAP_LAYER_NAME) }) ), this.customLayers$.pipe( @@ -378,7 +377,7 @@ export class NehubaLayerControlService implements OnDestroy{ }), distinctUntilChanged(), map(flag => flag - ? [ NehubaLayerControlService.PMAP_LAYER_NAME ] + ? [ PMAP_LAYER_NAME ] : [] ) ) diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts index 62a4ce09eb3d14682498812aee8e9e90dd01b253..ef3ac53320fbfa6ee86ec80ac140c105a2534af8 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts @@ -7,7 +7,7 @@ import { SapiRegionModel } from "src/atlasComponents/sapi" import * as configSvc from "../config.service" import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects" import { NEVER, of, pipe } from "rxjs" -import { mapTo } from "rxjs/operators" +import { mapTo, take } from "rxjs/operators" import { selectorAuxMeshes } from "../store" @@ -51,6 +51,12 @@ describe('> mesh.service.ts', () => { ) ) }) + + afterEach(() => { + getParcNgIdSpy.calls.reset() + getRegionLabelIndexSpy.calls.reset() + getATPSpy.calls.reset() + }) describe('> NehubaMeshService', () => { beforeEach(() => { TestBed.configureTestingModule({ @@ -72,37 +78,106 @@ describe('> mesh.service.ts', () => { expect(service).toBeTruthy() }) - it('> mixes in auxillaryMeshIndices', () => { - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ]) - mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, []) - mockStore.overrideSelector(selectorAuxMeshes, [auxMesh]) + describe("> loadMeshes$", () => { - const ngId = 'blabla' - const labelIndex = 12 - getParcNgIdSpy.and.returnValue(ngId) - getRegionLabelIndexSpy.and.returnValue(labelIndex) + describe("> auxMesh defined", () => { + + const ngId = 'blabla' + const labelIndex = 12 + + beforeEach(() => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ]) + mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, []) + mockStore.overrideSelector(selectorAuxMeshes, [auxMesh]) + + getParcNgIdSpy.and.returnValue(ngId) + getRegionLabelIndexSpy.and.returnValue(labelIndex) - const service = TestBed.inject(NehubaMeshService) - - expect( - service.loadMeshes$ - ).toBeObservable( - hot('(ab)', { - a: { - layer: { - name: ngId - }, - labelIndicies: [ labelIndex ] - }, - b: { - layer: { - name: auxMesh.ngId, - }, - labelIndicies: auxMesh.labelIndicies - } }) - ) + + it("> auxMesh ngId labelIndex emitted", () => { + + const service = TestBed.inject(NehubaMeshService) + expect( + service.loadMeshes$ + ).toBeObservable( + hot('(ab)', { + a: { + layer: { + name: ngId + }, + labelIndicies: [ labelIndex ] + }, + b: { + layer: { + name: auxMesh.ngId, + }, + labelIndicies: auxMesh.labelIndicies + } + }) + ) + }) + }) + + describe("> if multiple ngid and labelindicies are present", () => { + + const ngId1 = 'blabla' + const labelIndex1 = 12 + + const ngId2 = 'foobar' + const labelIndex2 = 13 + + beforeEach(() => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ]) + mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, [fits1, fits1]) + mockStore.overrideSelector(selectorAuxMeshes, []) + + getParcNgIdSpy.and.returnValues(ngId1, ngId2, ngId2) + getRegionLabelIndexSpy.and.returnValues(labelIndex1, labelIndex2, labelIndex2) + }) + + it('> should call getParcNgIdSpy and getRegionLabelIndexSpy thrice', () => { + const service = TestBed.inject(NehubaMeshService) + service.loadMeshes$.pipe( + take(1) + ).subscribe(() => { + + expect(getParcNgIdSpy).toHaveBeenCalledTimes(3) + expect(getRegionLabelIndexSpy).toHaveBeenCalledTimes(3) + }) + }) + + /** + * in the case of julich brain 2.9 in colin 27, we expect selecting a region will hide meshes from all relevant ngIds (both left and right) + */ + it('> expect the emitted value to be incl all ngIds', () => { + const service = TestBed.inject(NehubaMeshService) + expect( + service.loadMeshes$ + ).toBeObservable( + hot('(ab)', { + a: { + layer: { + name: ngId1 + }, + labelIndicies: [] + }, + b: { + layer: { + name: ngId2 + }, + labelIndicies: [ labelIndex2 ] + } + }) + ) + + }) + }) + }) }) }) diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.ts index 2585f00224e8729a4434afa568e127a486e66fcb..d372ce460746d01c5cf560518e1532f38616b882 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.ts @@ -47,7 +47,40 @@ export class NehubaMeshService implements OnDestroy { ]).pipe( switchMap(([{ atlas, template, parcellation }, regions, selectedRegions]) => { const ngIdRecord: Record<string, number[]> = {} + + const tree = new Tree( + regions, + (c, p) => (c.hasParent || []).some(_p => _p["@id"] === p["@id"]) + ) + + for (const r of regions) { + const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r ) + if (!regionLabelIndex) { + continue + } + if ( + tree.someAncestor(r, anc => !!getRegionLabelIndex(atlas, template, parcellation, anc)) + ) { + continue + } + const ngId = getParcNgId(atlas, template, parcellation, r) + if (!ngIdRecord[ngId]) { + ngIdRecord[ngId] = [] + } + ngIdRecord[ngId].push(regionLabelIndex) + } + if (selectedRegions.length > 0) { + /** + * If regions are selected, reset the meshes + */ + for (const key in ngIdRecord) { + ngIdRecord[key] = [] + } + + /** + * only show selected region + */ for (const r of selectedRegions) { const ngId = getParcNgId(atlas, template, parcellation, r) const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r ) @@ -56,28 +89,6 @@ export class NehubaMeshService implements OnDestroy { } ngIdRecord[ngId].push(regionLabelIndex) } - } else { - const tree = new Tree( - regions, - (c, p) => (c.hasParent || []).some(_p => _p["@id"] === p["@id"]) - ) - - for (const r of regions) { - const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r ) - if (!regionLabelIndex) { - continue - } - if ( - tree.someAncestor(r, (anc) => !!getRegionLabelIndex(atlas, template, parcellation, anc)) - ) { - continue - } - const ngId = getParcNgId(atlas, template, parcellation, r) - if (!ngIdRecord[ngId]) { - ngIdRecord[ngId] = [] - } - ngIdRecord[ngId].push(regionLabelIndex) - } } const arr: IMeshesToLoad[] = [] diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts index fd216151b9bbbf14d3b498eae8ffc98b2501ba05..f2e75e77f501517ac2f166f44a1b851042458489 100644 --- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts +++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts @@ -54,6 +54,10 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ private onDestroyCb: (() => void)[] = [] private removeLayer: () => void + public showOpacityCtrl = false + public hideNgTuneCtrl = 'lower_threshold,higher_threshold,brightness,contrast,colormap,hide-threshold-checkbox' + public defaultOpacity = 1 + @Input('ng-layer-ctl-name') name: string diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html index 2188b17d9f57f496928a0b40f2ba78e3508be9d5..e3442c7b4323dac373e0b9110549b8c97a206a3e 100644 --- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html +++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html @@ -7,4 +7,16 @@ <span> {{ name }} </span> + + <button mat-icon-button (click)="showOpacityCtrl = !showOpacityCtrl"> + <i class="fas fa-cog"></i> + </button> + + <ng-template [ngIf]="showOpacityCtrl"> + <ng-layer-tune + [ngLayerName]="name" + [hideCtrl]="hideNgTuneCtrl" + [opacity]="defaultOpacity"> + </ng-layer-tune> + </ng-template> </div> diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index a194176aa6d83027be793d3ebe1d098aa75f8c8d..f04ad6cc1b0a5b85875c6bc247150b737eb5605c 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -328,7 +328,8 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit * subscribe to main store and negotiate with relay to set camera */ const navSub = this.store$.pipe( - select(atlasSelection.selectors.navigation) + select(atlasSelection.selectors.navigation), + filter(v => !!v), ).subscribe(nav => { const { perspectiveOrientation, perspectiveZoom } = nav this.mainStoreCameraNav = { diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index c01b335222d6ed39caf95183524d7d4c7aae2fac..f0b32efea3c8233a648e0277bfd38ddeaebe2074 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -12,6 +12,9 @@ import { SAPI, SapiRegionModel } from "src/atlasComponents/sapi"; import { atlasSelection, userInteraction, } from "src/state"; import { SapiSpatialFeatureModel, SapiFeatureModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; import { getUuid } from "src/util/fn"; +import { environment } from "src/environments/environment" +import { SapiViewsFeaturesVoiQuery } from "src/atlasComponents/sapiViews/features"; +import { SapiViewsCoreSpaceBoundingBox } from "src/atlasComponents/sapiViews/core"; @Component({ selector: 'iav-cmp-viewer-container', @@ -62,10 +65,17 @@ export class ViewerCmp implements OnDestroy { public CONST = CONST public ARIA_LABELS = ARIA_LABELS + public VOI_QUERY_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG @ViewChild('genericInfoVCR', { read: ViewContainerRef }) genericInfoVCR: ViewContainerRef + @ViewChild('voiFeatures', { read: SapiViewsFeaturesVoiQuery }) + voiQueryDirective: SapiViewsFeaturesVoiQuery + + @ViewChild('bbox', { read: SapiViewsCoreSpaceBoundingBox }) + boundingBoxDirective: SapiViewsCoreSpaceBoundingBox + public quickTourRegionSearch: IQuickTourData = { order: 7, description: QUICKTOUR_DESC.REGION_SEARCH, diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 2012afcff01d3121e46f30ddca9ceff8faebb46d..90d6d4a3810ecb35d68f616af533a3579493d78a 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -29,7 +29,7 @@ </mat-list-item> - <ng-template [ngIf]="voiFeatures.onhover | async" let-feat> + <ng-template [ngIf]="voiQueryDirective && (voiQueryDirective.onhover | async)" let-feat> <mat-list-item> <mat-icon fontSet="fas" @@ -223,9 +223,11 @@ <ng-container *ngTemplateOutlet="autocompleteTmpl; context: { showTour: true }"> </ng-container> - <div *ngIf="!((selectedRegions$ | async)[0])" class="sxplr-p-2 w-100"> - <ng-container *ngTemplateOutlet="spatialFeatureListViewTmpl"></ng-container> - </div> + <ng-template [ngIf]="VOI_QUERY_FLAG"> + <div *ngIf="!((selectedRegions$ | async)[0])" class="sxplr-p-2 w-100"> + <ng-container *ngTemplateOutlet="spatialFeatureListViewTmpl"></ng-container> + </div> + </ng-template> </div> <!-- such a gross implementation --> @@ -264,7 +266,7 @@ isOpen: minTrayVisSwitch.switchState$ | async, regionSelected: selectedRegions$ | async, click: minTrayVisSwitch.toggle.bind(minTrayVisSwitch), - badge: (voiFeatures.features$ | async).length || null + badge: voiQueryDirective && (voiQueryDirective.features$ | async).length || null }"> </ng-container> </div> @@ -983,18 +985,18 @@ </ng-template> <ng-template #spatialFeatureListViewTmpl> - <div *ngIf="voiFeatures.busy$ | async; else notBusyTmpl" class="fs-200"> + <div *ngIf="voiQueryDirective && (voiQueryDirective.busy$ | async); else notBusyTmpl" class="fs-200"> <spinner-cmp></spinner-cmp> </div> <ng-template #notBusyTmpl> - <mat-card *ngIf="(voiFeatures.features$ | async).length > 0" class="pe-all mat-elevation-z4"> + <mat-card *ngIf="voiQueryDirective && (voiQueryDirective.features$ | async).length > 0" class="pe-all mat-elevation-z4"> <mat-card-title> Volumes of interest </mat-card-title> <mat-card-subtitle class="overflow-hidden"> <!-- TODO in future, perhaps encapsulate this as a component? seems like a nature fit in sapiView/space/boundingbox --> - <ng-template let-bbox [ngIf]="bbox.bbox$ | async | getProperty : 'bbox'" [ngIfElse]="bboxFallbackTmpl"> + <ng-template let-bbox [ngIf]="boundingBoxDirective && (boundingBoxDirective.bbox$ | async | getProperty : 'bbox')" [ngIfElse]="bboxFallbackTmpl"> Bounding box: {{ bbox[0] | numbers | json }} - {{ bbox[1] | numbers | json }} mm </ng-template> <ng-template #bboxFallbackTmpl> @@ -1005,17 +1007,21 @@ <mat-divider></mat-divider> - <div *ngFor="let feature of voiFeatures.features$ | async" - mat-ripple - (click)="showDataset(feature)" - class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses"> - {{ feature.metadata.fullName }} - </div> + <ng-template [ngIf]="voiQueryDirective"> + + <div *ngFor="let feature of voiQueryDirective.features$ | async" + mat-ripple + (click)="showDataset(feature)" + class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses"> + {{ feature.metadata.fullName }} + </div> + </ng-template> </mat-card> </ng-template> </ng-template> <div class="d-none" + *ngIf="VOI_QUERY_FLAG" sxplr-sapiviews-core-space-boundingbox [sxplr-sapiviews-core-space-boundingbox-atlas]="selectedAtlas$ | async" [sxplr-sapiviews-core-space-boundingbox-space]="templateSelected$ | async"