diff --git a/deploy/datasets/index.js b/deploy/datasets/index.js index abb1ef174225f58b62f5e9afdb3c9696589a5f05..e974366263cff811887167510658d9a63938f652 100644 --- a/deploy/datasets/index.js +++ b/deploy/datasets/index.js @@ -2,7 +2,7 @@ const express = require('express') const path = require('path') const fs = require('fs') const datasetsRouter = express.Router() -const { init, getDatasets, getPreview } = require('./query') +const { init, getDatasets, getPreview, getDatasetFromId, getDatasetFileAsZip } = require('./query') const url = require('url') const qs = require('querystring') @@ -10,21 +10,24 @@ const bodyParser = require('body-parser') datasetsRouter.use(bodyParser.urlencoded({ extended: false })) datasetsRouter.use(bodyParser.json()) - init().catch(e => { console.warn(`dataset init failed`, e) }) -datasetsRouter.use((req, res, next) => { - res.setHeader('Cache-Control', 'no-cache') +const cacheMaxAge24Hr = (_req, res, next) => { + const oneDay = 24 * 60 * 60 + res.setHeader('Cache-Control', `max-age=${oneDay}`) 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 { user } = req getDatasets({ templateName, user }) @@ -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 { user } = req getDatasets({ parcellationName, user }) @@ -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 ref = url.parse(req.headers.referer) const { templateSelected, parcellationSelected } = qs.parse(ref.query) @@ -97,7 +100,7 @@ fs.readdir(RECEPTOR_PATH, (err, files) => { 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 filePath = previewFileMap.get(file) if (filePath) { @@ -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"); @@ -140,14 +173,8 @@ datasetsRouter.post("/downloadParcellationThemself", (req,res, next) => { }) } - zip.generateAsync({type:"base64"}) - .then(function (content) { - // location.href="data:application/zip;base64,"+content; - res.end(content) - }); - - - + res.setHeader('Content-Type', 'application/zip') + zip.generateNodeStream().pipe(res) }); module.exports = datasetsRouter \ No newline at end of file diff --git a/deploy/datasets/nii/jubrain-max-pmap-v22c_space-mnicolin27.nii b/deploy/datasets/nii/jubrain-max-pmap-v22c_space-mnicolin27.nii deleted file mode 100644 index 502dcb1e31b901ea3191c86717795d91880b9e3e..0000000000000000000000000000000000000000 Binary files a/deploy/datasets/nii/jubrain-max-pmap-v22c_space-mnicolin27.nii and /dev/null differ diff --git a/deploy/datasets/query.js b/deploy/datasets/query.js index b0df2342c06065aeca75310e84db48a22571ebe3..19a4cdac963ee83c1c09c5476f0c6682e599da26 100644 --- a/deploy/datasets/query.js +++ b/deploy/datasets/query.js @@ -1,6 +1,8 @@ const fs = require('fs') const request = require('request') +const URL = require('url') const path = require('path') +const archiver = require('archiver') const { commonSenseDsFilter } = require('./supplements/commonSense') const { getPreviewFile, hasPreview } = require('./supplements/previewFile') const { manualFilter: manualFilterDWM, manualMap: manualMapDWM } = require('./supplements/util/mapDwm') @@ -9,7 +11,25 @@ const kgQueryUtil = require('./../auth/util') 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 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 STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, 'data') @@ -17,10 +37,10 @@ let getPublicAccessToken const fetchDatasetFromKg = async ({ user } = {}) => { - const { releasedOnly, option } = await getUserKGRequestInfo({ user }) + const { releasedOnly, option } = await getUserKGRequestParam({ user }) 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) return reject(err) if (resp.statusCode >= 400) @@ -238,7 +258,7 @@ function filterByqueryArg(cubeDots) { async function getSpatialSearchOk({ user, boundingBoxInWaxhomV2VoxelSpace }) { - const { releasedOnly, option } = await getUserKGRequestInfo({ user }) + const { releasedOnly, option } = await getUserKGRequestParam({ user }) const spatialQuery = 'https://kg.humanbrainproject.org/query/minds/core/dataset/v1.0.0/spatialSimple/instances?size=10' @@ -254,22 +274,30 @@ async function getSpatialSearchOk({ user, boundingBoxInWaxhomV2VoxelSpace }) { }) } -async function getUserKGRequestInfo({ user }) { - const accessToken = user && user.tokenset && user.tokenset.access_token +let publicAccessToken + +async function getUserKGRequestParam({ user }) { + /** + * n.b. ACCESS_TOKEN env var is usually only set during dev + */ + const accessToken = (user && user.tokenset && user.tokenset.access_token) || process.env.ACCESS_TOKEN const releasedOnly = !accessToken - let publicAccessToken - if (!accessToken && getPublicAccessToken) { + if (!accessToken && !publicAccessToken && getPublicAccessToken) { publicAccessToken = await getPublicAccessToken() } - const option = accessToken || publicAccessToken || process.env.ACCESS_TOKEN + const option = accessToken || publicAccessToken ? { auth: { - 'bearer': accessToken || publicAccessToken || process.env.ACCESS_TOKEN + 'bearer': accessToken || publicAccessToken } } : {} - return {option, releasedOnly, token: accessToken || publicAccessToken || process.env.ACCESS_TOKEN} + return { + option, + releasedOnly, + token: accessToken || publicAccessToken + } } /** @@ -293,4 +321,37 @@ const transformWaxholmV2NmToVoxel = (coord) => { return coord.map((v, idx) => (v - transl[idx]) / voxelDim[idx] ) } - +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 \ No newline at end of file diff --git a/deploy/package.json b/deploy/package.json index 72d3c417e28cb9541e4f1c1b0d947875dcda953b..a1d2f4121597df333198cedf3a0be87749356fa2 100644 --- a/deploy/package.json +++ b/deploy/package.json @@ -13,6 +13,7 @@ "author": "", "license": "ISC", "dependencies": { + "archiver": "^3.0.0", "body-parser": "^1.19.0", "express": "^4.16.4", "express-session": "^1.15.6", diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 628387b0a1a1054db5b5d5316f93e54764309060..954559f4d3a8d232f5732b4cbe7c128e890a443f 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -53,7 +53,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @ViewChild(FixedMouseContextualContainerDirective) rClContextualMenu: FixedMouseContextualContainerDirective @ViewChild('mobileMenuTabs') mobileMenuTabs: TabsetComponent - @ViewChild('publications') publications: TemplateRef<any> @ViewChild('sidenav', { read: ElementRef} ) mobileSideNav: ElementRef /** @@ -95,8 +94,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public sidePanelOpen$: Observable<boolean> - dismissToastHandler: any - get toggleMessage(){ return this.constantsService.toggleMessage } @@ -230,19 +227,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.subscriptions.push( this.selectedParcellation$.subscribe(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 - }) - } }) ) } diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 8c1054a713c72dfeac5ea24010bbb0cfe3751d3b..5f736ff590d58eb9df5d126cdbd1793914432b11 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -258,17 +258,3 @@ </ng-container> </div> </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 diff --git a/src/components/dropdown/dropdown.component.ts b/src/components/dropdown/dropdown.component.ts index 78ca10f1e31f217ebb9bbc25f79a76ab9ed540c6..b083c422a10e3895df87642aa9f80832431d7623 100644 --- a/src/components/dropdown/dropdown.component.ts +++ b/src/components/dropdown/dropdown.component.ts @@ -1,5 +1,6 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, HostListener, ViewChild, ElementRef } from "@angular/core"; import { dropdownAnimation } from "./dropdown.animation"; +import { HasExtraButtons, ExraBtnClickEvent } from '../radiolist/radiolist.component' @Component({ selector : 'dropdown-component', @@ -15,7 +16,7 @@ import { dropdownAnimation } from "./dropdown.animation"; export class DropdownComponent{ - @Input() inputArray : any[] = [] + @Input() inputArray : HasExtraButtons[] = [] @Input() selectedItem : any | null = null @Input() checkSelected: (selectedItem:any, item:any) => boolean = (si,i) => si === i @@ -23,7 +24,7 @@ export class DropdownComponent{ @Input() activeDisplay : (obj:any|null)=>string = (obj)=>obj ? obj.name : `Please select an item.` @Output() itemSelected : EventEmitter<any> = new EventEmitter() - @Output() listItemButtonClicked: EventEmitter<any> = new EventEmitter() + @Output() extraBtnClicked: EventEmitter<ExraBtnClickEvent> = new EventEmitter() @ViewChild('dropdownToggle',{read:ElementRef}) dropdownToggle : ElementRef diff --git a/src/components/dropdown/dropdown.template.html b/src/components/dropdown/dropdown.template.html index 0f5866d162c27ff2c8dd284985bf73ac640c05ab..7476c7408c1f340d1e10ecfa4c984af1bacc0f4e 100644 --- a/src/components/dropdown/dropdown.template.html +++ b/src/components/dropdown/dropdown.template.html @@ -22,5 +22,5 @@ [selectedItem]="selectedItem" [inputArray]="inputArray" [@showState]="openState ? 'show' : 'hide'" - (listItemButtonClicked) = listItemButtonClicked.emit($event)> + (extraBtnClicked)="extraBtnClicked.emit($event)"> </radio-list> diff --git a/src/components/radiolist/radiolist.component.ts b/src/components/radiolist/radiolist.component.ts index 7b0314f06bb7be2d6230e385465a1a31b216ded0..f1d3558baa125f695941b3a6153ce50945def293 100644 --- a/src/components/radiolist/radiolist.component.ts +++ b/src/components/radiolist/radiolist.component.ts @@ -20,20 +20,40 @@ export class RadioList{ selectedItem: any | null = null @Input() - inputArray: any[] = [] + inputArray: HasExtraButtons[] = [] @Input() ulClass: string = '' @Input() checkSelected: (selectedItem:any, item:any) => boolean = (si,i) => si === i - @Output() listItemButtonClicked = new EventEmitter<string>(); + @Output() extraBtnClicked = new EventEmitter<ExraBtnClickEvent>() - clickListButton(i) { - this.listItemButtonClicked.emit(i) + handleExtraBtnClick(extraBtn:ExtraButton, inputItem:any, event:MouseEvent){ + this.extraBtnClicked.emit({ + extraBtn, + inputItem, + event + }) } overflowText(event) { 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 diff --git a/src/components/radiolist/radiolist.style.css b/src/components/radiolist/radiolist.style.css index c6ee787207299fff88b50d856d84dcfb0fd0621d..2d8352e629901669d33dd8df685d0d4eaf0549be 100644 --- a/src/components/radiolist/radiolist.style.css +++ b/src/components/radiolist/radiolist.style.css @@ -59,19 +59,6 @@ ul,span.dropdown-item-1 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 { border-color: white; } @@ -83,6 +70,6 @@ ul,span.dropdown-item-1 border-style: solid; border-width: 0px 1px 1px 1px; } -:host-context([isMobile="false"]) radioListMenu { +:host-context([isMobile="false"]) .radioListMenu { opacity: 0.8; } \ No newline at end of file diff --git a/src/components/radiolist/radiolist.template.html b/src/components/radiolist/radiolist.template.html index eb1d111ba65c6fef07f5e135905ea1af38621c20..d9b7730a761a8073f387af13165462bbb88ce45f 100644 --- a/src/components/radiolist/radiolist.template.html +++ b/src/components/radiolist/radiolist.template.html @@ -11,15 +11,18 @@ (click)="itemSelected.emit({previous: selectedItem, current: input})"> <span class="dropdown-item-1 textSpan" - #DropDownText - [innerHTML] = "listDisplay(input)" - [style.fontWeight] = "checkSelected(selectedItem, input)? 'bold' : ''" - [matTooltip]="overflowText(DropDownText)? DropDownText.innerText: ''"> + #DropDownText + [innerHTML]="listDisplay(input)" + [ngClass]="checkSelected(selectedItem, input) ? 'font-weight-bold' : ''" + [matTooltip]="overflowText(DropDownText)? DropDownText.innerText: ''"> </span> - <span *ngIf="input['properties'] && (input['properties']['publications'] || input['properties']['description'])" - class="infoIcon align-self-end" (click)="clickListButton(i);$event.stopPropagation()"> - i - </span> + <ng-container *ngIf="input.extraButtons as extraButtons"> + <span *ngFor="let extraBtn of extraButtons" + [ngClass]="extraBtn.class" + (click)="handleExtraBtnClick(extraBtn, input, $event)"> + <i [ngClass]="extraBtn.faIcon"></i> + </span> + </ng-container> </li> </ul> \ No newline at end of file diff --git a/src/components/toast/toast.style.css b/src/components/toast/toast.style.css index 50624d5c59826c29247dcbfa9f6286229993120a..e39cb38ee456c759dfd5dbe029d11a8c29a37407 100644 --- a/src/components/toast/toast.style.css +++ b/src/components/toast/toast.style.css @@ -12,6 +12,7 @@ div[container] align-items: center; padding : 0.3em 1em 0em 1em; pointer-events: all; + max-width:80%; } :host-context([darktheme="false"]) div[container] @@ -36,8 +37,10 @@ div[close] { display:inline-block; } + timer-component { + flex: 0 0 0.5em; margin: 0 -1em; height:0.5em; width: calc(100% + 2em); diff --git a/src/components/toast/toast.template.html b/src/components/toast/toast.template.html index 95032c74b9fbc154f0bd1e7aa9542f3453bf4fa0..7d1eb664845b73687c1be2bf1cdc110b789138ea 100644 --- a/src/components/toast/toast.template.html +++ b/src/components/toast/toast.template.html @@ -1,29 +1,45 @@ -<div (mouseenter) = "hover = true" (mouseleave)="hover = false" container> - <div message> - <ng-template #messageContainer> +<div + class="d-flex flex-column m-auto" + (mouseenter)="hover = true" + (mouseleave)="hover = false" + container> - </ng-template> - </div> - <div message - [innerHTML]="htmlMessage" - *ngIf = "htmlMessage"> - </div> - <div - message - *ngIf="message && !htmlMessage"> - {{ message }} - </div> - <div - (click)="dismiss($event)" - class="ml-2" - *ngIf="dismissable" close> - <i class="fas fa-times"></i> + <!-- body --> + <div class="d-flex flex-row justify-content-between align-items-start"> + + <!-- contents --> + <div message> + <ng-template #messageContainer> + + </ng-template> + </div> + <div message + [innerHTML]="htmlMessage" + *ngIf = "htmlMessage"> + </div> + <div + message + *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> + + <!-- timer --> <timer-component + class="flex-" *ngIf="timeout > 0" (timerEnd)="dismissed.emit(false)" [pause]="hover" [timeout]="timeout" timer> </timer-component> + </div> \ No newline at end of file diff --git a/src/main.module.ts b/src/main.module.ts index daf52879997c91f152b4e8d66cf0b921dd82ad24..1fb66d55c46ea77a249b850a7a349d681d4528f2 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -10,6 +10,7 @@ import { GetNamesPipe } from "./util/pipes/getNames.pipe"; import { CommonModule } from "@angular/common"; import { GetNamePipe } from "./util/pipes/getName.pipe"; import { FormsModule } from "@angular/forms"; +import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' import { AtlasViewerDataService } from "./atlasViewer/atlasViewer.dataService.service"; import { WidgetUnit } from "./atlasViewer/widgetUnit/widgetUnit.component"; @@ -38,7 +39,6 @@ import { ViewerConfiguration } from "./services/state/viewerConfig.store"; import { FixedMouseContextualContainerDirective } from "./util/directives/FixedMouseContextualContainerDirective.directive"; import { DatabrowserService } from "./ui/databrowserModule/databrowser.service"; import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe"; -import { ZipFileDownloadService } from "./services/zipFileDownload.service"; import {HttpClientModule} from "@angular/common/http"; import { EffectsModule } from "@ngrx/effects"; import { UseEffects } from "./services/effect/effect"; @@ -51,6 +51,7 @@ import { UseEffects } from "./services/effect/effect"; ComponentsModule, DragDropModule, UIModule, + AngularMaterialModule, ModalModule.forRoot(), TooltipModule.forRoot(), @@ -112,7 +113,6 @@ import { UseEffects } from "./services/effect/effect"; ToastService, AtlasWorkerService, AuthService, - ZipFileDownloadService, /** * TODO diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index 2269aed7e731b97f5f97020cce0911ca1df34ea0..f21fd1d70d3865cb60a3555d2789183b46a4aadb 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -340,4 +340,9 @@ markdown-dom pre code .text-semi-transparent { opacity: 0.5; +} + +[darktheme="true"] .card +{ + background:none; } \ No newline at end of file diff --git a/src/res/ext/MNI152.json b/src/res/ext/MNI152.json index ba1e9e2389c663003907b00898fb93141045c93b..92dd39191c05148ccfcf53949aca8f4ee8c067ef 100644 --- a/src/res/ext/MNI152.json +++ b/src/res/ext/MNI152.json @@ -9,12 +9,14 @@ { "name": "JuBrain Cytoarchitectonic Atlas", "ngId": "jubrain v17 left", + "originDatasets":[{ + "kgSchema": "minds/core/dataset/v1.0.0", + "kgId": "4ac9f0bc-560d-47e0-8916-7b24da9bb0ce" + }], "properties": { "version": "1.0", "description": "not yet", - "publications": [], - "nifti": [{"file": "jubrain-max-pmap-v22c_space-mnicolin27.nii", "size": "4400000"}], - "externalLink": "https://doi.org/10.25493/8EGG-ZAR" + "publications": [] }, "regions": [ { @@ -5687,8 +5689,11 @@ "surfaceParcellation": true, "ngData": null, "name": "Fibre Bundle Atlas - Long Bundle", + "originDatasets":[{ + "kgSchema": "minds/core/dataset/v1.0.0", + "kgId": "fcbb049b-edd5-4fb5-acbc-7bf8ee933e24" + }], "properties": { - "externalLink": "https://doi.org/10.25493/V5BH-P7P" }, "regions": [ { @@ -5889,6 +5894,10 @@ "surfaceParcellation": true, "ngData": null, "name": "Fibre Bundle Atlas - Short Bundle", + "originDatasets":[{ + "kgSchema": "minds/core/dataset/v1.0.0", + "kgId": "f58e4425-6614-4ad9-ac26-5e946b1296cb" + }], "regions": [ { "name": "Left Hemisphere", @@ -6919,7 +6928,7 @@ } ], "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." } } diff --git a/src/res/ext/colin.json b/src/res/ext/colin.json index 985ebb575ae1051fd9cc06ab67e3339ac4ebf392..beb97256a249d7d24646d029876bf5abe8e4f68b 100644 --- a/src/res/ext/colin.json +++ b/src/res/ext/colin.json @@ -9,12 +9,14 @@ { "name": "JuBrain Cytoarchitectonic Atlas", "ngId": "jubrain colin v17 left", + "originDatasets":[{ + "kgSchema": "minds/core/dataset/v1.0.0", + "kgId": "4ac9f0bc-560d-47e0-8916-7b24da9bb0ce" + }], "properties": { "version": "1.0", "description": "not yet", - "publications": [], - "nifti": [{"file": "jubrain-max-pmap-v22c_space-mnicolin27.nii", "size": "4400000"}], - "externalLink": "https://doi.org/10.25493/8EGG-ZAR" + "publications": [] }, "regions": [ { diff --git a/src/services/toastService.service.ts b/src/services/toastService.service.ts index 6c755753f0165ba18eca5b3e4e73f40298592e1f..35ac359717662d07cb3172c5d37542f7e1ec9596 100644 --- a/src/services/toastService.service.ts +++ b/src/services/toastService.service.ts @@ -1,10 +1,12 @@ import { Injectable, TemplateRef } from "@angular/core"; +import { ToastHandler } from "src/util/pluginHandlerClasses/toastHandler"; @Injectable({ providedIn : 'root' }) export class ToastService{ showToast: (message: string | TemplateRef<any>, config?: Partial<ToastConfig>)=>()=>void + getToastHandler: () => ToastHandler } export interface ToastConfig{ diff --git a/src/services/zipFileDownload.service.ts b/src/services/zipFileDownload.service.ts deleted file mode 100644 index 68131c4bf6932e0dd8df8691936b3bd0d3563738..0000000000000000000000000000000000000000 --- a/src/services/zipFileDownload.service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Injectable } from "@angular/core"; -import {HttpClient} from "@angular/common/http"; -import {AtlasViewerConstantsServices} from "src/atlasViewer/atlasViewer.constantService.service"; -import {map} from "rxjs/operators"; - -@Injectable({ providedIn: 'root' }) -export class ZipFileDownloadService { - - constructor(private httpClient: HttpClient, private constantService: AtlasViewerConstantsServices) {} - - downloadZip(publicationsText, fileName, niiFiles) { - const correctedName = fileName.replace(/[|&;$%@"<>()+,/]/g, "") - return this.httpClient.post(this.constantService.backendUrl + 'datasets/downloadParcellationThemself', { - fileName: correctedName, - publicationsText: publicationsText, - niiFiles: niiFiles === 0 ? null : niiFiles - },{responseType: "text"} - ).pipe( - map (data => { - this.downloadFile(data, correctedName) - }) - ) - } - - downloadFile(data, fileName) { - const contentType = 'application/zip'; - const b64Data = data - - const b64toBlob = (b64Data, contentType='', sliceSize=512) => { - const byteCharacters = atob(b64Data); - const byteArrays = []; - - for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { - const slice = byteCharacters.slice(offset, offset + sliceSize); - - const byteNumbers = new Array(slice.length); - for (let i = 0; i < slice.length; i++) { - byteNumbers[i] = slice.charCodeAt(i); - } - - const byteArray = new Uint8Array(byteNumbers); - byteArrays.push(byteArray); - } - - const blob = new Blob(byteArrays, {type: contentType}); - return blob; - } - - const blob = b64toBlob(b64Data, contentType); - const url= window.URL.createObjectURL(blob); - const anchor = document.createElement("a"); - anchor.download = fileName + '.zip'; - anchor.href = url; - anchor.click(); - } - - -} \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser.module.ts b/src/ui/databrowserModule/databrowser.module.ts index 1238aa758a99074e33dfdfaccd0daf488e82cbc7..9868e3f2c55553d316e56b6f2dabbae9b0c83087 100644 --- a/src/ui/databrowserModule/databrowser.module.ts +++ b/src/ui/databrowserModule/databrowser.module.ts @@ -21,6 +21,9 @@ import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.consta import { PopoverModule } from "ngx-bootstrap/popover"; import { UtilModule } from "src/util/util.module"; import { AggregateArrayIntoRootPipe } from "./util/aggregateArrayIntoRoot.pipe"; +import { KgSingleDatasetService } from "./kgSingleDatasetService.service" +import { SingleDatasetView } from './singleDataset/singleDataset.component' +import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' @NgModule({ imports:[ @@ -29,6 +32,7 @@ import { AggregateArrayIntoRootPipe } from "./util/aggregateArrayIntoRoot.pipe"; ComponentsModule, FormsModule, UtilModule, + AngularMaterialModule, TooltipModule.forRoot(), PopoverModule.forRoot() ], @@ -41,6 +45,7 @@ import { AggregateArrayIntoRootPipe } from "./util/aggregateArrayIntoRoot.pipe"; RadarChart, LineChart, DedicatedViewer, + SingleDatasetView, /** * pipes @@ -52,10 +57,14 @@ import { AggregateArrayIntoRootPipe } from "./util/aggregateArrayIntoRoot.pipe"; AggregateArrayIntoRootPipe ], exports:[ - DataBrowser + DataBrowser, + SingleDatasetView ], entryComponents:[ DataBrowser + ], + providers: [ + KgSingleDatasetService ] /** * shouldn't need bootstrap, so no need for browser module diff --git a/src/ui/databrowserModule/kgSingleDatasetService.service.ts b/src/ui/databrowserModule/kgSingleDatasetService.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..31738eee7c4f55724f35c36dae69372b629b2b18 --- /dev/null +++ b/src/ui/databrowserModule/kgSingleDatasetService.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from "@angular/core"; +import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service" + +@Injectable({ providedIn: 'root' }) +export class KgSingleDatasetService { + + constructor(private constantService: AtlasViewerConstantsServices) { + } + + public getInfoFromKg({ kgId, kgSchema }: KgQueryInterface) { + const _url = new URL(`${this.constantService.backendUrl}datasets/kgInfo`) + const searchParam = _url.searchParams + searchParam.set('kgSchema', kgSchema) + searchParam.set('kgId', kgId) + return fetch(_url.toString()) + .then(res => { + if (res.status >= 400) throw new Error(res.status.toString()) + return res.json() + }) + } + + public downloadZipFromKg({ kgSchema, kgId } : KgQueryInterface, filename = 'download'){ + const _url = new URL(`${this.constantService.backendUrl}datasets/downloadKgFiles`) + const searchParam = _url.searchParams + searchParam.set('kgSchema', kgSchema) + searchParam.set('kgId', kgId) + return fetch(_url.toString()) + .then(res => { + if (res.status >= 400) throw new Error(res.status.toString()) + return res.blob() + }) + .then(data => this.simpleDownload(data, filename)) + } + + public simpleDownload(data, filename) { + const blob = new Blob([data], { type: 'application/zip'}) + const url= window.URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.download = filename + '.zip'; + anchor.href = url; + anchor.click(); + } +} + +interface KgQueryInterface{ + kgSchema: string + kgId: string +} \ No newline at end of file diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.component.ts b/src/ui/databrowserModule/singleDataset/singleDataset.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..997ed303fdfca77f1d6abce2241bf38837945e48 --- /dev/null +++ b/src/ui/databrowserModule/singleDataset/singleDataset.component.ts @@ -0,0 +1,106 @@ +import { Component, Input, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from "@angular/core"; +import { KgSingleDatasetService } from "../kgSingleDatasetService.service"; +import { Publication, File } from 'src/services/state/dataStore.store' + +@Component({ + selector: 'single-dataset-view', + templateUrl: './singleDataset.template.html', + styleUrls: [ + `./singleDataset.style.css` + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SingleDatasetView implements OnInit { + + /** + * the name/desc/publications are placeholder/fallback entries + * while the actual data is being loaded from KG with kgSchema and kgId + */ + @Input() name?: string + @Input() description?: string + @Input() publications?: Publication[] + + @Input() kgSchema?: string + @Input() kgId?: string + + /** + * sic! + */ + private kgReference: string[] = [] + private files: File[] = [] + private methods: string[] = [] + /** + * sic! + */ + private parcellationRegion: { name: string }[] + + private error: string = null + + public fetchingSingleInfoInProgress = false + public downloadInProgress = false + + constructor( + private singleDatasetService: KgSingleDatasetService, + private cdr: ChangeDetectorRef + ){} + + ngOnInit() { + const { kgId, kgSchema } = this + + if (!kgSchema || !kgId) return + this.fetchingSingleInfoInProgress = true + this.singleDatasetService.getInfoFromKg({ + kgId, + kgSchema + }) + .then(({ files, publications, name, description, kgReference}) => { + /** + * TODO dataset specific + */ + this.name = name + this.description = description + this.kgReference = kgReference + this.publications = publications + this.files = files + + this.cdr.markForCheck() + }) + .catch(e => { + this.error = e + }) + .finally(() => { + this.fetchingSingleInfoInProgress = false + }) + } + + get downloadEnabled() { + return this.kgSchema && this.kgId + } + + get appendedKgReferences() { + return this.kgReference.map(v => `https://doi.org/${v}`) + } + + get numOfFiles(){ + return this.files + ? this.files.length + : null + } + + get totalFileByteSize(){ + return this.files + ? this.files.reduce((acc, curr) => acc + curr.byteSize, 0) + : null + } + + downloadZipFromKg() { + this.downloadInProgress = true + const { kgId, kgSchema } = this + this.singleDatasetService.downloadZipFromKg({ + kgId, + kgSchema + }) + .catch(console.error) + .finally(() => this.downloadInProgress = false) + } +} diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.style.css b/src/ui/databrowserModule/singleDataset/singleDataset.style.css new file mode 100644 index 0000000000000000000000000000000000000000..9c4f33e35402143057541d30e4c76fe566cba76d --- /dev/null +++ b/src/ui/databrowserModule/singleDataset/singleDataset.style.css @@ -0,0 +1,4 @@ +:host +{ + text-align: left +} \ No newline at end of file diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.template.html b/src/ui/databrowserModule/singleDataset/singleDataset.template.html new file mode 100644 index 0000000000000000000000000000000000000000..8467234746854c190a08a2d9925176be62de1ba8 --- /dev/null +++ b/src/ui/databrowserModule/singleDataset/singleDataset.template.html @@ -0,0 +1,54 @@ +<div class="card"> + <div class="card-body"> + + <!-- title --> + <h5 class="card-title"> + {{ name }} + </h5> + + <!-- description --> + <p class="card-text"> + {{ description }} + </p> + + <hr> + <!-- publications --> + <a + *ngFor="let publication of publications" + [href]="'https://doi.org/' + publication.doi" + target="_blank"> + {{ publication.cite }} + </a> + + <!-- footer --> + <div class="d-flex justify-content-end"> + + <!-- explore --> + <a + *ngFor="let kgRef of appendedKgReferences" + class="m-2" + mat-raised-button + color="primary" + [href]="kgRef" + target="_blank"> + <span> + Explore + </span> + <i class="fas fa-external-link-alt"></i> + </a> + + <!-- download --> + <a + class="m-2" + *ngIf="files.length > 0" + mat-raised-button + color="primary"> + <span> + Download Zip + </span> + <span *ngIf="false">({{ numOfFiles }} files ~ {{ totalFileByteSize / 1e6 | number: '.1-2' }} MB)</span> + <i class="fas" [ngClass]="!downloadInProgress? 'fa-download' :'fa-spinner fa-pulse'"></i> + </a> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/ui/referenceToast/referenceToast.component.ts b/src/ui/referenceToast/referenceToast.component.ts deleted file mode 100644 index 6ac6485407b43d7a7ce3416868b95731b2c5e06b..0000000000000000000000000000000000000000 --- a/src/ui/referenceToast/referenceToast.component.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Component, Input, OnInit } from "@angular/core"; -import { ZipFileDownloadService } from "src/services/zipFileDownload.service"; - -@Component({ - selector : 'reference-toast-component', - templateUrl : './referenceToast.template.html', - styleUrls : [ - `./referenceToast.style.css` - ], -}) -export class ReferenceToastComponent implements OnInit{ - @Input() templateName? : string - @Input() parcellationName? : string - @Input() templateDescription? : string - @Input() parcellationDescription? : string - @Input() templatePublications? : any - @Input() parcellationPublications? : any - @Input() parcellationNifti? : any - @Input() templateExternalLink? : any - @Input() parcellationExternalLink? : any - - downloadingProcess = false - niiFileSize = 0 - - constructor(private zipFileDownloadService: ZipFileDownloadService) {} - - ngOnInit(): void { - if (this.parcellationNifti) { - this.parcellationNifti.forEach(nii => { - this.niiFileSize += nii['size'] - }) - } - } - - downloadPublications() { - this.downloadingProcess = true - - let fileName = '' - let publicationsText = '' - - if (this.templatePublications || this.templateDescription) { - fileName += this.templateName? this.templateName : 'Template' - - if (this.templateDescription) { - publicationsText += this.templateName + '\r\n' - this.templateDescription.split(" ").forEach((word, index) => { - publicationsText += word + ' ' - if (index && index%15 === 0) publicationsText += '\r\n' - }) - publicationsText += '\r\n' - } - - if (this.templatePublications) { - if (!this.templateDescription) publicationsText += this.templateName - publicationsText += ' Publications:\r\n' - this.templatePublications.forEach((tp, i) => { - publicationsText += '\t' + (i+1) + '. ' + tp['citation'] + ' - ' + tp['doi'] + '\r\n' - }) - } - } - - if (this.parcellationPublications || this.parcellationDescription) { - if (this.templateName) fileName += ' - ' - fileName += this.parcellationName? this.parcellationName : 'Parcellation' - if (this.templateDescription || this.templatePublications) publicationsText += '\r\n\r\n' - - if (this.parcellationDescription) { - publicationsText += this.parcellationName + '\r\n' - this.parcellationDescription.split(" ").forEach((word, index) => { - publicationsText += word + ' ' - if (index && index%15 === 0) publicationsText += '\r\n' - }) - publicationsText += '\r\n' - } - - if (this.parcellationPublications) { - if (!this.parcellationDescription) publicationsText += this.parcellationName - publicationsText += ' Publications:\r\n' - this.parcellationPublications.forEach((pp, i) => { - publicationsText += '\t' + (i+1) + '. ' + pp['citation'] + ' - ' + pp['doi'] + '\r\n' - }) - } - } - - this.zipFileDownloadService.downloadZip( - publicationsText, - fileName, - this.parcellationNifti? this.parcellationNifti : 0).subscribe(data => { - this.downloadingProcess = false - }) - publicationsText = '' - } -} diff --git a/src/ui/referenceToast/referenceToast.style.css b/src/ui/referenceToast/referenceToast.style.css deleted file mode 100644 index cf52094f562a63ff2034df2ee50b64240b889c00..0000000000000000000000000000000000000000 --- a/src/ui/referenceToast/referenceToast.style.css +++ /dev/null @@ -1,14 +0,0 @@ -.timerToast { - max-width: 700px; - max-height: 500px; - padding-right: 10px; - overflow-y: auto; -} - -.download-buttons-panel { - align-self: flex-end; -} -.downloadPublications { - margin: 5px; - outline: none; -} \ No newline at end of file diff --git a/src/ui/referenceToast/referenceToast.template.html b/src/ui/referenceToast/referenceToast.template.html deleted file mode 100644 index b35297ee1517389bd753fa41005dd75ac627d449..0000000000000000000000000000000000000000 --- a/src/ui/referenceToast/referenceToast.template.html +++ /dev/null @@ -1,36 +0,0 @@ -<div class="timerToast d-flex flex-column"> - <div *ngIf="templateDescription"> - <p *ngIf="templateName">{{templateName}}</p> - <p class="text-justify">{{templateDescription}}</p> - </div> - <div *ngIf="templatePublications"> - <p>Publication(s)</p> - <div *ngFor="let tp of templatePublications" class="text-justify"> - <a [href]="tp['doi']" target="_blank">{{tp['citation']}}</a> - </div> - <div class="d-flex justify-content-end"> - <a class="align-self-end" *ngIf="templateExternalLink" href="{{templateExternalLink}}" target="_blank"><button mat-raised-button color="primary" class="downloadPublications">Explore <i class="fas fa-external-link-alt"></i></button></a> - </div> - <hr *ngIf="parcellationPublications"> - </div> - <div *ngIf="parcellationDescription"> - <p *ngIf="parcellationName">{{parcellationName}}</p> - <p class="text-justify">{{parcellationDescription}}</p> - </div> - - <div *ngIf="parcellationPublications"> - <p>Publication(s)</p> - <div *ngFor="let pp of parcellationPublications" class="text-justify"> - <a [href]="pp['doi']" target="_blank">{{pp['citation']}}</a> - </div> - </div> - - <div class="align-self-end"> - <a *ngIf="parcellationExternalLink" href="{{parcellationExternalLink}}" target="_blank"><button mat-raised-button color="primary" class="downloadPublications">Explore <i class="fas fa-external-link-alt"></i></button></a> - <button mat-raised-button color="primary" class="downloadPublications" (click)="downloadPublications()" [disabled] = "downloadingProcess" > - Download - <span *ngIf="niiFileSize > 0">(.nii {{niiFileSize/1000000 | number:'.1-2'}} Mb) </span> - <i class="fas" [ngClass]="!downloadingProcess? 'fa-download' :'fa-spinner fa-pulse'"></i> - </button> - </div> -</div> \ No newline at end of file diff --git a/src/ui/signinBanner/signinBanner.components.ts b/src/ui/signinBanner/signinBanner.components.ts index 80fc79fb55464aaabb80b0afdaf40b06e70f46a3..93c18c3f7bec8f44f1d82403524ad24f052c64fd 100644 --- a/src/ui/signinBanner/signinBanner.components.ts +++ b/src/ui/signinBanner/signinBanner.components.ts @@ -1,13 +1,14 @@ -import {Component, ChangeDetectionStrategy, OnDestroy, OnInit, Input, ViewChild, TemplateRef} from "@angular/core"; +import {Component, ChangeDetectionStrategy, OnDestroy, OnInit, Input, ViewChild, TemplateRef } from "@angular/core"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; import { AuthService, User } from "src/services/auth.service"; import { Store, select } from "@ngrx/store"; import { ViewerConfiguration } from "src/services/state/viewerConfig.store"; -import { Subscription, Observable } from "rxjs"; +import { Subscription, Observable, merge, Subject, interval } from "rxjs"; import { safeFilter, isDefined, NEWVIEWER, SELECT_REGIONS, SELECT_PARCELLATION, CHANGE_NAVIGATION } from "src/services/stateStore.service"; -import { map, filter, distinctUntilChanged } from "rxjs/operators"; +import { map, filter, distinctUntilChanged, bufferTime, delay, share, tap } from "rxjs/operators"; import { regionFlattener } from "src/util/regionFlattener"; import { ToastService } from "src/services/toastService.service"; +import { getSchemaIdFromName } from "src/util/pipes/templateParcellationDecoration.pipe"; const compareParcellation = (o, n) => o.name === n.name @@ -36,9 +37,10 @@ export class SigninBanner implements OnInit, OnDestroy{ @ViewChild('publicationTemplate', {read:TemplateRef}) publicationTemplate: TemplateRef<any> - dismissToastHandler: any - chosenTemplateIndex: number - chosenParcellationIndex: number + public focusedDatasets$: Observable<any[]> + private userFocusedDataset$: Subject<any> = new Subject() + public focusedDatasets: any[] = [] + private dismissToastHandler: () => void constructor( private constantService: AtlasViewerConstantsServices, @@ -54,14 +56,15 @@ export class SigninBanner implements OnInit, OnDestroy{ this.selectedTemplate$ = this.store.pipe( select('viewerState'), - filter(state => isDefined(state) && isDefined(state.templateSelected)), - distinctUntilChanged((o, n) => o.templateSelected.name === n.templateSelected.name), - map(state => state.templateSelected) + select('templateSelected'), + filter(v => !!v), + distinctUntilChanged((o, n) => o.name === n.name), ) this.selectedParcellation$ = this.store.pipe( select('viewerState'), select('parcellationSelected'), + filter(v => !!v) ) this.selectedRegions$ = this.store.pipe( @@ -70,6 +73,43 @@ export class SigninBanner implements OnInit, OnDestroy{ map(state => state.regionsSelected), distinctUntilChanged((arr1, arr2) => arr1.length === arr2.length && (arr1 as any[]).every((item, index) => item.name === arr2[index].name)) ) + + this.focusedDatasets$ = merge( + this.selectedTemplate$.pipe( + map(v => [v]) + ), + this.selectedParcellation$.pipe( + distinctUntilChanged((o, n) => o.name === n.name), + map(p => { + if (!p.originDatasets || !p.originDatasets.map) return [p] + return p.originDatasets.map(od => { + return { + ...p, + ...od + } + }) + }) + ), + this.userFocusedDataset$.pipe( + filter(v => !!v) + ) + ).pipe( + bufferTime(100), + map(arrOfArr => arrOfArr.reduce((acc, curr) => acc.concat(curr), [])), + filter(arr => arr.length > 0), + /** + * merge properties field with the root level + * with the prop in properties taking priority + */ + map(arr => arr.map(item => { + const { properties } = item + return { + ...item, + ...properties + } + })), + share() + ) } ngOnInit(){ @@ -80,7 +120,28 @@ export class SigninBanner implements OnInit, OnDestroy{ }) ) this.subscriptions.push( - this.selectedTemplate$.subscribe(template => this.selectedTemplate = template) + this.selectedTemplate$.subscribe(template => this.selectedTemplate = template) + ) + + this.subscriptions.push( + this.focusedDatasets$.subscribe(() => { + if (this.dismissToastHandler) this.dismissToastHandler() + }) + ) + + this.subscriptions.push( + this.focusedDatasets$.pipe( + /** + * creates the illusion that the toast complete disappears before reappearing + */ + delay(100) + ).subscribe(arr => { + this.focusedDatasets = arr + this.dismissToastHandler = this.toastService.showToast(this.publicationTemplate, { + dismissable: true, + timeout:7000 + }) + }) ) } @@ -89,8 +150,7 @@ export class SigninBanner implements OnInit, OnDestroy{ } changeTemplate({ current, previous }){ - if (previous && current && current.name === previous.name) - return + if (previous && current && current.name === previous.name) return this.store.dispatch({ type: NEWVIEWER, selectTemplate: current, @@ -101,8 +161,7 @@ export class SigninBanner implements OnInit, OnDestroy{ changeParcellation({ current, previous }){ const { ngId: prevNgId} = previous const { ngId: currNgId} = current - if (prevNgId === currNgId) - return + if (prevNgId === currNgId) return this.store.dispatch({ type: SELECT_PARCELLATION, selectParcellation: current @@ -111,8 +170,7 @@ export class SigninBanner implements OnInit, OnDestroy{ // TODO handle mobile handleRegionClick({ mode = 'single', region }){ - if (!region) - return + if (!region) return /** * single click on region hierarchy => toggle selection @@ -180,17 +238,23 @@ export class SigninBanner implements OnInit, OnDestroy{ }) } - showInfoToast($event, toastType) { - this.chosenTemplateIndex = toastType === 'template'? $event : null - this.chosenParcellationIndex = toastType === 'parcellation'? $event : null + handleExtraBtnClicked(event, toastType: 'parcellation' | 'template'){ + const { + extraBtn, + inputItem, + event: extraBtnClickEvent + } = event - if (this.dismissToastHandler) { - this.dismissToastHandler() - this.dismissToastHandler = null - } - this.dismissToastHandler = this.toastService.showToast(this.publicationTemplate, { - timeout: 7000 - }) + const { name } = extraBtn + const { kgSchema, kgId } = getSchemaIdFromName(name) + + this.userFocusedDataset$.next([{ + ...inputItem, + kgSchema, + kgId + }]) + + extraBtnClickEvent.stopPropagation() } get isMobile(){ diff --git a/src/ui/signinBanner/signinBanner.template.html b/src/ui/signinBanner/signinBanner.template.html index a0f0c16be66195c163ae89b3f2c7bc2d9320e1c9..01000c0db06f00e056198a820bcde53ddc3397b0 100644 --- a/src/ui/signinBanner/signinBanner.template.html +++ b/src/ui/signinBanner/signinBanner.template.html @@ -7,8 +7,9 @@ (itemSelected)="changeTemplate($event)" [activeDisplay]="displayActiveTemplate" [selectedItem]="selectedTemplate$ | async" - [inputArray]="loadedTemplates$ | async | filterNull" + [inputArray]="loadedTemplates$ | async | filterNull | templateParcellationsDecorationPipe" [ngClass]="isMobile ? flexItemIsMobileClass : flexItemIsDesktopClass" + (extraBtnClicked)="handleExtraBtnClicked($event, 'template')" (listItemButtonClicked)="showInfoToast($event, 'template')"> </dropdown-component> @@ -19,9 +20,10 @@ [checkSelected]="compareParcellation" [activeDisplay]="displayActiveParcellation" [selectedItem]="selectedParcellation" - [inputArray]="selectedTemplate.parcellations" + [inputArray]="selectedTemplate.parcellations | templateParcellationsDecorationPipe" [ngClass]="isMobile ? flexItemIsMobileClass : flexItemIsDesktopClass" - (listItemButtonClicked)="showInfoToast($event, 'parcellation')">> + (extraBtnClicked)="handleExtraBtnClicked($event, 'parcellation')" + (listItemButtonClicked)="showInfoToast($event, 'parcellation')"> </dropdown-component> <region-hierarchy @@ -76,28 +78,16 @@ </div> </div> +<!-- TODO somehow, using async pipe here does not work --> +<!-- maybe have something to do with bufferTime, and that it replays from the beginning? --> <ng-template #publicationTemplate> - <div *ngIf="loadedTemplates$ | async as loadedTemplates"> - <div *ngIf="chosenTemplateIndex !== null; else parcellationToast"> - <reference-toast-component *ngIf="loadedTemplates[chosenTemplateIndex]['properties'] && (loadedTemplates[chosenTemplateIndex]['properties']['description'] || loadedTemplates[chosenTemplateIndex]['properties']['publications'])" - [templateName] = "loadedTemplates[chosenTemplateIndex]['name'] ? loadedTemplates[chosenTemplateIndex]['name'] : null" - [templateDescription] = "loadedTemplates[chosenTemplateIndex]['properties']['description']? loadedTemplates[chosenTemplateIndex]['properties']['description'] : null" - [templatePublications] = "loadedTemplates[chosenTemplateIndex]['properties']['publications']? loadedTemplates[chosenTemplateIndex]['properties']['publications']: null" - [templateExternalLink] ="loadedTemplates[chosenTemplateIndex]['properties']['externalLink']? loadedTemplates[chosenTemplateIndex]['properties']['externalLink']: null"> - </reference-toast-component> - </div> - <ng-template #parcellationToast> - <div *ngIf="(selectedTemplate$ | async) as selectedTemplate"> - <div *ngIf="selectedTemplate.parcellations[chosenParcellationIndex]['properties'] && (selectedTemplate.parcellations[chosenParcellationIndex]['properties']['description'] || selectedTemplate.parcellations[chosenParcellationIndex]['properties']['publications'])"> - <reference-toast-component - [parcellationName] = "selectedTemplate.parcellations[chosenParcellationIndex]['name']? selectedTemplate.parcellations[chosenParcellationIndex]['name'] : null" - [parcellationDescription] = "selectedTemplate.parcellations[chosenParcellationIndex]['properties']['description']? selectedTemplate.parcellations[chosenParcellationIndex]['properties']['description'] : null" - [parcellationPublications] = "selectedTemplate.parcellations[chosenParcellationIndex]['properties']['publications']? selectedTemplate.parcellations[chosenParcellationIndex]['properties']['publications']: null" - [parcellationNifti] = "selectedTemplate.parcellations[chosenParcellationIndex]['properties']['nifti']? selectedTemplate.parcellations[chosenParcellationIndex]['properties']['nifti'] : null" - [parcellationExternalLink] ="selectedTemplate.parcellations[chosenParcellationIndex]['properties']['externalLink']? selectedTemplate.parcellations[chosenParcellationIndex]['properties']['externalLink']: null"> - </reference-toast-component> - </div> - </div> - </ng-template> - </div> + <single-dataset-view + *ngFor="let focusedDataset of focusedDatasets" + [name]="focusedDataset.name" + [description]="focusedDataset.description" + [publications]="focusedDataset.publications" + [kgSchema]="focusedDataset.kgSchema" + [kgId]="focusedDataset.kgId"> + + </single-dataset-view> </ng-template> \ No newline at end of file diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 868912b551cb0c72e77aac85ebca80204d0abf5d..21f85b35e4777115981aa10a249c18bfc6f2cb97 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -45,8 +45,8 @@ import { FilterNameBySearch } from "./regionHierachy/filterNameBySearch.pipe"; import { StatusCardComponent } from "./nehubaContainer/statusCard/statusCard.component"; import { CookieAgreement } from "./cookieAgreement/cookieAgreement.component"; import { KGToS } from "./kgtos/kgtos.component"; -import { AngularMaterialModule } from "./sharedModules/angularMaterial.module"; -import {ReferenceToastComponent} from "src/ui/referenceToast/referenceToast.component"; +import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' +import { TemplateParcellationsDecorationPipe } from "src/util/pipes/templateParcellationDecoration.pipe"; @NgModule({ @@ -83,7 +83,6 @@ import {ReferenceToastComponent} from "src/ui/referenceToast/referenceToast.comp StatusCardComponent, CookieAgreement, KGToS, - ReferenceToastComponent, /* pipes */ GroupDatasetByRegion, @@ -97,6 +96,7 @@ import {ReferenceToastComponent} from "src/ui/referenceToast/referenceToast.comp FilterNullPipe, FilterNgLayer, FilterNameBySearch, + TemplateParcellationsDecorationPipe, /* directive */ DownloadDirective, @@ -127,9 +127,7 @@ import {ReferenceToastComponent} from "src/ui/referenceToast/referenceToast.comp SigninModal, CookieAgreement, KGToS, - AngularMaterialModule, - StatusCardComponent, - ReferenceToastComponent + StatusCardComponent ] }) diff --git a/src/util/directives/toastContainer.directive.ts b/src/util/directives/toastContainer.directive.ts index f32c5346226341b32e88d1ecacfcb658589871a2..d80740c2e549c68827bc8d296c8282ff49689e13 100644 --- a/src/util/directives/toastContainer.directive.ts +++ b/src/util/directives/toastContainer.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ViewContainerRef, ComponentFactoryResolver, TemplateRef, ComponentRef } from '@angular/core' +import { Directive, ViewContainerRef,ComponentFactory, ComponentFactoryResolver, TemplateRef, ComponentRef } from '@angular/core' import { ToastService, defaultToastConfig } from 'src/services/toastService.service'; import { ToastComponent } from 'src/components/toast/toast.component'; import { AtlasViewerAPIServices } from 'src/atlasViewer/atlasViewer.apiService.service'; @@ -9,13 +9,16 @@ import { ToastHandler } from '../pluginHandlerClasses/toastHandler'; }) export class ToastContainerDirective{ + + private toastComponentFactory: ComponentFactory<ToastComponent> + constructor( private viewContainerRef: ViewContainerRef, private toastService: ToastService, private cfr: ComponentFactoryResolver, private apiService: AtlasViewerAPIServices ){ - const toastComponentFactory = this.cfr.resolveComponentFactory(ToastComponent) + this.toastComponentFactory = this.cfr.resolveComponentFactory(ToastComponent) this.toastService.showToast = (message, config = {}) => { @@ -23,7 +26,7 @@ export class ToastContainerDirective{ ...defaultToastConfig, ...config } - const toastComponent = this.viewContainerRef.createComponent(toastComponentFactory) + const toastComponent = this.viewContainerRef.createComponent(this.toastComponentFactory) if(typeof message === 'string') toastComponent.instance.message = message if(message instanceof TemplateRef){ @@ -44,31 +47,41 @@ export class ToastContainerDirective{ return dismissToast } + this.toastService.getToastHandler = () => { + return this.getToastHandler() + } + this.apiService.interactiveViewer.uiHandle.getToastHandler = () => { - const handler = new ToastHandler() - let toastComponent:ComponentRef<ToastComponent> - handler.show = () => { - toastComponent = this.viewContainerRef.createComponent(toastComponentFactory) - - toastComponent.instance.dismissable = handler.dismissable - toastComponent.instance.message = handler.message - toastComponent.instance.htmlMessage = handler.htmlMessage - toastComponent.instance.timeout = handler.timeout - - const _subscription = toastComponent.instance.dismissed.subscribe(userInitiated => { - _subscription.unsubscribe() - handler.hide() - }) - } + return this.getToastHandler() + } + } - handler.hide = () => { - if(toastComponent){ - toastComponent.destroy() - toastComponent = null - } - } + public getToastHandler(){ + const handler = new ToastHandler() + let toastComponent:ComponentRef<ToastComponent> + handler.show = () => { + toastComponent = this.viewContainerRef.createComponent(this.toastComponentFactory) - return handler + toastComponent.instance.dismissable = handler.dismissable + + if (typeof handler.message === 'string') toastComponent.instance.message = handler.message + if (handler.message instanceof TemplateRef) toastComponent.instance.messageContainer.createEmbeddedView(handler.message as TemplateRef<any>) + toastComponent.instance.htmlMessage = handler.htmlMessage + toastComponent.instance.timeout = handler.timeout + + const _subscription = toastComponent.instance.dismissed.subscribe(userInitiated => { + _subscription.unsubscribe() + handler.hide() + }) } + + handler.hide = () => { + if(toastComponent){ + toastComponent.destroy() + toastComponent = null + } + } + + return handler } -} \ No newline at end of file +} diff --git a/src/util/pipes/templateParcellationDecoration.pipe.ts b/src/util/pipes/templateParcellationDecoration.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb199ed5bfeeb46645bcc23b7140177b87e05729 --- /dev/null +++ b/src/util/pipes/templateParcellationDecoration.pipe.ts @@ -0,0 +1,48 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +const notNullNotEmptyString = (string) => !!string && string !== '' +const notEmptyArray = (arr) => !!arr && arr instanceof Array && arr.length > 0 + +/** + * extraButtons are needed to render (i) btn in dropdown menu + * this pipe should append the extraButtons property according to: + * - originalDatasets is defined + * - description is defined on either root level or properties level + */ + +@Pipe({ + name: 'templateParcellationsDecorationPipe' +}) + +export class TemplateParcellationsDecorationPipe implements PipeTransform{ + public transform(parcellations:any[]){ + return parcellations.map(p => { + const { description, properties = {}, publications } = p + const { description:pDescriptions, publications: pPublications } = properties + const defaultOriginaldataset = notNullNotEmptyString(description) + || notNullNotEmptyString(pDescriptions) + || notEmptyArray(publications) + || notEmptyArray(pPublications) + ? [{}] + : [] + + const { originDatasets = defaultOriginaldataset } = p + return { + ...p, + extraButtons: originDatasets + .map(({ kgSchema, kgId }) => { + return { + name: getNameFromSchemaId({ kgSchema, kgId }), + faIcon: 'fas fa-info-circle' + } + }) + } + }) + } +} + +export const getNameFromSchemaId = ({ kgSchema=null, kgId=null } = {}) => JSON.stringify({kgSchema, kgId}) +export const getSchemaIdFromName = (string = '{}') => { + const {kgSchema=null, kgId=null} = JSON.parse(string) + return { kgSchema, kgId } +} \ No newline at end of file diff --git a/src/util/pluginHandlerClasses/toastHandler.ts b/src/util/pluginHandlerClasses/toastHandler.ts index 80d696980b907641d384600bcfe3643163037fe8..ed468840fa13ed4c09a73fcffe05ae14f50144db 100644 --- a/src/util/pluginHandlerClasses/toastHandler.ts +++ b/src/util/pluginHandlerClasses/toastHandler.ts @@ -1,5 +1,7 @@ +import { TemplateRef } from "@angular/core"; + export class ToastHandler{ - message : string = 'handler.body' + message : string | TemplateRef<any> = 'handler.body' timeout : number = 3000 dismissable : boolean = true show : () => void