diff --git a/deploy/regionalFeatures/index.js b/deploy/regionalFeatures/index.js index 009125ea42b90b88e711a024d8b0e3e9f8763940..14828c5c00eaa99be9f8a2e5a5ca129eb63ba4fa 100644 --- a/deploy/regionalFeatures/index.js +++ b/deploy/regionalFeatures/index.js @@ -1,4 +1,5 @@ const router = require('express').Router() +const { parse } = require('path') const request = require('request') /** @@ -29,49 +30,69 @@ const ITERABLE_KEY_SYMBOL = Symbol('ITERABLE_KEY_SYMBOL') * async await would mean it is fetched one at a time */ +const processRegionalFeatureObj = ({ regions, data, ['@id']: datasetId, type, name }) => { + + datasetIdDetailMap.set(datasetId, { + ['@id']: datasetId, + type, + name + }) + for (const { status, ['@id']: regionId, name, files } of regions) { + if (regionIdToDataIdMap.has(regionId)) { + const existingObj = regionIdToDataIdMap.get(regionId) + /** + * existingObj[datasetId] may be undefined + */ + if (!existingObj[datasetId]) { + existingObj[datasetId] = { + type, + } + existingObj[ITERABLE_KEY_SYMBOL] = existingObj[ITERABLE_KEY_SYMBOL].concat(datasetId) + } + existingObj[datasetId][status] = (existingObj[datasetId][status] || []).concat(files) + existingObj[datasetId][ITERABLE_KEY_SYMBOL] = (existingObj[datasetId][ITERABLE_KEY_SYMBOL] || []).concat(status) + } else { + const datasetObj = { + [status]: files, + type, + } + datasetObj[ITERABLE_KEY_SYMBOL] = [status] + const obj = { + name, + '@id': regionId, + [datasetId]: datasetObj + } + obj[ITERABLE_KEY_SYMBOL] = [datasetId] + regionIdToDataIdMap.set(regionId, obj) + } + } + + const dataIdToDataMap = new Map() + datasetIdToDataMap.set(datasetId, dataIdToDataMap) + + for (const { ['@id']: dataId, contact_points: contactPoints, referenceSpaces, ...rest } of data) { + dataIdToDataMap.set(dataId, { + ['@id']: dataId, + contactPoints, + referenceSpaces, + ...rest + }) + } +} + Promise.all( arrayToFetch.map(url => new Promise((rs, rj) => { request.get(url, (err, _resp, body) => { if (err) return rj(err) - const { regions, data, ['@id']: datasetId, type, name } = JSON.parse(body) - datasetIdDetailMap.set(datasetId, { - ['@id']: datasetId, - type, - name - }) - for (const { status, ['@id']: regionId, name, files } of regions) { - if (regionIdToDataIdMap.has(regionId)) { - const existingObj = regionIdToDataIdMap.get(regionId) - existingObj[datasetId][status] = (existingObj[datasetId][status] || []).concat(files) - existingObj[datasetId][ITERABLE_KEY_SYMBOL] = existingObj[datasetId][ITERABLE_KEY_SYMBOL].concat(status) - } else { - const datasetObj = { - [status]: files, - type, - } - datasetObj[ITERABLE_KEY_SYMBOL] = [status] - const obj = { - name, - '@id': regionId, - [datasetId]: datasetObj - } - obj[ITERABLE_KEY_SYMBOL] = [datasetId] - regionIdToDataIdMap.set(regionId, obj) - } - } - - const dataIdToDataMap = new Map() - datasetIdToDataMap.set(datasetId, dataIdToDataMap) - - for (const { ['@id']: dataId, contact_points: contactPoints, referenceSpaces, ...rest } of data) { - dataIdToDataMap.set(dataId, { - ['@id']: dataId, - contactPoints, - referenceSpaces, - ...rest - }) + const parsedObj = JSON.parse(body) + + if (Array.isArray(parsedObj)) { + parsedObj.map(processRegionalFeatureObj) + } else { + processRegionalFeatureObj(parsedObj) } + rs() }) }) @@ -195,7 +216,7 @@ const byRegionMiddleware = (req, res, next) => { if ( !!referenceSpaceId && !! dataObj['referenceSpaces'] - && dataObj['referenceSpaces'].every(rs => rs['fullId'] !== referenceSpaceId) + && dataObj['referenceSpaces'].every(rs => rs['fullId'] !== '*' && rs['fullId'] !== referenceSpaceId) ) { continue } diff --git a/docs/releases/v2.3.0.md b/docs/releases/v2.3.0.md index 4d6fc0859d24b48fd83fd7329d4dfd88fe88a7f1..bd3c04a72e17891577f1027cb2da88d59d969b3d 100644 --- a/docs/releases/v2.3.0.md +++ b/docs/releases/v2.3.0.md @@ -13,6 +13,7 @@ - showing contributors to a regional feature/dataset if publications are not available - added the ability to customize preview origin dataset to labels other to `View probability map` - Added short bundle HCP to the supported atlas +- reworked regional dataset previews (iEEG & receptor density) - **experimental** : previewing of curated regional features: iEEG coordinates ## Bugfixes: diff --git a/src/ui/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html b/src/ui/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html index e71d9cc744e8d596f258d4bd00a9a99e502a83ac..ba6354fc96315db3206186e7a152d01ffee08516 100644 --- a/src/ui/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html +++ b/src/ui/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html @@ -74,7 +74,7 @@ </ng-template> </mat-expansion-panel> - <!-- features --> + <!-- Features --> <div class="hidden" [region]="region$ | async" (loadingStateChanged)="detectChange()" @@ -83,19 +83,19 @@ </div> <ng-container *ngFor="let feature of (rfGetAllFeatures.features | filterRegionFeaturesById : fullId)"> - <mat-expansion-panel hideToggle> + <mat-expansion-panel hideToggle + #matExpansionPanel> <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-is-loading)="detectChange()"> - - </feature-explorer> + <ng-template [ngIf]="matExpansionPanel.expanded"> + <feature-container + [feature]="feature" + [region]="region$ | async"> + </feature-container> </ng-template> </mat-expansion-panel> </ng-container> diff --git a/src/ui/regionalFeatures/featureContainer/featureContainer.component.ts b/src/ui/regionalFeatures/featureContainer/featureContainer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d6752dae47b53d81f94c067ff653366f59c1a375 --- /dev/null +++ b/src/ui/regionalFeatures/featureContainer/featureContainer.component.ts @@ -0,0 +1,51 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Input, OnChanges, SimpleChanges, ViewContainerRef } from "@angular/core"; +import { Subscription } from "rxjs"; +import { IFeature, RegionalFeaturesService } from "../regionalFeature.service"; +import { ISingleFeature } from "../singleFeatures/interfaces"; + +@Component({ + selector: 'feature-container', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) + +export class FeatureContainer implements OnChanges{ + @Input() + feature: IFeature + + @Input() + region: any + + private cr: ComponentRef<ISingleFeature> + + constructor( + private vCRef: ViewContainerRef, + private rService: RegionalFeaturesService, + private cfr: ComponentFactoryResolver, + private cdr: ChangeDetectorRef + ){ + } + + private viewChangedSub: Subscription + + ngOnChanges(simpleChanges: SimpleChanges){ + if (!simpleChanges.feature) return + const { currentValue, previousValue } = simpleChanges.feature + if (currentValue === previousValue) return + this.clear() + /** + * TODO catch if map is undefined + */ + const comp = this.rService.mapFeatToCmp.get(currentValue.type) + const cf = this.cfr.resolveComponentFactory<ISingleFeature>(comp) + this.cr = this.vCRef.createComponent(cf) + this.cr.instance.feature = this.feature + this.cr.instance.region = this.region + this.viewChangedSub = this.cr.instance.viewChanged.subscribe(() => this.cdr.detectChanges()) + } + + clear(){ + if (this.viewChangedSub) this.viewChangedSub.unsubscribe() + this.vCRef.clear() + } +} diff --git a/src/ui/regionalFeatures/interactivity.directive.ts b/src/ui/regionalFeatures/interactivity.directive.ts deleted file mode 100644 index a5c24ff8f456e762a01da417307f504790b3c148..0000000000000000000000000000000000000000 --- a/src/ui/regionalFeatures/interactivity.directive.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Directive, Inject, EventEmitter, OnDestroy, Optional, Output } from "@angular/core"; -import { take } from "rxjs/operators"; -import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; -import { RegionalFeaturesService } from "./regionalFeature.service"; - -@Directive({ - selector: '[regional-feature-interactiviity]', - exportAs: 'regionalFeatureInteractivity' -}) - -export class RegionalFeatureInteractivity implements OnDestroy{ - - @Output('rf-interact-onclick-3d-landmark') - onClick3DLandmark: EventEmitter<{ landmark: any, next: Function }> = new EventEmitter() - - private onDestroyCb: Function[] = [] - - constructor( - private regionalFeatureService: RegionalFeaturesService, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) private regClickIntp: ClickInterceptor, - ){ - - if (this.regClickIntp) { - const { deregister, register } = this.regClickIntp - const clickIntp = this.clickIntp.bind(this) - register(clickIntp) - this.onDestroyCb.push(() => { - deregister(clickIntp) - }) - } - - } - - ngOnDestroy(){ - while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() - } - - private clickIntp(ev: any, next: Function) { - let hoveredLandmark = null - this.regionalFeatureService.onHoverLandmarks$.pipe( - take(1) - ).subscribe(val => { - hoveredLandmark = val - }) - if (hoveredLandmark) { - this.onClick3DLandmark.emit({ - landmark: hoveredLandmark, - next - }) - } else { - next() - } - } -} diff --git a/src/ui/regionalFeatures/module.ts b/src/ui/regionalFeatures/module.ts index 60cf760553927deafe08f244a51269ab271f7543..f90b3a6ee0593d324d30394b8ba746541f8fa2c0 100644 --- a/src/ui/regionalFeatures/module.ts +++ b/src/ui/regionalFeatures/module.ts @@ -2,30 +2,32 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { UtilModule } from "src/util"; import { AngularMaterialModule } from "../sharedModules/angularMaterial.module"; -import { FeatureExplorer } from "./featureExplorer/featureExplorer.component"; -import { RegionalFeatureInteractivity } from "./interactivity.directive"; +import { FeatureContainer } from "./featureContainer/featureContainer.component"; import { FilterRegionalFeaturesByTypePipe } from "./pipes/filterRegionalFeaturesByType.pipe"; import { FilterRegionFeaturesById } from "./pipes/filterRegionFeaturesById.pipe"; import { FindRegionFEatureById } from "./pipes/findRegionFeatureById.pipe"; import { RegionalFeaturesService } from "./regionalFeature.service"; import { RegionGetAllFeaturesDirective } from "./regionGetAllFeatures.directive"; +import { FeatureIEEGRecordings } from "./singleFeatures/iEEGRecordings/module"; +import { ReceptorDensityModule } from "./singleFeatures/receptorDensity/module"; @NgModule({ imports: [ CommonModule, UtilModule, AngularMaterialModule, + FeatureIEEGRecordings, + ReceptorDensityModule, ], declarations: [ /** * components */ - FeatureExplorer, + FeatureContainer, /** * Directives */ - RegionalFeatureInteractivity, RegionGetAllFeaturesDirective, /** @@ -36,9 +38,9 @@ import { RegionGetAllFeaturesDirective } from "./regionGetAllFeatures.directive" FilterRegionFeaturesById, ], exports: [ - FeatureExplorer, RegionGetAllFeaturesDirective, FilterRegionFeaturesById, + FeatureContainer, ], providers: [ RegionalFeaturesService, diff --git a/src/ui/regionalFeatures/regionFeature.base.ts b/src/ui/regionalFeatures/regionFeature.base.ts deleted file mode 100644 index 1064b07239661673f592fc9f4daa66ca9bed14ad..0000000000000000000000000000000000000000 --- a/src/ui/regionalFeatures/regionFeature.base.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Input, SimpleChanges } from "@angular/core" -import { BehaviorSubject, of } from "rxjs" -import { IFeature, RegionalFeaturesService } from "./regionalFeature.service" - -export class RegionFeatureBase{ - - @Input() - public region: any - - public features: IFeature[] = [] - - /** - * using isLoading flag for conditional rendering of root element (or display loading spinner) - * this is necessary, or the transcluded tab will always be the active tab, - * as this.features as populated via async - */ - public isLoading$ = new BehaviorSubject(false) - private _isLoading: boolean = false - get isLoading(){ - return this._isLoading - } - set isLoading(val){ - if (val !== this._isLoading) - this._isLoading = val - this.isLoading$.next(val) - } - - ngOnChanges(changes: SimpleChanges){ - if (changes.region && changes.region.previousValue !== changes.region.currentValue) { - this.isLoading = true - this.features = [] - - const _ = (changes.region.currentValue - ? this._regionalFeatureService.getAllFeaturesByRegion(changes.region.currentValue) - : of([]) - ).pipe( - - ).subscribe({ - next: features => this.features = features, - complete: () => this.isLoading = false - }) - } - } - - constructor( - private _regionalFeatureService: RegionalFeaturesService - ){ - - } -} diff --git a/src/ui/regionalFeatures/regionGetAllFeatures.directive.ts b/src/ui/regionalFeatures/regionGetAllFeatures.directive.ts index ae26c853c294a91b554e615cf4a58264fcb7b32f..920e1f0a98c7cd791514755d48351b63d4005684 100644 --- a/src/ui/regionalFeatures/regionGetAllFeatures.directive.ts +++ b/src/ui/regionalFeatures/regionGetAllFeatures.directive.ts @@ -1,7 +1,7 @@ import { Directive, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; import { Subscription } from "rxjs"; import { RegionalFeaturesService } from "./regionalFeature.service"; -import { RegionFeatureBase } from "./regionFeature.base"; +import { RegionFeatureBase } from "./singleFeatures/base/regionFeature.base"; @Directive({ selector: '[region-get-all-features-directive]', diff --git a/src/ui/regionalFeatures/regionalFeature.service.ts b/src/ui/regionalFeatures/regionalFeature.service.ts index 8e47bfeb5b62c14e243bb2df909d2af5e4030ba0..e10a5390ba9d3dac1c68fcacc9b7b351686e5a1e 100644 --- a/src/ui/regionalFeatures/regionalFeature.service.ts +++ b/src/ui/regionalFeatures/regionalFeature.service.ts @@ -36,6 +36,8 @@ export class RegionalFeaturesService implements OnDestroy{ ) } + public mapFeatToCmp = new Map<string, any>() + ngOnDestroy(){ while (this.subs.length > 0) this.subs.pop().unsubscribe() } @@ -46,7 +48,6 @@ export class RegionalFeaturesService implements OnDestroy{ public getAllFeaturesByRegion(region: {['fullId']: string}){ if (!region.fullId) throw new Error(`getAllFeaturesByRegion - region does not have fullId defined`) - const regionFullId = getIdFromFullId(region.fullId) const regionFullIds = getStringIdsFromRegion(region) const hemisphereObj = (() => { const hemisphere = getRegionHemisphere(region) diff --git a/src/ui/regionalFeatures/singleFeatures/base/regionFeature.base.ts b/src/ui/regionalFeatures/singleFeatures/base/regionFeature.base.ts new file mode 100644 index 0000000000000000000000000000000000000000..27c68f8e64429401e3fc7c36a095fef498e8d01c --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/base/regionFeature.base.ts @@ -0,0 +1,91 @@ +import { EventEmitter, Input, Output, SimpleChanges } from "@angular/core" +import { BehaviorSubject, forkJoin, Observable, of } from "rxjs" +import { shareReplay, switchMap, tap } from "rxjs/operators" +import { IHasId } from "src/util/interfaces" +import { IFeature, RegionalFeaturesService } from "../../regionalFeature.service" + +export class RegionFeatureBase{ + + private _feature: IFeature + + private feature$ = new BehaviorSubject(null) + @Input() + set feature(val) { + this._feature = val + this.feature$.next(val) + } + get feature(){ + return this._feature + } + + @Input() + public region: any + + @Output('feature-explorer-is-loading') + public dataIsLoadingEventEmitter: EventEmitter<boolean> = new EventEmitter() + + public features: IFeature[] = [] + public data$: Observable<IHasId[]> + + /** + * using isLoading flag for conditional rendering of root element (or display loading spinner) + * this is necessary, or the transcluded tab will always be the active tab, + * as this.features as populated via async + */ + public isLoading$ = new BehaviorSubject(false) + private _isLoading: boolean = false + get isLoading(){ + return this._isLoading + } + set isLoading(val){ + if (val !== this._isLoading) + this._isLoading = val + this.isLoading$.next(val) + } + + public dataIsLoading$ = new BehaviorSubject(false) + private _dataIsLoading = false + set dataIsLoading(val) { + if (val === this._dataIsLoading) return + this._dataIsLoading = val + this.dataIsLoading$.next(val) + this.dataIsLoadingEventEmitter.next(val) + } + get dataIsLoading(){ + return this._dataIsLoading + } + + ngOnChanges(changes: SimpleChanges){ + if (changes.region && changes.region.previousValue !== changes.region.currentValue) { + this.isLoading = true + this.features = [] + + const _ = (changes.region.currentValue + ? this._regionalFeatureService.getAllFeaturesByRegion(changes.region.currentValue) + : of([]) + ).pipe( + + ).subscribe({ + next: features => this.features = features, + complete: () => this.isLoading = false + }) + } + } + + constructor( + private _regionalFeatureService: RegionalFeaturesService + ){ + + /** + * once feature stops loading, watch for input feature + */ + this.data$ = this.feature$.pipe( + tap(() => this.dataIsLoading = true), + switchMap((feature: IFeature) => forkJoin( + feature.data.map(datum => this._regionalFeatureService.getFeatureData(this.region, feature, datum))) + ), + tap(() => this.dataIsLoading = false), + shareReplay(1), + ) + } +} diff --git a/src/ui/regionalFeatures/featureExplorer/featureExplorer.component.ts b/src/ui/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.component.ts similarity index 59% rename from src/ui/regionalFeatures/featureExplorer/featureExplorer.component.ts rename to src/ui/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.component.ts index 007a41c1acbe0b73e5845570c8bc15b87d005db5..9648e856a40f8f71f81a7f415a56f3f6a4bcec3f 100644 --- a/src/ui/regionalFeatures/featureExplorer/featureExplorer.component.ts +++ b/src/ui/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.component.ts @@ -1,58 +1,44 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { BehaviorSubject, forkJoin, merge, Observable, Subject, Subscription } from "rxjs"; -import { debounceTime, map, scan, shareReplay, switchMap, tap } from "rxjs/operators"; +import { Component, Inject, Optional, EventEmitter } from "@angular/core"; +import { merge, Subject, Subscription } from "rxjs"; +import { debounceTime, map, scan, take } from "rxjs/operators"; +import { RegionalFeaturesService } from "src/ui/regionalFeatures/regionalFeature.service"; +import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { IHasId } from "src/util/interfaces"; -import { IFeature, RegionalFeaturesService } from "../regionalFeature.service"; +import { RegionFeatureBase } from "../../base/regionFeature.base"; +import { ISingleFeature } from '../../interfaces' const selectedColor = [ 255, 0, 0 ] @Component({ - selector: 'feature-explorer', - templateUrl: './featureExplorer.template.html', + templateUrl: './iEEGRecordings.template.html', styleUrls: [ - './featureExplorer.style.css' + './iEEGRecordings.style.css' ] }) -export class FeatureExplorer implements OnInit, OnDestroy{ - +export class IEEGRecordingsCmp extends RegionFeatureBase implements ISingleFeature{ private landmarksLoaded: IHasId[] = [] private onDestroyCb: Function[] = [] private sub: Subscription[] = [] - private _feature: IFeature - - private feature$ = new BehaviorSubject(null) - @Input() - set feature(val) { - this._feature = val - this.feature$.next(val) - } - - @Input() - private region: any - - public data$: Observable<IHasId[]> - - @Output('feature-explorer-is-loading') - public dataIsLoadingEventEmitter: EventEmitter<boolean> = new EventEmitter() constructor( private regionFeatureService: RegionalFeaturesService, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) private regClickIntp: ClickInterceptor, ){ - /** - * once feature stops loading, watch for input feature - */ - this.data$ = this.feature$.pipe( - tap(() => this.dataIsLoading = true), - switchMap((feature: IFeature) => forkJoin( - feature.data.map(datum => this.regionFeatureService.getFeatureData(this.region, feature, datum))) - ), - tap(() => this.dataIsLoading = false), - shareReplay(1), - ) + super(regionFeatureService) } + public viewChanged = new EventEmitter<null>() + ngOnInit(){ + if (this.regClickIntp) { + const { deregister, register } = this.regClickIntp + const clickIntp = this.clickIntp.bind(this) + register(clickIntp) + this.onDestroyCb.push(() => { + deregister(clickIntp) + }) + } this.sub.push( this.data$.subscribe(data => { const landmarksTobeLoaded: IHasId[] = [] @@ -105,17 +91,6 @@ export class FeatureExplorer implements OnInit, OnDestroy{ ) } - public dataIsLoading$ = new BehaviorSubject(false) - private _dataIsLoading = false - set dataIsLoading(val) { - if (val === this._dataIsLoading) return - this._dataIsLoading = val - this.dataIsLoading$.next(val) - this.dataIsLoadingEventEmitter.next(val) - } - get dataIsLoading(){ - return this._dataIsLoading - } ngOnDestroy(){ while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() @@ -155,21 +130,18 @@ export class FeatureExplorer implements OnInit, OnDestroy{ }, []) ) - handleLandmarkClick(arg: { landmark: any, next: Function }) { - const { landmark, next } = arg - - /** - * there may be other custom landmarks - * so check if the landmark clicked is one that's managed by this cmp - */ + private clickIntp(ev: any, next: Function) { + let hoveredLandmark = null + this.regionFeatureService.onHoverLandmarks$.pipe( + take(1) + ).subscribe(val => { + hoveredLandmark = val + }) + if (!hoveredLandmark) return next() const isOne = this.landmarksLoaded.some(lm => { - return lm['_']['electrodeId'] === landmark['_']['electrodeId'] + return lm['_']['electrodeId'] === hoveredLandmark['_']['electrodeId'] }) - - if (isOne) { - this.exploreElectrode$.next(landmark['_']['electrodeId']) - } else { - next() - } + if (!isOne) return next() + this.exploreElectrode$.next(hoveredLandmark['_']['electrodeId']) } } diff --git a/src/ui/regionalFeatures/featureExplorer/featureExplorer.style.css b/src/ui/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.style.css similarity index 100% rename from src/ui/regionalFeatures/featureExplorer/featureExplorer.style.css rename to src/ui/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.style.css diff --git a/src/ui/regionalFeatures/featureExplorer/featureExplorer.template.html b/src/ui/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.template.html similarity index 90% rename from src/ui/regionalFeatures/featureExplorer/featureExplorer.template.html rename to src/ui/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.template.html index 7c5f3cf3367c8ca0408a1279f2ec8046dc66819b..f1389020cd588b1c507aef061ef62464bb7cfcc1 100644 --- a/src/ui/regionalFeatures/featureExplorer/featureExplorer.template.html +++ b/src/ui/regionalFeatures/singleFeatures/iEEGRecordings/iEEGRecordings/iEEGRecordings.template.html @@ -1,10 +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"> + class="ml-24px-n mr-24px-n d-block"> <mat-expansion-panel *ngFor="let datum of (data$ | async)" [expanded]="openElectrodeId$ | async | arrayContains : datum['@id']" (opened)="handleDatumExpansion(datum['@id'], true)" diff --git a/src/ui/regionalFeatures/singleFeatures/iEEGRecordings/module.ts b/src/ui/regionalFeatures/singleFeatures/iEEGRecordings/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a099cad95c2c4500b8737f1a52f25e8ac12f626 --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/iEEGRecordings/module.ts @@ -0,0 +1,28 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { UtilModule } from "src/util"; +import { RegionalFeaturesService } from "../../regionalFeature.service"; +import { IEEGRecordingsCmp } from "./iEEGRecordings/iEEGRecordings.component"; + +@NgModule({ + imports: [ + CommonModule, + UtilModule, + AngularMaterialModule, + ], + declarations: [ + IEEGRecordingsCmp + ], + exports: [ + IEEGRecordingsCmp + ] +}) + +export class FeatureIEEGRecordings{ + constructor( + rService: RegionalFeaturesService + ){ + rService.mapFeatToCmp.set('iEEG recording', IEEGRecordingsCmp) + } +} diff --git a/src/ui/regionalFeatures/singleFeatures/interfaces.ts b/src/ui/regionalFeatures/singleFeatures/interfaces.ts new file mode 100644 index 0000000000000000000000000000000000000000..37abcd4479aa87e9dabed72e589d2a3b82a091bd --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/interfaces.ts @@ -0,0 +1,8 @@ +import { EventEmitter } from "@angular/core"; +import { IFeature } from "../regionalFeature.service"; + +export interface ISingleFeature{ + feature: IFeature + region: any + viewChanged: EventEmitter<null> +} diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/filterReceptorBytype.pipe.ts b/src/ui/regionalFeatures/singleFeatures/receptorDensity/filterReceptorBytype.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b0a91dd80079acf6e3cc6709c4ac3cc74ec5f97 --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/filterReceptorBytype.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { IHasId } from "src/util/interfaces"; + +@Pipe({ + name: 'filterReceptorByType', + pure: true +}) + +export class FilterReceptorByType implements PipeTransform{ + public transform(arr: IHasId[], qualifer: string): IHasId[]{ + return (arr || []).filter(({ ['@id']: dId }) => dId.indexOf(qualifer) >= 0) + } +} diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/getAllReceptors.pipe.ts b/src/ui/regionalFeatures/singleFeatures/receptorDensity/getAllReceptors.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba3016186957b4851e469792cc834110152ddb1b --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/getAllReceptors.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { IHasId } from "src/util/interfaces"; + +@Pipe({ + name: 'getAllReceptors', + pure: true +}) + +export class GetAllReceptorsPipe implements PipeTransform{ + public transform(arr: IHasId[]): string[]{ + return (arr || []).reduce((acc, curr) => { + const thisType = /_(pr|ar)_([a-zA-Z0-9_]+)\./.exec(curr['@id']) + if (!thisType) return acc + return new Set(acc).has(thisType) ? acc : acc.concat(thisType[2]) + }, []) + } +} \ No newline at end of file diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/getId.pipe.ts b/src/ui/regionalFeatures/singleFeatures/receptorDensity/getId.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..3af1a02bd4a98455bc9b0166ff4adfc5afa53646 --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/getId.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'getId', + pure: true +}) + +export class GetIdPipe implements PipeTransform{ + public transform(fullId: string): string{ + const re = /\/([a-f0-9-]+)$/.exec(fullId) + return (re && re[1]) || null + } +} diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/module.ts b/src/ui/regionalFeatures/singleFeatures/receptorDensity/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..b090a287314d4371c393a45b6102251a99122880 --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/module.ts @@ -0,0 +1,32 @@ +import { CommonModule } from "@angular/common"; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { RegionalFeaturesService } from "../../regionalFeature.service"; +import { FilterReceptorByType } from "./filterReceptorBytype.pipe"; +import { GetAllReceptorsPipe } from "./getAllReceptors.pipe"; +import { GetIdPipe } from "./getId.pipe"; +import { ReceptorDensityFeatureCmp } from "./receptorDensity/receptorDensity.component"; + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + ], + declarations: [ + ReceptorDensityFeatureCmp, + FilterReceptorByType, + GetIdPipe, + GetAllReceptorsPipe, + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] +}) + +export class ReceptorDensityModule{ + constructor( + rService: RegionalFeaturesService + ){ + rService.mapFeatToCmp.set(`Receptor density measurement`, ReceptorDensityFeatureCmp) + } +} diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.component.ts b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..77aad9acc9a4b057464ba003d48d97c46153965c --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.component.ts @@ -0,0 +1,31 @@ +import { Component, EventEmitter, OnDestroy } from "@angular/core"; +import { Subscription } from "rxjs"; +import { RegionalFeaturesService } from "src/ui/regionalFeatures/regionalFeature.service"; +import { RegionFeatureBase } from "../../base/regionFeature.base"; +import { ISingleFeature } from "../../interfaces"; + +@Component({ + templateUrl: './receptorDensity.template.html', + styleUrls: [ + './receptorDensity.style.css' + ] +}) + +export class ReceptorDensityFeatureCmp extends RegionFeatureBase implements ISingleFeature, OnDestroy{ + public DS_PREVIEW_URL = DATASET_PREVIEW_URL + viewChanged: EventEmitter<null> = new EventEmitter() + + private subs: Subscription[] = [] + + constructor( + regService: RegionalFeaturesService + ){ + super(regService) + } + + public selectedReceptor: string + + ngOnDestroy(){ + while(this.subs.length > 0) this.subs.pop().unsubscribe() + } +} diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css new file mode 100644 index 0000000000000000000000000000000000000000..7b0d7bbed20b91de0a5e85f7516b4c23af593ceb --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css @@ -0,0 +1,5 @@ +kg-dataset-previewer +{ + display: block; + height: 20em; +} diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html new file mode 100644 index 0000000000000000000000000000000000000000..a061700c4a95b5e1ee3dab4ed0d6d5c32ffb0a1e --- /dev/null +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html @@ -0,0 +1,50 @@ +<label for="fingerprint-cmp" class="d-block mat-h4 mt-4 text-muted"> + Fingerprint +</label> +<kg-dataset-previewer + *ngFor="let datum of (data$ | async | filterReceptorByType : '_fp_')" + id="fingerprint-cmp" + (renderEvent)="viewChanged.emit()" + [backendUrl]="DS_PREVIEW_URL" + [kgId]="feature['@id'] | getId" + [filename]="datum['@id']"> +</kg-dataset-previewer> + +<mat-form-field class="mt-2"> + <mat-label> + Select a receptor + </mat-label> + <mat-select [(value)]="selectedReceptor"> + <mat-option + *ngFor="let receptor of (data$ | async | getAllReceptors)" + [value]="receptor"> + {{ receptor }} + </mat-option> + </mat-select> +</mat-form-field> + +<ng-template [ngIf]="selectedReceptor"> + <ng-container *ngTemplateOutlet="prArTmpl; context: { filter: '_pr_', label: 'Profile' }"> + </ng-container> + <ng-container *ngTemplateOutlet="prArTmpl; context: { filter: '_ar_', label: 'Autoradiograph' }"> + </ng-container> +</ng-template> + +<!-- ar/pr template --> +<ng-template #prArTmpl let-label="label" let-filter="filter"> + <ng-container *ngFor="let datum of (data$ | async | filterReceptorByType : selectedReceptor | filterReceptorByType : filter); let first = first"> + <ng-template [ngIf]="first"> + <label [attr.for]="label + '-cmp'" class="d-block mat-h4 mt-4 text-muted"> + {{ label }} + </label> + </ng-template> + + <kg-dataset-previewer + [attr.id]="label + '-cmp'" + (renderEvent)="viewChanged.emit()" + [backendUrl]="DS_PREVIEW_URL" + [kgId]="feature['@id'] | getId" + [filename]="datum['@id']"> + </kg-dataset-previewer> + </ng-container> +</ng-template>