diff --git a/package.json b/package.json index 04ddb7088000c63d48ff074d7893b1f927500d91..70aef493c61eba8bb3056b2b1a577f27dd0dee9e 100644 --- a/package.json +++ b/package.json @@ -3,25 +3,29 @@ "version": "1.0.0", "description": "", "scripts": { + "dev-server-export": "webpack-dev-server --config webpack.export.js", + "build-export": "webpack --config webpack.export.js", "build-aot": "webpack --config webpack.aot.js", "build-min": "webpack --config webpack.prod.js", "build": "webpack --config webpack.dev.js", "dev-server": "webpack-dev-server --config webpack.dev.js --mode development", - "serve-plugins" : "node src/plugin_examples/server.js", + "serve-plugins": "node src/plugin_examples/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { - "@angular/common": "^6.0.3", - "@angular/compiler": "^6.0.3", + "@angular/animations": "^6.0.7", + "@angular/common": "^6.0.7", + "@angular/compiler": "^6.0.7", "@angular/compiler-cli": "^6.0.3", - "@angular/core": "^6.0.3", - "@angular/forms": "^6.0.3", - "@angular/http": "^6.0.3", - "@angular/platform-browser": "^6.0.3", - "@angular/platform-browser-dynamic": "^6.0.3", + "@angular/core": "^6.0.7", + "@angular/elements": "^6.0.7", + "@angular/forms": "^6.0.7", + "@angular/http": "^6.0.7", + "@angular/platform-browser": "^6.0.7", + "@angular/platform-browser-dynamic": "^6.0.7", "@ngrx/store": "^6.0.1", "@ngtools/webpack": "^6.0.5", "@types/chart.js": "^2.7.20", diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index b0d0c59842c00c652c5507bdb1ee901bde00015e..efa52221e317b8c2fc1d883c56c4cb13a92a9df9 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Renderer2 } from "@angular/core"; +import { Injectable } from "@angular/core"; import { Store, select } from "@ngrx/store"; import { ViewerStateInterface, safeFilter } from "../services/stateStore.service"; import { Observable } from "rxjs"; diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index f7b94e6fef819da278d6f4084dd592f634b4feaa..1fb5fa739f7c70946a8091c9edfc4bd3180141e1 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -18,6 +18,7 @@ import { AtlasViewerAPIServices } from "./atlasViewer.apiService.service"; import { PluginServices } from "./atlasViewer.pluginService.service"; import '../res/css/extra_styles.css' +import { NehubaContainer } from "../ui/nehubaContainer/nehubaContainer.component"; @Component({ selector: 'atlas-viewer', @@ -33,16 +34,14 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @ViewChild('floatingContainer', { read: ViewContainerRef }) floatingContainer: ViewContainerRef @ViewChild('databrowser', { read: ElementRef }) databrowser: ElementRef @ViewChild('temporaryContainer', { read: ViewContainerRef }) temporaryContainer: ViewContainerRef - @ViewChild('toastContainer', { read: ViewContainerRef }) toastContainer: ViewContainerRef @ViewChild('dedicatedViewerToast', { read: TemplateRef }) dedicatedViewerToast: TemplateRef<any> - @ViewChild('floatingMouseContextualContainer', { read: ViewContainerRef }) floatingMouseContextualContainer: ViewContainerRef - @ViewChild('pluginFactory', { read: ViewContainerRef }) pluginViewContainerRef: ViewContainerRef - @ViewChild(LayoutMainSide) layoutMainSide: LayoutMainSide + @ViewChild(NehubaContainer) nehubaContainer: NehubaContainer + @HostBinding('attr.darktheme') darktheme: boolean = false @@ -325,6 +324,24 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { }) } + rafId : number | null + + panelAnimationFlag(flag:boolean){ + const redraw = ()=>{ + if( this.nehubaContainer && this.nehubaContainer.nehubaViewer && this.nehubaContainer.nehubaViewer.nehubaViewer ) + this.nehubaContainer.nehubaViewer.nehubaViewer.redraw() + this.rafId = requestAnimationFrame(()=>redraw()) + } + if( flag ){ + if(this.rafId) + cancelAnimationFrame(this.rafId) + this.rafId = requestAnimationFrame(()=>redraw()) + }else{ + cancelAnimationFrame(this.rafId) + this.rafId = null + } + } + clearDedicatedView() { this.store.dispatch({ type: UNLOAD_DEDICATED_LAYER diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 6c1d721e5e401df10ee528e1ebe76c6e100f79fe..cd80d350841f899b564a3f97deb3aed34021db89 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -1,7 +1,8 @@ <div *ngIf = "meetsRequirement" class = "atlas-container"> <layout-mainside - (panelShowStateChanged)="manualPanelToggle($event)"> + (panelAnimationFlag) = "panelAnimationFlag($event)" + (panelShowStateChanged) = "manualPanelToggle($event)"> <div maincontent> <ui-nehuba-container> </ui-nehuba-container> diff --git a/src/atlasViewerExports/export.html b/src/atlasViewerExports/export.html new file mode 100644 index 0000000000000000000000000000000000000000..88c7be9425f243a067464fa8542c58a8f6b61ef6 --- /dev/null +++ b/src/atlasViewerExports/export.html @@ -0,0 +1,166 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=720, initial-scale=1.0"> + <title>Interactive Viewer Exported Components</title> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> + + <script src = "https://unpkg.com/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> + <script src = "export.js"></script> + + <script> + document.addEventListener('DOMContentLoaded',()=>{ + const script = ` +const inputItem = { + "name" : "drinks", + "children" : [{ + "name":"coffee", + "children":[{ + "name":"flatwhite", + "children":[] + },{ + "name":"espresso", + "children":[] + }] + },{ + "name" : "tea", + "children" : [{ + "name" : "green tea", + "children" : [], + },{ + "name" : "black tea", + "children" : [] + }] + }] +} +const tree = document.getElementById('tree-element') +tree.setAttribute('input-item',JSON.stringify(inputItem)) +` + const treeSampleBox = document.getElementById('tree-element-sample-box') + treeSampleBox.setAttribute('script-input',script) + eval(script); + + const markdownIncludeString = ` +You will also need \`bootstrap 3.3.7\` for formatting and \`custom-elements-es5-adapter\` for es5 shim: + +\`\`\` +<${''}script src = "https://unpkg.com/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"><${''}/script> +<${''}link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> +\`\`\` + +To use any of the components, include \`export.js\` in the header: + +\`\`\` +<${''}script src = "third_party/atlasViewer/dist/export/export.js"></${''}script> +\`\`\` + +` + const markdownInclude = document.getElementById('markdown-include') + markdownInclude.setAttribute('markdown',markdownIncludeString) + + const correctMarkDown = ` +\`\`\` +const correctMarkDown = \` +<\$\{''\}script> + console.log('NOT GOTCHA') +<\$\{''\}/script> +\` + +const markdownCorrect = document.getElementById('markdown-id') +markdownCorrect.setAttribute('markdown',correctmarkDown) +\`\`\` +` + const markdownCorrect = document.getElementById('markdown-correct') + markdownCorrect.setAttribute('markdown',correctMarkDown) + }) + </script> +</head> +<body> + + <div class = "jumbotron"> + <div class = "container"> + <div class = "row"> + <h1>interactive viewer components</h1> + </div> + <div class = "row"> +<markdown-element id = "markdown-include"> +</markdown-element> + </div> + </div> + </div> + <div id = "container" class = "container-fluid"> + <div class = "row"> + <div class = "col-md-3"> + +<sample-box sample-box-title = "readmore component"> +<readmore-element> + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +</readmore-element> +</sample-box> + </div> + <div class = "col-md-3"> +<sample-box sample-box-title = "markdown component"> +<markdown-element> +# Heading1 +## Heading2 +### Heading3 + +this is a paragraph + +- a dot point +- a second dot point +</markdown-element> +</sample-box> +<h3> + n.b. +</h3> +<p> +<markdown-element> +- first attempts to parse `element.getAttribute('markdown')` then `element.innerHTML`, if non-empty. +- major gotcha: if markdown is rendered as a part of innerHTML, the dom will be rendered briefly. As a result, triple ticked script tags **will** will be evaluated. For example, the following script, if enclosed between `markdown-element` tags, will still be evaluated: + +``` +<script> +console.log('GOTCHA') +</script> +``` + +- To include script tags, pass it as an attribute instead: + +</markdown-element> +<markdown-element id = "markdown-correct"> +</markdown-element> +<markdown-element> +- The `${''}` is required to breakup the `</script>` tag, or the script tag will be terminated prematurely. +</markdown-element> +</p> + </div> + <div class = "col-md-3"> +<sample-box sample-box-title = "panel component"> +<panel-element body-collapsable = "true" show-footer = "true"> + <div style = "padding: 0.5em;" heading> + panel heading + </div> + <div style = "padding: 0.5em;" body> + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + </div> + <div style = "opacity:0.7;padding: 0.5em;" footer> + panel footer + </div> +</panel-element> +</sample-box> + </div> + <div class = "col-md-3"> +<sample-box sample-box-title = "tree component" id = "tree-element-sample-box"> +<tree-element id = 'tree-element'> + +</tree-element> +</sample-box> + </div> + </div> + </div> +</body> +<footer> +</footer> +</html> \ No newline at end of file diff --git a/src/atlasViewerExports/export.module.ts b/src/atlasViewerExports/export.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..d17b61f71594e5f56b5bdcacda4a7a2ecf160c9d --- /dev/null +++ b/src/atlasViewerExports/export.module.ts @@ -0,0 +1,66 @@ +import { NgModule, Injector } from "@angular/core"; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { createCustomElement } from '@angular/elements' +import { BrowserModule } from "@angular/platform-browser"; +import { FormsModule } from "@angular/forms"; +import { BsDropdownModule } from "ngx-bootstrap/dropdown"; +import { ReadmoreComponent } from "../components/readmoore/readmore.component"; +import { MarkdownDom } from '../components/markdown/markdown.component' +import { SafeHtmlPipe } from "../util/pipes/safeHtml.pipe"; +import { SampleBoxUnit } from "./sampleBox/sampleBox.component"; +import { PanelComponent } from "../components/panel/panel.component"; +import { HoverableBlockDirective } from "../components/hoverableBlock.directive"; +import { TreeComponent } from "../components/tree/tree.component"; +import { TreeSearchPipe } from "../util/pipes/treeSearch.pipe"; + +@NgModule({ + imports : [ + BrowserModule, + BrowserAnimationsModule, + FormsModule, + BsDropdownModule.forRoot() + ], + declarations : [ + SampleBoxUnit, + + ReadmoreComponent, + MarkdownDom, + PanelComponent, + TreeComponent, + + SafeHtmlPipe, + HoverableBlockDirective, + TreeSearchPipe + ], + entryComponents : [ + SampleBoxUnit, + + ReadmoreComponent, + MarkdownDom, + TreeComponent, + PanelComponent + ] +}) + +export class ExportModule{ + constructor(public injector:Injector){ + const SampleBox = createCustomElement(SampleBoxUnit,{injector:this.injector}) + customElements.define('sample-box',SampleBox) + + const ReadMore = createCustomElement(ReadmoreComponent,{ injector : this.injector }) + customElements.define('readmore-element',ReadMore) + + const MarkDown = createCustomElement(MarkdownDom,{injector : this.injector }) + customElements.define('markdown-element',MarkDown) + + const Panel = createCustomElement(PanelComponent,{injector : this.injector }) + customElements.define('panel-element',Panel) + + const Tree = createCustomElement(TreeComponent,{injector : this.injector }) + customElements.define('tree-element',Tree) + + } + ngDoBootstrap(){ + + } +} \ No newline at end of file diff --git a/src/atlasViewerExports/main.export.ts b/src/atlasViewerExports/main.export.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a908339a2f96703a7d3151d0875a83db45a87a4 --- /dev/null +++ b/src/atlasViewerExports/main.export.ts @@ -0,0 +1,6 @@ +import 'zone.js' +import 'reflect-metadata' +import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; +import { ExportModule } from "./export.module"; + +platformBrowserDynamic().bootstrapModule(ExportModule) \ No newline at end of file diff --git a/src/atlasViewerExports/sampleBox/sampleBox.component.ts b/src/atlasViewerExports/sampleBox/sampleBox.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..bec5619ff388d6dc794a5e770871a09c8116e7b5 --- /dev/null +++ b/src/atlasViewerExports/sampleBox/sampleBox.component.ts @@ -0,0 +1,44 @@ +import { + Component, + Input, + ViewChild, + ElementRef, + OnInit, + OnChanges, + Renderer2 +} from '@angular/core' + +@Component({ + selector : 'sample-box', + templateUrl : './sampleBox.template.html', + styleUrls : [ + './sampleBox.style.css' + ] +}) + +export class SampleBoxUnit implements OnInit, OnChanges{ + @Input() sampleBoxTitle = `` + @Input() scriptInput + + @ViewChild('ngContent',{read:ElementRef}) ngContent : ElementRef + + escapedHtml : string = `` + escapedScript : string = `` + + private scriptEl : HTMLScriptElement + + constructor(private rd2:Renderer2){ + this.scriptEl = this.rd2.createElement('script') + } + + ngOnInit(){ + this.escapedHtml = this.ngContent.nativeElement.innerHTML + } + + ngOnChanges(){ + this.escapedScript = this.scriptInput + if( this.scriptInput ){ + this.scriptEl.innerText = this.scriptInput + } + } +} \ No newline at end of file diff --git a/src/atlasViewerExports/sampleBox/sampleBox.style.css b/src/atlasViewerExports/sampleBox/sampleBox.style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/atlasViewerExports/sampleBox/sampleBox.template.html b/src/atlasViewerExports/sampleBox/sampleBox.template.html new file mode 100644 index 0000000000000000000000000000000000000000..b94af69fe74182a7fc5abbbdf9670fd2d3b08237 --- /dev/null +++ b/src/atlasViewerExports/sampleBox/sampleBox.template.html @@ -0,0 +1,19 @@ +<h2> + {{ sampleBoxTitle }} +</h2> +<hr /> +<h3> + Result: +</h3> +<div class = "well" #ngContent> + <ng-content> + </ng-content> +</div> +<h3> + HTML: +</h3> +<pre>{{ escapedHtml }}</pre> +<h3 *ngIf = "scriptInput"> + Script: +</h3> +<pre *ngIf = "scriptInput">{{ escapedScript }}</pre> diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 9f8c480e387b7fc582f0244ba700db77e897e8cc..d85c8b202e5f2a1624fd7870f9cbe47dfaa6d408 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core' import { FormsModule } from '@angular/forms' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; @@ -22,6 +23,7 @@ import { TreeSearchPipe } from '../util/pipes/treeSearch.pipe'; imports : [ BrowserModule, FormsModule, + BrowserAnimationsModule, BsDropdownModule.forRoot(), ], declarations : [ @@ -44,6 +46,8 @@ import { TreeSearchPipe } from '../util/pipes/treeSearch.pipe'; TreeSearchPipe ], exports : [ + BrowserAnimationsModule, + MarkdownDom, ReadmoreComponent, DropdownComponent, diff --git a/src/components/markdown/markdown.component.ts b/src/components/markdown/markdown.component.ts index e04d83eff5a5fe3edd6e8e05585f12c9153feb94..de6521bf0ee6314b8aac7f2df66e2df234e98108 100644 --- a/src/components/markdown/markdown.component.ts +++ b/src/components/markdown/markdown.component.ts @@ -1,4 +1,4 @@ -import { Component, OnChanges, Input, ChangeDetectionStrategy } from '@angular/core' +import { Component, OnChanges, Input, ChangeDetectionStrategy, ViewChild, ElementRef, OnInit } from '@angular/core' import * as showdown from 'showdown' @Component({ @@ -10,15 +10,26 @@ import * as showdown from 'showdown' changeDetection : ChangeDetectionStrategy.OnPush }) -export class MarkdownDom implements OnChanges{ +export class MarkdownDom implements OnChanges,OnInit{ @Input() markdown : string = `` public innerHtml : string = `` private converter = new showdown.Converter() + constructor(){ this.converter.setFlavor('github') } + ngOnChanges(){ this.innerHtml = this.converter.makeHtml(this.markdown) } + + ngOnInit(){ + if(this.contentWrapper.nativeElement.innerHTML.replace(/\w|\n/g,'') !== '') + this.innerHtml = this.converter.makeHtml(this.contentWrapper.nativeElement.innerHTML) + } + + @ViewChild('ngContentWrapper', {read : ElementRef}) + contentWrapper : ElementRef + } diff --git a/src/components/markdown/markdown.template.html b/src/components/markdown/markdown.template.html index d72c87c900e4e8be5e63d1cb9f1b9c8db9ccd6b3..bb29d3be594d16c2b49a920a81a81fccec40c6ae 100644 --- a/src/components/markdown/markdown.template.html +++ b/src/components/markdown/markdown.template.html @@ -1,3 +1,8 @@ <div [innerHTML] = "innerHtml | safeHtml"> +</div> +<div class = "hidden" #ngContentWrapper> + <ng-content> + + </ng-content> </div> \ No newline at end of file diff --git a/src/components/panel/panel.animation.ts b/src/components/panel/panel.animation.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bb5c3fc476e6abb38e4c673a8e2b96ae852df24 --- /dev/null +++ b/src/components/panel/panel.animation.ts @@ -0,0 +1,23 @@ +import { trigger, state, style, transition, animate } from "@angular/animations"; + + +export const panelAnimations = trigger('collapseState',[ + state('collapsed', + style({ + 'margin-top' : '-{{ fullHeight }}px' + }), + { params : { fullHeight : 9999 } } + ), + state('visible', + style({ + 'margin-top' : '0px' + }), + { params : { fullHeight : 0 } } + ), + transition('collapsed => visible',[ + animate('250ms ease-out') + ]), + transition('visible => collapsed',[ + animate('250ms ease-in') + ]) +]) \ No newline at end of file diff --git a/src/components/panel/panel.component.ts b/src/components/panel/panel.component.ts index 5f7220081300be309bf1089413d8de2d102cdbef..e030dc5bc2fd77b61f58f49f3b8916a0054cd559 100644 --- a/src/components/panel/panel.component.ts +++ b/src/components/panel/panel.component.ts @@ -1,14 +1,18 @@ -import { Component, Input, ChangeDetectionStrategy } from "@angular/core"; +import { Component, Input, ViewChild, ElementRef, AfterContentChecked } from "@angular/core"; +import { panelAnimations } from "./panel.animation"; @Component({ selector : 'panel', templateUrl : './panel.template.html', styleUrls : [ `./panel.style.css` + ], + animations : [ + panelAnimations ] }) -export class PanelComponent{ +export class PanelComponent implements AfterContentChecked{ @Input() showHeading : boolean = true @Input() showBody : boolean = true @@ -19,9 +23,30 @@ export class PanelComponent{ @Input() containerClass : string = '' + @ViewChild('panelBody',{ read : ElementRef }) efPanelBody : ElementRef + @ViewChild('panelFooter',{ read : ElementRef }) efPanelFooter : ElementRef + + _fullHeight : number = 0 + + ngAfterContentChecked(){ + this.fullHeight = (this.efPanelBody ? this.efPanelBody.nativeElement.offsetHeight : 0) + + (this.efPanelFooter ? this.efPanelFooter.nativeElement.offsetHeight : 0) + } + + set fullHeight(num:number){ + this._fullHeight = num + } + + get fullHeight(){ + return this._fullHeight + } + toggleCollapseBody(event:Event){ if(this.bodyCollapsable){ this.collapseBody = !this.collapseBody + + this.fullHeight = (this.efPanelBody ? this.efPanelBody.nativeElement.offsetHeight : 0) + + (this.efPanelFooter ? this.efPanelFooter.nativeElement.offsetHeight : 0) } event.stopPropagation() event.preventDefault() diff --git a/src/components/panel/panel.style.css b/src/components/panel/panel.style.css index 1802a29a767f050073e17900faec0ef047053c50..2163a4923d7798421cbffef5185aae76636532f2 100644 --- a/src/components/panel/panel.style.css +++ b/src/components/panel/panel.style.css @@ -12,6 +12,11 @@ margin:0; } +[bodyFooterContainer] +{ + overflow:hidden; +} + .panel > .panel-heading { border-radius : 0; @@ -20,17 +25,14 @@ border:none; } -.panel > .panel-body, -.panel > .panel-footer +.panel > [bodyFooterContainer] > .panel-body, +.panel > [bodyFooterContainer] > .panel-footer { + margin-top:0px; + padding: 0px; border:none; } -.panel > .panel-body -{ - padding: 0; -} - div.panel { background:none; diff --git a/src/components/panel/panel.template.html b/src/components/panel/panel.template.html index b571a62aefa107f97e60689c35e694208fccaa73..c6bab147f33e58c9f499fc8ad8e34489e1d3b31f 100644 --- a/src/components/panel/panel.template.html +++ b/src/components/panel/panel.template.html @@ -10,22 +10,26 @@ </ng-content> </div> - <div - *ngIf = "showBody" - class = "panel-body" - [hidden] = "collapseBody"> + <div bodyFooterContainer> + <div + *ngIf = "showBody" + class = "panel-body" + [@collapseState] = "{ value : collapseBody ? 'collapsed' : 'visible', params : { fullHeight : fullHeight } }" + #panelBody> - <ng-content select="[body]"> - </ng-content> + <ng-content select="[body]"> + </ng-content> - </div> - <div - *ngIf = "showFooter" - [hidden] = "collapseBody" - class = "panel-footer"> - - <ng-content select="[footer]"> - </ng-content> + </div> + <div + *ngIf = "showFooter" + class = "panel-footer" + [@collapseState] = "{ value : collapseBody ? 'collapsed' : 'visible', params : { fullHeight : fullHeight } }" + #panelFooter> + + <ng-content select="[footer]"> + </ng-content> + </div> </div> </div> \ No newline at end of file diff --git a/src/components/readmoore/readmore.animations.ts b/src/components/readmoore/readmore.animations.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c54dc15dfbb0c0bd60bb00d3f46a042d9663ce5 --- /dev/null +++ b/src/components/readmoore/readmore.animations.ts @@ -0,0 +1,25 @@ +import { + trigger, + state, + style, + transition, + animate, + AnimationTriggerMetadata +} from '@angular/animations' + +export const readmoreAnimations : AnimationTriggerMetadata = trigger('collapseState',[ + state('collapsed', + style({ 'height' : '{{ collapsedHeight }}px' }), + { params : { collapsedHeight : 45, fullHeight : 200 } } + ), + state('visible', + style({ 'height' : '{{ fullHeight }}px' }), + { params : { collapsedHeight : 45, fullHeight : 200 } } + ), + transition('collapsed => visible',[ + animate('180ms') + ]), + transition('visible => collapsed',[ + animate('180ms') + ]) +]) \ No newline at end of file diff --git a/src/components/readmoore/readmore.component.ts b/src/components/readmoore/readmore.component.ts index 9224ca5a9caa4641ddbb03d3c88ad3392461c68c..bad3a90a7188dd7b3e69b57ca644b822ac300083 100644 --- a/src/components/readmoore/readmore.component.ts +++ b/src/components/readmoore/readmore.component.ts @@ -1,31 +1,34 @@ -import { Component, Input, OnChanges, ViewChild, ElementRef } from "@angular/core"; +import { Component, Input, OnChanges, ViewChild, ElementRef, AfterContentChecked } from "@angular/core"; +import { readmoreAnimations } from "./readmore.animations"; @Component({ selector : 'readmore', templateUrl : './readmore.template.html', styleUrls : [ './readmore.style.css' - ] + ], + animations : [ readmoreAnimations ] }) -export class ReadmoreComponent implements OnChanges{ +export class ReadmoreComponent implements OnChanges, AfterContentChecked{ @Input() collapsedHeight : number = 45 @Input() show : boolean = false - @ViewChild('content') contentContainer : ElementRef + @ViewChild('contentContainer') contentContainer : ElementRef + public fullHeight : number = 200 + + ngAfterContentChecked(){ + this.fullHeight = this.contentContainer.nativeElement.offsetHeight + } + ngOnChanges(){ - + this.fullHeight = this.contentContainer.nativeElement.offsetHeight } public toggle(event:MouseEvent){ + this.show = !this.show event.stopPropagation() event.preventDefault() } - - get contentContainerMaxHeight(){ - return this.show ? - `9999px` : - `${this.collapsedHeight}px` - } } \ No newline at end of file diff --git a/src/components/readmoore/readmore.template.html b/src/components/readmoore/readmore.template.html index 6931e169c5c77c9b6aea302f2690936e2bb07bc9..a54e54bee9f5b799b7575d78c0ab53d344122e2a 100644 --- a/src/components/readmoore/readmore.template.html +++ b/src/components/readmoore/readmore.template.html @@ -1,9 +1,10 @@ <div - [style.maxHeight]="contentContainerMaxHeight" - #content + [@collapseState] = "{ value : show ? 'visible' : 'collapsed', params : { collapsedHeight : collapsedHeight, fullHeight : fullHeight } }" content> - <ng-content> - </ng-content> + <div #contentContainer> + <ng-content> + </ng-content> + </div> </div> <div (click)="toggle($event)" diff --git a/src/components/tree/tree.animation.ts b/src/components/tree/tree.animation.ts new file mode 100644 index 0000000000000000000000000000000000000000..b346e34b6b21c20b81a0e81965227f72f2d81a06 --- /dev/null +++ b/src/components/tree/tree.animation.ts @@ -0,0 +1,23 @@ +import { trigger, state, style, transition, animate } from "@angular/animations"; + + +export const treeAnimations = trigger('collapseState',[ + state('collapsed', + style({ + 'margin-top' : '-{{ fullHeight }}px' + }), + { params : { fullHeight : 9999 } } + ), + state('visible', + style({ + 'margin-top' : '0px' + }), + { params : { fullHeight : 0 } } + ), + transition('collapsed => visible',[ + animate('180ms') + ]), + transition('visible => collapsed',[ + animate('180ms') + ]) +]) \ No newline at end of file diff --git a/src/components/tree/tree.component.ts b/src/components/tree/tree.component.ts index 22d7698dd3d60778989f3d78d9b2c5c4334bf1f8..3c19da127550e451890e0bca6e61d86b49d0d1a2 100644 --- a/src/components/tree/tree.component.ts +++ b/src/components/tree/tree.component.ts @@ -1,4 +1,5 @@ -import { Component, Input, Output, EventEmitter, ViewChildren, QueryList, HostBinding, ChangeDetectionStrategy } from "@angular/core"; +import { Component, Input, Output, EventEmitter, ViewChildren, QueryList, HostBinding, ChangeDetectionStrategy, OnChanges, AfterContentChecked, ViewChild, ElementRef } from "@angular/core"; +import { treeAnimations } from "./tree.animation"; @Component({ @@ -7,10 +8,13 @@ import { Component, Input, Output, EventEmitter, ViewChildren, QueryList, HostBi styleUrls : [ './tree.style.css' ], + animations : [ + treeAnimations + ], changeDetection:ChangeDetectionStrategy.OnPush }) -export class TreeComponent{ +export class TreeComponent implements OnChanges,AfterContentChecked{ @Input() inputItem : any = { name : 'Untitled', children : [] @@ -26,6 +30,19 @@ export class TreeComponent{ @Output() mouseclicktree : EventEmitter<any> = new EventEmitter() @ViewChildren(TreeComponent) treeChildren : QueryList<TreeComponent> + @ViewChild('childrenContainer',{ read : ElementRef }) childrenContainer : ElementRef + + ngOnChanges(){ + if(typeof this.inputItem === 'string'){ + this.inputItem = JSON.parse(this.inputItem) + } + } + + fullHeight : number = 100 + + ngAfterContentChecked(){ + this.fullHeight = this.childrenContainer ? this.childrenContainer.nativeElement.offsetHeight : 0 + } get chevronClass():string{ return this.children ? diff --git a/src/components/tree/tree.style.css b/src/components/tree/tree.style.css index 22cd7ce11ed5845e590c01a0a4f3f5da278c4b53..8c4e5f0f231b9a6a62d60ec7e0aaf5d53851fb61 100644 --- a/src/components/tree/tree.style.css +++ b/src/components/tree/tree.style.css @@ -104,3 +104,8 @@ div[itemMasterContainer]:before position:absolute; z-index: 0; } + +div[childrenOverflowContainer] +{ + overflow:hidden; +} \ No newline at end of file diff --git a/src/components/tree/tree.template.html b/src/components/tree/tree.template.html index 0b7192da4ef22a9000b7e04f0e342054c0c89490..9d918d9a8227737195dab7bf7958d58db2065e17 100644 --- a/src/components/tree/tree.template.html +++ b/src/components/tree/tree.template.html @@ -1,7 +1,4 @@ <div - (mouseleave)="mouseleavetree.emit({inputItem:inputItem,node:this});handleEv($event)" - (mouseenter)="mouseentertree.emit({inputItem:inputItem,node:this});handleEv($event)" - (click)="mouseclicktree.emit({inputItem:inputItem,node:this});handleEv($event)" itemMasterContainer> <div itemContainer> <i @@ -11,21 +8,29 @@ glyphicon> </i> <span + (mouseleave)="mouseleavetree.emit({inputItem:inputItem,node:this});handleEv($event)" + (mouseenter)="mouseentertree.emit({inputItem:inputItem,node:this});handleEv($event)" + (click)="mouseclicktree.emit({inputItem:inputItem,node:this});handleEv($event)" [innerHTML] = "renderNode(inputItem)" itemName> </span> </div> - <tree - *ngFor = "let child of (findChildren(inputItem) | treeSearch : searchFilter : findChildren )" - [hidden] = "!childrenExpanded" - [childrenExpanded] = "childrenExpanded" - [inputItem] = "child" - [renderNode]="renderNode" - [searchFilter]="searchFilter" - [findChildren] = "findChildren" - (mouseentertree)="mouseentertree.emit($event)" - (mouseleavetree)="mouseleavetree.emit($event)" - (mouseclicktree)="mouseclicktree.emit($event)"> - - </tree> + <div childrenOverflowContainer> + <div + [@collapseState] = "{ value : childrenExpanded ? 'visible' : 'collapsed' , params : { fullHeight : fullHeight }}" + #childrenContainer> + <tree + *ngFor = "let child of (findChildren(inputItem) | treeSearch : searchFilter : findChildren )" + [childrenExpanded] = "childrenExpanded" + [inputItem] = "child" + [renderNode]="renderNode" + [searchFilter]="searchFilter" + [findChildren] = "findChildren" + (mouseentertree)="mouseentertree.emit($event)" + (mouseleavetree)="mouseleavetree.emit($event)" + (mouseclicktree)="mouseclicktree.emit($event)"> + + </tree> + </div> + </div> </div> \ No newline at end of file diff --git a/src/layouts/layout.module.ts b/src/layouts/layout.module.ts index 4541a74b9da560f4dd9f121818b4905d84c5957d..703b12e6955f4e8dce267e5c5f07136af4d75422 100644 --- a/src/layouts/layout.module.ts +++ b/src/layouts/layout.module.ts @@ -4,10 +4,12 @@ import { LayoutsExample } from "./layoutsExample/layoutsExample.component"; import { BrowserModule } from "@angular/platform-browser"; import { ComponentsModule } from "../components/components.module"; import { FloatingLayoutContainer } from "./floating/floating.component"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @NgModule({ imports : [ + BrowserAnimationsModule, BrowserModule, ComponentsModule ], @@ -18,6 +20,7 @@ import { FloatingLayoutContainer } from "./floating/floating.component"; LayoutsExample ], exports : [ + BrowserAnimationsModule, LayoutMainSide, FloatingLayoutContainer, diff --git a/src/layouts/mainside/mainside.animation.ts b/src/layouts/mainside/mainside.animation.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3a93afc431c6370d04406a2fe23c82283097727 --- /dev/null +++ b/src/layouts/mainside/mainside.animation.ts @@ -0,0 +1,25 @@ +import { trigger, state, style, transition, animate } from "@angular/animations"; + + +export const mainSideAnimation = trigger('collapseSide',[ + state('collapsed', + style({ + 'flex-basis' : '0px', + 'width' : '0px' + }), + { params : { sideWidth : 0 } } + ), + state('visible', + style({ + 'flex-basis' : '{{ sideWidth }}px', + 'width' : '{{ sideWidth }}px' + }), + { params : { sideWidth : 300 } } + ), + transition('collapsed => visible',[ + animate('180ms ease-out') + ]), + transition('visible => collapsed',[ + animate('180ms ease-in') + ]) +]) \ No newline at end of file diff --git a/src/layouts/mainside/mainside.component.ts b/src/layouts/mainside/mainside.component.ts index ab238235cf92ea09070666070e9c0f2519bc9bc4..83eeaf357d830482605675fabe485d72aecfe07f 100644 --- a/src/layouts/mainside/mainside.component.ts +++ b/src/layouts/mainside/mainside.component.ts @@ -1,11 +1,14 @@ import { Component, Input, EventEmitter, Output } from "@angular/core"; - +import { mainSideAnimation } from "./mainside.animation"; @Component({ selector : 'layout-mainside', templateUrl : './mainside.template.html', styleUrls : [ './mainside.style.css' + ], + animations : [ + mainSideAnimation ] }) @@ -16,9 +19,18 @@ export class LayoutMainSide{ @Input() sideWidth : number = 300 @Output() panelShowStateChanged : EventEmitter<boolean> = new EventEmitter() + @Output() panelAnimationFlag : EventEmitter<boolean> = new EventEmitter() togglePanelShow(){ this.showSide = !this.showSide this.panelShowStateChanged.emit(this.showSide) } + + animationStart(){ + this.panelAnimationFlag.emit(true) + } + + animationEnd(){ + this.panelAnimationFlag.emit(false) + } } \ No newline at end of file diff --git a/src/layouts/mainside/mainside.template.html b/src/layouts/mainside/mainside.template.html index 85db9a1f6ebb587a046eca656dcaf0e4e3b429d5..89609a3b357e34d975a3389b4a5448f30eeb1431 100644 --- a/src/layouts/mainside/mainside.template.html +++ b/src/layouts/mainside/mainside.template.html @@ -13,8 +13,9 @@ </i> </div> <div - *ngIf = "showSide" - [style.flexBasis]="sideWidth + 'px'" + [@collapseSide] = "{ value : showSide ? 'visible' : 'collapsed' , params : { sideWidth : sideWith } }" + (@collapseSide.start) = "animationStart()" + (@collapseSide.done) = "animationEnd()" sidecontent> <ng-content select="[sidecontent]"> </ng-content> diff --git a/src/plugin_examples/jugex/script.js b/src/plugin_examples/jugex/script.js index 6a88f9f2c1f9077d6e715967258fad7159554e6d..5a267d9b0ab00331c5526c0655000caaef718d66 100644 --- a/src/plugin_examples/jugex/script.js +++ b/src/plugin_examples/jugex/script.js @@ -546,8 +546,6 @@ domDownload.appendChild(col2) return [domDownload, col1, col2] } - - debugger container.removeChild(analysisCard) const resultCard = document.createElement('fzj-xg-webjugex-result-success-card') diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index aa4a5e77b82afdc6f987fdebd63cdb56e061aabb..09096fe46d7c788a3715f531350b41f8d9b44b45 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -114,7 +114,18 @@ span.regionSelected color : #dbb556 } +span.regionNotSelected, +span.regionSelected +{ + cursor : default; +} + markdown-dom pre code { white-space:pre; +} + +.highlight +{ + background-color:rgba(150,150,0,0.5); } \ No newline at end of file diff --git a/src/res/ext/bigbrain.json b/src/res/ext/bigbrain.json index fe9c346f86d64273b7316800924ae8017ce0c9a7..98a8fa262adcfac87b78f8363ce05a793bc236d4 100644 --- a/src/res/ext/bigbrain.json +++ b/src/res/ext/bigbrain.json @@ -1 +1 @@ -{"name":"Big Brain (Histology)","type":"template","species":"Human","useTheme":"light","nehubaId":" grey value: ","nehubaConfigURL":"res/json/bigbrainNehubaConfig.json","parcellations":[{"name":"Grey/White matter","type":"parcellation","ngData":null,"ngId":" tissue type: ","regions":[{"name":"Grey matter","labelIndex":100,"rgb":[200,200,200],"children":[]},{"name":"White matter","labelIndex":200,"rgb":[255,255,255],"children":[]}]}],"properties":{"name":"Big Brain (Histology)","description":"An ultrahigh resolution 3D model of a complete human brain (20 micron isotropic resolution), developed in a collaborative effort between the teams of Dr. Katrin Amunts and Dr. Karl Zilles (Forschungszentrum Jülich) and Dr. Alan Evans (Montreal Neurological Institute). Based on 7404 digitized histological brain sections, this so far unique reconstruction provides unprecedented neuroanatomical insight. The dataset contains a complete gray and white matter classification with corresponding surface reconstructions"}} \ No newline at end of file +{"name":"Big Brain (Histology)","type":"template","species":"Human","useTheme":"light","nehubaId":" grey value: ","nehubaConfigURL":"res/json/bigbrainNehubaConfig.json","parcellations":[{"name":"Grey/White matter","type":"parcellation","ngData":null,"ngId":" tissue type: ","regions":[{"name":"Grey matter","labelIndex":100,"rgb":[200,200,200],"children":[]},{"name":"White matter","labelIndex":200,"rgb":[255,255,255],"children":[]}]}],"properties":{"name":"Big Brain (Histology)","description":"An ultrahigh resolution 3D model of a complete human brain (20 micron isotropic resolution), developed in a collaborative effort between the teams of Dr. Katrin Amunts and Dr. Karl Zilles (Forschungszentrum Jülich) and Dr. Alan Evans (Montreal Neurological Institute). Based on 7404 digitized histological brain sections, this so far unique reconstruction provides unprecedented neuroanatomical insight. The dataset contains a complete gray and white matter classification with corresponding surface reconstructions","publications":[{"doi":"https://doi.org/10.1126/science.1235381","citation":"K. Amunts, A. Evans et al.: BigBrain: An Ultrahigh-Resolution 3D Human Brain Model. Science 2013"},{"doi":"http://bigbrain.loris.ca","citation":"http://bigbrain.loris.ca"}]}} \ No newline at end of file diff --git a/src/res/images/HBP_Primary_RGB_BlackText.png b/src/res/images/HBP_Primary_RGB_BlackText.png new file mode 100644 index 0000000000000000000000000000000000000000..3b39d00490c4274351e6ea4dcd4851bfc33da14d Binary files /dev/null and b/src/res/images/HBP_Primary_RGB_BlackText.png differ diff --git a/src/res/images/HBP_Primary_RGB_WhiteText.png b/src/res/images/HBP_Primary_RGB_WhiteText.png new file mode 100644 index 0000000000000000000000000000000000000000..891e8847fb5bc8fdffbb6784b0cc3db3129ddbc3 Binary files /dev/null and b/src/res/images/HBP_Primary_RGB_WhiteText.png differ diff --git a/src/ui/banner/banner.component.ts b/src/ui/banner/banner.component.ts index 78ac4ccfc5b0072d04517d32c690bdc761aa5a0f..70ae80f8fcdc2786bceb77d69c73c03d167fc883 100644 --- a/src/ui/banner/banner.component.ts +++ b/src/ui/banner/banner.component.ts @@ -1,8 +1,8 @@ import { Component, OnDestroy } from "@angular/core"; import { Store, select } from "@ngrx/store"; -import { ViewerStateInterface, safeFilter, SELECT_PARCELLATION, extractLabelIdx, SELECT_REGIONS, NEWVIEWER, getLabelIndexMap, isDefined } from "../../services/stateStore.service"; -import { Observable, Subscription, merge } from "rxjs"; -import { map, filter } from "rxjs/operators"; +import { ViewerStateInterface, safeFilter, SELECT_PARCELLATION, extractLabelIdx, SELECT_REGIONS, NEWVIEWER, getLabelIndexMap, isDefined, CHANGE_NAVIGATION } from "../../services/stateStore.service"; +import { Observable, Subscription, merge, Subject } from "rxjs"; +import { map, filter, debounceTime, buffer } from "rxjs/operators"; import { FilterNameBySearch } from "../../util/pipes/filterNameBySearch.pipe"; @Component({ @@ -25,6 +25,7 @@ export class AtlasBanner implements OnDestroy{ public selectedTemplate : any public selectedParcellation : any public selectedRegions : any[] = [] + private navigation : {position : [number,number,number]} = {position : [0,0,0]} private subscriptions : Subscription[] = [] @@ -73,8 +74,28 @@ export class AtlasBanner implements OnDestroy{ this.selectedRegions = ev }) ) + + this.subscriptions.push( + this.handleRegionTreeClickSubject.pipe( + buffer( + this.handleRegionTreeClickSubject.pipe( + debounceTime(200) + ) + ) + ).subscribe(arr=>arr.length > 1 ? this.doubleClick(arr[0]) : this.singleClick(arr[0])) + ) + + + this.subscriptions.push( + this.store.pipe( + select('viewerState'), + safeFilter('navigation'), + map(obj=>obj.navigation) + ).subscribe((navigation:any)=>this.navigation = navigation) + ) } + ngOnDestroy(){ this.subscriptions.forEach(s=>s.unsubscribe()) } @@ -104,7 +125,25 @@ export class AtlasBanner implements OnDestroy{ }) } - handleClickRegion(obj:any){ + /* double click navigate to the interested area */ + private doubleClick(obj:any){ + if( !(obj && obj.inputItem && obj.inputItem.position) ){ + return + } + + this.store.dispatch({ + type : CHANGE_NAVIGATION, + navigation : { + position : obj.inputItem.position, + animation : { + /* empty object is enough to be truthy */ + } + }, + }) + } + + /* single click selects/deselects region(s) */ + private singleClick(obj:any){ const region = obj.inputItem const selectedSet = new Set(extractLabelIdx(region)) const intersection = new Set([...this.selectedRegions.map(r=>r.labelIndex)].filter(v=>selectedSet.has(v))) @@ -116,6 +155,12 @@ export class AtlasBanner implements OnDestroy{ }) } + private handleRegionTreeClickSubject : Subject<any> = new Subject() + + handleClickRegion(obj:any){ + this.handleRegionTreeClickSubject.next(obj) + } + displayActiveTemplate(template:any){ return `<small>Template</small> <small class = "mute-text">${template ? '(' + template.name + ')' : ''}</small> <span class = "caret"></span>` } @@ -124,12 +169,18 @@ export class AtlasBanner implements OnDestroy{ return `<small>Parcellation</small> <small class = "mute-text">${parcellation ? '(' + parcellation.name + ')' : ''}</small> <span class = "caret"></span>` } + private insertHighlight(name:string,searchTerm:string):string{ + const regex = new RegExp(searchTerm,'gi') + return name.replace(regex,(s)=>`<span class = "highlight">${s}</span>`) + } + displayTreeNode(item:any){ return typeof item.labelIndex !== 'undefined' && this.selectedRegions.findIndex(re=>re.labelIndex === Number(item.labelIndex)) >= 0 ? - `<span class = "regionSelected">${item.name}</span>` : - `<span class = "regionNotSelected">${item.name}</span>` + `<span class = "regionSelected">${this.insertHighlight(item.name,this.searchTerm)}</span>` : + `<span class = "regionNotSelected">${this.insertHighlight(item.name,this.searchTerm)}</span>` } + /* TODO obsolete? */ get treeHeaderText():string{ return '' } diff --git a/src/ui/databrowser/databrowser.component.ts b/src/ui/databrowser/databrowser.component.ts index 85eb07f474e0a4c354dfd3babe57b0734a154790..96f6e9d06ce55300958ff4328180a20b0024f82e 100644 --- a/src/ui/databrowser/databrowser.component.ts +++ b/src/ui/databrowser/databrowser.component.ts @@ -323,7 +323,9 @@ export class DataBrowserUI implements OnDestroy,OnInit{ type : CHANGE_NAVIGATION, navigation : { position : position, - positionReal : true + animation : { + + } } }) } diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 5f25d436aad8d6540baee01b968f0a7ddd1a61a9..3eb347cb30e4d80cb54ce6d024917631cdd30baf 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -6,6 +6,7 @@ import { Observable, Subscription, fromEvent, combineLatest, merge } from "rxjs" import { filter,map, take, scan, debounceTime, distinctUntilChanged } from "rxjs/operators"; import * as export_nehuba from 'export_nehuba' import { AtlasViewerAPIServices } from "../../atlasViewer/atlasViewer.apiService.service"; +import { timedValues } from "../../util/generator"; @Component({ selector : 'ui-nehuba-container', @@ -36,7 +37,6 @@ export class NehubaContainer implements OnInit,OnDestroy,AfterViewInit{ public onHoverSegment$ : Observable<any> private navigationChanges$ : Observable<any> - private redrawObservable$ : Observable<any> public spatialResultsVisible$ : Observable<number> private selectedTemplate : any | null @@ -44,7 +44,7 @@ export class NehubaContainer implements OnInit,OnDestroy,AfterViewInit{ public fetchedSpatialData : DataEntry[] = [] private cr : ComponentRef<NehubaViewerUnit> - private nehubaViewer : NehubaViewerUnit + public nehubaViewer : NehubaViewerUnit private regionsLabelIndexMap : Map<number,any> = new Map() private subscriptions : Subscription[] = [] @@ -71,7 +71,9 @@ export class NehubaContainer implements OnInit,OnDestroy,AfterViewInit{ this.loadedParcellation$ = this.store.pipe( select('viewerState'), safeFilter('parcellationSelected'), - map(state=>state.parcellationSelected)) + map(state=>state.parcellationSelected), + distinctUntilChanged() + ) this.selectedRegions$ = this.store.pipe( select('viewerState'), @@ -92,11 +94,6 @@ export class NehubaContainer implements OnInit,OnDestroy,AfterViewInit{ map(state=>state.fetchedSpatialData) ) - this.redrawObservable$ = this.store.pipe( - select('uiState'), - safeFilter('sidePanelOpen') - ) - this.navigationChanges$ = this.store.pipe( select('viewerState'), safeFilter('navigation'), @@ -205,13 +202,6 @@ export class NehubaContainer implements OnInit,OnDestroy,AfterViewInit{ this.dedicatedView$.subscribe((this.handleDedicatedView).bind(this)) ) - this.subscriptions.push( - this.redrawObservable$.subscribe(()=>{ - if(this.nehubaViewer) - this.nehubaViewer.nehubaViewer.redraw() - }) - ) - /* setup init view state */ combineLatest( this.navigationChanges$, @@ -495,11 +485,63 @@ export class NehubaContainer implements OnInit,OnDestroy,AfterViewInit{ } handleDispatchedNavigationChange(navigation){ - /* set this.oldnavigation to represent the state of the store */ - this.oldNavigation = Object.assign({},this.oldNavigation,navigation) - this.nehubaViewer.setNavigationState(Object.assign({},this.oldNavigation,{ - positionReal : true - })) + + /* extract the animation object */ + const { animation, ..._navigation } = navigation + + if( animation ){ + /* animated */ + + const gen = timedValues() + const dest = Object.assign({},_navigation) + /* this.oldNavigation is old */ + const delta = Object.assign({}, ...Object.keys(dest).filter(key=>key !== 'positionReal').map(key=>{ + const returnObj = {} + returnObj[key] = typeof dest[key] === 'number' ? + dest[key] - this.oldNavigation[key] : + typeof dest[key] === 'object' ? + dest[key].map((val,idx)=>val - this.oldNavigation[key][idx]) : + true + return returnObj + })) + + const animate = ()=>{ + const next = gen.next() + const d = next.value + + this.nehubaViewer.setNavigationState( + Object.assign({}, ...Object.keys(dest).filter(k=>k !== 'positionReal').map(key=>{ + const returnObj = {} + returnObj[key] = typeof dest[key] === 'number' ? + dest[key] - ( delta[key] * ( 1 - d ) ) : + dest[key].map((val,idx)=>val - ( delta[key][idx] * ( 1 - d ) ) ) + return returnObj + }),{ + positionReal : true + }) + ) + + if( !next.done ){ + requestAnimationFrame(()=>animate()) + }else{ + + /* set this.oldnavigation to represent the state of the store */ + /* animation done, set this.oldNavigation */ + this.oldNavigation = Object.assign({},dest) + } + } + requestAnimationFrame(()=>animate()) + } else { + /* not animated */ + + /* set this.oldnavigation to represent the state of the store */ + /* since the emitted change of navigation state is debounced, we can safely set this.oldNavigation to the destination */ + this.oldNavigation = Object.assign({},this.oldNavigation,_navigation) + + this.nehubaViewer.setNavigationState(Object.assign({},_navigation,{ + positionReal : true + })) + } } /* related to info-card */ @@ -558,6 +600,51 @@ export class NehubaContainer implements OnInit,OnDestroy,AfterViewInit{ Array.from(this.nehubaViewer.navPosVoxel.map(n=> isNaN(n) ? 0 : n)).join(' , ') : `[0,0,0] (neubaViewer is undefined)` } + + get showCitation(){ + return this.selectedTemplate && this.selectedTemplate.properties && this.selectedTemplate.properties.publications && this.selectedTemplate.properties.publications.constructor === Array + } + + resetNavigation(){ + const initialNgState = this.selectedTemplate.nehubaConfig.dataset.initialNgState + + const perspectiveZoom = initialNgState ? initialNgState.perspectiveZoom : undefined + const perspectiveOrientation = initialNgState ? initialNgState.perspectiveOrientation : undefined + const zoom = initialNgState ? + initialNgState.navigation ? + initialNgState.navigation.zoomFactor : + undefined : + undefined + + const position = initialNgState ? + initialNgState.navigation ? + initialNgState.navigation.pose ? + initialNgState.navigation.pose.position.voxelCoordinates ? + initialNgState.navigation.pose.position.voxelCoordinates : + undefined : + undefined : + undefined : + undefined + + const orientation = [0,0,0,1] + + this.store.dispatch({ + type : CHANGE_NAVIGATION, + navigation : Object.assign({}, + { + perspectiveZoom, + perspectiveOrientation, + zoom, + position, + orientation + },{ + positionReal : false, + animation : { + + } + }) + }) + } } export const CM_THRESHOLD = `0.05` diff --git a/src/ui/nehubaContainer/nehubaContainer.style.css b/src/ui/nehubaContainer/nehubaContainer.style.css index 1adeb5bccea9afe544dfe94606782dc3e78d2c31..7062e973d437773271ff0082efe92b1682d07b99 100644 --- a/src/ui/nehubaContainer/nehubaContainer.style.css +++ b/src/ui/nehubaContainer/nehubaContainer.style.css @@ -36,7 +36,6 @@ div[statusCard] left:1em; bottom:1em; width : 20em; - height:7em; pointer-events: all; } @@ -89,4 +88,29 @@ div[landmarkMasterContainer] > div > [landmarkContainer] > div small[onHoverSegment] { margin-left:2em; +} + +div[citationContainer] +{ + max-width:300px; +} + +div[citationContainer] > a +{ + font-size: 90%; + text-align: left; + display:block; + white-space: normal; + padding-bottom:0px; +} + + +div[citationContainer] > a:not(:first-child) +{ + padding-top:0px; +} + +hr +{ + margin : 0.2em 2em; } \ No newline at end of file diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 5e810777cfa0d8dd7ff07a0d5312fd06ec9533d5..89b48be2bdd024f41480f13406972720140813f6 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -66,15 +66,35 @@ *ngIf = "viewerLoaded"> <div statusCard> + <!-- perhaps use a dedicated publication card next time (?) --> + <div + *ngIf = "showCitation" + citationContainer> + <a + *ngFor = "let publication of selectedTemplate.properties.publications" + href = "{{ publication.doi }}" + class = "btn btn-link"> + {{ publication.citation }} + </a> + </div> + + <hr /> + <span class = "btn btn-link" (click)="statusPanelRealSpace = !statusPanelRealSpace"> {{statusPanelRealSpace ? 'RealSpace' : 'VoxelSpace'}} </span> + + <span + (click) = "resetNavigation()" + class = "btn btn-link"> + reset navigation + </span> <br /> <div textContainer> - Navigation: + <small>Navigation: </small> <input (keydown.enter) = "textNavigateTo(navigateInput.value)" (keydown.tab) = "textNavigateTo(navigateInput.value)" @@ -84,13 +104,13 @@ navigateInput/> <br /> - Mouse: + <small>Mouse: </small> <small> {{ mouseCoord }} </small> <br /> <small onHoverSegment> - {{ onHoverSegment$ | async }} + {{ onHoverSegmentName$ | async }} </small> </div> </div> diff --git a/src/util/generator.ts b/src/util/generator.ts new file mode 100644 index 0000000000000000000000000000000000000000..6409a61fe65c1b3a8b81e9c6d3bd096cba860c09 --- /dev/null +++ b/src/util/generator.ts @@ -0,0 +1,15 @@ +export function* timedValues(sec:number = 500,mode:string = 'linear'){ + const startTime = Date.now() + + const getValue = (fraction) =>{ + switch (mode){ + case 'linear': + default: + return fraction < 1 ? fraction : 1 + } + } + while((Date.now() - startTime) < sec){ + yield getValue( (Date.now() - startTime) / sec ) + } + return 1 +} \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js index c8eb77d96461b482b2144761700c67a4ea04ac14..cd251dee634d871ad155294ed5e2c904eadde080 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -1,5 +1,4 @@ const webpack = require('webpack') -const HtmlWebpackPlugin = require('html-webpack-plugin') const path = require('path') module.exports = { @@ -42,10 +41,7 @@ module.exports = { ] }, plugins : [ - new webpack.ContextReplacementPlugin(/@angular(\\|\/)core(\\|\/)/,path.join(__dirname,'src')), - new HtmlWebpackPlugin({ - template : 'src/index.html' - }) + new webpack.ContextReplacementPlugin(/@angular(\\|\/)core(\\|\/)/,path.join(__dirname,'src')) ], resolve : { extensions : [ diff --git a/webpack.dev.js b/webpack.dev.js index c2197f8706b05ab2db0673772247a2352ec4013b..82f9a8dcb073d5f1c023712db97e0820550ecce2 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -2,6 +2,8 @@ const common = require('./webpack.common.js') const merge = require('webpack-merge') const path = require('path') const ngAssets = require('./webpack.ngassets') +const HtmlWebpackPlugin = require('html-webpack-plugin') + module.exports = merge(common,ngAssets,{ entry : './src/main.ts', @@ -10,5 +12,11 @@ module.exports = merge(common,ngAssets,{ filename : 'main.js', path : path.resolve(__dirname,'dist/dev') }, - devtool:'source-map' + devtool:'source-map', + + plugins : [ + new HtmlWebpackPlugin({ + template : 'src/index.html' + }) + ] }) \ No newline at end of file diff --git a/webpack.export.js b/webpack.export.js new file mode 100644 index 0000000000000000000000000000000000000000..1bb833dc1a37acf2f16642bee245f3c34ce3de70 --- /dev/null +++ b/webpack.export.js @@ -0,0 +1,27 @@ +const merge = require('webpack-merge') +const common = require('./webpack.common.js') +const ngAssets = require('./webpack.ngassets') +const path = require('path') +const ClosureCompilerPlugin = require('webpack-closure-compiler') +const HtmlWebpackPlugin = require('html-webpack-plugin') + +module.exports = merge(common,ngAssets,{ + entry : './src/atlasViewerExports/main.export.ts', + mode : 'development', + output : { + filename : 'export.js', + path : path.resolve(__dirname,'dist/export') + }, + plugins : [ + // new ClosureCompilerPlugin({ + // compiler : { + // compilation_level : 'SIMPLE' + // }, + // concurrency : 4 + // }), + + new HtmlWebpackPlugin({ + template : './src/atlasViewerExports/export.html' + }) + ] +}) \ No newline at end of file diff --git a/webpack.prod.js b/webpack.prod.js index 87339cbca0a212f635bc58508c79a7fa4b132bad..485b3b908834823d8aa9a22a77a17e8f93a13b58 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -4,6 +4,7 @@ const Uglify = require('uglifyjs-webpack-plugin') const path = require('path') const ClosureCompilerPlugin = require('webpack-closure-compiler') const ngAssets = require('./webpack.ngassets') +const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = merge(common,ngAssets,{ entry : './src/main.ts', @@ -17,6 +18,10 @@ module.exports = merge(common,ngAssets,{ compilation_level : 'SIMPLE' }, concurrency : 4 + }), + + new HtmlWebpackPlugin({ + template : 'src/index.html' }) ] }) \ No newline at end of file