Spaces:
Sleeping
Sleeping
| // @flow | |
| import window from './window'; | |
| import {extend, warnOnce, isWorker} from './util'; | |
| import {isMapboxHTTPURL, hasCacheDefeatingSku} from './mapbox'; | |
| import config from './config'; | |
| import assert from 'assert'; | |
| import {cacheGet, cachePut} from './tile_request_cache'; | |
| import webpSupported from './webp_supported'; | |
| import offscreenCanvasSupported from './offscreen_canvas_supported'; | |
| import type {Callback} from '../types/callback'; | |
| import type {Cancelable} from '../types/cancelable'; | |
| /** | |
| * The type of a resource. | |
| * @private | |
| * @readonly | |
| * @enum {string} | |
| */ | |
| const ResourceType = { | |
| Unknown: 'Unknown', | |
| Style: 'Style', | |
| Source: 'Source', | |
| Tile: 'Tile', | |
| Glyphs: 'Glyphs', | |
| SpriteImage: 'SpriteImage', | |
| SpriteJSON: 'SpriteJSON', | |
| Image: 'Image' | |
| }; | |
| export {ResourceType}; | |
| if (typeof Object.freeze == 'function') { | |
| Object.freeze(ResourceType); | |
| } | |
| /** | |
| * A `RequestParameters` object to be returned from Map.options.transformRequest callbacks. | |
| * @typedef {Object} RequestParameters | |
| * @property {string} url The URL to be requested. | |
| * @property {Object} headers The headers to be sent with the request. | |
| * @property {string} method Request method `'GET' | 'POST' | 'PUT'`. | |
| * @property {string} body Request body. | |
| * @property {string} type Response body type to be returned `'string' | 'json' | 'arrayBuffer'`. | |
| * @property {string} credentials `'same-origin'|'include'` Use 'include' to send cookies with cross-origin requests. | |
| * @property {boolean} collectResourceTiming If true, Resource Timing API information will be collected for these transformed requests and returned in a resourceTiming property of relevant data events. | |
| * @example | |
| * // use transformRequest to modify requests that begin with `http://myHost` | |
| * transformRequest: function(url, resourceType) { | |
| * if (resourceType === 'Source' && url.indexOf('http://myHost') > -1) { | |
| * return { | |
| * url: url.replace('http', 'https'), | |
| * headers: { 'my-custom-header': true }, | |
| * credentials: 'include' // Include cookies for cross-origin requests | |
| * } | |
| * } | |
| * } | |
| * | |
| */ | |
| export type RequestParameters = { | |
| url: string, | |
| headers?: Object, | |
| method?: 'GET' | 'POST' | 'PUT', | |
| body?: string, | |
| type?: 'string' | 'json' | 'arrayBuffer', | |
| credentials?: 'same-origin' | 'include', | |
| collectResourceTiming?: boolean | |
| }; | |
| export type ResponseCallback<T> = (error: ?Error, data: ?T, cacheControl: ?string, expires: ?string) => void; | |
| class AJAXError extends Error { | |
| status: number; | |
| url: string; | |
| constructor(message: string, status: number, url: string) { | |
| if (status === 401 && isMapboxHTTPURL(url)) { | |
| message += ': you may have provided an invalid Mapbox access token. See https://www.mapbox.com/api-documentation/#access-tokens-and-token-scopes'; | |
| } | |
| super(message); | |
| this.status = status; | |
| this.url = url; | |
| // work around for https://github.com/Rich-Harris/buble/issues/40 | |
| this.name = this.constructor.name; | |
| this.message = message; | |
| } | |
| toString() { | |
| return `${this.name}: ${this.message} (${this.status}): ${this.url}`; | |
| } | |
| } | |
| // Ensure that we're sending the correct referrer from blob URL worker bundles. | |
| // For files loaded from the local file system, `location.origin` will be set | |
| // to the string(!) "null" (Firefox), or "file://" (Chrome, Safari, Edge, IE), | |
| // and we will set an empty referrer. Otherwise, we're using the document's URL. | |
| /* global self */ | |
| export const getReferrer = isWorker() ? | |
| () => self.worker && self.worker.referrer : | |
| () => (window.location.protocol === 'blob:' ? window.parent : window).location.href; | |
| // Determines whether a URL is a file:// URL. This is obviously the case if it begins | |
| // with file://. Relative URLs are also file:// URLs iff the original document was loaded | |
| // via a file:// URL. | |
| const isFileURL = url => /^file:/.test(url) || (/^file:/.test(getReferrer()) && !/^\w+:/.test(url)); | |
| function makeFetchRequest(requestParameters: RequestParameters, callback: ResponseCallback<any>): Cancelable { | |
| const controller = new window.AbortController(); | |
| const request = new window.Request(requestParameters.url, { | |
| method: requestParameters.method || 'GET', | |
| body: requestParameters.body, | |
| credentials: requestParameters.credentials, | |
| headers: requestParameters.headers, | |
| referrer: getReferrer(), | |
| signal: controller.signal | |
| }); | |
| let complete = false; | |
| let aborted = false; | |
| const cacheIgnoringSearch = hasCacheDefeatingSku(request.url); | |
| if (requestParameters.type === 'json') { | |
| request.headers.set('Accept', 'application/json'); | |
| } | |
| const validateOrFetch = (err, cachedResponse, responseIsFresh) => { | |
| if (aborted) return; | |
| if (err) { | |
| // Do fetch in case of cache error. | |
| // HTTP pages in Edge trigger a security error that can be ignored. | |
| if (err.message !== 'SecurityError') { | |
| warnOnce(err); | |
| } | |
| } | |
| if (cachedResponse && responseIsFresh) { | |
| return finishRequest(cachedResponse); | |
| } | |
| if (cachedResponse) { | |
| // We can't do revalidation with 'If-None-Match' because then the | |
| // request doesn't have simple cors headers. | |
| } | |
| const requestTime = Date.now(); | |
| window.fetch(request).then(response => { | |
| if (response.ok) { | |
| const cacheableResponse = cacheIgnoringSearch ? response.clone() : null; | |
| return finishRequest(response, cacheableResponse, requestTime); | |
| } else { | |
| return callback(new AJAXError(response.statusText, response.status, requestParameters.url)); | |
| } | |
| }).catch(error => { | |
| if (error.code === 20) { | |
| // silence expected AbortError | |
| return; | |
| } | |
| callback(new Error(error.message)); | |
| }); | |
| }; | |
| const finishRequest = (response, cacheableResponse, requestTime) => { | |
| ( | |
| requestParameters.type === 'arrayBuffer' ? response.arrayBuffer() : | |
| requestParameters.type === 'json' ? response.json() : | |
| response.text() | |
| ).then(result => { | |
| if (aborted) return; | |
| if (cacheableResponse && requestTime) { | |
| // The response needs to be inserted into the cache after it has completely loaded. | |
| // Until it is fully loaded there is a chance it will be aborted. Aborting while | |
| // reading the body can cause the cache insertion to error. We could catch this error | |
| // in most browsers but in Firefox it seems to sometimes crash the tab. Adding | |
| // it to the cache here avoids that error. | |
| cachePut(request, cacheableResponse, requestTime); | |
| } | |
| complete = true; | |
| callback(null, result, response.headers.get('Cache-Control'), response.headers.get('Expires')); | |
| }).catch(err => { | |
| if (!aborted) callback(new Error(err.message)); | |
| }); | |
| }; | |
| if (cacheIgnoringSearch) { | |
| cacheGet(request, validateOrFetch); | |
| } else { | |
| validateOrFetch(null, null); | |
| } | |
| return {cancel: () => { | |
| aborted = true; | |
| if (!complete) controller.abort(); | |
| }}; | |
| } | |
| function makeXMLHttpRequest(requestParameters: RequestParameters, callback: ResponseCallback<any>): Cancelable { | |
| const xhr: XMLHttpRequest = new window.XMLHttpRequest(); | |
| xhr.open(requestParameters.method || 'GET', requestParameters.url, true); | |
| if (requestParameters.type === 'arrayBuffer') { | |
| xhr.responseType = 'arraybuffer'; | |
| } | |
| for (const k in requestParameters.headers) { | |
| xhr.setRequestHeader(k, requestParameters.headers[k]); | |
| } | |
| if (requestParameters.type === 'json') { | |
| xhr.responseType = 'text'; | |
| xhr.setRequestHeader('Accept', 'application/json'); | |
| } | |
| xhr.withCredentials = requestParameters.credentials === 'include'; | |
| xhr.onerror = () => { | |
| callback(new Error(xhr.statusText)); | |
| }; | |
| xhr.onload = () => { | |
| if (((xhr.status >= 200 && xhr.status < 300) || xhr.status === 0) && xhr.response !== null) { | |
| let data: mixed = xhr.response; | |
| if (requestParameters.type === 'json') { | |
| // We're manually parsing JSON here to get better error messages. | |
| try { | |
| data = JSON.parse(xhr.response); | |
| } catch (err) { | |
| return callback(err); | |
| } | |
| } | |
| callback(null, data, xhr.getResponseHeader('Cache-Control'), xhr.getResponseHeader('Expires')); | |
| } else { | |
| callback(new AJAXError(xhr.statusText, xhr.status, requestParameters.url)); | |
| } | |
| }; | |
| xhr.send(requestParameters.body); | |
| return {cancel: () => xhr.abort()}; | |
| } | |
| export const makeRequest = function(requestParameters: RequestParameters, callback: ResponseCallback<any>): Cancelable { | |
| // We're trying to use the Fetch API if possible. However, in some situations we can't use it: | |
| // - IE11 doesn't support it at all. In this case, we dispatch the request to the main thread so | |
| // that we can get an accruate referrer header. | |
| // - Safari exposes window.AbortController, but it doesn't work actually abort any requests in | |
| // some versions (see https://bugs.webkit.org/show_bug.cgi?id=174980#c2) | |
| // - Requests for resources with the file:// URI scheme don't work with the Fetch API either. In | |
| // this case we unconditionally use XHR on the current thread since referrers don't matter. | |
| if (!isFileURL(requestParameters.url)) { | |
| if (window.fetch && window.Request && window.AbortController && window.Request.prototype.hasOwnProperty('signal')) { | |
| return makeFetchRequest(requestParameters, callback); | |
| } | |
| if (isWorker() && self.worker && self.worker.actor) { | |
| const queueOnMainThread = true; | |
| return self.worker.actor.send('getResource', requestParameters, callback, undefined, queueOnMainThread); | |
| } | |
| } | |
| return makeXMLHttpRequest(requestParameters, callback); | |
| }; | |
| export const getJSON = function(requestParameters: RequestParameters, callback: ResponseCallback<Object>): Cancelable { | |
| return makeRequest(extend(requestParameters, {type: 'json'}), callback); | |
| }; | |
| export const getArrayBuffer = function(requestParameters: RequestParameters, callback: ResponseCallback<ArrayBuffer>): Cancelable { | |
| return makeRequest(extend(requestParameters, {type: 'arrayBuffer'}), callback); | |
| }; | |
| export const postData = function(requestParameters: RequestParameters, callback: ResponseCallback<string>): Cancelable { | |
| return makeRequest(extend(requestParameters, {method: 'POST'}), callback); | |
| }; | |
| function sameOrigin(url) { | |
| const a: HTMLAnchorElement = window.document.createElement('a'); | |
| a.href = url; | |
| return a.protocol === window.document.location.protocol && a.host === window.document.location.host; | |
| } | |
| const transparentPngUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; | |
| function arrayBufferToImage(data: ArrayBuffer, callback: (err: ?Error, image: ?HTMLImageElement) => void, cacheControl: ?string, expires: ?string) { | |
| const img: HTMLImageElement = new window.Image(); | |
| const URL = window.URL; | |
| img.onload = () => { | |
| callback(null, img); | |
| URL.revokeObjectURL(img.src); | |
| // prevent image dataURI memory leak in Safari; | |
| // but don't free the image immediately because it might be uploaded in the next frame | |
| // https://github.com/mapbox/mapbox-gl-js/issues/10226 | |
| img.onload = null; | |
| window.requestAnimationFrame(() => { img.src = transparentPngUrl; }); | |
| }; | |
| img.onerror = () => callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.')); | |
| const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'}); | |
| (img: any).cacheControl = cacheControl; | |
| (img: any).expires = expires; | |
| img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl; | |
| } | |
| function arrayBufferToImageBitmap(data: ArrayBuffer, callback: (err: ?Error, image: ?ImageBitmap) => void) { | |
| const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'}); | |
| window.createImageBitmap(blob).then((imgBitmap) => { | |
| callback(null, imgBitmap); | |
| }).catch((e) => { | |
| callback(new Error(`Could not load image because of ${e.message}. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.`)); | |
| }); | |
| } | |
| let imageQueue, numImageRequests; | |
| export const resetImageRequestQueue = () => { | |
| imageQueue = []; | |
| numImageRequests = 0; | |
| }; | |
| resetImageRequestQueue(); | |
| export const getImage = function(requestParameters: RequestParameters, callback: Callback<HTMLImageElement | ImageBitmap>): Cancelable { | |
| if (webpSupported.supported) { | |
| if (!requestParameters.headers) { | |
| requestParameters.headers = {}; | |
| } | |
| requestParameters.headers.accept = 'image/webp,*/*'; | |
| } | |
| // limit concurrent image loads to help with raster sources performance on big screens | |
| if (numImageRequests >= config.MAX_PARALLEL_IMAGE_REQUESTS) { | |
| const queued = { | |
| requestParameters, | |
| callback, | |
| cancelled: false, | |
| cancel() { this.cancelled = true; } | |
| }; | |
| imageQueue.push(queued); | |
| return queued; | |
| } | |
| numImageRequests++; | |
| let advanced = false; | |
| const advanceImageRequestQueue = () => { | |
| if (advanced) return; | |
| advanced = true; | |
| numImageRequests--; | |
| assert(numImageRequests >= 0); | |
| while (imageQueue.length && numImageRequests < config.MAX_PARALLEL_IMAGE_REQUESTS) { // eslint-disable-line | |
| const request = imageQueue.shift(); | |
| const {requestParameters, callback, cancelled} = request; | |
| if (!cancelled) { | |
| request.cancel = getImage(requestParameters, callback).cancel; | |
| } | |
| } | |
| }; | |
| // request the image with XHR to work around caching issues | |
| // see https://github.com/mapbox/mapbox-gl-js/issues/1470 | |
| const request = getArrayBuffer(requestParameters, (err: ?Error, data: ?ArrayBuffer, cacheControl: ?string, expires: ?string) => { | |
| advanceImageRequestQueue(); | |
| if (err) { | |
| callback(err); | |
| } else if (data) { | |
| if (offscreenCanvasSupported()) { | |
| arrayBufferToImageBitmap(data, callback); | |
| } else { | |
| arrayBufferToImage(data, callback, cacheControl, expires); | |
| } | |
| } | |
| }); | |
| return { | |
| cancel: () => { | |
| request.cancel(); | |
| advanceImageRequestQueue(); | |
| } | |
| }; | |
| }; | |
| export const getVideo = function(urls: Array<string>, callback: Callback<HTMLVideoElement>): Cancelable { | |
| const video: HTMLVideoElement = window.document.createElement('video'); | |
| video.muted = true; | |
| video.onloadstart = function() { | |
| callback(null, video); | |
| }; | |
| for (let i = 0; i < urls.length; i++) { | |
| const s: HTMLSourceElement = window.document.createElement('source'); | |
| if (!sameOrigin(urls[i])) { | |
| video.crossOrigin = 'Anonymous'; | |
| } | |
| s.src = urls[i]; | |
| video.appendChild(s); | |
| } | |
| return {cancel: () => {}}; | |
| }; | |