Source: Raster16Bit.js

import { Raster } from './Raster.js';

/**
* @typedef {('r16f'|'rg16f'|'rgb16f'|'rgba16f'|'r16ui'|'rg16ui'|'rgb16ui'|'rgba16ui'|'r16i'|'rg16i'|'rgb16i'|'rgba16i'|'depth16')} Raster16Bit#Format
* Defines the 16-bit format for image data storage in textures.
* @property {'r16f'} r16f - Single-channel 16-bit floating point format
* @property {'rg16f'} rg16f - Two-channel 16-bit floating point format
* @property {'rgb16f'} rgb16f - Three-channel 16-bit floating point format
* @property {'rgba16f'} rgba16f - Four-channel 16-bit floating point format
* @property {'r16ui'} r16ui - Single-channel 16-bit unsigned integer format
* @property {'rg16ui'} rg16ui - Two-channel 16-bit unsigned integer format
* @property {'rgb16ui'} rgb16ui - Three-channel 16-bit unsigned integer format
* @property {'rgba16ui'} rgba16ui - Four-channel 16-bit unsigned integer format
* @property {'r16i'} r16i - Single-channel 16-bit signed integer format
* @property {'rg16i'} rg16i - Two-channel 16-bit signed integer format
* @property {'rgb16i'} rgb16i - Three-channel 16-bit signed integer format
* @property {'rgba16i'} rgba16i - Four-channel 16-bit signed integer format
* @property {'depth16'} depth16 - 16-bit depth texture format
*/

/**
* @typedef {Function} DataLoaderCallback
* @param {Object} tile - The tile information object
* @param {WebGL2RenderingContext} gl - The WebGL2 rendering context
* @param {Object} options - Additional options for the data loader
* @returns {Promise<Object>} The loaded data object with properties:
*   - data: TypedArray or Image data
*   - width: Width of the image
*   - height: Height of the image
*   - channels: Number of channels in the data
*/

/**
* Raster16Bit class extends Raster to handle 16-bit textures with WebGL 2.0.
* Provides functionality for:
* - Loading 16-bit images from URLs or blobs via custom data loaders
* - Converting data to appropriate WebGL 2.0 texture formats
* - Supporting various 16-bit formats (float, int, uint)
* - Creating appropriate texture parameters for 16-bit data
* - Support for custom data loaders for specialized formats
*/
class Raster16Bit extends Raster {
  /**
   * Creates a new Raster16Bit instance.
   * @param {Object} [options] - Configuration options
   * @param {Raster16Bit#Format} [options.format='rgb16ui'] - 16-bit data format
   * @param {boolean} [options.useHalfFloat=false] - Use HALF_FLOAT type instead of FLOAT for better performance when applicable
   * @param {boolean} [options.flipY=false] - Whether to flip the image vertically during loading
   * @param {boolean} [options.premultiplyAlpha=false] - Whether to premultiply alpha during loading
   * @param {DataLoaderCallback} [options.dataLoader=null] - Custom data loader callback
   * @param {Object} [options.dataLoaderOptions={}] - Options to pass to the data loader
   * @param {boolean} [options.debug=false] - Enable debug output
   */
  constructor(options) {
    // Initialize with parent constructor but override defaults
    super(Object.assign({
        format: 'rgb16ui',
        debug: false,
        useHalfFloat: false,
        flipY: false,
        premultiplyAlpha: false,
    }, options));

    // Additional options specific to 16-bit handling
    Object.assign(this, {
        dataLoader: null,
        dataLoaderOptions: {},
        statInfo: {}
    });

    // Override with provided options
    if (options) {
        Object.assign(this, options);
    }

    // Check if the format is supported
    if (!this._isFormatSupported(this.format)) {
        throw new Error(`The format "${this.format}" is not supported by the browser.`);
    }

    if (this.debug) {
        console.log(`Raster16Bit created with format: ${this.format}`);
    }
  }

  /**
   * Gets the number of components for the current format
   * @private
   * @returns {number} Number of components (1, 2, 3, or 4)
   */
  _getComponentCount() {
    if (this.format.startsWith('r16') && !this.format.startsWith('rg16') && !this.format.startsWith('rgb16') && !this.format.startsWith('rgba16')) {
      return 1; // Single channel (r16f, r16ui, r16i)
    } else if (this.format.startsWith('rg16')) {
      return 2; // Two channels (rg16f, rg16ui, rg16i)
    } else if (this.format.startsWith('rgb16')) {
      return 3; // Three channels (rgb16f, rgb16ui, rgb16i)
    } else if (this.format.startsWith('rgba16')) {
      return 4; // Four channels (rgba16f, rgba16ui, rgba16i)
    } else if (this.format === 'depth16') {
      return 1; // Depth is single channel
    }
    return 1; // Default to 1 if unknown
  }

  /**
   * Loads a 16-bit image tile and converts it to a WebGL texture.
   * Overrides parent method to handle 16-bit specific formats.
   * @async
   * @param {Object} tile - The tile to load
   * @param {string} tile.url - URL of the image
   * @param {number} [tile.start] - Start byte for partial requests
   * @param {number} [tile.end] - End byte for partial requests
   * @param {WebGL2RenderingContext} gl - The WebGL2 rendering context
   * @returns {Promise<Array>} Promise resolving to [texture, size]:
   *   - texture: WebGLTexture object
   *   - size: Size of the image in bytes (width * height * components * bytesPerComponent)
   * @throws {Error} If context is not WebGL2
   */
  async loadImage(tile, gl) {
    // Ensure we have a WebGL2 context
    if (!(gl instanceof WebGL2RenderingContext)) {
      throw new Error("WebGL2 context is required for 16-bit textures");
    }

    if (this.debug) {
      console.log(`Raster16Bit.loadImage called for URL: ${tile.url}`);
    }

    let imageData;

    // Use the appropriate data loader
    if (this.dataLoader) {
      // Use custom data loader if provided
      if (this.debug) {
        console.log("Using custom data loader");
      }

      try {
        imageData = await this.dataLoader(tile, gl, this.dataLoaderOptions);
        this.statInfo.maxValue = imageData.statistics.maxValue;
        this.statInfo.avgLuminance = imageData.statistics.avgLuminance;
        this.statInfo.percentileLuminance = imageData.statistics.percentileLuminance;
        this.emit('loaded');
        if (this.debug) {
          console.log(`Data loader returned: ${imageData.width}x${imageData.height}, ${imageData.channels} channels`);
        }
      } catch (error) {
        console.error("Error in data loader:", error);
        throw error;
      }
    } else {
      // Use default parent class loading mechanism if no dataLoader provided
      if (this.debug) {
        console.log("Using default loader mechanism");
      }

      try {
        let [tex, size] = await super.loadImage(tile, gl);

        // Adjust size calculation for 16-bit (2 bytes per component)
        size = this.width * this.height * this._getComponentCount() * 2;

        return [tex, size];
      } catch (error) {
        console.error("Error in default loader:", error);
        throw error;
      }
    }

    // Store dimensions
    this.width = imageData.width;
    this.height = imageData.height;

    if (this.debug) {
      console.log(`Creating texture: ${this.width}x${this.height}`);
    }

    // Create texture from the loaded data
    const tex = this._createTextureFromData(gl, imageData.data, imageData.width, imageData.height, imageData.channels);

    // Calculate size in bytes
    const bytesPerComponent = 2; // 16 bits = 2 bytes
    const size = imageData.width * imageData.height * imageData.channels * bytesPerComponent;

    return [tex, size];
  }

  getStatInfo() {
    return this.statInfo;
  }

  /**
   * Creates a WebGL2 texture from raw data.
   * @private
   * @param {WebGL2RenderingContext} gl - The WebGL2 rendering context
   * @param {TypedArray} data - The raw pixel data
   * @param {number} width - Width of the image
   * @param {number} height - Height of the image
   * @param {number} channels - Number of channels in the data
   * @returns {WebGLTexture} The created texture
   */
  _createTextureFromData(gl, data, width, height, channels) {
    if (this.debug) {
      console.log(`Creating texture from data: ${width}x${height}, ${channels} channels, data type: ${data.constructor.name}`);
    }

    const tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);

    // Set texture parameters
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY);
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha);
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);

    // Determine format parameters based on format
    const formatParams = this._getFormatParameters(gl, channels);

    if (this.debug) {
      console.log("Format parameters:", formatParams);
    }

    try {
      // Upload data to texture
      gl.texImage2D(
        gl.TEXTURE_2D,                // target
        0,                            // level
        formatParams.internalFormat,  // internalformat
        width,                        // width
        height,                       // height
        0,                            // border
        formatParams.format,          // format
        formatParams.type,            // type
        data                          // pixels
      );
    } catch (error) {
      console.error("Error creating texture:", error);
      throw error;
    }

    // Set filtering and wrapping parameters
    if (width > 1024 || height > 1024) {
      gl.generateMipmap(gl.TEXTURE_2D);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    } else {
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    }

    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    // Store color space information on the texture
    this._texture = tex;

    return tex;
  }

  /**
   * Get format parameters for WebGL texture creation based on format and channels.
   * @private
   * @param {WebGL2RenderingContext} gl - The WebGL2 rendering context
   * @param {number} channels - Number of channels in the data
   * @returns {Object} Object with internalFormat, format, and type properties
   */
  _getFormatParameters(gl, channels) {
    let internalFormat, format, type;

    // Determine format parameters based on the specified format
    if (this.format.includes('16f')) {
      // Floating point formats
      type = this.useHalfFloat ? gl.HALF_FLOAT : gl.FLOAT;

      switch (channels) {
        case 1:
          internalFormat = gl.R16F;
          format = gl.RED;
          break;
        case 2:
          internalFormat = gl.RG16F;
          format = gl.RG;
          break;
        case 3:
          internalFormat = gl.RGB16F;
          format = gl.RGB;
          break;
        case 4:
          internalFormat = gl.RGBA16F;
          format = gl.RGBA;
          break;
        default:
          throw new Error(`Unsupported channel count: ${channels}`);
      }
    } else if (this.format.includes('16ui')) {
      // Unsigned integer formats
      type = gl.UNSIGNED_SHORT;

      switch (channels) {
        case 1:
          internalFormat = gl.R16UI;
          format = gl.RED_INTEGER;
          break;
        case 2:
          internalFormat = gl.RG16UI;
          format = gl.RG_INTEGER;
          break;
        case 3:
          internalFormat = gl.RGB16UI;
          format = gl.RGB_INTEGER;
          break;
        case 4:
          internalFormat = gl.RGBA16UI;
          format = gl.RGBA_INTEGER;
          break;
        default:
          throw new Error(`Unsupported channel count: ${channels}`);
      }
    } else if (this.format.includes('16i')) {
      // Signed integer formats
      type = gl.SHORT;

      switch (channels) {
        case 1:
          internalFormat = gl.R16I;
          format = gl.RED_INTEGER;
          break;
        case 2:
          internalFormat = gl.RG16I;
          format = gl.RG_INTEGER;
          break;
        case 3:
          internalFormat = gl.RGB16I;
          format = gl.RGB_INTEGER;
          break;
        case 4:
          internalFormat = gl.RGBA16I;
          format = gl.RGBA_INTEGER;
          break;
        default:
          throw new Error(`Unsupported channel count: ${channels}`);
      }
    } else if (this.format === 'depth16') {
      // Depth texture
      internalFormat = gl.DEPTH_COMPONENT16;
      format = gl.DEPTH_COMPONENT;
      type = gl.UNSIGNED_SHORT;
    } else {
      throw new Error(`Unsupported format: ${this.format}`);
    }

    return { internalFormat, format, type };
  }

  /**
 * Checks if the specified format is supported by the browser.
 * Also verifies that required WebGL extensions are available.
 * @private
 * @param {string} format - The format to check
 * @returns {boolean} True if the format is supported, false otherwise
 */
_isFormatSupported(format) {
    const canvas = document.createElement('canvas');
    const gl = canvas.getContext('webgl2');

    if (!gl) {
        console.error('WebGL2 is not supported by this browser.');
        return false;
    }

    const formatMap = {
        'r16f': { internalFormat: gl.R16F, requiredExtensions: ['EXT_color_buffer_float'] },
        'rg16f': { internalFormat: gl.RG16F, requiredExtensions: ['EXT_color_buffer_float'] },
        'rgb16f': { internalFormat: gl.RGB16F, requiredExtensions: ['EXT_color_buffer_float'] },
        'rgba16f': { internalFormat: gl.RGBA16F, requiredExtensions: ['EXT_color_buffer_float'] },
        'r16ui': { internalFormat: gl.R16UI, requiredExtensions: [] },
        'rg16ui': { internalFormat: gl.RG16UI, requiredExtensions: [] },
        'rgb16ui': { internalFormat: gl.RGB16UI, requiredExtensions: [] },
        'rgba16ui': { internalFormat: gl.RGBA16UI, requiredExtensions: [] },
        'r16i': { internalFormat: gl.R16I, requiredExtensions: [] },
        'rg16i': { internalFormat: gl.RG16I, requiredExtensions: [] },
        'rgb16i': { internalFormat: gl.RGB16I, requiredExtensions: [] },
        'rgba16i': { internalFormat: gl.RGBA16I, requiredExtensions: [] },
        'depth16': { internalFormat: gl.DEPTH_COMPONENT16, requiredExtensions: [] }
    };

    const formatInfo = formatMap[format];
    if (!formatInfo) {
        console.error(`Unknown format: ${format}`);
        return false;
    }

    // Check for required extensions
    for (const extension of formatInfo.requiredExtensions) {
        if (!gl.getExtension(extension)) {
            console.error(`Required WebGL extension "${extension}" is not supported for format "${format}".`);
            return false;
        }
    }

    // Check if the internal format is supported
    const isSupported = gl.getInternalformatParameter(gl.RENDERBUFFER, formatInfo.internalFormat, gl.SAMPLES);
    return isSupported && isSupported.length > 0;
}
}

export { Raster16Bit };