@jax-data-science/sci-image-visualizer 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/index.mjs +4 -1
- package/esm2022/lib/contracts/ports/region-io.port.mjs +1 -1
- package/esm2022/lib/contracts/region-editor-api.contract.mjs +1 -1
- package/esm2022/lib/implementations/osd/openseadragon-viewer-options.mjs +18 -2
- package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +9 -1
- package/esm2022/lib/implementations/osd/osd-tile-loading.mjs +71 -0
- package/esm2022/lib/implementations/osd/osd-viewer-like.mjs +1 -1
- package/esm2022/lib/implementations/osd/slice-cache.mjs +35 -6
- package/esm2022/lib/region-editor/region-editor.component.mjs +147 -4
- package/esm2022/lib/region-editor/region-persistence.service.mjs +9 -1
- package/esm2022/lib/region-import/label-classes.mjs +149 -0
- package/esm2022/lib/region-import/label-volume.mjs +194 -0
- package/esm2022/lib/region-import/region-import.mjs +71 -0
- package/esm2022/lib/routing-visualizer.service.mjs +93 -1
- package/esm2022/lib/store/region-source-scope.mjs +19 -4
- package/esm2022/lib/store/region-store.service.mjs +64 -14
- package/esm2022/lib/visualizer.component.mjs +28 -2
- package/fesm2022/{jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-BKQhpbHv.mjs → jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-BELcx0jv.mjs} +1049 -188
- package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-BELcx0jv.mjs.map +1 -0
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-z1IN-F7w.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-DEDSm_NL.mjs} +2 -2
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-z1IN-F7w.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-DEDSm_NL.mjs.map} +1 -1
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +1 -1
- package/index.d.ts +6 -0
- package/lib/contracts/ports/region-io.port.d.ts +24 -0
- package/lib/contracts/region-editor-api.contract.d.ts +19 -1
- package/lib/implementations/osd/openseadragon-viewer-options.d.ts +2 -0
- package/lib/implementations/osd/openseadragon-visualizer.service.d.ts +2 -0
- package/lib/implementations/osd/osd-tile-loading.d.ts +43 -0
- package/lib/implementations/osd/osd-viewer-like.d.ts +14 -0
- package/lib/implementations/osd/slice-cache.d.ts +13 -3
- package/lib/region-editor/region-editor.component.d.ts +26 -0
- package/lib/region-editor/region-persistence.service.d.ts +5 -1
- package/lib/region-import/label-classes.d.ts +39 -0
- package/lib/region-import/label-volume.d.ts +31 -0
- package/lib/region-import/region-import.d.ts +46 -0
- package/lib/routing-visualizer.service.d.ts +30 -0
- package/lib/store/region-source-scope.d.ts +8 -2
- package/lib/store/region-store.service.d.ts +15 -5
- package/lib/visualizer.component.d.ts +9 -0
- package/package.json +1 -1
- package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-BKQhpbHv.mjs.map +0 -1
|
@@ -6,7 +6,7 @@ import { LruCache, Colormap, buildLut, parseColor as parseColor$1, worldViewport
|
|
|
6
6
|
import { BehaviorSubject, firstValueFrom, Subject, Subscription, of, Observable, combineLatest, defer, from, EMPTY, merge } from 'rxjs';
|
|
7
7
|
import { timeout, debounceTime, takeUntil, switchMap, catchError, startWith, map } from 'rxjs/operators';
|
|
8
8
|
import * as ort from 'onnxruntime-web';
|
|
9
|
-
import { smoothMesh, surfaceNets, pointInRing as pointInRing$2, ringArea as ringArea$1, rasterizePolygon as rasterizePolygon$1,
|
|
9
|
+
import { smoothMesh, surfaceNets, pointInRing as pointInRing$2, ringArea as ringArea$1, traceContours, rasterizePolygon as rasterizePolygon$1, floodFill } from 'napari-js/geometry';
|
|
10
10
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
11
11
|
import * as i1$1 from '@angular/common';
|
|
12
12
|
import { CommonModule } from '@angular/common';
|
|
@@ -5814,6 +5814,418 @@ function fromWire(f) {
|
|
|
5814
5814
|
};
|
|
5815
5815
|
}
|
|
5816
5816
|
|
|
5817
|
+
/**
|
|
5818
|
+
* A label volume (a NIfTI-1 `.nii` / `.nii.gz` or a NRRD `.nrrd` of integer structure ids, an
|
|
5819
|
+
* atlas annotation say) read in the browser, sliced as the server opens the matching image:
|
|
5820
|
+
*
|
|
5821
|
+
* - **NRRD**: a 3D volume is cut along its FIRST axis, each section its third axis wide and its
|
|
5822
|
+
* second axis high, as jit-service opens a NRRD volume (`AtlasNrrdReader`: the Allen CCF's 528
|
|
5823
|
+
* coronal sections of 456 × 320, anterior first). A 2D NRRD is one slice.
|
|
5824
|
+
* - **NIfTI**: along its third axis, each slice its first axis wide and its second axis high, as
|
|
5825
|
+
* Bio-Formats reads one (no row flip).
|
|
5826
|
+
*
|
|
5827
|
+
* Only what label volumes use: a single file (NRRD with attached data, NIfTI-1 `n+1`), integer
|
|
5828
|
+
* or integer-valued float samples, either byte order, raw or gzip. Anything else throws rather
|
|
5829
|
+
* than be read approximately. Pure: usable from workers.
|
|
5830
|
+
*/
|
|
5831
|
+
/** Whether a file name is a label volume this reads. */
|
|
5832
|
+
function isLabelVolumeName(name) {
|
|
5833
|
+
return /\.(nrrd|nii|nii\.gz)$/i.test(name);
|
|
5834
|
+
}
|
|
5835
|
+
/** Read the label volume in `bytes`, named `name` (`.nrrd`, `.nii`, `.nii.gz`). */
|
|
5836
|
+
async function readLabelVolume(name, bytes, gunzip = platformGunzip) {
|
|
5837
|
+
if (/\.nrrd$/i.test(name))
|
|
5838
|
+
return readNrrd(bytes, gunzip);
|
|
5839
|
+
if (/\.nii(\.gz)?$/i.test(name))
|
|
5840
|
+
return readNifti(isGzip(bytes) ? await gunzip(bytes) : bytes);
|
|
5841
|
+
throw new Error(`${name}: not a NIfTI (.nii, .nii.gz) or NRRD (.nrrd) label volume`);
|
|
5842
|
+
}
|
|
5843
|
+
const SAMPLES = {
|
|
5844
|
+
u8: { bytes: 1, read: (v, at) => v.getUint8(at) },
|
|
5845
|
+
i8: { bytes: 1, read: (v, at) => v.getInt8(at) },
|
|
5846
|
+
u16: { bytes: 2, read: (v, at, le) => v.getUint16(at, le) },
|
|
5847
|
+
i16: { bytes: 2, read: (v, at, le) => v.getInt16(at, le) },
|
|
5848
|
+
u32: { bytes: 4, read: (v, at, le) => v.getUint32(at, le) },
|
|
5849
|
+
i32: { bytes: 4, read: (v, at, le) => v.getInt32(at, le) },
|
|
5850
|
+
f32: { bytes: 4, read: (v, at, le) => Math.round(v.getFloat32(at, le)) },
|
|
5851
|
+
f64: { bytes: 8, read: (v, at, le) => Math.round(v.getFloat64(at, le)) },
|
|
5852
|
+
};
|
|
5853
|
+
/** NRRD type names (every spelling the format allows) → sample type. */
|
|
5854
|
+
const NRRD_TYPES = {
|
|
5855
|
+
'signed char': 'i8',
|
|
5856
|
+
int8: 'i8',
|
|
5857
|
+
int8_t: 'i8',
|
|
5858
|
+
uchar: 'u8',
|
|
5859
|
+
'unsigned char': 'u8',
|
|
5860
|
+
uint8: 'u8',
|
|
5861
|
+
uint8_t: 'u8',
|
|
5862
|
+
short: 'i16',
|
|
5863
|
+
'short int': 'i16',
|
|
5864
|
+
'signed short': 'i16',
|
|
5865
|
+
'signed short int': 'i16',
|
|
5866
|
+
int16: 'i16',
|
|
5867
|
+
int16_t: 'i16',
|
|
5868
|
+
ushort: 'u16',
|
|
5869
|
+
'unsigned short': 'u16',
|
|
5870
|
+
'unsigned short int': 'u16',
|
|
5871
|
+
uint16: 'u16',
|
|
5872
|
+
uint16_t: 'u16',
|
|
5873
|
+
int: 'i32',
|
|
5874
|
+
'signed int': 'i32',
|
|
5875
|
+
int32: 'i32',
|
|
5876
|
+
int32_t: 'i32',
|
|
5877
|
+
uint: 'u32',
|
|
5878
|
+
'unsigned int': 'u32',
|
|
5879
|
+
uint32: 'u32',
|
|
5880
|
+
uint32_t: 'u32',
|
|
5881
|
+
float: 'f32',
|
|
5882
|
+
double: 'f64',
|
|
5883
|
+
};
|
|
5884
|
+
/** NIfTI datatype codes → sample type. */
|
|
5885
|
+
const NIFTI_TYPES = {
|
|
5886
|
+
2: 'u8',
|
|
5887
|
+
4: 'i16',
|
|
5888
|
+
8: 'i32',
|
|
5889
|
+
16: 'f32',
|
|
5890
|
+
64: 'f64',
|
|
5891
|
+
256: 'i8',
|
|
5892
|
+
512: 'u16',
|
|
5893
|
+
768: 'u32',
|
|
5894
|
+
};
|
|
5895
|
+
async function readNrrd(bytes, gunzip) {
|
|
5896
|
+
const end = headerEnd(bytes);
|
|
5897
|
+
if (end < 0)
|
|
5898
|
+
throw new Error('NRRD: no blank line ends the header');
|
|
5899
|
+
const lines = new TextDecoder('latin1').decode(bytes.subarray(0, end)).split(/\r?\n/);
|
|
5900
|
+
if (!/^NRRD\d{4}/.test(lines[0] ?? ''))
|
|
5901
|
+
throw new Error('NRRD: not a NRRD file');
|
|
5902
|
+
const field = {};
|
|
5903
|
+
for (const line of lines.slice(1)) {
|
|
5904
|
+
if (line.startsWith('#'))
|
|
5905
|
+
continue;
|
|
5906
|
+
const at = line.indexOf(': ');
|
|
5907
|
+
if (at > 0)
|
|
5908
|
+
field[line.slice(0, at).trim().toLowerCase()] = line.slice(at + 2).trim();
|
|
5909
|
+
}
|
|
5910
|
+
if (field['data file'] || field['datafile'])
|
|
5911
|
+
throw new Error('NRRD: detached data (a .nhdr) is not supported');
|
|
5912
|
+
const type = NRRD_TYPES[(field['type'] ?? '').toLowerCase()];
|
|
5913
|
+
if (!type)
|
|
5914
|
+
throw new Error(`NRRD: unsupported type "${field['type']}"`);
|
|
5915
|
+
const sizes = (field['sizes'] ?? '').split(/\s+/).filter(Boolean).map(Number);
|
|
5916
|
+
if (sizes.length !== 2 && sizes.length !== 3)
|
|
5917
|
+
throw new Error(`NRRD: need 2 or 3 dimensions, got ${sizes.length}`);
|
|
5918
|
+
const encoding = (field['encoding'] ?? 'raw').toLowerCase();
|
|
5919
|
+
let data = bytes.subarray(end);
|
|
5920
|
+
if (encoding === 'gzip' || encoding === 'gz')
|
|
5921
|
+
data = await gunzip(data);
|
|
5922
|
+
else if (encoding !== 'raw')
|
|
5923
|
+
throw new Error(`NRRD: unsupported encoding "${encoding}"`);
|
|
5924
|
+
const little = (field['endian'] ?? 'little').toLowerCase() !== 'big';
|
|
5925
|
+
// A 3D volume: sections along axis 0, each axis 2 wide and axis 1 high (jit-service's cut).
|
|
5926
|
+
const [n0, n1, n2] = sizes.length === 3 ? sizes : [sizes[0], sizes[1], 1];
|
|
5927
|
+
const axes = sizes.length === 3 ? { z: 0, x: 2, y: 1 } : { z: 2, x: 0, y: 1 };
|
|
5928
|
+
return volume('nrrd', [n0, n1, n2], axes, data, SAMPLES[type], little);
|
|
5929
|
+
}
|
|
5930
|
+
function readNifti(bytes) {
|
|
5931
|
+
if (bytes.length < 352)
|
|
5932
|
+
throw new Error('NIfTI: shorter than a header');
|
|
5933
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
5934
|
+
let little = true;
|
|
5935
|
+
if (view.getInt32(0, true) !== 348) {
|
|
5936
|
+
if (view.getInt32(0, false) !== 348)
|
|
5937
|
+
throw new Error('NIfTI: not a NIfTI-1 file');
|
|
5938
|
+
little = false;
|
|
5939
|
+
}
|
|
5940
|
+
const magic = new TextDecoder('latin1').decode(bytes.subarray(344, 347));
|
|
5941
|
+
if (magic !== 'n+1')
|
|
5942
|
+
throw new Error('NIfTI: a separate .hdr/.img pair is not supported');
|
|
5943
|
+
const ndim = view.getInt16(40, little);
|
|
5944
|
+
const dims = [1, 2, 3, 4].map((i) => view.getInt16(40 + 2 * i, little));
|
|
5945
|
+
if (ndim < 2)
|
|
5946
|
+
throw new Error(`NIfTI: need at least 2 dimensions, got ${ndim}`);
|
|
5947
|
+
if (ndim >= 4 && dims[3] > 1)
|
|
5948
|
+
throw new Error('NIfTI: a 4D file is not a label volume');
|
|
5949
|
+
const type = NIFTI_TYPES[view.getInt16(70, little)];
|
|
5950
|
+
if (!type)
|
|
5951
|
+
throw new Error(`NIfTI: unsupported datatype ${view.getInt16(70, little)}`);
|
|
5952
|
+
const offset = Math.max(352, Math.round(view.getFloat32(108, little)));
|
|
5953
|
+
const [n0, n1, n2] = [dims[0], dims[1], ndim >= 3 ? dims[2] : 1];
|
|
5954
|
+
// Slices along axis 2, each axis 0 wide and axis 1 high (Bio-Formats' planes).
|
|
5955
|
+
return volume('nifti', [n0, n1, n2], { z: 2, x: 0, y: 1 }, bytes.subarray(offset), SAMPLES[type], little);
|
|
5956
|
+
}
|
|
5957
|
+
function volume(format, n, axes, data, sample, little) {
|
|
5958
|
+
const count = n[0] * n[1] * n[2];
|
|
5959
|
+
if (!(count > 0))
|
|
5960
|
+
throw new Error(`${format}: empty volume`);
|
|
5961
|
+
if (data.length < count * sample.bytes) {
|
|
5962
|
+
throw new Error(`${format}: ${data.length} bytes of data, ${count * sample.bytes} needed`);
|
|
5963
|
+
}
|
|
5964
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
5965
|
+
const stride = [1, n[0], n[0] * n[1]];
|
|
5966
|
+
const width = n[axes.x];
|
|
5967
|
+
const height = n[axes.y];
|
|
5968
|
+
return {
|
|
5969
|
+
format,
|
|
5970
|
+
width,
|
|
5971
|
+
height,
|
|
5972
|
+
slices: n[axes.z],
|
|
5973
|
+
sliceLabels(z) {
|
|
5974
|
+
const out = new Uint32Array(width * height);
|
|
5975
|
+
const base = z * stride[axes.z];
|
|
5976
|
+
const sx = stride[axes.x];
|
|
5977
|
+
const sy = stride[axes.y];
|
|
5978
|
+
for (let y = 0; y < height; y++) {
|
|
5979
|
+
for (let x = 0; x < width; x++) {
|
|
5980
|
+
const v = sample.read(view, (base + x * sx + y * sy) * sample.bytes, little);
|
|
5981
|
+
out[y * width + x] = v > 0 ? v : 0;
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
return out;
|
|
5985
|
+
},
|
|
5986
|
+
};
|
|
5987
|
+
}
|
|
5988
|
+
/** The offset just past the header's blank line (`\n\n` or `\r\n\r\n`); -1 without one. */
|
|
5989
|
+
function headerEnd(bytes) {
|
|
5990
|
+
const limit = Math.min(bytes.length - 1, 1 << 20);
|
|
5991
|
+
for (let i = 0; i < limit; i++) {
|
|
5992
|
+
if (bytes[i] !== 10)
|
|
5993
|
+
continue;
|
|
5994
|
+
if (bytes[i + 1] === 10)
|
|
5995
|
+
return i + 2;
|
|
5996
|
+
if (bytes[i + 1] === 13 && bytes[i + 2] === 10)
|
|
5997
|
+
return i + 3;
|
|
5998
|
+
}
|
|
5999
|
+
return -1;
|
|
6000
|
+
}
|
|
6001
|
+
function isGzip(bytes) {
|
|
6002
|
+
return bytes.length > 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
6003
|
+
}
|
|
6004
|
+
async function platformGunzip(bytes) {
|
|
6005
|
+
if (typeof DecompressionStream === 'undefined')
|
|
6006
|
+
throw new Error('gzip data needs DecompressionStream');
|
|
6007
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
|
|
6008
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
6009
|
+
}
|
|
6010
|
+
|
|
6011
|
+
/** The look the Allen atlas region sets carry (see the brain-atlas example's GeoJSON). */
|
|
6012
|
+
const ATLAS_STYLE = {
|
|
6013
|
+
fillOpacity: 0.6,
|
|
6014
|
+
stroke: '#3c3c3c',
|
|
6015
|
+
strokeWidth: 1,
|
|
6016
|
+
labels: true,
|
|
6017
|
+
highlight: '#a020f0',
|
|
6018
|
+
};
|
|
6019
|
+
/**
|
|
6020
|
+
* The classes of a label volume from `ontology`, a parsed JSON document:
|
|
6021
|
+
*
|
|
6022
|
+
* - an **Allen structure graph** (`structure_graph_1.json`: `{ msg: [root] }`, or a node or a
|
|
6023
|
+
* list of nodes with `id`, `acronym`, `name`, `color_hex_triplet`, `graph_order` and
|
|
6024
|
+
* `children`): label = structure id, class = acronym, its tree the ontology's, drawn as the
|
|
6025
|
+
* atlas region sets are;
|
|
6026
|
+
* - a **region set description** (a `regionset.json`: `classHierarchy`, optional `style`), whose
|
|
6027
|
+
* classes name their label as `value` (or `id`), or with a `labels` map of value → class name.
|
|
6028
|
+
*
|
|
6029
|
+
* Without one (or with another document), each label is its own class `label <value>`.
|
|
6030
|
+
*/
|
|
6031
|
+
function labelClasses(ontology) {
|
|
6032
|
+
const allen = allenRoots(ontology);
|
|
6033
|
+
if (allen)
|
|
6034
|
+
return allenClasses(allen);
|
|
6035
|
+
if (ontology &&
|
|
6036
|
+
typeof ontology === 'object' &&
|
|
6037
|
+
Array.isArray(ontology.classHierarchy)) {
|
|
6038
|
+
return regionSetClasses(ontology);
|
|
6039
|
+
}
|
|
6040
|
+
return { classOf: unnamed };
|
|
6041
|
+
}
|
|
6042
|
+
function unnamed(value) {
|
|
6043
|
+
return { name: `label ${value}`, color: paletteColor(value) };
|
|
6044
|
+
}
|
|
6045
|
+
function allenRoots(doc) {
|
|
6046
|
+
const isNode = (n) => !!n &&
|
|
6047
|
+
typeof n === 'object' &&
|
|
6048
|
+
typeof n.id === 'number' &&
|
|
6049
|
+
typeof n.acronym === 'string';
|
|
6050
|
+
const msg = doc?.msg;
|
|
6051
|
+
const roots = Array.isArray(msg) ? msg : Array.isArray(doc) ? doc : doc ? [doc] : [];
|
|
6052
|
+
return roots.length && roots.every(isNode) ? roots : null;
|
|
6053
|
+
}
|
|
6054
|
+
function allenClasses(roots) {
|
|
6055
|
+
const byId = new Map();
|
|
6056
|
+
const hierarchy = [];
|
|
6057
|
+
const walk = (n, parent) => {
|
|
6058
|
+
const color = `#${(n.color_hex_triplet || '808080').toUpperCase()}`;
|
|
6059
|
+
byId.set(n.id, { name: n.acronym, color, title: n.name });
|
|
6060
|
+
hierarchy.push({
|
|
6061
|
+
name: n.acronym,
|
|
6062
|
+
...(n.name ? { title: n.name } : {}),
|
|
6063
|
+
...(parent ? { parent } : {}),
|
|
6064
|
+
color,
|
|
6065
|
+
...(n.graph_order != null ? { order: n.graph_order } : {}),
|
|
6066
|
+
});
|
|
6067
|
+
for (const c of n.children ?? [])
|
|
6068
|
+
walk(c, n.acronym);
|
|
6069
|
+
};
|
|
6070
|
+
for (const r of roots)
|
|
6071
|
+
walk(r, null);
|
|
6072
|
+
hierarchy.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
6073
|
+
return { classOf: (v) => byId.get(v) ?? unnamed(v), hierarchy, style: ATLAS_STYLE };
|
|
6074
|
+
}
|
|
6075
|
+
function regionSetClasses(doc) {
|
|
6076
|
+
const nodes = new Map(doc.classHierarchy.map((n) => [n.name, n]));
|
|
6077
|
+
const byValue = new Map();
|
|
6078
|
+
for (const n of doc.classHierarchy) {
|
|
6079
|
+
const v = typeof n.value === 'number' ? n.value : typeof n.id === 'number' ? n.id : undefined;
|
|
6080
|
+
if (v != null)
|
|
6081
|
+
byValue.set(v, n.name);
|
|
6082
|
+
}
|
|
6083
|
+
for (const [v, name] of Object.entries(doc.labels ?? {}))
|
|
6084
|
+
byValue.set(Number(v), name);
|
|
6085
|
+
const hierarchy = doc.classHierarchy.map(({ name, title, parent, color, order }) => ({
|
|
6086
|
+
name,
|
|
6087
|
+
...(title ? { title } : {}),
|
|
6088
|
+
...(parent ? { parent } : {}),
|
|
6089
|
+
...(color ? { color } : {}),
|
|
6090
|
+
...(order != null ? { order } : {}),
|
|
6091
|
+
}));
|
|
6092
|
+
return {
|
|
6093
|
+
classOf: (v) => {
|
|
6094
|
+
const name = byValue.get(v);
|
|
6095
|
+
const node = name ? nodes.get(name) : undefined;
|
|
6096
|
+
return node ? { name: node.name, color: node.color ?? paletteColor(v), title: node.title } : unnamed(v);
|
|
6097
|
+
},
|
|
6098
|
+
hierarchy,
|
|
6099
|
+
...(doc.style ? { style: doc.style } : {}),
|
|
6100
|
+
};
|
|
6101
|
+
}
|
|
6102
|
+
/** A colour per label value, spread around the hue circle. */
|
|
6103
|
+
function paletteColor(value) {
|
|
6104
|
+
const hue = (value * 137.508) % 360;
|
|
6105
|
+
const [r, g, b] = hsl(hue, 0.65, 0.55);
|
|
6106
|
+
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`.toUpperCase();
|
|
6107
|
+
}
|
|
6108
|
+
function hsl(h, s, l) {
|
|
6109
|
+
const k = (n) => (n + h / 30) % 12;
|
|
6110
|
+
const a = s * Math.min(l, 1 - l);
|
|
6111
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
6112
|
+
return [f(0), f(8), f(4)].map((c) => Math.round(c * 255));
|
|
6113
|
+
}
|
|
6114
|
+
const rgb$1 = (hex) => [1, 3, 5].map((o) => parseInt(hex.slice(o, o + 2), 16));
|
|
6115
|
+
/**
|
|
6116
|
+
* Slice `z` of `volume` as a GeoJSON FeatureCollection string: one feature per label on the
|
|
6117
|
+
* slice, its every piece (with holes) traced along pixel edges, in image pixels with y down;
|
|
6118
|
+
* classified by `classes`, which also give the collection its class tree and style.
|
|
6119
|
+
*/
|
|
6120
|
+
function labelSliceGeoJson(volume, z, classes) {
|
|
6121
|
+
const labels = volume.sliceLabels(z);
|
|
6122
|
+
const byLabel = new Map();
|
|
6123
|
+
for (const c of traceContours({ data: labels, width: volume.width, height: volume.height })) {
|
|
6124
|
+
const polygons = byLabel.get(c.value) ?? [];
|
|
6125
|
+
polygons.push([ring(c.outer), ...c.holes.map(ring)]);
|
|
6126
|
+
byLabel.set(c.value, polygons);
|
|
6127
|
+
}
|
|
6128
|
+
const features = [...byLabel].map(([value, polygons]) => {
|
|
6129
|
+
const cls = classes.classOf(value);
|
|
6130
|
+
return {
|
|
6131
|
+
type: 'Feature',
|
|
6132
|
+
id: String(value),
|
|
6133
|
+
geometry: { type: 'MultiPolygon', coordinates: polygons },
|
|
6134
|
+
properties: {
|
|
6135
|
+
objectType: 'annotation',
|
|
6136
|
+
name: cls.title ?? cls.name,
|
|
6137
|
+
classification: { name: cls.name, color: rgb$1(cls.color) },
|
|
6138
|
+
labelValue: value,
|
|
6139
|
+
},
|
|
6140
|
+
};
|
|
6141
|
+
});
|
|
6142
|
+
return JSON.stringify({
|
|
6143
|
+
type: 'FeatureCollection',
|
|
6144
|
+
...(classes.hierarchy ? { classHierarchy: classes.hierarchy } : {}),
|
|
6145
|
+
...(classes.style ? { style: classes.style } : {}),
|
|
6146
|
+
features,
|
|
6147
|
+
});
|
|
6148
|
+
}
|
|
6149
|
+
/** A closed GeoJSON ring from a flat `[x0, y0, …]` one. */
|
|
6150
|
+
function ring(flat) {
|
|
6151
|
+
const pts = [];
|
|
6152
|
+
for (let i = 0; i < flat.length; i += 2)
|
|
6153
|
+
pts.push([flat[i], flat[i + 1]]);
|
|
6154
|
+
if (pts.length)
|
|
6155
|
+
pts.push([...pts[0]]);
|
|
6156
|
+
return pts;
|
|
6157
|
+
}
|
|
6158
|
+
|
|
6159
|
+
/** `data`, named `name`, as an import file. A `File`'s own name is used when `name` is omitted. */
|
|
6160
|
+
function importFile(data, name = data.name ?? '') {
|
|
6161
|
+
const encoder = new TextEncoder();
|
|
6162
|
+
return {
|
|
6163
|
+
name,
|
|
6164
|
+
text: async () => typeof data === 'string'
|
|
6165
|
+
? data
|
|
6166
|
+
: data instanceof Blob
|
|
6167
|
+
? data.text()
|
|
6168
|
+
: new TextDecoder().decode(data instanceof Uint8Array ? data : new Uint8Array(data)),
|
|
6169
|
+
bytes: async () => typeof data === 'string'
|
|
6170
|
+
? encoder.encode(data)
|
|
6171
|
+
: data instanceof Blob
|
|
6172
|
+
? new Uint8Array(await data.arrayBuffer())
|
|
6173
|
+
: data instanceof Uint8Array
|
|
6174
|
+
? data
|
|
6175
|
+
: new Uint8Array(data),
|
|
6176
|
+
};
|
|
6177
|
+
}
|
|
6178
|
+
const leaf = (name) => name.split(/[\\/]/).pop() ?? name;
|
|
6179
|
+
const isRegionSet = (f) => leaf(f.name).toLowerCase() === 'regionset.json';
|
|
6180
|
+
const isGeoJson = (f) => /\.geojson$/i.test(f.name);
|
|
6181
|
+
const isJson = (f) => /\.json$/i.test(f.name);
|
|
6182
|
+
/**
|
|
6183
|
+
* What `files` import as: a label volume (`.nii`, `.nii.gz`, `.nrrd`) with the ontology JSON chosen
|
|
6184
|
+
* with it (a `regionset.json`, else an Allen `structure_graph*.json`, else any `.json`); several
|
|
6185
|
+
* region files (a folder of per-slice `.geojson`, in natural name order, its `regionset.json`
|
|
6186
|
+
* apart); or one region file (`.geojson` or `.json`). Null when none of them is one.
|
|
6187
|
+
*/
|
|
6188
|
+
function planRegionImport(files) {
|
|
6189
|
+
const volume = files.find((f) => isLabelVolumeName(f.name));
|
|
6190
|
+
if (volume) {
|
|
6191
|
+
const jsons = files.filter((f) => f !== volume && isJson(f));
|
|
6192
|
+
const ontology = jsons.find(isRegionSet) ??
|
|
6193
|
+
jsons.find((f) => /structure_graph.*\.json$/i.test(leaf(f.name))) ??
|
|
6194
|
+
jsons[0] ??
|
|
6195
|
+
null;
|
|
6196
|
+
return { kind: 'volume', volume, ontology };
|
|
6197
|
+
}
|
|
6198
|
+
const regionSet = files.find(isRegionSet) ?? null;
|
|
6199
|
+
const geojsons = files.filter((f) => isGeoJson(f));
|
|
6200
|
+
const regionFiles = geojsons.length ? geojsons : files.filter((f) => isJson(f) && !isRegionSet(f));
|
|
6201
|
+
if (regionFiles.length > 1 || (regionFiles.length === 1 && regionSet)) {
|
|
6202
|
+
const slices = [...regionFiles].sort((a, b) => naturalCompare(leaf(a.name), leaf(b.name)));
|
|
6203
|
+
return { kind: 'folder', slices, regionSet };
|
|
6204
|
+
}
|
|
6205
|
+
return regionFiles.length === 1 ? { kind: 'single', file: regionFiles[0] } : null;
|
|
6206
|
+
}
|
|
6207
|
+
/** "slice_2" before "slice_10": runs of digits compared as numbers. */
|
|
6208
|
+
function naturalCompare(a, b) {
|
|
6209
|
+
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
6210
|
+
}
|
|
6211
|
+
/**
|
|
6212
|
+
* A folder slice's GeoJSON with the set's class tree and style (its `regionset.json`) as the
|
|
6213
|
+
* collection's own members, unless the file has its own: what a served folder's slice carries.
|
|
6214
|
+
*/
|
|
6215
|
+
async function folderSliceGeoJson(file, regionSet) {
|
|
6216
|
+
const text = await file.text();
|
|
6217
|
+
if (!regionSet || (!regionSet.classHierarchy && !regionSet.style))
|
|
6218
|
+
return text;
|
|
6219
|
+
const doc = JSON.parse(text);
|
|
6220
|
+
if (doc['type'] !== 'FeatureCollection')
|
|
6221
|
+
return text;
|
|
6222
|
+
if (regionSet.classHierarchy && !doc['classHierarchy'])
|
|
6223
|
+
doc['classHierarchy'] = regionSet.classHierarchy;
|
|
6224
|
+
if (regionSet.style && !doc['style'])
|
|
6225
|
+
doc['style'] = regionSet.style;
|
|
6226
|
+
return JSON.stringify(doc);
|
|
6227
|
+
}
|
|
6228
|
+
|
|
5817
6229
|
/**
|
|
5818
6230
|
* The largest bbox (in pixels) {@link rasterizePolygon} allocates before it
|
|
5819
6231
|
* falls back to clipping at the window. Also the cap the wand/brush use to
|
|
@@ -9098,7 +9510,7 @@ function frameToRgba(cached, frameIndex) {
|
|
|
9098
9510
|
/** Lazy-import so onnxruntime-web is never pulled into unit tests or the
|
|
9099
9511
|
* initial bundle — only when segmentation actually runs. */
|
|
9100
9512
|
const onnxSessionFactory = async () => {
|
|
9101
|
-
const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-
|
|
9513
|
+
const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-DEDSm_NL.mjs');
|
|
9102
9514
|
return new OnnxSamSession();
|
|
9103
9515
|
};
|
|
9104
9516
|
/** Thrown inside a load that a model switch superseded. */
|
|
@@ -12838,6 +13250,14 @@ class RegionPersistenceService {
|
|
|
12838
13250
|
}
|
|
12839
13251
|
return out;
|
|
12840
13252
|
}
|
|
13253
|
+
/** Whether the host can pick regions on its server (see RegionIoPort.pickServerRegions). */
|
|
13254
|
+
canPickServerRegions() {
|
|
13255
|
+
return typeof this.io.pickServerRegions === 'function';
|
|
13256
|
+
}
|
|
13257
|
+
/** Let the user pick regions on the host's server; null when cancelled or unsupported. */
|
|
13258
|
+
pickServerRegions() {
|
|
13259
|
+
return this.io.pickServerRegions?.() ?? of(null);
|
|
13260
|
+
}
|
|
12841
13261
|
/** Write per-slice geojsons through the host port. */
|
|
12842
13262
|
saveSlices(slices) {
|
|
12843
13263
|
return this.io.saveSliceGeoJsons(slices);
|
|
@@ -14392,6 +14812,8 @@ class RegionEditorComponent {
|
|
|
14392
14812
|
this.persistence = persistence;
|
|
14393
14813
|
this.maskExport = maskExport;
|
|
14394
14814
|
this.isActive = true;
|
|
14815
|
+
/** The host's HttpClient: a served region set's requests go through it (its auth). */
|
|
14816
|
+
this.http = inject(HttpClient, { optional: true });
|
|
14395
14817
|
this.regions = [];
|
|
14396
14818
|
this.selectedRegions = [];
|
|
14397
14819
|
this.displayHelpDialog = false;
|
|
@@ -14517,6 +14939,8 @@ class RegionEditorComponent {
|
|
|
14517
14939
|
this.volumeRequest = 0;
|
|
14518
14940
|
this._pageKey = {};
|
|
14519
14941
|
this._page = [];
|
|
14942
|
+
/** An import from the server is in flight. */
|
|
14943
|
+
this.importing = false;
|
|
14520
14944
|
}
|
|
14521
14945
|
ngOnInit() {
|
|
14522
14946
|
this.showShapeLabel = this.regionApi.getShowShapeLabel();
|
|
@@ -15451,8 +15875,141 @@ class RegionEditorComponent {
|
|
|
15451
15875
|
}
|
|
15452
15876
|
this.changed();
|
|
15453
15877
|
}
|
|
15878
|
+
/** The files chosen in the import inputs: one GeoJSON, a label volume with its ontology, or a
|
|
15879
|
+
* folder of per-slice GeoJSON files. */
|
|
15454
15880
|
importRois(event) {
|
|
15455
|
-
|
|
15881
|
+
const files = Array.from(event.target.files ?? []);
|
|
15882
|
+
if (files.length)
|
|
15883
|
+
void this.importRegionFiles(files.map((f) => importFile(f, f.webkitRelativePath || f.name)));
|
|
15884
|
+
}
|
|
15885
|
+
/** Whether the host can pick regions on its server ("Import regions from server"). */
|
|
15886
|
+
get canImportFromServer() {
|
|
15887
|
+
return this.persistence.canPickServerRegions();
|
|
15888
|
+
}
|
|
15889
|
+
/** "Import regions from server": the host picks files or a served region set; they import as chosen files do. */
|
|
15890
|
+
importFromServer() {
|
|
15891
|
+
this.importing = true;
|
|
15892
|
+
this.changed();
|
|
15893
|
+
this.persistence.pickServerRegions().subscribe({
|
|
15894
|
+
next: (pick) => {
|
|
15895
|
+
if (!pick)
|
|
15896
|
+
return;
|
|
15897
|
+
if (pick.kind === 'files') {
|
|
15898
|
+
void this.importRegionFiles(pick.files.map((f) => importFile(f.data, f.name)));
|
|
15899
|
+
return;
|
|
15900
|
+
}
|
|
15901
|
+
void this.importServedRegionSet(pick.url, pick.name);
|
|
15902
|
+
},
|
|
15903
|
+
error: (err) => this.importFailed(err),
|
|
15904
|
+
complete: () => {
|
|
15905
|
+
this.importing = false;
|
|
15906
|
+
this.changed();
|
|
15907
|
+
},
|
|
15908
|
+
});
|
|
15909
|
+
}
|
|
15910
|
+
/**
|
|
15911
|
+
* Import `files` (see planRegionImport): one GeoJSON file as before; a label volume as one
|
|
15912
|
+
* region source per slice, traced from it the first time the slice is shown, its classes named
|
|
15913
|
+
* (and treed) by the ontology chosen with it; a folder as one source per slice file. Either
|
|
15914
|
+
* replaces the regions and sources of every slice.
|
|
15915
|
+
*/
|
|
15916
|
+
async importRegionFiles(files) {
|
|
15917
|
+
const plan = planRegionImport(files);
|
|
15918
|
+
if (!plan) {
|
|
15919
|
+
this.importFailed(new Error('Choose a GeoJSON file, a folder of GeoJSON files, or a .nii, .nii.gz or .nrrd label volume.'));
|
|
15920
|
+
return;
|
|
15921
|
+
}
|
|
15922
|
+
if (plan.kind === 'single') {
|
|
15923
|
+
await this.applyImportedRois(await plan.file.text());
|
|
15924
|
+
return;
|
|
15925
|
+
}
|
|
15926
|
+
const attach = this.regionApi.importSliceRegionSources?.bind(this.regionApi);
|
|
15927
|
+
if (!attach) {
|
|
15928
|
+
this.importFailed(new Error('This viewer cannot hold region sources.'));
|
|
15929
|
+
return;
|
|
15930
|
+
}
|
|
15931
|
+
// Taken before the files are read: decoding a volume takes a while, and the import must land
|
|
15932
|
+
// on the image it was started for, not on one shown meanwhile.
|
|
15933
|
+
const ticket = this.regionApi.regionImportTicket?.();
|
|
15934
|
+
try {
|
|
15935
|
+
const openers = new Map();
|
|
15936
|
+
let summary;
|
|
15937
|
+
if (plan.kind === 'volume') {
|
|
15938
|
+
const volume = await readLabelVolume(plan.volume.name, await plan.volume.bytes());
|
|
15939
|
+
const classes = labelClasses(plan.ontology ? JSON.parse(await plan.ontology.text()) : undefined);
|
|
15940
|
+
for (let z = 0; z < volume.slices; z++) {
|
|
15941
|
+
openers.set(z, () => openGeoJsonRegionSource(labelSliceGeoJson(volume, z, classes)));
|
|
15942
|
+
}
|
|
15943
|
+
summary = `${volume.slices} slices of ${volume.width} × ${volume.height}`;
|
|
15944
|
+
this.warnIfSizeDiffers(volume.width, volume.height);
|
|
15945
|
+
}
|
|
15946
|
+
else {
|
|
15947
|
+
const regionSet = plan.regionSet ? JSON.parse(await plan.regionSet.text()) : null;
|
|
15948
|
+
plan.slices.forEach((file, z) => openers.set(z, async () => openGeoJsonRegionSource(await folderSliceGeoJson(file, regionSet))));
|
|
15949
|
+
summary = `${plan.slices.length} slice files`;
|
|
15950
|
+
}
|
|
15951
|
+
const attached = attach(openers, ticket);
|
|
15952
|
+
if (attached === null) {
|
|
15953
|
+
this.importStale();
|
|
15954
|
+
return;
|
|
15955
|
+
}
|
|
15956
|
+
this.afterSourceImport();
|
|
15957
|
+
this.toast('success', 'Regions imported', `${summary}; ${attached} slice${attached === 1 ? '' : 's'} annotated.`);
|
|
15958
|
+
}
|
|
15959
|
+
catch (err) {
|
|
15960
|
+
this.importFailed(err);
|
|
15961
|
+
}
|
|
15962
|
+
}
|
|
15963
|
+
/** A region set the host's server serves (jit-service's /regions/{id}/), read through its HttpClient. */
|
|
15964
|
+
async importServedRegionSet(url, name) {
|
|
15965
|
+
const importSource = this.regionApi.importRegionSourceOpener?.bind(this.regionApi);
|
|
15966
|
+
if (!importSource) {
|
|
15967
|
+
this.importFailed(new Error('This viewer cannot hold region sources.'));
|
|
15968
|
+
return;
|
|
15969
|
+
}
|
|
15970
|
+
const ticket = this.regionApi.regionImportTicket?.();
|
|
15971
|
+
try {
|
|
15972
|
+
const applied = await importSource(() => openHttpRegionSource(url, { http: this.http ?? undefined }), ticket);
|
|
15973
|
+
if (applied === false) {
|
|
15974
|
+
this.importStale();
|
|
15975
|
+
return;
|
|
15976
|
+
}
|
|
15977
|
+
this.afterSourceImport();
|
|
15978
|
+
this.toast('success', 'Regions imported', name ?? url);
|
|
15979
|
+
}
|
|
15980
|
+
catch (err) {
|
|
15981
|
+
this.importFailed(err);
|
|
15982
|
+
}
|
|
15983
|
+
}
|
|
15984
|
+
/** An import that finished after another image was shown (or another import began): dropped. */
|
|
15985
|
+
importStale() {
|
|
15986
|
+
this.toast('info', 'Import discarded', 'The image changed while the regions were being read.');
|
|
15987
|
+
this.changed();
|
|
15988
|
+
}
|
|
15989
|
+
/** The table and selection after the regions were replaced by imported sources. */
|
|
15990
|
+
afterSourceImport() {
|
|
15991
|
+
this.selectedRegions = [];
|
|
15992
|
+
this.paginatorFirst = 0;
|
|
15993
|
+
this.commit([]);
|
|
15994
|
+
this.onSelectionChanged();
|
|
15995
|
+
}
|
|
15996
|
+
/** Warn when an imported volume's slices are not the image's size: its outlines would not fit. */
|
|
15997
|
+
warnIfSizeDiffers(width, height) {
|
|
15998
|
+
const meta = this.regionApi.getMaskImageSize();
|
|
15999
|
+
if (meta && (meta.width !== width || meta.height !== height)) {
|
|
16000
|
+
this.toast('info', 'Different size', `The volume's slices are ${width} × ${height}; the image is ${meta.width} × ${meta.height}.`);
|
|
16001
|
+
}
|
|
16002
|
+
}
|
|
16003
|
+
importFailed(err) {
|
|
16004
|
+
this.importing = false;
|
|
16005
|
+
this.messageService.add({
|
|
16006
|
+
key: VIZ_TOAST_KEY,
|
|
16007
|
+
severity: 'error',
|
|
16008
|
+
summary: 'Error importing regions',
|
|
16009
|
+
detail: `${err?.message ?? err}`,
|
|
16010
|
+
});
|
|
16011
|
+
console.error('Error importing regions:', err);
|
|
16012
|
+
this.changed();
|
|
15456
16013
|
}
|
|
15457
16014
|
/**
|
|
15458
16015
|
* Replace the regions with an imported GeoJSON file's, and the slice's region source with them.
|
|
@@ -15699,7 +16256,7 @@ class RegionEditorComponent {
|
|
|
15699
16256
|
this.messageService.add({ key: VIZ_TOAST_KEY, severity, summary, detail });
|
|
15700
16257
|
}
|
|
15701
16258
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: RegionEditorComponent, deps: [{ token: REGION_EDITOR_API }, { token: i1$2.MessageService }, { token: i1$2.ConfirmationService }, { token: RegionPersistenceService }, { token: MaskExportService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15702
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: RegionEditorComponent, isStandalone: true, selector: "region-editor", inputs: { active: "active" }, providers: [RegionPersistenceService], viewQueries: [{ propertyName: "overlayPanel", first: true, predicate: ["op"], descendants: true }], ngImport: i0, template: "<p-toolbar styleClass=\"region-toolbar\">\n <div class=\"p-toolbar-group-start align-items-center flex-wrap gap-1\">\n <p-button\n icon=\"pi pi-tag\"\n size=\"small\"\n [text]=\"!showShapeLabel\"\n pTooltip=\"Show region labels.\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showShapeLabel = !showShapeLabel; changeShowShapeLabel(showShapeLabel)\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-stop-circle\"\n size=\"small\"\n [text]=\"!regionsFilled\"\n pTooltip=\"Fill regions in their colour (an atlas's look); labels sit inside.\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Fill regions\"\n (onClick)=\"toggleFill()\"\n >\n </p-button>\n <p-button\n *ngIf=\"volumeView\"\n icon=\"pi pi-eye\"\n size=\"small\"\n [text]=\"!regionsThroughVolume\"\n [attr.aria-pressed]=\"regionsThroughVolume\"\n pTooltip=\"See the regions through the volume. Off, the volume hides the parts of them behind what it shows; on, none (layers traced inside bright structures, such as an OCT's).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Regions through volume\"\n (onClick)=\"toggleThroughVolume()\"\n >\n </p-button>\n <span class=\"toolbar-separator\"></span>\n <!-- New regions are now created from the diagram toolbar\n (rectangle, polyline, freeform, wand). -->\n <p-button\n icon=\"pi pi-upload\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Upload and display regions from geojson file.\"\n tooltipPosition=\"bottom\"\n (click)=\"roiFileInput.click()\"\n >\n </p-button>\n <input\n #roiFileInput\n type=\"file\"\n accept=\".geojson,.json\"\n hidden\n (change)=\"importRois($event); roiFileInput.value = ''\"\n />\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download regions as geojson file.\"\n tooltipPosition=\"bottom\"\n icon=\"pi pi-download\"\n (click)=\"exportRois()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-save\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Save regions to server.\"\n tooltipPosition=\"bottom\"\n (click)=\"persistRegions()\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download mask of the regions as PNG\"\n tooltipPosition=\"bottom\"\n (click)=\"openSaveMaskDialog()\"\n [disabled]=\"!regions || !regions.length\"\n >\n <!-- References the file so Inkscape edits to mask.svg show up directly\n (the lib's assets/** are served at assets/plotting/). The\n toolbar-icon-dark filter recolours the black SVG to the theme blue,\n same as region-editor.svg and the other SVG toolbar icons. -->\n <img\n src=\"assets/plotting/mask.svg\"\n class=\"toolbar-icon-dark\"\n style=\"width: 1rem; height: 1rem; vertical-align: middle\"\n alt=\"Download mask\"\n />\n </p-button>\n <p-button\n label=\"x512\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Round all rectangle regions\n to width and height that are multiple of 512\"\n tooltipPosition=\"bottom\"\n (click)=\"roundRectangleLengths()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-check-square\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Select all regions\"\n tooltipPosition=\"bottom\"\n (click)=\"selectAllRegions()\"\n [disabled]=\"!regions || !regions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-tags\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit class on selected rows\"\n tooltipPosition=\"bottom\"\n (click)=\"!selectedRegions || !selectedRegions.length ? op.hide() : op.toggle($event)\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-palette\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit colour of selected regions\"\n tooltipPosition=\"bottom\"\n (click)=\"openColorDialog()\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-overlayPanel #op (onHide)=\"bulkClass = ''; newBulkClass = ''\">\n <div class=\"flex flex-column gap-2\" style=\"width: 18rem\">\n <span class=\"font-medium text-900\"\n >Set class on {{ selectedRegions?.length || 0 }} selected region(s)</span\n >\n <!-- Selecting a class applies it immediately (choosing an option closes the\n overlay, so there's no separate Apply button). -->\n <p-dropdown\n styleClass=\"w-full\"\n appendTo=\"body\"\n [options]=\"presetSet.classes\"\n optionLabel=\"name\"\n optionValue=\"name\"\n [(ngModel)]=\"bulkClass\"\n placeholder=\"Choose a class\u2026\"\n [filter]=\"presetSet.classes.length > 8\"\n filterBy=\"name\"\n (onChange)=\"applyBulkClass()\"\n >\n <ng-template let-c pTemplate=\"item\">\n <span class=\"dd-value\"\n ><span class=\"chip-swatch\" [style.background]=\"c.color\"></span>{{ c.name }}</span\n >\n </ng-template>\n </p-dropdown>\n <div class=\"p-inputgroup\">\n <input\n type=\"text\"\n pInputText\n placeholder=\"\u2026or a new class\"\n [(ngModel)]=\"newBulkClass\"\n (keydown.enter)=\"addAndApplyBulkClass()\"\n />\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n pTooltip=\"Add class & set it\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!newBulkClass.trim()\"\n (click)=\"addAndApplyBulkClass()\"\n ></button>\n </div>\n </div>\n </p-overlayPanel>\n <p-button\n icon=\"pi pi-trash\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"deleteSelectedRegions()\"\n pTooltip=\"Delete selected regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n ></p-button>\n <p-button\n icon=\"pi pi-times-circle\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"clearAllRegions()\"\n pTooltip=\"Delete all regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n ></p-button>\n <p-button\n icon=\"pi pi-question\"\n size=\"small\"\n [text]=\"true\"\n (click)=\"showHelp()\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n ></p-button>\n </div>\n</p-toolbar>\n\n<region-classes-panel\n [classes]=\"displayClasses\"\n [counts]=\"classCounts\"\n [matchMode]=\"presetSet.matchMode\"\n [activeClass]=\"activeClass\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n [defaultClassName]=\"defaultClassName\"\n (pick)=\"selectActiveClass($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (remove)=\"deleteClass($event)\"\n (manage)=\"openManageDialog()\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (toggle3d)=\"toggleClass3d($event)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden([$event], !hiddenClasses.has($event))\"\n [treeView]=\"classHierarchy ? classTreeView : null\"\n (treeViewChange)=\"classTreeView = $event\"\n>\n <!-- A region set with a class hierarchy (an atlas ontology) can show its classes as a tree. -->\n <region-class-tree\n *ngIf=\"classHierarchy as hierarchy\"\n [nodes]=\"hierarchy\"\n [counts]=\"treeCounts\"\n [colorOf]=\"treeColor\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (pick)=\"pickClasses($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (toggle3d)=\"toggleClass3d($event.name, $event.classes)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden($event.classes, $event.hidden)\"\n >\n </region-class-tree>\n</region-classes-panel>\n\n<region-table\n [page]=\"pagedRegions\"\n [total]=\"tableTotal\"\n [first]=\"paginatorFirst\"\n [rows]=\"paginatorRows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [metaKey]=\"metaKey\"\n [selection]=\"selectedRegions\"\n (selectionChange)=\"selectedRegions = $event\"\n (rowSelect)=\"onSelectionChanged()\"\n [presetSet]=\"tablePresetSet\"\n [defaultColor]=\"shapeColor\"\n [mpp]=\"mpp\"\n [volumes]=\"tableVolumes\"\n [labelDrafts]=\"editingLabelRegions\"\n (labelEditStart)=\"startEditLabel($event)\"\n (labelDraftChange)=\"setLabelDraft($event.region, $event.value)\"\n (labelEditStop)=\"stopEditLabel($event.region, $event.commit)\"\n (classPick)=\"applyPresetToRegion($event.region, $event.name)\"\n (deleteRow)=\"deleteRegion($event)\"\n (pageChange)=\"onPageChange($event)\"\n>\n</region-table>\n\n<!-- Live-edit mode: no Save/Cancel \u2014 every edit commits immediately to the\n active image's region cache. -->\n\n<p-confirmDialog\n #cd\n [style]=\"{ width: '24rem', height: '14rem' }\"\n key=\"positionDialog\"\n [baseZIndex]=\"10000\"\n rejectButtonStyleClass=\"p-button-outlined\"\n>\n <ng-template pTemplate=\"footer\">\n <button type=\"button\" pButton icon=\"pi pi-check\" label=\"Yes\" (click)=\"cd.accept()\"></button>\n <button\n type=\"button\"\n class=\"p-button-outlined\"\n pButton\n icon=\"pi pi-times\"\n label=\"No\"\n (click)=\"cd.reject()\"\n ></button>\n </ng-template>\n</p-confirmDialog>\n\n<region-save-regions-dialog\n [(visible)]=\"showSaveAsDialog\"\n [filename]=\"saveAsFilename\"\n (filenameChange)=\"saveAsFilename = $event; checkSaveAsFileExists()\"\n [fileExists]=\"saveAsFileExists\"\n [busy]=\"saveAsBusy\"\n (confirm)=\"confirmSaveAs()\"\n (cancelSave)=\"cancelSaveAs()\"\n>\n</region-save-regions-dialog>\n\n<region-save-regions-dialog\n header=\"Export Regions\"\n confirmLabel=\"Export\"\n confirmIcon=\"pi pi-download\"\n inputId=\"export-filename\"\n [(visible)]=\"showExportDialog\"\n [(filename)]=\"exportFilename\"\n (confirm)=\"confirmExport()\"\n>\n</region-save-regions-dialog>\n\n<region-save-mask-dialog\n [(visible)]=\"showSaveMaskDialog\"\n [(filename)]=\"saveMaskFilename\"\n [(mode)]=\"maskMode\"\n [busy]=\"maskBusy\"\n [progress]=\"maskProgress\"\n [encoding]=\"maskEncoding\"\n (confirm)=\"confirmSaveMask()\"\n (cancelExport)=\"cancelSaveMask()\"\n>\n</region-save-mask-dialog>\n\n<region-color-dialog\n [(visible)]=\"showColorDialog\"\n [edits]=\"classColorEdits\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n (apply)=\"applyColorToSelected($event)\"\n>\n</region-color-dialog>\n\n<!-- Manage annotation classes (jit-ui#70): edits a working copy, committed on Apply. -->\n<region-manage-classes-dialog\n [(visible)]=\"showManageDialog\"\n [(draft)]=\"presetDraft\"\n (apply)=\"applyManageDialog(true)\"\n (resetDefaults)=\"resetPresetsToDefaults()\"\n (importFile)=\"importPresets($event)\"\n (export)=\"exportPresets()\"\n>\n</region-manage-classes-dialog>\n\n<region-editor-help [(visible)]=\"displayHelpDialog\"></region-editor-help>\n", styles: ["@charset \"UTF-8\";.chip-swatch{display:inline-block;width:.75rem;height:.75rem;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}.dd-value{display:inline-flex;align-items:center;gap:.4rem;min-width:0}.dd-value .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}::ng-deep region-editor .p-toolbar.region-toolbar{padding:.25rem .5rem;border-radius:0;border-left:none;border-right:none;border-top:none}::ng-deep region-editor .p-toolbar.region-toolbar .p-button.p-button-icon-only{width:auto;padding:.25rem .5rem}::ng-deep region-editor .p-toolbar.region-toolbar .p-button{padding:.25rem .5rem}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain"] }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: ConfirmDialogModule }, { kind: "component", type: i7$2.ConfirmDialog, selector: "p-confirmDialog", inputs: ["header", "icon", "message", "style", "styleClass", "maskStyleClass", "acceptIcon", "acceptLabel", "closeAriaLabel", "acceptAriaLabel", "acceptVisible", "rejectIcon", "rejectLabel", "rejectAriaLabel", "rejectVisible", "acceptButtonStyleClass", "rejectButtonStyleClass", "closeOnEscape", "dismissableMask", "blockScroll", "rtl", "closable", "appendTo", "key", "autoZIndex", "baseZIndex", "transitionOptions", "focusTrap", "defaultFocus", "breakpoints", "visible", "position"], outputs: ["onHide"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i4.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: ToolbarModule }, { kind: "component", type: i4$2.Toolbar, selector: "p-toolbar", inputs: ["style", "styleClass", "ariaLabelledBy"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: ClassesPanelComponent, selector: "region-classes-panel", inputs: ["classes", "counts", "matchMode", "activeClass", "picked", "marked", "selectedCount", "defaultClassName", "volume", "shown3d", "hidden", "treeView"], outputs: ["toggleHidden", "toggle3d", "treeViewChange", "pick", "recolor", "remove", "manage"] }, { kind: "component", type: RegionTableComponent, selector: "region-table", inputs: ["page", "total", "first", "rows", "rowsPerPageOptions", "metaKey", "selection", "presetSet", "defaultColor", "mpp", "volumes", "labelDrafts"], outputs: ["selectionChange", "rowSelect", "labelEditStart", "labelDraftChange", "labelEditStop", "classPick", "deleteRow", "pageChange"] }, { kind: "component", type: ClassTreeComponent, selector: "region-class-tree", inputs: ["nodes", "counts", "colorOf", "picked", "marked", "volume", "shown3d", "hidden"], outputs: ["pick", "recolor", "toggle3d", "toggleHidden"] }, { kind: "component", type: SaveRegionsDialogComponent, selector: "region-save-regions-dialog", inputs: ["header", "confirmLabel", "confirmIcon", "inputId", "visible", "filename", "fileExists", "busy"], outputs: ["visibleChange", "filenameChange", "confirm", "cancelSave"] }, { kind: "component", type: SaveMaskDialogComponent, selector: "region-save-mask-dialog", inputs: ["visible", "filename", "mode", "busy", "progress", "encoding"], outputs: ["visibleChange", "filenameChange", "modeChange", "confirm", "cancelExport"] }, { kind: "component", type: RegionColorDialogComponent, selector: "region-color-dialog", inputs: ["visible", "selectedCount", "edits"], outputs: ["visibleChange", "apply"] }, { kind: "component", type: ManageClassesDialogComponent, selector: "region-manage-classes-dialog", inputs: ["visible", "draft"], outputs: ["visibleChange", "draftChange", "apply", "resetDefaults", "importFile", "export"] }, { kind: "component", type: RegionEditorHelpComponent, selector: "region-editor-help", inputs: ["visible"], outputs: ["visibleChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16259
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: RegionEditorComponent, isStandalone: true, selector: "region-editor", inputs: { active: "active" }, providers: [RegionPersistenceService], viewQueries: [{ propertyName: "overlayPanel", first: true, predicate: ["op"], descendants: true }], ngImport: i0, template: "<p-toolbar styleClass=\"region-toolbar\">\n <div class=\"p-toolbar-group-start align-items-center flex-wrap gap-1\">\n <p-button\n icon=\"pi pi-tag\"\n size=\"small\"\n [text]=\"!showShapeLabel\"\n pTooltip=\"Show region labels.\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showShapeLabel = !showShapeLabel; changeShowShapeLabel(showShapeLabel)\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-stop-circle\"\n size=\"small\"\n [text]=\"!regionsFilled\"\n pTooltip=\"Fill regions in their colour (an atlas's look); labels sit inside.\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Fill regions\"\n (onClick)=\"toggleFill()\"\n >\n </p-button>\n <p-button\n *ngIf=\"volumeView\"\n icon=\"pi pi-eye\"\n size=\"small\"\n [text]=\"!regionsThroughVolume\"\n [attr.aria-pressed]=\"regionsThroughVolume\"\n pTooltip=\"See the regions through the volume. Off, the volume hides the parts of them behind what it shows; on, none (layers traced inside bright structures, such as an OCT's).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Regions through volume\"\n (onClick)=\"toggleThroughVolume()\"\n >\n </p-button>\n <span class=\"toolbar-separator\"></span>\n <!-- New regions are now created from the diagram toolbar\n (rectangle, polyline, freeform, wand). -->\n <p-button\n icon=\"pi pi-upload\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import regions: a GeoJSON file, or a label volume (.nii, .nii.gz, .nrrd) chosen with its ontology JSON for the class tree.\"\n tooltipPosition=\"bottom\"\n (click)=\"roiFileInput.click()\"\n >\n </p-button>\n <input\n #roiFileInput\n type=\"file\"\n accept=\".geojson,.json,.nii,.gz,.nrrd\"\n multiple\n hidden\n (change)=\"importRois($event); roiFileInput.value = ''\"\n />\n <p-button\n icon=\"pi pi-folder-open\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import a folder of per-slice GeoJSON files (one per slice, in name order, with its regionset.json class tree).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Import a folder of regions\"\n (click)=\"roiFolderInput.click()\"\n >\n </p-button>\n <input\n #roiFolderInput\n type=\"file\"\n webkitdirectory\n multiple\n hidden\n (change)=\"importRois($event); roiFolderInput.value = ''\"\n />\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download regions as geojson file.\"\n tooltipPosition=\"bottom\"\n icon=\"pi pi-download\"\n (click)=\"exportRois()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-save\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Save regions to server.\"\n tooltipPosition=\"bottom\"\n (click)=\"persistRegions()\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n >\n </p-button>\n <p-button\n *ngIf=\"canImportFromServer\"\n icon=\"pi pi-cloud-download\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import regions from server: a GeoJSON file, a folder of per-slice GeoJSON files or a label volume (.nii, .nii.gz, .nrrd).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Import regions from server\"\n [loading]=\"importing\"\n (click)=\"importFromServer()\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download mask of the regions as PNG\"\n tooltipPosition=\"bottom\"\n (click)=\"openSaveMaskDialog()\"\n [disabled]=\"!regions || !regions.length\"\n >\n <!-- References the file so Inkscape edits to mask.svg show up directly\n (the lib's assets/** are served at assets/plotting/). The\n toolbar-icon-dark filter recolours the black SVG to the theme blue,\n same as region-editor.svg and the other SVG toolbar icons. -->\n <img\n src=\"assets/plotting/mask.svg\"\n class=\"toolbar-icon-dark\"\n style=\"width: 1rem; height: 1rem; vertical-align: middle\"\n alt=\"Download mask\"\n />\n </p-button>\n <p-button\n label=\"x512\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Round all rectangle regions\n to width and height that are multiple of 512\"\n tooltipPosition=\"bottom\"\n (click)=\"roundRectangleLengths()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-check-square\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Select all regions\"\n tooltipPosition=\"bottom\"\n (click)=\"selectAllRegions()\"\n [disabled]=\"!regions || !regions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-tags\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit class on selected rows\"\n tooltipPosition=\"bottom\"\n (click)=\"!selectedRegions || !selectedRegions.length ? op.hide() : op.toggle($event)\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-palette\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit colour of selected regions\"\n tooltipPosition=\"bottom\"\n (click)=\"openColorDialog()\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-overlayPanel #op (onHide)=\"bulkClass = ''; newBulkClass = ''\">\n <div class=\"flex flex-column gap-2\" style=\"width: 18rem\">\n <span class=\"font-medium text-900\"\n >Set class on {{ selectedRegions?.length || 0 }} selected region(s)</span\n >\n <!-- Selecting a class applies it immediately (choosing an option closes the\n overlay, so there's no separate Apply button). -->\n <p-dropdown\n styleClass=\"w-full\"\n appendTo=\"body\"\n [options]=\"presetSet.classes\"\n optionLabel=\"name\"\n optionValue=\"name\"\n [(ngModel)]=\"bulkClass\"\n placeholder=\"Choose a class\u2026\"\n [filter]=\"presetSet.classes.length > 8\"\n filterBy=\"name\"\n (onChange)=\"applyBulkClass()\"\n >\n <ng-template let-c pTemplate=\"item\">\n <span class=\"dd-value\"\n ><span class=\"chip-swatch\" [style.background]=\"c.color\"></span>{{ c.name }}</span\n >\n </ng-template>\n </p-dropdown>\n <div class=\"p-inputgroup\">\n <input\n type=\"text\"\n pInputText\n placeholder=\"\u2026or a new class\"\n [(ngModel)]=\"newBulkClass\"\n (keydown.enter)=\"addAndApplyBulkClass()\"\n />\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n pTooltip=\"Add class & set it\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!newBulkClass.trim()\"\n (click)=\"addAndApplyBulkClass()\"\n ></button>\n </div>\n </div>\n </p-overlayPanel>\n <p-button\n icon=\"pi pi-trash\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"deleteSelectedRegions()\"\n pTooltip=\"Delete selected regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n ></p-button>\n <p-button\n icon=\"pi pi-times-circle\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"clearAllRegions()\"\n pTooltip=\"Delete all regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n ></p-button>\n <p-button\n icon=\"pi pi-question\"\n size=\"small\"\n [text]=\"true\"\n (click)=\"showHelp()\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n ></p-button>\n </div>\n</p-toolbar>\n\n<region-classes-panel\n [classes]=\"displayClasses\"\n [counts]=\"classCounts\"\n [matchMode]=\"presetSet.matchMode\"\n [activeClass]=\"activeClass\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n [defaultClassName]=\"defaultClassName\"\n (pick)=\"selectActiveClass($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (remove)=\"deleteClass($event)\"\n (manage)=\"openManageDialog()\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (toggle3d)=\"toggleClass3d($event)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden([$event], !hiddenClasses.has($event))\"\n [treeView]=\"classHierarchy ? classTreeView : null\"\n (treeViewChange)=\"classTreeView = $event\"\n>\n <!-- A region set with a class hierarchy (an atlas ontology) can show its classes as a tree. -->\n <region-class-tree\n *ngIf=\"classHierarchy as hierarchy\"\n [nodes]=\"hierarchy\"\n [counts]=\"treeCounts\"\n [colorOf]=\"treeColor\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (pick)=\"pickClasses($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (toggle3d)=\"toggleClass3d($event.name, $event.classes)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden($event.classes, $event.hidden)\"\n >\n </region-class-tree>\n</region-classes-panel>\n\n<region-table\n [page]=\"pagedRegions\"\n [total]=\"tableTotal\"\n [first]=\"paginatorFirst\"\n [rows]=\"paginatorRows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [metaKey]=\"metaKey\"\n [selection]=\"selectedRegions\"\n (selectionChange)=\"selectedRegions = $event\"\n (rowSelect)=\"onSelectionChanged()\"\n [presetSet]=\"tablePresetSet\"\n [defaultColor]=\"shapeColor\"\n [mpp]=\"mpp\"\n [volumes]=\"tableVolumes\"\n [labelDrafts]=\"editingLabelRegions\"\n (labelEditStart)=\"startEditLabel($event)\"\n (labelDraftChange)=\"setLabelDraft($event.region, $event.value)\"\n (labelEditStop)=\"stopEditLabel($event.region, $event.commit)\"\n (classPick)=\"applyPresetToRegion($event.region, $event.name)\"\n (deleteRow)=\"deleteRegion($event)\"\n (pageChange)=\"onPageChange($event)\"\n>\n</region-table>\n\n<!-- Live-edit mode: no Save/Cancel \u2014 every edit commits immediately to the\n active image's region cache. -->\n\n<p-confirmDialog\n #cd\n [style]=\"{ width: '24rem', height: '14rem' }\"\n key=\"positionDialog\"\n [baseZIndex]=\"10000\"\n rejectButtonStyleClass=\"p-button-outlined\"\n>\n <ng-template pTemplate=\"footer\">\n <button type=\"button\" pButton icon=\"pi pi-check\" label=\"Yes\" (click)=\"cd.accept()\"></button>\n <button\n type=\"button\"\n class=\"p-button-outlined\"\n pButton\n icon=\"pi pi-times\"\n label=\"No\"\n (click)=\"cd.reject()\"\n ></button>\n </ng-template>\n</p-confirmDialog>\n\n<region-save-regions-dialog\n [(visible)]=\"showSaveAsDialog\"\n [filename]=\"saveAsFilename\"\n (filenameChange)=\"saveAsFilename = $event; checkSaveAsFileExists()\"\n [fileExists]=\"saveAsFileExists\"\n [busy]=\"saveAsBusy\"\n (confirm)=\"confirmSaveAs()\"\n (cancelSave)=\"cancelSaveAs()\"\n>\n</region-save-regions-dialog>\n\n<region-save-regions-dialog\n header=\"Export Regions\"\n confirmLabel=\"Export\"\n confirmIcon=\"pi pi-download\"\n inputId=\"export-filename\"\n [(visible)]=\"showExportDialog\"\n [(filename)]=\"exportFilename\"\n (confirm)=\"confirmExport()\"\n>\n</region-save-regions-dialog>\n\n<region-save-mask-dialog\n [(visible)]=\"showSaveMaskDialog\"\n [(filename)]=\"saveMaskFilename\"\n [(mode)]=\"maskMode\"\n [busy]=\"maskBusy\"\n [progress]=\"maskProgress\"\n [encoding]=\"maskEncoding\"\n (confirm)=\"confirmSaveMask()\"\n (cancelExport)=\"cancelSaveMask()\"\n>\n</region-save-mask-dialog>\n\n<region-color-dialog\n [(visible)]=\"showColorDialog\"\n [edits]=\"classColorEdits\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n (apply)=\"applyColorToSelected($event)\"\n>\n</region-color-dialog>\n\n<!-- Manage annotation classes (jit-ui#70): edits a working copy, committed on Apply. -->\n<region-manage-classes-dialog\n [(visible)]=\"showManageDialog\"\n [(draft)]=\"presetDraft\"\n (apply)=\"applyManageDialog(true)\"\n (resetDefaults)=\"resetPresetsToDefaults()\"\n (importFile)=\"importPresets($event)\"\n (export)=\"exportPresets()\"\n>\n</region-manage-classes-dialog>\n\n<region-editor-help [(visible)]=\"displayHelpDialog\"></region-editor-help>\n", styles: ["@charset \"UTF-8\";.chip-swatch{display:inline-block;width:.75rem;height:.75rem;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}.dd-value{display:inline-flex;align-items:center;gap:.4rem;min-width:0}.dd-value .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}::ng-deep region-editor .p-toolbar.region-toolbar{padding:.25rem .5rem;border-radius:0;border-left:none;border-right:none;border-top:none}::ng-deep region-editor .p-toolbar.region-toolbar .p-button.p-button-icon-only{width:auto;padding:.25rem .5rem}::ng-deep region-editor .p-toolbar.region-toolbar .p-button{padding:.25rem .5rem}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain"] }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: ConfirmDialogModule }, { kind: "component", type: i7$2.ConfirmDialog, selector: "p-confirmDialog", inputs: ["header", "icon", "message", "style", "styleClass", "maskStyleClass", "acceptIcon", "acceptLabel", "closeAriaLabel", "acceptAriaLabel", "acceptVisible", "rejectIcon", "rejectLabel", "rejectAriaLabel", "rejectVisible", "acceptButtonStyleClass", "rejectButtonStyleClass", "closeOnEscape", "dismissableMask", "blockScroll", "rtl", "closable", "appendTo", "key", "autoZIndex", "baseZIndex", "transitionOptions", "focusTrap", "defaultFocus", "breakpoints", "visible", "position"], outputs: ["onHide"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i4.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: ToolbarModule }, { kind: "component", type: i4$2.Toolbar, selector: "p-toolbar", inputs: ["style", "styleClass", "ariaLabelledBy"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: ClassesPanelComponent, selector: "region-classes-panel", inputs: ["classes", "counts", "matchMode", "activeClass", "picked", "marked", "selectedCount", "defaultClassName", "volume", "shown3d", "hidden", "treeView"], outputs: ["toggleHidden", "toggle3d", "treeViewChange", "pick", "recolor", "remove", "manage"] }, { kind: "component", type: RegionTableComponent, selector: "region-table", inputs: ["page", "total", "first", "rows", "rowsPerPageOptions", "metaKey", "selection", "presetSet", "defaultColor", "mpp", "volumes", "labelDrafts"], outputs: ["selectionChange", "rowSelect", "labelEditStart", "labelDraftChange", "labelEditStop", "classPick", "deleteRow", "pageChange"] }, { kind: "component", type: ClassTreeComponent, selector: "region-class-tree", inputs: ["nodes", "counts", "colorOf", "picked", "marked", "volume", "shown3d", "hidden"], outputs: ["pick", "recolor", "toggle3d", "toggleHidden"] }, { kind: "component", type: SaveRegionsDialogComponent, selector: "region-save-regions-dialog", inputs: ["header", "confirmLabel", "confirmIcon", "inputId", "visible", "filename", "fileExists", "busy"], outputs: ["visibleChange", "filenameChange", "confirm", "cancelSave"] }, { kind: "component", type: SaveMaskDialogComponent, selector: "region-save-mask-dialog", inputs: ["visible", "filename", "mode", "busy", "progress", "encoding"], outputs: ["visibleChange", "filenameChange", "modeChange", "confirm", "cancelExport"] }, { kind: "component", type: RegionColorDialogComponent, selector: "region-color-dialog", inputs: ["visible", "selectedCount", "edits"], outputs: ["visibleChange", "apply"] }, { kind: "component", type: ManageClassesDialogComponent, selector: "region-manage-classes-dialog", inputs: ["visible", "draft"], outputs: ["visibleChange", "draftChange", "apply", "resetDefaults", "importFile", "export"] }, { kind: "component", type: RegionEditorHelpComponent, selector: "region-editor-help", inputs: ["visible"], outputs: ["visibleChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15703
16260
|
}
|
|
15704
16261
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: RegionEditorComponent, decorators: [{
|
|
15705
16262
|
type: Component,
|
|
@@ -15722,7 +16279,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
|
|
|
15722
16279
|
RegionColorDialogComponent,
|
|
15723
16280
|
ManageClassesDialogComponent,
|
|
15724
16281
|
RegionEditorHelpComponent,
|
|
15725
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, providers: [RegionPersistenceService], template: "<p-toolbar styleClass=\"region-toolbar\">\n <div class=\"p-toolbar-group-start align-items-center flex-wrap gap-1\">\n <p-button\n icon=\"pi pi-tag\"\n size=\"small\"\n [text]=\"!showShapeLabel\"\n pTooltip=\"Show region labels.\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showShapeLabel = !showShapeLabel; changeShowShapeLabel(showShapeLabel)\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-stop-circle\"\n size=\"small\"\n [text]=\"!regionsFilled\"\n pTooltip=\"Fill regions in their colour (an atlas's look); labels sit inside.\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Fill regions\"\n (onClick)=\"toggleFill()\"\n >\n </p-button>\n <p-button\n *ngIf=\"volumeView\"\n icon=\"pi pi-eye\"\n size=\"small\"\n [text]=\"!regionsThroughVolume\"\n [attr.aria-pressed]=\"regionsThroughVolume\"\n pTooltip=\"See the regions through the volume. Off, the volume hides the parts of them behind what it shows; on, none (layers traced inside bright structures, such as an OCT's).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Regions through volume\"\n (onClick)=\"toggleThroughVolume()\"\n >\n </p-button>\n <span class=\"toolbar-separator\"></span>\n <!-- New regions are now created from the diagram toolbar\n (rectangle, polyline, freeform, wand). -->\n <p-button\n icon=\"pi pi-upload\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Upload and display regions from geojson file.\"\n tooltipPosition=\"bottom\"\n (click)=\"roiFileInput.click()\"\n >\n </p-button>\n <input\n #roiFileInput\n type=\"file\"\n accept=\".geojson,.json\"\n hidden\n (change)=\"importRois($event); roiFileInput.value = ''\"\n />\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download regions as geojson file.\"\n tooltipPosition=\"bottom\"\n icon=\"pi pi-download\"\n (click)=\"exportRois()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-save\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Save regions to server.\"\n tooltipPosition=\"bottom\"\n (click)=\"persistRegions()\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download mask of the regions as PNG\"\n tooltipPosition=\"bottom\"\n (click)=\"openSaveMaskDialog()\"\n [disabled]=\"!regions || !regions.length\"\n >\n <!-- References the file so Inkscape edits to mask.svg show up directly\n (the lib's assets/** are served at assets/plotting/). The\n toolbar-icon-dark filter recolours the black SVG to the theme blue,\n same as region-editor.svg and the other SVG toolbar icons. -->\n <img\n src=\"assets/plotting/mask.svg\"\n class=\"toolbar-icon-dark\"\n style=\"width: 1rem; height: 1rem; vertical-align: middle\"\n alt=\"Download mask\"\n />\n </p-button>\n <p-button\n label=\"x512\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Round all rectangle regions\n to width and height that are multiple of 512\"\n tooltipPosition=\"bottom\"\n (click)=\"roundRectangleLengths()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-check-square\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Select all regions\"\n tooltipPosition=\"bottom\"\n (click)=\"selectAllRegions()\"\n [disabled]=\"!regions || !regions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-tags\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit class on selected rows\"\n tooltipPosition=\"bottom\"\n (click)=\"!selectedRegions || !selectedRegions.length ? op.hide() : op.toggle($event)\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-palette\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit colour of selected regions\"\n tooltipPosition=\"bottom\"\n (click)=\"openColorDialog()\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-overlayPanel #op (onHide)=\"bulkClass = ''; newBulkClass = ''\">\n <div class=\"flex flex-column gap-2\" style=\"width: 18rem\">\n <span class=\"font-medium text-900\"\n >Set class on {{ selectedRegions?.length || 0 }} selected region(s)</span\n >\n <!-- Selecting a class applies it immediately (choosing an option closes the\n overlay, so there's no separate Apply button). -->\n <p-dropdown\n styleClass=\"w-full\"\n appendTo=\"body\"\n [options]=\"presetSet.classes\"\n optionLabel=\"name\"\n optionValue=\"name\"\n [(ngModel)]=\"bulkClass\"\n placeholder=\"Choose a class\u2026\"\n [filter]=\"presetSet.classes.length > 8\"\n filterBy=\"name\"\n (onChange)=\"applyBulkClass()\"\n >\n <ng-template let-c pTemplate=\"item\">\n <span class=\"dd-value\"\n ><span class=\"chip-swatch\" [style.background]=\"c.color\"></span>{{ c.name }}</span\n >\n </ng-template>\n </p-dropdown>\n <div class=\"p-inputgroup\">\n <input\n type=\"text\"\n pInputText\n placeholder=\"\u2026or a new class\"\n [(ngModel)]=\"newBulkClass\"\n (keydown.enter)=\"addAndApplyBulkClass()\"\n />\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n pTooltip=\"Add class & set it\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!newBulkClass.trim()\"\n (click)=\"addAndApplyBulkClass()\"\n ></button>\n </div>\n </div>\n </p-overlayPanel>\n <p-button\n icon=\"pi pi-trash\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"deleteSelectedRegions()\"\n pTooltip=\"Delete selected regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n ></p-button>\n <p-button\n icon=\"pi pi-times-circle\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"clearAllRegions()\"\n pTooltip=\"Delete all regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n ></p-button>\n <p-button\n icon=\"pi pi-question\"\n size=\"small\"\n [text]=\"true\"\n (click)=\"showHelp()\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n ></p-button>\n </div>\n</p-toolbar>\n\n<region-classes-panel\n [classes]=\"displayClasses\"\n [counts]=\"classCounts\"\n [matchMode]=\"presetSet.matchMode\"\n [activeClass]=\"activeClass\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n [defaultClassName]=\"defaultClassName\"\n (pick)=\"selectActiveClass($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (remove)=\"deleteClass($event)\"\n (manage)=\"openManageDialog()\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (toggle3d)=\"toggleClass3d($event)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden([$event], !hiddenClasses.has($event))\"\n [treeView]=\"classHierarchy ? classTreeView : null\"\n (treeViewChange)=\"classTreeView = $event\"\n>\n <!-- A region set with a class hierarchy (an atlas ontology) can show its classes as a tree. -->\n <region-class-tree\n *ngIf=\"classHierarchy as hierarchy\"\n [nodes]=\"hierarchy\"\n [counts]=\"treeCounts\"\n [colorOf]=\"treeColor\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (pick)=\"pickClasses($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (toggle3d)=\"toggleClass3d($event.name, $event.classes)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden($event.classes, $event.hidden)\"\n >\n </region-class-tree>\n</region-classes-panel>\n\n<region-table\n [page]=\"pagedRegions\"\n [total]=\"tableTotal\"\n [first]=\"paginatorFirst\"\n [rows]=\"paginatorRows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [metaKey]=\"metaKey\"\n [selection]=\"selectedRegions\"\n (selectionChange)=\"selectedRegions = $event\"\n (rowSelect)=\"onSelectionChanged()\"\n [presetSet]=\"tablePresetSet\"\n [defaultColor]=\"shapeColor\"\n [mpp]=\"mpp\"\n [volumes]=\"tableVolumes\"\n [labelDrafts]=\"editingLabelRegions\"\n (labelEditStart)=\"startEditLabel($event)\"\n (labelDraftChange)=\"setLabelDraft($event.region, $event.value)\"\n (labelEditStop)=\"stopEditLabel($event.region, $event.commit)\"\n (classPick)=\"applyPresetToRegion($event.region, $event.name)\"\n (deleteRow)=\"deleteRegion($event)\"\n (pageChange)=\"onPageChange($event)\"\n>\n</region-table>\n\n<!-- Live-edit mode: no Save/Cancel \u2014 every edit commits immediately to the\n active image's region cache. -->\n\n<p-confirmDialog\n #cd\n [style]=\"{ width: '24rem', height: '14rem' }\"\n key=\"positionDialog\"\n [baseZIndex]=\"10000\"\n rejectButtonStyleClass=\"p-button-outlined\"\n>\n <ng-template pTemplate=\"footer\">\n <button type=\"button\" pButton icon=\"pi pi-check\" label=\"Yes\" (click)=\"cd.accept()\"></button>\n <button\n type=\"button\"\n class=\"p-button-outlined\"\n pButton\n icon=\"pi pi-times\"\n label=\"No\"\n (click)=\"cd.reject()\"\n ></button>\n </ng-template>\n</p-confirmDialog>\n\n<region-save-regions-dialog\n [(visible)]=\"showSaveAsDialog\"\n [filename]=\"saveAsFilename\"\n (filenameChange)=\"saveAsFilename = $event; checkSaveAsFileExists()\"\n [fileExists]=\"saveAsFileExists\"\n [busy]=\"saveAsBusy\"\n (confirm)=\"confirmSaveAs()\"\n (cancelSave)=\"cancelSaveAs()\"\n>\n</region-save-regions-dialog>\n\n<region-save-regions-dialog\n header=\"Export Regions\"\n confirmLabel=\"Export\"\n confirmIcon=\"pi pi-download\"\n inputId=\"export-filename\"\n [(visible)]=\"showExportDialog\"\n [(filename)]=\"exportFilename\"\n (confirm)=\"confirmExport()\"\n>\n</region-save-regions-dialog>\n\n<region-save-mask-dialog\n [(visible)]=\"showSaveMaskDialog\"\n [(filename)]=\"saveMaskFilename\"\n [(mode)]=\"maskMode\"\n [busy]=\"maskBusy\"\n [progress]=\"maskProgress\"\n [encoding]=\"maskEncoding\"\n (confirm)=\"confirmSaveMask()\"\n (cancelExport)=\"cancelSaveMask()\"\n>\n</region-save-mask-dialog>\n\n<region-color-dialog\n [(visible)]=\"showColorDialog\"\n [edits]=\"classColorEdits\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n (apply)=\"applyColorToSelected($event)\"\n>\n</region-color-dialog>\n\n<!-- Manage annotation classes (jit-ui#70): edits a working copy, committed on Apply. -->\n<region-manage-classes-dialog\n [(visible)]=\"showManageDialog\"\n [(draft)]=\"presetDraft\"\n (apply)=\"applyManageDialog(true)\"\n (resetDefaults)=\"resetPresetsToDefaults()\"\n (importFile)=\"importPresets($event)\"\n (export)=\"exportPresets()\"\n>\n</region-manage-classes-dialog>\n\n<region-editor-help [(visible)]=\"displayHelpDialog\"></region-editor-help>\n", styles: ["@charset \"UTF-8\";.chip-swatch{display:inline-block;width:.75rem;height:.75rem;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}.dd-value{display:inline-flex;align-items:center;gap:.4rem;min-width:0}.dd-value .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}::ng-deep region-editor .p-toolbar.region-toolbar{padding:.25rem .5rem;border-radius:0;border-left:none;border-right:none;border-top:none}::ng-deep region-editor .p-toolbar.region-toolbar .p-button.p-button-icon-only{width:auto;padding:.25rem .5rem}::ng-deep region-editor .p-toolbar.region-toolbar .p-button{padding:.25rem .5rem}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}\n"] }]
|
|
16282
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, providers: [RegionPersistenceService], template: "<p-toolbar styleClass=\"region-toolbar\">\n <div class=\"p-toolbar-group-start align-items-center flex-wrap gap-1\">\n <p-button\n icon=\"pi pi-tag\"\n size=\"small\"\n [text]=\"!showShapeLabel\"\n pTooltip=\"Show region labels.\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showShapeLabel = !showShapeLabel; changeShowShapeLabel(showShapeLabel)\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-stop-circle\"\n size=\"small\"\n [text]=\"!regionsFilled\"\n pTooltip=\"Fill regions in their colour (an atlas's look); labels sit inside.\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Fill regions\"\n (onClick)=\"toggleFill()\"\n >\n </p-button>\n <p-button\n *ngIf=\"volumeView\"\n icon=\"pi pi-eye\"\n size=\"small\"\n [text]=\"!regionsThroughVolume\"\n [attr.aria-pressed]=\"regionsThroughVolume\"\n pTooltip=\"See the regions through the volume. Off, the volume hides the parts of them behind what it shows; on, none (layers traced inside bright structures, such as an OCT's).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Regions through volume\"\n (onClick)=\"toggleThroughVolume()\"\n >\n </p-button>\n <span class=\"toolbar-separator\"></span>\n <!-- New regions are now created from the diagram toolbar\n (rectangle, polyline, freeform, wand). -->\n <p-button\n icon=\"pi pi-upload\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import regions: a GeoJSON file, or a label volume (.nii, .nii.gz, .nrrd) chosen with its ontology JSON for the class tree.\"\n tooltipPosition=\"bottom\"\n (click)=\"roiFileInput.click()\"\n >\n </p-button>\n <input\n #roiFileInput\n type=\"file\"\n accept=\".geojson,.json,.nii,.gz,.nrrd\"\n multiple\n hidden\n (change)=\"importRois($event); roiFileInput.value = ''\"\n />\n <p-button\n icon=\"pi pi-folder-open\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import a folder of per-slice GeoJSON files (one per slice, in name order, with its regionset.json class tree).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Import a folder of regions\"\n (click)=\"roiFolderInput.click()\"\n >\n </p-button>\n <input\n #roiFolderInput\n type=\"file\"\n webkitdirectory\n multiple\n hidden\n (change)=\"importRois($event); roiFolderInput.value = ''\"\n />\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download regions as geojson file.\"\n tooltipPosition=\"bottom\"\n icon=\"pi pi-download\"\n (click)=\"exportRois()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-save\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Save regions to server.\"\n tooltipPosition=\"bottom\"\n (click)=\"persistRegions()\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n >\n </p-button>\n <p-button\n *ngIf=\"canImportFromServer\"\n icon=\"pi pi-cloud-download\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import regions from server: a GeoJSON file, a folder of per-slice GeoJSON files or a label volume (.nii, .nii.gz, .nrrd).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Import regions from server\"\n [loading]=\"importing\"\n (click)=\"importFromServer()\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download mask of the regions as PNG\"\n tooltipPosition=\"bottom\"\n (click)=\"openSaveMaskDialog()\"\n [disabled]=\"!regions || !regions.length\"\n >\n <!-- References the file so Inkscape edits to mask.svg show up directly\n (the lib's assets/** are served at assets/plotting/). The\n toolbar-icon-dark filter recolours the black SVG to the theme blue,\n same as region-editor.svg and the other SVG toolbar icons. -->\n <img\n src=\"assets/plotting/mask.svg\"\n class=\"toolbar-icon-dark\"\n style=\"width: 1rem; height: 1rem; vertical-align: middle\"\n alt=\"Download mask\"\n />\n </p-button>\n <p-button\n label=\"x512\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Round all rectangle regions\n to width and height that are multiple of 512\"\n tooltipPosition=\"bottom\"\n (click)=\"roundRectangleLengths()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-check-square\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Select all regions\"\n tooltipPosition=\"bottom\"\n (click)=\"selectAllRegions()\"\n [disabled]=\"!regions || !regions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-tags\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit class on selected rows\"\n tooltipPosition=\"bottom\"\n (click)=\"!selectedRegions || !selectedRegions.length ? op.hide() : op.toggle($event)\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-palette\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit colour of selected regions\"\n tooltipPosition=\"bottom\"\n (click)=\"openColorDialog()\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-overlayPanel #op (onHide)=\"bulkClass = ''; newBulkClass = ''\">\n <div class=\"flex flex-column gap-2\" style=\"width: 18rem\">\n <span class=\"font-medium text-900\"\n >Set class on {{ selectedRegions?.length || 0 }} selected region(s)</span\n >\n <!-- Selecting a class applies it immediately (choosing an option closes the\n overlay, so there's no separate Apply button). -->\n <p-dropdown\n styleClass=\"w-full\"\n appendTo=\"body\"\n [options]=\"presetSet.classes\"\n optionLabel=\"name\"\n optionValue=\"name\"\n [(ngModel)]=\"bulkClass\"\n placeholder=\"Choose a class\u2026\"\n [filter]=\"presetSet.classes.length > 8\"\n filterBy=\"name\"\n (onChange)=\"applyBulkClass()\"\n >\n <ng-template let-c pTemplate=\"item\">\n <span class=\"dd-value\"\n ><span class=\"chip-swatch\" [style.background]=\"c.color\"></span>{{ c.name }}</span\n >\n </ng-template>\n </p-dropdown>\n <div class=\"p-inputgroup\">\n <input\n type=\"text\"\n pInputText\n placeholder=\"\u2026or a new class\"\n [(ngModel)]=\"newBulkClass\"\n (keydown.enter)=\"addAndApplyBulkClass()\"\n />\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n pTooltip=\"Add class & set it\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!newBulkClass.trim()\"\n (click)=\"addAndApplyBulkClass()\"\n ></button>\n </div>\n </div>\n </p-overlayPanel>\n <p-button\n icon=\"pi pi-trash\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"deleteSelectedRegions()\"\n pTooltip=\"Delete selected regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n ></p-button>\n <p-button\n icon=\"pi pi-times-circle\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"clearAllRegions()\"\n pTooltip=\"Delete all regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n ></p-button>\n <p-button\n icon=\"pi pi-question\"\n size=\"small\"\n [text]=\"true\"\n (click)=\"showHelp()\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n ></p-button>\n </div>\n</p-toolbar>\n\n<region-classes-panel\n [classes]=\"displayClasses\"\n [counts]=\"classCounts\"\n [matchMode]=\"presetSet.matchMode\"\n [activeClass]=\"activeClass\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n [defaultClassName]=\"defaultClassName\"\n (pick)=\"selectActiveClass($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (remove)=\"deleteClass($event)\"\n (manage)=\"openManageDialog()\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (toggle3d)=\"toggleClass3d($event)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden([$event], !hiddenClasses.has($event))\"\n [treeView]=\"classHierarchy ? classTreeView : null\"\n (treeViewChange)=\"classTreeView = $event\"\n>\n <!-- A region set with a class hierarchy (an atlas ontology) can show its classes as a tree. -->\n <region-class-tree\n *ngIf=\"classHierarchy as hierarchy\"\n [nodes]=\"hierarchy\"\n [counts]=\"treeCounts\"\n [colorOf]=\"treeColor\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (pick)=\"pickClasses($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (toggle3d)=\"toggleClass3d($event.name, $event.classes)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden($event.classes, $event.hidden)\"\n >\n </region-class-tree>\n</region-classes-panel>\n\n<region-table\n [page]=\"pagedRegions\"\n [total]=\"tableTotal\"\n [first]=\"paginatorFirst\"\n [rows]=\"paginatorRows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [metaKey]=\"metaKey\"\n [selection]=\"selectedRegions\"\n (selectionChange)=\"selectedRegions = $event\"\n (rowSelect)=\"onSelectionChanged()\"\n [presetSet]=\"tablePresetSet\"\n [defaultColor]=\"shapeColor\"\n [mpp]=\"mpp\"\n [volumes]=\"tableVolumes\"\n [labelDrafts]=\"editingLabelRegions\"\n (labelEditStart)=\"startEditLabel($event)\"\n (labelDraftChange)=\"setLabelDraft($event.region, $event.value)\"\n (labelEditStop)=\"stopEditLabel($event.region, $event.commit)\"\n (classPick)=\"applyPresetToRegion($event.region, $event.name)\"\n (deleteRow)=\"deleteRegion($event)\"\n (pageChange)=\"onPageChange($event)\"\n>\n</region-table>\n\n<!-- Live-edit mode: no Save/Cancel \u2014 every edit commits immediately to the\n active image's region cache. -->\n\n<p-confirmDialog\n #cd\n [style]=\"{ width: '24rem', height: '14rem' }\"\n key=\"positionDialog\"\n [baseZIndex]=\"10000\"\n rejectButtonStyleClass=\"p-button-outlined\"\n>\n <ng-template pTemplate=\"footer\">\n <button type=\"button\" pButton icon=\"pi pi-check\" label=\"Yes\" (click)=\"cd.accept()\"></button>\n <button\n type=\"button\"\n class=\"p-button-outlined\"\n pButton\n icon=\"pi pi-times\"\n label=\"No\"\n (click)=\"cd.reject()\"\n ></button>\n </ng-template>\n</p-confirmDialog>\n\n<region-save-regions-dialog\n [(visible)]=\"showSaveAsDialog\"\n [filename]=\"saveAsFilename\"\n (filenameChange)=\"saveAsFilename = $event; checkSaveAsFileExists()\"\n [fileExists]=\"saveAsFileExists\"\n [busy]=\"saveAsBusy\"\n (confirm)=\"confirmSaveAs()\"\n (cancelSave)=\"cancelSaveAs()\"\n>\n</region-save-regions-dialog>\n\n<region-save-regions-dialog\n header=\"Export Regions\"\n confirmLabel=\"Export\"\n confirmIcon=\"pi pi-download\"\n inputId=\"export-filename\"\n [(visible)]=\"showExportDialog\"\n [(filename)]=\"exportFilename\"\n (confirm)=\"confirmExport()\"\n>\n</region-save-regions-dialog>\n\n<region-save-mask-dialog\n [(visible)]=\"showSaveMaskDialog\"\n [(filename)]=\"saveMaskFilename\"\n [(mode)]=\"maskMode\"\n [busy]=\"maskBusy\"\n [progress]=\"maskProgress\"\n [encoding]=\"maskEncoding\"\n (confirm)=\"confirmSaveMask()\"\n (cancelExport)=\"cancelSaveMask()\"\n>\n</region-save-mask-dialog>\n\n<region-color-dialog\n [(visible)]=\"showColorDialog\"\n [edits]=\"classColorEdits\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n (apply)=\"applyColorToSelected($event)\"\n>\n</region-color-dialog>\n\n<!-- Manage annotation classes (jit-ui#70): edits a working copy, committed on Apply. -->\n<region-manage-classes-dialog\n [(visible)]=\"showManageDialog\"\n [(draft)]=\"presetDraft\"\n (apply)=\"applyManageDialog(true)\"\n (resetDefaults)=\"resetPresetsToDefaults()\"\n (importFile)=\"importPresets($event)\"\n (export)=\"exportPresets()\"\n>\n</region-manage-classes-dialog>\n\n<region-editor-help [(visible)]=\"displayHelpDialog\"></region-editor-help>\n", styles: ["@charset \"UTF-8\";.chip-swatch{display:inline-block;width:.75rem;height:.75rem;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}.dd-value{display:inline-flex;align-items:center;gap:.4rem;min-width:0}.dd-value .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}::ng-deep region-editor .p-toolbar.region-toolbar{padding:.25rem .5rem;border-radius:0;border-left:none;border-right:none;border-top:none}::ng-deep region-editor .p-toolbar.region-toolbar .p-button.p-button-icon-only{width:auto;padding:.25rem .5rem}::ng-deep region-editor .p-toolbar.region-toolbar .p-button{padding:.25rem .5rem}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}\n"] }]
|
|
15726
16283
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
15727
16284
|
type: Inject,
|
|
15728
16285
|
args: [REGION_EDITOR_API]
|
|
@@ -22971,6 +23528,8 @@ class VisualizerComponent {
|
|
|
22971
23528
|
/** Isosurface band as a 0–255 slider position, mapped onto the volume's real
|
|
22972
23529
|
* intensity range by the renderer. Defaults to the full range. */
|
|
22973
23530
|
this.isoRange = [0, 255];
|
|
23531
|
+
/** Waits for a viewer created outside the document to be attached (see {@link whenInDocument}). */
|
|
23532
|
+
this.attachObserver = null;
|
|
22974
23533
|
/** Debounced z-slice scrubbing (see SliceScrubber — refactoring plan Step 7). */
|
|
22975
23534
|
this.scrubber = new SliceScrubber((z) => {
|
|
22976
23535
|
this.plotService.setZIndex(z);
|
|
@@ -23250,12 +23809,35 @@ class VisualizerComponent {
|
|
|
23250
23809
|
// re-enters the zone (ngZone.run) only when it actually changes view state.
|
|
23251
23810
|
this.shortcuts = new ViewerShortcuts(this.shortcutHost(), this.ngZone);
|
|
23252
23811
|
this.shortcuts.attach();
|
|
23253
|
-
this.onViewReady();
|
|
23812
|
+
this.whenInDocument(() => this.onViewReady());
|
|
23254
23813
|
}
|
|
23255
23814
|
/** The plot div exists now: run an image-less draw that arrived before it did. */
|
|
23256
23815
|
onViewReady() {
|
|
23257
23816
|
this.render.viewIsReady(this.spatial.dataset, !!this.imageInfo);
|
|
23258
23817
|
}
|
|
23818
|
+
/**
|
|
23819
|
+
* Run `ready` once this viewer is in the document. A host may create it detached — a closed
|
|
23820
|
+
* dialog's projected content, such as jit-ui's pipeline preview — and the backends find the
|
|
23821
|
+
* plot div by id, so a draw then found no target and alerted "the renderer could not start"
|
|
23822
|
+
* over another viewer. Until then its view is not ready, and an image-less draw waits.
|
|
23823
|
+
*/
|
|
23824
|
+
whenInDocument(ready) {
|
|
23825
|
+
const el = this.hostRef?.nativeElement;
|
|
23826
|
+
if (!el || el.isConnected || typeof ResizeObserver === 'undefined') {
|
|
23827
|
+
ready();
|
|
23828
|
+
return;
|
|
23829
|
+
}
|
|
23830
|
+
// Attached and laid out, its size changes from nothing: that is when it can be drawn into.
|
|
23831
|
+
const observer = new ResizeObserver(() => {
|
|
23832
|
+
if (!el.isConnected)
|
|
23833
|
+
return;
|
|
23834
|
+
observer.disconnect();
|
|
23835
|
+
this.attachObserver = null;
|
|
23836
|
+
this.ngZone.run(ready);
|
|
23837
|
+
});
|
|
23838
|
+
this.attachObserver = observer;
|
|
23839
|
+
observer.observe(el);
|
|
23840
|
+
}
|
|
23259
23841
|
/** What the keyboard, wheel and context-menu shortcuts act on. Their listeners live
|
|
23260
23842
|
* outside this view, so every action that changes what it shows marks it for check. */
|
|
23261
23843
|
shortcutHost() {
|
|
@@ -23304,6 +23886,7 @@ class VisualizerComponent {
|
|
|
23304
23886
|
this.toggleDragMode('select');
|
|
23305
23887
|
}
|
|
23306
23888
|
ngOnDestroy() {
|
|
23889
|
+
this.attachObserver?.disconnect();
|
|
23307
23890
|
// A contributed mode's session ends first, while the viewer it drew over
|
|
23308
23891
|
// still exists. The panel goes with this view, so no change detection.
|
|
23309
23892
|
this.contributions.destroy();
|
|
@@ -27699,8 +28282,12 @@ class RegionSourceScope {
|
|
|
27699
28282
|
/** The entry of the slice on screen: kept open. */
|
|
27700
28283
|
this.shown = null;
|
|
27701
28284
|
}
|
|
27702
|
-
/**
|
|
27703
|
-
|
|
28285
|
+
/**
|
|
28286
|
+
* Attach `open` to `image` for `slices`, replacing (and closing) what was there. `imported` names
|
|
28287
|
+
* the slices it brings that a save must write (see {@link importedSlices}): for a source of one
|
|
28288
|
+
* slice, that slice when non-empty; for a `'planes'` source, the slices its planes cover.
|
|
28289
|
+
*/
|
|
28290
|
+
attach(image, open, slices, imported = []) {
|
|
27704
28291
|
const sources = this.imageSources(image);
|
|
27705
28292
|
const entry = { open, source: null, ready: null };
|
|
27706
28293
|
if (slices === 'planes') {
|
|
@@ -27710,6 +28297,7 @@ class RegionSourceScope {
|
|
|
27710
28297
|
sources.perSlice.forEach((e) => this.close(e));
|
|
27711
28298
|
sources.perSlice.clear();
|
|
27712
28299
|
sources.taken.clear();
|
|
28300
|
+
sources.imported = new Set(imported);
|
|
27713
28301
|
}
|
|
27714
28302
|
else {
|
|
27715
28303
|
const old = sources.perSlice.get(slices.slice);
|
|
@@ -27718,6 +28306,10 @@ class RegionSourceScope {
|
|
|
27718
28306
|
entry.plane = slices.plane ?? 0;
|
|
27719
28307
|
sources.perSlice.set(slices.slice, entry);
|
|
27720
28308
|
sources.taken.delete(slices.slice);
|
|
28309
|
+
if (imported.length)
|
|
28310
|
+
sources.imported.add(slices.slice);
|
|
28311
|
+
else
|
|
28312
|
+
sources.imported.delete(slices.slice);
|
|
27721
28313
|
}
|
|
27722
28314
|
}
|
|
27723
28315
|
/** Drop every source of `image` (closing the open ones) and what was taken from them. */
|
|
@@ -27739,6 +28331,7 @@ class RegionSourceScope {
|
|
|
27739
28331
|
this.close(entry);
|
|
27740
28332
|
sources.perSlice.delete(slice);
|
|
27741
28333
|
sources.taken.delete(slice);
|
|
28334
|
+
sources.imported.delete(slice);
|
|
27742
28335
|
}
|
|
27743
28336
|
/** The slice on screen (null: none): its source stays open while others are read. */
|
|
27744
28337
|
show(image, slice) {
|
|
@@ -27782,6 +28375,11 @@ class RegionSourceScope {
|
|
|
27782
28375
|
const taken = image ? this.images.get(image)?.taken : undefined;
|
|
27783
28376
|
return taken ? Array.from(taken.values()).flatMap((ids) => Array.from(ids)) : [];
|
|
27784
28377
|
}
|
|
28378
|
+
/** The slices of `image` whose source was imported (see {@link attach}), in order. */
|
|
28379
|
+
importedSlices(image) {
|
|
28380
|
+
const imported = image ? this.images.get(image)?.imported : undefined;
|
|
28381
|
+
return imported ? Array.from(imported).sort((a, b) => a - b) : [];
|
|
28382
|
+
}
|
|
27785
28383
|
/** The slices of `image` with anything taken from their source. */
|
|
27786
28384
|
slicesWithTaken(image) {
|
|
27787
28385
|
const taken = image ? this.images.get(image)?.taken : undefined;
|
|
@@ -27806,7 +28404,7 @@ class RegionSourceScope {
|
|
|
27806
28404
|
imageSources(image) {
|
|
27807
28405
|
let sources = this.images.get(image);
|
|
27808
28406
|
if (!sources) {
|
|
27809
|
-
sources = { planes: null, perSlice: new Map(), taken: new Map() };
|
|
28407
|
+
sources = { planes: null, perSlice: new Map(), taken: new Map(), imported: new Set() };
|
|
27810
28408
|
this.images.set(image, sources);
|
|
27811
28409
|
}
|
|
27812
28410
|
return sources;
|
|
@@ -28824,8 +29422,12 @@ class RegionStore {
|
|
|
28824
29422
|
*/
|
|
28825
29423
|
getStackSaveSlices() {
|
|
28826
29424
|
const slices = this.scope.stackSaveSlices(this.regions);
|
|
28827
|
-
// A slice whose region source lost features (taken, deleted) is re-written too
|
|
28828
|
-
|
|
29425
|
+
// A slice whose region source lost features (taken, deleted) is re-written too, and so is one
|
|
29426
|
+
// whose source was imported: it is on no file yet.
|
|
29427
|
+
const image = this.scope.activeImage;
|
|
29428
|
+
const changed = [
|
|
29429
|
+
...new Set([...this.sources.slicesWithTaken(image), ...this.sources.importedSlices(image)]),
|
|
29430
|
+
].filter((z) => !slices.has(z));
|
|
28829
29431
|
if (!changed.length)
|
|
28830
29432
|
return slices;
|
|
28831
29433
|
for (const z of changed)
|
|
@@ -29218,13 +29820,19 @@ class RegionStore {
|
|
|
29218
29820
|
* source was closed to save memory). `slices` says which slice of the current image it
|
|
29219
29821
|
* serves: one (default: the slice shown; plane 0 of the source), or `'planes'`, every slice z
|
|
29220
29822
|
* from plane z of a z-indexed source. Replaces the source those slices had.
|
|
29823
|
+
*
|
|
29824
|
+
* `imported` marks a source brought in over the image (an imported volume, folder or served set)
|
|
29825
|
+
* rather than the image's own regions: it is on no file yet, so a save writes its slices though
|
|
29826
|
+
* nothing was taken from it — `true` for a source of one slice, the slices its planes cover for a
|
|
29827
|
+
* `'planes'` source. The image's own sources are not marked: loading them changes nothing to save.
|
|
29221
29828
|
*/
|
|
29222
|
-
attachRegionSource(source, slices = { slice: this.sourceSlice }) {
|
|
29829
|
+
attachRegionSource(source, slices = { slice: this.sourceSlice }, imported = false) {
|
|
29223
29830
|
const image = this.scope.activeImage;
|
|
29224
29831
|
if (!image)
|
|
29225
29832
|
return;
|
|
29226
29833
|
const open = typeof source === 'function' ? source : () => Promise.resolve(source);
|
|
29227
|
-
|
|
29834
|
+
const importedSlices = imported === false ? [] : imported === true ? (slices === 'planes' ? [] : [slices.slice]) : imported;
|
|
29835
|
+
this.sources.attach(image, open, slices, importedSlices);
|
|
29228
29836
|
this.sourcesVersion++;
|
|
29229
29837
|
this.refreshSourceView();
|
|
29230
29838
|
}
|
|
@@ -29602,22 +30210,50 @@ class RegionStore {
|
|
|
29602
30210
|
return view.source.getIndex(view.plane, { ...query, exclude: Array.from(view.taken) });
|
|
29603
30211
|
}
|
|
29604
30212
|
/**
|
|
29605
|
-
* `regions` as a GeoJSON FeatureCollection string merged with the region
|
|
29606
|
-
* the
|
|
29607
|
-
* {@link getGeoJsonString}.
|
|
29608
|
-
*
|
|
30213
|
+
* `regions` as a GeoJSON FeatureCollection string merged with the region sources behind them:
|
|
30214
|
+
* the sources' features that were not taken (or deleted), then `regions`. Without a source,
|
|
30215
|
+
* {@link getGeoJsonString}.
|
|
30216
|
+
*
|
|
30217
|
+
* `slice` names one slice-file of a folder stack (a per-slice-file save): its source's features,
|
|
30218
|
+
* on the file's own plane — of a z-indexed source over every slice, only that slice's plane.
|
|
30219
|
+
* Without it, the export is the image's: on a stack saved as one file, every slice's source
|
|
30220
|
+
* (each feature on its slice's plane, `geometry.plane.z`) and a z-indexed source's every plane;
|
|
30221
|
+
* otherwise the source of the slice shown.
|
|
29609
30222
|
*/
|
|
29610
|
-
async getGeoJsonWithSource(regions, slice
|
|
30223
|
+
async getGeoJsonWithSource(regions, slice) {
|
|
29611
30224
|
const image = this.scope.activeImage;
|
|
29612
30225
|
const planes = this.scope.stackMode && this.sources.hasPlanesSource(image);
|
|
29613
|
-
const binding = await this.sources.binding(image, slice);
|
|
29614
|
-
if (!binding)
|
|
29615
|
-
return regionsToGeoJson(regions);
|
|
29616
30226
|
const features = regions
|
|
29617
30227
|
.filter((r) => !r.isProfile?.())
|
|
29618
30228
|
.map((r) => featureFromRegion(r))
|
|
29619
30229
|
.filter((f) => !!f);
|
|
29620
|
-
|
|
30230
|
+
if (slice === undefined && this.scope.stackMode && this.scope.saveLayout === 'combined' && !planes) {
|
|
30231
|
+
const sliced = this.sources.slicesWithSource(image);
|
|
30232
|
+
if (sliced.length) {
|
|
30233
|
+
const sourceFeatures = [];
|
|
30234
|
+
for (const z of sliced) {
|
|
30235
|
+
const binding = await this.sources.binding(image, z);
|
|
30236
|
+
if (!binding)
|
|
30237
|
+
continue;
|
|
30238
|
+
const exported = await binding.source.exportGeoJson(Array.from(this.sources.taken(image, z)), []);
|
|
30239
|
+
sourceFeatures.push(...onPlane(planeFeatures(exported, binding.plane), z));
|
|
30240
|
+
}
|
|
30241
|
+
return JSON.stringify({ type: 'FeatureCollection', features: [...sourceFeatures, ...features] });
|
|
30242
|
+
}
|
|
30243
|
+
}
|
|
30244
|
+
const at = slice ?? this.sourceSlice;
|
|
30245
|
+
const binding = await this.sources.binding(image, at);
|
|
30246
|
+
if (!binding)
|
|
30247
|
+
return regionsToGeoJson(regions);
|
|
30248
|
+
if (planes && slice !== undefined) {
|
|
30249
|
+
// One slice-file of a z-indexed source: that slice's plane, written on the file's plane 0.
|
|
30250
|
+
const exported = await binding.source.exportGeoJson(this.sources.allTaken(image), []);
|
|
30251
|
+
return JSON.stringify({
|
|
30252
|
+
type: 'FeatureCollection',
|
|
30253
|
+
features: [...onPlane(planeFeatures(exported, binding.plane), 0), ...features],
|
|
30254
|
+
});
|
|
30255
|
+
}
|
|
30256
|
+
const exclude = planes ? this.sources.allTaken(image) : Array.from(this.sources.taken(image, at));
|
|
29621
30257
|
return binding.source.exportGeoJson(exclude, features);
|
|
29622
30258
|
}
|
|
29623
30259
|
// ── Region look ────────────────────────────────────────────────────────
|
|
@@ -29858,6 +30494,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
|
|
|
29858
30494
|
type: Injectable,
|
|
29859
30495
|
args: [{ providedIn: 'root' }]
|
|
29860
30496
|
}], ctorParameters: () => [{ type: VisualizerStore }] });
|
|
30497
|
+
/** The features of an exported FeatureCollection on `plane` (a missing plane is plane 0). */
|
|
30498
|
+
function planeFeatures(geoJson, plane) {
|
|
30499
|
+
const features = (JSON.parse(geoJson)?.features ?? []);
|
|
30500
|
+
return features.filter((f) => (f?.geometry?.plane?.z ?? 0) === plane);
|
|
30501
|
+
}
|
|
30502
|
+
/** `features` moved onto plane `z`, as QuPath writes it: `geometry.plane`, left out for plane 0. */
|
|
30503
|
+
function onPlane(features, z) {
|
|
30504
|
+
return features.map((f) => {
|
|
30505
|
+
const { plane: _plane, ...geometry } = f.geometry ?? {};
|
|
30506
|
+
return { ...f, geometry: z === 0 ? geometry : { ...geometry, plane: { c: -1, z, t: 0 } } };
|
|
30507
|
+
});
|
|
30508
|
+
}
|
|
29861
30509
|
|
|
29862
30510
|
/**
|
|
29863
30511
|
* Pure intensity-profile math: sampling a line through a pixel frame, placing a
|
|
@@ -31396,6 +32044,12 @@ function silenceOsdMultiImageAdvisory() {
|
|
|
31396
32044
|
});
|
|
31397
32045
|
osd.__multiImageFilterInstalled = true;
|
|
31398
32046
|
}
|
|
32047
|
+
/** Tile cache entries a single image keeps per tiled image (one per channel when drawn per channel). */
|
|
32048
|
+
const TILES_PER_IMAGE = 150;
|
|
32049
|
+
/** The most tiles a single per-channel image keeps (each a decoded 512² tile, ~1 MB). */
|
|
32050
|
+
const MAX_CHANNEL_TILES = 1200;
|
|
32051
|
+
/** Tile requests in flight at once for an image drawn per channel. */
|
|
32052
|
+
const CHANNEL_REQUEST_LIMIT = 12;
|
|
31399
32053
|
/**
|
|
31400
32054
|
* The options object handed to the OpenSeadragon factory. Pure: every value
|
|
31401
32055
|
* that depends on the service comes in through `input`.
|
|
@@ -31481,7 +32135,17 @@ function buildViewerOptions(input) {
|
|
|
31481
32135
|
// so they get a much larger tile budget — otherwise switching slices evicts
|
|
31482
32136
|
// each other's tiles and scrubbing back re-fetches. Scaled to the resident
|
|
31483
32137
|
// slice count; single images keep the lean default.
|
|
31484
|
-
|
|
32138
|
+
// A single image drawn per channel needs room for every visible channel's tiles in view: at
|
|
32139
|
+
// 150 a 22-channel plate evicted tiles while they were still being converted.
|
|
32140
|
+
maxImageCacheCount: input.sliceCount > 1
|
|
32141
|
+
? Math.min(2400, Math.max(600, input.maxSlices * 40))
|
|
32142
|
+
: Math.min(MAX_CHANNEL_TILES, TILES_PER_IMAGE * Math.max(1, Math.ceil((input.channelImages ?? 1) / 2))),
|
|
32143
|
+
// OSD sends every tile request at once by default (imageLoaderLimit 0) and times each out
|
|
32144
|
+
// after 30 s from when it is sent. A view of a 22-channel image asks for hundreds of tiles;
|
|
32145
|
+
// the browser holds all but a few connections' worth back, so those timed out while still
|
|
32146
|
+
// waiting their turn. Limited, they wait in OSD's queue (not timed), where a channel being
|
|
32147
|
+
// hidden drops its own (SliceCache.revealChannelSlice). Single images keep OSD's default.
|
|
32148
|
+
...((input.channelImages ?? 1) > 1 ? { imageLoaderLimit: CHANNEL_REQUEST_LIMIT } : {}),
|
|
31485
32149
|
};
|
|
31486
32150
|
}
|
|
31487
32151
|
|
|
@@ -32935,6 +33599,234 @@ class OsdRegionOverlay {
|
|
|
32935
33599
|
}
|
|
32936
33600
|
}
|
|
32937
33601
|
|
|
33602
|
+
/**
|
|
33603
|
+
* "Transcripts reloading…" at the bottom of the canvas while a layer's data is in flight: one
|
|
33604
|
+
* line naming every layer still loading, on a semi-opaque backing so it reads over any tissue.
|
|
33605
|
+
* Longer work names itself ("Building HPF in 3D…") and joins the line.
|
|
33606
|
+
*
|
|
33607
|
+
* Shown only once something has been loading for {@link SHOW_AFTER_MS}, so a redraw served from
|
|
33608
|
+
* cache does not flash it.
|
|
33609
|
+
*/
|
|
33610
|
+
const SHOW_AFTER_MS = 150;
|
|
33611
|
+
class NapariLoadingBadge {
|
|
33612
|
+
constructor(host) {
|
|
33613
|
+
this.names = [];
|
|
33614
|
+
this.tasks = [];
|
|
33615
|
+
this.timer = null;
|
|
33616
|
+
if (getComputedStyle(host).position === 'static')
|
|
33617
|
+
host.style.position = 'relative';
|
|
33618
|
+
this.el = document.createElement('div');
|
|
33619
|
+
this.el.className = 'napari-loading-badge';
|
|
33620
|
+
this.el.setAttribute('role', 'status');
|
|
33621
|
+
this.el.setAttribute('aria-live', 'polite');
|
|
33622
|
+
Object.assign(this.el.style, {
|
|
33623
|
+
position: 'absolute',
|
|
33624
|
+
left: '50%',
|
|
33625
|
+
bottom: '12px',
|
|
33626
|
+
transform: 'translateX(-50%)',
|
|
33627
|
+
padding: '4px 12px',
|
|
33628
|
+
borderRadius: '12px',
|
|
33629
|
+
background: 'rgba(0, 0, 0, 0.6)',
|
|
33630
|
+
color: '#fff',
|
|
33631
|
+
font: '12px/1.4 system-ui, sans-serif',
|
|
33632
|
+
whiteSpace: 'nowrap',
|
|
33633
|
+
pointerEvents: 'none',
|
|
33634
|
+
zIndex: '5',
|
|
33635
|
+
display: 'none',
|
|
33636
|
+
});
|
|
33637
|
+
host.appendChild(this.el);
|
|
33638
|
+
}
|
|
33639
|
+
/** The layers loading now, and the tasks running (their messages); both empty hides the badge. */
|
|
33640
|
+
set(names, tasks = []) {
|
|
33641
|
+
this.names = [...new Set(names)];
|
|
33642
|
+
this.tasks = [...new Set(tasks)];
|
|
33643
|
+
if (!this.names.length && !this.tasks.length) {
|
|
33644
|
+
if (this.timer)
|
|
33645
|
+
clearTimeout(this.timer);
|
|
33646
|
+
this.timer = null;
|
|
33647
|
+
this.el.style.display = 'none';
|
|
33648
|
+
return;
|
|
33649
|
+
}
|
|
33650
|
+
const parts = this.names.length ? [`${listOf(this.names)} reloading…`, ...this.tasks] : this.tasks;
|
|
33651
|
+
this.el.textContent = parts.join(' · ');
|
|
33652
|
+
if (this.el.style.display === 'block' || this.timer)
|
|
33653
|
+
return;
|
|
33654
|
+
this.timer = setTimeout(() => {
|
|
33655
|
+
this.timer = null;
|
|
33656
|
+
if (this.names.length || this.tasks.length)
|
|
33657
|
+
this.el.style.display = 'block';
|
|
33658
|
+
}, SHOW_AFTER_MS);
|
|
33659
|
+
}
|
|
33660
|
+
/** The text currently set (shown or about to be). */
|
|
33661
|
+
get text() {
|
|
33662
|
+
return this.names.length || this.tasks.length ? (this.el.textContent ?? '') : '';
|
|
33663
|
+
}
|
|
33664
|
+
destroy() {
|
|
33665
|
+
if (this.timer)
|
|
33666
|
+
clearTimeout(this.timer);
|
|
33667
|
+
this.el.remove();
|
|
33668
|
+
}
|
|
33669
|
+
}
|
|
33670
|
+
/** "A", "A and b", "A, b and c": the first as given, the rest with a lower-cased initial. */
|
|
33671
|
+
function listOf(names) {
|
|
33672
|
+
const [first, ...rest] = names;
|
|
33673
|
+
const lower = rest.map((n) => n.charAt(0).toLowerCase() + n.slice(1));
|
|
33674
|
+
const all = [first, ...lower];
|
|
33675
|
+
return all.length === 1 ? all[0] : `${all.slice(0, -1).join(', ')} and ${all[all.length - 1]}`;
|
|
33676
|
+
}
|
|
33677
|
+
|
|
33678
|
+
/**
|
|
33679
|
+
* "x reloading…" at the bottom of the canvas, aggregated over everything that loads into one
|
|
33680
|
+
* scene: image tiles (the tile client), observation colourings (the spatial scenes) and the
|
|
33681
|
+
* spatial tile layers' own loads (which report a list of layer names).
|
|
33682
|
+
*
|
|
33683
|
+
* Each load is a {@link begin} that hands back its own `end`. {@link reset} starts a new scene:
|
|
33684
|
+
* the counts are dropped, and an `end` from a load that began before it is a no-op, so a load
|
|
33685
|
+
* that settles after a re-plot can neither un-count the new scene's work nor drive a count below
|
|
33686
|
+
* zero (which hid the next scene's "Observations reloading…" until a second load overlapped it).
|
|
33687
|
+
*
|
|
33688
|
+
* A {@link beginTask} is work that outlives a scene (a region's 3D reconstruction, built for
|
|
33689
|
+
* whichever volume is on screen when it is done): its message stays across a reset.
|
|
33690
|
+
*/
|
|
33691
|
+
class LoadingBadgeState {
|
|
33692
|
+
constructor() {
|
|
33693
|
+
this.host = null;
|
|
33694
|
+
this.badge = null;
|
|
33695
|
+
this.counts = new Map();
|
|
33696
|
+
this.tileLayers = [];
|
|
33697
|
+
this.generation = 0;
|
|
33698
|
+
this.tasks = new Map();
|
|
33699
|
+
}
|
|
33700
|
+
/** The element the badge is drawn into (the plot host); set per plot. */
|
|
33701
|
+
attach(host) {
|
|
33702
|
+
this.host = host;
|
|
33703
|
+
if (this.tasks.size)
|
|
33704
|
+
this.show();
|
|
33705
|
+
}
|
|
33706
|
+
/** One load of `source` started; call the returned function once it settles. */
|
|
33707
|
+
begin(source) {
|
|
33708
|
+
const generation = this.generation;
|
|
33709
|
+
this.counts.set(source, (this.counts.get(source) ?? 0) + 1);
|
|
33710
|
+
this.show();
|
|
33711
|
+
let ended = false;
|
|
33712
|
+
return () => {
|
|
33713
|
+
if (ended || generation !== this.generation)
|
|
33714
|
+
return;
|
|
33715
|
+
ended = true;
|
|
33716
|
+
this.counts.set(source, (this.counts.get(source) ?? 0) - 1);
|
|
33717
|
+
this.show();
|
|
33718
|
+
};
|
|
33719
|
+
}
|
|
33720
|
+
/** A task started, shown by its `message` ("Building HPF in 3D…"); call the returned function
|
|
33721
|
+
* once it settles. */
|
|
33722
|
+
beginTask(message) {
|
|
33723
|
+
const id = Symbol(message);
|
|
33724
|
+
this.tasks.set(id, message);
|
|
33725
|
+
this.show();
|
|
33726
|
+
return () => {
|
|
33727
|
+
if (this.tasks.delete(id))
|
|
33728
|
+
this.show();
|
|
33729
|
+
};
|
|
33730
|
+
}
|
|
33731
|
+
/** The spatial tile layers still loading (their own names, in their order). */
|
|
33732
|
+
setTileLayers(layers) {
|
|
33733
|
+
this.tileLayers = layers;
|
|
33734
|
+
this.show();
|
|
33735
|
+
}
|
|
33736
|
+
/** The text the badge has set (shown, or about to be once the show delay passes). */
|
|
33737
|
+
get text() {
|
|
33738
|
+
return this.badge?.text ?? '';
|
|
33739
|
+
}
|
|
33740
|
+
/** A new scene: forget every count and remove the badge (tasks show again on the next attach). */
|
|
33741
|
+
reset() {
|
|
33742
|
+
this.generation++;
|
|
33743
|
+
this.counts.clear();
|
|
33744
|
+
this.tileLayers = [];
|
|
33745
|
+
this.badge?.destroy();
|
|
33746
|
+
this.badge = null;
|
|
33747
|
+
}
|
|
33748
|
+
show() {
|
|
33749
|
+
if (!this.host)
|
|
33750
|
+
return;
|
|
33751
|
+
this.badge ??= new NapariLoadingBadge(this.host);
|
|
33752
|
+
this.badge.set([
|
|
33753
|
+
...((this.counts.get('Image') ?? 0) > 0 ? ['Image'] : []),
|
|
33754
|
+
...((this.counts.get('Observations') ?? 0) > 0 ? ['Observations'] : []),
|
|
33755
|
+
...this.tileLayers,
|
|
33756
|
+
], [...this.tasks.values()]);
|
|
33757
|
+
}
|
|
33758
|
+
}
|
|
33759
|
+
|
|
33760
|
+
/**
|
|
33761
|
+
* "Image reloading…" over the OpenSeadragon view while it fetches the tiles of what it shows, as
|
|
33762
|
+
* napari's views show it for their image, cells and transcripts (the same badge). Driven by each
|
|
33763
|
+
* world item's own `fully-loaded-change` (an image, a stack slice, a channel), so it covers every
|
|
33764
|
+
* tile source alike, and shown only after a short delay, so tiles served from cache never flash it.
|
|
33765
|
+
*
|
|
33766
|
+
* Only items on screen count (opacity above 0): a hidden channel loads nothing by design (it is
|
|
33767
|
+
* not preloaded), so it never becomes fully loaded, and a slice cached or loading in the
|
|
33768
|
+
* background is not what the view shows. An item is weighed again when its opacity changes.
|
|
33769
|
+
*/
|
|
33770
|
+
class OsdTileLoading {
|
|
33771
|
+
constructor(viewer) {
|
|
33772
|
+
this.viewer = viewer;
|
|
33773
|
+
this.state = new LoadingBadgeState();
|
|
33774
|
+
/** Each item still loading, with the function that un-counts it. */
|
|
33775
|
+
this.loading = new Map();
|
|
33776
|
+
this.watched = new Map();
|
|
33777
|
+
this.onAdd = (e) => this.watch(e.item);
|
|
33778
|
+
this.onRemove = (e) => this.unwatch(e.item);
|
|
33779
|
+
this.state.attach(viewer.element);
|
|
33780
|
+
viewer.world.addHandler('add-item', this.onAdd);
|
|
33781
|
+
viewer.world.addHandler('remove-item', this.onRemove);
|
|
33782
|
+
for (let i = 0; i < viewer.world.getItemCount(); i++)
|
|
33783
|
+
this.watch(viewer.world.getItemAt(i));
|
|
33784
|
+
}
|
|
33785
|
+
/** The badge's text (shown, or about to be). */
|
|
33786
|
+
get text() {
|
|
33787
|
+
return this.state.text;
|
|
33788
|
+
}
|
|
33789
|
+
destroy() {
|
|
33790
|
+
this.viewer.world.removeHandler('add-item', this.onAdd);
|
|
33791
|
+
this.viewer.world.removeHandler('remove-item', this.onRemove);
|
|
33792
|
+
for (const item of [...this.watched.keys()])
|
|
33793
|
+
this.unwatch(item);
|
|
33794
|
+
this.state.reset();
|
|
33795
|
+
}
|
|
33796
|
+
watch(item) {
|
|
33797
|
+
if (!item || this.watched.has(item))
|
|
33798
|
+
return;
|
|
33799
|
+
const handler = () => this.set(item, this.isLoading(item));
|
|
33800
|
+
this.watched.set(item, handler);
|
|
33801
|
+
item.addHandler('fully-loaded-change', handler);
|
|
33802
|
+
item.addHandler('opacity-change', handler);
|
|
33803
|
+
handler();
|
|
33804
|
+
}
|
|
33805
|
+
unwatch(item) {
|
|
33806
|
+
const handler = this.watched.get(item);
|
|
33807
|
+
if (handler) {
|
|
33808
|
+
item.removeHandler('fully-loaded-change', handler);
|
|
33809
|
+
item.removeHandler('opacity-change', handler);
|
|
33810
|
+
}
|
|
33811
|
+
this.watched.delete(item);
|
|
33812
|
+
this.set(item, false);
|
|
33813
|
+
}
|
|
33814
|
+
/** Whether the item is on screen and still fetching its tiles. */
|
|
33815
|
+
isLoading(item) {
|
|
33816
|
+
const opacity = typeof item.getOpacity === 'function' ? item.getOpacity() : 1;
|
|
33817
|
+
return opacity > 0 && !item.getFullyLoaded();
|
|
33818
|
+
}
|
|
33819
|
+
set(item, loading) {
|
|
33820
|
+
const end = this.loading.get(item);
|
|
33821
|
+
if (loading && !end)
|
|
33822
|
+
this.loading.set(item, this.state.begin('Image'));
|
|
33823
|
+
else if (!loading && end) {
|
|
33824
|
+
end();
|
|
33825
|
+
this.loading.delete(item);
|
|
33826
|
+
}
|
|
33827
|
+
}
|
|
33828
|
+
}
|
|
33829
|
+
|
|
32938
33830
|
/**
|
|
32939
33831
|
* A physical scale bar drawn over an OpenSeadragon viewer. The length maths (the nice 1/2/5 × 10ⁿ
|
|
32940
33832
|
* length nearest the target, and its unit) are napari-js's `scaleBarFor` / `formatLength`, which
|
|
@@ -33790,7 +34682,10 @@ class SliceCache {
|
|
|
33790
34682
|
/**
|
|
33791
34683
|
* Add slice z's channel group: one additively-composited ('lighter' = Fiji
|
|
33792
34684
|
* "Composite") TiledImage per channel, hidden (opacity 0) until revealed.
|
|
33793
|
-
*
|
|
34685
|
+
* The visible channels' are preloaded, so the background loader can pre-fill
|
|
34686
|
+
* every slice; a hidden channel's loads nothing until it is shown (a 22-channel
|
|
34687
|
+
* plate fetched and decoded every channel's tiles, overflowing the tile cache:
|
|
34688
|
+
* tiles evicted mid-conversion failed to decode).
|
|
33794
34689
|
* Cached so a revisit is an instant opacity toggle — no re-fetch, no
|
|
33795
34690
|
* white-flicker. Mirrors addSlice for the composite path.
|
|
33796
34691
|
*/
|
|
@@ -33831,7 +34726,7 @@ class SliceCache {
|
|
|
33831
34726
|
width: 1,
|
|
33832
34727
|
opacity: 0, // revealed by revealChannelSlice once the group is in
|
|
33833
34728
|
compositeOperation: 'lighter',
|
|
33834
|
-
preload:
|
|
34729
|
+
preload: this.host.channelVisible(c),
|
|
33835
34730
|
success: (e) => {
|
|
33836
34731
|
const item = e?.item;
|
|
33837
34732
|
if (token !== this.sliceLoadToken) {
|
|
@@ -33854,17 +34749,37 @@ class SliceCache {
|
|
|
33854
34749
|
}
|
|
33855
34750
|
}
|
|
33856
34751
|
/** Reveal slice z's channel group (each visible channel → opacity 1) and
|
|
33857
|
-
* hide every other cached slice's group. Instant; no fetch/decode.
|
|
34752
|
+
* hide every other cached slice's group. Instant; no fetch/decode. Only the
|
|
34753
|
+
* visible channels keep loading (preload): a hidden channel's tiles are not
|
|
34754
|
+
* fetched or decoded until it is shown. */
|
|
33858
34755
|
revealChannelSlice(z) {
|
|
34756
|
+
const hidden = new Set();
|
|
33859
34757
|
for (const [zz, group] of this.channelSliceItems) {
|
|
33860
34758
|
const active = zz === z;
|
|
33861
34759
|
group.forEach((it, c) => {
|
|
33862
34760
|
if (!it)
|
|
33863
34761
|
return;
|
|
33864
34762
|
const visible = this.host.channelVisible(c);
|
|
34763
|
+
if (!visible)
|
|
34764
|
+
hidden.add(it);
|
|
34765
|
+
quiet(() => it.setPreload?.(visible));
|
|
33865
34766
|
quiet(() => it.setOpacity(active && visible ? 1 : 0));
|
|
33866
34767
|
});
|
|
33867
34768
|
}
|
|
34769
|
+
this.dropQueuedTiles(hidden);
|
|
34770
|
+
}
|
|
34771
|
+
/** Drop the tile requests still waiting in OSD's queue for `items` (channels just hidden), so
|
|
34772
|
+
* the visible channels' tiles are not stuck behind them. A request already sent completes. */
|
|
34773
|
+
dropQueuedTiles(items) {
|
|
34774
|
+
const loader = this.host.viewer()?.imageLoader;
|
|
34775
|
+
if (!items.size || !Array.isArray(loader?.jobQueue))
|
|
34776
|
+
return;
|
|
34777
|
+
loader.jobQueue = loader.jobQueue.filter((job) => {
|
|
34778
|
+
if (!items.has(job?.tile?.tiledImage))
|
|
34779
|
+
return true;
|
|
34780
|
+
quiet(() => job.abort?.());
|
|
34781
|
+
return false;
|
|
34782
|
+
});
|
|
33868
34783
|
}
|
|
33869
34784
|
/** Show the given composite slice (opacity 1) and hide all other cached slices. */
|
|
33870
34785
|
showOnlySlice(z) {
|
|
@@ -34028,12 +34943,18 @@ class SliceCache {
|
|
|
34028
34943
|
const it = this.sliceItems.get(z);
|
|
34029
34944
|
return it ? [it] : [];
|
|
34030
34945
|
}
|
|
34031
|
-
/** Whether slice z's tiles are still streaming (any cached image not fully loaded).
|
|
34946
|
+
/** Whether slice z's tiles are still streaming (any cached image not fully loaded). A hidden
|
|
34947
|
+
* channel loads nothing, so it never becomes fully loaded: only the visible channels count, or
|
|
34948
|
+
* the background loader would wait on it forever. */
|
|
34032
34949
|
sliceTilesLoading(z) {
|
|
34033
34950
|
const g = this.sliceGroup(z);
|
|
34034
34951
|
if (!g.length)
|
|
34035
34952
|
return false;
|
|
34036
|
-
|
|
34953
|
+
const multi = this.host.isMultiChannel();
|
|
34954
|
+
return g.some((it, c) => it &&
|
|
34955
|
+
(!multi || this.host.channelVisible(c)) &&
|
|
34956
|
+
typeof it.getFullyLoaded === 'function' &&
|
|
34957
|
+
!it.getFullyLoaded());
|
|
34037
34958
|
}
|
|
34038
34959
|
/** Remove slice z's cached image(s) from the world and the active cache. */
|
|
34039
34960
|
removeCachedSlice(z) {
|
|
@@ -35526,6 +36447,8 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
35526
36447
|
this.viewer = null;
|
|
35527
36448
|
this.overlay = null;
|
|
35528
36449
|
this.scaleBar = null;
|
|
36450
|
+
/** "Image reloading…" while the view fetches its tiles. */
|
|
36451
|
+
this.tileLoading = null;
|
|
35529
36452
|
this.descriptor = null;
|
|
35530
36453
|
/** Base64 RawFileInfo + current z-slice, kept so setZIndex can rebuild the
|
|
35531
36454
|
* tile source and swap slices live (stack navigation). */
|
|
@@ -35812,6 +36735,7 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
35812
36735
|
authHeaders: this.authHeaders,
|
|
35813
36736
|
sliceCount: d.z ?? 1,
|
|
35814
36737
|
maxSlices: this.cache.maxSlices(),
|
|
36738
|
+
channelImages: this.isMultiChannel ? (d.channels ?? 1) : 1,
|
|
35815
36739
|
});
|
|
35816
36740
|
this.viewer = createViewer(options);
|
|
35817
36741
|
return openViewer(this.viewer, tileSource, (v) => this.onViewerOpened(v, d, simple, plotDiv));
|
|
@@ -35827,6 +36751,8 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
35827
36751
|
this.overlay = new OsdRegionOverlay(viewer, this.regionStore);
|
|
35828
36752
|
this.coordTransform = new OsdCoordinateTransform(viewer);
|
|
35829
36753
|
this.scaleBar = new OsdScaleBar(viewer, d.mppX ?? 0);
|
|
36754
|
+
this.tileLoading?.destroy();
|
|
36755
|
+
this.tileLoading = new OsdTileLoading(viewer);
|
|
35830
36756
|
if (simple) {
|
|
35831
36757
|
// A single, self-contained image — no slice cache to seed. Its z-scrub and
|
|
35832
36758
|
// recomposite re-open the viewer; one persistent handler reports a failed
|
|
@@ -35886,6 +36812,8 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
35886
36812
|
this.scaleBar.destroy();
|
|
35887
36813
|
this.scaleBar = null;
|
|
35888
36814
|
}
|
|
36815
|
+
this.tileLoading?.destroy();
|
|
36816
|
+
this.tileLoading = null;
|
|
35889
36817
|
if (this.viewer) {
|
|
35890
36818
|
this.viewer.destroy();
|
|
35891
36819
|
this.viewer = null;
|
|
@@ -37387,164 +38315,6 @@ class NapariDisplayState {
|
|
|
37387
38315
|
}
|
|
37388
38316
|
}
|
|
37389
38317
|
|
|
37390
|
-
/**
|
|
37391
|
-
* "Transcripts reloading…" at the bottom of the canvas while a layer's data is in flight: one
|
|
37392
|
-
* line naming every layer still loading, on a semi-opaque backing so it reads over any tissue.
|
|
37393
|
-
* Longer work names itself ("Building HPF in 3D…") and joins the line.
|
|
37394
|
-
*
|
|
37395
|
-
* Shown only once something has been loading for {@link SHOW_AFTER_MS}, so a redraw served from
|
|
37396
|
-
* cache does not flash it.
|
|
37397
|
-
*/
|
|
37398
|
-
const SHOW_AFTER_MS = 150;
|
|
37399
|
-
class NapariLoadingBadge {
|
|
37400
|
-
constructor(host) {
|
|
37401
|
-
this.names = [];
|
|
37402
|
-
this.tasks = [];
|
|
37403
|
-
this.timer = null;
|
|
37404
|
-
if (getComputedStyle(host).position === 'static')
|
|
37405
|
-
host.style.position = 'relative';
|
|
37406
|
-
this.el = document.createElement('div');
|
|
37407
|
-
this.el.className = 'napari-loading-badge';
|
|
37408
|
-
this.el.setAttribute('role', 'status');
|
|
37409
|
-
this.el.setAttribute('aria-live', 'polite');
|
|
37410
|
-
Object.assign(this.el.style, {
|
|
37411
|
-
position: 'absolute',
|
|
37412
|
-
left: '50%',
|
|
37413
|
-
bottom: '12px',
|
|
37414
|
-
transform: 'translateX(-50%)',
|
|
37415
|
-
padding: '4px 12px',
|
|
37416
|
-
borderRadius: '12px',
|
|
37417
|
-
background: 'rgba(0, 0, 0, 0.6)',
|
|
37418
|
-
color: '#fff',
|
|
37419
|
-
font: '12px/1.4 system-ui, sans-serif',
|
|
37420
|
-
whiteSpace: 'nowrap',
|
|
37421
|
-
pointerEvents: 'none',
|
|
37422
|
-
zIndex: '5',
|
|
37423
|
-
display: 'none',
|
|
37424
|
-
});
|
|
37425
|
-
host.appendChild(this.el);
|
|
37426
|
-
}
|
|
37427
|
-
/** The layers loading now, and the tasks running (their messages); both empty hides the badge. */
|
|
37428
|
-
set(names, tasks = []) {
|
|
37429
|
-
this.names = [...new Set(names)];
|
|
37430
|
-
this.tasks = [...new Set(tasks)];
|
|
37431
|
-
if (!this.names.length && !this.tasks.length) {
|
|
37432
|
-
if (this.timer)
|
|
37433
|
-
clearTimeout(this.timer);
|
|
37434
|
-
this.timer = null;
|
|
37435
|
-
this.el.style.display = 'none';
|
|
37436
|
-
return;
|
|
37437
|
-
}
|
|
37438
|
-
const parts = this.names.length ? [`${listOf(this.names)} reloading…`, ...this.tasks] : this.tasks;
|
|
37439
|
-
this.el.textContent = parts.join(' · ');
|
|
37440
|
-
if (this.el.style.display === 'block' || this.timer)
|
|
37441
|
-
return;
|
|
37442
|
-
this.timer = setTimeout(() => {
|
|
37443
|
-
this.timer = null;
|
|
37444
|
-
if (this.names.length || this.tasks.length)
|
|
37445
|
-
this.el.style.display = 'block';
|
|
37446
|
-
}, SHOW_AFTER_MS);
|
|
37447
|
-
}
|
|
37448
|
-
/** The text currently set (shown or about to be). */
|
|
37449
|
-
get text() {
|
|
37450
|
-
return this.names.length || this.tasks.length ? (this.el.textContent ?? '') : '';
|
|
37451
|
-
}
|
|
37452
|
-
destroy() {
|
|
37453
|
-
if (this.timer)
|
|
37454
|
-
clearTimeout(this.timer);
|
|
37455
|
-
this.el.remove();
|
|
37456
|
-
}
|
|
37457
|
-
}
|
|
37458
|
-
/** "A", "A and b", "A, b and c": the first as given, the rest with a lower-cased initial. */
|
|
37459
|
-
function listOf(names) {
|
|
37460
|
-
const [first, ...rest] = names;
|
|
37461
|
-
const lower = rest.map((n) => n.charAt(0).toLowerCase() + n.slice(1));
|
|
37462
|
-
const all = [first, ...lower];
|
|
37463
|
-
return all.length === 1 ? all[0] : `${all.slice(0, -1).join(', ')} and ${all[all.length - 1]}`;
|
|
37464
|
-
}
|
|
37465
|
-
|
|
37466
|
-
/**
|
|
37467
|
-
* "x reloading…" at the bottom of the canvas, aggregated over everything that loads into one
|
|
37468
|
-
* scene: image tiles (the tile client), observation colourings (the spatial scenes) and the
|
|
37469
|
-
* spatial tile layers' own loads (which report a list of layer names).
|
|
37470
|
-
*
|
|
37471
|
-
* Each load is a {@link begin} that hands back its own `end`. {@link reset} starts a new scene:
|
|
37472
|
-
* the counts are dropped, and an `end` from a load that began before it is a no-op, so a load
|
|
37473
|
-
* that settles after a re-plot can neither un-count the new scene's work nor drive a count below
|
|
37474
|
-
* zero (which hid the next scene's "Observations reloading…" until a second load overlapped it).
|
|
37475
|
-
*
|
|
37476
|
-
* A {@link beginTask} is work that outlives a scene (a region's 3D reconstruction, built for
|
|
37477
|
-
* whichever volume is on screen when it is done): its message stays across a reset.
|
|
37478
|
-
*/
|
|
37479
|
-
class LoadingBadgeState {
|
|
37480
|
-
constructor() {
|
|
37481
|
-
this.host = null;
|
|
37482
|
-
this.badge = null;
|
|
37483
|
-
this.counts = new Map();
|
|
37484
|
-
this.tileLayers = [];
|
|
37485
|
-
this.generation = 0;
|
|
37486
|
-
this.tasks = new Map();
|
|
37487
|
-
}
|
|
37488
|
-
/** The element the badge is drawn into (the plot host); set per plot. */
|
|
37489
|
-
attach(host) {
|
|
37490
|
-
this.host = host;
|
|
37491
|
-
if (this.tasks.size)
|
|
37492
|
-
this.show();
|
|
37493
|
-
}
|
|
37494
|
-
/** One load of `source` started; call the returned function once it settles. */
|
|
37495
|
-
begin(source) {
|
|
37496
|
-
const generation = this.generation;
|
|
37497
|
-
this.counts.set(source, (this.counts.get(source) ?? 0) + 1);
|
|
37498
|
-
this.show();
|
|
37499
|
-
let ended = false;
|
|
37500
|
-
return () => {
|
|
37501
|
-
if (ended || generation !== this.generation)
|
|
37502
|
-
return;
|
|
37503
|
-
ended = true;
|
|
37504
|
-
this.counts.set(source, (this.counts.get(source) ?? 0) - 1);
|
|
37505
|
-
this.show();
|
|
37506
|
-
};
|
|
37507
|
-
}
|
|
37508
|
-
/** A task started, shown by its `message` ("Building HPF in 3D…"); call the returned function
|
|
37509
|
-
* once it settles. */
|
|
37510
|
-
beginTask(message) {
|
|
37511
|
-
const id = Symbol(message);
|
|
37512
|
-
this.tasks.set(id, message);
|
|
37513
|
-
this.show();
|
|
37514
|
-
return () => {
|
|
37515
|
-
if (this.tasks.delete(id))
|
|
37516
|
-
this.show();
|
|
37517
|
-
};
|
|
37518
|
-
}
|
|
37519
|
-
/** The spatial tile layers still loading (their own names, in their order). */
|
|
37520
|
-
setTileLayers(layers) {
|
|
37521
|
-
this.tileLayers = layers;
|
|
37522
|
-
this.show();
|
|
37523
|
-
}
|
|
37524
|
-
/** The text the badge has set (shown, or about to be once the show delay passes). */
|
|
37525
|
-
get text() {
|
|
37526
|
-
return this.badge?.text ?? '';
|
|
37527
|
-
}
|
|
37528
|
-
/** A new scene: forget every count and remove the badge (tasks show again on the next attach). */
|
|
37529
|
-
reset() {
|
|
37530
|
-
this.generation++;
|
|
37531
|
-
this.counts.clear();
|
|
37532
|
-
this.tileLayers = [];
|
|
37533
|
-
this.badge?.destroy();
|
|
37534
|
-
this.badge = null;
|
|
37535
|
-
}
|
|
37536
|
-
show() {
|
|
37537
|
-
if (!this.host)
|
|
37538
|
-
return;
|
|
37539
|
-
this.badge ??= new NapariLoadingBadge(this.host);
|
|
37540
|
-
this.badge.set([
|
|
37541
|
-
...((this.counts.get('Image') ?? 0) > 0 ? ['Image'] : []),
|
|
37542
|
-
...((this.counts.get('Observations') ?? 0) > 0 ? ['Observations'] : []),
|
|
37543
|
-
...this.tileLayers,
|
|
37544
|
-
], [...this.tasks.values()]);
|
|
37545
|
-
}
|
|
37546
|
-
}
|
|
37547
|
-
|
|
37548
38318
|
const SVG_NS$1 = 'http://www.w3.org/2000/svg';
|
|
37549
38319
|
/** Rendered handle size (screen px). */
|
|
37550
38320
|
const HANDLE_SIZE = 7;
|
|
@@ -45918,6 +46688,8 @@ class RoutingVisualizerService {
|
|
|
45918
46688
|
this.sectionSubs = new Subscription();
|
|
45919
46689
|
/** The image info and screen height of the last plot (what the slice pane mounts). */
|
|
45920
46690
|
this.plottedInfo = null;
|
|
46691
|
+
/** The current region import (see regionImportTicket): its id and the image it was taken for. */
|
|
46692
|
+
this.importTicket = { id: 0, image: undefined };
|
|
45921
46693
|
this.plottedHeight = 0;
|
|
45922
46694
|
this.viewOptions = {
|
|
45923
46695
|
setNavigatorVisible: (visible) => {
|
|
@@ -46488,6 +47260,94 @@ class RoutingVisualizerService {
|
|
|
46488
47260
|
dropRegionSource() {
|
|
46489
47261
|
return this.regionStore.dropSliceRegionSource();
|
|
46490
47262
|
}
|
|
47263
|
+
/**
|
|
47264
|
+
* A ticket for a region import that is about to read or decode its files: the import methods
|
|
47265
|
+
* given it change nothing once it is stale — another import took a ticket since, another image
|
|
47266
|
+
* was plotted, or the service was destroyed — so an import that finishes late never lands on an
|
|
47267
|
+
* image it was not started for.
|
|
47268
|
+
*/
|
|
47269
|
+
regionImportTicket() {
|
|
47270
|
+
this.importTicket = { id: this.importTicket.id + 1, image: this.importImageKey() };
|
|
47271
|
+
return this.importTicket.id;
|
|
47272
|
+
}
|
|
47273
|
+
/** Whether `ticket` is still the current import's, on the image it was taken for. */
|
|
47274
|
+
importTicketValid(ticket) {
|
|
47275
|
+
return ticket === this.importTicket.id && this.importTicket.image === this.importImageKey();
|
|
47276
|
+
}
|
|
47277
|
+
importImageKey() {
|
|
47278
|
+
return RegionScopeCache.imageKey(this.plottedInfo ?? undefined);
|
|
47279
|
+
}
|
|
47280
|
+
/**
|
|
47281
|
+
* Replace the image's regions and region sources with one source per slice, as an imported label
|
|
47282
|
+
* volume or folder of per-slice region files gives them: `openers` by slice index, each opened
|
|
47283
|
+
* the first time its slice is shown. A single image takes slice 0's. Returns how many slices
|
|
47284
|
+
* have a source, or null (changing nothing) when `ticket` is stale (see
|
|
47285
|
+
* {@link regionImportTicket}). The sources are marked imported, so a save writes every slice.
|
|
47286
|
+
*/
|
|
47287
|
+
importSliceRegionSources(openers, ticket) {
|
|
47288
|
+
if (ticket !== undefined && !this.importTicketValid(ticket))
|
|
47289
|
+
return null;
|
|
47290
|
+
this.regionImportTicket(); // supersedes any import still in flight
|
|
47291
|
+
const info = this.plottedInfo;
|
|
47292
|
+
this.regionStore.detachRegionSources();
|
|
47293
|
+
if (!info?.isStack) {
|
|
47294
|
+
const first = openers.get(0) ?? openers.values().next().value;
|
|
47295
|
+
this.regionStore.setRegions([]);
|
|
47296
|
+
this.regionStore.resetUndoHistory();
|
|
47297
|
+
if (!first)
|
|
47298
|
+
return 0;
|
|
47299
|
+
this.regionStore.attachRegionSource(first, { slice: 0 }, true);
|
|
47300
|
+
return 1;
|
|
47301
|
+
}
|
|
47302
|
+
this.enterImportedStack(info);
|
|
47303
|
+
for (const [z, open] of openers)
|
|
47304
|
+
this.regionStore.attachRegionSource(open, { slice: z }, true);
|
|
47305
|
+
return openers.size;
|
|
47306
|
+
}
|
|
47307
|
+
/**
|
|
47308
|
+
* Replace the image's regions and region sources with one imported source (a region set the
|
|
47309
|
+
* host's server serves): over every slice of a stack when it is z-indexed, else on the slice
|
|
47310
|
+
* shown, as {@link applyImageRois} lays out an image's own. Resolves false, changing nothing
|
|
47311
|
+
* (and disposing the source), when the import went stale while it opened: another import, or
|
|
47312
|
+
* another image (see {@link regionImportTicket}; one is taken here when `ticket` is not given).
|
|
47313
|
+
*/
|
|
47314
|
+
async importRegionSourceOpener(open, ticket) {
|
|
47315
|
+
const mine = ticket ?? this.regionImportTicket();
|
|
47316
|
+
if (!this.importTicketValid(mine))
|
|
47317
|
+
return false;
|
|
47318
|
+
let first = await open();
|
|
47319
|
+
if (!this.importTicketValid(mine)) {
|
|
47320
|
+
first.dispose();
|
|
47321
|
+
return false;
|
|
47322
|
+
}
|
|
47323
|
+
const reuse = () => {
|
|
47324
|
+
const source = first;
|
|
47325
|
+
first = null;
|
|
47326
|
+
return source ? Promise.resolve(source) : open();
|
|
47327
|
+
};
|
|
47328
|
+
const info = this.plottedInfo;
|
|
47329
|
+
const planes = first.info.planes;
|
|
47330
|
+
const zIndexed = !!info?.isStack && planes.some((p) => p !== 0);
|
|
47331
|
+
this.regionStore.detachRegionSources();
|
|
47332
|
+
if (zIndexed) {
|
|
47333
|
+
this.enterImportedStack(info);
|
|
47334
|
+
this.regionStore.attachRegionSource(reuse, 'planes', [...planes]);
|
|
47335
|
+
return true;
|
|
47336
|
+
}
|
|
47337
|
+
this.regionStore.setRegions([]);
|
|
47338
|
+
this.regionStore.resetUndoHistory();
|
|
47339
|
+
this.regionStore.attachRegionSource(reuse, { slice: this.regionStore.getDisplaySlice() }, true);
|
|
47340
|
+
return true;
|
|
47341
|
+
}
|
|
47342
|
+
/** Empty every slice of a stack (its regions and undo history) for regions imported over it. */
|
|
47343
|
+
enterImportedStack(info) {
|
|
47344
|
+
const layout = this.regionStore.isStackMode()
|
|
47345
|
+
? this.regionStore.getStackSaveLayout()
|
|
47346
|
+
: info.tiled === false
|
|
47347
|
+
? 'per-slice-file'
|
|
47348
|
+
: 'combined';
|
|
47349
|
+
this.regionStore.enterStackMode(new Map(), this.regionStore.getDisplaySlice(), layout);
|
|
47350
|
+
}
|
|
46491
47351
|
getRegionSourceView$() {
|
|
46492
47352
|
return this.regionStore.getRegionSourceView$();
|
|
46493
47353
|
}
|
|
@@ -46938,6 +47798,7 @@ class RoutingVisualizerService {
|
|
|
46938
47798
|
* component for a `provideVisualization()` chain. The dataset subscription would
|
|
46939
47799
|
* otherwise keep the whole isolated chain reachable from the root data port. */
|
|
46940
47800
|
ngOnDestroy() {
|
|
47801
|
+
this.importTicket = { id: this.importTicket.id + 1, image: undefined }; // a late import changes nothing
|
|
46941
47802
|
this.spatialFacade?.dispose();
|
|
46942
47803
|
this.spatialFacade = null;
|
|
46943
47804
|
this.regionUpdates.unsubscribe();
|
|
@@ -47165,5 +48026,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
|
|
|
47165
48026
|
* Generated bundle index. Do not edit.
|
|
47166
48027
|
*/
|
|
47167
48028
|
|
|
47168
|
-
export { TOOLBAR_TOOLS as $, ALL_GENES as A, REGION_IO_PORT as B, CANVAS_TOOL_IDS as C, DEFAULT_CATEGORICAL_PALETTE as D, REGION_SOURCE_MIN_CHARS as E, Rectangle as F, Region as G, HexColorPickerComponent as H, IMAGE_STATE_PORT as I, RegionEditorComponent as J, RoutingVisualizerService as K, LUT_COLORS as L, MISSING_COLOR as M, NAPARI_DECIMATE_OPTIONS as N, SEMANTIC_SEGMENTER as O, PLOT_MODE_CONTEXT as P, SPATIAL_DATA_PORT as Q, REGION_EDITOR_API as R, SAM_MODELS as S, SPATIAL_WIRE_VERSION as T, ShapeSelection as U, SpatialChartsComponent as V, SpatialControlsComponent as W, SpatialDataHttpService as X, SpatialSelectionStore as Y, SupersededError as Z, TILE_ACCESS_PORT as _, runDecoder as a,
|
|
47169
|
-
//# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-
|
|
48029
|
+
export { TOOLBAR_TOOLS as $, ALL_GENES as A, REGION_IO_PORT as B, CANVAS_TOOL_IDS as C, DEFAULT_CATEGORICAL_PALETTE as D, REGION_SOURCE_MIN_CHARS as E, Rectangle as F, Region as G, HexColorPickerComponent as H, IMAGE_STATE_PORT as I, RegionEditorComponent as J, RoutingVisualizerService as K, LUT_COLORS as L, MISSING_COLOR as M, NAPARI_DECIMATE_OPTIONS as N, SEMANTIC_SEGMENTER as O, PLOT_MODE_CONTEXT as P, SPATIAL_DATA_PORT as Q, REGION_EDITOR_API as R, SAM_MODELS as S, SPATIAL_WIRE_VERSION as T, ShapeSelection as U, SpatialChartsComponent as V, SpatialControlsComponent as W, SpatialDataHttpService as X, SpatialSelectionStore as Y, SupersededError as Z, TILE_ACCESS_PORT as _, runDecoder as a, provideVisualization as a$, VISUALIZER as a0, VIZ_ALERT_TOAST_KEY as a1, VIZ_CONFIG as a2, VIZ_TOAST_KEY as a3, ViewerFeature as a4, VisualizationModule as a5, VisualizerComponent as a6, assertManifestVersion as a7, benefitsFromGrouping as a8, buildOmicsTraces as a9, isContinuousColumn as aA, isDialogToolContribution as aB, isEmptyPresetSet as aC, isLabelVolumeName as aD, isLittleEndian as aE, isNapari3d as aF, isNapariIsosurface as aG, isNapariScatter as aH, isNapariScatter3d as aI, isNapariSurface as aJ, isNapariVolume as aK, isSamModelReady as aL, isSpatialOmics as aM, isSpatialOmics3d as aN, isThreeDimensional as aO, labelClasses as aP, labelSliceGeoJson as aQ, lutFor as aR, markerDiameters as aS, maskToIndices as aT, mutedFromSelection as aU, omicsLayout as aV, openGeoJsonRegionSource as aW, openHttpRegionSource as aX, parsePresetSet as aY, planRegionImport as aZ, pointInRing as a_, capabilitiesOf as aa, clearSamModelCache as ab, contrastWindow as ac, contributedPlotTypeOption as ad, countMask as ae, cropImage as af, datasetFromManifest as ag, decodeColumn as ah, decodeCoords as ai, decodeFeatureVector as aj, decodePolygons as ak, decodeRadius as al, defaultPresetSet as am, dialogToolContributions as an, emptySelection as ao, encodeCategorical as ap, encodeContinuous as aq, findColumnMeta as ar, getDefaultSamModelId as as, getPlotTypeDescriptor as at, getSamModel as au, hydrateBounds as av, importFile as aw, isBuiltinPlotType as ax, isCanvasToolId as ay, isCategoricalColumn as az, ALL_TOOLBAR_TOOLS as b, readLabelVolume as b0, rendererOwnsWheel as b1, resolveCategoryColors as b2, selectByCategory as b3, selectInRegions as b4, setDefaultSamModel as b5, setOrtWasmBase as b6, setSamModelUrls as b7, sortToolContributions as b8, visibleToolContributions as b9, CELL_SEGMENTER as c, CHANNEL_HISTOGRAM_API as d, CellposeSegmenterService as e, fetchModel as f, getOrtWasmBase as g, ChannelHistogramComponent as h, DEFAULT_FALLBACK_PALETTE as i, DEFAULT_MUTED_OPACITY as j, DEFAULT_SAM_MODEL_ID as k, DEFAULT_SPATIAL_VIEW as l, INSTANCE_SEGMENTER as m, ImageConverterService as n, MultiPolygon as o, NAPARI_DEFAULT_DECIMATE as p, NO_CATEGORY as q, runEncoder as r, NO_OBSERVATION as s, PLOT_MODE_SESSION as t, PLOT_TYPE_CONTRIBUTIONS as u, PLOT_TYPE_DESCRIPTORS as v, PREFERENCES_PORT as w, PlotType as x, Polygon as y, ProcessingImage as z };
|
|
48030
|
+
//# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-BELcx0jv.mjs.map
|