diff --git a/deploy/csp/index.js b/deploy/csp/index.js index 616a0ab03fe626d20af4c007eca18a0347e749a7..87a6fe19e8e0652322a121858aef0a7394b46721 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -124,6 +124,7 @@ module.exports = { } else { console.warn(`CSP Violation: no data received!`) } + res.status(204).end() }) } } diff --git a/docs/releases/v2.4.4.md b/docs/releases/v2.4.4.md index 184c33ef099225d7e22b99a0f69f7b94c0dc9a33..cadb5d9df79de2f9c768008c4a35d1b705a0bb81 100644 --- a/docs/releases/v2.4.4.md +++ b/docs/releases/v2.4.4.md @@ -1,5 +1,13 @@ # v2.4.4 +# Features + +- Allow name and description of annotations to be exported + ## Bugfixes - Fix version of connectivity web component + +## Under the hood stuff + +- Respond in csp violation reports diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts index 525f5eaa5dad6a2ca706664963bda383d8bef9ac..f3b0e82f73509655e5135fb13041645a4a83cb63 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -3,7 +3,7 @@ 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 { map, shareReplay, startWith } from "rxjs/operators"; import { Observable } from "rxjs"; import { TZipFileConfig } from "src/zipFilesOutput/type"; import { TFileInputEvent } from "src/getFileInput/type"; @@ -11,7 +11,7 @@ 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' +const README = `{id}.sands.json file contains the data of annotations. {id}.desc.json contains the metadata of annotations.` @Component({ selector: 'annotation-list', @@ -39,6 +39,7 @@ export class AnnotationList { public filesExport$: Observable<TZipFileConfig[]> = this.managedAnnotations$.pipe( startWith([] as IAnnotationGeometry[]), + shareReplay(1), map(manAnns => { const readme = { filename: 'README.md', @@ -50,7 +51,13 @@ export class AnnotationList { filecontent: JSON.stringify(ann.toSands(), null, 2), } }) - return [ readme, ...annotationSands ] + const annotationDesc = manAnns.map(ann => { + return { + filename: `${ann.id}.desc.json`, + filecontent: JSON.stringify(this.annotSvc.exportAnnotationMetadata(ann), null, 2) + } + }) + return [ readme, ...annotationSands, ...annotationDesc ] }) ) constructor( @@ -71,7 +78,7 @@ export class AnnotationList { private parseAndAddAnnotation(input: string) { const json = JSON.parse(input) const annotation = this.annotSvc.parseAnnotationObject(json) - this.annotSvc.importAnnotation(annotation) + if (annotation) this.annotSvc.importAnnotation(annotation) } async handleImportEvent(ev: TFileInputEvent<'text' | 'file'>){ diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html index 17ac890eafc6f4049b039ad7c36147a2d481642d..2bfd568c082e19312a3a29748ed2b6ad02abe042 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html @@ -26,7 +26,7 @@ <!-- export --> <button mat-icon-button - [zip-files-output]="filesExport$ | async" + [zip-files-output]="filesExport$" zip-files-output-zip-filename="exported_annotations.zip" [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT" [matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT" diff --git a/src/atlasComponents/userAnnotations/tools/delete.ts b/src/atlasComponents/userAnnotations/tools/delete.ts index dc817a65012bd3dbe5409fb8656d5b9d5bcb9990..4c47b673b553b04f3d0e9ab25716caf715ea70a7 100644 --- a/src/atlasComponents/userAnnotations/tools/delete.ts +++ b/src/atlasComponents/userAnnotations/tools/delete.ts @@ -7,6 +7,7 @@ import { AbsToolClass, IAnnotationEvents, IAnnotationGeometry, IAnnotationTools, export class ToolDelete extends AbsToolClass<Point> implements IAnnotationTools, OnDestroy { public subs: Subscription[] = [] + protected managedAnnotations = [] toolType: TToolType = 'deletion' iconClass = 'fas fa-trash' name = 'Delete' diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts index 1303612f991d1cadc53a1ce73ddf9f043babca52..a24dd9267fd9b993b2cc2cb8cfa84f3ed5350077 100644 --- a/src/atlasComponents/userAnnotations/tools/line.ts +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -192,7 +192,7 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On subs: Subscription[] = [] - private managedAnnotations: Line[] = [] + protected managedAnnotations: Line[] = [] public managedAnnotations$ = new Subject<Line[]>() onMouseMoveRenderPreview(pos: [number, number, number]) { @@ -311,14 +311,6 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On this.subs.forEach(s => s.unsubscribe()) } - addAnnotation(line: Line) { - const idx = this.managedAnnotations.findIndex(ann => ann.id === line.id) - if (idx >= 0) throw new Error(`Line annotation has already been added`) - line.remove = () => this.removeAnnotation(line.id) - this.managedAnnotations.push(line) - this.managedAnnotations$.next(this.managedAnnotations) - } - removeAnnotation(id: string){ const idx = this.managedAnnotations.findIndex(ann => ann.id === id) if (idx < 0) { diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts index 2dcafc2490a82b2dacb3bf5180b7457947de2770..43f13978df340405932be5c6fb39678a3596d83c 100644 --- a/src/atlasComponents/userAnnotations/tools/point.ts +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -1,5 +1,5 @@ import { AbsToolClass, getCoord, IAnnotationEvents, IAnnotationGeometry, IAnnotationTools, INgAnnotationTypes, TAnnotationEvent, TBaseAnnotationGeomtrySpec, TCallbackFunction, TNgAnnotationEv, TSandsPoint, TToolType } from "./type"; -import { merge, Observable, Subject, Subscription } from "rxjs"; +import { Observable, Subject, Subscription } from "rxjs"; import { OnDestroy } from "@angular/core"; import { filter, switchMapTo, takeUntil } from "rxjs/operators"; @@ -108,7 +108,7 @@ export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools, public iconClass = POINT_ICON_CLASS public subs: Subscription[] = [] - private managedAnnotations: Point[] = [] + protected managedAnnotations: Point[] = [] public managedAnnotations$ = new Subject<Point[]>() constructor( @@ -176,14 +176,6 @@ export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools, ) } - addAnnotation(point: Point){ - const found = this.managedAnnotations.find(p => p.id === point.id) - if (found) throw new Error(`Point annotation already added`) - point.remove = () => this.removeAnnotation(point.id) - this.managedAnnotations.push(point) - this.managedAnnotations$.next(this.managedAnnotations) - } - /** * @description remove managed annotation via id * @param id id of annotation diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index c33662c2afc022ae8240884c629946587057b662..304f3854ee3a99aa1806f3a738267a6f0653b0d2 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -89,7 +89,7 @@ export class Polygon extends IAnnotationGeometry{ } toString() { - return `Points: ${JSON.stringify(this.points.map(p => p.toString()))}, edges: ${JSON.stringify(this.edges)}.` + return `Name: ${this.name}, Desc: ${this.desc}, Points: ${JSON.stringify(this.points.map(p => p.toString()))}, edges: ${JSON.stringify(this.edges)}.` } toSands(): TSandsPolyLine{ @@ -233,7 +233,7 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo private selectedPoly: Polygon private lastAddedPoint: Point - private managedAnnotations: Polygon[] = [] + protected managedAnnotations: Polygon[] = [] public managedAnnotations$ = new Subject<Polygon[]>() public subs: Subscription[] = [] @@ -403,14 +403,6 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo ) } - addAnnotation(poly: Polygon){ - const idx = this.managedAnnotations.findIndex(ann => ann.id === poly.id) - if (idx >= 0) throw new Error(`Polygon already added.`) - poly.remove = () => this.removeAnnotation(poly.id) - this.managedAnnotations.push(poly) - this.managedAnnotations$.next(this.managedAnnotations) - } - removeAnnotation(id: string) { const idx = this.managedAnnotations.findIndex(ann => ann.id === id) if (idx < 0) { diff --git a/src/atlasComponents/userAnnotations/tools/select.ts b/src/atlasComponents/userAnnotations/tools/select.ts index 38a0b11ba39ec9df2ddbb20b875f01cd48575257..a06d36e988723f95db5ad30e926966642ce8a78c 100644 --- a/src/atlasComponents/userAnnotations/tools/select.ts +++ b/src/atlasComponents/userAnnotations/tools/select.ts @@ -10,6 +10,7 @@ export class ToolSelect extends AbsToolClass<Point> implements IAnnotationTools, toolType: TToolType = 'selecting' iconClass = 'fas fa-mouse-pointer' name = 'Select' + protected managedAnnotations = [] onMouseMoveRenderPreview(){ return [] diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index fde183e33b5d8fd56059d857f4c5dfb600502350..24c26c5a2d74ccb8eb96146e88642c11962eef3c 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -26,6 +26,17 @@ const IAV_VOXEL_SIZES_NM = { 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2': [1000000, 1000000, 1000000] } +type TAnnotationMetadata = { + id: string + name: string + desc: string +} + +const descType: 'siibra-ex/meta/desc' = 'siibra-ex/meta/desc' +type TTypedAnnMetadata = { + '@type': 'siibra-ex/meta/desc' +} & TAnnotationMetadata + function scanCollapse<T>(){ return (src: Observable<{ tool: string @@ -551,7 +562,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ const anns: IAnnotationGeometry[] = [] for (const obj of arr) { const geometry = this.parseAnnotationObject(obj) - anns.push(geometry) + if (geometry) anns.push(geometry) } for (const ann of anns) { @@ -559,6 +570,21 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } } + public exportAnnotationMetadata(ann: IAnnotationGeometry): TAnnotationMetadata & { '@type': 'siibra-ex/meta/desc' } { + return { + '@type': descType, + id: ann.id, + name: ann.name, + desc: ann.desc, + } + } + + /** + * stop gap measure when exporting/import annotations in sands format + * metadata (name/desc) will be saved in a separate metadata file + */ + private metadataMap = new Map<string, TAnnotationMetadata>() + private storeAnnotation(anns: IAnnotationGeometry[]){ const arr = [] for (const ann of anns) { @@ -626,25 +652,49 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }) } - parseAnnotationObject(json: TSands | TGeometryJson): IAnnotationGeometry{ + parseAnnotationObject(json: TSands | TGeometryJson | TTypedAnnMetadata): IAnnotationGeometry | null{ + let returnObj: IAnnotationGeometry if (json['@type'] === 'tmp/poly') { - return Polygon.fromSANDS(json) + returnObj = Polygon.fromSANDS(json) } if (json['@type'] === 'tmp/line') { - return Line.fromSANDS(json) + returnObj = Line.fromSANDS(json) } if (json['@type'] === 'https://openminds.ebrains.eu/sands/CoordinatePoint') { - return Point.fromSANDS(json) + returnObj = Point.fromSANDS(json) } if (json['@type'] === 'siibra-ex/annotation/point') { - return Point.fromJSON(json) + returnObj = Point.fromJSON(json) } if (json['@type'] === 'siibra-ex/annotation/line') { - return Line.fromJSON(json) + returnObj = Line.fromJSON(json) } if (json['@type'] === 'siibra-ex/annotation/polyline') { - return Polygon.fromJSON(json) + returnObj = Polygon.fromJSON(json) + } + if (json['@type'] === descType) { + const existingAnn = this.managedAnnotations.find(ann => json.id === ann.id) + if (existingAnn) { + + // potentially overwriting existing name and desc... + // maybe should show warning? + existingAnn.setName(json.name) + existingAnn.setDesc(json.desc) + return existingAnn + } else { + const { id, name, desc } = json + this.metadataMap.set(id, { id, name, desc }) + return + } + } else { + const metadata = this.metadataMap.get(returnObj.id) + if (returnObj && metadata) { + returnObj.setName(metadata?.name || null) + returnObj.setDesc(metadata?.desc || null) + this.metadataMap.delete(returnObj.id) + } } + if (returnObj) return returnObj throw new Error(`cannot parse annotation object`) } diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts index 1716457a166eea87cc6b9943b4c3ebed3538c03a..439d48c3b7ea338d1889890b9d8fbeb78fb49396 100644 --- a/src/atlasComponents/userAnnotations/tools/type.ts +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -16,9 +16,9 @@ export abstract class AbsToolClass<T extends IAnnotationGeometry> { public abstract name: string public abstract iconClass: string - public abstract addAnnotation(annotation: T): void public abstract removeAnnotation(id: string): void - public abstract managedAnnotations$: Observable<T[]> + public abstract managedAnnotations$: Subject<T[]> + protected abstract managedAnnotations: T[] = [] abstract subs: Subscription[] protected space: TBaseAnnotationGeomtrySpec['space'] @@ -152,6 +152,14 @@ export abstract class AbsToolClass<T extends IAnnotationGeometry> { }), )) ) + + public addAnnotation(geom: T) { + const found = this.managedAnnotations.find(ann => ann.id === geom.id) + if (found) found.remove() + geom.remove = () => this.removeAnnotation(geom.id) + this.managedAnnotations.push(geom) + this.managedAnnotations$.next(this.managedAnnotations) + } } export type TToolType = 'selecting' | 'drawing' | 'deletion' diff --git a/src/zipFilesOutput/zipFilesOutput.directive.ts b/src/zipFilesOutput/zipFilesOutput.directive.ts index bcf70bddfaf8e3e84ab97f887ae93ae8e95fcfc6..dc980728cfd8675ecfaaf57eb1c4317f74556ba0 100644 --- a/src/zipFilesOutput/zipFilesOutput.directive.ts +++ b/src/zipFilesOutput/zipFilesOutput.directive.ts @@ -2,6 +2,8 @@ import { Directive, HostListener, Inject, Input } from "@angular/core"; import { TZipFileConfig } from "./type"; import * as JSZip from "jszip"; import { DOCUMENT } from "@angular/common"; +import { isObservable, Observable } from "rxjs"; +import { take } from "rxjs/operators"; @Directive({ selector: '[zip-files-output]', @@ -10,15 +12,15 @@ import { DOCUMENT } from "@angular/common"; export class ZipFilesOutput { @Input('zip-files-output') - zipFiles: TZipFileConfig[] = [] + zipFiles: Observable<TZipFileConfig[]> | TZipFileConfig[] = [] @Input('zip-files-output-zip-filename') zipFilename = 'archive.zip' - @HostListener('click') - async onClick(){ + private async zipArray(arrZipConfig: TZipFileConfig[]){ + const zip = new JSZip() - for (const zipFile of this.zipFiles) { + for (const zipFile of arrZipConfig) { const { filecontent, filename, base64 } = zipFile zip.file(filename, filecontent, { base64 }) } @@ -32,6 +34,21 @@ export class ZipFilesOutput { this.doc.body.removeChild(anchor) URL.revokeObjectURL(anchor.href) } + + @HostListener('click') + async onClick(){ + if (Array.isArray(this.zipFiles)) { + await this.zipArray(this.zipFiles) + return + } + if (isObservable(this.zipFiles)) { + const zipFiles = await this.zipFiles.pipe( + take(1) + ).toPromise() + await this.zipArray(zipFiles) + return + } + } constructor( @Inject(DOCUMENT) private doc: Document ){