@jax-data-science/sci-image-visualizer 0.3.3 → 0.5.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/README.md +268 -4
- package/esm2022/index.mjs +21 -1
- package/esm2022/lib/contracts/display-types.mjs +20 -2
- package/esm2022/lib/contracts/image.contract.mjs +1 -1
- package/esm2022/lib/contracts/plot-type-contribution.contract.mjs +66 -0
- package/esm2022/lib/contracts/plot-type.mjs +41 -1
- package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -0
- package/esm2022/lib/contracts/spatial-dataset.contract.mjs +45 -0
- package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-axes-labels.mjs +10 -19
- package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +71 -7
- package/esm2022/lib/implementations/napari-js/napari-scale-bar.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +96 -0
- package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +1934 -55
- package/esm2022/lib/implementations/napari-js/napari-zoom.mjs +18 -0
- package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +73 -8
- package/esm2022/lib/implementations/osd/osd-coordinate-transform.mjs +8 -1
- package/esm2022/lib/implementations/osd/osd-region-overlay.mjs +3 -4
- package/esm2022/lib/implementations/osd/osd-zoom.mjs +20 -0
- package/esm2022/lib/implementations/plotly/omics-trace-builders.mjs +555 -0
- package/esm2022/lib/implementations/plotly/plotly.service.mjs +13 -7
- package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +304 -0
- package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +179 -0
- package/esm2022/lib/plot-mode/plot-mode-controller.mjs +329 -0
- package/esm2022/lib/routing-visualizer.service.mjs +228 -12
- package/esm2022/lib/spatial/embedding-compute.mjs +142 -0
- package/esm2022/lib/spatial/gene-search.mjs +77 -0
- package/esm2022/lib/spatial/spatial-density.mjs +224 -0
- package/esm2022/lib/spatial/spatial-encoding.mjs +232 -0
- package/esm2022/lib/spatial/spatial-expression.mjs +257 -0
- package/esm2022/lib/spatial/spatial-framing.mjs +60 -0
- package/esm2022/lib/spatial/spatial-heatmap.mjs +185 -0
- package/esm2022/lib/spatial/spatial-hover.mjs +79 -0
- package/esm2022/lib/spatial/spatial-sections.mjs +96 -0
- package/esm2022/lib/spatial/spatial-selection.mjs +264 -0
- package/esm2022/lib/spatial/spatial-volume-image.mjs +163 -0
- package/esm2022/lib/spatial/tsne-worker.mjs +10 -0
- package/esm2022/lib/spatial/world-grid.mjs +77 -0
- package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +1278 -0
- package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +621 -0
- package/esm2022/lib/store/spatial-selection.service.mjs +47 -0
- package/esm2022/lib/store/visualizer-store.service.mjs +23 -1
- package/esm2022/lib/toolbar/toolbar.component.mjs +61 -21
- package/esm2022/lib/visualization.module.mjs +23 -5
- package/esm2022/lib/visualizer.component.mjs +527 -44
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs} +6 -4
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs.map} +1 -1
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15971 -7808
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/fesm2022/tsne.worker.js +19091 -0
- package/index.d.ts +16 -0
- package/lib/contracts/display-types.d.ts +131 -0
- package/lib/contracts/image.contract.d.ts +7 -0
- package/lib/contracts/plot-type-contribution.contract.d.ts +172 -0
- package/lib/contracts/plot-type.d.ts +52 -2
- package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
- package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
- package/lib/contracts/visualizer.contract.d.ts +104 -1
- package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
- package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
- package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
- package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
- package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
- package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
- package/lib/implementations/osd/openseadragon-visualizer.service.d.ts +21 -0
- package/lib/implementations/osd/osd-coordinate-transform.d.ts +6 -0
- package/lib/implementations/osd/osd-zoom.d.ts +19 -0
- package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
- package/lib/implementations/plotly/plotly.service.d.ts +9 -0
- package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
- package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
- package/lib/plot-mode/plot-mode-controller.d.ts +130 -0
- package/lib/routing-visualizer.service.d.ts +49 -6
- package/lib/spatial/embedding-compute.d.ts +73 -0
- package/lib/spatial/gene-search.d.ts +48 -0
- package/lib/spatial/spatial-density.d.ts +73 -0
- package/lib/spatial/spatial-encoding.d.ts +155 -0
- package/lib/spatial/spatial-expression.d.ts +144 -0
- package/lib/spatial/spatial-framing.d.ts +37 -0
- package/lib/spatial/spatial-heatmap.d.ts +111 -0
- package/lib/spatial/spatial-hover.d.ts +49 -0
- package/lib/spatial/spatial-sections.d.ts +59 -0
- package/lib/spatial/spatial-selection.d.ts +113 -0
- package/lib/spatial/spatial-volume-image.d.ts +67 -0
- package/lib/spatial/tsne-worker.d.ts +7 -0
- package/lib/spatial/world-grid.d.ts +58 -0
- package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
- package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
- package/lib/store/spatial-selection.service.d.ts +27 -0
- package/lib/store/visualizer-store.service.d.ts +13 -0
- package/lib/toolbar/toolbar.component.d.ts +36 -8
- package/lib/visualization.module.d.ts +28 -24
- package/lib/visualizer.component.d.ts +139 -7
- package/package.json +6 -2
- package/src/lib/assets/spatial-omics-3d.svg +31 -0
- package/src/lib/assets/spatial-omics.svg +27 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { EventEmitter, OnDestroy, OnInit } from '@angular/core';
|
|
2
|
+
import { IVisualizer, ISpatialControls } from '../contracts/visualizer.contract';
|
|
3
|
+
import { SpatialDataset } from '../contracts/spatial-dataset.contract';
|
|
4
|
+
import { ColormapNode, SpatialViewState } from '../contracts/display-types';
|
|
5
|
+
import { SpatialSelectionMask } from '../spatial/spatial-selection';
|
|
6
|
+
import * as i0 from "@angular/core";
|
|
7
|
+
/** One legend row for a categorical colouring. */
|
|
8
|
+
export interface SpatialLegendEntry {
|
|
9
|
+
label: string;
|
|
10
|
+
color: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Spatial-omics controls: a non-modal, resizable, draggable dialog for choosing
|
|
14
|
+
* what the observation markers are coloured by, and how they are drawn.
|
|
15
|
+
*
|
|
16
|
+
* Depends only on {@link ISpatialControls}, reached through the `VISUALIZER`
|
|
17
|
+
* contract token — never a concrete backend — mirroring how
|
|
18
|
+
* `ChannelHistogramComponent` depends only on `CHANNEL_HISTOGRAM_API`. When no
|
|
19
|
+
* host has bound `SPATIAL_DATA_PORT`, `getSpatialControls()` returns null and
|
|
20
|
+
* the dialog renders an explanatory empty state rather than dead controls.
|
|
21
|
+
*
|
|
22
|
+
* The legend swatches and the continuous colour bar are built with the SAME
|
|
23
|
+
* functions the renderer uses (`categoryColors` → `resolveCategoryColors`,
|
|
24
|
+
* `spatialContinuousLut`), so the key cannot drift from what is on screen.
|
|
25
|
+
*/
|
|
26
|
+
export declare class SpatialControlsComponent implements OnInit, OnDestroy {
|
|
27
|
+
private readonly viz;
|
|
28
|
+
/** The embedded chart, so a dialog resize can re-fit it. */
|
|
29
|
+
private charts?;
|
|
30
|
+
/**
|
|
31
|
+
* Re-fit the chart after the dialog is resized.
|
|
32
|
+
*
|
|
33
|
+
* Plotly does not follow a container resize on its own — its `responsive`
|
|
34
|
+
* option listens for WINDOW resizes only, so dragging the dialog's handle
|
|
35
|
+
* reaches it through nothing. PrimeNG tells us when the drag ends, which is a
|
|
36
|
+
* far more direct signal than watching boxes, and firing once on release means
|
|
37
|
+
* no relayout churn during the drag.
|
|
38
|
+
*/
|
|
39
|
+
onResizeEnd(): void;
|
|
40
|
+
/** Dialog visibility, two-way bound so a toolbar button can open it. */
|
|
41
|
+
visible: boolean;
|
|
42
|
+
/** Set while the 3D cloud is the active mode. Changes what the ROI selection
|
|
43
|
+
* MEANS — a screen-space lasso cutting through the cloud's full depth rather
|
|
44
|
+
* than a shape in tissue coordinates — so the hint below it says so. */
|
|
45
|
+
is3d: boolean;
|
|
46
|
+
visibleChange: EventEmitter<boolean>;
|
|
47
|
+
readonly clipOptions: {
|
|
48
|
+
label: string;
|
|
49
|
+
value: [number, number];
|
|
50
|
+
}[];
|
|
51
|
+
/** Null when the host bound no `SPATIAL_DATA_PORT`. */
|
|
52
|
+
controls: ISpatialControls | null;
|
|
53
|
+
dataset: SpatialDataset | null;
|
|
54
|
+
view: SpatialViewState;
|
|
55
|
+
/** Colour-by column choices — "None" plus every column the dataset declares. */
|
|
56
|
+
columnOptions: {
|
|
57
|
+
label: string;
|
|
58
|
+
value: string | null;
|
|
59
|
+
}[];
|
|
60
|
+
selectedColumn: string | null;
|
|
61
|
+
/** Virtual-scrolled only past this many options: the scroller earns its
|
|
62
|
+
* complexity for a few thousand names, and for eight it adds only overhead —
|
|
63
|
+
* a virtual viewport that short swallows the clicks it is meant to forward. */
|
|
64
|
+
readonly geneVirtualScrollFrom = 200;
|
|
65
|
+
/**
|
|
66
|
+
* Gene picker: a filterable dropdown rather than a free-text typeahead, so the
|
|
67
|
+
* options are visible before anything is typed and each keystroke narrows a list
|
|
68
|
+
* the user can see.
|
|
69
|
+
*
|
|
70
|
+
* `geneOptions` is the full name list when the dataset inlines it (a targeted
|
|
71
|
+
* panel: 8 for the ABC demo, 300–5,000 for Xenium/CosMx). A whole-transcriptome
|
|
72
|
+
* dataset does not ship its ~31k names, so there the list is what the port's last
|
|
73
|
+
* search returned and filtering is server-side — same control either way.
|
|
74
|
+
*/
|
|
75
|
+
geneOptions: {
|
|
76
|
+
label: string;
|
|
77
|
+
value: string;
|
|
78
|
+
}[];
|
|
79
|
+
/**
|
|
80
|
+
* Every gene the dataset inlined, whether or not it is currently an option.
|
|
81
|
+
*
|
|
82
|
+
* Held as plain strings and searched per keystroke. A whole-transcriptome dataset
|
|
83
|
+
* inlines ~18k names — cheap to keep, ruinous to hand a dropdown all at once — so
|
|
84
|
+
* {@link geneOptions} is only ever the best few hundred of these.
|
|
85
|
+
*/
|
|
86
|
+
private geneNames;
|
|
87
|
+
selectedGene: string | null;
|
|
88
|
+
/** True when the options come from the port per keystroke rather than a resident
|
|
89
|
+
* list, which changes what an empty list means (nothing matched *yet*). */
|
|
90
|
+
genesAreRemote: boolean;
|
|
91
|
+
geneSearchFailed: boolean;
|
|
92
|
+
/** Categorical key, or null when the active colouring is continuous. */
|
|
93
|
+
legend: SpatialLegendEntry[] | null;
|
|
94
|
+
/** CSS gradient for a continuous colouring, or null when categorical. */
|
|
95
|
+
colorBarCss: string | null;
|
|
96
|
+
/** Current selection — drives the count, the Clear button and the muting. */
|
|
97
|
+
selection: SpatialSelectionMask;
|
|
98
|
+
/** The legend row whose category is currently selected, for highlighting. */
|
|
99
|
+
selectedCategory: number | null;
|
|
100
|
+
/** An ROI selection that matched nothing, so the UI can say so rather than
|
|
101
|
+
* looking like the button did nothing. */
|
|
102
|
+
selectionMissed: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Whether the Distribution section is expanded. Collapsed by default: the
|
|
105
|
+
* panel's primary job is the colour controls, and the chart roughly doubles
|
|
106
|
+
* its height.
|
|
107
|
+
*/
|
|
108
|
+
chartsOpen: boolean;
|
|
109
|
+
/** Per-instance id for the collapsible body, so `aria-controls` points at one
|
|
110
|
+
* element and expanding the second panel cannot scroll the first one's chart. */
|
|
111
|
+
readonly chartsBodyId: string;
|
|
112
|
+
/**
|
|
113
|
+
* The dataset's imaged section positions, or null when its z is continuous
|
|
114
|
+
* rather than sectioned. Read once per dataset — the scan walks the z of every
|
|
115
|
+
* observation, so it must not sit in a template getter.
|
|
116
|
+
*/
|
|
117
|
+
sections: Float32Array | null;
|
|
118
|
+
/**
|
|
119
|
+
* Colormap tree for the continuous colour scale, and the node currently picked
|
|
120
|
+
* from it. The library's own `COLORMAP_OPTIONS`, shown with the same swatches
|
|
121
|
+
* the image's colormap picker uses — choosing a gradient is a visual decision,
|
|
122
|
+
* so a list of names would be the wrong control.
|
|
123
|
+
*/
|
|
124
|
+
colormapOptions: ColormapNode[];
|
|
125
|
+
selectedColormapNode: ColormapNode | null;
|
|
126
|
+
private colormap;
|
|
127
|
+
private reverse;
|
|
128
|
+
/** Guards the gene typeahead and the legend/colour-bar rebuild: both are async
|
|
129
|
+
* and both are driven by input the user changes faster than they resolve. */
|
|
130
|
+
private geneSearchToken;
|
|
131
|
+
private keyToken;
|
|
132
|
+
private readonly subs;
|
|
133
|
+
constructor(viz: IVisualizer);
|
|
134
|
+
ngOnInit(): void;
|
|
135
|
+
ngOnDestroy(): void;
|
|
136
|
+
onVisibleChange(value: boolean): void;
|
|
137
|
+
/** Column dropdown. Choosing a column supersedes any gene selection. */
|
|
138
|
+
onColumn(name: string | null): void;
|
|
139
|
+
/**
|
|
140
|
+
* A keystroke in the gene dropdown's filter box.
|
|
141
|
+
*
|
|
142
|
+
* With the names resident the dropdown filters them itself and this only clears a
|
|
143
|
+
* stale failure. Without them there is nothing to filter, so the query goes to the
|
|
144
|
+
* port and its answer BECOMES the option list — the same control, filtering one
|
|
145
|
+
* hop further away.
|
|
146
|
+
*/
|
|
147
|
+
onGeneFilter(query: string): Promise<void>;
|
|
148
|
+
/** A gene was picked; it supersedes any column selection. */
|
|
149
|
+
onGene(name: string | null): void;
|
|
150
|
+
/** Select every observation inside the drawn regions (their union). */
|
|
151
|
+
selectFromRegions(): void;
|
|
152
|
+
/** Legend click: select one category. Clicking the active row clears it, so a
|
|
153
|
+
* second click is an undo rather than a no-op. */
|
|
154
|
+
selectCategory(index: number): Promise<void>;
|
|
155
|
+
/**
|
|
156
|
+
* Expand or collapse the distribution section.
|
|
157
|
+
*
|
|
158
|
+
* Expanding SCROLLS it into view, because the panel is taller than the viewport
|
|
159
|
+
* once this section is open and the chart is its last row: expanding it drew a
|
|
160
|
+
* chart ~300 px below the fold, which reads as the section being empty. The
|
|
161
|
+
* scroll is deferred a task so the chart's own deferred first draw has given the
|
|
162
|
+
* body its height.
|
|
163
|
+
*/
|
|
164
|
+
toggleCharts(): void;
|
|
165
|
+
clearSelection(): void;
|
|
166
|
+
/** True while a selection is active — everything else renders muted. */
|
|
167
|
+
get hasSelection(): boolean;
|
|
168
|
+
onPointScale(value: number | undefined): void;
|
|
169
|
+
onOpacity(value: number | undefined): void;
|
|
170
|
+
onLogScale(on: boolean): void;
|
|
171
|
+
/**
|
|
172
|
+
* Set when the active categorical colouring has more categories than the 3D
|
|
173
|
+
* cloud can keep apart, so the points are drawing FLAT.
|
|
174
|
+
*
|
|
175
|
+
* Said in the panel rather than left to a console warning: `subclass` (338) is
|
|
176
|
+
* served precisely because the density volumes and the 2D view can render it, and
|
|
177
|
+
* a user who picks it in the cloud and sees one colour deserves to know both why
|
|
178
|
+
* and what to do instead.
|
|
179
|
+
*/
|
|
180
|
+
get exceedsCloudPalette(): boolean;
|
|
181
|
+
/** The ceiling itself, for the message. */
|
|
182
|
+
readonly cloudPaletteLimit = 95;
|
|
183
|
+
onGeneMap(on: boolean): void;
|
|
184
|
+
/** 3D: smooth the per-section sheets along z into a continuous volume. */
|
|
185
|
+
onGeneMapVolume(on: boolean): void;
|
|
186
|
+
/** 3D: restrict the sheets to one imaged section. */
|
|
187
|
+
onGeneMapOneSection(on: boolean): void;
|
|
188
|
+
onGeneMapSection(value: number | undefined): void;
|
|
189
|
+
get geneMapOneSection(): boolean;
|
|
190
|
+
/** "12 of 53" for the gene map's own section, 1-based like the cloud's. */
|
|
191
|
+
get geneMapSectionLabel(): string;
|
|
192
|
+
onGeneMapSmoothing(value: number | undefined): void;
|
|
193
|
+
onGeneMapOpacity(value: number | undefined): void;
|
|
194
|
+
/**
|
|
195
|
+
* What the 3D gene map is currently showing, said plainly — the sheets are a
|
|
196
|
+
* measurement and the volume is an estimate, and the panel has to be the place
|
|
197
|
+
* that says which one is on screen.
|
|
198
|
+
*/
|
|
199
|
+
get geneMapVolumeNote(): string;
|
|
200
|
+
/** True while a gene is the colour source — the only thing a gene map can map. */
|
|
201
|
+
get canMapGene(): boolean;
|
|
202
|
+
onShowVolume(on: boolean): void;
|
|
203
|
+
onVolumeOpacity(value: number | undefined): void;
|
|
204
|
+
onShowPoints(on: boolean): void;
|
|
205
|
+
/** The "one section at a time" switch: null restores the whole stack. */
|
|
206
|
+
onOneSection(on: boolean): void;
|
|
207
|
+
onPointSection(value: number | undefined): void;
|
|
208
|
+
/** True while the cloud is restricted to a single section. */
|
|
209
|
+
get oneSection(): boolean;
|
|
210
|
+
/** Highest section index the slider can reach. */
|
|
211
|
+
get lastSection(): number;
|
|
212
|
+
/** "12 of 53" — 1-based, because the sections are slides, not array slots. */
|
|
213
|
+
get sectionLabel(): string;
|
|
214
|
+
/** Whether this dataset has sections to pick from at all. */
|
|
215
|
+
get isSectioned(): boolean;
|
|
216
|
+
onDensityVolume(on: boolean): void;
|
|
217
|
+
onDensitySmoothing(value: number | undefined): void;
|
|
218
|
+
/** What the density volumes are actually showing, said plainly — an estimate is
|
|
219
|
+
* only honest if the reader knows it is one, and which clusters are in view. */
|
|
220
|
+
get densityNote(): string;
|
|
221
|
+
/** Mirrors the renderer's cap, for the note only. */
|
|
222
|
+
private static readonly DENSITY_MAX_CLUSTERS;
|
|
223
|
+
onClip(value: [number, number]): void;
|
|
224
|
+
reset(): void;
|
|
225
|
+
/**
|
|
226
|
+
* Description of the active column, when it has one. Surfaced because a
|
|
227
|
+
* DERIVED column (k-means clusters, QC totals computed at conversion) must not
|
|
228
|
+
* read as though it came with the data.
|
|
229
|
+
*/
|
|
230
|
+
get activeDescription(): string | null;
|
|
231
|
+
/** Label for the current colouring, for the key's heading. */
|
|
232
|
+
get colorByLabel(): string;
|
|
233
|
+
/** True when the active colouring is a categorical column (drives the key). */
|
|
234
|
+
get isCategorical(): boolean;
|
|
235
|
+
/** Whether log/clip apply — they are continuous-only knobs. */
|
|
236
|
+
get isContinuous(): boolean;
|
|
237
|
+
private columnLabel;
|
|
238
|
+
/** Rebuild the legend or colour bar for the current colouring. */
|
|
239
|
+
private refreshKey;
|
|
240
|
+
/**
|
|
241
|
+
* The continuous colour scale's colormap. Clearing it goes back to following
|
|
242
|
+
* the image's, which is the default.
|
|
243
|
+
*/
|
|
244
|
+
onContinuousColormap(node: ColormapNode | null): void;
|
|
245
|
+
/** The tree node holding a colormap value, so the picker shows what is in use. */
|
|
246
|
+
private colormapNodeFor;
|
|
247
|
+
/**
|
|
248
|
+
* `linear-gradient(...)` sampling the active colormap at 16 stops.
|
|
249
|
+
*
|
|
250
|
+
* Resolved exactly the way the renderer resolves it — the same override, the
|
|
251
|
+
* same grey fallback. Built from `lutFor` alone, this bar showed a black-to-white
|
|
252
|
+
* ramp while the canvas drew Viridis, which makes the key worse than no key.
|
|
253
|
+
*/
|
|
254
|
+
private buildColorBar;
|
|
255
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SpatialControlsComponent, never>;
|
|
256
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<SpatialControlsComponent, "spatial-controls", never, { "visible": { "alias": "visible"; "required": false; }; "is3d": { "alias": "is3d"; "required": false; }; }, { "visibleChange": "visibleChange"; }, never, never, false, never>;
|
|
257
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { SpatialSelectionMask } from '../spatial/spatial-selection';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
/**
|
|
5
|
+
* The set of currently-selected spatial observations, shared the way
|
|
6
|
+
* `RegionStore` shares regions: one store, many writers (an ROI selection, a
|
|
7
|
+
* legend click, a future chart brush), one reader per consumer.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately thin — every rule about WHICH observations are selected lives in
|
|
10
|
+
* the pure `spatial-selection.ts`; this only holds the answer and announces it.
|
|
11
|
+
*/
|
|
12
|
+
export declare class SpatialSelectionStore {
|
|
13
|
+
private readonly selection$;
|
|
14
|
+
/** Emits on every change, including a clear. */
|
|
15
|
+
getSelection$(): Observable<SpatialSelectionMask>;
|
|
16
|
+
/** Synchronous read, for a renderer building its first frame. */
|
|
17
|
+
current(): SpatialSelectionMask;
|
|
18
|
+
/** True when nothing is selected — the state in which nothing is muted. */
|
|
19
|
+
isEmpty(): boolean;
|
|
20
|
+
/** Replace the selection. An empty result is published like any other, so a
|
|
21
|
+
* "selected nothing" ROI visibly clears rather than silently keeping the
|
|
22
|
+
* previous selection on screen. */
|
|
23
|
+
set(selection: SpatialSelectionMask): void;
|
|
24
|
+
clear(): void;
|
|
25
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SpatialSelectionStore, never>;
|
|
26
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<SpatialSelectionStore>;
|
|
27
|
+
}
|
|
@@ -4,6 +4,7 @@ import { ClassPreset, PresetSet } from '../models/class-preset';
|
|
|
4
4
|
import { PreferencesPort } from '../contracts/ports/preferences.port';
|
|
5
5
|
import { IImageMetadata } from '../contracts/image.contract';
|
|
6
6
|
import { IChannelState } from '../contracts/channel-histogram-api.contract';
|
|
7
|
+
import { SpatialViewState } from '../contracts/display-types';
|
|
7
8
|
import * as i0 from "@angular/core";
|
|
8
9
|
/**
|
|
9
10
|
* Backend-neutral store for the **visualization session** — the state that
|
|
@@ -43,6 +44,10 @@ export declare class VisualizerStore {
|
|
|
43
44
|
private readonly savePresets$;
|
|
44
45
|
private presetsLoaded;
|
|
45
46
|
private readonly activeTool$;
|
|
47
|
+
/** Spatial-omics display state (colour source, point size, alpha, scaling).
|
|
48
|
+
* Held here, next to the colormap/channel state, so the active backend reacts
|
|
49
|
+
* to an edit exactly as it does for an image. */
|
|
50
|
+
private readonly spatialView$;
|
|
46
51
|
constructor(http?: HttpClient | null, prefsPort?: PreferencesPort | null);
|
|
47
52
|
/**
|
|
48
53
|
* Resolve the colormap tree's LUT keys (e.g. "GREYS_LUT") to their colour
|
|
@@ -122,6 +127,14 @@ export declare class VisualizerStore {
|
|
|
122
127
|
resetPresets(): void;
|
|
123
128
|
/** Force a (debounced) write-back of the current preset set. */
|
|
124
129
|
persistPresets(): void;
|
|
130
|
+
getSpatialView$(): Observable<SpatialViewState>;
|
|
131
|
+
/** Synchronous read, for a renderer building its first frame. */
|
|
132
|
+
currentSpatialView(): SpatialViewState;
|
|
133
|
+
/** Patch the spatial view state; emits once with the merged value. */
|
|
134
|
+
setSpatialView(partial: Partial<SpatialViewState>): void;
|
|
135
|
+
/** Reset to defaults — e.g. when a different dataset is selected, where the
|
|
136
|
+
* previous colour column almost certainly does not exist. */
|
|
137
|
+
resetSpatialView(): void;
|
|
125
138
|
getActiveTool$(): Observable<string | null>;
|
|
126
139
|
getActiveTool(): string | null;
|
|
127
140
|
setActiveTool(tool: string | null): void;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
|
2
2
|
import { MenuItem } from 'primeng/api';
|
|
3
3
|
import { IImageInfo } from '../contracts/image.contract';
|
|
4
|
-
import { PlotType,
|
|
4
|
+
import { PlotType, PlotTypeId } from '../contracts/plot-type';
|
|
5
5
|
import { ToolbarToolVisibility } from '../contracts/toolbar-config';
|
|
6
6
|
import { ToolbarToolContribution } from '../contracts/toolbar-tool.contract';
|
|
7
|
+
import { PlotTypeOption } from '../contracts/plot-type-contribution.contract';
|
|
7
8
|
import * as i0 from "@angular/core";
|
|
8
9
|
/**
|
|
9
10
|
* Presentational toolbar for the plotting viewport.
|
|
@@ -20,9 +21,18 @@ import * as i0 from "@angular/core";
|
|
|
20
21
|
export declare class ToolbarComponent implements OnChanges {
|
|
21
22
|
/** Current image (gates which control groups are shown). */
|
|
22
23
|
imageInfo: IImageInfo | undefined;
|
|
23
|
-
/** Plot types the active backend advertises. */
|
|
24
|
-
plotTypeOptions:
|
|
25
|
-
|
|
24
|
+
/** Plot types the active backend advertises, then any contributed modes. */
|
|
25
|
+
plotTypeOptions: PlotTypeOption[];
|
|
26
|
+
/** The selector's value: a built-in type or a contributed mode's id. */
|
|
27
|
+
selectedPlotType: PlotTypeId;
|
|
28
|
+
/**
|
|
29
|
+
* The built-in type that renders {@link selectedPlotType} — its `baseType`
|
|
30
|
+
* for a contributed mode. Every tool/control decision below reads
|
|
31
|
+
* {@link effectivePlotType}, so a contributed mode gets exactly its base
|
|
32
|
+
* type's toolbar. When unbound, a built-in selection is its own base and
|
|
33
|
+
* anything else is treated as the default Image view.
|
|
34
|
+
*/
|
|
35
|
+
basePlotType: PlotType | null;
|
|
26
36
|
/** Isosurface band as a 0–255 slider position (mapped onto the volume's real
|
|
27
37
|
* intensity range by the renderer). Defaults to the full range. */
|
|
28
38
|
isoRange: number[];
|
|
@@ -30,6 +40,13 @@ export declare class ToolbarComponent implements OnChanges {
|
|
|
30
40
|
zIndex: number;
|
|
31
41
|
/** True for any 2D (non Surface-3D) plot type. */
|
|
32
42
|
isHeatmap: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Region tools apply even though this is a 3D view, because they draw a
|
|
45
|
+
* SCREEN-SPACE lasso (the 3D spatial cloud). Separate from {@link isHeatmap},
|
|
46
|
+
* which means "2D view" and also drives the zoom tools and the 3D camera
|
|
47
|
+
* controls — flipping that would show 2D pan/zoom and hide the orbit controls.
|
|
48
|
+
*/
|
|
49
|
+
is3dRegions: boolean;
|
|
33
50
|
activeDragMode: string | null;
|
|
34
51
|
activeSurface3dMode: string;
|
|
35
52
|
/** Whether the napari 3D axes/scale gizmo is currently shown (drives the toggle's look). */
|
|
@@ -75,7 +92,7 @@ export declare class ToolbarComponent implements OnChanges {
|
|
|
75
92
|
contributedTools: ToolbarToolContribution[];
|
|
76
93
|
/** Active checkpoint per contributed tool, keyed by tool id. */
|
|
77
94
|
toolModelIds: Record<string, string>;
|
|
78
|
-
selectPlotType: EventEmitter<
|
|
95
|
+
selectPlotType: EventEmitter<string>;
|
|
79
96
|
/** Intensity (LINE) mode: add another colored line ROI + inset trace. */
|
|
80
97
|
addProfileLine: EventEmitter<void>;
|
|
81
98
|
/** Toggle image smoothing (bilinear) vs nearest-neighbour (crisp pixels). */
|
|
@@ -83,6 +100,8 @@ export declare class ToolbarComponent implements OnChanges {
|
|
|
83
100
|
isoRangeChange: EventEmitter<number[] | undefined>;
|
|
84
101
|
/** Open the Channels & Histogram dialog (brightness/contrast, colormap, …). */
|
|
85
102
|
openChannelHistogram: EventEmitter<void>;
|
|
103
|
+
/** Open the spatial-omics controls (colour-by, legend, point display). */
|
|
104
|
+
openSpatialControls: EventEmitter<void>;
|
|
86
105
|
openRegionEditor: EventEmitter<void>;
|
|
87
106
|
selectStackOption: EventEmitter<{
|
|
88
107
|
name: string;
|
|
@@ -187,9 +206,16 @@ export declare class ToolbarComponent implements OnChanges {
|
|
|
187
206
|
/** Keeps each contributed tool's `p-menu` overlay alive across CD ticks —
|
|
188
207
|
* re-creating it mid-interaction swallows the click on a menu item. */
|
|
189
208
|
trackToolById(_index: number, tool: ToolbarToolContribution): string;
|
|
209
|
+
/** The built-in type whose tools and controls apply to the current selection. */
|
|
210
|
+
get effectivePlotType(): PlotType;
|
|
190
211
|
/** The Image plot type renders as a natively pan/zoom-able raster, so the
|
|
191
212
|
* generic zoom/pan tools are hidden. */
|
|
192
213
|
get isImageView(): boolean;
|
|
214
|
+
/** Either spatial-omics mode is active, so its controls are worth offering.
|
|
215
|
+
* The 3D cloud needs the panel MORE than the 2D view does, not less: colouring,
|
|
216
|
+
* the legend and category show/hide are the only way to make sense of a
|
|
217
|
+
* million overlapping points. */
|
|
218
|
+
get isSpatialMode(): boolean;
|
|
193
219
|
/** A plot-type icon is a PrimeNG font glyph (e.g. `pi pi-image`) rather than an
|
|
194
220
|
* SVG asset path — drives which element the selector item template renders. */
|
|
195
221
|
isPiIcon(icon: string | undefined): boolean;
|
|
@@ -197,8 +223,10 @@ export declare class ToolbarComponent implements OnChanges {
|
|
|
197
223
|
* (jit-ui#102). Gates the polygon / add-vertex / delete-vertex / Bézier-convert tools. */
|
|
198
224
|
get supportsRegionVertexTools(): boolean;
|
|
199
225
|
/** Plot types that scrub a z-stack live (the renderer swaps the slice in place): the OSD Image
|
|
200
|
-
* view, the napari-js WebGPU image,
|
|
201
|
-
* from the picked slice (jit-ui#102)
|
|
226
|
+
* view, the napari-js WebGPU image, the napari-js surface — which rebuilds the height field
|
|
227
|
+
* from the picked slice (jit-ui#102) — and the 2D spatial-omics view, where a 3D dataset's
|
|
228
|
+
* registered volume is the image and the slider picks the section whose observations are
|
|
229
|
+
* drawn. Drives the per-slice slider. */
|
|
202
230
|
get showsLiveSliceScrubber(): boolean;
|
|
203
231
|
/** Any napari-js WebGPU plot type. The single-image/stack toggle + slice-number field are
|
|
204
232
|
* Plotly-only stack controls, so they're hidden for napari (jit-ui#102). */
|
|
@@ -214,5 +242,5 @@ export declare class ToolbarComponent implements OnChanges {
|
|
|
214
242
|
get isIntensityCapable(): boolean;
|
|
215
243
|
showHelp(): void;
|
|
216
244
|
static ɵfac: i0.ɵɵFactoryDeclaration<ToolbarComponent, never>;
|
|
217
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<ToolbarComponent, "plotting-toolbar", never, { "imageInfo": { "alias": "imageInfo"; "required": false; }; "plotTypeOptions": { "alias": "plotTypeOptions"; "required": false; }; "selectedPlotType": { "alias": "selectedPlotType"; "required": false; }; "isoRange": { "alias": "isoRange"; "required": false; }; "maxIndex": { "alias": "maxIndex"; "required": false; }; "zIndex": { "alias": "zIndex"; "required": false; }; "isHeatmap": { "alias": "isHeatmap"; "required": false; }; "activeDragMode": { "alias": "activeDragMode"; "required": false; }; "activeSurface3dMode": { "alias": "activeSurface3dMode"; "required": false; }; "axesVisible": { "alias": "axesVisible"; "required": false; }; "wireframeActive": { "alias": "wireframeActive"; "required": false; }; "resolutionScale": { "alias": "resolutionScale"; "required": false; }; "wandSensitivity": { "alias": "wandSensitivity"; "required": false; }; "brushSize": { "alias": "brushSize"; "required": false; }; "vertexEraserRadius": { "alias": "vertexEraserRadius"; "required": false; }; "canUndo": { "alias": "canUndo"; "required": false; }; "canRedo": { "alias": "canRedo"; "required": false; }; "stackOptions": { "alias": "stackOptions"; "required": false; }; "tools": { "alias": "tools"; "required": false; }; "imageSmoothingEnabled": { "alias": "imageSmoothingEnabled"; "required": false; }; "samModels": { "alias": "samModels"; "required": false; }; "samModelId": { "alias": "samModelId"; "required": false; }; "contributedTools": { "alias": "contributedTools"; "required": false; }; "toolModelIds": { "alias": "toolModelIds"; "required": false; }; }, { "selectPlotType": "selectPlotType"; "addProfileLine": "addProfileLine"; "toggleImageSmoothing": "toggleImageSmoothing"; "isoRangeChange": "isoRangeChange"; "openChannelHistogram": "openChannelHistogram"; "openRegionEditor": "openRegionEditor"; "selectStackOption": "selectStackOption"; "zScrub": "zScrub"; "zSlide": "zSlide"; "zIndexInput": "zIndexInput"; "reloadAndPlot": "reloadAndPlot"; "downloadImage": "downloadImage"; "autoscaleImage": "autoscaleImage"; "toggleDragMode": "toggleDragMode"; "zoomIn": "zoomIn"; "zoomOut": "zoomOut"; "toggleSurface3dMode": "toggleSurface3dMode"; "resetSurfaceCamera": "resetSurfaceCamera"; "toggleAxes": "toggleAxes"; "toggleWireframe": "toggleWireframe"; "selectResolution": "selectResolution"; "deleteRegion": "deleteRegion"; "undoRegion": "undoRegion"; "redoRegion": "redoRegion"; "segmentRegions": "segmentRegions"; "segmentCellpose": "segmentCellpose"; "runTool": "runTool"; "toolModelChange": "toolModelChange"; "openToolParams": "openToolParams"; "samModelChange": "samModelChange"; "wandSensitivityChange": "wandSensitivityChange"; "brushSizeChange": "brushSizeChange"; "vertexEraserRadiusChange": "vertexEraserRadiusChange"; }, never, ["*"], false, never>;
|
|
245
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<ToolbarComponent, "plotting-toolbar", never, { "imageInfo": { "alias": "imageInfo"; "required": false; }; "plotTypeOptions": { "alias": "plotTypeOptions"; "required": false; }; "selectedPlotType": { "alias": "selectedPlotType"; "required": false; }; "basePlotType": { "alias": "basePlotType"; "required": false; }; "isoRange": { "alias": "isoRange"; "required": false; }; "maxIndex": { "alias": "maxIndex"; "required": false; }; "zIndex": { "alias": "zIndex"; "required": false; }; "isHeatmap": { "alias": "isHeatmap"; "required": false; }; "is3dRegions": { "alias": "is3dRegions"; "required": false; }; "activeDragMode": { "alias": "activeDragMode"; "required": false; }; "activeSurface3dMode": { "alias": "activeSurface3dMode"; "required": false; }; "axesVisible": { "alias": "axesVisible"; "required": false; }; "wireframeActive": { "alias": "wireframeActive"; "required": false; }; "resolutionScale": { "alias": "resolutionScale"; "required": false; }; "wandSensitivity": { "alias": "wandSensitivity"; "required": false; }; "brushSize": { "alias": "brushSize"; "required": false; }; "vertexEraserRadius": { "alias": "vertexEraserRadius"; "required": false; }; "canUndo": { "alias": "canUndo"; "required": false; }; "canRedo": { "alias": "canRedo"; "required": false; }; "stackOptions": { "alias": "stackOptions"; "required": false; }; "tools": { "alias": "tools"; "required": false; }; "imageSmoothingEnabled": { "alias": "imageSmoothingEnabled"; "required": false; }; "samModels": { "alias": "samModels"; "required": false; }; "samModelId": { "alias": "samModelId"; "required": false; }; "contributedTools": { "alias": "contributedTools"; "required": false; }; "toolModelIds": { "alias": "toolModelIds"; "required": false; }; }, { "selectPlotType": "selectPlotType"; "addProfileLine": "addProfileLine"; "toggleImageSmoothing": "toggleImageSmoothing"; "isoRangeChange": "isoRangeChange"; "openChannelHistogram": "openChannelHistogram"; "openSpatialControls": "openSpatialControls"; "openRegionEditor": "openRegionEditor"; "selectStackOption": "selectStackOption"; "zScrub": "zScrub"; "zSlide": "zSlide"; "zIndexInput": "zIndexInput"; "reloadAndPlot": "reloadAndPlot"; "downloadImage": "downloadImage"; "autoscaleImage": "autoscaleImage"; "toggleDragMode": "toggleDragMode"; "zoomIn": "zoomIn"; "zoomOut": "zoomOut"; "toggleSurface3dMode": "toggleSurface3dMode"; "resetSurfaceCamera": "resetSurfaceCamera"; "toggleAxes": "toggleAxes"; "toggleWireframe": "toggleWireframe"; "selectResolution": "selectResolution"; "deleteRegion": "deleteRegion"; "undoRegion": "undoRegion"; "redoRegion": "redoRegion"; "segmentRegions": "segmentRegions"; "segmentCellpose": "segmentCellpose"; "runTool": "runTool"; "toolModelChange": "toolModelChange"; "openToolParams": "openToolParams"; "samModelChange": "samModelChange"; "wandSensitivityChange": "wandSensitivityChange"; "brushSizeChange": "brushSizeChange"; "vertexEraserRadiusChange": "vertexEraserRadiusChange"; }, never, ["*"], false, never>;
|
|
218
246
|
}
|
|
@@ -4,29 +4,33 @@ import * as i2 from "./toolbar/toolbar.component";
|
|
|
4
4
|
import * as i3 from "./region-editor/region-editor.component";
|
|
5
5
|
import * as i4 from "./hex-color-picker/hex-color-picker.component";
|
|
6
6
|
import * as i5 from "./channel-histogram/channel-histogram.component";
|
|
7
|
-
import * as i6 from "
|
|
8
|
-
import * as i7 from "
|
|
9
|
-
import * as i8 from "
|
|
10
|
-
import * as i9 from "
|
|
11
|
-
import * as i10 from "primeng/
|
|
12
|
-
import * as i11 from "primeng/
|
|
13
|
-
import * as i12 from "primeng/
|
|
14
|
-
import * as i13 from "primeng/
|
|
15
|
-
import * as i14 from "primeng/
|
|
16
|
-
import * as i15 from "primeng/
|
|
17
|
-
import * as i16 from "primeng/
|
|
18
|
-
import * as i17 from "primeng/
|
|
19
|
-
import * as i18 from "primeng/
|
|
20
|
-
import * as i19 from "primeng/
|
|
21
|
-
import * as i20 from "primeng/
|
|
22
|
-
import * as i21 from "primeng/
|
|
23
|
-
import * as i22 from "primeng/
|
|
24
|
-
import * as i23 from "primeng/
|
|
25
|
-
import * as i24 from "primeng/
|
|
26
|
-
import * as i25 from "primeng/
|
|
27
|
-
import * as i26 from "primeng/
|
|
28
|
-
import * as i27 from "primeng/
|
|
29
|
-
import * as i28 from "primeng/
|
|
7
|
+
import * as i6 from "./spatial-controls/spatial-controls.component";
|
|
8
|
+
import * as i7 from "./spatial-controls/spatial-charts/spatial-charts.component";
|
|
9
|
+
import * as i8 from "@angular/common";
|
|
10
|
+
import * as i9 from "@angular/forms";
|
|
11
|
+
import * as i10 from "primeng/toolbar";
|
|
12
|
+
import * as i11 from "primeng/button";
|
|
13
|
+
import * as i12 from "primeng/menu";
|
|
14
|
+
import * as i13 from "primeng/dropdown";
|
|
15
|
+
import * as i14 from "primeng/slider";
|
|
16
|
+
import * as i15 from "primeng/treeselect";
|
|
17
|
+
import * as i16 from "primeng/inputnumber";
|
|
18
|
+
import * as i17 from "primeng/dialog";
|
|
19
|
+
import * as i18 from "primeng/contextmenu";
|
|
20
|
+
import * as i19 from "primeng/progressbar";
|
|
21
|
+
import * as i20 from "primeng/toast";
|
|
22
|
+
import * as i21 from "primeng/tooltip";
|
|
23
|
+
import * as i22 from "primeng/ripple";
|
|
24
|
+
import * as i23 from "primeng/table";
|
|
25
|
+
import * as i24 from "primeng/paginator";
|
|
26
|
+
import * as i25 from "primeng/overlaypanel";
|
|
27
|
+
import * as i26 from "primeng/confirmdialog";
|
|
28
|
+
import * as i27 from "primeng/inputtext";
|
|
29
|
+
import * as i28 from "primeng/checkbox";
|
|
30
|
+
import * as i29 from "primeng/multiselect";
|
|
31
|
+
import * as i30 from "primeng/radiobutton";
|
|
32
|
+
import * as i31 from "primeng/selectbutton";
|
|
33
|
+
import * as i32 from "primeng/autocomplete";
|
|
30
34
|
/**
|
|
31
35
|
* Self-contained plotting UI: the {@link VisualizerComponent} (plot surface
|
|
32
36
|
* + render orchestration), its {@link ToolbarComponent}, and the
|
|
@@ -40,6 +44,6 @@ import * as i28 from "primeng/selectbutton";
|
|
|
40
44
|
*/
|
|
41
45
|
export declare class VisualizationModule {
|
|
42
46
|
static ɵfac: i0.ɵɵFactoryDeclaration<VisualizationModule, never>;
|
|
43
|
-
static ɵmod: i0.ɵɵNgModuleDeclaration<VisualizationModule, [typeof i1.VisualizerComponent, typeof i2.ToolbarComponent, typeof i3.RegionEditorComponent, typeof i4.HexColorPickerComponent, typeof i5.ChannelHistogramComponent], [typeof
|
|
47
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<VisualizationModule, [typeof i1.VisualizerComponent, typeof i2.ToolbarComponent, typeof i3.RegionEditorComponent, typeof i4.HexColorPickerComponent, typeof i5.ChannelHistogramComponent, typeof i6.SpatialControlsComponent, typeof i7.SpatialChartsComponent], [typeof i8.CommonModule, typeof i9.FormsModule, typeof i10.ToolbarModule, typeof i11.ButtonModule, typeof i12.MenuModule, typeof i13.DropdownModule, typeof i14.SliderModule, typeof i15.TreeSelectModule, typeof i16.InputNumberModule, typeof i17.DialogModule, typeof i18.ContextMenuModule, typeof i19.ProgressBarModule, typeof i20.ToastModule, typeof i21.TooltipModule, typeof i22.RippleModule, typeof i23.TableModule, typeof i24.PaginatorModule, typeof i25.OverlayPanelModule, typeof i26.ConfirmDialogModule, typeof i27.InputTextModule, typeof i28.CheckboxModule, typeof i29.MultiSelectModule, typeof i30.RadioButtonModule, typeof i31.SelectButtonModule, typeof i32.AutoCompleteModule], [typeof i1.VisualizerComponent, typeof i3.RegionEditorComponent, typeof i4.HexColorPickerComponent, typeof i5.ChannelHistogramComponent, typeof i6.SpatialControlsComponent, typeof i7.SpatialChartsComponent]>;
|
|
44
48
|
static ɵinj: i0.ɵɵInjectorDeclaration<VisualizationModule>;
|
|
45
49
|
}
|