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

feat: alternative ATP selector

parent 4b728407
No related branches found
No related tags found
No related merge requests found
Showing
with 942 additions and 11 deletions
...@@ -8,6 +8,11 @@ ...@@ -8,6 +8,11 @@
{ {
overflow: scroll!important; overflow: scroll!important;
} }
#root
{
width: 100%;
height: 100%;
}
</style> </style>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" defer></script> <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" defer></script>
......
...@@ -76,6 +76,11 @@ export async function getAtlas(id: string): Promise<SapiAtlasModel>{ ...@@ -76,6 +76,11 @@ export async function getAtlas(id: string): Promise<SapiAtlasModel>{
return await (await fetch(`${endPt}/atlases/${id}`)).json() return await (await fetch(`${endPt}/atlases/${id}`)).json()
} }
export async function getParcellations(atlasId: string): Promise<SapiParcellationModel[]> {
const endPt = await SAPI.BsEndpoint$.toPromise()
return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations`)).json()
}
export async function getParc(atlasId: string, id: string): Promise<SapiParcellationModel>{ export async function getParc(atlasId: string, id: string): Promise<SapiParcellationModel>{
const endPt = await SAPI.BsEndpoint$.toPromise() const endPt = await SAPI.BsEndpoint$.toPromise()
return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}`)).json() return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}`)).json()
...@@ -85,6 +90,11 @@ export async function getParcRegions(atlasId: string, id: string, spaceId: strin ...@@ -85,6 +90,11 @@ export async function getParcRegions(atlasId: string, id: string, spaceId: strin
return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json() return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json()
} }
export async function getSpaces(atlasId: string): Promise<SapiSpaceModel[]> {
const endPt = await SAPI.BsEndpoint$.toPromise()
return await (await fetch(`${endPt}/atlases/${atlasId}/spaces`)).json()
}
export async function getSpace(atlasId: string, id: string): Promise<SapiSpaceModel> { export async function getSpace(atlasId: string, id: string): Promise<SapiSpaceModel> {
const endPt = await SAPI.BsEndpoint$.toPromise() const endPt = await SAPI.BsEndpoint$.toPromise()
return await (await fetch(`${endPt}/atlases/${atlasId}/spaces/${id}`)).json() return await (await fetch(`${endPt}/atlases/${atlasId}/spaces/${id}`)).json()
......
export { export { SapiViewsCoreParcellationModule } from "./module"
SapiViewsCoreParcellationModule export { FilterGroupedParcellationPipe } from "./filterGroupedParcellations.pipe"
} from "./module" export { FilterUnsupportedParcPipe } from "./filterUnsupportedParc.pipe"
export { GroupedParcellation } from "./groupedParcellation"
export { export { ParcellationDoiPipe } from "./parcellationDoi.pipe"
FilterGroupedParcellationPipe export { ParcellationGroupSelectedPipe } from "./parcellationGroupSelected.pipe"
} from "./filterGroupedParcellations.pipe"
export {
FilterUnsupportedParcPipe
} from "./filterUnsupportedParc.pipe"
\ No newline at end of file
import { Pipe, PipeTransform } from "@angular/core";
import { SapiParcellationModel } from "src/atlasComponents/sapi/type";
import { GroupedParcellation } from "./groupedParcellation";
function isGroupedParc(parc: GroupedParcellation|unknown): parc is GroupedParcellation {
if (!parc['parcellations']) return false
return (parc['parcellations'] as SapiParcellationModel[]).every(p => p["@type"] === "minds/core/parcellationatlas/v1.0.0")
}
@Pipe({
name: 'parcellationGroupSelected',
pure: true
})
export class ParcellationGroupSelectedPipe implements PipeTransform {
public transform(parc: GroupedParcellation|unknown, selectedParcellation: SapiParcellationModel): boolean {
if (!isGroupedParc(parc)) return false
return parc.parcellations.some(p => p["@id"] === selectedParcellation["@id"])
}
}
export {
ATPSelectorModule
} from "./module"
\ No newline at end of file
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { MatButtonModule } from "@angular/material/button";
import { MatRippleModule } from "@angular/material/core";
import { MatIconModule } from "@angular/material/icon";
import { MarkdownModule } from "src/components/markdown";
import { SmartChipModule } from "src/components/smartChip";
import { DialogModule } from "src/ui/dialogInfo";
import { UtilModule } from "src/util";
import { FilterGroupedParcellationPipe, ParcellationDoiPipe, ParcellationGroupSelectedPipe } from "src/atlasComponents/sapiViews/core/parcellation";
import { PureATPSelector } from "./pureDumb/pureATPSelector.components";
import { WrapperATPSelector } from "./wrapper/wrapper.component";
import { SAPIModule } from "src/atlasComponents/sapi/module";
@NgModule({
imports: [
CommonModule,
SmartChipModule,
UtilModule,
MarkdownModule,
MatRippleModule,
MatIconModule,
MatButtonModule,
DialogModule,
SAPIModule,
],
declarations: [
PureATPSelector,
WrapperATPSelector,
FilterGroupedParcellationPipe,
ParcellationDoiPipe,
ParcellationGroupSelectedPipe,
],
exports: [
PureATPSelector,
WrapperATPSelector,
]
})
export class ATPSelectorModule{}
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core";
import { SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type";
import { FilterGroupedParcellationPipe, GroupedParcellation } from "src/atlasComponents/sapiViews/core/parcellation";
export const defaultColorPalette = [
"#480202",
"#6b1205",
"#921d1d",
]
export type ATP = {
atlas: SapiAtlasModel
template: SapiSpaceModel
parcellation: SapiParcellationModel
}
const pipe = new FilterGroupedParcellationPipe()
@Component({
selector: 'sxplr-pure-atp-selector',
templateUrl: `./pureATPSelector.template.html`,
styleUrls: [
`./pureATPSelector.style.scss`
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PureATPSelector implements OnChanges{
@Input('sxplr-pure-atp-selector-color-palette')
colorPalette: string[] = defaultColorPalette
@Input(`sxplr-pure-atp-selector-selected-atp`)
public selectedATP: ATP
public selectedIds: string[] = []
@Input(`sxplr-pure-atp-selector-atlases`)
public allAtlases: SapiAtlasModel[] = []
@Input(`sxplr-pure-atp-selector-templates`)
public availableTemplates: SapiSpaceModel[] = []
@Input(`sxplr-pure-atp-selector-parcellations`)
public parcellations: SapiParcellationModel[] = []
public parcAndGroup: (GroupedParcellation|SapiParcellationModel)[] = []
@Input('sxplr-pure-atp-selector-is-busy')
public isBusy: boolean = false
@Output('sxplr-pure-atp-selector-on-select')
selectLeafEmitter = new EventEmitter<ATP>()
getChildren(parc: GroupedParcellation|SapiParcellationModel){
return (parc as GroupedParcellation).parcellations || []
}
selectLeaf(atp: ATP) {
if (this.isBusy) return
this.selectLeafEmitter.emit(atp)
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.selectedATP) {
if (!changes.selectedATP.currentValue) {
this.selectedIds = []
} else {
const { atlas, parcellation, template } = changes.selectedATP.currentValue as ATP
this.selectedIds = [atlas?.["@id"], parcellation?.["@id"], template?.["@id"]].filter(v => !!v)
}
}
if (changes.parcellations) {
if (!changes.parcellations.currentValue) {
this.parcAndGroup = []
} else {
this.parcAndGroup = [
...pipe.transform(changes.parcellations.currentValue, true),
...pipe.transform(changes.parcellations.currentValue, false),
]
}
}
}
}
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { MatButtonModule } from "@angular/material/button";
import { action } from "@storybook/addon-actions";
import { componentWrapperDecorator, Meta, moduleMetadata, Story } from "@storybook/angular";
import { atlasId, provideDarkTheme } from "src/atlasComponents/sapi/stories.base";
import { SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type";
import { UtilModule } from "src/util";
import { ATPSelectorModule } from "../module";
import { defaultColorPalette } from "./pureATPSelector.components"
import { loadAtlasEtcData, wrapperDecoratorFn } from "../story.base"
@Component({
selector: 'atlas-selector-wrapper-story',
template:
`<sxplr-pure-atp-selector
[sxplr-pure-atp-selector-color-palette]="[atlasColor, spaceColor, parcellationColor]"
[sxplr-pure-atp-selector-selected-atp]="selectedATP"
[sxplr-pure-atp-selector-atlases]="allAtlases"
[sxplr-pure-atp-selector-templates]="availableTemplates"
[sxplr-pure-atp-selector-parcellations]="parcs"
[sxplr-pure-atp-selector-is-busy]="isBusy"
(sxplr-pure-atp-selector-on-select)="selectLeaf($event)"
>
<button mat-icon-button
(click)="toggleParcellationVisibility()"
parcellation-chip-suffix
iav-stop="mousedown click">
<i class="fas fa-eye"></i>
</button>
</sxplr-pure-atp-selector>`,
styles: [
`[parcellation-chip-suffix]
{
margin-right: -1rem;
margin-left: 0.2rem;
}`
]
})
class AtlasLayerSelectorWrapper {
atlasColor: string = defaultColorPalette[0]
spaceColor: string = defaultColorPalette[1]
parcellationColor: string = defaultColorPalette[2]
allAtlases: SapiAtlasModel[]
availableTemplates: SapiSpaceModel[]
selectedATP: {
atlas: SapiAtlasModel
parcellation: SapiParcellationModel
template: SapiSpaceModel
}
parcs: SapiParcellationModel[]
isBusy: boolean = false
selectLeaf(arg: {
atlas: SapiAtlasModel
parcellation: SapiParcellationModel
template: SapiSpaceModel
}) {}
toggleParcellationVisibility() {
}
}
export default {
component: AtlasLayerSelectorWrapper,
decorators: [
moduleMetadata({
imports: [
CommonModule,
ATPSelectorModule,
MatButtonModule,
UtilModule,
],
declarations: [
AtlasLayerSelectorWrapper
],
providers: [
...provideDarkTheme,
]
}),
componentWrapperDecorator(wrapperDecoratorFn)
]
} as Meta
const Template: Story<AtlasLayerSelectorWrapper> = (args: AtlasLayerSelectorWrapper, { loaded }) => {
const {
combinedAtlas,
atlases
} = loaded
const { atlas, parcs, spaces } = combinedAtlas
const selectedATP = {
atlas, parcellation: parcs[0], template: spaces[0]
}
const {
atlasColor,
spaceColor,
parcellationColor,
} = args
return ({
props: {
...args,
allAtlases: atlases,
parcs,
availableTemplates: spaces,
selectedATP: {
atlas,
parcellation: parcs[0],
template: spaces[0]
},
selectLeaf: action(`selectLeaf`),
toggleParcellationVisibility: action('toggleParcellationVisibility'),
atlasColor,
spaceColor,
parcellationColor,
}
})
}
export const Default = Template.bind({})
Default.args = {
atlasColor: defaultColorPalette[0],
spaceColor: defaultColorPalette[1],
parcellationColor: defaultColorPalette[2],
}
Default.loaders = [
async () => {
const {
atlases,
combinedAtlases
} = await loadAtlasEtcData()
return {
atlases,
combinedAtlas: combinedAtlases.filter(v => v.atlas["@id"] === atlasId.human)[0]
}
}
]
:host
{
display: inline-flex;
flex-direction: row-reverse;
}
.full-sized-button
{
width: 100%;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.icons
{
margin-right: -1rem;
margin-left: 0.2rem;
}
sxplr-smart-chip:not(:last-child)
{
margin-left: -3.5rem;
.chip-text
{
padding-left: 1rem;
}
}
<ng-template [ngIf]="selectedATP" let-ATP>
<!-- parcellation smart chip -->
<sxplr-smart-chip *ngIf="ATP.parcellation"
[items]="parcAndGroup"
[color]="colorPalette[2]"
[getChildren]="getChildren"
(itemClicked)="selectLeaf({ parcellation: $event })"
[elevation]="2"
[disabled]="isBusy">
<ng-template sxplrSmartChipContent>
<span class="chip-text">
<span>
</span>
{{ ATP.parcellation.name }}
</span>
<ng-content select="[parcellation-chip-suffix]">
</ng-content>
<button iav-stop="mousedown click"
class="icons"
mat-icon-button
sxplr-dialog
[sxplr-dialog-size]="null"
[sxplr-dialog-data]="{
title: ATP.parcellation.name || ATP.parcellation.fullName,
descMd: (ATP.parcellation.brainAtlasVersions || [])[0]?.versionInnovation,
actions: ATP.parcellation | parcellationDoiPipe
}">
<i class="fas fa-info"></i>
</button>
</ng-template>
<ng-template sxplrSmartChipMenu let-parc>
<ng-container *ngTemplateOutlet="optionTmpl; context: {
$implicit: parc,
overridePrefixIconTmpl: (parc | parcellationGroupSelected : ATP.parcellation)
? halfSelectedTmpl
: null
}">
</ng-container>
</ng-template>
</sxplr-smart-chip>
<!-- space smart chip -->
<sxplr-smart-chip *ngIf="ATP.template"
[items]="availableTemplates"
[color]="colorPalette[1]"
(itemClicked)="selectLeaf({ template: $event })"
[elevation]="4"
[disabled]="isBusy">
<ng-template sxplrSmartChipContent>
<span class="chip-text">
{{ ATP.template.fullName }}
</span>
</ng-template>
<ng-template sxplrSmartChipMenu let-space>
<ng-container *ngTemplateOutlet="optionTmpl; context: { $implicit: space }"></ng-container>
</ng-template>
</sxplr-smart-chip>
<!-- atlas smart chip -->
<sxplr-smart-chip *ngIf="ATP.atlas"
[items]="allAtlases"
[color]="colorPalette[0]"
(itemClicked)="selectLeaf({ atlas: $event})"
[elevation]="6"
[disabled]="isBusy">
<ng-template sxplrSmartChipContent>
<span class="chip-text">
{{ ATP.atlas.name }}
</span>
</ng-template>
<ng-template sxplrSmartChipMenu let-atlas>
<ng-container *ngTemplateOutlet="optionTmpl; context: { $implicit: atlas }"></ng-container>
</ng-template>
</sxplr-smart-chip>
</ng-template>
<!-- half selected -->
<!-- only active in nested menus (e.g. parcellation groups) -->
<ng-template #halfSelectedTmpl>
<mat-icon fontSet="far" fontIcon="fa-circle"></mat-icon>
</ng-template>
<!-- option template -->
<ng-template
#optionTmpl
let-item
let-overridePrefixIconTmpl="overridePrefixIconTmpl"
let-overrideSuffixIcon="overrideSuffixIcon">
<!-- prefix -->
<ng-template [ngIf]="overridePrefixIconTmpl" [ngIfElse]="defaultPrefix">
<ng-template [ngTemplateOutlet]="overridePrefixIconTmpl"></ng-template>
</ng-template>
<ng-template #defaultPrefix>
<ng-template [ngIf]="selectedIds" let-selectedIds>
<mat-icon
fontSet="fas"
[fontIcon]="selectedIds.includes(item['@id']) ? 'fa-circle' : 'fa-none'"
>
</mat-icon>
</ng-template>
</ng-template>
<!-- button body -->
<span *ngIf="item; else noItemTmpl" class="full-sized-button">
{{ item.name || item.fullName }}
</span>
<!-- suffix -->
<ng-template [ngIf]="overrideSuffixIcon">
<i [class]="overrideSuffixIcon"></i>
</ng-template>
</ng-template>
<ng-template #isBusyTmpl>
</ng-template>
\ No newline at end of file
import { getAtlases, getParcellations, getSpaces } from "src/atlasComponents/sapi/stories.base"
import { SapiAtlasModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"
export const wrapperDecoratorFn = (story: string) => `
<style>
.wrapper {
width: 100%;
height: 100%;
background-color: white;
display: flex;
flex-direction: column-reverse;
border: 1px solid rgba(128, 128, 128, 0.5);
}
:host-context([darktheme="true"]) .wrapper
{
background-color: #2f2f2f;
}
</style>
<div class="wrapper">${story}</div>
`
export type ReturnAtlas = {
atlas: SapiAtlasModel
spaces: SapiSpaceModel[]
parcs: SapiParcellationModel[]
}
export const loadAtlasEtcData = async () => {
const combinedAtlases: ReturnAtlas[] = []
const atlases = await getAtlases()
for (const atlas of atlases) {
const parcs = await getParcellations(atlas["@id"])
const spaces = await getSpaces(atlas["@id"])
combinedAtlases.push({
atlas,
parcs,
spaces
})
}
return { combinedAtlases, atlases }
}
import { Component, OnDestroy } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { Store } from "@ngrx/store";
import { Observable, of, Subject, Subscription } from "rxjs";
import { filter, map, switchMap, tap, withLatestFrom } from "rxjs/operators";
import { SAPI } from "src/atlasComponents/sapi/sapi.service";
import { ParcellationSupportedInSpacePipe } from "src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe";
import { atlasSelection } from "src/state";
import { fromRootStore } from "src/state/atlasSelection";
import { DialogFallbackCmp } from "src/ui/dialogInfo";
import { ParcellationVisibilityService } from "../../../parcellation/parcellationVis.service";
import { defaultColorPalette, ATP } from "../pureDumb/pureATPSelector.components"
function isATPGuard(obj: any): obj is ATP {
if (!obj) return false
return obj.atlas || obj.template || obj.parcellation
}
@Component({
selector: 'sxplr-wrapper-atp-selector',
templateUrl: './wrapper.template.html',
styleUrls: [
`./wrapper.style.css`
]
})
export class WrapperATPSelector implements OnDestroy{
defaultColorPalette = defaultColorPalette
#subscription: Subscription[] = []
#parcSupportedInSpacePipe = new ParcellationSupportedInSpacePipe(this.sapi)
#askUser(title: string, descMd: string): Observable<boolean> {
const agree = "OK"
return this.dialog.open(DialogFallbackCmp, {
data: {
title,
descMd,
actions: [agree]
}
}).afterClosed().pipe(
map(val => val === agree)
)
}
selectedATP$ = this.store$.pipe(
fromRootStore.distinctATP(),
)
allAtlases$ = this.sapi.atlases$
availableTemplates$ = this.store$.pipe(
fromRootStore.allAvailSpaces(this.sapi),
)
parcs$ = this.store$.pipe(
fromRootStore.allAvailParcs(this.sapi),
)
isBusy$ = new Subject<boolean>()
parcellationVisibility$ = this.svc.visibility$
constructor(
private dialog: MatDialog,
private store$: Store,
private sapi: SAPI,
private svc: ParcellationVisibilityService,
){
this.#subscription.push(
this.selectLeaf$.pipe(
tap(() => this.isBusy$.next(true)),
withLatestFrom(this.selectedATP$),
switchMap(([{ atlas, template, parcellation }, selectedATP]) => {
if (atlas) {
/**
* do not need to ask permission to switch atlas
*/
return of({ atlas })
}
if (template) {
return this.#parcSupportedInSpacePipe.transform(selectedATP.parcellation, template).pipe(
switchMap(({ supported }) => supported
? of({ template })
: this.#askUser(`Incompatible parcellation`, `Attempting to load template **${template.fullName}**, which does not support parcellation **${selectedATP.parcellation.name}**. Proceed anyway and load the default parcellation?`).pipe(
switchMap(flag => of(flag ? { template } : null))
))
)
}
if (parcellation) {
return this.#parcSupportedInSpacePipe.transform(parcellation, selectedATP.template).pipe(
switchMap(({ supported }) => supported
? of({ parcellation })
: this.#askUser(`Incompatible template`, `Attempting to load parcellation **${parcellation.name}**, which is not supported in template **${selectedATP.template.fullName}**. Proceed anyway and load the default template?`).pipe(
switchMap(flag => of(flag ? { parcellation } : null))
))
)
}
return of(null)
}),
filter(val => {
this.isBusy$.next(false)
return !!val
})
).subscribe((obj) => {
if (!isATPGuard(obj)) return
const { atlas, parcellation, template } = obj
if (atlas) {
this.store$.dispatch(
atlasSelection.actions.selectAtlas({ atlas })
)
}
if (parcellation) {
this.store$.dispatch(
atlasSelection.actions.selectParcellation({ parcellation })
)
}
if (template) {
this.store$.dispatch(
atlasSelection.actions.selectTemplate({ template })
)
}
})
)
}
private selectLeaf$ = new Subject<ATP>()
selectLeaf(atp: ATP) {
this.selectLeaf$.next(atp)
}
toggleParcellationVisibility() {
this.svc.toggleVisibility()
}
ngOnDestroy(): void {
while (this.#subscription.length > 0) this.#subscription.pop().unsubscribe()
}
}
import { Component } from "@angular/core";
import { MatDialogModule } from "@angular/material/dialog";
import { Store } from "@ngrx/store";
import { action } from "@storybook/addon-actions";
import { componentWrapperDecorator, Meta, moduleMetadata, Story } from "@storybook/angular";
import { atlasId, provideDarkTheme } from "src/atlasComponents/sapi/stories.base";
import { atlasSelection, RootStoreModule } from "src/state";
import { ParcellationVisibilityService } from "../../../parcellation/parcellationVis.service";
import { ATPSelectorModule } from "../module";
import { ATP } from "../pureDumb/pureATPSelector.components";
import { loadAtlasEtcData, wrapperDecoratorFn } from "../story.base";
@Component({
selector: 'wrapper-wrapper',
template: '<sxplr-wrapper-atp-selector></sxplr-wrapper-atp-selector>'
})
class Wrapper{
set ATP(atp: ATP) {
const { atlas, parcellation, template } = atp
this.store$.dispatch(
atlasSelection.actions.setAtlasSelectionState({
selectedAtlas: atlas,
selectedTemplate: template,
selectedParcellation: parcellation
})
)
this.store$.dispatch = action('dispatch')
}
constructor(private store$: Store) {}
}
export default {
component: Wrapper,
decorators: [
moduleMetadata({
imports: [
ATPSelectorModule,
MatDialogModule,
RootStoreModule,
],
providers: [
...provideDarkTheme,
ParcellationVisibilityService,
]
}),
componentWrapperDecorator(wrapperDecoratorFn)
]
} as Meta
const Template: Story<Wrapper> = (args: Wrapper, { loaded }) => {
const { combinedAtlas } = loaded
return {
props: {
...args,
ATP: {
atlas: combinedAtlas.atlas,
parcellation: combinedAtlas.parcs[0],
template: combinedAtlas.spaces[0],
}
}
}
}
export const Default = Template.bind({})
Default.loaders = [
async () => {
const {
atlases,
combinedAtlases
} = await loadAtlasEtcData()
return {
atlases,
combinedAtlas: combinedAtlases.filter(v => v.atlas["@id"] === atlasId.human)[0]
}
}
]
[parcellation-chip-suffix]
{
margin-right: -1rem;
margin-left: 0.2rem;
}
<sxplr-pure-atp-selector
[sxplr-pure-atp-selector-color-palette]="defaultColorPalette"
[sxplr-pure-atp-selector-selected-atp]="selectedATP$ | async"
[sxplr-pure-atp-selector-atlases]="allAtlases$ | async"
[sxplr-pure-atp-selector-templates]="availableTemplates$ | async"
[sxplr-pure-atp-selector-parcellations]="parcs$ | async"
[sxplr-pure-atp-selector-is-busy]="isBusy$ | async"
(sxplr-pure-atp-selector-on-select)="selectLeaf($event)"
>
<button mat-icon-button
(click)="toggleParcellationVisibility()"
parcellation-chip-suffix
iav-stop="mousedown click"
[iav-key-listener]="[{'type': 'keydown', 'key': 'q', 'capture': true, 'target': 'document' }]"
(iav-key-event)="toggleParcellationVisibility()" >
<i class="fas" [ngClass]="(parcellationVisibility$ | async) ? 'fa-eye' : 'fa-eye-slash'"></i>
</button>
</sxplr-pure-atp-selector>
import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, HostBinding, Input, OnChanges, Output, SimpleChanges } from "@angular/core";
import { SmartChipContent } from "../smartChip.content.directive"
import { SmartChipMenu } from "../smartChip.menu.directive";
import { rgbToHsl, hexToRgb } from 'common/util'
const cssColorIsDark = (input: string) => {
if (/rgb/i.test(input)) {
const match = /\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(input)
if (!match) throw new Error(`rgb cannot be extracted ${input}`)
const rgb = [
parseInt(match[1]),
parseInt(match[2]),
parseInt(match[3]),
]
const [_h, _s, l] = rgbToHsl(...rgb)
return l < 0.4
}
if (/hsl/i.test(input)) {
const match = /\((.*)\)/.exec(input)
const [h, s, l] = match[1].split(",")
const trimmedL = l.trim()
if (/\%$/.test(trimmedL)) {
const match = /^([0-9]+)\%/.exec(trimmedL)
return (parseInt(match[1]) / 100) < 0.4
}
}
if (/^#/i.test(input) && input.length === 7) {
const [r, g, b] = hexToRgb(input)
const [_h, _s, l] = rgbToHsl(r, g, b)
return l < 0.6
}
throw new Error(`Cannot parse css color: ${input}`)
}
@Component({
selector: `sxplr-smart-chip`,
templateUrl: `./smartChip.template.html`,
styleUrls: [
`/smartChip.style.css`
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SmartChip<T extends object> implements OnChanges{
@Input('color')
color = `rgba(200, 200, 200, 1)`
@Input('disabled')
disabled: boolean = false
@Input('elevation')
elevation: number = 4 // translates to mat-elevation-z{elevation}
smartChipClass = 'mat-elevation-z4'
@Input('items')
items: T[] = []
@Input('getChildren')
getChildren: (item: T) => T[] = item => item['children'] || []
@Output('itemClicked')
itemClicked = new EventEmitter<T>()
@ContentChild(SmartChipContent)
contentTmpl: SmartChipContent
@ContentChild(SmartChipMenu)
menuTmpl: SmartChipMenu
@HostBinding('class')
darkTheme: string = 'lighttheme'
ngOnChanges(simpleChanges: SimpleChanges) {
if (simpleChanges.color) {
this.darkTheme = cssColorIsDark(this.color)
? 'darktheme'
: 'lighttheme'
}
if (simpleChanges.disabled || simpleChanges.elevation) {
this.smartChipClass = [
this.disabled ? 'disabled' : null,
`mat-elevation-z${this.elevation}`
].filter(v => !!v).join(' ')
}
}
}
.smart-chip
{
display: inline-block;
padding: 0.5rem 1rem;
margin: 0.5rem 1rem;
border-radius: 1rem;
height: 1.25rem;
display: inline-flex;
flex-direction: row;
align-items: center;
}
.smart-chip
{
opacity: 1.0;
transition: opacity 160ms ease-in-out;
}
.smart-chip:not(.disabled):hover
{
cursor: default;
}
.smart-chip.disabled
{
opacity: 0.5;
pointer-events: none;
}
.smart-chip.disabled:hover
{
cursor: not-allowed;
}
<div [style.background-color]="color"
[matMenuTriggerFor]="mainMenu"
matRipple
[ngClass]="smartChipClass"
class="mat-body smart-chip sxplr-custom-cmp text">
<ng-template [ngTemplateOutlet]="contentTmpl?.templateRef || fallbackContentTmpl">
</ng-template>
</div>
<!-- main menu is fired from chip -->
<mat-menu #mainMenu="matMenu">
<ng-template ngFor [ngForOf]="items" let-item>
<!-- if item is has submenu -->
<ng-template [ngIf]="item | hasSubMenu : getChildren" [ngIfElse]="noSubMenuTmpl">
<button
mat-menu-item
[matMenuTriggerFor]="subMenu"
[matMenuTriggerData]="{ $implicit: getChildren(item) }">
<ng-container *ngTemplateOutlet="menuTmpl?.templateRef || fallbackMenu; context: {
$implicit: item
}">
</ng-container>
</button>
</ng-template>
<!-- if item has no submenu -->
<ng-template #noSubMenuTmpl>
<ng-container *ngTemplateOutlet="leafTmpl; context: { $implicit: item }"></ng-container>
</ng-template>
</ng-template>
</mat-menu>
<!-- submenu (fired from menu item) -->
<mat-menu #subMenu="matMenu">
<ng-template matMenuContent let-items>
<ng-template ngFor [ngForOf]="items" let-item>
<ng-container *ngTemplateOutlet="leafTmpl; context: { $implicit: item }"></ng-container>
</ng-template>
</ng-template>
</mat-menu>
<!-- template to render the leaf nodes -->
<ng-template #leafTmpl let-item>
<button mat-menu-item (click)="itemClicked.emit(item)">
<ng-container *ngTemplateOutlet="menuTmpl?.templateRef || fallbackMenu; context: {
$implicit: item
}">
</ng-container>
</button>
</ng-template>
<ng-template #fallbackContentTmpl>
Fallback Content
</ng-template>
<ng-template #fallbackMenu let-item>
{{ item.name || 'Item Name' }}
</ng-template>
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: 'hasSubMenu',
pure: true
})
export class HasSubMenuPipe<T extends object> implements PipeTransform{
public transform(item: T, getChildren: (obj: T) => T[]): boolean {
return (getChildren(item) || []).length > 0
}
}
export { SmartChipModule } from "./module"
export { SmartChip } from "./component/smartChip.component"
export { SmartChipContent } from "./smartChip.content.directive"
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