diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04f5a9d4cd45690589842a134d2985614ed62403..c3c07d114df83ae2c2dc663da720cc5451914f31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,6 @@ name: '[ci]' on: push: - branches-ignore: - - 'dev' - - 'staging' - - 'master' # ignore changes to docs and mkdocs.yml paths-ignore: @@ -66,4 +62,4 @@ jobs: cd deploy npm i npm run test - \ No newline at end of file + diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index daa098e920be68ce850beb81fd1450505dbfc1cd..3366bae8a3fcb7db4160d5c103970f0ae56d39e2 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -1,6 +1,13 @@ name: '[docker image]' -on: [ 'push' ] +on: + push: + # do not rebuild if... + paths-ignore: + # changes to .openshift directory... mostly devops config + - '.openshift/*' + # docs (docs are built on readthedocs any way) + - 'docs/*' jobs: build-docker-img: diff --git a/.gitignore b/.gitignore index 4a28d67aecc150bc9b509a5abf305128d1784839..05ba580503a02c6b0557b29836fa8bc2a42b4295 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ site *.log cachedKgDataset.json +tmp diff --git a/angular.json b/angular.json index 8e887675e54154cb46b0eac5e871ce64c6079695..68b6001b6a2bd6b197221bcf374c11f378bff086 100644 --- a/angular.json +++ b/angular.json @@ -1,5 +1,8 @@ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "cli": { + "analytics": false + }, "version": 1, "newProjectRoot": "projects", "projects": { diff --git a/common/constants.js b/common/constants.js index a4880c3a454d1d505182126643a24fab6c197a79..1527bc74e86405a3f23353e182103e9a7da31882 100644 --- a/common/constants.js +++ b/common/constants.js @@ -112,7 +112,10 @@ QUICKTOUR_CANCEL: `Dismiss`, DELETE_ALL_ANNOTATION_CONFIRMATION_MSG: `Are you sure you want to delete all annotations?`, - LOADING_ANNOTATION_MSG: `Loading annotations... Please wait...` + LOADING_ANNOTATION_MSG: `Loading annotations... Please wait...`, + + ATLAS_SELECTOR_LABEL_SPACES: `Spaces`, + ATLAS_SELECTOR_LABEL_PARC_MAPS: `Parcellation maps` } exports.QUICKTOUR_DESC ={ diff --git a/deploy/app.js b/deploy/app.js index 0a9aaa02df174ea5d934dddc6c7d998bd647f220..3fa93a530fe2ea7076fe7978d00654afc0ab7bc4 100644 --- a/deploy/app.js +++ b/deploy/app.js @@ -197,9 +197,6 @@ app.get('/', (req, res, next) => { } }) - -app.use('/logo', require('./logo')) - app.get('/ready', async (req, res) => { const authIsReady = authReady ? await authReady() : false diff --git a/deploy/csp/index.js b/deploy/csp/index.js index b7e07519626b1a0b3cf8f8f86728a1c6d7d333fd..b3e439e8160c050734f23307341d71b782705dc2 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -116,8 +116,8 @@ module.exports = { 'unpkg.com/kg-dataset-previewer@1.2.0/', // preview component 'cdnjs.cloudflare.com/ajax/libs/mathjax/', // math jax 'https://unpkg.com/three-surfer@0.0.10/dist/bundle.js', // for threeSurfer (freesurfer support in browser) - 'https://unpkg.com/ng-layer-tune@0.0.2/dist/ng-layer-tune/', // needed for ng layer control - (req, res) => res.locals.nonce ? [`'nonce-${res.locals.nonce}'`] : [], + 'https://unpkg.com/ng-layer-tune@0.0.5/dist/ng-layer-tune/', // needed for ng layer control + (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null, ...SCRIPT_SRC, ...WHITE_LIST_SRC, ...defaultAllowedSites diff --git a/deploy/logo/index.js b/deploy/logo/index.js deleted file mode 100644 index d4750d225f9d0a09510db3fa40175544670c2969..0000000000000000000000000000000000000000 --- a/deploy/logo/index.js +++ /dev/null @@ -1,40 +0,0 @@ -const fs = require('fs') -const path = require('path') -const { getHandleErrorFn } = require('../util/streamHandleError') -const router = require('express').Router() - -const map = new Map([ - ['hbp', { - mimetype: 'image/png', - light: 'HBP_Primary_RGB_BlackText.png', - dark: 'HBP_Primary_RGB_WhiteText.png' - }], - ['ebrains', { - mimetype: 'image/svg+xml', - light: 'ebrains-logo-dark.svg', - dark: 'ebrains-logo-light.svg' - }], - ['fzj', { - mimetype: 'image/svg+xml', - light: 'fzj_black_transparent_svg.svg', - dark: 'fzj_white_transparent_svg.svg' - }] -]) - -router.get('/', (req, res) => { - - const USE_LOGO = process.env.USE_LOGO || 'hbp' - const { mimetype, light, dark } = map.get(USE_LOGO) || map.get('hbp') - const darktheme = !!req.query.darktheme - try { - res.setHeader('Content-type', mimetype) - fs.createReadStream( - path.join(__dirname, `assets/${darktheme ? dark : light}`) - ).pipe(res).on('error', getHandleErrorFn(req, res)) - } catch (e) { - console.error(`Fetching logo error ${e.toString()}`) - res.status(500).end() - } -}) - -module.exports = router \ No newline at end of file diff --git a/deploy/plugins/index.js b/deploy/plugins/index.js index 929f19499786e69b7dc1461a27ae96972e32b8a0..8d6bee52dadaf45913c133479a42142b3c2a4a3e 100644 --- a/deploy/plugins/index.js +++ b/deploy/plugins/index.js @@ -7,6 +7,16 @@ const express = require('express') const lruStore = require('../lruStore') const got = require('got') const router = express.Router() +const DEV_PLUGINS = (() => { + try { + return JSON.parse( + process.env.DEV_PLUGINS || `[]` + ) + } catch (e) { + console.warn(`Parsing DEV_PLUGINS failed: ${e}`) + return [] + } +})() const PLUGIN_URLS = (process.env.PLUGIN_URLS && process.env.PLUGIN_URLS.split(';')) || [] const STAGING_PLUGIN_URLS = (process.env.STAGING_PLUGIN_URLS && process.env.STAGING_PLUGIN_URLS.split(';')) || [] @@ -44,7 +54,7 @@ router.get('/manifests', async (_req, res) => { })) res.status(200).json( - allManifests.filter(v => !!v) + [...DEV_PLUGINS, ...allManifests.filter(v => !!v)] ) }) diff --git a/deploy/saneUrl/index.js b/deploy/saneUrl/index.js index 72d0f9f9e2a8a13f98f96b7989c167ae61e2f47a..010f28e87db03d0f143fd78e961f3182d766af20 100644 --- a/deploy/saneUrl/index.js +++ b/deploy/saneUrl/index.js @@ -7,7 +7,12 @@ const RedisStore = require('rate-limit-redis') const { redisURL } = require('../lruStore') const { ProxyStore, NotExactlyPromiseAny } = require('./util') -const store = new Store() +let store +try { + store = new Store() +} catch (e) { + console.error(`Failed to new store.`, e) +} const depStore = new DepcStore() const proxyStore = new ProxyStore(store) diff --git a/deploy/saneUrl/util.js b/deploy/saneUrl/util.js index 949025cdc76bbf6b8794222bc67e2995ae1da482..12fce0a38ac846600110d638b1ea7a96cb6fd3cd 100644 --- a/deploy/saneUrl/util.js +++ b/deploy/saneUrl/util.js @@ -2,6 +2,7 @@ const { NotFoundError } = require('./store') class ProxyStore { static async StaticGet(store, req, name) { + if (!store) throw new Error(`store is falsy`) const payload = JSON.parse(await store.get(name)) const { expiry, value, ...rest } = payload if (expiry && (Date.now() > expiry)) { @@ -21,6 +22,7 @@ class ProxyStore { } async set(req, name, value) { + if (!this.store) throw new Error(`store is falsy`) const supplementary = req.user ? { userId: req.user.id, diff --git a/docs/releases/v2.6.0.md b/docs/releases/v2.6.0.md new file mode 100644 index 0000000000000000000000000000000000000000..90f139cfc720729684972e5cc609ea6bc8be60ea --- /dev/null +++ b/docs/releases/v2.6.0.md @@ -0,0 +1,14 @@ +# v2.6.0 + +## New features + +- add the capability of add 3rd party plugins (experimental) + +## Under the hood stuff + +- refactor: simplify DOM, remove unused functions +- reworked logo path +- added visual indication when a new template is being loaded +- reworked visual for `supported template` in region side panel +- improve performance by disabling `zone.js` patch of `requestAnimationFrame` and change detection strategy of nehuba viewer +- added message when no regional features found (#1111) diff --git a/e2e/checklist.md b/e2e/checklist.md index 8434c5cbb39ae3f35be8fbd72d6ad4bfe48fb37a..3c0cbe0a538168ea49aeff84877386c05826459b 100644 --- a/e2e/checklist.md +++ b/e2e/checklist.md @@ -54,4 +54,11 @@ - [ ] Waxholm - [ ] v4 are visible - [ ] on hover, show correct region name(s) - - [ ] whole mesh loads \ No newline at end of file + - [ ] whole mesh loads +## saneURL +- [ ] [saneUrl](https://siibra-explorer.apps.hbp.eu/staging/saneUrl/bigbrainGreyWhite) redirects to big brain +- [ ] [saneUrl](https://siibra-explorer.apps.hbp.eu/staging/saneUrl/julichbrain) redirects to julich brain (colin 27) +- [ ] [saneUrl](https://siibra-explorer.apps.hbp.eu/staging/saneUrl/whs4) redirects to waxholm v4 +- [ ] [saneUrl](https://siibra-explorer.apps.hbp.eu/staging/saneUrl/allen2017) redirects to allen 2017 +- [ ] [saneUrl](https://siibra-explorer.apps.hbp.eu/staging/saneUrl/mebrains) redirects to monkey + diff --git a/mkdocs.yml b/mkdocs.yml index a2ade830ee78908a40bb8dcd9ee1b7b489753329..95827211ee2faac6753b6dae9b4d023d6e716530 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,7 @@ pages: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.6.0: 'releases/v2.6.0.md' - v2.5.8: 'releases/v2.5.8.md' - v2.5.7: 'releases/v2.5.7.md' - v2.5.6: 'releases/v2.5.6.md' diff --git a/package.json b/package.json index b538486fe3ccd90b081f90e8e2d462f80acbf90f..c9e41e939e48afb2cd1edaa045ce9fc801eb10ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactive-viewer", - "version": "2.5.8", + "version": "2.6.0", "description": "HBP interactive atlas viewer. Integrating KG query, dataset previews & more. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "build-aot": "ng build && node ./third_party/matomo/processMatomo.js", diff --git a/deploy/logo/assets/HBP_Primary_RGB_BlackText.png b/src/assets/logo/HBP_Primary_RGB_BlackText.png similarity index 100% rename from deploy/logo/assets/HBP_Primary_RGB_BlackText.png rename to src/assets/logo/HBP_Primary_RGB_BlackText.png diff --git a/deploy/logo/assets/HBP_Primary_RGB_WhiteText.png b/src/assets/logo/HBP_Primary_RGB_WhiteText.png similarity index 100% rename from deploy/logo/assets/HBP_Primary_RGB_WhiteText.png rename to src/assets/logo/HBP_Primary_RGB_WhiteText.png diff --git a/deploy/logo/assets/ebrains-logo-dark.svg b/src/assets/logo/ebrains-logo-dark.svg similarity index 100% rename from deploy/logo/assets/ebrains-logo-dark.svg rename to src/assets/logo/ebrains-logo-dark.svg diff --git a/deploy/logo/assets/ebrains-logo-light.svg b/src/assets/logo/ebrains-logo-light.svg similarity index 100% rename from deploy/logo/assets/ebrains-logo-light.svg rename to src/assets/logo/ebrains-logo-light.svg diff --git a/deploy/logo/assets/fzj_black_transparent_svg.svg b/src/assets/logo/fzj_black_transparent_svg.svg similarity index 100% rename from deploy/logo/assets/fzj_black_transparent_svg.svg rename to src/assets/logo/fzj_black_transparent_svg.svg diff --git a/deploy/logo/assets/fzj_white_transparent_svg.svg b/src/assets/logo/fzj_white_transparent_svg.svg similarity index 100% rename from deploy/logo/assets/fzj_white_transparent_svg.svg rename to src/assets/logo/fzj_white_transparent_svg.svg diff --git a/src/atlasComponents/parcellation/getParcPreviewUrl.pipe.ts b/src/atlasComponents/parcellation/getParcPreviewUrl.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f70d60e90bdb1645d44f74edbe86ce7b6aec463 --- /dev/null +++ b/src/atlasComponents/parcellation/getParcPreviewUrl.pipe.ts @@ -0,0 +1,49 @@ +import { Pipe, PipeTransform } from "@angular/core" + +const previewImgMap = new Map([ + + ['minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579', 'cytoarchitectonic-maps.png'], + ['juelich/iav/atlas/v1.0.0/3', 'cortical-layers.png'], + ['juelich/iav/atlas/v1.0.0/4', 'grey-white-matter.png'], + ['juelich/iav/atlas/v1.0.0/5', 'firbe-long.png'], + ['juelich/iav/atlas/v1.0.0/6', 'firbe-short.png'], + ['minds/core/parcellationatlas/v1.0.0/d80fbab2-ce7f-4901-a3a2-3c8ef8a3b721', 'difumo-64.png'], + ['minds/core/parcellationatlas/v1.0.0/73f41e04-b7ee-4301-a828-4b298ad05ab8', 'difumo-128.png'], + ['minds/core/parcellationatlas/v1.0.0/141d510f-0342-4f94-ace7-c97d5f160235', 'difumo-256.png'], + ['minds/core/parcellationatlas/v1.0.0/63b5794f-79a4-4464-8dc1-b32e170f3d16', 'difumo-512.png'], + ['minds/core/parcellationatlas/v1.0.0/12fca5c5-b02c-46ce-ab9f-f12babf4c7e1', 'difumo-1024.png'], + + + ['minds/core/parcellationatlas/v1.0.0/05655b58-3b6f-49db-b285-64b5a0276f83', 'allen-mouse-2017.png'], + ['minds/core/parcellationatlas/v1.0.0/39a1384b-8413-4d27-af8d-22432225401f', 'allen-mouse-2015.png'], + + + ['minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe', 'waxholm-v3.png'], + ['minds/core/parcellationatlas/v1.0.0/2449a7f0-6dd0-4b5a-8f1e-aec0db03679d', 'waxholm-v2.png'], + ['minds/core/parcellationatlas/v1.0.0/11017b35-7056-4593-baad-3934d211daba', 'waxholm-v1.png'], + ['juelich/iav/atlas/v1.0.0/79cbeaa4ee96d5d3dfe2876e9f74b3dc3d3ffb84304fb9b965b1776563a1069c', 'short-bundle-hcp.png'], + + ['minds/core/parcellationatlas/v1.0.0/mebrains-tmp-id', 'primate-parc.png'], +]) + +/** + * used for directories + */ +const previewNameToPngMap = new Map([ + ['fibre architecture', 'firbe-long.png'], + ['functional modes', 'difumo-128.png'] +]) + +@Pipe({ + name: 'getParcPreviewUrl', + pure: true +}) + +export class GetParcPreviewUrlPipe implements PipeTransform{ + public transform(tile: any){ + const filename = tile['@id'] + ? previewImgMap.get(tile['@id']) + : previewNameToPngMap.get(tile['name']) + return filename && `assets/images/atlas-selection/${filename}` + } +} diff --git a/src/atlasComponents/parcellation/index.ts b/src/atlasComponents/parcellation/index.ts index 4af8252e223c89fce580454087e7038563e1fab4..f49438e683dafb818113eb571433a364854eb24b 100644 --- a/src/atlasComponents/parcellation/index.ts +++ b/src/atlasComponents/parcellation/index.ts @@ -1,3 +1,4 @@ +export { GetParcPreviewUrlPipe } from "./getParcPreviewUrl.pipe"; export { FilterNameBySearch } from "./regionHierachy/filterNameBySearch.pipe"; export { AtlasCmpParcellationModule } from "./module"; export { RegionHierarchy } from "./regionHierachy/regionHierarchy.component"; diff --git a/src/atlasComponents/parcellation/module.ts b/src/atlasComponents/parcellation/module.ts index 7763c155a14babdc53f3d81c2c8466410066d5e0..c3e101c3a1d4bf3b2a6384723d79414e59a76429 100644 --- a/src/atlasComponents/parcellation/module.ts +++ b/src/atlasComponents/parcellation/module.ts @@ -8,6 +8,7 @@ import { FilterNameBySearch } from "./regionHierachy/filterNameBySearch.pipe"; import { UtilModule } from "src/util"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { ComponentsModule } from "src/components"; +import { GetParcPreviewUrlPipe } from "./getParcPreviewUrl.pipe"; @NgModule({ imports: [ @@ -24,11 +25,13 @@ import { ComponentsModule } from "src/components"; RegionTextSearchAutocomplete, FilterNameBySearch, + GetParcPreviewUrlPipe, ], exports: [ RegionHierarchy, RegionTextSearchAutocomplete, FilterNameBySearch, + GetParcPreviewUrlPipe, ] }) export class AtlasCmpParcellationModule{} \ No newline at end of file diff --git a/src/atlasComponents/parcellationRegion/module.ts b/src/atlasComponents/parcellationRegion/module.ts index d4507dcaa7e390cbeb96d0a1519c3070bb6ee1d8..03aceeb2d6353c60a3bdb08a634fbbd87a4199d8 100644 --- a/src/atlasComponents/parcellationRegion/module.ts +++ b/src/atlasComponents/parcellationRegion/module.ts @@ -11,6 +11,9 @@ import { BSFeatureModule } from "../regionalFeatures/bsFeatures"; import { RegionAccordionTooltipTextPipe } from "./regionAccordionTooltipText.pipe"; import { AtlasCmptConnModule } from "../connectivity"; import { HttpClientModule } from "@angular/common/http"; +import { RegionInOtherTmplPipe } from "./regionInOtherTmpl.pipe"; +import { SiibraExplorerTemplateModule } from "../template"; +import { KgDatasetModule } from "../regionalFeatures/bsFeatures/kgDataset"; @NgModule({ imports: [ @@ -21,6 +24,8 @@ import { HttpClientModule } from "@angular/common/http"; BSFeatureModule, AtlasCmptConnModule, HttpClientModule, + SiibraExplorerTemplateModule, + KgDatasetModule, ], declarations: [ RegionMenuComponent, @@ -29,6 +34,7 @@ import { HttpClientModule } from "@angular/common/http"; RegionDirective, RenderViewOriginDatasetLabelPipe, RegionAccordionTooltipTextPipe, + RegionInOtherTmplPipe, ], exports: [ RegionMenuComponent, diff --git a/src/atlasComponents/parcellationRegion/region.base.spec.ts b/src/atlasComponents/parcellationRegion/region.base.spec.ts index df343917f7bb050a4a0b3553c1fabd7f918bfaa1..3e4085c70e1df483bd89f47e31dda4c3d167f1da 100644 --- a/src/atlasComponents/parcellationRegion/region.base.spec.ts +++ b/src/atlasComponents/parcellationRegion/region.base.spec.ts @@ -1,7 +1,8 @@ import { TestBed } from '@angular/core/testing' import { MockStore, provideMockStore } from '@ngrx/store/testing' import { viewerStateSelectTemplateWithId } from 'src/services/state/viewerState/actions' -import { RegionBase, regionInOtherTemplateSelector, getRegionParentParcRefSpace } from './region.base' +import { RegionBase, getRegionParentParcRefSpace } from './region.base' +import { TSiibraExRegion } from './type' // eslint-disable-next-line @typescript-eslint/no-var-requires const util = require('common/util') @@ -27,430 +28,7 @@ enum EnumParcRegVersion{ V2_4 = "V2_4" } -const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => { - switch (version) { - case EnumParcRegVersion.V1_18: { - /** - * regions - */ - - const mr1wrong = { - labelIndex: 1, - name: 'mr1', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'fff-bbb' - } - } - } - - const mr0wrong = { - labelIndex: 1, - name: 'mr0', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'aaa-fff' - } - } - } - - - const mr1lh = { - labelIndex: 1, - name: 'mr1 left', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'ccc-bbb' - } - } - } - - const mr1rh = { - labelIndex: 1, - name: 'mr1 right', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'ccc-bbb' - } - } - } - - const mr0nh = { - labelIndex: 11, - name: 'mr0', - } - - const mr0lh = { - labelIndex: 1, - name: 'mr0 left', - availableIn: [{id: 'fzj/mock/rs/v0.0.0/aaa-bbb'}, {id: 'fzj/mock/rs/v0.0.0/bbb-bbb'}, {id: 'fzj/mock/rs/v0.0.0/ccc-bbb'}], - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'aaa-bbb' - } - } - } - - const mr0rh = { - labelIndex: 1, - name: 'mr0 right', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'aaa-bbb' - } - } - } - - const mr1 = { - labelIndex: 1, - name: 'mr1', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'ccc-bbb' - } - } - } - - // parcellations - - const mp1h = { - name: 'mp1h', - '@id': 'parcellation/id', - regions: [ mr0nh, mr1lh, mr0lh, mr0rh, mr1rh ] - } - - const mpWrong = { - name: 'mp1h', - '@id': 'parcellation/id', - regions: [ mr1wrong, mr0wrong ] - } - - const mp0 = { - name: 'mp0', - '@id': 'parcellation/id', - regions: [ mr1, mr0 ] - } - - // templates - - const mt0 = { - name: 'mt0', - '@id': 'fzj/mock/rs/v0.0.0/aaa-bbb', - parcellations: [ mp0 ] - } - - const mt1 = { - name: 'mt1', - '@id': 'fzj/mock/rs/v0.0.0/bbb-bbb', - parcellations: [ mp0 ] - } - - const mt2 = { - name: 'mt2', - '@id': 'fzj/mock/rs/v0.0.0/ccc-bbb', - parcellations: [ mp1h ] - } - - const mt3 = { - name: 'mt3', - '@id': 'fzj/mock/rs/v0.0.0/ddd-bbb', - parcellations: [ mp1h ] - } - - const mtWrong = { - name: 'mtWrong', - '@id': 'fzj/mock/rs/v0.0.0/ddd-bbb', - parcellations: [ mpWrong ] - } - - const mockFetchedTemplates = [ mt0, mt1, mt2, mt3, mtWrong ] - return { - mockFetchedTemplates, - mt2, - mt0, - mp0, - mt1, - mp1h, - mr0lh, - mt3, - mr0, - mr0rh, - mr0nh - } - - } - case EnumParcRegVersion.V2_4: { - /** - * regions - */ - - const mr1wrong = { - labelIndex: 1, - name: 'mr1', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'fff-bbb' - } - } - } - - const mr0wrong = { - labelIndex: 1, - name: 'mr0', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'aaa-fff' - } - } - } - - - const mr1lh = { - labelIndex: 1, - name: 'mr1', - status: 'left', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'ccc-bbb' - } - } - } - - const mr1rh = { - labelIndex: 1, - name: 'mr1', - status: 'right', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'ccc-bbb' - } - } - } - - const mr0nh = { - labelIndex: 11, - name: 'mr0', - } - - const mr0lh = { - labelIndex: 1, - name: 'mr0 left', - availableIn: [{id: 'fzj/mock/rs/v0.0.0/aaa-bbb'}, {id: 'fzj/mock/rs/v0.0.0/bbb-bbb'}, {id: 'fzj/mock/rs/v0.0.0/ccc-bbb'}], - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'aaa-bbb' - } - } - } - - const mr0rh = { - labelIndex: 1, - name: 'mr0 right', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'aaa-bbb' - } - } - } - - const mr1 = { - labelIndex: 1, - name: 'mr1', - id: { - kg: { - kgSchema: 'fzj/mock/pr', - kgId: 'ccc-bbb' - } - } - } - - // parcellations - - const mp1h = { - name: 'mp1h', - '@id': 'parcellation/id', - regions: [ mr0nh, mr1lh, mr0lh, mr0rh, mr1rh ] - } - - const mpWrong = { - name: 'mp1h', - '@id': 'parcellation/id', - regions: [ mr1wrong, mr0wrong ] - } - - const mp0 = { - name: 'mp0', - '@id': 'parcellation/id', - regions: [ mr1, mr0 ] - } - - // templates - - const mt0 = { - name: 'mt0', - '@id': 'fzj/mock/rs/v0.0.0/aaa-bbb', - parcellations: [ mp0 ] - } - - const mt1 = { - name: 'mt1', - '@id': 'fzj/mock/rs/v0.0.0/bbb-bbb', - parcellations: [ mp0 ] - } - - const mt2 = { - name: 'mt2', - '@id': 'fzj/mock/rs/v0.0.0/ccc-bbb', - parcellations: [ mp1h ] - } - - const mt3 = { - name: 'mt3', - '@id': 'fzj/mock/rs/v0.0.0/ddd-bbb', - parcellations: [ mp1h ] - } - - const mtWrong = { - name: 'mtWrong', - '@id': 'fzj/mock/rs/v0.0.0/ddd-bbb', - parcellations: [ mpWrong ] - } - - const mockFetchedTemplates = [ mt0, mt1, mt2, mt3, mtWrong ] - return { - mockFetchedTemplates, - mt2, - mt0, - mp0, - mt1, - mp1h, - mr0lh, - mr0nh, - mt3, - mr0, - mr0rh - } - } - default: throw new Error(`version needs to be v1.18 or v2.4`) - } -} - describe('> region.base.ts', () => { - describe('> regionInOtherTemplateSelector', () => { - - // TODO - it('> only selects region in the template specified by selected atlas') - - for (const enumKey of Object.keys(EnumParcRegVersion)) { - describe(`> selector version for ${enumKey}`, () => { - - const { mockFetchedTemplates, mr0, mt2, mt0, mp0, mt1, mp1h, mr0lh, mt3, mr0rh, mr0nh } = getRegionInOtherTemplateSelectorBundle(enumKey as EnumParcRegVersion) - - let selectedAtlas = { - templateSpaces: mockFetchedTemplates - } - describe('> no hemisphere selected, simulates big brain cyto map', () => { - - let result: any[] - beforeAll(() => { - result = regionInOtherTemplateSelector.projector(selectedAtlas, mockFetchedTemplates, { region: {...mr0, context: {template: mt0, parcellation: mp0} }}) - }) - - it('> length checks out', () => { - expect(result.length).toEqual(4) - }) - - it('> does not contain itself', () => { - expect(result).not.toContain( - jasmine.objectContaining({ - template: mt0, - parcellation: mp0, - region: mr0 - }) - ) - }) - - it('> no hemisphere result has no hemisphere meta data', () => { - expect(result).toContain( - jasmine.objectContaining({ - template: mt1, - parcellation: mp0, - region: mr0 - }) - ) - }) - - it('> hemisphere result has hemisphere metadata # 1', () => { - expect(result).toContain( - jasmine.objectContaining({ - template: mt2, - parcellation: mp1h, - region: mr0lh, - hemisphere: 'left' - }) - ) - }) - it('> hemisphere result has hemisphere metadata # 2', () => { - expect(result).toContain( - jasmine.objectContaining({ - template: mt2, - parcellation: mp1h, - region: mr0rh, - hemisphere: 'right' - }) - ) - }) - }) - - describe('> hemisphere data selected (left), simulates julich-brain in mni152', () => { - let result - beforeAll(() => { - result = regionInOtherTemplateSelector.projector(selectedAtlas, mockFetchedTemplates, { region: {...mr0lh, context: {template: mt0, parcellation: mp1h} }}) - }) - - it('> length checks out', () => { - expect(result.length).toEqual(3) - }) - - it('> does not select wrong hemisphere (right)', () => { - expect(result).not.toContain( - jasmine.objectContaining({ - template: mt2, - parcellation: mp1h, - region: mr0rh, - }) - ) - }) - - it('> select the region with correct hemisphere', () => { - expect(result).toContain( - jasmine.objectContaining({ - template: mt2, - parcellation: mp1h, - region: mr0lh - }) - ) - }) - }) - - }) - } - - }) - describe('> RegionBase', () => { let regionBase: RegionBase let mockStore: MockStore @@ -461,7 +39,6 @@ describe('> region.base.ts', () => { ] }) mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(regionInOtherTemplateSelector, []) mockStore.overrideSelector(getRegionParentParcRefSpace, { template: null, parcellation: null }) }) describe('> position', () => { @@ -539,7 +116,6 @@ describe('> region.base.ts', () => { beforeEach(() => { strToRgbSpy = spyOn(util, 'strToRgb') mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(regionInOtherTemplateSelector, []) mockStore.overrideSelector(getRegionParentParcRefSpace, { template: null, parcellation: null }) }) @@ -647,10 +223,20 @@ describe('> region.base.ts', () => { it('> calls viewerStateSelectTemplateWithId', () => { - regionBase.changeView({ - template: fakeTmpl, - parcellation: fakeParc, - }) + const partialRegion = { + context: { + parcellation: fakeParc, + atlas: { + "@id": '', + name: '', + parcellations: [], + templateSpaces: [fakeTmpl] + }, + template: null + } + } as Partial<TSiibraExRegion> + regionBase.region = partialRegion as any + regionBase.changeView(fakeTmpl) expect(dispatchSpy).toHaveBeenCalledWith( viewerStateSelectTemplateWithId({ diff --git a/src/atlasComponents/parcellationRegion/region.base.ts b/src/atlasComponents/parcellationRegion/region.base.ts index f596afe76f7cfbffc7e83a39c24993e0b52e7530..5f4b7d327e4e1432abb128187ec7c738b6a1bd29 100644 --- a/src/atlasComponents/parcellationRegion/region.base.ts +++ b/src/atlasComponents/parcellationRegion/region.base.ts @@ -1,15 +1,16 @@ import { Directive, EventEmitter, Input, Output, Pipe, PipeTransform } from "@angular/core"; import { select, Store, createSelector } from "@ngrx/store"; import { uiStateOpenSidePanel, uiStateExpandSidePanel, uiActionShowSidePanelConnectivity } from 'src/services/state/uiState.store.helper' -import { distinctUntilChanged, switchMap, filter, map, withLatestFrom, tap } from "rxjs/operators"; +import { map, tap } from "rxjs/operators"; import { Observable, BehaviorSubject, combineLatest } from "rxjs"; -import { flattenRegions, getIdFromKgIdObj, rgbToHsl } from 'common/util' -import { viewerStateSetConnectivityRegion, viewerStateNavigateToRegion, viewerStateToggleRegionSelect, viewerStateNewViewer, isNewerThan, viewerStateSelectTemplateWithId } from "src/services/state/viewerState.store.helper"; -import { viewerStateFetchedTemplatesSelector, viewerStateGetSelectedAtlas, viewerStateSelectedTemplateFullInfoSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; -import { strToRgb, verifyPositionArg, getRegionHemisphere } from 'common/util' +import { rgbToHsl } from 'common/util' +import { viewerStateSetConnectivityRegion, viewerStateNavigateToRegion, viewerStateToggleRegionSelect, viewerStateSelectTemplateWithId } from "src/services/state/viewerState.store.helper"; +import { viewerStateGetSelectedAtlas, viewerStateSelectedTemplatePureSelector } from "src/services/state/viewerState/selectors"; +import { strToRgb, verifyPositionArg } from 'common/util' import { getPosFromRegion } from "src/util/siibraApiConstants/fn"; import { TRegionDetail } from "src/util/siibraApiConstants/types"; import { IHasId } from "src/util/interfaces"; +import { TSiibraExTemplate } from "./type"; @Directive() export class RegionBase { @@ -83,75 +84,16 @@ export class RegionBase { @Output() public closeRegionMenu: EventEmitter<boolean> = new EventEmitter() - public sameRegionTemplate: any[] = [] - public regionInOtherTemplates$: Observable<any[]> public regionOriginDatasetLabels$: Observable<{ name: string }[]> public selectedAtlas$: Observable<any> = this.store$.pipe( select(viewerStateGetSelectedAtlas) ) - public selectedTemplateFullInfo$: Observable<any[]> constructor( private store$: Store<any>, ) { - this.selectedTemplateFullInfo$ = this.store$.pipe( - select(viewerStateSelectedTemplateFullInfoSelector), - ) - - this.regionInOtherTemplates$ = this.region$.pipe( - distinctUntilChanged(), - filter(v => !!v && !!v.context), - switchMap(region => this.store$.pipe( - select( - regionInOtherTemplateSelector, - { region } - ), - withLatestFrom( - this.store$.pipe( - select(viewerStateGetSelectedAtlas) - ) - ), - map(([ regionsInOtherTemplates, selectedatlas ]) => { - const { parcellations } = selectedatlas - const filteredRsInOtherTmpls = [] - for (const bundledObj of regionsInOtherTemplates) { - const { template, parcellation, region } = bundledObj - - /** - * trying to find duplicate region - * with same templateId, and same hemisphere - */ - const sameEntityIdx = filteredRsInOtherTmpls.findIndex(({ template: _template, region: _region }) => { - return _template['@id'] === template['@id'] - && getRegionHemisphere(_region) === getRegionHemisphere(region) - }) - /** - * if doesn't exist, just push to output - */ - if ( sameEntityIdx < 0 ) { - filteredRsInOtherTmpls.push(bundledObj) - } else { - - /** - * if exists, only append the latest version - */ - const { parcellation: currentParc } = filteredRsInOtherTmpls[sameEntityIdx] - /** - * if the new element is newer than existing item - */ - if (isNewerThan(parcellations, parcellation, currentParc)) { - filteredRsInOtherTmpls.splice(sameEntityIdx, 1) - filteredRsInOtherTmpls.push(bundledObj) - } - } - } - return filteredRsInOtherTmpls - }) - )) - ) - this.regionOriginDatasetLabels$ = combineLatest([ this.store$, this.region$ @@ -161,6 +103,9 @@ export class RegionBase { ) } + public selectedTemplate$ = this.store$.pipe( + select(viewerStateSelectedTemplatePureSelector), + ) public navigateToRegion() { this.closeRegionMenu.emit() @@ -190,13 +135,14 @@ export class RegionBase { ) } - changeView(sameRegion) { - const { - template, - parcellation, - } = sameRegion + changeView(template: TSiibraExTemplate) { + this.closeRegionMenu.emit() + const { + parcellation + } = (this.region?.context || {}) + /** * TODO use createAction in future * for now, not importing const because it breaks tests @@ -280,81 +226,3 @@ export class RenderViewOriginDatasetLabelPipe implements PipeTransform{ return `origin dataset` } } - -export const regionInOtherTemplateSelector = createSelector( - viewerStateGetSelectedAtlas, - viewerStateFetchedTemplatesSelector, - (atlas, fetchedTemplates, prop) => { - const atlasTemplateSpacesIds = atlas.templateSpaces.map(a => a['@id']) - const { region: regionOfInterest } = prop - const returnArr = [] - - const regionOfInterestHemisphere = getRegionHemisphere(regionOfInterest) - - // need to ensure that the templates are defined in atlas definition - // atlas is the single source of truth - - const otherTemplates = fetchedTemplates - .filter(({ ['@id']: id }) => id !== regionOfInterest.context.template['@id'] - && atlasTemplateSpacesIds.includes(id) - && (regionOfInterest.availableIn || []).map(ai => ai.id).includes(id)) - - for (const template of otherTemplates) { - const parcellation = template.parcellations.find(p => p['@id'] === regionOfInterest.context.parcellation['@id']) - - const flattenedRegions = flattenRegions(parcellation.regions) - const selectableRegions = flattenedRegions.filter(({ labelIndex }) => !!labelIndex) - - for (const region of selectableRegions) { - if (regionsEqual(regionOfInterest, region)) { - - const regionHemisphere = getRegionHemisphere(region) - - /** - * if both hemisphere metadatas are defined - */ - if ( - !!regionOfInterestHemisphere && - !!regionHemisphere - ) { - if (regionHemisphere === regionOfInterestHemisphere) { - returnArr.push({ - template, - parcellation, - region, - }) - } - } else { - returnArr.push({ - template, - parcellation, - region, - hemisphere: regionHemisphere - }) - } - } - } - - } - return returnArr - } -) - -const regionsEqual = (region1, region2) => { - const region1Hemisphere = getRegionHemisphere(region1) - const region2Hemisphere = getRegionHemisphere(region2) - - if (region1.id && region1.id.kg && region2.id && region2.id.kg) { - return getIdFromKgIdObj(region1.id.kg) === getIdFromKgIdObj(region2.id.kg) - // If both has hemispheres, they should be equal - && (!(region1Hemisphere && region2Hemisphere) || region1Hemisphere === region2Hemisphere) - } - - if (region1Hemisphere && region2Hemisphere) { - return region1.name === region2.name - } else { - const region1NameBasis = region1Hemisphere? region1.name.substring(0, region1.name.lastIndexOf(' ')) : region1.name - const region2NameBasis = region2Hemisphere? region2.name.substring(0, region2.name.lastIndexOf(' ')) : region2.name - return region1NameBasis === region2NameBasis - } -} diff --git a/src/atlasComponents/parcellationRegion/regionInOtherTmpl.pipe.ts b/src/atlasComponents/parcellationRegion/regionInOtherTmpl.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a0e81dc8ee756aa29b6112e1ebab77987f33ed5 --- /dev/null +++ b/src/atlasComponents/parcellationRegion/regionInOtherTmpl.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { TSiibraExRegion } from "./type"; + +@Pipe({ + name: 'regionInOtherTmpl', + pure: true +}) + +export class RegionInOtherTmplPipe implements PipeTransform{ + public transform(region: TSiibraExRegion){ + const { templateSpaces: allTmpl = [] } = region?.context?.atlas || {} + return allTmpl.filter(t => (region?.availableIn || []).find(availTmpl => availTmpl['id'] === t["@id"])) + } +} diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.ts b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.ts index 879815ee4ae1d4a510927cde5193522dd5178aa9..f6456eba6b90135ae35ea96b110834093b46a4f8 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.ts +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.ts @@ -1,9 +1,10 @@ import { Component, OnDestroy } from "@angular/core"; import { Store } from "@ngrx/store"; -import { Observable, Subscription } from "rxjs"; +import { merge, Observable, Subject, Subscription } from "rxjs"; import { RegionBase } from '../region.base' import { CONST, ARIA_LABELS } from 'common/constants' import { ComponentStore } from "src/viewerModule/componentStore"; +import { distinctUntilChanged, mapTo } from "rxjs/operators"; @Component({ selector: 'region-menu', @@ -19,6 +20,19 @@ export class RegionMenuComponent extends RegionBase implements OnDestroy { public activePanelTitles$: Observable<string[]> private activePanelTitles: string[] = [] + + public intentToChgTmpl$ = new Subject() + public lockOtherTmpl$ = merge( + this.selectedTemplate$.pipe( + mapTo(false) + ), + this.intentToChgTmpl$.pipe( + mapTo(true) + ) + ).pipe( + distinctUntilChanged() + ) + constructor( store$: Store<any>, private viewerCmpLocalUiStore: ComponentStore<{ activePanelsTitle: string[] }>, diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.style.css b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.style.css index 34d152c481193d92a7b78b2fc6c0901f955556f9..386e688cd8e1adc6489f109fbadf71148c4d5ccc 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.style.css +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.style.css @@ -20,3 +20,35 @@ mat-icon font-size: 95%; line-height: normal; } + +:host-context([darktheme="true"]) .loading-overlay +{ + background-color: rgba(10, 10, 10, 0.8); +} + +.loading-overlay +{ + background-color: rgba(250, 250, 250, 0.8); +} + +.loading-overlay +{ + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + font-size: 200%; + + display: grid; + grid-template-columns: auto; + grid-template-rows: 1fr auto 1fr; + grid-template-columns: 1fr auto 1fr; + grid-template-areas: "." "vertical-center" "."; +} + +.loading-overlay > .spinner +{ + grid-column: 2; + grid-row: 2; +} diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html index db607952a6369db63f42fc4bbf83d7d965c4d711..06aac677565c2f5f7f4f169710bdcf64b39cb915 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html @@ -74,32 +74,53 @@ <!-- Explore in other template --> - <ng-container *ngIf="regionInOtherTemplates$ | async as regionInOtherTemplates"> - + <ng-template [ngIf]="region$ | async | regionInOtherTmpl" let-otherTmpls> <ng-template #exploreInOtherTmpl> - <mat-card *ngFor="let sameRegion of regionInOtherTemplates" - class="p-0 border-0 box-shadow-none mt-1 tb-1 cursor-pointer" - (click)="changeView(sameRegion)" - [matTooltip]="sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '')" - mat-ripple> - <small> - {{ sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '') }} - </small> - </mat-card> + <mat-grid-list cols="3" + rowHeight="2:3" + gutterSize="16" + class="position-relative"> + <mat-grid-tile *ngFor="let otherTmpl of otherTmpls"> + + <div [hidden] + iav-dataset-show-dataset-dialog + [iav-dataset-show-dataset-dialog-name]="otherTmpl.originDatainfos[0]?.name" + [iav-dataset-show-dataset-dialog-description]="otherTmpl.originDatainfos[0]?.description" + [iav-dataset-show-dataset-dialog-urls]="otherTmpl.originDatainfos[0]?.urls" + [iav-dataset-show-dataset-dialog-ignore-overwrite]="true" + #kgInfo="iavDatasetShowDatasetDialog"> + </div> + <tile-cmp [tile-image-src]="otherTmpl | getTemplatePreviewUrl" + class="cursor-pointer pe-all" + tile-image-alt="Preview of this tile" + [tile-text]="otherTmpl.displayName || otherTmpl.name" + [tile-show-info]="otherTmpl.originDatainfos?.length > 0" + [tile-image-darktheme]="otherTmpl | templateIsDarkTheme" + [tile-selected]="(selectedTemplate$ | async | getProperty : '@id') === otherTmpl['@id']" + (tile-on-click)="(tileCmp.selected || changeView(otherTmpl)); (tileCmp.selected || intentToChgTmpl$.next(true))" + (tile-on-info-click)="kgInfo && kgInfo.onClick()" + #tileCmp="tileCmp"> + </tile-cmp> + </mat-grid-tile> + </mat-grid-list> + + <div *ngIf="lockOtherTmpl$ | async" class="loading-overlay"> + <spinner-cmp class="spinner"></spinner-cmp> + </div> + </ng-template> <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { title: 'Explore in other templates', - desc: regionInOtherTemplates.length, + desc: otherTmpls.length, iconClass: 'fas fa-brain', - iconTooltip: regionInOtherTemplates.length | regionAccordionTooltipTextPipe : 'regionInOtherTmpl', - iavNgIf: regionInOtherTemplates.length, + iconTooltip: otherTmpls.length | regionAccordionTooltipTextPipe : 'regionInOtherTmpl', + iavNgIf: otherTmpls.length > 0, content: exploreInOtherTmpl }"> - - </ng-container> - </ng-container> + </ng-template> + <!-- kg regional features list --> <ng-template #kgRegionalFeatureList> diff --git a/src/atlasComponents/parcellationRegion/type.ts b/src/atlasComponents/parcellationRegion/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5c40a75fc1c63e88c92e4e388c56603feef1625 --- /dev/null +++ b/src/atlasComponents/parcellationRegion/type.ts @@ -0,0 +1,24 @@ +import { IHasId } from "src/util/interfaces"; +import { TRegionSummary } from "src/util/siibraApiConstants/types"; + +type TAny = { + [key: string]: any +} + +export type TSiibraExTemplate = IHasId & TAny +export type TSiibraExParcelation = IHasId & TAny + +export type TSiibraExAtlas = { + name: string + '@id': string + parcellations: TSiibraExParcelation[] + templateSpaces: TSiibraExTemplate[] +} + +export type TSiibraExRegion = TRegionSummary & { + context: { + atlas: TSiibraExAtlas + template: TSiibraExTemplate + parcellation: TSiibraExParcelation + } +} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.component.ts index 4b911265407de5f74d5615cfe2eae2e7a14b6b6b..ca0d7c57c1c17a4fe011b05d05bd5d1cea13f6f5 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.component.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/genericInfo/genericInfoCmp/genericInfo.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Inject, Input, OnChanges, OnDestroy, Optional, TemplateRef, ViewChild, ViewContainerRef, ViewRef } from "@angular/core"; +import { AfterViewInit, ChangeDetectorRef, Component, Inject, Input, OnChanges, OnDestroy, Optional, TemplateRef, ViewChild, ViewContainerRef, ViewRef } from "@angular/core"; import { BsRegionInputBase } from "../../bsRegionInputBase"; import { KG_REGIONAL_FEATURE_KEY, TBSDetail, UNDER_REVIEW } from "../../kgRegionalFeature/type"; import { ARIA_LABELS, CONST } from 'common/constants' @@ -69,6 +69,7 @@ export class GenericInfoCmp extends BsRegionInputBase implements OnChanges, Afte constructor( svc: BsFeatureService, + private cdr: ChangeDetectorRef, @Optional() @Inject(MAT_DIALOG_DATA) data: TInjectableData ){ super(svc) @@ -76,7 +77,7 @@ export class GenericInfoCmp extends BsRegionInputBase implements OnChanges, Afte const { dataType, description, name, urls, useClassicUi, view, region, summary, isGdprProtected } = data this.description = description this.name = name - this.urls = urls + this.urls = urls || [] this.doiUrls = this.urls.filter(d => !!d.doi) this.useClassicUi = useClassicUi if (dataType) this.dataType = dataType @@ -130,6 +131,7 @@ export class GenericInfoCmp extends BsRegionInputBase implements OnChanges, Afte }, () => { this.loadingFlag = false + this.cdr.markForCheck() } ) } diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.component.ts index 338ce1fc1ec372dba80a5a68a7e862b3febae770..1694e857d446519312a46d43254edd66323627ee 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.component.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/ieeg/ieegCmp/ieeg.component.ts @@ -35,7 +35,6 @@ export class BsFeatureIEEGCmp extends BsRegionInputBase implements OnDestroy{ this.subs.push( this.results$.subscribe(results => { this.results = results - console.log(results) this.loadLandmarks() }) ) diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts index 5aeb3c1eb2310b954ae6346ca97fe78d67afeb85..d4b02e120d85776b02b0db606da5aa059cd46113 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts @@ -39,6 +39,9 @@ export class ShowDatasetDialogDirective{ doi: string }[] = [] + @Input('iav-dataset-show-dataset-dialog-ignore-overwrite') + ignoreOverwrite = false + @Input('iav-dataset-show-dataset-dialog-contexted-region') region: TSiibraRegion & TContextRegion @@ -67,7 +70,7 @@ export class ShowDatasetDialogDirective{ return this.snackbar.open(`Cannot show dataset. Neither fullId nor kgId provided.`) } - if (this.overwriteFn) { + if (!this.ignoreOverwrite && this.overwriteFn) { return this.overwriteFn(data) } diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts index 8acaa8e680f108574e319b3eb5d032b69f70de24..33bc2bbde0ce20039d5bcf33de72782da242cc21 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts @@ -1,12 +1,11 @@ -import { Component, Inject, OnDestroy, Optional } from "@angular/core"; -import { Observable, of, Subject, Subscription } from "rxjs"; +import { ChangeDetectorRef, Component, Inject, OnDestroy, Optional } from "@angular/core"; +import { BehaviorSubject, Observable, of, Subscription } from "rxjs"; import { filter, map, shareReplay, startWith, switchMap, tap } from "rxjs/operators"; import { BsRegionInputBase } from "../../bsRegionInputBase"; import { REGISTERED_FEATURE_INJECT_DATA } from "../../constants"; import { BsFeatureService, TFeatureCmpInput } from "../../service"; import { TBSDetail } from "../type"; import { ARIA_LABELS } from 'common/constants' -import { isPr } from "../profile/profile.component"; @Component({ selector: 'bs-features-receptor-entry', @@ -21,7 +20,7 @@ export class BsFeatureReceptorEntry extends BsRegionInputBase implements OnDestr private sub: Subscription[] = [] public ARIA_LABELS = ARIA_LABELS - private selectedREntryId$ = new Subject<string>() + private selectedREntryId$ = new BehaviorSubject<string>(null) private _selectedREntryId: string set selectedREntryId(id: string){ this.selectedREntryId$.next(id) @@ -50,13 +49,6 @@ export class BsFeatureReceptorEntry extends BsRegionInputBase implements OnDestr public receptorsSummary$ = this.region$.pipe( filter(v => !!v), switchMap(() => this.getFeatureInstancesList('ReceptorDistribution')), - tap(arr => { - if (arr && arr.length > 0) { - this.selectedREntryId = arr[0]['@id'] - } else { - this.selectedREntryId = null - } - }), startWith([]), shareReplay(1), ) @@ -75,11 +67,21 @@ export class BsFeatureReceptorEntry extends BsRegionInputBase implements OnDestr constructor( svc: BsFeatureService, + cdr: ChangeDetectorRef, @Optional() @Inject(REGISTERED_FEATURE_INJECT_DATA) data: TFeatureCmpInput ){ super(svc, data) this.sub.push( - this.selectedReceptor$.subscribe() + this.selectedReceptor$.subscribe(() => { + cdr.markForCheck() + }), + this.receptorsSummary$.subscribe(arr => { + if (arr && arr.length > 0) { + this.selectedREntryId = arr[0]['@id'] + } else { + this.selectedREntryId = null + } + }) ) } } diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.template.html index 813c489a6f2e2e054ed1019ebc36d5dc6ef2388c..48a24c834f0ef7e943434a6242c12fe49b0f5b91 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.template.html +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.template.html @@ -8,8 +8,11 @@ </mat-option> </mat-select> -<ng-container *ngIf="selectedReceptor$ | async as selectedRec"> +<ng-template [ngIf]="!(selectedReceptor$ | async)"> + <spinner-cmp></spinner-cmp> +</ng-template> +<ng-template let-selectedRec [ngIf]="selectedReceptor$ | async"> <bs-features-receptor-fingerprint (onSelectReceptor)="onSelectReceptor($event)" [bsFeature]="selectedRec"> @@ -44,4 +47,4 @@ </bs-features-receptor-autoradiograph> </ng-template> -</ng-container> +</ng-template> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.component.ts index 72d4595fade4fec861614188df507ba3d5a42b56..76a2ad1f2f9bdffbbb598992d542989a48127279 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.component.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.component.ts @@ -2,7 +2,7 @@ import { Component, ComponentFactory, ComponentFactoryResolver, Inject, Injector import { IBSSummaryResponse, TContextedFeature, TRegion } from "../type"; import { BsFeatureService, TFeatureCmpInput } from "../service"; import { combineLatest, Observable, Subject } from "rxjs"; -import { debounceTime, map, shareReplay, startWith, tap } from "rxjs/operators"; +import { debounceTime, map, shareReplay, startWith } from "rxjs/operators"; import { REGISTERED_FEATURE_INJECT_DATA } from "../constants"; import { ARIA_LABELS } from 'common/constants' import { diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.template.html index adecf93c84ab9c417168b4588e1aef63e4bb55e0..020145c3c36c406a74c3575422068d0b43700892 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.template.html +++ b/src/atlasComponents/regionalFeatures/bsFeatures/regionalFeatureWrapper/regionalFeatureWrapper.template.html @@ -33,9 +33,17 @@ [attr.aria-label]="ARIA_LABELS.LIST_OF_DATASETS_ARIA_LABEL"> <!-- if busy, show spinner --> - <ng-template [ngIf]="busy$ | async"> + <ng-template [ngIf]="busy$ | async" [ngIfElse]="notBusyTmpl"> <ng-template [ngTemplateOutlet]="busyTmpl"></ng-template> </ng-template> + + <ng-template #notBusyTmpl> + <ng-template [ngIf]="registeredFeatures.length === 0"> + <span class="text-muted"> + No regional features found. + </span> + </ng-template> + </ng-template> <div *ngFor="let feature of registeredFeatures; let index = index" class="overflow-hidden"> diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts b/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts index 3d4ed603f7586a8395df2c4daf06949f4b68802c..3df8c71c099444e4be81158e3093c8c9e6d3dcde 100644 --- a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts +++ b/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, Pipe, PipeTransform, ViewChild } from "@angular/core"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Pipe, PipeTransform, ViewChild } from "@angular/core"; import { MatSnackBar } from "@angular/material/snack-bar"; import { select, Store } from "@ngrx/store"; import { Observable, Subject, Subscription } from "rxjs"; @@ -13,6 +13,7 @@ import { CONST } from 'common/constants' styleUrls : [ `./splashScreen.style.css`, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SplashScreen { @@ -33,10 +34,14 @@ export class SplashScreen { constructor( private store: Store<any>, private snack: MatSnackBar, - private pureConstantService: PureContantService + private pureConstantService: PureContantService, + private cdr: ChangeDetectorRef, ) { this.subscriptions.push( - this.pureConstantService.allFetchingReady$.subscribe(flag => this.finishedLoading = flag) + this.pureConstantService.allFetchingReady$.subscribe(flag => { + this.finishedLoading = flag + this.cdr.markForCheck() + }) ) this.loadedAtlases$ = this.store.pipe( diff --git a/src/atlasComponents/template/getTemplatePreviewUrl.pipe.ts b/src/atlasComponents/template/getTemplatePreviewUrl.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2071c8af1b686baee241c47608695b7a709667d --- /dev/null +++ b/src/atlasComponents/template/getTemplatePreviewUrl.pipe.ts @@ -0,0 +1,30 @@ +import { Pipe, PipeTransform } from "@angular/core" + +const previewImgMap = new Map([ + ['minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588', 'bigbrain.png'], + ['minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2', 'icbm2009c.png'], + ['minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992', 'colin27.png'], + + ['minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9', 'allen-mouse.png'], + + ['minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8', 'waxholm.png'], + + ['minds/core/referencespace/v1.0.0/tmp-fsaverage', 'freesurfer.png'], + ['minds/core/referencespace/v1.0.0/tmp-fsaverage6', 'freesurfer.png'], + + ['minds/core/referencespace/v1.0.0/tmp-hcp32k', 'freesurfer.png'], + ['minds/core/referencespace/v1.0.0/MEBRAINS_T1.masked', 'primate.png'], + +]) + +@Pipe({ + name: 'getTemplatePreviewUrl', + pure: true +}) + +export class GetTemplatePreviewUrlPipe implements PipeTransform{ + public transform(tile: any){ + const filename = previewImgMap.get(tile['@id']) + return filename && `assets/images/atlas-selection/${filename}` + } +} diff --git a/src/atlasComponents/template/index.ts b/src/atlasComponents/template/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..38cd6578cc518656abb1303d11557b86529cc996 --- /dev/null +++ b/src/atlasComponents/template/index.ts @@ -0,0 +1,2 @@ +export { GetTemplatePreviewUrlPipe } from "./getTemplatePreviewUrl.pipe"; +export { SiibraExplorerTemplateModule } from './module' diff --git a/src/atlasComponents/template/module.ts b/src/atlasComponents/template/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f1a49646b274dc7936b24800f99a4bf12e152c7 --- /dev/null +++ b/src/atlasComponents/template/module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { GetTemplatePreviewUrlPipe } from "./getTemplatePreviewUrl.pipe"; +import { TemplateIsDarkThemePipe } from "./templateIsDarkTheme.pipe"; + +@NgModule({ + imports: [], + declarations: [ + GetTemplatePreviewUrlPipe, + TemplateIsDarkThemePipe, + ], + exports: [ + GetTemplatePreviewUrlPipe, + TemplateIsDarkThemePipe, + ] +}) + +export class SiibraExplorerTemplateModule{} \ No newline at end of file diff --git a/src/atlasComponents/template/templateIsDarkTheme.pipe.ts b/src/atlasComponents/template/templateIsDarkTheme.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..8454fd96e81e9815f8ab0bb627eb1e01d18b95b0 --- /dev/null +++ b/src/atlasComponents/template/templateIsDarkTheme.pipe.ts @@ -0,0 +1,33 @@ +import { OnDestroy, Pipe, PipeTransform } from "@angular/core"; +import { select, Store } from "@ngrx/store"; +import { Subscription } from "rxjs"; +import { viewerStateSelectedTemplateFullInfoSelector } from "src/services/state/viewerState/selectors"; +import { IHasId } from "src/util/interfaces"; + +@Pipe({ + name: 'templateIsDarkTheme', + pure: true, +}) + +export class TemplateIsDarkThemePipe implements OnDestroy, PipeTransform{ + + private templateFullInfo: any[] = [] + constructor(store: Store<any>){ + this.sub.push( + store.pipe( + select(viewerStateSelectedTemplateFullInfoSelector) + ).subscribe(val => this.templateFullInfo = val) + ) + } + + private sub: Subscription[] = [] + + ngOnDestroy(){ + while(this.sub.length) this.sub.pop().unsubscribe() + } + + public transform(template: IHasId): boolean{ + const found = this.templateFullInfo.find(t => t['@id'] === template["@id"]) + return found && found.darktheme + } +} \ No newline at end of file diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts index 84c42160985d551a495cf6a2713aa25738578b36..1752a7d5f686fbd441993f3a33898f9b194b0744 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts @@ -1,13 +1,15 @@ -import { Component, OnInit, ViewChildren, QueryList, HostBinding, ViewChild, TemplateRef, ElementRef, Pipe, PipeTransform } from "@angular/core"; +import { Component, OnInit, ViewChildren, QueryList, HostBinding, ViewChild, ElementRef, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { distinctUntilChanged, map, withLatestFrom, shareReplay, groupBy, mergeMap, toArray, switchMap, scan, filter, tap } from "rxjs/operators"; -import { Observable, Subscription, from, zip, of, combineLatest } from "rxjs"; +import { distinctUntilChanged, map, withLatestFrom, shareReplay, mapTo } from "rxjs/operators"; +import { merge, Observable, Subject, Subscription } from "rxjs"; import { viewerStateSelectTemplateWithId, viewerStateToggleLayer } from "src/services/state/viewerState.store.helper"; import { MatMenuTrigger } from "@angular/material/menu"; import { viewerStateGetSelectedAtlas, viewerStateAtlasLatestParcellationSelector, viewerStateSelectedTemplateFullInfoSelector, viewerStateSelectedTemplatePureSelector, viewerStateSelectedParcellationSelector } from "src/services/state/viewerState/selectors"; -import { ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' +import { ARIA_LABELS, CONST, QUICKTOUR_DESC } from 'common/constants' import { IQuickTourData } from "src/ui/quickTour/constrants"; import { animate, state, style, transition, trigger } from "@angular/animations"; +import { IHasId, OVERWRITE_SHOW_DATASET_DIALOG_TOKEN } from "src/util/interfaces"; +import { CurrentTmplSupportsParcPipe } from "../pipes/currTmplSupportsParc.pipe"; @Component({ selector: 'atlas-layer-selector', @@ -41,275 +43,124 @@ import { animate, state, style, transition, trigger } from "@angular/animations" } ] }) -export class AtlasLayerSelector implements OnInit { - - public TOGGLE_ATLAS_LAYER_SELECTOR = ARIA_LABELS.TOGGLE_ATLAS_LAYER_SELECTOR - - @ViewChildren(MatMenuTrigger) matMenuTriggers: QueryList<MatMenuTrigger> - public atlas: any - - @ViewChild('selectorPanelTmpl', { read: ElementRef }) - selectorPanelTemplateRef: ElementRef - - public selectedAtlas$: Observable<any> = this.store$.pipe( - select(viewerStateGetSelectedAtlas), - distinctUntilChanged(), - shareReplay(1) - ) - private layersGroupBy$ = this.selectedAtlas$.pipe( - switchMap(selectedAtlas => from((selectedAtlas?.parcellations) || []).pipe( - /** - * do not show base layers - */ - filter(p => !(p as any).baseLayer), - groupBy((parcellation: any) => parcellation.groupName, p => p), - mergeMap(group => zip( - of(group.key), - group.pipe(toArray())) - ), - scan((acc, curr) => acc.concat([ curr ]), []), - shareReplay(1), - )) - ) - - - private atlasLayersLatest$ = this.store$.pipe( - select(viewerStateAtlasLatestParcellationSelector), - shareReplay(1), +export class AtlasLayerSelector implements OnInit, OnDestroy { + + public ARIA_LABELS = ARIA_LABELS + public CONST = CONST + + @ViewChildren(MatMenuTrigger) + matMenuTriggers: QueryList<MatMenuTrigger> + + @ViewChild('selectorPanelTmpl', { read: ElementRef }) + selectorPanelTemplateRef: ElementRef + + public selectedAtlas$: Observable<any> = this.store$.pipe( + select(viewerStateGetSelectedAtlas), + distinctUntilChanged(), + shareReplay(1) + ) + + public atlasLayersLatest$ = this.store$.pipe( + select(viewerStateAtlasLatestParcellationSelector), + shareReplay(1), + ) + + public availableTemplates$ = this.store$.pipe<any[]>( + select(viewerStateSelectedTemplateFullInfoSelector), + ) + + private selectedTemplate: any + public selectedTemplate$ = this.store$.pipe( + select(viewerStateSelectedTemplatePureSelector), + withLatestFrom(this.availableTemplates$), + map(([selectedTmpl, fullInfoTemplates]) => { + return fullInfoTemplates.find(t => t['@id'] === selectedTmpl['@id']) + }) + ) + private showOverlayIntent$ = new Subject() + public showLoadingOverlay$ = merge( + this.showOverlayIntent$.pipe( + mapTo(true) + ), + this.selectedTemplate$.pipe( + mapTo(false) ) + ).pipe( + distinctUntilChanged(), + ) + + public selectedParcellation$ = this.store$.pipe( + select(viewerStateSelectedParcellationSelector), + ) + + private subscriptions: Subscription[] = [] + + @HostBinding('attr.data-opened') + public selectorExpanded: boolean = false + + public quickTourData: IQuickTourData = { + order: 4, + description: QUICKTOUR_DESC.LAYER_SELECTOR, + } - public nonGroupedLayers$: Observable<any[]> = this.atlasLayersLatest$.pipe( - map(allParcellations => - allParcellations - .filter(p => !p['groupName']) - .filter(p => !p['baseLayer']) - ), - ) + constructor(private store$: Store<any>) {} - public groupedLayers$: Observable<any[]> = combineLatest([ - this.atlasLayersLatest$.pipe( - map(allParcellations => - allParcellations.filter(p => !p['baseLayer']) - ), - ), - this.layersGroupBy$ - ]).pipe( - map(([ allParcellations, arr]) => arr - .filter(([ key ]) => !!key ) - .map(([key, parcellations]) => ({ - name: key, - previewUrl: parcellations[0].previewUrl, - parcellations: parcellations.map(p => { - const fullInfo = allParcellations.find(fullP => fullP['@id'] === p['@id']) || {} - return { - ...fullInfo, - ...p, - darktheme: (fullInfo || {}).useTheme === 'dark' - } - }) - })) - ), + ngOnInit(): void { + this.subscriptions.push( + this.selectedTemplate$.subscribe(st => { + this.selectedTemplate = st + }), ) - public selectedTemplateSpaceId: string - public selectedLayers = [] - - public selectedTemplate$: Observable<any> - private selectedParcellation$: Observable<any> - - private subscriptions: Subscription[] = [] - - @HostBinding('attr.data-opened') - public selectorExpanded: boolean = false - public selectedTemplatePreviewUrl: string = '' - - public quickTourData: IQuickTourData = { - order: 4, - description: QUICKTOUR_DESC.LAYER_SELECTOR, - } + } - public availableTemplates$ = this.store$.pipe<any[]>( - select(viewerStateSelectedTemplateFullInfoSelector), - ) + ngOnDestroy() { + while(this.subscriptions.length) this.subscriptions.pop().unsubscribe() + } - public containerMaxWidth: number - public shouldShowRenderPlaceHolder$ = combineLatest([ - this.availableTemplates$, - this.groupedLayers$, - this.nonGroupedLayers$, - ]).pipe( - map(([ availTmpl, grpL, ungrpL ]) => { - return availTmpl?.length > 0 || (grpL?.length || 0) + (ungrpL?.length || 0) > 0 - }) - ) - - constructor(private store$: Store<any>) { + toggleSelector() { + this.selectorExpanded = !this.selectorExpanded + } - this.selectedTemplate$ = this.store$.pipe( - select(viewerStateSelectedTemplatePureSelector), - withLatestFrom(this.selectedAtlas$), - map(([templateSelected, templateFromAtlas]) => { - return { - ...templateFromAtlas, - ...templateSelected - } - }) - ) - this.selectedParcellation$ = this.store$.pipe( - select(viewerStateSelectedParcellationSelector) - ) + selectTemplatewithId(templateId: string) { + this.showOverlayIntent$.next(true) + this.store$.dispatch(viewerStateSelectTemplateWithId({ + payload: { + '@id': templateId + } + })) + } - } + private currTmplSupportParcPipe = new CurrentTmplSupportsParcPipe() - ngOnInit(): void { - this.subscriptions.push( - this.selectedTemplate$.subscribe(st => { - this.selectedTemplatePreviewUrl = st.templateSpaces?.find(t => t['@id'] === st['@id']).previewUrl - this.selectedTemplateSpaceId = st['@id'] - }), - ) - this.subscriptions.push( - this.selectedParcellation$.subscribe(ps => { - this.selectedLayers = (this.atlas && [this.atlas.parcellations.find(l => l['@id'] === ps['@id'])['@id']]) || [] - }) - ) - this.subscriptions.push( - this.selectedAtlas$.subscribe(sa => { - this.atlas = sa - }) + selectParcellationWithName(layer: any) { + const tmplChangeReq = this.currTmplSupportParcPipe.transform(this.selectedTemplate, layer) + if (!tmplChangeReq) { + this.store$.dispatch( + viewerStateToggleLayer({ payload: layer }) ) - } - - toggleSelector() { - this.selectorExpanded = !this.selectorExpanded - } - - selectTemplateWithName(template) { + } else { this.store$.dispatch( - viewerStateSelectTemplateWithId({ payload: template }) + viewerStateSelectTemplateWithId({ + payload: layer.availableIn[0], + config: { + selectParcellation: layer + } + }) ) } + } - selectParcellationWithName(layer) { - const templateChangeRequired = !this.currentTemplateIncludesLayer(layer) - if (!templateChangeRequired) { - this.store$.dispatch( - viewerStateToggleLayer({ payload: layer }) - ) - } else { - this.store$.dispatch( - viewerStateSelectTemplateWithId({ payload: layer.availableIn[0], config: { selectParcellation: layer } }) - ) - } - } - - currentTemplateIncludesLayer(layer) { - return layer && layer.availableIn.map(a => a['@id']).includes(this.selectedTemplateSpaceId) - } - - templateIncludesGroup(group) { - return group.parcellations.some(v => v.availableIn.map(t => t['@id']).includes(this.selectedTemplateSpaceId)) - } - - selectedOneOfTheLayers(layers) { - const includes = layers.map(l=>l['@id']).some(id=> this.selectedLayers.includes(id)) - return includes - } - - selectedLayersIncludes(id) { - return this.selectedLayers.includes(id) - } - - collapseExpandedGroup(){ - this.matMenuTriggers.forEach(t => t.menuOpen && t.closeMenu()) - } - - getTileTmplClickFnAsCtx(fn: (...arg) => void, param: any) { - return () => fn.call(this, param) - } - - getTooltipText(layer) { - if (!this.atlas) return - if (this.atlas.templateSpaces.map(tmpl => tmpl['@id']).includes(layer['@id'])) return layer.name - if (layer.availableIn) { - if (this.currentTemplateIncludesLayer(layer)) return layer.name - else { - const firstAvailableRefSpaceId = layer && Array.isArray(layer.availableIn) && layer.availableIn.length > 0 && layer.availableIn[0]['@id'] - const firstAvailableRefSpace = firstAvailableRefSpaceId && this.atlas.templateSpaces.find(t => t['@id'] === firstAvailableRefSpaceId) - return `${layer.name} 🔄 ${(firstAvailableRefSpace && firstAvailableRefSpace.name) || ''}` - } - } - - if (layer.parcellations) { - if (this.templateIncludesGroup(layer)) return layer.name - else return `${layer.name} 🔄` - } - - return layer.name - } - - trackbyAtId(t){ - return t['@id'] - } - - trackbyName(t) { - return t['name'] - } -} - -import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN } from "src/util/interfaces"; - -const previewImgMap = new Map([ - ['minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588', 'bigbrain.png'], - ['minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2', 'icbm2009c.png'], - ['minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992', 'colin27.png'], - ['minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579', 'cytoarchitectonic-maps.png'], - ['juelich/iav/atlas/v1.0.0/3', 'cortical-layers.png'], - ['juelich/iav/atlas/v1.0.0/4', 'grey-white-matter.png'], - ['juelich/iav/atlas/v1.0.0/5', 'firbe-long.png'], - ['juelich/iav/atlas/v1.0.0/6', 'firbe-short.png'], - ['minds/core/parcellationatlas/v1.0.0/d80fbab2-ce7f-4901-a3a2-3c8ef8a3b721', 'difumo-64.png'], - ['minds/core/parcellationatlas/v1.0.0/73f41e04-b7ee-4301-a828-4b298ad05ab8', 'difumo-128.png'], - ['minds/core/parcellationatlas/v1.0.0/141d510f-0342-4f94-ace7-c97d5f160235', 'difumo-256.png'], - ['minds/core/parcellationatlas/v1.0.0/63b5794f-79a4-4464-8dc1-b32e170f3d16', 'difumo-512.png'], - ['minds/core/parcellationatlas/v1.0.0/12fca5c5-b02c-46ce-ab9f-f12babf4c7e1', 'difumo-1024.png'], - ['minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9', 'allen-mouse.png'], - ['minds/core/parcellationatlas/v1.0.0/05655b58-3b6f-49db-b285-64b5a0276f83', 'allen-mouse-2017.png'], - ['minds/core/parcellationatlas/v1.0.0/39a1384b-8413-4d27-af8d-22432225401f', 'allen-mouse-2015.png'], - ['minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8', 'waxholm.png'], - ['minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4', 'waxholm-v4.png'], - ['minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe', 'waxholm-v3.png'], - ['minds/core/parcellationatlas/v1.0.0/2449a7f0-6dd0-4b5a-8f1e-aec0db03679d', 'waxholm-v2.png'], - ['minds/core/parcellationatlas/v1.0.0/11017b35-7056-4593-baad-3934d211daba', 'waxholm-v1.png'], - ['juelich/iav/atlas/v1.0.0/79cbeaa4ee96d5d3dfe2876e9f74b3dc3d3ffb84304fb9b965b1776563a1069c', 'short-bundle-hcp.png'], - ['minds/core/referencespace/v1.0.0/tmp-fsaverage', 'freesurfer.png'], - ['minds/core/referencespace/v1.0.0/tmp-fsaverage6', 'freesurfer.png'], - ['minds/core/referencespace/v1.0.0/tmp-hcp32k', 'freesurfer.png'], - ['minds/core/referencespace/v1.0.0/MEBRAINS_T1.masked', 'primate.png'], - ['minds/core/parcellationatlas/v1.0.0/mebrains-tmp-id', 'primate-parc.png'], -]) + collapseExpandedGroup(){ + this.matMenuTriggers.forEach(t => t.menuOpen && t.closeMenu()) + } -/** - * used for directories - */ -const previewNameToPngMap = new Map([ - ['fibre architecture', 'firbe-long.png'], - ['functional modes', 'difumo-128.png'] -]) -@Pipe({ - name: 'getPreviewUrlPipe', - pure: true -}) + trackbyAtId(t: IHasId){ + return t['@id'] + } -export class GetPreviewUrlPipe implements PipeTransform{ - public transform(tile: any){ - const filename = tile['@id'] - ? previewImgMap.get(tile['@id']) - : previewNameToPngMap.get(tile['name']) - if (!filename) { - console.log(tile) - } - return filename && `assets/images//atlas-selection/${filename}` + trackKeyVal(obj: {key: string, value: any}) { + return obj.key } } diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.style.css b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.style.css index 56cfffcf79c4db7ee31690dbe90d44a4016ad97f..425a45a5e252f904956712930e5996740733d60f 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.style.css +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.style.css @@ -1,41 +1,3 @@ -.singleLayerImageContainer img { - flex-shrink: 0; - width: 70px; - height: 70px; - border-radius: 10px; - -webkit-border-radius: 10px; - -moz-border-radius: 10px; -} - - -.selectedTemplateDefaultContainer { - width: 100px; - height: 100px; - border-radius: 10px; -} -.selectedTemplateDefaultContainer img { - border-radius: 10px; - min-width: 100%; - min-height: 100%; - width: 100%; -} - -.selectedLayerBorder { - border: 2px solid #FED363; -} - -.folder-container -{ - margin-right:0.5rem; - margin-bottom:-0.5rem; -} - -.info-container -{ - margin-right:-0.5rem; - margin-top:-0.25rem; -} - .selector-container { overflow-y:scroll; @@ -45,15 +7,34 @@ z-index: 5; } -.single-column-tile +:host-context([darktheme="true"]) .loading-overlay +{ + background-color: rgba(10, 10, 10, 0.8); +} + +.loading-overlay { - width: calc((28rem - 32px)/3); + background-color: rgba(250, 250, 250, 0.8); } -.infoButton { - height: 18px; - width: 18px; - margin-top: 10px; - margin-right: 12px; - border-radius: 50%; +.loading-overlay +{ + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + font-size: 200%; + + display: grid; + grid-template-columns: auto; + grid-template-rows: 1fr auto 1fr; + grid-template-columns: 1fr auto 1fr; + grid-template-areas: "." "vertical-center" "."; +} + +.loading-overlay > .spinner +{ + grid-column: 2; + grid-row: 2; } diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html index e7b687d28f0262123213da9f38d0aaf77e36a2f6..ed78029cd9c644c84e4c809e9498ff21a5eece81 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html @@ -1,192 +1,169 @@ -<div class="position-relative d-flex flex-column justify-content-start"> - - <!-- selector panel when expanded --> - - <mat-card class="selector-container position-absolute" - [ngClass]="{'pe-all': selectorExpanded}" - [@toggleAtlasLayerSelector]="selectorExpanded" - (@toggleAtlasLayerSelector.done)="atlasSelectorTour?.attachTo(selectorExpanded ? selectorPanelTemplateRef : null)" - #selectorPanelTmpl> - <mat-card-content> - - <!-- templates --> - <mat-card-subtitle> - Templates - </mat-card-subtitle> - <mat-grid-list cols="3" rowHeight="3:4"> - <!-- template tiles --> - <mat-grid-tile *ngFor="let template of availableTemplates$ | async; trackBy: trackbyAtId" - [attr.aria-checked]="selectedTemplateSpaceId === template['@id']"> - <ng-container *ngTemplateOutlet="tileTmpl; context: { - tileSrc: template, - selected: selectedTemplateSpaceId === template['@id'], - onClick: getTileTmplClickFnAsCtx(selectTemplateWithName, template) - }"> - </ng-container> - </mat-grid-tile> - </mat-grid-list> - - <mat-divider></mat-divider> - - <!-- levels/maps/segregations --> - <mat-card-subtitle class="mt-2"> - Levels - </mat-card-subtitle> - - <mat-grid-list cols="3" rowHeight="3:4"> - - <!-- non grouped layers --> - <mat-grid-tile *ngFor="let layer of (nonGroupedLayers$ | async); trackBy: trackbyAtId" - [attr.aria-checked]="selectedLayersIncludes(layer['@id'])"> - <ng-container *ngTemplateOutlet="tileTmpl; context: { - tileSrc: layer, - selected: selectedLayersIncludes(layer['@id']), - onClick: getTileTmplClickFnAsCtx(selectParcellationWithName, layer), - disabled: !currentTemplateIncludesLayer(layer) - }"> - - </ng-container> - </mat-grid-tile> - - <!-- grouped layers --> - <mat-grid-tile *ngFor="let group of (groupedLayers$ | async); trackBy: trackbyName" - [attr.aria-checked]="selectedOneOfTheLayers(group.parcellations)"> - <ng-container *ngTemplateOutlet="tileTmpl; context: { - tileSrc: group, - selected: selectedOneOfTheLayers(group.parcellations), - disabled: !templateIncludesGroup(group), - menuTriggerFor: layerGroupMenu, - menuTriggerData: { layerGroupItems: group.parcellations }, - isDirectory: true - }"> - - </ng-container> - </mat-grid-tile> - </mat-grid-list> - </mat-card-content> - </mat-card> - - <!-- place holder when not expanded --> - <div class="position-relative m-2 cursor-pointer scale-up-bl pe-all" - quick-tour - [quick-tour-description]="quickTourData.description" - [quick-tour-order]="quickTourData.order" - #atlasSelectorTour="quickTour"> - <button color="primary" - matTooltip="Select layer" - mat-mini-fab - *ngIf="shouldShowRenderPlaceHolder$ | async" - [attr.aria-label]="TOGGLE_ATLAS_LAYER_SELECTOR" - (click)="toggleSelector()"> - <i class="fas fa-layer-group"></i> - </button> - </div> +<!-- selector panel when expanded --> + +<mat-card class="selector-container m-2 position-absolute" + [ngClass]="{'pe-all': selectorExpanded}" + [@toggleAtlasLayerSelector]="selectorExpanded" + (@toggleAtlasLayerSelector.done)="atlasSelectorTour?.attachTo(selectorExpanded ? selectorPanelTemplateRef : null)" + #selectorPanelTmpl> + <mat-card-content> + + <!-- templates --> + <mat-card-subtitle> + {{ CONST.ATLAS_SELECTOR_LABEL_SPACES }} + </mat-card-subtitle> + + <!-- template grid and tiles --> + <mat-grid-list cols="3" + rowHeight="2:3" + gutterSize="16"> + + <mat-grid-tile *ngFor="let template of availableTemplates$ | async; trackBy: trackbyAtId" + [attr.aria-checked]="(selectedTemplate$ | async | getProperty : '@id') === template['@id']"> + + <div [hidden] + iav-dataset-show-dataset-dialog + [iav-dataset-show-dataset-dialog-name]="template.originDatainfos[0]?.name" + [iav-dataset-show-dataset-dialog-description]="template.originDatainfos[0]?.description" + [iav-dataset-show-dataset-dialog-urls]="template.originDatainfos[0]?.urls" + #kgInfo="iavDatasetShowDatasetDialog"> + </div> + <tile-cmp [tile-image-src]="template | getPreviewUrlPipe" + class="cursor-pointer pe-all" + tile-image-alt="Preview of this tile" + [tile-text]="template.displayName || template.name" + [tile-show-info]="template.originDatainfos?.length > 0" + [tile-disabled]="!(selectedParcellation$ | async | currParcSupportsTmpl : template)" + [tile-image-darktheme]="template.darktheme" + [tile-selected]="(selectedTemplate$ | async | getProperty : '@id') === template['@id']" + (tile-on-click)="selectTemplatewithId(template['@id'])" + (tile-on-info-click)="kgInfo && kgInfo.onClick()"> + </tile-cmp> + </mat-grid-tile> + </mat-grid-list> -</div> + <mat-divider></mat-divider> -<!-- image tile tmpl --> -<ng-template #tileTmpl - let-tileSrc="tileSrc" - let-selected="selected" - let-onClick="onClick" - let-disabled="disabled" - let-isDirectory="isDirectory" - let-menuTriggerFor="menuTriggerFor" - let-menuTriggerData="menuTriggerData"> - <div *ngIf="menuTriggerFor; else noMatMenuTriggerTmpl" - iav-stop="click" - class="d-flex flex-column justify-content-start w-100 h-100 mb-1 mt-1 overflow-hidden cursor-pointer singleLayerImageContainer" - [matMenuTriggerFor]="menuTriggerFor" - [matMenuTriggerData]="menuTriggerData"> - - <ng-container *ngTemplateOutlet="tileContent"> - </ng-container> - </div> + <!-- parcellations --> + <mat-card-subtitle class="mt-2"> + {{ CONST.ATLAS_SELECTOR_LABEL_PARC_MAPS }} + </mat-card-subtitle> - <ng-template #noMatMenuTriggerTmpl> - <div class="d-flex flex-column justify-content-start w-100 h-100 mb-1 mt-1 overflow-hidden cursor-pointer singleLayerImageContainer"> - <ng-container *ngTemplateOutlet="tileContent"> - </ng-container> - </div> - </ng-template> + <mat-grid-list cols="3" + rowHeight="2:3" + gutterSize="16"> + + <!-- non grouped layers --> + <mat-grid-tile *ngFor="let layer of (atlasLayersLatest$ | async | getNonbaseParc | getIndividualParc); trackBy: trackbyAtId" + [attr.aria-checked]="selectedParcellation$ | async | groupParcSelected : layer"> - <ng-template #tileContent> - <div class="d-flex flex-column justify-content-start w-100 mb-1 mt-1 overflow-hidden cursor-pointer singleLayerImageContainer" - [matTooltip]="getTooltipText(tileSrc)" - matTooltipPosition="above" - (click)="onClick && onClick()" - [ngStyle]="{opacity: disabled ? '0.2': '1' }"> - - <div class="position-relative d-flex flex-column align-items-center"> - <div class="position-relative"> - <!-- info icon btn --> - <div class="position-absolute top-0 right-0 info-container" - [ngClass]="{ 'darktheme': tileSrc.darktheme, 'lighttheme': !tileSrc.darktheme }"> - <ng-container *ngTemplateOutlet="infoBtn; context: { tileSrc: tileSrc}"> - - </ng-container> - </div> - - <!-- preview image --> - <img [src]="tileSrc | getPreviewUrlPipe" - alt="Preview of this tile" - class="layer-image align-self-center" - [ngClass]="{ 'selectedLayerBorder': selected }" - draggable="false"> - - <!-- if is directory, show directory icon --> - <div *ngIf="isDirectory" class="position-absolute bottom-0 right-0"> - <i class="fas fa-folder folder-container fa-2x"></i> - </div> + <div [hidden] + iav-dataset-show-dataset-dialog + [iav-dataset-show-dataset-dialog-name]="layer.originDatainfos[0]?.name" + [iav-dataset-show-dataset-dialog-description]="layer.originDatainfos[0]?.description" + [iav-dataset-show-dataset-dialog-urls]="layer.originDatainfos[0]?.urls" + #kgInfo="iavDatasetShowDatasetDialog"> </div> - </div> + <tile-cmp [tile-image-src]="layer | getPreviewUrlPipe" + class="cursor-pointer pe-all" + tile-image-alt="Preview of this tile" + [tile-text]="layer.displayName || layer.name" + [tile-show-info]="layer.originDatainfos?.length > 0" + [tile-disabled]="!(selectedTemplate$ | async | currentTemplateSupportsParcellation : layer)" + [tile-image-darktheme]="layer.darktheme" - <!-- text container --> - <div class="d-flex justify-content-center"> - <small class="iv-custom-comp text ml-1 mr-1 mt-2 text-break text-center">{{ tileSrc.displayName || tileSrc.name }}</small> - </div> - </div> - </ng-template> -</ng-template> + [tile-selected]="selectedParcellation$ | async | groupParcSelected : layer" -<ng-template #infoBtn let-tileSrc="tileSrc"> - <ng-container *ngFor="let originDatainfo of tileSrc.originDatainfos"> - <div mat-icon-button - iav-stop="click" - class="iv-custom-comp d-flex justify-content-center align-items-center infoButton" - [ngStyle]="{backgroundColor: tileSrc.darktheme ? 'white': 'black', color: tileSrc.darktheme ? 'black': 'white' }" - iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-name]="originDatainfo.name" - [iav-dataset-show-dataset-dialog-description]="originDatainfo.description" - [iav-dataset-show-dataset-dialog-urls]="originDatainfo.urls"> - <small><i class="fas fa-info"></i></small> - </div> - </ng-container> - -</ng-template> + (tile-on-click)="selectParcellationWithName(layer)" + (tile-on-info-click)="kgInfo && kgInfo.onClick()"> + </tile-cmp> + + </mat-grid-tile> + + <!-- grouped layers --> + <mat-grid-tile *ngFor="let groupKeyVal of (atlasLayersLatest$ | async | getNonbaseParc | getGroupedParc | keyvalue); trackBy: trackKeyVal" + [attr.aria-checked]="false"> + + <!-- prevent click bubbling to document is necessary --> + <!-- or else, the outsideclick directive will fire immediately --> + <!-- resulting in immediate opening and closing of mat menu --> + <tile-cmp [tile-image-src]="groupKeyVal['value'][0] | getPreviewUrlPipe" + class="cursor-pointer pe-all" + tile-image-alt="Preview of this tile" + [tile-text]="groupKeyVal['key']" + [tile-show-info]="false" + [tile-disabled]="!(selectedTemplate$ | async | currentTemplateSupportsParcellation : groupKeyVal['value'])" + [tile-image-darktheme]="groupKeyVal['value'][0].darktheme" + [tile-is-dir]="true" + [matMenuTriggerFor]=layerGroupMenu + [matMenuTriggerData]="{ + layerGroupItems: groupKeyVal['value'] + }" + [tile-selected]="selectedParcellation$ | async | groupParcSelected : groupKeyVal['value']" + iav-stop="click"> + </tile-cmp> + </mat-grid-tile> + </mat-grid-list> + </mat-card-content> + + <div [hidden]="!(showLoadingOverlay$ | async)" + class="loading-overlay"> + <spinner-cmp class="spinner"></spinner-cmp> + </div> +</mat-card> + +<!-- place holder when not expanded --> +<div class="position-relative m-2 cursor-pointer scale-up-bl pe-all" + quick-tour + [quick-tour-description]="quickTourData.description" + [quick-tour-order]="quickTourData.order" + #atlasSelectorTour="quickTour"> + <!-- TODO check when do we disable atlas selector --> + <button color="primary" + *ngIf="true" + matTooltip="Select layer" + mat-mini-fab + [attr.aria-label]="ARIA_LABELS.TOGGLE_ATLAS_LAYER_SELECTOR" + (click)="toggleSelector()"> + <i class="fas fa-layer-group"></i> + </button> +</div> <!-- mat menu for grouped layer --> <mat-menu #layerGroupMenu="matMenu" - class="layerGroupMenu" hasBackdrop="false"> <ng-template matMenuContent let-layerGroupItems="layerGroupItems"> <mat-grid-list cols="1" - rowHeight="1:1" + rowHeight="6:7" + gutterSize="8" iav-stop="click" (iav-outsideClick)="collapseExpandedGroup()"> <mat-grid-tile *ngFor="let layer of layerGroupItems" - [attr.aria-checked]="selectedLayersIncludes(layer['@id'])"> + [attr.aria-checked]="selectedParcellation$ | async | groupParcSelected : layer"> + + <div [hidden] + iav-dataset-show-dataset-dialog + [iav-dataset-show-dataset-dialog-name]="layer.originDatainfos[0]?.name" + [iav-dataset-show-dataset-dialog-description]="layer.originDatainfos[0]?.description" + [iav-dataset-show-dataset-dialog-urls]="layer.originDatainfos[0]?.urls" + #kgInfo="iavDatasetShowDatasetDialog"> + </div> + + <tile-cmp [tile-image-src]="layer | getPreviewUrlPipe" + class="iv-custom-comp text m-3 cursor-pointer pe-all" + tile-image-alt="Preview of this tile" + [tile-text]="layer.displayName || layer.name" + [tile-show-info]="layer.originDatainfos?.length > 0" + [tile-disabled]="!(selectedTemplate$ | async | currentTemplateSupportsParcellation : layer)" + [tile-image-darktheme]="layer.darktheme" + + [tile-selected]="selectedParcellation$ | async | groupParcSelected : layer" - <ng-container *ngTemplateOutlet="tileTmpl; context: { - tileSrc: layer, - onClick: getTileTmplClickFnAsCtx(selectParcellationWithName, layer), - selected: selectedLayersIncludes(layer['@id']), - disabled: !currentTemplateIncludesLayer(layer) - } "> + (tile-on-click)="selectParcellationWithName(layer)" + (tile-on-info-click)="kgInfo && kgInfo.onClick()"> + </tile-cmp> - </ng-container> </mat-grid-tile> </mat-grid-list> diff --git a/src/atlasComponents/uiSelectors/module.ts b/src/atlasComponents/uiSelectors/module.ts index d655e9887d960671c3c02ceb798647d7691d67ef..384dc294ca684560a8d8b0cf7f71d85b7fb488f4 100644 --- a/src/atlasComponents/uiSelectors/module.ts +++ b/src/atlasComponents/uiSelectors/module.ts @@ -3,9 +3,20 @@ import { NgModule } from "@angular/core"; import { AngularMaterialModule } from "src/sharedModules"; import { UtilModule } from "src/util"; import { AtlasDropdownSelector } from "./atlasDropdown/atlasDropdown.component"; -import { AtlasLayerSelector, GetPreviewUrlPipe } from "./atlasLayerSelector/atlasLayerSelector.component"; +import { AtlasLayerSelector } from "./atlasLayerSelector/atlasLayerSelector.component"; import {QuickTourModule} from "src/ui/quickTour/module"; import { KgDatasetModule } from "../regionalFeatures/bsFeatures/kgDataset"; +import { AtlaslayerTooltipPipe } from "./pipes/atlasLayerTooltip.pipe"; +import { ComponentsModule } from "src/components"; +import { GetNonbaseParcPipe } from "./pipes/getNonBaseParc.pipe"; +import { GetIndividualParcPipe } from "./pipes/getIndividualParc.pipe"; +import { getGroupedParcPipe } from "./pipes/getGroupedParc.pipe"; +import { CurrentTmplSupportsParcPipe } from "./pipes/currTmplSupportsParc.pipe"; +import { GroupParcSelectedPipe } from "./pipes/groupParcSelected.pipe"; +import { GetPreviewUrlPipe } from "./pipes/getPreviewUrl.pipe"; +import { CurrParcSupportsTmplPipe } from "./pipes/currParcSupportsTmpl.pipe"; +import { AtlasCmpParcellationModule } from "../parcellation"; +import { SiibraExplorerTemplateModule } from "../template"; @NgModule({ imports: [ @@ -14,11 +25,21 @@ import { KgDatasetModule } from "../regionalFeatures/bsFeatures/kgDataset"; UtilModule, QuickTourModule, KgDatasetModule, + ComponentsModule, + AtlasCmpParcellationModule, + SiibraExplorerTemplateModule, ], declarations: [ AtlasDropdownSelector, AtlasLayerSelector, GetPreviewUrlPipe, + AtlaslayerTooltipPipe, + GetNonbaseParcPipe, + GetIndividualParcPipe, + getGroupedParcPipe, + CurrentTmplSupportsParcPipe, + GroupParcSelectedPipe, + CurrParcSupportsTmplPipe, ], exports: [ AtlasDropdownSelector, diff --git a/src/atlasComponents/uiSelectors/pipes/atlasLayerTooltip.pipe.ts b/src/atlasComponents/uiSelectors/pipes/atlasLayerTooltip.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..285f134543afa5151de4627fe2f507515e5f81de --- /dev/null +++ b/src/atlasComponents/uiSelectors/pipes/atlasLayerTooltip.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'atlasLayerTooltip', + pure: true +}) + +export class AtlaslayerTooltipPipe implements PipeTransform{ + public transform(layer: any){ + return layer.name + } +} diff --git a/src/atlasComponents/uiSelectors/pipes/currParcSupportsTmpl.pipe.ts b/src/atlasComponents/uiSelectors/pipes/currParcSupportsTmpl.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..77fd66327ee0a4fa5bd004d46474d9ff5a95fb0a --- /dev/null +++ b/src/atlasComponents/uiSelectors/pipes/currParcSupportsTmpl.pipe.ts @@ -0,0 +1,20 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'currParcSupportsTmpl', + pure: true +}) + +export class CurrParcSupportsTmplPipe implements PipeTransform{ + public transform(parc: any, tmpl: any){ + /** + * TODO + * buggy. says julich brain v290 is not supported in fsaverage + * related to https://github.com/FZJ-INM1-BDA/siibra-python/issues/98 + */ + const parcSupportTmpl = (p: any) => !!(tmpl.availableIn || []).find(tmplP => tmplP['@id'] === p['@id']) + return Array.isArray(parc) + ? parc.some(parcSupportTmpl) + : parcSupportTmpl(parc) + } +} diff --git a/src/atlasComponents/uiSelectors/pipes/currTmplSupportsParc.pipe.ts b/src/atlasComponents/uiSelectors/pipes/currTmplSupportsParc.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d1ed06bd92acea21148e64bc6815c5d4345aa73 --- /dev/null +++ b/src/atlasComponents/uiSelectors/pipes/currTmplSupportsParc.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'currentTemplateSupportsParcellation', + pure: true +}) + +export class CurrentTmplSupportsParcPipe implements PipeTransform{ + public transform(tmpl: any, parc: any): boolean { + const testParc = (p: any) => !!(p?.availableIn || []).find((availTmpl: any) => availTmpl['@id'] === tmpl['@id']) + return Array.isArray(parc) + ? parc.some(testParc) + : testParc(parc) + } +} diff --git a/src/atlasComponents/uiSelectors/pipes/getGroupedParc.pipe.ts b/src/atlasComponents/uiSelectors/pipes/getGroupedParc.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..683d9d7904e588d64d734118723b60f577f20b7b --- /dev/null +++ b/src/atlasComponents/uiSelectors/pipes/getGroupedParc.pipe.ts @@ -0,0 +1,22 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +type TReturn = { + [key: string]: any[] +} + +@Pipe({ + name: 'getGroupedParc', + pure: true +}) +export class getGroupedParcPipe implements PipeTransform{ + + public transform(arr: any[]):TReturn{ + const returnObj: TReturn = {} + const filteredArr = arr.filter(p => p['groupName']) + for (const obj of filteredArr) { + const groupName: string = obj['groupName'] + returnObj[groupName] = (returnObj[groupName] || []).concat(obj) + } + return returnObj + } +} diff --git a/src/atlasComponents/uiSelectors/pipes/getIndividualParc.pipe.ts b/src/atlasComponents/uiSelectors/pipes/getIndividualParc.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e7b155b258819a4490ed41083aa0f55f6051ee4 --- /dev/null +++ b/src/atlasComponents/uiSelectors/pipes/getIndividualParc.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'getIndividualParc', + pure: true +}) +export class GetIndividualParcPipe implements PipeTransform{ + + public transform(arr: any[]){ + return arr.filter(p => !p['groupName']) + } +} diff --git a/src/atlasComponents/uiSelectors/pipes/getNonBaseParc.pipe.ts b/src/atlasComponents/uiSelectors/pipes/getNonBaseParc.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0b7997bfa191ee6e790fc935a541e7c8d9a5bb1 --- /dev/null +++ b/src/atlasComponents/uiSelectors/pipes/getNonBaseParc.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'getNonbaseParc', + pure: true +}) +export class GetNonbaseParcPipe implements PipeTransform{ + + public transform(arr: any[]){ + return arr.filter(p => !p['baseLayer']) + } +} diff --git a/src/atlasComponents/uiSelectors/pipes/getPreviewUrl.pipe.ts b/src/atlasComponents/uiSelectors/pipes/getPreviewUrl.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..7826f44e267119a0cab9c848f0dd368280629b95 --- /dev/null +++ b/src/atlasComponents/uiSelectors/pipes/getPreviewUrl.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { GetParcPreviewUrlPipe } from "src/atlasComponents/parcellation"; +import { GetTemplatePreviewUrlPipe } from "src/atlasComponents/template"; + +const templateUrlPipe = new GetTemplatePreviewUrlPipe() +const parcUrlPipe = new GetParcPreviewUrlPipe() + +@Pipe({ + name: 'getPreviewUrlPipe', + pure: true +}) + +export class GetPreviewUrlPipe implements PipeTransform{ + public transform(tile: any){ + const filename = templateUrlPipe.transform(tile) || parcUrlPipe.transform(tile) + return filename + } +} diff --git a/src/atlasComponents/uiSelectors/pipes/groupParcSelected.pipe.ts b/src/atlasComponents/uiSelectors/pipes/groupParcSelected.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..6beb302be626272f72e62f23b722ce1ce7411015 --- /dev/null +++ b/src/atlasComponents/uiSelectors/pipes/groupParcSelected.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'groupParcSelected', + pure: true +}) + +export class GroupParcSelectedPipe implements PipeTransform{ + public transform(selectedParc: any, parc: any){ + const isSelected = (p: any) => p['@id'] === selectedParc['@id'] + return Array.isArray(parc) + ? parc.some(isSelected) + : isSelected(parc) + } +} diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts index bea53cdf42b49e494e9f3419813f4b9178dc3b99..3d01d57dc41678c9958d1c52b41e734fce28ae28 100644 --- a/src/atlasComponents/userAnnotations/tools/line.ts +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -12,7 +12,7 @@ import { TCallbackFunction, } from "./type"; import { Point, TPointJsonSpec } from './point' -import { Directive, Injectable, OnDestroy } from "@angular/core"; +import { Directive, OnDestroy } from "@angular/core"; import { Observable, Subject, Subscription } from "rxjs"; import { filter, switchMapTo, takeUntil } from "rxjs/operators"; import { getUuid } from "src/util/fn"; @@ -24,6 +24,7 @@ export type TLineJsonSpec = { export class Line extends IAnnotationGeometry{ public id: string + public annotationType = 'Line' public points: Point[] = [] diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts index 1db473a6713ed78b82c0d12c4a34acf65f13bb84..f69baf0e4045e8300de93c42a086b57ceee7292e 100644 --- a/src/atlasComponents/userAnnotations/tools/point.ts +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -16,6 +16,7 @@ export class Point extends IAnnotationGeometry { y: number z: number + public annotationType = 'Point' static threshold = 1e-6 static eql(p1: Point, p2: Point) { return Math.abs(p1.x - p2.x) < Point.threshold diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index b7929b5dfe77e378ae6a52c2f5c67d5333547025..47dd27c7dc56924a1be4911045523577e7e9d1ad 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -13,6 +13,7 @@ export type TPolyJsonSpec = { export class Polygon extends IAnnotationGeometry{ public id: string + public annotationType = 'Polygon' public points: Point[] = [] public edges: [number, number][] = [] diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 196d77ec0563fa57183f10fb2c7d865e462f6d00..dccdc60427a025b8c0e9fc974e03bc6e23f959ee 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -3,7 +3,7 @@ import { ARIA_LABELS } from 'common/constants' import { Inject, Optional } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { BehaviorSubject, combineLatest, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs"; -import { map, switchMap, filter, shareReplay, pairwise } from "rxjs/operators"; +import {map, switchMap, filter, shareReplay, pairwise } from "rxjs/operators"; import { viewerStateSelectedTemplatePureSelector, viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors"; import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; @@ -135,6 +135,18 @@ export class ModularUserAnnotationToolService implements OnDestroy{ map(mann => mann.length > 0 ? mann.length : null) ) + public hoveringAnnotations$ = this.annotnEvSubj.pipe( + filter<TAnnotationEvent<'hoverAnnotation'>>(ev => ev.type === 'hoverAnnotation'), + map(ev => { + if (!(ev?.detail)) return null + const { pickedAnnotationId } = ev.detail + const annId = (pickedAnnotationId || '').split('_')[0] + const foundAnn = this.managedAnnotations.find(ann => ann.id === annId) + if (!foundAnn) return null + return foundAnn + }) + ) + private registeredTools: { name: string iconClass: string @@ -467,6 +479,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ this.deleteNgAnnotationById(annotation.id) continue } + if (!this.ngAnnotationLayer) continue const localAnnotations = this.ngAnnotationLayer.layer.localAnnotations const annRef = localAnnotations.references.get(annotation.id) const annSpec = parseNgAnnotation(annotation) diff --git a/src/atlasComponents/userAnnotations/tools/type.spec.ts b/src/atlasComponents/userAnnotations/tools/type.spec.ts index f4b28738913465c7eda8ee898f28fdc724dbf016..599f20eabb8289dc975b501c876418df055e7d57 100644 --- a/src/atlasComponents/userAnnotations/tools/type.spec.ts +++ b/src/atlasComponents/userAnnotations/tools/type.spec.ts @@ -2,6 +2,7 @@ import { Subject, Subscription } from "rxjs" import { AbsToolClass, IAnnotationEvents, IAnnotationGeometry, TAnnotationEvent } from "./type" class TmpCls extends IAnnotationGeometry{ + annotationType: 'tmpl-cls' getNgAnnotationIds(){ return [] } @@ -110,6 +111,7 @@ describe('> types.ts', () => { describe('> updateSignal$', () => { class TmpCls extends IAnnotationGeometry{ + annotationType = 'tmp-cls' getNgAnnotationIds(){ return [] } diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts index 85a4c2e0f6783a024017a34fc0b50588a89ccfcd..8e33221b9bdf575fd3e2a196711236dce78b6d3b 100644 --- a/src/atlasComponents/userAnnotations/tools/type.ts +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -313,6 +313,7 @@ export abstract class IAnnotationGeometry extends Highlightable { public space: TBaseAnnotationGeomtrySpec['space'] + abstract annotationType: string abstract getNgAnnotationIds(): string[] abstract toNgAnnotation(): INgAnnotationTypes[keyof INgAnnotationTypes][] abstract toJSON(): TRecord diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index 9ee73c58689f765e94e54829a1b9138e43f38f86..ad6d2fb7e7057784edadb8b227f015f4e83f74ac 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -455,7 +455,7 @@ export interface IVIewerHandle { mouseEvent: Observable<{eventName: string, event: MouseEvent}> mouseOverNehuba: Observable<{labelIndex: number, foundRegion: any | null}> mouseOverNehubaLayers: Observable<Array<{layer: {name: string}, segment: any | number }>> - mouseOverNehubaUI: Observable<{ segments: any, landmark: any, customLandmark: any }> + mouseOverNehubaUI: Observable<{ annotation: any, segments: any, landmark: any, customLandmark: any }> getNgHash: () => string } diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index f866c771f21c42422f72bc7c32a9c9938146d7de..f407f68944dad36fb049901fcc59e9e91fc77f47 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -8,10 +8,11 @@ import { TemplateRef, ViewChild, ElementRef, + Inject, } from "@angular/core"; -import { Store, select, ActionsSubject } from "@ngrx/store"; -import { Observable, Subscription, interval, merge, of, timer, fromEvent } from "rxjs"; -import { map, filter, distinctUntilChanged, delay, withLatestFrom, switchMapTo, take, startWith } from "rxjs/operators"; +import { Store, select } from "@ngrx/store"; +import { Observable, Subscription, merge, timer, fromEvent } from "rxjs"; +import { map, filter, distinctUntilChanged, delay, switchMapTo, take, startWith } from "rxjs/operators"; import { IavRootStoreInterface, @@ -21,8 +22,7 @@ import { import { WidgetServices } from "src/widget"; import { LocalFileService } from "src/services/localFile.service"; -import { AGREE_COOKIE, AGREE_KG_TOS } from "src/services/state/uiState.store"; -import { SHOW_KG_TOS } from 'src/services/state/uiState.store.helper' +import { AGREE_COOKIE } from "src/services/state/uiState.store"; import { isSame } from "src/util/fn"; import { colorAnimation } from "./atlasViewer.animation" import { MouseHoverDirective } from "src/mouseoverModule"; @@ -34,6 +34,7 @@ import { SlServiceService } from "src/spotlight/sl-service.service"; import { PureContantService } from "src/util"; import { ClickInterceptorService } from "src/glue"; import { environment } from 'src/environments/environment' +import { DOCUMENT } from "@angular/common"; /** * TODO @@ -61,8 +62,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @ViewChild('cookieAgreementComponent', {read: TemplateRef}) public cookieAgreementComponent: TemplateRef<any> - @ViewChild('kgToS', {read: TemplateRef}) public kgTosComponent: TemplateRef<any> - @ViewChild(MouseHoverDirective) private mouseOverNehuba: MouseHoverDirective @ViewChild('idleOverlay', {read: TemplateRef}) idelTmpl: TemplateRef<any> @@ -85,20 +84,19 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public selectedParcellation: any private cookieDialogRef: MatDialogRef<any> - private kgTosDialogRef: MatDialogRef<any> constructor( private store: Store<IavRootStoreInterface>, private widgetServices: WidgetServices, private pureConstantService: PureContantService, private matDialog: MatDialog, - private dispatcher$: ActionsSubject, private rd: Renderer2, public localFileService: LocalFileService, private snackbar: MatSnackBar, private el: ElementRef, private slService: SlServiceService, - private clickIntService: ClickInterceptorService + private clickIntService: ClickInterceptorService, + @Inject(DOCUMENT) private document, ) { this.snackbarMessage$ = this.store.pipe( @@ -147,7 +145,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.subscriptions.push( merge( - fromEvent(window.document, 'mouseup'), + fromEvent(this.document, 'mouseup'), this.slService.onClick ).pipe( startWith(true), @@ -196,7 +194,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.subscriptions.push( this.pureConstantService.darktheme$.subscribe(flag => { - this.rd.setAttribute(document.body, 'darktheme', this.meetsRequirement && flag.toString()) + this.rd.setAttribute(this.document.body, 'darktheme', this.meetsRequirement && flag.toString()) }), ) } @@ -212,13 +210,9 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { prefecthMainBundle.rel = 'preload' prefecthMainBundle.as = 'script' prefecthMainBundle.href = 'main.bundle.js' - this.rd.appendChild(document.head, prefecthMainBundle) + this.rd.appendChild(this.document.head, prefecthMainBundle) } - // this.onhoverLandmark$ = this.mouseOverNehuba.currentOnHoverObs$.pipe( - // select('landmark') - // ) - /** * Show Cookie disclaimer if not yet agreed */ @@ -234,18 +228,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.cookieDialogRef = this.matDialog.open(this.cookieAgreementComponent) }) - this.dispatcher$.pipe( - filter(({type}) => type === SHOW_KG_TOS), - withLatestFrom(this.store.pipe( - select('uiState'), - select('agreedKgTos'), - )), - map(([_, agreed]) => agreed), - filter(flag => !flag), - delay(0), - ).subscribe(() => { - this.kgTosDialogRef = this.matDialog.open(this.kgTosComponent) - }) } /** @@ -264,7 +246,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { */ public meetsRequirements(): boolean { - const canvas = document.createElement('canvas') + const canvas = this.document.createElement('canvas') const gl = canvas.getContext('webgl2') as WebGLRenderingContext if (!gl) { @@ -280,13 +262,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { return true } - public kgTosClickedOk() { - if (this.kgTosDialogRef) { this.kgTosDialogRef.close() } - this.store.dispatch({ - type: AGREE_KG_TOS, - }) - } - public cookieClickedOk() { if (this.cookieDialogRef) { this.cookieDialogRef.close() } this.store.dispatch({ diff --git a/src/atlasViewer/atlasViewer.style.css b/src/atlasViewer/atlasViewer.style.css index c6c87ac87da646244f55ce6654b404763ee369c8..0f938ac6632a634a35ee793318a408c0e70248fd 100644 --- a/src/atlasViewer/atlasViewer.style.css +++ b/src/atlasViewer/atlasViewer.style.css @@ -34,17 +34,13 @@ layout-floating-container > * top: 0; } -[signinWrapper] -{ - margin: 0.8em 0.4em; -} - -[contextualBlock] +mat-list[dense].contextual-block { + display: inline-block; background-color:rgba(200,200,200,0.8); } -:host-context([darktheme="true"]) [contextualBlock] +:host-context([darktheme="true"]) mat-list[dense].contextual-block { background-color : rgba(30,30,30,0.8); } diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index fbaa8bf4c5dee3f37516c8d928c4c1a008897130..5549a8e38914948e7157fcfc6cde6a4f63bd9153 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -1,26 +1,14 @@ + +<!-- required for manufacturing plugin templates --> +<div pluginFactoryDirective> +</div> + <ng-container *ngIf="meetsRequirement; else doesNotMeetReqTemplate"> <ng-container *ngTemplateOutlet="viewerBody"> </ng-container> </ng-container> -<!-- kg tos --> -<ng-template #kgToS> - <h2 mat-dialog-title>Knowledge Graph ToS</h2> - <mat-dialog-content> - <small> - <kgtos-component> - </kgtos-component> - </small> - </mat-dialog-content> - - <mat-dialog-actions class="justify-content-end"> - <button color="primary" mat-raised-button (click)="kgTosClickedOk()" cdkFocusInitial> - Ok - </button> - </mat-dialog-actions> -</ng-template> - <!-- cookie --> <ng-template #cookieAgreementComponent> <h2 mat-dialog-title>Privacy Policy</h2> @@ -40,7 +28,7 @@ <!-- atlas template --> <ng-template #viewerBody> - <div class="atlas-container w-100 h-100" + <div class="w-100 h-100" iav-media-query quick-tour [quick-tour-position]="quickTourFinale.position" @@ -65,41 +53,33 @@ <div floatingContainerDirective> </div> - <div - *ngIf="(media.mediaBreakPoint$ | async) < 3" + <div *ngIf="(media.mediaBreakPoint$ | async) < 3" class="fixed-bottom pe-none mb-2 d-flex justify-content-center"> <ng-container *ngTemplateOutlet="logoTmpl"> </ng-container> </div> - <div floatingMouseContextualContainerDirective> + <div *ngIf="!ismobile" floatingMouseContextualContainerDirective> - <div *ngIf="!ismobile" - class="d-inline-block" + <div class="h-0" iav-mouse-hover - #iavMouseHoverContextualBlock="iavMouseHover" - contextualBlock> - - <ng-container - *ngFor="let labelText of iavMouseHoverContextualBlock.currentOnHoverObs$ | async | mouseOverTextPipe"> - - <mat-list dense> - - <mat-list-item class="h-auto"> + #iavMouseHoverContextualBlock="iavMouseHover"> + </div> + <mat-list dense class="contextual-block"> - <mat-icon - [fontSet]="(labelText.label | mouseOverIconPipe).fontSet" - [fontIcon]="(labelText.label | mouseOverIconPipe).fontIcon" - mat-list-icon> + <mat-list-item *ngFor="let cvtOutput of iavMouseHoverContextualBlock.currentOnHoverObs$ | async | mouseoverCvt" + class="h-auto"> - </mat-icon> + <mat-icon + [fontSet]="cvtOutput.icon.fontSet" + [fontIcon]="cvtOutput.icon.fontIcon" + mat-list-icon> + </mat-icon> - <div matLine *ngFor="let text of labelText.text" [innerHTML]="text"></div> + <div matLine>{{ cvtOutput.text }}</div> - </mat-list-item> - </mat-list> - </ng-container> - </div> + </mat-list-item> + </mat-list> <!-- TODO Potentially implementing plugin contextual info --> </div> @@ -113,10 +93,6 @@ </div> </layout-floating-container> - - <!-- required for manufacturing plugin templates --> - <div pluginFactoryDirective> - </div> </div> </ng-template> diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 1eafa1a3db4056567b6d554b6dbc1ffa65e89355..c8d9ff210b5530a6e001dcd983a3fb8487c560cf 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -25,6 +25,7 @@ import { IAVVerticalButton } from './vButton/vButton.component'; import { DynamicMaterialBtn } from './dynamicMaterialBtn/dynamicMaterialBtn.component'; import { SpinnerCmp } from './spinner/spinner.component'; import { ReadmoreModule } from './readmore'; +import { TileCmp } from './tile/tile.component'; @NgModule({ imports : [ @@ -46,6 +47,7 @@ import { ReadmoreModule } from './readmore'; IAVVerticalButton, DynamicMaterialBtn, SpinnerCmp, + TileCmp, /* directive */ TreeBaseDirective, @@ -72,6 +74,7 @@ import { ReadmoreModule } from './readmore'; IAVVerticalButton, DynamicMaterialBtn, SpinnerCmp, + TileCmp, TreeSearchPipe, TreeBaseDirective, diff --git a/src/components/markdown/markdown.component.ts b/src/components/markdown/markdown.component.ts index e48f409c0cbd604c5be32c424a491552cedaa0a5..bd5ea00320923c15e4517232021822b91fd8e630 100644 --- a/src/components/markdown/markdown.component.ts +++ b/src/components/markdown/markdown.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild, ChangeDetectorRef, AfterViewChecked } from '@angular/core' +import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild, ChangeDetectorRef, AfterViewChecked, OnChanges } from '@angular/core' import * as showdown from 'showdown' @Component({ @@ -10,7 +10,7 @@ import * as showdown from 'showdown' changeDetection : ChangeDetectionStrategy.OnPush, }) -export class MarkdownDom implements AfterViewChecked { +export class MarkdownDom implements OnChanges { @Input() public markdown: string = `` public innerHtml: string = `` @@ -21,6 +21,7 @@ export class MarkdownDom implements AfterViewChecked { constructor( private cdr: ChangeDetectorRef ) { + this.cdr.detach() this.converter.setFlavor('github') } @@ -30,7 +31,7 @@ export class MarkdownDom implements AfterViewChecked { return '' } - public ngAfterViewChecked(){ + ngOnChanges(){ this.innerHtml = this.converter.makeHtml( this.getMarkdown() ) diff --git a/src/components/tile/tile.component.ts b/src/components/tile/tile.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcab669508d12e06852675c41e7a20e38bc72843 --- /dev/null +++ b/src/components/tile/tile.component.ts @@ -0,0 +1,55 @@ +import { Component, EventEmitter, HostBinding, HostListener, Input, OnChanges, Output } from "@angular/core"; + +@Component({ + selector: 'tile-cmp', + templateUrl: './tile.template.html', + styleUrls: [ + './tile.style.css' + ], + exportAs: 'tileCmp' +}) + +export class TileCmp implements OnChanges{ + @Input('tile-image-src') + tileImgSrc: string + + @Input('tile-image-alt') + tileImgAlt: string = 'Thumbnail of this tile.' + + @Input('tile-text') + tileText: string = 'Tile' + + @Input('tile-show-info') + tileShowInfo: boolean = false + + @Input('tile-disabled') + tileDisabled: boolean = false + + @Input('tile-image-darktheme') + darktheme: boolean = false + + @Input('tile-selected') + selected: boolean = false + + @Output('tile-on-click') + onClick: EventEmitter<MouseEvent> = new EventEmitter() + + @Input('tile-is-dir') + isDir: boolean = false + + @Output('tile-on-info-click') + onInfoClick: EventEmitter<MouseEvent> = new EventEmitter() + + @HostBinding('class') + hostBindingClass = '' + + @HostListener('click', ['$event']) + protected _onClick(event: MouseEvent) { + this.onClick.emit(event) + } + + ngOnChanges(){ + if (this.tileDisabled) this.hostBindingClass = 'muted' + else this.hostBindingClass = '' + } +} diff --git a/src/components/tile/tile.style.css b/src/components/tile/tile.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e877c720820a2a9b6fbf6a0d988fad5b90385054 --- /dev/null +++ b/src/components/tile/tile.style.css @@ -0,0 +1,39 @@ +.info-button { + height: 20px; + width: 20px; + margin-top: 10px; + margin-right: 12px; + border-radius: 50%; + text-align: center; +} + +.tile-selected { + border: 2px solid #FED363; +} + +:host, +img +{ + display: block; + width: 100%; + height: 100%; +} + +img +{ + border-radius: 10px; +} + +.tile-text +{ + margin-top: 0.5rem; + text-align: center; + white-space: normal; + font-size: 80%; +} + +.folder-container +{ + margin-right:0.5rem; + margin-bottom:-0.5rem; +} diff --git a/src/components/tile/tile.template.html b/src/components/tile/tile.template.html new file mode 100644 index 0000000000000000000000000000000000000000..44cc134d2a135d88ad24d04c1631cb68ed827b5e --- /dev/null +++ b/src/components/tile/tile.template.html @@ -0,0 +1,35 @@ +<div class="position-relative"> + <!-- info icon --> + <div *ngIf="tileShowInfo" + class="position-absolute right-0"> + <div mat-icon-button + iav-stop="click" + (click)="onInfoClick.emit($event)" + [ngStyle]="{ + backgroundColor: darktheme ? 'white' : 'black', + color: darktheme ? 'black': 'white' + }" + class="mat-elevation-z2 iv-custom-comp info-button"> + <small> + <i class="fas fa-info"></i> + </small> + </div> + </div> + + + <!-- directory icon --> + <div *ngIf="isDir" class="position-absolute bottom-0 right-0"> + <i class="fas fa-folder folder-container fa-2x"></i> + </div> + + <img [src]="tileImgSrc" + [alt]="tileImgAlt" + [ngClass]="{ + 'tile-selected': selected + }" + draggable="false"> +</div> + +<div class="tile-text"> + {{ tileText }} +</div> diff --git a/src/extra_styles.css b/src/extra_styles.css index ae2f870b0e9158e1b3032959d203fca162c9c4bd..928bcf43c3700cef7c1b006cc7b85ae1bedda609 100644 --- a/src/extra_styles.css +++ b/src/extra_styles.css @@ -320,6 +320,11 @@ markdown-dom p width: 20em!important; } +.w-100vw +{ + width: 100vw!important; +} + .mw-100 { max-width: 100%!important; @@ -758,6 +763,16 @@ kg-dataset-previewer > img opacity: 1.0; } +.m-15 +{ + margin: 15px; +} + +.m-1px +{ + margin:1px; +} + .ml-15px-n { margin-left: -15px!important; @@ -805,6 +820,10 @@ kg-dataset-previewer > img { margin-top: -1rem!important; } +.mt-1-n +{ + margin-top: -0.5rem!important; +} .mb-6 { @@ -828,11 +847,6 @@ kg-dataset-previewer > img bottom: unset!important; } -.layerGroupMenu > .mat-menu-content -{ - width: 100%; -} - mat-list.sm mat-list-item { height:2rem!important; @@ -886,3 +900,8 @@ mat-list.sm mat-list-item margin-top: auto; margin-bottom: auto; } + +.v-align-top +{ + vertical-align: top; +} \ No newline at end of file diff --git a/src/index.html b/src/index.html index 5701618243131618751f45ef96644368a9baf333..8fd23a5d9027b1514df06dc820b766c9b0027af7 100644 --- a/src/index.html +++ b/src/index.html @@ -11,11 +11,14 @@ <link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="version.css"> <link rel="icon" type="image/png" href="res/favicons/favicon-128-light.png"/> - + <script> + // disable zone patching of raf. This hampers NG performance significantly + window['__Zone_disable_requestAnimationFrame'] = true + </script> <script src="https://unpkg.com/kg-dataset-previewer@1.2.0/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer> </script> <script src="https://unpkg.com/three-surfer@0.0.10/dist/bundle.js" defer></script> - <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.2/dist/ng-layer-tune/ng-layer-tune.esm.js"></script> + <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.4/dist/ng-layer-tune/ng-layer-tune.esm.js"></script> <title>Interactive Atlas Viewer</title> </head> diff --git a/src/layouts/currentLayout/currentLayout.component.ts b/src/layouts/currentLayout/currentLayout.component.ts index 313f1cd93467cb08955bd92f2dc728568427bf62..be31bf00347ec1bee75eba254c3ff866b7ac4304 100644 --- a/src/layouts/currentLayout/currentLayout.component.ts +++ b/src/layouts/currentLayout/currentLayout.component.ts @@ -4,7 +4,6 @@ import { Observable } from "rxjs"; import { startWith } from "rxjs/operators"; import { SUPPORTED_PANEL_MODES } from "src/services/state/ngViewerState.store"; import { ngViewerSelectorPanelMode } from "src/services/state/ngViewerState/selectors"; -import { IavRootStoreInterface } from "src/services/stateStore.service"; @Component({ selector: 'current-layout', @@ -20,7 +19,7 @@ export class CurrentLayout { public panelMode$: Observable<string> constructor( - private store$: Store<IavRootStoreInterface>, + private store$: Store<any>, ) { this.panelMode$ = this.store$.pipe( select(ngViewerSelectorPanelMode), diff --git a/src/layouts/currentLayout/currentLayout.style.css b/src/layouts/currentLayout/currentLayout.style.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a633460dc9cd04a4e563a2a926b4d486cba421d4 100644 --- a/src/layouts/currentLayout/currentLayout.style.css +++ b/src/layouts/currentLayout/currentLayout.style.css @@ -0,0 +1,6 @@ +:host +{ + width: 100%; + height: 100%; + display: block; +} \ No newline at end of file diff --git a/src/layouts/currentLayout/currentLayout.template.html b/src/layouts/currentLayout/currentLayout.template.html index f3d04c104239b9e81f3ff0552c892e86c444ba0d..9575932ed1de52ea23341d6570a48b32225c9160 100644 --- a/src/layouts/currentLayout/currentLayout.template.html +++ b/src/layouts/currentLayout/currentLayout.template.html @@ -1,72 +1,72 @@ -<div [ngSwitch]="panelMode$ | async" class="w-100 h-100 d-flex flex-row"> +<ng-container [ngSwitch]="panelMode$ | async"> <layout-four-panel *ngSwitchCase="supportedPanelModes[0]" class="d-block w-100 h-100"> - <div class="w-100 h-100" cell-i> + <ng-container cell-i> <ng-content *ngTemplateOutlet="celli"></ng-content> - </div> - <div class="w-100 h-100" cell-ii> + </ng-container> + <ng-container cell-ii> <ng-content *ngTemplateOutlet="cellii"></ng-content> - </div> - <div class="w-100 h-100" cell-iii> + </ng-container> + <ng-container cell-iii> <ng-content *ngTemplateOutlet="celliii"></ng-content> - </div> - <div class="w-100 h-100" cell-iv> + </ng-container> + <ng-container cell-iv> <ng-content *ngTemplateOutlet="celliv"></ng-content> - </div> + </ng-container> </layout-four-panel> <layout-horizontal-one-three *ngSwitchCase="supportedPanelModes[1]" class="d-block w-100 h-100"> - <div class="w-100 h-100" cell-i> + <ng-container cell-i> <ng-content *ngTemplateOutlet="celli"></ng-content> - </div> - <div class="w-100 h-100" cell-ii> + </ng-container> + <ng-container cell-ii> <ng-content *ngTemplateOutlet="cellii"></ng-content> - </div> - <div class="w-100 h-100" cell-iii> + </ng-container> + <ng-container cell-iii> <ng-content *ngTemplateOutlet="celliii"></ng-content> - </div> - <div class="w-100 h-100" cell-iv> + </ng-container> + <ng-container cell-iv> <ng-content *ngTemplateOutlet="celliv"></ng-content> - </div> + </ng-container> </layout-horizontal-one-three> <layout-vertical-one-three *ngSwitchCase="supportedPanelModes[2]" class="d-block w-100 h-100"> - <div class="w-100 h-100" cell-i> + <ng-container cell-i> <ng-content *ngTemplateOutlet="celli"></ng-content> - </div> - <div class="w-100 h-100" cell-ii> + </ng-container> + <ng-container cell-ii> <ng-content *ngTemplateOutlet="cellii"></ng-content> - </div> - <div class="w-100 h-100" cell-iii> + </ng-container> + <ng-container cell-iii> <ng-content *ngTemplateOutlet="celliii"></ng-content> - </div> - <div class="w-100 h-100" cell-iv> + </ng-container> + <ng-container cell-iv> <ng-content *ngTemplateOutlet="celliv"></ng-content> - </div> + </ng-container> </layout-vertical-one-three> <layout-single-panel *ngSwitchCase="supportedPanelModes[3]" class="d-block w-100 h-100"> - <div class="w-100 h-100" cell-i> + <ng-container cell-i> <ng-content *ngTemplateOutlet="celli"></ng-content> - </div> - <div class="w-100 h-100" cell-ii> + </ng-container> + <ng-container cell-ii> <ng-content *ngTemplateOutlet="cellii"></ng-content> - </div> - <div class="w-100 h-100" cell-iii> + </ng-container> + <ng-container cell-iii> <ng-content *ngTemplateOutlet="celliii"></ng-content> - </div> - <div class="w-100 h-100" cell-iv> + </ng-container> + <ng-container cell-iv> <ng-content *ngTemplateOutlet="celliv"></ng-content> - </div> + </ng-container> </layout-single-panel> <div *ngSwitchDefault> A panel mode which I have never seen before ... </div> -</div> +</ng-container> <ng-template #celli> <ng-content select="[cell-i]"></ng-content> diff --git a/src/layouts/fourCorners/fourCorners.style.css b/src/layouts/fourCorners/fourCorners.style.css index 352b490b2acf42d4d92344bdc0be28d4f8cf80da..e81a3cd69216d94cb228f602acf732f72a0dd44b 100644 --- a/src/layouts/fourCorners/fourCorners.style.css +++ b/src/layouts/fourCorners/fourCorners.style.css @@ -9,5 +9,33 @@ .corner-container { - z-index: 5; -} \ No newline at end of file + max-width: 100%; +} + +.grid-container +{ + height: 50%; + display: grid; + grid-template-columns: minmax(auto, 100%) minmax(auto, 100%); +} + +.top-left +{ + place-self: start start; + z-index: 2; +} +.top-right +{ + place-self: start end; + z-index: 1; +} +.bottom-left +{ + place-self: end start; + z-index: 2; +} +.bottom-right +{ + place-self: end end; + z-index: 1; +} diff --git a/src/layouts/fourCorners/fourCorners.template.html b/src/layouts/fourCorners/fourCorners.template.html index 96cfe86cdb46e296f4b44fa92adfb377dec010fd..c0e98df279d32db83b96227fa439642ff79175cc 100644 --- a/src/layouts/fourCorners/fourCorners.template.html +++ b/src/layouts/fourCorners/fourCorners.template.html @@ -2,19 +2,24 @@ <ng-content select="[iavLayoutFourCornersContent]"></ng-content> </div> -<div [ngClass]="cornerContainerClasses" - class="corner-container position-absolute top-0 left-0"> - <ng-content select="[iavLayoutFourCornersTopLeft]"></ng-content> +<div class="grid-container"> + <div [ngClass]="cornerContainerClasses" + class="corner-container top-left"> + <ng-content select="[iavLayoutFourCornersTopLeft]"></ng-content> + </div> + <div [ngClass]="cornerContainerClasses" + class="corner-container top-right"> + <ng-content select="[iavLayoutFourCornersTopRight]"></ng-content> + </div> </div> -<div [ngClass]="cornerContainerClasses" - class="corner-container position-absolute top-0 right-0"> - <ng-content select="[iavLayoutFourCornersTopRight]"></ng-content> -</div> -<div [ngClass]="cornerContainerClasses" - class="corner-container position-absolute bottom-0 left-0"> - <ng-content select="[iavLayoutFourCornersBottomLeft]"></ng-content> + +<div class="grid-container"> + <div [ngClass]="cornerContainerClasses" + class="corner-container bottom-left"> + <ng-content select="[iavLayoutFourCornersBottomLeft]"></ng-content> + </div> + <div [ngClass]="cornerContainerClasses" + class="corner-container bottom-right"> + <ng-content select="[iavLayoutFourCornersBottomRight]"></ng-content> + </div> </div> -<div [ngClass]="cornerContainerClasses" - class="corner-container position-absolute bottom-0 right-0"> - <ng-content select="[iavLayoutFourCornersBottomRight]"></ng-content> -</div> \ No newline at end of file diff --git a/src/layouts/layouts/fourPanel/fourPanel.style.css b/src/layouts/layouts/fourPanel/fourPanel.style.css index 03169dfb4b9564f62c86ca9250303c5337927861..c967cad95281436e99d2fe073018148ae818c4bd 100644 --- a/src/layouts/layouts/fourPanel/fourPanel.style.css +++ b/src/layouts/layouts/fourPanel/fourPanel.style.css @@ -1,4 +1,12 @@ .four-panel-cell { flex: 0 0 50%; +} + +.four-panel-container +{ + display: grid; + grid-template-columns: 50% 50%; + grid-template-rows: 50% 50%; + height: 100%; } \ No newline at end of file diff --git a/src/layouts/layouts/fourPanel/fourPanel.template.html b/src/layouts/layouts/fourPanel/fourPanel.template.html index ddb10f1f6a34adda68f578625dd843baf536dfa0..9e747c2a9cf43469711a520b225b92353090c523 100644 --- a/src/layouts/layouts/fourPanel/fourPanel.template.html +++ b/src/layouts/layouts/fourPanel/fourPanel.template.html @@ -1,18 +1,14 @@ -<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-stretch"> - <div class="d-flex flex-row flex-grow-1 flex-shrink-1"> - <div class="d-flex flex-row four-panel-cell align-items-center justify-content-center"> - <ng-content select="[cell-i]"></ng-content> - </div> - <div class="d-flex flex-row four-panel-cell align-items-center justify-content-center"> - <ng-content select="[cell-ii]"></ng-content> - </div> +<div class="four-panel-container"> + <div> + <ng-content select="[cell-i]"></ng-content> </div> - <div class="d-flex flex-row flex-grow-1 flex-shrink-1"> - <div class="d-flex flex-row four-panel-cell align-items-center justify-content-center"> - <ng-content select="[cell-iii]"></ng-content> - </div> - <div class="d-flex flex-row four-panel-cell align-items-center justify-content-center"> - <ng-content select="[cell-iv]"></ng-content> - </div> + <div> + <ng-content select="[cell-ii]"></ng-content> + </div> + <div> + <ng-content select="[cell-iii]"></ng-content> + </div> + <div> + <ng-content select="[cell-iv]"></ng-content> </div> </div> diff --git a/src/layouts/layouts/single/single.template.html b/src/layouts/layouts/single/single.template.html index 561a6363b701518ec5d27b1f0d78507e02afe30a..fcf3a11e33d91bac185b09150761e7487b3e7ae5 100644 --- a/src/layouts/layouts/single/single.template.html +++ b/src/layouts/layouts/single/single.template.html @@ -1,18 +1,3 @@ -<div class="w-100 h-100 d-flex flex-row justify-content-center align-items-stretch"> - <div class="d-flex flex-column major-column"> - <div class="overflow-hidden flex-grow-1 d-flex align-items-center justify-content-center"> - <ng-content select="[cell-i]"></ng-content> - </div> - </div> - <div class="d-flex flex-column minor-column"> - <div class="overflow-hidden layout-31-cell d-flex align-items-center justify-content-center"> - <ng-content select="[cell-ii]"></ng-content> - </div> - <div class="overflow-hidden layout-31-cell d-flex align-items-center justify-content-center"> - <ng-content select="[cell-iii]"></ng-content> - </div> - <div class="overflow-hidden layout-31-cell d-flex align-items-center justify-content-center"> - <ng-content select="[cell-iv]"></ng-content> - </div> - </div> - </div> \ No newline at end of file +<div class="w-100 h-100"> + <ng-content select="[cell-i]"></ng-content> +</div> diff --git a/src/mouseoverModule/index.ts b/src/mouseoverModule/index.ts index 6e9cd09486d0a2da56b1f28f86dce2cc6382d78d..8dea7b959fa47feda07047ca34a1fa0d6e204cec 100644 --- a/src/mouseoverModule/index.ts +++ b/src/mouseoverModule/index.ts @@ -1,5 +1,3 @@ -export { MouseOverIconPipe } from './mouseOverIcon.pipe' -export { MouseOverTextPipe } from './mouseOverText.pipe' export { MouseHoverDirective } from './mouseover.directive' export { MouseoverModule } from './mouseover.module' export { TransformOnhoverSegmentPipe } from './transformOnhoverSegment.pipe' \ No newline at end of file diff --git a/src/mouseoverModule/mouseOverCvt.pipe.ts b/src/mouseoverModule/mouseOverCvt.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0a86234237ad87ab895e70575e8dd1964ba5d51 --- /dev/null +++ b/src/mouseoverModule/mouseOverCvt.pipe.ts @@ -0,0 +1,89 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { TOnHoverObj } from "./util"; + +function render<T extends keyof TOnHoverObj>(key: T, value: TOnHoverObj[T]){ + if (!value) return [] + switch (key) { + case 'segments': { + return (value as TOnHoverObj['segments']).map(seg => { + return { + icon: { + fontSet: 'fas', + fontIcon: 'fa-brain' + }, + text: typeof seg.segment === 'string' + ? seg.segment + : seg.segment.name + } + }) + } + case 'landmark': { + return [{ + icon: { + fontSet: 'fas', + fontIcon: 'fa-map-marker-alt', + }, + text: (value as TOnHoverObj['landmark']).landmarkName + }] + } + case 'userLandmark': { + return [{ + icon: { + fontSet: 'fas', + fontIcon: 'fa-map-marker-alt', + }, + text: value as TOnHoverObj['userLandmark'] + }] + } + case 'annotation': { + const { annotationType, name } = (value as TOnHoverObj['annotation']) + let fontIcon: string + if (annotationType === 'Point') fontIcon = 'fa-circle' + if (annotationType === 'Line') fontIcon = 'fa-slash' + if (annotationType === 'Polygon') fontIcon = 'fa-draw-polygon' + if (!annotationType) fontIcon = 'fa-file' + return [{ + icon: { + fontSet: 'fas', + fontIcon + }, + text: name || `Unnamed ${annotationType}` + }] + } + default: { + return [{ + icon: { + fontSet: 'fas', + fontIcon: 'fa-file', + }, + text: `Unknown hovered object` + }] + } + } +} + +type TCvtOutput = { + icon: { + fontSet: string + fontIcon: string + } + text: string +} + +@Pipe({ + name: 'mouseoverCvt', + pure: true +}) + +export class MouseOverConvertPipe implements PipeTransform{ + + public transform(dict: TOnHoverObj){ + const output: TCvtOutput[] = [] + for (const key in dict) { + output.push( + ...render(key as keyof TOnHoverObj, dict[key]) + ) + } + return output + } +} \ No newline at end of file diff --git a/src/mouseoverModule/mouseOverIcon.pipe.ts b/src/mouseoverModule/mouseOverIcon.pipe.ts deleted file mode 100644 index ba5f7686798d9417eb365580a84f16955cdf53d7..0000000000000000000000000000000000000000 --- a/src/mouseoverModule/mouseOverIcon.pipe.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core" - -@Pipe({ - name: 'mouseOverIconPipe', -}) - -export class MouseOverIconPipe implements PipeTransform { - - public transform(type: string): {fontSet: string, fontIcon: string} { - - switch (type) { - case 'landmark': - return { - fontSet: 'fas', - fontIcon: 'fa-map-marker-alt', - } - case 'segments': - return { - fontSet: 'fas', - fontIcon: 'fa-brain', - } - case 'userLandmark': - return { - fontSet: 'fas', - fontIcon: 'fa-map-marker-alt', - } - default: - return { - fontSet: 'fas', - fontIcon: 'fa-file', - } - } - } -} diff --git a/src/mouseoverModule/mouseOverText.pipe.ts b/src/mouseoverModule/mouseOverText.pipe.ts deleted file mode 100644 index 5ba11425c0e78eb8dee14e9a25ba02a0ed0f9adf..0000000000000000000000000000000000000000 --- a/src/mouseoverModule/mouseOverText.pipe.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Pipe, PipeTransform, SecurityContext } from "@angular/core" -import { DomSanitizer, SafeHtml } from "@angular/platform-browser" -import { TransformOnhoverSegmentPipe } from "./transformOnhoverSegment.pipe" - -@Pipe({ - name: 'mouseOverTextPipe', -}) - -export class MouseOverTextPipe implements PipeTransform { - - private transformOnHoverSegmentPipe: TransformOnhoverSegmentPipe - constructor(private sanitizer: DomSanitizer) { - this.transformOnHoverSegmentPipe = new TransformOnhoverSegmentPipe(this.sanitizer) - } - - private renderText = ({ label, obj }): SafeHtml[] => { - switch (label) { - case 'landmark': { - const { dataset = [] } = obj - return [ - this.sanitizer.sanitize(SecurityContext.HTML, obj.landmarkName), - ...(dataset.map(ds => this.sanitizer.bypassSecurityTrustHtml(` -<span class="text-muted"> - ${this.sanitizer.sanitize(SecurityContext.HTML, ds.name)} -</span> -`))) - ] - } - case 'segments': - return obj.map(({ segment }) => this.transformOnHoverSegmentPipe.transform(segment)) - case 'userLandmark': - return [this.sanitizer.sanitize(SecurityContext.HTML, obj.name)] - default: - // ts-lint:disable-next-line - console.warn(`mouseOver.directive.ts#mouseOverTextPipe: Cannot be displayed: label: ${label}`) - return [this.sanitizer.bypassSecurityTrustHtml(`Cannot be displayed: label: ${label}`)] - } - } - - public transform(inc: {segments: any, landmark: any, userLandmark: any}): Array<{label: string, text: SafeHtml[]}> { - const keys = Object.keys(inc) - return keys - // if is segments, filter out if lengtth === 0 - .filter(key => Array.isArray(inc[key]) ? inc[key].length > 0 : true ) - // for other properties, check if value is defined - .filter(key => !!inc[key]) - .map(key => { - return { - label: key, - text: this.renderText({ label: key, obj: inc[key] }) - } - }) - } -} diff --git a/src/mouseoverModule/mouseover.directive.ts b/src/mouseoverModule/mouseover.directive.ts index 25f33743ce536d91150185cfab6c2e52e3764c8e..cc4ae00872d32f4463aeb4d6a9ce7fbdee37ebe3 100644 --- a/src/mouseoverModule/mouseover.directive.ts +++ b/src/mouseoverModule/mouseover.directive.ts @@ -1,12 +1,11 @@ import { Directive } from "@angular/core" import { select, Store } from "@ngrx/store" import { merge, Observable } from "rxjs" -import { distinctUntilChanged, filter, map, scan, shareReplay, startWith, withLatestFrom } from "rxjs/operators" +import { distinctUntilChanged, map, scan, shareReplay } from "rxjs/operators" import { LoggingService } from "src/logging" -import { uiStateMouseOverSegmentsSelector, uiStateMouseoverUserLandmark } from "src/services/state/uiState/selectors" -import { viewerStateSelectedParcellationSelector } from "src/services/state/viewerState/selectors" -import { deserialiseParcRegionId } from "common/util" -import { temporalPositveScanFn } from "./util" +import { uiStateMouseOverLandmarkSelector, uiStateMouseOverSegmentsSelector, uiStateMouseoverUserLandmark } from "src/services/state/uiState/selectors" +import { TOnHoverObj, temporalPositveScanFn } from "./util" +import { ModularUserAnnotationToolService } from "src/atlasComponents/userAnnotations/tools/service"; @Directive({ selector: '[iav-mouse-hover]', @@ -15,12 +14,12 @@ import { temporalPositveScanFn } from "./util" export class MouseHoverDirective { - public onHoverObs$: Observable<{segments: any, landmark: any, userLandmark: any}> - public currentOnHoverObs$: Observable<{segments: any, landmark: any, userLandmark: any}> + public currentOnHoverObs$: Observable<TOnHoverObj> constructor( private store$: Store<any>, private log: LoggingService, + private annotSvc: ModularUserAnnotationToolService, ) { // TODO consider moving these into a single obs serviced by a DI service @@ -31,11 +30,10 @@ export class MouseHoverDirective { ) const onHoverLandmark$ = this.store$.pipe( - select('uiState'), - select('mouseOverLandmark'), + select(uiStateMouseOverLandmarkSelector) ).pipe( map(landmark => { - if (landmark === null) { return landmark } + if (landmark === null) { return null } const idx = Number(landmark.replace('label=', '')) if (isNaN(idx)) { this.log.warn(`Landmark index could not be parsed as a number: ${landmark}`) @@ -48,27 +46,27 @@ export class MouseHoverDirective { const onHoverSegments$ = this.store$.pipe( select(uiStateMouseOverSegmentsSelector), - filter(v => !!v), - withLatestFrom( - this.store$.pipe( - select(viewerStateSelectedParcellationSelector), - startWith(null), - ), - ), - map(([ arr, parcellationSelected ]) => parcellationSelected && parcellationSelected.auxillaryMeshIndices - ? arr.filter(({ segment }) => { - // if segment is not a string (i.e., not labelIndexId) return true - if (typeof segment !== 'string') { return true } - const { labelIndex } = deserialiseParcRegionId(segment) - return parcellationSelected.auxillaryMeshIndices.indexOf(labelIndex) < 0 - }) - : arr), - distinctUntilChanged((o, n) => o.length === n.length - && n.every(segment => - o.find(oSegment => oSegment.layer.name === segment.layer.name - && oSegment.segment === segment.segment))), + + // TODO fix aux mesh filtering + + // withLatestFrom( + // this.store$.pipe( + // select(viewerStateSelectedParcellationSelector), + // startWith(null as any), + // ), + // ), + // map(([ arr, parcellationSelected ]) => parcellationSelected && parcellationSelected.auxillaryMeshIndices + // ? arr.filter(({ segment }) => { + // // if segment is not a string (i.e., not labelIndexId) return true + // if (typeof segment !== 'string') { return true } + // const { labelIndex } = deserialiseParcRegionId(segment) + // return parcellationSelected.auxillaryMeshIndices.indexOf(labelIndex) < 0 + // }) + // : arr), ) + const onHoverAnnotation$ = this.annotSvc.hoveringAnnotations$ + const mergeObs = merge( onHoverSegments$.pipe( distinctUntilChanged(), @@ -76,6 +74,12 @@ export class MouseHoverDirective { return { segments } }), ), + onHoverAnnotation$.pipe( + distinctUntilChanged(), + map(annotation => { + return { annotation } + }), + ), onHoverLandmark$.pipe( distinctUntilChanged(), map(landmark => { @@ -92,22 +96,13 @@ export class MouseHoverDirective { shareReplay(1), ) - this.onHoverObs$ = mergeObs.pipe( - scan((acc, curr) => { - return { - ...acc, - ...curr, - } - }, { segments: null, landmark: null, userLandmark: null }), - shareReplay(1), - ) - this.currentOnHoverObs$ = mergeObs.pipe( scan(temporalPositveScanFn, []), map(arr => { let returnObj = { segments: null, + annotation: null, landmark: null, userLandmark: null, } diff --git a/src/mouseoverModule/mouseover.module.ts b/src/mouseoverModule/mouseover.module.ts index 476fc900a2a6a3731b5bae0d7f6a2f60d836dd8a..b5fbcc9feb9f04b1336d1df7209445144e569cc5 100644 --- a/src/mouseoverModule/mouseover.module.ts +++ b/src/mouseoverModule/mouseover.module.ts @@ -2,8 +2,8 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { TransformOnhoverSegmentPipe } from "src/atlasViewer/onhoverSegment.pipe"; import { MouseHoverDirective } from "./mouseover.directive"; -import { MouseOverIconPipe } from "./mouseOverIcon.pipe"; -import { MouseOverTextPipe } from "./mouseOverText.pipe"; +import { MouseOverConvertPipe } from "./mouseOverCvt.pipe"; + @NgModule({ imports: [ @@ -11,15 +11,13 @@ import { MouseOverTextPipe } from "./mouseOverText.pipe"; ], declarations: [ MouseHoverDirective, - MouseOverTextPipe, TransformOnhoverSegmentPipe, - MouseOverIconPipe, + MouseOverConvertPipe, ], exports: [ MouseHoverDirective, - MouseOverTextPipe, TransformOnhoverSegmentPipe, - MouseOverIconPipe, + MouseOverConvertPipe, ] }) diff --git a/src/mouseoverModule/type.ts b/src/mouseoverModule/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..07c4312fa7896c53a7f4a8ca58b842e3cd88dc59 --- /dev/null +++ b/src/mouseoverModule/type.ts @@ -0,0 +1,13 @@ +import { TRegionSummary } from "src/util/siibraApiConstants/types"; + +export type TMouseOverSegment = { + layer: { + name: string + } + segmentId: number + segment: TRegionSummary | string // if cannot decode, then segment will be {ngId}#{labelIndex} +} + +export type TMouseOverVtkLandmark = { + landmarkName: string +} \ No newline at end of file diff --git a/src/mouseoverModule/util.spec..ts b/src/mouseoverModule/util.spec.ts similarity index 91% rename from src/mouseoverModule/util.spec..ts rename to src/mouseoverModule/util.spec.ts index 07453e0343eb31d5e6b1279b3a249a19b5301c2c..31920019b049537cadb4df833acc11a8d8faf20c 100644 --- a/src/mouseoverModule/util.spec..ts +++ b/src/mouseoverModule/util.spec.ts @@ -50,11 +50,12 @@ describe('temporalPositveScanFn', () => { ]).pipe( take(1), ).subscribe(([ arr1, arr2, arr3, arr4 ]) => { - expect(arr1).toEqual([ segmentsPositive ]) - expect(arr2).toEqual([ userLandmarkPostive, segmentsPositive ]) - expect(arr3).toEqual([ userLandmarkPostive ]) + expect(arr1).toEqual([ segmentsPositive ] as any) + expect(arr2).toEqual([ userLandmarkPostive, segmentsPositive ] as any) + expect(arr3).toEqual([ userLandmarkPostive ] as any) expect(arr4).toEqual([]) - }, null, () => done() ) + done() + }) source.next(segmentsPositive) source.next(userLandmarkPostive) diff --git a/src/mouseoverModule/util.ts b/src/mouseoverModule/util.ts index 519202f8878440faf249ad3da41b163e5c66fd4b..ed4fbe50f42c9580f554e19b6e98678438c1b038 100644 --- a/src/mouseoverModule/util.ts +++ b/src/mouseoverModule/util.ts @@ -1,3 +1,14 @@ +import { IAnnotationGeometry } from "src/atlasComponents/userAnnotations/tools/type" +import { TMouseOverSegment } from "./type" + +export type TOnHoverObj = { + segments: TMouseOverSegment[] + annotation: IAnnotationGeometry + landmark: { + landmarkName: number + } + userLandmark: any +} /** * Scan function which prepends newest positive (i.e. defined) value @@ -10,7 +21,7 @@ * * */ -export const temporalPositveScanFn = (acc: Array<{segments: any, landmark: any, userLandmark: any}>, curr: {segments: any, landmark: any, userLandmark: any}) => { +export const temporalPositveScanFn = (acc: Array<TOnHoverObj>, curr: Partial<TOnHoverObj>) => { const keys = Object.keys(curr) @@ -21,6 +32,6 @@ export const temporalPositveScanFn = (acc: Array<{segments: any, landmark: any, ) return isPositive - ? [curr, ...(acc.filter(item => !keys.some(key => !!item[key])))] as Array<{segments?: any, landmark?: any, userLandmark?: any}> + ? [curr, ...(acc.filter(item => !keys.some(key => !!item[key])))] as Array<TOnHoverObj> : acc.filter(item => !keys.some(key => !!item[key])) -} \ No newline at end of file +} diff --git a/src/overwrite.scss b/src/overwrite.scss index 9aa3141ed73802f6b6b75ecc85cade85afdbcbae..c8e7e12e46e899f162b3085f1cd523be12835d24 100644 --- a/src/overwrite.scss +++ b/src/overwrite.scss @@ -1,3 +1,5 @@ +@use 'sass:math'; + iav-cmp-viewer-container { @@ -33,4 +35,39 @@ kg-ds-prv-regional-feature-view { min-height: 20em; } -} \ No newline at end of file +} + +// no prefix +@for $i from 1 through 12 { + $vw: math.div(100vw * $i, 12); + .vw-col-#{$i} { + width: $vw; + } + .vw-col-#{$i}-nm { + margin-left: -1 * $vw; + } +} + +$medias: "-sm","-md","-lg","-xl","-xxl"; +$media-map: ( + "-sm": 576px, + "-md": 768px, + "-lg": 992px, + "-xl": 1200px, + "-xxl": 2000px, +); + +@each $media in $medias { + $size: map-get($media-map, $media); + @media (min-width: $size) { + @for $i from 1 through 12 { + $vw: math.div(100vw * $i, 12); + .vw-col#{$media}-#{$i} { + width: $vw; + } + .vw-col#{$media}-#{$i}-nm { + margin-left: -1 * $vw; + } + } + } +} diff --git a/src/plugin/atlasViewer.pluginService.service.ts b/src/plugin/atlasViewer.pluginService.service.ts index 1c83a6d28b92622728c9653cf0562846cd28e2cb..668027764cf639fe3be327119a12fb4140352f08 100644 --- a/src/plugin/atlasViewer.pluginService.service.ts +++ b/src/plugin/atlasViewer.pluginService.service.ts @@ -375,6 +375,18 @@ export class PluginServices { return handler } + + public async addPluginViaManifestUrl(manifestUrl: string){ + try { + const json = await this.fetch(manifestUrl) + this.fetchedPluginManifests = [ + ...this.fetchedPluginManifests, + json + ] + } catch (e) { + throw new Error(e.statusText) + } + } } export interface IPluginManifest { diff --git a/src/plugin/pluginBanner/pluginBanner.component.ts b/src/plugin/pluginBanner/pluginBanner.component.ts index 59de1360d44b7db00fc1f9e5567371debf03b650..689f0aa32bfc4a7c16442bbc22e56c95d44a2b0d 100644 --- a/src/plugin/pluginBanner/pluginBanner.component.ts +++ b/src/plugin/pluginBanner/pluginBanner.component.ts @@ -1,6 +1,8 @@ import { Component, ViewChild, TemplateRef } from "@angular/core"; import { IPluginManifest, PluginServices } from "../atlasViewer.pluginService.service"; import { MatDialog } from "@angular/material/dialog"; +import { environment } from 'src/environments/environment'; +import { MatSnackBar } from "@angular/material/snack-bar"; @Component({ selector : 'plugin-banner', @@ -12,12 +14,15 @@ import { MatDialog } from "@angular/material/dialog"; export class PluginBannerUI { + EXPERIMENTAL_FEATURE_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG + @ViewChild('pluginInfoTmpl', { read: TemplateRef }) private pluginInfoTmpl: TemplateRef<any> constructor( public pluginServices: PluginServices, private matDialog: MatDialog, + private matSnackbar: MatSnackBar, ) { } @@ -34,4 +39,28 @@ export class PluginBannerUI { } ) } + + public showTmpl(tmpl: TemplateRef<any>){ + this.matDialog.open(tmpl, { + minWidth: '60vw' + }) + } + + public loadingThirdpartyPlugin = false + + public async addThirdPartyPlugin(manifestUrl: string) { + this.loadingThirdpartyPlugin = true + try { + await this.pluginServices.addPluginViaManifestUrl(manifestUrl) + this.loadingThirdpartyPlugin = false + this.matSnackbar.open(`Adding plugin successful`, 'Dismiss', { + duration: 5000 + }) + } catch (e) { + this.loadingThirdpartyPlugin = false + this.matSnackbar.open(`Error adding plugin: ${e.toString()}`, 'Dismiss', { + duration: 5000 + }) + } + } } diff --git a/src/plugin/pluginBanner/pluginBanner.template.html b/src/plugin/pluginBanner/pluginBanner.template.html index 6216ee8247a5ac011f3ec41acf7f58cb7e52d0d3..3e452612ebef6347181b825c02c22c5ba2ce8c74 100644 --- a/src/plugin/pluginBanner/pluginBanner.template.html +++ b/src/plugin/pluginBanner/pluginBanner.template.html @@ -15,8 +15,46 @@ {{ plugin.displayName ? plugin.displayName : plugin.name }} </span> </button> + + <button mat-menu-item *ngIf="EXPERIMENTAL_FEATURE_FLAG" + (click)="showTmpl(thirdPartyPluginTmpl)"> + <span> + Add third party plugin + </span> + </button> </mat-action-list> +<ng-template #thirdPartyPluginTmpl> + <h2 mat-dialog-title> + Add thirdparty plugin + </h2> + + <mat-dialog-content> + <form> + <mat-form-field class="d-block"> + <mat-label> + manifest.json URL + </mat-label> + <input type="text" matInput placeholder="https://example.com/manifest.json" #urlInput> + </mat-form-field> + </form> + </mat-dialog-content> + + <mat-dialog-actions align="end"> + <button (click)="addThirdPartyPlugin(urlInput.value)" + mat-raised-button + [disabled]="loadingThirdpartyPlugin" + color="primary"> + Load + </button> + <button mat-dialog-close + mat-button> + cancel + </button> + </mat-dialog-actions> + +</ng-template> + <ng-template #pluginInfoTmpl let-manifest> <h1 mat-dialog-title> About {{ manifest.displayName || manifest.name }} diff --git a/src/routerModule/router.service.spec.ts b/src/routerModule/router.service.spec.ts index 2b933c6f1a5711b8503776d082203e684299f563..a06a700ff5c0d726381c0ecb9e3de24d08170bc7 100644 --- a/src/routerModule/router.service.spec.ts +++ b/src/routerModule/router.service.spec.ts @@ -267,27 +267,56 @@ describe('> router.service.ts', () => { discardPeriodicTasks() })) - it('> ... returns same value, does not dispatches', fakeAsync(() => { - const fakeParsedState = { - bizz: 'buzz' - } - cvtFullRouteToStateSpy.and.callFake(() => fakeParsedState) - cvtStateToHashedRoutesSpy.and.callFake(() => { - return `foo/bar` - }) - router = TestBed.inject(Router) - router.navigate(['foo', 'bar']) - - const service = TestBed.inject(RouterService) - const store = TestBed.inject(MockStore) - const dispatchSpy = spyOn(store, 'dispatch') + describe('> returns the same value', () => { + it('> ... returns same value, does not dispatches', fakeAsync(() => { + const fakeParsedState = { + bizz: 'buzz' + } + cvtFullRouteToStateSpy.and.callFake(() => fakeParsedState) + cvtStateToHashedRoutesSpy.and.callFake(() => { + return `foo/bar` + }) + router = TestBed.inject(Router) + router.navigate(['foo', 'bar']) + + const service = TestBed.inject(RouterService) + const store = TestBed.inject(MockStore) + const dispatchSpy = spyOn(store, 'dispatch') + + tick(320) + + expect(dispatchSpy).not.toHaveBeenCalled() + + discardPeriodicTasks() + })) - tick(320) + it('> takes into account of customRoute', fakeAsync(() => { + const fakeParsedState = { + bizz: 'buzz' + } + cvtFullRouteToStateSpy.and.callFake(() => fakeParsedState) + cvtStateToHashedRoutesSpy.and.callFake(() => { + return `foo/bar` + }) + + const service = TestBed.inject(RouterService) + service.customRoute$ = of({ + 'x-foo': 'hello' + }) - expect(dispatchSpy).not.toHaveBeenCalled() - - discardPeriodicTasks() - })) + router = TestBed.inject(Router) + router.navigate(['foo', 'bar', 'x-foo:hello']) + + const store = TestBed.inject(MockStore) + const dispatchSpy = spyOn(store, 'dispatch') + + tick(320) + + expect(dispatchSpy).not.toHaveBeenCalled() + + discardPeriodicTasks() + })) + }) }) }) }) diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts index ee0246f79302d60fae4c9caa8477c65120929d52..a17b554adcc8d430c279d7c241f8f96438ca018d 100644 --- a/src/routerModule/router.service.ts +++ b/src/routerModule/router.service.ts @@ -7,7 +7,7 @@ import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith import { generalApplyState } from "src/services/stateStore.helper"; import { PureContantService } from "src/util"; import { cvtStateToHashedRoutes, cvtFullRouteToState, encodeCustomState, decodeCustomState, verifyCustomState } from "./util"; -import { BehaviorSubject, combineLatest, merge, Observable } from 'rxjs' +import { BehaviorSubject, combineLatest, merge, Observable, of } from 'rxjs' import { scan } from 'rxjs/operators' @Injectable({ @@ -45,7 +45,7 @@ export class RouterService { // could be navigation (history api) // could be on init const navEnd$ = router.events.pipe( - filter(ev => ev instanceof NavigationEnd), + filter<NavigationEnd>(ev => ev instanceof NavigationEnd), shareReplay(1) ) @@ -94,10 +94,17 @@ export class RouterService { ready$.pipe( switchMapTo( navEnd$.pipe( - withLatestFrom(store$) + withLatestFrom( + store$, + this.customRoute$.pipe( + startWith({}) + ) + ) ) ) - ).subscribe(([ev, state]: [NavigationEnd, any]) => { + ).subscribe(arg => { + const [ev, state, customRoutes] = arg + const fullPath = ev.urlAfterRedirects const stateFromRoute = cvtFullRouteToState(router.parseUrl(fullPath), state, this.logError) let routeFromState: string @@ -107,6 +114,12 @@ export class RouterService { routeFromState = `` } + for (const key in customRoutes) { + const customStatePath = encodeCustomState(key, customRoutes[key]) + if (!customStatePath) continue + routeFromState += `/${customStatePath}` + } + if ( fullPath !== `/${routeFromState}`) { store$.dispatch( generalApplyState({ @@ -135,10 +148,10 @@ export class RouterService { ), this.customRoute$, ]).pipe( - map(([ routePath, customPath ]) => { + map(([ routePath, customRoutes ]) => { let returnPath = routePath - for (const key in customPath) { - const customStatePath = encodeCustomState(key, customPath[key]) + for (const key in customRoutes) { + const customStatePath = encodeCustomState(key, customRoutes[key]) if (!customStatePath) continue returnPath += `/${customStatePath}` } diff --git a/src/services/state/uiState/actions.ts b/src/services/state/uiState/actions.ts index 7f3f864483c29e791532642da6351cfd42e0b395..ae2fbc3bd9535428de9c3912a967ea71ee298c2c 100644 --- a/src/services/state/uiState/actions.ts +++ b/src/services/state/uiState/actions.ts @@ -26,7 +26,7 @@ export const uiStateShowBottomSheet = createAction( export const uiActionMouseoverLandmark = createAction( `[uiState] mouseoverLandmark`, - props<{ landmark: any }>() + props<{ landmark: string }>() ) export const uiActionMouseoverSegments = createAction( diff --git a/src/services/state/uiState/common.ts b/src/services/state/uiState/common.ts index 1ba2e621ec2db19bc0f24bcae524ba33a847c326..dd5da3140f280c9cef5c7cd29637c2577f47587f 100644 --- a/src/services/state/uiState/common.ts +++ b/src/services/state/uiState/common.ts @@ -12,7 +12,7 @@ export interface IUiState{ sidePanelExploreCurrentViewIsOpen: boolean mouseOverSegment: any | number - mouseOverLandmark: any + mouseOverLandmark: string mouseOverUserLandmark: any focusedSidePanel: string | null diff --git a/src/services/state/uiState/selectors.spec.ts b/src/services/state/uiState/selectors.spec.ts index 8f26ad8b2ade3ba0d0e9cc4d6873738fbc3fefc7..2464c9c8a1a4770a1e1c14afd28cb7c3dd4a3833 100644 --- a/src/services/state/uiState/selectors.spec.ts +++ b/src/services/state/uiState/selectors.spec.ts @@ -2,26 +2,6 @@ import { uiStateMouseOverSegmentsSelector } from './selectors' describe('> uiState/selectors.ts', () => { describe('> mouseOverSegments', () => { - it('> should filter out regions explicitly designated as unselectable', () => { - const unSelSeg = { - segment: { - unselectable: true - } - } - const selSeg0 = { - segment: 1 - } - - const selSeg1 = { - segment: { - name: 'hello world', - unselectable: false - } - } - const filteredResult = uiStateMouseOverSegmentsSelector.projector([unSelSeg, selSeg0, selSeg1]) - - expect(filteredResult).toEqual([selSeg0, selSeg1]) - }) }) }) diff --git a/src/services/state/uiState/selectors.ts b/src/services/state/uiState/selectors.ts index f72757b436adbf77e19becde70cff55213484504..483539b2d784f567564bc2963233b8dd91dabcb1 100644 --- a/src/services/state/uiState/selectors.ts +++ b/src/services/state/uiState/selectors.ts @@ -1,4 +1,5 @@ import { createSelector } from "@ngrx/store"; +import { TMouseOverSegment } from "src/mouseoverModule/type"; import { IUiState } from './common' export const uiStatePreviewingDatasetFilesSelector = createSelector( @@ -7,19 +8,13 @@ export const uiStatePreviewingDatasetFilesSelector = createSelector( ) export const uiStateMouseOverSegmentsSelector = createSelector( - state => state['uiState']['mouseOverSegments'], - mouseOverSegments => { - /** - * filter out the regions explicitly declared `unselectable` - */ - return mouseOverSegments - .filter(({ segment }) => { - if (typeof segment === 'object' && segment !== null) { - if (segment.unselectable) return false - } - return true - }) - } + state => state['uiState'], + uiState => uiState['mouseOverSegments'] as TMouseOverSegment[] +) + +export const uiStateMouseOverLandmarkSelector = createSelector( + state => state['uiState'], + uiState => uiState['mouseOverLandmark'] as string ) export const uiStateMouseoverUserLandmark = createSelector( diff --git a/src/services/state/viewerState/actions.ts b/src/services/state/viewerState/actions.ts index 74d44f9b86e3c5bf2327f00b64a3581f9317e436..d20ff0cd29af5855535d0ca27fc519e9204aa360 100644 --- a/src/services/state/viewerState/actions.ts +++ b/src/services/state/viewerState/actions.ts @@ -98,7 +98,7 @@ export const viewerStateSetViewerMode = createAction( export const viewerStateDblClickOnViewer = createAction( `[viewerState] dblClickOnViewer`, - props<{ payload: { segments: any, landmark: any, userLandmark: any } }>() + props<{ payload: { annotation: any, segments: any, landmark: any, userLandmark: any } }>() ) export const viewerStateAddUserLandmarks = createAction( diff --git a/src/ui/logoContainer/logoContainer.component.ts b/src/ui/logoContainer/logoContainer.component.ts index 6b3ce834c7ae34d1459fc1965ab51dec8c14b3a1..105e0726251a75522bc2d747767c0fe7b3eb50c2 100644 --- a/src/ui/logoContainer/logoContainer.component.ts +++ b/src/ui/logoContainer/logoContainer.component.ts @@ -3,6 +3,9 @@ import { PureContantService } from "src/util"; import { Subscription } from "rxjs"; import { distinctUntilChanged } from "rxjs/operators"; +const imageDark = 'assets/logo/ebrains-logo-dark.svg' +const imageLight = 'assets/logo/ebrains-logo-light.svg' + @Component({ selector : 'logo-container', templateUrl : './logoContainer.template.html', @@ -13,10 +16,10 @@ import { distinctUntilChanged } from "rxjs/operators"; export class LogoContainer { // only used to define size - public imgSrc = `${this.pureConstantService.backendUrl}logo` + public imgSrc = imageDark public containerStyle = { - backgroundImage: `url('${this.pureConstantService.backendUrl}logo')` + backgroundImage: `url('${this.imgSrc}')` } private subscriptions: Subscription[] = [] @@ -28,7 +31,7 @@ export class LogoContainer { distinctUntilChanged() ).subscribe(flag => { this.containerStyle = { - backgroundImage: `url('${this.pureConstantService.backendUrl}logo${!!flag ? '?darktheme=true' : ''}')` + backgroundImage: `url('${flag ? imageLight : imageDark}')` } }) ) diff --git a/src/util/directives/switch.directive.ts b/src/util/directives/switch.directive.ts index eb582253a951b061958c69ff19dd1317c97ab30f..89dd445186afe7d49e2ee8c7b7d50a27429edff7 100644 --- a/src/util/directives/switch.directive.ts +++ b/src/util/directives/switch.directive.ts @@ -1,16 +1,28 @@ import { Directive, Input, Output, EventEmitter } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; @Directive({ selector: '[iav-switch]', exportAs: 'iavSwitch' }) export class SwitchDirective{ - @Input('iav-switch-initstate') switchState: boolean = false + + switchState: boolean = false + @Input('iav-switch-delay') delay: number = 0 @Output('iav-switch-event') eventemitter: EventEmitter<boolean> = new EventEmitter() - emit(flag){ + @Input('iav-switch-state') + set setSwitchState(val: boolean) { + this.switchState = val + this.emit() + } + + public switchState$ = new BehaviorSubject(this.switchState) + + emit(){ this.eventemitter.emit(this.switchState) + this.switchState$.next(this.switchState) } toggle(){ diff --git a/src/util/pipes/combineFn.pipe.ts b/src/util/pipes/combineFn.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..cfd98e4e11c293cfbb7fd09541312ccc2b97e4b3 --- /dev/null +++ b/src/util/pipes/combineFn.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'combineFn', + pure: true +}) +export class CombineFnPipe implements PipeTransform{ + public transform(fns: CallableFunction[]): CallableFunction{ + return () => { + for (const fn of fns) fn() + } + } +} diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts index ba33d3b9388d8acbde91776d3826230be0074487..5d69b140153b700a2d76e35070602b6fba98dc23 100644 --- a/src/util/pureConstant.service.ts +++ b/src/util/pureConstant.service.ts @@ -56,6 +56,20 @@ type TIAVAtlas = { } & THasId)[] } & THasId +type TNehubaConfig = Record<string, { + source: string + transform: number[][] + type: 'segmentation' | 'image' +}> + +type TViewerConfig = TNehubaConfig + +/** + * key value pair of + * atlasId -> templateId -> viewerConfig + */ +type TAtlasTmplViewerConfig = Record<string, Record<string, TViewerConfig>> + export const spaceMiscInfoMap = new Map([ ['minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588', { name: 'bigbrain', @@ -514,12 +528,20 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" shareReplay(1) ) + private atlasTmplConfig: TAtlasTmplViewerConfig = {} + + async getViewerConfig(atlasId: string, templateId: string, parcId: string) { + const atlasLayers = this.atlasTmplConfig[atlasId] + const templateLayers = atlasLayers && atlasLayers[templateId] + return templateLayers || {} + } + public initFetchTemplate$ = this.fetchedAtlases$.pipe( switchMap(atlases => { return forkJoin( atlases.map(atlas => this.getSpacesAndParc(atlas['@id']).pipe( switchMap(({ templateSpaces, parcellations }) => { - const ngLayerObj = {} + this.atlasTmplConfig[atlas["@id"]] = {} return forkJoin( templateSpaces.map( tmpl => { @@ -534,7 +556,7 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" name: 'Julich-Brain Probabilistic Cytoarchitectonic Maps (v2.9)' }) } - ngLayerObj[tmpl.id] = {} + this.atlasTmplConfig[atlas["@id"]][tmpl.id] = {} return tmpl.availableParcellations.map( parc => this.getRegions(atlas['@id'], parc.id, tmpl.id).pipe( tap(regions => { @@ -557,7 +579,7 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" const ngId = getNgId(atlas['@id'], tmpl.id, parc.id, dedicatedMap[0]['@id']) region['ngId'] = ngId region['labelIndex'] = dedicatedMap[0].detail['neuroglancer/precomputed'].labelIndex - ngLayerObj[tmpl.id][ngId] = { + this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngId] = { source: `precomputed://${dedicatedMap[0].url}`, type: "segmentation", transform: dedicatedMap[0].detail['neuroglancer/precomputed'].transform @@ -602,7 +624,7 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" const key = 'whole brain' const ngIdKey = getNgId(atlas['@id'], tmpl.id, parseId(parc.id), key) - ngLayerObj[tmpl.id][ngIdKey] = { + this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngIdKey] = { source: `precomputed://${vol.url}`, type: "segmentation", transform: vol.detail['neuroglancer/precomputed'].transform @@ -619,7 +641,7 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" }] for (const { key, mapIndex } of mapIndexKey) { const ngIdKey = getNgId(atlas['@id'], tmpl.id, parseId(parc.id), key) - ngLayerObj[tmpl.id][ngIdKey] = { + this.atlasTmplConfig[atlas["@id"]][tmpl.id][ngIdKey] = { source: `precomputed://${precomputedVols[mapIndex].url}`, type: "segmentation", transform: precomputedVols[mapIndex].detail['neuroglancer/precomputed'].transform @@ -640,7 +662,7 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" } ).reduce(flattenReducer, []) ).pipe( - mapTo({ templateSpaces, parcellations, ngLayerObj }) + mapTo({ templateSpaces, parcellations, ngLayerObj: this.atlasTmplConfig }) ) }), map(({ templateSpaces, parcellations, ngLayerObj }) => { @@ -779,8 +801,8 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" } } - for (const key in (ngLayerObj[tmpl.id] || {})) { - initialLayers[key] = ngLayerObj[tmpl.id][key] + for (const key in (ngLayerObj[atlas["@id"]][tmpl.id] || {})) { + initialLayers[key] = ngLayerObj[atlas["@id"]][tmpl.id][key] } return { diff --git a/src/util/util.module.ts b/src/util/util.module.ts index bde4b907f1d01f911d5d9886ed967ba5b4698f3f..f128cf3fd266a1851d2951bf21c78ffffd4cedfd 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -20,6 +20,7 @@ import { GetPropertyPipe } from "./pipes/getProperty.pipe"; import { FilterArrayPipe } from "./pipes/filterArray.pipe"; import { DoiParserPipe } from "./pipes/doiPipe.pipe"; import { GetFilenamePipe } from "./pipes/getFilename.pipe"; +import { CombineFnPipe } from "./pipes/combineFn.pipe"; import { MergeObjPipe } from "./mergeObj.pipe"; @NgModule({ @@ -46,6 +47,7 @@ import { MergeObjPipe } from "./mergeObj.pipe"; FilterArrayPipe, DoiParserPipe, GetFilenamePipe, + CombineFnPipe, MergeObjPipe, ], exports: [ @@ -68,6 +70,7 @@ import { MergeObjPipe } from "./mergeObj.pipe"; FilterArrayPipe, DoiParserPipe, GetFilenamePipe, + CombineFnPipe, MergeObjPipe, ] }) diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 248b72fe0285b2e5510e948bebbe5f23da1d1f07..8281c6565f4f6c717e8586877285781f631a1069 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, Inject, Optional } from "@angular/core"; +import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, Inject, Optional, ChangeDetectionStrategy } from "@angular/core"; import { fromEvent, Subscription, ReplaySubject, BehaviorSubject, Observable, race, timer, Subject } from 'rxjs' import { debounceTime, filter, map, scan, startWith, mapTo, switchMap, take, skip, tap, distinctUntilChanged } from "rxjs/operators"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; @@ -62,6 +62,8 @@ export const scanFn = (acc: LayerLabelIndex[], curr: LayerLabelIndex) => { styleUrls : [ './nehubaViewer.style.css', ], + // OnPush seems to improve performance significantly + changeDetection: ChangeDetectionStrategy.OnPush }) export class NehubaViewerUnit implements OnInit, OnDestroy { @@ -94,7 +96,7 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { url?: string } }> = new EventEmitter() - @Output() public mouseoverLandmarkEmitter: EventEmitter<number | null> = new EventEmitter() + @Output() public mouseoverLandmarkEmitter: EventEmitter<string> = new EventEmitter() @Output() public mouseoverUserlandmarkEmitter: EventEmitter<string> = new EventEmitter() @Output() public regionSelectionEmitter: EventEmitter<{segment: number, layer: {name?: string, url?: string}}> = new EventEmitter() @Output() public errorEmitter: EventEmitter<any> = new EventEmitter() diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts index 4b081a97abd78533d6ab06c6202fb587453b23b9..8aa04373cf3c7d7a8c145d221d30c3d4f1b15a3e 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts @@ -183,9 +183,10 @@ describe('> nehubaViewerGlue.component.ts', () => { const testObj0 = { segment: 'hello world' } + const testObj1 = 'hello world' beforeEach(() => { fallbackSpy = spyOn(clickIntServ, 'fallback') - mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, ['hello world', testObj0]) + mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, [testObj1, testObj0] as any) TestBed.createComponent(NehubaGlueCmp) clickIntServ.callRegFns(null) }) @@ -214,7 +215,7 @@ describe('> nehubaViewerGlue.component.ts', () => { } beforeEach(() => { fallbackSpy = spyOn(clickIntServ, 'fallback') - mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, [testObj0, testObj1, testObj2]) + mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, [testObj0, testObj1, testObj2] as any) }) afterEach(() => { diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 15504851b024378aeb96c8c96ef2f04d73489842..f8df7f66789021d41fe5f0435e6b24ae83a27c39 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -642,7 +642,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A map(({ segments }) => segments) ), mouseOverNehubaUI: this.mouseoverDirective.currentOnHoverObs$.pipe( - map(({ landmark, segments, userLandmark: customLandmark }) => ({ segments, landmark, customLandmark })), + map(({annotation, landmark, segments, userLandmark: customLandmark }) => ({annotation, segments, landmark, customLandmark })), shareReplay(1), ), getNgHash : this.nehubaContainerDirective.nehubaViewerInstance.getNgHash, diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.style.css b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.style.css index a03d05dd199a30e712eb7062ee253473ae894552..c859c65655905d25cd4f65fe332d3c3567cc6868 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.style.css +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.style.css @@ -29,3 +29,13 @@ { background-color: rgba(0, 0, 0, 0.7); } + +.nehuba-viewer-container-parent +{ + z-index: 1; +} + +current-layout +{ + z-index: 2; +} diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html index 73aeec9212a1f91a1fa8c16ce1c047e655a0177d..104c5add581a8bcb216d1afe8d15b42002657938 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html @@ -1,4 +1,4 @@ -<div class="d-block w-100 h-100" +<div class="d-block w-100 h-100 nehuba-viewer-container-parent" (touchmove)="$event.preventDefault()" (drag-drop-file)="handleFileDrop($event)" iav-viewer-touch-interface @@ -122,7 +122,7 @@ let-panelIndex="panelIndex" let-visible="visible"> - <div class="opacity-crossfade always-show-touchdevice pe-all overlay-btn-container" + <div class="ws-no-wrap opacity-crossfade always-show-touchdevice pe-all overlay-btn-container" [ngClass]="{ onHover: visible }" [attr.data-viewer-controller-visible]="visible" [attr.data-viewer-controller-index]="panelIndex"> @@ -220,6 +220,7 @@ <div *ngIf="data.moreInfoFlag" class="iv-custom-comp darker-bg overflow-hidden grid-wide-3"> <ng-layer-tune + advanced-control="true" [ngLayerName]="data.layerName" [thresholdMin]="data.min" [thresholdMax]="data.max"> diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index a6daa233d6acdc5197d19d4a9dbc284c3e26f329..6ac175a1d93dba044ed00e3a7d5e09bc6857dd31 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -11,7 +11,7 @@ import { select, Store } from "@ngrx/store"; import { LoggingService } from "src/logging"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; import { Observable, Subscription, of, combineLatest } from "rxjs"; -import { map, filter, startWith } from "rxjs/operators"; +import { map, filter, startWith, throttleTime } from "rxjs/operators"; import { MatBottomSheet } from "@angular/material/bottom-sheet"; import { MatDialog } from "@angular/material/dialog"; import { ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' @@ -50,7 +50,6 @@ export class StatusCardComponent implements OnInit, OnChanges{ public navVal$: Observable<string> public mouseVal$: Observable<string> - public useTouchInterface$: Observable<boolean> public quickTourData: IQuickTourData = { description: QUICKTOUR_DESC.STATUS_CARD, @@ -70,7 +69,6 @@ export class StatusCardComponent implements OnInit, OnChanges{ private dialog: MatDialog, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable<NehubaViewerUnit> ) { - this.useTouchInterface$ = of(true) //this.pureConstantService.useTouchUI$ if (nehubaViewer$) { this.subscriptions.push( @@ -128,10 +126,12 @@ export class StatusCardComponent implements OnInit, OnChanges{ this.mouseVal$ = combineLatest([ this.statusPanelRealSpace$, this.nehubaViewer.mousePosInReal$.pipe( - filter(v => !!v) + filter(v => !!v), + throttleTime(16) ), this.nehubaViewer.mousePosInVoxel$.pipe( - filter(v => !!v) + filter(v => !!v), + throttleTime(16) ) ]).pipe( map(([realFlag, real, voxel]) => realFlag diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html index 69d2a44547d2761e2c20c96f5a7d1bb155dcf535..d3086513b1c12eb6207f9b142a66777cb59cde88 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.template.html +++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html @@ -89,7 +89,7 @@ </div> <!-- cursor pos --> - <mat-form-field *ngIf="!(useTouchInterface$ | async)" + <mat-form-field class="w-100"> <mat-label> Cursor Position @@ -115,17 +115,6 @@ {{ navVal$ | async }} </span> - <!-- only show cursor if touch interface is not on --> - <span *ngIf="false && !(useTouchInterface$ | async)"> - <!-- padding --> - <span class="pl-4"></span> - - <i aria-label="cursor location" class="fas fa-mouse-pointer"></i> - <span class="pl2"> - {{ mouseVal$ | async }} - </span> - </span> - <mat-divider [vertical]="true"></mat-divider> <button mat-icon-button diff --git a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts index 44a1fb490ccdfeb69020a48fa9a25154ef92a9dd..54b6e08fa3d70dea75042b7372251e8be51e2de7 100644 --- a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts +++ b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts @@ -7,7 +7,7 @@ import { ComponentsModule } from "src/components" import { ngViewerSelectorOctantRemoval } from "src/services/state/ngViewerState.store.helper" import { viewerStateCustomLandmarkSelector, viewerStateSelectedTemplatePureSelector } from "src/services/state/viewerState/selectors" import { AngularMaterialModule } from "src/sharedModules" -import { UtilModule } from "src/util" +import {PureContantService, UtilModule} from "src/util" import { actionSetAuxMeshes, selectorAuxMeshes } from "../../store" import { NEHUBA_INSTANCE_INJTKN } from "../../util" import { ViewerCtrlCmp } from "./viewerCtrlCmp.component" @@ -20,12 +20,31 @@ describe('> viewerCtrlCmp.component.ts', () => { let fixture: ComponentFixture<ViewerCtrlCmp> let loader: HarnessLoader let mockStore: MockStore + let mockNehubaViewer = { - updateUserLandmarks: jasmine.createSpy() + updateUserLandmarks: jasmine.createSpy(), + nehubaViewer: { + ngviewer: { + layerManager: { + getLayerByName: jasmine.createSpy('getLayerByName'), + get managedLayers() { + return [] + }, + set managedLayers(val) { + return + } + }, + display: { + scheduleRedraw: jasmine.createSpy('scheduleRedraw') + } + } + } } afterEach(() => { mockNehubaViewer.updateUserLandmarks.calls.reset() + mockNehubaViewer.nehubaViewer.ngviewer.layerManager.getLayerByName.calls.reset() + mockNehubaViewer.nehubaViewer.ngviewer.display.scheduleRedraw.calls.reset() }) beforeEach( async () => { @@ -43,7 +62,17 @@ describe('> viewerCtrlCmp.component.ts', () => { provideMockStore(), { provide: NEHUBA_INSTANCE_INJTKN, - useValue: new BehaviorSubject(mockNehubaViewer) + useFactory: () => { + return new BehaviorSubject(mockNehubaViewer).asObservable() + } + }, + { + provide: PureContantService, + useFactory: () => { + return { + getViewerConfig: jasmine.createSpy('getViewerConfig') + } + } } ] }).compileComponents() @@ -54,12 +83,12 @@ describe('> viewerCtrlCmp.component.ts', () => { mockStore.overrideSelector(viewerStateSelectedTemplatePureSelector, {}) mockStore.overrideSelector(ngViewerSelectorOctantRemoval, true) mockStore.overrideSelector(viewerStateCustomLandmarkSelector, []) + mockStore.overrideSelector(selectorAuxMeshes, []) }) describe('> can be init', () => { beforeEach(() => { - mockStore.overrideSelector(selectorAuxMeshes, []) fixture = TestBed.createComponent(ViewerCtrlCmp) fixture.detectChanges() loader = TestbedHarnessEnvironment.loader(fixture) @@ -207,5 +236,120 @@ describe('> viewerCtrlCmp.component.ts', () => { ) }) }) + + describe('> flagDelin', () => { + let toggleParcVsblSpy: jasmine.Spy + beforeEach(() => { + fixture = TestBed.createComponent(ViewerCtrlCmp) + toggleParcVsblSpy = spyOn(fixture.componentInstance as any, 'toggleParcVsbl') + fixture.detectChanges() + }) + it('> calls toggleParcVsbl', () => { + toggleParcVsblSpy.and.callFake(() => {}) + fixture.componentInstance.flagDelin = false + expect(toggleParcVsblSpy).toHaveBeenCalled() + }) + }) + describe('> toggleParcVsbl', () => { + let getViewerConfigSpy: jasmine.Spy + let getLayerByNameSpy: jasmine.Spy + beforeEach(() => { + const pureCstSvc = TestBed.inject(PureContantService) + getLayerByNameSpy = mockNehubaViewer.nehubaViewer.ngviewer.layerManager.getLayerByName + getViewerConfigSpy = pureCstSvc.getViewerConfig as jasmine.Spy + fixture = TestBed.createComponent(ViewerCtrlCmp) + fixture.detectChanges() + }) + + it('> calls pureSvc.getViewerConfig', async () => { + getViewerConfigSpy.and.returnValue({}) + await fixture.componentInstance['toggleParcVsbl']() + expect(getViewerConfigSpy).toHaveBeenCalled() + }) + + describe('> if _flagDelin is true', () => { + beforeEach(() => { + fixture.componentInstance['_flagDelin'] = true + fixture.componentInstance['hiddenLayerNames'] = [ + 'foo', + 'bar', + 'baz' + ] + }) + it('> go through all hideen layer names and set them to true', async () => { + const setVisibleSpy = jasmine.createSpy('setVisible') + getLayerByNameSpy.and.returnValue({ + setVisible: setVisibleSpy + }) + await fixture.componentInstance['toggleParcVsbl']() + expect(getLayerByNameSpy).toHaveBeenCalledTimes(3) + for (const arg of ['foo', 'bar', 'baz']) { + expect(getLayerByNameSpy).toHaveBeenCalledWith(arg) + } + expect(setVisibleSpy).toHaveBeenCalledTimes(3) + expect(setVisibleSpy).toHaveBeenCalledWith(true) + expect(setVisibleSpy).not.toHaveBeenCalledWith(false) + }) + it('> hiddenLayerNames resets', async () => { + await fixture.componentInstance['toggleParcVsbl']() + expect(fixture.componentInstance['hiddenLayerNames']).toEqual([]) + }) + }) + + describe('> if _flagDelin is false', () => { + let managedLayerSpyProp: jasmine.Spy + let setVisibleSpy: jasmine.Spy + beforeEach(() => { + fixture.componentInstance['_flagDelin'] = false + setVisibleSpy = jasmine.createSpy('setVisible') + getLayerByNameSpy.and.returnValue({ + setVisible: setVisibleSpy + }) + getViewerConfigSpy.and.resolveTo({ + 'foo': {}, + 'bar': {}, + 'baz': {} + }) + managedLayerSpyProp = spyOnProperty(mockNehubaViewer.nehubaViewer.ngviewer.layerManager, 'managedLayers') + managedLayerSpyProp.and.returnValue([{ + visible: true, + name: 'foo' + }, { + visible: false, + name: 'bar' + }, { + visible: true, + name: 'baz' + }]) + }) + + afterEach(() => { + managedLayerSpyProp.calls.reset() + }) + + it('> calls schedulRedraw', async () => { + await fixture.componentInstance['toggleParcVsbl']() + await new Promise(rs => requestAnimationFrame(rs)) + expect(mockNehubaViewer.nehubaViewer.ngviewer.display.scheduleRedraw).toHaveBeenCalled() + }) + + it('> only calls setVisible false on visible layers', async () => { + await fixture.componentInstance['toggleParcVsbl']() + expect(getLayerByNameSpy).toHaveBeenCalledTimes(2) + + for (const arg of ['foo', 'baz']) { + expect(getLayerByNameSpy).toHaveBeenCalledWith(arg) + } + expect(setVisibleSpy).toHaveBeenCalledTimes(2) + expect(setVisibleSpy).toHaveBeenCalledWith(false) + expect(setVisibleSpy).not.toHaveBeenCalledWith(true) + }) + + it('> sets hiddenLayerNames correctly', async () => { + await fixture.componentInstance['toggleParcVsbl']() + expect(fixture.componentInstance['hiddenLayerNames']).toEqual(['foo', 'baz']) + }) + }) + }) }) }) diff --git a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.ts b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.ts index cbe0380a6c5a06350b7ec29287072094343a25c2..d46a84cfab7d129e94d0c542fd718fa7390e0b6c 100644 --- a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.ts +++ b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.ts @@ -1,15 +1,16 @@ import { Component, HostBinding, Inject, Optional } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { combineLatest, merge, Observable, of, Subscription } from "rxjs"; -import { filter, map, pairwise, withLatestFrom } from "rxjs/operators"; +import {filter, map, pairwise, withLatestFrom} from "rxjs/operators"; import { ngViewerActionSetPerspOctantRemoval } from "src/services/state/ngViewerState/actions"; import { ngViewerSelectorOctantRemoval } from "src/services/state/ngViewerState/selectors"; -import { viewerStateCustomLandmarkSelector, viewerStateSelectedTemplatePureSelector } from "src/services/state/viewerState/selectors"; +import { viewerStateCustomLandmarkSelector, viewerStateGetSelectedAtlas, viewerStateSelectedTemplatePureSelector } from "src/services/state/viewerState/selectors"; import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; import { ARIA_LABELS } from 'common/constants' import { actionSetAuxMeshes, selectorAuxMeshes } from "../../store"; import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; +import {PureContantService} from "src/util"; @Component({ selector: 'viewer-ctrl-component', @@ -27,6 +28,9 @@ export class ViewerCtrlCmp{ @HostBinding('attr.darktheme') darktheme = false + private selectedAtlasId: string + private selectedTemplateId: string + private _flagDelin = true get flagDelin(){ return this._flagDelin @@ -44,6 +48,7 @@ export class ViewerCtrlCmp{ return this._removeOctantFlag } set removeOctantFlag(val){ + if (val === this._removeOctantFlag) return this._removeOctantFlag = val this.setOctantRemoval(this._removeOctantFlag) } @@ -68,9 +73,16 @@ export class ViewerCtrlCmp{ select(selectorAuxMeshes), ) + private nehubaInst: NehubaViewerUnit + + get ngViewer() { + return this.nehubaInst?.nehubaViewer.ngviewer || (window as any).viewer + } + constructor( private store$: Store<any>, formBuilder: FormBuilder, + private pureConstantService: PureContantService, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaInst$: Observable<NehubaViewerUnit>, ){ @@ -83,20 +95,25 @@ export class ViewerCtrlCmp{ this.customLandmarks$, this.nehubaInst$, ]).pipe( - filter(([_, neubaInst]) => !!neubaInst), + filter(([_, nehubaInst]) => !!nehubaInst), ).subscribe(([landmarks, nehubainst]) => { this.setOctantRemoval(landmarks.length === 0) nehubainst.updateUserLandmarks(landmarks) - }) + }), + this.nehubaInst$.subscribe(nehubaInst => this.nehubaInst = nehubaInst) ) } else { console.warn(`NEHUBA_INSTANCE_INJTKN not provided`) } this.sub.push( + this.store$.select(viewerStateGetSelectedAtlas) + .pipe(filter(a => !!a)) + .subscribe(sa => this.selectedAtlasId = sa['@id']), this.store$.pipe( select(viewerStateSelectedTemplatePureSelector) ).subscribe(tmpl => { + this.selectedTemplateId = tmpl['@id'] const { useTheme } = tmpl || {} this.darktheme = useTheme === 'dark' }), @@ -152,29 +169,32 @@ export class ViewerCtrlCmp{ ) } - private toggleParcVsbl(){ - const visibleParcLayers = ((window as any).viewer.layerManager.managedLayers) - .slice(1) - .filter(({ visible }) => visible) - .filter(layer => !this.auxMeshesNamesSet.has(layer.name)) + private async toggleParcVsbl(){ + const viewerConfig = await this.pureConstantService.getViewerConfig(this.selectedAtlasId, this.selectedTemplateId, null) if (this.flagDelin) { for (const name of this.hiddenLayerNames) { - const l = (window as any).viewer.layerManager.getLayerByName(name) + const l = this.ngViewer.layerManager.getLayerByName(name) l && l.setVisible(true) } this.hiddenLayerNames = [] } else { this.hiddenLayerNames = [] - for (const { name } of visibleParcLayers) { - const l = (window as any).viewer.layerManager.getLayerByName(name) + const segLayerNames: string[] = [] + for (const layer of this.ngViewer.layerManager.managedLayers) { + if (layer.visible && layer.name in viewerConfig) { + segLayerNames.push(layer.name) + } + } + for (const name of segLayerNames) { + const l = this.ngViewer.layerManager.getLayerByName(name) l && l.setVisible(false) this.hiddenLayerNames.push( name ) } } - - setTimeout(() => { - (window as any).viewer.display.scheduleRedraw() + + requestAnimationFrame(() => { + this.ngViewer.display.scheduleRedraw() }) } diff --git a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.template.html b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.template.html index 60a004bbd682af81cecb8244ce2207d42a85c55e..76aa348a8f659039b6c0ece363db4a79921be756 100644 --- a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.template.html +++ b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.template.html @@ -8,8 +8,8 @@ (iav-key-event)="delinToggle.toggle()" name="toggle-delineation"> - <markdown-dom class="d-inline-block iv-custom-comp text"> - Show delineations `[q]` + <markdown-dom class="d-inline-block iv-custom-comp text" + markdown="Show delineations `[q]`"> </markdown-dom> </mat-slide-toggle> diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html index c1981ce4f144361f24e86026bf15de4d063f89fc..31f59c7293be5be213c6634d42722f4cb852bf45 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html @@ -42,8 +42,8 @@ {{ mode.name }} </span> <markdown-dom *ngIf="mode.name === selectedMode" - class="d-inline-block"> - `[q]` + class="d-inline-block" + markdown="`[q]`"> </markdown-dom> </button> </mat-menu> diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index fc946ca3f2a946852d1a19093fb35a359d5dd2b7..68b8a93746f8cc72d2c29750e8b73aee581f12eb 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,7 +1,7 @@ -import { Component, ComponentFactory, ComponentFactoryResolver, ElementRef, Inject, Injector, Input, OnDestroy, Optional, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactory, ComponentFactoryResolver, Inject, Injector, Input, OnDestroy, Optional, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import {combineLatest, merge, NEVER, Observable, of, Subject, Subscription} from "rxjs"; -import {catchError, debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMap } from "rxjs/operators"; +import { combineLatest, merge, NEVER, Observable, of, Subscription } from "rxjs"; +import {catchError, debounceTime, distinctUntilChanged, map, shareReplay, startWith, switchMap } from "rxjs/operators"; import { viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; import { viewerStateContextedSelectedRegionsSelector, @@ -14,9 +14,7 @@ import { import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, REGION_OF_INTEREST } from "src/util/interfaces"; import { animate, state, style, transition, trigger } from "@angular/animations"; -import { SwitchDirective } from "src/util/directives/switch.directive"; -import { QuickTourThis, IQuickTourData } from "src/ui/quickTour"; -import { MatDrawer } from "@angular/material/sidenav"; +import { IQuickTourData } from "src/ui/quickTour"; import { PureContantService } from "src/util"; import { EnumViewerEvt, TContextArg, TSupportedViewers, TViewerEvent } from "../viewer.interface"; import { getGetRegionFromLabelIndexId, switchMapWaitFor } from "src/util/fn"; @@ -26,6 +24,7 @@ import { MAT_DIALOG_DATA } from "@angular/material/dialog"; import { GenericInfoCmp } from "src/atlasComponents/regionalFeatures/bsFeatures/genericInfo"; import { _PLI_VOLUME_INJ_TOKEN, _TPLIVal } from "src/glue"; import { uiActionSetPreviewingDatasetFiles } from "src/services/state/uiState.store.helper"; +import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper"; import { DialogService } from "src/services/dialogService.service"; type TCStoreViewerCmp = { @@ -116,7 +115,8 @@ export function ROIFactory(store: Store<any>, svc: PureContantService){ }, ComponentStore, DialogService - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ViewerCmp implements OnDestroy { @@ -127,12 +127,6 @@ export class ViewerCmp implements OnDestroy { public CONST = CONST public ARIA_LABELS = ARIA_LABELS - @ViewChild('sideNavTopSwitch', { static: true }) - private sidenavTopSwitch: SwitchDirective - - @ViewChild('sideNavFullLeftSwitch', { static: true }) - private sidenavLeftSwitch: SwitchDirective - @ViewChild('genericInfoVCR', { read: ViewContainerRef }) genericInfoVCR: ViewContainerRef @@ -172,6 +166,7 @@ export class ViewerCmp implements OnDestroy { public viewerMode$: Observable<string> = this.store$.pipe( select(viewerStateViewerModeSelector), + shareReplay(1), ) public overlaySidenav$ = this.cStore.select(s => s.overlaySideNav).pipe( @@ -190,12 +185,8 @@ export class ViewerCmp implements OnDestroy { return 'notsupported' }) ) - - /** - * TODO may need to be deprecated - * in favour of regional feature/data feature - */ - public iavAdditionalLayers$ = new Subject<any[]>() + + public pliVol$ = this._pliVol$ || NEVER /** * if no regions are selected, nor any additional layers (being deprecated) @@ -203,13 +194,16 @@ export class ViewerCmp implements OnDestroy { * and the full left side bar should not be expandable * if it is already expanded, it should collapse */ - public alwaysHideMinorPanel$: Observable<boolean> = combineLatest([ + public onlyShowMiniTray$: Observable<boolean> = combineLatest([ this.selectedRegions$, - this.iavAdditionalLayers$.pipe( + this.pliVol$.pipe( startWith([]) - ) + ), + this.viewerMode$.pipe( + startWith(null as string) + ), ]).pipe( - map(([ regions, layers ]) => regions.length === 0 && layers.length === 0) + map(([ regions, layers, viewerMode ]) => regions.length === 0 && layers.length === 0 && !viewerMode) ) @ViewChild('viewerStatusCtxMenu', { read: TemplateRef }) @@ -224,7 +218,6 @@ export class ViewerCmp implements OnDestroy { private genericInfoCF: ComponentFactory<GenericInfoCmp> - public pliVol$ = this._pliVol$ || NEVER public clearVoi(){ this.store$.dispatch( uiActionSetPreviewingDatasetFiles({ @@ -238,6 +231,7 @@ export class ViewerCmp implements OnDestroy { private cStore: ComponentStore<TCStoreViewerCmp>, cfr: ComponentFactoryResolver, private dialogSvc: DialogService, + private cdr: ChangeDetectorRef, @Optional() @Inject(_PLI_VOLUME_INJ_TOKEN) private _pliVol$: Observable<_TPLIVal[]>, @Optional() @Inject(REGION_OF_INTEREST) public regionOfInterest$: Observable<any> ){ @@ -245,24 +239,9 @@ export class ViewerCmp implements OnDestroy { this.genericInfoCF = cfr.resolveComponentFactory(GenericInfoCmp) this.subscriptions.push( - this.pliVol$.subscribe(val => { - if (val.length > 0) { - this.sidenavTopSwitch && this.sidenavTopSwitch.open() - this.sidenavLeftSwitch && this.sidenavLeftSwitch.open() - } else { - this.sidenavTopSwitch && this.sidenavTopSwitch.close() - this.sidenavLeftSwitch && this.sidenavLeftSwitch.close() - } - }), this.selectedRegions$.subscribe(() => { this.clearPreviewingDataset() }), - this.alwaysHideMinorPanel$.pipe( - distinctUntilChanged(), - filter(flag => !flag), - ).subscribe(() => { - this.openSideNavs() - }), this.viewerModuleSvc.context$.subscribe( (ctx: any) => this.context = ctx ), @@ -397,7 +376,7 @@ export class ViewerCmp implements OnDestroy { this.genericInfoVCR.clear() this.genericInfoVCR.createComponent(this.genericInfoCF, null, injector) - + this.cdr.markForCheck() }) ) } @@ -415,13 +394,12 @@ export class ViewerCmp implements OnDestroy { ) } - public handleChipClick(){ - this.openSideNavs() - } - - private openSideNavs() { - this.sidenavLeftSwitch && this.sidenavLeftSwitch.open() - this.sidenavTopSwitch && this.sidenavTopSwitch.open() + public exitSpecialViewMode(){ + this.store$.dispatch( + viewerStateSetViewerMode({ + payload: null + }) + ) } public clearPreviewingDataset(){ @@ -433,21 +411,6 @@ export class ViewerCmp implements OnDestroy { }) } - @ViewChild('regionSelRef', { read: ElementRef }) - regionSelRef: ElementRef<any> - - @ViewChild('regionSearchQuickTour', { read: QuickTourThis }) - regionSearchQuickTour: QuickTourThis - - @ViewChild('matDrawerLeft', { read: MatDrawer }) - matDrawerLeft: MatDrawer - - handleSideNavAnimationDone(sideNavExpanded: boolean) { - this.regionSearchQuickTour?.attachTo( - !sideNavExpanded ? null : this.regionSelRef - ) - } - public handleViewerEvent(event: TViewerEvent<'nehuba' | 'threeSurfer'>){ switch(event.type) { case EnumViewerEvt.VIEWERLOADED: diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index 19b64d6b88116e5954dd0fb2226e6fc79b3dcc01..6633b28f8ec233a078953ba66fbb6c73877b2274 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -49,3 +49,13 @@ mat-drawer { overflow-x: hidden; } + +.transition-margin-left +{ + transition: margin-left 200ms cubic-bezier(0.35, 0, 0.25, 1); +} + +._pli-container +{ + padding-top: 5rem; +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index b77622c13103c80c4ccc5e2c938ccd1bca2a97d1..abb3c1c34715215fae7f6d98ed850d1804436bbd 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -3,133 +3,221 @@ </ng-container> </div> -<layout-floating-container [zIndex]="10"> - - <!-- Annotation mode --> - <mat-drawer-container *ngIf="viewerMode$ | async as viewerMode" - class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" - [hasBackdrop]="false"> - - <mat-drawer #viewerModeDrawer="matDrawer" - mode="side" - (annotation-event-directive)="viewerModeDrawer.open()" - [annotation-event-directive-filter]="['showList']" - [autoFocus]="false" - [disableClose]="true" - class="p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2"> - - <!-- annotation --> - <ng-template [ngIf]="viewerMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> - <annotation-list></annotation-list> - </ng-template> - <ng-template [ngIf]="viewerMode === ARIA_LABELS.VIEWER_MODE_KEYFRAME"> - <key-frame-controller></key-frame-controller> +<!-- master draw container --> +<mat-drawer-container + *ngIf="viewerLoaded" + iav-switch + [iav-switch-state]="!(onlyShowMiniTray$ | async)" + #showFullSidenavSwitch="iavSwitch" + class="position-absolute w-100 h-100 mat-drawer-content-overflow-visible invisible" + [hasBackdrop]="false"> + + <!-- master drawer --> + <mat-drawer + mode="side" + #drawer="matDrawer" + [opened]="!(onlyShowMiniTray$ | async)" + [@openClose]="showFullSidenavSwitch && (showFullSidenavSwitch.switchState$ | async) ? 'open' : 'closed'" + (@openClose.start)="$event.toState === 'open' && drawer.open()" + (@openClose.done)="$event.toState === 'closed' && drawer.close()" + [autoFocus]="false" + [disableClose]="true" + class="iv-custom-comp darker-bg p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2 z-index-10"> + + <!-- entry template --> + <ng-template [ngIf]="viewerMode$ | async" let-mode [ngIfElse]="regularTmpl"> + <ng-template [ngTemplateOutlet]="alternateModeDrawerTmpl" + [ngTemplateOutletContext]="{ + mode: mode + }"></ng-template> + </ng-template> + + <!-- regular mode --> + <ng-template #regularTmpl> + <ng-template + [ngTemplateOutlet]="regularModeDrawerTmpl" + [ngTemplateOutletContext]="{ + drawer: drawer, + showFullSidenavSwitch: showFullSidenavSwitch + }"> + </ng-template> + </ng-template> + </mat-drawer> + + <!-- master content --> + <mat-drawer-content class="visible pe-none position-relative"> + <iav-layout-fourcorners> + + <!-- top left --> + <div iavLayoutFourCornersTopLeft class="ws-no-wrap"> + + <!-- special mode --> + <ng-template [ngIf]="viewerMode$ | async" let-mode [ngIfElse]="defaultTopLeftTmpl"> + <ng-template [ngTemplateOutlet]="specialModeTopLeftTmpl" + [ngTemplateOutletContext]="{ + mode: mode, + toggleMatDrawer: drawer.toggle.bind(drawer) + }"> + </ng-template> </ng-template> - </mat-drawer> - <mat-drawer-content class="visible position-relative pe-none"> + <!-- default mode top left tmpl --> + <ng-template #defaultTopLeftTmpl> + <ng-template [ngTemplateOutlet]="defaultMainContentTopLeft" + [ngTemplateOutletContext]="{ + isOpen: drawer.opened, + drawer: drawer, + showFullSidenavSwitch: showFullSidenavSwitch + }"> + </ng-template> + </ng-template> + </div> - <!-- annotation specific --> - <iav-layout-fourcorners *ngIf="viewerMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> - <!-- pullable tab top right corner --> - <div iavLayoutFourCornersTopLeft class="tab-toggle-container"> + <!-- top right --> + <div iavLayoutFourCornersTopRight class="ws-no-wrap"> - <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { - matColor: 'primary', - fontIcon: 'fa-list', - tooltip: 'Annotation list', - click: viewerModeDrawer.toggle.bind(viewerModeDrawer), - badge: toolPanel?.annBadges$ | async + <!-- exit special mode --> + <ng-template [ngIf]="viewerMode$ | async" let-mode [ngIfElse]="defaultTopRightTmpl"> + <ng-template [ngTemplateOutlet]="specialTopRightTmpl" + [ngTemplateOutletContext]="{ + mode: mode }"> - </ng-container> - - <annotating-tools-panel class="z-index-10" - #toolPanel="annoToolsPanel"> - </annotating-tools-panel> - </div> - - <div iavLayoutFourCornersTopRight> - <mat-card class="mat-card-sm pe-all m-4"> - <span> - Annotating - </span> - <button mat-icon-button - [matTooltip]="ARIA_LABELS.EXIT_ANNOTATION_MODE" - color="warn" - annotation-switch - annotation-switch-mode="off"> - <i class="fas fa-times"></i> - </button> - </mat-card> - </div> - - </iav-layout-fourcorners> - - - <!-- key frame specific --> - <iav-layout-fourcorners *ngIf="viewerMode === ARIA_LABELS.VIEWER_MODE_KEYFRAME"> - <!-- pullable tab top right corner --> - <div iavLayoutFourCornersTopLeft class="tab-toggle-container"> - - <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { - matColor: 'primary', - fontIcon: 'fa-play', - tooltip: 'Annotation list', - click: viewerModeDrawer.toggle.bind(viewerModeDrawer) + </ng-template> + </ng-template> + + <!-- default mode top right tmpl --> + <ng-template #defaultTopRightTmpl> + <ng-template [ngTemplateOutlet]="minDefaultMainContentTopRight"> + </ng-template> + </ng-template> + </div> + + + <!-- bottom left --> + <div iavLayoutFourCornersBottomLeft class="ws-no-wrap d-inline-flex w-100vw pe-none align-items-center mb-4"> + + <!-- special bottom left --> + <ng-template [ngIf]="viewerMode$ | async" let-mode [ngIfElse]="localBottomLeftTmpl"></ng-template> + + <!-- default mode bottom left tmpl --> + <ng-template #localBottomLeftTmpl> + + <!-- not the most elegant, but it's a hard problem to solve --> + <!-- on the one hand, showFullSidenavSwitch can be of two states --> + <!-- and drawer.opened can be of two states --> + <ng-template [ngTemplateOutlet]="bottomLeftTmpl" + [ngTemplateOutletContext]="{ + showFullSideNav: (showFullSidenavSwitch.switchState$ | async) + ? drawer.open.bind(drawer) + : showFullSidenavSwitch.open.bind(showFullSidenavSwitch) }"> - </ng-container> - </div> - - <div iavLayoutFourCornersTopRight> - <mat-card class="mat-card-sm pe-all m-4"> - <span> - Key Frame - </span> - <button mat-icon-button - color="warn" - key-frame-play-now="off"> - <i class="fas fa-times"></i> - </button> - </mat-card> - </div> - - </iav-layout-fourcorners> - </mat-drawer-content> - - </mat-drawer-container> - - <!-- top drawer --> - <mat-drawer-container - [hidden]="viewerMode$ | async" - [iav-switch-initstate]="false" - iav-switch - #sideNavTopSwitch="iavSwitch" - class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" - [hasBackdrop]="false"> - - <!-- sidenav-content --> - - <!-- (closedStart)="sideNavwFullLeftSwitch.switchState && matDrawerLeft.close()" - (openedStart)="sideNavFullLeftSwitch.switchState && matDrawerLeft.open()" --> - <mat-drawer class="box-shadow-none border-0 pe-none bg-none col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2" - mode="side" - [attr.data-mat-drawer-top-open]="matDrawerTop.opened" - [opened]="sideNavTopSwitch.switchState" - [autoFocus]="false" - [disableClose]="true" - (openedChange)="handleSideNavAnimationDone($event)" - #matDrawerTop="matDrawer"> - - <div class="h-0 w-100 region-text-search-autocomplete-position"> - <ng-container *ngTemplateOutlet="autocompleteTmpl; context: { showTour: true }"> - </ng-container> + </ng-template> + </ng-template> + </div> + </iav-layout-fourcorners> + </mat-drawer-content> +</mat-drawer-container> + +<!-- alternate mode drawer tmpl --> +<ng-template #alternateModeDrawerTmpl let-mode="mode"> + <ng-container [ngSwitch]="mode"> + <annotation-list *ngSwitchCase="ARIA_LABELS.VIEWER_MODE_ANNOTATING"> + </annotation-list> + <key-frame-controller *ngSwitchCase="ARIA_LABELS.VIEWER_MODE_KEYFRAME"> + </key-frame-controller> + <span *ngSwitchDefault>View mode {{ mode }} does not have side nav registered.</span> + </ng-container> +</ng-template> + + +<!-- regular mode drawer tmpl --> +<ng-template #regularModeDrawerTmpl + let-drawer="drawer" + let-showFullSidenavSwitch="showFullSidenavSwitch"> + + <!-- check if preview volume --> + <ng-template [ngIf]="overlaySidenav$ | async" let-overlaySideNav> + + <!-- back btn --> + <button mat-button + (click)="clearPreviewingDataset()" + [attr.aria-label]="ARIA_LABELS.CLOSE" + class="position-absolute z-index-10 m-2"> + <i class="fas fa-chevron-left"></i> + <span class="ml-1"> + Back + </span> + </button> + + <ng-template #genericInfoVCR> + </ng-template> + </ng-template> + + <div [ngClass]="{ + 'invisible overflow-hidden h-0': overlaySidenav$ | async, + 'h-100': !(overlaySidenav$ | async) + }" class="pe-all position-relative d-flex flex-column"> + + + <!-- if pli voi is visible, show pli template + otherwise show region tmpl --> + <ng-template + [ngTemplateOutlet]="(pliVol$ | async)?.[0] + ? voiTmpl + : sidenavRegionTmpl" + [ngTemplateOutletContext]="{ + drawer: drawer, + showFullSidenavSwitch: showFullSidenavSwitch + }"> + </ng-template> + + <!-- <ng-template let-pliVol [ngIf]="pliVol$ | async" [ngIfElse]="sidenavRegionTmpl"> + <ng-template [ngIf]="pliVol.length > 0" [ngIfElse]="sidenavRegionTmpl"> + <ng-template [ngTemplateOutlet]="voiTmpl"> + + </ng-template> + </ng-template> + </ng-template> --> + + <!-- TODO dataset preview will become deprecated in the future. + Regional feature/data feature will replace it --> + <!-- <div class="hidden" + iav-shown-dataset + #iavShownDataset="iavShownDataset"> + </div> --> + + </div> +</ng-template> + +<!-- minimal default drawer content --> +<ng-template #minSearchTray + let-showFullSidenav="showFullSidenav" + let-drawer="drawer"> + + <div class="mt-2 d-inline-block vw-col-10 vw-col-sm-10 vw-col-md-5 vw-col-lg-4 vw-col-xl-3 vw-col-xxl-2" + iav-switch + [iav-switch-state]="true" + #minTrayVisSwitch="iavSwitch" + [ngClass]="{ + 'vw-col-10-nm vw-col-sm-10-nm vw-col-md-5-nm vw-col-lg-4-nm vw-col-xl-3-nm vw-col-xxl-2-nm': !(minTrayVisSwitch.switchState$ | async), + 'transition-margin-left': !drawer.opened + }"> + + <div class="h-0 w-100 region-text-search-autocomplete-position"> + <ng-container *ngTemplateOutlet="autocompleteTmpl; context: { showTour: true }"> + </ng-container> + </div> + + <!-- such a gross implementation --> + <!-- TODO fix this --> + <div class="mt-1-n w-100 pl-2 pr-2 m-1px"> <button mat-raised-button - *ngIf="!(alwaysHideMinorPanel$ | async)" + *ngIf="!(onlyShowMiniTray$ | async)" [attr.aria-label]="ARIA_LABELS.EXPAND" - (click)="sideNavFullLeftSwitch && sideNavFullLeftSwitch.open()" + (click)="showFullSidenav()" class="explore-btn pe-all w-100" [ngClass]="{ 'darktheme': iavRegion.rgbDarkmode === true, @@ -145,215 +233,205 @@ [region]="(selectedRegions$ | async) && (selectedRegions$ | async)[0]" #iavRegion="iavRegion"> </div> - </button> - </mat-drawer> - - <mat-drawer-content class="visible position-relative pe-none"> - - <iav-layout-fourcorners [iav-layout-fourcorners-cnr-cntr-ngclass]="{'w-100': true}"> - - <!-- pullable tab top left corner --> - <div iavLayoutFourCornersTopLeft class="d-flex flex-nowrap w-100"> - - <!-- top left --> - <div class="flex-grow-1 d-flex flex-nowrap align-items-start"> - - <div *ngIf="viewerLoaded" - class="pe-all tab-toggle-container" - (click)="sideNavTopSwitch && sideNavTopSwitch.toggle()" - quick-tour - [quick-tour-description]="quickTourRegionSearch.description" - [quick-tour-order]="quickTourRegionSearch.order" - #regionSearchQuickTour="quickTour"> - <ng-container *ngTemplateOutlet="tabTmpl; context: { - isOpen: sideNavTopSwitch.switchState, - regionSelected: selectedRegions$ | async, - iavAdditionallayers: iavAdditionalLayers$ | async - }"> - </ng-container> - </div> - - <iav-cmp-viewer-nehuba-status *ngIf="(useViewer$ | async) === 'nehuba'" - class="pe-all mt-2 muted-7"> - </iav-cmp-viewer-nehuba-status> - </div> - - <!-- top right --> - <div class="flex-grow-0 d-inline-flex align-items-start"> - - <!-- signin banner at top right corner --> - - - <top-menu-cmp class="mt-3 mr-2 d-inline-block" - [ismobile]="ismobile" - [viewerLoaded]="viewerLoaded"> - </top-menu-cmp> - - <div *ngIf="viewerLoaded" - class="iv-custom-comp bg card m-2 mat-elevation-z2" - quick-tour - [quick-tour-description]="quickTourAtlasSelector.description" - [quick-tour-order]="quickTourAtlasSelector.order"> - <atlas-dropdown-selector class="pe-all mt-2"> - </atlas-dropdown-selector> - </div> - </div> - </div> + </div> + + </div> - </iav-layout-fourcorners> + <!-- tab toggling hide/show of min search tray --> + <div class="tab-toggle-container d-inline-block v-align-top"> + <ng-container *ngTemplateOutlet="tabTmpl; context: { + isOpen: minTrayVisSwitch.switchState$ | async, + regionSelected: selectedRegions$ | async, + click: minTrayVisSwitch.toggle.bind(minTrayVisSwitch) + }"> + </ng-container> + </div> - </mat-drawer-content> - </mat-drawer-container> +</ng-template> - <!-- full left drawer --> - <mat-drawer-container - [hidden]="viewerMode$ | async" - [iav-switch-initstate]="!(alwaysHideMinorPanel$ | async)" - iav-switch - #sideNavFullLeftSwitch="iavSwitch" - class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" - [hasBackdrop]="false"> - - <!-- sidenav-content --> - <mat-drawer class="darker-bg iv-custom-comp visible col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2 d-flex flex-column pe-all" - mode="push" - [opened]="sideNavTopSwitch.switchState && sideNavFullLeftSwitch.switchState" - [attr.data-mat-drawer-fullleft-open]="matDrawerLeft.opened" - [autoFocus]="false" - #matDrawerLeft="matDrawer" - (openedChange)="$event && sideNavFullLeftSwitch.open()" - [@openClose]="sideNavTopSwitch.switchState && sideNavFullLeftSwitch.switchState ? 'open' : 'closed'" - (@openClose.done)="$event.toState === 'closed' && matDrawerLeft.close()" - [disableClose]="true"> - - <!-- check if preview volume --> - <div *ngIf="overlaySidenav$ | async as overlaySideNav" class="position-relative d-flex flex-column h-100"> - <div class="position-relative ml-15px-n mr-15px-n"> - - <!-- back btn --> - <button mat-button - (click)="clearPreviewingDataset()" - [attr.aria-label]="ARIA_LABELS.CLOSE" - class="position-absolute z-index-10 m-2"> - <i class="fas fa-chevron-left"></i> - <span class="ml-1"> - Back - </span> - </button> - <ng-template #genericInfoVCR> - </ng-template> - </div> - </div> +<!-- top left --> +<!-- default top left --> +<ng-template #defaultMainContentTopLeft + let-isOpen="isOpen" + let-drawer="drawer" + let-showFullSidenavSwitch="showFullSidenavSwitch"> + + <!-- min search tray --> + <ng-template [ngIf]="!(showFullSidenavSwitch.switchState$ | async)"> + <ng-template + [ngTemplateOutlet]="minSearchTray" + [ngTemplateOutletContext]="{ + showFullSidenav: showFullSidenavSwitch.open.bind(showFullSidenavSwitch), + drawer: drawer + }"> + </ng-template> + </ng-template> - <div [ngClass]="{ - 'invisible overflow-hidden h-0': overlaySidenav$ | async, - 'h-100': !(overlaySidenav$ | async) - }" class="position-relative d-flex flex-column"> - - <ng-template let-pliVol [ngIf]="pliVol$ | async" [ngIfElse]="sidenavRegionTmpl"> - <ng-template [ngIf]="pliVol.length > 0" [ngIfElse]="sidenavRegionTmpl"> - <ng-template [ngTemplateOutlet]="voiTmpl"> + <!-- pullable tab top left corner --> + <div *ngIf="showFullSidenavSwitch.switchState$ | async" + class="v-align-top pe-all tab-toggle-container d-inline-block" + (click)="drawer.toggle()" + quick-tour + [quick-tour-description]="quickTourRegionSearch.description" + [quick-tour-order]="quickTourRegionSearch.order"> + <ng-container *ngTemplateOutlet="tabTmpl; context: { + isOpen: isOpen, + regionSelected: selectedRegions$ | async + }"> + </ng-container> + </div> - </ng-template> - </ng-template> - </ng-template> + <!-- status panel for (for nehuba viewer) --> + <iav-cmp-viewer-nehuba-status *ngIf="(useViewer$ | async) === 'nehuba'" + class="pe-all mt-2 muted-7 d-inline-block v-align-top"> + </iav-cmp-viewer-nehuba-status> +</ng-template> - <!-- TODO dataset preview will become deprecated in the future. - Regional feature/data feature will replace it --> - <!-- <div class="hidden" - iav-shown-dataset - #iavShownDataset="iavShownDataset"> - </div> --> - </div> - </mat-drawer> +<!-- special mode top left --> +<ng-template #specialModeTopLeftTmpl + let-mode="mode" + let-toggleMatDrawer="toggleMatDrawer"> - <!-- main-content --> - <mat-drawer-content class="visible position-relative" [hidden]="viewerMode$ | async"> + <div class="tab-toggle-container"> - <iav-layout-fourcorners [iav-layout-fourcorners-cnr-cntr-ngclass]="{'w-100': true}"> + <ng-container [ngSwitch]="mode"> + <!-- annotating top left --> + <ng-template [ngSwitchCase]="ARIA_LABELS.VIEWER_MODE_ANNOTATING"> + <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { + matColor: 'primary', + fontIcon: 'fa-list', + tooltip: 'Annotation list', + click: toggleMatDrawer, + badge: toolPanel?.annBadges$ | async + }"> + </ng-container> + + <annotating-tools-panel class="z-index-10 d-block" + #toolPanel="annoToolsPanel"> + </annotating-tools-panel> + </ng-template> - <!-- bottom left corner (atlas selector and currently selected) --> - <div iavLayoutFourCornersBottomLeft class="d-inline-flex align-items-center mb-4 ml-2 w-100"> + <ng-template [ngSwitchCase]="ARIA_LABELS.VIEWER_MODE_KEYFRAME"> - <!-- atlas selector --> - <atlas-layer-selector *ngIf="viewerLoaded && !(isStandaloneVolumes$ | async)" - #alSelector="atlasLayerSelector" - (iav-outsideClick)="alSelector.selectorExpanded = false"> - </atlas-layer-selector> + <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { + matColor: 'primary', + fontIcon: 'fa-play', + tooltip: 'Annotation list', + click: toggleMatDrawer + }"> + </ng-container> + </ng-template> + </ng-container> + </div> +</ng-template> - <!-- chips --> - <div *ngIf="parcellationSelected$ | async" class="flex-grow-0 p-1 pr-2 flex-shrink-1 overflow-y-hidden overflow-x-auto pe-all"> - <viewer-state-breadcrumb - (on-item-click)="handleChipClick()"> - </viewer-state-breadcrumb> - </div> - </div> +<!-- top right --> +<!-- default top right --> +<ng-template #minDefaultMainContentTopRight> - </iav-layout-fourcorners> + <!-- signin banner at top right corner --> + <top-menu-cmp class="mt-3 mr-2 d-inline-block" + [ismobile]="ismobile" + [viewerLoaded]="viewerLoaded"> + </top-menu-cmp> - </mat-drawer-content> - </mat-drawer-container> + <atlas-dropdown-selector + class="v-align-top pt-2 pe-all mt-2 iv-custom-comp bg card m-2 mat-elevation-z2 d-inline-block" + quick-tour + [quick-tour-description]="quickTourAtlasSelector.description" + [quick-tour-order]="quickTourAtlasSelector.order"> + </atlas-dropdown-selector> -</layout-floating-container> +</ng-template> -<!-- viewer tmpl --> -<ng-template #viewerTmpl> - <iav-layout-fourcorners> - <div iavLayoutFourCornersContent - class="w-100 h-100 position-absolute"> - <div class="h-100 w-100 overflow-hidden position-relative" - ctx-menu-host - [ctx-menu-host-tmpl]="viewerCtxMenuTmpl"> - - <ng-container [ngSwitch]="useViewer$ | async"> - - <!-- nehuba viewer --> - <iav-cmp-viewer-nehuba-glue class="d-block w-100 h-100 position-absolute left-0 top-0" - *ngSwitchCase="'nehuba'" - (viewerEvent)="handleViewerEvent($event)" - [selectedTemplate]="templateSelected$ | async" - [selectedParcellation]="parcellationSelected$ | async" - #iavCmpViewerNehubaGlue="iavCmpViewerNehubaGlue"> - </iav-cmp-viewer-nehuba-glue> - - <!-- three surfer (free surfer viewer) --> - <three-surfer-glue-cmp class="d-block w-100 h-100 position-absolute left-0 top-0" - *ngSwitchCase="'threeSurfer'" - (viewerEvent)="handleViewerEvent($event)" - [selectedTemplate]="templateSelected$ | async" - [selectedParcellation]="parcellationSelected$ | async"> - </three-surfer-glue-cmp> - - <!-- if not supported, show not supported message --> - <div *ngSwitchCase="'notsupported'">Template not supported by any of the viewers</div> - - <!-- by default, show splash screen --> - <div *ngSwitchDefault> - <ui-splashscreen class="position-absolute left-0 top-0"> - </ui-splashscreen> - </div> - </ng-container> +<!-- special mode top right --> +<ng-template #specialTopRightTmpl let-mode="mode"> + <mat-card class="mat-card-sm pe-all m-4"> + <span> + {{ mode }} + </span> + <button mat-icon-button + color="warn" + (click)="exitSpecialViewMode()"> + <i class="fas fa-times"></i> + </button> + </mat-card> +</ng-template> + +<!-- bottom left --> +<ng-template #bottomLeftTmpl let-showFullSideNav="showFullSideNav"> + + <!-- atlas selector --> + <atlas-layer-selector *ngIf="viewerLoaded && !(isStandaloneVolumes$ | async)" + #alSelector="atlasLayerSelector" + class="d-inline-block flex-grow-0 flex-shrink-0 pe-all" + (iav-outsideClick)="alSelector.selectorExpanded = false"> + </atlas-layer-selector> + + <!-- chips --> + <div *ngIf="parcellationSelected$ | async" + class="d-inline-block flex-grow-1 flex-shrink-1 pe-none overflow-x-auto overflow-y-hidden"> + + <viewer-state-breadcrumb class="d-inline-block pe-all" (on-item-click)="showFullSideNav()"> + </viewer-state-breadcrumb> + </div> +</ng-template> + + +<!-- viewer tmpl --> +<ng-template #viewerTmpl> + <div class="position-absolute w-100 h-100 z-index-1"> + + <ng-container [ngSwitch]="useViewer$ | async"> + + <!-- nehuba viewer --> + <iav-cmp-viewer-nehuba-glue class="d-block w-100 h-100 position-absolute left-0 top-0" + *ngSwitchCase="'nehuba'" + (viewerEvent)="handleViewerEvent($event)" + [selectedTemplate]="templateSelected$ | async" + [selectedParcellation]="parcellationSelected$ | async" + #iavCmpViewerNehubaGlue="iavCmpViewerNehubaGlue"> + </iav-cmp-viewer-nehuba-glue> + + <!-- three surfer (free surfer viewer) --> + <three-surfer-glue-cmp class="d-block w-100 h-100 position-absolute left-0 top-0" + *ngSwitchCase="'threeSurfer'" + (viewerEvent)="handleViewerEvent($event)" + [selectedTemplate]="templateSelected$ | async" + [selectedParcellation]="parcellationSelected$ | async"> + </three-surfer-glue-cmp> + + <!-- if not supported, show not supported message --> + <div *ngSwitchCase="'notsupported'">Template not supported by any of the viewers</div> + + <!-- by default, show splash screen --> + <div *ngSwitchDefault> + <ui-splashscreen class="position-absolute left-0 top-0"> + </ui-splashscreen> </div> - </div> - </iav-layout-fourcorners> + </ng-container> + + <!-- <div class="h-100 w-100 overflow-hidden position-relative" + ctx-menu-host + [ctx-menu-host-tmpl]="viewerCtxMenuTmpl"> + </div> --> + </div> </ng-template> -<!-- auto complete search box --> +<!-- auto complete search box --> <ng-template #autocompleteTmpl let-showTour="showTour"> - <div class="iv-custom-comp bg card w-100 mat-elevation-z8 pe-all"> + <div class="iv-custom-comp bg card ml-2 mr-2 mat-elevation-z8 pe-all"> <region-text-search-autocomplete class="w-100 pt-2 flex-shrink-0 flex-grow-0"> </region-text-search-autocomplete> - <div class="w-100 h-100 position-absolute pe-none" - *ngIf="showTour" - #regionSelRef> + <div class="w-100 h-100 position-absolute pe-none" *ngIf="showTour"> </div> </div> </ng-template> @@ -362,15 +440,18 @@ <ng-template #tabTmpl let-isOpen="isOpen" let-regionSelected="regionSelected" - let-iavAdditionallayers="iavAdditionallayers"> + let-iavAdditionallayers="iavAdditionallayers" + let-click="click"> <!-- if mat drawer is open --> <ng-template [ngIf]="isOpen" [ngIfElse]="tabTmpl_closedTmpl"> - <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { - matColor: 'basic', - fontIcon: 'fa-chevron-left' - }"> - </ng-container> + <ng-template [ngTemplateOutlet]="tabTmpl_defaultTmpl" + [ngTemplateOutletContext]="{ + matColor: 'basic', + fontIcon: 'fa-chevron-left', + click: click + }"> + </ng-template> </ng-template> <!-- if matdrawer is closed --> @@ -381,7 +462,8 @@ <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { matColor: 'accent', fontIcon: 'fa-database', - tooltip: 'Explore dataset preview' + tooltip: 'Explore dataset preview', + click: click }"> </ng-container> </ng-template> @@ -402,7 +484,8 @@ customColor: tabTmpl_iavRegion.rgbString, customColorDarkmode: tabTmpl_iavRegion.rgbDarkmode, fontIcon: 'fa-brain', - tooltip: 'Explore ' + tabTmpl_iavRegion.region.name + tooltip: 'Explore ' + tabTmpl_iavRegion.region.name, + click: click }"> </ng-container> @@ -413,7 +496,8 @@ <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { matColor: 'primary', fontIcon: 'fa-sitemap', - tooltip: 'Explore regions' + tooltip: 'Explore regions', + click: click }"> </ng-container> </ng-template> @@ -422,6 +506,7 @@ </ng-template> + <ng-template #tabTmpl_defaultTmpl let-matColor="matColor" let-fontIcon="fontIcon" @@ -466,31 +551,29 @@ </span> </button> - <mat-card class="sidenav-cover-header-container"> - <div class="sidenav-cover-header-container"> - <mat-card-title> - {{ _pliTitle }} - </mat-card-title> + <mat-card class="_pli-container"> + <mat-card-title> + {{ _pliTitle }} + </mat-card-title> - <mat-card-subtitle class="d-inline-flex align-items-center flex-wrap"> - <mat-icon fontSet="fas" fontIcon="fa-database"></mat-icon> - <span> - Dataset preview - </span> + <mat-card-subtitle class="d-inline-flex align-items-center flex-wrap"> + <mat-icon fontSet="fas" fontIcon="fa-database"></mat-icon> + <span> + Dataset preview + </span> - <mat-divider vertical="true" class="ml-2 h-2rem"></mat-divider> + <mat-divider vertical="true" class="ml-2 h-2rem"></mat-divider> - <a [href]="_pliLink" - mat-icon-button - matTooltip="Explore in EBRAINS Knowledge Graph" - target="_blank"> - <i class="fas fa-external-link-alt"></i> - </a> + <a [href]="_pliLink" + mat-icon-button + matTooltip="Explore in EBRAINS Knowledge Graph" + target="_blank"> + <i class="fas fa-external-link-alt"></i> + </a> - </mat-card-subtitle> - </div> + </mat-card-subtitle> - <small class="text-muted iv-custom-comp darker-bg"> + <small class="d-block text-muted iv-custom-comp darker-bg"> {{ _pliDesc }} </small> @@ -506,7 +589,9 @@ </ng-template> <!-- region sidenav tmpl --> -<ng-template #sidenavRegionTmpl> +<ng-template #sidenavRegionTmpl + let-drawer="drawer" + let-showFullSidenavSwitch="showFullSidenavSwitch"> <!-- region search autocomplete --> <!-- [@openCloseAnchor]="sideNavFullLeftSwitch.switchState ? 'open' : 'closed'" --> @@ -549,8 +634,11 @@ </div> <!-- collapse btn --> - <ng-container *ngTemplateOutlet="collapseBtn"> - </ng-container> + <ng-template [ngTemplateOutlet]="collapseBtn" + [ngTemplateOutletContext]="{ + collapse: showFullSidenavSwitch.close.bind(showFullSidenavSwitch) + }"> + </ng-template> </ng-template> @@ -559,7 +647,7 @@ <!-- region detail --> <region-menu [region]="region" - class="flex-grow-1 bs-border-box ml-15px-n mr-15px-n mat-elevation-z4"> + class="flex-grow-1 bs-border-box mat-elevation-z4"> </region-menu> </ng-template> @@ -666,13 +754,13 @@ </ng-template> <!-- collapse btn --> -<ng-template #collapseBtn> +<ng-template #collapseBtn let-collapse="collapse"> <div class="h-0 w-100 collapse-position d-flex flex-column justify-content-end align-items-center"> <button mat-raised-button class="mat-elevation-z8" [attr.aria-label]="ARIA_LABELS.COLLAPSE" - (click)="sideNavFullLeftSwitch.close()" + (click)="collapse()" color="basic"> <i class="fas fa-chevron-up"></i> <span>