Skip to content
Snippets Groups Projects
Unverified Commit e0f9ff12 authored by Xiao Gui's avatar Xiao Gui
Browse files

feat: add filter to flattened features

bugfix: add explore in kg for ebrains features
bugfix: url encoding of ebrains data features
chore: fix typo
parent 2ddb10fc
No related branches found
No related tags found
No related merge requests found
Showing
with 350 additions and 23 deletions
......@@ -544,10 +544,16 @@ export interface components {
FeatureMetaModel: {
/** Name */
name: string
/** Display Name */
display_name: string
/** Path */
path?: string
/** Query Params */
query_params?: (string)[]
/** Required Query Params */
required_query_params?: (string)[]
/** Optional Query Params */
optional_query_params?: (string)[]
/** Path Params */
path_params?: (string)[]
/** Category */
......@@ -716,6 +722,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[FeatureMetaModel] */
Page_FeatureMetaModel_: {
......@@ -727,6 +735,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[ParcellationEntityVersionModel] */
Page_ParcellationEntityVersionModel_: {
......@@ -738,6 +748,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[SiibraAtlasModel] */
Page_SiibraAtlasModel_: {
......@@ -749,6 +761,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[SiibraCorticalProfileModel] */
Page_SiibraCorticalProfileModel_: {
......@@ -760,6 +774,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[SiibraEbrainsDataFeatureModel] */
Page_SiibraEbrainsDataFeatureModel_: {
......@@ -771,6 +787,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[SiibraParcellationModel] */
Page_SiibraParcellationModel_: {
......@@ -782,6 +800,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[SiibraRegionalConnectivityModel] */
Page_SiibraRegionalConnectivityModel_: {
......@@ -793,6 +813,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[SiibraTabularModel] */
Page_SiibraTabularModel_: {
......@@ -804,6 +826,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[SiibraVoiModel] */
Page_SiibraVoiModel_: {
......@@ -815,6 +839,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** Page[Union[SiibraCorticalProfileModel, SiibraReceptorDensityFp, SiibraTabularModel]] */
Page_Union_SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__: {
......@@ -826,6 +852,8 @@ export interface components {
page: number
/** Size */
size: number
/** Pages */
pages?: number
}
/** ParcellationEntityVersionModel */
ParcellationEntityVersionModel: {
......@@ -1809,7 +1837,6 @@ export interface operations {
parameters: {
query: {
space_id: string
bbox?: string
type?: components["schemas"]["ImageTypes"]
}
path: {
......
import { NgModule } from "@angular/core";
import { AddUnitAndJoin } from "./addUnitAndJoin.pipe";
import { EqualityPipe } from "./equality.pipe";
import { IncludesPipe } from "./includes.pipe";
import { NumbersPipe } from "./numbers.pipe";
import { ParseDoiPipe } from "./parseDoi.pipe";
......@@ -11,14 +10,12 @@ import { ParseDoiPipe } from "./parseDoi.pipe";
ParseDoiPipe,
NumbersPipe,
AddUnitAndJoin,
IncludesPipe,
],
exports: [
EqualityPipe,
ParseDoiPipe,
NumbersPipe,
AddUnitAndJoin,
IncludesPipe,
]
})
......
......@@ -4,6 +4,13 @@ import { map } from 'rxjs/operators';
import { Feature } from "src/atlasComponents/sapi/sxplrTypes"
import { ListDirective } from './list/list.directive';
export type GroupedFeature = {
features: Feature[]
meta: {
displayName: string
}
}
@Directive({
selector: '[sxplrCategoryAcc]',
exportAs: 'categoryAcc'
......@@ -12,7 +19,10 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy {
public isBusy$ = new BehaviorSubject<boolean>(false)
public total$ = new BehaviorSubject<number>(0)
public features$ = new BehaviorSubject<Feature[]>([])
public groupedFeatures$ = new BehaviorSubject<GroupedFeature[]>([])
public features$ = this.groupedFeatures$.pipe(
map(arr => arr.flatMap(val => val.features))
)
@ContentChildren(ListDirective, { read: ListDirective, descendants: true })
listCmps: QueryList<ListDirective>
......@@ -37,10 +47,14 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy {
const listCmp = Array.from(this.listCmps)
this.#subscriptions.push(
this.#subscriptions.push(
combineLatest(
listCmp.map(listC => listC.features$)
).subscribe(features => this.features$.next(features.flatMap(f => f))),
listCmp.map(
listC => listC.features$.pipe(
map(features => ({ features, meta: { displayName: listC.displayName } }))
)
)
).subscribe(val => this.groupedFeatures$.next(val)),
combineLatest(
listCmp.map(listC => listC.features$)
......
......@@ -69,6 +69,62 @@
</mat-panel-description>
</mat-expansion-panel-header>
<!-- <button mat-button>Show all</button> -->
<div class="mat-chip-container"
feature-filter-directive
[initValue]="true"
[items]="categoryAcc.groupedFeatures$ | async "
#filterFeatureCls="featureFilterDirective">
<div class="mat-chip-inner-container">
<button mat-icon-button matTooltip="Reset filter"
(click)="filterFeatureCls.setAll(true)">
<i class="fas fa-filter"></i>
</button>
<ng-template ngFor [ngForOf]="filterFeatureCls.items" let-grpFeat>
<ng-template [ngIf]="grpFeat.features.length > 0">
<ng-template [ngIf]="filterFeatureCls.checked$ | async | grpFeatToName | includes : grpFeat.meta.displayName"
[ngIfThen]="selectedTmpl"
[ngIfElse]="notSelectedTmpl">
</ng-template>
<ng-template #textTmpl>
<span>
{{ grpFeat.meta.displayName }}
</span>
<span class="text-muted1">
({{ grpFeat.features.length }})
</span>
</ng-template>
<ng-template #selectedTmpl>
<button mat-flat-button
(click)="filterFeatureCls.toggle(grpFeat)"
color="primary">
<i class="fas fa-eye"></i>
<ng-template [ngTemplateOutlet]="textTmpl">
</ng-template>
</button>
</ng-template>
<ng-template #notSelectedTmpl>
<button mat-flat-button
(click)="filterFeatureCls.toggle(grpFeat)"
color="default">
<i class="fas fa-eye-slash"></i>
<ng-template [ngTemplateOutlet]="textTmpl">
</ng-template>
</button>
</ng-template>
</ng-template>
</ng-template>
</div>
</div>
<ng-template ngFor [ngForOf]="keyvalue.value" let-feature>
<div sxplr-feature-list-directive
......@@ -78,13 +134,20 @@
[bbox]="bbox"
[queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }"
[featureRoute]="feature.path"
[name]="feature.name"
[displayName]="feature.display_name"
#featureListDirective="featureListDirective">
</div>
</ng-template>
<cdk-virtual-scroll-viewport itemSize="36"
filter-grp-feat
[featureDisplayName]="filterFeatureCls.checked$ | async | mapToProperty : 'meta' | mapToProperty : 'displayName'"
[groupFeature]="categoryAcc.groupedFeatures$ | async"
#filterGrpFeat="filterGrpFeat"
class="virtual-scroll-viewport">
<button *cdkVirtualFor="let feature of categoryAcc.features$ | async"
<button *cdkVirtualFor="let feature of filterGrpFeat.filteredFeatures$"
mat-button
class="virtual-scroll-item sxplr-w-100"
[matTooltip]="feature.name"
......
......@@ -8,4 +8,23 @@ cdk-virtual-scroll-viewport button
text-align: left;
height: 36px;
display: block;
}
\ No newline at end of file
}
.mat-chip-container
{
width: 100%;
overflow-x: scroll;
overflow-y: hidden;
padding: 0.5rem;
}
.mat-chip-inner-container
{
display: inline-flex;
}
.mat-chip-inner-container button
{
white-space: nowrap;
margin: 0.2rem 0.4rem;
}
......@@ -33,9 +33,39 @@
<spinner-cmp></spinner-cmp>
</ng-template>
<a mat-icon-button sxplr-hide-when-local *ngFor="let url of feature.link" [href]="url.href" target="_blank">
<i class="fas fa-external-link-alt"></i>
</a>
<!-- template for external link -->
<ng-template #externalLinkTmpl let-url>
<a mat-icon-button sxplr-hide-when-local [href]="url" target="_blank">
<i class="fas fa-external-link-alt"></i>
</a>
</ng-template>
<!-- if link is prepopulated -->
<ng-template
ngFor
[ngForOf]="feature.link"
let-url>
<ng-template
[ngTemplateOutlet]="externalLinkTmpl"
[ngTemplateOutletContext]="{
$implicit: url.href
}">
</ng-template>
</ng-template>
<!-- if link is lazy fetched -->
<ng-template
ngFor
[ngForOf]="additionalLinks$ | async"
let-url>
<ng-template
[ngTemplateOutlet]="externalLinkTmpl"
[ngTemplateOutletContext]="{
$implicit: url
}">
</ng-template>
</ng-template>
</mat-card-subtitle>
</mat-card>
......
import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { SAPI } from 'src/atlasComponents/sapi/sapi.service';
import { Feature, TabularFeature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes';
import { DARKTHEME } from 'src/util/injectionTokens';
......@@ -28,6 +28,15 @@ export class FeatureViewComponent implements OnChanges {
@Input()
feature: Feature
#detailLinks = new Subject<string[]>()
additionalLinks$ = this.#detailLinks.pipe(
distinctUntilChanged((o, n) => o.length == n.length),
map(links => {
const set = new Set((this.feature.link || []).map(v => v.href))
return links.filter(l => !set.has(l))
})
)
busy$ = new BehaviorSubject<boolean>(false)
tabular$ = new BehaviorSubject<TabularFeature<number|string|number[]>>(null)
......@@ -90,6 +99,9 @@ export class FeatureViewComponent implements OnChanges {
if (isVoiData(val)) {
this.voi$.next(val)
}
this.#detailLinks.next((val.link || []).map(l => l.href))
},
() => this.busy$.next(false)
)
......
import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core";
import { BehaviorSubject, combineLatest, concat, merge, of, Subject } from "rxjs";
import { map, scan, shareReplay, switchMap } from "rxjs/operators";
@Directive({
selector: '[feature-filter-directive]',
exportAs: 'featureFilterDirective'
})
export class FeatureFilterDirective<T> implements OnChanges{
@Input()
items: T[] = []
@Input()
initValue = false
#items$ = new BehaviorSubject<T[]>(this.items)
#initValue$ = new BehaviorSubject<boolean>(this.initValue)
#toggle$ = new Subject<{ target: T }>()
#setValue$ = new Subject<{ target: T, flag: boolean}>()
#setAll$ = new Subject<{ flag: boolean }>()
ngOnChanges(changes: SimpleChanges): void {
if (changes.items) {
this.#items$.next(changes.items.currentValue)
}
if (changes.initValue) {
this.#initValue$.next(changes.initValue.currentValue)
}
}
#checkbox$ = combineLatest([
this.#items$,
this.#initValue$
]).pipe(
switchMap(([items, initFlag]) => {
const initialCondition = items.map(item => ({ item, flag: initFlag }))
return merge<{ target: T, flag?: boolean, op: string }>(
this.#toggle$.pipe(
map(v => ({ ...v, op: 'toggle' }))
),
this.#setValue$.pipe(
map(v => ({ ...v, op: 'set' }))
),
this.#setAll$.pipe(
map(v => ({ ...v, op: 'setAll' }))
),
of({ op: 'noop' })
).pipe(
scan((acc, { target, op, flag }) => {
if (op === 'noop') return acc
if (op === 'setAll') {
return acc.map(({ item }) => ({ item, flag }))
}
const found = acc.find(({ item }) => item === target)
const other = acc.filter(({ item }) => item !== target)
const itemToAppend = found
? [{
item: found.item,
flag: op === 'set'
? flag
: !found.flag
}]
: []
return [ ...other, ...itemToAppend ]
}, initialCondition)
)
}),
shareReplay(1),
)
checked$ = concat(
of([] as T[]),
this.#checkbox$.pipe(
map(arr => arr.filter(v => v.flag).map(v => v.item)),
)
)
unchecked$ = concat(
of([] as T[]),
this.#checkbox$.pipe(
map(arr => arr.filter(v => !v.flag).map(v => v.item)),
)
)
toggle(target: T){
this.#toggle$.next({ target })
}
setValue(target: T, flag: boolean) {
this.#setValue$.next({ target, flag })
}
setAll(flag: boolean){
this.#setAll$.next({ flag })
}
}
import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core";
import { GroupedFeature } from "./category-acc.directive";
import { combineLatest, Subject } from "rxjs";
import { map } from "rxjs/operators";
@Directive({
selector: '[filter-grp-feat]',
exportAs: 'filterGrpFeat'
})
export class FilterGroupList implements OnChanges{
@Input()
featureDisplayName: string[] = []
#featureDisplayName = new Subject<string[]>()
@Input()
groupFeature: GroupedFeature[] = []
#groupFeature = new Subject<GroupedFeature[]>()
filteredFeatures$ = combineLatest([
this.#featureDisplayName,
this.#groupFeature
]).pipe(
map(([ displaynames, grpfeats ]) => grpfeats.filter(feat => displaynames.includes(feat.meta.displayName)).flatMap(f => f.features))
)
ngOnChanges(): void {
this.#featureDisplayName.next(this.featureDisplayName)
this.#groupFeature.next(this.groupFeature)
}
}
import { Pipe, PipeTransform } from "@angular/core";
import { GroupedFeature } from "./category-acc.directive";
@Pipe({
name: 'grpFeatToName',
pure: true
})
export class GroupFeaturesToName implements PipeTransform{
public transform(groupFeats: GroupedFeature[]): string[] {
return groupFeats.map(f => f.meta.displayName)
}
}
......@@ -12,6 +12,12 @@ import { AllFeatures, FeatureBase } from "../base";
})
export class ListDirective extends FeatureBase{
@Input()
name: string
@Input()
displayName: string
@Input()
featureRoute: string
private guardedRoute$ = new BehaviorSubject<FeatureType>(null)
......
......@@ -24,6 +24,10 @@ import { NgLayerCtlModule } from "src/viewerModule/nehuba/ngLayerCtlModule/modul
import { VoiBboxDirective } from "./voi-bbox.directive";
import { FilterCategoriesPipe } from "./filterCategories.pipe";
import { ListDirective } from "./list/list.directive";
import { MatChipsModule } from "@angular/material/chips";
import { FeatureFilterDirective } from "./feature.filter.directive";
import { FilterGroupList } from "./filterGrpFeat.directive"
import { GroupFeaturesToName } from "./grpFeatToName.pipe";
@NgModule({
imports: [
......@@ -43,6 +47,7 @@ import { ListDirective } from "./list/list.directive";
MarkdownModule,
MatTableModule,
NgLayerCtlModule,
MatChipsModule,
],
declarations: [
EntryComponent,
......@@ -50,12 +55,15 @@ import { ListDirective } from "./list/list.directive";
FeatureViewComponent,
FilterCategoriesPipe,
ListDirective,
FeatureFilterDirective,
FilterGroupList,
CategoryAccDirective,
VoiBboxDirective,
FeatureNamePipe,
TransformPdToDsPipe,
GroupFeaturesToName,
],
exports: [
EntryComponent,
......
......@@ -8,7 +8,7 @@ import { atlasSelection, defaultState, MainState, plugins, userInteraction } fro
import { getParcNgId } from "src/viewerModule/nehuba/config.service";
import { decodeToNumber, encodeNumber, encodeURIFull, separator } from "./cipher";
import { TUrlAtlas, TUrlPathObj, TUrlStandaloneVolume } from "./type";
import { decodePath, encodeId, decodeId, endcodePath } from "./util";
import { decodePath, encodeId, decodeId, encodePath } from "./util";
@Injectable()
export class RouteStateTransformSvc {
......@@ -210,6 +210,8 @@ export class RouteStateTransformSvc {
try {
if (returnObj.f && returnObj.f.length === 1) {
const decodedFeatId = decodeId(returnObj.f[0])
.replace(/~ptc~/g, '://')
.replace(/~/g, ':')
const feature = await this.sapi.getV3FeatureDetailWithId(decodedFeatId).toPromise()
returnState["[state.userInteraction]"].selectedFeature = feature
}
......@@ -290,7 +292,15 @@ export class RouteStateTransformSvc {
// nav
['@']: cNavString,
// showing dataset
f: selectedFeature && encodeId(selectedFeature.id)
f: (() => {
return selectedFeature && encodeId(
encodeURIFull(
selectedFeature.id
.replace(/:\/\//, '~ptc~')
.replace(/:/g, '~')
)
)
})()
}
/**
......@@ -307,7 +317,7 @@ export class RouteStateTransformSvc {
const routesArr: string[] = []
for (const key in routes) {
if (!!routes[key]) {
const segStr = endcodePath(key, routes[key])
const segStr = encodePath(key, routes[key])
routesArr.push(segStr)
}
}
......
......@@ -4,7 +4,7 @@ import { Component } from "@angular/core"
export const encodeId = (id: string) => id && id.replace(/\//g, ':')
export const decodeId = (codedId: string) => codedId && codedId.replace(/:/g, '/')
export const endcodePath = (key: string, val: string|string[]) =>
export const encodePath = (key: string, val: string|string[]) =>
key[0] === '?'
? `?${key}=${val}`
: `${key}:${Array.isArray(val)
......@@ -56,7 +56,7 @@ export const encodeCustomState = (key: string, value: string|string[]) => {
throw new Error(`custom state must start with x-`)
}
if (!value) return null
return endcodePath(key, value).replace(/\//g, '%2F')
return encodePath(key, value).replace(/\//g, '%2F')
}
@Component({
......
......@@ -6,7 +6,7 @@ import { Pipe, PipeTransform } from "@angular/core";
})
export class FilterArrayPipe implements PipeTransform{
public transform<T>(arr: T[], filterFn: (item: T, index: number, array: T[]) => boolean){
public transform<T>(arr: T[], filterFn: (item: T, index?: number, array?: T[]) => boolean){
return (arr || []).filter(filterFn)
}
}
import { NgModule } from "@angular/core";
import { KeyListner } from "./directives/keyDownListener.directive";
import { StopPropagationDirective } from "./directives/stopPropagation.directive";
import { SafeResourcePipe } from "./pipes/safeResource.pipe";
import { CaptureClickListenerDirective } from "./directives/captureClickListener.directive";
......@@ -19,6 +18,7 @@ import { DoiParserPipe } from "./pipes/doiPipe.pipe";
import { GetFilenamePipe } from "./pipes/getFilename.pipe";
import { CombineFnPipe } from "./pipes/combineFn.pipe";
import { MergeObjPipe } from "./mergeObj.pipe";
import { IncludesPipe } from "./includes.pipe";
@NgModule({
imports:[
......@@ -43,6 +43,7 @@ import { MergeObjPipe } from "./mergeObj.pipe";
GetFilenamePipe,
CombineFnPipe,
MergeObjPipe,
IncludesPipe,
],
exports: [
StopPropagationDirective,
......@@ -63,6 +64,7 @@ import { MergeObjPipe } from "./mergeObj.pipe";
GetFilenamePipe,
CombineFnPipe,
MergeObjPipe,
IncludesPipe,
]
})
......
......@@ -1010,7 +1010,7 @@
</ng-template>
</button>
<div
*ngIf="voiSwitch.switchState$| async"
*ngIf="voiSwitch.switchState$ | async"
voiBbox
[features]="voiFeatureEntryCmp.features$ | async">
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment