geotiff.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. /** @module geotiff */
  2. import GeoTIFFImage from './geotiffimage.js';
  3. import DataView64 from './dataview64.js';
  4. import DataSlice from './dataslice.js';
  5. import Pool from './pool.js';
  6. import { makeRemoteSource } from './source/remote.js';
  7. import { makeBufferSource } from './source/arraybuffer.js';
  8. import { makeFileReaderSource } from './source/filereader.js';
  9. import { makeFileSource } from './source/file.js';
  10. import { fieldTypes, fieldTagNames, arrayFields, geoKeyNames } from './globals.js';
  11. import { writeGeotiff } from './geotiffwriter.js';
  12. import * as globals from './globals.js';
  13. import * as rgb from './rgb.js';
  14. import { getDecoder, addDecoder } from './compression/index.js';
  15. import { setLogger } from './logging.js';
  16. // add by wzx
  17. // #ifdef APP-PLUS
  18. import { TextEncoder, TextDecoder } from 'text-decoding';
  19. // #endif
  20. export { globals };
  21. export { rgb };
  22. export { default as BaseDecoder } from './compression/basedecoder.js';
  23. export { getDecoder, addDecoder };
  24. export { setLogger };
  25. /**
  26. * @typedef {Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | Float32Array | Float64Array}
  27. * TypedArray
  28. */
  29. /**
  30. * @typedef {{ height:number, width: number }} Dimensions
  31. */
  32. /**
  33. * The autogenerated docs are a little confusing here. The effective type is:
  34. *
  35. * `TypedArray & { height: number; width: number}`
  36. * @typedef {TypedArray & Dimensions} TypedArrayWithDimensions
  37. */
  38. /**
  39. * The autogenerated docs are a little confusing here. The effective type is:
  40. *
  41. * `TypedArray[] & { height: number; width: number}`
  42. * @typedef {TypedArray[] & Dimensions} TypedArrayArrayWithDimensions
  43. */
  44. /**
  45. * The autogenerated docs are a little confusing here. The effective type is:
  46. *
  47. * `(TypedArray | TypedArray[]) & { height: number; width: number}`
  48. * @typedef {TypedArrayWithDimensions | TypedArrayArrayWithDimensions} ReadRasterResult
  49. */
  50. function getFieldTypeLength(fieldType) {
  51. switch (fieldType) {
  52. case fieldTypes.BYTE: case fieldTypes.ASCII: case fieldTypes.SBYTE: case fieldTypes.UNDEFINED:
  53. return 1;
  54. case fieldTypes.SHORT: case fieldTypes.SSHORT:
  55. return 2;
  56. case fieldTypes.LONG: case fieldTypes.SLONG: case fieldTypes.FLOAT: case fieldTypes.IFD:
  57. return 4;
  58. case fieldTypes.RATIONAL: case fieldTypes.SRATIONAL: case fieldTypes.DOUBLE:
  59. case fieldTypes.LONG8: case fieldTypes.SLONG8: case fieldTypes.IFD8:
  60. return 8;
  61. default:
  62. throw new RangeError(`Invalid field type: ${fieldType}`);
  63. }
  64. }
  65. function parseGeoKeyDirectory(fileDirectory) {
  66. const rawGeoKeyDirectory = fileDirectory.GeoKeyDirectory;
  67. if (!rawGeoKeyDirectory) {
  68. return null;
  69. }
  70. const geoKeyDirectory = {};
  71. for (let i = 4; i <= rawGeoKeyDirectory[3] * 4; i += 4) {
  72. const key = geoKeyNames[rawGeoKeyDirectory[i]];
  73. const location = (rawGeoKeyDirectory[i + 1])
  74. ? (fieldTagNames[rawGeoKeyDirectory[i + 1]]) : null;
  75. const count = rawGeoKeyDirectory[i + 2];
  76. const offset = rawGeoKeyDirectory[i + 3];
  77. let value = null;
  78. if (!location) {
  79. value = offset;
  80. } else {
  81. value = fileDirectory[location];
  82. if (typeof value === 'undefined' || value === null) {
  83. throw new Error(`Could not get value of geoKey '${key}'.`);
  84. } else if (typeof value === 'string') {
  85. value = value.substring(offset, offset + count - 1);
  86. } else if (value.subarray) {
  87. value = value.subarray(offset, offset + count);
  88. if (count === 1) {
  89. value = value[0];
  90. }
  91. }
  92. }
  93. geoKeyDirectory[key] = value;
  94. }
  95. return geoKeyDirectory;
  96. }
  97. function getValues(dataSlice, fieldType, count, offset) {
  98. let values = null;
  99. let readMethod = null;
  100. const fieldTypeLength = getFieldTypeLength(fieldType);
  101. switch (fieldType) {
  102. case fieldTypes.BYTE: case fieldTypes.ASCII: case fieldTypes.UNDEFINED:
  103. values = new Uint8Array(count); readMethod = dataSlice.readUint8;
  104. break;
  105. case fieldTypes.SBYTE:
  106. values = new Int8Array(count); readMethod = dataSlice.readInt8;
  107. break;
  108. case fieldTypes.SHORT:
  109. values = new Uint16Array(count); readMethod = dataSlice.readUint16;
  110. break;
  111. case fieldTypes.SSHORT:
  112. values = new Int16Array(count); readMethod = dataSlice.readInt16;
  113. break;
  114. case fieldTypes.LONG: case fieldTypes.IFD:
  115. values = new Uint32Array(count); readMethod = dataSlice.readUint32;
  116. break;
  117. case fieldTypes.SLONG:
  118. values = new Int32Array(count); readMethod = dataSlice.readInt32;
  119. break;
  120. case fieldTypes.LONG8: case fieldTypes.IFD8:
  121. values = new Array(count); readMethod = dataSlice.readUint64;
  122. break;
  123. case fieldTypes.SLONG8:
  124. values = new Array(count); readMethod = dataSlice.readInt64;
  125. break;
  126. case fieldTypes.RATIONAL:
  127. values = new Uint32Array(count * 2); readMethod = dataSlice.readUint32;
  128. break;
  129. case fieldTypes.SRATIONAL:
  130. values = new Int32Array(count * 2); readMethod = dataSlice.readInt32;
  131. break;
  132. case fieldTypes.FLOAT:
  133. values = new Float32Array(count); readMethod = dataSlice.readFloat32;
  134. break;
  135. case fieldTypes.DOUBLE:
  136. values = new Float64Array(count); readMethod = dataSlice.readFloat64;
  137. break;
  138. default:
  139. throw new RangeError(`Invalid field type: ${fieldType}`);
  140. }
  141. // normal fields
  142. if (!(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) {
  143. for (let i = 0; i < count; ++i) {
  144. values[i] = readMethod.call(
  145. dataSlice, offset + (i * fieldTypeLength),
  146. );
  147. }
  148. } else { // RATIONAL or SRATIONAL
  149. for (let i = 0; i < count; i += 2) {
  150. values[i] = readMethod.call(
  151. dataSlice, offset + (i * fieldTypeLength),
  152. );
  153. values[i + 1] = readMethod.call(
  154. dataSlice, offset + ((i * fieldTypeLength) + 4),
  155. );
  156. }
  157. }
  158. if (fieldType === fieldTypes.ASCII) {
  159. return new TextDecoder('utf-8').decode(values);
  160. }
  161. return values;
  162. }
  163. /**
  164. * Data class to store the parsed file directory, geo key directory and
  165. * offset to the next IFD
  166. */
  167. class ImageFileDirectory {
  168. constructor(fileDirectory, geoKeyDirectory, nextIFDByteOffset) {
  169. this.fileDirectory = fileDirectory;
  170. this.geoKeyDirectory = geoKeyDirectory;
  171. this.nextIFDByteOffset = nextIFDByteOffset;
  172. }
  173. }
  174. /**
  175. * Error class for cases when an IFD index was requested, that does not exist
  176. * in the file.
  177. */
  178. class GeoTIFFImageIndexError extends Error {
  179. constructor(index) {
  180. super(`No image at index ${index}`);
  181. this.index = index;
  182. }
  183. }
  184. class GeoTIFFBase {
  185. /**
  186. * (experimental) Reads raster data from the best fitting image. This function uses
  187. * the image with the lowest resolution that is still a higher resolution than the
  188. * requested resolution.
  189. * When specified, the `bbox` option is translated to the `window` option and the
  190. * `resX` and `resY` to `width` and `height` respectively.
  191. * Then, the [readRasters]{@link GeoTIFFImage#readRasters} method of the selected
  192. * image is called and the result returned.
  193. * @see GeoTIFFImage.readRasters
  194. * @param {import('./geotiffimage').ReadRasterOptions} [options={}] optional parameters
  195. * @returns {Promise<ReadRasterResult>} the decoded array(s), with `height` and `width`, as a promise
  196. */
  197. async readRasters(options = {}) {
  198. const { window: imageWindow, width, height } = options;
  199. let { resX, resY, bbox } = options;
  200. const firstImage = await this.getImage();
  201. let usedImage = firstImage;
  202. const imageCount = await this.getImageCount();
  203. const imgBBox = firstImage.getBoundingBox();
  204. if (imageWindow && bbox) {
  205. throw new Error('Both "bbox" and "window" passed.');
  206. }
  207. // if width/height is passed, transform it to resolution
  208. if (width || height) {
  209. // if we have an image window (pixel coordinates), transform it to a BBox
  210. // using the origin/resolution of the first image.
  211. if (imageWindow) {
  212. const [oX, oY] = firstImage.getOrigin();
  213. const [rX, rY] = firstImage.getResolution();
  214. bbox = [
  215. oX + (imageWindow[0] * rX),
  216. oY + (imageWindow[1] * rY),
  217. oX + (imageWindow[2] * rX),
  218. oY + (imageWindow[3] * rY),
  219. ];
  220. }
  221. // if we have a bbox (or calculated one)
  222. const usedBBox = bbox || imgBBox;
  223. if (width) {
  224. if (resX) {
  225. throw new Error('Both width and resX passed');
  226. }
  227. resX = (usedBBox[2] - usedBBox[0]) / width;
  228. }
  229. if (height) {
  230. if (resY) {
  231. throw new Error('Both width and resY passed');
  232. }
  233. resY = (usedBBox[3] - usedBBox[1]) / height;
  234. }
  235. }
  236. // if resolution is set or calculated, try to get the image with the worst acceptable resolution
  237. if (resX || resY) {
  238. const allImages = [];
  239. for (let i = 0; i < imageCount; ++i) {
  240. const image = await this.getImage(i);
  241. const { SubfileType: subfileType, NewSubfileType: newSubfileType } = image.fileDirectory;
  242. if (i === 0 || subfileType === 2 || newSubfileType & 1) {
  243. allImages.push(image);
  244. }
  245. }
  246. allImages.sort((a, b) => a.getWidth() - b.getWidth());
  247. for (let i = 0; i < allImages.length; ++i) {
  248. const image = allImages[i];
  249. const imgResX = (imgBBox[2] - imgBBox[0]) / image.getWidth();
  250. const imgResY = (imgBBox[3] - imgBBox[1]) / image.getHeight();
  251. usedImage = image;
  252. if ((resX && resX > imgResX) || (resY && resY > imgResY)) {
  253. break;
  254. }
  255. }
  256. }
  257. let wnd = imageWindow;
  258. if (bbox) {
  259. const [oX, oY] = firstImage.getOrigin();
  260. const [imageResX, imageResY] = usedImage.getResolution(firstImage);
  261. wnd = [
  262. Math.round((bbox[0] - oX) / imageResX),
  263. Math.round((bbox[1] - oY) / imageResY),
  264. Math.round((bbox[2] - oX) / imageResX),
  265. Math.round((bbox[3] - oY) / imageResY),
  266. ];
  267. wnd = [
  268. Math.min(wnd[0], wnd[2]),
  269. Math.min(wnd[1], wnd[3]),
  270. Math.max(wnd[0], wnd[2]),
  271. Math.max(wnd[1], wnd[3]),
  272. ];
  273. }
  274. return usedImage.readRasters({ ...options, window: wnd });
  275. }
  276. }
  277. /**
  278. * @typedef {Object} GeoTIFFOptions
  279. * @property {boolean} [cache=false] whether or not decoded tiles shall be cached.
  280. */
  281. /**
  282. * The abstraction for a whole GeoTIFF file.
  283. * @augments GeoTIFFBase
  284. */
  285. class GeoTIFF extends GeoTIFFBase {
  286. /**
  287. * @constructor
  288. * @param {*} source The datasource to read from.
  289. * @param {boolean} littleEndian Whether the image uses little endian.
  290. * @param {boolean} bigTiff Whether the image uses bigTIFF conventions.
  291. * @param {number} firstIFDOffset The numeric byte-offset from the start of the image
  292. * to the first IFD.
  293. * @param {GeoTIFFOptions} [options] further options.
  294. */
  295. constructor(source, littleEndian, bigTiff, firstIFDOffset, options = {}) {
  296. super();
  297. this.source = source;
  298. this.littleEndian = littleEndian;
  299. this.bigTiff = bigTiff;
  300. this.firstIFDOffset = firstIFDOffset;
  301. this.cache = options.cache || false;
  302. this.ifdRequests = [];
  303. this.ghostValues = null;
  304. }
  305. async getSlice(offset, size) {
  306. const fallbackSize = this.bigTiff ? 4048 : 1024;
  307. return new DataSlice(
  308. (await this.source.fetch([{
  309. offset,
  310. length: typeof size !== 'undefined' ? size : fallbackSize,
  311. }]))[0],
  312. offset,
  313. this.littleEndian,
  314. this.bigTiff,
  315. );
  316. }
  317. /**
  318. * Instructs to parse an image file directory at the given file offset.
  319. * As there is no way to ensure that a location is indeed the start of an IFD,
  320. * this function must be called with caution (e.g only using the IFD offsets from
  321. * the headers or other IFDs).
  322. * @param {number} offset the offset to parse the IFD at
  323. * @returns {Promise<ImageFileDirectory>} the parsed IFD
  324. */
  325. async parseFileDirectoryAt(offset) {
  326. const entrySize = this.bigTiff ? 20 : 12;
  327. const offsetSize = this.bigTiff ? 8 : 2;
  328. let dataSlice = await this.getSlice(offset);
  329. const numDirEntries = this.bigTiff
  330. ? dataSlice.readUint64(offset)
  331. : dataSlice.readUint16(offset);
  332. // if the slice does not cover the whole IFD, request a bigger slice, where the
  333. // whole IFD fits: num of entries + n x tag length + offset to next IFD
  334. const byteSize = (numDirEntries * entrySize) + (this.bigTiff ? 16 : 6);
  335. if (!dataSlice.covers(offset, byteSize)) {
  336. dataSlice = await this.getSlice(offset, byteSize);
  337. }
  338. const fileDirectory = {};
  339. // loop over the IFD and create a file directory object
  340. let i = offset + (this.bigTiff ? 8 : 2);
  341. for (let entryCount = 0; entryCount < numDirEntries; i += entrySize, ++entryCount) {
  342. const fieldTag = dataSlice.readUint16(i);
  343. const fieldType = dataSlice.readUint16(i + 2);
  344. const typeCount = this.bigTiff
  345. ? dataSlice.readUint64(i + 4)
  346. : dataSlice.readUint32(i + 4);
  347. let fieldValues;
  348. let value;
  349. const fieldTypeLength = getFieldTypeLength(fieldType);
  350. const valueOffset = i + (this.bigTiff ? 12 : 8);
  351. // check whether the value is directly encoded in the tag or refers to a
  352. // different external byte range
  353. if (fieldTypeLength * typeCount <= (this.bigTiff ? 8 : 4)) {
  354. fieldValues = getValues(dataSlice, fieldType, typeCount, valueOffset);
  355. } else {
  356. // resolve the reference to the actual byte range
  357. const actualOffset = dataSlice.readOffset(valueOffset);
  358. const length = getFieldTypeLength(fieldType) * typeCount;
  359. // check, whether we actually cover the referenced byte range; if not,
  360. // request a new slice of bytes to read from it
  361. if (dataSlice.covers(actualOffset, length)) {
  362. fieldValues = getValues(dataSlice, fieldType, typeCount, actualOffset);
  363. } else {
  364. const fieldDataSlice = await this.getSlice(actualOffset, length);
  365. fieldValues = getValues(fieldDataSlice, fieldType, typeCount, actualOffset);
  366. }
  367. }
  368. // unpack single values from the array
  369. if (typeCount === 1 && arrayFields.indexOf(fieldTag) === -1
  370. && !(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) {
  371. value = fieldValues[0];
  372. } else {
  373. value = fieldValues;
  374. }
  375. // write the tags value to the file directly
  376. fileDirectory[fieldTagNames[fieldTag]] = value;
  377. }
  378. const geoKeyDirectory = parseGeoKeyDirectory(fileDirectory);
  379. const nextIFDByteOffset = dataSlice.readOffset(
  380. offset + offsetSize + (entrySize * numDirEntries),
  381. );
  382. return new ImageFileDirectory(
  383. fileDirectory,
  384. geoKeyDirectory,
  385. nextIFDByteOffset,
  386. );
  387. }
  388. async requestIFD(index) {
  389. // see if we already have that IFD index requested.
  390. if (this.ifdRequests[index]) {
  391. // attach to an already requested IFD
  392. return this.ifdRequests[index];
  393. } else if (index === 0) {
  394. // special case for index 0
  395. this.ifdRequests[index] = this.parseFileDirectoryAt(this.firstIFDOffset);
  396. return this.ifdRequests[index];
  397. } else if (!this.ifdRequests[index - 1]) {
  398. // if the previous IFD was not yet loaded, load that one first
  399. // this is the recursive call.
  400. try {
  401. this.ifdRequests[index - 1] = this.requestIFD(index - 1);
  402. } catch (e) {
  403. // if the previous one already was an index error, rethrow
  404. // with the current index
  405. if (e instanceof GeoTIFFImageIndexError) {
  406. throw new GeoTIFFImageIndexError(index);
  407. }
  408. // rethrow anything else
  409. throw e;
  410. }
  411. }
  412. // if the previous IFD was loaded, we can finally fetch the one we are interested in.
  413. // we need to wrap this in an IIFE, otherwise this.ifdRequests[index] would be delayed
  414. this.ifdRequests[index] = (async () => {
  415. const previousIfd = await this.ifdRequests[index - 1];
  416. if (previousIfd.nextIFDByteOffset === 0) {
  417. throw new GeoTIFFImageIndexError(index);
  418. }
  419. return this.parseFileDirectoryAt(previousIfd.nextIFDByteOffset);
  420. })();
  421. return this.ifdRequests[index];
  422. }
  423. /**
  424. * Get the n-th internal subfile of an image. By default, the first is returned.
  425. *
  426. * @param {number} [index=0] the index of the image to return.
  427. * @returns {Promise<GeoTIFFImage>} the image at the given index
  428. */
  429. async getImage(index = 0) {
  430. const ifd = await this.requestIFD(index);
  431. return new GeoTIFFImage(
  432. ifd.fileDirectory, ifd.geoKeyDirectory,
  433. this.dataView, this.littleEndian, this.cache, this.source,
  434. );
  435. }
  436. /**
  437. * Returns the count of the internal subfiles.
  438. *
  439. * @returns {Promise<number>} the number of internal subfile images
  440. */
  441. async getImageCount() {
  442. let index = 0;
  443. // loop until we run out of IFDs
  444. let hasNext = true;
  445. while (hasNext) {
  446. try {
  447. await this.requestIFD(index);
  448. ++index;
  449. } catch (e) {
  450. if (e instanceof GeoTIFFImageIndexError) {
  451. hasNext = false;
  452. } else {
  453. throw e;
  454. }
  455. }
  456. }
  457. return index;
  458. }
  459. /**
  460. * Get the values of the COG ghost area as a parsed map.
  461. * See https://gdal.org/drivers/raster/cog.html#header-ghost-area for reference
  462. * @returns {Promise<Object>} the parsed ghost area or null, if no such area was found
  463. */
  464. async getGhostValues() {
  465. const offset = this.bigTiff ? 16 : 8;
  466. if (this.ghostValues) {
  467. return this.ghostValues;
  468. }
  469. const detectionString = 'GDAL_STRUCTURAL_METADATA_SIZE=';
  470. const heuristicAreaSize = detectionString.length + 100;
  471. let slice = await this.getSlice(offset, heuristicAreaSize);
  472. if (detectionString === getValues(slice, fieldTypes.ASCII, detectionString.length, offset)) {
  473. const valuesString = getValues(slice, fieldTypes.ASCII, heuristicAreaSize, offset);
  474. const firstLine = valuesString.split('\n')[0];
  475. const metadataSize = Number(firstLine.split('=')[1].split(' ')[0]) + firstLine.length;
  476. if (metadataSize > heuristicAreaSize) {
  477. slice = await this.getSlice(offset, metadataSize);
  478. }
  479. const fullString = getValues(slice, fieldTypes.ASCII, metadataSize, offset);
  480. this.ghostValues = {};
  481. fullString
  482. .split('\n')
  483. .filter((line) => line.length > 0)
  484. .map((line) => line.split('='))
  485. .forEach(([key, value]) => {
  486. this.ghostValues[key] = value;
  487. });
  488. }
  489. return this.ghostValues;
  490. }
  491. /**
  492. * Parse a (Geo)TIFF file from the given source.
  493. *
  494. * @param {*} source The source of data to parse from.
  495. * @param {GeoTIFFOptions} [options] Additional options.
  496. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  497. * to be aborted
  498. */
  499. static async fromSource(source, options, signal) {
  500. const headerData = (await source.fetch([{ offset: 0, length: 1024 }], signal))[0];
  501. const dataView = new DataView64(headerData);
  502. const BOM = dataView.getUint16(0, 0);
  503. let littleEndian;
  504. if (BOM === 0x4949) {
  505. littleEndian = true;
  506. } else if (BOM === 0x4D4D) {
  507. littleEndian = false;
  508. } else {
  509. throw new TypeError('Invalid byte order value.');
  510. }
  511. const magicNumber = dataView.getUint16(2, littleEndian);
  512. let bigTiff;
  513. if (magicNumber === 42) {
  514. bigTiff = false;
  515. } else if (magicNumber === 43) {
  516. bigTiff = true;
  517. const offsetByteSize = dataView.getUint16(4, littleEndian);
  518. if (offsetByteSize !== 8) {
  519. throw new Error('Unsupported offset byte-size.');
  520. }
  521. } else {
  522. throw new TypeError('Invalid magic number.');
  523. }
  524. const firstIFDOffset = bigTiff
  525. ? dataView.getUint64(8, littleEndian)
  526. : dataView.getUint32(4, littleEndian);
  527. return new GeoTIFF(source, littleEndian, bigTiff, firstIFDOffset, options);
  528. }
  529. /**
  530. * Closes the underlying file buffer
  531. * N.B. After the GeoTIFF has been completely processed it needs
  532. * to be closed but only if it has been constructed from a file.
  533. */
  534. close() {
  535. if (typeof this.source.close === 'function') {
  536. return this.source.close();
  537. }
  538. return false;
  539. }
  540. }
  541. export { GeoTIFF };
  542. export default GeoTIFF;
  543. /**
  544. * Wrapper for GeoTIFF files that have external overviews.
  545. * @augments GeoTIFFBase
  546. */
  547. class MultiGeoTIFF extends GeoTIFFBase {
  548. /**
  549. * Construct a new MultiGeoTIFF from a main and several overview files.
  550. * @param {GeoTIFF} mainFile The main GeoTIFF file.
  551. * @param {GeoTIFF[]} overviewFiles An array of overview files.
  552. */
  553. constructor(mainFile, overviewFiles) {
  554. super();
  555. this.mainFile = mainFile;
  556. this.overviewFiles = overviewFiles;
  557. this.imageFiles = [mainFile].concat(overviewFiles);
  558. this.fileDirectoriesPerFile = null;
  559. this.fileDirectoriesPerFileParsing = null;
  560. this.imageCount = null;
  561. }
  562. async parseFileDirectoriesPerFile() {
  563. const requests = [this.mainFile.parseFileDirectoryAt(this.mainFile.firstIFDOffset)]
  564. .concat(this.overviewFiles.map((file) => file.parseFileDirectoryAt(file.firstIFDOffset)));
  565. this.fileDirectoriesPerFile = await Promise.all(requests);
  566. return this.fileDirectoriesPerFile;
  567. }
  568. /**
  569. * Get the n-th internal subfile of an image. By default, the first is returned.
  570. *
  571. * @param {number} [index=0] the index of the image to return.
  572. * @returns {Promise<GeoTIFFImage>} the image at the given index
  573. */
  574. async getImage(index = 0) {
  575. await this.getImageCount();
  576. await this.parseFileDirectoriesPerFile();
  577. let visited = 0;
  578. let relativeIndex = 0;
  579. for (let i = 0; i < this.imageFiles.length; i++) {
  580. const imageFile = this.imageFiles[i];
  581. for (let ii = 0; ii < this.imageCounts[i]; ii++) {
  582. if (index === visited) {
  583. const ifd = await imageFile.requestIFD(relativeIndex);
  584. return new GeoTIFFImage(
  585. ifd.fileDirectory, ifd.geoKeyDirectory,
  586. imageFile.dataView, imageFile.littleEndian, imageFile.cache, imageFile.source,
  587. );
  588. }
  589. visited++;
  590. relativeIndex++;
  591. }
  592. relativeIndex = 0;
  593. }
  594. throw new RangeError('Invalid image index');
  595. }
  596. /**
  597. * Returns the count of the internal subfiles.
  598. *
  599. * @returns {Promise<number>} the number of internal subfile images
  600. */
  601. async getImageCount() {
  602. if (this.imageCount !== null) {
  603. return this.imageCount;
  604. }
  605. const requests = [this.mainFile.getImageCount()]
  606. .concat(this.overviewFiles.map((file) => file.getImageCount()));
  607. this.imageCounts = await Promise.all(requests);
  608. this.imageCount = this.imageCounts.reduce((count, ifds) => count + ifds, 0);
  609. return this.imageCount;
  610. }
  611. }
  612. export { MultiGeoTIFF };
  613. /**
  614. * Creates a new GeoTIFF from a remote URL.
  615. * @param {string} url The URL to access the image from
  616. * @param {object} [options] Additional options to pass to the source.
  617. * See {@link makeRemoteSource} for details.
  618. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  619. * to be aborted
  620. * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
  621. */
  622. export async function fromUrl(url, options = {}, signal) {
  623. return GeoTIFF.fromSource(makeRemoteSource(url, options), signal);
  624. }
  625. /**
  626. * Construct a new GeoTIFF from an
  627. * [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer}.
  628. * @param {ArrayBuffer} arrayBuffer The data to read the file from.
  629. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  630. * to be aborted
  631. * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
  632. */
  633. export async function fromArrayBuffer(arrayBuffer, signal) {
  634. return GeoTIFF.fromSource(makeBufferSource(arrayBuffer), signal);
  635. }
  636. /**
  637. * Construct a GeoTIFF from a local file path. This uses the node
  638. * [filesystem API]{@link https://nodejs.org/api/fs.html} and is
  639. * not available on browsers.
  640. *
  641. * N.B. After the GeoTIFF has been completely processed it needs
  642. * to be closed but only if it has been constructed from a file.
  643. * @param {string} path The file path to read from.
  644. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  645. * to be aborted
  646. * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
  647. */
  648. export async function fromFile(path, signal) {
  649. return GeoTIFF.fromSource(makeFileSource(path), signal);
  650. }
  651. /**
  652. * Construct a GeoTIFF from an HTML
  653. * [Blob]{@link https://developer.mozilla.org/en-US/docs/Web/API/Blob} or
  654. * [File]{@link https://developer.mozilla.org/en-US/docs/Web/API/File}
  655. * object.
  656. * @param {Blob|File} blob The Blob or File object to read from.
  657. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  658. * to be aborted
  659. * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
  660. */
  661. export async function fromBlob(blob, signal) {
  662. return GeoTIFF.fromSource(makeFileReaderSource(blob), signal);
  663. }
  664. /**
  665. * Construct a MultiGeoTIFF from the given URLs.
  666. * @param {string} mainUrl The URL for the main file.
  667. * @param {string[]} overviewUrls An array of URLs for the overview images.
  668. * @param {Object} [options] Additional options to pass to the source.
  669. * See [makeRemoteSource]{@link module:source.makeRemoteSource}
  670. * for details.
  671. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  672. * to be aborted
  673. * @returns {Promise<MultiGeoTIFF>} The resulting MultiGeoTIFF file.
  674. */
  675. export async function fromUrls(mainUrl, overviewUrls = [], options = {}, signal) {
  676. const mainFile = await GeoTIFF.fromSource(makeRemoteSource(mainUrl, options), signal);
  677. const overviewFiles = await Promise.all(
  678. overviewUrls.map((url) => GeoTIFF.fromSource(makeRemoteSource(url, options))),
  679. );
  680. return new MultiGeoTIFF(mainFile, overviewFiles);
  681. }
  682. /**
  683. * Main creating function for GeoTIFF files.
  684. * @param {(Array)} array of pixel values
  685. * @returns {metadata} metadata
  686. */
  687. export function writeArrayBuffer(values, metadata) {
  688. return writeGeotiff(values, metadata);
  689. }
  690. export { Pool };
  691. export { GeoTIFFImage };