diff --git a/Dockerfile b/Dockerfile index 80d15098c500e692b918b354dc01f783d3d787a7..55c13a784be9eecee69cbdcf44db2bb6915f264c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ARG DATASET_PREVIEW_URL ENV DATASET_PREVIEW_URL=${DATASET_PREVIEW_URL:-https://hbp-kg-dataset-previewer.apps.hbp.eu/v2} ARG BS_REST_URL -ENV BS_REST_URL=${BS_REST_URL:-https://siibra-api-tmpfullvolmetadata.apps-dev.hbp.eu/v1_0} +ENV BS_REST_URL=${BS_REST_URL:-https://siibra-api-latest.apps-dev.hbp.eu/v1_0} ARG STRICT_LOCAL ENV STRICT_LOCAL=${STRICT_LOCAL:-false} @@ -38,7 +38,7 @@ WORKDIR /iv RUN for f in $(find . -type f); do gzip < $f > $f.gz && brotli < $f > $f.br; done # prod container -FROM node:12-alpine +FROM node:12-alpine ENV NODE_ENV=production diff --git a/build_env.md b/build_env.md index 849c7e068d3c4ec433cbea158c3680f8b6ed0111..0cb122e3194e0efbf775678d65a0590bdd4f4063 100644 --- a/build_env.md +++ b/build_env.md @@ -7,10 +7,10 @@ As interactive atlas viewer uses [webpack define plugin](https://webpack.js.org/ | `VERSION` | printed in console on viewer startup | `GIT_HASH` \|\| unspecificed hash | v2.2.2 | | `PRODUCTION` | if the build is for production, toggles optimisations such as minification | `undefined` | true | | `BACKEND_URL` | backend that the viewer calls to fetch available template spaces, parcellations, plugins, datasets | `null` | https://interactive-viewer.apps.hbp.eu/ | -| `BS_REST_URL` | [brainscape-api](https://jugit.fz-juelich.de/v.marcenko/brainscapes-api) used to fetch different resources | https://siibra-api-tmpfullvolmetadata.apps-dev.hbp.eu/v1_0 | +| `BS_REST_URL` | [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | https://siibra-api-latest.apps-dev.hbp.eu/v1_0 | | `DATASET_PREVIEW_URL` | dataset preview url used by component <https://github.com/fzj-inm1-bda/kg-dataset-previewer>. Useful for diagnosing issues with dataset previews.| https://hbp-kg-dataset-previewer.apps.hbp.eu/datasetPreview | http://localhost:1234/datasetPreview | | `MATOMO_URL` | base url for matomo analytics | `null` | https://example.com/matomo/ | | `MATOMO_ID` | application id for matomo analytics | `null` | 6 | | `STRICT_LOCAL` | hides **Explore** and **Download** buttons. Useful for offline demo's | `false` | `true` | | `KIOSK_MODE` | after 5 minutes of inactivity, shows overlay inviting users to interact | `false` | `true` | -| `BUILD_TEXT` | overlay text at bottom right of the viewer. set to `''` to hide. | | \ No newline at end of file +| `BUILD_TEXT` | overlay text at bottom right of the viewer. set to `''` to hide. | | diff --git a/common/constants.js b/common/constants.js index 0c5cda5ab7e3a0b9e5229b3d19afafd3d70c98cd..2ba7fc2bb8c3a9ca8dcdf85ba95d770e2dbb9d2b 100644 --- a/common/constants.js +++ b/common/constants.js @@ -78,6 +78,8 @@ } exports.CONST = { + CANNOT_DECIPHER_HEMISPHERE: 'Cannot decipher region hemisphere.', + DOES_NOT_SUPPORT_MULTI_REGION_SELECTION: `Please only select a single region.`, MULTI_REGION_SELECTION: `Multi region selection`, REGIONAL_FEATURES: 'Regional features', NO_ADDIONTAL_INFO_AVAIL: `Currently, no additional information is linked to this region.`, @@ -94,6 +96,7 @@ RECEPTOR_PR_CAPTION: `For a single tissue sample, an exemplary density distribution for a single receptor from the pial surface to the border between layer VI and the white matter.`, RECEPTOR_AR_CAPTION: `An exemplary density distribution of a single receptor for one laminar cross-section in a single tissue sample.`, + DATA_NOT_READY: `Still fetching data. Please try again in a few moments.`, QUICKTOUR_HEADER: `Welcome to ebrains siibra explorer`, PERMISSION_TO_QUICKTOUR: `Would you like a quick tour?`, QUICKTOUR_OK: `Start`, diff --git a/common/util.js b/common/util.js index 6b6df94eb08068247b17e3c9865f055752f3743a..bf18e5dd531164bf8ffac6b40160051eec3fa190 100644 --- a/common/util.js +++ b/common/util.js @@ -21,15 +21,15 @@ } const HEMISPHERE = { - LEFT_HEMISPHERE: `left hemisphere`, - RIGHT_HEMISPHERE: `right hemisphere` + LEFT_HEMISPHERE: `left`, + RIGHT_HEMISPHERE: `right` } exports.getRegionHemisphere = region => { if (!region) return null - return (region.name && region.name.includes('- right hemisphere') || (!!region.status && region.status.includes('right hemisphere'))) + return (region.name && region.name.includes(' right') || (!!region.status && region.status.includes('right'))) ? HEMISPHERE.RIGHT_HEMISPHERE - : (region.name && region.name.includes('- left hemisphere') || (!!region.status && region.status.includes('left hemisphere'))) + : (region.name && region.name.includes(' left') || (!!region.status && region.status.includes('left'))) ? HEMISPHERE.LEFT_HEMISPHERE : null } @@ -72,6 +72,13 @@ exports.getIdObj = getIdObj + exports.getIdFromKgIdObj = kg => { + if(kg.kgId && kg.kgSchema) { + return `${kg.kgSchema}/${kg.kgId}` + } + return null + } + exports.getIdFromFullId = fullId => { const idObj = getIdObj(fullId) if (!idObj) return null @@ -117,7 +124,7 @@ await (() => new Promise(rs => setTimeout(rs, timeout)))() } } - + throw new Error(`fn failed ${retries} times. Aborting.`) } const flattenRegions = regions => regions.concat( diff --git a/deploy/app.js b/deploy/app.js index 0d237b64dc35677ebfe31ab0030989571eaf6ae9..36cdf2a1963ca03257a81a01a42f95d50370c4ea 100644 --- a/deploy/app.js +++ b/deploy/app.js @@ -41,74 +41,74 @@ app.use((req, _, next) => { }) /** - * load env first, then load other modules + * configure Auth + * async function, but can start server without */ -const { configureAuth, ready: authReady } = require('./auth') +let authReady - -const store = (() => { - - const { USE_DEFAULT_MEMORY_STORE } = process.env - if (!!USE_DEFAULT_MEMORY_STORE) { - console.warn(`USE_DEFAULT_MEMORY_STORE is set to true, memleak expected. Do NOT use in prod.`) - return null - } - - const { redisURL } = require('./lruStore') - if (!!redisURL) { - const redis = require('redis') - const RedisStore = require('connect-redis')(session) - const client = redis.createClient({ - url: redisURL - }) - return new RedisStore({ - client - }) - } - - /** - * memorystore (or perhaps lru-cache itself) does not properly close when server shuts - * this causes problems during tests - * So when testing app.js, set USE_DEFAULT_MEMORY_STORE to true - * see app.spec.js - */ - const MemoryStore = require('memorystore')(session) - return new MemoryStore({ - checkPeriod: 86400000 - }) - -})() - -const SESSIONSECRET = process.env.SESSIONSECRET || 'this is not really a random session secret' +const _ = (async () => { /** - * passport application of oidc requires session + * load env first, then load other modules */ -app.use(session({ - secret: SESSIONSECRET, - resave: true, - saveUninitialized: false, - store -})) -/** - * configure CSP - */ -if (process.env.DISABLE_CSP && process.env.DISABLE_CSP === 'true') { - console.warn(`DISABLE_CSP is set to true, csp will not be enabled`) -} else { - require('./csp')(app) -} + const { configureAuth, ready } = require('./auth') + authReady = ready + const store = await (async () => { + + const { USE_DEFAULT_MEMORY_STORE } = process.env + if (!!USE_DEFAULT_MEMORY_STORE) { + console.warn(`USE_DEFAULT_MEMORY_STORE is set to true, memleak expected. Do NOT use in prod.`) + return null + } + + const { _initPr, redisURL, StoreType } = require('./lruStore') + await _initPr + console.log('StoreType', redisURL, StoreType) + if (!!redisURL) { + const redis = require('redis') + const RedisStore = require('connect-redis')(session) + const client = redis.createClient({ + url: redisURL + }) + return new RedisStore({ + client + }) + } + + /** + * memorystore (or perhaps lru-cache itself) does not properly close when server shuts + * this causes problems during tests + * So when testing app.js, set USE_DEFAULT_MEMORY_STORE to true + * see app.spec.js + */ + const MemoryStore = require('memorystore')(session) + return new MemoryStore({ + checkPeriod: 86400000 + }) + + })() -/** - * configure Auth - * async function, but can start server without - */ + const SESSIONSECRET = process.env.SESSIONSECRET || 'this is not really a random session secret' + + /** + * passport application of oidc requires session + */ + app.use(session({ + secret: SESSIONSECRET, + resave: true, + saveUninitialized: false, + store + })) -(async () => { await configureAuth(app) app.use('/user', require('./user')) + + /** + * saneUrl end points + */ + app.use('/saneUrl', require('./saneUrl')) })() const PUBLIC_PATH = process.env.NODE_ENV === 'production' @@ -164,7 +164,21 @@ app.use(require('./devBanner')) * populate nonce token */ const { indexTemplate } = require('./constants') -app.get('/', bkwdMdl, cookieParser(), (req, res) => { +app.get('/', (req, res, next) => { + + /** + * configure CSP + */ + if (process.env.DISABLE_CSP && process.env.DISABLE_CSP === 'true') { + console.warn(`DISABLE_CSP is set to true, csp will not be enabled`) + next() + } else { + const { bootstrapReportViolation, middelware } = require('./csp') + bootstrapReportViolation(app) + middelware(req, res, next) + } + +}, bkwdMdl, cookieParser(), (req, res) => { const iavError = req.cookies && req.cookies['iav-error'] res.setHeader('Content-Type', 'text/html') @@ -187,7 +201,7 @@ app.get('/', bkwdMdl, cookieParser(), (req, res) => { app.use('/logo', require('./logo')) app.get('/ready', async (req, res) => { - const authIsReady = await authReady() + const authIsReady = authReady ? await authReady() : false const regionalFeatureReady = await regionalFeatureIsReady() const datasetReady = await datasetRouteIsReady() const allReady = [ @@ -212,12 +226,6 @@ if (LOCAL_CDN_FLAG) setAlwaysOff(true) app.use(compressionMiddleware, express.static(PUBLIC_PATH)) -/** - * saneUrl end points - */ -const saneUrlRouter = require('./saneUrl') -app.use('/saneUrl', saneUrlRouter) - const jsonMiddleware = (req, res, next) => { if (!res.get('Content-Type')) res.set('Content-Type', 'application/json') next() diff --git a/deploy/app.spec.js b/deploy/app.spec.js index 9683de23be3cdb57493c10f53ca2964e0d779e7e..dee5f9aeaaded654504191b7bcdc0561c79761f1 100644 --- a/deploy/app.spec.js +++ b/deploy/app.spec.js @@ -58,6 +58,10 @@ describe('authentication', () => { }) after(() => { + delete require.cache[require.resolve('./datasets')] + delete require.cache[require.resolve('./saneUrl')] + delete require.cache[require.resolve('./user')] + delete require.cache[require.resolve('./constants')] server.close() }) it('> auth middleware is called', async () => { diff --git a/deploy/assets/images/atlas-selection/freesurfer.png b/deploy/assets/images/atlas-selection/freesurfer.png deleted file mode 100644 index 3f85d87f42345c3487e8548fa2ab91f406b0c67c..0000000000000000000000000000000000000000 Binary files a/deploy/assets/images/atlas-selection/freesurfer.png and /dev/null differ diff --git a/deploy/bkwdCompat/urlState.js b/deploy/bkwdCompat/urlState.js index e63fcd2355ad260a61c3e21048c1688b78ed2020..f902a8a9f32a10dd843a0d0e4e561db18f28c7e3 100644 --- a/deploy/bkwdCompat/urlState.js +++ b/deploy/bkwdCompat/urlState.js @@ -57,7 +57,7 @@ const templateMap = { id: 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2', parc: { 'Cytoarchitectonic Maps - v2.5.1': { - id: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25' + id: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26' }, 'Short Fiber Bundles - HCP': { id: 'juelich/iav/atlas/v1.0.0/79cbeaa4ee96d5d3dfe2876e9f74b3dc3d3ffb84304fb9b965b1776563a1069c' @@ -92,9 +92,6 @@ const templateMap = { aId: 'juelich/iav/atlas/v1.0.0/1', id: 'minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992', parc: { - 'Cytoarchitectonic Maps - v2.5.1': { - id: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25' - }, 'Cytoarchitectonic Maps - v1.18': { id: 'juelich/iav/atlas/v1.0.0/8' } diff --git a/deploy/csp/index.js b/deploy/csp/index.js index 928402e53ace8cec953238727a4eb45dd6ce8b5e..87a6fe19e8e0652322a121858aef0a7394b46721 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -1,6 +1,5 @@ const csp = require('helmet-csp') const bodyParser = require('body-parser') -const crypto = require('crypto') let WHITE_LIST_SRC, CSP_CONNECT_SRC, SCRIPT_SRC @@ -49,13 +48,9 @@ const connectSrc = [ ...CSP_CONNECT_SRC ] -module.exports = (app) => { - app.use((req, res, next) => { - if (req.path === '/') res.locals.nonce = crypto.randomBytes(16).toString('hex') - next() - }) +module.exports = { + middelware: (req, res, next) => { - app.use((req, res, next) => { const permittedCsp = (req.session && req.session.permittedCsp) || {} const userConnectSrc = [] const userScriptSrc = [] @@ -69,18 +64,6 @@ module.exports = (app) => { ...(permittedCsp[key]['scriptSrc'] || []) ) } - res.locals.userCsp = { - userConnectSrc, - userScriptSrc, - } - next() - }) - - app.use((req, res, next) => { - const { - userConnectSrc = [], - userScriptSrc = [], - } = res.locals.userCsp || {} csp({ directives: { defaultSrc: [ @@ -120,7 +103,7 @@ module.exports = (app) => { 'unpkg.com/react@16/umd/', // plugin load external lib -> react '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.8/dist/bundle.js', // for threeSurfer (freesurfer support in browser) + 'https://unpkg.com/three-surfer@0.0.10/dist/bundle.js', // for threeSurfer (freesurfer support in browser) (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null, ...SCRIPT_SRC, ...WHITE_LIST_SRC, @@ -130,9 +113,9 @@ module.exports = (app) => { }, reportOnly })(req, res, next) - }) - if (!CSP_REPORT_URI) { + }, + bootstrapReportViolation: app => { app.post('/report-violation', bodyParser.json({ type: ['json', 'application/csp-report'] }), (req, res) => { @@ -144,4 +127,4 @@ module.exports = (app) => { res.status(204).end() }) } -} \ No newline at end of file +} diff --git a/deploy/csp/index.spec.js b/deploy/csp/index.spec.js index 70b4e144245c5430f7b4a804fe4e148d7e235498..93a478e25b498521f32c8f833a017a1f390db8c3 100644 --- a/deploy/csp/index.spec.js +++ b/deploy/csp/index.spec.js @@ -1,6 +1,6 @@ const express = require('express') const app = express() -const csp = require('./index') +const { middelware: cspMiddleware } = require('./index') const got = require('got') const { expect, assert } = require('chai') @@ -45,7 +45,7 @@ describe('> csp/index.js', () => { } before(done => { app.use(middleware) - csp(app) + app.use(cspMiddleware) app.get('/', (req, res) => { res.status(200).send('OK') }) diff --git a/deploy/datasets/index.js b/deploy/datasets/index.js index 4f63e80b831696154b557f37fc2cb81c56aedc39..7f55dff26b24fc8158c32c6be06554fd2e0d1e7f 100644 --- a/deploy/datasets/index.js +++ b/deploy/datasets/index.js @@ -192,6 +192,7 @@ datasetsRouter.get('/hasPreview', cacheMaxAge24Hr, async (req, res) => { }) datasetsRouter.get('/kgInfo', checkKgQuery, cacheMaxAge24Hr, async (req, res) => { + return res.status(400).send('Deprecated') const { kgId } = req.query const { kgSchema } = req.query const { user } = req diff --git a/deploy/datasets/query.js b/deploy/datasets/query.js index 500bc6867925210e3509905dbeed3595f76f26d3..4911e4f805627a38e5d08959eec2304ed752b7d9 100644 --- a/deploy/datasets/query.js +++ b/deploy/datasets/query.js @@ -7,7 +7,7 @@ const { getPreviewFile, hasPreview } = require('./supplements/previewFile') const { constants, init: kgQueryUtilInit, getUserKGRequestParam, filterDatasets, filterDatasetsByRegion } = require('./util') const ibc = require('./importIBS') const { returnAdditionalDatasets } = require('../regionalFeatures') -const { store } = require('../lruStore') +const lruStore = require('../lruStore') const IAV_DS_CACHE_KEY = 'IAV_DS_CACHE_KEY' const IAV_DS_TIMESTAMP_KEY = 'IAV_DS_TIMESTAMP_KEY' @@ -55,6 +55,8 @@ const fetchDatasetFromKg = async ({ user } = {}) => { } const refreshCache = async () => { + await lruStore._initPr + const { store } = lruStore store.set(IAV_DS_REFRESH_TIMESTAMP_KEY, new Date().toString()) const text = await fetchDatasetFromKg() await store.set(IAV_DS_CACHE_KEY, text) @@ -64,6 +66,9 @@ const refreshCache = async () => { const getPublicDs = async () => { console.log(`fetching public ds ...`) + + await lruStore._initPr + const { store } = lruStore let cachedData = await store.get(IAV_DS_CACHE_KEY) if (!cachedData) { diff --git a/deploy/datasets/testData/bigbrain.js b/deploy/datasets/testData/bigbrain.js index ca8e64871a35dcdc8685f64faf14c1a982c23868..6e5c5ad6fee360e1d8b2770e3a4c3cda98782cf1 100644 --- a/deploy/datasets/testData/bigbrain.js +++ b/deploy/datasets/testData/bigbrain.js @@ -75,7 +75,7 @@ module.exports = [ "parcellationAtlas": [ { "name": "Jülich Cytoarchitechtonic Brain Atlas (human)", - "fullId": "https://nexus.humanbrainproject.org/v0/data/minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", + "fullId": "https://nexus.humanbrainproject.org/v0/data/minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", "id": [ "deec923ec31a82f89a9c7c76a6fefd6b", "e2d45e028b6da0f6d9fdb9491a4de80a" diff --git a/deploy/datasets/testData/colin27.js b/deploy/datasets/testData/colin27.js index 44957a98138564316f0cfd01be4cd58d4e12c4ae..43389fe15164959058e4faace389da3113c51cdd 100644 --- a/deploy/datasets/testData/colin27.js +++ b/deploy/datasets/testData/colin27.js @@ -61,7 +61,7 @@ module.exports = [ "parcellationAtlas": [ { "name": "Jülich Cytoarchitechtonic Brain Atlas (human)", - "fullId": "https://nexus.humanbrainproject.org/v0/data/minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", + "fullId": "https://nexus.humanbrainproject.org/v0/data/minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", "id": [ "deec923ec31a82f89a9c7c76a6fefd6b", "e2d45e028b6da0f6d9fdb9491a4de80a" diff --git a/deploy/datasets/util.js b/deploy/datasets/util.js index 229b03891d1c0b498d734d017655d6a7127ad892..48620a093e4ddc666e29be7f811930354f839e5d 100644 --- a/deploy/datasets/util.js +++ b/deploy/datasets/util.js @@ -12,7 +12,7 @@ const KG_IDS = { LONG_BUNDLE: 'juelich/iav/atlas/v1.0.0/5', SHORT_BUNDLE: 'juelich/iav/atlas/v1.0.0/6', JULICH_BRAIN: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579', - JULICH_BRAIN_V25: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25', + JULICH_BRAIN_V25: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26', JULICH_BRAIN_V24_BIGBRAIN: 'juelich/iav/atlas/v1.0.0/7' } } diff --git a/deploy/lruStore/index.js b/deploy/lruStore/index.js index 6a3c8a908e47f98ec60760f0cd4b87e19deebd8e..474f23d5ec9d9ae9462df84439f672954edd66da 100644 --- a/deploy/lruStore/index.js +++ b/deploy/lruStore/index.js @@ -33,7 +33,7 @@ const userPass = (() => { : `${returnString}@` })() -const redisURL = redisAddr && `${redisProto || ''}://${userPass}${redisAddr}:${redisPort}` +const _redisURL = redisAddr && `${redisProto || ''}://${userPass}${redisAddr}:${redisPort}` const crypto = require('crypto') @@ -56,76 +56,109 @@ const ensureString = val => { if (typeof val !== 'string') throw new Error(`both key and val must be string`) } -if (redisURL) { - const redis = require('redis') - const { promisify } = require('util') - const client = redis.createClient({ - url: redisURL - }) - - const asyncGet = promisify(client.get).bind(client) - const asyncSet = promisify(client.set).bind(client) - const asyncDel = promisify(client.del).bind(client) - - const keys = [] - - /** - * maxage in milli seconds - */ - exports.store = { - set: async (key, val, { maxAge } = {}) => { - ensureString(key) - ensureString(val) - asyncSet(key, val, ...( maxAge ? [ 'PX', maxAge ] : [] )) - keys.push(key) - }, - get: async (key) => { - ensureString(key) - return asyncGet(key) - }, - clear: async auth => { - if (auth !== authKey) { - getAuthKey() - throw new Error(`unauthorized`) - } - await asyncDel(keys.splice(0)) - } +class ExportObj { + constructor(){ + this.StoreType = null + this.redisURL = null + this.store = null + this._rs = null + this._rj = null + + this._initPr = new Promise((rs, rj) => { + this._rs = rs + this._rj = rj + }) } +} - exports.StoreType = `redis` - exports.redisURL = redisURL - console.log(`redis`) +const exportObj = new ExportObj() + +const setupLru = () => { -} else { const LRU = require('lru-cache') - const store = new LRU({ + const lruStore = new LRU({ max: 1024 * 1024 * 1024, // 1gb length: (n, key) => n.length, maxAge: Infinity, // never expires }) - exports.store = { + exportObj.store = { /** * maxage in milli seconds */ set: async (key, val, { maxAge } = {}) => { ensureString(key) ensureString(val) - store.set(key, val, ...( maxAge ? [ maxAge ] : [] )) + lruStore.set(key, val, ...( maxAge ? [ maxAge ] : [] )) }, get: async (key) => { ensureString(key) - return store.get(key) + return lruStore.get(key) }, clear: async auth => { if (auth !== authKey) { getAuthKey() throw new Error(`unauthorized`) } - store.reset() + lruStore.reset() } } - exports.StoreType = `lru-cache` - console.log(`lru-cache`) + exportObj.StoreType = `lru-cache` + console.log(`using lru-cache`) + exportObj._rs() } + +if (_redisURL) { + const redis = require('redis') + const { promisify } = require('util') + const client = redis.createClient({ + url: _redisURL + }) + + client.on('ready', () => { + + const asyncGet = promisify(client.get).bind(client) + const asyncSet = promisify(client.set).bind(client) + const asyncDel = promisify(client.del).bind(client) + + const keys = [] + + /** + * maxage in milli seconds + */ + exportObj.store = { + set: async (key, val, { maxAge } = {}) => { + ensureString(key) + ensureString(val) + asyncSet(key, val, ...( maxAge ? [ 'PX', maxAge ] : [] )) + keys.push(key) + }, + get: async (key) => { + ensureString(key) + return asyncGet(key) + }, + clear: async auth => { + if (auth !== authKey) { + getAuthKey() + throw new Error(`unauthorized`) + } + await asyncDel(keys.splice(0)) + } + } + + exportObj.StoreType = `redis` + exportObj.redisURL = _redisURL + console.log(`using redis`) + exportObj._rs() + }).on('error', () => { + console.warn(`setting up Redis error, fallback to setupLru`) + setupLru() + client.quit() + }) + +} else { + setupLru() +} + +module.exports = exportObj \ No newline at end of file diff --git a/deploy/package.json b/deploy/package.json index 5aae0cdb9dc7fcb6eead64e0b6e15b95baf0463d..3b422f04a5d559dc417dec740e113007e1de0edc 100644 --- a/deploy/package.json +++ b/deploy/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "start": "node server.js", - "test": "DISABLE_LIMITER=1 node -r dotenv/config ./node_modules/.bin/mocha './**/*.spec.js' --exclude 'node_modules/*' --timeout 60000", + "test": "DISABLE_LIMITER=1 node ./node_modules/.bin/mocha './**/*.spec.js' --exclude 'node_modules/*' --timeout 60000", "mocha": "mocha" }, "keywords": [], diff --git a/deploy/plugins/index.js b/deploy/plugins/index.js index cd829c98d7c6dcef03b4128427ae1f05093a9eef..929f19499786e69b7dc1461a27ae96972e32b8a0 100644 --- a/deploy/plugins/index.js +++ b/deploy/plugins/index.js @@ -4,7 +4,7 @@ */ const express = require('express') -const { store } = require('../lruStore') +const lruStore = require('../lruStore') const got = require('got') const router = express.Router() const PLUGIN_URLS = (process.env.PLUGIN_URLS && process.env.PLUGIN_URLS.split(';')) || [] @@ -26,6 +26,10 @@ router.get('/manifests', async (_req, res) => { ...STAGING_PLUGIN_URLS ].map(async url => { const key = getKey(url) + + await lruStore._initPr + const { store } = lruStore + try { const storedManifest = await store.get(key) if (storedManifest) return JSON.parse(storedManifest) diff --git a/deploy/saneUrl/index.js b/deploy/saneUrl/index.js index b3adb89077db2b1fbf110f9183c95f30d3ca4d28..2be8a15a3d518eaf17c4074aa439a291839b5b17 100644 --- a/deploy/saneUrl/index.js +++ b/deploy/saneUrl/index.js @@ -2,40 +2,12 @@ const router = require('express').Router() const RateLimit = require('express-rate-limit') const RedisStore = require('rate-limit-redis') const { Store, NotFoundError } = require('./store') +const { redisURL } = require('../lruStore') const bodyParser = require('body-parser') const { readUserData, saveUserData } = require('../user/store') const store = new Store() - -const { - REDIS_PROTO, - REDIS_ADDR, - REDIS_PORT, - - REDIS_RATE_LIMITING_DB_EPHEMERAL_PORT_6379_TCP_PROTO, - REDIS_RATE_LIMITING_DB_EPHEMERAL_PORT_6379_TCP_ADDR, - REDIS_RATE_LIMITING_DB_EPHEMERAL_PORT_6379_TCP_PORT, - - REDIS_USERNAME, - REDIS_PASSWORD, - - HOSTNAME, - HOST_PATHNAME, - DISABLE_LIMITER, -} = process.env - -const redisProto = REDIS_PROTO || REDIS_RATE_LIMITING_DB_EPHEMERAL_PORT_6379_TCP_PROTO || 'redis' -const redisAddr = REDIS_ADDR || REDIS_RATE_LIMITING_DB_EPHEMERAL_PORT_6379_TCP_ADDR || null -const redisPort = REDIS_PORT || REDIS_RATE_LIMITING_DB_EPHEMERAL_PORT_6379_TCP_PORT || 6379 - -/** - * nb this way to set username and pswd can be risky, but given that site adnimistrator sets the username and pswd via env var - * it should not be a security concern - */ -const userPass = (REDIS_USERNAME || REDIS_PASSWORD) && `${REDIS_USERNAME || ''}:${REDIS_PASSWORD || ''}@` - -const redisURL = redisAddr && `${redisProto}://${userPass || ''}${redisAddr}:${redisPort}` - +const { DISABLE_LIMITER, HOSTNAME, HOST_PATHNAME } = process.env const limiter = new RateLimit({ windowMs: 1e3 * 5, max: 5, diff --git a/deploy/saneUrl/index.spec.js b/deploy/saneUrl/index.spec.js index cb0038819264714888747130cd1ba5d2029326ed..a0be5ad9f9132f50eb684749309395542f3de22d 100644 --- a/deploy/saneUrl/index.spec.js +++ b/deploy/saneUrl/index.spec.js @@ -1,9 +1,19 @@ const sinon = require('sinon') const cookie = require('cookie') -const { Store, NotFoundError } = require('./store') const userStore = require('../user/store') +class StubStore { + async set(key, val) { + + } + async get(key) { + + } +} + +class StubNotFoundError extends Error{} + const savedUserDataPayload = { otherData: 'not relevant data', savedCustomLinks: [ @@ -21,69 +31,75 @@ const saveUserDataStub = sinon .returns(Promise.resolve()) const express = require('express') -const router = require('./index') const got = require('got') const { expect } = require('chai') -const app = express() -let user -app.use('', (req, res, next) => { - req.user = user - next() -}, router) +describe('> saneUrl/index.js', () => { -const name = `nameme` + let getTokenStub, app, user, server, setStub, getStub + const payload = { + ver: '0.0.1', + queryString: 'test_test' + } -const payload = { - ver: '0.0.1', - queryString: 'test_test' -} + const name = `nameme` -describe('> saneUrl/index.js', () => { + after(() => { - let getTokenStub - before(() => { - getTokenStub = sinon - .stub(Store.prototype, 'getToken') - .returns(Promise.resolve(`--fake-token--`)) - }) + server.close() + setStub && setStub.restore() - after(() => { - getTokenStub.restore() + delete require.cache[require.resolve('../lruStore')] + delete require.cache[require.resolve('./store')] }) - describe('> router', () => { + before(() => { + getStub = sinon.stub(StubStore.prototype, 'get') + setStub = sinon.stub(StubStore.prototype, 'set') + require.cache[require.resolve('../lruStore')] = { + exports: { + redisURL: null + } + } - let server, setStub - before(() => { + require.cache[require.resolve('./store')] = { + exports: { + Store: StubStore, + NotFoundError: StubNotFoundError + } + } - setStub = sinon - .stub(Store.prototype, 'set') - .returns(Promise.resolve()) - server = app.listen(50000) - }) + app = express() - afterEach(() => { - setStub.resetHistory() - }) + const router = require('./index') + app.use('', (req, res, next) => { + req.user = user + next() + }, router) - after(() => { - server.close() - setStub.restore() - }) + server = app.listen(50000) + }) + + after(() => { + getTokenStub && getTokenStub.restore() + }) + + afterEach(() => { + setStub.resetHistory() + getStub.resetHistory() + }) + + describe('> router', () => { it('> works', async () => { const body = { ...payload } - const getStub = sinon - .stub(Store.prototype, 'get') - .returns(Promise.resolve(JSON.stringify(body))) + getStub.returns(Promise.resolve(JSON.stringify(body))) const { body: respBody } = await got(`http://localhost:50000/${name}`) expect(getStub.calledWith(name)).to.be.true expect(respBody).to.equal(JSON.stringify(body)) - getStub.restore() }) it('> get on expired returns 404', async () => { @@ -91,16 +107,13 @@ describe('> saneUrl/index.js', () => { ...payload, expiry: Date.now() - 1e3 * 60 } - const getStub = sinon - .stub(Store.prototype, 'get') - .returns(Promise.resolve(JSON.stringify(body))) + getStub.returns(Promise.resolve(JSON.stringify(body))) const { statusCode } = await got(`http://localhost:50000/${name}`, { throwHttpErrors: false }) expect(statusCode).to.equal(404) expect(getStub.calledWith(name)).to.be.true - getStub.restore() }) it('> get on expired with txt html header sets cookie and redirect', async () => { @@ -109,9 +122,7 @@ describe('> saneUrl/index.js', () => { ...payload, expiry: Date.now() - 1e3 * 60 } - const getStub = sinon - .stub(Store.prototype, 'get') - .returns(Promise.resolve(JSON.stringify(body))) + getStub.returns(Promise.resolve(JSON.stringify(body))) const { statusCode, headers } = await got(`http://localhost:50000/${name}`, { headers: { @@ -123,7 +134,6 @@ describe('> saneUrl/index.js', () => { expect(statusCode).to.be.lessThan(303) expect(getStub.calledWith(name)).to.be.true - getStub.restore() const c = cookie.parse(...headers['set-cookie']) expect(!!c['iav-error']).to.be.true @@ -133,9 +143,7 @@ describe('> saneUrl/index.js', () => { it('> checks if the name is available', async () => { - const getStub = sinon - .stub(Store.prototype, 'get') - .returns(Promise.reject(new NotFoundError())) + getStub.returns(Promise.reject(new StubNotFoundError())) await got(`http://localhost:50000/${name}`, { method: 'POST', @@ -150,16 +158,12 @@ describe('> saneUrl/index.js', () => { expect(storedName).to.equal(name) expect(getStub.called).to.be.true expect(setStub.called).to.be.true - - getStub.restore() }) it('> if file exist, will return 409 conflict', async () => { - const getStub = sinon - .stub(Store.prototype, 'get') - .returns(Promise.resolve('{}')) + getStub.returns(Promise.resolve('{}')) const { statusCode } = await got(`http://localhost:50000/${name}`, { method: 'POST', @@ -174,14 +178,11 @@ describe('> saneUrl/index.js', () => { expect(getStub.called).to.be.true expect(setStub.called).to.be.false - getStub.restore() }) it('> if other error, will return 500', async () => { - const getStub = sinon - .stub(Store.prototype, 'get') - .returns(Promise.reject(new Error(`other errors`))) + getStub.returns(Promise.reject(new Error(`other errors`))) const { statusCode } = await got(`http://localhost:50000/${name}`, { method: 'POST', @@ -195,21 +196,12 @@ describe('> saneUrl/index.js', () => { expect(statusCode).to.equal(500) expect(getStub.called).to.be.true expect(setStub.called).to.be.false - - getStub.restore() }) describe('> set with unauthenticated user', () => { - let getStub before(() => { - getStub = sinon - .stub(Store.prototype, 'get') - .returns(Promise.reject(new NotFoundError())) - }) - - after(() => { - getStub.restore() + getStub.returns(Promise.reject(new StubNotFoundError())) }) it('> set with anonymous user has user undefined and expiry as defined', async () => { @@ -236,13 +228,7 @@ describe('> saneUrl/index.js', () => { describe('> set with authenticated user', () => { before(() => { - getStub = sinon - .stub(Store.prototype, 'get') - .returns(Promise.reject(new NotFoundError())) - }) - - after(() => { - getStub.restore() + getStub.returns(Promise.reject(new StubNotFoundError())) }) before(() => { diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js index 500dbdf9f082055cd82cbdeecd0a2d33c1370a7a..2fbc49e5b62647cd050e47ea1fa2dc513c80753d 100644 --- a/e2e/protractor.conf.js +++ b/e2e/protractor.conf.js @@ -22,7 +22,7 @@ const PROTRACTOR_SPECS = process.env.PROTRACTOR_SPECS const localConfig = { ...(SELENIUM_ADDRESS ? { seleniumAddress: SELENIUM_ADDRESS } - : { directConnect: true } + : { directConnect: true } ), capabilities: { // Use headless chrome @@ -71,13 +71,13 @@ let bsLocal /** * config adapted from * https://github.com/browserstack/protractor-browserstack - * + * * MIT licensed */ const bsConfig = { 'browserstackUser': BROWSERSTACK_USERNAME, 'browserstackKey': BROWSERSTACK_ACCESS_KEY, - + 'capabilities': { 'build': 'protractor-browserstack', 'name': BROWSERSTACK_TEST_NAME || 'iav_e2e', @@ -121,7 +121,7 @@ exports.config = { jasmineNodeOpts: { defaultTimeoutInterval: 1000 * 60 * 10 }, - + ...( BROWSERSTACK_ACCESS_KEY && BROWSERSTACK_USERNAME ? bsConfig @@ -131,4 +131,4 @@ exports.config = { ...( (directConnect && { directConnect }) || {} ) -} \ No newline at end of file +} diff --git a/e2e/util/selenium/iav.js b/e2e/util/selenium/iav.js index 758ff2a88783bf714069e84db8ef7c7eb02faacc..6d75eaa5b7b3ed38e1310d7460fdd24ef874aac2 100644 --- a/e2e/util/selenium/iav.js +++ b/e2e/util/selenium/iav.js @@ -110,6 +110,7 @@ class WdIavPage extends WdLayoutPage{ } _getSingleDatasetListView(){ + throw new Error(`data-browser has been deprecated. rewrite selector`) return this._browser .findElement( By.css('data-browser') ) .findElements( By.css('single-dataset-list-view') ) diff --git a/package.json b/package.json index bea7e83f4ea815d1df8d8fa3a129530ccf442cdb..b35b456f28f9dbd58e26dd6cf04a53a025f4c52c 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@ngrx/store": "^9.1.1", "@types/node": "12.12.39", "export-nehuba": "0.0.12", - "hbp-connectivity-component": "^0.3.18", + "hbp-connectivity-component": "^0.4.4", "jszip": "^3.6.0", "zone.js": "^0.10.2" } diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts index 5c46a0f721a4991140e75f3bd41b4fcdc48dbeb4..0e0d85ad8173e2c7c578a99df8621b6ec0ffe2bf 100644 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts +++ b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts @@ -8,6 +8,7 @@ import {MockStore, provideMockStore} from "@ngrx/store/testing"; import {Observable, of} from "rxjs"; import { viewerStateAllRegionsFlattenedRegionSelector, viewerStateOverwrittenColorMapSelector } from "src/services/state/viewerState/selectors"; import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState.store.helper"; +import {BS_ENDPOINT} from "src/util/constants"; /** * injecting databrowser module is bad idea @@ -15,6 +16,7 @@ import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerSta * since the only reason why data browser is imported is to use show dataset dialogue * just use a dummy directive */ +const MOCK_BS_ENDPOINT = `http://localhost:1234` @Directive({ selector: '[iav-dataset-show-dataset-dialog]' @@ -39,15 +41,15 @@ describe('ConnectivityComponent', () => { let datasetList = [ { - id: 'id1', - name: 'n1', - description: 'd1', + ['@id']: 'id1', + src_name: 'id1', + src_info: 'd1', kgId: 'kgId1', kgschema: 'kgschema1' }, { - id: 'id2', - name: 'n2', - description: 'd2', + ['@id']: 'id2', + src_name: 'id2', + src_info: 'd2', kgId: 'kgId2', kgschema: 'kgschema2' } @@ -60,7 +62,11 @@ describe('ConnectivityComponent', () => { ], providers: [ provideMockActions(() => actions$), - provideMockStore() + provideMockStore(), + { + provide: BS_ENDPOINT, + useValue: MOCK_BS_ENDPOINT + } ], declarations: [ ConnectivityBrowserComponent, @@ -92,17 +98,17 @@ describe('ConnectivityComponent', () => { component.datasetList = datasetList - component.changeDataset({value: 'n1'}) + component.changeDataset({value: 'id1'}) expect(component.selectedDatasetDescription).toEqual('d1') expect(component.selectedDatasetKgId).toEqual('kgId1') expect(component.selectedDatasetKgSchema).toEqual('kgschema1') - component.changeDataset({value: 'n2'}) + component.changeDataset({value: 'id2'}) expect(component.selectedDatasetDescription).toEqual('d2') expect(component.selectedDatasetKgId).toEqual('kgId2') expect(component.selectedDatasetKgSchema).toEqual('kgschema2') }) -}); \ No newline at end of file +}); diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts index 7be783d314f81b62d69045d3cdc75538205acc30..e0e984689063879c1f740d6f239861496b8f71c4 100644 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.component.ts @@ -7,7 +7,7 @@ import { Output, ViewChild, Input, - OnInit, + OnInit, Inject, } from "@angular/core"; import {select, Store} from "@ngrx/store"; import {fromEvent, Observable, Subscription, Subject, combineLatest} from "rxjs"; @@ -20,6 +20,9 @@ import { viewerStateOverwrittenColorMapSelector } from "src/services/state/viewerState/selectors"; import {HttpClient} from "@angular/common/http"; +import {BS_ENDPOINT} from "src/util/constants"; +import {getIdFromKgIdObj} from "common/util"; + const CONNECTIVITY_NAME_PLATE = 'Connectivity' @@ -38,7 +41,7 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe */ private _isFirstUpdate = true - public connectivityUrl = 'https://connectivity-query-v1-1-connectivity.apps.hbp.eu/v1.1/studies' + public connectivityUrl: string private accordionIsExpanded = false @@ -99,11 +102,20 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe } this.regionName = newRegionName + this.regionId = val.id? val.id.kg? getIdFromKgIdObj(val.id.kg) : val.id : null + this.atlasId = val.context.atlas['@id'] + this.parcellationId = val.context.parcellation['@id'] + if(this.selectedDataset) { + this.setConnectivityUrl() + this.setProfileLoadUrl() + } // TODO may not be necessary this.changeDetectionRef.detectChanges() } - @Input() parcellationId: any + public atlasId: any + public parcellationId: any + public regionId: string public regionName: string public regionHemisphere: string = null public datasetList: any[] = [] @@ -132,6 +144,7 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe private store$: Store<any>, private changeDetectionRef: ChangeDetectorRef, private httpClient: HttpClient, + @Inject(BS_ENDPOINT) private siibraApiUrl: string, ) { this.overwrittenColorMap$ = this.store$.pipe( @@ -144,10 +157,12 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe public fullConnectivityLoadUrl: string ngOnInit(): void { + this.setConnectivityUrl() + this.httpClient.get<[]>(this.connectivityUrl).subscribe(res => { - this.datasetList = res.filter(dl => dl['parcellation id'] === this.parcellationId) - this.selectedDataset = this.datasetList[0]?.name - this.selectedDatasetDescription = this.datasetList[0]?.description + this.datasetList = res + this.selectedDataset = this.datasetList[0]?.['@id'] + this.selectedDatasetDescription = this.datasetList[0]?.['src_info'] this.changeDataset() }) @@ -275,6 +290,16 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe this.subscriptions.forEach(s => s.unsubscribe()) } + private setConnectivityUrl() { + this.connectivityUrl = `${this.siibraApiUrl}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcellationId)}/regions/${encodeURIComponent(this.regionId || this.regionName)}/features/ConnectivityProfile` + } + + private setProfileLoadUrl() { + const url = `${this.connectivityUrl}/${encodeURIComponent(this.selectedDataset)}` + this.connectivityLoadUrl.emit(url) + this.loadUrl = url + } + clearViewer() { this.store$.dispatch( ngViewerActionClearView({ @@ -293,18 +318,15 @@ export class ConnectivityBrowserComponent implements OnInit, AfterViewInit, OnDe changeDataset(event = null) { if (event) { this.selectedDataset = event.value - const foundDataset = this.datasetList.find(d => d.name === this.selectedDataset) - this.selectedDatasetDescription = foundDataset?.description + const foundDataset = this.datasetList.find(d => d['@id'] === this.selectedDataset) + this.selectedDatasetDescription = foundDataset?.['src_info'] this.selectedDatasetKgId = foundDataset?.kgId || null this.selectedDatasetKgSchema = foundDataset?.kgschema || null } if (this.datasetList.length && this.selectedDataset) { - const selectedDatasetId = this.datasetList.find(d => d.name === this.selectedDataset).id - const url = selectedDatasetId ? `${this.connectivityUrl}/${selectedDatasetId}` : null - this.connectivityLoadUrl.emit(url) - this.loadUrl = url + this.setProfileLoadUrl() - this.fullConnectivityLoadUrl = selectedDatasetId ? `${this.connectivityUrl}/${selectedDatasetId}/full_matrix` : null + this.fullConnectivityLoadUrl = `${this.siibraApiUrl}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcellationId)}/features/ConnectivityMatrix/${encodeURIComponent(this.selectedDataset)}` } } diff --git a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html index 178cab32f593d5eb1690fcd46c89ab1cb44b4ea5..a24a3df856d33d7f678134ae110f6c0a6f59686a 100644 --- a/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ b/src/atlasComponents/connectivity/connectivityBrowser/connectivityBrowser.template.html @@ -30,8 +30,8 @@ (selectionChange)="changeDataset($event)"> <mat-option *ngFor="let dataset of datasetList" - [value]="dataset.name"> - {{ dataset.name }} + [value]="dataset['@id']"> + {{ dataset['src_name'] }} </mat-option> </mat-select> </mat-form-field> diff --git a/src/atlasComponents/connectivity/hasConnectivity.directive.ts b/src/atlasComponents/connectivity/hasConnectivity.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..c547b715a7dfb5c7937ea8b7233199a790976333 --- /dev/null +++ b/src/atlasComponents/connectivity/hasConnectivity.directive.ts @@ -0,0 +1,62 @@ +import {Directive, Inject, Input, OnDestroy, OnInit} from "@angular/core"; +import {of, Subscription} from "rxjs"; +import {switchMap} from "rxjs/operators"; +import {BS_ENDPOINT} from "src/util/constants"; +import {HttpClient} from "@angular/common/http"; +import {getIdFromKgIdObj} from "common/util"; + +@Directive({ + selector: '[has-connectivity]', + exportAs: 'hasConnectivityDirective' +}) + +export class HasConnectivity implements OnInit, OnDestroy { + + private subscriptions: Subscription[] = [] + + @Input() region: any + + public hasConnectivity = false + public connectivityNumber = 0 + + constructor(@Inject(BS_ENDPOINT) private siibraApiUrl: string, + private httpClient: HttpClient) {} + + ngOnInit() { + this.checkConnectivity(this.region[0]) + } + + checkConnectivity(region) { + const {atlas, parcellation, template} = region.context + if (region.id || region.name) { + const regionId = region.id? region.id.kg? getIdFromKgIdObj(region.id.kg) + : region.id : null + + const connectivityUrl = `${this.siibraApiUrl}/atlases/${encodeURIComponent(atlas['@id'])}/parcellations/${encodeURIComponent(parcellation['@id'])}/regions/${encodeURIComponent(regionId || region.name)}/features/ConnectivityProfile` + + this.subscriptions.push( + this.httpClient.get<[]>(connectivityUrl).pipe(switchMap((res: any[]) => { + if (res && res.length) { + this.hasConnectivity = true + const url = `${connectivityUrl}/${encodeURIComponent(res[0]['@id'])}` + return this.httpClient.get(url) + } else { + this.hasConnectivity = false + this.connectivityNumber = 0 + } + return of(null) + })).subscribe(res => { + + if (res && res['__profile']) { + this.connectivityNumber = res['__profile'].filter(p => p > 0).length + } + }) + ) + } + } + + ngOnDestroy(){ + while (this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe() + } + +} diff --git a/src/atlasComponents/connectivity/module.ts b/src/atlasComponents/connectivity/module.ts index 8d629f66cfeb00b9f7b1f38fa45780725cac8f87..1f505eb7ca659ecef7d6a7f088e82eb31a605e3f 100644 --- a/src/atlasComponents/connectivity/module.ts +++ b/src/atlasComponents/connectivity/module.ts @@ -3,22 +3,25 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; import { DatabrowserModule } from "../databrowserModule"; import { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; +import {HasConnectivity} from "src/atlasComponents/connectivity/hasConnectivity.directive"; @NgModule({ imports: [ CommonModule, DatabrowserModule, - AngularMaterialModule, + AngularMaterialModule ], declarations: [ ConnectivityBrowserComponent, + HasConnectivity ], exports: [ ConnectivityBrowserComponent, + HasConnectivity ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, ], }) -export class AtlasCmptConnModule{} \ No newline at end of file +export class AtlasCmptConnModule{} diff --git a/src/atlasComponents/databrowserModule/databrowser.module.ts b/src/atlasComponents/databrowserModule/databrowser.module.ts index 60aa012d2ee3cb06d4f9364125156a8f2fc0ada1..b1faaa923efd736c1c7b689e5ef2c1676ed986d8 100644 --- a/src/atlasComponents/databrowserModule/databrowser.module.ts +++ b/src/atlasComponents/databrowserModule/databrowser.module.ts @@ -4,10 +4,7 @@ import { FormsModule } from "@angular/forms"; import { ComponentsModule } from "src/components/components.module"; import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' import { UtilModule } from "src/util"; -import { DataBrowser } from "./databrowser/databrowser.component"; import { KgSingleDatasetService } from "./kgSingleDatasetService.service" -import { ModalityPicker, SortModalityAlphabeticallyPipe } from "./modalityPicker/modalityPicker.component"; -import { SingleDatasetView } from './singleDataset/detailedView/singleDataset.component' import { AggregateArrayIntoRootPipe } from "./util/aggregateArrayIntoRoot.pipe"; import { CopyPropertyPipe } from "./util/copyProperty.pipe"; import { DatasetIsFavedPipe } from "./util/datasetIsFaved.pipe"; @@ -20,14 +17,11 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { PreviewFileIconPipe } from "./preview/previewFileIcon.pipe"; import { PreviewFileTypePipe } from "./preview/previewFileType.pipe"; import { SingleDatasetListView } from "./singleDataset/listView/singleDatasetListView.component"; -import { AppendFilerModalityPipe } from "./util/appendFilterModality.pipe"; import { GetKgSchemaIdFromFullIdPipe, getKgSchemaIdFromFullId } from "./util/getKgSchemaIdFromFullId.pipe"; -import { ResetCounterModalityPipe } from "./util/resetCounterModality.pipe"; import { PreviewFileVisibleInSelectedReferenceTemplatePipe } from "./util/previewFileDisabledByReferenceSpace.pipe"; import { DatasetPreviewList, UnavailableTooltip } from "./preview/datasetPreviews/datasetPreviewsList/datasetPreviewList.component"; import { PreviewComponentWrapper } from "./preview/previewComponentWrapper/previewCW.component"; import { BulkDownloadBtn, TransformDatasetToIdPipe } from "./bulkDownload/bulkDownloadBtn.component"; -import { ShowDatasetDialogDirective, IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "./showDatasetDialog.directive"; import { PreviewDatasetFile, IAV_DATASET_PREVIEW_DATASET_FN, IAV_DATASET_PREVIEW_ACTIVE, TypePreviewDispalyed } from "./preview/previewDatasetFile.directive"; import { @@ -38,16 +32,14 @@ import { OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN, } from './constants' import { ShownPreviewsDirective } from "./preview/shownPreviews.directive"; -import { FilterPreviewByType } from "./preview/filterPreview.pipe"; -import { PreviewCardComponent } from "./preview/previewCard/previewCard.component"; import { LayerBrowserModule } from "../../ui/layerbrowser"; -import { DatabrowserDirective } from "./databrowser/databrowser.directive"; + import { ContributorModule } from "./contributor"; import { DatabrowserService } from "./databrowser.service"; import { ShownDatasetDirective } from "./shownDataset.directive"; -import { SingleDatasetSideNavView } from "./singleDataset/sideNavView/sDsSideNavView.component"; import { RegionalFeaturesModule } from "../regionalFeatures"; import { SingleDatasetDirective } from "./singleDataset/singleDataset.directive"; +import { KgDatasetModule } from "../regionalFeatures/bsFeatures/kgDataset"; const previewEmitFactory = ( overrideFn: (file: any, dataset: any) => void) => { @@ -55,6 +47,9 @@ const previewEmitFactory = ( overrideFn: (file: any, dataset: any) => void) => { return () => console.error(`previewEmitFactory not overriden`) } +/** + * TODO deprecate + */ @NgModule({ imports: [ CommonModule, @@ -66,26 +61,20 @@ const previewEmitFactory = ( overrideFn: (file: any, dataset: any) => void) => { LayerBrowserModule, ContributorModule, RegionalFeaturesModule, + KgDatasetModule, ], declarations: [ - DataBrowser, - ModalityPicker, - SingleDatasetView, SingleDatasetDirective, SingleDatasetListView, DatasetPreviewList, PreviewComponentWrapper, BulkDownloadBtn, - PreviewCardComponent, - SingleDatasetSideNavView, /** * Directives */ - ShowDatasetDialogDirective, PreviewDatasetFile, ShownPreviewsDirective, - DatabrowserDirective, ShownDatasetDirective, /** @@ -101,47 +90,31 @@ const previewEmitFactory = ( overrideFn: (file: any, dataset: any) => void) => { GetKgSchemaIdFromFullIdPipe, PreviewFileIconPipe, PreviewFileTypePipe, - AppendFilerModalityPipe, - ResetCounterModalityPipe, PreviewFileVisibleInSelectedReferenceTemplatePipe, UnavailableTooltip, TransformDatasetToIdPipe, - SortModalityAlphabeticallyPipe, PreviewFileTypePipe, - FilterPreviewByType, ], exports: [ - DataBrowser, - SingleDatasetView, + KgDatasetModule, SingleDatasetDirective, SingleDatasetListView, - ModalityPicker, FilterDataEntriesbyMethods, GetKgSchemaIdFromFullIdPipe, BulkDownloadBtn, TransformDatasetToIdPipe, - ShowDatasetDialogDirective, PreviewDatasetFile, PreviewFileTypePipe, ShownPreviewsDirective, - FilterPreviewByType, - PreviewCardComponent, - DatabrowserDirective, ShownDatasetDirective, - SingleDatasetSideNavView, ], entryComponents: [ - DataBrowser, - SingleDatasetView, PreviewComponentWrapper ], providers: [ KgSingleDatasetService, DatabrowserService, { - provide: IAV_DATASET_SHOW_DATASET_DIALOG_CMP, - useValue: SingleDatasetView - },{ provide: IAV_DATASET_PREVIEW_DATASET_FN, useFactory: previewEmitFactory, deps: [ [new Optional(), OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN] ] diff --git a/src/atlasComponents/databrowserModule/databrowser.service.ts b/src/atlasComponents/databrowserModule/databrowser.service.ts index 1bf1b91d68ed60bce9c21a2a78b4c52aa52d2a7a..31fd9b517d5818e168e0d47ce73dc64fdfe44159 100644 --- a/src/atlasComponents/databrowserModule/databrowser.service.ts +++ b/src/atlasComponents/databrowserModule/databrowser.service.ts @@ -10,7 +10,7 @@ import { WidgetUnit } from "src/widget"; import { LoggingService } from "src/logging"; import { SHOW_KG_TOS } from "src/services/state/uiState.store.helper"; -import { DataBrowser } from "./databrowser/databrowser.component"; + import { NO_METHODS } from "./util/filterDataEntriesByMethods.pipe"; import { FilterDataEntriesByRegion } from "./util/filterDataEntriesByRegion.pipe"; import { datastateActionToggleFav, datastateActionUnfavDataset, datastateActionFavDataset, datastateActionFetchedDataentries } from "src/services/state/dataState/actions"; @@ -57,14 +57,7 @@ export class DatabrowserService implements OnDestroy { public darktheme: boolean = false public instantiatedWidgetUnits: WidgetUnit[] = [] - public queryData: (arg: {regions: any[], template: any, parcellation: any}) => void = (arg) => { - const { widgetUnit } = this.createDatabrowser(arg) - this.instantiatedWidgetUnits.push(widgetUnit.instance) - widgetUnit.onDestroy(() => { - this.instantiatedWidgetUnits = this.instantiatedWidgetUnits.filter(db => db !== widgetUnit.instance) - }) - } - public createDatabrowser: (arg: {regions: any[], template: any, parcellation: any}) => {dataBrowser: ComponentRef<DataBrowser>, widgetUnit: ComponentRef<WidgetUnit>} + public getDataByRegion: (arg: {regions: any[] }) => Observable<IKgDataEntry[]> = ({ regions }) => forkJoin(regions.map(this.getDatasetsByRegion.bind(this))).pipe( map( @@ -352,12 +345,6 @@ export class DatabrowserService implements OnDestroy { }) } - public dbComponentInit(_db: DataBrowser) { - this.store.dispatch({ - type: SHOW_KG_TOS, - }) - } - public getModalityFromDE = getModalityFromDE } diff --git a/src/atlasComponents/databrowserModule/databrowser/databrowser.base.ts b/src/atlasComponents/databrowserModule/databrowser/databrowser.base.ts deleted file mode 100644 index c2c8279a94419ca203a44a2a011aa6c9c5b098d3..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/databrowser/databrowser.base.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Input, Output, EventEmitter, OnDestroy } from "@angular/core" -import { LoggingService } from "src/logging" -import { DatabrowserService } from "../singleDataset/singleDataset.base" -import { Observable, Subject, Subscription } from "rxjs" -import { IDataEntry } from "src/services/stateStore.service" -import { setsEql } from 'common/util' -import { switchMap, tap } from "rxjs/operators" -import { getStringIdsFromRegion, flattenReducer } from 'common/util' - -export class DatabrowserBase implements OnDestroy{ - - private _subscriptions: Subscription[] = [] - - @Output() - public dataentriesUpdated: EventEmitter<IDataEntry[]> = new EventEmitter() - - private _regions: any[] = [] - - public regions$ = new Subject<any[]>() - get regions(){ - return this._regions - } - @Input() - set regions(arr: any[]){ - const currentSet = new Set(this._regions.map(r => getStringIdsFromRegion(r)).reduce(flattenReducer, [])) - const newSet = new Set(arr.map(r => getStringIdsFromRegion(r)).reduce(flattenReducer, []).filter(v => !!v)) - if (setsEql(newSet, currentSet)) return - this._regions = arr.filter(r => !getStringIdsFromRegion(r).every(id => !id)) - this.regions$.next(this._regions) - } - - public fetchError: boolean = false - public fetchingFlag = false - - public favDataentries$: Observable<Partial<IDataEntry>[]> - - public dataentries: IDataEntry[] = [] - - constructor( - private dbService: DatabrowserService, - private log: LoggingService, - ){ - - this.favDataentries$ = this.dbService.favedDataentries$ - - this._subscriptions.push( - this.regions$.pipe( - tap(() => this.fetchingFlag = true), - switchMap(regions => this.dbService.getDataByRegion({ regions })), - ).subscribe( - de => { - this.fetchingFlag = false - this.dataentries = de - this.dataentriesUpdated.emit(de) - }, - e => { - this.log.error(e) - this.fetchError = true - } - ) - ) - } - - ngOnDestroy(){ - while(this._subscriptions.length > 0) this._subscriptions.pop().unsubscribe() - } - - public retryFetchData(event: MouseEvent) { - event.preventDefault() - this.dbService.manualFetchDataset$.next(null) - } - - public toggleFavourite(dataset: IDataEntry) { - this.dbService.toggleFav(dataset) - } - - public saveToFavourite(dataset: IDataEntry) { - this.dbService.saveToFav(dataset) - } - - public removeFromFavourite(dataset: IDataEntry) { - this.dbService.removeFromFav(dataset) - } -} diff --git a/src/atlasComponents/databrowserModule/databrowser/databrowser.component.ts b/src/atlasComponents/databrowserModule/databrowser/databrowser.component.ts deleted file mode 100644 index 59b813972f72d3b3fdd5fb36b248ab5eaba6049e..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/databrowser/databrowser.component.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { Subscription } from "rxjs"; -import { LoggingService } from "src/logging"; -import { IDataEntry } from "src/services/state/dataStore.store"; -import { CountedDataModality, DatabrowserService } from "../databrowser.service"; -import { ModalityPicker } from "../modalityPicker/modalityPicker.component"; -import { ARIA_LABELS } from 'common/constants.js' -import { DatabrowserBase } from "./databrowser.base"; -import { debounceTime } from "rxjs/operators"; -import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, TOverwriteShowDatasetDialog } from "src/util/interfaces"; -import { Store } from "@ngrx/store"; -import { uiActionShowDatasetWtihId } from "src/services/state/uiState/actions"; - -const { MODALITY_FILTER, LIST_OF_DATASETS } = ARIA_LABELS - -@Component({ - selector : 'data-browser', - templateUrl : './databrowser.template.html', - styleUrls : [ - `./databrowser.style.css`, - ], - exportAs: 'dataBrowser', - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ - - { - provide: OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, - useFactory: (store: Store<any>) => { - return function overwriteShowDatasetDialog( arg: { fullId?: string, name: string, description: string } ){ - if (arg.fullId) { - store.dispatch( - uiActionShowDatasetWtihId({ - id: arg.fullId - }) - ) - } - } as TOverwriteShowDatasetDialog - }, - deps: [ - Store - ] - } - ] - -}) - -export class DataBrowser extends DatabrowserBase implements OnDestroy, OnInit { - - @Input() - disableVirtualScroll: boolean = false - - @Input() - showList: boolean = true - - public MODALITY_FILTER_ARIA_LABEL = MODALITY_FILTER - public LIST_OF_DATASETS_ARIA_LABEL = LIST_OF_DATASETS - - - /** - * TODO filter types - */ - private subscriptions: Subscription[] = [] - public countedDataM: CountedDataModality[] = [] - public visibleCountedDataM: CountedDataModality[] = [] - - @ViewChild(ModalityPicker) - public modalityPicker: ModalityPicker - - - /** - * TODO - * viewport - * user defined filter - * etc - */ - public gemoetryFilter: any - - constructor( - private dataService: DatabrowserService, - private cdr: ChangeDetectorRef, - log: LoggingService, - ) { - super(dataService, log) - } - - public ngOnInit() { - - /** - * in the event that dataentries are updated before ngInit lifecycle hook - */ - this.countedDataM = this.dataService.getModalityFromDE(this.dataentries) - - this.subscriptions.push( - this.dataentriesUpdated.pipe( - debounceTime(60) - ).subscribe(() => { - this.countedDataM = this.dataService.getModalityFromDE(this.dataentries) - this.cdr.markForCheck() - }) - ) - - /** - * TODO gets init'ed everytime when appends to ngtemplateoutlet - */ - this.dataService.dbComponentInit(this) - - /** - * TODO fix - */ - // this.subscriptions.push( - // this.filterApplied$.subscribe(() => this.currentPage = 0) - // ) - } - - public ngOnDestroy() { - super.ngOnDestroy() - this.subscriptions.forEach(s => s.unsubscribe()) - } - - public clearAll() { - this.countedDataM = this.countedDataM.map(cdm => { - return { - ...cdm, - visible: false, - } - }) - this.visibleCountedDataM = [] - } - - public handleModalityFilterEvent(modalityFilter: CountedDataModality[]) { - this.countedDataM = modalityFilter - this.visibleCountedDataM = modalityFilter.filter(dm => dm.visible) - this.cdr.markForCheck() - } - - public showParcellationList: boolean = false - - public filePreviewName: string - public onShowPreviewDataset(payload: {datasetName: string, event: MouseEvent}) { - const { datasetName } = payload - this.filePreviewName = datasetName - } - - public resetFilters(_event?: MouseEvent) { - this.clearAll() - } - - public trackByFn(index: number, dataset: IDataEntry) { - return dataset.id - } -} - -export interface IDataEntryFilter { - filter: (dataentries: IDataEntry[]) => IDataEntry[] -} diff --git a/src/atlasComponents/databrowserModule/databrowser/databrowser.directive.ts b/src/atlasComponents/databrowserModule/databrowser/databrowser.directive.ts deleted file mode 100644 index 911a8a2b9497f821150c125cc12006cb8e2f2d7e..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/databrowser/databrowser.directive.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Directive, OnDestroy } from "@angular/core"; -import { DatabrowserBase } from "./databrowser.base"; -import { DatabrowserService } from "../singleDataset/singleDataset.base"; -import { LoggingService } from "src/logging"; - -@Directive({ - selector: '[iav-databrowser-directive]', - exportAs: 'iavDatabrowserDirective' -}) - -export class DatabrowserDirective extends DatabrowserBase implements OnDestroy{ - constructor( - dataService: DatabrowserService, - log: LoggingService, - ){ - super(dataService, log) - } - - ngOnDestroy(){ - super.ngOnDestroy() - } -} diff --git a/src/atlasComponents/databrowserModule/databrowser/databrowser.style.css b/src/atlasComponents/databrowserModule/databrowser/databrowser.style.css deleted file mode 100644 index 0320f94c00443a8ec0a67f97b4aa1ea7d9842a09..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/databrowser/databrowser.style.css +++ /dev/null @@ -1,24 +0,0 @@ -:host -{ - display: flex; - flex-direction: column; - width: 100%; - height: 100%; -} - -modality-picker -{ - font-size: 90%; -} - -radio-list -{ - display: block; -} - - -/* grow 3x shrink 1x basis 2.5 x 50px (2.5 rows) */ -.dataset-container -{ - flex: 3 1 125px; -} diff --git a/src/atlasComponents/databrowserModule/databrowser/databrowser.template.html b/src/atlasComponents/databrowserModule/databrowser/databrowser.template.html deleted file mode 100644 index 7c684c6cdf0fbe4332c4488c551b4caf63c98e17..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/databrowser/databrowser.template.html +++ /dev/null @@ -1,173 +0,0 @@ -<!-- transclusion header --> -<ng-content select="[card-header]"> -</ng-content> - -<!-- transclusion content prepend --> -<ng-content select="[card-content='prepend']"> -</ng-content> - -<!-- modality filter --> -<div class="mb-1" > - <ng-container *ngTemplateOutlet="modalitySelector"> - </ng-container> -</div> - -<!-- if still loading, show spinner --> -<ng-template [ngIf]="fetchingFlag" [ngIfElse]="resultsTmpl"> - <ng-container *ngTemplateOutlet="loadingSpinner"> - </ng-container> -</ng-template> - -<!-- else, show fetched --> -<ng-template #resultsTmpl> - - <!-- if error, show error only --> - <ng-template [ngIf]="fetchError"> - <ng-container *ngTemplateOutlet="errorTemplate"> - </ng-container> - </ng-template> - - <!-- if not error, show dataset template --> - - <ng-template [ngIf]="!fetchError"> - <ng-template [ngTemplateOutlet]="datasetListTmpl" - [ngIf]="disableVirtualScroll" - [ngIfElse]="datasetVirtualScrollTmpl"> - - </ng-template> - </ng-template> -</ng-template> - -<!-- footer, populated by content transclusion --> -<ng-content select="[card-footer]"> -</ng-content> - -<ng-template #loadingSpinner> - <mat-card-content class="h-100 d-flex justify-content-start p-2"> - <spinner-cmp class="mr-2"></spinner-cmp> - <span>Fetching datasets...</span> - </mat-card-content> -</ng-template> - -<ng-template #errorTemplate> - <mat-card-content> - <div class="ml-2 mr-2 alert alert-danger"> - <i class="fas fa-exclamation-triangle"></i> Error fetching data. <a href="#" (click)="retryFetchData($event)" class="btn btn-link text-info">retry</a> - </div> - </mat-card-content> -</ng-template> - -<ng-template #datasetVirtualScrollTmpl> - <!-- datawrapper --> - - <ng-container *ngIf="dataentries | filterDataEntriesByMethods : visibleCountedDataM as filteredDataEntry"> - <mat-card-content class="dataset-container w-100 overflow-hidden"> - <!-- TODO export aria labels to common/constants --> - <cdk-virtual-scroll-viewport - *ngIf="showList" - [attr.aria-label]="LIST_OF_DATASETS_ARIA_LABEL" - class="h-100" - minBufferPx="200" - maxBufferPx="400" - itemSize="50"> - <div *cdkVirtualFor="let dataset of filteredDataEntry; trackBy: trackByFn; templateCacheSize: 20; let index = index" - class="virtual-scroll-element overflow-hidden"> - - <!-- divider, show if not first --> - <mat-divider *ngIf="index !== 0"></mat-divider> - - <single-dataset-list-view - class="d-block pt-1 pb-1 h-100" - [kgSchema]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[0]" - [kgId]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[1]" - [dataset]="dataset" - [ripple]="true"> - - </single-dataset-list-view> - - - </div> - </cdk-virtual-scroll-viewport> - </mat-card-content> - </ng-container> -</ng-template> - -<ng-template #datasetListTmpl> - - <ng-container *ngIf="dataentries | filterDataEntriesByMethods : visibleCountedDataM as filteredDataEntry"> - <mat-card-content class="w-100"> - <!-- TODO export aria labels to common/constants --> - <div *ngIf="showList"> - <div *ngFor="let dataset of filteredDataEntry; trackBy: trackByFn; let index = index" - class="scroll-element overflow-hidden"> - - <mat-divider *ngIf="index !== 0"></mat-divider> - - <single-dataset-list-view - class="d-block pt-1 pb-1 h-100" - [kgSchema]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[0]" - [kgId]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[1]" - [dataset]="dataset" - [ripple]="true"> - - </single-dataset-list-view> - - </div> - </div> - </mat-card-content> - </ng-container> -</ng-template> - -<!-- modality picker / filter --> -<ng-template #modalitySelector> - <mat-accordion class="flex-grow-0 flex-shrink-0"> - - <!-- Filters --> - <mat-expansion-panel hideToggle> - - <mat-expansion-panel-header class="align-items-center" - [attr.aria-label]="MODALITY_FILTER_ARIA_LABEL"> - <mat-panel-title class="d-inline-flex align-items-center"> - <div class="flex-grow-1 flex-shrink-1 d-flex flex-column"> - <span> - Filter features - </span> - <small *ngIf="dataentries.length > 0" class="text-muted"> - <ng-template [ngIf]="modalityPickerCmp && modalityPickerCmp.checkedModality.length > 0" - [ngIfElse]="noFilterTmpl"> - {{ (dataentries | filterDataEntriesByMethods : visibleCountedDataM).length }} / {{ dataentries.length }} - </ng-template> - - <ng-template #noFilterTmpl> - {{ dataentries.length }} features - </ng-template> - </small> - </div> - - <button mat-icon-button - [matTooltip]="visibleCountedDataM.length > 0 ? 'Reset filters' : null" - iav-delay-event="click" - (iav-delay-event-emit)="visibleCountedDataM.length > 0 ? clearAll() : null" - [iav-stop]="visibleCountedDataM.length > 0 ? 'click' : null" - [color]="visibleCountedDataM.length > 0 ? 'primary' : 'basic'"> - <i class="fas fa-filter"></i> - </button> - </mat-panel-title> - - </mat-expansion-panel-header> - - <div class="max-h-10em overflow-y-auto overflow-x-hidden"> - <modality-picker - iav-stop="click" - class="w-100" - [countedDataM]="visibleCountedDataM | resetcounterModalityPipe | appendFilterModalityPipe : [countedDataM]" - (modalityFilterEmitter)="handleModalityFilterEvent($event)" - #modalityPickerCmp> - - </modality-picker> - </div> - - </mat-expansion-panel> - </mat-accordion> - -</ng-template> diff --git a/src/atlasComponents/databrowserModule/preview/filterPreview.pipe.ts b/src/atlasComponents/databrowserModule/preview/filterPreview.pipe.ts deleted file mode 100644 index 21e909d58b905e5b27ddc6d46f0196164421ac64..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/preview/filterPreview.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { EnumPreviewFileTypes } from "../pure"; -import { ViewerPreviewFile } from "src/services/state/dataStore.store"; -import { determinePreviewFileType } from "../constants"; - -@Pipe({ - name: 'filterPreviewByType' -}) - -export class FilterPreviewByType implements PipeTransform{ - public transform(files: ViewerPreviewFile[], types: EnumPreviewFileTypes[]){ - return files.filter(f => { - const currentFileType = determinePreviewFileType(f) - return types.includes(currentFileType) - }) - } -} \ No newline at end of file diff --git a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.component.ts b/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.component.ts deleted file mode 100644 index 73ddae99dc17d0574fccbd2ac4890d7a44ec4b96..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, Optional, Inject } from "@angular/core"; -import { PreviewBase } from "../preview.base"; -import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "../../pure"; - -// TODO deprecate in favour of datafeature/region feature - -@Component({ - selector: 'preview-card', - templateUrl: './previewCard.template.html', - styleUrls: [ - './previewCard.style.css' - ] -}) - -export class PreviewCardComponent extends PreviewBase { - constructor( - @Optional() @Inject(GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME) getDatasetPreviewFromId, - ){ - super(getDatasetPreviewFromId) - } -} diff --git a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.template.html b/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.template.html deleted file mode 100644 index 4438e03e4e2b0b055211ffc49d7ab06bcc08c514..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.template.html +++ /dev/null @@ -1,115 +0,0 @@ -<mat-card class="mat-elevation-z4"> - <div class="sidenav-cover-header-container bg-50-grey-20"> - <mat-card-title> - {{ singleDsView?.name || file.name || filename }} - </mat-card-title> - - <mat-card-subtitle class="d-inline-flex align-items-center"> - <mat-icon fontSet="fas" fontIcon="fa-database"></mat-icon> - <span> - Dataset preview - </span> - - <mat-divider [vertical]="true" class="ml-2 h-2rem"></mat-divider> - - <!-- explore btn --> - <a *ngFor="let kgRef of singleDsView.kgReference" - [href]="kgRef | doiParserPipe" - class="color-inherit" - target="_blank"> - <button mat-icon-button - [matTooltip]="singleDsView.EXPLORE_DATASET_IN_KG_ARIA_LABEL"> - <i class="fas fa-external-link-alt"></i> - </button> - </a> - - <!-- pin dataset btn --> - <ng-container *ngTemplateOutlet="favDatasetBtn; context: { singleDataset: singleDsView }"> - </ng-container> - - <!-- download zip btn --> - <a *ngIf="singleDsView.files && singleDsView.files.length > 0" - [href]="singleDsView.dlFromKgHref" - class="color-inherit" - target="_blank"> - <button mat-icon-button - [matTooltip]="singleDsView.tooltipText" - [disabled]="singleDsView.downloadInProgress"> - <i class="ml-1 fas" [ngClass]="!singleDsView.downloadInProgress? 'fa-download' :'fa-spinner fa-pulse'"></i> - </button> - </a> - - </mat-card-subtitle> - </div> - - <mat-card-content class="mt-2 ml-15px-n mr-15px-n pb-4"> - <mat-accordion> - <mat-expansion-panel hideToggle - [expanded]="true"> - - <mat-expansion-panel-header> - <mat-panel-title> - Description - </mat-panel-title> - </mat-expansion-panel-header> - - <ng-template matExpansionPanelContent> - <single-dataset-view [fullId]="datasetId" - [hideTitle]="true" - [hidePreview]="true" - [hideExplore]="true" - [hidePinBtn]="true" - [hideDownloadBtn]="true" - #singleDsView="singleDatasetView"> - - </single-dataset-view> - </ng-template> - </mat-expansion-panel> - - - <mat-expansion-panel hideToggle> - - <mat-expansion-panel-header> - <mat-panel-title> - Registered Volumes - </mat-panel-title> - </mat-expansion-panel-header> - - <ng-template matExpansionPanelContent> - <!-- TODO --> - <!-- this is not exactly right --> - <layer-browser class="ml-24px-n mr-24px-n"></layer-browser> - </ng-template> - - </mat-expansion-panel> - </mat-accordion> - - </mat-card-content> -</mat-card> - -<!-- templates --> -<ng-template #favDatasetBtn let-singleDataset="singleDataset"> - <ng-container *ngTemplateOutlet="isFavCtxTmpl; context: { isFav: (singleDataset.favedDataentries$ | async | datasetIsFaved : singleDataset.dataset) }"> - </ng-container> - - <ng-template #isFavCtxTmpl let-isFav="isFav"> - <button mat-icon-button - (click)="isFav ? singleDataset.undoableRemoveFav() : singleDataset.undoableAddFav()" - [attr.aria-label]="singleDataset.PIN_DATASET_ARIA_LABEL" - [matTooltip]="singleDataset.PIN_DATASET_ARIA_LABEL" - [color]="isFav ? 'primary' : 'basic'"> - <i class="fas fa-thumbtack"></i> - </button> - </ng-template> -</ng-template> - -<single-dataset-view [fullId]="datasetId" - [hidden]="true" - [hideTitle]="true" - [hidePreview]="true" - [hideExplore]="true" - [hidePinBtn]="true" - [hideDownloadBtn]="true" - #singleDsView="singleDatasetView"> - -</single-dataset-view> \ No newline at end of file diff --git a/src/atlasComponents/databrowserModule/preview/previewDatasetFile.directive.spec.ts b/src/atlasComponents/databrowserModule/preview/previewDatasetFile.directive.spec.ts deleted file mode 100644 index 6d09d69bdc2b7c2b10e8631a7d2450fefb147e6a..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/preview/previewDatasetFile.directive.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Component } from "@angular/core"; -import { async, TestBed } from "@angular/core/testing"; -import { By } from "@angular/platform-browser"; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; -import { PreviewDatasetFile, IAV_DATASET_PREVIEW_DATASET_FN, IAV_DATASET_PREVIEW_ACTIVE } from './previewDatasetFile.directive' -import { Subject } from "rxjs"; - -@Component({ - template: '' -}) - -class TestCmp{ - testmethod(arg) {} -} - -const dummyMatSnackBar = { - open: jasmine.createSpy('open') -} - -const previewDatasetFnSpy = jasmine.createSpy('previewDatasetFn') -const mockDatasetActiveObs = new Subject() -const getDatasetActiveObs = jasmine.createSpy('getDatasetActive').and.returnValue(mockDatasetActiveObs) - -describe('ShowDatasetDialogDirective', () => { - let testModule - beforeEach(async(() => { - testModule = TestBed - .configureTestingModule({ - imports: [ - AngularMaterialModule - ], - declarations: [ - TestCmp, - PreviewDatasetFile, - ], - providers: [ - { - provide: MatSnackBar, - useValue: dummyMatSnackBar - }, - { - provide: IAV_DATASET_PREVIEW_DATASET_FN, - useValue: previewDatasetFnSpy - }, - ] - }) - - })) - - afterEach(() => { - dummyMatSnackBar.open.calls.reset() - previewDatasetFnSpy.calls.reset() - }) - - it('should be able to test directive', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: '<div iav-dataset-preview-dataset-file></div>', - } - }).compileComponents() - - const fixutre = TestBed.createComponent(TestCmp) - const directive = fixutre.debugElement.query( By.directive( PreviewDatasetFile ) ) - - expect(directive).not.toBeNull() - }) - - it('without providing file or filename, should not call emitFn', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: '<div iav-dataset-preview-dataset-file></div>', - } - }).compileComponents() - - const fixutre = TestBed.createComponent(TestCmp) - fixutre.detectChanges() - const directive = fixutre.debugElement.query( By.directive( PreviewDatasetFile ) ) - directive.nativeElement.click() - - expect(dummyMatSnackBar.open).toHaveBeenCalled() - expect(previewDatasetFnSpy).not.toHaveBeenCalled() - - }) - - it('only providing filename, should call emitFn', () => { - - TestBed.overrideComponent(TestCmp, { - set: { - template: ` - <div iav-dataset-preview-dataset-file - iav-dataset-preview-dataset-file-filename="banana"> - </div> - `, - } - }).compileComponents() - - const fixutre = TestBed.createComponent(TestCmp) - fixutre.detectChanges() - const directive = fixutre.debugElement.query( By.directive( PreviewDatasetFile ) ) - directive.nativeElement.click() - - expect(dummyMatSnackBar.open).not.toHaveBeenCalled() - expect(previewDatasetFnSpy).toHaveBeenCalledWith({ filename: 'banana' }, { fullId: null }) - - }) -}) \ No newline at end of file diff --git a/src/atlasComponents/databrowserModule/pure.spec.ts b/src/atlasComponents/databrowserModule/pure.spec.ts index 30d3bbf8e308180db32d061fbce6fdc81796a0f8..6527e6a3ef4e305c7426060a87f0f6eb9ede5f4b 100644 --- a/src/atlasComponents/databrowserModule/pure.spec.ts +++ b/src/atlasComponents/databrowserModule/pure.spec.ts @@ -1,7 +1,7 @@ -import * as _ from './pure' +// import * as _ from './pure' describe('> pure.ts', () => { it('> should be importable without hiccups', () => { - console.log(Object.keys(_)) + }) }) diff --git a/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.component.spec.ts b/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.component.spec.ts deleted file mode 100644 index 7cf7c9f2f7d0a11f35d7551131079eefaf7b299f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.component.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('singleDataset.component.ts', () => { - describe('', () => { - - }) -}) \ No newline at end of file diff --git a/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.component.ts b/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.component.ts deleted file mode 100644 index d273ee2f1388898c08789dccb31a9e3ef4d5ee48..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ChangeDetectorRef, Component, Inject, Optional, Input} from "@angular/core"; -import { - DatabrowserService, - KgSingleDatasetService, - SingleDatasetBase, -} from "../singleDataset.base"; -import {MAT_DIALOG_DATA} from "@angular/material/dialog"; -import { MatSnackBar } from "@angular/material/snack-bar"; - -@Component({ - selector: 'single-dataset-view', - templateUrl: './singleDataset.template.html', - styleUrls: [ - `./singleDataset.style.css`, - ], - exportAs: 'singleDatasetView' -}) - -export class SingleDatasetView extends SingleDatasetBase { - - constructor( - dbService: DatabrowserService, - singleDatasetService: KgSingleDatasetService, - cdr: ChangeDetectorRef, - snackbar: MatSnackBar, - @Optional() @Inject(MAT_DIALOG_DATA) data: any, - ) { - super(dbService, singleDatasetService, cdr,snackbar, data) - } - - @Input() - hideTitle = false - - @Input() - hidePreview = false - - @Input() - hideExplore = false - - @Input() - hidePinBtn = false - - @Input() - hideDownloadBtn = false - - @Input() - useSmallIcon = false -} diff --git a/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.style.css b/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.style.css deleted file mode 100644 index 9c4f33e35402143057541d30e4c76fe566cba76d..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.style.css +++ /dev/null @@ -1,4 +0,0 @@ -:host -{ - text-align: left -} \ No newline at end of file diff --git a/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.template.html b/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.template.html deleted file mode 100644 index 54e8c9e7be8ed518ad053151e79b6e1f12cfb574..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/singleDataset/detailedView/singleDataset.template.html +++ /dev/null @@ -1,162 +0,0 @@ - -<!-- title --> -<mat-card-subtitle *ngIf="!hideTitle"> - <span *ngIf="name; else isLoadingTmpl"> - {{ name }} - </span> -</mat-card-subtitle> - -<mat-card-content mat-dialog-content> - - <!-- description --> - <small> - <markdown-dom - *ngIf="description; else isLoadingTmpl" - class="d-block" - [markdown]="description"> - - </markdown-dom> - </small> - - <!-- publications --> - <ng-container *ngIf="!strictLocal"> - - <small class="d-block mb-2" - *ngFor="let publication of publications"> - <a *ngIf="publication.doi; else plainText" - iav-stop="click mousedown" - [href]="publication.doi | doiParserPipe" - target="_blank"> - {{ publication.cite }} - </a> - <ng-template #plainText> - {{ publication.cite }} - </ng-template> - </small> - </ng-container> - - <!-- contributors, if publications not available --> - <ng-container *ngIf="publications && publications.length == 0 && contributors && contributors.length > 0"> - <ng-container *ngFor="let contributor of contributors; let lastFlag = last;"> - <a [href]="contributor | getContributorKgLink" class="iv-custom-comp" target="_blank"> - {{ contributor['schema.org/shortName'] || contributor['shortName'] || contributor['name'] }} - </a> - <span *ngIf="!lastFlag">,</span> - </ng-container> - </ng-container> -</mat-card-content> - - -<!-- footer --> -<mat-card-actions iav-media-query #iavMediaQuery="iavMediaQuery"> - <ng-container *ngTemplateOutlet="actionBtns; context: { - $implicit: useSmallIcon || (iavMediaQuery.mediaBreakPoint$ | async) > 1 - }" > - </ng-container> -</mat-card-actions> - -<mat-card-footer></mat-card-footer> - -<ng-template #previewFilesListTemplate> - <dataset-preview-list - [kgId]="kgId"> - - </dataset-preview-list> -</ng-template> - -<!-- using ng template for context binding of media breakpoints --> -<ng-template #actionBtns let-useSmallIcon> - - <!-- explore --> - <ng-container *ngIf="!strictLocal && !hideExplore"> - - <a *ngFor="let kgRef of kgReference" - [href]="kgRef | doiParserPipe" - target="_blank"> - <iav-dynamic-mat-button - [iav-dynamic-mat-button-style]="useSmallIcon ? 'mat-icon-button' : 'mat-raised-button'" - iav-dynamic-mat-button-color="primary"> - - <span *ngIf="!useSmallIcon"> - Explore - </span> - <i class="fas fa-external-link-alt"></i> - </iav-dynamic-mat-button> - </a> - </ng-container> - - <!-- pin data --> - <ng-container *ngIf="downloadEnabled && kgId"> - - <ng-container *ngTemplateOutlet="favDatasetBtn; context: { $implicit: (favedDataentries$ | async | datasetIsFaved : ({ fullId: fullId })) }"> - </ng-container> - - <ng-template #favDatasetBtn let-isFav> - <iav-dynamic-mat-button - *ngIf="!hidePinBtn" - (click)="isFav ? undoableRemoveFav() : undoableAddFav()" - iav-stop="click mousedown" - [iav-dynamic-mat-button-aria-label]="PIN_DATASET_ARIA_LABEL" - [iav-dynamic-mat-button-style]="useSmallIcon ? 'mat-icon-button' : 'mat-button'" - [iav-dynamic-mat-button-color]="isFav ? 'primary' : 'basic'"> - - <span *ngIf="!useSmallIcon"> - {{ isFav ? 'Unpin this dataset' : 'Pin this dataset' }} - </span> - <i class="fas fa-thumbtack"></i> - </iav-dynamic-mat-button> - </ng-template> - </ng-container> - - <!-- download --> - <ng-container *ngIf="!strictLocal && !hideDownloadBtn"> - - <a *ngIf="files && files.length > 0" - [href]="dlFromKgHref" - target="_blank"> - <iav-dynamic-mat-button - [matTooltip]="tooltipText" - [disabled]="downloadInProgress" - [iav-dynamic-mat-button-style]="useSmallIcon ? 'mat-icon-button' : 'mat-button'"> - - <span *ngIf="!useSmallIcon"> - Download Zip - </span> - <i class="ml-1 fas" [ngClass]="!downloadInProgress? 'fa-download' :'fa-spinner fa-pulse'"></i> - </iav-dynamic-mat-button> - </a> - </ng-container> - - - <!-- check if has preview --> - - <ng-template [ngIf]="!hidePreview"> - - <kg-dataset-list - class="d-none" - [backendUrl]="DS_PREVIEW_URL" - *ngIf="kgId" - (kgDsPrvUpdated)="handleKgDsPrvUpdate($event)" - [kgId]="kgId"> - - </kg-dataset-list> - - <iav-dynamic-mat-button - *ngIf="hasPreview" - mat-dialog-close - [iav-dynamic-mat-button-style]="useSmallIcon ? 'mat-icon-button' : 'mat-button'" - [iav-dynamic-mat-button-aria-label]="SHOW_DATASET_PREVIEW_ARIA_LABEL" - (click)="showPreviewList(previewFilesListTemplate)"> - - <span *ngIf="!useSmallIcon"> - Preview - </span> - <i class="ml-1 far fa-eye"></i> - </iav-dynamic-mat-button> - </ng-template> - -</ng-template> - -<ng-template #isLoadingTmpl> - <spinner-cmp></spinner-cmp> -</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.component.ts b/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.component.ts deleted file mode 100644 index fe8eb871cc1b166fcf962add30a7402f3d907313..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnChanges, OnDestroy, Optional, Output, EventEmitter } from "@angular/core"; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { Observable } from "rxjs"; -import { REGION_OF_INTEREST, TRegionOfInterest } from "src/util/interfaces"; -import { DatabrowserService } from "../../databrowser.service"; -import { KgSingleDatasetService } from "../../kgSingleDatasetService.service"; -import { SingleDatasetBase } from "../singleDataset.base"; -import { CONST, ARIA_LABELS } from 'common/constants' - -@Component({ - selector: 'single-dataset-sidenav-view', - templateUrl: './sDsSideNavView.template.html', - styleUrls: [ - './sDsSideNavView.style.css' - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) - -export class SingleDatasetSideNavView extends SingleDatasetBase implements OnChanges, OnDestroy{ - public BACK_BTN_ARIA_TXT = ARIA_LABELS.CLOSE - @Output() - clear: EventEmitter<null> = new EventEmitter() - - public GDPR_TEXT = CONST.GDPR_TEXT - - constructor( - dbService: DatabrowserService, - sDsService: KgSingleDatasetService, - private _cdr: ChangeDetectorRef, - snackBar: MatSnackBar, - @Optional() @Inject(REGION_OF_INTEREST) public region$: Observable<TRegionOfInterest> - ){ - super( - dbService, - sDsService, - _cdr, - snackBar, - ) - } - ngOnDestroy(){ - super.ngOnDestroy() - } - ngOnChanges(){ - super.ngOnChanges() - } - - detectChange(){ - this._cdr.detectChanges() - } -} diff --git a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.style.css b/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.style.css deleted file mode 100644 index 9105967131e061d7d2959065d49a71d716ee398c..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.style.css +++ /dev/null @@ -1,4 +0,0 @@ -:host -{ - position: relative; -} diff --git a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html b/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html deleted file mode 100644 index 2acd816673e51556d4b66eae36f8b3078472b485..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html +++ /dev/null @@ -1,169 +0,0 @@ -<button mat-button - [attr.aria-label]="BACK_BTN_ARIA_TXT" - class="position-absolute z-index-10 m-2" - (click)="clear.emit()"> - <i class="fas fa-chevron-left"></i> - <span class="ml-1"> - Back - </span> -</button> - -<mat-card class="mat-elevation-z4"> - <div class="sidenav-cover-header-container bg-50-grey-20"> - <mat-card-title> - <ng-content select="[region-of-interest]"></ng-content> - <div *ngIf="!fetchFlag; else isLoadingTmpl"> - {{ name }} - </div> - </mat-card-title> - - <mat-card-subtitle class="d-inline-flex align-items-center"> - <mat-icon fontSet="fas" fontIcon="fa-database"></mat-icon> - <span> - Dataset - </span> - - <button *ngIf="isGdprProtected" - [matTooltip]="GDPR_TEXT" - mat-icon-button color="warn"> - <i class="fas fa-exclamation-triangle"></i> - </button> - - <mat-divider [vertical]="true" class="ml-2 h-2rem"></mat-divider> - - <!-- explore btn --> - <a *ngFor="let kgRef of kgReference" - [href]="kgRef | doiParserPipe" - class="color-inherit" - mat-icon-button - [matTooltip]="EXPLORE_DATASET_IN_KG_ARIA_LABEL" - target="_blank"> - <i class="fas fa-external-link-alt"></i> - </a> - - <!-- in case no doi is available, directly link to KG --> - <ng-template [ngIf]="kgReference.length === 0"> - <a [href]="directLinkToKg" - class="color-inherit" - mat-icon-button - [matTooltip]="EXPLORE_DATASET_IN_KG_ARIA_LABEL" - target="_blank"> - <i class="fas fa-external-link-alt"></i> - </a> - </ng-template> - - <!-- fav btn --> - <ng-container *ngTemplateOutlet="favDatasetBtn"> - </ng-container> - - </mat-card-subtitle> - </div> - - <mat-card-content class="mt-2 ml-15px-n mr-15px-n pb-4"> - <mat-accordion> - - <!-- Description --> - <mat-expansion-panel> - <mat-expansion-panel-header> - <mat-panel-title> - Description - </mat-panel-title> - </mat-expansion-panel-header> - <ng-template matExpansionPanelContent> - <small *ngIf="!fetchFlag; else isLoadingTmpl" class="m-1"> - - <!-- desc --> - <markdown-dom [markdown]="description"> - </markdown-dom> - - <mat-divider></mat-divider> - - <!-- citations --> - <div class="d-block mb-2 " - [ngClass]="{'mt-2': first}" - *ngFor="let publication of publications; let first = first"> - <a *ngIf="publication.doi; else plainText" - iav-stop="click mousedown" - [href]="publication.doi | doiParserPipe" - target="_blank"> - {{ publication.cite }} - </a> - <ng-template #plainText> - {{ publication.cite }} - </ng-template> - </div> - - <!-- contributors, if publications not available --> - <ng-container *ngIf="publications && publications.length == 0 && contributors && contributors.length > 0"> - <ng-container *ngFor="let contributor of contributors; let lastFlag = last;"> - <a [href]="contributor | getContributorKgLink" class="iv-custom-comp" target="_blank"> - {{ contributor['schema.org/shortName'] || contributor['shortName'] || contributor['name'] }} - </a> - <span *ngIf="!lastFlag">,</span> - </ng-container> - </ng-container> - - </small> - </ng-template> - </mat-expansion-panel> - - <!-- Features --> - <div class="hidden" - [region]="region$ | async" - (loadingStateChanged)="detectChange()" - region-get-all-features-directive - #rfGetAllFeatures="rfGetAllFeatures"> - </div> - - <!-- loading tmpl --> - <ng-template [ngIf]="rfGetAllFeatures.isLoading$ | async" [ngIfElse]="featureTmpl"> - <div class="d-flex justify-content-center"> - <ng-container *ngTemplateOutlet="isLoadingTmpl"></ng-container> - </div> - </ng-template> - - <!-- feature tmpl --> - <ng-template #featureTmpl> - <ng-container *ngFor="let feature of (rfGetAllFeatures.features | filterRegionFeaturesById : fullId)"> - <mat-expansion-panel #matExpansionPanel> - <mat-expansion-panel-header> - <mat-panel-title> - {{ feature.type }} - </mat-panel-title> - </mat-expansion-panel-header> - - <ng-template [ngIf]="matExpansionPanel.expanded"> - <feature-container - [feature]="feature" - [region]="region$ | async" - (viewChanged)="detectChange()"> - </feature-container> - </ng-template> - </mat-expansion-panel> - </ng-container> - </ng-template> - </mat-accordion> - - </mat-card-content> -</mat-card> - -<ng-template #favDatasetBtn> - <ng-container *ngTemplateOutlet="isFavCtxTmpl; context: { - isFav: (favedDataentries$ | async | datasetIsFaved : ({ fullId: fullId })) - }"> - </ng-container> - - <ng-template #isFavCtxTmpl let-isFav="isFav"> - <button mat-icon-button - (click)="isFav ? undoableRemoveFav() : undoableAddFav()" - [attr.aria-label]="PIN_DATASET_ARIA_LABEL" - [matTooltip]="PIN_DATASET_ARIA_LABEL" - [color]="isFav ? 'primary' : 'basic'"> - <i class="fas fa-thumbtack"></i> - </button> - </ng-template> -</ng-template> - -<ng-template #isLoadingTmpl> - <spinner-cmp></spinner-cmp> -</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/databrowserModule/singleDataset/singleDataset.base.spec.ts b/src/atlasComponents/databrowserModule/singleDataset/singleDataset.base.spec.ts index 8389e1c832e42267bb8adf29de8084561fa6f3a5..9636fe82b78960f4396cd1ba7df905e0c5b8633e 100644 --- a/src/atlasComponents/databrowserModule/singleDataset/singleDataset.base.spec.ts +++ b/src/atlasComponents/databrowserModule/singleDataset/singleDataset.base.spec.ts @@ -1,4 +1,3 @@ -import { SingleDatasetView } from './detailedView/singleDataset.component' import { TestBed, async } from '@angular/core/testing'; import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module'; import { ComponentsModule } from 'src/components/components.module'; diff --git a/src/atlasComponents/databrowserModule/util/appendFilterModality.pipe.ts b/src/atlasComponents/databrowserModule/util/appendFilterModality.pipe.ts deleted file mode 100644 index 7894ca1bed066cb3eb9fef91a6c43212d6cee282..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/util/appendFilterModality.pipe.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { CountedDataModality } from "../databrowser.service"; - -@Pipe({ - name: 'appendFilterModalityPipe', -}) - -export class AppendFilerModalityPipe implements PipeTransform { - public transform(root: CountedDataModality[], appending: CountedDataModality[][]): CountedDataModality[] { - let returnArr: CountedDataModality[] = [...root] - for (const mods of appending) { - for (const mod of mods) { - // preserve the visibility - const { visible } = returnArr.find(({ name }) => name === mod.name) || mod - returnArr = returnArr.filter(({ name }) => name !== mod.name) - returnArr = returnArr.concat({ - ...mod, - visible, - }) - } - } - return returnArr - } -} diff --git a/src/atlasComponents/databrowserModule/util/resetCounterModality.pipe.ts b/src/atlasComponents/databrowserModule/util/resetCounterModality.pipe.ts deleted file mode 100644 index 484c3aaaf5357ff9ec3eba57cee7b216d792d3e5..0000000000000000000000000000000000000000 --- a/src/atlasComponents/databrowserModule/util/resetCounterModality.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { CountedDataModality } from "../databrowser.service"; - -@Pipe({ - name: 'resetcounterModalityPipe', -}) - -export class ResetCounterModalityPipe implements PipeTransform { - public transform(inc: CountedDataModality[]): CountedDataModality[] { - return inc.map(({ occurance:_occurance, ...rest }) => { - return { - occurance: 0, - ...rest, - } - }) - } -} diff --git a/src/atlasComponents/parcellationRegion/module.ts b/src/atlasComponents/parcellationRegion/module.ts index 1e400b5e25835f7ae6cee44a939f993b2e1f2253..ea4826233f38bc1510ba27efe42329297dc9ba2a 100644 --- a/src/atlasComponents/parcellationRegion/module.ts +++ b/src/atlasComponents/parcellationRegion/module.ts @@ -9,6 +9,9 @@ import { RegionDirective } from "./region.directive"; import { RegionListSimpleViewComponent } from "./regionListSimpleView/regionListSimpleView.component"; import { RegionMenuComponent } from "./regionMenu/regionMenu.component"; import { SimpleRegionComponent } from "./regionSimple/regionSimple.component"; +import { BSFeatureModule } from "../regionalFeatures/bsFeatures"; +import { RegionAccordionTooltipTextPipe } from "./regionAccordionTooltipText.pipe"; +import { AtlasCmptConnModule } from "../connectivity"; @NgModule({ imports: [ @@ -17,6 +20,8 @@ import { SimpleRegionComponent } from "./regionSimple/regionSimple.component"; DatabrowserModule, AngularMaterialModule, ComponentsModule, + BSFeatureModule, + AtlasCmptConnModule, ], declarations: [ RegionMenuComponent, @@ -25,6 +30,7 @@ import { SimpleRegionComponent } from "./regionSimple/regionSimple.component"; RegionDirective, RenderViewOriginDatasetLabelPipe, + RegionAccordionTooltipTextPipe, ], exports: [ RegionMenuComponent, diff --git a/src/atlasComponents/parcellationRegion/region.base.spec.ts b/src/atlasComponents/parcellationRegion/region.base.spec.ts index 712e323298045aa049c7e8e2961fbb58480f9be2..0fb3792e93d1fead8552c2c4b7f813f3a2582d1f 100644 --- a/src/atlasComponents/parcellationRegion/region.base.spec.ts +++ b/src/atlasComponents/parcellationRegion/region.base.spec.ts @@ -11,7 +11,8 @@ const util = require('common/util') const mr0 = { labelIndex: 1, name: 'mr0', - fullId: { + 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' @@ -34,7 +35,7 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr1wrong = { labelIndex: 1, name: 'mr1', - fullId: { + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'fff-bbb' @@ -45,7 +46,7 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr0wrong = { labelIndex: 1, name: 'mr0', - fullId: { + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'aaa-fff' @@ -56,8 +57,8 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr1lh = { labelIndex: 1, - name: 'mr1 - left hemisphere', - fullId: { + name: 'mr1 left', + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'ccc-bbb' @@ -67,8 +68,8 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr1rh = { labelIndex: 1, - name: 'mr1 - right hemisphere', - fullId: { + name: 'mr1 right', + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'ccc-bbb' @@ -76,10 +77,16 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => } } + const mr0nh = { + labelIndex: 11, + name: 'mr0', + } + const mr0lh = { labelIndex: 1, - name: 'mr0 - left hemisphere', - fullId: { + 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' @@ -89,8 +96,8 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr0rh = { labelIndex: 1, - name: 'mr0 - right hemisphere', - fullId: { + name: 'mr0 right', + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'aaa-bbb' @@ -101,7 +108,7 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr1 = { labelIndex: 1, name: 'mr1', - fullId: { + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'ccc-bbb' @@ -113,16 +120,19 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mp1h = { name: 'mp1h', - regions: [ mr1lh, mr0lh, mr0rh, mr1rh ] + '@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 ] } @@ -130,31 +140,31 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mt0 = { name: 'mt0', - fullId: 'fzj/mock/rs/v0.0.0/aaa-bbb', + '@id': 'fzj/mock/rs/v0.0.0/aaa-bbb', parcellations: [ mp0 ] } const mt1 = { name: 'mt1', - fullId: 'fzj/mock/rs/v0.0.0/bbb-bbb', + '@id': 'fzj/mock/rs/v0.0.0/bbb-bbb', parcellations: [ mp0 ] } const mt2 = { name: 'mt2', - fullId: 'fzj/mock/rs/v0.0.0/ccc-bbb', + '@id': 'fzj/mock/rs/v0.0.0/ccc-bbb', parcellations: [ mp1h ] } const mt3 = { name: 'mt3', - fullId: 'fzj/mock/rs/v0.0.0/ddd-bbb', + '@id': 'fzj/mock/rs/v0.0.0/ddd-bbb', parcellations: [ mp1h ] } const mtWrong = { name: 'mtWrong', - fullId: 'fzj/mock/rs/v0.0.0/ddd-bbb', + '@id': 'fzj/mock/rs/v0.0.0/ddd-bbb', parcellations: [ mpWrong ] } @@ -169,7 +179,8 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => mr0lh, mt3, mr0, - mr0rh + mr0rh, + mr0nh } } @@ -181,7 +192,7 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr1wrong = { labelIndex: 1, name: 'mr1', - fullId: { + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'fff-bbb' @@ -192,7 +203,7 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr0wrong = { labelIndex: 1, name: 'mr0', - fullId: { + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'aaa-fff' @@ -204,8 +215,8 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr1lh = { labelIndex: 1, name: 'mr1', - status: 'left hemisphere', - fullId: { + status: 'left', + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'ccc-bbb' @@ -216,8 +227,8 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr1rh = { labelIndex: 1, name: 'mr1', - status: 'right hemisphere', - fullId: { + status: 'right', + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'ccc-bbb' @@ -225,11 +236,16 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => } } + const mr0nh = { + labelIndex: 11, + name: 'mr0', + } + const mr0lh = { labelIndex: 1, - name: 'mr0', - status: 'left hemisphere', - fullId: { + 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' @@ -239,9 +255,8 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr0rh = { labelIndex: 1, - name: 'mr0', - status: 'right hemisphere', - fullId: { + name: 'mr0 right', + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'aaa-bbb' @@ -252,7 +267,7 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mr1 = { labelIndex: 1, name: 'mr1', - fullId: { + id: { kg: { kgSchema: 'fzj/mock/pr', kgId: 'ccc-bbb' @@ -264,16 +279,19 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mp1h = { name: 'mp1h', - regions: [ mr1lh, mr0lh, mr0rh, mr1rh ] + '@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 ] } @@ -281,31 +299,31 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => const mt0 = { name: 'mt0', - fullId: 'fzj/mock/rs/v0.0.0/aaa-bbb', + '@id': 'fzj/mock/rs/v0.0.0/aaa-bbb', parcellations: [ mp0 ] } const mt1 = { name: 'mt1', - fullId: 'fzj/mock/rs/v0.0.0/bbb-bbb', + '@id': 'fzj/mock/rs/v0.0.0/bbb-bbb', parcellations: [ mp0 ] } const mt2 = { name: 'mt2', - fullId: 'fzj/mock/rs/v0.0.0/ccc-bbb', + '@id': 'fzj/mock/rs/v0.0.0/ccc-bbb', parcellations: [ mp1h ] } const mt3 = { name: 'mt3', - fullId: 'fzj/mock/rs/v0.0.0/ddd-bbb', + '@id': 'fzj/mock/rs/v0.0.0/ddd-bbb', parcellations: [ mp1h ] } const mtWrong = { name: 'mtWrong', - fullId: 'fzj/mock/rs/v0.0.0/ddd-bbb', + '@id': 'fzj/mock/rs/v0.0.0/ddd-bbb', parcellations: [ mpWrong ] } @@ -318,6 +336,7 @@ const getRegionInOtherTemplateSelectorBundle = (version: EnumParcRegVersion) => mt1, mp1h, mr0lh, + mr0nh, mt3, mr0, mr0rh @@ -336,22 +355,22 @@ describe('> region.base.ts', () => { for (const enumKey of Object.keys(EnumParcRegVersion)) { describe(`> selector version for ${enumKey}`, () => { - const { mockFetchedTemplates, mr0, mt2, mt0, mp0, mt1, mp1h, mr0lh, mt3, mr0rh } = getRegionInOtherTemplateSelectorBundle(enumKey as EnumParcRegVersion) + 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, mt0, { region: mr0 }) + result = regionInOtherTemplateSelector.projector(selectedAtlas, mockFetchedTemplates, { region: {...mr0, context: {template: mt0, parcellation: mp0} }}) }) - + it('> length checks out', () => { - expect(result.length).toEqual(5) + expect(result.length).toEqual(4) }) - + it('> does not contain itself', () => { expect(result).not.toContain( jasmine.objectContaining({ @@ -361,7 +380,7 @@ describe('> region.base.ts', () => { }) ) }) - + it('> no hemisphere result has no hemisphere meta data', () => { expect(result).toContain( jasmine.objectContaining({ @@ -371,85 +390,65 @@ describe('> region.base.ts', () => { }) ) }) - + it('> hemisphere result has hemisphere metadata # 1', () => { expect(result).toContain( jasmine.objectContaining({ template: mt2, parcellation: mp1h, region: mr0lh, - hemisphere: 'left hemisphere' + hemisphere: 'left' }) ) }) it('> hemisphere result has hemisphere metadata # 2', () => { expect(result).toContain( jasmine.objectContaining({ - template: mt3, - parcellation: mp1h, - region: mr0lh, - hemisphere: 'left hemisphere' - }) - ) - }) - it('> hemisphere result has hemisphere metadata # 3', () => { - expect(result).toContain( - jasmine.objectContaining({ - template: mt3, - parcellation: mp1h, - region: mr0rh, - hemisphere: 'right hemisphere' - }) - ) - }) - it('> hemisphere result has hemisphere metadata # 4', () => { - expect(result).toContain( - jasmine.objectContaining({ - template: mt3, + template: mt2, parcellation: mp1h, region: mr0rh, - hemisphere: 'right hemisphere' + hemisphere: 'right' }) ) }) }) - - describe('> hemisphere data selected (left hemisphere), simulates julich-brain in mni152', () => { + + describe('> hemisphere data selected (left), simulates julich-brain in mni152', () => { let result beforeAll(() => { - result = regionInOtherTemplateSelector.projector(selectedAtlas, mockFetchedTemplates, mt2, { region: mr0lh }) + 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 hemisphere)', () => { + + it('> does not select wrong hemisphere (right)', () => { expect(result).not.toContain( jasmine.objectContaining({ - template: mt3, + template: mt2, parcellation: mp1h, region: mr0rh, }) ) }) - - it('> select the corresponding hemisphere (left hemisphere), but without hemisphere metadata', () => { + + it('> select the region with correct hemisphere', () => { expect(result).toContain( jasmine.objectContaining({ - template: mt3, + template: mt2, parcellation: mp1h, region: mr0lh }) ) }) }) - + }) } }) - + describe('> RegionBase', () => { let regionBase: RegionBase let mockStore: MockStore @@ -520,7 +519,7 @@ describe('> region.base.ts', () => { expect(regionBase.position).toBeFalsy() }) }) - + it('> populates if position property is array with length 3 and non NaN element', () => { regionBase.region = { ...mr0, @@ -529,7 +528,7 @@ describe('> region.base.ts', () => { expect(regionBase.position).toBeTruthy() }) }) - + describe('> rgb', () => { let strToRgbSpy: jasmine.Spy let mockStore: MockStore @@ -569,7 +568,7 @@ describe('> region.base.ts', () => { describe('> arguments for strToRgb', () => { it('> if ngId is defined, use ngId', () => { - + const regionBase = new RegionBase(mockStore) regionBase.region = { ngId: 'foo', diff --git a/src/atlasComponents/parcellationRegion/region.base.ts b/src/atlasComponents/parcellationRegion/region.base.ts index 33f84531de4c0bcc599b53542d8c5209ccd9f8e7..56321ecad0a54fc773b4030e4d8375a16c5548ee 100644 --- a/src/atlasComponents/parcellationRegion/region.base.ts +++ b/src/atlasComponents/parcellationRegion/region.base.ts @@ -4,7 +4,7 @@ import { uiStateOpenSidePanel, uiStateExpandSidePanel, uiActionShowSidePanelConn import { distinctUntilChanged, switchMap, filter, map, withLatestFrom } from "rxjs/operators"; import { Observable, BehaviorSubject, combineLatest } from "rxjs"; import { ARIA_LABELS } from 'common/constants' -import { flattenRegions, getIdFromFullId, rgbToHsl } from 'common/util' +import { flattenRegions, getIdFromKgIdObj, rgbToHsl } from 'common/util' import { viewerStateSetConnectivityRegion, viewerStateNavigateToRegion, viewerStateToggleRegionSelect, viewerStateNewViewer, isNewerThan } from "src/services/state/viewerState.store.helper"; import { viewerStateFetchedTemplatesSelector, viewerStateGetSelectedAtlas, viewerStateSelectedTemplateFullInfoSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; import { strToRgb, verifyPositionArg, getRegionHemisphere } from 'common/util' @@ -33,14 +33,17 @@ export class RegionBase { set region(val) { this._region = val this.region$.next(this._region) - this.position = val && val.position + + this.position = val?.position + // bug the centroid returned is currently nonsense + // this.position = val?.props?.centroid_mm if (!this._region) return const rgb = this._region.rgb || (this._region.labelIndex > 65500 && [255, 255, 255]) || strToRgb(`${this._region.ngId || this._region.name}${this._region.labelIndex}`) || [255, 200, 200] - + this.rgbString = `rgb(${rgb.join(',')})` const [_h, _s, l] = rgbToHsl(...rgb) this.rgbDarkmode = l < 0.4 @@ -49,7 +52,7 @@ export class RegionBase { get region(){ return this._region } - + private region$: BehaviorSubject<any> = new BehaviorSubject(null) @Input() @@ -215,12 +218,12 @@ export const getRegionParentParcRefSpace = createSelector( */ const checkRegions = regions => { for (const region of regions) { - + /** * check ROI to iterating regions */ if (region.name === regionOfInterest.name) return true - + if (region && region.children && Array.isArray(region.children)) { const flag = checkRegions(region.children) if (flag) return true @@ -239,8 +242,8 @@ export const getRegionParentParcRefSpace = createSelector( parcellation: p } } - } - + } + return { template: null, parcellation: null @@ -264,59 +267,77 @@ export class RenderViewOriginDatasetLabelPipe implements PipeTransform{ export const regionInOtherTemplateSelector = createSelector( viewerStateGetSelectedAtlas, viewerStateFetchedTemplatesSelector, - viewerStateSelectedTemplateSelector, - (atlas, fetchedTemplates, templateSelected, prop) => { - const atlasTemplateSpacesIds = atlas.templateSpaces.map(({ ['@id']: id, fullId }) => id || fullId) + (atlas, fetchedTemplates, prop) => { + const atlasTemplateSpacesIds = atlas.templateSpaces.map(a => a['@id']) const { region: regionOfInterest } = prop const returnArr = [] const regionOfInterestHemisphere = getRegionHemisphere(regionOfInterest) - const regionOfInterestId = getIdFromFullId(regionOfInterest.fullId) - if (!templateSelected) return [] - const selectedTemplateId = getIdFromFullId(templateSelected.fullId) - // need to ensure that the templates are defined in atlas definition - // atlas is the single source of truth - + // atlas is the single source of truth + const otherTemplates = fetchedTemplates - .filter(({ fullId }) => getIdFromFullId(fullId) !== selectedTemplateId) - .filter(({ ['@id']: id, fullId }) => atlasTemplateSpacesIds.includes(id || fullId)) + .filter(({ ['@id']: id }) => id !== regionOfInterest.context.template['@id'] + && atlasTemplateSpacesIds.includes(id) + && regionOfInterest.availableIn.map(ai => ai.id).includes(id)) + for (const template of otherTemplates) { - for (const parcellation of template.parcellations) { - const flattenedRegions = flattenRegions(parcellation.regions) - const selectableRegions = flattenedRegions.filter(({ labelIndex }) => !!labelIndex) - - for (const region of selectableRegions) { - const id = getIdFromFullId(region.fullId) - if (!!id && id === regionOfInterestId) { - const regionHemisphere = getRegionHemisphere(region) - /** + 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 && + if ( + !!regionOfInterestHemisphere && !!regionHemisphere - ) { - if (regionHemisphere === regionOfInterestHemisphere) { - returnArr.push({ - template, - parcellation, - region, - }) - } - } else { + ) { + if (regionHemisphere === regionOfInterestHemisphere) { returnArr.push({ template, parcellation, region, - hemisphere: regionHemisphere }) } + } 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/viewerModule/util/regionAccordionTooltipText.pipe.ts b/src/atlasComponents/parcellationRegion/regionAccordionTooltipText.pipe.ts similarity index 88% rename from src/viewerModule/util/regionAccordionTooltipText.pipe.ts rename to src/atlasComponents/parcellationRegion/regionAccordionTooltipText.pipe.ts index 65c5f21990c6933682b4a511caa8882ea31cf805..c2a92b83de75ee5660c0d59a60e19f561581bf31 100644 --- a/src/viewerModule/util/regionAccordionTooltipText.pipe.ts +++ b/src/atlasComponents/parcellationRegion/regionAccordionTooltipText.pipe.ts @@ -1,9 +1,5 @@ import { Pipe, PipeTransform } from "@angular/core" -/** - * TODO find this pipe a home - * not too sure where this should stay - */ @Pipe({ name: 'regionAccordionTooltipTextPipe', pure: true diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.spec.ts b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.spec.ts index fa2cc46ef39e251dad93d3a96cdcc6fa6c467112..91c25d47bedf7f1ceee968ea8d3f0b24b967f624 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.spec.ts +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.spec.ts @@ -4,10 +4,11 @@ import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.modu import { UtilModule } from "src/util/util.module" import { CommonModule } from "@angular/common" import { provideMockStore } from "@ngrx/store/testing" -import { RenderViewOriginDatasetLabelPipe } from '../region.base' -import { Directive, Input } from "@angular/core" +import { Component, Directive, Input } from "@angular/core" import { NoopAnimationsModule } from "@angular/platform-browser/animations" import { ComponentsModule } from "src/components" +import { ParcellationRegionModule } from "../module" +import { BS_ENDPOINT } from "src/util/constants" const mt0 = { name: 'mt0' @@ -55,42 +56,22 @@ const hemisphereMrms = [ { const nohemisphereHrms = [mrm0, mrm1] -@Directive({ - selector: '[iav-dataset-preview-dataset-file]', - exportAs: 'iavDatasetPreviewDatasetFile' +@Component({ + selector: 'kg-regional-features-list', + template: '' }) -class MockPrvDsFileDirective { - @Input('iav-dataset-preview-dataset-file') - file - - @Input('iav-dataset-preview-dataset-file-filename') - filefilename - - @Input('iav-dataset-preview-dataset-file-dataset') - filedataset - - @Input('iav-dataset-preview-dataset-file-kgid') - filekgid - @Input('iav-dataset-preview-dataset-file-kgschema') - filekgschema - - @Input('iav-dataset-preview-dataset-file-fullid') - filefullid - -} +class DummyKgRegionalFeatureList{} @Directive({ - selector: '[single-dataset-directive]', - exportAs: 'singleDatasetDirective' + selector: '[kg-regional-features-list-directive]', + exportAs: 'kgRegionalFeaturesListDirective' }) class DummySingleDatasetDirective{ @Input() - kgId: string + region: string - @Input() - kgSchema: string } describe('> regionMenu.component.ts', () => { @@ -104,18 +85,21 @@ describe('> regionMenu.component.ts', () => { CommonModule, NoopAnimationsModule, ComponentsModule, + ParcellationRegionModule, ], declarations: [ - RegionMenuComponent, - RenderViewOriginDatasetLabelPipe, /** * Used by regionMenu.template.html to show region preview */ - MockPrvDsFileDirective, DummySingleDatasetDirective, + DummyKgRegionalFeatureList, ], providers: [ - provideMockStore({ initialState: {} }) + provideMockStore({ initialState: {} }), + { + provide: BS_ENDPOINT, + useValue: 'http://example.dev/1_0' + } ] }).compileComponents() diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.ts b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.ts index 8d9f0854522ede479b6ab101d2e535d1504932fa..8747535e5c6129095a01d3d983566852d60bc542 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.ts +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.component.ts @@ -1,30 +1,72 @@ -import { Component, OnDestroy, Input } from "@angular/core"; +import { Component, OnDestroy } from "@angular/core"; import { Store } from "@ngrx/store"; -import { Subscription } from "rxjs"; +import { Observable, Subscription } from "rxjs"; import { RegionBase } from '../region.base' -import { ARIA_LABELS } from 'common/constants' +import { CONST } from 'common/constants' +import { ComponentStore } from "src/viewerModule/componentStore"; @Component({ selector: 'region-menu', templateUrl: './regionMenu.template.html', styleUrls: ['./regionMenu.style.css'], + providers: [ ComponentStore ] }) export class RegionMenuComponent extends RegionBase implements OnDestroy { + public CONST = CONST private subscriptions: Subscription[] = [] + public activePanelTitles$: Observable<string[]> + private activePanelTitles: string[] = [] constructor( store$: Store<any>, + private viewerCmpLocalUiStore: ComponentStore<{ activePanelsTitle: string[] }>, ) { super(store$) + this.viewerCmpLocalUiStore.setState({ + activePanelsTitle: [] + }) + + this.activePanelTitles$ = this.viewerCmpLocalUiStore.select( + state => state.activePanelsTitle + ) as Observable<string[]> + + this.subscriptions.push( + this.activePanelTitles$.subscribe( + (activePanelTitles: string[]) => this.activePanelTitles = activePanelTitles + ) + ) } ngOnDestroy(): void { this.subscriptions.forEach(s => s.unsubscribe()) } - @Input() - showRegionInOtherTmpl: boolean = true + handleExpansionPanelClosedEv(title: string){ + this.viewerCmpLocalUiStore.setState({ + activePanelsTitle: this.activePanelTitles.filter(n => n !== title) + }) + } + handleExpansionPanelAfterExpandEv(title: string){ + if (this.activePanelTitles.includes(title)) return + this.viewerCmpLocalUiStore.setState({ + activePanelsTitle: [ + ...this.activePanelTitles, + title + ] + }) + } - SHOW_IN_OTHER_REF_SPACE = ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE + public busyFlag = false + private busyMap = new Map<string, boolean>() + handleBusySignal(namespace: string, flag: boolean) { + this.busyMap.set(namespace, flag) + for (const [_key, val] of this.busyMap.entries()) { + if (val) { + this.busyFlag = true + return + } + } + this.busyFlag = false + } } diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html index 8f7b7adcbf4304301b7010e8f02d33f88f1777bc..2ce44030ff3a719e2d13fe3986e3a36598d754d9 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html @@ -1,4 +1,6 @@ -<mat-card> +<ng-template [ngIf]="region"> + +<mat-card class="mat-elevation-z4"> <!-- rgbDarkmode must be checked for strict equality to true/false as if rgb is undefined, rgbDarkmode will be null/undefined which is falsy --> @@ -32,55 +34,6 @@ {{ regionOriginDatasetLabels$ | async | renderViewOriginDatasetlabel : index }} </span> </ng-template> - - <span - aria-hidden="true" - [kgSchema]="originDataset.kgSchema" - [kgId]="originDataset.kgId" - single-dataset-directive - #sdDirective="singleDatasetDirective"> - </span> - - <ng-template [ngIf]="sdDirective.fetchFlag" [ngIfElse]="contentTmpl"> - <spinner-cmp></spinner-cmp> - </ng-template> - - <ng-template #contentTmpl> - - <!-- fall back if no kg ref is available --> - <a *ngIf="sdDirective.kgReference.length === 0" - [href]="sdDirective.directLinkToKg" - target="_blank"> - <button mat-icon-button - color="primary"> - <i class="fas fa-external-link-alt"></i> - </button> - </a> - - <!-- kg ref, normally doi --> - <a *ngFor="let kgRef of sdDirective.kgReference" - [href]="kgRef | doiParserPipe" - target="_blank"> - <button mat-icon-button - color="primary"> - <i class="fas fa-external-link-alt"></i> - </button> - </a> - - <!-- pin/unpin --> - <ng-container *ngTemplateOutlet="pinTmpl; context: { $implicit: sdDirective.isFav$ | async }"> - </ng-container> - - <ng-template #pinTmpl let-isFav> - - <button mat-icon-button - (click)="isFav ? sdDirective.undoableRemoveFav() : sdDirective.undoableAddFav()" - [color]="isFav ? 'primary' : 'default'"> - <i class="fas fa-thumbtack"></i> - </button> - </ng-template> - - </ng-template> </div> <mat-divider vertical="true" class="ml-2 h-2rem"></mat-divider> @@ -88,54 +41,207 @@ <!-- position --> <button mat-icon-button *ngIf="position" (click)="navigateToRegion()" - [matTooltip]="GO_TO_REGION_CENTROID + ': ' + (position | nmToMm | addUnitAndJoin : 'mm')"> + [matTooltip]="GO_TO_REGION_CENTROID + ': ' + (position | addUnitAndJoin : 'mm')"> <mat-icon fontSet="fas" fontIcon="fa-map-marked-alt"> </mat-icon> </button> - <!-- region in other templates --> - <button mat-icon-button - *ngIf="showRegionInOtherTmpl" - [attr.data-available-in-tmpl-count]="(regionInOtherTemplates$ | async).length" - [attr.aria-label]="AVAILABILITY_IN_OTHER_REF_SPACE" - [matMenuTriggerFor]="regionInOtherTemplatesMenu" - [matMenuTriggerData]="{ regionInOtherTemplates: regionInOtherTemplates$ | async }"> - <i class="fas fa-globe"></i> - </button> + <!-- explore doi --> + <ng-template let-infos [ngIf]="region?.originDatainfos"> + <ng-container *ngFor="let info of infos"> + <a *ngFor="let url of info.urls" + [href]="url.doi | doiParserPipe" + target="_blank" + mat-icon-button> + <i class="fas fa-external-link-alt"></i> + </a> + </ng-container> + </ng-template> </mat-card-subtitle> </div> </mat-card> -<!-- template for switching template --> -<mat-menu #regionInOtherTemplatesMenu="matMenu" - [aria-label]="SHOW_IN_OTHER_REF_SPACE"> - <ng-template matMenuContent let-regionInOtherTemplates="regionInOtherTemplates"> - - <mat-list-item *ngFor="let sameRegion of regionInOtherTemplates; let i = index" - [attr.aria-label]="SHOW_IN_OTHER_REF_SPACE + ': ' + sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '') " - (click)="changeView(sameRegion)" - mat-ripple - [attr.role]="'button'"> - <mat-icon fontSet="fas" fontIcon="fa-none" mat-list-icon></mat-icon> - <div mat-line> - <ng-container *ngTemplateOutlet="regionInOtherTemplate; context: sameRegion"> - </ng-container> - </div> - </mat-list-item> +<mat-accordion class="d-block mt-2"> + + <!-- description --> + <ng-template [ngIf]="(region.originDatainfos || []).length > 0"> + <ng-container *ngFor="let originData of region.originDatainfos"> + <ng-template #descTmpl> + <markdown-dom [markdown]="originData.description" + class="text-muted text-sm"> + </markdown-dom> + </ng-template> + + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: 'Description', + iconClass: 'fas fa-info', + iavNgIf: true, + content: descTmpl + }"> + + </ng-container> + </ng-container> + </ng-template> + + <!-- receptor --> + <div bs-features-receptor-directive + (bs-features-receptor-directive-fetching-flag$)="handleBusySignal('receptor', $event)" + [region]="region" + #bsFeatureReceptorDirective="bsFeatureReceptorDirective"> + </div> + <ng-template #regionalReceptorTmpl> + <bs-features-receptor-entry [region]="region"> + </bs-features-receptor-entry> </ng-template> -</mat-menu> - -<!-- template for rendering template name and template hemisphere --> -<ng-template #regionInOtherTemplate let-template="template" let-hemisphere="hemisphere"> - <span class="overflow-x-hidden text-truncate" - [matTooltip]="template.name + (hemisphere ? (' ' + hemisphere) : '')"> - <span> - {{ template.name }} - </span> - <span *ngIf="hemisphere" class="text-muted"> - ({{ hemisphere }}) - </span> - </span> + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: 'ReceptorDistribution', + iconClass: 'fas fa-info', + iavNgIf: bsFeatureReceptorDirective.hasReceptor$ | async, + content: regionalReceptorTmpl + }"> + </ng-container> + + + <!-- Explore in other template --> + <ng-container *ngIf="regionInOtherTemplates$ | async as regionInOtherTemplates"> + + <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> + </ng-template> + + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: 'Explore in other templates', + desc: regionInOtherTemplates.length, + iconClass: 'fas fa-brain', + iconTooltip: regionInOtherTemplates.length | regionAccordionTooltipTextPipe : 'regionInOtherTmpl', + iavNgIf: regionInOtherTemplates.length, + content: exploreInOtherTmpl + }"> + + + </ng-container> + </ng-container> + + <!-- kg regional features list --> + <ng-template #kgRegionalFeatureList> + <kg-regional-features-list [region]="region"> + </kg-regional-features-list> + </ng-template> + + <div kg-regional-features-list-directive + [region]="region" + (kg-regional-features-list-directive-busy)="handleBusySignal('regionFeatureList', $event)" + #kgRegFeatlist="kgRegionalFeaturesListDirective"> + </div> + + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: CONST.REGIONAL_FEATURES, + iconClass: 'fas fa-database', + content: kgRegionalFeatureList, + desc: kgRegFeatlist.kgRegionalFeatures.length, + iconTooltip: kgRegFeatlist.kgRegionalFeatures.length | regionAccordionTooltipTextPipe : 'regionalFeatures', + iavNgIf: (kgRegFeatlist.kgRegionalFeatures$ | async ).length + }"> + </ng-container> + + <!-- Connectivity --> + + <ng-template #connectivityContentTmpl let-expansionPanel="expansionPanel"> + <mat-card-content class="flex-grow-1 flex-shrink-1 w-100"> + <connectivity-browser class="pe-all flex-shrink-1" + [region]="region" + (setOpenState)="expansionPanel.expanded = $event" + [accordionExpanded]="expansionPanel.expanded" + (connectivityNumberReceived)="hasConnectivityDirective.connectivityNumber = $event"> + </connectivity-browser> + </mat-card-content> + </ng-template> + + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: 'Connectivity', + desc: hasConnectivityDirective.connectivityNumber, + iconClass: 'fas fa-braille', + iconTooltip: hasConnectivityDirective.connectivityNumber | regionAccordionTooltipTextPipe : 'connectivity', + iavNgIf: hasConnectivityDirective.hasConnectivity, + content: connectivityContentTmpl + }"> + </ng-container> + + <div has-connectivity + [region]="[region]" + #hasConnectivityDirective="hasConnectivityDirective"> + </div> +</mat-accordion> + +<div *ngIf="busyFlag" class="mt-2 d-flex justify-content-center"> + <spinner-cmp></spinner-cmp> +</div> + +<!-- expansion tmpl --> +<ng-template #ngMatAccordionTmpl + let-title="title" + let-desc="desc" + let-iconClass="iconClass" + let-iconTooltip="iconTooltip" + let-iavNgIf="iavNgIf" + let-content="content"> + <mat-expansion-panel + [expanded]="activePanelTitles$ | async | arrayContains : title" + [attr.data-opened]="expansionPanel.expanded" + [attr.data-mat-expansion-title]="title" + (closed)="handleExpansionPanelClosedEv(title)" + (afterExpand)="handleExpansionPanelAfterExpandEv(title)" + hideToggle + *ngIf="iavNgIf" + #expansionPanel="matExpansionPanel"> + + <mat-expansion-panel-header> + + <!-- title --> + <mat-panel-title> + {{ title }} + </mat-panel-title> + + <!-- desc + icon --> + <mat-panel-description class="d-flex align-items-center justify-content-end" + [matTooltip]="iconTooltip"> + <span class="mr-3">{{ desc }}</span> + <span class="accordion-icon d-inline-flex justify-content-center"> + <i [class]="iconClass"></i> + </span> + </mat-panel-description> + + </mat-expansion-panel-header> + + <!-- content --> + <ng-template matExpansionPanelContent> + <ng-container *ngTemplateOutlet="content; context: { + expansionPanel: expansionPanel + }"> + </ng-container> + </ng-template> + </mat-expansion-panel> </ng-template> +</ng-template> + + +<ng-template [ngIf]="!region"> + <mat-card class="mat-elevation-z4"> + <h1 class="mat-h1 sidenav-cover-header-container"> + <spinner-cmp class="d-inline-block"></spinner-cmp> + <span class="text-muted"> + Loading region + </span> + </h1> + </mat-card> +</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/bsRegionInputBase.ts b/src/atlasComponents/regionalFeatures/bsFeatures/bsRegionInputBase.ts index 2060660532e2aa42975b4c277beceea04a68f6c6..9ea4ebbceb006224a914cfef04db02b1337be386 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/bsRegionInputBase.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/bsRegionInputBase.ts @@ -1,6 +1,9 @@ +import { BehaviorSubject, throwError } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; +import { TRegion, IBSSummaryResponse, IBSDetailResponse } from "./type"; +import { BsFeatureService } from "./service"; +import { flattenReducer } from 'common/util' import { Input } from "@angular/core"; -import { BehaviorSubject } from "rxjs"; -import { TRegion } from "./constants"; export class BsRegionInputBase{ @@ -17,6 +20,22 @@ export class BsRegionInputBase{ return this._region } - // eslint-disable-next-line @typescript-eslint/no-empty-function - constructor(){} -} \ No newline at end of file + constructor( + private svc: BsFeatureService + ){} + + protected featuresList$ = this.region$.pipe( + switchMap(region => this.svc.listFeatures(region)), + map(result => result.features.map(obj => Object.keys(obj).reduce(flattenReducer, []))) + ) + + protected getFeatureInstancesList<T extends keyof IBSSummaryResponse>(feature: T){ + if (!this._region) return throwError('#getFeatureInstancesList region needs to be defined') + return this.svc.getFeatures<T>(feature, this._region) + } + + protected getFeatureInstance<T extends keyof IBSDetailResponse>(feature: T, id: string) { + if (!this._region) return throwError('#getFeatureInstance region needs to be defined') + return this.svc.getFeature<T>(feature, this._region, id) + } +} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts b/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts index 4bbc8df0f28b6f595f8e6e2c83c29fe2d2daa5b9..c7060327105a8de788c95a959af676b3c2f871ee 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts @@ -1,26 +1,5 @@ import { InjectionToken } from "@angular/core"; import { Observable } from "rxjs"; -import { IHasId } from "src/util/interfaces"; -import { TBSDetail, TBSSummary } from "./receptor/type"; - -export const BS_ENDPOINT = new InjectionToken<string>('BS_ENDPOINT') - -export type TRegion = { - name: string - status?: string - context: { - atlas: IHasId - template: IHasId - parcellation: IHasId - } -} +export { BS_ENDPOINT } from 'src/util/constants' export const BS_DARKTHEME = new InjectionToken<Observable<boolean>>('BS_DARKTHEME') - -export interface IBSSummaryResponse { - 'ReceptorDistribution': TBSSummary -} - -export interface IBSDetailResponse { - 'ReceptorDistribution': TBSDetail -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/index.ts index c032b1e616b5779164711e13aa8c87f035459805..e571d3c4272b3c27af2343832de0f8d8aabd2399 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/index.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/index.ts @@ -1,4 +1,5 @@ export { BSFeatureModule } from './module' -export { BS_ENDPOINT, TRegion, BS_DARKTHEME } from './constants' +export { BS_ENDPOINT, BS_DARKTHEME } from './constants' +export { TRegion } from './type' // nb do not export BsRegionInputBase from here // will result in cyclic imports \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/getTrailingHex.pipe.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/getTrailingHex.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..5314267e7e1f31b8226ab6eea1ac890277781a68 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/getTrailingHex.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'getTrailingHex', + pure: true +}) + +export class GetTrailingHexPipe implements PipeTransform{ + public transform(input: string) { + const match = /[0-9a-f-]+$/.exec(input) + return match && match[0] + } +} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..337cdf646dd4827cc0f0a9a14f6637d0f4cb129e --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/index.ts @@ -0,0 +1,2 @@ +export { KgDatasetModule } from './module' +export { TCountedDataModality, TBSDetail, TBSSummary } from './type' \ No newline at end of file diff --git a/src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.component.spec.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.spec.ts similarity index 88% rename from src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.component.spec.ts rename to src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.spec.ts index f7313420c694f946111c9de6e6fd43f1a847e42a..9618701aaa39492065095230cc37d7b524d3fbec 100644 --- a/src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.component.spec.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.spec.ts @@ -1,5 +1,5 @@ +import { TCountedDataModality } from "../type" import { SortModalityAlphabeticallyPipe } from "./modalityPicker.component" -import { CountedDataModality } from "../databrowser.service" describe('> modalityPicker.component.ts', () => { describe('> ModalityPicker', () => { @@ -8,7 +8,7 @@ describe('> modalityPicker.component.ts', () => { describe('> SortModalityAlphabeticallyPipe', () => { - const mods: CountedDataModality[] = [{ + const mods: TCountedDataModality[] = [{ name: 'bbb', occurance: 0, visible: false diff --git a/src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.ts similarity index 74% rename from src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.component.ts rename to src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.ts index ff2ffc6bdaeab85b60ebf9288fa9743948a8cfdc..c00a636533018ec616110d4f366d967144254afa 100644 --- a/src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.component.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnChanges, Output, Pipe, PipeTransform } from "@angular/core"; -import { CountedDataModality } from "../databrowser.service"; +import { TCountedDataModality } from "../type"; import { ARIA_LABELS } from 'common/constants' @@ -19,12 +19,12 @@ export class ModalityPicker implements OnChanges { public modalityVisibility: Set<string> = new Set() @Input() - public countedDataM: CountedDataModality[] = [] + public countedDataM: TCountedDataModality[] = [] - public checkedModality: CountedDataModality[] = [] + public checkedModality: TCountedDataModality[] = [] @Output() - public modalityFilterEmitter: EventEmitter<CountedDataModality[]> = new EventEmitter() + public modalityFilterEmitter: EventEmitter<TCountedDataModality[]> = new EventEmitter() // filter(dataentries:DataEntry[]) { // return this.modalityVisibility.size === 0 @@ -40,7 +40,7 @@ export class ModalityPicker implements OnChanges { * TODO * togglemodailty should emit event, and let parent handle state */ - public toggleModality(modality: Partial<CountedDataModality>) { + public toggleModality(modality: Partial<TCountedDataModality>) { this.modalityFilterEmitter.emit( this.countedDataM.map(d => d.name === modality.name ? { @@ -67,7 +67,7 @@ export class ModalityPicker implements OnChanges { } } -const sortByFn = (a: CountedDataModality, b: CountedDataModality) => (a.name || '0').toLowerCase().charCodeAt(0) - (b.name || '0').toLowerCase().charCodeAt(0) +const sortByFn = (a: TCountedDataModality, b: TCountedDataModality) => (a.name || '0').toLowerCase().charCodeAt(0) - (b.name || '0').toLowerCase().charCodeAt(0) @Pipe({ name: 'sortModalityAlphabetically', @@ -75,7 +75,7 @@ const sortByFn = (a: CountedDataModality, b: CountedDataModality) => (a.name || }) export class SortModalityAlphabeticallyPipe implements PipeTransform{ - public transform(arr: CountedDataModality[]): CountedDataModality[]{ + public transform(arr: TCountedDataModality[]): TCountedDataModality[]{ return [...arr].sort(sortByFn) } } diff --git a/src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.style.css similarity index 100% rename from src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.style.css rename to src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.style.css diff --git a/src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.template.html similarity index 92% rename from src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.template.html rename to src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.template.html index e9aa626b7348aca8cb86085723943b01174d57c8..7bde04e8b2cc46f0872d978914bf9c254e79113a 100644 --- a/src/atlasComponents/databrowserModule/modalityPicker/modalityPicker.template.html +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/modalityPicker/modalityPicker.template.html @@ -4,7 +4,7 @@ </legend> <mat-checkbox - [ariaLabel]="datamodality.name" + [checked]="datamodality.visible" (change)="toggleModality(datamodality)" [ngClass]="{'muted': datamodality.occurance === 0}" diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..d49cd9f3ca10dd6a966cd3d520012b9a48573746 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/module.ts @@ -0,0 +1,26 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { ShowDatasetDialogDirective } from "./showDataset/showDataset.directive"; +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { GetTrailingHexPipe } from "./getTrailingHex.pipe"; +import { ModalityPicker, SortModalityAlphabeticallyPipe } from "./modalityPicker/modalityPicker.component"; + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + ], + declarations: [ + ShowDatasetDialogDirective, + GetTrailingHexPipe, + ModalityPicker, + SortModalityAlphabeticallyPipe, + ], + exports: [ + ShowDatasetDialogDirective, + GetTrailingHexPipe, + ModalityPicker, + ] +}) + +export class KgDatasetModule{} diff --git a/src/atlasComponents/databrowserModule/showDatasetDialog.directive.spec.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.spec.ts similarity index 95% rename from src/atlasComponents/databrowserModule/showDatasetDialog.directive.spec.ts rename to src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.spec.ts index 4dbac0cb6856c0bacb1218aef34d2e66a456d666..b2e76a977405a529d82a74b6e66d49a2711ded3c 100644 --- a/src/atlasComponents/databrowserModule/showDatasetDialog.directive.spec.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.spec.ts @@ -1,7 +1,7 @@ import { Component } from "@angular/core"; import { async, TestBed } from "@angular/core/testing"; -import { AngularMaterialModule } from "../../ui/sharedModules/angularMaterial.module"; -import { ShowDatasetDialogDirective, IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "./showDatasetDialog.directive"; +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { ShowDatasetDialogDirective, IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "./showDataset.directive"; import { By } from "@angular/platform-browser"; import { MatDialog } from "@angular/material/dialog"; import { MatSnackBar } from "@angular/material/snack-bar"; @@ -115,6 +115,7 @@ describe('ShowDatasetDialogDirective', () => { expect(args[0]).toEqual(DummyDialogCmp) expect(args[1]).toEqual({ ...ShowDatasetDialogDirective.defaultDialogConfig, + panelClass: ['no-padding-dialog'], data: { fullId: `minds/core/dataset/v1.0.0/aaa-bbb` } @@ -151,6 +152,7 @@ describe('ShowDatasetDialogDirective', () => { expect(args[0]).toEqual(DummyDialogCmp) expect(args[1]).toEqual({ ...ShowDatasetDialogDirective.defaultDialogConfig, + panelClass: ['no-padding-dialog'], data: { fullId: `abc/ccc-ddd` } diff --git a/src/atlasComponents/databrowserModule/showDatasetDialog.directive.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts similarity index 72% rename from src/atlasComponents/databrowserModule/showDatasetDialog.directive.ts rename to src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts index 0cdd393e638bbca5f85ffa094521489ce8cdda5b..e8052369bf5226a29285ab37e7d65093f29d5728 100644 --- a/src/atlasComponents/databrowserModule/showDatasetDialog.directive.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/showDataset/showDataset.directive.ts @@ -1,5 +1,5 @@ -import { Directive, Input, HostListener, Inject, InjectionToken, Optional } from "@angular/core"; -import { MatDialog } from "@angular/material/dialog"; +import { Component, Directive, HostListener, Inject, Input, Optional } from "@angular/core"; +import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; import { MatSnackBar } from "@angular/material/snack-bar"; import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, TOverwriteShowDatasetDialog } from "src/util/interfaces"; @@ -10,10 +10,9 @@ export const IAV_DATASET_SHOW_DATASET_DIALOG_CONFIG = `IAV_DATASET_SHOW_DATASET_ selector: '[iav-dataset-show-dataset-dialog]', exportAs: 'iavDatasetShowDatasetDialog' }) - export class ShowDatasetDialogDirective{ - static defaultDialogConfig = { + static defaultDialogConfig: MatDialogConfig = { autoFocus: false } @@ -32,10 +31,16 @@ export class ShowDatasetDialogDirective{ @Input('iav-dataset-show-dataset-dialog-fullid') fullId: string + @Input('iav-dataset-show-dataset-dialog-urls') + urls: { + cite: string + doi: string + }[] = [] + constructor( private matDialog: MatDialog, private snackbar: MatSnackBar, - @Inject(IAV_DATASET_SHOW_DATASET_DIALOG_CMP) private dialogCmp: any, + @Optional() @Inject(IAV_DATASET_SHOW_DATASET_DIALOG_CMP) private dialogCmp: any, @Optional() @Inject(OVERWRITE_SHOW_DATASET_DIALOG_TOKEN) private overwriteFn: TOverwriteShowDatasetDialog ){ } @@ -48,8 +53,8 @@ export class ShowDatasetDialogDirective{ } } if (this.name || this.description) { - const { name, description } = this - return { name, description } + const { name, description, urls } = this + return { name, description, urls } } })() @@ -61,8 +66,10 @@ export class ShowDatasetDialogDirective{ return this.overwriteFn(data) } + if (!this.dialogCmp) throw new Error(`IAV_DATASET_SHOW_DATASET_DIALOG_CMP not provided!`) this.matDialog.open(this.dialogCmp, { ...ShowDatasetDialogDirective.defaultDialogConfig, + panelClass: ['no-padding-dialog'], data }) } diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..e56921ce5c5e1e9adc110ba666dcf1d2eb192be3 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/type.ts @@ -0,0 +1,82 @@ +export type TCountedDataModality = { + name: string + occurance: number + visible: boolean +} + +export type TBSSummary = { + ['@id']: string + src_name: string +} + +export type TBSDetail = TBSSummary & { + __detail: { + formats: string[] + datasetDOI: { + cite: string + doi: string + }[] + activity: { + protocols: string[] + preparation: string[] + }[] + referenceSpaces: { + name: string + fullId: string + }[] + methods: string[] + custodians: { + "schema.org/shortName": string + identifier: string + name: string + '@id': string + shortName: string + }[] + project: string[] + description: string + parcellationAtlas: { + name: string + fullId: string + id: string[] + }[] + licenseInfo: { + name: string + url: string + }[] + embargoStatus: { + identifier: string[] + name: string + '@id': string + }[] + license: any[] + parcellationRegion: { + species: any[] + name: string + alias: string + fullId: string + }[] + species: string[] + name: string + files: { + byteSize: number + name: string + absolutePath: string + contentType: string + }[] + fullId: string + contributors: { + "schema.org/shortName": string + identifier: string + name: string + '@id': string + shortName: string + }[] + id: string + kgReference: string[] // aka doi + publications: { + name: string + cite: string + doi: string + }[] + } +} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/util.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..d94e309d3d4eefc1aa39b1e63797f12586f0676d --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgDataset/util.ts @@ -0,0 +1,12 @@ +import { TBSDetail, TCountedDataModality } from "./type"; + +export function filterKgFeatureByModailty(modalities: TCountedDataModality[]){ + const visibleModNames = modalities + .filter(mod => mod.visible) + .map(mod => mod.name) + const visibleModNameSet = new Set(visibleModNames) + return function(feature: TBSDetail){ + if (modalities.every(m => !m.visible)) return true + return feature.__detail.methods.some(m => visibleModNameSet.has(m)) + } +} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/index.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..251565d349b8e93ea67179cdb72b54c56a5af05d --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/index.ts @@ -0,0 +1,3 @@ +export { + KgRegionalFeatureModule +} from './module' diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegDetail/kgRegDetail.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegDetail/kgRegDetail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f59ba7947db158e35b5c57dacb34e7bdf248054a --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegDetail/kgRegDetail.component.ts @@ -0,0 +1,86 @@ +import { Component, Inject, Input, OnChanges, Optional } from "@angular/core"; +import { BsRegionInputBase } from "../../bsRegionInputBase"; +import { KG_REGIONAL_FEATURE_KEY, TBSDetail, UNDER_REVIEW } from "../type"; +import { ARIA_LABELS, CONST } from 'common/constants' +import { TBSSummary } from "../../kgDataset"; +import { BsFeatureService } from "../../service"; +import { MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { TDatainfos } from "src/util/siibraApiConstants/types"; + +/** + * this component is specifically used to render side panel ebrains dataset view + */ + +@Component({ + selector: 'kg-regional-feature-detail', + templateUrl: './kgRegDetail.template.html', + styleUrls: [ + './kgRegDetail.style.css' + ] +}) + +export class KgRegDetailCmp extends BsRegionInputBase implements OnChanges { + + public ARIA_LABELS = ARIA_LABELS + public CONST = CONST + + @Input() + public summary: TBSSummary + + @Input() + public detail: TBSDetail + + public loadingFlag = false + public error = null + + public nameFallback = `[This dataset cannot be fetched right now]` + public isGdprProtected = false + + public descriptionFallback = `[This dataset cannot be fetched right now]` + + public description: string + public name: string + public urls: { + cite: string + doi: string + }[] + + constructor( + svc: BsFeatureService, + @Optional() @Inject(MAT_DIALOG_DATA) data: TDatainfos + ){ + super(svc) + if (data) { + const { description, name, urls } = data + this.description = description + this.name = name + this.urls = urls + } + } + + ngOnChanges(){ + if (!this.region) return + if (!this.summary) return + if (!!this.detail) return + this.loadingFlag = true + this.getFeatureInstance(KG_REGIONAL_FEATURE_KEY, this.summary['@id']).subscribe( + detail => { + this.detail = detail + + this.name = this.detail.src_name + this.description = this.detail.__detail?.description + this.urls = this.detail.__detail.kgReference.map(url => { + return { cite: null, doi: url } + }) + + this.isGdprProtected = detail.__detail.embargoStatus && detail.__detail.embargoStatus.some(status => status["@id"] === UNDER_REVIEW["@id"]) + }, + err => { + this.error = err.toString() + }, + () => { + this.loadingFlag = false + } + ) + } +} diff --git a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegDetail/kgRegDetail.style.css similarity index 100% rename from src/atlasComponents/databrowserModule/preview/previewCard/previewCard.style.css rename to src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegDetail/kgRegDetail.style.css diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegDetail/kgRegDetail.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegDetail/kgRegDetail.template.html new file mode 100644 index 0000000000000000000000000000000000000000..bcde6503f0e15b016d4f19b77040394f184f96d8 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegDetail/kgRegDetail.template.html @@ -0,0 +1,49 @@ +<!-- header --> +<mat-card class="mat-elevation-z4"> + + <div class="sidenav-cover-header-container bg-50-grey-20"> + <mat-card-title> + <ng-content select="[region-of-interest]"></ng-content> + <div *ngIf="!loadingFlag; else isLoadingTmpl"> + {{ name || nameFallback }} + </div> + </mat-card-title> + + <mat-card-subtitle class="d-inline-flex align-items-center"> + <mat-icon fontSet="fas" fontIcon="fa-database"></mat-icon> + <span> + ebrains regional dataset + </span> + + <button *ngIf="isGdprProtected" + [matTooltip]="CONST.GDPR_TEXT" + mat-icon-button color="warn"> + <i class="fas fa-exclamation-triangle"></i> + </button> + + <mat-divider [vertical]="true" class="ml-2 h-2rem"></mat-divider> + + <!-- explore btn --> + <a *ngFor="let kgRef of (urls || [])" + [href]="kgRef.doi | doiParserPipe" + class="color-inherit" + mat-icon-button + [matTooltip]="ARIA_LABELS.EXPLORE_DATASET_IN_KG" + target="_blank"> + <i class="fas fa-external-link-alt"></i> + </a> + + </mat-card-subtitle> + </div> + +</mat-card> + +<!-- description --> + +<markdown-dom class="text-muted d-block mat-body m-4" *ngIf="!loadingFlag" + [markdown]="description || descriptionFallback"> +</markdown-dom> + +<ng-template #isLoadingTmpl> + <spinner-cmp></spinner-cmp> +</ng-template> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2623472ab4b4f369358562f9d1540caec88d4ab4 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.component.ts @@ -0,0 +1,97 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, ViewChild } from "@angular/core"; +import { Subscription } from "rxjs"; +import { filter, switchMap, tap } from "rxjs/operators"; +import { TCountedDataModality } from '../../kgDataset' +import { BsRegionInputBase } from "../../bsRegionInputBase"; +import { BsFeatureService } from "../../service"; +import { KG_REGIONAL_FEATURE_KEY, TBSDetail, TBSSummary } from "../type"; +import { ARIA_LABELS } from 'common/constants' +import { filterKgFeatureByModailty } from "../../kgDataset/util"; + +@Component({ + selector: 'kg-regional-features-list', + templateUrl: './kgRegList.template.html', + styleUrls: [ + './kgRegList.style.css' + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class KgRegionalFeaturesList extends BsRegionInputBase implements OnDestroy{ + + public ARIA_LABELS = ARIA_LABELS + + public dataModalities: TCountedDataModality[] = [] + + @Input() + public disableVirtualScroll = false + + public visibleRegionalFeatures: TBSSummary[] = [] + public kgRegionalFeatures: TBSSummary[] = [] + public kgRegionalFeatures$ = this.region$.pipe( + filter(v => !!v), + // must not use switchmapto here + switchMap(() => this.getFeatureInstancesList(KG_REGIONAL_FEATURE_KEY)) + ) + constructor(private cdr: ChangeDetectorRef, svc: BsFeatureService){ + super(svc) + this.sub.push( + this.kgRegionalFeatures$.subscribe(val => { + this.kgRegionalFeatures = val + this.visibleRegionalFeatures = val + }) + ) + } + private sub: Subscription[] = [] + ngOnDestroy(){ + while (this.sub.length) this.sub.pop().unsubscribe() + } + + public trackByFn(_index: number, dataset: TBSSummary) { + return dataset['@id'] + } + + public detailDict: { + [key: string]: TBSDetail + } = {} + + public handlePopulatedDetailEv(detail: TBSDetail){ + this.detailDict = { + ...this.detailDict, + [detail["@id"]]: detail + } + for (const method of detail.__detail.methods) { + const found = this.dataModalities.find(v => v.name === method) + if (found) found.occurance = found.occurance + 1 + else this.dataModalities.push({ + name: method, + occurance: 1, + visible: false + }) + } + this.dataModalities = [...this.dataModalities] + } + + public handleModalityVisbilityChange(modalityFilter: TCountedDataModality[]){ + this.dataModalities = modalityFilter + const visibleCountedDataM = modalityFilter.filter(dm => dm.visible) + + const filterFunc = filterKgFeatureByModailty(visibleCountedDataM) + this.visibleRegionalFeatures = this.kgRegionalFeatures.filter(sum => { + const detail = this.detailDict[sum['@id']] + if (!detail) return false + return filterFunc(detail) + }) + this.cdr.markForCheck() + } + + public clearFilters(){ + const dataModalities = this.dataModalities.map(v => { + return { + ...v, + visible: false + } + }) + this.handleModalityVisbilityChange(dataModalities) + } +} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.style.css b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.style.css new file mode 100644 index 0000000000000000000000000000000000000000..f3a3b5acb3092a376a8030a1334573b5efdc0785 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.style.css @@ -0,0 +1,13 @@ +cdk-virtual-scroll-viewport +{ + min-height: 24rem; +} + +modality-picker +{ + font-size: 90%; +} +.virtual-scroll-element +{ + height:50px; +} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.template.html new file mode 100644 index 0000000000000000000000000000000000000000..1dd9791e0de3cbf35194f9f357cb5c2610dc33a6 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgRegList.template.html @@ -0,0 +1,30 @@ +<ng-container *ngTemplateOutlet="virtualScrollTmpl"> +</ng-container> + +<ng-template #virtualScrollTmpl> + <cdk-virtual-scroll-viewport + [attr.aria-label]="ARIA_LABELS.LIST_OF_DATASETS_ARIA_LABEL" + class="h-100" + minBufferPx="200" + maxBufferPx="400" + itemSize="50"> + <div *cdkVirtualFor="let dataset of visibleRegionalFeatures; trackBy: trackByFn; templateCacheSize: 20; let index = index" + class="virtual-scroll-element overflow-hidden"> + + <!-- divider, show if not first --> + <mat-divider class="mt-1" *ngIf="index !== 0"></mat-divider> + + <kg-regional-feature-summary + mat-ripple + iav-dataset-show-dataset-dialog + [iav-dataset-show-dataset-dialog-fullid]="dataset['@id']" + class="d-block pb-1 pt-1" + [region]="region" + [loadFull]="false" + [summary]="dataset" + (loadedDetail)="handlePopulatedDetailEv($event)"> + </kg-regional-feature-summary> + + </div> + </cdk-virtual-scroll-viewport> +</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgReglist.directive.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgReglist.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..53d4ce42ad73df004454cae121db8f81d6ffa684 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegList/kgReglist.directive.ts @@ -0,0 +1,42 @@ +import { Directive, EventEmitter, OnDestroy, Output } from "@angular/core"; +import { KG_REGIONAL_FEATURE_KEY, TBSSummary } from "../type"; +import { BsFeatureService } from "../../service"; +import { BsRegionInputBase } from "../../bsRegionInputBase"; +import { Subscription } from "rxjs"; +import { filter, startWith, switchMap, tap } from "rxjs/operators"; + +@Directive({ + selector: '[kg-regional-features-list-directive]', + exportAs: 'kgRegionalFeaturesListDirective' +}) + +export class KgRegionalFeaturesListDirective extends BsRegionInputBase implements OnDestroy { + public kgRegionalFeatures: TBSSummary[] = [] + public kgRegionalFeatures$ = this.region$.pipe( + filter(v => !!v), + // must not use switchmapto here + switchMap(() => { + this.busyEmitter.emit(true) + return this.getFeatureInstancesList(KG_REGIONAL_FEATURE_KEY).pipe( + tap(() => this.busyEmitter.emit(false)) + ) + }), + startWith([]) + ) + + constructor(svc: BsFeatureService){ + super(svc) + this.sub.push( + this.kgRegionalFeatures$.subscribe(val => { + this.kgRegionalFeatures = val + }) + ) + } + private sub: Subscription[] = [] + ngOnDestroy(){ + while (this.sub.length) this.sub.pop().unsubscribe() + } + + @Output('kg-regional-features-list-directive-busy') + busyEmitter = new EventEmitter<boolean>() +} \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..841226ad69f75fd821954f624878c4e1a4081de6 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.component.ts @@ -0,0 +1,59 @@ +import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core"; +import { BsRegionInputBase } from "../../bsRegionInputBase"; +import { BsFeatureService } from "../../service"; +import { KG_REGIONAL_FEATURE_KEY, TBSDetail, TBSSummary } from '../type' + +@Component({ + selector: 'kg-regional-feature-summary', + templateUrl: './kgRegSummary.template.html', + styleUrls: [ + './kgRegSummary.style.css' + ], + exportAs: 'kgRegionalFeatureSummary' +}) + +export class KgRegSummaryCmp extends BsRegionInputBase implements OnChanges{ + + @Input() + public loadFull = false + + @Input() + public summary: TBSSummary = null + + public detailLoaded = false + public loadingDetail = false + public detail: TBSDetail = null + @Output() + public loadedDetail = new EventEmitter<TBSDetail>() + + public error: string = null + @Output() + public errorEmitter = new EventEmitter<string>() + + constructor(svc: BsFeatureService){ + super(svc) + } + + ngOnChanges(){ + if (this.loadFull && !!this.summary) { + if (this.loadingDetail || this.detailLoaded) { + return + } + this.loadingDetail = true + this.getFeatureInstance(KG_REGIONAL_FEATURE_KEY, this.summary["@id"]).subscribe( + detail => { + this.detail = detail + this.loadedDetail.emit(detail) + }, + err => { + this.error = err + this.errorEmitter.emit(err) + }, + () => { + this.detailLoaded = true + this.loadingDetail = false + } + ) + } + } +} diff --git a/src/atlasViewer/atlasViewer.constantService.service.spec.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.style.css similarity index 100% rename from src/atlasViewer/atlasViewer.constantService.service.spec.ts rename to src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.style.css diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.template.html b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.template.html new file mode 100644 index 0000000000000000000000000000000000000000..5fcb11b2b8f7a550e66a3491430a0629d62f7533 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/kgRegSummary/kgRegSummary.template.html @@ -0,0 +1,3 @@ +<small> + {{ summary.src_name }} +</small> diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..0698f46c7e07d519434f34642557593aa0e25450 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/module.ts @@ -0,0 +1,59 @@ +import { CommonModule } from "@angular/common"; +import { Component, NgModule } from "@angular/core"; +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { KgRegSummaryCmp } from "./kgRegSummary/kgRegSummary.component"; +import { KgRegionalFeaturesList } from "./kgRegList/kgRegList.component"; +import { KgRegionalFeaturesListDirective } from "./kgRegList/kgReglist.directive"; +import { KgRegDetailCmp } from "./kgRegDetail/kgRegDetail.component"; +import { KgDatasetModule } from "../kgDataset"; +import { IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "../kgDataset/showDataset/showDataset.directive"; +import { UtilModule } from "src/util"; +import { ComponentsModule } from "src/components"; + +@Component({ + selector: 'blabla', + template: ` +<mat-dialog-content class="m-0 p-0"> + <kg-regional-feature-detail></kg-regional-feature-detail> +</mat-dialog-content> + +<mat-dialog-actions align="center"> + <button mat-button mat-dialog-close> + Close + </button> +</mat-dialog-actions> +` +}) + +export class ShowDsDialogCmp{} + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + KgDatasetModule, + UtilModule, + ComponentsModule, + ], + declarations:[ + KgRegSummaryCmp, + KgRegionalFeaturesList, + KgRegionalFeaturesListDirective, + KgRegDetailCmp, + ShowDsDialogCmp, + ], + exports:[ + KgRegSummaryCmp, + KgRegionalFeaturesList, + KgRegionalFeaturesListDirective, + KgRegDetailCmp, + ], + providers: [ + { + provide: IAV_DATASET_SHOW_DATASET_DIALOG_CMP, + useValue: ShowDsDialogCmp + } + ] +}) + +export class KgRegionalFeatureModule{} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..beb275754148db67b87781c8a33afcc8f49fed20 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature/type.ts @@ -0,0 +1,10 @@ +export { + TBSDetail, TBSSummary +} from '../kgDataset' + +export const KG_REGIONAL_FEATURE_KEY = 'EbrainsRegionalDataset' + +export const UNDER_REVIEW = { + ['@id']: "https://nexus.humanbrainproject.org/v0/data/minds/core/embargostatus/v1.0.0/1d726b76-b176-47ed-96f0-b4f2e17d5f19" +} + diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/module.ts index f4f16b8fa2c84a6c607dd8e25506bb9812c9bd47..f646c4f733dcf72fac4097b1b5b7774f7fc53a3a 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/module.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/module.ts @@ -1,6 +1,6 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { BS_ENDPOINT } from "./constants"; +import { KgRegionalFeatureModule } from "./kgRegionalFeature"; import { BSFeatureReceptorModule } from "./receptor"; import { BsFeatureService } from "./service"; @@ -8,16 +8,14 @@ import { BsFeatureService } from "./service"; imports: [ CommonModule, BSFeatureReceptorModule, + KgRegionalFeatureModule, ], providers: [ - { - provide: BS_ENDPOINT, - useValue: BS_REST_URL || `https://brainscapes.apps-dev.hbp.eu` - }, BsFeatureService ], exports: [ - BSFeatureReceptorModule + BSFeatureReceptorModule, + KgRegionalFeatureModule, ] }) diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts index f280d6e09f6675fcd74959f0383e30720f3830da..f739d6af10b216efdec304935383f8c0c175e473 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/entry/entry.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy } from "@angular/core"; import { Observable, of, Subject, Subscription } from "rxjs"; import { filter, map, shareReplay, startWith, switchMap, tap } from "rxjs/operators"; import { BsRegionInputBase } from "../../bsRegionInputBase"; -import { BsFeatureReceptorService } from '../service' +import { BsFeatureService } from "../../service"; import { TBSDetail } from "../type"; @Component({ @@ -29,7 +29,7 @@ export class BsFeatureReceptorEntry extends BsRegionInputBase implements OnDestr public selectedReceptor$: Observable<TBSDetail> = this.selectedREntryId$.pipe( switchMap(id => id - ? this.featureReceptorService.getReceptorRegionalFeatureDetail(this.region, id) + ? this.getFeatureInstance('ReceptorDistribution', id) : of(null) ), shareReplay(1), @@ -41,7 +41,7 @@ export class BsFeatureReceptorEntry extends BsRegionInputBase implements OnDestr public receptorsSummary$ = this.region$.pipe( filter(v => !!v), - switchMap(val => this.featureReceptorService.getReceptorRegionalFeature(val)), + switchMap(() => this.getFeatureInstancesList('ReceptorDistribution')), tap(arr => { if (arr && arr.length > 0) { this.selectedREntryId = arr[0]['@id'] @@ -66,9 +66,11 @@ export class BsFeatureReceptorEntry extends BsRegionInputBase implements OnDestr ) constructor( - private featureReceptorService: BsFeatureReceptorService + svc: BsFeatureService ){ - super() - + super(svc) + this.sub.push( + this.selectedReceptor$.subscribe() + ) } } diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/hasReceptor.directive.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/hasReceptor.directive.ts index c3c0bc1acdf7839dc32432aadcd828d26c73ea11..73ec965c35bccf346916b121c8da1526ab91d5cf 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/hasReceptor.directive.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/hasReceptor.directive.ts @@ -1,8 +1,8 @@ -import { Directive, OnDestroy } from "@angular/core"; +import { Directive, EventEmitter, OnDestroy, Output } from "@angular/core"; import { merge, of, Subscription } from "rxjs"; import { catchError, map, mapTo, switchMap } from "rxjs/operators"; import { BsRegionInputBase } from "../bsRegionInputBase"; -import { BsFeatureReceptorService } from "./service"; +import { BsFeatureService } from "../service"; @Directive({ selector: '[bs-features-receptor-directive]', @@ -20,7 +20,7 @@ export class BsFeatureReceptorDirective extends BsRegionInputBase implements OnD public hasReceptor$ = this.region$.pipe( switchMap(val => merge( of(null), - this.featureReceptorService.getReceptorRegionalFeature(val).pipe( + this.getFeatureInstancesList('ReceptorDistribution').pipe( map(arr => arr.length > 0), catchError(() => of(false)) ) @@ -32,8 +32,14 @@ export class BsFeatureReceptorDirective extends BsRegionInputBase implements OnD ) constructor( - private featureReceptorService: BsFeatureReceptorService + svc: BsFeatureService ){ - super() + super(svc) + this.sub.push( + this.fetching$.subscribe(flag => this.fetchingFlagEmitter.emit(flag)) + ) } + + @Output('bs-features-receptor-directive-fetching-flag') + public fetchingFlagEmitter = new EventEmitter<boolean>() } \ No newline at end of file diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/module.ts index b59e2ecb00361e51475d635cf36b8e57cd3f4b8d..79b0306b85974468a104ace9eb79e6f7254e110f 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/module.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/module.ts @@ -8,7 +8,6 @@ import { BsFeatureReceptorEntry } from "./entry/entry.component"; import { BsFeatureReceptorFingerprint } from "./fp/fp.component"; import { BsFeatureReceptorDirective } from "./hasReceptor.directive"; import { BsFeatureReceptorProfile } from "./profile/profile.component"; -import { BsFeatureReceptorService } from "./service"; @NgModule({ imports: [ @@ -28,9 +27,6 @@ import { BsFeatureReceptorService } from "./service"; BsFeatureReceptorEntry, BsFeatureReceptorDirective, ], - providers: [ - BsFeatureReceptorService, - ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ] diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/service.ts b/src/atlasComponents/regionalFeatures/bsFeatures/receptor/service.ts deleted file mode 100644 index 43384b1e00a23695d3be8fe358a82780b5f61e39..0000000000000000000000000000000000000000 --- a/src/atlasComponents/regionalFeatures/bsFeatures/receptor/service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from "@angular/core"; -import { TRegion } from "../constants"; -import { BsFeatureService } from "../service"; - -@Injectable() -export class BsFeatureReceptorService{ - - public getReceptorRegionalFeature(region: TRegion) { - return this.bsFeatureService.getFeatures('ReceptorDistribution', region) - } - public getReceptorRegionalFeatureDetail(region: TRegion, id: string) { - return this.bsFeatureService.getFeature('ReceptorDistribution', region, id) - } - constructor( - private bsFeatureService: BsFeatureService - ){ - - } -} diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/service.ts b/src/atlasComponents/regionalFeatures/bsFeatures/service.ts index 587e01d69308d87310eb7e2222a9faffc5ca8a1e..8b3e647382fe15c777a948fed5361b0d73f9e225 100644 --- a/src/atlasComponents/regionalFeatures/bsFeatures/service.ts +++ b/src/atlasComponents/regionalFeatures/bsFeatures/service.ts @@ -1,7 +1,13 @@ import { HttpClient } from "@angular/common/http"; import { Inject, Injectable } from "@angular/core"; import { shareReplay } from "rxjs/operators"; -import { BS_ENDPOINT, IBSSummaryResponse, IBSDetailResponse, TRegion } from "./constants"; +import { CachedFunction } from "src/util/fn"; +import { BS_ENDPOINT } from "./constants"; +import { IBSSummaryResponse, IBSDetailResponse, TRegion, IFeatureList } from './type' + +function processRegion(region: TRegion) { + return `${region.name} ${region.status ? region.status : '' }` +} @Injectable() export class BsFeatureService{ @@ -10,23 +16,39 @@ export class BsFeatureService{ shareReplay(1) ) - private processRegion(region: TRegion) { - return `${region.name} ${region.status ? region.status : '' }` + public listFeatures(region: TRegion){ + const { context } = region + const { atlas, parcellation } = context + return this.http.get<IFeatureList>( + `${this.bsEndpoint}/atlases/${encodeURIComponent(atlas["@id"])}/parcellations/${encodeURIComponent(parcellation['@id'])}/regions/${encodeURIComponent(processRegion(region))}/features` + ) } + @CachedFunction({ + serialization: (featureName, region) => `${featureName}::${processRegion(region)}` + }) public getFeatures<T extends keyof IBSSummaryResponse>(featureName: T, region: TRegion){ const { context } = region const { atlas, parcellation } = context + const url = `${this.bsEndpoint}/atlases/${encodeURIComponent(atlas["@id"])}/parcellations/${encodeURIComponent(parcellation['@id'])}/regions/${encodeURIComponent(processRegion(region))}/features/${encodeURIComponent(featureName)}` + return this.http.get<IBSSummaryResponse[T][]>( - `${this.bsEndpoint}/atlases/${encodeURIComponent(atlas["@id"])}/parcellations/${encodeURIComponent(parcellation['@id'])}/regions/${encodeURIComponent(this.processRegion(region))}/features/${encodeURIComponent(featureName)}` + url + ).pipe( + shareReplay(1) ) } + @CachedFunction({ + serialization: (featureName, region, featureId) => `${featureName}::${processRegion(region)}::${featureId}` + }) public getFeature<T extends keyof IBSDetailResponse>(featureName: T, region: TRegion, featureId: string) { const { context } = region const { atlas, parcellation } = context return this.http.get<IBSDetailResponse[T]>( - `${this.bsEndpoint}/atlases/${encodeURIComponent(atlas["@id"])}/parcellations/${encodeURIComponent(parcellation['@id'])}/regions/${encodeURIComponent(this.processRegion(region))}/features/${encodeURIComponent(featureName)}/${encodeURIComponent(featureId)}` + `${this.bsEndpoint}/atlases/${encodeURIComponent(atlas["@id"])}/parcellations/${encodeURIComponent(parcellation['@id'])}/regions/${encodeURIComponent(processRegion(region))}/features/${encodeURIComponent(featureName)}/${encodeURIComponent(featureId)}` + ).pipe( + shareReplay(1) ) } diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/type.ts b/src/atlasComponents/regionalFeatures/bsFeatures/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff20d84d7886f95ffd544e24293250ec811328d1 --- /dev/null +++ b/src/atlasComponents/regionalFeatures/bsFeatures/type.ts @@ -0,0 +1,33 @@ +import { IHasId } from "src/util/interfaces"; +import { TBSDetail as TReceptorDetail, TBSSummary as TReceptorSummary } from "./receptor/type"; +import { KG_REGIONAL_FEATURE_KEY, TBSDetail as TKGDetail, TBSSummary as TKGSummary } from './kgRegionalFeature/type' + +/** + * change KgRegionalFeature -> EbrainsRegionalDataset in prod + */ + +export interface IBSSummaryResponse { + 'ReceptorDistribution': TReceptorSummary + [KG_REGIONAL_FEATURE_KEY]: TKGSummary +} + +export interface IBSDetailResponse { + 'ReceptorDistribution': TReceptorDetail + [KG_REGIONAL_FEATURE_KEY]: TKGDetail +} + +export type TRegion = { + name: string + status?: string + context: { + atlas: IHasId + template: IHasId + parcellation: IHasId + } +} + +export interface IFeatureList { + features: { + [key: string]: string + }[] +} diff --git a/src/atlasComponents/regionalFeatures/module.ts b/src/atlasComponents/regionalFeatures/module.ts index d18d51d9002d2a764c460b1da5a65ffad3ed8e79..26b4faf36a3bbad5e17117927da743afa09ee696 100644 --- a/src/atlasComponents/regionalFeatures/module.ts +++ b/src/atlasComponents/regionalFeatures/module.ts @@ -1,4 +1,5 @@ import { CommonModule } from "@angular/common"; +import { HttpClientModule } from "@angular/common/http"; import { NgModule } from "@angular/core"; import { UtilModule } from "src/util"; import { AngularMaterialModule } from "../../ui/sharedModules/angularMaterial.module"; @@ -18,6 +19,7 @@ import { ReceptorDensityModule } from "./singleFeatures/receptorDensity/module"; AngularMaterialModule, FeatureIEEGRecordings, ReceptorDensityModule, + HttpClientModule, ], declarations: [ /** diff --git a/src/atlasComponents/regionalFeatures/regionalFeature.service.ts b/src/atlasComponents/regionalFeatures/regionalFeature.service.ts index 07507f9ebeb1f8812325d3ec9afdb23e032d48fc..84d5de54b416f58cc741c7a4e77c57b9755ec751 100644 --- a/src/atlasComponents/regionalFeatures/regionalFeature.service.ts +++ b/src/atlasComponents/regionalFeatures/regionalFeature.service.ts @@ -65,8 +65,17 @@ export class RegionalFeaturesService implements OnDestroy{ select(uiStateMouseoverUserLandmark) ) - public getAllFeaturesByRegion(region: {['fullId']: string}){ - if (!region.fullId) throw new Error(`getAllFeaturesByRegion - region does not have fullId defined`) + public getAllFeaturesByRegion(_region: {['fullId']: string} | { id: { kg: {kgSchema: string, kgId: string} } }){ + + const region = { + ..._region, + } + if (!region['fullId']) { + const { kgSchema, kgId } = region['id']?.kg || {} + if (kgSchema && kgId) region['fullId'] = `${kgSchema}/${kgId}` + } + + if (!region['fullId']) throw new Error(`getAllFeaturesByRegion - region does not have fullId defined`) const regionFullIds = getStringIdsFromRegion(region) const hemisphereObj = (() => { const hemisphere = getRegionHemisphere(region) diff --git a/src/atlasComponents/splashScreen/index.ts b/src/atlasComponents/splashScreen/index.ts index 182552e5a83684d6b8b762e7ad6a5debda9e1d0f..5927a7b64a7f19529a4998770dd5f27a3d3a2546 100644 --- a/src/atlasComponents/splashScreen/index.ts +++ b/src/atlasComponents/splashScreen/index.ts @@ -1,2 +1,2 @@ -export { GetTemplateImageSrcPipe, ImgSrcSetPipe, SplashScreen } from "./splashScreen/splashScreen.component"; +export { GetTemplateImageSrcPipe, SplashScreen } from "./splashScreen/splashScreen.component"; export { SplashUiModule } from './module' \ No newline at end of file diff --git a/src/atlasComponents/splashScreen/module.ts b/src/atlasComponents/splashScreen/module.ts index e5817edb31543ba8c3680bc010129436df4774bf..28f361cd4c80a751efa5d0e719fc37073faee18d 100644 --- a/src/atlasComponents/splashScreen/module.ts +++ b/src/atlasComponents/splashScreen/module.ts @@ -4,7 +4,7 @@ import { ComponentsModule } from "src/components"; import { KgTosModule } from "src/ui/kgtos/module"; import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; import { UtilModule } from "src/util"; -import { GetTemplateImageSrcPipe, SplashScreen, ImgSrcSetPipe } from "./splashScreen/splashScreen.component"; +import { GetTemplateImageSrcPipe, SplashScreen } from "./splashScreen/splashScreen.component"; @NgModule({ imports: [ @@ -17,7 +17,6 @@ import { GetTemplateImageSrcPipe, SplashScreen, ImgSrcSetPipe } from "./splashSc declarations: [ SplashScreen, GetTemplateImageSrcPipe, - ImgSrcSetPipe, ], exports: [ SplashScreen, diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts b/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts index d70b2ba321c9ab90d0a50ea28c2190d318f433c6..2435c4e089495a84549374d29f77e1bce8bec238 100644 --- a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts +++ b/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts @@ -1,11 +1,11 @@ -import { AfterViewInit, Component, ElementRef, Pipe, PipeTransform, ViewChild } from "@angular/core"; +import { Component, ElementRef, Pipe, PipeTransform, ViewChild } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; import { select, Store } from "@ngrx/store"; -import { fromEvent, Observable, Subject, Subscription, combineLatest } from "rxjs"; -import { bufferTime, filter, map, switchMap, take, withLatestFrom, shareReplay, startWith } from 'rxjs/operators' -import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; -import { IavRootStoreInterface } from "src/services/stateStore.service"; -import { viewerStateHelperStoreName, viewerStateNewViewer, viewerStateSelectAtlas } from "src/services/state/viewerState.store.helper"; +import { Observable, Subject, Subscription } from "rxjs"; +import { filter } from 'rxjs/operators' +import { viewerStateHelperStoreName, viewerStateSelectAtlas } from "src/services/state/viewerState.store.helper"; import { PureContantService } from "src/util"; +import { CONST } from 'common/constants' @Component({ selector : 'ui-splashscreen', @@ -15,32 +15,26 @@ import { PureContantService } from "src/util"; ], }) -export class SplashScreen implements AfterViewInit { +export class SplashScreen { - public finishedLoading$: Observable<boolean> - public loadedTemplate$: Observable<any[]> + public finishedLoading: boolean = false public loadedAtlases$: Observable<any[]> @ViewChild('parentContainer', {read: ElementRef}) - private parentContainer: ElementRef public activatedTemplate$: Subject<any> = new Subject() private subscriptions: Subscription[] = [] constructor( - private store: Store<IavRootStoreInterface>, - private constanceService: AtlasViewerConstantsServices, + private store: Store<any>, + private snack: MatSnackBar, private pureConstantService: PureContantService ) { - this.loadedTemplate$ = this.store.pipe( - select('viewerState'), - select('fetchedTemplates'), - shareReplay(1), + this.subscriptions.push( + this.pureConstantService.allFetchingReady$.subscribe(flag => this.finishedLoading = flag) ) - this.finishedLoading$ = this.pureConstantService.allFetchingReady$ - this.loadedAtlases$ = this.store.pipe( select(state => state[viewerStateHelperStoreName]), select(state => state.fetchedAtlases), @@ -48,53 +42,17 @@ export class SplashScreen implements AfterViewInit { ) } - public ngAfterViewInit() { - - /** - * instead of blindly listening to click event, this event stream waits to see if user mouseup within 200ms - * if yes, it is interpreted as a click - * if no, user may want to select a text - */ - /** - * TODO change to onclick listener - */ - this.subscriptions.push( - fromEvent(this.parentContainer.nativeElement, 'mousedown').pipe( - filter((ev: MouseEvent) => ev.button === 0), - switchMap(() => fromEvent(this.parentContainer.nativeElement, 'mouseup').pipe( - bufferTime(200), - take(1), - )), - filter(arr => arr.length > 0), - withLatestFrom(this.activatedTemplate$), - map(([_, atlas]) => atlas), - ).subscribe(atlas => this.store.dispatch( - viewerStateSelectAtlas({ atlas }) - )), - ) - } - - public selectTemplateParcellation(template, parcellation) { - this.store.dispatch( - viewerStateNewViewer({ - selectParcellation: parcellation, - selectTemplate: template + public selectAtlas(atlas: any){ + if (!this.finishedLoading) { + this.snack.open(CONST.DATA_NOT_READY, null, { + duration: 3000 }) - ) - } - - public selectTemplate(template: any) { + return + } this.store.dispatch( - viewerStateNewViewer({ - selectTemplate: template, - selectParcellation: template.parcellations[0] - }) + viewerStateSelectAtlas({ atlas }) ) } - - get totalTemplates() { - return this.constanceService.templateUrls.length - } } @Pipe({ @@ -106,17 +64,3 @@ export class GetTemplateImageSrcPipe implements PipeTransform { return `./res/image/${name.replace(/[|&;$%@()+,\s./]/g, '')}.png` } } - -@Pipe({ - name: 'imgSrcSetPipe', -}) - -export class ImgSrcSetPipe implements PipeTransform { - public transform(src: string): string { - const regex = /^(.*?)(\.\w*?)$/.exec(src) - if (!regex) { throw new Error(`cannot find filename, ext ${src}`) } - const filename = regex[1] - const ext = regex[2] - return [100, 200, 300, 400].map(val => `${filename}-${val}${ext} ${val}w`).join(',') - } -} diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html b/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html index 1f8105d2759a3c1f79afed5e0c8235950cf77d02..bb855b2ef8cf1226bc3793b664644e14d902250b 100644 --- a/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html +++ b/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html @@ -5,7 +5,7 @@ #parentContainer class="p-5 w-100 d-flex flex-column flex-wrap justify-content-center align-items-stretch pe-all"> <mat-card - (mousedown)="activatedTemplate$.next(atlas)" + (click)="selectAtlas(atlas)" matRipple *ngFor="let atlas of loadedAtlases$ | async | filterNull" class="m-3 col-md-12 col-lg-12 pe-all"> @@ -21,7 +21,7 @@ </mat-card-footer> </mat-card> - <ng-template [ngIf]="!(finishedLoading$ | async)"> + <ng-template [ngIf]="!finishedLoading"> <div class="d-flex align-items-center p-4"> <h1 class="mat-h1"> <spinner-cmp></spinner-cmp> diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts index c85dff73d22f01af6888cb6a451b40ed46e12af0..dba91ed89252484d6d40c8f14e0baf30241d4f1c 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit, ViewChildren, QueryList, HostBinding, ViewChild, TemplateRef, ElementRef } from "@angular/core"; +import { Component, OnInit, ViewChildren, QueryList, HostBinding, ViewChild, TemplateRef, ElementRef, Pipe, PipeTransform } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { distinctUntilChanged, map, withLatestFrom, shareReplay, groupBy, mergeMap, toArray, switchMap, scan, filter, startWith } from "rxjs/operators"; +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 { viewerStateSelectTemplateWithId, viewerStateToggleLayer } from "src/services/state/viewerState.store.helper"; import { MatMenuTrigger } from "@angular/material/menu"; @@ -33,6 +33,12 @@ import { animate, state, style, transition, trigger } from "@angular/animations" animate('200ms cubic-bezier(0.35, 0, 0.25, 1)') ]) ]) + ], + providers:[ + { + provide: OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, + useValue: null + } ] }) export class AtlasLayerSelector implements OnInit { @@ -122,7 +128,7 @@ export class AtlasLayerSelector implements OnInit { } public availableTemplates$ = this.store$.pipe<any[]>( - select(viewerStateSelectedTemplateFullInfoSelector) + select(viewerStateSelectedTemplateFullInfoSelector), ) public containerMaxWidth: number @@ -250,3 +256,79 @@ export class AtlasLayerSelector implements OnInit { return t['name'] } } + +import "src/res/images/atlas-selection/bigbrain.png" +import 'src/res/images/atlas-selection/icbm2009c.png' +import 'src/res/images/atlas-selection/colin27.png' +import 'src/res/images/atlas-selection/cytoarchitectonic-maps.png' +import 'src/res/images/atlas-selection/cortical-layers.png' +import 'src/res/images/atlas-selection/grey-white-matter.png' +import 'src/res/images/atlas-selection/firbe-long.png' +import 'src/res/images/atlas-selection/firbe-short.png' +import 'src/res/images/atlas-selection/difumo-64.png' +import 'src/res/images/atlas-selection/difumo-128.png' +import 'src/res/images/atlas-selection/difumo-256.png' +import 'src/res/images/atlas-selection/difumo-512.png' +import 'src/res/images/atlas-selection/difumo-1024.png' +import 'src/res/images/atlas-selection/allen-mouse.png' +import 'src/res/images/atlas-selection/allen-mouse-2017.png' +import 'src/res/images/atlas-selection/allen-mouse-2015.png' +import 'src/res/images/atlas-selection/waxholm.png' +import 'src/res/images/atlas-selection/waxholm-v3.png' +import 'src/res/images/atlas-selection/waxholm-v2.png' +import 'src/res/images/atlas-selection/waxholm-v1.png' +import 'src/res/images/atlas-selection/short-bundle-hcp.png' +import 'src/res/images/atlas-selection/freesurfer.png' +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', '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'], +]) + +/** + * used for directories + */ +const previewNameToPngMap = new Map([ + ['fibre architecture', 'firbe-long.png'], + ['functional modes', 'difumo-128.png'] +]) + +@Pipe({ + name: 'getPreviewUrlPipe', + pure: true +}) + +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 && `res/image/${filename}` + } +} \ No newline at end of file diff --git a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html index c6e4db70b3c81bb994a7290d78f486df3287c16b..d4af6f260fb03dc5335ff2dc283c5976b5c13880 100644 --- a/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html +++ b/src/atlasComponents/uiSelectors/atlasLayerSelector/atlasLayerSelector.template.html @@ -119,25 +119,25 @@ <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.previewUrl" - 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> + <!-- 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> </div> @@ -150,29 +150,20 @@ </ng-template> <ng-template #infoBtn let-tileSrc="tileSrc"> - <div *ngIf="tileSrc.originDatasets?.length > 0; else plainBtn" - mat-icon-button - iav-stop="click" - class="iv-custom-comp d-flex justify-content-center infoButton" - [ngStyle]="{backgroundColor: tileSrc.darktheme ? 'white': 'black', color: tileSrc.darktheme ? 'black': 'white' }" - iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-kgid]="tileSrc.originDatasets[0].kgId" - [iav-dataset-show-dataset-dialog-kgschema]="tileSrc.originDatasets[0].kgSchema"> - <small><i class="fas fa-info"></i></small> - </div> + <ng-container *ngFor="let originDatainfo of tileSrc.originDatainfos"> - <ng-template #plainBtn> - <div *ngIf="tileSrc.properties?.name || tileSrc.properties?.description" - mat-icon-button + <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]="tileSrc.properties.name" - [iav-dataset-show-dataset-dialog-description]="tileSrc.properties.description"> + [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-template> + </ng-container> + </ng-template> <!-- mat menu for grouped layer --> diff --git a/src/atlasComponents/uiSelectors/module.ts b/src/atlasComponents/uiSelectors/module.ts index 96ee9a085d82d4d04e0f8bc4c85567a982b6ef0a..2291f425c7c9e64f146e061790f4c17d07317f96 100644 --- a/src/atlasComponents/uiSelectors/module.ts +++ b/src/atlasComponents/uiSelectors/module.ts @@ -4,7 +4,7 @@ import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.modu import { UtilModule } from "src/util"; import { DatabrowserModule } from "src/atlasComponents/databrowserModule"; import { AtlasDropdownSelector } from "./atlasDropdown/atlasDropdown.component"; -import { AtlasLayerSelector } from "./atlasLayerSelector/atlasLayerSelector.component"; +import { AtlasLayerSelector, GetPreviewUrlPipe } from "./atlasLayerSelector/atlasLayerSelector.component"; import {QuickTourModule} from "src/ui/quickTour/module"; @NgModule({ @@ -18,6 +18,7 @@ import {QuickTourModule} from "src/ui/quickTour/module"; declarations: [ AtlasDropdownSelector, AtlasLayerSelector, + GetPreviewUrlPipe, ], exports: [ AtlasDropdownSelector, diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts index 2ffb0a698f1adc4d4d4d133c40a6da36b665fbf2..525f5eaa5dad6a2ca706664963bda383d8bef9ac 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -19,7 +19,8 @@ const README = 'EXAMPLE OF READ ME TEXT' styleUrls: ['./annotationList.style.css'], providers: [ ComponentStore, - ] + ], + exportAs: 'annotationListCmp' }) export class AnnotationList { diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css index 66ea5da7917740147c7d789a0630117a726ec805..c525622e4f3c7081ec4f3a6a65de4fed4c367c45 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css @@ -8,6 +8,6 @@ .tab-toggle { margin: 0.25rem -1rem 0.25rem 0rem; - padding-right: 1rem; - text-align: right; + padding-right: 0; + padding-left: 1.5rem; } \ No newline at end of file diff --git a/src/atlasComponents/userAnnotations/directives/annotationEv.directive.ts b/src/atlasComponents/userAnnotations/directives/annotationEv.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e92f09776e2b5eaecb653061fda92af02bbf981 --- /dev/null +++ b/src/atlasComponents/userAnnotations/directives/annotationEv.directive.ts @@ -0,0 +1,42 @@ +import { Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; +import { Subscription } from "rxjs"; +import { ModularUserAnnotationToolService } from "../tools/service"; +import { TCallback } from "../tools/type"; + +type TAnnotationEv<T extends keyof TCallback> = { + type: T + callArg?: TCallback[T]['callArg'] +} + +@Directive({ + selector: '[annotation-event-directive]', + exportAs: 'annotationEventDir' +}) +export class AnnotationEventDirective implements OnDestroy{ + + @Input('annotation-event-directive-filter') + filter: (keyof TCallback)[] = null + + @Output('annotation-event-directive') + ev = new EventEmitter<TAnnotationEv<keyof TCallback>>() + + private subs: Subscription[] = [] + + constructor(svc: ModularUserAnnotationToolService){ + this.subs.push( + svc.toolEvents.subscribe(<T extends keyof TCallback>(event: { type: T } & TCallback[T]['callArg']) => { + if (this.filter?.includes) { + if (this.filter.includes(event.type)) { + this.ev.emit(event) + } + } else { + this.ev.emit(event) + } + }) + ) + } + + ngOnDestroy(){ + while(this.subs.length > 0)this.subs.pop().unsubscribe() + } +} diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts index efeb89edaa368240e3ed73ee4e82548d44fd5de9..c4ecb05849b239e3528e7c411769ec474dd98555 100644 --- a/src/atlasComponents/userAnnotations/module.ts +++ b/src/atlasComponents/userAnnotations/module.ts @@ -13,6 +13,7 @@ import { AnnotationVisiblePipe } from "./annotationVisible.pipe"; import { FileInputModule } from "src/getFileInput/module"; import { ZipFilesOutputModule } from "src/zipFilesOutput/module"; import { FilterAnnotationsBySpace } from "./filterAnnotationBySpace.pipe"; +import { AnnotationEventDirective } from "./directives/annotationEv.directive"; @NgModule({ imports: [ @@ -35,11 +36,13 @@ import { FilterAnnotationsBySpace } from "./filterAnnotationBySpace.pipe"; SingleAnnotationClsIconPipe, AnnotationVisiblePipe, FilterAnnotationsBySpace, + AnnotationEventDirective, ], exports: [ AnnotationMode, AnnotationList, - AnnotationSwitch + AnnotationSwitch, + AnnotationEventDirective ] }) diff --git a/src/atlasComponents/userAnnotations/tools/delete.ts b/src/atlasComponents/userAnnotations/tools/delete.ts index 63a73c57816ffc9a2e5dce69459bbe4f74bf681b..dc817a65012bd3dbe5409fb8656d5b9d5bcb9990 100644 --- a/src/atlasComponents/userAnnotations/tools/delete.ts +++ b/src/atlasComponents/userAnnotations/tools/delete.ts @@ -33,8 +33,8 @@ export class ToolDelete extends AbsToolClass<Point> implements IAnnotationTools, } init(){ if (this.callback) { - const obs$ = this.callback({ type: 'requestManAnnStreeam' }) - if (!obs$) throw new Error(`Error requestManAnnStreeam`) + const obs$ = this.callback({ type: 'requestManAnnStream' }) + if (!obs$) throw new Error(`Error requestManAnnStream`) const toolDeselect$ = this.toolSelected$.pipe( filter(flag => !flag) diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts index f48247a65abe28e6c92cf334cf1ec85cb4c81b6e..1303612f991d1cadc53a1ce73ddf9f043babca52 100644 --- a/src/atlasComponents/userAnnotations/tools/line.ts +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -270,6 +270,12 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On this.selectedLine = null this.managedAnnotations$.next(this.managedAnnotations) if (this.callback) { + this.callback({ + type: 'message', + message: 'Line added.', + action: 'Open', + actionCallback: () => this.callback({ type: 'showList' }) + }) this.callback({ type: 'paintingEnd' }) } } diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts index 7ea3b5f55ecc64e0ac20e7b7a9cded0502a1ba80..2dcafc2490a82b2dacb3bf5180b7457947de2770 100644 --- a/src/atlasComponents/userAnnotations/tools/point.ts +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -141,11 +141,24 @@ export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools, '@type': 'siibra-ex/annotation/point' }) this.addAnnotation(pt) - - /** - * deselect on selecting a point - */ - this.callback({ type: 'paintingEnd' }) + + if (this.callback) { + + /** + * message + */ + this.callback({ + type: 'message', + message: `Point added`, + action: 'Open', + actionCallback: () => this.callback({ type: 'showList' }) + }) + + /** + * deselect on selecting a point + */ + this.callback({ type: 'paintingEnd' }) + } }), /** diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index c0a65bca5b81b91eb6e918b5c6fd13f7565ce475..c33662c2afc022ae8240884c629946587057b662 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -343,9 +343,18 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo this.selectedPoly.points[0], this.lastAddedPoint ) - this.callback({ - type: 'paintingEnd', - }) + + if (this.callback) { + this.callback({ + type: 'message', + message: 'Polyline added.', + action: 'Open', + actionCallback: () => this.callback({ type: 'showList' }) + }) + this.callback({ + type: 'paintingEnd', + }) + } return } } diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.style.css b/src/atlasComponents/userAnnotations/tools/poly/poly.style.css index 06677c5fea38a957e76e0f0e816aa9bd43bc3328..35465c66c929b4586f68c6121822f345cdb8ea21 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.style.css +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.style.css @@ -3,3 +3,8 @@ point-update-cmp width: 100%; display: block; } + +:host >>> .mat-chip-list-wrapper +{ + flex-wrap: wrap; +} diff --git a/src/atlasComponents/userAnnotations/tools/select.ts b/src/atlasComponents/userAnnotations/tools/select.ts index b5f614c39809580d519aef08e3270a9425536c3d..38a0b11ba39ec9df2ddbb20b875f01cd48575257 100644 --- a/src/atlasComponents/userAnnotations/tools/select.ts +++ b/src/atlasComponents/userAnnotations/tools/select.ts @@ -34,8 +34,8 @@ export class ToolSelect extends AbsToolClass<Point> implements IAnnotationTools, private allManAnnotations: IAnnotationGeometry[] = [] init(){ if (this.callback) { - const obs$ = this.callback({ type: 'requestManAnnStreeam' }) - if (!obs$) throw new Error(`Error requestManAnnStreeam`) + const obs$ = this.callback({ type: 'requestManAnnStream' }) + if (!obs$) throw new Error(`Error requestManAnnStream`) this.subs.push( /** * Get stream of all managed annotations diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index ab789a0d32a9142e8484c69b1285c2dc41726269..fde183e33b5d8fd56059d857f4c5dfb600502350 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -7,13 +7,14 @@ 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"; -import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson, TNgAnnotationLine } from "./type"; +import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson, TNgAnnotationLine, TCallback } from "./type"; import { switchMapWaitFor } from "src/util/fn"; import { Polygon } from "./poly"; import { Line } from "./line"; import { Point } from "./point"; import { FilterAnnotationsBySpace } from "../filterAnnotationBySpace.pipe"; import { retry } from 'common/util' +import { MatSnackBar } from "@angular/material/snack-bar"; const LOCAL_STORAGE_KEY = 'userAnnotationKey' @@ -113,15 +114,30 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }[] = [] private mousePosReal: [number, number, number] + public toolEvents = new Subject() private handleToolCallback: TCallbackFunction = arg => { + this.toolEvents.next(arg) switch (arg.type) { case 'paintingEnd': { this.deselectTools() return } - case 'requestManAnnStreeam': { + case 'requestManAnnStream': { return this.managedAnnotations$ } + case 'message': { + const d = (arg as TCallback['message']['callArg'] & { type: any }) + const { message, actionCallback, action = null } = d + this.snackbar.open(message, action, { + duration: 3000 + }).afterDismissed().subscribe(({ dismissedByAction }) => { + if (dismissedByAction && actionCallback) actionCallback() + }) + return + } + case 'showList': { + return + } } } @@ -209,6 +225,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ constructor( private store: Store<any>, + private snackbar: MatSnackBar, @Inject(INJ_ANNOT_TARGET) annotTarget$: Observable<HTMLElement>, @Inject(ANNOTATION_EVENT_INJ_TOKEN) private annotnEvSubj: Subject<TAnnotationEvent<keyof IAnnotationEvents>>, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable<NehubaViewerUnit>, @@ -453,10 +470,11 @@ export class ModularUserAnnotationToolService implements OnDestroy{ ModularUserAnnotationToolService.ANNOTATION_LAYER_NAME, { ...ModularUserAnnotationToolService.USER_ANNOTATION_LAYER_SPEC, + // since voxel coordinates are no longer defined, so voxel size will always be 1/1/1 transform: [ - [1/voxelSize[0], 0, 0, 0], - [0, 1/voxelSize[1], 0, 0], - [0, 0, 1/voxelSize[2], 0], + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], [0, 0, 0, 1], ] } @@ -548,6 +566,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ arr.push(json) } const stringifiedJSON = JSON.stringify(arr) + if (!(window as any).export_nehuba) return const { pako } = (window as any).export_nehuba const compressed = pako.deflate(stringifiedJSON) let out = '' diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts index ce9f776c15c841ea01cd0b767a0cd93b43c12cb7..1716457a166eea87cc6b9943b4c3ebed3538c03a 100644 --- a/src/atlasComponents/userAnnotations/tools/type.ts +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -161,10 +161,22 @@ export type TCallback = { callArg: {} returns: void } - requestManAnnStreeam: { + requestManAnnStream: { callArg: {} returns: Observable<IAnnotationGeometry[]> } + message: { + callArg: { + message: string + action?: string + actionCallback?: () => void + } + returns: void + } + showList: { + callArg: {} + returns: void + } } export type TCallbackFunction = <T extends keyof TCallback>(arg: TCallback[T]['callArg'] & { type: T }) => TCallback[T]['returns'] | void diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index f827c138a545b47be30975ba91f357a7ea805eb0..2c406866e5bf53b0005f5af699746c56e02db764 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -18,7 +18,7 @@ import { isDefined, safeFilter, } from "../services/stateStore.service"; -import { AtlasViewerConstantsServices, UNSUPPORTED_INTERVAL, UNSUPPORTED_PREVIEW } from "./atlasViewer.constantService.service"; +import { UNSUPPORTED_INTERVAL, UNSUPPORTED_PREVIEW } from "src/util/constants"; import { WidgetServices } from "src/widget"; import { LocalFileService } from "src/services/localFile.service"; @@ -96,7 +96,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { constructor( private store: Store<IavRootStoreInterface>, private widgetServices: WidgetServices, - private constantsService: AtlasViewerConstantsServices, private pureConstantService: PureContantService, private matDialog: MatDialog, private dispatcher$: ActionsSubject, @@ -220,7 +219,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { ) this.subscriptions.push( - this.constantsService.darktheme$.subscribe(flag => { + this.pureConstantService.darktheme$.subscribe(flag => { this.rd.setAttribute(document.body, 'darktheme', flag.toString()) }), ) diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts deleted file mode 100644 index b9059e25b441ca6d1057277fc45db9c8f7f6eac9..0000000000000000000000000000000000000000 --- a/src/atlasViewer/atlasViewer.constantService.service.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { HttpClient, HttpHeaders } from "@angular/common/http"; -import { Injectable, OnDestroy } from "@angular/core"; -import { select, Store } from "@ngrx/store"; -import { Observable, Subscription } from "rxjs"; -import { map, shareReplay } from "rxjs/operators"; -import { SNACKBAR_MESSAGE } from "src/services/state/uiState.store"; -import { IavRootStoreInterface } from "../services/stateStore.service"; -import { PureContantService } from "src/util"; - -@Injectable({ - providedIn : 'root', -}) - -export class AtlasViewerConstantsServices implements OnDestroy { - - public darktheme: boolean = false - public darktheme$: Observable<boolean> - - public citationToastDuration = 7e3 - - /** - * Timeout can be longer, since configs are lazy loaded. - */ - private TIMEOUT = 16000 - - // instead of using window.location.href, which includes query param etc - public backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}` - - public totalTemplates = null - - public getTemplateEndpoint$ = this.http.get(`${this.backendUrl}templates`, { responseType: 'json' }).pipe( - shareReplay(1) - ) - - public templateUrls = Array(100) - - /* to be provided by KG in future */ - private _mapArray: Array<[string, string[]]> = [ - [ 'JuBrain Cytoarchitectonic Atlas' , - [ - 'res/json/pmapsAggregatedData.json', - 'res/json/receptorAggregatedData.json', - ], - ], - [ - 'Fibre Bundle Atlas - Short Bundle', - [ - 'res/json/swmAggregatedData.json', - ], - ], - [ - 'Allen Mouse Common Coordinate Framework v3 2015', - [ - 'res/json/allenAggregated.json', - ], - ], - [ - 'Fibre Bundle Atlas - Long Bundle', - [ - 'res/json/dwmAggregatedData.json', - ], - ], - [ - 'Whole Brain (v2.0)', - [ - 'res/json/waxholmAggregated.json', - ], - ], - ] - - public mapParcellationNameToFetchUrl: Map<string, string[]> = new Map(this._mapArray) - public spatialSearchUrl = 'https://kg-int.humanbrainproject.org/solr/' - public spatialResultsPerPage = 10 - - public chartBaseStyle = { - fill : 'origin', - } - - public chartSdStyle = { - fill : false, - backgroundColor : 'rgba(0,0,0,0)', - borderDash : [10, 3], - pointRadius : 0, - pointHitRadius : 0, - } - - public minReqMD = ` -# Hmm... it seems like we hit a snag -It seems your browser has trouble loading interactive atlas viewer. -Interactive atlas viewer requires **webgl2.0**, and the \`EXT_color_buffer_float\` extension enabled. -- We recommend using _Chrome >= 56_ or _Firefox >= 51_. You can check your browsers' support of webgl2.0 by visiting <https://caniuse.com/#feat=webgl2> -- If you are on _Chrome < 56_ or _Firefox < 51_, you may be able to enable **webgl2.0** by turning on experimental flag <https://get.webgl.org/webgl2/enable.html>. -- If you are on an Android device we recommend _Chrome for Android_ or _Firefox for Android_. -- Unfortunately, Safari and iOS devices currently do not support **webgl2.0**: <https://webkit.org/status/#specification-webgl-2> -` - public minReqModalHeader = `Hmm... it seems your browser and is having trouble loading interactive atlas viewer` - public minReqWebGl2 = `Your browser does not support WebGL2.` - public minReqColorBufferFloat = `Your browser does not support EXT_color_bugger_float extension` - - public mobileWarningHeader = `Power and Network Usage warning` - public mobileWarning = `It looks like you are on a mobile device. Please note that the atlas viewer is power and network usage intensive.` - - /** - * When the selected regions becomes exceedingly many, referer header often gets too hard - * in nginx, it can result in 400 header to large - * as result, trim referer to only template and parcellation selected - */ - private getScopedReferer(): string { - const url = new URL(window.location.href) - url.searchParams.delete('regionsSelected') - return url.toString() - } - - public getHttpHeader(): HttpHeaders { - const header = new HttpHeaders() - header.set('referrer', this.getScopedReferer()) - return header - } - - public getFetchOption(): RequestInit { - return { - referrer: this.getScopedReferer(), - } - } - - /** - * message when user on hover a segment or landmark - */ - public toggleMessage: string = 'double click to toggle select, right click to search' - - /** - * Observable for showing config modal - */ - public showConfigTitle: string = 'Settings' - - public incorrectParcellationNameSearchParam(title) { - return `The selected parcellation - ${title} - is not available. The the first parcellation of the template is selected instead.` - } - - public incorrectTemplateNameSearchParam(title) { - return `The selected template - ${title} - is not available.` - } - - constructor( - private store$: Store<IavRootStoreInterface>, - private http: HttpClient, - private pureConstantService: PureContantService - ) { - - this.darktheme$ = this.store$.pipe( - select('viewerState'), - select('templateSelected'), - map(template => { - if (!template) { return false } - return template.useTheme === 'dark' - }), - shareReplay(1), - ) - - this.subscriptions.push( - this.darktheme$.subscribe(flag => this.darktheme = flag), - ) - this.pureConstantService.getTemplateEndpoint$.subscribe(arr => { - this.totalTemplates = arr.length - }) - } - - private subscriptions: Subscription[] = [] - - public ngOnDestroy() { - while (this.subscriptions.length > 0) { - this.subscriptions.pop().unsubscribe() - } - } - - public catchError(e: Error | string) { - this.store$.dispatch({ - type: SNACKBAR_MESSAGE, - snackbarMessage: e.toString(), - }) - } - -} - -export const UNSUPPORTED_PREVIEW = [{ - text: 'Preview of Colin 27 and JuBrain Cytoarchitectonic', - previewSrc: './res/image/1.png', -}, { - text: 'Preview of Big Brain 2015 Release', - previewSrc: './res/image/2.png', -}, { - text: 'Preview of Waxholm Rat V2.0', - previewSrc: './res/image/3.png', -}] - -export const UNSUPPORTED_INTERVAL = 7000 - diff --git a/src/glue.spec.ts b/src/glue.spec.ts index 79f82db2535f0ab4defa6c9ad669a7ea8beb34e7..0124829a8a439ffa3e38920f98d741a450a98bb8 100644 --- a/src/glue.spec.ts +++ b/src/glue.spec.ts @@ -2,18 +2,13 @@ import { TestBed, tick, fakeAsync, discardPeriodicTasks } from "@angular/core/te import { DatasetPreviewGlue, glueSelectorGetUiStatePreviewingFiles, glueActionRemoveDatasetPreview, datasetPreviewMetaReducer, glueActionAddDatasetPreview, GlueEffects, ClickInterceptorService } from "./glue" import { ACTION_TO_WIDGET_TOKEN, EnumActionToWidget } from "./widget" import { provideMockStore, MockStore } from "@ngrx/store/testing" -import { getRandomHex } from 'common/util' +import { getRandomHex, getIdObj } from 'common/util' import { EnumWidgetTypes, TypeOpenedWidget, uiActionSetPreviewingDatasetFiles, uiStatePreviewingDatasetFilesSelector } from "./services/state/uiState.store.helper" import { hot } from "jasmine-marbles" import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" import { glueActionToggleDatasetPreview } from './glue' -import { getIdObj } from 'common/util' import { DS_PREVIEW_URL } from 'src/util/constants' import { NgLayersService } from "./ui/layerbrowser/ngLayerService.service" -import { EnumColorMapName } from "./util/colorMaps" -import { ngViewerSelectorClearView } from "./services/state/ngViewerState/selectors" -import { tap, ignoreElements } from "rxjs/operators" -import { merge, of } from "rxjs" import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "./atlasComponents/databrowserModule/pure" import { viewerStateSelectedTemplateSelector } from "./services/state/viewerState/selectors" import { generalActionError } from "./services/stateStore.helper" @@ -289,42 +284,6 @@ describe('> glue.ts', () => { discardPeriodicTasks() })) - it('> on previewing nifti, thresholds, colormap and remove bg flag set properly', fakeAsync(() => { - const store = TestBed.inject(MockStore) - const ctrl = TestBed.inject(HttpTestingController) - - const layerService = TestBed.inject(NgLayersService) - - const highThresholdMapSpy = spyOn(layerService.highThresholdMap, 'set').and.callThrough() - const lowThresholdMapSpy = spyOn(layerService.lowThresholdMap, 'set').and.callThrough() - const colorMapMapSpy = spyOn(layerService.colorMapMap, 'set').and.callThrough() - const bgFlagSpy = spyOn(layerService.removeBgMap, 'set').and.callThrough() - - const glue = TestBed.inject(DatasetPreviewGlue) - - store.setState({ - uiState: { - previewingDatasetFiles: [ file1 ] - } - }) - - const { datasetId, filename } = file1 - // debounce at 100ms - tick(200) - - const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent('minds/core/dataset/v1.0.0')}/${datasetId}/${encodeURIComponent(filename)}`) - req.flush(nifti) - - tick(200) - const { name, volumeMetadata } = nifti - const { min, max } = volumeMetadata - expect(highThresholdMapSpy).toHaveBeenCalledWith(name, max) - expect(lowThresholdMapSpy).toHaveBeenCalledWith(name, min) - expect(colorMapMapSpy).toHaveBeenCalledWith(name, EnumColorMapName.VIRIDIS) - expect(bgFlagSpy).toHaveBeenCalledWith(name, true) - discardPeriodicTasks() - })) - it('> if returns 404, should be handled gracefully', fakeAsync(() => { const ctrl = TestBed.inject(HttpTestingController) @@ -510,328 +469,6 @@ describe('> glue.ts', () => { }) - describe('> selectedRegionPreview$', () => { - it('> when one region with origindataset is selected, emits correctly', fakeAsync(() => { - - const store = TestBed.inject(MockStore) - const glue = TestBed.inject(DatasetPreviewGlue) - const ctrl = TestBed.inject(HttpTestingController) - store.overrideSelector(ngViewerSelectorClearView, false) - store.setState({ - ...initialState, - viewerState: { - regionsSelected: [region1] - } - }) - - const { kgSchema, kgId, filename } = region1.originDatasets[0] - const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent(kgSchema)}/${kgId}/${encodeURIComponent(filename)}`) - req.flush(nifti) - tick(200) - expect(glue.selectedRegionPreview$).toBeObservable( - hot('a', { - a: region1.originDatasets - }) - ) - - discardPeriodicTasks() - })) - - it('> when regions are selected without originDatasets, emits empty array', () => { - - const store = TestBed.inject(MockStore) - const glue = TestBed.inject(DatasetPreviewGlue) - store.overrideSelector(ngViewerSelectorClearView, false) - store.setState({ - ...initialState, - viewerState: { - regionsSelected: [{ - ...region0, - originDatasets: [] - }, { - ...region1, - originDatasets: [] - }] - } - }) - - expect(glue.selectedRegionPreview$).toBeObservable( - hot('a', { - a: [] - }) - ) - }) - - it('> if multiple region, each with origin datasets are selected, emit array', fakeAsync(() => { - - const store = TestBed.inject(MockStore) - const glue = TestBed.inject(DatasetPreviewGlue) - const ctrl = TestBed.inject(HttpTestingController) - store.overrideSelector(ngViewerSelectorClearView, false) - store.setState({ - ...initialState, - viewerState: { - regionsSelected: [region0, region1] - } - }) - - const expectedOriginDatasets = [ - ...region0.originDatasets, - ...region1.originDatasets, - ] - - for (const { kgSchema, kgId, filename } of expectedOriginDatasets) { - const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent(kgSchema)}/${kgId}/${encodeURIComponent(filename)}`) - req.flush(nifti) - } - tick(200) - expect(glue.selectedRegionPreview$).toBeObservable( - hot('a', { - a: expectedOriginDatasets - }) - ) - - discardPeriodicTasks() - })) - - it('> if regions with multiple originDatasets are selected, emit array containing all origindatasets', fakeAsync(() => { - - const store = TestBed.inject(MockStore) - const glue = TestBed.inject(DatasetPreviewGlue) - const ctrl = TestBed.inject(HttpTestingController) - store.overrideSelector(ngViewerSelectorClearView, false) - const originDatasets0 = [ - ...region0.originDatasets, - { - kgId: getRandomHex(), - kgSchema: 'minds/core/dataset/v1.0.0', - filename: getRandomHex() - } - ] - const origindataset1 = [ - ...region1.originDatasets, - { - kgSchema: 'minds/core/dataset/v1.0.0', - kgId: getRandomHex(), - filename: getRandomHex() - } - ] - store.setState({ - ...initialState, - viewerState: { - regionsSelected: [{ - ...region0, - originDatasets: originDatasets0 - }, { - ...region1, - originDatasets: origindataset1 - }] - } - }) - - const expectedOriginDatasets = [ - ...originDatasets0, - ...origindataset1, - ] - - for (const { kgSchema, kgId, filename } of expectedOriginDatasets) { - const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent(kgSchema)}/${kgId}/${encodeURIComponent(filename)}`) - req.flush(nifti) - } - tick(200) - expect(glue.selectedRegionPreview$).toBeObservable( - hot('a', { - a: expectedOriginDatasets - }) - ) - discardPeriodicTasks() - })) - }) - - describe('> onRegionSelectChangeShowPreview$', () => { - it('> calls getDatasetPreviewFromId for each of the selectedRegion', fakeAsync(() => { - - /** - * Testing Store observable - * https://stackoverflow.com/a/61871144/6059235 - */ - const store = TestBed.inject(MockStore) - const glue = TestBed.inject(DatasetPreviewGlue) - const ctrl = TestBed.inject(HttpTestingController) - store.overrideSelector(ngViewerSelectorClearView, false) - - const getDatasetPreviewFromIdSpy = spyOn(glue, 'getDatasetPreviewFromId').and.callThrough() - store.setState({ - ...initialState, - viewerState: { - regionsSelected: [region1] - } - }) - - const { kgSchema, kgId, filename } = region1.originDatasets[0] - const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent(kgSchema)}/${kgId}/${encodeURIComponent(filename)}`) - req.flush(nifti) - tick(200) - - for (const { kgId, kgSchema, filename } of region1.originDatasets) { - expect(getDatasetPreviewFromIdSpy).toHaveBeenCalledWith({ - datasetId: kgId, - datasetSchema: kgSchema, - filename - }) - } - - expect(glue.onRegionSelectChangeShowPreview$).toBeObservable( - hot('a', { - a: [ { - ...nifti, - filename: region1.originDatasets[0].filename, - datasetId: region1.originDatasets[0].kgId, - datasetSchema: kgSchema, - } ] - }) - ) - - discardPeriodicTasks() - })) - }) - - describe('> onRegionDeselectRemovePreview$', () => { - it('> on region selected [ region ] > [], emits', fakeAsync(() => { - - const store = TestBed.inject(MockStore) - store.overrideSelector(ngViewerSelectorClearView, false) - const glue = TestBed.inject(DatasetPreviewGlue) - - const regionsSelected$ = hot('bab', { - a: [region1], - b: [] - }) - - const spy = spyOn(glue, 'getDatasetPreviewFromId') - spy.and.returnValue(of({ - ...nifti, - filename: region1.originDatasets[0].filename, - datasetId: region1.originDatasets[0].kgId, - })) - - const src$ = merge( - regionsSelected$.pipe( - tap(regionsSelected => store.setState({ - ...initialState, - viewerState: { - regionsSelected - } - })), - ignoreElements() - ), - glue.onRegionDeselectRemovePreview$ - ) - - src$.subscribe() - - expect(glue.onRegionDeselectRemovePreview$).toBeObservable( - hot('bba', { - a: [{ - ...nifti, - filename: region1.originDatasets[0].filename, - datasetId: region1.originDatasets[0].kgId, - }], - b: [] - }) - ) - - tick(200) - discardPeriodicTasks() - })) - }) - - describe('> onClearviewRemovePreview$', () => { - it('> on regions selected [ region ] > clear view selector returns true, emits ', fakeAsync(() => { - const store = TestBed.inject(MockStore) - store.overrideSelector(ngViewerSelectorClearView, true) - - const glue = TestBed.inject(DatasetPreviewGlue) - - const spy = spyOn(glue, 'getDatasetPreviewFromId') - spy.and.returnValue(of({ - ...nifti, - filename: region1.originDatasets[0].filename, - datasetId: region1.originDatasets[0].kgId, - })) - - store.setState({ - ...initialState, - viewerState: { - regionsSelected: [region1] - } - }) - - expect(glue.onClearviewRemovePreview$).toBeObservable( - hot('a', { - a: [{ - ...nifti, - filename: region1.originDatasets[0].filename, - datasetId: region1.originDatasets[0].kgId, - }], - b: [] - }) - ) - - tick(200) - discardPeriodicTasks() - })) - }) - - describe('> onClearviewAddPreview$', () => { - it('> on region selected [ region ] > clear view selector returns false, emits', fakeAsync(() => { - const store = TestBed.inject(MockStore) - const overridenSelector = store.overrideSelector(ngViewerSelectorClearView, true) - - /** - * skips first false - */ - const overridenSelector$ = hot('bab', { - a: true, - b: false - }) - - const glue = TestBed.inject(DatasetPreviewGlue) - - const spy = spyOn(glue, 'getDatasetPreviewFromId') - spy.and.returnValue(of({ - ...nifti, - filename: region1.originDatasets[0].filename, - datasetId: region1.originDatasets[0].kgId, - })) - - store.setState({ - ...initialState, - viewerState: { - regionsSelected: [region1] - } - }) - - overridenSelector$.subscribe(flag => { - overridenSelector.setResult(flag) - store.refreshState() - }) - - expect(glue.onClearviewAddPreview$).toBeObservable( - hot('--a', { - a: [{ - ...nifti, - filename: region1.originDatasets[0].filename, - datasetId: region1.originDatasets[0].kgId, - }], - b: [] - }) - ) - - tick(200) - discardPeriodicTasks() - })) - }) }) diff --git a/src/glue.ts b/src/glue.ts index a2c1652af870a4b992b9392b131da237447a15ef..ea07500c18b3839ba05e9d637c1387c48a370d03 100644 --- a/src/glue.ts +++ b/src/glue.ts @@ -1,21 +1,19 @@ -import { uiActionSetPreviewingDatasetFiles, IDatasetPreviewData, uiStateShowBottomSheet, uiStatePreviewingDatasetFilesSelector } from "./services/state/uiState.store.helper" +import { uiActionSetPreviewingDatasetFiles, IDatasetPreviewData, uiStatePreviewingDatasetFilesSelector } from "./services/state/uiState.store.helper" import { OnDestroy, Injectable, Optional, Inject, InjectionToken } from "@angular/core" import { PreviewComponentWrapper, DatasetPreview, determinePreviewFileType, EnumPreviewFileTypes, IKgDataEntry, getKgSchemaIdFromFullId, GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "./atlasComponents/databrowserModule/pure" import { Subscription, Observable, forkJoin, of, merge, combineLatest } from "rxjs" import { select, Store, ActionReducer, createAction, props, createSelector, Action } from "@ngrx/store" -import { startWith, map, shareReplay, pairwise, debounceTime, distinctUntilChanged, tap, switchMap, withLatestFrom, mapTo, switchMapTo, filter, skip, catchError, bufferTime } from "rxjs/operators" +import { startWith, map, shareReplay, pairwise, debounceTime, distinctUntilChanged, tap, switchMap, withLatestFrom, mapTo, switchMapTo, filter, skip, catchError } from "rxjs/operators" import { TypeActionToWidget, EnumActionToWidget, ACTION_TO_WIDGET_TOKEN } from "./widget" import { getIdObj } from 'common/util' import { MatDialogRef } from "@angular/material/dialog" import { HttpClient } from "@angular/common/http" -import { DS_PREVIEW_URL, getShader, PMAP_DEFAULT_CONFIG } from 'src/util/constants' -import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, INgLayerInterface } from "./services/state/ngViewerState.store.helper" +import { DS_PREVIEW_URL } from 'src/util/constants' +import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from "./services/state/ngViewerState.store.helper" import { ARIA_LABELS } from 'common/constants' import { NgLayersService } from "src/ui/layerbrowser/ngLayerService.service" -import { EnumColorMapName } from "./util/colorMaps" import { Effect } from "@ngrx/effects" import { viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector, viewerStateSelectedParcellationSelector } from "./services/state/viewerState/selectors" -import { ngViewerSelectorClearView } from "./services/state/ngViewerState/selectors" import { ngViewerActionClearView } from './services/state/ngViewerState/actions' import { generalActionError } from "./services/stateStore.helper" import { RegDeregController } from "./util/regDereg.base" @@ -25,8 +23,6 @@ const PREVIEW_FILE_TYPES_NO_UI = [ EnumPreviewFileTypes.VOLUMES ] -const DATASET_PREVIEW_ANNOTATION = `DATASET_PREVIEW_ANNOTATION` - const prvFilterNull = ({ prvToDismiss, prvToShow }) => ({ prvToDismiss: prvToDismiss.filter(v => !!v), prvToShow: prvToShow.filter(v => !!v), @@ -296,57 +292,6 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ ) } - public selectedRegionPreview$ = this.store$.pipe( - select(state => state?.viewerState?.regionsSelected), - filter(regions => !!regions), - map(regions => /** effectively flatMap */ regions.reduce((acc, curr) => acc.concat( - curr.originDatasets && Array.isArray(curr.originDatasets) && curr.originDatasets.length > 0 - ? curr.originDatasets - : [] - ), [])), - ) - - public onRegionSelectChangeShowPreview$ = this.selectedRegionPreview$.pipe( - switchMap(arr => arr.length > 0 - ? forkJoin(arr.map(({ kgId, kgSchema, filename }) => this.getDatasetPreviewFromId({ datasetId: kgId, datasetSchema: kgSchema, filename }))) - : of([]) - ), - map(arr => arr.filter(item => !!item)), - shareReplay(1), - ) - - public onRegionDeselectRemovePreview$ = this.onRegionSelectChangeShowPreview$.pipe( - pairwise(), - map(([oArr, nArr]) => oArr.filter((item: any) => { - return !nArr - .map(DatasetPreviewGlue.GetDatasetPreviewId) - .includes( - DatasetPreviewGlue.GetDatasetPreviewId(item) - ) - })), - ) - - public onClearviewRemovePreview$ = this.onRegionSelectChangeShowPreview$.pipe( - filter(arr => arr.length > 0), - switchMap(arr => this.store$.pipe( - select(ngViewerSelectorClearView), - distinctUntilChanged(), - filter(val => val), - mapTo(arr) - )), - ) - - public onClearviewAddPreview$ = this.onRegionSelectChangeShowPreview$.pipe( - filter(arr => arr.length > 0), - switchMap(arr => this.store$.pipe( - select(ngViewerSelectorClearView), - distinctUntilChanged(), - filter(val => !val), - skip(1), - mapTo(arr) - )) - ) - private fetchedDatasetPreviewCache: Map<string, Observable<any>> = new Map() public getDatasetPreviewFromId({ datasetSchema = 'minds/core/dataset/v1.0.0', datasetId, filename }: IDatasetPreviewData){ const dsPrvId = DatasetPreviewGlue.GetDatasetPreviewId({ datasetSchema, datasetId, filename }) @@ -423,154 +368,6 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ } }) ) - - // managing niftiVolumes - // monitors previewDatasetFile obs to add/remove ng layer - - this.subscriptions.push( - merge( - this.getDiffDatasetFilesPreviews( - dsPrv => determinePreviewFileType(dsPrv) === EnumPreviewFileTypes.NIFTI - ), - this.onRegionSelectChangeShowPreview$.pipe( - map(prvToShow => ({ prvToShow, prvToDismiss: [] })) - ), - this.onRegionDeselectRemovePreview$.pipe( - map(prvToDismiss => ({ prvToShow: [], prvToDismiss })) - ), - this.onClearviewRemovePreview$.pipe( - map(prvToDismiss => ({ prvToDismiss, prvToShow: [] })) - ), - this.onClearviewAddPreview$.pipe( - map(prvToShow => ({ prvToDismiss: [], prvToShow })) - ) - ).pipe( - map(prvFilterNull), - bufferTime(15), - map(arr => { - const prvToDismiss = [] - const prvToShow = [] - - const showPrvIds = new Set() - const dismissPrvIds = new Set() - - for (const { prvToDismiss: dismisses, prvToShow: shows } of arr) { - for (const dismiss of dismisses) { - - const id = DatasetPreviewGlue.GetDatasetPreviewId(dismiss) - if (!dismissPrvIds.has(id)) { - dismissPrvIds.add(id) - prvToDismiss.push(dismiss) - } - } - - for (const show of shows) { - const id = DatasetPreviewGlue.GetDatasetPreviewId(show) - if (!dismissPrvIds.has(id) && !showPrvIds.has(id)) { - showPrvIds.add(id) - prvToShow.push(show) - } - } - } - - return { - prvToDismiss, - prvToShow - } - }), - withLatestFrom(this.store$.pipe( - select(state => state?.viewerState?.templateSelected || null), - distinctUntilChanged(), - )) - ).subscribe(([ { prvToShow, prvToDismiss }, templateSelected ]) => { - // TODO consider where to check validity of previewed nifti file - for (const prv of prvToShow) { - - const { url, filename, name, volumeMetadata = {} } = prv - const { min, max, colormap = EnumColorMapName.VIRIDIS } = volumeMetadata || {} - - const previewFileId = DatasetPreviewGlue.GetDatasetPreviewId(prv) - - const shaderObj = { - ...PMAP_DEFAULT_CONFIG, - ...{ colormap }, - ...( typeof min !== 'undefined' ? { lowThreshold: min } : {} ), - ...( max ? { highThreshold: max } : { highThreshold: 1 } ) - } - - const layer = { - // name: filename, - name: name || filename, - id: previewFileId, - source : `nifti://${url}`, - mixability : 'nonmixable', - shader : getShader(shaderObj), - annotation: `${DATASET_PREVIEW_ANNOTATION} ${filename}` - } - - const { name: layerName } = layer - const { colormap: cmap, lowThreshold, highThreshold, removeBg } = shaderObj - - this.layersService.highThresholdMap.set(layerName, highThreshold) - this.layersService.lowThresholdMap.set(layerName, lowThreshold) - this.layersService.colorMapMap.set(layerName, cmap) - this.layersService.removeBgMap.set(layerName, removeBg) - - this.store$.dispatch( - ngViewerActionAddNgLayer({ layer }) - ) - } - - for (const prv of prvToDismiss) { - const { url, filename, name } = prv - const previewFileId = DatasetPreviewGlue.GetDatasetPreviewId(prv) - const layer = { - name: name || filename, - id: previewFileId, - source : `nifti://${url}`, - mixability : 'nonmixable', - shader : getShader(PMAP_DEFAULT_CONFIG), - annotation: `${DATASET_PREVIEW_ANNOTATION} ${filename}` - } - this.store$.dispatch( - ngViewerActionRemoveNgLayer({ layer }) - ) - } - - if (prvToShow.length > 0) this.store$.dispatch(uiStateShowBottomSheet({ bottomSheetTemplate: null })) - }) - ) - - // monitors ngViewerStateLayers, and if user removes, also remove dataset preview, if exists - this.subscriptions.push( - this.store$.pipe( - select(state => state?.ngViewerState?.layers || []), - distinctUntilChanged(), - pairwise(), - map(([o, n]: [INgLayerInterface[], INgLayerInterface[]]) => { - const nNameSet = new Set(n.map(({ name }) => name)) - const oNameSet = new Set(o.map(({ name }) => name)) - return { - add: n.filter(({ name: nName }) => !oNameSet.has(nName)), - remove: o.filter(({ name: oName }) => !nNameSet.has(oName)), - } - }), - map(({ remove }) => remove), - ).subscribe(layers => { - for (const layer of layers) { - const { id } = layer - if (!id) return console.warn(`monitoring ngViewerStateLayers id is undefined`) - try { - const { datasetId, filename } = DatasetPreviewGlue.GetDatasetPreviewFromId(layer.id) - this.store$.dispatch( - glueActionRemoveDatasetPreview({ datasetPreviewFile: { filename, datasetId } }) - ) - } catch (e) { - console.warn(`monitoring ngViewerStateLayers parsing id or dispatching action failed`, e) - } - } - }) - ) } private closeDatasetPreviewWidget(data: IDatasetPreviewData){ diff --git a/src/index.html b/src/index.html index 39aafb5fb94360cdc71dba34b69214dfa8e76fdb..f7c2ab2955adf509da76f7df12a95dc993158231 100644 --- a/src/index.html +++ b/src/index.html @@ -15,7 +15,7 @@ <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.8/dist/bundle.js" defer></script> + <script src="https://unpkg.com/three-surfer@0.0.10/dist/bundle.js" defer></script> <title>Interactive Atlas Viewer</title> <script type="application/ld+json"> diff --git a/src/main.module.ts b/src/main.module.ts index d2b33215ad7635ecc47e3daf66646dd72959dd36..cc25b011578ad18e3a9fb1e0d6f8fd21adc94742 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -32,7 +32,7 @@ import { DockedContainerDirective } from "./util/directives/dockedContainer.dire import { FloatingContainerDirective } from "./util/directives/floatingContainer.directive"; import { FloatingMouseContextualContainerDirective } from "./util/directives/floatingMouseContextualContainer.directive"; import { NewViewerDisctinctViewToLayer } from "./util/pipes/newViewerDistinctViewToLayer.pipe"; -import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, CONTEXT_MENU_ITEM_INJECTOR, PureContantService, UtilModule } from "src/util"; +import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, PureContantService, UtilModule } from "src/util"; import { SpotLightModule } from 'src/spotlight/spot-light.module' import { TryMeComponent } from "./ui/tryme/tryme.component"; import { UiStateUseEffect } from "src/services/state/uiState.store"; @@ -61,6 +61,7 @@ import { KgTosModule } from './ui/kgtos/module'; import { MouseoverModule } from './mouseoverModule/mouseover.module'; import { AtlasViewerRouterModule } from './routerModule'; import { MessagingGlue } from './messagingGlue'; +import { BS_ENDPOINT } from './util/constants'; import { QuickTourModule } from './ui/quickTour'; export function debug(reducer: ActionReducer<any>): ActionReducer<any> { @@ -262,7 +263,11 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { { provide: WINDOW_MESSAGING_HANDLER_TOKEN, useClass: MessagingGlue - } + }, + { + provide: BS_ENDPOINT, + useValue: (BS_REST_URL || `https://siibra-api-latest.apps-dev.hbp.eu/v1_0`).replace(/\/$/, '') + }, ], bootstrap : [ AtlasViewer, diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index d55624cb78aa1a54e114edcea645ede26c4c56a0..9be9a689c0d989b75d8596cd57f7c9b25255065b 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -849,4 +849,11 @@ mat-list.sm mat-list-item padding: 16px; margin: -16px!important; padding-top: 6rem; -} \ No newline at end of file +} + +.no-padding-dialog > mat-dialog-container +{ + padding-top:0 !important; + padding-right:0 !important; + padding-left:0 !important; +} diff --git a/src/res/ext/MNI152.json b/src/res/ext/MNI152.json index c47348065055770a66277a9b384fdc05962200c0..967ff0c943444f348219fa9509f3ec30d3ffdf65 100644 --- a/src/res/ext/MNI152.json +++ b/src/res/ext/MNI152.json @@ -27,8 +27,8 @@ "nehubaConfigURL": "nehubaConfig/MNI152NehubaConfig", "parcellations": [ { - "fullId": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", - "@id": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", + "fullId": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", + "@id": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", "name": "Cytoarchitectonic Maps - v2.5.1", "displayName": "Cytoarchitectonic Maps", "hasAdditionalViewMode": [ diff --git a/src/res/ext/atlas/atlas_multiLevelHuman.json b/src/res/ext/atlas/atlas_multiLevelHuman.json index 8bff76f3ec6d19cd1c975824bad57841f8bae5d4..d249b0fc87ded3b44b4db65559c688d6f5fef8a0 100644 --- a/src/res/ext/atlas/atlas_multiLevelHuman.json +++ b/src/res/ext/atlas/atlas_multiLevelHuman.json @@ -9,7 +9,7 @@ "displayName": "ICBM 152 2009c Nonlinear Asymmetric", "availableIn": [ { - "@id": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", + "@id": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", "name": "Cytoarchitectonic maps - v2.5" }, { @@ -85,7 +85,7 @@ "name": "Cytoarchitectonic Maps - v1.18", "baseLayer": true, "@version": { - "@next": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", + "@next": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", "@this": "juelich/iav/atlas/v1.0.0/8", "name": "v1.18", "@previous": null @@ -118,12 +118,12 @@ ] }, { - "@id": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", + "@id": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", "name": "Cytoarchitectonic maps", "baseLayer": true, "@version": { "@next": null, - "@this": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", + "@this": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", "name": "v2.6", "@previous": "juelich/iav/atlas/v1.0.0/8" }, diff --git a/src/res/ext/colin.json b/src/res/ext/colin.json index 2c1d0cd2639a41c661fb6e45c8f695e84299ee33..372a589b763d8141dd444b752b833a8814227c87 100644 --- a/src/res/ext/colin.json +++ b/src/res/ext/colin.json @@ -9,8 +9,8 @@ "nehubaConfigURL": "nehubaConfig/colinNehubaConfig", "parcellations": [ { - "fullId": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", - "@id": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", + "fullId": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", + "@id": "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26", "name": "Cytoarchitectonic Maps - v2.5.1", "displayName": "Cytoarchitectonic Maps", "auxillaryMeshIndices": [ diff --git a/deploy/assets/images/atlas-selection/allen-mouse-2015.png b/src/res/images/atlas-selection/allen-mouse-2015.png similarity index 100% rename from deploy/assets/images/atlas-selection/allen-mouse-2015.png rename to src/res/images/atlas-selection/allen-mouse-2015.png diff --git a/deploy/assets/images/atlas-selection/allen-mouse-2017.png b/src/res/images/atlas-selection/allen-mouse-2017.png similarity index 100% rename from deploy/assets/images/atlas-selection/allen-mouse-2017.png rename to src/res/images/atlas-selection/allen-mouse-2017.png diff --git a/deploy/assets/images/atlas-selection/allen-mouse.png b/src/res/images/atlas-selection/allen-mouse.png similarity index 100% rename from deploy/assets/images/atlas-selection/allen-mouse.png rename to src/res/images/atlas-selection/allen-mouse.png diff --git a/deploy/assets/images/atlas-selection/bugbrain.png b/src/res/images/atlas-selection/bigbrain.png similarity index 100% rename from deploy/assets/images/atlas-selection/bugbrain.png rename to src/res/images/atlas-selection/bigbrain.png diff --git a/deploy/assets/images/atlas-selection/colin27.png b/src/res/images/atlas-selection/colin27.png similarity index 100% rename from deploy/assets/images/atlas-selection/colin27.png rename to src/res/images/atlas-selection/colin27.png diff --git a/deploy/assets/images/atlas-selection/cortical-layers.png b/src/res/images/atlas-selection/cortical-layers.png similarity index 100% rename from deploy/assets/images/atlas-selection/cortical-layers.png rename to src/res/images/atlas-selection/cortical-layers.png diff --git a/deploy/assets/images/atlas-selection/cytoarchitectonic-maps.png b/src/res/images/atlas-selection/cytoarchitectonic-maps.png similarity index 100% rename from deploy/assets/images/atlas-selection/cytoarchitectonic-maps.png rename to src/res/images/atlas-selection/cytoarchitectonic-maps.png diff --git a/deploy/assets/images/atlas-selection/difumo-1024.png b/src/res/images/atlas-selection/difumo-1024.png similarity index 100% rename from deploy/assets/images/atlas-selection/difumo-1024.png rename to src/res/images/atlas-selection/difumo-1024.png diff --git a/deploy/assets/images/atlas-selection/difumo-128.png b/src/res/images/atlas-selection/difumo-128.png similarity index 100% rename from deploy/assets/images/atlas-selection/difumo-128.png rename to src/res/images/atlas-selection/difumo-128.png diff --git a/deploy/assets/images/atlas-selection/difumo-256.png b/src/res/images/atlas-selection/difumo-256.png similarity index 100% rename from deploy/assets/images/atlas-selection/difumo-256.png rename to src/res/images/atlas-selection/difumo-256.png diff --git a/deploy/assets/images/atlas-selection/difumo-512.png b/src/res/images/atlas-selection/difumo-512.png similarity index 100% rename from deploy/assets/images/atlas-selection/difumo-512.png rename to src/res/images/atlas-selection/difumo-512.png diff --git a/deploy/assets/images/atlas-selection/difumo-64.png b/src/res/images/atlas-selection/difumo-64.png similarity index 100% rename from deploy/assets/images/atlas-selection/difumo-64.png rename to src/res/images/atlas-selection/difumo-64.png diff --git a/deploy/assets/images/atlas-selection/firbe-long.png b/src/res/images/atlas-selection/firbe-long.png similarity index 100% rename from deploy/assets/images/atlas-selection/firbe-long.png rename to src/res/images/atlas-selection/firbe-long.png diff --git a/deploy/assets/images/atlas-selection/firbe-short.png b/src/res/images/atlas-selection/firbe-short.png similarity index 100% rename from deploy/assets/images/atlas-selection/firbe-short.png rename to src/res/images/atlas-selection/firbe-short.png diff --git a/src/res/images/atlas-selection/freesurfer.png b/src/res/images/atlas-selection/freesurfer.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b2819bc44292bb5fde7a86241846b33a5c50b2 Binary files /dev/null and b/src/res/images/atlas-selection/freesurfer.png differ diff --git a/deploy/assets/images/atlas-selection/grey-white-matter.png b/src/res/images/atlas-selection/grey-white-matter.png similarity index 100% rename from deploy/assets/images/atlas-selection/grey-white-matter.png rename to src/res/images/atlas-selection/grey-white-matter.png diff --git a/deploy/assets/images/atlas-selection/icbm2009c.png b/src/res/images/atlas-selection/icbm2009c.png similarity index 100% rename from deploy/assets/images/atlas-selection/icbm2009c.png rename to src/res/images/atlas-selection/icbm2009c.png diff --git a/deploy/assets/images/atlas-selection/short-bundle-hcp.png b/src/res/images/atlas-selection/short-bundle-hcp.png similarity index 100% rename from deploy/assets/images/atlas-selection/short-bundle-hcp.png rename to src/res/images/atlas-selection/short-bundle-hcp.png diff --git a/deploy/assets/images/atlas-selection/waxholm-v1.png b/src/res/images/atlas-selection/waxholm-v1.png similarity index 100% rename from deploy/assets/images/atlas-selection/waxholm-v1.png rename to src/res/images/atlas-selection/waxholm-v1.png diff --git a/deploy/assets/images/atlas-selection/waxholm-v2.png b/src/res/images/atlas-selection/waxholm-v2.png similarity index 100% rename from deploy/assets/images/atlas-selection/waxholm-v2.png rename to src/res/images/atlas-selection/waxholm-v2.png diff --git a/deploy/assets/images/atlas-selection/waxholm-v3.png b/src/res/images/atlas-selection/waxholm-v3.png similarity index 100% rename from deploy/assets/images/atlas-selection/waxholm-v3.png rename to src/res/images/atlas-selection/waxholm-v3.png diff --git a/deploy/assets/images/atlas-selection/waxholm.png b/src/res/images/atlas-selection/waxholm.png similarity index 100% rename from deploy/assets/images/atlas-selection/waxholm.png rename to src/res/images/atlas-selection/waxholm.png diff --git a/src/routerModule/parseRouteToTmplParcReg.spec.ts b/src/routerModule/parseRouteToTmplParcReg.spec.ts index aea1764afe001414d13690c19c30b7b14181c4d3..d81c9491c540006a068f13ad7b8e573f645d0345 100644 --- a/src/routerModule/parseRouteToTmplParcReg.spec.ts +++ b/src/routerModule/parseRouteToTmplParcReg.spec.ts @@ -1,6 +1,6 @@ import { parseSearchParamForTemplateParcellationRegion } from './parseRouteToTmplParcReg' -const url = `/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2/p:minds:core:parcellationatlas:v1.0.0:94c1125b-b87e-45e4-901c-00daee7f2579-25/@:0.0.0.-W000..2_ZG29.-ASCS.2-8jM2._aAY3..BSR0..0.1w4W0~.0..1jtG` +const url = `/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2/p:minds:core:parcellationatlas:v1.0.0:94c1125b-b87e-45e4-901c-00daee7f2579-26/@:0.0.0.-W000..2_ZG29.-ASCS.2-8jM2._aAY3..BSR0..0.1w4W0~.0..1jtG` const fakeState = { } diff --git a/src/routerModule/util.ts b/src/routerModule/util.ts index 6f93f0da7e29c9fe31a3534a5c92df5bbc9ac3b4..cbe862d6c28572a60acca844e0de4df162e19aa2 100644 --- a/src/routerModule/util.ts +++ b/src/routerModule/util.ts @@ -3,7 +3,7 @@ import { encodeNumber, decodeToNumber, separator } from './cipher' import { UrlSegment, UrlTree } from "@angular/router" import { getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants" import { mixNgLayers } from "src/services/state/ngViewerState.store" -import { PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.store' +import { PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.helper' import { viewerStateHelperStoreName } from "src/services/state/viewerState.store.helper" import { uiStatePreviewingDatasetFilesSelector } from "src/services/state/uiState/selectors" import { Component } from "@angular/core" diff --git a/src/services/effect/pluginUseEffect.spec.ts b/src/services/effect/pluginUseEffect.spec.ts index 693f8235226e9e2cca2514742404ff4b8d16a714..e89947a19bd3544260c890b369c69ab92ad7d732 100644 --- a/src/services/effect/pluginUseEffect.spec.ts +++ b/src/services/effect/pluginUseEffect.spec.ts @@ -6,13 +6,13 @@ import { Action } from "@ngrx/store"; import { provideMockActions } from "@ngrx/effects/testing"; import { provideMockStore } from "@ngrx/store/testing"; import { defaultRootState } from "../stateStore.service"; -import { PLUGINSTORE_CONSTANTS } from '../state/pluginState.store' -import { PLUGINSTORE_ACTION_TYPES } from '../state/pluginState.helper' +import { PLUGINSTORE_CONSTANTS, PLUGINSTORE_ACTION_TYPES } from '../state/pluginState.helper' import { Injectable } from "@angular/core"; import { getRandomHex } from 'common/util' import { PluginServices } from "src/plugin"; import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; import { hot } from "jasmine-marbles"; +import { BS_ENDPOINT } from "src/util/constants"; const actions$: Observable<Action> = of({type: 'TEST'}) @@ -103,6 +103,10 @@ describe('pluginUseEffect.ts', () => { { provide: PluginServices, useClass: MockPluginService + }, + { + provide: BS_ENDPOINT, + useValue: `http://localhost:1234` } ] }).compileComponents() diff --git a/src/services/effect/pluginUseEffect.ts b/src/services/effect/pluginUseEffect.ts index e9263e7b7c5f6cbad29cda9c8ac50a0d1f8edbf3..bd0a74772c60d75cceda439cb5402df584c4e2af 100644 --- a/src/services/effect/pluginUseEffect.ts +++ b/src/services/effect/pluginUseEffect.ts @@ -3,12 +3,10 @@ import { Effect } from "@ngrx/effects" import { select, Store } from "@ngrx/store" import { Observable, forkJoin } from "rxjs" import { filter, map, startWith, switchMap } from "rxjs/operators" -import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service" import { PluginServices } from "src/plugin/atlasViewer.pluginService.service" -import { PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.store' -import { PLUGINSTORE_ACTION_TYPES, pluginStateSelectorInitManifests } from 'src/services/state/pluginState.helper' -import { IavRootStoreInterface } from "../stateStore.service" +import { PLUGINSTORE_CONSTANTS, PLUGINSTORE_ACTION_TYPES, pluginStateSelectorInitManifests } from 'src/services/state/pluginState.helper' import { HttpClient } from "@angular/common/http" +import { getHttpHeader } from "src/util/constants" @Injectable({ providedIn: 'root', @@ -20,8 +18,7 @@ export class PluginServiceUseEffect { public initManifests$: Observable<any> constructor( - store$: Store<IavRootStoreInterface>, - constantService: AtlasViewerConstantsServices, + store$: Store<any>, pluginService: PluginServices, http: HttpClient ) { @@ -36,7 +33,7 @@ export class PluginServiceUseEffect { switchMap(arr => forkJoin( arr.map(([_source, url]) => http.get(url, { - headers: constantService.getHttpHeader(), + headers: getHttpHeader(), responseType: 'json' }) ) diff --git a/src/services/state/pluginState.helper.ts b/src/services/state/pluginState.helper.ts index d620e3f40b148f06c33b08fed673392cdf9f124a..e1c48c1941ab716f1bacc6c8980f62c2367a933a 100644 --- a/src/services/state/pluginState.helper.ts +++ b/src/services/state/pluginState.helper.ts @@ -8,4 +8,8 @@ export const PLUGINSTORE_ACTION_TYPES = { export const pluginStateSelectorInitManifests = createSelector( state => state['pluginState'], pluginState => pluginState.initManifests -) \ No newline at end of file +) + +export const PLUGINSTORE_CONSTANTS = { + INIT_MANIFEST_SRC: 'INIT_MANIFEST_SRC', +} diff --git a/src/services/state/pluginState.store.ts b/src/services/state/pluginState.store.ts index e71b49dc076b66bd375dbaddd2ede233d6e90fce..85bfa20a915ec8ab2e11497321cd66d95edabb91 100644 --- a/src/services/state/pluginState.store.ts +++ b/src/services/state/pluginState.store.ts @@ -1,6 +1,6 @@ import { Action } from '@ngrx/store' import { generalApplyState } from '../stateStore.helper' -import { PLUGINSTORE_ACTION_TYPES } from './pluginState.helper' +import { PLUGINSTORE_ACTION_TYPES, PLUGINSTORE_CONSTANTS } from './pluginState.helper' export const defaultState: StateInterface = { initManifests: [] } @@ -17,10 +17,6 @@ export interface ActionInterface extends Action { } -export const PLUGINSTORE_CONSTANTS = { - INIT_MANIFEST_SRC: 'INIT_MANIFEST_SRC', -} - export const getStateStore = ({ state = defaultState } = {}) => (prevState: StateInterface = state, action: ActionInterface): StateInterface => { switch (action.type) { case PLUGINSTORE_ACTION_TYPES.SET_INIT_PLUGIN: { diff --git a/src/services/state/viewerState/selectors.ts b/src/services/state/viewerState/selectors.ts index cb0ac92e83985becfedcb4eb8b264e34332081d2..61cd2b7dd54d4f3275ea01c5aa63fbebe60e54f7 100644 --- a/src/services/state/viewerState/selectors.ts +++ b/src/services/state/viewerState/selectors.ts @@ -29,7 +29,7 @@ export const viewerStateFetchedTemplatesSelector = createSelector( export const viewerStateSelectedTemplateSelector = createSelector( state => state['viewerState'], - viewerState => viewerState['templateSelected'] + viewerState => viewerState?.['templateSelected'] ) export const viewerStateSelectorStandaloneVolumes = createSelector( diff --git a/src/state/effects/viewerState.useEffect.spec.ts b/src/state/effects/viewerState.useEffect.spec.ts index 05adfe3a67c49d203c5d70cb10786dfc2407e698..21db1cfa3eb68607b9700f45e262a7b35670dcb4 100644 --- a/src/state/effects/viewerState.useEffect.spec.ts +++ b/src/state/effects/viewerState.useEffect.spec.ts @@ -266,7 +266,7 @@ describe('> viewerState.useEffect.ts', () => { expect(ctrlUseEffect.navigateToRegion$).toBeObservable( hot('a', { a: generalActionError({ - message: `${region.name} - does not have a position defined` + message: `${region.name} has malformed position property: []` }) }) ) @@ -418,7 +418,8 @@ describe('> viewerState.useEffect.ts', () => { ]) mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, [{ ['@id']: 'foo-bar', - templateSpaces: [ mockTmplSpc ] + templateSpaces: [ mockTmplSpc ], + parcellations: [ mockParc0 ] }]) actions$ = hot('a', { a: viewerStateSelectAtlas({ @@ -450,7 +451,8 @@ describe('> viewerState.useEffect.ts', () => { ]) mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, [{ ['@id']: 'foo-bar', - templateSpaces: [ mockTmplSpc1 ] + templateSpaces: [ mockTmplSpc1 ], + parcellations: [ mockParc1 ] }]) actions$ = hot('a', { a: viewerStateSelectAtlas({ @@ -494,7 +496,8 @@ describe('> viewerState.useEffect.ts', () => { ]) mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, [{ ['@id']: 'foo-bar', - templateSpaces: [ mockTmplSpc, mockTmplSpc1 ] + templateSpaces: [ mockTmplSpc, mockTmplSpc1 ], + parcellations: [ mockParc0, mockParc1 ] }]) }) it('> will select template.@id', () => { @@ -560,14 +563,7 @@ describe('> viewerState.useEffect.ts', () => { it('> if no arg is provided', () => { const obj = cvtNehubaConfigToNavigationObj() - expect(obj).toEqual({ - orientation: [0, 0, 0, 1], - perspectiveOrientation: [0 , 0, 0, 1], - perspectiveZoom: 1e6, - zoom: 1e6, - position: [0, 0, 0], - positionReal: true - }) + expect(obj).toEqual(defaultNavigationObject) }) it('> if null or undefined is provided', () => { diff --git a/src/state/effects/viewerState.useEffect.ts b/src/state/effects/viewerState.useEffect.ts index 814e323555a2974326d73f939a113f63fec5f8d1..0c96a3b9a43af9690294170bbd3950462aa2745d 100644 --- a/src/state/effects/viewerState.useEffect.ts +++ b/src/state/effects/viewerState.useEffect.ts @@ -22,7 +22,7 @@ const defaultZoom = 1e6 export const defaultNavigationObject = { orientation: [0, 0, 0, 1], - perspectiveOrientation: [0 , 0, 0, 1], + perspectiveOrientation: [0.5, -0.5, -0.5, 0.5], perspectiveZoom: defaultPerspectiveZoom, zoom: defaultZoom, position: [0, 0, 0], @@ -45,7 +45,11 @@ export const defaultNehubaConfigObject = { } export function cvtNehubaConfigToNavigationObj(nehubaConfig?){ - const { navigation, perspectiveOrientation = [0, 0, 0, 1], perspectiveZoom = 1e6 } = nehubaConfig || {} + const { + navigation, + perspectiveOrientation = defaultNavigationObject.perspectiveOrientation, + perspectiveZoom = defaultNavigationObject.perspectiveZoom + } = nehubaConfig || {} const { pose, zoomFactor = 1e6 } = navigation || {} const { position, orientation = [0, 0, 0, 1] } = pose || {} const { voxelSize = [1, 1, 1], voxelCoordinates = [0, 0, 0] } = position || {} @@ -116,19 +120,27 @@ export class ViewerStateControllerUseEffect implements OnDestroy { ) || atlas.templateSpaces[0] const templateSpaceId = templateTobeSelected['@id'] - - const parcellationId = ( - templateTobeSelected.availableIn.find(p => !!p.baseLayer) || - templateTobeSelected.availableIn[0] - )['@id'] - + const atlasTmpl = atlas.templateSpaces.find(t => t['@id'] === templateSpaceId) + const templateSelected = fetchedTemplates.find(t => templateSpaceId === t['@id']) if (!templateSelected) { return generalActionError({ message: CONST.TEMPLATE_NOT_FOUND }) } - const parcellationSelected = templateSelected.parcellations.find(p => p['@id'] === parcellationId) + + const atlasParcs = atlasTmpl.availableIn + .map(availP => atlas.parcellations.find(p => availP['@id'] === p['@id'])) + .filter(fullP => !!fullP) + const atlasParc = atlasParcs.find(p => { + if (!p.baseLayer) return false + if (p['@version']) { + return !p['@version']['@next'] + } + return true + }) || templateSelected.parcellations[0] + const parcellationId = atlasParc && atlasParc['@id'] + const parcellationSelected = parcellationId && templateSelected.parcellations.find(p => p['@id'] === parcellationId) return viewerStateNewViewer({ selectTemplate: templateSelected, selectParcellation: parcellationSelected @@ -388,7 +400,7 @@ export class ViewerStateControllerUseEffect implements OnDestroy { }) } - const { position } = region + const position = region.position || (region?.props?.centroid_mm || []).map((v: number) => v*1e6) if (!position) { return generalActionError({ message: `${region.name} - does not have a position defined` diff --git a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts index 4641e4ca4f72f187ddfd27771a50d74a4f5d06df..72a0687e3c67ec3cd078aed5a6bb9d309b9445f4 100644 --- a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts +++ b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts @@ -53,7 +53,7 @@ export class LayerBrowser implements OnInit, OnDestroy { constructor( private store: Store<any>, - private constantsService: PureContantService, + private pureConstantSvc: PureContantService, private log: LoggingService, ) { this.ngLayers$ = store.pipe( @@ -109,7 +109,7 @@ export class LayerBrowser implements OnInit, OnDestroy { startWith(false) ) - this.darktheme$ = this.constantsService.darktheme$.pipe( + this.darktheme$ = this.pureConstantSvc.darktheme$.pipe( shareReplay(1), ) diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts index 103062d18308a409572c44a61d39c18ea8925c37..92f65b7268a2760a01f2639b3ecb721edd1f046e 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts +++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts @@ -11,8 +11,7 @@ import { AuthService } from "src/auth"; import { IavRootStoreInterface, IDataEntry } from "src/services/stateStore.service"; import { MatDialog, MatDialogConfig, MatDialogRef } from "@angular/material/dialog"; import { MatBottomSheet } from "@angular/material/bottom-sheet"; -import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' -import {viewerStateSetViewerMode} from "src/services/state/viewerState/actions"; +import { CONST, QUICKTOUR_DESC } from 'common/constants' import { IQuickTourData } from "src/ui/quickTour/constrants"; @Component({ diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 7145621bc27bf7ae34fd0d85dde3d3d43a7dc14e..ff474d2afc77c8adee2ec5174493770321d320c7 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -48,7 +48,6 @@ import { Landmark2DModule } from "./nehubaContainer/2dLandmarks/module"; import { HANDLE_SCREENSHOT_PROMISE, TypeHandleScrnShotPromise } from "./screenshot"; import { ParcellationRegionModule } from "src/atlasComponents/parcellationRegion"; import { AtlasCmpParcellationModule } from "src/atlasComponents/parcellation"; -import { AtlasCmptConnModule } from "src/atlasComponents/connectivity"; @NgModule({ imports : [ @@ -68,7 +67,6 @@ import { AtlasCmptConnModule } from "src/atlasComponents/connectivity"; Landmark2DModule, ParcellationRegionModule, AtlasCmpParcellationModule, - AtlasCmptConnModule, ], declarations : [ diff --git a/src/util/constants.ts b/src/util/constants.ts index 7b3d33613918c65b4c6eb0a1e51511b598eca3f0..7a8bb4a12b67fa82447a8d82749dc5d319489c87 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -116,3 +116,17 @@ export const compareLandmarksChanged: (prevLandmarks: any[], newLandmarks: any[] } export const CYCLE_PANEL_MESSAGE = `[spacebar] to cycle through views` +export const BS_ENDPOINT = new InjectionToken<string>('BS_ENDPOINT') + +export const UNSUPPORTED_PREVIEW = [{ + text: 'Preview of Colin 27 and JuBrain Cytoarchitectonic', + previewSrc: './res/image/1.png', +}, { + text: 'Preview of Big Brain 2015 Release', + previewSrc: './res/image/2.png', +}, { + text: 'Preview of Waxholm Rat V2.0', + previewSrc: './res/image/3.png', +}] + +export const UNSUPPORTED_INTERVAL = 7000 diff --git a/src/util/directives/captureClickListener.directive.ts b/src/util/directives/captureClickListener.directive.ts index b8e04e77f862b0841fb23cac3b801b494a729809..1a9fb9b583b34f9c1c0c5d6cab929636951da35d 100644 --- a/src/util/directives/captureClickListener.directive.ts +++ b/src/util/directives/captureClickListener.directive.ts @@ -27,9 +27,9 @@ export class CaptureClickListenerDirective implements OnInit, OnDestroy { } public ngOnInit() { - const mouseDownObs$ = fromEvent(this.element, 'mousedown', { capture: this.captureDocument }) - const mouseMoveObs$ = fromEvent(this.element, 'mousemove', { capture: this.captureDocument }) - const mouseUpObs$ = fromEvent(this.element, 'mouseup', { capture: this.captureDocument }) + const mouseDownObs$ = fromEvent(this.element, 'pointerdown', { capture: this.captureDocument }) + const mouseMoveObs$ = fromEvent(this.element, 'pointermove', { capture: this.captureDocument }) + const mouseUpObs$ = fromEvent(this.element, 'pointerup', { capture: this.captureDocument }) this.subscriptions.push( mouseDownObs$.subscribe(event => { diff --git a/src/util/fn.spec.ts b/src/util/fn.spec.ts index be7d8b7dd660ec877b5fc640883eaae03712d2f5..bbdbff61026839a3ce6a020d75bc25a4a5cd1aca 100644 --- a/src/util/fn.spec.ts +++ b/src/util/fn.spec.ts @@ -1,9 +1,9 @@ import { fakeAsync, tick } from '@angular/core/testing' import {} from 'jasmine' -import { cold, hot } from 'jasmine-marbles' -import { of } from 'rxjs' +import { hot } from 'jasmine-marbles' +import { Observable, of } from 'rxjs' import { switchMap } from 'rxjs/operators' -import { isSame, getGetRegionFromLabelIndexId, switchMapWaitFor } from './fn' +import { isSame, getGetRegionFromLabelIndexId, switchMapWaitFor, bufferUntil } from './fn' describe(`> util/fn.ts`, () => { @@ -11,7 +11,7 @@ describe(`> util/fn.ts`, () => { const colinsJson = require('!json-loader!../res/ext/colin.json') const COLIN_JULICHBRAIN_LAYER_NAME = `COLIN_V25_LEFT_NG_SPLIT_HEMISPHERE` - const COLIN_V25_ID = 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25' + const COLIN_V25_ID = 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26' it('translateds hoc1 from labelIndex to region', () => { @@ -78,4 +78,53 @@ describe(`> util/fn.ts`, () => { })) }) }) + + describe('> #bufferUntil', () => { + let src: Observable<number> + beforeEach(() => { + src = hot('a-b-c|', { + a: 1, + b: 2, + c: 3, + }) + }) + it('> outputs array of original emitted value', () => { + + expect( + src.pipe( + bufferUntil({ + condition: () => true, + leading: true, + }) + ) + ).toBeObservable( + hot('a-b-c|', { + a: [1], + b: [2], + c: [3], + }) + ) + }) + + it('> on condition success, emit all in array', () => { + + let counter = 0 + expect( + src.pipe( + bufferUntil({ + condition: () => { + counter ++ + return counter > 2 + }, + leading: true, + interval: 60000, + }) + ) + ).toBeObservable( + hot('----c|', { + c: [1,2,3], + }) + ) + }) + }) }) diff --git a/src/util/fn.ts b/src/util/fn.ts index db302960443d418e798526711cbbe196259663cd..f4eb08ac40900f621ecc25b7b237fa74fc6cf78d 100644 --- a/src/util/fn.ts +++ b/src/util/fn.ts @@ -1,5 +1,5 @@ import { deserialiseParcRegionId } from 'common/util' -import { interval, of } from 'rxjs' +import { interval, Observable, of } from 'rxjs' import { filter, mapTo, take } from 'rxjs/operators' export function isSame(o, n) { @@ -87,3 +87,214 @@ export function switchMapWaitFor<T>(opts: ISwitchMapWaitFor){ ) } } + + +type TCacheFunctionArg = { + serialization?: (...arg: any[]) => string +} + +/** + * Member function decorator + * Multiple function calls with strictly equal arguments will return cached result + * @returns cached result if exists, else call original function + */ +export const CachedFunction = (config?: TCacheFunctionArg) => { + const { serialization } = config || {} + const cache = {} + const cachedValKeySym = Symbol('cachedValKeySym') + return (_target: Record<string, any>, _propertyKey: string, descriptor: PropertyDescriptor) => { + const originalMethod = descriptor.value + descriptor.value = function(...args: any[]) { + let found = cache + if (serialization) { + const key = serialization(...args) + if (!cache[key]) cache[key] = {} + found = cache[key] + } else { + for (const arg of args) { + if (!cache[arg]) cache[arg] = {} + found = cache[arg] + } + } + if (found[cachedValKeySym]) return found[cachedValKeySym] + const returnVal = originalMethod.apply(this, args) + found[cachedValKeySym] = returnVal + return returnVal + } + } +} + +// A quick, non security hash function +export class QuickHash { + private length = 6 + constructor(opts?: any){ + if (opts?.length) this.length = opts.length + } + + @CachedFunction() + getHash(str: string){ + let hash = 0 + for (const char of str) { + const charCode = char.charCodeAt(0) + hash = ((hash << 5) - hash) + charCode + hash = hash & hash + } + return hash.toString(16).slice(1) + } +} + +/** + * in order to maintain backwards compat with url encoding of selected regions + * TODO setup a sentry to catch if these are ever used. if not, retire the hard coding + */ +const BACKCOMAP_KEY_DICT = { + + // human multi level + 'juelich/iav/atlas/v1.0.0/1': { + // icbm152 + 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2': { + // julich brain v2.6 + 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26': { + 'left hemisphere': 'MNI152_V25_LEFT_NG_SPLIT_HEMISPHERE', + 'right hemisphere': 'MNI152_V25_RIGHT_NG_SPLIT_HEMISPHERE' + }, + // bundle hcp + "juelich/iav/atlas/v1.0.0/79cbeaa4ee96d5d3dfe2876e9f74b3dc3d3ffb84304fb9b965b1776563a1069c": { + "whole brain": "superficial-white-bundle-HCP" + }, + // julich brain v1.18 + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579": { + "left hemisphere": "jubrain mni152 v18 left", + "right hemisphere": "jubrain mni152 v18 right", + }, + // long bundle + "juelich/iav/atlas/v1.0.0/5": { + "whole brain": "fibre bundle long" + }, + // bundle short + "juelich/iav/atlas/v1.0.0/6": { + "whole brain": "fibre bundle short" + }, + // difumo 64 + "minds/core/parcellationatlas/v1.0.0/d80fbab2-ce7f-4901-a3a2-3c8ef8a3b721": { + "whole brain": "DiFuMo Atlas (64 dimensions)" + }, + "minds/core/parcellationatlas/v1.0.0/73f41e04-b7ee-4301-a828-4b298ad05ab8": { + "whole brain": "DiFuMo Atlas (128 dimensions)" + }, + "minds/core/parcellationatlas/v1.0.0/141d510f-0342-4f94-ace7-c97d5f160235": { + "whole brain": "DiFuMo Atlas (256 dimensions)" + }, + "minds/core/parcellationatlas/v1.0.0/63b5794f-79a4-4464-8dc1-b32e170f3d16": { + "whole brain": "DiFuMo Atlas (512 dimensions)" + }, + "minds/core/parcellationatlas/v1.0.0/12fca5c5-b02c-46ce-ab9f-f12babf4c7e1": { + "whole brain": "DiFuMo Atlas (1024 dimensions)" + }, + }, + // colin 27 + "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992": { + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26": { + "left hemisphere": "COLIN_V25_LEFT_NG_SPLIT_HEMISPHERE", + "right hemisphere": "COLIN_V25_RIGHT_NG_SPLIT_HEMISPHERE", + }, + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579": { + "left hemisphere": "jubrain colin v18 left", + "right hemisphere": "jubrain colin v18 right", + } + }, + // big brain + "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588": { + "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26": { + + }, + // isocortex + "juelich/iav/atlas/v1.0.0/4": { + "whole brain": " tissue type: " + }, + // cortical layers + "juelich/iav/atlas/v1.0.0/3": { + "whole brain": "cortical layers" + }, + } + } +} + +export class MultiDimMap{ + + private map = new Map() + + static KeyHash = new QuickHash() + + static GetProxyKeyMatch(...arg: any[]): string { + + let proxyKeyMatch = BACKCOMAP_KEY_DICT + for (let i = 0; i < arg.length; i++) { + if (proxyKeyMatch) proxyKeyMatch = proxyKeyMatch[arg[i]] + } + if (proxyKeyMatch) return proxyKeyMatch as any + return null + } + + static GetKey(...arg: any[]){ + let mapKey = `` + for (let i = 0; i < arg.length; i++) { + mapKey += arg[i] + } + return MultiDimMap.KeyHash.getHash(mapKey) + } + + set(...arg: any[]) { + const mapKey = MultiDimMap.GetKey(...(arg.slice(0, -1))) + this.map.set(mapKey, arg[arg.length - 1]) + } + get(...arg: any[]) { + const mapKey = MultiDimMap.GetKey(...arg) + return this.map.get(mapKey) + } + delete(...arg: any[]) { + const mapKey = MultiDimMap.GetKey(...arg) + return this.map.delete(mapKey) + } +} + +export function recursiveMutate<T>(arr: T[], getChildren: (obj: T) => T[], mutateFn: (obj: T) => void){ + for (const obj of arr) { + mutateFn(obj) + recursiveMutate( + getChildren(obj), + getChildren, + mutateFn + ) + } +} + +export function bufferUntil<T>(opts: ISwitchMapWaitFor) { + const { condition, leading, interval: int = 160 } = opts + let buffer: T[] = [] + return (src: Observable<T>) => new Observable<T[]>(obs => { + const sub = interval(int).pipe( + filter(() => buffer.length > 0) + ).subscribe(() => { + if (condition()) { + obs.next(buffer) + buffer = [] + } + }) + src.subscribe( + val => { + if (leading && condition()) { + obs.next([...buffer, val]) + buffer = [] + } else { + buffer.push(val) + } + }, + err => obs.error(err), + () => { + obs.complete() + sub.unsubscribe() + } + ) + }) +} diff --git a/src/util/pipes/filterArray.pipe.ts b/src/util/pipes/filterArray.pipe.ts index eca8e461026de20361e62e71eeefe4d76ba96d37..5e7c9653d73430b7631b78cd7a739a2a2ca13c75 100644 --- a/src/util/pipes/filterArray.pipe.ts +++ b/src/util/pipes/filterArray.pipe.ts @@ -1,11 +1,12 @@ import { Pipe, PipeTransform } from "@angular/core"; @Pipe({ - name: 'filterArray' + name: 'filterArray', + pure: true }) export class FilterArrayPipe implements PipeTransform{ public transform<T>(arr: T[], filterFn: (item: T, index: number, array: T[]) => boolean){ - return arr.filter(filterFn) + return (arr || []).filter(filterFn) } } diff --git a/src/util/pureConstant.service.spec.ts b/src/util/pureConstant.service.spec.ts index 68302ae81cd99b1d239390d4a2b9aa6b2639f538..2ca6fb127878caace8d1fdf0d5049a65bd2cc2af 100644 --- a/src/util/pureConstant.service.spec.ts +++ b/src/util/pureConstant.service.spec.ts @@ -2,9 +2,13 @@ import { HttpClientTestingModule, HttpTestingController } from "@angular/common/ import { TestBed } from "@angular/core/testing" import { MockStore, provideMockStore } from "@ngrx/store/testing" import { hot } from "jasmine-marbles" +import { BS_ENDPOINT } from "src/atlasComponents/regionalFeatures/bsFeatures" import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service" import { viewerStateFetchedAtlasesSelector, viewerStateFetchedTemplatesSelector } from "src/services/state/viewerState/selectors" import { PureContantService } from "./pureConstant.service" +import { TAtlas } from "./siibraApiConstants/types" + +const MOCK_BS_ENDPOINT = `http://localhost:1234` describe('> pureConstant.service.ts', () => { describe('> PureContantService', () => { @@ -21,6 +25,10 @@ describe('> pureConstant.service.ts', () => { useValue: { worker: null } + }, + { + provide: BS_ENDPOINT, + useValue: MOCK_BS_ENDPOINT } ] }) @@ -37,26 +45,33 @@ describe('> pureConstant.service.ts', () => { it('> can be init', () => { const service = TestBed.inject(PureContantService) - const exp = httpController.expectOne(`${service.backendUrl}/atlases/`) + const exp = httpController.expectOne(`${MOCK_BS_ENDPOINT}/atlases`) exp.flush([]) expect(service).toBeTruthy() }) describe('> allFetchingReady$', () => { - + const mockAtlas: TAtlas = { + id: 'mockatlas id', + name: 'mockatlas name', + links: { + parcellations: { + href: `${MOCK_BS_ENDPOINT}/mockatlas-parcellation-href` + }, + spaces: { + href: `${MOCK_BS_ENDPOINT}/atlas-spaces` + } + } + } it('> can be init, and configuration emits allFetchingReady$', () => { const service = TestBed.inject(PureContantService) - const exp = httpController.expectOne(`${service.backendUrl}/atlases/`) - exp.flush([]) + const exp = httpController.expectOne(`${MOCK_BS_ENDPOINT}/atlases`) + exp.flush([mockAtlas]) service.allFetchingReady$.subscribe() - const expT = httpController.expectOne(`${service.backendUrl}templates`) - expT.flush([]) - expect( - service.allFetchingReady$ - ).toBeObservable( - hot('a', { - a: true, - }) - ) + + const expT1 = httpController.expectOne(`${MOCK_BS_ENDPOINT}/atlases/${encodeURIComponent(mockAtlas.id)}/spaces`) + expT1.flush([]) + const expP1 = httpController.expectOne(`${MOCK_BS_ENDPOINT}/atlases/${encodeURIComponent(mockAtlas.id)}/parcellations`) + expP1.flush([]) }) }) diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts index b97c8e586b341a36eb2bcded6dd59fbb1ce3d82b..0d0b2d9aef929afe356bcec3421dd45551c2700c 100644 --- a/src/util/pureConstant.service.ts +++ b/src/util/pureConstant.service.ts @@ -1,15 +1,153 @@ -import { Injectable, OnDestroy } from "@angular/core"; +import { Inject, Injectable, OnDestroy } from "@angular/core"; import { Store, select } from "@ngrx/store"; -import { Observable, merge, Subscription, of, forkJoin, fromEvent, combineLatest, timer } from "rxjs"; +import { Observable, Subscription, of, forkJoin, fromEvent, combineLatest } from "rxjs"; import { viewerConfigSelectorUseMobileUi } from "src/services/state/viewerConfig.store.helper"; -import { shareReplay, tap, scan, catchError, filter, switchMap, map, take, distinctUntilChanged } from "rxjs/operators"; +import { shareReplay, tap, scan, catchError, filter, switchMap, map, take, distinctUntilChanged, mapTo } from "rxjs/operators"; import { HttpClient } from "@angular/common/http"; import { viewerStateFetchedTemplatesSelector, viewerStateSetFetchedAtlases } from "src/services/state/viewerState.store.helper"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { LoggingService } from "src/logging"; -import { viewerStateFetchedAtlasesSelector } from "src/services/state/viewerState/selectors"; +import { viewerStateFetchedAtlasesSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; +import { BS_ENDPOINT } from "src/util/constants"; +import { flattenReducer } from 'common/util' +import { TAtlas, TId, TParc, TRegion, TRegionDetail, TSpaceFull, TSpaceSummary } from "./siibraApiConstants/types"; +import { MultiDimMap, recursiveMutate } from "./fn"; -const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16) +function getNgId(atlasId: string, tmplId: string, parcId: string, regionKey: string){ + const proxyId = MultiDimMap.GetProxyKeyMatch(atlasId, tmplId, parcId, regionKey) + if (proxyId) return proxyId + return '_' + MultiDimMap.GetKey(atlasId, tmplId, parcId, regionKey) +} + +function parseId(id: TId){ + if (typeof id === 'string') return id + return `${id.kg.kgSchema}/${id.kg.kgId}` +} + +type THasId = { + ['@id']: string + name: string +} + +type TIAVAtlas = { + templateSpaces: ({ availableIn: THasId[] } & THasId)[] + parcellations: ({ + availableIn: THasId[] + baseLayer: boolean + '@version': { + name: string + '@next': string + '@previous': string + '@this': string + } + } & THasId)[] +} & THasId + +const spaceMiscInfoMap = new Map([ + ['minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588', { + name: 'bigbrain', + scale: 1, + }], + ['minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2', { + name: 'icbm2009c', + scale: 1, + }], + ['minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992', { + name: 'colin27', + scale: 1, + }], + ['minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9', { + name: 'allen-mouse', + scale: 0.1, + }], + ['minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8', { + name: 'waxholm', + scale: 0.1, + }], +]) + +function getNehubaConfig(space: TSpaceFull) { + + const darkTheme = space.src_volume_type === 'mri' + const { scale } = spaceMiscInfoMap.get(space.id) || { scale: 1 } + const backgrd = darkTheme + ? [0,0,0,1] + : [1,1,1,1] + + const rmPsp = darkTheme + ? {"mode":"<","color":[0.1,0.1,0.1,1]} + :{"color":[1,1,1,1],"mode":"=="} + const drawSubstrates = darkTheme + ? {"color":[0.5,0.5,1,0.2]} + : {"color":[0,0,0.5,0.15]} + const drawZoomLevels = darkTheme + ? {"cutOff":150000 * scale } + : {"cutOff":200000 * scale,"color":[0.5,0,0,0.15] } + + return { + "configName": "", + "globals": { + "hideNullImageValues": true, + "useNehubaLayout": { + "keepDefaultLayouts": false + }, + "useNehubaMeshLayer": true, + "rightClickWithCtrlGlobal": false, + "zoomWithoutCtrlGlobal": false, + "useCustomSegmentColors": true + }, + "zoomWithoutCtrl": true, + "hideNeuroglancerUI": true, + "rightClickWithCtrl": true, + "rotateAtViewCentre": true, + "enableMeshLoadingControl": true, + "zoomAtViewCentre": true, + // "restrictUserNavigation": true, + "dataset": { + "imageBackground": backgrd, + "initialNgState": { + "showDefaultAnnotations": false, + "layers": {}, + "navigation": { + "zoomFactor": 350000 * scale, + }, + "perspectiveOrientation": [ + 0.3140767216682434, + -0.7418519854545593, + 0.4988985061645508, + -0.3195493221282959 + ], + "perspectiveZoom": 1922235.5293810747 * scale + } + }, + "layout": { + "useNehubaPerspective": { + "perspectiveSlicesBackground": backgrd, + "removePerspectiveSlicesBackground": rmPsp, + "perspectiveBackground": backgrd, + "fixedZoomPerspectiveSlices": { + "sliceViewportWidth": 300, + "sliceViewportHeight": 300, + "sliceZoom": 563818.3562426177 * scale, + "sliceViewportSizeMultiplier": 2 + }, + "mesh": { + "backFaceColor": backgrd, + "removeBasedOnNavigation": true, + "flipRemovedOctant": true + }, + "centerToOrigin": true, + "drawSubstrates": drawSubstrates, + "drawZoomLevels": drawZoomLevels, + "restrictZoomLevel": { + "minZoom": 1200000 * scale, + "maxZoom": 3500000 * scale + } + } + } + } + +} @Injectable({ providedIn: 'root' @@ -17,6 +155,7 @@ const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16) export class PureContantService implements OnDestroy{ + private subscriptions: Subscription[] = [] public repoUrl = `https://github.com/HumanBrainProject/interactive-viewer` public supportEmailAddress = `support@ebrains.eu` public docUrl = `https://interactive-viewer.readthedocs.io/en/latest/` @@ -28,98 +167,140 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" ` public useTouchUI$: Observable<boolean> - public fetchedAtlases$: Observable<any> public darktheme$: Observable<boolean> public totalAtlasesLength: number public allFetchingReady$: Observable<boolean> - public backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}` + private atlasParcSpcRegionMap = new MultiDimMap() + + private _backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}` + get backendUrl() { + console.warn(`something is using backendUrl`) + return this._backendUrl + } + /** + * TODO remove + * when removing, also remove relevant worker code + */ private workerUpdateParcellation$ = fromEvent(this.workerService.worker, 'message').pipe( filter((message: MessageEvent) => message && message.data && message.data.type === 'UPDATE_PARCELLATION_REGIONS'), map(({ data }) => data) ) - private fetchTemplate = (templateUrl) => this.http.get(`${this.backendUrl}${templateUrl}`, { responseType: 'json' }).pipe( - switchMap((template: any) => { - if (template.nehubaConfig) { - return of(template) + public getRegionDetail(atlasId: string, parcId: string, spaceId: string, region: any) { + return this.http.get<TRegionDetail>( + `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}/regions/${encodeURIComponent(region.name)}`, + { + params: { + 'space_id': spaceId + }, + responseType: 'json' } - if (template.nehubaConfigURL) { - return this.http.get(`${this.backendUrl}${template.nehubaConfigURL}`, { responseType: 'json' }).pipe( - map(nehubaConfig => { - return { - ...template, - nehubaConfig, - } - }), - ) + ) + } + + private getRegions(atlasId: string, parcId: string, spaceId: string){ + return this.http.get<TRegion[]>( + `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}/regions`, + { + params: { + 'space_id': spaceId + }, + responseType: 'json' } - return of(template) - }), - ) + ) + } - private processTemplate = template => forkJoin( - template.parcellations.map(parcellation => { + private getParcs(atlasId: string){ + return this.http.get<TParc[]>( + `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations`, + { responseType: 'json' } + ) + } - const id = getUniqueId() + private httpCallCache = new Map<string, Observable<any>>() - this.workerService.worker.postMessage({ - type: 'PROPAGATE_PARC_REGION_ATTR', - parcellation, - inheritAttrsOpts: { - ngId: (parcellation as any ).ngId, - relatedAreas: [], - fullId: null - }, - id - }) + private getParcDetail(atlasId: string, parcId: string) { + return this.http.get<TParc>( + `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}`, + { responseType: 'json' } + ) + } - return this.workerUpdateParcellation$.pipe( - filter(({ id: returnedId }) => id === returnedId), - take(1), - map(({ parcellation }) => parcellation) - ) - }) - ) + private getSpaces(atlasId: string){ + return this.http.get<TSpaceSummary[]>( + `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/spaces`, + { responseType: 'json' } + ) + } - public getTemplateEndpoint$ = this.http.get<any[]>(`${this.backendUrl}templates`, { responseType: 'json' }).pipe( - catchError(() => { - this.log.warn(`fetching root /tempaltes error`) - return of([]) - }), - shareReplay(), - ) + private getSpaceDetail(atlasId: string, spaceId: string) { + return this.http.get<TSpaceFull>( + `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/spaces/${encodeURIComponent(spaceId)}`, + { responseType: 'json' } + ) + } - public initFetchTemplate$ = this.getTemplateEndpoint$.pipe( - switchMap((templates: string[]) => merge( - ...templates.map(templateName => this.fetchTemplate(templateName).pipe( - switchMap(template => this.processTemplate(template).pipe( - map(parcellations => { - return { - ...template, - parcellations + private getSpacesAndParc(atlasId: string): Observable<{ templateSpaces: TSpaceFull[], parcellations: TParc[] }> { + const cacheKey = `getSpacesAndParc::${atlasId}` + if (this.httpCallCache.has(cacheKey)) return this.httpCallCache.get(cacheKey) + + const spaces$ = this.getSpaces(atlasId).pipe( + switchMap(spaces => spaces.length > 0 + ? forkJoin( + spaces.map(space => this.getSpaceDetail(atlasId, parseId(space.id))) + ) + : of([])) + ) + const parcs$ = this.getParcs(atlasId).pipe( + // need not to get full parc data. first level gets all data + // switchMap(parcs => forkJoin( + // parcs.map(parc => this.getParcDetail(atlasId, parseId(parc.id))) + // )) + ) + const returnObs = forkJoin([ + spaces$, + parcs$, + ]).pipe( + map(([ templateSpaces, parcellations ]) => { + /** + * select only parcellations that contain renderable volume(s) + */ + const filteredParcellations = parcellations.filter(p => { + for (const spaceKey in p.volumeSrc) { + for (const hemisphereKey in p.volumeSrc[spaceKey]) { + if (p.volumeSrc[spaceKey][hemisphereKey].some(vol => vol.volume_type === 'neuroglancer/precomputed')) return true + if (p.volumeSrc[spaceKey][hemisphereKey].some(vol => vol.volume_type === 'neuroglancer/precompmesh')) return true + if (p.volumeSrc[spaceKey][hemisphereKey].some(vol => vol.volume_type === 'threesurfer/gii')) return true + if (p.volumeSrc[spaceKey][hemisphereKey].some(vol => vol.volume_type === 'threesurfer/gii-label')) return true } - }) - )) - )), - )), - catchError((err) => { - this.log.warn(`fetching templates error`, err) - return of(null) - }), - ) + } + return false + }) + return { + templateSpaces, + parcellations: filteredParcellations + } + }), + shareReplay(1) + ) + this.httpCallCache.set(cacheKey, returnObs) + return returnObs + } constructor( private store: Store<any>, private http: HttpClient, private log: LoggingService, private workerService: AtlasWorkerService, + @Inject(BS_ENDPOINT) private bsEndpoint: string, ){ this.darktheme$ = this.store.pipe( - select(state => state?.viewerState?.templateSelected?.useTheme === 'dark') + select(viewerStateSelectedTemplateSelector), + map(tmpl => tmpl?.useTheme === 'dark') ) this.useTouchUI$ = this.store.pipe( @@ -127,21 +308,6 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" shareReplay(1) ) - this.fetchedAtlases$ = this.http.get(`${this.backendUrl.replace(/\/$/, '')}/atlases/`, { responseType: 'json' }).pipe( - catchError((err, obs) => of(null)), - filter(v => !!v), - tap((arr: any[]) => this.totalAtlasesLength = arr.length), - switchMap(atlases => merge( - ...atlases.map(({ url }) => this.http.get( - /^http/.test(url) - ? url - : `${this.backendUrl.replace(/\/$/, '')}/${url}`, - { responseType: 'json' })) - )), - scan((acc, curr) => acc.concat(curr).sort((a, b) => (a.order || 1000) - (b.order || 1001)), []), - shareReplay(1) - ) - this.subscriptions.push( this.fetchedAtlases$.subscribe(fetchedAtlases => this.store.dispatch( @@ -151,7 +317,8 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" ) this.allFetchingReady$ = combineLatest([ - this.getTemplateEndpoint$.pipe( + this.initFetchTemplate$.pipe( + filter(v => !!v), map(arr => arr.length), ), this.store.pipe( @@ -171,7 +338,351 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" ) } - private subscriptions: Subscription[] = [] + private getAtlases$ = this.http.get<TAtlas[]>( + `${this.bsEndpoint}/atlases`, + { + responseType: 'json' + } + ).pipe( + shareReplay(1) + ) + + public fetchedAtlases$: Observable<TIAVAtlas[]> = this.getAtlases$.pipe( + switchMap(atlases => { + return forkJoin( + atlases.map( + atlas => this.getSpacesAndParc(atlas.id).pipe( + map(({ templateSpaces, parcellations }) => { + return { + '@id': atlas.id, + name: atlas.name, + templateSpaces: templateSpaces.map(tmpl => { + return { + '@id': tmpl.id, + name: tmpl.name, + availableIn: tmpl.availableParcellations.map(parc => { + return { + '@id': parc.id, + name: parc.name + } + }), + originDatainfos: tmpl.originDatainfos || [] + } + }), + parcellations: parcellations.map(parc => { + return { + '@id': parseId(parc.id), + name: parc.name, + baseLayer: parc.modality === 'cytoarchitecture', + '@version': { + '@next': parc.version?.next, + '@previous': parc.version?.prev, + 'name': parc.version?.name, + '@this': parseId(parc.id) + }, + groupName: parc.modality || null, + availableIn: parc.availableSpaces.map(space => { + return { + '@id': space.id, + name: space.name, + /** + * TODO need original data format + */ + // originalDatasetFormats: [{ + // name: "probability map" + // }] + } + }), + originDatainfos: parc.originDatainfos || [] + } + }) + } + }), + catchError((err, obs) => { + console.error(err) + return of(null) + }) + ) + ) + ) + }), + catchError((err, obs) => of([])), + tap((arr: any[]) => this.totalAtlasesLength = arr.length), + scan((acc, curr) => acc.concat(curr).sort((a, b) => (a.order || 1001) - (b.order || 1000)), []), + shareReplay(1) + ) + + public initFetchTemplate$ = this.fetchedAtlases$.pipe( + switchMap(atlases => { + return forkJoin( + atlases.map(atlas => this.getSpacesAndParc(atlas['@id']).pipe( + switchMap(({ templateSpaces, parcellations }) => { + const ngLayerObj = {} + return forkJoin( + templateSpaces.map( + tmpl => { + ngLayerObj[tmpl.id] = {} + return tmpl.availableParcellations.map( + parc => this.getRegions(atlas['@id'], parc.id, tmpl.id).pipe( + tap(regions => { + recursiveMutate( + regions, + region => region.children, + region => { + /** + * individual map(s) + * this should work for both fully mapped and interpolated + * in the case of interpolated, it sucks that the ngLayerObj will be set multiple times + */ + if ( + tmpl.id in (region.volumeSrc || {}) + && 'collect' in region.volumeSrc[tmpl.id] + ) { + const dedicatedMap = region.volumeSrc[tmpl.id]['collect'].filter(v => v.volume_type === 'neuroglancer/precomputed') + if (dedicatedMap.length === 1) { + 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] = { + source: `precomputed://${dedicatedMap[0].url}`, + type: "segmentation", + transform: dedicatedMap[0].detail['neuroglancer/precomputed'].transform + } + } + } + + /** + * if label index is defined + */ + if (!!region.labelIndex) { + const hemisphereKey = /left hemisphere|left/.test(region.name) + // these two keys are, unfortunately, more or less hardcoded + // which is less than ideal + ? 'left hemisphere' + : /right hemisphere|right/.test(region.name) + ? 'right hemisphere' + : 'whole brain' + + /** + * TODO fix in siibra-api + */ + if ( + tmpl.id !== 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588' + && parc.id === 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290' + && hemisphereKey === 'whole brain' + ) { + region.labelIndex = null + return + } + + if ( + tmpl.id === 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588' + && parc.id === 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290' + && hemisphereKey === 'whole brain' + ) { + region.children = [] + return + } + + const hemispheredNgId = getNgId(atlas['@id'], tmpl.id, parc.id, hemisphereKey) + region['ngId'] = hemispheredNgId + } + } + ) + this.atlasParcSpcRegionMap.set( + atlas['@id'], tmpl.id, parc.id, regions + ) + + /** + * populate maps for parc + */ + for (const parc of parcellations) { + if (tmpl.id in (parc.volumeSrc || {})) { + // key: 'left hemisphere' | 'right hemisphere' | 'whole brain' + for (const key in (parc.volumeSrc[tmpl.id] || {})) { + for (const vol of parc.volumeSrc[tmpl.id][key]) { + if (vol.volume_type === 'neuroglancer/precomputed') { + const ngIdKey = getNgId(atlas['@id'], tmpl.id, parseId(parc.id), key) + ngLayerObj[tmpl.id][ngIdKey] = { + source: `precomputed://${vol.url}`, + type: "segmentation", + transform: vol.detail['neuroglancer/precomputed'].transform + } + } + } + } + } + } + }), + catchError((err, obs) => { + return of(null) + }) + ) + ) + } + ).reduce(flattenReducer, []) + ).pipe( + mapTo({ templateSpaces, parcellations, ngLayerObj }) + ) + }), + map(({ templateSpaces, parcellations, ngLayerObj }) => { + return templateSpaces.map(tmpl => { + + // configuring three-surfer + let threeSurferConfig = {} + const threeSurferVolSrc = tmpl.volume_src.find(v => v.volume_type === 'threesurfer/gii') + if (threeSurferVolSrc) { + const foundP = parcellations.find(p => { + return !!p.volumeSrc[tmpl.id] + }) + const url = threeSurferVolSrc.url + const { surfaces } = threeSurferVolSrc.detail['threesurfer/gii'] as { surfaces: {mode: string, hemisphere: 'left' | 'right', url: string}[] } + const modObj = {} + for (const surface of surfaces) { + + const hemisphereKey = surface.hemisphere === 'left' + ? 'left hemisphere' + : 'right hemisphere' + + + /** + * concating all available gii maps + */ + // const allFreesurferLabels = foundP.volumeSrc[tmpl.id][hemisphereKey].filter(v => v.volume_type === 'threesurfer/gii-label') + // for (const lbl of allFreesurferLabels) { + // const modeToConcat = { + // mesh: surface.url, + // hemisphere: surface.hemisphere, + // colormap: lbl.url + // } + + // const key = `${surface.mode} - ${lbl.name}` + // if (!modObj[key]) { + // modObj[key] = [] + // } + // modObj[key].push(modeToConcat) + // } + + /** + * only concat first matching gii map + */ + const key = surface.mode + const modeToConcat = { + mesh: surface.url, + hemisphere: surface.hemisphere, + colormap: (() => { + const lbl = foundP.volumeSrc[tmpl.id][hemisphereKey].find(v => v.volume_type === 'threesurfer/gii-label') + return lbl?.url + })() + } + if (!modObj[key]) { + modObj[key] = [] + } + modObj[key].push(modeToConcat) + + } + foundP[tmpl.id] + threeSurferConfig = { + "three-surfer": { + '@context': { + root: url + }, + modes: Object.keys(modObj).map(name => { + return { + name, + meshes: modObj[name] + } + }) + }, + nehubaConfig: null, + nehubaConfigURL: null, + useTheme: 'dark' + } + } + const darkTheme = tmpl.src_volume_type === 'mri' + const nehubaConfig = getNehubaConfig(tmpl) + const initialLayers = nehubaConfig.dataset.initialNgState.layers + + const tmplNgId = tmpl.name + const tmplAuxMesh = `${tmpl.name} auxmesh` + + const precomputed = tmpl.volume_src.find(src => src.volume_type === 'neuroglancer/precomputed') + if (precomputed) { + initialLayers[tmplNgId] = { + type: "image", + source: `precomputed://${precomputed.url}`, + transform: precomputed.detail['neuroglancer/precomputed'].transform + } + } + + // TODO + // siibra-python accidentally left out volume type of precompmesh + // https://github.com/FZJ-INM1-BDA/siibra-python/pull/55 + // use url to determine for now + // const precompmesh = tmpl.volume_src.find(src => src.volume_type === 'neuroglancer/precompmesh') + const precompmesh = tmpl.volume_src.find(src => !!src.detail?.['neuroglancer/precompmesh']) + const auxMeshes = [] + if (precompmesh){ + initialLayers[tmplAuxMesh] = { + source: `precompmesh://${precompmesh.url}`, + type: "segmentation", + transform: precompmesh.detail['neuroglancer/precompmesh'].transform + } + for (const auxMesh of precompmesh.detail['neuroglancer/precompmesh'].auxMeshes) { + + auxMeshes.push({ + ...auxMesh, + ngId: tmplAuxMesh, + '@id': `${tmplAuxMesh} ${auxMesh.name}`, + visible: true + }) + } + } + + for (const key in (ngLayerObj[tmpl.id] || {})) { + initialLayers[key] = ngLayerObj[tmpl.id][key] + } + + return { + name: tmpl.name, + '@id': tmpl.id, + fullId: tmpl.id, + useTheme: darkTheme ? 'dark' : 'light', + ngId: tmplNgId, + nehubaConfig, + auxMeshes, + /** + * only populate the parcelltions made available + */ + parcellations: tmpl.availableParcellations.filter( + p => parcellations.some(p2 => parseId(p2.id) === p.id) + ).map(parc => { + const fullParcInfo = parcellations.find(p => parseId(p.id) === parc.id) + const regions = this.atlasParcSpcRegionMap.get(atlas['@id'], tmpl.id, parc.id) || [] + return { + fullId: parc.id, + '@id': parc.id, + name: parc.name, + regions, + originDatainfos: fullParcInfo?.originDatainfos || [] + } + }), + ...threeSurferConfig + } + }) + }) + )) + ) + }), + map(arr => { + return arr.reduce(flattenReducer, []) + }), + catchError((err) => { + this.log.warn(`fetching templates error`, err) + return of(null) + }), + shareReplay(1), + ) ngOnDestroy(){ while(this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe() diff --git a/src/util/siibraApiConstants/types.ts b/src/util/siibraApiConstants/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..32c79048e9ab6f1d8904f866d8eef2589ea86aa1 --- /dev/null +++ b/src/util/siibraApiConstants/types.ts @@ -0,0 +1,180 @@ +type THref = { + href: string +} + +type TSpaceType = 'mri' | 'histology' + +type TNgTransform = number[][] + +type TVolumeType = 'nii' | 'neuroglancer/precomputed' | 'neuroglancer/precompmesh' | 'detailed maps' | 'threesurfer/gii' | 'threesurfer/gii-label' +type TParcModality = 'cytoarchitecture' | 'functional modes' | 'fibre architecture' + +type TAuxMesh = { + name: string + labelIndicies: number[] +} + +interface IVolumeTypeDetail { + 'nii': null + 'neuroglancer/precomputed': { + 'neuroglancer/precomputed': { + 'transform': TNgTransform + } + } + 'neuroglancer/precompmesh': { + 'neuroglancer/precompmesh': { + 'auxMeshes': TAuxMesh[] + 'transform': TNgTransform + } + } + 'detailed maps': null +} + +type TVolumeSrc<VolumeType extends keyof IVolumeTypeDetail> = { + '@id': string + '@type': 'fzj/tmp/volume_type/v0.0.1' + name: string + url: string + volume_type: TVolumeType + detail: IVolumeTypeDetail[VolumeType] +} + +type TKgIdentifier = { + kgSchema: string + kgId: string +} + +type TVersion = { + name: string + prev: string | null + next: string | null +} + +export type TId = string | { kg: TKgIdentifier } + +export type TAtlas = { + id: string + name: string + links: { + parcellations: THref + spaces: THref + } +} + +export type TSpaceSummary = { + id: { + kg: TKgIdentifier + } + name: string + links: { + self: THref + } +} + +export type TParcSummary = { + id: string + name: string +} + +export type TDatainfos = { + name: string + description: string + urls: { + cite: string + doi: string + }[] +} + +export type TSpaceFull = { + id: string + name: string + key: string //??? + type: string //??? + url: string //??? + ziptarget: string //??? + src_volume_type: TSpaceType + volume_src: TVolumeSrc<keyof IVolumeTypeDetail>[] + availableParcellations: TParcSummary[] + links: { + templates: THref + parcellation_maps: THref + } + originDatainfos: TDatainfos[] +} + +export type TParc = { + id: { + kg: TKgIdentifier + } + name: string + availableSpaces: { + id: string + name: string + }[] + links: { + self: THref + } + regions: THref + modality: TParcModality + version: TVersion + volumeSrc: { + [key: string]: { + [key: string]: TVolumeSrc<keyof IVolumeTypeDetail>[] + } + } + originDatainfos: TDatainfos[] +} + +export type TRegionDetail = { + name: string + children: TRegionDetail[] + rgb: number[] + id: string + labelIndex: number + volumeSrc: { + [key: string]: { + [key: string]: TVolumeSrc<keyof IVolumeTypeDetail>[] + } + } + availableIn: { + id: string + name: string + }[] + hasRegionalMap: boolean + props: { + centroid_mm: number[] + volume_mm: number + surface_mm: number + is_cortical: number + } + links: { + [key: string]: string + } + originDatainfos: TDatainfos[] +} + +export type TRegion = { + name: string + children: TRegion[] + volumeSrc: { + [key: string]: { + [key: string]: TVolumeSrc<keyof IVolumeTypeDetail>[] + } + } + + labelIndex?: number + rgb?: number[] + id?: { + kg: TKgIdentifier + } + + /** + * missing + */ + + originDatasets?: ({ + filename: string + } & TKgIdentifier) [] + + position?: number[] +} diff --git a/src/viewerModule/componentStore.ts b/src/viewerModule/componentStore.ts index ca3d8e5082ed0529924f7fdd33b792f62aa3f0aa..5fe24c51085c329be8ed3eaee2048f3114cbda43 100644 --- a/src/viewerModule/componentStore.ts +++ b/src/viewerModule/componentStore.ts @@ -3,6 +3,8 @@ import { select } from "@ngrx/store"; import { ReplaySubject, Subject } from "rxjs"; import { shareReplay } from "rxjs/operators"; +export class LockError extends Error{} + /** * polyfill for ngrx component store * until upgrade to v11 @@ -12,13 +14,23 @@ import { shareReplay } from "rxjs/operators"; @Injectable() export class ComponentStore<T>{ private _state$: Subject<T> = new ReplaySubject<T>(1) + private _lock: boolean = false + get isLocked() { + return this._lock + } setState(state: T){ + if (this.isLocked) throw new LockError('State is locked') this._state$.next(state) } - select(selectorFn: (state: T) => unknown) { + select<V>(selectorFn: (state: T) => V) { return this._state$.pipe( select(selectorFn), shareReplay(1), ) } + getLock(): () => void { + if (this.isLocked) throw new LockError('Cannot get lock. State is locked') + this._lock = true + return () => this._lock = false + } } diff --git a/src/viewerModule/constants.ts b/src/viewerModule/constants.ts index aa974a56e592618f67ae248f9b1224c15466ff2c..8fa2d25231b1435428d4ca4f652617904fcc95bc 100644 --- a/src/viewerModule/constants.ts +++ b/src/viewerModule/constants.ts @@ -2,9 +2,3 @@ import { InjectionToken } from "@angular/core"; import { Observable } from "rxjs"; export const VIEWERMODULE_DARKTHEME = new InjectionToken<Observable<boolean>>('VIEWERMODULE_DARKTHEME') - -export interface IViewerCmpUiState { - sideNav: { - activePanelsTitle: string[] - } -} diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 3c74f7b74700f9088e54b54523c9b318859694c2..566471f479781b9646e644e19b0f8f63445693b1 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -1,7 +1,6 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { Observable } from "rxjs"; -import { AtlasCmptConnModule } from "src/atlasComponents/connectivity"; import { DatabrowserModule } from "src/atlasComponents/databrowserModule"; import { AtlasCmpParcellationModule } from "src/atlasComponents/parcellation"; import { ParcellationRegionModule } from "src/atlasComponents/parcellationRegion"; @@ -17,7 +16,6 @@ import { CONTEXT_MENU_ITEM_INJECTOR, TContextMenu, UtilModule } from "src/util"; import { VIEWERMODULE_DARKTHEME } from "./constants"; import { NehubaModule, NehubaViewerUnit } from "./nehuba"; import { ThreeSurferModule } from "./threeSurfer"; -import { RegionAccordionTooltipTextPipe } from "./util/regionAccordionTooltipText.pipe"; import { ViewerCmp } from "./viewerCmp/viewerCmp.component"; import {UserAnnotationsModule} from "src/atlasComponents/userAnnotations"; import {QuickTourModule} from "src/ui/quickTour/module"; @@ -25,6 +23,7 @@ import { INJ_ANNOT_TARGET } from "src/atlasComponents/userAnnotations/tools/type import { NEHUBA_INSTANCE_INJTKN } from "./nehuba/util"; import { map } from "rxjs/operators"; import { TContextArg } from "./viewer.interface"; +import { ViewerStateBreadCrumbModule } from "./viewerStateBreadCrumb/module"; @NgModule({ imports: [ @@ -40,16 +39,15 @@ import { TContextArg } from "./viewer.interface"; ParcellationRegionModule, UtilModule, AtlasCmpParcellationModule, - AtlasCmptConnModule, ComponentsModule, BSFeatureModule, UserAnnotationsModule, QuickTourModule, ContextMenuModule, + ViewerStateBreadCrumbModule, ], declarations: [ ViewerCmp, - RegionAccordionTooltipTextPipe, ], providers: [ { diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts index 9428b63efb8eed35e8d634de1ddd82667e77107b..bbd36606525015fcd2f8e6315a061ea2a7f56a36 100644 --- a/src/viewerModule/nehuba/constants.ts +++ b/src/viewerModule/nehuba/constants.ts @@ -29,9 +29,9 @@ export function getMultiNgIdsRegionsLabelIndexMap(parcellation: any = {}, inheri const processRegion = (region: any) => { const { ngId: rNgId } = region - const existingMap = map.get(rNgId) const labelIndex = Number(region.labelIndex) - if (labelIndex) { + if (labelIndex && rNgId) { + const existingMap = map.get(rNgId) if (!existingMap) { const newMap = new Map() newMap.set(labelIndex, region) diff --git a/src/viewerModule/nehuba/layerCtrl.service/index.ts b/src/viewerModule/nehuba/layerCtrl.service/index.ts index 61975bcfe1e269f45f4774e535089977b02bf1ea..05cbb34fe0a5803c935749c2c45e6345bb467746 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/index.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/index.ts @@ -1,5 +1,5 @@ export { - NehubaLayerControlService + NehubaLayerControlService, } from './layerCtrl.service' export { @@ -7,4 +7,5 @@ export { SET_COLORMAP_OBS, SET_LAYER_VISIBILITY, getRgb, + INgLayerInterface, } from './layerCtrl.util' diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts index cb0e151e25db43987e08f28862d77be36a52b112..3d2ae0f4fbaa47b75eb2f24d21dc155a9b481a89 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.spec.ts @@ -1,10 +1,13 @@ -import { TestBed } from "@angular/core/testing" +import { fakeAsync, TestBed, tick } from "@angular/core/testing" import { MockStore, provideMockStore } from "@ngrx/store/testing" -import { viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors" +import { viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors" import { NehubaLayerControlService } from "./layerCtrl.service" import * as layerCtrlUtil from '../constants' import { hot } from "jasmine-marbles" - +import { IColorMap } from "./layerCtrl.util" +import { debounceTime } from "rxjs/operators" +import { ngViewerSelectorClearView, ngViewerSelectorLayers } from "src/services/state/ngViewerState.store.helper" +const util = require('common/util') describe('> layerctrl.service.ts', () => { describe('> NehubaLayerControlService', () => { @@ -169,6 +172,18 @@ describe('> layerctrl.service.ts', () => { }) }) }) + + const foobar1 = { + 'foo-bar': { + 1: { red: 100, green: 200, blue: 255 }, + 2: { red: 15, green: 15, blue: 15 }, + } + } + const foobar2 = { + 'foo-bar': { + 2: { red: 255, green: 255, blue: 255 }, + } + } describe('> overwriteColorMap$ firing', () => { beforeEach(() => { @@ -191,32 +206,47 @@ describe('> layerctrl.service.ts', () => { it('> should overwrite existing colormap', () => { const service = TestBed.inject(NehubaLayerControlService) - service.overwriteColorMap$.next({ - 'foo-bar': { - 2: { - red: 255, - green: 255, - blue: 255, - } - } - }) + service.overwriteColorMap$.next(foobar2) expect(service.setColorMap$).toBeObservable( - hot('(ab)', { - a: { - 'foo-bar': { - 1: { red: 100, green: 200, blue: 255 }, - 2: { red: 15, green: 15, blue: 15 }, - } - }, - b: { - 'foo-bar': { - 2: { red: 255, green: 255, blue: 255 }, - } - } + hot('(b)', { + a: foobar1, + b: foobar2 }) ) }) + + it('> unsub/resub should not result in overwritecolormap last emitted value', fakeAsync(() => { + const service = TestBed.inject(NehubaLayerControlService) + + let subscrbiedVal: IColorMap + const sub = service.setColorMap$.pipe( + debounceTime(16), + ).subscribe(val => { + subscrbiedVal = val + }) + + service.overwriteColorMap$.next(foobar2) + tick(32) + expect(subscrbiedVal).toEqual(foobar2) + tick(16) + sub.unsubscribe() + subscrbiedVal = null + + // mock emit selectParc etc... + mockStore.overrideSelector(viewerStateSelectedParcellationSelector, {}) + mockStore.setState({}) + const sub2 = service.setColorMap$.pipe( + debounceTime(16), + ).subscribe(val => { + subscrbiedVal = val + }) + + tick(32) + expect(subscrbiedVal).toEqual(foobar1) + sub2.unsubscribe() + + })) }) }) @@ -267,5 +297,124 @@ describe('> layerctrl.service.ts', () => { })) }) }) + + describe('> segmentVis$', () => { + const region1= { + ngId: 'ngid', + labelIndex: 1 + } + const region2= { + ngId: 'ngid', + labelIndex: 2 + } + beforeEach(() => { + mockStore.overrideSelector(viewerStateSelectedRegionsSelector, []) + mockStore.overrideSelector(ngViewerSelectorLayers, []) + mockStore.overrideSelector(ngViewerSelectorClearView, false) + mockStore.overrideSelector(viewerStateSelectedParcellationSelector, {}) + }) + + it('> by default, should return []', () => { + const service = TestBed.inject(NehubaLayerControlService) + expect(service.segmentVis$).toBeObservable( + hot('a', { + a: [] + }) + ) + }) + + describe('> if sel regions exist', () => { + beforeEach(() => { + mockStore.overrideSelector(viewerStateSelectedRegionsSelector, [ + region1, region2 + ]) + }) + + it('> default, should return encoded strings', () => { + mockStore.overrideSelector(viewerStateSelectedRegionsSelector, [ + region1, region2 + ]) + const service = TestBed.inject(NehubaLayerControlService) + expect(service.segmentVis$).toBeObservable( + hot('a', { + a: [`ngid#1`, `ngid#2`] + }) + ) + }) + + it('> if clearflag is true, then return []', () => { + + mockStore.overrideSelector(ngViewerSelectorClearView, true) + const service = TestBed.inject(NehubaLayerControlService) + expect(service.segmentVis$).toBeObservable( + hot('a', { + a: [] + }) + ) + }) + }) + + describe('> if non mixable layer exist', () => { + beforeEach(() => { + mockStore.overrideSelector(ngViewerSelectorLayers, [{ + mixability: 'nonmixable' + }]) + }) + + it('> default, should return null', () => { + const service = TestBed.inject(NehubaLayerControlService) + expect(service.segmentVis$).toBeObservable( + hot('a', { + a: null + }) + ) + }) + + it('> if regions selected, should still return null', () => { + + mockStore.overrideSelector(viewerStateSelectedRegionsSelector, [ + region1, region2 + ]) + const service = TestBed.inject(NehubaLayerControlService) + expect(service.segmentVis$).toBeObservable( + hot('a', { + a: null + }) + ) + }) + + describe('> if clear flag is set', () => { + beforeEach(() => { + mockStore.overrideSelector(ngViewerSelectorClearView, true) + }) + + it('> default, should return []', () => { + const service = TestBed.inject(NehubaLayerControlService) + expect(service.segmentVis$).toBeObservable( + hot('a', { + a: [] + }) + ) + }) + + it('> if reg selected, should return []', () => { + + mockStore.overrideSelector(viewerStateSelectedRegionsSelector, [ + region1, region2 + ]) + const service = TestBed.inject(NehubaLayerControlService) + expect(service.segmentVis$).toBeObservable( + hot('a', { + a: [] + }) + ) + }) + }) + }) + }) + + describe('> ngLayersController$', () => { + + }) }) }) diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index 7a338c72e24a09592fdc718d5064687e7d67a6db..a7df04ef14769d32ed0d0e92ebe8fc6a8cc6478c 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -1,16 +1,22 @@ -import { Injectable } from "@angular/core"; +import { Inject, Injectable, OnDestroy, Optional } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { BehaviorSubject, combineLatest, merge, Observable, Subject } from "rxjs"; -import { filter, map, shareReplay, tap } from "rxjs/operators"; -import { viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; -import { getRgb, IColorMap } from "./layerCtrl.util"; +import { BehaviorSubject, combineLatest, from, merge, Observable, of, Subject, Subscription } from "rxjs"; +import { distinctUntilChanged, filter, map, shareReplay, switchMap, withLatestFrom } from "rxjs/operators"; +import { viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; +import { getRgb, IColorMap, INgLayerCtrl, INgLayerInterface, TNgLayerCtrl } from "./layerCtrl.util"; import { getMultiNgIdsRegionsLabelIndexMap } from "../constants"; import { IAuxMesh } from '../store' +import { REGION_OF_INTEREST } from "src/util/interfaces"; +import { TRegionDetail } from "src/util/siibraApiConstants/types"; +import { EnumColorMapName } from "src/util/colorMaps"; +import { getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants"; +import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerSelectorClearView, ngViewerSelectorLayers } from "src/services/state/ngViewerState.store.helper"; +import { serialiseParcellationRegion } from 'common/util' export function getAuxMeshesAndReturnIColor(auxMeshes: IAuxMesh[]): IColorMap{ const returnVal: IColorMap = {} for (const auxMesh of auxMeshes as IAuxMesh[]) { - const { ngId, labelIndicies, rgb } = auxMesh + const { ngId, labelIndicies, rgb = [255, 255, 255] } = auxMesh const auxMeshColorMap = returnVal[ngId] || {} for (const lblIdx of labelIndicies) { auxMeshColorMap[lblIdx as number] = { @@ -25,7 +31,9 @@ export function getAuxMeshesAndReturnIColor(auxMeshes: IAuxMesh[]): IColorMap{ } @Injectable() -export class NehubaLayerControlService { +export class NehubaLayerControlService implements OnDestroy{ + + static PMAP_LAYER_NAME = 'regional-pmap' private selectedParcellation$ = this.store$.pipe( select(viewerStateSelectedParcellationSelector) @@ -100,10 +108,106 @@ export class NehubaLayerControlService { }) ) + private sub: Subscription[] = [] + + ngOnDestroy(){ + while (this.sub.length > 0) this.sub.pop().unsubscribe() + } + constructor( private store$: Store<any>, + @Optional() @Inject(REGION_OF_INTEREST) roi$: Observable<TRegionDetail> ){ + if (roi$) { + + this.sub.push( + roi$.pipe( + switchMap(roi => { + if (!roi || !roi.hasRegionalMap) { + // clear pmap + return of(null) + } + + const { links } = roi + const { regional_map: regionalMapUrl, regional_map_info: regionalMapInfoUrl } = links + return from(fetch(regionalMapInfoUrl).then(res => res.json())).pipe( + map(regionalMapInfo => { + return { + roi, + regionalMapUrl, + regionalMapInfo + } + }) + ) + }) + ).subscribe(processedRoi => { + if (!processedRoi) { + this.store$.dispatch( + ngViewerActionRemoveNgLayer({ + layer: { + name: NehubaLayerControlService.PMAP_LAYER_NAME + } + }) + ) + return + } + const { + roi, + regionalMapUrl, + regionalMapInfo + } = processedRoi + const { min, max, colormap = EnumColorMapName.VIRIDIS } = regionalMapInfo || {} as any + const shaderObj = { + ...PMAP_DEFAULT_CONFIG, + ...{ colormap }, + ...( typeof min !== 'undefined' ? { lowThreshold: min } : {} ), + ...( max ? { highThreshold: max } : { highThreshold: 1 } ) + } + + const layer = { + name: NehubaLayerControlService.PMAP_LAYER_NAME, + source : `nifti://${regionalMapUrl}`, + mixability : 'nonmixable', + shader : getShader(shaderObj), + } + + this.store$.dispatch( + ngViewerActionAddNgLayer({ layer }) + ) + + // this.layersService.highThresholdMap.set(layerName, highThreshold) + // this.layersService.lowThresholdMap.set(layerName, lowThreshold) + // this.layersService.colorMapMap.set(layerName, cmap) + // this.layersService.removeBgMap.set(layerName, removeBg) + }) + ) + } + + this.sub.push( + this.ngLayers$.subscribe(({ ngLayers }) => { + this.ngLayersRegister.layers = ngLayers + }) + ) + + this.sub.push( + this.store$.pipe( + select(ngViewerSelectorClearView), + distinctUntilChanged() + ).subscribe(flag => { + const pmapLayer = this.ngLayersRegister.layers.find(l => l.name === NehubaLayerControlService.PMAP_LAYER_NAME) + if (!pmapLayer) return + const payload = { + type: 'update', + payload: { + [NehubaLayerControlService.PMAP_LAYER_NAME]: { + visible: !flag + } + } + } as TNgLayerCtrl<'update'> + this.manualNgLayersControl$.next(payload) + }) + ) } public activeColorMap: IColorMap @@ -113,8 +217,10 @@ export class NehubaLayerControlService { public setColorMap$: Observable<IColorMap> = merge( this.activeColorMap$, this.overwriteColorMap$.pipe( - filter(v => !!v) + filter(v => !!v), ) + ).pipe( + shareReplay(1) ) public visibleLayer$: Observable<string[]> = combineLatest([ @@ -136,4 +242,122 @@ export class NehubaLayerControlService { return Array.from(ngIdSet) }) ) + + /** + * define when shown segments should be updated + */ + public segmentVis$: Observable<string[]> = combineLatest([ + /** + * selectedRegions + */ + this.store$.pipe( + select(viewerStateSelectedRegionsSelector) + ), + /** + * if layer contains non mixable layer + */ + this.store$.pipe( + select(ngViewerSelectorLayers), + map(layers => layers.findIndex(l => l.mixability === 'nonmixable') >= 0), + ), + /** + * clearviewqueue, indicating something is controlling colour map + * show all seg + */ + this.store$.pipe( + select(ngViewerSelectorClearView), + distinctUntilChanged() + ) + ]).pipe( + withLatestFrom(this.selectedParcellation$), + map(([[ regions, nonmixableLayerExists, clearViewFlag ], selParc]) => { + if (nonmixableLayerExists && !clearViewFlag) { + return null + } + const { ngId: defaultNgId } = selParc || {} + + /* selectedregionindexset needs to be updated regardless of forceshowsegment */ + const selectedRegionIndexSet = new Set<string>(regions.map(({ngId = defaultNgId, labelIndex}) => serialiseParcellationRegion({ ngId, labelIndex }))) + if (selectedRegionIndexSet.size > 0 && !clearViewFlag) { + return [...selectedRegionIndexSet] + } else { + return [] + } + }) + ) + + /** + * ngLayers controller + */ + + private ngLayersRegister: {layers: INgLayerInterface[]} = { + layers: [] + } + public removeNgLayers(layerNames: string[]) { + this.ngLayersRegister.layers + .filter(layer => layerNames?.findIndex(l => l === layer.name) >= 0) + .map(l => l.name) + .forEach(layerName => { + this.store$.dispatch(ngViewerActionRemoveNgLayer({ + layer: { + name: layerName + } + })) + }) + } + public addNgLayer(layers: INgLayerInterface[]){ + this.store$.dispatch(ngViewerActionAddNgLayer({ + layer: layers + })) + } + private ngLayers$ = this.store$.pipe( + select(ngViewerSelectorLayers), + map((ngLayers: INgLayerInterface[]) => { + const newLayers = ngLayers.filter(l => { + const registeredLayerNames = this.ngLayersRegister.layers.map(l => l.name) + return !registeredLayerNames.includes(l.name) + }) + const removeLayers = this.ngLayersRegister.layers.filter(l => { + const stateLayerNames = ngLayers.map(l => l.name) + return !stateLayerNames.includes(l.name) + }) + return { newLayers, removeLayers, ngLayers } + }), + shareReplay(1) + ) + private manualNgLayersControl$ = new Subject<TNgLayerCtrl<keyof INgLayerCtrl>>() + ngLayersController$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>> = merge( + this.ngLayers$.pipe( + map(({ newLayers }) => newLayers), + filter(layers => layers.length > 0), + map(newLayers => { + + const newLayersObj: any = {} + newLayers.forEach(({ name, source, ...rest }) => newLayersObj[name] = { + ...rest, + source, + // source: getProxyUrl(source), + // ...getProxyOther({source}) + }) + + return { + type: 'add', + payload: newLayersObj + } as TNgLayerCtrl<'add'> + }) + ), + this.ngLayers$.pipe( + map(({ removeLayers }) => removeLayers), + filter(layers => layers.length > 0), + map(removeLayers => { + const removeLayerNames = removeLayers.map(v => v.name) + return { + type: 'remove', + payload: { names: removeLayerNames } + } as TNgLayerCtrl<'remove'> + }) + ), + this.manualNgLayersControl$, + ).pipe( + ) } diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts index 9ad22f97de5527b7620985ae1ad72f6d0ad4947c..a815d3c682e50eb1b4797076cf238eb7917e0c83 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts @@ -36,5 +36,36 @@ export function getRgb(labelIndex: number, region: { rgb?: [number, number, numb } } + +export interface INgLayerCtrl { + remove: { + names: string[] + } + add: { + [key: string]: INgLayerInterface + } + update: { + [key: string]: INgLayerInterface + } +} + +export type TNgLayerCtrl<T extends keyof INgLayerCtrl> = { + type: T + payload: INgLayerCtrl[T] +} + export const SET_COLORMAP_OBS = new InjectionToken<Observable<IColorMap>>('SET_COLORMAP_OBS') export const SET_LAYER_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_LAYER_VISIBILITY') +export const SET_SEGMENT_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_SEGMENT_VISIBILITY') +export const NG_LAYER_CONTROL = new InjectionToken<TNgLayerCtrl<keyof INgLayerCtrl>>('NG_LAYER_CONTROL') + +export interface INgLayerInterface { + name: string // displayName + source: string + mixability: string // base | mixable | nonmixable + annotation?: string // + id?: string // unique identifier + visible?: boolean + shader?: string + transform?: any +} diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts index 7faa79281c19f9f82c5d0a0910ec9b856aede274..f6b977efc20ceeccf1f729079e5308367a948bf5 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts @@ -309,6 +309,12 @@ describe('> nehubaViewer.component.ts', () => { const setup = () => { const fixture = TestBed.createComponent(NehubaViewerUnit) + /** + * set nehubaViewer, since some methods check viewer is loaded + */ + fixture.componentInstance.nehubaViewer = { + ngviewer: {} + } fixture.detectChanges() prvSetCMSpy = spyOn<any>(fixture.componentInstance, 'setColorMap').and.callFake(() => {}) } diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 8da75583b69334cc2a49a060ed2a3751a95f860d..23adf8d1c91fae693f223a0c4cc2bc71eda542a8 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -4,7 +4,7 @@ import { debounceTime, filter, map, scan, startWith, mapTo, switchMap, take, ski import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { StateInterface as ViewerConfiguration } from "src/services/state/viewerConfig.store"; import { LoggingService } from "src/logging"; -import { getExportNehuba, getViewer, setNehubaViewer, switchMapWaitFor } from "src/util/fn"; +import { bufferUntil, getExportNehuba, getViewer, setNehubaViewer, switchMapWaitFor } from "src/util/fn"; import { NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn } from "../util"; import { deserialiseParcRegionId } from 'common/util' import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants"; @@ -12,6 +12,7 @@ import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl. import '!!file-loader?context=third_party&name=main.bundle.js!export-nehuba/dist/min/main.bundle.js' import '!!file-loader?context=third_party&name=chunk_worker.bundle.js!export-nehuba/dist/min/chunk_worker.bundle.js' +import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl } from "../layerCtrl.service/layerCtrl.util"; const NG_LANDMARK_LAYER_NAME = 'spatial landmark layer' const NG_USER_LANDMARK_LAYER_NAME = 'user landmark layer' @@ -151,6 +152,8 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { @Optional() @Inject(SET_MESHES_TO_LOAD) private injSetMeshesToLoad$: Observable<IMeshesToLoad>, @Optional() @Inject(SET_COLORMAP_OBS) private setColormap$: Observable<IColorMap>, @Optional() @Inject(SET_LAYER_VISIBILITY) private layerVis$: Observable<string[]>, + @Optional() @Inject(SET_SEGMENT_VISIBILITY) private segVis$: Observable<string[]>, + @Optional() @Inject(NG_LAYER_CONTROL) private layerCtrl$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>>, ) { if (this.nehubaViewer$) { @@ -306,13 +309,15 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { this.ondestroySubscriptions.push( this.layerVis$.pipe( switchMap(switchMapWaitFor({ condition: () => !!(this.nehubaViewer?.ngviewer) })), - tap(() => { - const managedLayers = this.nehubaViewer.ngviewer.layerManager.managedLayers - managedLayers.forEach(layer => layer.setVisible(false)) - }), debounceTime(160), ).subscribe((layerNames: string[]) => { - + /** + * debounce 160ms to set layer invisible etc + * on switch from freesurfer -> volumetric viewer, race con results in managed layer not necessarily setting layer visible correctly + */ + const managedLayers = this.nehubaViewer.ngviewer.layerManager.managedLayers + managedLayers.forEach(layer => layer.setVisible(false)) + for (const layerName of layerNames) { const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName) if (layer) { @@ -326,6 +331,56 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { } else { this.log.error(`SET_LAYER_VISIBILITY not provided`) } + + if (this.segVis$) { + this.ondestroySubscriptions.push( + this.segVis$.pipe().subscribe(val => { + // null === hide all seg + if (val === null) { + this.hideAllSeg() + return + } + // empty array === show all seg + if (val.length === 0) { + this.showAllSeg() + return + } + // show limited seg + this.showSegs(val) + }) + ) + } else { + this.log.error(`SET_SEGMENT_VISIBILITY not provided`) + } + + if (this.layerCtrl$) { + this.ondestroySubscriptions.push( + this.layerCtrl$.pipe( + bufferUntil(({ + condition: () => !!this.nehubaViewer?.ngviewer + })) + ).subscribe(messages => { + for (const message of messages) { + if (message.type === 'add') { + const p = message as TNgLayerCtrl<'add'> + this.loadLayer(p.payload) + } + if (message.type === 'remove') { + const p = message as TNgLayerCtrl<'remove'> + for (const name of p.payload.names){ + this.removeLayer({ name }) + } + } + if (message.type === 'update') { + const p = message as TNgLayerCtrl<'update'> + this.updateLayer(p.payload) + } + } + }) + ) + } else { + this.log.error(`NG_LAYER_CONTROL not provided`) + } } public numMeshesToBeLoaded: number = 0 @@ -389,12 +444,6 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { } private loadMeshes$: ReplaySubject<{labelIndicies: number[], layer: { name: string }}> = new ReplaySubject() - private loadMeshes(labelIndicies: number[], { name }) { - this.loadMeshes$.next({ - labelIndicies, - layer: { name }, - }) - } public mouseOverSegment: number | null public mouseOverLayer: {name: string, url: string}| null @@ -656,6 +705,18 @@ export class NehubaViewerUnit implements OnInit, OnDestroy { }) } + public updateLayer(layerObj: INgLayerCtrl['update']) { + + const viewer = this.nehubaViewer.ngviewer + + for (const layerName in layerObj) { + const layer = viewer.layerManager.getLayerByName(layerName) + if (!layer) continue + const { visible } = layerObj[layerName] + layer.setVisible(!!visible) + } + } + public hideAllSeg() { if (!this.nehubaViewer) { return } Array.from(this.multiNgIdsLabelIndexMap.keys()).forEach(ngId => { diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts index 1459d4e14021804607718b0d13cffa6c1262fdc9..0686ddaaefa6239e2c35db18f7e038dab4b3c3d5 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts @@ -8,11 +8,12 @@ import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/sel import { viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions" import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors" import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util" +import { NehubaLayerControlService } from "../layerCtrl.service" import { NehubaGlueCmp } from "./nehubaViewerGlue.component" describe('> nehubaViewerGlue.component.ts', () => { let mockStore: MockStore - beforeEach( () => { + beforeEach(() => { TestBed.configureTestingModule({ imports: [ CommonModule, @@ -33,7 +34,7 @@ describe('> nehubaViewerGlue.component.ts', () => { deps: [ ClickInterceptorService ] - }, + } ] }).overrideComponent(NehubaGlueCmp, { set: { @@ -41,6 +42,9 @@ describe('> nehubaViewerGlue.component.ts', () => { templateUrl: null } }).compileComponents() + }) + + beforeEach(() => { mockStore = TestBed.inject(MockStore) mockStore.overrideSelector(ngViewerSelectorPanelMode, PANELS.FOUR_PANEL) mockStore.overrideSelector(ngViewerSelectorPanelOrder, '0123') @@ -51,6 +55,7 @@ describe('> nehubaViewerGlue.component.ts', () => { mockStore.overrideSelector(viewerStateNavigationStateSelector, null) }) + it('> can be init', () => { const fixture = TestBed.createComponent(NehubaGlueCmp) expect(fixture.componentInstance).toBeTruthy() diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 06461155be7e199c7d414e41e6df0711f2395b9c..2ac10c76f98958eb624e23b9f923b8f506e647a3 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -1,13 +1,14 @@ +// import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, ViewChild } from "@angular/core"; import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { asyncScheduler, combineLatest, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs"; -import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions"; +import { asyncScheduler, combineLatest, fromEvent, merge, Observable, of, Subject } from "rxjs"; +import { ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors"; import { debounceTime, distinctUntilChanged, filter, map, mapTo, scan, shareReplay, startWith, switchMap, switchMapTo, take, tap, throttleTime } from "rxjs/operators"; import { viewerStateAddUserLandmarks, viewerStateChangeNavigation, viewerStateMouseOverCustomLandmark, viewerStateSelectRegionWithIdDeprecated, viewerStateSetSelectedRegions, viewreStateRemoveUserLandmarks } from "src/services/state/viewerState/actions"; -import { ngViewerSelectorLayers, ngViewerSelectorClearView, ngViewerSelectorPanelOrder, ngViewerSelectorPanelMode } from "src/services/state/ngViewerState/selectors"; -import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors"; +import { ngViewerSelectorPanelOrder, ngViewerSelectorPanelMode } from "src/services/state/ngViewerState/selectors"; +import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector } from "src/services/state/viewerState/selectors"; import { serialiseParcellationRegion } from 'common/util' import { ARIA_LABELS, IDS, QUICKTOUR_DESC } from 'common/constants' import { PANELS } from "src/services/state/ngViewerState/constants"; @@ -25,17 +26,7 @@ import { IQuickTourData } from "src/ui/quickTour/constrants"; import { NehubaLayerControlService, IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service"; import { switchMapWaitFor } from "src/util/fn"; import { INavObj } from "../navigation.service"; - -interface INgLayerInterface { - name: string // displayName - source: string - mixability: string // base | mixable | nonmixable - annotation?: string // - id?: string // unique identifier - visible?: boolean - shader?: string - transform?: any -} +import { NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util"; @Component({ selector: 'iav-cmp-viewer-nehuba-glue', @@ -61,6 +52,16 @@ interface INgLayerInterface { useFactory: (layerCtrl: NehubaLayerControlService) => layerCtrl.visibleLayer$, deps: [ NehubaLayerControlService ] }, + { + provide: SET_SEGMENT_VISIBILITY, + useFactory: (layerCtrl: NehubaLayerControlService) => layerCtrl.segmentVis$, + deps: [ NehubaLayerControlService ] + }, + { + provide: NG_LAYER_CONTROL, + useFactory: (layerCtrl: NehubaLayerControlService) => layerCtrl.ngLayersController$, + deps: [ NehubaLayerControlService ] + }, NehubaLayerControlService ] }) @@ -81,9 +82,6 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A private onhoverSegments = [] private onDestroyCb: Function[] = [] private viewerUnit: NehubaViewerUnit - private ngLayersRegister: {layers: INgLayerInterface[]} = { - layers: [] - } private multiNgIdsRegionsLabelIndexMap: Map<string, Map<number, any>> @Input() @@ -222,16 +220,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A /* on selecting of new template, remove additional nglayers */ const baseLayerNames = Object.keys(tmpl.nehubaConfig.dataset.initialNgState.layers) - this.ngLayersRegister.layers - .filter(layer => baseLayerNames?.findIndex(l => l === layer.name) >= 0) - .map(l => l.name) - .forEach(layerName => { - this.store$.dispatch(ngViewerActionRemoveNgLayer({ - layer: { - name: layerName - } - })) - }) + this.layerCtrlService.removeNgLayers(baseLayerNames) } private async loadTmpl(_template: any, parcellation: any) { @@ -295,14 +284,11 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A ? null : layers[key].transform, } - this.ngLayersRegister.layers.push(layer) return layer }) + this.layerCtrlService.addNgLayer(dispatchLayers) this.newViewer$.next(true) - this.store$.dispatch(ngViewerActionAddNgLayer({ - layer: dispatchLayers - })) } @Output() @@ -400,98 +386,6 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A }) this.onDestroyCb.push(() => onhovSegSub.unsubscribe()) - /** - * subscribe to when ngLayer gets updated, and add/remove layer as necessary - */ - const addRemoveAdditionalLayerSub = this.store$.pipe( - select(ngViewerSelectorLayers), - switchMap(this.waitForNehuba.bind(this)), - ).subscribe((ngLayers: INgLayerInterface[]) => { - - const newLayers = ngLayers.filter(l => this.ngLayersRegister.layers?.findIndex(ol => ol.name === l.name) < 0) - const removeLayers = this.ngLayersRegister.layers.filter(l => ngLayers?.findIndex(nl => nl.name === l.name) < 0) - - if (newLayers?.length > 0) { - const newLayersObj: any = {} - newLayers.forEach(({ name, source, ...rest }) => newLayersObj[name] = { - ...rest, - source, - // source: getProxyUrl(source), - // ...getProxyOther({source}) - }) - - this.nehubaContainerDirective.nehubaViewerInstance.loadLayer(newLayersObj) - - /** - * previous miplementation... if nehubaViewer has not yet be instantiated, add it to the queue - * may no longer be necessary - * or implement proper queue'ing rather than ... this... half assed queue'ing - */ - // if (!this.nehubaViewer.nehubaViewer || !this.nehubaViewer.nehubaViewer.ngviewer) { - // this.nehubaViewer.initNiftiLayers.push(newLayersObj) - // } else { - // this.nehubaViewer.loadLayer(newLayersObj) - // } - this.ngLayersRegister.layers = this.ngLayersRegister.layers.concat(newLayers) - } - - if (removeLayers?.length > 0) { - removeLayers.forEach(l => { - if (this.nehubaContainerDirective.nehubaViewerInstance.removeLayer({ - name : l.name, - })) { - this.ngLayersRegister.layers = this.ngLayersRegister.layers.filter(rl => rl.name !== l.name) - } - }) - } - }) - this.onDestroyCb.push(() => addRemoveAdditionalLayerSub.unsubscribe()) - - /** - * define when shown segments should be updated - */ - const regSelectSub = combineLatest([ - /** - * selectedRegions - */ - this.store$.pipe( - select(viewerStateSelectedRegionsSelector) - ), - /** - * if layer contains non mixable layer - */ - this.store$.pipe( - select(ngViewerSelectorLayers), - map(layers => layers.findIndex(l => l.mixability === 'nonmixable') >= 0), - ), - /** - * clearviewqueue, indicating something is controlling colour map - * show all seg - */ - this.store$.pipe( - select(ngViewerSelectorClearView), - distinctUntilChanged() - ) - ]).pipe( - switchMap(this.waitForNehuba.bind(this)), - ).subscribe(([ regions, nonmixableLayerExists, clearViewFlag ]) => { - if (nonmixableLayerExists) { - this.nehubaContainerDirective.nehubaViewerInstance.hideAllSeg() - return - } - const { ngId: defaultNgId } = this.selectedParcellation || {} - - /* selectedregionindexset needs to be updated regardless of forceshowsegment */ - const selectedRegionIndexSet = new Set<string>(regions.map(({ngId = defaultNgId, labelIndex}) => serialiseParcellationRegion({ ngId, labelIndex }))) - - if (selectedRegionIndexSet.size > 0 && !clearViewFlag) { - this.nehubaContainerDirective.nehubaViewerInstance.showSegs([...selectedRegionIndexSet]) - } else { - this.nehubaContainerDirective.nehubaViewerInstance.showAllSeg() - } - }) - this.onDestroyCb.push(() => regSelectSub.unsubscribe()) - const perspectiveRenderEvSub = this.newViewer$.pipe( switchMapTo(fromEvent<CustomEvent>(this.el.nativeElement, 'perpspectiveRenderEvent').pipe( take(1) @@ -517,7 +411,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A /** * TODO dig into event detail to see if the exact mesh loaded */ - const { meshesLoaded, meshFragmentsLoaded, lastLoadedMeshId } = (event as any).detail + const { meshesLoaded, meshFragmentsLoaded: _meshFragmentsLoaded, lastLoadedMeshId: _lastLoadedMeshId } = (event as any).detail return meshesLoaded >= this.nehubaContainerDirective.nehubaViewerInstance.numMeshesToBeLoaded ? null : 'Loading meshes ...' @@ -586,7 +480,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A positionReal : typeof realSpace !== 'undefined' ? realSpace : true, }), /* TODO introduce animation */ - moveToNavigationLoc : (coord, realSpace?) => { + moveToNavigationLoc : (coord, _realSpace?) => { this.store$.dispatch( viewerStateChangeNavigation({ navigation: { @@ -828,7 +722,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A ) } - public handleMouseLeaveCustomLandmark(lm) { + public handleMouseLeaveCustomLandmark(_lm) { this.store$.dispatch( viewerStateMouseOverCustomLandmark({ payload: { userLandmark: null } diff --git a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerTouch.directive.ts b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerTouch.directive.ts index 99841bfc0ec082fc6ae98907320141e045b0f2e1..28eeda243621049da1342169ef19d70e721eb459 100644 --- a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerTouch.directive.ts +++ b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerTouch.directive.ts @@ -1,6 +1,6 @@ -import { Directive, ElementRef, Input, HostListener, Output, OnDestroy } from "@angular/core"; +import { Directive, ElementRef, Input, OnDestroy } from "@angular/core"; import { Observable, fromEvent, merge, Subscription } from "rxjs"; -import { map, filter, shareReplay, switchMap, pairwise, takeUntil, tap, switchMapTo } from "rxjs/operators"; +import { map, filter, shareReplay, switchMap, pairwise, takeUntil, switchMapTo } from "rxjs/operators"; import { getExportNehuba } from 'src/util/fn' import { computeDistance } from "../nehubaViewer/nehubaViewer.component"; diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index c0cc7d57ebd8407e9f100d921f8115a6c1f216fc..a6daa233d6acdc5197d19d4a9dbc284c3e26f329 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -179,8 +179,6 @@ export class StatusCardComponent implements OnInit, OnChanges{ public resetNavigation({rotation: rotationFlag = false, position: positionFlag = false, zoom : zoomFlag = false}: {rotation?: boolean, position?: boolean, zoom?: boolean}) { const { orientation, - perspectiveOrientation, - perspectiveZoom, position, zoom } = getNavigationStateFromConfig(this.selectedTemplatePure.nehubaConfig) diff --git a/src/viewerModule/nehuba/util.ts b/src/viewerModule/nehuba/util.ts index 3da17749f49e13881ea708a9aa5b5f831ae21f47..398ad2d82aafbbdafc6c8b8c50d72239a91ded92 100644 --- a/src/viewerModule/nehuba/util.ts +++ b/src/viewerModule/nehuba/util.ts @@ -304,9 +304,9 @@ export function cvtNavigationObjToNehubaConfig(navigationObj, nehubaConfigObj){ const { navigation = {} } = nehubaConfigObj || {} - const { pose = {}, zoomFactor = 1e6 } = navigation - const { position = {}, orientation = [0, 0, 0, 1] } = pose - const { voxelSize = [1, 1, 1], voxelCoordinates = [0, 0, 0] } = position + const { pose = {} } = navigation + const { position = {} } = pose + const { voxelSize = [1, 1, 1] } = position return voxelSize })() diff --git a/src/viewerModule/threeSurfer/module.ts b/src/viewerModule/threeSurfer/module.ts index fcc19bc37b1cb77bf04e0749534c2e8730715a96..626566404f2aa624d603184e40ee36895e80cedd 100644 --- a/src/viewerModule/threeSurfer/module.ts +++ b/src/viewerModule/threeSurfer/module.ts @@ -1,6 +1,8 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { UtilModule } from "src/util"; import { ThreeSurferGlueCmp } from "./threeSurferGlue/threeSurfer.component"; import { ThreeSurferViewerConfig } from "./tsViewerConfig/tsViewerConfig.component"; @@ -8,6 +10,8 @@ import { ThreeSurferViewerConfig } from "./tsViewerConfig/tsViewerConfig.compone imports: [ CommonModule, AngularMaterialModule, + UtilModule, + FormsModule, ], declarations: [ ThreeSurferGlueCmp, diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index 96ba31a146366dffa8868c69f0ef4b01abec1c49..47e58895e7c2260afae4cb3e0df91e3375ec1440 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -1,24 +1,69 @@ -import { Component, Input, Output, EventEmitter, ElementRef, OnChanges, OnDestroy, AfterViewInit } from "@angular/core"; +import { Component, Input, Output, EventEmitter, ElementRef, OnChanges, OnDestroy, AfterViewInit, Inject, Optional } from "@angular/core"; import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface"; import { TThreeSurferConfig, TThreeSurferMode } from "../types"; import { parseContext } from "../util"; -import { retry } from 'common/util' +import { retry, flattenRegions } from 'common/util' +import { Observable, Subject } from "rxjs"; +import { debounceTime, filter, switchMap } from "rxjs/operators"; +import { ComponentStore } from "src/viewerModule/componentStore"; +import { select, Store } from "@ngrx/store"; +import { viewerStateChangeNavigation, viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; +import { viewerStateSelectorNavigation } from "src/services/state/viewerState/selectors"; +import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; +import { REGION_OF_INTEREST } from "src/util/interfaces"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { CONST } from 'common/constants' +import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service"; +import { switchMapWaitFor } from "src/util/fn"; + +const pZoomFactor = 5e3 type THandlingCustomEv = { regions: ({ name?: string, error?: string })[] - event: CustomEvent evMesh?: { faceIndex: number verticesIndicies: number[] } } +type TCameraOrientation = { + perspectiveOrientation: [number, number, number, number] + perspectiveZoom: number +} + +const threshold = 1e-3 + +function getHemisphereKey(region: { name: string }){ + return /left/.test(region.name) + ? 'left' + : /right/.test(region.name) + ? 'right' + : null +} + +function cameraNavsAreSimilar(c1: TCameraOrientation, c2: TCameraOrientation){ + if (c1 === c2) return true + if (!!c1 && !!c2) return true + + if (!c1 && !!c2) return false + if (!c2 && !!c1) return false + + if (Math.abs(c1.perspectiveZoom - c2.perspectiveZoom) > threshold) return false + if ([0, 1, 2, 3].some( + idx => Math.abs(c1.perspectiveOrientation[idx] - c2.perspectiveOrientation[idx]) > threshold + )) { + return false + } + return true +} + @Component({ selector: 'three-surfer-glue-cmp', templateUrl: './threeSurfer.template.html', styleUrls: [ './threeSurfer.style.css' - ] + ], + providers: [ ComponentStore ] }) export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, AfterViewInit, OnDestroy { @@ -36,12 +81,233 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af private config: TThreeSurferConfig public modes: TThreeSurferMode[] = [] public selectedMode: string - private colormap: Map<string, Map<number, [number, number, number]>> = new Map() + private mainStoreCameraNav: TCameraOrientation = null + private localCameraNav: TCameraOrientation = null + + public allKeys: {name: string, checked: boolean}[] = [] + + private regionMap: Map<string, Map<number, any>> = new Map() + private mouseoverRegions = [] constructor( private el: ElementRef, + private store$: Store<any>, + private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>, + private snackbar: MatSnackBar, + @Optional() @Inject(REGION_OF_INTEREST) private roi$: Observable<any>, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, + @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle, ){ + + // set viewer handle + // the API won't be 100% compatible with ngviewer + if (setViewerHandle) { + const nyi = () => { + throw new Error(`Not yet implemented`) + } + setViewerHandle({ + add3DLandmarks: nyi, + loadLayer: nyi, + applyLayersColourMap: (map: Map<string, Map<number, { red: number, green: number, blue: number }>>) => { + const applyCm = new Map() + for (const [hem, m] of map.entries()) { + const nMap = new Map() + applyCm.set(hem, nMap) + for (const [lbl, vals] of m.entries()) { + const { red, green, blue } = vals + nMap.set(lbl, [red/255, green/255, blue/255]) + } + } + this.externalHemisphLblColorMap = applyCm + }, + getLayersSegmentColourMap: () => { + const map = this.getColormapCopy() + const outmap = new Map<string, Map<number, { red: number, green: number, blue: number }>>() + for (const [ hem, m ] of map.entries()) { + const nMap = new Map<number, {red: number, green: number, blue: number}>() + outmap.set(hem, nMap) + for (const [ lbl, vals ] of m.entries()) { + nMap.set(lbl, { + red: vals[0] * 255, + green: vals[1] * 255, + blue: vals[2] * 255, + }) + } + } + return outmap + }, + getNgHash: nyi, + hideAllSegments: nyi, + hideSegment: nyi, + mouseEvent: null, + mouseOverNehuba: null, + mouseOverNehubaLayers: null, + mouseOverNehubaUI: null, + moveToNavigationLoc: null, + moveToNavigationOri: null, + remove3DLandmarks: null, + removeLayer: null, + setLayerVisibility: null, + setNavigationLoc: null, + setNavigationOri: null, + showAllSegments: nyi, + showSegment: nyi, + }) + } + + this.onDestroyCb.push( + () => setViewerHandle(null) + ) + + if (this.roi$) { + const sub = this.roi$.pipe( + switchMap(switchMapWaitFor({ + condition: () => this.colormapLoaded + })) + ).subscribe(r => { + try { + if (!r) throw new Error(`No region selected.`) + const cmap = this.getColormapCopy() + const hemisphere = getHemisphereKey(r) + if (!hemisphere) { + this.snackbar.open(CONST.CANNOT_DECIPHER_HEMISPHERE, 'Dismiss', { + duration: 3000 + }) + throw new Error(CONST.CANNOT_DECIPHER_HEMISPHERE) + } + for (const [ hem, m ] of cmap.entries()) { + for (const lbl of m.keys()) { + if (hem !== hemisphere || lbl !== r.labelIndex) { + m.set(lbl, [1, 1, 1]) + } + } + } + this.internalHemisphLblColorMap = cmap + } catch (e) { + this.internalHemisphLblColorMap = null + } + + this.applyColorMap() + }) + this.onDestroyCb.push( + () => sub.unsubscribe() + ) + } + + /** + * intercept click and act + */ + if (clickInterceptor) { + const handleClick = (ev: MouseEvent) => { + + // if does not click inside container, ignore + if (!(this.el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) { + return true + } + + if (this.mouseoverRegions.length === 0) return true + if (this.mouseoverRegions.length > 1) { + this.snackbar.open(CONST.DOES_NOT_SUPPORT_MULTI_REGION_SELECTION, 'Dismiss', { + duration: 3000 + }) + return true + } + this.store$.dispatch( + viewerStateSetSelectedRegions({ + selectRegions: this.mouseoverRegions + }) + ) + return true + } + const { register, deregister } = clickInterceptor + register(handleClick) + this.onDestroyCb.push( + () => deregister(register) + ) + } + this.domEl = this.el.nativeElement + + /** + * subscribe to camera custom event + */ + const cameraSub = this.cameraEv$.pipe( + filter(v => !!v), + debounceTime(160) + ).subscribe(() => { + + const THREE = (window as any).ThreeSurfer.THREE + + const q = new THREE.Quaternion() + const t = new THREE.Vector3() + const s = new THREE.Vector3() + + const cameraM = this.tsRef.camera.matrix + cameraM.decompose(t, q, s) + try { + this.navStateStoreRelay.setState({ + perspectiveOrientation: q.toArray(), + perspectiveZoom: t.length() + }) + } catch (_e) { + // LockError, ignore + } + }) + + this.onDestroyCb.push( + () => cameraSub.unsubscribe() + ) + + /** + * subscribe to navstore relay store and negotiate setting global state + */ + const navStateSub = this.navStateStoreRelay.select(s => s).subscribe(v => { + this.store$.dispatch( + viewerStateChangeNavigation({ + navigation: { + position: [0, 0, 0], + orientation: [0, 0, 0, 1], + zoom: 1, + perspectiveOrientation: v.perspectiveOrientation, + perspectiveZoom: v.perspectiveZoom * pZoomFactor + } + }) + ) + }) + + this.onDestroyCb.push( + () => navStateSub.unsubscribe() + ) + + /** + * subscribe to main store and negotiate with relay to set camera + */ + const navSub = this.store$.pipe( + select(viewerStateSelectorNavigation) + ).subscribe(nav => { + const { perspectiveOrientation, perspectiveZoom } = nav + this.mainStoreCameraNav = { + perspectiveOrientation, + perspectiveZoom + } + + if (!cameraNavsAreSimilar(this.mainStoreCameraNav, this.localCameraNav)) { + this.relayStoreLock = this.navStateStoreRelay.getLock() + const THREE = (window as any).ThreeSurfer.THREE + + const cameraQuat = new THREE.Quaternion(...this.mainStoreCameraNav.perspectiveOrientation) + const cameraPos = new THREE.Vector3(0, 0, this.mainStoreCameraNav.perspectiveZoom / pZoomFactor) + cameraPos.applyQuaternion(cameraQuat) + this.toTsRef(tsRef => { + tsRef.camera.position.copy(cameraPos) + if (this.relayStoreLock) this.relayStoreLock() + }) + } + }) + + this.onDestroyCb.push( + () => navSub.unsubscribe() + ) } tsRef: any @@ -52,12 +318,33 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af hemisphere: string vIdxArr: number[] }[] = [] + private hemisphLblColorMap: Map<string, Map<number, [number, number, number]>> = new Map() + private internalHemisphLblColorMap: Map<string, Map<number, [number, number, number]>> + private externalHemisphLblColorMap: Map<string, Map<number, [number, number, number]>> + + get activeColorMap() { + if (this.externalHemisphLblColorMap) return this.externalHemisphLblColorMap + if (this.internalHemisphLblColorMap) return this.internalHemisphLblColorMap + return this.hemisphLblColorMap + } + private relayStoreLock: () => void = null + private tsRefInitCb: ((tsRef: any) => void)[] = [] + private toTsRef(callback: (tsRef: any) => void) { + if (this.tsRef) { + callback(this.tsRef) + return + } + this.tsRefInitCb.push(callback) + } private unloadAllMeshes() { + this.allKeys = [] while(this.loadedMeshes.length > 0) { const m = this.loadedMeshes.pop() this.tsRef.unloadMesh(m.threeSurfer) } + this.hemisphLblColorMap.clear() + this.colormapLoaded = false } public async loadMode(mode: TThreeSurferMode) { @@ -66,13 +353,29 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af this.selectedMode = mode.name const { meshes } = mode + await retry(async () => { + for (const singleMesh of meshes) { + const { hemisphere } = singleMesh + if (!this.regionMap.has(hemisphere)) throw new Error(`regionmap does not have hemisphere defined!`) + } + }, { + timeout: 32, + retries: 10 + }) for (const singleMesh of meshes) { const { mesh, colormap, hemisphere } = singleMesh - + this.allKeys.push({name: hemisphere, checked: true}) + const tsM = await this.tsRef.loadMesh( parseContext(mesh, [this.config['@context']]) ) - const applyCM = this.colormap.get(hemisphere) + + if (!this.regionMap.has(hemisphere)) continue + const rMap = this.regionMap.get(hemisphere) + const applyCM = new Map() + for (const [ lblIdx, region ] of rMap.entries()) { + applyCM.set(lblIdx, (region.rgb || [200, 200, 200]).map(v => v/255)) + } const tsC = await this.tsRef.loadColormap( parseContext(colormap, [this.config['@context']]) @@ -90,7 +393,44 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af hemisphere, vIdxArr: colorIdx }) - this.tsRef.applyColorMap(tsM, colorIdx, + + this.hemisphLblColorMap.set(hemisphere, applyCM) + } + this.colormapLoaded = true + this.applyColorMap() + } + + private colormapLoaded = false + + private getColormapCopy(): Map<string, Map<number, [number, number, number]>> { + const outmap = new Map() + for (const [key, value] of this.hemisphLblColorMap.entries()) { + outmap.set(key, new Map(value)) + } + return outmap + } + + /** + * TODO perhaps debounce calls to applycolormap + * so that the colormap doesn't "flick" + */ + private applyColorMap(){ + /** + * on apply color map, reset mesh visibility + * this issue is more difficult to solve than first anticiplated. + * test scenarios: + * + * 1/ hide hemisphere, select region + * 2/ hide hemisphere, select region, unhide hemisphere + * 3/ select region, hide hemisphere, deselect region + */ + for (const key of this.allKeys) { + key.checked = true + } + for (const mesh of this.loadedMeshes) { + const { hemisphere, threeSurfer, vIdxArr } = mesh + const applyCM = this.activeColorMap.get(hemisphere) + this.tsRef.applyColorMap(threeSurfer, vIdxArr, { custom: applyCM } @@ -99,7 +439,10 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af } async ngOnChanges(){ - if (this.tsRef) this.ngOnDestroy() + if (this.tsRef) { + this.ngOnDestroy() + this.ngAfterViewInit() + } if (this.selectedTemplate) { /** @@ -122,16 +465,22 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af this.tsRef.dispose() this.tsRef = null } - ) + ); + this.tsRef.control.enablePan = false + while (this.tsRefInitCb.length > 0) this.tsRefInitCb.pop()(this.tsRef) } - for (const region of this.selectedParcellation.regions) { - const map = new Map<number, [number, number, number]>() - for (const child of region.children) { - const color = (child.iav?.rgb as [number, number, number] ) || [200, 200, 200] - map.set(Number(child.grayvalue), color.map(v => v/255) as [number, number, number]) + const flattenedRegions = flattenRegions(this.selectedParcellation.regions) + for (const region of flattenedRegions) { + if (region.labelIndex) { + const hemisphere = getHemisphereKey(region) + if (!hemisphere) throw new Error(`region ${region.name} does not have hemisphere defined`) + if (!this.regionMap.has(hemisphere)) { + this.regionMap.set(hemisphere, new Map()) + } + const rMap = this.regionMap.get(hemisphere) + rMap.set(region.labelIndex, region) } - this.colormap.set(region.name, map) } // load mode0 by default @@ -144,81 +493,101 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af } } - ngAfterViewInit(){ - const customEvHandler = (ev: CustomEvent) => { - const evMesh = ev.detail?.mesh && { - faceIndex: ev.detail.mesh.faceIndex, - // typo in three-surfer - verticesIndicies: ev.detail.mesh.verticesIdicies - } - const custEv: THandlingCustomEv = { - event: ev, - regions: [], - evMesh - } - - if (!ev.detail.mesh) { - return this.handleMouseoverEvent(custEv) - } + private handleCustomMouseEv(detail: any){ + const evMesh = detail.mesh && { + faceIndex: detail.mesh.faceIndex, + // typo in three-surfer + verticesIndicies: detail.mesh.verticesIdicies + } + const custEv: THandlingCustomEv = { + regions: [], + evMesh + } + + if (!detail.mesh) { + return this.handleMouseoverEvent(custEv) + } - const evGeom = ev.detail.mesh.geometry - const evVertIdx = ev.detail.mesh.verticesIdicies - const found = this.loadedMeshes.find(({ threeSurfer }) => threeSurfer === evGeom) - - if (!found) return this.handleMouseoverEvent(custEv) + const evGeom = detail.mesh.geometry + const evVertIdx = detail.mesh.verticesIdicies + const found = this.loadedMeshes.find(({ threeSurfer }) => threeSurfer === evGeom) + if (!found) return this.handleMouseoverEvent(custEv) - const { hemisphere: key, vIdxArr } = found + /** + * check if the mesh is toggled off + * if so, do not proceed + */ + const checkKey = this.allKeys.find(key => key.name === found.hemisphere) + if (checkKey && !checkKey.checked) return - if (!key || !evVertIdx) { - return this.handleMouseoverEvent(custEv) - } + const { hemisphere: key, vIdxArr } = found - const labelIdxSet = new Set<number>() - - for (const vIdx of evVertIdx) { - labelIdxSet.add( - vIdxArr[vIdx] - ) - } - if (labelIdxSet.size === 0) { - return this.handleMouseoverEvent(custEv) - } + if (!key || !evVertIdx) { + return this.handleMouseoverEvent(custEv) + } + + const labelIdxSet = new Set<number>() + + for (const vIdx of evVertIdx) { + labelIdxSet.add( + vIdxArr[vIdx] + ) + } + if (labelIdxSet.size === 0) { + return this.handleMouseoverEvent(custEv) + } - const foundRegion = this.selectedParcellation.regions.find(({ name }) => name === key) + const hemisphereMap = this.regionMap.get(key) - if (!foundRegion) { - custEv.regions = Array.from(labelIdxSet).map(v => { + if (!hemisphereMap) { + custEv.regions = Array.from(labelIdxSet).map(v => { + return { + error: `unknown#${v}` + } + }) + return this.handleMouseoverEvent(custEv) + } + + custEv.regions = Array.from(labelIdxSet) + .map(lblIdx => { + const ontoR = hemisphereMap.get(lblIdx) + if (ontoR) { + return ontoR + } else { return { - error: `unknown#${v}` + error: `unkonwn#${lblIdx}` } - }) - return this.handleMouseoverEvent(custEv) - } + } + }) + return this.handleMouseoverEvent(custEv) - custEv.regions = Array.from(labelIdxSet) - .map(lblIdx => { - const ontoR = foundRegion.children.find(ontR => Number(ontR.grayvalue) === lblIdx) - if (ontoR) { - return ontoR - } else { - return { - error: `unkonwn#${lblIdx}` - } - } - }) - return this.handleMouseoverEvent(custEv) + } - + private cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>() + private handleCustomCameraEvent(detail: any){ + this.cameraEv$.next(detail) + } + + ngAfterViewInit(){ + const customEvHandler = (ev: CustomEvent) => { + const { type, data } = ev.detail + if (type === 'mouseover') { + return this.handleCustomMouseEv(data) + } + if (type === 'camera') { + return this.handleCustomCameraEvent(data) + } } - this.domEl.addEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME, customEvHandler) + this.domEl.addEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME_UPDATED, customEvHandler) this.onDestroyCb.push( - () => this.domEl.removeEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME, customEvHandler) + () => this.domEl.removeEventListener((window as any).ThreeSurfer.CUSTOM_EVENTNAME_UPDATED, customEvHandler) ) } public mouseoverText: string private handleMouseoverEvent(ev: THandlingCustomEv){ const { regions: mouseover, evMesh } = ev + this.mouseoverRegions = mouseover this.viewerEvent.emit({ type: EnumViewerEvt.VIEWER_CTX, data: { @@ -239,6 +608,18 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af ).join(' / ') } + public handleCheckBox(key: { name: string, checked: boolean }, flag: boolean){ + const foundMesh = this.loadedMeshes.find(m => m.hemisphere === key.name) + if (!foundMesh) { + throw new Error(`Cannot find mesh with name: ${key.name}`) + } + const meshObj = this.tsRef.customColormap.get(foundMesh.threeSurfer) + if (!meshObj) { + throw new Error(`mesh obj not found!`) + } + meshObj.mesh.visible = flag + } + private onDestroyCb: (() => void) [] = [] ngOnDestroy() { diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.style.css b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.style.css index 7f0e93fecac50602687cf91191a9dc0a93bdd54a..c5538b29efd15dbcca152e91581581b6525e90f2 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.style.css +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.style.css @@ -5,19 +5,18 @@ width: 100%; } -:host > div -{ - display: block; - height: 100%; - width: 100%; -} - -button[mat-icon-button] +.button-container { z-index: 1; position: fixed; bottom: 1rem; right: 1rem; + width: 0px; + height:0px; + + display: flex; + justify-content: flex-end; + align-items: flex-end; } span.mouseover diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html index 9189a3cc7b2ae4bcc48b0127660a58460411bcb8..56a25de0502ffc08a17a2a560cc83be60d7e7bcc 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.template.html @@ -2,14 +2,32 @@ class="mouseover iv-custom-comp text"> {{ mouseoverText }} </span> -<button mat-icon-button - color="primary" - class="pe-all" - [matMenuTriggerFor]="fsModeSelMenu"> - <i class="fas fa-bars"></i> -</button> +<div class="button-container"> + + <!-- selector & configurator --> + <button mat-icon-button + color="primary" + class="pe-all" + [matMenuTriggerFor]="fsModeSelMenu"> + <i class="fas fa-bars"></i> + </button> +</div> + + +<!-- selector/configurator menu --> <mat-menu #fsModeSelMenu="matMenu"> + + <div class="iv-custom-comp text pl-2 m-2"> + <mat-checkbox *ngFor="let key of allKeys" + class="d-block" + iav-stop="click" + (ngModelChange)="handleCheckBox(key, $event)" + [(ngModel)]="key.checked"> + {{ key.name }} + </mat-checkbox> + </div> + <mat-divider></mat-divider> <button *ngFor="let mode of modes" mat-menu-item (click)="loadMode(mode)" diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index cc6316f183c9fa034e11b46a83e18b24d563d9f0..9c684f753b226855c80fd0592f8dd234359595fe 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,28 +1,23 @@ -import { Component, ElementRef, Inject, Input, OnDestroy, Optional, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, ElementRef, Inject, Input, OnDestroy, Optional, TemplateRef, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { combineLatest, Observable, Subject, Subscription } from "rxjs"; -import { distinctUntilChanged, filter, map, startWith } from "rxjs/operators"; -import { viewerStateHelperSelectParcellationWithId, viewerStateRemoveAdditionalLayer, viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; +import {combineLatest, merge, Observable, of, Subject, Subscription} from "rxjs"; +import {catchError, distinctUntilChanged, filter, map, shareReplay, startWith, switchMap } from "rxjs/operators"; +import { viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; import { viewerStateContextedSelectedRegionsSelector, - viewerStateGetOverlayingAdditionalParcellations, - viewerStateParcVersionSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector, viewerStateStandAloneVolumes, viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors" import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' -import { ngViewerActionClearView } from "src/services/state/ngViewerState/actions"; -import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState/selectors"; -import { uiActionHideAllDatasets, uiActionHideDatasetWithId } from "src/services/state/uiState/actions"; -import { REGION_OF_INTEREST } from "src/util/interfaces"; +import { uiActionHideAllDatasets, uiActionHideDatasetWithId, uiActionShowDatasetWtihId } from "src/services/state/uiState/actions"; +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 { IViewerCmpUiState } from "../constants"; import { QuickTourThis, IQuickTourData } from "src/ui/quickTour"; import { MatDrawer } from "@angular/material/sidenav"; -import { ComponentStore } from "../componentStore"; +import { PureContantService } from "src/util"; import { EnumViewerEvt, TContextArg, TSupportedViewers, TViewerEvent } from "../viewer.interface"; import { getGetRegionFromLabelIndexId } from "src/util/fn"; import { ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; @@ -69,18 +64,49 @@ import { ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; providers: [ { provide: REGION_OF_INTEREST, - useFactory: (store: Store<any>) => store.pipe( + useFactory: (store: Store<any>, svc: PureContantService) => store.pipe( select(viewerStateContextedSelectedRegionsSelector), - map(rs => { - if (!rs[0]) return null - return rs[0] - }) + switchMap(r => { + if (!r[0]) return of(null) + const { context } = r[0] + const { atlas, template, parcellation } = context || {} + return merge( + of(null), + svc.getRegionDetail(atlas['@id'], parcellation['@id'], template['@id'], r[0]).pipe( + map(det => { + console.log('region detail', { + id: r[0], + det + }) + return { + ...det, + context + } + }), + // in case detailed requests + catchError((_err, _obs) => of(r[0])), + ) + ) + }), + shareReplay(1) ), - deps: [ - Store - ] + deps: [ Store, PureContantService ] + }, + { + provide: OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, + useFactory: (store: Store) => { + return function overwriteShowDatasetDialog( arg: { fullId?: string, name: string, description: string } ){ + if (arg.fullId) { + store.dispatch( + uiActionShowDatasetWtihId({ + id: arg.fullId + }) + ) + } + } + }, + deps: [ Store ] }, - ComponentStore ] }) @@ -104,11 +130,6 @@ export class ViewerCmp implements OnDestroy { order: 0, description: QUICKTOUR_DESC.ATLAS_SELECTOR, } - public quickTourChips: IQuickTourData = { - order: 5, - description: QUICKTOUR_DESC.CHIPS, - } - @Input() ismobile = false @@ -135,11 +156,8 @@ export class ViewerCmp implements OnDestroy { map(v => v.length > 0) ) - public viewerMode: string - public hideUi$: Observable<boolean> = this.store$.pipe( + public viewerMode$: Observable<string> = this.store$.pipe( select(viewerStateViewerModeSelector), - map(h => h === ARIA_LABELS.VIEWER_MODE_ANNOTATING), - distinctUntilChanged(), ) public useViewer$: Observable<TSupportedViewers | 'notsupported'> = combineLatest([ @@ -155,24 +173,6 @@ export class ViewerCmp implements OnDestroy { }) ) - public selectedLayerVersions$ = this.store$.pipe( - select(viewerStateParcVersionSelector), - map(arr => arr.map(item => { - const overwrittenName = item['@version'] && item['@version']['name'] - return overwrittenName - ? { ...item, displayName: overwrittenName } - : item - })) - ) - - public selectedAdditionalLayers$ = this.store$.pipe( - select(viewerStateGetOverlayingAdditionalParcellations), - ) - - public clearViewKeys$ = this.store$.pipe( - select(ngViewerSelectorClearViewEntries) - ) - /** * TODO may need to be deprecated * in favour of regional feature/data feature @@ -206,24 +206,9 @@ export class ViewerCmp implements OnDestroy { constructor( private store$: Store<any>, - private viewerCmpLocalUiStore: ComponentStore<IViewerCmpUiState>, private viewerModuleSvc: ContextMenuService<TContextArg<'threeSurfer' | 'nehuba'>>, @Optional() @Inject(REGION_OF_INTEREST) public regionOfInterest$: Observable<any> ){ - this.viewerCmpLocalUiStore.setState({ - sideNav: { - activePanelsTitle: [] - } - }) - - this.activePanelTitles$ = this.viewerCmpLocalUiStore.select( - state => state.sideNav.activePanelsTitle - ) as Observable<string[]> - this.subscriptions.push( - this.activePanelTitles$.subscribe( - (activePanelTitles: string[]) => this.activePanelTitles = activePanelTitles - ) - ) this.subscriptions.push( this.alwaysHideMinorPanel$.pipe( @@ -316,43 +301,6 @@ export class ViewerCmp implements OnDestroy { while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() } - public activePanelTitles$: Observable<string[]> - private activePanelTitles: string[] = [] - handleExpansionPanelClosedEv(title: string){ - this.viewerCmpLocalUiStore.setState({ - sideNav: { - activePanelsTitle: this.activePanelTitles.filter(n => n !== title) - } - }) - } - handleExpansionPanelAfterExpandEv(title: string){ - if (this.activePanelTitles.includes(title)) return - this.viewerCmpLocalUiStore.setState({ - sideNav: { - activePanelsTitle: [ - ...this.activePanelTitles, - title - ] - } - }) - } - - public bindFns(fns){ - return () => { - for (const [ fn, ...arg] of fns) { - fn(...arg) - } - } - } - - public clearAdditionalLayer(layer: { ['@id']: string }){ - this.store$.dispatch( - viewerStateRemoveAdditionalLayer({ - payload: layer - }) - ) - } - public selectRoi(roi: any) { this.store$.dispatch( viewerStateSetSelectedRegions({ @@ -361,22 +309,6 @@ export class ViewerCmp implements OnDestroy { ) } - public clearSelectedRegions(){ - this.store$.dispatch( - viewerStateSetSelectedRegions({ - selectRegions: [] - }) - ) - } - - public selectParcellation(parc: any) { - this.store$.dispatch( - viewerStateHelperSelectParcellationWithId({ - payload: parc - }) - ) - } - public handleChipClick(){ this.openSideNavs() } @@ -386,14 +318,7 @@ export class ViewerCmp implements OnDestroy { this.sidenavTopSwitch && this.sidenavTopSwitch.open() } - public unsetClearViewByKey(key: string){ - this.store$.dispatch( - ngViewerActionClearView({ payload: { - [key]: false - }}) - ) - } - public clearPreviewingDataset(id: string){ + public clearPreviewingDataset(id?: string){ /** * clear all preview */ diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 60c5381d55ed4e532dfe7c25550da99d5d3545b1..0d8938c15635d9856ee6ba14a3c16933a0225528 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -6,11 +6,13 @@ <layout-floating-container [zIndex]="10"> <!-- Annotation mode --> - <div *ngIf="hideUi$ | async"> + <div *ngIf="(viewerMode$ | async) === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> <mat-drawer-container class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" [hasBackdrop]="false"> - <mat-drawer #annotationDrawer + <mat-drawer #annotationDrawer="matDrawer" mode="side" + (annotation-event-directive)="annotationDrawer.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"> @@ -61,7 +63,7 @@ <!-- top drawer --> <mat-drawer-container - [hidden]="hideUi$ | async" + [hidden]="viewerMode$ | async" [iav-switch-initstate]="false" iav-switch #sideNavTopSwitch="iavSwitch" @@ -113,11 +115,11 @@ <iav-layout-fourcorners [iav-layout-fourcorners-cnr-cntr-ngclass]="{'w-100': true}"> - <!-- pullable tab top right corner --> + <!-- 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"> + <div class="flex-grow-1 d-flex flex-nowrap align-items-start"> <div *ngIf="viewerLoaded" class="pe-all tab-toggle-container" @@ -168,6 +170,7 @@ <!-- full left drawer --> <mat-drawer-container + [hidden]="viewerMode$ | async" [iav-switch-initstate]="!(alwaysHideMinorPanel$ | async)" iav-switch #sideNavFullLeftSwitch="iavSwitch" @@ -205,32 +208,32 @@ <ng-container *ngIf="iavShownDataset.shownDatasetId$ | async as shownDatasetId"> <ng-template [ngIf]="shownDatasetId.length > 0" [ngIfElse]="sideNavVolumePreview"> - <!-- single dataset side nav panel --> - <single-dataset-sidenav-view *ngFor="let id of shownDatasetId" - (clear)="clearPreviewingDataset(id)" - [fullId]="id" - class="bs-border-box ml-15px-n mr-15px-n"> - <mat-chip *ngIf="regionOfInterest$ && regionOfInterest$ | async as region" - region-of-interest - iav-region - [region]="region" - [ngClass]="{ - 'darktheme':regionDirective.rgbDarkmode === true, - 'lighttheme': regionDirective.rgbDarkmode === false - }" - [style.backgroundColor]="regionDirective.rgbString" - #regionDirective="iavRegion"> - <span class="iv-custom-comp text text-truncate d-inline"> - {{ region.name }} + <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> - </mat-chip> - </single-dataset-sidenav-view> + </button> + + <!-- ebrains region --> + <kg-regional-feature-detail + *ngFor="let datasetId of shownDatasetId" + [summary]="{'@id': datasetId}" + [region]="selectedRegions$ | async | getNthElement : 0"> + </kg-regional-feature-detail> + </div> </ng-template> </ng-container> <!-- preview volumes --> <ng-template #sideNavVolumePreview> - <ng-container *ngIf="previews.iavAdditionalLayers$ | async | filterPreviewByType : [previews.FILETYPES.VOLUMES] as volumePreviews"> + <ng-container *ngIf="previews.iavAdditionalLayers$ | async as volumePreviews"> <ng-template [ngIf]="volumePreviews.length > 0" [ngIfElse]="sidenavRegionTmpl"> <ng-container *ngFor="let vPreview of volumePreviews"> <ng-container *ngTemplateOutlet="sidenavDsPreviewTmpl; context: vPreview"> @@ -245,7 +248,7 @@ </mat-drawer> <!-- main-content --> - <mat-drawer-content class="visible position-relative" [hidden]="hideUi$ | async"> + <mat-drawer-content class="visible position-relative" [hidden]="viewerMode$ | async"> <iav-layout-fourcorners [iav-layout-fourcorners-cnr-cntr-ngclass]="{'w-100': true}"> @@ -259,31 +262,11 @@ </atlas-layer-selector> <!-- chips --> - <div *ngIf="parcellationSelected$ | async" class="flex-grow-0 p-1 flex-shrink-1 overflow-y-hidden overflow-x-auto pe-all"> - - <mat-chip-list class="d-inline-block" - quick-tour - [quick-tour-description]="quickTourChips.description" - [quick-tour-order]="quickTourChips.order"> - <!-- additional layer --> - - <ng-container> - <ng-container *ngTemplateOutlet="currParcellationTmpl; context: { addParc: (selectedAdditionalLayers$ | async), parc: (parcellationSelected$ | async) }"> - </ng-container> - </ng-container> + <div *ngIf="parcellationSelected$ | async" class="flex-grow-0 p-1 pr-2 flex-shrink-1 overflow-y-hidden overflow-x-auto pe-all"> - <!-- any selected region(s) --> - <ng-container> - <ng-container *ngTemplateOutlet="selectedRegionTmpl"> - </ng-container> - </ng-container> - - <!-- controls for iav volumes --> - <div class="hidden" iav-shown-previews #previews="iavShownPreviews"></div> - <ng-container *ngTemplateOutlet="selectedDatasetPreview; context: { layers: previews.iavAdditionalLayers$ | async | filterPreviewByType : [previews.FILETYPES.VOLUMES] }"> - </ng-container> - - </mat-chip-list> + <viewer-state-breadcrumb + (on-item-click)="handleChipClick()"> + </viewer-state-breadcrumb> </div> </div> @@ -338,245 +321,6 @@ </iav-layout-fourcorners> </ng-template> -<!-- parcellation chip / region chip --> -<ng-template #currParcellationTmpl let-parc="parc" let-addParc="addParc"> - <div [matMenuTriggerFor]="layerVersionMenu" - [matMenuTriggerData]="{ layerVersionMenuTrigger: layerVersionMenuTrigger }" - #layerVersionMenuTrigger="matMenuTrigger"> - - <ng-template [ngIf]="addParc.length > 0" [ngIfElse]="defaultParcTmpl"> - <ng-container *ngFor="let p of addParc"> - <ng-container *ngTemplateOutlet="chipTmpl; context: { - parcel: p, - selected: true, - dismissable: true, - ariaLabel: ARIA_LABELS.PARC_VER_SELECT, - onclick: layerVersionMenuTrigger.toggleMenu.bind(layerVersionMenuTrigger) - }"> - </ng-container> - </ng-container> - </ng-template> - <ng-template #defaultParcTmpl> - <ng-template [ngIf]="parc"> - - <ng-container *ngTemplateOutlet="chipTmpl; context: { - parcel: parc, - selected: false, - dismissable: false, - ariaLabel: ARIA_LABELS.PARC_VER_SELECT, - onclick: layerVersionMenuTrigger.toggleMenu.bind(layerVersionMenuTrigger) - }"> - </ng-container> - </ng-template> - </ng-template> - </div> -</ng-template> - - -<!-- layer version selector --> -<mat-menu #layerVersionMenu - class="bg-none box-shadow-none" - [aria-label]="ARIA_LABELS.PARC_VER_CONTAINER" - [hasBackdrop]="false"> - <ng-template matMenuContent let-layerVersionMenuTrigger="layerVersionMenuTrigger"> - <div (iav-outsideClick)="layerVersionMenuTrigger.closeMenu()"> - <ng-container *ngFor="let parcVer of selectedLayerVersions$ | async"> - <ng-container *ngIf="parcellationSelected$ | async as selectedParcellation"> - - <ng-container *ngTemplateOutlet="chipTmpl; context: { - parcel: parcVer, - selected: selectedParcellation['@id'] === parcVer['@id'], - dismissable: false, - class: 'w-100', - ariaLabel: parcVer.displayName || parcVer.name, - onclick: bindFns([ - [ selectParcellation.bind(this), parcVer ], - [ layerVersionMenuTrigger.closeMenu.bind(layerVersionMenuTrigger) ] - ]) - }"> - </ng-container> - </ng-container> - <div class="mt-1"></div> - </ng-container> - </div> - </ng-template> -</mat-menu> - -<!-- chip tmpl --> -<ng-template #chipTmpl - let-parcel="parcel" - let-selected="selected" - let-dismissable="dismissable" - let-chipClass="class" - let-ariaLabel="ariaLabel" - let-onclick="onclick"> - <mat-chip class="pe-all position-relative z-index-2 d-inline-flex justify-content-between" - [ngClass]="chipClass" - [attr.aria-label]="ariaLabel" - (click)="onclick && onclick()" - [selected]="selected"> - - <span class="ws-no-wrap"> - {{ parcel?.groupName ? (parcel?.groupName + ' - ') : '' }}{{ parcel && (parcel.displayName || parcel.name) }} - </span> - - <!-- info icon --> - <ng-template [ngIf]="parcel?.originDatasets?.length > 0" [ngIfElse]="infoIconBasic"> - - <mat-icon - *ngFor="let ds of parcel.originDatasets" - fontSet="fas" - fontIcon="fa-info-circle" - iav-stop="click" - iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-kgid]="ds['kgId']" - [iav-dataset-show-dataset-dialog-kgschema]="ds['kgSchema']" - [iav-dataset-show-dataset-dialog-name]="parcel?.properties?.name" - [iav-dataset-show-dataset-dialog-description]="parcel?.properties?.description"> - </mat-icon> - - </ng-template> - - <ng-template #infoIconBasic> - <mat-icon *ngIf="parcel?.properties?.name && parcel?.properties?.description" - fontSet="fas" - fontIcon="fa-info-circle" - iav-stop="click" - iav-dataset-show-dataset-dialog - [iav-dataset-show-dataset-dialog-name]="parcel.properties.name" - [iav-dataset-show-dataset-dialog-description]="parcel.properties.description"> - - </mat-icon> - </ng-template> - - <!-- dismiss icon --> - <mat-icon - *ngIf="dismissable" - (click)="clearAdditionalLayer(parcel); $event.stopPropagation()" - fontSet="fas" - fontIcon="fa-times"> - </mat-icon> - </mat-chip> -</ng-template> - - -<ng-template #selectedRegionTmpl> - - <!-- regions chip --> - <ng-template [ngIf]="selectedRegions$ | async" let-selectedRegions="ngIf"> - <!-- if regions.length > 1 --> - <!-- use group chip --> - <ng-template [ngIf]="selectedRegions.length > 1" [ngIfElse]="singleRegionChipTmpl"> - <mat-chip - color="primary" - selected - (click)="handleChipClick()" - class="pe-all position-relative z-index-1 ml-8-n"> - <span class="iv-custom-comp text text-truncate d-inline pl-4"> - {{ CONST.MULTI_REGION_SELECTION }} - </span> - <mat-icon - (click)="clearSelectedRegions()" - fontSet="fas" - iav-stop="click" - fontIcon="fa-times"> - </mat-icon> - </mat-chip> - </ng-template> - - <!-- if reginos.lengt === 1 --> - <!-- use single region chip --> - <ng-template #singleRegionChipTmpl> - <ng-container *ngFor="let r of selectedRegions"> - - <!-- region chip for discrete map --> - <mat-chip - iav-region - (click)="handleChipClick()" - [region]="r" - class="pe-all position-relative z-index-1 ml-8-n" - [ngClass]="{ - 'darktheme':regionDirective.rgbDarkmode === true, - 'lighttheme': regionDirective.rgbDarkmode === false - }" - [style.backgroundColor]="regionDirective.rgbString" - #regionDirective="iavRegion"> - <span class="iv-custom-comp text text-truncate d-inline pl-4"> - {{ r.name }} - </span> - <mat-icon - class="iv-custom-comp text" - (click)="clearSelectedRegions()" - fontSet="fas" - iav-stop="click" - fontIcon="fa-times"> - </mat-icon> - </mat-chip> - - <!-- chips for previewing origin datasets/continous map --> - <ng-container *ngFor="let originDataset of (r.originDatasets || []); let index = index"> - <div class="hidden" - iav-dataset-preview-dataset-file - [iav-dataset-preview-dataset-file-kgid]="originDataset.kgId" - [iav-dataset-preview-dataset-file-filename]="originDataset.filename" - #previewDirective="iavDatasetPreviewDatasetFile"> - </div> - <mat-chip *ngIf="previewDirective.active" - (click)="handleChipClick()" - class="pe-all position-relative ml-8-n"> - <span class="pl-4"> - {{ regionDirective.regionOriginDatasetLabels$ | async | renderViewOriginDatasetlabel : index }} - </span> - <mat-icon (click)="previewDirective.onClick()" - fontSet="fas" - iav-stop="click" - fontIcon="fa-times"> - </mat-icon> - </mat-chip> - - <mat-chip *ngFor="let key of clearViewKeys$ | async" - (click)="handleChipClick()" - class="pe-all position-relative ml-8-n"> - <span class="pl-4"> - {{ key }} - </span> - <mat-icon (click)="unsetClearViewByKey(key)" - fontSet="fas" - iav-stop="click" - fontIcon="fa-times"> - - </mat-icon> - </mat-chip> - </ng-container> - - </ng-container> - </ng-template> - </ng-template> - -</ng-template> - - -<ng-template #selectedDatasetPreview let-layers="layers"> - - <ng-container *ngFor="let layer of layers"> - <div class="hidden" - iav-dataset-preview-dataset-file - [iav-dataset-preview-dataset-file-kgid]="layer.datasetId" - [iav-dataset-preview-dataset-file-filename]="layer.filename" - #preview="iavDatasetPreviewDatasetFile"> - - </div> - <mat-chip class="pe-all" - (click)="handleChipClick()"> - {{ layer.file?.name || layer.filename || 'Unknown data preview' }} - <mat-icon fontSet="fas" fontIcon="fa-times" - (click)="preview.onClick()" - iav-stop="click"> - </mat-icon> - </mat-chip> - </ng-container> -</ng-template> - <!-- auto complete search box --> <ng-template #autocompleteTmpl let-showTour="showTour"> @@ -591,7 +335,6 @@ </div> </ng-template> - <!-- template for rendering tab --> <ng-template #tabTmpl let-isOpen="isOpen" @@ -701,9 +444,7 @@ <ng-template [ngIf]="selectedRegions.length === 1" [ngIfElse]="multiRegionWrapperTmpl"> <!-- a series of bugs result in requiring this hacky --> <!-- see https://github.com/HumanBrainProject/interactive-viewer/issues/698 --> - <ng-container *ngFor="let region of selectedRegions"> - <ng-container *ngTemplateOutlet="singleRegionTmpl; context: { region: region }"> - </ng-container> + <ng-container *ngTemplateOutlet="singleRegionTmpl; context: { region: (regionOfInterest$ | async) }"> </ng-container> </ng-template> @@ -736,178 +477,10 @@ <!-- single region tmpl --> <ng-template #singleRegionTmpl let-region="region"> <!-- region detail --> - <ng-container *ngIf="region; else regionPlaceholderTmpl"> - <region-menu - [showRegionInOtherTmpl]="false" - [region]="region" - class="bs-border-box ml-15px-n mr-15px-n mat-elevation-z4"> - </region-menu> - </ng-container> - - <!-- other region detail accordion --> - <mat-accordion *ngIf="region" - class="bs-border-box ml-15px-n mr-15px-n mt-2" - iav-region + <region-menu [region]="region" - #iavRegion="iavRegion"> - - <!-- desc --> - <ng-container *ngFor="let ods of (region.originDatasets || [])"> - <ng-template #regionDescTmpl> - <single-dataset-view - [hideTitle]="true" - [hideExplore]="true" - [hidePreview]="true" - [hidePinBtn]="true" - [hideDownloadBtn]="true" - [kgSchema]="ods.kgSchema" - [kgId]="ods.kgId"> - - </single-dataset-view> - </ng-template> - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: 'Description', - iconClass: 'fas fa-info', - iavNgIf: true, - content: regionDescTmpl - }"> - - </ng-container> - </ng-container> - - <!-- Explore in other template --> - <ng-container *ngIf="iavRegion.regionInOtherTemplates$ | async as regionInOtherTemplates"> - - <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)="iavRegion.changeView(sameRegion)" - [matTooltip]="sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '')" - mat-ripple> - <small> - {{ sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '') }} - </small> - </mat-card> - </ng-template> - - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: 'Explore in other templates', - desc: regionInOtherTemplates.length, - iconClass: 'fas fa-brain', - iconTooltip: regionInOtherTemplates.length | regionAccordionTooltipTextPipe : 'regionInOtherTmpl', - iavNgIf: regionInOtherTemplates.length, - content: exploreInOtherTmpl - }"> - - - </ng-container> - </ng-container> - - <!-- tmp experimtal --> - <ng-template #contenttmpl> - <bs-features-receptor-entry - [region]="region"> - </bs-features-receptor-entry> - </ng-template> - - <div bs-features-receptor-directive - [region]="region" - #bsFeatureReceptorDirective="bsFeatureReceptorDirective"> - </div> - <spinner-cmp *ngIf="bsFeatureReceptorDirective.fetching$ | async"></spinner-cmp> - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: 'ReceptorDistribution', - iconClass: 'fas fa-info', - iavNgIf: bsFeatureReceptorDirective.hasReceptor$ | async, - content: contenttmpl - }"> - - </ng-container> - - <!-- regional features--> - <ng-template #regionalFeaturesTmpl let-expansionPanel="expansionPanel"> - - <data-browser - *ngIf="expansionPanel.expanded" - [disableVirtualScroll]="true" - [regions]="[region]"> - </data-browser> - </ng-template> - - <div class="hidden" iav-databrowser-directive - [regions]="[region]" - #iavDbDirective="iavDatabrowserDirective"> - </div> - - <!-- if dataset is loading --> - <ng-template - [ngIf]="iavDbDirective?.fetchingFlag" - [ngIfElse]="featureLoadedTmpl"> - <div class="d-flex justify-content-center"> - <spinner-cmp></spinner-cmp> - </div> - </ng-template> - - <ng-template #featureLoadedTmpl> - - <!-- place holder content, if no regional features or connectivity or change ref space options are available --> - <ng-template [ngIf]="iavDbDirective?.dataentries?.length === 0"> - <ng-container *ngIf="parcellationSelected$ | async as selectedParcellation"> - <ng-template [ngIf]="selectedParcellation?.hasAdditionalViewMode?.includes('connectivity')"> - <div class="p-4"> - {{ CONST.NO_ADDIONTAL_INFO_AVAIL }} - </div> - </ng-template> - </ng-container> - </ng-template> - - </ng-template> - - - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: CONST.REGIONAL_FEATURES, - desc: iavDbDirective?.dataentries?.length, - iconClass: 'fas fa-database', - iconTooltip: iavDbDirective?.dataentries?.length | regionAccordionTooltipTextPipe : 'regionalFeatures', - iavNgIf: iavDbDirective?.dataentries?.length, - content: regionalFeaturesTmpl - }"> - </ng-container> - - <!-- Connectivity --> - <ng-container *ngIf="parcellationSelected$ | async as selectedParcellation"> - - <ng-template #connectivityContentTmpl let-expansionPanel="expansionPanel"> - <mat-card-content class="flex-grow-1 flex-shrink-1 w-100"> - <ng-container *ngFor="let region of selectedRegions$ | async"> - <connectivity-browser class="pe-all flex-shrink-1" - [region]="region" - [parcellationId]="selectedParcellation['@id']" - (setOpenState)="expansionPanel.expanded = $event" - [accordionExpanded]="expansionPanel.expanded" - (connectivityNumberReceived)="connectedCounterDir.value = $event"> - </connectivity-browser> - </ng-container> - </mat-card-content> - </ng-template> - - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: 'Connectivity', - desc: connectedCounterDir.value, - iconClass: 'fas fa-braille', - iconTooltip: connectedCounterDir.value | regionAccordionTooltipTextPipe : 'connectivity', - iavNgIf: selectedParcellation?.hasAdditionalViewMode?.includes('connectivity'), - content: connectivityContentTmpl - }"> - </ng-container> - - <div class="w-0 h-0" - iav-counter - #connectedCounterDir="iavCounter"> - </div> - </ng-container> - - </mat-accordion> + class="flex-grow-1 bs-border-box ml-15px-n mr-15px-n mat-elevation-z4"> + </region-menu> </ng-template> @@ -920,11 +493,8 @@ let-iavNgIf="iavNgIf" let-content="content"> <mat-expansion-panel - [expanded]="activePanelTitles$ | async | arrayContains : title" [attr.data-opened]="expansionPanel.expanded" [attr.data-mat-expansion-title]="title" - (closed)="handleExpansionPanelClosedEv(title)" - (afterExpand)="handleExpansionPanelAfterExpandEv(title)" hideToggle *ngIf="iavNgIf" #expansionPanel="matExpansionPanel"> @@ -955,15 +525,10 @@ </mat-expansion-panel> </ng-template> -<!-- TODO deprecate in favour of dedicated dataset preview side nav --> <ng-template #sidenavDsPreviewTmpl let-file="file" let-filename="filename" let-datasetId="datasetId"> <div class="w-100 flex-grow-1 d-flex flex-column"> - <preview-card class="d-block bs-border-box ml-15px-n mr-15px-n flex-grow-1" - [attr.aria-label]="ARIA_LABELS.ADDITIONAL_VOLUME_CONTROL" - [datasetId]="datasetId" - [filename]="filename"> - </preview-card> + Previewing misc dataset <!-- collapse btn --> <ng-container *ngTemplateOutlet="collapseBtn"> @@ -981,40 +546,13 @@ <ng-template #multiRegionTmpl let-regions="regions"> <ng-template [ngIf]="regions.length > 0" [ngIfElse]="regionPlaceholderTmpl"> <region-menu - [showRegionInOtherTmpl]="false" - [region]="{ - name: CONST.MULTI_REGION_SELECTION - }" + [region]="{ name: CONST.MULTI_REGION_SELECTION }" class="bs-border-box ml-15px-n mr-15px-n mat-elevation-z4"> </region-menu> <!-- other regions detail accordion --> <mat-accordion class="bs-border-box ml-15px-n mr-15px-n mt-2"> - <!-- regional features--> - <ng-template #regionalFeaturesTmpl> - <data-browser - [disableVirtualScroll]="true" - [regions]="regions"> - </data-browser> - </ng-template> - - <div class="hidden" - iav-databrowser-directive - [regions]="regions" - #iavDbDirective="iavDatabrowserDirective"> - </div> - - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: CONST.REGIONAL_FEATURES, - desc: iavDbDirective?.dataentries?.length, - iconClass: 'fas fa-database', - iconTooltip: iavDbDirective?.dataentries?.length | regionAccordionTooltipTextPipe : 'regionalFeatures', - iavNgIf: iavDbDirective?.dataentries?.length, - content: regionalFeaturesTmpl - }"> - </ng-container> - <!-- Multi regions include --> <ng-template #multiRegionInclTmpl> <mat-chip-list> @@ -1124,7 +662,7 @@ </ng-container> <ng-container *ngSwitchCase="'threeSurfer'"> - <mat-list-item> + <mat-list-item *ngIf="data?.context?.payload?.faceIndex && data?.context?.payload?.vertexIndices"> <span mat-line> face#{{ data.context.payload.faceIndex }} </span> diff --git a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed62809f3454b9cc317cc8f51cc4c1d1859cb431 --- /dev/null +++ b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.component.ts @@ -0,0 +1,115 @@ +import { Component, EventEmitter, Output } from "@angular/core"; +import { IQuickTourData } from "src/ui/quickTour"; +import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' +import { select, Store } from "@ngrx/store"; +import { viewerStateContextedSelectedRegionsSelector, viewerStateGetOverlayingAdditionalParcellations, viewerStateParcVersionSelector, viewerStateSelectedParcellationSelector } from "src/services/state/viewerState/selectors"; +import { distinctUntilChanged, map } from "rxjs/operators"; +import { viewerStateHelperSelectParcellationWithId, viewerStateRemoveAdditionalLayer, viewerStateSetSelectedRegions } from "src/services/state/viewerState.store.helper"; +import { ngViewerActionClearView, ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState.store.helper"; +import { OVERWRITE_SHOW_DATASET_DIALOG_TOKEN } from "src/util/interfaces"; + +@Component({ + selector: 'viewer-state-breadcrumb', + templateUrl: './breadcrumb.template.html', + styleUrls: [ + './breadcrumb.style.css' + ], + providers: [ + { + provide: OVERWRITE_SHOW_DATASET_DIALOG_TOKEN, + useValue: null + } + ] +}) + +export class ViewerStateBreadCrumb { + + public CONST = CONST + public ARIA_LABELS = ARIA_LABELS + + @Output('on-item-click') + onChipClick = new EventEmitter() + + public quickTourChips: IQuickTourData = { + order: 5, + description: QUICKTOUR_DESC.CHIPS, + } + + public clearViewKeys$ = this.store$.pipe( + select(ngViewerSelectorClearViewEntries) + ) + + public selectedAdditionalLayers$ = this.store$.pipe( + select(viewerStateGetOverlayingAdditionalParcellations), + ) + + public parcellationSelected$ = this.store$.pipe( + select(viewerStateSelectedParcellationSelector), + distinctUntilChanged(), + ) + + public selectedRegions$ = this.store$.pipe( + select(viewerStateContextedSelectedRegionsSelector), + distinctUntilChanged(), + ) + + public selectedLayerVersions$ = this.store$.pipe( + select(viewerStateParcVersionSelector), + map(arr => arr.map(item => { + const overwrittenName = item['@version'] && item['@version']['name'] + return overwrittenName + ? { ...item, displayName: overwrittenName } + : item + })) + ) + + + constructor(private store$: Store<any>){ + + } + + handleChipClick(){ + this.onChipClick.emit(null) + } + + public clearSelectedRegions(){ + this.store$.dispatch( + viewerStateSetSelectedRegions({ + selectRegions: [] + }) + ) + } + + public unsetClearViewByKey(key: string){ + this.store$.dispatch( + ngViewerActionClearView({ payload: { + [key]: false + }}) + ) + } + + public clearAdditionalLayer(layer: { ['@id']: string }){ + this.store$.dispatch( + viewerStateRemoveAdditionalLayer({ + payload: layer + }) + ) + } + + public selectParcellation(parc: any) { + this.store$.dispatch( + viewerStateHelperSelectParcellationWithId({ + payload: parc + }) + ) + } + + public bindFns(fns){ + return () => { + for (const [ fn, ...arg] of fns) { + fn(...arg) + } + } + } + +} \ No newline at end of file diff --git a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.style.css b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html new file mode 100644 index 0000000000000000000000000000000000000000..c36eb235ee86b398c06c47102d46f437dd0bcbb1 --- /dev/null +++ b/src/viewerModule/viewerStateBreadCrumb/breadcrumb/breadcrumb.template.html @@ -0,0 +1,206 @@ +<mat-chip-list + quick-tour + [quick-tour-description]="quickTourChips.description" + [quick-tour-order]="quickTourChips.order"> + + <!-- additional layer --> + + <ng-container> + <ng-container *ngTemplateOutlet="currParcellationTmpl; context: { + addParc: (selectedAdditionalLayers$ | async), + parc: (parcellationSelected$ | async) + }"> + </ng-container> + </ng-container> + + <!-- any selected region(s) --> + <ng-container> + <ng-container *ngTemplateOutlet="selectedRegionTmpl"> + </ng-container> + </ng-container> +</mat-chip-list> + + +<!-- parcellation chip / region chip --> +<ng-template #currParcellationTmpl let-parc="parc" let-addParc="addParc"> + <div [matMenuTriggerFor]="layerVersionMenu" + [matMenuTriggerData]="{ layerVersionMenuTrigger: layerVersionMenuTrigger }" + #layerVersionMenuTrigger="matMenuTrigger"> + + <ng-template [ngIf]="addParc.length > 0" [ngIfElse]="defaultParcTmpl"> + <ng-container *ngFor="let p of addParc"> + <ng-container *ngTemplateOutlet="chipTmpl; context: { + parcel: p, + selected: true, + dismissable: true, + ariaLabel: ARIA_LABELS.PARC_VER_SELECT, + onclick: layerVersionMenuTrigger.toggleMenu.bind(layerVersionMenuTrigger) + }"> + </ng-container> + </ng-container> + </ng-template> + <ng-template #defaultParcTmpl> + <ng-template [ngIf]="parc"> + + <ng-container *ngTemplateOutlet="chipTmpl; context: { + parcel: parc, + selected: false, + dismissable: false, + ariaLabel: ARIA_LABELS.PARC_VER_SELECT, + onclick: layerVersionMenuTrigger.toggleMenu.bind(layerVersionMenuTrigger) + }"> + </ng-container> + </ng-template> + </ng-template> + </div> +</ng-template> + + +<ng-template #selectedRegionTmpl> + + <!-- regions chip --> + <ng-template [ngIf]="selectedRegions$ | async" let-selectedRegions="ngIf"> + <!-- if regions.length > 1 --> + <!-- use group chip --> + <ng-template [ngIf]="selectedRegions.length > 1" [ngIfElse]="singleRegionChipTmpl"> + <mat-chip + color="primary" + selected + (click)="handleChipClick()" + class="pe-all position-relative z-index-1 ml-8-n"> + <span class="iv-custom-comp text text-truncate d-inline pl-4"> + {{ CONST.MULTI_REGION_SELECTION }} + </span> + <mat-icon + (click)="clearSelectedRegions()" + fontSet="fas" + iav-stop="click" + fontIcon="fa-times"> + </mat-icon> + </mat-chip> + </ng-template> + + <!-- if reginos.lengt === 1 --> + <!-- use single region chip --> + <ng-template #singleRegionChipTmpl> + <ng-container *ngFor="let r of selectedRegions"> + + <!-- region chip for discrete map --> + <mat-chip + (click)="handleChipClick()" + [region]="r" + class="pe-all position-relative z-index-1 ml-8-n" + [ngClass]="{ + 'darktheme':regionDirective.rgbDarkmode === true, + 'lighttheme': regionDirective.rgbDarkmode === false + }" + [style.backgroundColor]="regionDirective.rgbString" + iav-region + #regionDirective="iavRegion"> + <span class="iv-custom-comp text text-truncate d-inline pl-4"> + {{ r.name }} + </span> + <mat-icon + class="iv-custom-comp text" + (click)="clearSelectedRegions()" + fontSet="fas" + iav-stop="click" + fontIcon="fa-times"> + </mat-icon> + </mat-chip> + + <!-- chips for previewing origin datasets/continous map --> + <ng-container *ngFor="let originDataset of (r.originDatasets || []); let index = index"> + + <mat-chip *ngFor="let key of clearViewKeys$ | async" + (click)="handleChipClick()" + class="pe-all position-relative ml-8-n"> + <span class="pl-4"> + {{ key }} + </span> + <mat-icon (click)="unsetClearViewByKey(key)" + fontSet="fas" + iav-stop="click" + fontIcon="fa-times"> + + </mat-icon> + </mat-chip> + </ng-container> + + </ng-container> + </ng-template> + </ng-template> + +</ng-template> + +<!-- layer version selector --> +<mat-menu #layerVersionMenu + class="bg-none box-shadow-none" + [aria-label]="ARIA_LABELS.PARC_VER_CONTAINER" + [hasBackdrop]="false"> + <ng-template matMenuContent let-layerVersionMenuTrigger="layerVersionMenuTrigger"> + <div (iav-outsideClick)="layerVersionMenuTrigger.closeMenu()"> + <ng-container *ngFor="let parcVer of selectedLayerVersions$ | async"> + <ng-container *ngIf="parcellationSelected$ | async as selectedParcellation"> + + <ng-container *ngTemplateOutlet="chipTmpl; context: { + parcel: parcVer, + selected: selectedParcellation['@id'] === parcVer['@id'], + dismissable: false, + class: 'w-100', + ariaLabel: parcVer.displayName || parcVer.name, + onclick: bindFns([ + [ selectParcellation.bind(this), parcVer ], + [ layerVersionMenuTrigger.closeMenu.bind(layerVersionMenuTrigger) ] + ]) + }"> + </ng-container> + </ng-container> + <div class="mt-1"></div> + </ng-container> + </div> + </ng-template> +</mat-menu> + + +<ng-template #chipTmpl + let-parcel="parcel" + let-selected="selected" + let-dismissable="dismissable" + let-chipClass="class" + let-ariaLabel="ariaLabel" + let-onclick="onclick"> + <mat-chip class="pe-all position-relative z-index-2 d-inline-flex justify-content-between" + [ngClass]="chipClass" + [attr.aria-label]="ariaLabel" + (click)="onclick && onclick()" + [selected]="selected"> + + <span class="ws-no-wrap"> + {{ parcel?.groupName ? (parcel?.groupName + ' - ') : '' }}{{ parcel && (parcel.displayName || parcel.name) }} + </span> + + <!-- info icon --> + <ng-container *ngFor="let originDatainfo of (parcel?.originDatainfos || [])"> + + <mat-icon + fontSet="fas" + fontIcon="fa-info-circle" + iav-stop="click" + 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"> + </mat-icon> + + </ng-container> + + <!-- dismiss icon --> + <mat-icon + *ngIf="dismissable" + (click)="clearAdditionalLayer(parcel); $event.stopPropagation()" + fontSet="fas" + fontIcon="fa-times"> + </mat-icon> + </mat-chip> +</ng-template> diff --git a/src/viewerModule/viewerStateBreadCrumb/module.ts b/src/viewerModule/viewerStateBreadCrumb/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a0d4a53c86ead4983a824da0b4652cf3cabf04c --- /dev/null +++ b/src/viewerModule/viewerStateBreadCrumb/module.ts @@ -0,0 +1,29 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { ParcellationRegionModule } from "src/atlasComponents/parcellationRegion"; +import { KgDatasetModule } from "src/atlasComponents/regionalFeatures/bsFeatures/kgDataset"; +import { QuickTourModule } from "src/ui/quickTour"; +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { UtilModule } from "src/util"; +import { ViewerStateBreadCrumb } from "./breadcrumb/breadcrumb.component"; + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + QuickTourModule, + ParcellationRegionModule, + KgDatasetModule, + UtilModule, + ], + declarations: [ + ViewerStateBreadCrumb, + ], + exports: [ + ViewerStateBreadCrumb, + ], + providers:[ + ] +}) + +export class ViewerStateBreadCrumbModule{} \ No newline at end of file diff --git a/webpack/webpack.staticassets.js b/webpack/webpack.staticassets.js index d187f6b5a5a2fea0c9a79f5248835ed14ca3354b..a5d7ef3eeb12982fcb10716bf747027327e11e1b 100644 --- a/webpack/webpack.staticassets.js +++ b/webpack/webpack.staticassets.js @@ -59,15 +59,15 @@ module.exports = { }), new webpack.DefinePlugin({ - VERSION: process.env.VERSION - ? JSON.stringify(process.env.VERSION) + VERSION: process.env.VERSION + ? JSON.stringify(process.env.VERSION) : process.env.GIT_HASH ? JSON.stringify(process.env.GIT_HASH) : JSON.stringify('unspecificied hash'), PRODUCTION: !!process.env.PRODUCTION, BACKEND_URL: (process.env.BACKEND_URL && JSON.stringify(process.env.BACKEND_URL)) || 'null', DATASET_PREVIEW_URL: JSON.stringify(process.env.DATASET_PREVIEW_URL || 'https://hbp-kg-dataset-previewer.apps.hbp.eu/v2'), - BS_REST_URL: JSON.stringify(process.env.BS_REST_URL || 'https://siibra-api-tmpfullvolmetadata.apps-dev.hbp.eu/v1_0'), + BS_REST_URL: JSON.stringify(process.env.BS_REST_URL || 'https://siibra-api-latest.apps-dev.hbp.eu/v1_0'), SPATIAL_TRANSFORM_BACKEND: JSON.stringify(process.env.SPATIAL_TRANSFORM_BACKEND || 'https://hbp-spatial-backend.apps.hbp.eu'), MATOMO_URL: JSON.stringify(process.env.MATOMO_URL || null), MATOMO_ID: JSON.stringify(process.env.MATOMO_ID || null), @@ -84,4 +84,4 @@ module.exports = { '.scss' ] } -} \ No newline at end of file +}