NGL@1.0.0-beta.7 Home Manual Reference Source Gallery

src/utils/netcdf-reader.js

/**
 * @file Netcdf Reader
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @private
 *
 * Adapted from https://github.com/cheminfo-js/netcdfjs
 * MIT License, Copyright (c) 2016 cheminfo
 */

import IOBuffer from './io-buffer.js'

/**
 * Throws a non-valid NetCDF exception if the statement it's true
 * @ignore
 * @param {boolean} statement - Throws if true
 * @param {string} reason - Reason to throw
 */
function notNetcdf (statement, reason) {
  if (statement) {
    throw new TypeError('Not a valid NetCDF v3.x file: ' + reason)
  }
}

/**
 * Moves 1, 2, or 3 bytes to next 4-byte boundary
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 */
function padding (buffer) {
  if ((buffer.offset % 4) !== 0) {
    buffer.skip(4 - (buffer.offset % 4))
  }
}

/**
 * Reads the name
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 * @return {string} - Name
 */
function readName (buffer) {
  // Read name
  const nameLength = buffer.readUint32()
  const name = buffer.readChars(nameLength)

  // validate name
  // TODO

  // Apply padding
  padding(buffer)
  return name
}

const types = {
  BYTE: 1,
  CHAR: 2,
  SHORT: 3,
  INT: 4,
  FLOAT: 5,
  DOUBLE: 6
}

/**
 * Parse a number into their respective type
 * @ignore
 * @param {number} type - integer that represents the type
 * @return {string} - parsed value of the type
 */
function num2str (type) {
  switch (Number(type)) {
    case types.BYTE:
      return 'byte'
    case types.CHAR:
      return 'char'
    case types.SHORT:
      return 'short'
    case types.INT:
      return 'int'
    case types.FLOAT:
      return 'float'
    case types.DOUBLE:
      return 'double'
    default:
      return 'undefined'
  }
}

/**
 * Parse a number type identifier to his size in bytes
 * @ignore
 * @param {number} type - integer that represents the type
 * @return {number} -size of the type
 */
function num2bytes (type) {
  switch (Number(type)) {
    case types.BYTE:
      return 1
    case types.CHAR:
      return 1
    case types.SHORT:
      return 2
    case types.INT:
      return 4
    case types.FLOAT:
      return 4
    case types.DOUBLE:
      return 8
    default:
      return -1
  }
}

/**
 * Reverse search of num2str
 * @ignore
 * @param {string} type - string that represents the type
 * @return {number} - parsed value of the type
 */
function str2num (type) {
  switch (String(type)) {
    case 'byte':
      return types.BYTE
    case 'char':
      return types.CHAR
    case 'short':
      return types.SHORT
    case 'int':
      return types.INT
    case 'float':
      return types.FLOAT
    case 'double':
      return types.DOUBLE
    default:
      return -1
  }
}

/**
 * Auxiliary function to read numeric data
 * @ignore
 * @param {number} size - Size of the element to read
 * @param {function} bufferReader - Function to read next value
 * @return {Array<number>|number}
 */
function readNumber (size, bufferReader) {
  if (size !== 1) {
    const numbers = new Array(size)
    for (let i = 0; i < size; i++) {
      numbers[i] = bufferReader()
    }
    return numbers
  } else {
    return bufferReader()
  }
}

/**
 * Given a type and a size reads the next element
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 * @param {number} type - Type of the data to read
 * @param {number} size - Size of the element to read
 * @return {string|Array<number>|number}
 */
function readType (buffer, type, size) {
  switch (type) {
    case types.BYTE:
      return buffer.readBytes(size)
    case types.CHAR:
      return trimNull(buffer.readChars(size))
    case types.SHORT:
      return readNumber(size, buffer.readInt16.bind(buffer))
    case types.INT:
      return readNumber(size, buffer.readInt32.bind(buffer))
    case types.FLOAT:
      return readNumber(size, buffer.readFloat32.bind(buffer))
    case types.DOUBLE:
      return readNumber(size, buffer.readFloat64.bind(buffer))
    default:
      notNetcdf(true, 'non valid type ' + type)
      return undefined
  }
}

/**
 * Removes null terminate value
 * @ignore
 * @param {string} value - String to trim
 * @return {string} - Trimmed string
 */
function trimNull (value) {
  if (value.charCodeAt(value.length - 1) === 0) {
    return value.substring(0, value.length - 1)
  }
  return value
}

// const STREAMING = 4294967295;

/**
 * Read data for the given non-record variable
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 * @param {object} variable - Variable metadata
 * @return {Array} - Data of the element
 */
function nonRecord (buffer, variable) {
  // variable type
  const type = str2num(variable.type)

  // size of the data
  const size = variable.size / num2bytes(type)

  // iterates over the data
  const data = new Array(size)
  for (let i = 0; i < size; i++) {
    data[i] = readType(buffer, type, 1)
  }

  return data
}

/**
 * Read data for the given record variable
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 * @param {object} variable - Variable metadata
 * @param {object} recordDimension - Record dimension metadata
 * @return {Array} - Data of the element
 */
function record (buffer, variable, recordDimension) {
  // variable type
  const type = str2num(variable.type)
  const width = variable.size ? variable.size / num2bytes(type) : 1

  // size of the data
  // TODO streaming data
  const size = recordDimension.length

  // iterates over the data
  const data = new Array(size)
  const step = recordDimension.recordStep

  for (let i = 0; i < size; i++) {
    const currentOffset = buffer.offset
    data[i] = readType(buffer, type, width)
    buffer.seek(currentOffset + step)
  }

  return data
}

// Grammar constants
const ZERO = 0
const NC_DIMENSION = 10
const NC_VARIABLE = 11
const NC_ATTRIBUTE = 12

/**
 * Read the header of the file
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 * @param {number} version - Version of the file
 * @return {object} - Object with the fields:
 *  * `recordDimension`: Number with the length of record dimension
 *  * `dimensions`: List of dimensions
 *  * `globalAttributes`: List of global attributes
 *  * `variables`: List of variables
 */
function header (buffer, version) {
  // Length of record dimension
  // sum of the varSize's of all the record variables.
  const header = {recordDimension: {length: buffer.readUint32()}}

  // Version
  header.version = version

  // List of dimensions
  const dimList = dimensionsList(buffer)
  header.recordDimension.id = dimList.recordId
  header.recordDimension.name = dimList.recordName
  header.dimensions = dimList.dimensions

  // List of global attributes
  header.globalAttributes = attributesList(buffer)

  // List of variables
  const variables = variablesList(buffer, dimList.recordId, version)
  header.variables = variables.variables
  header.recordDimension.recordStep = variables.recordStep

  return header
}

/**
 * List of dimensions
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 * @return {object} - List of dimensions and record dimension with:
 *  * `name`: String with the name of the dimension
 *  * `size`: Number with the size of the dimension
 */
function dimensionsList (buffer) {
  let dimensions, recordId, recordName
  const dimList = buffer.readUint32()
  if (dimList === ZERO) {
    notNetcdf((buffer.readUint32() !== ZERO), 'wrong empty tag for list of dimensions')
    return []
  } else {
    notNetcdf((dimList !== NC_DIMENSION), 'wrong tag for list of dimensions')

    // Length of dimensions
    const dimensionSize = buffer.readUint32()
    dimensions = new Array(dimensionSize)
    for (let dim = 0; dim < dimensionSize; dim++) {
      // Read name
      const name = readName(buffer)

      // Read dimension size
      const size = buffer.readUint32()
      if (size === 0) {
        recordId = dim
        recordName = name
      }

      dimensions[dim] = {
        name: name,
        size: size
      }
    }
  }
  return {
    dimensions: dimensions,
    recordId: recordId,
    recordName: recordName
  }
}

/**
 * List of attributes
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 * @return {Array<object>} - List of attributes with:
 *  * `name`: String with the name of the attribute
 *  * `type`: String with the type of the attribute
 *  * `value`: A number or string with the value of the attribute
 */
function attributesList (buffer) {
  let attributes
  const gAttList = buffer.readUint32()
  if (gAttList === ZERO) {
    notNetcdf((buffer.readUint32() !== ZERO), 'wrong empty tag for list of attributes')
    return []
  } else {
    notNetcdf((gAttList !== NC_ATTRIBUTE), 'wrong tag for list of attributes')

    // Length of attributes
    const attributeSize = buffer.readUint32()
    attributes = new Array(attributeSize)
    for (let gAtt = 0; gAtt < attributeSize; gAtt++) {
      // Read name
      const name = readName(buffer)

      // Read type
      const type = buffer.readUint32()
      notNetcdf(((type < 1) || (type > 6)), 'non valid type ' + type)

      // Read attribute
      const size = buffer.readUint32()
      const value = readType(buffer, type, size)

      // Apply padding
      padding(buffer)

      attributes[gAtt] = {
        name: name,
        type: num2str(type),
        value: value
      }
    }
  }
  return attributes
}

/**
 * List of variables
 * @ignore
 * @param {IOBuffer} buffer - Buffer for the file data
 * @param {number} recordId - Id if the record dimension
 * @param {number} version - Version of the file
 * @return {object} - Number of recordStep and list of variables with:
 *  * `name`: String with the name of the variable
 *  * `dimensions`: Array with the dimension IDs of the variable
 *  * `attributes`: Array with the attributes of the variable
 *  * `type`: String with the type of the variable
 *  * `size`: Number with the size of the variable
 *  * `offset`: Number with the offset where of the variable begins
 *  * `record`: True if is a record variable, false otherwise
 */
function variablesList (buffer, recordId, version) {
  const varList = buffer.readUint32()
  let recordStep = 0
  let variables
  if (varList === ZERO) {
    notNetcdf(
      (buffer.readUint32() !== ZERO),
      'wrong empty tag for list of variables'
    )
    return []
  } else {
    notNetcdf((varList !== NC_VARIABLE), 'wrong tag for list of variables')

    // Length of variables
    const variableSize = buffer.readUint32()
    variables = new Array(variableSize)
    for (let v = 0; v < variableSize; v++) {
      // Read name
      const name = readName(buffer)

      // Read dimensionality of the variable
      const dimensionality = buffer.readUint32()

      // Index into the list of dimensions
      const dimensionsIds = new Array(dimensionality)
      for (let dim = 0; dim < dimensionality; dim++) {
        dimensionsIds[dim] = buffer.readUint32()
      }

      // Read variables size
      const attributes = attributesList(buffer)

      // Read type
      const type = buffer.readUint32()
      notNetcdf(((type < 1) && (type > 6)), 'non valid type ' + type)

      // Read variable size
      // The 32-bit varSize field is not large enough to contain the
      // size of variables that require more than 2^32 - 4 bytes,
      // so 2^32 - 1 is used in the varSize field for such variables.
      const varSize = buffer.readUint32()

      // Read offset
      let offset = buffer.readUint32()
      if (version === 2) {
        notNetcdf((offset > 0), 'offsets larger than 4GB not supported')
        offset = buffer.readUint32()
      }

      // Count amount of record variables
      if (dimensionsIds[0] === recordId) {
        recordStep += varSize
      }

      variables[v] = {
        name: name,
        dimensions: dimensionsIds,
        attributes: attributes,
        type: num2str(type),
        size: varSize,
        offset: offset,
        record: (dimensionsIds[0] === recordId)
      }
    }
  }

  return {
    variables: variables,
    recordStep: recordStep
  }
}

/**
 * Reads a NetCDF v3.x file
 * https://www.unidata.ucar.edu/software/netcdf/docs/file_format_specifications.html
 */
class NetcdfReader {
  /**
   * @param {ArrayBuffer} data - ArrayBuffer or any Typed Array with the data
   */
  constructor (data) {
    const buffer = new IOBuffer(data)
    buffer.setBigEndian()

    // Validate that it's a NetCDF file
    notNetcdf((buffer.readChars(3) !== 'CDF'), 'should start with CDF')

    // Check the NetCDF format
    const version = buffer.readByte()
    notNetcdf((version > 2), 'unknown version')

    // Read the header
    this.header = header(buffer, version)
    this.buffer = buffer
  }

  /**
   * @return {string} - Version for the NetCDF format
   */
  get version () {
    if (this.header.version === 1) {
      return 'classic format'
    } else {
      return '64-bit offset format'
    }
  }

  /**
   * @return {object} - Metadata for the record dimension
   *  * `length`: Number of elements in the record dimension
   *  * `id`: Id number in the list of dimensions for the record dimension
   *  * `name`: String with the name of the record dimension
   *  * `recordStep`: Number with the record variables step size
   */
  get recordDimension () {
    return this.header.recordDimension
  }

  /**
   * @return {Array<object>} - List of dimensions with:
   *  * `name`: String with the name of the dimension
   *  * `size`: Number with the size of the dimension
   */
  get dimensions () {
    return this.header.dimensions
  }

  /**
   * @return {Array<object>} - List of global attributes with:
   *  * `name`: String with the name of the attribute
   *  * `type`: String with the type of the attribute
   *  * `value`: A number or string with the value of the attribute
   */
  get globalAttributes () {
    return this.header.globalAttributes
  }

  /**
   * @return {Array<object>} - List of variables with:
   *  * `name`: String with the name of the variable
   *  * `dimensions`: Array with the dimension IDs of the variable
   *  * `attributes`: Array with the attributes of the variable
   *  * `type`: String with the type of the variable
   *  * `size`: Number with the size of the variable
   *  * `offset`: Number with the offset where of the variable begins
   *  * `record`: True if is a record variable, false otherwise
   */
  get variables () {
    return this.header.variables
  }

  /**
   * Checks if a variable is available
   * @param {string|object} variableName - Name of the variable to check
   * @return {Boolean} - Variable existence
   */
  hasDataVariable (variableName) {
    return this.header.variables.findIndex(function (val) {
      return val.name === variableName
    }) !== -1
  }

  /**
   * Retrieves the data for a given variable
   * @param {string|object} variableName - Name of the variable to search or variable object
   * @return {Array} - List with the variable values
   */
  getDataVariable (variableName) {
    let variable
    if (typeof variableName === 'string') {
            // search the variable
      variable = this.header.variables.find(function (val) {
        return val.name === variableName
      })
    } else {
      variable = variableName
    }

    // throws if variable not found
    notNetcdf((variable === undefined), 'variable not found')

    // go to the offset position
    this.buffer.seek(variable.offset)

    if (variable.record) {
      // record variable case
      return record(this.buffer, variable, this.header.recordDimension)
    } else {
      // non-record variable case
      return nonRecord(this.buffer, variable)
    }
  }
}

export default NetcdfReader