import { fieldTypes, fieldTagNames, arrayFields, geoKeyNames } from './globals';
import GeoTIFFImage from './geotiffimage';
import DataView64 from './dataview64';
import DataSlice from './dataslice';
import { makeRemoteSource, makeBufferSource, makeFileSource, makeFileReaderSource } from './source';
import Pool from './pool';
function getFieldTypeLength(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}`);
}
}
function parseGeoKeyDirectory(fileDirectory) {
const rawGeoKeyDirectory = fileDirectory.GeoKeyDirectory;
if (!rawGeoKeyDirectory) {
return null;
}
const geoKeyDirectory = {};
for (let i = 4; i <= rawGeoKeyDirectory[3] * 4; i += 4) {
const key = geoKeyNames[rawGeoKeyDirectory[i]];
const location = (rawGeoKeyDirectory[i + 1]) ?
(fieldTagNames[rawGeoKeyDirectory[i + 1]]) : null;
const count = rawGeoKeyDirectory[i + 2];
const offset = rawGeoKeyDirectory[i + 3];
let 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;
}
function getValues(dataSlice, fieldType, count, offset) {
let values = null;
let readMethod = null;
const fieldTypeLength = getFieldTypeLength(fieldType);
switch (fieldType) {
case fieldTypes.BYTE: case fieldTypes.ASCII: case fieldTypes.UNDEFINED:
values = new Uint8Array(count); readMethod = dataSlice.readUint8;
break;
case fieldTypes.SBYTE:
values = new Int8Array(count); readMethod = dataSlice.readInt8;
break;
case fieldTypes.SHORT:
values = new Uint16Array(count); readMethod = dataSlice.readUint16;
break;
case fieldTypes.SSHORT:
values = new Int16Array(count); readMethod = dataSlice.readInt16;
break;
case fieldTypes.LONG:
values = new Uint32Array(count); readMethod = dataSlice.readUint32;
break;
case fieldTypes.SLONG:
values = new Int32Array(count); readMethod = dataSlice.readInt32;
break;
case fieldTypes.LONG8: case fieldTypes.IFD8:
values = new Array(count); readMethod = dataSlice.readUint64;
break;
case fieldTypes.SLONG8:
values = new Array(count); readMethod = dataSlice.readInt64;
break;
case fieldTypes.RATIONAL:
values = new Uint32Array(count * 2); readMethod = dataSlice.readUint32;
break;
case fieldTypes.SRATIONAL:
values = new Int32Array(count * 2); readMethod = dataSlice.readInt32;
break;
case fieldTypes.FLOAT:
values = new Float32Array(count); readMethod = dataSlice.readFloat32;
break;
case fieldTypes.DOUBLE:
values = new Float64Array(count); readMethod = dataSlice.readFloat64;
break;
default:
throw new RangeError(`Invalid field type: ${fieldType}`);
}
// normal fields
if (!(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) {
for (let i = 0; i < count; ++i) {
values[i] = readMethod.call(
dataSlice, offset + (i * fieldTypeLength),
);
}
} else { // RATIONAL or SRATIONAL
for (let i = 0; i < count; i += 2) {
values[i] = readMethod.call(
dataSlice, offset + (i * fieldTypeLength),
);
values[i + 1] = readMethod.call(
dataSlice, offset + ((i * fieldTypeLength) + 4),
);
}
}
if (fieldType === fieldTypes.ASCII) {
return String.fromCharCode.apply(null, values);
}
return values;
}
class GeoTIFFBase {
/**
* (experimental) Reads raster data from the best fitting image. This function uses
* the image with the lowest resolution that is still a higher resolution than the
* requested resolution.
* When specified, the `bbox` option is translated to the `window` option and the
* `resX` and `resY` to `width` and `height` respectively.
* Then, the [readRasters]{@link GeoTIFFImage#readRasters} method of the selected
* image is called and the result returned.
* @see GeoTIFFImage.readRasters
* @param {Object} [options] optional parameters
* @param {Array} [options.window=whole image] the subset to read data from.
* @param {Array} [options.bbox=whole image] the subset to read data from in
* geographical coordinates.
* @param {Array} [options.samples=all samples] the selection of samples to read from.
* @param {Boolean} [options.interleave=false] whether the data shall be read
* in one single array or separate
* arrays.
* @param {Number} [pool=null] The optional decoder pool to use.
* @param {Number} [width] The desired width of the output. When the width is no the
* same as the images, resampling will be performed.
* @param {Number} [height] The desired height of the output. When the width is no the
* same as the images, resampling will be performed.
* @param {String} [resampleMethod='nearest'] The desired resampling method.
* @param {Number|Number[]} [fillValue] The value to use for parts of the image
* outside of the images extent. When multiple
* samples are requested, an array of fill values
* can be passed.
* @returns {Promise.<(TypedArray|TypedArray[])>} the decoded arrays as a promise
*/
async readRasters(options = {}) {
const { window: imageWindow, width, height } = options;
let { resX, resY, bbox } = options;
const firstImage = await this.getImage();
let usedImage = firstImage;
const imageCount = await this.getImageCount();
const imgBBox = firstImage.getBoundingBox();
if (imageWindow && bbox) {
throw new Error('Both "bbox" and "window" passed.');
}
// if width/height is passed, transform it to resolution
if (width || height) {
// if we have an image window (pixel coordinates), transform it to a BBox
// using the origin/resolution of the first image.
if (imageWindow) {
const [oX, oY] = firstImage.getOrigin();
const [rX, rY] = firstImage.getResolution();
bbox = [
oX + (imageWindow[0] * rX),
oY + (imageWindow[1] * rY),
oX + (imageWindow[2] * rX),
oY + (imageWindow[3] * rY),
];
}
// if we have a bbox (or calculated one)
const usedBBox = bbox || imgBBox;
if (width) {
if (resX) {
throw new Error('Both width and resX passed');
}
resX = (usedBBox[2] - usedBBox[0]) / width;
}
if (height) {
if (resY) {
throw new Error('Both width and resY passed');
}
resY = (usedBBox[3] - usedBBox[1]) / height;
}
}
// if resolution is set or calculated, try to get the image with the worst acceptable resolution
if (resX || resY) {
const allImages = [];
for (let i = 0; i < imageCount; ++i) {
const image = await this.getImage(i);
const { SubfileType: subfileType, NewSubfileType: newSubfileType } = image.fileDirectory;
if (i === 0 || subfileType === 2 || newSubfileType & 1) {
allImages.push(image);
}
}
allImages.sort((a, b) => a.getWidth() - b.getWidth());
for (let i = 0; i < allImages.length; ++i) {
const image = allImages[i];
const imgResX = (imgBBox[2] - imgBBox[0]) / image.getWidth();
const imgResY = (imgBBox[3] - imgBBox[1]) / image.getHeight();
usedImage = image;
if ((resX && resX > imgResX) || (resY && resY > imgResY)) {
break;
}
}
}
let wnd = imageWindow;
if (bbox) {
const [oX, oY] = firstImage.getOrigin();
const [imageResX, imageResY] = usedImage.getResolution(firstImage);
wnd = [
Math.round((bbox[0] - oX) / imageResX),
Math.round((bbox[1] - oY) / imageResY),
Math.round((bbox[2] - oX) / imageResX),
Math.round((bbox[3] - oY) / imageResY),
];
wnd = [
Math.min(wnd[0], wnd[2]),
Math.min(wnd[1], wnd[3]),
Math.max(wnd[0], wnd[2]),
Math.max(wnd[1], wnd[3]),
];
}
return usedImage.readRasters(Object.assign({}, options, {
window: wnd,
}));
}
}
/**
* The abstraction for a whole GeoTIFF file.
* @augments GeoTIFFBase
*/
class GeoTIFF extends GeoTIFFBase {
/**
* @constructor
* @param {Source} source The datasource to read from.
* @param {Boolean} littleEndian Whether the image uses little endian.
* @param {Boolean} bigTiff Whether the image uses bigTIFF conventions.
* @param {Number} firstIFDOffset The numeric byte-offset from the start of the image
* to the first IFD.
* @param {Object} [options] further options.
* @param {Boolean} [options.cache=false] whether or not decoded tiles shall be cached.
*/
constructor(source, littleEndian, bigTiff, firstIFDOffset, options = {}) {
super();
this.source = source;
this.littleEndian = littleEndian;
this.bigTiff = bigTiff;
this.firstIFDOffset = firstIFDOffset;
this.cache = options.cache || false;
this.fileDirectories = null;
this.fileDirectoriesParsing = null;
}
async getSlice(offset, size) {
const fallbackSize = this.bigTiff ? 4048 : 1024;
return new DataSlice(
await this.source.fetch(
offset, typeof size !== 'undefined' ? size : fallbackSize,
), offset, this.littleEndian, this.bigTiff,
);
}
async parseFileDirectories() {
let nextIFDByteOffset = this.firstIFDOffset;
const offsetSize = this.bigTiff ? 8 : 2;
const entrySize = this.bigTiff ? 20 : 12;
const fileDirectories = [];
while (nextIFDByteOffset !== 0x00000000) {
let dataSlice = await this.getSlice(nextIFDByteOffset);
const numDirEntries = this.bigTiff ?
dataSlice.readUint64(nextIFDByteOffset) :
dataSlice.readUint16(nextIFDByteOffset);
// if the slice does not cover the whole IFD, request a bigger slice, where the
// whole IFD fits: num of entries + n x tag length + offset to next IFD
const byteSize = (numDirEntries * entrySize) + (this.bigTiff ? 16 : 6);
if (!dataSlice.covers(nextIFDByteOffset, byteSize)) {
dataSlice = await this.getSlice(nextIFDByteOffset, byteSize);
}
const fileDirectory = {};
// loop over the IFD and create a file directory object
let i = nextIFDByteOffset + (this.bigTiff ? 8 : 2);
for (let entryCount = 0; entryCount < numDirEntries; i += entrySize, ++entryCount) {
const fieldTag = dataSlice.readUint16(i);
const fieldType = dataSlice.readUint16(i + 2);
const typeCount = this.bigTiff ?
dataSlice.readUint64(i + 4) :
dataSlice.readUint32(i + 4);
let fieldValues;
let value;
const fieldTypeLength = getFieldTypeLength(fieldType);
const valueOffset = i + (this.bigTiff ? 12 : 8);
// check whether the value is directly encoded in the tag or refers to a
// different external byte range
if (fieldTypeLength * typeCount <= (this.bigTiff ? 8 : 4)) {
fieldValues = getValues(dataSlice, fieldType, typeCount, valueOffset);
} else {
// resolve the reference to the actual byte range
const actualOffset = dataSlice.readOffset(valueOffset);
const length = getFieldTypeLength(fieldType) * typeCount;
// check, whether we actually cover the referenced byte range; if not,
// request a new slice of bytes to read from it
if (dataSlice.covers(actualOffset, length)) {
fieldValues = getValues(dataSlice, fieldType, typeCount, actualOffset);
} else {
const fieldDataSlice = await this.getSlice(actualOffset, length);
fieldValues = getValues(fieldDataSlice, fieldType, typeCount, actualOffset);
}
}
// unpack single values from the array
if (typeCount === 1 && arrayFields.indexOf(fieldTag) === -1 &&
!(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) {
value = fieldValues[0];
} else {
value = fieldValues;
}
// write the tags value to the file directly
fileDirectory[fieldTagNames[fieldTag]] = value;
}
fileDirectories.push([
fileDirectory, parseGeoKeyDirectory(fileDirectory),
]);
// continue with the next IFD
nextIFDByteOffset = dataSlice.readOffset(
nextIFDByteOffset + offsetSize + (entrySize * numDirEntries),
);
}
return fileDirectories;
}
/**
* Get the n-th internal subfile of 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
*/
async getImage(index = 0) {
if (!this.fileDirectories) {
if (!this.fileDirectoriesParsing) {
this.fileDirectoriesParsing = this.parseFileDirectories();
}
this.fileDirectories = await this.fileDirectoriesParsing;
}
const 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, this.source,
);
}
/**
* Returns the count of the internal subfiles.
*
* @returns {Number} the number of internal subfile images
*/
async getImageCount() {
if (!this.fileDirectories) {
if (!this.fileDirectoriesParsing) {
this.fileDirectoriesParsing = this.parseFileDirectories();
}
this.fileDirectories = await this.fileDirectoriesParsing;
}
return this.fileDirectories.length;
}
/**
* Parse a (Geo)TIFF file from the given source.
*
* @param {source~Source} source The source of data to parse from.
* @param {object} options Additional options.
*/
static async fromSource(source, options) {
const headerData = await source.fetch(0, 1024);
const dataView = new DataView64(headerData);
const BOM = dataView.getUint16(0, 0);
let littleEndian;
if (BOM === 0x4949) {
littleEndian = true;
} else if (BOM === 0x4D4D) {
littleEndian = false;
} else {
throw new TypeError('Invalid byte order value.');
}
const magicNumber = dataView.getUint16(2, littleEndian);
let bigTiff;
if (magicNumber === 42) {
bigTiff = false;
} else if (magicNumber === 43) {
bigTiff = true;
const offsetByteSize = dataView.getUint16(4, littleEndian);
if (offsetByteSize !== 8) {
throw new Error('Unsupported offset byte-size.');
}
} else {
throw new TypeError('Invalid magic number.');
}
const firstIFDOffset = bigTiff ?
dataView.getUint64(8, littleEndian) :
dataView.getUint32(4, littleEndian);
return new GeoTIFF(source, littleEndian, bigTiff, firstIFDOffset, options);
}
}
export { GeoTIFF };
export default GeoTIFF;
/**
* Wrapper for GeoTIFF files that have external overviews.
* @augments GeoTIFFBase
*/
class MultiGeoTIFF extends GeoTIFFBase {
/**
* Construct a new MultiGeoTIFF from a main and several overview files.
* @param {GeoTIFF} mainFile The main GeoTIFF file.
* @param {GeoTIFF[]} overviewFiles An array of overview files.
*/
constructor(mainFile, overviewFiles) {
super();
this.mainFile = mainFile;
this.overviewFiles = overviewFiles;
this.imageFiles = [mainFile].concat(overviewFiles);
this.fileDirectoriesPerFile = null;
this.fileDirectoriesPerFileParsing = null;
this.imageCount = null;
}
async parseFileDirectoriesPerFile() {
const requests = [this.mainFile.parseFileDirectories()]
.concat(this.overviewFiles.map(file => file.parseFileDirectories()));
this.fileDirectoriesPerFile = await Promise.all(requests);
return this.fileDirectoriesPerFile;
}
/**
* Get the n-th internal subfile of 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
*/
async getImage(index = 0) {
if (!this.fileDirectoriesPerFile) {
if (!this.fileDirectoriesPerFileParsing) {
this.fileDirectoriesPerFileParsing = this.parseFileDirectoriesPerFile();
}
this.fileDirectoriesPerFile = await this.fileDirectoriesPerFileParsing;
}
let relativeIndex = index;
for (let i = 0; i < this.fileDirectoriesPerFile.length; ++i) {
const fileDirectories = this.fileDirectoriesPerFile[i];
if (relativeIndex < fileDirectories.length) {
const file = this.imageFiles[i];
return new GeoTIFFImage(
fileDirectories[relativeIndex][0], fileDirectories[relativeIndex][1],
file.dataView, file.littleEndian, file.cache, file.source,
);
}
relativeIndex -= fileDirectories.length;
}
throw new RangeError('Invalid image index');
}
/**
* Returns the count of the internal subfiles.
*
* @returns {Number} the number of internal subfile images
*/
async getImageCount() {
if (!this.fileDirectoriesPerFile) {
if (!this.fileDirectoriesPerFileParsing) {
this.fileDirectoriesPerFileParsing = this.parseFileDirectoriesPerFile();
}
this.fileDirectoriesPerFile = await this.fileDirectoriesPerFileParsing;
}
return this.fileDirectoriesPerFile.reduce((count, ifds) => count + ifds.length, 0);
}
}
export { MultiGeoTIFF };
/**
* Creates a new GeoTIFF from a remote URL.
* @param {string} url The URL to access the image from
* @param {object} [options] Additional options to pass to the source.
* See {@link makeRemoteSource} for details.
* @returns {Promise.<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromUrl(url, options = {}) {
return GeoTIFF.fromSource(makeRemoteSource(url, options));
}
/**
* Construct a new GeoTIFF from an
* [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer}.
* @param {ArrayBuffer} arrayBuffer The data to read the file from.
* @returns {Promise.<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromArrayBuffer(arrayBuffer) {
return GeoTIFF.fromSource(makeBufferSource(arrayBuffer));
}
/**
* Construct a GeoTIFF from a local file path. This uses the node
* [filesystem API]{@link https://nodejs.org/api/fs.html} and is
* not available on browsers.
* @param {string} path The filepath to read from.
* @returns {Promise.<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromFile(path) {
return GeoTIFF.fromSource(makeFileSource(path));
}
/**
* Construct a GeoTIFF from an HTML
* [Blob]{@link https://developer.mozilla.org/en-US/docs/Web/API/Blob} or
* [File]{@link https://developer.mozilla.org/en-US/docs/Web/API/File}
* object.
* @param {Blob|File} blob The Blob or File object to read from.
* @returns {Promise.<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromBlob(blob) {
return GeoTIFF.fromSource(makeFileReaderSource(blob));
}
/**
* Construct a MultiGeoTIFF from the given URLs.
* @param {string} mainUrl The URL for the main file.
* @param {string[]} overviewUrls An array of URLs for the overview images.
* @param {object} [options] Additional options to pass to the source.
* See [makeRemoteSource]{@link module:source.makeRemoteSource}
* for details.
* @returns {Promise.<MultiGeoTIFF>} The resulting MultiGeoTIFF file.
*/
export async function fromUrls(mainUrl, overviewUrls = [], options = {}) {
const mainFile = await GeoTIFF.fromSource(makeRemoteSource(mainUrl, options));
const overviewFiles = await Promise.all(
overviewUrls.map(url => GeoTIFF.fromSource(makeRemoteSource(url, options))),
);
return new MultiGeoTIFF(mainFile, overviewFiles);
}
export { Pool };