Skip to content
Snippets Groups Projects
Unverified Commit 051fb2a6 authored by xgui3783's avatar xgui3783 Committed by GitHub
Browse files

Merge branch 'dev' into dev_spatial_search_from_kg

parents 7d0df31d 447459b7
No related branches found
No related tags found
No related merge requests found
Showing
with 542 additions and 135 deletions
...@@ -2,7 +2,7 @@ const express = require('express') ...@@ -2,7 +2,7 @@ const express = require('express')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
const datasetsRouter = express.Router() const datasetsRouter = express.Router()
const { init, getDatasets, getPreview } = require('./query') const { init, getDatasets, getPreview, getDatasetFromId, getDatasetFileAsZip } = require('./query')
const url = require('url') const url = require('url')
const qs = require('querystring') const qs = require('querystring')
...@@ -10,21 +10,24 @@ const bodyParser = require('body-parser') ...@@ -10,21 +10,24 @@ const bodyParser = require('body-parser')
datasetsRouter.use(bodyParser.urlencoded({ extended: false })) datasetsRouter.use(bodyParser.urlencoded({ extended: false }))
datasetsRouter.use(bodyParser.json()) datasetsRouter.use(bodyParser.json())
init().catch(e => { init().catch(e => {
console.warn(`dataset init failed`, e) console.warn(`dataset init failed`, e)
}) })
datasetsRouter.use((req, res, next) => { const cacheMaxAge24Hr = (_req, res, next) => {
res.setHeader('Cache-Control', 'no-cache') const oneDay = 24 * 60 * 60
res.setHeader('Cache-Control', `max-age=${oneDay}`)
next() next()
}) }
const noCacheMiddleWare = (_req, res, next) => {
res.setHeader('Cache-Control', 'no-cache')
next()
}
datasetsRouter.use('/spatialSearch', require('./spatialRouter')) datasetsRouter.use('/spatialSearch', noCacheMiddleWare, require('./spatialRouter'))
datasetsRouter.get('/templateName/:templateName', (req, res, next) => { datasetsRouter.get('/templateName/:templateName', noCacheMiddleWare, (req, res, next) => {
const { templateName } = req.params const { templateName } = req.params
const { user } = req const { user } = req
getDatasets({ templateName, user }) getDatasets({ templateName, user })
...@@ -40,7 +43,7 @@ datasetsRouter.get('/templateName/:templateName', (req, res, next) => { ...@@ -40,7 +43,7 @@ datasetsRouter.get('/templateName/:templateName', (req, res, next) => {
}) })
}) })
datasetsRouter.get('/parcellationName/:parcellationName', (req, res, next) => { datasetsRouter.get('/parcellationName/:parcellationName', noCacheMiddleWare, (req, res, next) => {
const { parcellationName } = req.params const { parcellationName } = req.params
const { user } = req const { user } = req
getDatasets({ parcellationName, user }) getDatasets({ parcellationName, user })
...@@ -56,7 +59,7 @@ datasetsRouter.get('/parcellationName/:parcellationName', (req, res, next) => { ...@@ -56,7 +59,7 @@ datasetsRouter.get('/parcellationName/:parcellationName', (req, res, next) => {
}) })
}) })
datasetsRouter.get('/preview/:datasetName', (req, res, next) => { datasetsRouter.get('/preview/:datasetName', cacheMaxAge24Hr, (req, res, next) => {
const { datasetName } = req.params const { datasetName } = req.params
const ref = url.parse(req.headers.referer) const ref = url.parse(req.headers.referer)
const { templateSelected, parcellationSelected } = qs.parse(ref.query) const { templateSelected, parcellationSelected } = qs.parse(ref.query)
...@@ -97,7 +100,7 @@ fs.readdir(RECEPTOR_PATH, (err, files) => { ...@@ -97,7 +100,7 @@ fs.readdir(RECEPTOR_PATH, (err, files) => {
files.forEach(file => previewFileMap.set(`res/image/receptor/${file}`, path.join(RECEPTOR_PATH, file))) files.forEach(file => previewFileMap.set(`res/image/receptor/${file}`, path.join(RECEPTOR_PATH, file)))
}) })
datasetsRouter.get('/previewFile', (req, res) => { datasetsRouter.get('/previewFile', cacheMaxAge24Hr, (req, res) => {
const { file } = req.query const { file } = req.query
const filePath = previewFileMap.get(file) const filePath = previewFileMap.get(file)
if (filePath) { if (filePath) {
...@@ -107,7 +110,37 @@ datasetsRouter.get('/previewFile', (req, res) => { ...@@ -107,7 +110,37 @@ datasetsRouter.get('/previewFile', (req, res) => {
} }
}) })
const checkKgQuery = (req, res, next) => {
const { kgSchema } = req.query
if (kgSchema !== 'minds/core/dataset/v1.0.0') return res.status(400).send('Only kgSchema is required and the only accepted value is minds/core/dataset/v1.0.0')
else return next()
}
datasetsRouter.get('/kgInfo', checkKgQuery, cacheMaxAge24Hr, async (req, res) => {
const { kgId } = req.query
const { user } = req
const stream = await getDatasetFromId({ user, kgId, returnAsStream: true })
stream.pipe(res)
})
datasetsRouter.get('/downloadKgFiles', checkKgQuery, cacheMaxAge24Hr, async (req, res) => {
const { kgId } = req.query
const { user } = req
try {
const stream = await getDatasetFileAsZip({ user, kgId })
res.setHeader('Content-Type', 'application/zip')
stream.pipe(res)
} catch (e) {
console.log('datasets/index#downloadKgFiles', e)
res.status(400).send(e)
}
})
/**
* TODO
* deprecate jszip in favour of archiver
*/
var JSZip = require("jszip"); var JSZip = require("jszip");
...@@ -140,14 +173,8 @@ datasetsRouter.post("/downloadParcellationThemself", (req,res, next) => { ...@@ -140,14 +173,8 @@ datasetsRouter.post("/downloadParcellationThemself", (req,res, next) => {
}) })
} }
zip.generateAsync({type:"base64"}) res.setHeader('Content-Type', 'application/zip')
.then(function (content) { zip.generateNodeStream().pipe(res)
// location.href="data:application/zip;base64,"+content;
res.end(content)
});
}); });
module.exports = datasetsRouter module.exports = datasetsRouter
\ No newline at end of file
File deleted
const fs = require('fs') const fs = require('fs')
const request = require('request') const request = require('request')
const URL = require('url')
const path = require('path') const path = require('path')
const archiver = require('archiver')
const { commonSenseDsFilter } = require('./supplements/commonSense') const { commonSenseDsFilter } = require('./supplements/commonSense')
const { getPreviewFile, hasPreview } = require('./supplements/previewFile') const { getPreviewFile, hasPreview } = require('./supplements/previewFile')
const { manualFilter: manualFilterDWM, manualMap: manualMapDWM } = require('./supplements/util/mapDwm') const { manualFilter: manualFilterDWM, manualMap: manualMapDWM } = require('./supplements/util/mapDwm')
...@@ -9,7 +11,25 @@ const kgQueryUtil = require('./../auth/util') ...@@ -9,7 +11,25 @@ const kgQueryUtil = require('./../auth/util')
let cachedData = null let cachedData = null
let otherQueryResult = null let otherQueryResult = null
const queryUrl = process.env.KG_DATASET_QUERY_URL || `https://kg.humanbrainproject.org/query/minds/core/dataset/v1.0.0/interactiveViewerKgQuery/instances?size=450&vocab=https%3A%2F%2Fschema.hbp.eu%2FmyQuery%2F`
const KG_ROOT = process.env.KG_ROOT || `https://kg.humanbrainproject.org`
const KG_PATH = process.env.KG_PATH || `/query/minds/core/dataset/v1.0.0/interactiveViewerKgQuery-v0_1`
const KG_PARAM = {
size: process.env.KG_SEARCH_SIZE || '450',
vocab: process.env.KG_SEARCH_VOCAB || 'https://schema.hbp.eu/myQuery/'
}
const KG_QUERY_DATASETS_URL = new URL.URL(`${KG_ROOT}${KG_PATH}/instances`)
for (let key in KG_PARAM) {
KG_QUERY_DATASETS_URL.searchParams.set(key, KG_PARAM[key])
}
const getKgQuerySingleDatasetUrl = ({ kgId }) => {
const _newUrl = new URL.URL(KG_QUERY_DATASETS_URL)
_newUrl.pathname = `${KG_PATH}/instances/${kgId}`
return _newUrl
}
const timeout = process.env.TIMEOUT || 5000 const timeout = process.env.TIMEOUT || 5000
const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, 'data') const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, 'data')
...@@ -17,10 +37,10 @@ let getPublicAccessToken ...@@ -17,10 +37,10 @@ let getPublicAccessToken
const fetchDatasetFromKg = async ({ user } = {}) => { const fetchDatasetFromKg = async ({ user } = {}) => {
const { releasedOnly, option } = await getUserKGRequestInfo({ user }) const { releasedOnly, option } = await getUserKGRequestParam({ user })
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
request(`${queryUrl}${releasedOnly ? '&databaseScope=RELEASED' : ''}`, option, (err, resp, body) => { request(`${KG_QUERY_DATASETS_URL}${releasedOnly ? '&databaseScope=RELEASED' : ''}`, option, (err, resp, body) => {
if (err) if (err)
return reject(err) return reject(err)
if (resp.statusCode >= 400) if (resp.statusCode >= 400)
...@@ -279,22 +299,30 @@ exports.getSpatialDatasets = async ({ templateName, queryGeometry, queryArg, use ...@@ -279,22 +299,30 @@ exports.getSpatialDatasets = async ({ templateName, queryGeometry, queryArg, use
return await fetchSpatialDataFromKg({ templateName, queryGeometry, queryArg, user }) return await fetchSpatialDataFromKg({ templateName, queryGeometry, queryArg, user })
} }
async function getUserKGRequestInfo({ user }) { let publicAccessToken
const accessToken = user && user.tokenset && user.tokenset.access_token
const releasedOnly = !accessToken async function getUserKGRequestParam({ user }) {
let publicAccessToken /**
if (!accessToken && getPublicAccessToken) { * n.b. ACCESS_TOKEN env var is usually only set during dev
publicAccessToken = await getPublicAccessToken() */
} const accessToken = (user && user.tokenset && user.tokenset.access_token) || process.env.ACCESS_TOKEN
const option = accessToken || publicAccessToken || process.env.ACCESS_TOKEN const releasedOnly = !accessToken
? { if (!accessToken && !publicAccessToken && getPublicAccessToken) {
auth: { publicAccessToken = await getPublicAccessToken()
'bearer': accessToken || publicAccessToken || process.env.ACCESS_TOKEN
}
} }
: {} const option = accessToken || publicAccessToken
? {
auth: {
'bearer': accessToken || publicAccessToken
}
}
: {}
return {option, releasedOnly} return {
option,
releasedOnly,
token: accessToken || publicAccessToken
}
} }
/** /**
...@@ -325,4 +353,38 @@ const transformWaxholmV2VoxelToNm = (coord) => { ...@@ -325,4 +353,38 @@ const transformWaxholmV2VoxelToNm = (coord) => {
return coord.map((v, idx) => (v * voxelDim[idx]) + transl[idx]) return coord.map((v, idx) => (v * voxelDim[idx]) + transl[idx])
} }
const defaultXform = (coord) => coord const defaultXform = (coord) => coord
\ No newline at end of file const getDatasetFromId = async ({ user, kgId, returnAsStream = false }) => {
const { option, releasedOnly } = await getUserKGRequestParam({ user })
const _url = getKgQuerySingleDatasetUrl({ kgId })
if (releasedOnly) _url.searchParams.set('databaseScope', 'RELEASED')
if (returnAsStream) return request(_url, option)
else return new Promise((resolve, reject) => {
request(_url, option, (err, resp, body) => {
if (err) return reject(err)
if (resp.statusCode >= 400) return reject(resp.statusCode)
return resolve(JSON.parse(body))
})
})
}
const getDatasetFileAsZip = async ({ user, kgId } = {}) => {
if (!kgId) {
throw new Error('kgId must be defined')
}
const result = await getDatasetFromId({ user, kgId })
const { files } = result
const zip = archiver('zip')
for (let file of files) {
const { name, absolutePath } = file
zip.append(request(absolutePath), { name })
}
zip.finalize()
return zip
}
exports.getDatasetFromId = getDatasetFromId
exports.getDatasetFileAsZip = getDatasetFileAsZip
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"archiver": "^3.0.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.16.4", "express": "^4.16.4",
"express-session": "^1.15.6", "express-session": "^1.15.6",
......
...@@ -53,7 +53,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { ...@@ -53,7 +53,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit {
@ViewChild(FixedMouseContextualContainerDirective) rClContextualMenu: FixedMouseContextualContainerDirective @ViewChild(FixedMouseContextualContainerDirective) rClContextualMenu: FixedMouseContextualContainerDirective
@ViewChild('mobileMenuTabs') mobileMenuTabs: TabsetComponent @ViewChild('mobileMenuTabs') mobileMenuTabs: TabsetComponent
@ViewChild('publications') publications: TemplateRef<any>
@ViewChild('sidenav', { read: ElementRef} ) mobileSideNav: ElementRef @ViewChild('sidenav', { read: ElementRef} ) mobileSideNav: ElementRef
/** /**
...@@ -95,8 +94,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { ...@@ -95,8 +94,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit {
public sidePanelOpen$: Observable<boolean> public sidePanelOpen$: Observable<boolean>
dismissToastHandler: any
get toggleMessage(){ get toggleMessage(){
return this.constantsService.toggleMessage return this.constantsService.toggleMessage
} }
...@@ -230,19 +227,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { ...@@ -230,19 +227,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit {
this.subscriptions.push( this.subscriptions.push(
this.selectedParcellation$.subscribe(parcellation => { this.selectedParcellation$.subscribe(parcellation => {
this.selectedParcellation = parcellation this.selectedParcellation = parcellation
if ((this.selectedParcellation['properties'] &&
(this.selectedParcellation['properties']['publications'] || this.selectedParcellation['properties']['description']))
|| (this.selectedTemplate['properties'] &&
(this.selectedTemplate['properties']['publications'] || this.selectedTemplate['properties']['description']))) {
if (this.dismissToastHandler) {
this.dismissToastHandler()
this.dismissToastHandler = null
}
this.dismissToastHandler = this.toastService.showToast(this.publications, {
timeout: 7000
})
}
}) })
) )
} }
......
import { encodeNumber, decodeToNumber } from './atlasViewer.constantService.service'
import {} from 'jasmine'
const FLOAT_PRECISION = 6
describe('encodeNumber/decodeToNumber', () => {
const getCompareOriginal = (original: number[]) => (element:string, index: number) =>
original[index].toString().length >= element.length
const lengthShortened = (original: number[], encodedString: string[]) =>
encodedString.every(getCompareOriginal(original))
it('should encode/decode positive integer as expected', () => {
const positiveInt = [
0,
1,
99999999999,
12347
]
const encodedString = positiveInt.map(n => encodeNumber(n))
const decodedString = encodedString.map(s => decodeToNumber(s))
expect(decodedString).toEqual(positiveInt)
expect(lengthShortened(positiveInt, encodedString)).toBe(true)
})
it('should encode/decode ANY positive integer as expected', () => {
const posInt = Array(1000).fill(null).map(() => {
const numDig = Math.ceil(Math.random() * 7)
return Math.floor(Math.random() * Math.pow(10, numDig))
})
const encodedString = posInt.map(n => encodeNumber(n))
const decodedNumber = encodedString.map(s => decodeToNumber(s))
expect(decodedNumber).toEqual(posInt)
expect(lengthShortened(posInt, encodedString)).toBe(true)
})
it('should encode/decode signed integer as expected', () => {
const signedInt = [
0,
-0,
-1,
1,
128,
-54
]
const encodedString = signedInt.map(n => encodeNumber(n))
const decodedNumber = encodedString.map(s => decodeToNumber(s))
/**
* -0 will be converted to 0 by the encode/decode process, but does not deep equal, according to jasmine
*/
expect(decodedNumber).toEqual(signedInt.map(v => v === 0 ? 0 : v))
expect(lengthShortened(signedInt, encodedString)).toBe(true)
})
it('should encode/decode ANY signed integer as expected', () => {
const signedInt = Array(1000).fill(null).map(() => {
const numDig = Math.ceil(Math.random() * 7)
return Math.floor(Math.random() * Math.pow(10, numDig)) * (Math.random() > 0.5 ? 1 : -1)
})
const encodedString = signedInt.map(n => encodeNumber(n))
const decodedNumber = encodedString.map(s => decodeToNumber(s))
/**
* -0 will be converted to 0 by the encode/decode process, but does not deep equal, according to jasmine
*/
expect(decodedNumber).toEqual(signedInt.map(v => v === 0 ? 0 : v))
expect(lengthShortened(signedInt, encodedString)).toBe(true)
})
it('should encode/decode float as expected', () => {
const floatNum = [
0.111,
12.23,
1723.0
]
const encodedString = floatNum.map(f => encodeNumber(f, { float: true }))
const decodedNumber = encodedString.map(s => decodeToNumber(s, { float: true }))
expect(decodedNumber.map(n => n.toFixed(FLOAT_PRECISION))).toEqual(floatNum.map(n => n.toFixed(FLOAT_PRECISION)))
})
it('should encode/decode ANY float as expected', () => {
const floatNums = Array(1000).fill(null).map(() => {
const numDig = Math.ceil(Math.random() * 7)
return (Math.random() > 0.5 ? 1 : -1) * Math.floor(
Math.random() * Math.pow(10, numDig)
)
})
const encodedString = floatNums.map(f => encodeNumber(f, { float: true }))
const decodedNumber = encodedString.map(s => decodeToNumber(s, { float: true }))
expect(floatNums.map(v => v.toFixed(FLOAT_PRECISION))).toEqual(decodedNumber.map(n => n.toFixed(FLOAT_PRECISION)))
})
})
\ No newline at end of file
...@@ -290,4 +290,98 @@ export const SUPPORT_LIBRARY_MAP : Map<string,HTMLElement> = new Map([ ...@@ -290,4 +290,98 @@ export const SUPPORT_LIBRARY_MAP : Map<string,HTMLElement> = new Map([
['vue@2.5.16',parseURLToElement('https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js')], ['vue@2.5.16',parseURLToElement('https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js')],
['preact@8.4.2',parseURLToElement('https://cdn.jsdelivr.net/npm/preact@8.4.2/dist/preact.min.js')], ['preact@8.4.2',parseURLToElement('https://cdn.jsdelivr.net/npm/preact@8.4.2/dist/preact.min.js')],
['d3@5.7.0',parseURLToElement('https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js')] ['d3@5.7.0',parseURLToElement('https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js')]
]) ])
\ No newline at end of file
/**
* First attempt at encoding int (e.g. selected region, navigation location) from number (loc info density) to b64 (higher info density)
* The constraint is that the cipher needs to be commpatible with URI encoding
* and a URI compatible separator is required.
*
* The implementation below came from
* https://stackoverflow.com/a/6573119/6059235
*
* While a faster solution exist in the same post, this operation is expected to be done:
* - once per 1 sec frequency
* - on < 1000 numbers
*
* So performance is not really that important (Also, need to learn bitwise operation)
*/
const cipher = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-'
export const separator = "."
const negString = '~'
const encodeInt = (number: number) => {
if (number % 1 !== 0) throw 'cannot encodeInt on a float. Ensure float flag is set'
if (isNaN(Number(number)) || number === null || number === Number.POSITIVE_INFINITY)
throw 'The input is not valid'
let rixit // like 'digit', only in some non-decimal radix
let residual
let result = ''
if (number < 0) {
result += negString
residual = Math.floor(number * -1)
} else {
residual = Math.floor(number)
}
while (true) {
rixit = residual % 64
// console.log("rixit : " + rixit)
// console.log("result before : " + result)
result = cipher.charAt(rixit) + result
// console.log("result after : " + result)
// console.log("residual before : " + residual)
residual = Math.floor(residual / 64)
// console.log("residual after : " + residual)
if (residual == 0)
break;
}
return result
}
interface B64EncodingOption {
float: boolean
}
const defaultB64EncodingOption = {
float: false
}
export const encodeNumber: (number:number, option?: B64EncodingOption) => string = (number: number, { float = false }: B64EncodingOption = defaultB64EncodingOption) => {
if (!float) return encodeInt(number)
else {
const floatArray = new Float32Array(1)
floatArray[0] = number
const intArray = new Uint32Array(floatArray.buffer)
const castedInt = intArray[0]
return encodeInt(castedInt)
}
}
const decodetoInt = (encodedString: string) => {
let _encodedString, negFlag = false
if (encodedString.slice(-1) === negString) {
negFlag = true
_encodedString = encodedString.slice(0, -1)
} else {
_encodedString = encodedString
}
return (negFlag ? -1 : 1) * [..._encodedString].reduce((acc,curr) => {
return acc * 64 + cipher.indexOf(curr)
}, 0)
}
export const decodeToNumber: (encodedString:string, option?: B64EncodingOption) => number = (encodedString: string, {float = false} = defaultB64EncodingOption) => {
if (!float) return decodetoInt(encodedString)
else {
const _int = decodetoInt(encodedString)
const intArray = new Uint32Array(1)
intArray[0] = _int
const castedFloat = new Float32Array(intArray.buffer)
return castedFloat[0]
}
}
\ No newline at end of file
...@@ -258,17 +258,3 @@ ...@@ -258,17 +258,3 @@
</ng-container> </ng-container>
</div> </div>
</ng-template> </ng-template>
<ng-template #publications >
<reference-toast-component *ngIf="selectedTemplate['properties'] || selectedParcellation['properties']"
[templateName] = "selectedTemplate['name']? selectedTemplate['name'] : null"
[parcellationName] = "selectedParcellation['name']? selectedParcellation['name'] : null"
[templateDescription] = "selectedTemplate['properties'] && selectedTemplate['properties']['description']? selectedTemplate['properties']['description'] : null"
[parcellationDescription] = "selectedParcellation['properties'] && selectedParcellation['properties']['description']? selectedParcellation['properties']['description'] : null"
[templatePublications] = "selectedTemplate['properties'] && selectedTemplate['properties']['publications']? selectedTemplate['properties']['publications']: null"
[parcellationPublications] = "selectedParcellation['properties'] && selectedParcellation['properties']['publications']? selectedParcellation['properties']['publications']: null"
[parcellationNifti] = "selectedParcellation['properties'] && selectedParcellation['properties']['nifti']? selectedParcellation['properties']['nifti'] : null"
[templateExternalLink] ="selectedTemplate['properties'] && selectedTemplate['properties']['externalLink']? selectedTemplate['properties']['externalLink']: null"
[parcellationExternalLink] ="selectedParcellation['properties'] && selectedParcellation['properties']['externalLink']? selectedParcellation['properties']['externalLink']: null">
</reference-toast-component>
</ng-template>
\ No newline at end of file
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Store, select } from "@ngrx/store"; import { Store, select } from "@ngrx/store";
import { ViewerStateInterface, isDefined, NEWVIEWER, CHANGE_NAVIGATION, ADD_NG_LAYER, generateLabelIndexId } from "../services/stateStore.service"; import { ViewerStateInterface, isDefined, NEWVIEWER, CHANGE_NAVIGATION, ADD_NG_LAYER } from "../services/stateStore.service";
import { PluginInitManifestInterface } from 'src/services/state/pluginState.store' import { PluginInitManifestInterface } from 'src/services/state/pluginState.store'
import { Observable,combineLatest } from "rxjs"; import { Observable,combineLatest } from "rxjs";
import { filter, map, scan, distinctUntilChanged, skipWhile, take } from "rxjs/operators"; import { filter, map, scan, distinctUntilChanged, skipWhile, take } from "rxjs/operators";
import { PluginServices } from "./atlasViewer.pluginService.service"; import { PluginServices } from "./atlasViewer.pluginService.service";
import { AtlasViewerConstantsServices } from "./atlasViewer.constantService.service"; import { AtlasViewerConstantsServices, encodeNumber, separator, decodeToNumber } from "./atlasViewer.constantService.service";
import { ToastService } from "src/services/toastService.service"; import { ToastService } from "src/services/toastService.service";
import { SELECT_REGIONS_WITH_ID } from "src/services/state/viewerState.store"; import { SELECT_REGIONS_WITH_ID } from "src/services/state/viewerState.store";
...@@ -148,6 +148,9 @@ export class AtlasViewerURLService{ ...@@ -148,6 +148,9 @@ export class AtlasViewerURLService{
/** /**
* either or both parcellationToLoad and .regions maybe empty * either or both parcellationToLoad and .regions maybe empty
*/ */
/**
* backwards compatibility
*/
const selectedRegionsParam = searchparams.get('regionsSelected') const selectedRegionsParam = searchparams.get('regionsSelected')
if(selectedRegionsParam){ if(selectedRegionsParam){
const ids = selectedRegionsParam.split('_') const ids = selectedRegionsParam.split('_')
...@@ -157,6 +160,34 @@ export class AtlasViewerURLService{ ...@@ -157,6 +160,34 @@ export class AtlasViewerURLService{
selectRegionIds: ids selectRegionIds: ids
}) })
} }
const cRegionsSelectedParam = searchparams.get('cRegionsSelected')
if (cRegionsSelectedParam) {
try {
const json = JSON.parse(cRegionsSelectedParam)
const selectRegionIds = []
for (let ngId in json) {
const val = json[ngId]
const labelIndicies = val.split(separator).map(n =>decodeToNumber(n))
for (let labelIndex of labelIndicies) {
selectRegionIds.push(`${ngId}#${labelIndex}`)
}
}
this.store.dispatch({
type: SELECT_REGIONS_WITH_ID,
selectRegionIds
})
} catch (e) {
/**
* parsing cRegionSelected error
*/
console.log('parsing cRegionSelected error', e)
}
}
} }
/* now that the parcellation is loaded, load the navigation state */ /* now that the parcellation is loaded, load the navigation state */
...@@ -175,6 +206,26 @@ export class AtlasViewerURLService{ ...@@ -175,6 +206,26 @@ export class AtlasViewerURLService{
}) })
} }
const cViewerState = searchparams.get('cNavigation')
if (cViewerState) {
const [ cO, cPO, cPZ, cP, cZ ] = cViewerState.split(`${separator}${separator}`)
const o = cO.split(separator).map(s => decodeToNumber(s, {float: true}))
const po = cPO.split(separator).map(s => decodeToNumber(s, {float: true}))
const pz = decodeToNumber(cPZ)
const p = cP.split(separator).map(s => decodeToNumber(s))
const z = decodeToNumber(cZ)
this.store.dispatch({
type : CHANGE_NAVIGATION,
navigation : {
orientation: o,
perspectiveOrientation: po,
perspectiveZoom: pz,
position: p,
zoom: z
}
})
}
const niftiLayers = searchparams.get('niftiLayers') const niftiLayers = searchparams.get('niftiLayers')
if(niftiLayers){ if(niftiLayers){
const layers = niftiLayers.split('__') const layers = niftiLayers.split('__')
...@@ -213,18 +264,56 @@ export class AtlasViewerURLService{ ...@@ -213,18 +264,56 @@ export class AtlasViewerURLService{
isDefined(state[key].position) && isDefined(state[key].position) &&
isDefined(state[key].zoom) isDefined(state[key].zoom)
){ ){
_[key] = [ const {
state[key].orientation.join('_'), orientation,
state[key].perspectiveOrientation.join('_'), perspectiveOrientation,
state[key].perspectiveZoom, perspectiveZoom,
state[key].position.join('_'), position,
state[key].zoom zoom
].join('__') } = state[key]
_['cNavigation'] = [
orientation.map(n => encodeNumber(n, {float: true})).join(separator),
perspectiveOrientation.map(n => encodeNumber(n, {float: true})).join(separator),
encodeNumber(Math.floor(perspectiveZoom)),
Array.from(position).map((v:number) => Math.floor(v)).map(n => encodeNumber(n)).join(separator),
encodeNumber(Math.floor(zoom))
].join(`${separator}${separator}`)
_[key] = null
} }
break; break;
case 'regionsSelected': case 'regionsSelected': {
_[key] = state[key].map(({ ngId, labelIndex })=> generateLabelIndexId({ ngId,labelIndex })).join('_') // _[key] = state[key].map(({ ngId, labelIndex })=> generateLabelIndexId({ ngId,labelIndex })).join('_')
const ngIdLabelIndexMap : Map<string, number[]> = state[key].reduce((acc, curr) => {
const returnMap = new Map(acc)
const { ngId, labelIndex } = curr
const existingArr = (returnMap as Map<string, number[]>).get(ngId)
if (existingArr) {
existingArr.push(labelIndex)
} else {
returnMap.set(ngId, [labelIndex])
}
return returnMap
}, new Map())
if (ngIdLabelIndexMap.size === 0) {
_['cRegionsSelected'] = null
_[key] = null
break;
}
const returnObj = {}
for (let entry of ngIdLabelIndexMap) {
const [ ngId, labelIndicies ] = entry
returnObj[ngId] = labelIndicies.map(n => encodeNumber(n)).join(separator)
}
_['cRegionsSelected'] = JSON.stringify(returnObj)
_[key] = null
break; break;
}
case 'templateSelected': case 'templateSelected':
case 'parcellationSelected': case 'parcellationSelected':
_[key] = state[key].name _[key] = state[key].name
......
import {} from 'jasmine' import {} from 'jasmine'
import { TestBed, async } from '@angular/core/testing' import { TestBed, async } from '@angular/core/testing'
import { DropdownComponent } from './dropdown.component'; import { DropdownComponent } from './dropdown.component';
import { HoverableBlockDirective } from '../hoverableBlock.directive'
import { RadioList } from '../radiolist/radiolist.component'
import { AngularMaterialModule } from '../../ui/sharedModules/angularMaterial.module'
describe('dropdown component', () => { describe('dropdown component', () => {
it('jasmine works', () => { it('jasmine works', () => {
...@@ -8,7 +11,14 @@ describe('dropdown component', () => { ...@@ -8,7 +11,14 @@ describe('dropdown component', () => {
}) })
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations : [ DropdownComponent ] imports: [
AngularMaterialModule
],
declarations : [
DropdownComponent,
HoverableBlockDirective,
RadioList
]
}).compileComponents() }).compileComponents()
})) }))
it('should create component', async(()=>{ it('should create component', async(()=>{
......
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, HostListener, ViewChild, ElementRef } from "@angular/core"; import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, HostListener, ViewChild, ElementRef } from "@angular/core";
import { dropdownAnimation } from "./dropdown.animation"; import { dropdownAnimation } from "./dropdown.animation";
import { HasExtraButtons, ExraBtnClickEvent } from '../radiolist/radiolist.component'
@Component({ @Component({
selector : 'dropdown-component', selector : 'dropdown-component',
...@@ -15,7 +16,7 @@ import { dropdownAnimation } from "./dropdown.animation"; ...@@ -15,7 +16,7 @@ import { dropdownAnimation } from "./dropdown.animation";
export class DropdownComponent{ export class DropdownComponent{
@Input() inputArray : any[] = [] @Input() inputArray : HasExtraButtons[] = []
@Input() selectedItem : any | null = null @Input() selectedItem : any | null = null
@Input() checkSelected: (selectedItem:any, item:any) => boolean = (si,i) => si === i @Input() checkSelected: (selectedItem:any, item:any) => boolean = (si,i) => si === i
...@@ -23,7 +24,7 @@ export class DropdownComponent{ ...@@ -23,7 +24,7 @@ export class DropdownComponent{
@Input() activeDisplay : (obj:any|null)=>string = (obj)=>obj ? obj.name : `Please select an item.` @Input() activeDisplay : (obj:any|null)=>string = (obj)=>obj ? obj.name : `Please select an item.`
@Output() itemSelected : EventEmitter<any> = new EventEmitter() @Output() itemSelected : EventEmitter<any> = new EventEmitter()
@Output() listItemButtonClicked: EventEmitter<any> = new EventEmitter() @Output() extraBtnClicked: EventEmitter<ExraBtnClickEvent> = new EventEmitter()
@ViewChild('dropdownToggle',{read:ElementRef}) dropdownToggle : ElementRef @ViewChild('dropdownToggle',{read:ElementRef}) dropdownToggle : ElementRef
......
...@@ -22,5 +22,5 @@ ...@@ -22,5 +22,5 @@
[selectedItem]="selectedItem" [selectedItem]="selectedItem"
[inputArray]="inputArray" [inputArray]="inputArray"
[@showState]="openState ? 'show' : 'hide'" [@showState]="openState ? 'show' : 'hide'"
(listItemButtonClicked) = listItemButtonClicked.emit($event)> (extraBtnClicked)="extraBtnClicked.emit($event)">
</radio-list> </radio-list>
...@@ -20,20 +20,40 @@ export class RadioList{ ...@@ -20,20 +20,40 @@ export class RadioList{
selectedItem: any | null = null selectedItem: any | null = null
@Input() @Input()
inputArray: any[] = [] inputArray: HasExtraButtons[] = []
@Input() @Input()
ulClass: string = '' ulClass: string = ''
@Input() checkSelected: (selectedItem:any, item:any) => boolean = (si,i) => si === i @Input() checkSelected: (selectedItem:any, item:any) => boolean = (si,i) => si === i
@Output() listItemButtonClicked = new EventEmitter<string>(); @Output() extraBtnClicked = new EventEmitter<ExraBtnClickEvent>()
clickListButton(i) { handleExtraBtnClick(extraBtn:ExtraButton, inputItem:any, event:MouseEvent){
this.listItemButtonClicked.emit(i) this.extraBtnClicked.emit({
extraBtn,
inputItem,
event
})
} }
overflowText(event) { overflowText(event) {
return (event.offsetWidth < event.scrollWidth) return (event.offsetWidth < event.scrollWidth)
} }
}
interface ExtraButton{
name: string,
faIcon: string
class?: string
}
export interface HasExtraButtons{
extraButtons?: ExtraButton[]
}
export interface ExraBtnClickEvent{
extraBtn:ExtraButton
inputItem:any
event:MouseEvent
} }
\ No newline at end of file
...@@ -59,19 +59,6 @@ ul,span.dropdown-item-1 ...@@ -59,19 +59,6 @@ ul,span.dropdown-item-1
text-overflow: ellipsis text-overflow: ellipsis
} }
.infoIcon {
margin-left: 5px;
display: inline-block;
border: 1px solid gray;
border-radius: 15px;
width: 24px;
height: 24px;
min-width: 24px;
cursor: pointer;
text-align: center;
}
:host-context([darktheme="true"]) .radioListMenu { :host-context([darktheme="true"]) .radioListMenu {
border-color: white; border-color: white;
} }
...@@ -83,6 +70,6 @@ ul,span.dropdown-item-1 ...@@ -83,6 +70,6 @@ ul,span.dropdown-item-1
border-style: solid; border-style: solid;
border-width: 0px 1px 1px 1px; border-width: 0px 1px 1px 1px;
} }
:host-context([isMobile="false"]) radioListMenu { :host-context([isMobile="false"]) .radioListMenu {
opacity: 0.8; opacity: 0.8;
} }
\ No newline at end of file
...@@ -11,15 +11,18 @@ ...@@ -11,15 +11,18 @@
(click)="itemSelected.emit({previous: selectedItem, current: input})"> (click)="itemSelected.emit({previous: selectedItem, current: input})">
<span class="dropdown-item-1 textSpan" <span class="dropdown-item-1 textSpan"
#DropDownText #DropDownText
[innerHTML] = "listDisplay(input)" [innerHTML]="listDisplay(input)"
[style.fontWeight] = "checkSelected(selectedItem, input)? 'bold' : ''" [ngClass]="checkSelected(selectedItem, input) ? 'font-weight-bold' : ''"
[matTooltip]="overflowText(DropDownText)? DropDownText.innerText: ''"> [matTooltip]="overflowText(DropDownText)? DropDownText.innerText: ''">
</span> </span>
<span *ngIf="input['properties'] && (input['properties']['publications'] || input['properties']['description'])" <ng-container *ngIf="input.extraButtons as extraButtons">
class="infoIcon align-self-end" (click)="clickListButton(i);$event.stopPropagation()"> <span *ngFor="let extraBtn of extraButtons"
i [ngClass]="extraBtn.class"
</span> (click)="handleExtraBtnClick(extraBtn, input, $event)">
<i [ngClass]="extraBtn.faIcon"></i>
</span>
</ng-container>
</li> </li>
</ul> </ul>
\ No newline at end of file
...@@ -12,6 +12,7 @@ div[container] ...@@ -12,6 +12,7 @@ div[container]
align-items: center; align-items: center;
padding : 0.3em 1em 0em 1em; padding : 0.3em 1em 0em 1em;
pointer-events: all; pointer-events: all;
max-width:80%;
} }
:host-context([darktheme="false"]) div[container] :host-context([darktheme="false"]) div[container]
...@@ -36,8 +37,10 @@ div[close] ...@@ -36,8 +37,10 @@ div[close]
{ {
display:inline-block; display:inline-block;
} }
timer-component timer-component
{ {
flex: 0 0 0.5em;
margin: 0 -1em; margin: 0 -1em;
height:0.5em; height:0.5em;
width: calc(100% + 2em); width: calc(100% + 2em);
......
<div (mouseenter) = "hover = true" (mouseleave)="hover = false" container> <div
<div message> class="d-flex flex-column m-auto"
<ng-template #messageContainer> (mouseenter)="hover = true"
(mouseleave)="hover = false"
container>
</ng-template> <!-- body -->
</div> <div class="d-flex flex-row justify-content-between align-items-start">
<div message
[innerHTML]="htmlMessage" <!-- contents -->
*ngIf = "htmlMessage"> <div message>
</div> <ng-template #messageContainer>
<div
message </ng-template>
*ngIf="message && !htmlMessage"> </div>
{{ message }} <div message
</div> [innerHTML]="htmlMessage"
<div *ngIf = "htmlMessage">
(click)="dismiss($event)" </div>
class="ml-2" <div
*ngIf="dismissable" close> message
<i class="fas fa-times"></i> *ngIf="message && !htmlMessage">
{{ message }}
</div>
<!-- dismiss btn -->
<div
(click)="dismiss($event)"
class="m-2"
*ngIf="dismissable" close>
<i class="fas fa-times"></i>
</div>
</div> </div>
<!-- timer -->
<timer-component <timer-component
class="flex-"
*ngIf="timeout > 0" *ngIf="timeout > 0"
(timerEnd)="dismissed.emit(false)" (timerEnd)="dismissed.emit(false)"
[pause]="hover" [pause]="hover"
[timeout]="timeout" [timeout]="timeout"
timer> timer>
</timer-component> </timer-component>
</div> </div>
\ No newline at end of file
...@@ -10,6 +10,7 @@ import { GetNamesPipe } from "./util/pipes/getNames.pipe"; ...@@ -10,6 +10,7 @@ import { GetNamesPipe } from "./util/pipes/getNames.pipe";
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { GetNamePipe } from "./util/pipes/getName.pipe"; import { GetNamePipe } from "./util/pipes/getName.pipe";
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module'
import { AtlasViewerDataService } from "./atlasViewer/atlasViewer.dataService.service"; import { AtlasViewerDataService } from "./atlasViewer/atlasViewer.dataService.service";
import { WidgetUnit } from "./atlasViewer/widgetUnit/widgetUnit.component"; import { WidgetUnit } from "./atlasViewer/widgetUnit/widgetUnit.component";
...@@ -38,7 +39,6 @@ import { ViewerConfiguration } from "./services/state/viewerConfig.store"; ...@@ -38,7 +39,6 @@ import { ViewerConfiguration } from "./services/state/viewerConfig.store";
import { FixedMouseContextualContainerDirective } from "./util/directives/FixedMouseContextualContainerDirective.directive"; import { FixedMouseContextualContainerDirective } from "./util/directives/FixedMouseContextualContainerDirective.directive";
import { DatabrowserService } from "./ui/databrowserModule/databrowser.service"; import { DatabrowserService } from "./ui/databrowserModule/databrowser.service";
import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe"; import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe";
import { ZipFileDownloadService } from "./services/zipFileDownload.service";
import {HttpClientModule} from "@angular/common/http"; import {HttpClientModule} from "@angular/common/http";
import { EffectsModule } from "@ngrx/effects"; import { EffectsModule } from "@ngrx/effects";
import { UseEffects } from "./services/effect/effect"; import { UseEffects } from "./services/effect/effect";
...@@ -51,6 +51,7 @@ import { UseEffects } from "./services/effect/effect"; ...@@ -51,6 +51,7 @@ import { UseEffects } from "./services/effect/effect";
ComponentsModule, ComponentsModule,
DragDropModule, DragDropModule,
UIModule, UIModule,
AngularMaterialModule,
ModalModule.forRoot(), ModalModule.forRoot(),
TooltipModule.forRoot(), TooltipModule.forRoot(),
...@@ -112,7 +113,6 @@ import { UseEffects } from "./services/effect/effect"; ...@@ -112,7 +113,6 @@ import { UseEffects } from "./services/effect/effect";
ToastService, ToastService,
AtlasWorkerService, AtlasWorkerService,
AuthService, AuthService,
ZipFileDownloadService,
/** /**
* TODO * TODO
......
...@@ -340,4 +340,9 @@ markdown-dom pre code ...@@ -340,4 +340,9 @@ markdown-dom pre code
.text-semi-transparent .text-semi-transparent
{ {
opacity: 0.5; opacity: 0.5;
}
[darktheme="true"] .card
{
background:none;
} }
\ No newline at end of file
...@@ -9,12 +9,14 @@ ...@@ -9,12 +9,14 @@
{ {
"name": "JuBrain Cytoarchitectonic Atlas", "name": "JuBrain Cytoarchitectonic Atlas",
"ngId": "jubrain v17 left", "ngId": "jubrain v17 left",
"originDatasets":[{
"kgSchema": "minds/core/dataset/v1.0.0",
"kgId": "4ac9f0bc-560d-47e0-8916-7b24da9bb0ce"
}],
"properties": { "properties": {
"version": "1.0", "version": "1.0",
"description": "not yet", "description": "not yet",
"publications": [], "publications": []
"nifti": [{"file": "jubrain-max-pmap-v22c_space-mnicolin27.nii", "size": "4400000"}],
"externalLink": "https://doi.org/10.25493/8EGG-ZAR"
}, },
"regions": [ "regions": [
{ {
...@@ -5687,8 +5689,11 @@ ...@@ -5687,8 +5689,11 @@
"surfaceParcellation": true, "surfaceParcellation": true,
"ngData": null, "ngData": null,
"name": "Fibre Bundle Atlas - Long Bundle", "name": "Fibre Bundle Atlas - Long Bundle",
"originDatasets":[{
"kgSchema": "minds/core/dataset/v1.0.0",
"kgId": "fcbb049b-edd5-4fb5-acbc-7bf8ee933e24"
}],
"properties": { "properties": {
"externalLink": "https://doi.org/10.25493/V5BH-P7P"
}, },
"regions": [ "regions": [
{ {
...@@ -5889,6 +5894,10 @@ ...@@ -5889,6 +5894,10 @@
"surfaceParcellation": true, "surfaceParcellation": true,
"ngData": null, "ngData": null,
"name": "Fibre Bundle Atlas - Short Bundle", "name": "Fibre Bundle Atlas - Short Bundle",
"originDatasets":[{
"kgSchema": "minds/core/dataset/v1.0.0",
"kgId": "f58e4425-6614-4ad9-ac26-5e946b1296cb"
}],
"regions": [ "regions": [
{ {
"name": "Left Hemisphere", "name": "Left Hemisphere",
...@@ -6919,7 +6928,7 @@ ...@@ -6919,7 +6928,7 @@
} }
], ],
"properties": { "properties": {
"name": "MNI 152", "name": "MNI 152 ICBM 2009c Nonlinear Asymmetric",
"description": "An unbiased non-linear average of multiple subjects from the MNI152 database, which provides high-spatial resolution and signal-to-noise while not being biased towards a single brain (Fonov et al., 2011). This template space is widely used as a reference space in neuroimaging. HBP provides the JuBrain probabilistic cytoarchitectonic atlas (Amunts/Zilles, 2015) as well as a probabilistic atlas of large fibre bundles (Guevara, Mangin et al., 2017) in this space." "description": "An unbiased non-linear average of multiple subjects from the MNI152 database, which provides high-spatial resolution and signal-to-noise while not being biased towards a single brain (Fonov et al., 2011). This template space is widely used as a reference space in neuroimaging. HBP provides the JuBrain probabilistic cytoarchitectonic atlas (Amunts/Zilles, 2015) as well as a probabilistic atlas of large fibre bundles (Guevara, Mangin et al., 2017) in this space."
} }
} }
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment