Source: geotiff.js

"use strict";

var globals = require("./globals.js");
var GeoTIFFImage = require("./geotiffimage.js");
var DataView64 = require("./dataview64.js");

var fieldTypes = globals.fieldTypes,
    fieldTagNames = globals.fieldTagNames,
    arrayFields = globals.arrayFields,
    geoKeyNames = globals.geoKeyNames;

/**
 * The abstraction for a whole GeoTIFF file.
 * @constructor
 * @param {ArrayBuffer} rawData the raw data stream of the file as an ArrayBuffer.
 * @param {Object} [options] further options.
 * @param {Boolean} [options.cache=false] whether or not decoded tiles shall be cached.
 */
function GeoTIFF(rawData, options) {
  this.dataView = new DataView64(rawData);
  options = options || {};
  this.cache = options.cache || false;

  var BOM = this.dataView.getUint16(0, 0);
  if (BOM === 0x4949) {
    this.littleEndian = true;
  }
  else if (BOM === 0x4D4D) {
    this.littleEndian = false;
  }
  else {
    throw new TypeError("Invalid byte order value.");
  }

  var magicNumber = this.dataView.getUint16(2, this.littleEndian);
  if (this.dataView.getUint16(2, this.littleEndian) === 42) {
    this.bigTiff = false;
  }
  else if (magicNumber === 43) {
    this.bigTiff = true;
    var offsetBytesize = this.dataView.getUint16(4, this.littleEndian);
    if (offsetBytesize !== 8) {
      throw new Error("Unsupported offset byte-size.");
    }
  }
  else {
    throw new TypeError("Invalid magic number.");
  }

  this.fileDirectories = this.parseFileDirectories(
    this.getOffset((this.bigTiff) ? 8 : 4)
  );
}

GeoTIFF.prototype = {
  getOffset: function(offset) {
    if (this.bigTiff) {
      return this.dataView.getUint64(offset, this.littleEndian);
    }
    return this.dataView.getUint32(offset, this.littleEndian);
  },

  getFieldTypeLength: function(fieldType) {
    switch (fieldType) {
      case fieldTypes.BYTE: case fieldTypes.ASCII: case fieldTypes.SBYTE: case fieldTypes.UNDEFINED:
        return 1;
      case fieldTypes.SHORT: case fieldTypes.SSHORT:
        return 2;
      case fieldTypes.LONG: case fieldTypes.SLONG: case fieldTypes.FLOAT:
        return 4;
      case fieldTypes.RATIONAL: case fieldTypes.SRATIONAL: case fieldTypes.DOUBLE:
      case fieldTypes.LONG8: case fieldTypes.SLONG8: case fieldTypes.IFD8:
        return 8;
      default:
        throw new RangeError("Invalid field type: " + fieldType);
    }
  },

  getValues: function(fieldType, count, offset) {
    var values = null;
    var readMethod = null;
    var fieldTypeLength = this.getFieldTypeLength(fieldType);
    var i;

    switch (fieldType) {
      case fieldTypes.BYTE: case fieldTypes.ASCII: case fieldTypes.UNDEFINED:
        values = new Uint8Array(count); readMethod = this.dataView.getUint8;
        break;
      case fieldTypes.SBYTE:
        values = new Int8Array(count); readMethod = this.dataView.getInt8;
        break;
      case fieldTypes.SHORT:
        values = new Uint16Array(count); readMethod = this.dataView.getUint16;
        break;
      case fieldTypes.SSHORT:
        values = new Int16Array(count); readMethod = this.dataView.getInt16;
        break;
      case fieldTypes.LONG:
        values = new Uint32Array(count); readMethod = this.dataView.getUint32;
        break;
      case fieldTypes.SLONG:
        values = new Int32Array(count); readMethod = this.dataView.getInt32;
        break;
      case fieldTypes.LONG8: case fieldTypes.IFD8:
        values = new Array(count); readMethod = this.dataView.getUint64;
        break;
      case fieldTypes.SLONG8:
        values = new Array(count); readMethod = this.dataView.getInt64;
        break;
      case fieldTypes.RATIONAL:
        values = new Uint32Array(count*2); readMethod = this.dataView.getUint32;
        break;
      case fieldTypes.SRATIONAL:
        values = new Int32Array(count*2); readMethod = this.dataView.getInt32;
        break;
      case fieldTypes.FLOAT:
        values = new Float32Array(count); readMethod = this.dataView.getFloat32;
        break;
      case fieldTypes.DOUBLE:
        values = new Float64Array(count); readMethod = this.dataView.getFloat64;
        break;
      default:
        throw new RangeError("Invalid field type: " + fieldType);
    }

    // normal fields
    if (!(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) {
      for (i=0; i < count; ++i) {
        values[i] = readMethod.call(
          this.dataView, offset + (i*fieldTypeLength), this.littleEndian
        );
      }
    }
    // RATIONAL or SRATIONAL
    else {
      for (i=0; i < count; i+=2) {
        values[i] = readMethod.call(
          this.dataView, offset + (i*fieldTypeLength), this.littleEndian
        );
        values[i+1] = readMethod.call(
          this.dataView, offset + (i*fieldTypeLength + 4), this.littleEndian
        );
      }
    }

    if (fieldType === fieldTypes.ASCII) {
      return String.fromCharCode.apply(null, values);
    }
    return values;
  },

  getFieldValues: function(fieldTag, fieldType, typeCount, valueOffset) {
    var fieldValues;
    var fieldTypeLength = this.getFieldTypeLength(fieldType);

    if (fieldTypeLength * typeCount <= (this.bigTiff ? 8 : 4)) {
      fieldValues = this.getValues(fieldType, typeCount, valueOffset);
    }
    else {
      var actualOffset = this.getOffset(valueOffset);
      fieldValues = this.getValues(fieldType, typeCount, actualOffset);
    }

    if (typeCount === 1 && arrayFields.indexOf(fieldTag) === -1 && !(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) {
      return fieldValues[0];
    }

    return fieldValues;
  },

  parseGeoKeyDirectory: function(fileDirectory) {
    var rawGeoKeyDirectory = fileDirectory.GeoKeyDirectory;
    if (!rawGeoKeyDirectory) {
      return null;
    }

    var geoKeyDirectory = {};
    for (var i = 4; i < rawGeoKeyDirectory[3] * 4; i += 4) {
      var key = geoKeyNames[rawGeoKeyDirectory[i]],
        location = (rawGeoKeyDirectory[i+1]) ? (fieldTagNames[rawGeoKeyDirectory[i+1]]) : null,
        count = rawGeoKeyDirectory[i+2],
        offset = rawGeoKeyDirectory[i+3];

      var value = null;
      if (!location) {
        value = offset;
      }
      else {
        value = fileDirectory[location];
        if (typeof value === "undefined" || value === null) {
          throw new Error("Could not get value of geoKey '" + key + "'.");
        }
        else if (typeof value === "string") {
          value = value.substring(offset, offset + count - 1);
        }
        else if (value.subarray) {
          value = value.subarray(offset, offset + count - 1);
        }
      }
      geoKeyDirectory[key] = value;
    }
    return geoKeyDirectory;
  },

  parseFileDirectories: function(byteOffset) {
    var nextIFDByteOffset = byteOffset;
    var fileDirectories = [];

    while (nextIFDByteOffset !== 0x00000000) {
      var numDirEntries = this.bigTiff ?
          this.dataView.getUint64(nextIFDByteOffset, this.littleEndian) :
          this.dataView.getUint16(nextIFDByteOffset, this.littleEndian);

      var fileDirectory = {};
      var i = nextIFDByteOffset + (this.bigTiff ? 8 : 2);
      for (var entryCount = 0; entryCount < numDirEntries; i += (this.bigTiff ? 20 : 12), ++entryCount) {
        var fieldTag = this.dataView.getUint16(i, this.littleEndian);
        var fieldType = this.dataView.getUint16(i + 2, this.littleEndian);
        var typeCount = this.bigTiff ?
            this.dataView.getUint64(i + 4, this.littleEndian):
            this.dataView.getUint32(i + 4, this.littleEndian);

        fileDirectory[fieldTagNames[fieldTag]] = this.getFieldValues(
          fieldTag, fieldType, typeCount, i + (this.bigTiff ? 12 : 8)
        );
      }
      fileDirectories.push([
        fileDirectory, this.parseGeoKeyDirectory(fileDirectory)
      ]);

      nextIFDByteOffset = this.getOffset(i);
    }
    return fileDirectories;
  },

  /**
   * Get the n-th internal subfile a an image. By default, the first is returned.
   *
   * @param {Number} [index=0] the index of the image to return.
   * @returns {GeoTIFFImage} the image at the given index
   */
  getImage: function(index) {
    index = index || 0;
    var fileDirectoryAndGeoKey = this.fileDirectories[index];
    if (!fileDirectoryAndGeoKey) {
      throw new RangeError("Invalid image index");
    }
    return new GeoTIFFImage(fileDirectoryAndGeoKey[0], fileDirectoryAndGeoKey[1], this.dataView, this.littleEndian, this.cache);
  },

  /**
   * Returns the count of the internal subfiles.
   *
   * @returns {Number} the number of internal subfile images
   */
  getImageCount: function() {
    return this.fileDirectories.length;
  }
};

module.exports = GeoTIFF;