Source: LayerMultispectral.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
import { Layer } from './Layer.js'
import { Raster } from './Raster.js'
import { ShaderMultispectral } from './ShaderMultispectral.js'
import { Util } from './Util.js'
import { Transform } from './Transform.js'

/**
 * @typedef {Object} LayerMultispectralOptions
 * @property {string} url - URL to multispectral info.json file (required)
 * @property {string} layout - Layout type: 'image', 'deepzoom', 'google', 'iiif', 'zoomify', 'tarzoom', 'itarzoom'
 * @property {string} [defaultMode='single_band'] - Initial visualization mode ('rgb' or 'single_band')
 * @property {string} [server] - IIP server URL (for IIP layout)
 * @property {boolean} [linearRaster=true] - Whether to use linear color space for rasters (recommended for scientific accuracy)
 * @property {string|Object} presets - Path to presets JSON file or presets object containing CTW configurations
 * @extends LayerOptions
 */

/**
 * LayerMultispectral - Advanced multispectral imagery visualization layer
 * 
 * This layer provides specialized handling of multispectral image data with configurable 
 * visualization modes and interactive spectral analysis capabilities through Color Twist 
 * Weights (CTW). It supports scientific visualization workflows for remote sensing, art analysis,
 * medical imaging, and other multispectral applications.
 * 
 * Features:
 * - Multiple visualization modes (RGB, single band)
 * - UBO-optimized Color Twist Weights implementation for real-time spectral transformations
 * - Preset system for common visualization configurations (false color, etc.)
 * - Support for multiple image layouts and tiling schemes 
 * - Compatible with both single images and tile-based formats (DeepZoom, etc.)
 * 
 * Technical implementation:
 * - Uses WebGL2 features for efficient processing
 * - Implements shader-based visualization pipeline
 * - Supports multiple image layouts and tiling schemes
 * 
 * @extends Layer
 * 
 * @example
 * // Create multispectral layer with deepzoom layout
 * const msLayer = new OpenLIME.Layer({
 *   type: 'multispectral',
 *   url: 'path/to/info.json',
 *   layout: 'deepzoom',
 *   defaultMode: 'rgb',
 *   presets: 'path/to/presets.json'
 * });
 * 
 * // Add to viewer
 * viewer.addLayer('ms', msLayer);
 * 
 * // Apply a preset CTW
 * msLayer.applyPreset('falseColor');
 */
class LayerMultispectral extends Layer {
  /**
   * Creates a new LayerMultispectral instance
   * @param {LayerMultispectralOptions} options - Configuration options
   * @throws {Error} If rasters options is not empty (rasters are created automatically)
   * @throws {Error} If url to info.json is not provided
   * @throws {Error} If presets option is not provided
   */
  constructor(options) {
    super(options);

    if (Object.keys(this.rasters).length != 0)
      throw new Error("Rasters options should be empty!");

    if (!this.url)
      throw new Error("Url option is required");

    if (!this.presets) {
      throw new Error("Presets option is required");
    }
    this.loadPresets();

    // Set default options
    this.linearRaster = true;
    this.defaultMode = this.defaultMode || 'single_band';

    // Create shader
    this.shaders['multispectral'] = new ShaderMultispectral();
    this.setShader('multispectral');

    // Set current CTW arrays
    this._currentCTW = {
      red: null,
      green: null,
      blue: null
    };

    // Load configuration
    this.info = null;
    this.loadInfo(this.url);
  }

  /**
   * Constructs URL for image resources based on layout type
   * 
   * Handles different image layout conventions including deepzoom, google maps tiles,
   * zoomify, and specialized formats like tarzoom.
   * 
   * @param {string} url - Base URL
   * @param {string} filename - Base filename without extension
   * @returns {string} Complete URL for the resource
   * @private
   */
  imageUrl(url, filename) {
    let path = this.url.substring(0, this.url.lastIndexOf('/') + 1);
    switch (this.layout.type) {
      case 'image': return path + filename + '.jpg';
      case 'google': return path + filename;
      case 'deepzoom':
        // Special handling for multispectral deepzoom
        return path + filename + '.dzi';
      case 'tarzoom': return path + filename + '.tzi';
      case 'itarzoom': return path + filename + '.tzi';
      case 'zoomify': return path + filename + '/ImageProperties.xml';
      case 'iip': return url;
      case 'iiif': throw new Error("Unimplemented");
      default: throw new Error("Unknown layout: " + this.layout.type);
    }
  }

  /**
   * Loads and processes multispectral configuration
   * 
   * Fetches the info.json file containing wavelength, basename, and other
   * configuration parameters, then sets up the rasters and shader accordingly.
   * 
   * @param {string} url - URL to info.json
   * @private
   * @async
   */
  async loadInfo(url) {
    try {
      let infoUrl = url;
      // Need to handle embedded info.json when using IIP and TIFF image stacks
      if (this.layout.type == "iip") infoUrl = (this.server ? this.server + '?FIF=' : '') + url + "&obj=description";

      this.info = await Util.loadJSON(infoUrl);
      console.log("Multispectral info loaded:", this.info);

      // Check if basename is present
      if (!this.info.basename) {
        this.status = "Error: 'basename' is required in the multispectral configuration file";
        console.error(this.status);
        return;
      }

      // Update layout image format and pixelSize if provided in info.json
      if (this.info.format) this.layout.suffix = this.info.format;
      if (this.info.pixelSizeInMM) this.pixelSize = this.info.pixelSizeInMM;

      // Initialize shader with info
      this.shader.init(this.info);

      // Set texture size if available
      if (this.info.width && this.info.height) {
        this.width = this.info.width;
        this.height = this.info.height;
      }

      // Get basename from info
      const baseName = this.info.basename;
      console.log("Using basename:", baseName);

      // Create rasters and URLs array for each image
      const urls = [];

      // Handle special case for itarzoom (all planes in one file)
      if (this.layout.type === 'itarzoom') {
        // Create a single raster for all planes
        let raster = new Raster({ format: 'vec3', isLinear: this.linearRaster });
        this.rasters.push(raster);

        // Add a single URL for all planes
        urls.push(this.imageUrl(url, baseName));
      } else {
        // Standard case: one file per image
        for (let p = 0; p < this.shader.nimg; p++) {
          // Create raster with linear color space
          let raster = new Raster({ format: 'vec3', isLinear: this.linearRaster });
          this.rasters.push(raster);

          // Format index with leading zeros (e.g., 00, 01, 02)
          const indexStr = p.toString().padStart(2, '0');

          // Generate URL for this image
          const imgUrl = this.imageUrl(url, `${baseName}_${indexStr}`);
          urls.push(imgUrl);
          console.log(`Plane ${p} URL: ${imgUrl}`);
        }
      }

      // Set URLs for layout
      if (urls.length > 0) {
        this.layout.setUrls(urls);
      }

      // Set up the shader
      this.setMode(this.defaultMode);
      this.initDefault();

    } catch (e) {
      console.error("Error loading multispectral info:", e);
      this.status = e;
    }
  }

  /**
   * Loads preset definitions for Color Twist Weights
   * 
   * Can load presets from a URL or use directly provided preset object.
   * Presets define predefined CTW configurations for common visualization needs.
   * 
   * @private
   * @async
   */
  async loadPresets() {
    if (typeof this.presets === 'string' && this.presets.trim() !== '') {
      this.presets = await Util.loadJSON(this.presets);
    }
    if (typeof this.presets !== 'object') {
      throw new Error("presets not well formed");
    }
  }

  /**
   * Gets info
   * 
   * @returns {Object|null} Object with info on multispectral dataset or null if not found
   */
  info() {
    return this.info;
  }

  /**
   * Initializes default CTW based on default mode
   * 
   * Creates initial CTW arrays with zeros and applies default
   * visualization settings.
   * 
   * @private
   */
  initDefault() {
    // Create default CTW arrays
    const nplanes = this.shader.nplanes;
    if (!nplanes) return; // Not yet initialized

    let redCTW = new Float32Array(nplanes).fill(0);
    let greenCTW = new Float32Array(nplanes).fill(0);
    let blueCTW = new Float32Array(nplanes).fill(0);

    // Update current CTW and shader
    this._currentCTW.red = redCTW;
    this._currentCTW.green = greenCTW;
    this._currentCTW.blue = blueCTW;

    if (this.defaultMode === 'single-band') {
      this.setSingleBand(0, 0);
    }
  }

  /**
   * Sets the visualization mode
   * 
   * Changes how multispectral data is visualized:
   * - 'rgb': Uses CTW coefficients to create RGB visualization
   * - 'single_band': Shows a single spectral band
   * 
   * @param {string} mode - Mode name ('rgb', 'single_band')
   */
  setMode(mode) {
    if (this.shader) {
      this.shader.setMode(mode);
      this.emit('update');
    }
  }

  /**
   * Sets single band visualization
   * 
   * Displays a single spectral band on a specific output channel.
   * 
   * @param {number} bandIndex - Index of band to visualize
   * @param {number} [channel=0] - Output channel (0=all/gray, 1=R, 2=G, 3=B)
   */
  setSingleBand(bandIndex, channel = 0) {
    if (this.shader) {
      this.shader.setSingleBand(bandIndex, channel);
      this.emit('update');
    }
  }

  /**
   * Sets Color Twist Weights coefficients manually
   * 
   * CTW coefficients define how spectral bands are combined to create
   * RGB visualization. Each array contains weights for each spectral band.
   * 
   * @param {Float32Array} redCTW - Red channel coefficients
   * @param {Float32Array} greenCTW - Green channel coefficients
   * @param {Float32Array} blueCTW - Blue channel coefficients
   * @throws {Error} If arrays have incorrect length
   */
  setCTW(redCTW, greenCTW, blueCTW) {
    if (!this.shader || !this.gl) return;

    // Validate array lengths
    const nplanes = this.shader.nplanes;
    if (redCTW.length !== nplanes || greenCTW.length !== nplanes || blueCTW.length !== nplanes) {
      throw new Error(`CTW arrays must be of length ${nplanes}`);
    }

    // Update current CTW
    this._currentCTW.red = redCTW;
    this._currentCTW.green = greenCTW;
    this._currentCTW.blue = blueCTW;

    // Update shader CTW
    this.shader.setupCTW(this.gl, redCTW, greenCTW, blueCTW);

    // Set to rgb mode
    this.setMode('rgb');
  }

  /**
   * Gets a preset CTW configuration by name
   * 
   * Retrieves the preset's red, green, and blue CTW arrays from
   * the presets collection.
   * 
   * @param {string} presetName - Name of the preset
   * @returns {Object|null} Object with red, green, blue arrays or null if not found
   */
  getPreset(presetName) {
    if (presetName in this.presets) {
      const { red, green, blue } = this.presets[presetName];
      return { red, green, blue };
    } else {
      console.warn(`Preset "${presetName}" not found.`);
      return null;
    }
  }

  /**
   * Applies a preset CTW from the presets library
   * 
   * Loads and applies a predefined set of CTW coefficients for
   * specialized visualization (e.g., false color, vegetation analysis).
   * 
   * @param {string} presetName - Name of the preset
   * @throws {Error} If preset doesn't exist
   */
  applyPreset(presetName) {
    if (!this.shader) return;

    // Get preset from the preset manager
    const preset = this.getPreset(presetName);

    if (!preset) {
      throw new Error(`Preset '${presetName}' not found`);
    }

    // Apply the preset
    this.setCTW(preset.red, preset.green, preset.blue);
  }

  /**
   * Gets the wavelength array for spectral bands
   * 
   * Returns the wavelength values (in nm) for each spectral band.
   * 
   * @returns {number[]} Array of wavelengths
   */
  getWavelengths() {
    return this.shader ? this.shader.wavelength : [];
  }

  /**
   * Gets the number of spectral bands
   * 
   * Returns the count of spectral planes in the multispectral dataset.
   * 
   * @returns {number} Number of bands
   */
  getBandCount() {
    return this.shader ? this.shader.nplanes : 0;
  }

  /**
   * Gets available presets
   * 
   * Returns the names of all available preset CTW configurations.
   * 
   * @returns {string[]} Array of preset names
   */
  getAvailablePresets() {
    return this.presets ? Object.keys(this.presets) : [];
  }

  /**
   * Prepares WebGL resources including UBO for CTW
   * 
   * Sets up WebGL context and ensures CTW arrays are uploaded to GPU.
   * 
   * @override
   * @private
   */
  prepareWebGL() {
    // Call parent implementation
    super.prepareWebGL();

    // Setup CTW if needed
    if (this._currentCTW.red && this.shader && this.gl) {
      this.shader.setupCTW(
        this.gl,
        this._currentCTW.red,
        this._currentCTW.green,
        this._currentCTW.blue
      );
    }
  }

  /**
   * Gets spectrum data for a specific pixel
   * 
   * For tiled formats, this method finds the appropriate tiles
   * and reads the spectral values.
   * 
   * @param {number} x - X coordinate in image space
   * @param {number} y - Y coordinate in image space  
   * @returns {number[]} Array of spectral values (0-100)
   */
  getSpectrum(x, y) {
    // For tiled formats, we need a special approach
    if (this.isTiledFormat()) {
      return this.getTiledSpectrum(x, y);
    }

    // For standard formats, use the original approach
    const pixelData = this.getPixelValues(x, y);
    const spectrum = [];

    if (!pixelData || pixelData.length === 0) {
      return new Array(this.shader.nplanes).fill(0);
    }

    for (let i = 0; i < this.info.nplanes; i++) {
      const idx = Math.floor(i / 3);
      if (idx < pixelData.length) {
        const px = pixelData[idx];
        const pxIdx = i % 3;
        if (px && pxIdx < 3) {
          spectrum.push(px[pxIdx] / 255.0 * 100);
        } else {
          spectrum.push(0);
        }
      } else {
        spectrum.push(0);
      }
    }
    return spectrum;
  }

  /**
   * Checks if current layout is a tiled format
   * @private
   * @returns {boolean} True if using a tiled format
   */
  isTiledFormat() {
    const tiledFormats = ['deepzoom', 'deepzoom1px', 'google', 'zoomify', 'iiif', 'tarzoom'];
    return tiledFormats.includes(this.layout.type);
  }

  /**
   * Gets spectrum data for a specific pixel
   * 
   * Uses the improved getPixelValues method from the base Layer class
   * to obtain spectral values across all bands.
   * 
   * @param {number} x - X coordinate in image space
   * @param {number} y - Y coordinate in image space  
   * @returns {number[]} Array of spectral values (0-100)
   */
  getSpectrum(x, y) {
    // Get pixel data from base Layer class method
    const pixelData = this.getPixelValues(x, y);

    // Create spectrum array for all planes
    const spectrum = new Array(this.shader.nplanes).fill(0);

    // If no data was returned, return empty spectrum
    if (!pixelData || pixelData.length === 0) {
      return spectrum;
    }

    // Convert pixel data to spectrum values
    for (let i = 0; i < this.shader.nplanes; i++) {
      const idx = Math.floor(i / 3); // Which texture contains this band
      const pxIdx = i % 3;           // Which channel (R, G, B) in the texture

      if (idx < pixelData.length) {
        const px = pixelData[idx];
        if (px && pxIdx < 3) {
          // Convert to percentage (0-100)
          spectrum[i] = px[pxIdx] / 255.0 * 100;
        }
      }
    }

    return spectrum;
  }

}

/**
 * Register this layer type with the Layer factory
 * @type {Function}
 * @private
 */
Layer.prototype.types['multispectral'] = (options) => { return new LayerMultispectral(options); }

export { LayerMultispectral }