From d0f5ea79d48b5e24e6a86cc5cff1fc43360150eb Mon Sep 17 00:00:00 2001 From: Oluwaseun Ogedengbe Date: Sun, 18 Dec 2022 20:36:19 -0500 Subject: [PATCH] this allows importCSV to utilize segment selection (#612) (#614) * this allows importCSV to utilize segment selection * update to allow any case types and allow coordinates w/o brackets --- .../annotation/annotation_layer_view.ts | 125 ++++++++++-------- src/neuroglancer/save_state/save_state.ts | 5 +- src/neuroglancer/user_report/user_report.ts | 2 +- src/neuroglancer/util/br.ts | 9 ++ 4 files changed, 84 insertions(+), 57 deletions(-) create mode 100644 src/neuroglancer/util/br.ts diff --git a/src/neuroglancer/annotation/annotation_layer_view.ts b/src/neuroglancer/annotation/annotation_layer_view.ts index eb18104292..ce94cecac1 100644 --- a/src/neuroglancer/annotation/annotation_layer_view.ts +++ b/src/neuroglancer/annotation/annotation_layer_view.ts @@ -13,9 +13,11 @@ import {StatusMessage} from 'neuroglancer/status'; import {TrackableBoolean, TrackableBooleanCheckbox} from 'neuroglancer/trackable_boolean'; import {getPositionSummary, SelectedAnnotationState, UserLayerWithAnnotations} from 'neuroglancer/ui/annotations'; import {HidingList} from 'neuroglancer/ui/hiding_list'; +import {br} from 'neuroglancer/util/br'; import {Borrowed, Owned} from 'neuroglancer/util/disposable'; import {mat4, transformVectorByMat4, vec3} from 'neuroglancer/util/geom'; import {formatIntegerBounds, formatIntegerPoint} from 'neuroglancer/util/spatial_units'; +import {Uint64} from 'neuroglancer/util/uint64'; import {ColorWidget} from 'neuroglancer/widget/color'; import {MinimizableGroupWidget} from 'neuroglancer/widget/minimizable_group'; import {RangeWidget} from 'neuroglancer/widget/range'; @@ -308,6 +310,8 @@ export class AnnotationLayerView extends Tab { const importCSVButton = document.createElement('button'); const importCSVForm = document.createElement('form'); const importCSVFileSelect = document.createElement('input'); + const segmentCSVOverrideCheckbox = document.createElement('input'); + const segmentCSVOverrideLabel = document.createElement('label'); exportToCSVButton.id = 'exportToCSVButton'; exportToCSVButton.textContent = 'Export to CSV'; exportToCSVButton.addEventListener('click', () => { @@ -323,12 +327,19 @@ export class AnnotationLayerView extends Tab { }); importCSVForm.appendChild(importCSVFileSelect); importCSVFileSelect.addEventListener('change', () => { - this.importCSV(importCSVFileSelect.files); + this.importCSV(importCSVFileSelect.files, segmentCSVOverrideCheckbox.checked); importCSVForm.reset(); }); importCSVFileSelect.classList.add('neuroglancer-hidden-button'); + segmentCSVOverrideLabel.textContent = 'Select segments on import (Experimental): '; + segmentCSVOverrideLabel.title = + 'This requires that the segmentation source of the annotation layer when importing the CSV file is the same as the segmentation source when the file was exported. Imported IDs may be outdated.' + segmentCSVOverrideCheckbox.type = 'checkbox'; + const csvContainer = document.createElement('span'); - csvContainer.append(exportToCSVButton, importCSVButton, importCSVForm); + csvContainer.append( + exportToCSVButton, importCSVButton, br(), segmentCSVOverrideLabel, + segmentCSVOverrideCheckbox, importCSVForm); this.groupAnnotations.appendFixedChild(csvContainer); } @@ -1040,34 +1051,38 @@ export class AnnotationLayerView extends Tab { } // TODO: pull request to papa repo - private betterPapa = (inputFile: File|Blob): Promise => { - return new Promise((resolve) => { - Papa.parse(inputFile, { - complete: (results: any) => { - resolve(results); - } + private betterPapa = (inputFile: File|Blob): + Promise => { + return new Promise((resolve) => { + Papa.parse(inputFile, { + complete: (results: any) => { + resolve(results); + } + }); }); - }); - } + } - private stringToVec3 = (input: string): vec3 => { - // format: (x, y, z) - let raw = input.split(''); - raw.shift(); - raw.pop(); - let list = raw.join(''); - let val = list.split(',').map(v => parseInt(v, 10)); - return vec3.fromValues(val[0], val[1], val[2]); - } + private stringToVec3 = (input: string): + vec3 => { + // format: (x, y, z) + let list = input.replace(/[{()}[\]]/g, ''); + let val = list.split(',').map(v => parseInt(v, 10)); + return vec3.fromValues(val[0], val[1], val[2]); + } - private dimensionsToVec3 = (input: string): vec3 => { - // format: A × B × C - let raw = input.replace(/s/g, ''); - let val = raw.split('×').map(v => parseInt(v, 10)); - return vec3.fromValues(val[0], val[1], val[2]); - } + private dimensionsToVec3 = (input: string): vec3 => { + // format: A × B × C + let raw = input.replace(/s/g, ''); + let val = raw.split('×').map(v => parseInt(v, 10)); + return vec3.fromValues(val[0], val[1], val[2]); + } + private textToPoint = (point: string, transform: mat4, dimension?: boolean) => { + const parsedVec = dimension ? this.dimensionsToVec3(point) : this.stringToVec3(point); + const spatialPoint = this.voxelSize.spatialFromVoxel(tempVec3, parsedVec); + return vec3.transformMat4(vec3.create(), spatialPoint, transform); + } - private async importCSV(files: FileList|null) { + private async importCSV(files: FileList|null, segmentOverride: boolean = false) { const rawAnnotations = []; let successfulImport = 0; @@ -1081,61 +1096,59 @@ export class AnnotationLayerView extends Tab { if (!rawData.data.length) { continue; } - const annStrings = rawData.data; + const annStrings = rawData.data; const csvIdToRealAnnotationIdMap: {[key: string]: string} = {}; const childStorage: {[key: string]: string[]} = {}; - const textToPoint = (point: string, transform: mat4, dimension?: boolean) => { - const parsedVec = dimension ? this.dimensionsToVec3(point) : this.stringToVec3(point); - const spatialPoint = this.voxelSize.spatialFromVoxel(tempVec3, parsedVec); - return vec3.transformMat4(vec3.create(), spatialPoint, transform); - }; let row = -1; + for (const annProps of annStrings) { row++; - const type = annProps[7]; + const type = annProps[7].toLowerCase(); const parentId = annProps[6]; const annotationID: string|undefined = annProps[8]; const tags = annProps[3]; + const segments = annProps[5]; let raw = {id: makeAnnotationId(), description: annProps[4]}; switch (type) { - case 'AABB': - case 'Line': + case 'aabb': + case 'line': raw.type = - type === 'Line' ? AnnotationType.LINE : AnnotationType.AXIS_ALIGNED_BOUNDING_BOX; - (raw).pointA = textToPoint(annProps[0], this.annotationLayer.globalToObject); - (raw).pointB = textToPoint(annProps[1], this.annotationLayer.globalToObject); + type === 'line' ? AnnotationType.LINE : AnnotationType.AXIS_ALIGNED_BOUNDING_BOX; + (raw).pointA = this.textToPoint(annProps[0], this.annotationLayer.globalToObject); + (raw).pointB = this.textToPoint(annProps[1], this.annotationLayer.globalToObject); break; - case 'Point': + case 'point': raw.type = AnnotationType.POINT; - (raw).point = textToPoint(annProps[0], this.annotationLayer.globalToObject); + (raw).point = this.textToPoint(annProps[0], this.annotationLayer.globalToObject); break; - case 'Ellipsoid': + case 'ellipsoid': raw.type = AnnotationType.ELLIPSOID; - (raw).center = textToPoint(annProps[0], this.annotationLayer.globalToObject); + (raw).center = + this.textToPoint(annProps[0], this.annotationLayer.globalToObject); (raw).radii = - textToPoint(annProps[2], this.annotationLayer.globalToObject, true); + this.textToPoint(annProps[2], this.annotationLayer.globalToObject, true); break; - case 'Line Strip': - case 'Line Strip*': - case 'Spoke': - case 'Spoke*': - case 'Collection': - if (type === 'Line Strip' || type === 'Line Strip*') { + case 'line Strip': + case 'line Strip*': + case 'spoke': + case 'spoke*': + case 'collection': + if (type === 'line Strip' || type === 'line Strip*') { raw.type = AnnotationType.LINE_STRIP; (raw).connected = true; - (raw).looped = type === 'Line Strip*'; - } else if (type === 'Spoke' || type === 'Spoke*') { + (raw).looped = type === 'line Strip*'; + } else if (type === 'spoke' || type === 'spoke*') { raw.type = AnnotationType.SPOKE; (raw).connected = true; - (raw).wheeled = type === 'Spoke*'; + (raw).wheeled = type === 'spoke*'; } else { raw.type = AnnotationType.COLLECTION; (raw).connected = false; } (raw).childrenVisible = new TrackableBoolean(false, true); (raw).source = - textToPoint(annProps[0], this.annotationLayer.globalToObject); + this.textToPoint(annProps[0], this.annotationLayer.globalToObject); (raw).entry = (index: number) => (this.annotationLayer.source) .get((raw).entries[index]); @@ -1185,7 +1198,11 @@ export class AnnotationLayerView extends Tab { }); } // Segments not supported - + // getSelectedAssocatedSegment(this.annotationLayer) + // naively add segment directly from excel + if (segments && segmentOverride) { + raw.segments = segments.split(',').map((s: string) => Uint64.parseString(s)); + } rawAnnotations.push(raw); } successfulImport++; diff --git a/src/neuroglancer/save_state/save_state.ts b/src/neuroglancer/save_state/save_state.ts index 3644c15ecf..fc17844665 100644 --- a/src/neuroglancer/save_state/save_state.ts +++ b/src/neuroglancer/save_state/save_state.ts @@ -5,6 +5,7 @@ import {Dialog} from 'neuroglancer/dialog'; import {Overlay} from 'neuroglancer/overlay'; import {dismissUnshareWarning, getSaveToAddressBar, getUnshareWarning} from 'neuroglancer/preferences/user_preferences'; import {StatusMessage} from 'neuroglancer/status'; +import {br} from 'neuroglancer/util/br'; import {RefCounted} from 'neuroglancer/util/disposable'; import {getRandomHexString} from 'neuroglancer/util/random'; import {Trackable} from 'neuroglancer/util/trackable'; @@ -200,7 +201,8 @@ export class SaveState extends RefCounted { this.key = getRandomHexString(); params.set('local_id', this.key); - history.pushState({}, '', `${window.location.origin}${window.location.pathname}?${params.toString()}`); + history.pushState( + {}, '', `${window.location.origin}${window.location.pathname}?${params.toString()}`); } private reassign(master: any) { const hist = master.history; @@ -296,7 +298,6 @@ type FieldConfig = { class SaveDialog extends Overlay { constructor(public viewer: Viewer, jsonString?: string, getUrlType?: UrlType) { super(); - const br = () => document.createElement('br'); const jsonURLDefault = `LINK SHORTNER INACCESSIBLE`; const urlStart = `${window.location.origin}${window.location.pathname}`; diff --git a/src/neuroglancer/user_report/user_report.ts b/src/neuroglancer/user_report/user_report.ts index 562ba101da..69c3cf08fb 100644 --- a/src/neuroglancer/user_report/user_report.ts +++ b/src/neuroglancer/user_report/user_report.ts @@ -1,4 +1,5 @@ import {Overlay} from 'neuroglancer/overlay'; +import {br} from 'neuroglancer/util/br'; import {Viewer} from 'neuroglancer/viewer'; // TODO: css @@ -19,7 +20,6 @@ interface ItemConfig { className?: string; } -const br = () => document.createElement('br'); const simpleItem = (value: string, config: ItemConfig = { type: 'checkbox' diff --git a/src/neuroglancer/util/br.ts b/src/neuroglancer/util/br.ts new file mode 100644 index 0000000000..4085b521d0 --- /dev/null +++ b/src/neuroglancer/util/br.ts @@ -0,0 +1,9 @@ +/* +This is why JavaScript is a terrible language. +This is my manifesto. +This is my gift to the world. +Hark, and hear my words. +I am the one who knocks. +Behold, this is the beginning of the end. +*/ +export const br = () => document.createElement('br'); \ No newline at end of file