Source: formats/geotiff/GeoTiffReader.js

/*
 * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
 * National Aeronautics and Space Administration. All rights reserved.
 *
 * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * @exports GeoTiffReader
 */
define([
        '../../error/AbstractError',
        '../../error/ArgumentError',
        './GeoTiffConstants',
        './GeoTiffKeyEntry',
        './GeoTiffMetadata',
        './GeoTiffUtil',
        '../../geom/Location',
        '../../geom/Sector',
        '../../util/Logger',
        '../../util/proj4-src',
        './TiffConstants',
        './TiffIFDEntry',
        '../../util/WWUtil'
    ],
    function (AbstractError,
              ArgumentError,
              GeoTiffConstants,
              GeoTiffKeyEntry,
              GeoTiffMetadata,
              GeoTiffUtil,
              Location,
              Sector,
              Logger,
              Proj4,
              TiffConstants,
              TiffIFDEntry,
              WWUtil) {
        "use strict";

        /**
         * Constructs a geotiff reader object for a specified geotiff URL.
         * Call [readAsImage]{@link GeoTiffReader#readAsImage} to retrieve the image as a canvas or
         * [readAsData]{@link GeoTiffReader#readAsData} to retrieve the elevations as an array of elevation values.
         * @alias GeoTiffReader
         * @constructor
         * @classdesc Parses a geotiff and creates an image or an elevation array representing its contents.
         * @param {String} ArrayBuffer The ArrayBuffer of the GeoTiff
         * @throws {ArgumentError} If the specified URL is null or undefined.
         */
        var GeoTiffReader = function (arrayBuffer) {
            if (!arrayBuffer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "constructor", "missingArrayBuffer"));
            }

            // Documented in defineProperties below.
            this._isLittleEndian = false;

            // Documented in defineProperties below.
            this._imageFileDirectories = [];

            // Documented in defineProperties below.
            this._geoTiffData = new DataView(arrayBuffer);

            // Documented in defineProperties below.
            this._metadata = new GeoTiffMetadata();

            this.parse();
        };

        Object.defineProperties(GeoTiffReader.prototype, {

            /**
             *Indicates whether the geotiff byte order is little endian..
             * @memberof GeoTiffReader.prototype
             * @type {Boolean}
             * @readonly
             */
            isLittleEndian: {
                get: function () {
                    return this._isLittleEndian;
                }
            },

            /**
             * An array containing all the image file directories of the geotiff file.
             * @memberof GeoTiffReader.prototype
             * @type {TiffIFDEntry[]}
             * @readonly
             */
            imageFileDirectories: {
                get: function () {
                    return this._imageFileDirectories;
                }
            },

            /**
             * The buffer descriptor of the geotiff file's content.
             * @memberof GeoTiffReader.prototype
             * @type {ArrayBuffer}
             * @readonly
             */
            geoTiffData: {
                get: function () {
                    return this._geoTiffData;
                }
            },

            /**
             * An objct containing all tiff and geotiff metadata of the geotiff file.
             * @memberof GeoTiffReader.prototype
             * @type {GeoTiffMetadata}
             * @readonly
             */
            metadata: {
                get: function () {
                    return this._metadata;
                }
            }
        });

        /**
         * Attempts to retrieve the GeoTiff data from the provided URL, parse the data and return a GeoTiffReader
         * using the provided parserCompletionCallback.
         *
         * @param url the URL source for the GeoTiff
         * @param parserCompletionCallback a callback wher
         */
        GeoTiffReader.retrieveFromUrl = function (url, parserCompletionCallback) {
            if (!url) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "retrieveFromUrl",
                        "missingUrl"));
            }

            if (!parserCompletionCallback) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "retrieveFromUrl",
                        "The specified callback is null or undefined."));
            }

            var xhr = new XMLHttpRequest();

            xhr.open("GET", url, true);
            xhr.responseType = 'arraybuffer';
            xhr.onreadystatechange = (function () {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        var arrayBuffer = xhr.response;
                        if (arrayBuffer) {
                            parserCompletionCallback(new GeoTiffReader(arrayBuffer), xhr);
                        }
                    }
                    else {
                        Logger.log(Logger.LEVEL_WARNING, "GeoTiff retrieval failed (" + xhr.statusText + "): " + url);
                        parserCompletionCallback(null, xhr);
                    }
                }
            }).bind(this);

            xhr.onerror = function () {
                Logger.log(Logger.LEVEL_WARNING, "GeoTiff retrieval failed: " + url);
                parserCompletionCallback(null, xhr);
            };

            xhr.ontimeout = function () {
                Logger.log(Logger.LEVEL_WARNING, "GeoTiff retrieval timed out: " + url);
                parserCompletionCallback(null, xhr);
            };

            xhr.send(null);
        };

        // Parse geotiff file. Internal use only
        GeoTiffReader.prototype.parse = function () {

            // check if it's been parsed before
            if (this._imageFileDirectories.length) {
                return;
            }

            this.getEndianness();

            if (!this.isTiffFileType()) {
                throw new AbstractError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "parse", "invalidTiffFileType"));
            }

            var firstIFDOffset = GeoTiffUtil.getBytes(this.geoTiffData, 4, 4, this.isLittleEndian);

            this.parseImageFileDirectory(firstIFDOffset);
            this.getMetadataFromImageFileDirectory();
            this.parseGeoKeys();
            this.setBBox();
        };

        // Get byte order of the geotiff file. Internal use only.
        GeoTiffReader.prototype.getEndianness = function () {
            var byteOrderValue = GeoTiffUtil.getBytes(this.geoTiffData, 0, 2, this.isLittleEndian);
            if (byteOrderValue === 0x4949) {
                this._isLittleEndian = true;
            }
            else if (byteOrderValue === 0x4D4D) {
                this._isLittleEndian = false;
            }
            else {
                throw new AbstractError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "getEndianness", "invalidByteOrderValue"));
            }
        };

        /**
         * Creates an RGB canvas from the GeoTiff image data.
         *
         * @return {Canvas}
         *
         */
        GeoTiffReader.prototype.getImage = function () {
            return this.createImage();
        };

        /**
         * Creates a typed array based on the contents of the GeoTiff.
         *
         * @return {TypedArray}
         */
        GeoTiffReader.prototype.getImageData = function () {
            return this.createTypedElevationArray();
        };

        /**
         * Indicates whether this geotiff is a tiff file type.
         *
         * @return {Boolean} True if this geotiff file is a tiff file type.
         */
        GeoTiffReader.prototype.isTiffFileType = function () {
            var fileTypeValue = GeoTiffUtil.getBytes(this.geoTiffData, 2, 2, this.isLittleEndian);
            if (fileTypeValue === 42) {
                return true;
            }
            else {
                return false;
            }
        };

        /**
         * Indicates whether this geotiff is a geotiff file type.
         *
         * @return {Boolean} True if this geotiff file is a geotiff file type.
         */
        GeoTiffReader.prototype.isGeoTiff = function () {
            if (this.getIFDByTag(GeoTiffConstants.Tag.GEO_KEY_DIRECTORY)) {
                return true;
            }
            else {
                return false;
            }
        };

        // Generate a canvas image. Internal use only.
        GeoTiffReader.prototype.createImage = function () {
            var bitsPerSample = this.metadata.bitsPerSample;
            var samplesPerPixel = this.metadata.samplesPerPixel;
            var photometricInterpretation = this.metadata.photometricInterpretation;
            var imageLength = this.metadata.imageLength;
            var imageWidth = this.metadata.imageWidth;

            if (this.metadata.colorMap) {
                var colorMapValues = this.metadata.colorMap;
                var colorMapSampleSize = Math.pow(2, bitsPerSample[0]);
            }

            var canvas = document.createElement('canvas');
            canvas.width = imageWidth;
            canvas.height = imageLength;
            var ctx = canvas.getContext("2d");

            if (this.metadata.stripOffsets) {
                var strips = this.parseStrips(false);
                if (this.metadata.rowsPerStrip) {
                    var rowsPerStrip = this.metadata.rowsPerStrip;
                } else {
                    var rowsPerStrip = imageLength;
                }
                var numOfStrips = strips.length;
                var numRowsInPreviousStrip = 0;
                var numRowsInStrip = rowsPerStrip;
                var imageLengthModRowsPerStrip = imageLength % rowsPerStrip;
                var rowsInLastStrip = (imageLengthModRowsPerStrip === 0) ? rowsPerStrip :
                    imageLengthModRowsPerStrip;

                for (var i = 0; i < numOfStrips; i++) {
                    if ((i + 1) === numOfStrips) {
                        numRowsInStrip = rowsInLastStrip;
                    }

                    var numOfPixels = strips[i].length;
                    var yPadding = numRowsInPreviousStrip * i;

                    for (var y = 0, j = 0; y < numRowsInStrip, j < numOfPixels; y++) {
                        for (var x = 0; x < imageWidth; x++, j++) {
                            var pixelSamples = strips[i][j];

                            ctx.fillStyle = this.getFillStyle(
                                pixelSamples,
                                photometricInterpretation,
                                bitsPerSample,
                                samplesPerPixel,
                                colorMapValues,
                                colorMapSampleSize
                            );
                            ctx.fillRect(x, yPadding + y, 1, 1);
                        }
                    }
                    numRowsInPreviousStrip = rowsPerStrip;
                }
            }
            else if (this.metadata.tileOffsets) {
                var tiles = this.parseTiles(false);
                var tileWidth = this.metadata.tileWidth;
                var tileLength = this.metadata.tileLength;
                var tilesAcross = Math.ceil(imageWidth / tileWidth);

                for (var y = 0; y < imageLength; y++) {
                    for (var x = 0; x < imageWidth; x++) {
                        var tileAcross = Math.floor(x / tileWidth);
                        var tileDown = Math.floor(y / tileLength);
                        var tileIndex = tileDown * tilesAcross + tileAcross;
                        var xInTile = x % tileWidth;
                        var yInTile = y % tileLength;
                        var sampleIndex = yInTile * tileWidth + xInTile;
                        var pixelSamples = tiles[tileIndex][sampleIndex];
                        ctx.fillStyle = this.getFillStyle(
                            pixelSamples,
                            photometricInterpretation,
                            bitsPerSample,
                            samplesPerPixel,
                            colorMapValues,
                            colorMapSampleSize
                        );
                        ctx.fillRect(x, y, 1, 1);
                    }
                }
            }

            this._geoTiffData = null;

            return canvas;
        };

        // Get pixel fill style. Internal use only.
        GeoTiffReader.prototype.getFillStyle = function (pixelSamples, photometricInterpretation, bitsPerSample,
                                                         samplesPerPixel, colorMapValues, colorMapSampleSize) {
            var red = 0.0;
            var green = 0.0;
            var blue = 0.0;
            var opacity = 1.0;

            if (this.metadata.noData && pixelSamples[0] == this.metadata.noData) {
                opacity = 0.0;
            }

            switch (photometricInterpretation) {
                case TiffConstants.PhotometricInterpretation.WHITE_IS_ZERO:
                    var invertValue = Math.pow(2, bitsPerSample) - 1;
                    pixelSamples[0] = invertValue - pixelSamples[0];
                case TiffConstants.PhotometricInterpretation.BLACK_IS_ZERO:
                    red = green = blue = GeoTiffUtil.clampColorSample(
                        pixelSamples[0],
                        bitsPerSample[0]);
                    break;
                case TiffConstants.PhotometricInterpretation.RGB:
                    red = GeoTiffUtil.clampColorSample(pixelSamples[0], bitsPerSample[0]);
                    green = GeoTiffUtil.clampColorSample(pixelSamples[1], bitsPerSample[1]);
                    blue = GeoTiffUtil.clampColorSample(pixelSamples[2], bitsPerSample[2]);

                    if (samplesPerPixel === 4 && this.metadata.extraSamples[0] === 2) {
                        var maxValue = Math.pow(2, bitsPerSample[3]);
                        opacity = pixelSamples[3] / maxValue;
                    }
                    break;
                case TiffConstants.PhotometricInterpretation.RGB_PALETTE:
                    if (colorMapValues) {
                        var colorMapIndex = pixelSamples[0];

                        red = GeoTiffUtil.clampColorSample(
                            colorMapValues[colorMapIndex],
                            16);
                        green = GeoTiffUtil.clampColorSample(
                            colorMapValues[colorMapSampleSize + colorMapIndex],
                            16);
                        blue = GeoTiffUtil.clampColorSample(
                            colorMapValues[(2 * colorMapSampleSize) + colorMapIndex],
                            16);
                    }
                    break;
                case TiffConstants.PhotometricInterpretation.TRANSPARENCY_MASK:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Photometric interpretation not yet implemented: " +
                        "TRANSPARENCY_MASK");
                    break;
                case TiffConstants.PhotometricInterpretation.CMYK:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Photometric interpretation not yet implemented: CMYK");
                    break;
                case TiffConstants.PhotometricInterpretation.Y_Cb_Cr:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Photometric interpretation not yet implemented: Y_Cb_Cr");
                    break;
                case TiffConstants.PhotometricInterpretation.CIE_LAB:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Photometric interpretation not yet implemented: CIE_LAB");
                    break;
                default:
                    //todo
                    Logger.log("Unknown photometric interpretation: " + photometricInterpretation);
                    break;
            }

            return GeoTiffUtil.getRGBAFillValue(red, green, blue, opacity);
        }

        GeoTiffReader.prototype.createTypedElevationArray = function () {
            var elevationArray = [], typedElevationArray;
            var bitsPerSample = this.metadata.bitsPerSample[0];

            if (this.metadata.stripOffsets) {
                var strips = this.parseStrips(true);

                for (var i = 0; i < strips.length; i++) {
                    elevationArray = elevationArray.concat(strips[i]);
                }
            }
            else if (this.metadata.tileOffsets) {
                var tiles = this.parseTiles(true);
                var imageWidth = this.metadata.imageWidth;
                var imageLength = this.metadata.imageLength;
                var tileWidth = this.metadata.tileWidth;
                var tileLength = this.metadata.tileLength;
                var tilesAcross = Math.ceil(imageWidth / tileWidth);

                for (var y = 0; y < imageLength; y++) {
                    for (var x = 0; x < imageWidth; x++) {
                        var tileAcross = Math.floor(x / tileWidth);
                        var tileDown = Math.floor(y / tileLength);
                        var tileIndex = tileDown * tilesAcross + tileAcross;
                        var xInTile = x % tileWidth;
                        var yInTile = y % tileLength;
                        var sampleIndex = yInTile * tileWidth + xInTile;
                        var pixelSamples = tiles[tileIndex][sampleIndex];
                        elevationArray.push(pixelSamples);
                    }
                }
            }

            if (this.metadata.sampleFormat) {
                var sampleFormat = this.metadata.sampleFormat[0];
            }
            else {
                var sampleFormat = TiffConstants.SampleFormat.UNSIGNED;
            }

            switch (bitsPerSample) {
                case 8:
                    if (sampleFormat === TiffConstants.SampleFormat.SIGNED) {
                        typedElevationArray = new Int8Array(elevationArray);
                    }
                    else {
                        typedElevationArray = new Uint8Array(elevationArray);
                    }
                    break
                case 16:
                    if (sampleFormat === TiffConstants.SampleFormat.SIGNED) {
                        typedElevationArray = new Int16Array(elevationArray);
                    }
                    else {
                        typedElevationArray = new Uint16Array(elevationArray);
                    }
                    break;
                case 32:
                    if (sampleFormat === TiffConstants.SampleFormat.SIGNED) {
                        typedElevationArray = new Int32Array(elevationArray);
                    }
                    else if (sampleFormat === TiffConstants.SampleFormat.IEEE_FLOAT) {
                        typedElevationArray = new Float32Array(elevationArray);
                    }
                    else {
                        typedElevationArray = new Uint32Array(elevationArray);
                    }
                    break;
                case 64:
                    typedElevationArray = new Float64Array(elevationArray);
                    break;
                default:
                    break;
            }

            return typedElevationArray;
        }

        // Parse geotiff strips. Internal use only
        GeoTiffReader.prototype.parseStrips = function (returnElevation) {
            var samplesPerPixel = this.metadata.samplesPerPixel;
            var bitsPerSample = this.metadata.bitsPerSample;
            var stripOffsets = this.metadata.stripOffsets;
            var stripByteCounts = this.metadata.stripByteCounts;
            var compression = this.metadata.compression;
            if (this.metadata.sampleFormat) {
                var sampleFormat = this.metadata.sampleFormat;
            }
            else {
                var sampleFormat = TiffConstants.SampleFormat.UNSIGNED;
            }

            var bitsPerPixel = samplesPerPixel * bitsPerSample[0];
            var bytesPerPixel = bitsPerPixel / 8;

            var strips = [];
            // Loop through strips
            for (var i = 0; i < stripOffsets.length; i++) {
                var stripOffset = stripOffsets[i];
                var stripByteCount = stripByteCounts[i];

                strips[i] = this.parseBlock(returnElevation, compression, bytesPerPixel, stripByteCount, stripOffset,
                    bitsPerSample, sampleFormat);
            }

            return strips;
        }

        // Parse geotiff block. A block may be a strip or a tile. Internal use only.
        GeoTiffReader.prototype.parseBlock = function (returnElevation, compression, bytesPerPixel, blockByteCount,
                                                       blockOffset, bitsPerSample, sampleFormat) {
            var block = [];
            switch (compression) {
                case TiffConstants.Compression.UNCOMPRESSED:
                    // Loop through pixels.
                    for (var byteOffset = 0, increment = bytesPerPixel;
                         byteOffset < blockByteCount; byteOffset += increment) {
                        // Loop through samples (sub-pixels).
                        for (var m = 0, pixel = []; m < bitsPerSample.length; m++) {
                            var bytesPerSample = bitsPerSample[m] / 8;
                            var sampleOffset = m * bytesPerSample;

                            pixel.push(GeoTiffUtil.getSampleBytes(
                                this.geoTiffData,
                                blockOffset + byteOffset + sampleOffset,
                                bytesPerSample,
                                sampleFormat[m],
                                this.isLittleEndian));
                        }
                        if (returnElevation) {
                            block.push(pixel[0]);
                        }
                        else {
                            block.push(pixel);
                        }
                    }
                    break;
                case TiffConstants.Compression.CCITT_1D:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Compression type not yet implemented: CCITT_1D");
                    break;
                case TiffConstants.Compression.GROUP_3_FAX:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Compression type not yet implemented: GROUP_3_FAX");
                    break;
                case TiffConstants.Compression.GROUP_4_FAX:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Compression type not yet implemented: GROUP_4_FAX");
                    break;
                case TiffConstants.Compression.LZW:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Compression type not yet implemented: LZW");
                    break;
                case TiffConstants.Compression.JPEG:
                    //todo
                    Logger.log(Logger.LEVEL_WARNING, "Compression type not yet implemented: JPEG");
                    break;
                case TiffConstants.Compression.PACK_BITS:
                    if (this.metadata.tileOffsets) {
                        var tileWidth = this.metadata.tileWidth;
                        var tileLength = this.metadata.tileWidth;
                        var arrayBuffer = new ArrayBuffer(tileWidth * tileLength * bytesPerPixel);
                    }
                    else {
                        var rowsPerStrip = this.metadata.rowsPerStrip;
                        var imageWidth = this.metadata.imageWidth;
                        var arrayBuffer = new ArrayBuffer(rowsPerStrip * imageWidth * bytesPerPixel);
                    }

                    var uncompressedDataView = new DataView(arrayBuffer);

                    var newBlock = true;
                    var pixel = [];
                    var blockLength = 0;
                    var numOfIterations = 0;
                    var uncompressedOffset = 0;


                    for (var byteOffset = 0; byteOffset < blockByteCount; byteOffset += 1) {

                        if (newBlock) {
                            blockLength = 1;
                            numOfIterations = 1;
                            newBlock = false;

                            var nextSourceByte = this.geoTiffData.getInt8(blockOffset + byteOffset,
                                this.isLittleEndian);

                            if (nextSourceByte >= 0 && nextSourceByte <= 127) {
                                blockLength = nextSourceByte + 1;
                            }
                            else if (nextSourceByte >= -127 && nextSourceByte <= -1) {
                                numOfIterations = -nextSourceByte + 1;
                            }
                            else {
                                newBlock = true;
                            }
                        }
                        else {
                            var currentByte = GeoTiffUtil.getBytes(
                                this.geoTiffData,
                                blockOffset + byteOffset,
                                1,
                                this.isLittleEndian);

                            for (var currentIteration = 0; currentIteration < numOfIterations; currentIteration++) {
                                uncompressedDataView.setInt8(uncompressedOffset, currentByte);
                                uncompressedOffset++;
                            }

                            blockLength--;

                            if (blockLength === 0) {
                                newBlock = true;
                            }
                        }
                    }

                    for (var byteOffset = 0, increment = bytesPerPixel;
                         byteOffset < arrayBuffer.byteLength; byteOffset += increment) {
                        // Loop through samples (sub-pixels).
                        for (var m = 0, pixel = []; m < bitsPerSample.length; m++) {
                            var bytesPerSample = bitsPerSample[m] / 8;
                            var sampleOffset = m * bytesPerSample;

                            pixel.push(GeoTiffUtil.getSampleBytes(
                                uncompressedDataView,
                                byteOffset + sampleOffset,
                                bytesPerSample,
                                sampleFormat[m],
                                this.isLittleEndian));
                        }
                        if (returnElevation) {
                            block.push(pixel[0]);
                        }
                        else {
                            block.push(pixel);
                        }
                    }
                    break;
            }

            return block;
        }

        // Parse geotiff tiles. Internal use only
        GeoTiffReader.prototype.parseTiles = function (returnElevation) {
            var samplesPerPixel = this.metadata.samplesPerPixel;
            var bitsPerSample = this.metadata.bitsPerSample;
            var compression = this.metadata.compression;
            if (this.metadata.sampleFormat) {
                var sampleFormat = this.metadata.sampleFormat;
            }
            else {
                var sampleFormat = new Array(samplesPerPixel);
                WWUtil.fillArray(sampleFormat, TiffConstants.SampleFormat.UNSIGNED);
            }
            var bitsPerPixel = samplesPerPixel * bitsPerSample[0];
            var bytesPerPixel = bitsPerPixel / 8;
            var tileWidth = this.metadata.tileWidth;
            var tileLength = this.metadata.tileLength;
            var tileOffsets = this.metadata.tileOffsets;
            var tileByteCounts = this.metadata.tileByteCounts;
            var imageLength = this.metadata.imageLength;
            var imageWidth = this.metadata.imageWidth;

            var tilesAcross = Math.ceil(imageWidth / tileWidth);
            var tilesDown = Math.ceil(imageLength / tileLength);

            var tiles = [];

            for (var i = 0; i < tilesDown; i++) {
                for (var j = 0; j < tilesAcross; j++) {
                    var index = tilesAcross * i + j;
                    var tileOffset = tileOffsets[index];
                    var tileByteCount = tileByteCounts[index];
                    tiles[index] = this.parseBlock(returnElevation, compression, bytesPerPixel, tileByteCount,
                        tileOffset, bitsPerSample, sampleFormat);
                }
            }

            return tiles;
        }

        // Translate a pixel/line coordinates to projection coordinate. Internal use only.
        GeoTiffReader.prototype.geoTiffImageToPCS = function (xValue, yValue) {
            if (xValue === null || xValue === undefined) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "geoTiffImageToPCS", "missingXValue"));
            }
            if (yValue === null || yValue === undefined) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "geoTiffImageToPCS", "missingYValue"));
            }

            var res = [xValue, yValue];

            var tiePointValues = this.metadata.modelTiepoint;
            var modelPixelScaleValues = this.metadata.modelPixelScale;
            var modelTransformationValues = this.metadata.modelTransformation;

            var tiePointCount = tiePointValues ? tiePointValues.length : 0;
            var modelPixelScaleCount = modelPixelScaleValues ? modelPixelScaleValues.length : 0;
            var modelTransformationCount = modelTransformationValues ? modelTransformationValues.length : 0;

            if (tiePointCount > 6 && modelPixelScaleCount === 0) {
                //todo
            }
            else if (modelTransformationCount === 16) {
                var x_in = xValue;
                var y_in = yValue;

                xValue = x_in * modelTransformationValues[0] + y_in * modelTransformationValues[1] +
                    modelTransformationValues[3];
                yValue = x_in * modelTransformationValues[4] + y_in * modelTransformationValues[5] +
                    modelTransformationValues[7];

                res = [xValue, yValue];
            }
            else if (modelPixelScaleCount < 3 || tiePointCount < 6) {
                res = [xValue, yValue];
            }
            else {
                xValue = (xValue - tiePointValues[0]) * modelPixelScaleValues[0] + tiePointValues[3];
                yValue = (yValue - tiePointValues[1]) * (-1 * modelPixelScaleValues[1]) + tiePointValues[4];

                res = [xValue, yValue];
            }

            Proj4.defs([
                [
                    'EPSG:26771',
                    '+proj=tmerc +lat_0=36.66666666666666 +lon_0=-88.33333333333333 +k=0.9999749999999999 +' +
                    'x_0=152400.3048006096 +y_0=0 +ellps=clrk66 +datum=NAD27 +to_meter=0.3048006096012192 +no_defs '
                ],
                [
                    'EPSG:32633',
                    '+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs'
                ]
            ]);

            if (this.metadata.projectedCSType) {
                res = Proj4('EPSG:' + this.metadata.projectedCSType, 'EPSG:4326', res);
            }

            return new Location(res[1], res[0]);
        };

        /**
         * Set the bounding box of the geotiff file. Internal use only.
         */
        GeoTiffReader.prototype.setBBox = function () {
            var upperLeft = this.geoTiffImageToPCS(0, 0);
            var upperRight = this.geoTiffImageToPCS(this.metadata.imageWidth, 0);
            var lowerLeft = this.geoTiffImageToPCS(0, this.metadata.imageLength);
            var lowerRight = this.geoTiffImageToPCS(
                this.metadata.imageWidth, this.metadata.imageLength);

            this.metadata.bbox = new Sector(
                lowerLeft.latitude,
                upperLeft.latitude,
                upperLeft.longitude,
                upperRight.longitude
            );
        }

        // Get metadata from image file directory. Internal use only.
        GeoTiffReader.prototype.getMetadataFromImageFileDirectory = function () {
            for (var i = 0; i < this.imageFileDirectories[0].length; i++) {

                switch (this.imageFileDirectories[0][i].tag) {
                    case TiffConstants.Tag.BITS_PER_SAMPLE:
                        this.metadata.bitsPerSample = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.COLOR_MAP:
                        this.metadata.colorMap = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.COMPRESSION:
                        this.metadata.compression = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.EXTRA_SAMPLES:
                        this.metadata.extraSamples = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.IMAGE_DESCRIPTION:
                        this.metadata.imageDescription = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.IMAGE_LENGTH:
                        this.metadata.imageLength = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.IMAGE_WIDTH:
                        this.metadata.imageWidth = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.MAX_SAMPLE_VALUE:
                        this.metadata.maxSampleValue = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.MIN_SAMPLE_VALUE:
                        this.metadata.minSampleValue = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.ORIENTATION:
                        this.metadata.orientation = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.PHOTOMETRIC_INTERPRETATION:
                        this.metadata.photometricInterpretation = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.PLANAR_CONFIGURATION:
                        this.metadata.planarConfiguration = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.ROWS_PER_STRIP:
                        this.metadata.rowsPerStrip = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.RESOLUTION_UNIT:
                        this.metadata.resolutionUnit = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.SAMPLES_PER_PIXEL:
                        this.metadata.samplesPerPixel = this.imageFileDirectories[0][i].getIFDEntryValue()[0];
                        break;
                    case TiffConstants.Tag.SAMPLE_FORMAT:
                        this.metadata.sampleFormat = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.SOFTWARE:
                        this.metadata.software = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.STRIP_BYTE_COUNTS:
                        this.metadata.stripByteCounts = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.STRIP_OFFSETS:
                        this.metadata.stripOffsets = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.TILE_BYTE_COUNTS:
                        this.metadata.tileByteCounts = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.TILE_OFFSETS:
                        this.metadata.tileOffsets = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.TILE_LENGTH:
                        this.metadata.tileLength = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.TILE_WIDTH:
                        this.metadata.tileWidth = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.X_RESOLUTION:
                        this.metadata.xResolution = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case TiffConstants.Tag.Y_RESOLUTION:
                        this.metadata.tileWidth = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;

                    //geotiff
                    case GeoTiffConstants.Tag.GEO_ASCII_PARAMS:
                        this.metadata.geoAsciiParams = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case GeoTiffConstants.Tag.GEO_DOUBLE_PARAMS:
                        this.metadata.geoDubleParams = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case GeoTiffConstants.Tag.GEO_KEY_DIRECTORY:
                        this.metadata.geoKeyDirectory = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case GeoTiffConstants.Tag.MODEL_PIXEL_SCALE:
                        this.metadata.modelPixelScale = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case GeoTiffConstants.Tag.MODEL_TIEPOINT:
                        this.metadata.modelTiepoint = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case GeoTiffConstants.Tag.GDAL_METADATA:
                        this.metadata.metaData = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    case GeoTiffConstants.Tag.GDAL_NODATA:
                        this.metadata.noData = this.imageFileDirectories[0][i].getIFDEntryValue();
                        break;
                    default:
                        Logger.log(Logger.LEVEL_WARNING, "Ignored GeoTiff tag: " + this.imageFileDirectories[0][i].tag);
                }
            }
        }

        // Parse GeoKeys. Internal use only.
        GeoTiffReader.prototype.parseGeoKeys = function () {
            if (!this.isGeoTiff()) {
                throw new AbstractError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "parse", "invalidGeoTiffFile"));
            }

            var geoKeyDirectory = this.metadata.geoKeyDirectory;
            if (geoKeyDirectory) {
                var keyDirectoryVersion = geoKeyDirectory[0];
                var keyRevision = geoKeyDirectory[1];
                var minorRevision = geoKeyDirectory[2];
                var numberOfKeys = geoKeyDirectory[3];

                for (var i = 0; i < numberOfKeys; i++) {
                    var keyId = geoKeyDirectory[4 + i * 4];
                    var tiffTagLocation = geoKeyDirectory[5 + i * 4];
                    var count = geoKeyDirectory[6 + i * 4];
                    var valueOffset = geoKeyDirectory[7 + i * 4];

                    switch (keyId) {
                        case GeoTiffConstants.Key.GTModelTypeGeoKey:
                            this.metadata.gtModelTypeGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.GTRasterTypeGeoKey:
                            this.metadata.gtRasterTypeGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.GTCitationGeoKey:
                            this.metadata.gtCitationGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.GeographicTypeGeoKey:
                            this.metadata.geographicTypeGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.GeogCitationGeoKey:
                            this.metadata.geogCitationGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.GeogAngularUnitsGeoKey:
                            this.metadata.geogAngularUnitsGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.GeogAngularUnitSizeGeoKey:
                            this.metadata.geogAngularUnitSizeGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.GeogSemiMajorAxisGeoKey:
                            this.metadata.geogSemiMajorAxisGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.GeogInvFlatteningGeoKey:
                            this.metadata.geogInvFlatteningGeoKey =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.ProjectedCSTypeGeoKey:
                            this.metadata.projectedCSType =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        case GeoTiffConstants.Key.ProjLinearUnitsGeoKey:
                            this.metadata.projLinearUnits =
                                new GeoTiffKeyEntry(keyId, tiffTagLocation, count, valueOffset).getGeoKeyValue(
                                    this.metadata.geoDoubleParams,
                                    this.metadata.geoAsciiParams);
                            break;
                        default:
                            Logger.log(Logger.LEVEL_WARNING, "Ignored GeoTiff key: " + keyId);
                            break;

                    }
                }
            }
            else {
                throw new AbstractError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "parseGeoKeys",
                        "missingGeoKeyDirectoryTag"));
            }
        };

        // Parse image file directory. Internal use only.
        GeoTiffReader.prototype.parseImageFileDirectory = function (offset) {
            if (!offset) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "parseImageFileDirectory",
                        "missingOffset"));
            }

            var noOfDirectoryEntries = GeoTiffUtil.getBytes(this.geoTiffData, offset, 2, this.isLittleEndian);

            var directoryEntries = [];

            for (var i = offset + 2, directoryEntryCounter = 0; directoryEntryCounter < noOfDirectoryEntries;
                 i += 12, directoryEntryCounter++) {
                var tag = GeoTiffUtil.getBytes(this.geoTiffData, i, 2, this.isLittleEndian);
                var type = GeoTiffUtil.getBytes(this.geoTiffData, i + 2, 2, this.isLittleEndian);
                var count = GeoTiffUtil.getBytes(this.geoTiffData, i + 4, 4, this.isLittleEndian);
                var valueOffset = GeoTiffUtil.getBytes(this.geoTiffData, i + 8, 4, this.isLittleEndian);

                directoryEntries.push(new TiffIFDEntry(
                    tag,
                    type,
                    count,
                    valueOffset,
                    this.geoTiffData,
                    this.isLittleEndian));
            }

            this._imageFileDirectories.push(directoryEntries);

            var nextIFDOffset = GeoTiffUtil.getBytes(this.geoTiffData, i, 4, this.isLittleEndian);

            if (nextIFDOffset === 0) {
                return;
            }
            else {
                this.parseImageFileDirectory(nextIFDOffset);
            }
        };

        // Get image file directory by tag value. Internal use only.
        GeoTiffReader.prototype.getIFDByTag = function (tag) {
            if (!tag) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoTiffReader", "getIFDByTag", "missingTag"));
            }

            for (var i = 0; i < this.imageFileDirectories[0].length; i++) {
                if (this.imageFileDirectories[0][i].tag === tag) {
                    return this.imageFileDirectories[0][i];
                }
            }

            return null;
        };

        return GeoTiffReader;
    }
);