geotiff
Version:
GeoTIFF image decoding in JavaScript
936 lines (767 loc) • 30.2 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray2 = require('babel-runtime/helpers/slicedToArray');
var _slicedToArray3 = _interopRequireDefault(_slicedToArray2);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _regenerator = require('babel-runtime/regenerator');
var _regenerator2 = _interopRequireDefault(_regenerator);
var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);
/*
* Promisified wrapper around 'setTimeout' to allow 'await'
*/
var wait = function () {
var _ref = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee(milliseconds) {
return _regenerator2.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
return _context.abrupt('return', new Promise(function (resolve) {
return setTimeout(resolve, milliseconds);
}));
case 1:
case 'end':
return _context.stop();
}
}
}, _callee, this);
}));
return function wait(_x) {
return _ref.apply(this, arguments);
};
}();
/**
* BlockedSource - an abstraction of (remote) files.
* @implements Source
*/
exports.makeFetchSource = makeFetchSource;
exports.makeXHRSource = makeXHRSource;
exports.makeHttpSource = makeHttpSource;
exports.makeRemoteSource = makeRemoteSource;
exports.makeBufferSource = makeBufferSource;
exports.makeFileSource = makeFileSource;
exports.makeFileReaderSource = makeFileReaderSource;
var _buffer = require('buffer');
var _fs = require('fs');
var _http = require('http');
var _http2 = _interopRequireDefault(_http);
var _https = require('https');
var _https2 = _interopRequireDefault(_https);
var _url = require('url');
var _url2 = _interopRequireDefault(_url);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function readRangeFromBlocks(blocks, rangeOffset, rangeLength) {
var rangeTop = rangeOffset + rangeLength;
var rangeData = new ArrayBuffer(rangeLength);
var rangeView = new Uint8Array(rangeData);
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = blocks[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var block = _step.value;
var delta = block.offset - rangeOffset;
var topDelta = block.top - rangeTop;
var blockInnerOffset = 0;
var rangeInnerOffset = 0;
var usedBlockLength = void 0;
if (delta < 0) {
blockInnerOffset = -delta;
} else if (delta > 0) {
rangeInnerOffset = delta;
}
if (topDelta < 0) {
usedBlockLength = block.length - blockInnerOffset;
} else if (topDelta > 0) {
usedBlockLength = rangeTop - block.offset - blockInnerOffset;
}
var blockView = new Uint8Array(block.data, blockInnerOffset, usedBlockLength);
rangeView.set(blockView, rangeInnerOffset);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
return rangeData;
}
/**
* Interface for Source objects.
* @interface Source
*/
/**
* @function Source#fetch
* @summary The main method to retrieve the data from the source.
* @param {number} offset The offset to read from in the source
* @param {number} length The requested number of bytes
*/
/**
* @typedef {object} Block
* @property {ArrayBuffer} data The actual data of the block.
* @property {number} offset The actual offset of the block within the file.
* @property {number} length The actual size of the block in bytes.
*/
/**
* Callback type for sources to request patches of data.
* @callback requestCallback
* @async
* @param {number} offset The offset within the file.
* @param {number} length The desired length of data to be read.
* @returns {Promise<Block>} The block of data.
*/
/**
* @module source
*/
/*
* Split a list of identifiers to form groups of coherent ones
*/
function getCoherentBlockGroups(blockIds) {
if (blockIds.length === 0) {
return [];
}
var groups = [];
var current = [];
groups.push(current);
for (var i = 0; i < blockIds.length; ++i) {
if (i === 0 || blockIds[i] === blockIds[i - 1] + 1) {
current.push(blockIds[i]);
} else {
current = [blockIds[i]];
groups.push(current);
}
}
return groups;
}
var BlockedSource = function () {
/**
* @param {requestCallback} retrievalFunction Callback function to request data
* @param {object} options Additional options
* @param {object} options.blockSize Size of blocks to be fetched
*/
function BlockedSource(retrievalFunction) {
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref2$blockSize = _ref2.blockSize,
blockSize = _ref2$blockSize === undefined ? 65535 : _ref2$blockSize;
(0, _classCallCheck3.default)(this, BlockedSource);
this.retrievalFunction = retrievalFunction;
this.blockSize = blockSize;
// currently running block requests
this.blockRequests = new Map();
// already retrieved blocks
this.blocks = new Map();
// block ids waiting for a batched request. Either a Set or null
this.blockIdsAwaitingRequest = null;
}
/**
* Fetch a subset of the file.
* @param {number} offset The offset within the file to read from.
* @param {number} length The length in bytes to read from.
* @returns {ArrayBuffer} The subset of the file.
*/
(0, _createClass3.default)(BlockedSource, [{
key: 'fetch',
value: function () {
var _ref3 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee3(offset, length) {
var _this = this;
var immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
var top, firstBlockOffset, allBlockIds, missingBlockIds, blockRequests, current, blockId, i, id, groups, _loop, _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _iterator2, _step2, group, missingRequests, _iteratorNormalCompletion3, _didIteratorError3, _iteratorError3, _iterator3, _step3, _blockId, blocks;
return _regenerator2.default.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
top = offset + length;
// calculate what blocks intersect the specified range (offset + length)
// determine what blocks are already stored or beeing requested
firstBlockOffset = Math.floor(offset / this.blockSize) * this.blockSize;
allBlockIds = [];
missingBlockIds = [];
blockRequests = [];
for (current = firstBlockOffset; current < top; current += this.blockSize) {
blockId = Math.floor(current / this.blockSize);
if (!this.blocks.has(blockId) && !this.blockRequests.has(blockId)) {
missingBlockIds.push(blockId);
}
if (this.blockRequests.has(blockId)) {
blockRequests.push(this.blockRequests.get(blockId));
}
allBlockIds.push(blockId);
}
// determine whether there are already blocks in the queue to be requested
// if so, add the missing blocks to this list
if (!this.blockIdsAwaitingRequest) {
this.blockIdsAwaitingRequest = new Set(missingBlockIds);
} else {
for (i = 0; i < missingBlockIds.length; ++i) {
id = missingBlockIds[i];
this.blockIdsAwaitingRequest.add(id);
}
}
// in immediate mode, we don't want to wait for possible additional requests coming in
if (immediate) {
_context3.next = 10;
break;
}
_context3.next = 10;
return wait();
case 10:
if (!this.blockIdsAwaitingRequest) {
_context3.next = 33;
break;
}
// get all coherent blocks as groups to be requested in a single request
groups = getCoherentBlockGroups(Array.from(this.blockIdsAwaitingRequest).sort());
// iterate over all blocks
_loop = function _loop(group) {
// fetch a group as in a single request
var request = _this.requestData(group[0] * _this.blockSize, group.length * _this.blockSize);
// for each block in the request, make a small 'splitter',
// i.e: wait for the request to finish, then cut out the bytes for
// that block and store it there.
// we keep that as a promise in 'blockRequests' to allow waiting on
// a single block.
var _loop2 = function _loop2(_i) {
var id = group[_i];
_this.blockRequests.set(id, (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee2() {
var response, o, t, data;
return _regenerator2.default.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
_context2.next = 2;
return request;
case 2:
response = _context2.sent;
o = _i * _this.blockSize;
t = Math.min(o + _this.blockSize, response.data.byteLength);
data = response.data.slice(o, t);
_this.blockRequests.delete(id);
_this.blocks.set(id, {
data: data,
offset: response.offset + o,
length: data.byteLength,
top: response.offset + t
});
case 8:
case 'end':
return _context2.stop();
}
}
}, _callee2, _this);
}))());
};
for (var _i = 0; _i < group.length; ++_i) {
_loop2(_i);
}
};
_iteratorNormalCompletion2 = true;
_didIteratorError2 = false;
_iteratorError2 = undefined;
_context3.prev = 16;
for (_iterator2 = groups[Symbol.iterator](); !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
group = _step2.value;
_loop(group);
}
_context3.next = 24;
break;
case 20:
_context3.prev = 20;
_context3.t0 = _context3['catch'](16);
_didIteratorError2 = true;
_iteratorError2 = _context3.t0;
case 24:
_context3.prev = 24;
_context3.prev = 25;
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
case 27:
_context3.prev = 27;
if (!_didIteratorError2) {
_context3.next = 30;
break;
}
throw _iteratorError2;
case 30:
return _context3.finish(27);
case 31:
return _context3.finish(24);
case 32:
this.blockIdsAwaitingRequest = null;
case 33:
// get a list of currently running requests for the blocks still missing
missingRequests = [];
_iteratorNormalCompletion3 = true;
_didIteratorError3 = false;
_iteratorError3 = undefined;
_context3.prev = 37;
for (_iterator3 = missingBlockIds[Symbol.iterator](); !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
_blockId = _step3.value;
if (this.blockRequests.has(_blockId)) {
missingRequests.push(this.blockRequests.get(_blockId));
}
}
// wait for all missing requests to finish
_context3.next = 45;
break;
case 41:
_context3.prev = 41;
_context3.t1 = _context3['catch'](37);
_didIteratorError3 = true;
_iteratorError3 = _context3.t1;
case 45:
_context3.prev = 45;
_context3.prev = 46;
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
case 48:
_context3.prev = 48;
if (!_didIteratorError3) {
_context3.next = 51;
break;
}
throw _iteratorError3;
case 51:
return _context3.finish(48);
case 52:
return _context3.finish(45);
case 53:
_context3.next = 55;
return Promise.all(missingRequests);
case 55:
_context3.next = 57;
return Promise.all(blockRequests);
case 57:
// now get all blocks for the request and return a summary buffer
blocks = allBlockIds.map(function (id) {
return _this.blocks.get(id);
});
return _context3.abrupt('return', readRangeFromBlocks(blocks, offset, length));
case 59:
case 'end':
return _context3.stop();
}
}
}, _callee3, this, [[16, 20, 24, 32], [25,, 27, 31], [37, 41, 45, 53], [46,, 48, 52]]);
}));
function fetch(_x4, _x5) {
return _ref3.apply(this, arguments);
}
return fetch;
}()
}, {
key: 'requestData',
value: function () {
var _ref5 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee4(requestedOffset, requestedLength) {
var response;
return _regenerator2.default.wrap(function _callee4$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
_context4.next = 2;
return this.retrievalFunction(requestedOffset, requestedLength);
case 2:
response = _context4.sent;
if (!response.length) {
response.length = response.data.byteLength;
} else if (response.length !== response.data.byteLength) {
response.data = response.data.slice(0, response.length);
}
response.top = response.offset + response.length;
return _context4.abrupt('return', response);
case 6:
case 'end':
return _context4.stop();
}
}
}, _callee4, this);
}));
function requestData(_x6, _x7) {
return _ref5.apply(this, arguments);
}
return requestData;
}()
}]);
return BlockedSource;
}();
/**
* Create a new source to read from a remote file using the
* [fetch]{@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} API.
* @param {string} url The URL to send requests to.
* @param {Object} [options] Additional options.
* @param {Number} [options.blockSize] The block size to use.
* @param {object} [options.headers] Additional headers to be sent to the server.
* @returns The constructed source
*/
function makeFetchSource(url) {
var _this2 = this;
var _ref6 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref6$headers = _ref6.headers,
headers = _ref6$headers === undefined ? {} : _ref6$headers,
blockSize = _ref6.blockSize;
return new BlockedSource(function () {
var _ref7 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee5(offset, length) {
var response, data, _data;
return _regenerator2.default.wrap(function _callee5$(_context5) {
while (1) {
switch (_context5.prev = _context5.next) {
case 0:
_context5.next = 2;
return fetch(url, {
headers: Object.assign({}, headers, {
Range: 'bytes=' + offset + '-' + (offset + length)
})
});
case 2:
response = _context5.sent;
if (response.ok) {
_context5.next = 7;
break;
}
throw new Error('Error fetching data.');
case 7:
if (!(response.status === 206)) {
_context5.next = 21;
break;
}
if (!response.arrayBuffer) {
_context5.next = 14;
break;
}
_context5.next = 11;
return response.arrayBuffer();
case 11:
_context5.t0 = _context5.sent;
_context5.next = 17;
break;
case 14:
_context5.next = 16;
return response.buffer();
case 16:
_context5.t0 = _context5.sent.buffer;
case 17:
data = _context5.t0;
return _context5.abrupt('return', {
data: data,
offset: offset,
length: length
});
case 21:
if (!response.arrayBuffer) {
_context5.next = 27;
break;
}
_context5.next = 24;
return response.arrayBuffer();
case 24:
_context5.t1 = _context5.sent;
_context5.next = 30;
break;
case 27:
_context5.next = 29;
return response.buffer();
case 29:
_context5.t1 = _context5.sent.buffer;
case 30:
_data = _context5.t1;
return _context5.abrupt('return', {
data: _data,
offset: 0,
length: _data.byteLength
});
case 32:
case 'end':
return _context5.stop();
}
}
}, _callee5, _this2);
}));
return function (_x9, _x10) {
return _ref7.apply(this, arguments);
};
}(), { blockSize: blockSize });
}
/**
* Create a new source to read from a remote file using the
* [XHR]{@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest} API.
* @param {string} url The URL to send requests to.
* @param {Object} [options] Additional options.
* @param {Number} [options.blockSize] The block size to use.
* @param {object} [options.headers] Additional headers to be sent to the server.
* @returns The constructed source
*/
function makeXHRSource(url) {
var _this3 = this;
var _ref8 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref8$headers = _ref8.headers,
headers = _ref8$headers === undefined ? {} : _ref8$headers,
blockSize = _ref8.blockSize;
return new BlockedSource(function () {
var _ref9 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee6(offset, length) {
return _regenerator2.default.wrap(function _callee6$(_context6) {
while (1) {
switch (_context6.prev = _context6.next) {
case 0:
return _context6.abrupt('return', new Promise(function (resolve, reject) {
var request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'arraybuffer';
Object.entries(Object.assign({}, headers, {
Range: 'bytes=' + offset + '-' + (offset + length)
})).forEach(function (_ref10) {
var _ref11 = (0, _slicedToArray3.default)(_ref10, 2),
key = _ref11[0],
value = _ref11[1];
return request.setRequestHeader(key, value);
});
request.onload = function () {
var data = request.response;
if (request.status === 206) {
resolve({
data: data,
offset: offset,
length: length
});
} else {
resolve({
data: data,
offset: 0,
length: data.byteLength
});
}
};
request.onerror = reject;
request.send();
}));
case 1:
case 'end':
return _context6.stop();
}
}
}, _callee6, _this3);
}));
return function (_x12, _x13) {
return _ref9.apply(this, arguments);
};
}(), { blockSize: blockSize });
}
/**
* Create a new source to read from a remote file using the node
* [http]{@link https://nodejs.org/api/http.html} API.
* @param {string} url The URL to send requests to.
* @param {Object} [options] Additional options.
* @param {Number} [options.blockSize] The block size to use.
* @param {object} [options.headers] Additional headers to be sent to the server.
*/
function makeHttpSource(url) {
var _this4 = this;
var _ref12 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref12$headers = _ref12.headers,
headers = _ref12$headers === undefined ? {} : _ref12$headers,
blockSize = _ref12.blockSize;
return new BlockedSource(function () {
var _ref13 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee7(offset, length) {
return _regenerator2.default.wrap(function _callee7$(_context7) {
while (1) {
switch (_context7.prev = _context7.next) {
case 0:
return _context7.abrupt('return', new Promise(function (resolve, reject) {
var parsed = _url2.default.parse(url);
var request = (parsed.protocol === 'http:' ? _http2.default : _https2.default).get(Object.assign({}, parsed, {
headers: Object.assign({}, headers, {
Range: 'bytes=' + offset + '-' + (offset + length)
})
}), function (result) {
var chunks = [];
// collect chunks
result.on('data', function (chunk) {
chunks.push(chunk);
});
// concatenate all chunks and resolve the promise with the resulting buffer
result.on('end', function () {
var data = _buffer.Buffer.concat(chunks).buffer;
resolve({
data: data,
offset: offset,
length: data.byteLength
});
});
});
request.on('error', reject);
}));
case 1:
case 'end':
return _context7.stop();
}
}
}, _callee7, _this4);
}));
return function (_x15, _x16) {
return _ref13.apply(this, arguments);
};
}(), { blockSize: blockSize });
}
/**
* Create a new source to read from a remote file. Uses either XHR, fetch or nodes http API.
* @param {string} url The URL to send requests to.
* @param {Object} [options] Additional options.
* @param {Boolean} [options.forceXHR] Force the usage of XMLHttpRequest.
* @param {Number} [options.blockSize] The block size to use.
* @param {object} [options.headers] Additional headers to be sent to the server.
* @returns The constructed source
*/
function makeRemoteSource(url, options) {
var forceXHR = options.forceXHR;
if (typeof fetch === 'function' && !forceXHR) {
return makeFetchSource(url, options);
} else if (typeof XMLHttpRequest !== 'undefined') {
return makeXHRSource(url, options);
} else if (_http2.default.get) {
return makeHttpSource(url, options);
}
throw new Error('No remote source available');
}
/**
* Create a new source to read from a local
* [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer}.
* @param {ArrayBuffer} arrayBuffer The ArrayBuffer to parse the GeoTIFF from.
* @returns The constructed source
*/
function makeBufferSource(arrayBuffer) {
return {
fetch: function () {
var _ref14 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee8(offset, length) {
return _regenerator2.default.wrap(function _callee8$(_context8) {
while (1) {
switch (_context8.prev = _context8.next) {
case 0:
return _context8.abrupt('return', arrayBuffer.slice(offset, offset + length));
case 1:
case 'end':
return _context8.stop();
}
}
}, _callee8, this);
}));
function fetch(_x17, _x18) {
return _ref14.apply(this, arguments);
}
return fetch;
}()
};
}
function openAsync(path, flags) {
var mode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
return new Promise(function (resolve, reject) {
(0, _fs.open)(path, flags, mode, function (err, fd) {
if (err) {
reject(err);
} else {
resolve(fd);
}
});
});
}
function readAsync() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return new Promise(function (resolve, reject) {
_fs.read.apply(undefined, args.concat([function (err, bytesRead, buffer) {
if (err) {
reject(err);
} else {
resolve({ bytesRead: bytesRead, buffer: buffer });
}
}]));
});
}
/**
* Creates a new source using the node filesystem API.
* @param {string} path The path to the file in the local filesystem.
* @returns The constructed source
*/
function makeFileSource(path) {
var fileOpen = openAsync(path, 'r');
return {
fetch: function () {
var _ref15 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee9(offset, length) {
var fd, _ref16, buffer;
return _regenerator2.default.wrap(function _callee9$(_context9) {
while (1) {
switch (_context9.prev = _context9.next) {
case 0:
_context9.next = 2;
return fileOpen;
case 2:
fd = _context9.sent;
_context9.next = 5;
return readAsync(fd, _buffer.Buffer.alloc(length), 0, length, offset);
case 5:
_ref16 = _context9.sent;
buffer = _ref16.buffer;
return _context9.abrupt('return', buffer.buffer);
case 8:
case 'end':
return _context9.stop();
}
}
}, _callee9, this);
}));
function fetch(_x20, _x21) {
return _ref15.apply(this, arguments);
}
return fetch;
}()
};
}
/**
* Create a new source from a given file/blob.
* @param {Blob} file The file or blob to read from.
* @returns The constructed source
*/
function makeFileReaderSource(file) {
return {
fetch: function () {
var _ref17 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee10(offset, length) {
return _regenerator2.default.wrap(function _callee10$(_context10) {
while (1) {
switch (_context10.prev = _context10.next) {
case 0:
return _context10.abrupt('return', new Promise(function (resolve, reject) {
var blob = file.slice(offset, offset + length);
var reader = new FileReader();
reader.onload = function (event) {
return resolve(event.target.result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(blob);
}));
case 1:
case 'end':
return _context10.stop();
}
}
}, _callee10, this);
}));
function fetch(_x22, _x23) {
return _ref17.apply(this, arguments);
}
return fetch;
}()
};
}