geotiff
Version:
GeoTIFF image decoding in JavaScript
211 lines • 8.27 kB
JavaScript
import { parseByteRanges, parseContentRange, parseContentType } from './httputils.js';
import { BaseSource } from './basesource.js';
import { BlockedSource } from './blockedsource.js';
import { FetchClient } from './client/fetch.js';
import { XHRClient } from './client/xhr.js';
import { HttpClient } from './client/http.js';
/** @import { RemoteSourceOptions, BlockedSourceOptions } from '../geotiff.js' */
class RemoteSource extends BaseSource {
/**
* @param {import("../geotiff.js").BaseClient} client
* @param {RemoteSourceOptions} options
*/
constructor(client, { headers, maxRanges = 0, allowFullFile } = {}) {
super();
this.client = client;
this.headers = headers;
this.maxRanges = maxRanges;
this.allowFullFile = allowFullFile;
this._fileSize = null;
}
/**
* @param {import('./basesource.js').Slice[]} slices
* @param {AbortSignal} [signal]
* @returns {Promise<ArrayBufferLike[]>}
*/
async fetch(slices, signal) {
// if we allow multi-ranges, split the incoming request into that many sub-requests
// and join them afterwards
if (this.maxRanges >= slices.length) {
return this.fetchSlices(slices, signal).then((results) => results.map((r) => r.data));
}
else if (this.maxRanges > 0 && slices.length > 1) {
// TODO: split into multiple multi-range requests
// const subSlicesRequests = [];
// for (let i = 0; i < slices.length; i += this.maxRanges) {
// subSlicesRequests.push(
// this.fetchSlices(slices.slice(i, i + this.maxRanges), signal),
// );
// }
// return (await Promise.all(subSlicesRequests)).flat();
}
// otherwise make a single request for each slice
return Promise.all(slices.map(async (slice) => (await this.fetchSlice(slice, signal)).data));
}
/**
* @param {Array<import('./basesource.js').Slice>} slices
* @param {AbortSignal} [signal]
* @returns {Promise<Array<import('./basesource.js').SliceWithData>>}
*/
async fetchSlices(slices, signal) {
const response = await this.client.request({
headers: {
...this.headers,
Range: `bytes=${slices
.map(({ offset, length }) => `${offset}-${offset + length - 1}`)
.join(',')}`,
},
signal,
});
if (!response.ok) {
throw new Error('Error fetching data.');
}
else if (response.status === 206) {
const { type, params } = parseContentType(response.getHeader('content-type'));
if (type === 'multipart/byteranges') {
const byteRanges = parseByteRanges(await response.getData(), params.boundary);
this._fileSize = byteRanges[0].fileSize || null;
return byteRanges;
}
const data = await response.getData();
const { start, end, total } = parseContentRange(response.getHeader('content-range'));
this._fileSize = total || null;
/** @type {import('./basesource.js').SliceWithData[]} */
const first = [{
data,
offset: start,
length: end + 1 - start,
}];
if (slices.length > 1) {
// we requested more than one slice, but got only the first
// unfortunately, some HTTP Servers don't support multi-ranges
// and return only the first
// get the rest of the slices and fetch them iteratively
const others = await Promise.all(slices.slice(1).map((slice) => this.fetchSlice(slice, signal)));
return first.concat(others);
}
return first;
}
else {
if (!this.allowFullFile) {
throw new Error('Server responded with full file');
}
const data = await response.getData();
this._fileSize = data.byteLength;
return [{
data,
offset: 0,
length: data.byteLength,
}];
}
}
/**
* @param {import('./basesource.js').Slice} slice
* @param {AbortSignal} [signal]
* @returns {Promise<import('./basesource.js').SliceWithData>}
*/
async fetchSlice(slice, signal) {
const { offset, length } = slice;
const response = await this.client.request({
headers: {
...this.headers,
Range: `bytes=${offset}-${offset + length - 1}`,
},
signal,
});
// check the response was okay and if the server actually understands range requests
if (!response.ok) {
throw new Error('Error fetching data.');
}
else if (response.status === 206) {
const data = await response.getData();
const { total } = parseContentRange(response.getHeader('content-range'));
this._fileSize = total || null;
return {
data,
offset,
length,
};
}
else {
if (!this.allowFullFile) {
throw new Error('Server responded with full file');
}
const data = await response.getData();
this._fileSize = data.byteLength;
return {
data,
offset: 0,
length: data.byteLength,
};
}
}
get fileSize() {
return this._fileSize;
}
}
/**
* @param {BaseSource} source
* @param {BlockedSourceOptions} blockedSourceOptions
* @returns {BaseSource}
*/
function maybeWrapInBlockedSource(source, { blockSize, cacheSize }) {
if (blockSize === undefined) {
return source;
}
return new BlockedSource(source, { blockSize, cacheSize });
}
/**
* @param {string} url
* @param {RemoteSourceOptions & BlockedSourceOptions & { credentials?: RequestCredentials}} [param1]
* @returns {BaseSource}
*/
export function makeFetchSource(url, { headers = {}, credentials, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
const client = new FetchClient(url, credentials);
const source = new RemoteSource(client, { headers, maxRanges, allowFullFile });
return maybeWrapInBlockedSource(source, blockOptions);
}
/**
* @param {string} url
* @param {RemoteSourceOptions & BlockedSourceOptions} [param1]
* @returns {BaseSource}
*/
export function makeXHRSource(url, { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
const client = new XHRClient(url);
const source = new RemoteSource(client, { headers, maxRanges, allowFullFile });
return maybeWrapInBlockedSource(source, blockOptions);
}
/**
* @param {string} url
* @param {RemoteSourceOptions & BlockedSourceOptions} [param1]
* @returns {BaseSource}
*/
export function makeHttpSource(url, { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
const client = new HttpClient(url);
const source = new RemoteSource(client, { headers, maxRanges, allowFullFile });
return maybeWrapInBlockedSource(source, blockOptions);
}
/**
* @param {import("../geotiff.js").BaseClient} client
* @param {RemoteSourceOptions & BlockedSourceOptions} [param1]
* @returns {BaseSource}
*/
export function makeCustomSource(client, { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
const source = new RemoteSource(client, { headers, maxRanges, allowFullFile });
return maybeWrapInBlockedSource(source, blockOptions);
}
/**
*
* @param {string} url
* @param {RemoteSourceOptions} options
*/
export function makeRemoteSource(url, { forceXHR = false, ...clientOptions } = {}) {
if (typeof fetch === 'function' && !forceXHR) {
return makeFetchSource(url, clientOptions);
}
if (typeof XMLHttpRequest !== 'undefined') {
return makeXHRSource(url, clientOptions);
}
return makeHttpSource(url, clientOptions);
}
//# sourceMappingURL=remote.js.map