UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

1,439 lines (1,356 loc) 2.12 MB
/** * MapLibre GL JS * @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v4.7.0/LICENSE.txt */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.maplibregl = factory()); })(this, (function () { 'use strict'; /* eslint-disable */ var maplibregl = {}; var modules = {}; function define(moduleName, _dependencies, moduleFactory) { modules[moduleName] = moduleFactory; // to get the list of modules see generated dist/maplibre-gl-dev.js file (look for `define(` calls) if (moduleName !== 'index') { return; } // we assume that when an index module is initializing then other modules are loaded already var workerBundleString = 'var sharedModule = {}; (' + modules.shared + ')(sharedModule); (' + modules.worker + ')(sharedModule);' var sharedModule = {}; // the order of arguments of a module factory depends on rollup (it decides who is whose dependency) // to check the correct order, see dist/maplibre-gl-dev.js file (look for `define(` calls) // we assume that for our 3 chunks it will generate 3 modules and their order is predefined like the following modules.shared(sharedModule); modules.index(maplibregl, sharedModule); if (typeof window !== 'undefined') { maplibregl.setWorkerUrl(window.URL.createObjectURL(new Blob([workerBundleString], { type: 'text/javascript' }))); } return maplibregl; }; define('shared', ['exports'], (function (exports) { 'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; function getDefaultExportFromCjs$1 (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var pointGeometry = Point$1; /** * A standalone point geometry with useful accessor, comparison, and * modification methods. * * @class Point * @param {Number} x the x-coordinate. this could be longitude or screen * pixels, or any other sort of unit. * @param {Number} y the y-coordinate. this could be latitude or screen * pixels, or any other sort of unit. * @example * var point = new Point(-77, 38); */ function Point$1(x, y) { this.x = x; this.y = y; } Point$1.prototype = { /** * Clone this point, returning a new point that can be modified * without affecting the old one. * @return {Point} the clone */ clone: function() { return new Point$1(this.x, this.y); }, /** * Add this point's x & y coordinates to another point, * yielding a new point. * @param {Point} p the other point * @return {Point} output point */ add: function(p) { return this.clone()._add(p); }, /** * Subtract this point's x & y coordinates to from point, * yielding a new point. * @param {Point} p the other point * @return {Point} output point */ sub: function(p) { return this.clone()._sub(p); }, /** * Multiply this point's x & y coordinates by point, * yielding a new point. * @param {Point} p the other point * @return {Point} output point */ multByPoint: function(p) { return this.clone()._multByPoint(p); }, /** * Divide this point's x & y coordinates by point, * yielding a new point. * @param {Point} p the other point * @return {Point} output point */ divByPoint: function(p) { return this.clone()._divByPoint(p); }, /** * Multiply this point's x & y coordinates by a factor, * yielding a new point. * @param {Point} k factor * @return {Point} output point */ mult: function(k) { return this.clone()._mult(k); }, /** * Divide this point's x & y coordinates by a factor, * yielding a new point. * @param {Point} k factor * @return {Point} output point */ div: function(k) { return this.clone()._div(k); }, /** * Rotate this point around the 0, 0 origin by an angle a, * given in radians * @param {Number} a angle to rotate around, in radians * @return {Point} output point */ rotate: function(a) { return this.clone()._rotate(a); }, /** * Rotate this point around p point by an angle a, * given in radians * @param {Number} a angle to rotate around, in radians * @param {Point} p Point to rotate around * @return {Point} output point */ rotateAround: function(a,p) { return this.clone()._rotateAround(a,p); }, /** * Multiply this point by a 4x1 transformation matrix * @param {Array<Number>} m transformation matrix * @return {Point} output point */ matMult: function(m) { return this.clone()._matMult(m); }, /** * Calculate this point but as a unit vector from 0, 0, meaning * that the distance from the resulting point to the 0, 0 * coordinate will be equal to 1 and the angle from the resulting * point to the 0, 0 coordinate will be the same as before. * @return {Point} unit vector point */ unit: function() { return this.clone()._unit(); }, /** * Compute a perpendicular point, where the new y coordinate * is the old x coordinate and the new x coordinate is the old y * coordinate multiplied by -1 * @return {Point} perpendicular point */ perp: function() { return this.clone()._perp(); }, /** * Return a version of this point with the x & y coordinates * rounded to integers. * @return {Point} rounded point */ round: function() { return this.clone()._round(); }, /** * Return the magitude of this point: this is the Euclidean * distance from the 0, 0 coordinate to this point's x and y * coordinates. * @return {Number} magnitude */ mag: function() { return Math.sqrt(this.x * this.x + this.y * this.y); }, /** * Judge whether this point is equal to another point, returning * true or false. * @param {Point} other the other point * @return {boolean} whether the points are equal */ equals: function(other) { return this.x === other.x && this.y === other.y; }, /** * Calculate the distance from this point to another point * @param {Point} p the other point * @return {Number} distance */ dist: function(p) { return Math.sqrt(this.distSqr(p)); }, /** * Calculate the distance from this point to another point, * without the square root step. Useful if you're comparing * relative distances. * @param {Point} p the other point * @return {Number} distance */ distSqr: function(p) { var dx = p.x - this.x, dy = p.y - this.y; return dx * dx + dy * dy; }, /** * Get the angle from the 0, 0 coordinate to this point, in radians * coordinates. * @return {Number} angle */ angle: function() { return Math.atan2(this.y, this.x); }, /** * Get the angle from this point to another point, in radians * @param {Point} b the other point * @return {Number} angle */ angleTo: function(b) { return Math.atan2(this.y - b.y, this.x - b.x); }, /** * Get the angle between this point and another point, in radians * @param {Point} b the other point * @return {Number} angle */ angleWith: function(b) { return this.angleWithSep(b.x, b.y); }, /* * Find the angle of the two vectors, solving the formula for * the cross product a x b = |a||b|sin(θ) for θ. * @param {Number} x the x-coordinate * @param {Number} y the y-coordinate * @return {Number} the angle in radians */ angleWithSep: function(x, y) { return Math.atan2( this.x * y - this.y * x, this.x * x + this.y * y); }, _matMult: function(m) { var x = m[0] * this.x + m[1] * this.y, y = m[2] * this.x + m[3] * this.y; this.x = x; this.y = y; return this; }, _add: function(p) { this.x += p.x; this.y += p.y; return this; }, _sub: function(p) { this.x -= p.x; this.y -= p.y; return this; }, _mult: function(k) { this.x *= k; this.y *= k; return this; }, _div: function(k) { this.x /= k; this.y /= k; return this; }, _multByPoint: function(p) { this.x *= p.x; this.y *= p.y; return this; }, _divByPoint: function(p) { this.x /= p.x; this.y /= p.y; return this; }, _unit: function() { this._div(this.mag()); return this; }, _perp: function() { var y = this.y; this.y = this.x; this.x = -y; return this; }, _rotate: function(angle) { var cos = Math.cos(angle), sin = Math.sin(angle), x = cos * this.x - sin * this.y, y = sin * this.x + cos * this.y; this.x = x; this.y = y; return this; }, _rotateAround: function(angle, p) { var cos = Math.cos(angle), sin = Math.sin(angle), x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y), y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y); this.x = x; this.y = y; return this; }, _round: function() { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; } }; /** * Construct a point from an array if necessary, otherwise if the input * is already a Point, or an unknown type, return it unchanged * @param {Array<Number>|Point|*} a any kind of input value * @return {Point} constructed point, or passed-through value. * @example * // this * var point = Point.convert([0, 1]); * // is equivalent to * var point = new Point(0, 1); */ Point$1.convert = function (a) { if (a instanceof Point$1) { return a; } if (Array.isArray(a)) { return new Point$1(a[0], a[1]); } return a; }; var Point$2 = /*@__PURE__*/getDefaultExportFromCjs$1(pointGeometry); var unitbezier$1 = UnitBezier$2; function UnitBezier$2(p1x, p1y, p2x, p2y) { // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1). this.cx = 3.0 * p1x; this.bx = 3.0 * (p2x - p1x) - this.cx; this.ax = 1.0 - this.cx - this.bx; this.cy = 3.0 * p1y; this.by = 3.0 * (p2y - p1y) - this.cy; this.ay = 1.0 - this.cy - this.by; this.p1x = p1x; this.p1y = p1y; this.p2x = p2x; this.p2y = p2y; } UnitBezier$2.prototype = { sampleCurveX: function (t) { // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. return ((this.ax * t + this.bx) * t + this.cx) * t; }, sampleCurveY: function (t) { return ((this.ay * t + this.by) * t + this.cy) * t; }, sampleCurveDerivativeX: function (t) { return (3.0 * this.ax * t + 2.0 * this.bx) * t + this.cx; }, solveCurveX: function (x, epsilon) { if (epsilon === undefined) epsilon = 1e-6; if (x < 0.0) return 0.0; if (x > 1.0) return 1.0; var t = x; // First try a few iterations of Newton's method - normally very fast. for (var i = 0; i < 8; i++) { var x2 = this.sampleCurveX(t) - x; if (Math.abs(x2) < epsilon) return t; var d2 = this.sampleCurveDerivativeX(t); if (Math.abs(d2) < 1e-6) break; t = t - x2 / d2; } // Fall back to the bisection method for reliability. var t0 = 0.0; var t1 = 1.0; t = x; for (i = 0; i < 20; i++) { x2 = this.sampleCurveX(t); if (Math.abs(x2 - x) < epsilon) break; if (x > x2) { t0 = t; } else { t1 = t; } t = (t1 - t0) * 0.5 + t0; } return t; }, solve: function (x, epsilon) { return this.sampleCurveY(this.solveCurveX(x, epsilon)); } }; var UnitBezier$3 = /*@__PURE__*/getDefaultExportFromCjs$1(unitbezier$1); let supportsOffscreenCanvas; function offscreenCanvasSupported() { if (supportsOffscreenCanvas == null) { supportsOffscreenCanvas = typeof OffscreenCanvas !== 'undefined' && new OffscreenCanvas(1, 1).getContext('2d') && typeof createImageBitmap === 'function'; } return supportsOffscreenCanvas; } let offscreenCanvasDistorted; /** * Some browsers don't return the exact pixels from a canvas to prevent user fingerprinting (see #3185). * This function writes pixels to an OffscreenCanvas and reads them back using getImageData, returning false * if they don't match. * * @returns true if the browser supports OffscreenCanvas but it distorts getImageData results, false otherwise. */ function isOffscreenCanvasDistorted() { if (offscreenCanvasDistorted == null) { offscreenCanvasDistorted = false; if (offscreenCanvasSupported()) { const size = 5; const canvas = new OffscreenCanvas(size, size); const context = canvas.getContext('2d', { willReadFrequently: true }); if (context) { // fill each pixel with an RGB value that should make the byte at index i equal to i (except alpha channel): // [0, 1, 2, 255, 4, 5, 6, 255, 8, 9, 10, 255, ...] for (let i = 0; i < size * size; i++) { const base = i * 4; context.fillStyle = `rgb(${base},${base + 1},${base + 2})`; context.fillRect(i % size, Math.floor(i / size), 1, 1); } const data = context.getImageData(0, 0, size, size).data; for (let i = 0; i < size * size * 4; i++) { if (i % 4 !== 3 && data[i] !== i) { offscreenCanvasDistorted = true; break; } } } } } return offscreenCanvasDistorted || false; } /** * For a given collection of 2D points, returns their axis-aligned bounding box, * in the format [minX, minY, maxX, maxY]. */ function getAABB(points) { let tlX = Infinity; let tlY = Infinity; let brX = -Infinity; let brY = -Infinity; for (const p of points) { tlX = Math.min(tlX, p.x); tlY = Math.min(tlY, p.y); brX = Math.max(brX, p.x); brY = Math.max(brY, p.y); } return [tlX, tlY, brX, brY]; } /** * Given a value `t` that varies between 0 and 1, return * an interpolation function that eases between 0 and 1 in a pleasing * cubic in-out fashion. */ function easeCubicInOut(t) { if (t <= 0) return 0; if (t >= 1) return 1; const t2 = t * t, t3 = t2 * t; return 4 * (t < 0.5 ? t3 : 3 * (t - t2) + t3 - 0.75); } /** * Given given (x, y), (x1, y1) control points for a bezier curve, * return a function that interpolates along that curve. * * @param p1x - control point 1 x coordinate * @param p1y - control point 1 y coordinate * @param p2x - control point 2 x coordinate * @param p2y - control point 2 y coordinate */ function bezier(p1x, p1y, p2x, p2y) { const bezier = new UnitBezier$3(p1x, p1y, p2x, p2y); return (t) => { return bezier.solve(t); }; } /** * A default bezier-curve powered easing function with * control points (0.25, 0.1) and (0.25, 1) */ const defaultEasing = bezier(0.25, 0.1, 0.25, 1); /** * constrain n to the given range via min + max * * @param n - value * @param min - the minimum value to be returned * @param max - the maximum value to be returned * @returns the clamped value */ function clamp$1(n, min, max) { return Math.min(max, Math.max(min, n)); } /** * constrain n to the given range, excluding the minimum, via modular arithmetic * * @param n - value * @param min - the minimum value to be returned, exclusive * @param max - the maximum value to be returned, inclusive * @returns constrained number */ function wrap(n, min, max) { const d = max - min; const w = ((n - min) % d + d) % d + min; return (w === min) ? max : w; } /** * Compute the difference between the keys in one object and the keys * in another object. * * @returns keys difference */ function keysDifference(obj, other) { const difference = []; for (const i in obj) { if (!(i in other)) { difference.push(i); } } return difference; } function extend(dest, ...sources) { for (const src of sources) { for (const k in src) { dest[k] = src[k]; } } return dest; } /** * Given an object and a number of properties as strings, return version * of that object with only those properties. * * @param src - the object * @param properties - an array of property names chosen * to appear on the resulting object. * @returns object with limited properties. * @example * ```ts * let foo = { name: 'Charlie', age: 10 }; * let justName = pick(foo, ['name']); // justName = { name: 'Charlie' } * ``` */ function pick(src, properties) { const result = {}; for (let i = 0; i < properties.length; i++) { const k = properties[i]; if (k in src) { result[k] = src[k]; } } return result; } let id = 1; /** * Return a unique numeric id, starting at 1 and incrementing with * each call. * * @returns unique numeric id. */ function uniqueId() { return id++; } /** * Return whether a given value is a power of two */ function isPowerOfTwo(value) { return (Math.log(value) / Math.LN2) % 1 === 0; } /** * Return the next power of two, or the input value if already a power of two */ function nextPowerOfTwo(value) { if (value <= 1) return 1; return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2)); } /** * Create an object by mapping all the values of an existing object while * preserving their keys. */ function mapObject(input, iterator, context) { const output = {}; for (const key in input) { output[key] = iterator.call(this, input[key], key, input); } return output; } /** * Create an object by filtering out values of an existing object. */ function filterObject(input, iterator, context) { const output = {}; for (const key in input) { if (iterator.call(this, input[key], key, input)) { output[key] = input[key]; } } return output; } /** * Deeply compares two object literals. * @param a - first object literal to be compared * @param b - second object literal to be compared * @returns true if the two object literals are deeply equal, false otherwise */ function deepEqual$1(a, b) { if (Array.isArray(a)) { if (!Array.isArray(b) || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!deepEqual$1(a[i], b[i])) return false; } return true; } if (typeof a === 'object' && a !== null && b !== null) { if (!(typeof b === 'object')) return false; const keys = Object.keys(a); if (keys.length !== Object.keys(b).length) return false; for (const key in a) { if (!deepEqual$1(a[key], b[key])) return false; } return true; } return a === b; } /** * Deeply clones two objects. */ function clone$1(input) { if (Array.isArray(input)) { return input.map(clone$1); } else if (typeof input === 'object' && input) { return mapObject(input, clone$1); } else { return input; } } /** * Check if two arrays have at least one common element. */ function arraysIntersect(a, b) { for (let l = 0; l < a.length; l++) { if (b.indexOf(a[l]) >= 0) return true; } return false; } /** * Print a warning message to the console and ensure duplicate warning messages * are not printed. */ const warnOnceHistory = {}; function warnOnce(message) { if (!warnOnceHistory[message]) { // console isn't defined in some WebWorkers, see #2558 if (typeof console !== 'undefined') console.warn(message); warnOnceHistory[message] = true; } } /** * Indicates if the provided Points are in a counter clockwise (true) or clockwise (false) order * * @returns true for a counter clockwise set of points */ // https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/ function isCounterClockwise(a, b, c) { return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x); } /** * For two lines a and b in 2d space, defined by any two points along the lines, * find the intersection point, or return null if the lines are parallel * * @param a1 - First point on line a * @param a2 - Second point on line a * @param b1 - First point on line b * @param b2 - Second point on line b * * @returns the intersection point of the two lines or null if they are parallel */ function findLineIntersection(a1, a2, b1, b2) { const aDeltaY = a2.y - a1.y; const aDeltaX = a2.x - a1.x; const bDeltaY = b2.y - b1.y; const bDeltaX = b2.x - b1.x; const denominator = (bDeltaY * aDeltaX) - (bDeltaX * aDeltaY); if (denominator === 0) { // Lines are parallel return null; } const originDeltaY = a1.y - b1.y; const originDeltaX = a1.x - b1.x; const aInterpolation = (bDeltaX * originDeltaY - bDeltaY * originDeltaX) / denominator; // Find intersection by projecting out from origin of first segment return new Point$2(a1.x + (aInterpolation * aDeltaX), a1.y + (aInterpolation * aDeltaY)); } /** * Converts spherical coordinates to cartesian coordinates. * * @param spherical - Spherical coordinates, in [radial, azimuthal, polar] * @returns cartesian coordinates in [x, y, z] */ function sphericalToCartesian([r, azimuthal, polar]) { // We abstract "north"/"up" (compass-wise) to be 0° when really this is 90° (π/2): // correct for that here azimuthal += 90; // Convert azimuthal and polar angles to radians azimuthal *= Math.PI / 180; polar *= Math.PI / 180; return { x: r * Math.cos(azimuthal) * Math.sin(polar), y: r * Math.sin(azimuthal) * Math.sin(polar), z: r * Math.cos(polar) }; } /** * Returns true if the when run in the web-worker context. * * @returns `true` if the when run in the web-worker context. */ function isWorker(self) { // @ts-ignore return typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && self instanceof WorkerGlobalScope; } /** * Parses data from 'Cache-Control' headers. * * @param cacheControl - Value of 'Cache-Control' header * @returns object containing parsed header info. */ function parseCacheControl(cacheControl) { // Taken from [Wreck](https://github.com/hapijs/wreck) const re = /(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g; const header = {}; cacheControl.replace(re, ($0, $1, $2, $3) => { const value = $2 || $3; header[$1] = value ? value.toLowerCase() : true; return ''; }); if (header['max-age']) { const maxAge = parseInt(header['max-age'], 10); if (isNaN(maxAge)) delete header['max-age']; else header['max-age'] = maxAge; } return header; } let _isSafari = null; /** * Returns true when run in WebKit derived browsers. * This is used as a workaround for a memory leak in Safari caused by using Transferable objects to * transfer data between WebWorkers and the main thread. * https://github.com/mapbox/mapbox-gl-js/issues/8771 * * This should be removed once the underlying Safari issue is fixed. * * @param scope - Since this function is used both on the main thread and WebWorker context, * let the calling scope pass in the global scope object. * @returns `true` when run in WebKit derived browsers. */ function isSafari(scope) { if (_isSafari == null) { const userAgent = scope.navigator ? scope.navigator.userAgent : null; _isSafari = !!scope.safari || !!(userAgent && (/\b(iPad|iPhone|iPod)\b/.test(userAgent) || (!!userAgent.match('Safari') && !userAgent.match('Chrome')))); } return _isSafari; } function isImageBitmap(image) { return typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap; } /** * Converts an ArrayBuffer to an ImageBitmap. * * Used mostly for testing purposes only, because mocking libs don't know how to work with ArrayBuffers, but work * perfectly fine with ImageBitmaps. Might also be used for environments (other than testing) not supporting * ArrayBuffers. * * @param data - Data to convert * @returns - A promise resolved when the conversion is finished */ const arrayBufferToImageBitmap = (data) => __awaiter(void 0, void 0, void 0, function* () { if (data.byteLength === 0) { return createImageBitmap(new ImageData(1, 1)); } const blob = new Blob([new Uint8Array(data)], { type: 'image/png' }); try { return createImageBitmap(blob); } catch (e) { throw 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.`); } }); const transparentPngUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; /** * Converts an ArrayBuffer to an HTMLImageElement. * * Used mostly for testing purposes only, because mocking libs don't know how to work with ArrayBuffers, but work * perfectly fine with ImageBitmaps. Might also be used for environments (other than testing) not supporting * ArrayBuffers. * * @param data - Data to convert * @returns - A promise resolved when the conversion is finished */ const arrayBufferToImage = (data) => { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { resolve(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 = () => reject(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 = new Blob([new Uint8Array(data)], { type: 'image/png' }); img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl; }); }; /** * Computes the webcodecs VideoFrame API options to select a rectangle out of * an image and write it into the destination rectangle. * * Rect (x/y/width/height) select the overlapping rectangle from the source image * and layout (offset/stride) write that overlapping rectangle to the correct place * in the destination image. * * Offset is the byte offset in the dest image that the first pixel appears at * and stride is the number of bytes to the start of the next row: * ┌───────────┐ * │ dest │ * │ ┌───┼───────┐ * │offset→│▓▓▓│ source│ * │ │▓▓▓│ │ * │ └───┼───────┘ * │stride ⇠╌╌╌│ * │╌╌╌╌╌╌→ │ * └───────────┘ * * @param image - source image containing a width and height attribute * @param x - top-left x coordinate to read from the image * @param y - top-left y coordinate to read from the image * @param width - width of the rectangle to read from the image * @param height - height of the rectangle to read from the image * @returns the layout and rect options to pass into VideoFrame API */ function computeVideoFrameParameters(image, x, y, width, height) { const destRowOffset = Math.max(-x, 0) * 4; const firstSourceRow = Math.max(0, y); const firstDestRow = firstSourceRow - y; const offset = firstDestRow * width * 4 + destRowOffset; const stride = width * 4; const sourceLeft = Math.max(0, x); const sourceTop = Math.max(0, y); const sourceRight = Math.min(image.width, x + width); const sourceBottom = Math.min(image.height, y + height); return { rect: { x: sourceLeft, y: sourceTop, width: sourceRight - sourceLeft, height: sourceBottom - sourceTop }, layout: [{ offset, stride }] }; } /** * Reads pixels from an ImageBitmap/Image/canvas using webcodec VideoFrame API. * * @param data - image, imagebitmap, or canvas to parse * @param x - top-left x coordinate to read from the image * @param y - top-left y coordinate to read from the image * @param width - width of the rectangle to read from the image * @param height - height of the rectangle to read from the image * @returns a promise containing the parsed RGBA pixel values of the image, or the error if an error occurred */ function readImageUsingVideoFrame(image, x, y, width, height) { return __awaiter(this, void 0, void 0, function* () { if (typeof VideoFrame === 'undefined') { throw new Error('VideoFrame not supported'); } const frame = new VideoFrame(image, { timestamp: 0 }); try { const format = frame === null || frame === void 0 ? void 0 : frame.format; if (!format || !(format.startsWith('BGR') || format.startsWith('RGB'))) { throw new Error(`Unrecognized format ${format}`); } const swapBR = format.startsWith('BGR'); const result = new Uint8ClampedArray(width * height * 4); yield frame.copyTo(result, computeVideoFrameParameters(image, x, y, width, height)); if (swapBR) { for (let i = 0; i < result.length; i += 4) { const tmp = result[i]; result[i] = result[i + 2]; result[i + 2] = tmp; } } return result; } finally { frame.close(); } }); } let offscreenCanvas; let offscreenCanvasContext; /** * Reads pixels from an ImageBitmap/Image/canvas using OffscreenCanvas * * @param data - image, imagebitmap, or canvas to parse * @param x - top-left x coordinate to read from the image * @param y - top-left y coordinate to read from the image * @param width - width of the rectangle to read from the image * @param height - height of the rectangle to read from the image * @returns a promise containing the parsed RGBA pixel values of the image, or the error if an error occurred */ function readImageDataUsingOffscreenCanvas(imgBitmap, x, y, width, height) { const origWidth = imgBitmap.width; const origHeight = imgBitmap.height; // Lazily initialize OffscreenCanvas if (!offscreenCanvas || !offscreenCanvasContext) { // Dem tiles are typically 256x256 offscreenCanvas = new OffscreenCanvas(origWidth, origHeight); offscreenCanvasContext = offscreenCanvas.getContext('2d', { willReadFrequently: true }); } offscreenCanvas.width = origWidth; offscreenCanvas.height = origHeight; offscreenCanvasContext.drawImage(imgBitmap, 0, 0, origWidth, origHeight); const imgData = offscreenCanvasContext.getImageData(x, y, width, height); offscreenCanvasContext.clearRect(0, 0, origWidth, origHeight); return imgData.data; } /** * Reads RGBA pixels from an preferring OffscreenCanvas, but falling back to VideoFrame if supported and * the browser is mangling OffscreenCanvas getImageData results. * * @param data - image, imagebitmap, or canvas to parse * @param x - top-left x coordinate to read from the image * @param y - top-left y coordinate to read from the image * @param width - width of the rectangle to read from the image * @param height - height of the rectangle to read from the image * @returns a promise containing the parsed RGBA pixel values of the image */ function getImageData(image, x, y, width, height) { return __awaiter(this, void 0, void 0, function* () { if (isOffscreenCanvasDistorted()) { try { return yield readImageUsingVideoFrame(image, x, y, width, height); } catch (e) { // fall back to OffscreenCanvas } } return readImageDataUsingOffscreenCanvas(image, x, y, width, height); }); } /** * This method is used in order to register an event listener using a lambda function. * The return value will allow unsubscribing from the event, without the need to store the method reference. * @param target - The target * @param message - The message * @param listener - The listener * @param options - The options * @returns a subscription object that can be used to unsubscribe from the event */ function subscribe(target, message, listener, options) { target.addEventListener(message, listener, options); return { unsubscribe: () => { target.removeEventListener(message, listener, options); } }; } /** * This method converts degrees to radians. * The return value is the radian value. * @param degrees - The number of degrees * @returns radians */ function degreesToRadians(degrees) { return degrees * Math.PI / 180; } /** * The maximum world tile zoom (Z). * In other words, the upper bound supported for tile zoom. */ const MAX_TILE_ZOOM = 25; /** * The minimum world tile zoom (Z). * In other words, the lower bound supported for tile zoom. */ const MIN_TILE_ZOOM = 0; /** * An error message to use when an operation is aborted */ const ABORT_ERROR = 'AbortError'; /** * Check if an error is an abort error * @param error - An error object * @returns - true if the error is an abort error */ function isAbortError(error) { return error.message === ABORT_ERROR; } /** * Use this when you need to create an abort error. * @returns An error object with the message "AbortError" */ function createAbortError() { return new Error(ABORT_ERROR); } const config = { MAX_PARALLEL_IMAGE_REQUESTS: 16, MAX_PARALLEL_IMAGE_REQUESTS_PER_FRAME: 8, MAX_TILE_CACHE_ZOOM_LEVELS: 5, REGISTERED_PROTOCOLS: {}, WORKER_URL: '' }; function getProtocol(url) { return config.REGISTERED_PROTOCOLS[url.substring(0, url.indexOf('://'))]; } /** * Adds a custom load resource function that will be called when using a URL that starts with a custom url schema. * This will happen in the main thread, and workers might call it if they don't know how to handle the protocol. * The example below will be triggered for custom:// urls defined in the sources list in the style definitions. * The function passed will receive the request parameters and should return with the resulting resource, * for example a pbf vector tile, non-compressed, represented as ArrayBuffer. * * @param customProtocol - the protocol to hook, for example 'custom' * @param loadFn - the function to use when trying to fetch a tile specified by the customProtocol * @example * ```ts * // This will fetch a file using the fetch API (this is obviously a non interesting example...) * addProtocol('custom', async (params, abortController) => { * const t = await fetch(`https://${params.url.split("://")[1]}`); * if (t.status == 200) { * const buffer = await t.arrayBuffer(); * return {data: buffer} * } else { * throw new Error(`Tile fetch error: ${t.statusText}`); * } * }); * // the following is an example of a way to return an error when trying to load a tile * addProtocol('custom2', async (params, abortController) => { * throw new Error('someErrorMessage')); * }); * ``` */ function addProtocol(customProtocol, loadFn) { config.REGISTERED_PROTOCOLS[customProtocol] = loadFn; } /** * Removes a previously added protocol in the main thread. * * @param customProtocol - the custom protocol to remove registration for * @example * ```ts * removeProtocol('custom'); * ``` */ function removeProtocol(customProtocol) { delete config.REGISTERED_PROTOCOLS[customProtocol]; } /** * This is used to identify the global dispatcher id when sending a message from the worker without a target map id. */ const GLOBAL_DISPATCHER_ID = 'global-dispatcher'; /** * An error thrown when a HTTP request results in an error response. */ class AJAXError extends Error { /** * @param status - The response's HTTP status code. * @param statusText - The response's HTTP status text. * @param url - The request's URL. * @param body - The response's body. */ constructor(status, statusText, url, body) { super(`AJAXError: ${statusText} (${status}): ${url}`); this.status = status; this.statusText = statusText; this.url = url; this.body = body; } } /** * 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), * and we will set an empty referrer. Otherwise, we're using the document's URL. */ const getReferrer = () => isWorker(self) ? 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. * @param url - The URL to check * @returns `true` if the URL is a file:// URL, `false` otherwise */ const isFileURL = url => /^file:/.test(url) || (/^file:/.test(getReferrer()) && !/^\w+:/.test(url)); function makeFetchRequest(requestParameters, abortController) { return __awaiter(this, void 0, void 0, function* () { const request = new Request(requestParameters.url, { method: requestParameters.method || 'GET', body: requestParameters.body, credentials: requestParameters.credentials, headers: requestParameters.headers, cache: requestParameters.cache, referrer: getReferrer(), signal: abortController.signal }); // If the user has already set an Accept header, do not overwrite it here if (requestParameters.type === 'json' && !request.headers.has('Accept')) { request.headers.set('Accept', 'application/json'); } const response = yield fetch(request); if (!response.ok) { const body = yield response.blob(); throw new AJAXError(response.status, response.statusText, requestParameters.url, body); } let parsePromise; if ((requestParameters.type === 'arrayBuffer' || requestParameters.type === 'image')) { parsePromise = response.arrayBuffer(); } else if (requestParameters.type === 'json') { parsePromise = response.json(); } else { parsePromise = response.text(); } const result = yield parsePromise; if (abortController.signal.aborted) { throw createAbortError(); } return { data: result, cacheControl: response.headers.get('Cache-Control'), expires: response.headers.get('Expires') }; }); } function makeXMLHttpRequest(requestParameters, abortController) { return new Promise((resolve, reject) => { var _a; const xhr = new XMLHttpRequest(); xhr.open(requestParameters.method || 'GET', requestParameters.url, true); if (requestParameters.type === 'arrayBuffer' || requestParameters.type === 'image') { xhr.responseType = 'arraybuffer'; } for (const k in requestParameters.headers) { xhr.setRequestHeader(k, requestParameters.headers[k]); } if (requestParameters.type === 'json') { xhr.responseType = 'text'; // Do not overwrite the user-provided Accept header if (!((_a = requestParameters.headers) === null || _a === void 0 ? void 0 : _a.Accept)) { xhr.setRequestHeader('Accept', 'application/json'); } } xhr.withCredentials = requestParameters.credentials === 'include'; xhr.onerror = () => { reject(new Error(xhr.statusText)); }; xhr.onload = () => { if (abortController.signal.aborted) { return; } if (((xhr.status >= 200 && xhr.status < 300) || xhr.status === 0) && xhr.response !== null) { let data = 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) { reject(err); return; } } resolve({ data, cacheControl: xhr.getResponseHeader('Cache-Control'), expires: xhr.getResponseHeader('Expires') }); } else { const body = new Blob([xhr.response], { type: xhr.getResponseHeader('Content-Type') }); reject(new AJAXError(xhr.status, xhr.statusText, requestParameters.url, body)); } }; abortController.signal.addEventListener('abort', () => { xhr.abort(); reject(createAbortError()); }); xhr.send(requestParameters.body); }); } /** * We're trying to use the Fetch API if possible. However, requests for resources with the file:// URI scheme don't work with the Fetch API. * In this case we unconditionally use XHR on the current thread since referrers don't matter. * This method can also use the registered method if `addProtocol` was called. * @param requestParameters - The request parameters * @param abortController - The abort controller allowing to cancel the request * @returns a promise resolving to the response, including cache control and expiry data */ const makeRequest = function (requestParameters, abortController) { if (/:\/\//.test(requestParameters.url) && !(/^https?:|^file:/.test(requestParameters.url))) { const protocolLoadFn = getProtocol(requestParameters.url); if (protocolLoadFn) { return protocolLoadFn(requestParameters, abortController); } if (isWorker(self) && self.worker && self.worker.actor) { return self.worker.actor.sendAsync({ type: "GR" /* MessageType.getResource */, data: requestParameters, targetMapId: GLOBAL_DISPATCHER_ID }, abortController); } } if (!isFileURL(requestParameters.url)) { if (fetch && Request && AbortController && Object.prototype.hasOwnProperty.call(Request.prototype, 'signal')) { return makeFetchRequest(requestParameters, abortController); } if (isWorker(self) && self.worker && self.worker.actor) { return self.worker.actor.sendAsync({ type: "GR" /* MessageType.getResource */, data: requestParameters, mustQueue: true, targetMapId: GLOBAL_DISPATCHER_ID }, abortController); } } return makeXMLHttpRequest(requestParameters, abortController); }; const getJSON = (requestParameters, abortController) => { return makeRequest(extend(requestParameters, { type: 'json' }), abortController); }; const getArrayBuffer = (requestParameters, abortController) => { return makeRequest(extend(requestParameters, { type: 'arrayBuffer' }), abortController); }; function sameOrigin(inComingUrl) { // A relative URL "/foo" or "./foo" will throw exception in URL's ctor, // try-catch is expansive so just use a heuristic check to avoid it // also check data URL if (!inComingUrl || inComingUrl.indexOf('://') <= 0 || // relative URL inComingUrl.indexOf('data:image/') === 0 || // data image URL inComingUrl.indexOf('blob:') === 0) { // blob return true; } const urlObj = new URL(inComingUrl); const locationObj = window.location; return urlObj.protocol === locationObj.protocol && urlObj.host === locationObj.host; } const getVideo = (urls) => { const video = window.document.createElement('video'); video.muted = true; return new Promise((resolve) => { video.onloadstart = () => { resolve(video); }; for (const url of urls) { const s = window.document.createElement('source'); if (!sameOrigin(url)) { video.crossOrigin = 'Anonymous'; } s.src = url; video.appendChild(s); } }); }; function _addEventListener(type, listener, listenerList) { const listenerExists = listenerList[type] && listenerList[type].indexOf(listener) !== -1; if (!listenerExists) { listenerList[type] = listenerList[type] || []; listenerList[type].push(listener); } } function _removeEventListener(type, listener, listenerList) { if (listenerList && listenerList[type]) { const index = listenerList[type].indexOf(listener); if (index !== -1) { listenerList[type].splice(index, 1); } } } /** * The event class */ class Event { constructor(type, data = {}) { extend(this, data); this.type = type; } } /** * An error event */ class ErrorEvent extends Event { constructor(error, data = {}) { super('error', extend({ error }, data)); } } /** * Methods mixed in to other classes for event capabilities. * * @group Event Related */ class Evented { /** * Adds a listener to a specified event type. * * @param type - The event type to add a listen for. * @param listener - The function to be called when the event is fired. * The listener function is called with the data object passed to `fire`, * extended with `target` and `type` properties. */ on(type, listener) { this._listeners = this._listeners || {}; _addEventListener(type, listener, this._listeners); return this; } /** * Removes a previously registered event listener. * * @param type - The event type to remove listeners for. * @param listener - The listener function to remove. */ off(type, listener) { _removeEventListener(type, listener, this._listeners); _removeEventListener(type, listener, this._oneTimeListeners); return this; } /** * Adds a listener that will be called only once to a specified event type. * * The listener will be called first time the event fires after the listener is registered. * * @param type - The event type to listen for. * @param listener - The function to be called when the event is fired the first time. * @returns `this` or a promise if a listener is not provided */ once(type, listener) { if (!listener) { return new Promise((resolve) => this.once(type, resolve)); } this._oneTimeListeners = this._oneTimeListeners || {}; _addEventListener(type, listener, this._oneTimeListeners); return this; } fire(event, properties) { // Compatibility with (type: string, properties: Object) signature from previous versions. // See https://github.com/mapbox/mapbox-gl-js/issues/6522, // https://github.com/mapbox/