1 | import { BoundsCheckError } from 'zarr';
|
2 |
|
3 | import { guessRgb, padTileWithZeros, byteSwapInplace } from './utils';
|
4 | import { DTYPE_VALUES } from '../constants';
|
5 | import HTTPStore from './httpStore';
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | export default class ZarrLoader {
|
11 | constructor({
|
12 | data,
|
13 | dimensions,
|
14 | isRgb,
|
15 | scale = 1,
|
16 | translate = { x: 0, y: 0 }
|
17 | }) {
|
18 | let base;
|
19 | if (Array.isArray(data)) {
|
20 | [base] = data;
|
21 | this.numLevels = data.length;
|
22 | } else {
|
23 | base = data;
|
24 | this.numLevels = 1;
|
25 | }
|
26 | this.type = 'zarr';
|
27 | this.scale = scale;
|
28 | this.translate = translate;
|
29 | this.isRgb = isRgb || guessRgb(base.shape);
|
30 | this.dimensions = dimensions;
|
31 |
|
32 | this._data = data;
|
33 | this._dimIndices = new Map();
|
34 | dimensions.forEach(({ field }, i) => this._dimIndices.set(field, i));
|
35 |
|
36 | const { dtype, chunks } = base;
|
37 | |
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | this.dtype = `<${dtype.slice(1)}`;
|
47 | this.tileSize = chunks[this._dimIndices.get('x')];
|
48 | }
|
49 |
|
50 | get isPyramid() {
|
51 | return Array.isArray(this._data);
|
52 | }
|
53 |
|
54 | get base() {
|
55 | return this.isPyramid ? this._data[0] : this._data;
|
56 | }
|
57 |
|
58 | |
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | async getTile({ x, y, z, loaderSelection = [], signal }) {
|
67 | const { TypedArray } = DTYPE_VALUES[this.dtype];
|
68 | const source = this._getSource(z);
|
69 | const [xIndex, yIndex] = ['x', 'y'].map(k => this._dimIndices.get(k));
|
70 |
|
71 | const dataRequests = loaderSelection.map(async sel => {
|
72 | const chunkKey = this._serializeSelection(sel);
|
73 | chunkKey[yIndex] = y;
|
74 | chunkKey[xIndex] = x;
|
75 |
|
76 | const key = source.keyPrefix + chunkKey.join('.');
|
77 | let buffer;
|
78 | try {
|
79 | buffer = await source.store.getItem(key, { signal });
|
80 | } catch (err) {
|
81 | if (err.name !== 'AbortError') {
|
82 | throw err;
|
83 | }
|
84 | return null;
|
85 | }
|
86 | let bytes = new Uint8Array(buffer);
|
87 | if (source.compressor) {
|
88 | bytes = await source.compressor.decode(bytes);
|
89 | }
|
90 | const data = new TypedArray(bytes.buffer);
|
91 | if (source.dtype[0] === '>') {
|
92 |
|
93 | byteSwapInplace(data);
|
94 | }
|
95 | const width = source.chunks[xIndex];
|
96 | const height = source.chunks[yIndex];
|
97 | if (height < this.tileSize || width < this.tileSize) {
|
98 | return padTileWithZeros(
|
99 | { data, width, height },
|
100 | this.tileSize,
|
101 | this.tileSize
|
102 | );
|
103 | }
|
104 | return data;
|
105 | });
|
106 | const data = await Promise.all(dataRequests);
|
107 | if (source.store instanceof HTTPStore && signal?.aborted) return null;
|
108 | return {
|
109 | data,
|
110 | width: this.tileSize,
|
111 | height: this.tileSize
|
112 | };
|
113 | }
|
114 |
|
115 | |
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | async getRaster({ z, loaderSelection = [] }) {
|
122 | const source = this._getSource(z);
|
123 | const [xIndex, yIndex] = ['x', 'y'].map(k => this._dimIndices.get(k));
|
124 |
|
125 | const dataRequests = loaderSelection.map(async sel => {
|
126 | const chunkKey = this._serializeSelection(sel);
|
127 | chunkKey[yIndex] = null;
|
128 | chunkKey[xIndex] = null;
|
129 | if (this.isRgb) {
|
130 | chunkKey[chunkKey.length - 1] = null;
|
131 | }
|
132 | const { data } = await source.getRaw(chunkKey);
|
133 | return data;
|
134 | });
|
135 |
|
136 | const data = await Promise.all(dataRequests);
|
137 | const { shape } = source;
|
138 | const width = shape[xIndex];
|
139 | const height = shape[yIndex];
|
140 | return { data, width, height };
|
141 | }
|
142 |
|
143 | |
144 |
|
145 |
|
146 |
|
147 |
|
148 | onTileError(err) {
|
149 | if (!(err instanceof BoundsCheckError)) {
|
150 |
|
151 | throw err;
|
152 | }
|
153 | }
|
154 |
|
155 | |
156 |
|
157 |
|
158 |
|
159 |
|
160 | getRasterSize({ z }) {
|
161 | const { shape } = this._getSource(z);
|
162 | const [height, width] = ['y', 'x'].map(k => shape[this._dimIndices.get(k)]);
|
163 | return { height, width };
|
164 | }
|
165 |
|
166 | |
167 |
|
168 |
|
169 |
|
170 |
|
171 | getMetadata() {
|
172 | return {};
|
173 | }
|
174 |
|
175 | _getSource(z) {
|
176 | return typeof z === 'number' && this.isPyramid ? this._data[z] : this._data;
|
177 | }
|
178 |
|
179 | |
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | _serializeSelection(selection) {
|
189 |
|
190 | if (Array.isArray(selection)) return [...selection];
|
191 |
|
192 | const serialized = Array(this.dimensions.length).fill(0);
|
193 | Object.entries(selection).forEach(([dimName, value]) => {
|
194 | if (!this._dimIndices.has(dimName)) {
|
195 | throw Error(
|
196 | `Dimension "${dimName}" does not exist on loader.
|
197 | Must be one of "${this.dimensions.map(d => d.field)}."`
|
198 | );
|
199 | }
|
200 | const dimIndex = this._dimIndices.get(dimName);
|
201 | switch (typeof value) {
|
202 | case 'number': {
|
203 |
|
204 | serialized[dimIndex] = value;
|
205 | break;
|
206 | }
|
207 | case 'string': {
|
208 | const { values, type } = this.dimensions[dimIndex];
|
209 | if (type === 'nominal' || type === 'ordinal') {
|
210 |
|
211 | serialized[dimIndex] = values.indexOf(value);
|
212 | break;
|
213 | } else {
|
214 |
|
215 | throw Error(
|
216 | `Cannot use selection "${value}" for dimension "${dimName}" with type "${type}".`
|
217 | );
|
218 | }
|
219 | }
|
220 | default: {
|
221 | throw Error(
|
222 | `Named selection must be a string or number. Got ${value} for ${dimName}.`
|
223 | );
|
224 | }
|
225 | }
|
226 | });
|
227 | return serialized;
|
228 | }
|
229 | }
|