diff --git a/common/util.js b/common/util.js index b2da27670fad0bcdd6b7a3465af8f3c486f729b6..4120e4c2744b0661cfdf34293a66ae9bc072068a 100644 --- a/common/util.js +++ b/common/util.js @@ -20,6 +20,20 @@ return true } + const HEMISPHERE = { + LEFT_HEMISPHERE: `left hemisphere`, + RIGHT_HEMISPHERE: `right hemisphere` + } + + exports.getRegionHemisphere = region => { + if (!region) return null + return (region.name && region.name.includes('- right hemisphere') || (!!region.status && region.status.includes('right hemisphere'))) + ? HEMISPHERE.RIGHT_HEMISPHERE + : (region.name && region.name.includes('- left hemisphere') || (!!region.status && region.status.includes('left hemisphere'))) + ? HEMISPHERE.LEFT_HEMISPHERE + : null + } + exports.setsContain = setsContain exports.setsEql = (set1, set2) => { diff --git a/deploy/regionalFeatures/index.js b/deploy/regionalFeatures/index.js index ad46b1a4ef7d5f56c49fc5299fb1f3ee3ea1d37b..009125ea42b90b88e711a024d8b0e3e9f8763940 100644 --- a/deploy/regionalFeatures/index.js +++ b/deploy/regionalFeatures/index.js @@ -64,11 +64,12 @@ Promise.all( const dataIdToDataMap = new Map() datasetIdToDataMap.set(datasetId, dataIdToDataMap) - for (const { ['@id']: dataId, contact_points: contactPoints, referenceSpaces } of data) { + for (const { ['@id']: dataId, contact_points: contactPoints, referenceSpaces, ...rest } of data) { dataIdToDataMap.set(dataId, { ['@id']: dataId, contactPoints, referenceSpaces, + ...rest }) } rs() @@ -114,6 +115,7 @@ const sendFeatureResponse = (req, res) => { const fullIdMap = res.locals['getFeatureMiddleware_cache_0'] const featureDetail = res.locals['getFeatureMiddleware_cache_1'] || {} const dataKeys = Array.from(fullIdMap.keys()) + if (dataKeys.length === 0) return res.status(404).end() return res.status(200).json({ ...featureDetail, data: dataKeys.map(dataId => { @@ -219,7 +221,14 @@ router.get( const returnMap = res.locals['byRegionMiddleware_cache_0'] return res.status(200).json({ - features: Array.from(returnMap.keys()).map(id => ({ ['@id']: id })) + features: Array.from( + returnMap.keys() + ).filter(id => { + /** + * do not return where there are no datas + */ + return returnMap.get(id).size || 0 > 0 + }).map(id => ({ ['@id']: id })) }) } ) diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index 0d07ed8dae5ba71a1ccca9abd2f8ecbca4fb7c1f..e6e394ac82a97772f934c078c3d065e96dae846c 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -403,6 +403,11 @@ markdown-dom pre code height:20em!important; } +.overflow-x-auto +{ + overflow-x: auto; +} + .overflow-x-scroll { overflow-x: scroll; @@ -439,6 +444,11 @@ markdown-dom pre code width: 1em; } +.bs-border-box +{ + box-sizing: border-box; +} + .bs-content-box { box-sizing: content-box; @@ -719,6 +729,26 @@ kg-dataset-previewer > img opacity: 1.0; } +.ml-15px-n +{ + margin-left: -15px!important; +} + +.mr-15px-n +{ + margin-right: -15px!important; +} + +.ml-24px-n +{ + margin-left: -24px!important; +} + +.mr-24px-n +{ + margin-right: -24px!important; +} + .ml-4-n { margin-left: -1rem!important; diff --git a/src/ui/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html b/src/ui/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html index f86265a5e28e97fa691246ce3c020eb4c48fe69f..c9fceb2e755e665c77e3d3784d1eea2de7430992 100644 --- a/src/ui/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html +++ b/src/ui/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html @@ -8,7 +8,8 @@ </button> <mat-card class="mat-elevation-z4"> - <div class="header-container"> + <div class="header-container" + [style.backgroundColor]="'rgba(128,128,128, 0.2)'"> <mat-card-title> <ng-content select="[region-of-interest]"></ng-content> <div> @@ -28,34 +29,50 @@ <a *ngFor="let kgRef of kgReference" [href]="kgRef | doiParserPipe" class="color-inherit" + mat-icon-button + [matTooltip]="EXPLORE_DATASET_IN_KG_ARIA_LABEL" target="_blank"> - <button mat-icon-button - [matTooltip]="EXPLORE_DATASET_IN_KG_ARIA_LABEL"> - <i class="fas fa-external-link-alt"></i> - </button> + <i class="fas fa-external-link-alt"></i> </a> + <!-- in case no doi is available, directly link to KG --> + <ng-template [ngIf]="kgReference.length === 0"> + <a [href]="directLinkToKg" + class="color-inherit" + mat-icon-button + [matTooltip]="EXPLORE_DATASET_IN_KG_ARIA_LABEL" + target="_blank"> + <i class="fas fa-external-link-alt"></i> + </a> + </ng-template> + <!-- fav btn --> <ng-container *ngTemplateOutlet="favDatasetBtn"> </ng-container> </mat-card-subtitle> </div> - <mat-card-content> - <mat-tab-group> - <!-- details --> - <mat-tab> - <ng-template mat-tab-label> - Details - </ng-template> - <ng-template matTabContent> + <mat-card-content class="mt-2 ml-15px-n mr-15px-n"> + <mat-accordion> + + <!-- Details --> + <mat-expansion-panel hideToggle + [expanded]="true"> + <mat-expansion-panel-header> + <mat-panel-title> + Details + </mat-panel-title> + </mat-expansion-panel-header> + + + <ng-template matExpansionPanelContent> <small class="m-1"> <markdown-dom [markdown]="description"> </markdown-dom> </small> </ng-template> - </mat-tab> + </mat-expansion-panel> <!-- features --> <div class="hidden" @@ -66,19 +83,23 @@ </div> <ng-container *ngFor="let feature of (rfGetAllFeatures.features | filterRegionFeaturesById : fullId)"> - <mat-tab> - <ng-template mat-tab-label> - {{ feature.type }} - </ng-template> - <ng-template matTabContent> - <feature-explorer [feature]="feature"> + <mat-expansion-panel hideToggle> + <mat-expansion-panel-header> + <mat-panel-title> + {{ feature.type }} + </mat-panel-title> + </mat-expansion-panel-header> + <ng-template matExpansionPanelContent> + <feature-explorer [feature]="feature" + [region]="region$ | async"> + </feature-explorer> </ng-template> - </mat-tab> + </mat-expansion-panel> </ng-container> + </mat-accordion> - </mat-tab-group> </mat-card-content> </mat-card> diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.base.ts b/src/ui/databrowserModule/singleDataset/singleDataset.base.ts index 9111a1264dcbfb944a6852758116add55cc8f27e..93367c8ae2283dd216c7d861f9b05b82198af5be 100644 --- a/src/ui/databrowserModule/singleDataset/singleDataset.base.ts +++ b/src/ui/databrowserModule/singleDataset/singleDataset.base.ts @@ -13,6 +13,12 @@ import { ARIA_LABELS } from 'common/constants' import { switchMap, distinctUntilChanged, filter } from "rxjs/operators"; import { IContributor } from "../contributor"; +const getDirectLinkToKg = (dataset: { fullId: string, id: string }) => { + const { id } = dataset + if (id) return `https://kg.ebrains.eu/search/instances/Dataset/${encodeURIComponent(id)}` + return null +} + export { DatabrowserService, KgSingleDatasetService, @@ -94,6 +100,7 @@ export class SingleDatasetBase implements OnChanges, OnDestroy { * sic! */ public kgReference: string[] = [] + public directLinkToKg: string public files: IFile[] = [] private methods: string[] = [] @@ -146,6 +153,7 @@ export class SingleDatasetBase implements OnChanges, OnDestroy { this.fullId = fullId this.kgReference = kgReference + this.directLinkToKg = getDirectLinkToKg(dataset) this.dlFromKgHref = this.singleDatasetService.getDownloadZipFromKgHref({ kgSchema, kgId }) diff --git a/src/ui/nehubaContainer/nehubaContainer.style.css b/src/ui/nehubaContainer/nehubaContainer.style.css index f5100b6837542f6cda0361ea4385bd47df2d025d..769f2ab231dfb056ba38112cf443bb597c6da101 100644 --- a/src/ui/nehubaContainer/nehubaContainer.style.css +++ b/src/ui/nehubaContainer/nehubaContainer.style.css @@ -102,15 +102,6 @@ div#scratch-pad background-color: rgba(0, 0, 0, 0.7); } -.side-nav-cover -{ - - margin-left: -15px; - margin-right: -15px; - - box-sizing: border-box; -} - .feature-card { diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 1c9c490c1105b34065b29a217d20ce09723ad474..7cb435de9468301db8cb8fb32adb891d93bd7df0 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -153,7 +153,7 @@ <single-dataset-sidenav-view *ngFor="let id of shownDatasetId" (clear)="clearPreviewingDataset(id)" [fullId]="id" - class="side-nav-cover"> + class="bs-border-box ml-15px-n mr-15px-n"> <mat-chip *ngIf="regionOfInterest$ && regionOfInterest$ | async as region" region-of-interest iav-region @@ -272,7 +272,7 @@ <ng-template #sidenavDsPreviewTmpl let-file="file" let-filename="filename" let-datasetId="datasetId"> <div class="w-100 flex-grow-1 d-flex flex-column"> - <preview-card class="d-block side-nav-cover flex-grow-1" + <preview-card class="d-block bs-border-box ml-15px-n mr-15px-n flex-grow-1" [attr.aria-label]="ARIA_LABEL_ADDITIONAL_VOLUME_CONTROL" [datasetId]="datasetId" [filename]="filename"> @@ -329,7 +329,7 @@ <!-- region tmpl placeholder --> <ng-template #regionPlaceholderTmpl> - <div class="placeholder-region-detail side-nav-cover mat-elevation-z4"> + <div class="placeholder-region-detail bs-border-box ml-15px-n mr-15px-n mat-elevation-z4"> <span class="text-muted"> Select a region by clicking on the viewer or search from above </span> @@ -384,11 +384,11 @@ [region]="{ name: CONST.MULTI_REGION_SELECTION }" - class="side-nav-cover mat-elevation-z4"> + class="bs-border-box ml-15px-n mr-15px-n mat-elevation-z4"> </region-menu> <!-- other regions detail accordion --> - <mat-accordion class="side-nav-cover mt-2"> + <mat-accordion class="bs-border-box ml-15px-n mr-15px-n mt-2"> <!-- regional features--> <ng-template #regionalFeaturesTmpl> @@ -455,13 +455,13 @@ <region-menu [showRegionInOtherTmpl]="false" [region]="region" - class="side-nav-cover mat-elevation-z4"> + class="bs-border-box ml-15px-n mr-15px-n mat-elevation-z4"> </region-menu> </ng-container> <!-- other region detail accordion --> <mat-accordion *ngIf="region" - class="side-nav-cover mt-2" + class="bs-border-box ml-15px-n mr-15px-n mt-2" iav-region [region]="region" #iavRegion="iavRegion"> diff --git a/src/ui/parcellationRegion/region.base.ts b/src/ui/parcellationRegion/region.base.ts index 3f9c62d1e86418065171fd681afe460e59c6de78..2d341de8eb904b8ac23eace486ef5ac2a9e720a5 100644 --- a/src/ui/parcellationRegion/region.base.ts +++ b/src/ui/parcellationRegion/region.base.ts @@ -7,7 +7,7 @@ import { ARIA_LABELS } from 'common/constants' import { flattenRegions, getIdFromFullId, rgbToHsl } from 'common/util' import { viewerStateSetConnectivityRegion, viewerStateNavigateToRegion, viewerStateToggleRegionSelect, viewerStateNewViewer, isNewerThan } from "src/services/state/viewerState.store.helper"; import { viewerStateFetchedTemplatesSelector, viewerStateGetSelectedAtlas, viewerStateSelectedTemplateFullInfoSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; -import { intToRgb, verifyPositionArg } from 'common/util' +import { intToRgb, verifyPositionArg, getRegionHemisphere } from 'common/util' export class RegionBase { @@ -231,11 +231,6 @@ export const getRegionParentParcRefSpace = createSelector( } ) -enum EnumHemisphere{ - LEFT_HEMISPHERE = 'left hemisphere', - RIGHT_HEMISPHERE = 'right hemisphere', -} - @Pipe({ name: 'renderViewOriginDatasetlabel' }) @@ -255,7 +250,6 @@ export const regionInOtherTemplateSelector = createSelector( (fetchedTemplates, templateSelected, prop) => { const { region: regionOfInterest } = prop const returnArr = [] - // const regionOfInterestHemisphere = regionOfInterest.status const regionOfInterestHemisphere = getRegionHemisphere(regionOfInterest) @@ -301,11 +295,3 @@ export const regionInOtherTemplateSelector = createSelector( return returnArr } ) - -export function getRegionHemisphere(region: any): EnumHemisphere{ - return (region.name.includes('- right hemisphere') || (!!region.status && region.status.includes('right hemisphere'))) - ? EnumHemisphere.RIGHT_HEMISPHERE - : (region.name.includes('- left hemisphere') || (!!region.status && region.status.includes('left hemisphere'))) - ? EnumHemisphere.LEFT_HEMISPHERE - : null -} diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html index b8af09822ad398f590fdaabed6056755b1fc8d5b..b6fd52dd0bc4eb97a1cf6d184e8b4c1b5de118fc 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html @@ -68,7 +68,6 @@ <!-- template for switching template --> <mat-menu #regionInOtherTemplatesMenu="matMenu" [aria-label]="SHOW_IN_OTHER_REF_SPACE"> - hello world <ng-template matMenuContent let-regionInOtherTemplates="regionInOtherTemplates"> <mat-list-item *ngFor="let sameRegion of regionInOtherTemplates; let i = index" diff --git a/src/ui/regionalFeatures/featureExplorer/featureExplorer.template.html b/src/ui/regionalFeatures/featureExplorer/featureExplorer.template.html index 06f519eb256af715bf1f419ee13089f1e6d20ee5..59d2d580504b33f5f7f5032b909f62a0e7600ae5 100644 --- a/src/ui/regionalFeatures/featureExplorer/featureExplorer.template.html +++ b/src/ui/regionalFeatures/featureExplorer/featureExplorer.template.html @@ -1,6 +1,7 @@ <ng-container *ngIf="!dataIsLoading; else loadingTmpl"> <mat-accordion + class="ml-24px-n mr-24px-n d-block" regional-feature-interactiviity (rf-interact-onclick-3d-landmark)="handleLandmarkClick($event)" #interactDir="regionalFeatureInteractivity"> @@ -8,7 +9,7 @@ [expanded]="(exploreElectrode$ | async | getProperty : '_' | getProperty : 'electrodeId') === datum['@id']" (opened)="handleDatumExpansion(datum['@id'], true)" (closed)="handleDatumExpansion(datum['@id'], false)" - hideToggle> + togglePosition="before"> <mat-expansion-panel-header> <mat-panel-title> Electrode @@ -18,14 +19,31 @@ </mat-panel-description> </mat-expansion-panel-header> - <span> - contact points - </span> - <mat-list> - <mat-list-item *ngFor="let contactPt of datum['contactPoints']"> - {{ contactPt.position | addUnitAndJoin : '' }} - </mat-list-item> - </mat-list> + <label for="task-list" class="d-block mat-h4 mt-4 text-muted"> + Tasks + </label> + <section class="d-flex align-items-center mt-1"> + <section id="task-list" class="flex-grow-1 flex-shrink-1 overflow-x-auto"> + <div role="list" class="ws-no-wrap"> + <mat-chip *ngFor="let task of datum['tasks']" class="ml-1"> + {{ task }} + </mat-chip> + </div> + </section> + </section> + + <label for="contact-points-list" class="d-block mat-h4 mt-4 text-muted"> + Contact Points + </label> + <section class="d-flex align-items-center mt-1"> + <section id="contact-points-list" class="flex-grow-1 flex-shrink-1 overflow-x-auto"> + <div role="list" class="ws-no-wrap"> + <mat-chip *ngFor="let contactPt of datum['contactPoints']" class="ml-1"> + {{ contactPt.position | addUnitAndJoin : '' }} + </mat-chip> + </div> + </section> + </section> </mat-expansion-panel> </mat-accordion> diff --git a/src/ui/regionalFeatures/regionFeature.base.ts b/src/ui/regionalFeatures/regionFeature.base.ts index de8a190b216c86b1ad5d1ab486c1e8bcfa1b5729..1064b07239661673f592fc9f4daa66ca9bed14ad 100644 --- a/src/ui/regionalFeatures/regionFeature.base.ts +++ b/src/ui/regionalFeatures/regionFeature.base.ts @@ -1,5 +1,5 @@ import { Input, SimpleChanges } from "@angular/core" -import { BehaviorSubject } from "rxjs" +import { BehaviorSubject, of } from "rxjs" import { IFeature, RegionalFeaturesService } from "./regionalFeature.service" export class RegionFeatureBase{ @@ -29,7 +29,11 @@ export class RegionFeatureBase{ if (changes.region && changes.region.previousValue !== changes.region.currentValue) { this.isLoading = true this.features = [] - this._regionalFeatureService.getAllFeaturesByRegion(changes.region.currentValue).pipe( + + const _ = (changes.region.currentValue + ? this._regionalFeatureService.getAllFeaturesByRegion(changes.region.currentValue) + : of([]) + ).pipe( ).subscribe({ next: features => this.features = features, diff --git a/src/ui/regionalFeatures/regionalFeature.service.ts b/src/ui/regionalFeatures/regionalFeature.service.ts index e49fd8ff53188e8511a93afe965be4d34206eef4..faa71b067870bdcaf85f4f39443a6b0d3fd800dd 100644 --- a/src/ui/regionalFeatures/regionalFeature.service.ts +++ b/src/ui/regionalFeatures/regionalFeature.service.ts @@ -1,7 +1,7 @@ import { HttpClient } from "@angular/common/http"; import { Injectable, OnDestroy } from "@angular/core"; import { PureContantService } from "src/util"; -import { getIdFromFullId } from 'common/util' +import { getIdFromFullId, getRegionHemisphere } from 'common/util' import { forkJoin, Subject, Subscription } from "rxjs"; import { switchMap } from "rxjs/operators"; import { IHasId } from "src/util/interfaces"; @@ -48,7 +48,11 @@ export class RegionalFeaturesService implements OnDestroy{ if (!region.fullId) throw new Error(`getAllFeaturesByRegion - region does not have fullId defined`) const regionFullId = getIdFromFullId(region.fullId) - const hemisphereObj = region['status'] ? { hemisphere: region['status'] } : {} + const hemisphereObj = (() => { + const hemisphere = getRegionHemisphere(region) + return hemisphere ? { hemisphere } : {} + })() + const refSpaceObj = this.templateSelected && this.templateSelected.fullId ? { referenceSpaceId: getIdFromFullId(this.templateSelected.fullId) } : {} @@ -59,7 +63,6 @@ export class RegionalFeaturesService implements OnDestroy{ params: { ...hemisphereObj, ...refSpaceObj, - }, responseType: 'json' } @@ -87,10 +90,20 @@ export class RegionalFeaturesService implements OnDestroy{ const refSpaceObj = this.templateSelected && this.templateSelected.fullId ? { referenceSpaceId: getIdFromFullId(this.templateSelected.fullId) } : {} + const hemisphereObj = (() => { + const hemisphere = getRegionHemisphere(region) + return hemisphere ? { hemisphere } : {} + })() + + const regionId = getIdFromFullId(region && region.fullId) + const url = regionId + ? `${this.pureConstantService.backendUrl}regionalFeatures/byRegion/${encodeURIComponent(regionId)}/${encodeURIComponent(feature['@id'])}/${encodeURIComponent(data['@id'])}` + : `${this.pureConstantService.backendUrl}regionalFeatures/byFeature/${encodeURIComponent(feature['@id'])}/${encodeURIComponent(data['@id'])}` return this.http.get<IHasId>( - `${this.pureConstantService.backendUrl}regionalFeatures/byFeature/${encodeURIComponent(feature['@id'])}/${encodeURIComponent(data['@id'])}`, + url, { params: { + ...hemisphereObj, ...refSpaceObj, }, responseType: 'json' diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts index 8e9f842a3132628fb3afe72898b72db3047b8bf2..f5ed55c0457129b8f615d8f59fc693ceccf2b852 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.ts @@ -3,15 +3,16 @@ 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, startWith } from "rxjs/operators"; -import { CHANGE_NAVIGATION, FETCHED_TEMPLATE, IavRootStoreInterface, NEWVIEWER, SELECT_PARCELLATION, SELECT_REGIONS, generalActionError } from "src/services/stateStore.service"; +import { CHANGE_NAVIGATION, FETCHED_TEMPLATE, IavRootStoreInterface, 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, viewerStateFetchedTemplatesSelector, viewerStateNewViewer, viewerStateSelectedParcellationSelector, viewerStateNavigationStateSelector, viewerStateSelectTemplateWithName } from "src/services/state/viewerState.store.helper"; +import { viewerStateToggleRegionSelect, viewerStateHelperSelectParcellationWithId, viewerStateSelectTemplateWithId, viewerStateNavigateToRegion, viewerStateSelectedTemplateSelector, viewerStateFetchedTemplatesSelector, viewerStateNewViewer, viewerStateSelectedParcellationSelector, viewerStateNavigationStateSelector, viewerStateSelectTemplateWithName, viewerStateSelectedRegionsSelector } 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"; import { verifyPositionArg } from 'common/util' +import { uiActionHideAllDatasets } from "src/services/state/uiState/actions"; const defaultPerspectiveZoom = 1e6 const defaultZoom = 1e6 @@ -114,6 +115,17 @@ export class ViewerStateControllerUseEffect implements OnDestroy { }), ) + /** + * on region selected change (clear, select, or change selection), clear selected dataset ids + */ + @Effect() + public clearShownDatasetIdOnRegionClear: Observable<any> = this.store$.pipe( + select(viewerStateSelectedRegionsSelector), + mapTo( + uiActionHideAllDatasets() + ) + ) + @Effect() public selectParcellation$: Observable<any>