Skip to content
Snippets Groups Projects
Commit 2df61a65 authored by Xiao Gui's avatar Xiao Gui
Browse files

Merge remote-tracking branch 'origin/dev' into chore_migSiibraApi

parents d763603e 8a4b1d35
No related branches found
No related tags found
No related merge requests found
Showing
with 540 additions and 22 deletions
......@@ -71,7 +71,7 @@ jobs:
if: success()
runs-on: ubuntu-latest
env:
GITHUB_API_ROOT: https://api.github.com/repos/HumanBrainProject/interactive-viewer
GITHUB_API_ROOT: https://api.github.com/repos/fzj-inm1-bda/siibra-explorer
needs: build-docker-img
steps:
......
name: '[undeploy from OKD]'
# only trigger on delete non master/staging branch
on:
delete:
branches:
- '!master'
- '!staging'
jobs:
remove-deploy:
runs-on: ubuntu-latest
steps:
- uses: action/checkout@v2
- name: 'Set env var'
run: |
echo "Using github.ref: $GITHUB_REF"
BRANCH_NAME=${GITHUB_REF#refs/heads/}
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
echo "OKD_URL=https://okd-dev.hbp.eu:443" >> $GITHUB_ENV
echo "OKD_SECRET=${{ secrets.OKD_DEV_SECRET }}" >> $GITHUB_ENV
echo "OKD_PROJECT=interactive-atlas-viewer" >> $GITHUB_ENV
echo "Remove deploy from dev cluster..."
- name: 'Login via oc cli'
run: |
oc login $OKD_URL --token=$OKD_SECRET
oc project $OKD_PROJECT
# sanitized branchname == remove _ / and lowercase everything
SANITIZED_BRANCH_NAME=$(echo ${BRANCH_NAME//[_\/]/} | awk '{ print tolower($0) }')
echo "SANITIZED_BRANCH_NAME=$SANITIZED_BRANCH_NAME" >> $GITHUB_ENV
echo "Working branch name: $BRANCH_NAME, sanitized branch name: $SANITIZED_BRANCH_NAME"
- name: 'List and delete all labelled resoures'
run: |
oc get all \
-l template=siibra-explorer-branch-deploy-template \
-l app=siibra-explorer-branch-deploy-$SANITIZED_BRANCH_NAME
oc delete all \
-l template=siibra-explorer-branch-deploy-template \
-l app=siibra-explorer-branch-deploy-$SANITIZED_BRANCH_NAME
......@@ -6,6 +6,7 @@
OPEN: 'Open',
EXPAND: 'Expand',
COLLAPSE: 'Collapse',
COPY_TO_CLIPBOARD: 'Copy to clipboard',
OPEN_IN_NEW_WINDOW: 'Open in a new window',
// dataset specific
......@@ -55,7 +56,20 @@
// additional volumes
TOGGLE_SHOW_LAYER_CONTROL: `Show layer control`,
ADDITIONAL_VOLUME_CONTROL: 'Additional volumes control'
ADDITIONAL_VOLUME_CONTROL: 'Additional volumes control',
//Viewer mode
VIEWER_MODE_ANNOTATING: 'annotating',
// Annotations
USER_ANNOTATION_LIST: 'user annotations footer',
USER_ANNOTATION_IMPORT: 'Import annotations',
USER_ANNOTATION_EXPORT: 'Export all of my annotations',
USER_ANNOTATION_EXPORT_SINGLE: 'Export annotation',
USER_ANNOTATION_HIDE: 'user annotations hide',
USER_ANNOTATION_DELETE: 'Delete annotation',
GOTO_ANNOTATION_ROI: 'Navigate to annotation location of interest',
EXIT_ANNOTATION_MODE: 'Exit annotation mode'
}
exports.IDS = {
......
......@@ -16,6 +16,7 @@
- Preliminary support for freesurfer (#900)
- Allow for finer controls over meshes display (#883, #470)
- Added `quick tour` feature (#899)
- Added user annotation feature (#886, #888, #887)
## Under the hood stuff
......
......@@ -2,10 +2,3 @@
{
position: relative;
}
.header-container
{
padding: 16px;
margin: -16px!important;
padding-top: 6rem;
}
......@@ -9,7 +9,7 @@
</button>
<mat-card class="mat-elevation-z4">
<div class="header-container bg-50-grey-20">
<div class="sidenav-cover-header-container bg-50-grey-20">
<mat-card-title>
<ng-content select="[region-of-interest]"></ng-content>
<div *ngIf="!fetchFlag; else isLoadingTmpl">
......
......@@ -20,10 +20,3 @@ mat-icon
font-size: 95%;
line-height: normal;
}
.header-container
{
padding: 16px;
margin: -16px!important;
padding-top: 6rem;
}
......@@ -2,7 +2,7 @@
<!-- rgbDarkmode must be checked for strict equality to true/false
as if rgb is undefined, rgbDarkmode will be null/undefined
which is falsy -->
<div class="header-container"
<div class="sidenav-cover-header-container"
[ngClass]="{'darktheme': rgbDarkmode === true, 'lighttheme': rgbDarkmode === false}"
[style.backgroundColor]="rgbString">
<mat-card-title>
......
......@@ -150,18 +150,18 @@ export class IEEGRecordingsCmp extends RegionFeatureBase implements ISingleFeatu
}, [])
)
private clickIntp(ev: any, next: Function) {
private clickIntp(ev: any): boolean {
let hoveredLandmark = null
this.regionFeatureService.onHoverLandmarks$.pipe(
take(1)
).subscribe(val => {
hoveredLandmark = val
})
if (!hoveredLandmark) return next()
if (!hoveredLandmark) return true
const isOne = this.landmarksLoaded.some(lm => {
return lm['_']['electrodeId'] === hoveredLandmark['_']['electrodeId']
})
if (!isOne) return next()
if (!isOne) return true
this.exploreElectrode$.next(hoveredLandmark['_']['electrodeId'])
}
}
export interface ViewerAnnotation {
id: string
position1: number[]
position2: number[]
name: string
description: string
type: string
circular: boolean
atlas: {name: string, id: string}
template: {name: string, id: string}
annotationVisible: boolean
}
export interface GroupedAnnotation {
id: string
position1?: number[]
position2?: number[]
annotations?: PolygonAnnotations[]
positions?: PolygonPositions[]
dimension?: string
name: string
description: string
type: string
circular?: boolean
atlas: {name: string, id: string}
template: {name: string, id: string}
annotationVisible: boolean
}
export interface PolygonAnnotations {
id: string
position1: number[]
position2: number[]
}
export interface PolygonPositions {
position: number[]
lines: {id: string, point: number}[]
}
export interface AnnotationType {
name: string
class: string
type: string
action: string
}
import {Component, ViewChild} from "@angular/core";
import {ARIA_LABELS} from "common/constants";
import { ModularUserAnnotationToolService } from "../tools/service";
import { IAnnotationGeometry, TExportFormats } from "../tools/type";
import { ComponentStore } from "src/viewerModule/componentStore";
import { map, startWith, tap } from "rxjs/operators";
import { Observable } from "rxjs";
import { TZipFileConfig } from "src/zipFilesOutput/type";
import { TFileInputEvent } from "src/getFileInput/type";
import { FileInputDirective } from "src/getFileInput/getFileInput.directive";
import { MatSnackBar } from "@angular/material/snack-bar";
import { unzip } from "src/zipFilesOutput/zipFilesOutput.directive";
const README = 'EXAMPLE OF READ ME TEXT'
@Component({
selector: 'annotation-list',
templateUrl: './annotationList.template.html',
styleUrls: ['./annotationList.style.css'],
providers: [
ComponentStore,
]
})
export class AnnotationList {
public ARIA_LABELS = ARIA_LABELS
@ViewChild(FileInputDirective)
fileInput: FileInputDirective
public managedAnnotations$ = this.annotSvc.spaceFilteredManagedAnnotations$
public manAnnExists$ = this.managedAnnotations$.pipe(
map(arr => !!arr && arr.length > 0),
startWith(false)
)
public filesExport$: Observable<TZipFileConfig[]> = this.managedAnnotations$.pipe(
startWith([] as IAnnotationGeometry[]),
map(manAnns => {
const readme = {
filename: 'README.md',
filecontent: README,
}
const annotationSands = manAnns.map(ann => {
return {
filename: `${ann.id}.sands.json`,
filecontent: JSON.stringify(ann.toSands(), null, 2),
}
})
return [ readme, ...annotationSands ]
})
)
constructor(
private annotSvc: ModularUserAnnotationToolService,
private snackbar: MatSnackBar,
cStore: ComponentStore<{ useFormat: TExportFormats }>,
) {
cStore.setState({
useFormat: 'sands'
})
}
public hiddenAnnotations$ = this.annotSvc.hiddenAnnotations$
toggleManagedAnnotationVisibility(id: string) {
this.annotSvc.toggleAnnotationVisibilityById(id)
}
private parseAndAddAnnotation(input: string) {
const json = JSON.parse(input)
const annotation = this.annotSvc.parseAnnotationObject(json)
this.annotSvc.importAnnotation(annotation)
}
async handleImportEvent(ev: TFileInputEvent<'text' | 'file'>){
try {
const clearFileInputAndInform = () => {
if (this.fileInput) {
this.fileInput.clear()
}
this.snackbar.open('Annotation imported successfully!', 'Dismiss', {
duration: 3000
})
}
if (ev.type === 'text') {
const input = (ev as TFileInputEvent<'text'>).payload.input
/**
* parse as json, and go through the parsers
*/
this.parseAndAddAnnotation(input)
clearFileInputAndInform()
return
}
if (ev.type === 'file') {
const files = (ev as TFileInputEvent<'file'>).payload.files
if (files.length === 0) throw new Error(`Need at least one file.`)
if (files.length > 1) throw new Error(`Parsing multiple files are not yet supported`)
const file = files[0]
const isJson = /\.json$/.test(file.name)
const isZip = /\.zip$/.test(file.name)
if (isZip) {
const files = await unzip(file)
const sands = files.filter(f => /\.json$/.test(f.filename))
for (const sand of sands) {
this.parseAndAddAnnotation(sand.filecontent)
}
clearFileInputAndInform()
}
if (isJson) {
const reader = new FileReader()
reader.onload = evt => {
const out = evt.target.result
this.parseAndAddAnnotation(out as string)
clearFileInputAndInform()
}
reader.onerror = e => { throw e }
reader.readAsText(file, 'utf-8')
}
/**
* check if zip or json
*/
return
}
} catch (e) {
this.snackbar.open(`Error importing: ${e.toString()}`, 'Dismiss', {
duration: 3000
})
}
}
}
.inactive-filter {
color: #bababa;
}
:host-context([darktheme="true"]) .hovering-header {
background-color: #737373;
}
:host-context([darktheme="false"]) .hovering-header {
background-color: rgb(245, 245, 245);
}
<mat-card class="mat-elevantion-z4 h-100">
<div class="sidenav-cover-header-container bg-50-grey-20">
<!-- title -->
<mat-card-title>
My Annotations
</mat-card-title>
<!-- actions -->
<mat-card-subtitle>
<!-- import -->
<ng-template #importMessageTmpl>
Please select a annotation file to import.
</ng-template>
<button mat-icon-button
(file-input-directive)="handleImportEvent($event)"
[file-input-directive-title]="ARIA_LABELS.USER_ANNOTATION_IMPORT"
[file-input-directive-text]="true"
[file-input-directive-file]="true"
[file-input-directive-message]="importMessageTmpl"
[attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_IMPORT"
[matTooltip]="ARIA_LABELS.USER_ANNOTATION_IMPORT">
<i class="fas fa-folder-open"></i>
</button>
<!-- export -->
<button mat-icon-button
[zip-files-output]="filesExport$ | async"
zip-files-output-zip-filename="exported_annotations.zip"
[attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT"
[matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT"
[disabled]="!(manAnnExists$ | async)">
<i class="fas fa-download"></i>
</button>
</mat-card-subtitle>
</div>
<!-- content -->
<mat-card-content class="mt-4 ml-15px-n mr-15px-n pb-4">
<!-- list of annotations -->
<ng-template [ngIf]="managedAnnotations$ | async" [ngIfElse]="placeholderTmpl" let-managedAnnotations>
<mat-accordion *ngIf="managedAnnotations.length > 0; else placeholderTmpl"
[attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_LIST"
class="h-100 d-flex flex-column overflow-auto">
<!-- expansion panel -->
<mat-expansion-panel *ngFor="let managedAnnotation of managedAnnotations"
hideToggle>
<mat-expansion-panel-header [ngClass]="{'highlight': managedAnnotation.highlighted }">
<mat-panel-title class="d-flex align-items-center">
<!-- toggle visibility -->
<button
mat-icon-button
iav-stop="click"
(click)="toggleManagedAnnotationVisibility(managedAnnotation.id)">
<i [ngClass]="(hiddenAnnotations$ | async | annotationVisiblePipe : managedAnnotation) ? 'fa-eye' : 'fa-eye-slash'" class="fas"></i>
</button>
<span class="flex-shrink-1 flex-grow-1" [ngClass]="{ 'text-muted': !managedAnnotation.name }">
{{ managedAnnotation | singleAnnotationNamePipe : managedAnnotation.name }}
</span>
<i class="flex-shrink-0 flex-grow-0" [ngClass]="managedAnnotation | singleannotationClsIconPipe"></i>
</mat-panel-title>
</mat-expansion-panel-header>
<!-- single annotation edit body -->
<ng-template matExpansionPanelContent>
<div class="d-flex">
<!-- spacer for inset single-annotation-unit -->
<div class="w-3em flex-grow-0 flex-shrink-0"></div>
<single-annotation-unit [single-annotation-unit-annotation]="managedAnnotation"
class="flex-grow-1 flex-shrink-1">
</single-annotation-unit>
</div>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</ng-template>
<!-- place holder when no annotations exist -->
<ng-template #placeholderTmpl>
<div class="p-4 text-muted">
<p>Start by adding an annotation, or import existing annotations.</p>
</div>
</ng-template>
</mat-card-content>
</mat-card>
import { Component, Inject, OnDestroy, Optional } from "@angular/core";
import { Store } from "@ngrx/store";
import { ModularUserAnnotationToolService } from "../tools/service";
import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper";
import { ARIA_LABELS } from 'common/constants'
import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util";
import { TContextArg } from "src/viewerModule/viewer.interface";
import { TContextMenuReg } from "src/contextMenuModule";
import { MatSnackBar } from "@angular/material/snack-bar";
@Component({
selector: 'annotating-tools-panel',
templateUrl: './annotationMode.template.html',
styleUrls: ['./annotationMode.style.css']
})
export class AnnotationMode implements OnDestroy{
public ARIA_LABELS = ARIA_LABELS
public moduleAnnotationTypes: {
instance: {
name: string
iconClass: string
}
onClick: Function
}[] = []
private onDestroyCb: Function[] = []
constructor(
private store$: Store<any>,
private modularToolSvc: ModularUserAnnotationToolService,
snackbar: MatSnackBar,
@Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor,
@Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu<TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>>>
) {
this.moduleAnnotationTypes = this.modularToolSvc.moduleAnnotationTypes
const stopClickProp = () => false
if (clickInterceptor) {
const { register, deregister } = clickInterceptor
register(stopClickProp)
this.onDestroyCb.push(() => deregister(stopClickProp))
}
if (ctxMenuInterceptor) {
const { deregister, register } = ctxMenuInterceptor
register(stopClickProp)
this.onDestroyCb.push(() => deregister(stopClickProp))
}
this.modularToolSvc.loadStoredAnnotations()
.catch(e => {
snackbar.open(`Loading annotations from storage failed: ${e.toString()}`, 'Dismiss', {
duration: 3000
})
})
}
exitAnnotationMode(){
this.store$.dispatch(
viewerStateSetViewerMode({
payload: null
})
)
}
deselectTools(){
this.modularToolSvc.deselectTools()
}
ngOnDestroy(){
while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()()
}
}
.tab-toggle-container
{
margin-left: -3rem;
padding-top: 0;
padding-bottom: 0;
}
.tab-toggle
{
margin: 0.25rem -1rem 0.25rem 0rem;
padding-right: 1rem;
text-align: right;
}
\ No newline at end of file
<mat-card class="d-inline-flex flex-column pe-all tab-toggle-container"
[iav-key-listener]="[{ type: 'keydown', key: 'Escape', target: 'document', capture: true }]"
(iav-key-event)="deselectTools()">
<button
*ngFor="let moduleAnnotType of moduleAnnotationTypes"
mat-button
class="tab-toggle"
(click)="moduleAnnotType.onClick()"
[color]="(moduleAnnotType.instance.toolSelected$ | async) ? 'primary' : 'basic'"
type="button">
<i [class]="moduleAnnotType.instance.iconClass"></i>
</button>
<mat-divider class="d-block"></mat-divider>
<button
mat-button
(click)="exitAnnotationMode()"
class="tab-toggle"
[matTooltip]="ARIA_LABELS.EXIT_ANNOTATION_MODE"
color="warn">
<i class="fas fa-times"></i>
</button>
</mat-card>
\ No newline at end of file
import { Pipe, PipeTransform } from "@angular/core";
import { IAnnotationGeometry } from "./tools/type";
@Pipe({
name: 'annotationVisiblePipe',
pure: true
})
export class AnnotationVisiblePipe implements PipeTransform{
public transform(hiddenAnns: IAnnotationGeometry[], thisAnn: IAnnotationGeometry): boolean {
return hiddenAnns.findIndex(a => a.id === thisAnn.id) < 0
}
}
import { Directive, HostListener, Inject, Input, Optional } from "@angular/core";
import { viewerStateSetViewerMode } from "src/services/state/viewerState/actions";
import { ARIA_LABELS } from "common/constants";
import { select, Store } from "@ngrx/store";
import { TContextArg } from "src/viewerModule/viewer.interface";
import { TContextMenuReg } from "src/contextMenuModule";
import { CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util";
import { ModularUserAnnotationToolService } from "../tools/service";
import { Subscription } from "rxjs";
import { viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors";
@Directive({
selector: '[annotation-switch]'
})
export class AnnotationSwitch {
@Input('annotation-switch-mode')
mode: 'toggle' | 'off' | 'on' = 'on'
private currMode = null
private subs: Subscription[] = []
constructor(
private store$: Store<any>,
private svc: ModularUserAnnotationToolService,
@Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu<TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>>>
) {
this.subs.push(
this.store$.pipe(
select(viewerStateViewerModeSelector)
).subscribe(mode => {
this.currMode = mode
})
)
}
@HostListener('click')
onClick() {
let payload = null
if (this.mode === 'on') payload = ARIA_LABELS.VIEWER_MODE_ANNOTATING
if (this.mode === 'off') {
if (this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING) payload = null
else return
}
if (this.mode === 'toggle') {
payload = this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING
? null
: ARIA_LABELS.VIEWER_MODE_ANNOTATING
}
this.store$.dispatch(
viewerStateSetViewerMode({ payload })
)
}
}
import { Pipe, PipeTransform } from "@angular/core";
import { IAnnotationGeometry } from "./tools/type";
@Pipe({
name: 'filterAnnotationsBySpace',
pure: true
})
export class FilterAnnotationsBySpace implements PipeTransform{
public transform(annotations: IAnnotationGeometry[], space: { '@id': string }): IAnnotationGeometry[]{
return annotations.filter(ann => ann.space["@id"] === space["@id"])
}
}
\ No newline at end of file
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment