From 671927d02a61d9c045df7f8bfda8e812afa9570b Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Tue, 30 Apr 2019 09:08:02 +0200 Subject: [PATCH] feat: migrate spatial search into the backend --- deploy/datasets/index.js | 2 + deploy/datasets/query.js | 59 ++++++++++++++-- deploy/datasets/spatialRouter.js | 47 +++++++++++++ .../databrowserModule/databrowser.service.ts | 70 +++++++++++++++++-- .../nehubaContainer.component.ts | 14 ++-- src/util/regionFlattener.ts | 2 +- 6 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 deploy/datasets/spatialRouter.js diff --git a/deploy/datasets/index.js b/deploy/datasets/index.js index a6f15dd43..01a7925bf 100644 --- a/deploy/datasets/index.js +++ b/deploy/datasets/index.js @@ -11,6 +11,8 @@ datasetsRouter.use((req, res, next) => { next() }) +datasetsRouter.use('/spatialSearch', require('./spatialRouter')) + datasetsRouter.get('/templateName/:templateName', (req, res, next) => { const { templateName } = req.params const { user } = req diff --git a/deploy/datasets/query.js b/deploy/datasets/query.js index 200d062e0..85ff46409 100644 --- a/deploy/datasets/query.js +++ b/deploy/datasets/query.js @@ -9,6 +9,7 @@ let cachedData = 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 timeout = process.env.TIMEOUT || 5000 +const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, 'data') let getPublicAccessToken @@ -84,8 +85,10 @@ const getDs = ({ user }) => user */ const flattenArray = (array) => { - return array.filter(item => item.children.length === 0).concat( - ...array.filter(item => item.children.length > 0).map(item => flattenArray(item.children)) + return array.concat( + ...array.map(item => item.children && item.children instanceof Array + ? flattenArray(item.children) + : []) ) } @@ -105,6 +108,8 @@ const readConfigFile = (filename) => new Promise((resolve, reject) => { let juBrain = null let shortBundle = null let longBundle = null +let waxholm = null +let allen = null readConfigFile('colin.json') .then(data => JSON.parse(data)) @@ -121,9 +126,16 @@ readConfigFile('MNI152.json') }) .catch(console.error) +readConfigFile('waxholmRatV2_0.json') + .then(data => JSON.parse(data)) + .then(json => { + waxholm = flattenArray(json.parcellations[0].regions) + }) + .catch(console.error) + const filterByPRs = (prs, atlasPr) => atlasPr ? prs.some(pr => { - const regex = new RegExp(pr.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')) + const regex = new RegExp(pr.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), 'i') return atlasPr.some(aPr => regex.test(aPr.name)) }) : false @@ -132,6 +144,8 @@ const manualFilter = require('./supplements/parcellation') const filter = (datasets, {templateName, parcellationName}) => datasets .filter(ds => { + if (/infant/.test(ds.name)) + return false if (templateName) { return ds.referenceSpaces.some(rs => rs.name === templateName) } @@ -139,13 +153,15 @@ const filter = (datasets, {templateName, parcellationName}) => datasets return ds.parcellationRegion.length > 0 ? filterByPRs( ds.parcellationRegion, - parcellationName === 'JuBrain Cytoarchitectonic Atlas' && juBrain && !/infant/.test(ds.name) + parcellationName === 'JuBrain Cytoarchitectonic Atlas' && juBrain ? juBrain : parcellationName === 'Fibre Bundle Atlas - Long Bundle' && longBundle ? longBundle : parcellationName === 'Fibre Bundle Atlas - Short Bundle' && shortBundle ? shortBundle - : null + : parcellationName === 'Whole Brain (v2.0)' + ? waxholm + : null ) : manualFilter({ parcellationName, dataset: ds }) } @@ -173,4 +189,35 @@ exports.init = () => fetchDatasetFromKg() exports.getDatasets = ({ templateName, parcellationName, user }) => getDs({ user }) .then(json => filter(json, {templateName, parcellationName})) -exports.getPreview = ({ datasetName }) => getPreviewFile({ datasetName }) \ No newline at end of file +exports.getPreview = ({ datasetName }) => getPreviewFile({ datasetName }) + +/** + * TODO + * change to real spatial query + */ +const cachedMap = new Map() +const fetchSpatialDataFromKg = async ({ templateName }) => { + const cachedResult = cachedMap.get(templateName) + if (cachedResult) + return cachedResult + + try { + const filename = path.join(STORAGE_PATH, templateName + '.json') + const exists = fs.existsSync(filename) + + if (!exists) + return [] + + const data = fs.readFileSync(filename, 'utf-8') + const json = JSON.parse(data) + cachedMap.set(templateName, json) + return json + } catch (e) { + console.log('datasets#query.js#fetchSpatialDataFromKg', 'read file and parse json failed', e) + return [] + } +} + +exports.getSpatialDatasets = async ({ templateName, queryGeometry, queryArg }) => { + return await fetchSpatialDataFromKg({ templateName }) +} \ No newline at end of file diff --git a/deploy/datasets/spatialRouter.js b/deploy/datasets/spatialRouter.js new file mode 100644 index 000000000..584eae6db --- /dev/null +++ b/deploy/datasets/spatialRouter.js @@ -0,0 +1,47 @@ +const router = require('express').Router() +const { getSpatialDatasets } = require('./query') + +const badRequestString = `spatialSearch endpoint uses param as follows: + +GET /templateName/<templateName>/<queryGeometry>/<queryArg> + +for example; + +GET /templateName/Colin/bbox/0_0_0__1_1_1` + +router.get('/templateName/:templateName/:queryGeometry/:queryArg', (req, res, next) => { + const { templateName, queryGeometry, queryArg } = req.params + let errorString = `` + if (!templateName) + errorString += `templateName is required\n` + if (!queryGeometry) + errorString += `queryGeometry is required\n` + if (!queryArg) + errorString += `queryArg is required\n` + if (errorString !== ``) + return next({ + code: 400, + error: errorString, + trace: 'dataset#spatialRouter' + }) + + getSpatialDatasets({ templateName, queryGeometry, queryArg }) + .then(arr => res.status(200).json(arr)) + .catch(error => { + next({ + code: 500, + error, + trace: 'dataset#spatialRouter#getSpatialDatasets' + }) + }) +}) + +router.use((req, res, next) => { + next({ + code: 400, + error: badRequestString, + trace: 'dataset#spatialRouter#notFound' + }) +}) + +module.exports = router \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser.service.ts b/src/ui/databrowserModule/databrowser.service.ts index 35adfb7c6..0d977cce1 100644 --- a/src/ui/databrowserModule/databrowser.service.ts +++ b/src/ui/databrowserModule/databrowser.service.ts @@ -1,10 +1,10 @@ import { Injectable, OnDestroy } from "@angular/core"; -import { Subscription, Observable, combineLatest, BehaviorSubject, fromEvent } from "rxjs"; +import { Subscription, Observable, combineLatest, BehaviorSubject, fromEvent, from, of } from "rxjs"; import { ViewerConfiguration } from "src/services/state/viewerConfig.store"; import { select, Store } from "@ngrx/store"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; -import { ADD_NG_LAYER, REMOVE_NG_LAYER, DataEntry, safeFilter, FETCHED_DATAENTRIES } from "src/services/stateStore.service"; -import { map, distinctUntilChanged, debounceTime, filter, tap } from "rxjs/operators"; +import { ADD_NG_LAYER, REMOVE_NG_LAYER, DataEntry, safeFilter, FETCHED_DATAENTRIES, FETCHED_SPATIAL_DATA, UPDATE_SPATIAL_DATA } from "src/services/stateStore.service"; +import { map, distinctUntilChanged, debounceTime, filter, tap, switchMap, catchError } from "rxjs/operators"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { FilterDataEntriesByRegion } from "./util/filterDataEntriesByRegion.pipe"; import { NO_METHODS } from "./util/filterDataEntriesByMethods.pipe"; @@ -14,6 +14,18 @@ import { WidgetUnit } from "src/atlasViewer/widgetUnit/widgetUnit.component"; const noMethodDisplayName = 'No methods described' +/** + * param for .toFixed method + * 6: nm + * 3: um + * 0: mm + */ +const SPATIAL_SEARCH_PRECISION = 6 +/** + * in ms + */ +const SPATIAL_SEARCH_DEBOUNCE = 500 + export function temporaryFilterDataentryName(name: string):string{ return /autoradiography/.test(name) ? 'autoradiography' @@ -57,6 +69,9 @@ export class DatabrowserService implements OnDestroy{ public fetchDataObservable$: Observable<any> public manualFetchDataset$: BehaviorSubject<null> = new BehaviorSubject(null) + private fetchSpatialData$: Observable<any> + public spatialDatasets$: Observable<any> + constructor( private workerService: AtlasWorkerService, private constantService: AtlasViewerConstantsServices, @@ -80,8 +95,42 @@ export class DatabrowserService implements OnDestroy{ }) ) + this.fetchSpatialData$ = combineLatest( + this.store.pipe( + select('viewerState'), + select('navigation') + ), + this.store.pipe( + select('viewerState'), + select('templateSelected') + ) + ).pipe( + debounceTime(SPATIAL_SEARCH_DEBOUNCE) + ) - this.fetchDataObservable$ = combineLatest( + this.spatialDatasets$ = this.fetchSpatialData$.pipe( + switchMap(([navigation, templateSelected]) => { + + /** + * templateSelected and templateSelected.name must be defined for spatial search + */ + if (!templateSelected || !templateSelected.name) + return from(Promise.reject('templateSelected must not be empty')) + const encodedTemplateName = encodeURI(templateSelected.name) + + // in mm + const center = navigation.position.map(n=>n/1e6) + const searchWidth = this.constantService.spatialWidth / 4 * navigation.zoom / 1e6 + const pt1 = center.map(v => (v - searchWidth).toFixed(SPATIAL_SEARCH_PRECISION)) + const pt2 = center.map(v => (v + searchWidth).toFixed(SPATIAL_SEARCH_PRECISION)) + + return from(fetch(`${this.constantService.backendUrl}datasets/spatialSearch/templateName/${encodedTemplateName}/bbox/${pt1.join('_')}__${pt2.join("_")}`) + .then(res => res.json())) + }), + catchError((err) => (console.log(err), of([]))) + ) + + this.fetchDataObservable$ = combineLatest( this.store.pipe( select('viewerState'), safeFilter('templateSelected'), @@ -102,6 +151,19 @@ export class DatabrowserService implements OnDestroy{ this.fetchDataObservable$ ) + this.subscriptions.push( + this.spatialDatasets$.subscribe(arr => { + this.store.dispatch({ + type: FETCHED_SPATIAL_DATA, + fetchedDataEntries: arr + }) + this.store.dispatch({ + type : UPDATE_SPATIAL_DATA, + totalResults : arr.length + }) + }) + ) + this.subscriptions.push( this.fetchDataObservable$.pipe( debounceTime(16) diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 58a0fe4c1..2275936c2 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -840,13 +840,13 @@ export class NehubaContainer implements OnInit, OnDestroy{ const center = navigation.position.map(n=>n/1e6) const searchWidth = this.constantService.spatialWidth / 4 * navigation.zoom / 1e6 - const templateSpace = this.selectedTemplate.name - this.atlasViewerDataService.spatialSearch({ - center, - searchWidth, - templateSpace, - pageNo : 0 - }) + const { selectedTemplate } = this + // this.atlasViewerDataService.spatialSearch({ + // center, + // searchWidth, + // selectedTemplate, + // pageNo : 0 + // }) } /* because the navigation can be changed from two sources, diff --git a/src/util/regionFlattener.ts b/src/util/regionFlattener.ts index 3d93fe488..d5ae471e1 100644 --- a/src/util/regionFlattener.ts +++ b/src/util/regionFlattener.ts @@ -1,6 +1,6 @@ export function regionFlattener(region:any){ return[ [ region ], - ...region.children && region.children.map && region.children.map(regionFlattener) + ...((region.children && region.children.map && region.children.map(regionFlattener)) || []) ].reduce((acc, item) => acc.concat(item), []) } \ No newline at end of file -- GitLab