UNPKG

maplibre-gl

Version:

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

1,427 lines 1.15 MB
/** * MapLibre GL JS * @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v6.2.0/LICENSE.txt */ import { $ as tileCoordinatesToMercatorCoordinates, $n as degreesToRadians, $r as clone, $t as EvaluationParameters, A as getOverlapMode, Ai as scale, An as sameOrigin, Ar as rollPitchBearingToQuat, At as Uniform1i, Bn as angleToRotateBetweenVectors2D, Br as pixelsToTileUnits, Bt as CollisionCircleLayoutArray, C as createStyleLayer, Ci as invert$1, Cn as AJAXError, Cr as pick, Ct as renderColorRamp, D as isSymbolStyleLayer, Di as rotateX, Dn as getReferrer, Dr as readImageUsingVideoFrame, Dt as polygonIntersectsPolygon, E as isBackgroundStyleLayer, Ei as perspective, En as getJSON, Er as rayPlaneIntersection, Et as isCircleStyleLayer, Fi as invert, Fn as AbortError, Fr as translatePosition, Ft as UniformColorArray, G as isFillExtrusionStyleLayer, Gn as clone$2, Gr as sqrLen, Gt as RasterBoundsArray, H as isLineStyleLayer, Hn as arrayBufferToImageBitmap, Hr as create$3, Ht as Pos3dArray, I as ImagePosition, Ii as rotate, In as isAbortError, Ir as uniqueId, It as UniformFloatArray, J as cameraDirectionFromPitchBearing, Jn as createMat4f64, Jr as slerp, Jt as isRasterStyleLayer, Kn as createIdentityMat4f32, Kr as zero, Kt as TriangleIndexArray, L as potpack, Li as isOffscreenCanvasDistorted, Ln as throwIfAborted, Lr as warnOnce, Lt as UniformMatrix4f, M as evaluateSizeForZoom, Mi as create$4, Mn as getProtocol, Mr as sphericalToCartesian, Mt as Uniform3f, Ni as fromRotation, Nn as removeProtocol, Nr as subscribe, Nt as Uniform4f, O as SymbolBucket, Oi as rotateY$1, On as getVideo, Or as remapSaturate, Ot as toEvaluationFeature, P as getAnchorAlignment, Pi as create$2, Pn as config, Pr as threePlaneIntersection, Pt as UniformColor, Q as projectToWorldCoordinates, Qn as defaultEasing, Qr as add, Qt as Transitionable, R as renderStyleImage, Ri as offscreenCanvasSupported, Rr as wrap, Rt as SegmentVector, S as Actor, Si as identity, Sn as Evented, Sr as parseCacheControl, St as isHeatmapStyleLayer, T as validateCustomStyleLayer, Ti as ortho, Tn as getArrayBuffer, Tr as radiansToDegrees, Tt as RGBAImage, Un as bezier, Ur as length, Ut as PosArray, V as collisionCircleLayout, Vn as arrayBufferToImage, Vr as EXTENT, Vt as LineStripIndexArray, Wn as clamp, Wr as scale$1, Wt as QuadTriangleArray, X as getMercatorHorizon, Xn as createVec4f64, Xr as scale$3, Xt as Properties, Y as cameraMercatorCoordinateFromCenterAndRotation, Yn as createVec3f64, Yr as mul, Yt as DataConstantProperty, Z as maxMercatorHorizonAngle, Zn as deepEqual, Zr as transformMat4, Zt as TRANSITION_SUFFIX, _ as OverscaledTileID, _i as copy, _r as isWorker, _t as isColorReliefStyleLayer, a as clipLine, ai as normalize, an as emitValidationErrors, ar as filterObject, at as mercatorXfromLng, b as compareTileId, bi as exactEquals, bn as ErrorEvent, br as mod, bt as isHillshadeStyleLayer, ci as rotateZ$1, cn as validateStyleAndEmit, cr as getAngleDelta, ct as LngLat, di as sub, dn as ValidationError, dr as getRollPitchBearing, dt as isFillStyleLayer, ei as cross, er as differenceOfAnglesDegrees, et as unprojectFromWorldCoordinates, f as GEOJSON_TILE_LAYER_NAME, fi as transformMat3, fr as isImageBitmap, g as CanonicalTileID, gi as clone$1, gn as featureFilter, gr as isTouchableOrPointableType, gt as SubdivisionGranularitySetting, h as Bounds, hi as zero$1, hn as emptyStyle, hr as isTouchableEvent, ht as SubdivisionGranularityExpression, ii as negate, in as SPEC_SOURCE_TYPES, ir as extend, it as lngFromMercatorX, j as evaluateSizeForFeature, ji as translate, jn as addProtocol, jr as scaleZoom, jt as Uniform2f, k as addDynamicAttributes, ki as rotateZ, kn as makeRequest, kr as rollPitchBearingEqual, kt as Uniform1f, li as scale$2, ln as Color, lr as getEdgeTiles, lt as earthRadius, mi as transformQuat, mn as diff, mr as isSafari, mt as SOUTH_POLE_Y, ni as len, nn as ZoomHistory, nr as ensureError, nt as altitudeFromMercatorZ, oi as rotateX$1, on as validateAndEmit, or as findLineIntersection, ot as mercatorYfromLat, pi as transformMat4$1, pn as derefLayers, pr as isPointableEvent, pt as NORTH_POLE_Y, q as calculateTileMatrix, qn as createIdentityMat4f64, qr as fromEuler, qt as createLayout, r as TextAnchorEnum, ri as length$1, rr as evaluateZoomSnap, rt as latFromMercatorY, s as TileCache, si as rotateY, sn as validateStyle, sr as getAABB, st as mercatorZfromAltitude, t as getAnchorJustification, ti as dot, tn as codePointUsesLocalIdeographFontFamily, tr as distanceOfAnglesRadians, tt as MercatorCoordinate, u as GeoJSONFeature, ui as scaleAndAdd, un as ProjectionDefinition, v as UnwrappedTileID, vi as create$1, vn as interpolateFactory, vr as lerp, w as isCustomStyleLayer, wi as multiply, wn as GLOBAL_DISPATCHER_ID, wr as pointPlaneSignedDistance, wt as AlphaImage, x as isInBoundsForZoomLngLat, xi as fromScaling, xn as Event, xr as nextPowerOfTwo, xt as HEATMAP_FULL_RENDER_FBO_KEY, y as calculateTileKey, yi as equals, yn as latest, yr as mapObject, yt as Texture, z as parseGlyphPbf, zi as Point, zn as MAX_VALID_LATITUDE, zr as zoomScale, zt as CollisionBoxArray } from "./maplibre-gl-shared-dev.mjs"; //#region package.json var version$2 = "6.2.0"; //#endregion //#region src/util/browser.ts let linkEl; let reducedMotionQuery; let reducedMotionOverride; /** */ const browser = { /** * Schedules a callback to be invoked on the next animation frame. * @param abortController - Controller to abort the scheduled frame. * @param fn - Callback to invoke with the paint start timestamp. * @param reject - Callback to invoke if the frame is aborted. * @param targetWindow - Optional window to use for requestAnimationFrame. * When the map is rendered in a popup window or iframe, pass the owning * window to ensure animation frames continue even when the main window * is not focused. */ frame(abortController, fn, reject, targetWindow) { const win = targetWindow || window; const frameId = win.requestAnimationFrame((paintStartTimestamp) => { unsubscribe(); fn(paintStartTimestamp); }); const { unsubscribe } = subscribe(abortController.signal, "abort", () => { unsubscribe(); win.cancelAnimationFrame(frameId); reject(new AbortError(abortController.signal.reason)); }, false); }, /** * Returns a promise that resolves on the next animation frame. * @param abortController - Controller to abort the scheduled frame. * @param targetWindow - Optional window to use for requestAnimationFrame. * @see {@link browser.frame} */ frameAsync(abortController, targetWindow) { return new Promise((resolve, reject) => { this.frame(abortController, resolve, reject, targetWindow); }); }, getImageData(img, padding = 0) { return this.getImageCanvasContext(img).getImageData(-padding, -padding, img.width + 2 * padding, img.height + 2 * padding); }, getImageCanvasContext(img) { const canvas = window.document.createElement("canvas"); const context = canvas.getContext("2d", { willReadFrequently: true }); if (!context) throw new Error("failed to create canvas 2d context"); canvas.width = img.width; canvas.height = img.height; context.drawImage(img, 0, 0, img.width, img.height); return context; }, resolveURL(path) { linkEl ||= document.createElement("a"); linkEl.href = path; return linkEl.href; }, get hardwareConcurrency() { return typeof navigator !== "undefined" && navigator.hardwareConcurrency || 4; }, get prefersReducedMotion() { if (reducedMotionOverride !== void 0) return reducedMotionOverride; if (!matchMedia) return false; reducedMotionQuery ??= matchMedia("(prefers-reduced-motion: reduce)"); return reducedMotionQuery.matches; }, set prefersReducedMotion(value) { reducedMotionOverride = value; } }; //#endregion //#region src/util/time_control.ts /** * Manages time flow with optional freezing capability for deterministic rendering. */ var TimeManager = class { constructor() { this._frozenAt = null; } /** * Gets the current time, either real or frozen. * @returns Current time in milliseconds */ getCurrentTime() { return this._frozenAt !== null ? this._frozenAt : performance.now(); } /** * Sets time at a specific timestamp. * @param timestamp - Time in milliseconds to set */ setNow(timestamp) { this._frozenAt = timestamp; } /** * Restores normal time flow. */ restoreNow() { this._frozenAt = null; } /** * Returns whether time is currently frozen. * @returns True if time is frozen, false otherwise */ isFrozen() { return this._frozenAt !== null; } }; const timeManager = new TimeManager(); /** * Returns the current time in milliseconds. * When time is frozen via setNow(), returns the frozen timestamp. * Otherwise returns real browser time via performance.now(). * * @returns Current time in milliseconds * @example * ```ts * // Measure elapsed time * const start = maplibregl.now(); * // ... later ... * const elapsed = maplibregl.now() - start; * * // During frozen time * maplibregl.setNow(16.67); * console.log(maplibregl.now()); // 16.67 * maplibregl.restoreNow(); * console.log(maplibregl.now()); // real time * ``` */ function now() { return timeManager.getCurrentTime(); } /** * Freezes time at a specific timestamp for deterministic rendering. * Useful for frame-by-frame video capture where each frame needs * a consistent time value. * * @param timestamp - Time in milliseconds to freeze at * @example * ```ts * // Freeze time for video export at 60fps * setNow(0); // First frame * // ... render frame ... * setNow(16.67); // Second frame * // ... render frame ... * setNow(33.34); // Third frame * // ... done ... * restoreNow(); // Resume normal time * ``` */ function setNow(timestamp) { timeManager.setNow(timestamp); } /** * Restores normal time flow after freezing with setNow(). * Call this after finishing deterministic rendering operations. * * @example * ```ts * // After video export, resume normal time * setNow(0); * // ... export frames ... * restoreNow(); // Map animations resume normally * ``` */ function restoreNow() { timeManager.restoreNow(); } /** * Returns whether time is currently frozen. * @returns True if time is frozen via setNow(), false otherwise * @example * ```ts * setNow(1000); * console.log(isTimeFrozen()); // true * restoreNow(); * console.log(isTimeFrozen()); // false * ``` */ function isTimeFrozen() { return timeManager.isFrozen(); } //#endregion //#region src/util/dom.ts var DOM = class DOM { static { this.docStyle = typeof window !== "undefined" && window.document?.documentElement.style; } static { this.selectProp = !DOM.docStyle || "userSelect" in DOM.docStyle ? "userSelect" : "webkitUserSelect"; } static create(tagName, className, container) { const el = window.document.createElement(tagName); if (className !== void 0) el.className = className; if (container) container.appendChild(el); return el; } static createNS(namespaceURI, tagName) { return window.document.createElementNS(namespaceURI, tagName); } static disableDrag() { if (DOM.docStyle && DOM.selectProp) { DOM.userSelect = DOM.docStyle[DOM.selectProp]; DOM.docStyle[DOM.selectProp] = "none"; } } static enableDrag() { if (DOM.docStyle && DOM.selectProp) DOM.docStyle[DOM.selectProp] = DOM.userSelect; } static suppressClickInternal(e) { e.preventDefault(); e.stopPropagation(); window.removeEventListener("click", DOM.suppressClickInternal, true); } static suppressClick() { window.addEventListener("click", DOM.suppressClickInternal, true); window.setTimeout(() => { window.removeEventListener("click", DOM.suppressClickInternal, true); }, 0); } static getScale(element) { const rect = element.getBoundingClientRect(); return { x: rect.width / element.offsetWidth || 1, y: rect.height / element.offsetHeight || 1, boundingClientRect: rect }; } static getPoint(el, scale, e) { const rect = scale.boundingClientRect; return new Point((e.clientX - rect.left) / scale.x - el.clientLeft, (e.clientY - rect.top) / scale.y - el.clientTop); } static mousePos(el, e) { const scale = DOM.getScale(el); return DOM.getPoint(el, scale, e); } static touchPos(el, touches) { const points = []; const scale = DOM.getScale(el); for (const touch of touches) points.push(DOM.getPoint(el, scale, touch)); return points; } /** * Sanitize an HTML string - this might not be enough to prevent all XSS attacks * Base on https://javascriptsource.com/sanitize-an-html-string-to-reduce-the-risk-of-xss-attacks/ * (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com */ static sanitize(str) { const html = new DOMParser().parseFromString(str, "text/html").body || document.createElement("body"); const scripts = html.querySelectorAll("script"); for (const script of scripts) script.remove(); DOM.clean(html); return html.innerHTML; } /** * Check if the attribute is potentially dangerous */ static isPossiblyDangerous(name, value) { const val = value.replace(/\s+/g, "").toLowerCase(); if ([ "src", "href", "xlink:href" ].includes(name)) { if (val.includes("javascript:") || val.includes("data:")) return true; } if (name.startsWith("on")) return true; } /** * Remove dangerous stuff from the HTML document's nodes * @param html - The HTML document */ static clean(html) { const nodes = html.children; for (const node of nodes) { DOM.removeAttributes(node); DOM.clean(node); } } /** * Remove potentially dangerous attributes from an element * @param elem - The element */ static removeAttributes(elem) { for (const { name, value } of elem.attributes) { if (!DOM.isPossiblyDangerous(name, value)) continue; elem.removeAttribute(name); } } }; //#endregion //#region src/util/image_request.ts let ImageRequest; (function(_ImageRequest) { let imageRequestQueue; let currentParallelImageRequests; let throttleControlCallbackHandleCounter; let throttleControlCallbacks; _ImageRequest.resetRequestQueue = () => { imageRequestQueue = []; currentParallelImageRequests = 0; throttleControlCallbackHandleCounter = 0; throttleControlCallbacks = {}; }; _ImageRequest.addThrottleControl = (callback) => { const handle = throttleControlCallbackHandleCounter++; throttleControlCallbacks[handle] = callback; return handle; }; _ImageRequest.removeThrottleControl = (callbackHandle) => { delete throttleControlCallbacks[callbackHandle]; processQueue(); }; /** * Check to see if any of the installed callbacks are requesting the queue * to be throttled. * @returns `true` if any callback is causing the queue to be throttled. */ const isThrottled = () => { for (const key of Object.keys(throttleControlCallbacks)) if (throttleControlCallbacks[key]()) return true; return false; }; _ImageRequest.getImage = (requestParameters, abortController, supportImageRefresh = true, imageBitmapOptions) => { return new Promise((resolve, reject) => { requestParameters.headers ||= {}; requestParameters.headers.accept = "image/webp,*/*"; extend(requestParameters, { type: "image" }); const request = { abortController, requestParameters, supportImageRefresh, imageBitmapOptions, state: "queued", onError: (error) => { reject(error); }, onSuccess: (response) => { resolve(response); } }; imageRequestQueue.push(request); processQueue(); }); }; const arrayBufferToCanvasImageSource = (data, imageBitmapOptions) => { if (typeof createImageBitmap === "function") return arrayBufferToImageBitmap(data, imageBitmapOptions); else return arrayBufferToImage(data); }; const doImageRequest = async (itemInQueue) => { itemInQueue.state = "running"; const { requestParameters, supportImageRefresh, imageBitmapOptions, onError, onSuccess, abortController } = itemInQueue; const canUseHTMLImageElement = supportImageRefresh === false && !imageBitmapOptions && !isWorker(self) && !getProtocol(requestParameters.url) && (!requestParameters.headers || Object.keys(requestParameters.headers).reduce((acc, item) => acc && item === "accept", true)); currentParallelImageRequests++; const getImagePromise = canUseHTMLImageElement ? getImageUsingHtmlImage(requestParameters, abortController) : makeRequest(requestParameters, abortController); try { const response = await getImagePromise; delete itemInQueue.abortController; itemInQueue.state = "completed"; if (response.data instanceof HTMLImageElement || isImageBitmap(response.data)) onSuccess(response); else if (response.data) onSuccess({ data: await arrayBufferToCanvasImageSource(response.data, imageBitmapOptions), cacheControl: response.cacheControl, expires: response.expires }); } catch (err) { delete itemInQueue.abortController; onError(ensureError(err)); } finally { currentParallelImageRequests--; processQueue(); } }; /** * Process some number of items in the image request queue. */ const processQueue = () => { const maxImageRequests = isThrottled() ? config.MAX_PARALLEL_IMAGE_REQUESTS_PER_FRAME : config.MAX_PARALLEL_IMAGE_REQUESTS; for (let numImageRequests = currentParallelImageRequests; numImageRequests < maxImageRequests && imageRequestQueue.length > 0; numImageRequests++) { const topItemInQueue = imageRequestQueue.shift(); if (topItemInQueue.abortController.signal.aborted) { numImageRequests--; continue; } doImageRequest(topItemInQueue); } }; const getImageUsingHtmlImage = (requestParameters, abortController) => { return new Promise((resolve, reject) => { const image = new Image(); const url = requestParameters.url; const credentials = requestParameters.credentials; if (credentials && credentials === "include") image.crossOrigin = "use-credentials"; else if (credentials && credentials === "same-origin" || !sameOrigin(url)) image.crossOrigin = "anonymous"; abortController.signal.addEventListener("abort", () => { image.src = ""; reject(new AbortError(abortController.signal.reason)); }); image.fetchPriority = "high"; image.onload = () => { image.onerror = image.onload = null; resolve({ data: image }); }; image.onerror = () => { image.onerror = image.onload = null; if (abortController.signal.aborted) return; reject(/* @__PURE__ */ 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.")); }; image.src = url; }); }; })(ImageRequest || (ImageRequest = {})); ImageRequest.resetRequestQueue(); var RequestManager = class { constructor(transformRequestFn) { this._transformRequestFn = transformRequestFn ?? null; } transformRequest(url, type) { if (this._transformRequestFn) return this._transformRequestFn(url, type) || { url }; return { url }; } setTransformRequest(transformRequest) { this._transformRequestFn = transformRequest; } }; //#endregion //#region src/ui/events.ts /** * The base event for MapLibre * * @group Event Related */ var MapLibreEvent = class extends Event {}; /** * `MapMovementEvent` is the event type for the camera-transition map events: * `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `rotatestart`, `rotate`, `rotateend`, * `dragstart`, `drag`, `dragend`, `pitchstart`, `pitch`, `pitchend`, `rollstart`, `roll` and `rollend`. * These are fired as the map's view changes, as a result of either user interaction or methods such * as {@link Map.jumpTo} / {@link Map.flyTo}. * * @group Event Related */ var MapMovementEvent = class extends MapLibreEvent {}; /** * The `style.load` event, fired once the map's style has fully loaded or changed. * * @group Event Related */ var MapStyleLoadEvent = class extends MapLibreEvent { constructor(data = {}) { super("style.load", data); } }; /** * The style data event * * @group Event Related */ var MapStyleDataEvent = class extends MapLibreEvent { constructor(type, data = {}) { super(type, data); this.dataType = "style"; } }; /** * A `MapSourceDataEvent` is emitted with the source-related `data`, `dataloading`, `dataabort`, * `sourcedata`, `sourcedataloading` and `sourcedataabort` events. Its `dataType` is always `'source'`. * * Possible values for `sourceDataType`s are: * * - `'metadata'`: indicates that any necessary source metadata has been loaded (such as TileJSON) and it is ok to start loading tiles * - `'content'`: indicates the source data has changed (such as when source.setData() has been called on GeoJSONSource) * - `'visibility'`: send when the source becomes used when at least one of its layers becomes visible in style sense (inside the layer's zoom range and with layout.visibility set to 'visible') * - `'idle'`: indicates that no new source data has been fetched (but the source has done loading) * * @group Event Related * * @example * ```ts * // The sourcedata event is an example of a MapSourceDataEvent. * // Set up an event listener on the map. * map.on('sourcedata', (e) => { * if (e.isSourceLoaded) { * // Do something when the source has finished loading * } * }); * ``` */ var MapSourceDataEvent = class extends MapLibreEvent { constructor(type, data = {}) { super(type, data); this.dataType = "source"; } }; /** * `MapMouseEvent` is the event type for mouse-related map events. * * @group Event Related * * @example * ```ts * // The `click` event is an example of a `MapMouseEvent`. * // Set up an event listener on the map. * map.on('click', (e) => { * // The event object (e) contains information like the * // coordinates of the point on the map that was clicked. * console.log('A click event has occurred at ' + e.lngLat); * }); * ``` */ var MapMouseEvent = class extends MapLibreEvent { /** * Prevents subsequent default processing of the event by the map. * * Calling this method will prevent the following default map behaviors: * * * On `mousedown` events, the behavior of {@link DragPanHandler} * * On `mousedown` events, the behavior of {@link DragRotateHandler} * * On `mousedown` events, the behavior of {@link BoxZoomHandler} * * On `dblclick` events, the behavior of {@link DoubleClickZoomHandler} * */ preventDefault() { this._defaultPrevented = true; } /** * `true` if `preventDefault` has been called. */ get defaultPrevented() { return this._defaultPrevented; } constructor(type, map, originalEvent, data = {}) { originalEvent = originalEvent instanceof MouseEvent ? originalEvent : new MouseEvent(type, originalEvent); const point = DOM.mousePos(map.getCanvas(), originalEvent); const lngLat = map.unproject(point); super(type, extend({ point, lngLat, originalEvent }, data)); this._defaultPrevented = false; this.target = map; } }; /** * `MapTouchEvent` is the event type for touch-related map events. * * @group Event Related */ var MapTouchEvent = class extends MapLibreEvent { /** * Prevents subsequent default processing of the event by the map. * * Calling this method will prevent the following default map behaviors: * * * On `touchstart` events, the behavior of {@link DragPanHandler} * * On `touchstart` events, the behavior of {@link TwoFingersTouchZoomRotateHandler} * */ preventDefault() { this._defaultPrevented = true; } /** * `true` if `preventDefault` has been called. */ get defaultPrevented() { return this._defaultPrevented; } constructor(type, map, originalEvent) { const touches = type === "touchend" ? originalEvent.changedTouches : originalEvent.touches; const points = DOM.touchPos(map.getCanvasContainer(), touches); const lngLats = points.map((t) => map.unproject(t)); const point = points.reduce((prev, curr, i, arr) => { return prev.add(curr.div(arr.length)); }, new Point(0, 0)); const lngLat = map.unproject(point); super(type, { points, point, lngLats, lngLat, originalEvent }); this._defaultPrevented = false; } }; /** * `MapWheelEvent` is the event type for the `wheel` map event. * * @group Event Related */ var MapWheelEvent = class extends MapLibreEvent { /** * Prevents subsequent default processing of the event by the map. * * Calling this method will prevent the behavior of {@link ScrollZoomHandler}. */ preventDefault() { this._defaultPrevented = true; } /** * `true` if `preventDefault` has been called. */ get defaultPrevented() { return this._defaultPrevented; } /** */ constructor(map, originalEvent) { super("wheel", { originalEvent }); this._defaultPrevented = false; } }; /** * A `MapBoxZoomEvent` is the event type for the boxzoom-related map events emitted by the {@link BoxZoomHandler}. * * @group Event Related */ var MapBoxZoomEvent = class extends MapLibreEvent {}; /** * The terrain event * * @group Event Related */ var MapTerrainEvent = class extends MapLibreEvent { constructor(data = {}) { super("terrain", data); } }; /** * The map projection event * * @group Event Related */ var MapProjectionEvent = class extends MapLibreEvent { constructor(data = {}) { super("projectiontransition", data); } }; /** * An event related to the web gl context * * @group Event Related */ var MapContextEvent = class extends MapLibreEvent {}; /** * The style image missing event, fired when an image is still missing after the missing style image * resolver has been given a chance to supply it. To load or generate images on demand, * use {@link Map.setMissingStyleImageResolver}. * Event listeners cannot resolve the missing image for the current request. * * @group Event Related * * @see [Generate and add a missing icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/generate-and-add-a-missing-icon-to-the-map/) */ var MapStyleImageMissingEvent = class extends MapLibreEvent { constructor(data = {}) { super("styleimagemissing", data); } }; //#endregion //#region src/util/style.ts /** * Takes a SpriteSpecification value and returns it in its array form. If `undefined` is passed as an input value, an * empty array is returned. * duplicated entries with identical id/url will be removed in returned array * @param sprite - optional sprite to coerce * @returns an empty array in case `undefined` is passed; id-url pairs otherwise */ function coerceSpriteToArray(sprite) { const resultArray = []; if (typeof sprite === "string") resultArray.push({ id: "default", url: sprite }); else if (sprite && sprite.length > 0) { const dedupArray = []; for (const { id, url } of sprite) { const key = `${id}${url}`; if (!dedupArray.includes(key)) { dedupArray.push(key); resultArray.push({ id, url }); } } } return resultArray; } //#endregion //#region src/style/load_sprite.ts function normalizeSpriteURL(url, format, extension) { try { const parsed = new URL(url); parsed.pathname += `${format}${extension}`; return parsed.toString(); } catch { throw new Error(`Invalid sprite URL "${url}", must be absolute. Modify style specification directly or use TransformStyleFunction to correct the issue dynamically`); } } async function loadSprite(originalSprite, requestManager, pixelRatio, abortController) { const spriteArray = coerceSpriteToArray(originalSprite); const format = pixelRatio > 1 ? "@2x" : ""; const jsonsMap = {}; const imagesMap = {}; for (const { id, url } of spriteArray) { const jsonRequestParameters = await requestManager.transformRequest(normalizeSpriteURL(url, format, ".json"), "SpriteJSON"); jsonsMap[id] = getJSON(jsonRequestParameters, abortController); const imageRequestParameters = await requestManager.transformRequest(normalizeSpriteURL(url, format, ".png"), "SpriteImage"); imagesMap[id] = ImageRequest.getImage(imageRequestParameters, abortController); } await Promise.all([...Object.values(jsonsMap), ...Object.values(imagesMap)]); return doOnceCompleted(jsonsMap, imagesMap); } /** * @param jsonsMap - JSON data map * @param imagesMap - image data map */ async function doOnceCompleted(jsonsMap, imagesMap) { const result = {}; for (const spriteName in jsonsMap) { result[spriteName] = {}; const context = browser.getImageCanvasContext((await imagesMap[spriteName]).data); const json = (await jsonsMap[spriteName]).data; for (const id in json) { const { width, height, x, y, sdf, pixelRatio, stretchX, stretchY, content, textFitWidth, textFitHeight } = json[id]; const spriteData = { width, height, x, y, context }; result[spriteName][id] = { data: null, pixelRatio, sdf, stretchX, stretchY, content, textFitWidth, textFitHeight, spriteData }; } } return result; } //#endregion //#region src/render/image_manager.ts /** * When copied into the atlas texture, image data is padded by one pixel on each side. Icon * images are padded with fully transparent pixels, while pattern images are padded with a * copy of the image data wrapped from the opposite side. In both cases, this ensures the * correct behavior of GL_LINEAR texture sampling mode. */ const padding = 1; /** * ImageManager does three things: * * 1. Tracks requests for icon images from tile workers and sends responses when the requests are fulfilled. * 2. Builds a texture atlas for pattern images. * 3. Rerenders renderable images once per frame * * These are disparate responsibilities and should eventually be handled by different classes. When we implement * data-driven support for `*-pattern`, we'll likely use per-bucket pattern atlases, and that would be a good time * to refactor this. */ var ImageManager = class extends Evented { constructor() { super(); this.images = {}; this.updatedImages = {}; this.callbackDispatchedThisFrame = {}; this.loaded = false; this.requestors = []; this.missingImageResolver = null; this.patterns = {}; this.atlasImage = new RGBAImage({ width: 1, height: 1 }); this.dirty = true; } destroy() { if (this.atlasTexture) { this.atlasTexture.destroy(); this.atlasTexture = null; } for (const id of Object.keys(this.images)) this.removeImage(id); this.patterns = {}; this.atlasImage = new RGBAImage({ width: 1, height: 1 }); this.dirty = true; } isLoaded() { return this.loaded; } setLoaded(loaded) { if (this.loaded === loaded) return; this.loaded = loaded; if (loaded) { for (const { ids, promiseResolve } of this.requestors) promiseResolve(this._getImagesForIds(ids)); this.requestors = []; } } getImage(id) { const image = this.images[id]; if (image && !image.data && image.spriteData) { const spriteData = image.spriteData; image.data = new RGBAImage({ width: spriteData.width, height: spriteData.height }, spriteData.context.getImageData(spriteData.x, spriteData.y, spriteData.width, spriteData.height).data); image.spriteData = null; } return image; } addImage(id, image) { if (this.images[id]) throw new Error(`Image id ${id} already exist, use updateImage instead`); if (this._validate(id, image)) this.images[id] = image; } _validate(id, image) { let valid = true; const data = image.data || image.spriteData; if (!this._validateStretch(image.stretchX, data?.width)) { this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Image "${id}" has invalid "stretchX" value`))); valid = false; } if (!this._validateStretch(image.stretchY, data?.height)) { this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Image "${id}" has invalid "stretchY" value`))); valid = false; } if (!this._validateContent(image.content, image)) { this.fire(new ErrorEvent(/* @__PURE__ */ new Error(`Image "${id}" has invalid "content" value`))); valid = false; } return valid; } _validateStretch(stretch, size) { if (!stretch) return true; let last = 0; for (const part of stretch) { if (part[0] < last || part[1] < part[0] || size < part[1]) return false; last = part[1]; } return true; } _validateContent(content, image) { if (!content) return true; if (content.length !== 4) return false; const spriteData = image.spriteData; const width = spriteData?.width || image.data.width; const height = spriteData?.height || image.data.height; if (content[0] < 0 || width < content[0]) return false; if (content[1] < 0 || height < content[1]) return false; if (content[2] < 0 || width < content[2]) return false; if (content[3] < 0 || height < content[3]) return false; if (content[2] < content[0]) return false; return content[3] >= content[1]; } updateImage(id, image, validate = true) { const oldImage = this.getImage(id); if (validate && (oldImage.data.width !== image.data.width || oldImage.data.height !== image.data.height)) throw new Error(`size mismatch between old image (${oldImage.data.width}x${oldImage.data.height}) and new image (${image.data.width}x${image.data.height}).`); image.version = oldImage.version + 1; this.images[id] = image; this.updatedImages[id] = true; } removeImage(id) { const image = this.images[id]; delete this.images[id]; delete this.patterns[id]; if (image.userImage?.onRemove) image.userImage.onRemove(); } listImages() { return Object.keys(this.images); } setMissingImageResolver(resolver) { this.missingImageResolver = resolver; } getImages(ids) { return new Promise((resolve, _reject) => { let hasAllDependencies = true; if (!this.isLoaded()) { for (const id of ids) if (!this.images[id]) hasAllDependencies = false; } if (this.isLoaded() || hasAllDependencies) resolve(this._getImagesForIds(ids)); else this.requestors.push({ ids, promiseResolve: resolve }); }); } async _getImagesForIds(ids) { const unresolvedIds = new Set(ids.filter((id) => !this.getImage(id))); const resolver = this.missingImageResolver; if (resolver) await Promise.all(Array.from(unresolvedIds, (id) => resolver(id))); const response = {}; for (const id of ids) { const image = this.getImage(id); if (image) { unresolvedIds.delete(id); response[id] = { data: image.data.clone(), pixelRatio: image.pixelRatio, sdf: image.sdf, version: image.version, stretchX: image.stretchX, stretchY: image.stretchY, content: image.content, textFitWidth: image.textFitWidth, textFitHeight: image.textFitHeight, hasRenderCallback: Boolean(image.userImage?.render) }; } } for (const id of unresolvedIds) { this.fire(new MapStyleImageMissingEvent({ id })); warnOnce(`Image "${id}" could not be loaded. Please make sure you have added the image before it is needed with map.addImage(), resolved it with map.setMissingStyleImageResolver(), or included it in a "sprite" property in your style.`); } return response; } getPixelSize() { const { width, height } = this.atlasImage; return { width, height }; } getPattern(id) { const pattern = this.patterns[id]; const image = this.getImage(id); if (!image) return null; if (pattern && pattern.position.version === image.version) return pattern.position; if (!pattern) { const bin = { w: image.data.width + 2, h: image.data.height + 2, x: 0, y: 0 }; const position = new ImagePosition(bin, image); this.patterns[id] = { bin, position }; } else pattern.position.version = image.version; this._updatePatternAtlas(); return this.patterns[id].position; } bind(context) { const gl = context.gl; if (!this.atlasTexture) this.atlasTexture = new Texture(context, this.atlasImage, gl.RGBA); else if (this.dirty) { this.atlasTexture.update(this.atlasImage); this.dirty = false; } this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); } _updatePatternAtlas() { const bins = []; for (const id in this.patterns) bins.push(this.patterns[id].bin); const { w, h } = potpack(bins); const dst = this.atlasImage; dst.resize({ width: w || 1, height: h || 1 }); for (const id in this.patterns) { const { bin } = this.patterns[id]; const x = bin.x + padding; const y = bin.y + padding; const src = this.getImage(id).data; const w = src.width; const h = src.height; RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y }, { width: w, height: h }); RGBAImage.copy(src, dst, { x: 0, y: h - 1 }, { x, y: y - 1 }, { width: w, height: 1 }); RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y: y + h }, { width: w, height: 1 }); RGBAImage.copy(src, dst, { x: w - 1, y: 0 }, { x: x - 1, y }, { width: 1, height: h }); RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x + w, y }, { width: 1, height: h }); } this.dirty = true; } beginFrame() { this.callbackDispatchedThisFrame = {}; } dispatchRenderCallbacks(ids) { for (const id of ids) { if (this.callbackDispatchedThisFrame[id]) continue; this.callbackDispatchedThisFrame[id] = true; const image = this.getImage(id); if (!image) warnOnce(`Image with ID: "${id}" was not found`); if (renderStyleImage(image)) this.updateImage(id, image); } } cloneImages() { const clonedImages = {}; for (const id in this.images) { const image = this.images[id]; clonedImages[id] = { ...image, data: image.data ? image.data.clone() : null }; } return clonedImages; } }; //#endregion //#region src/style/load_glyph_range.ts async function loadGlyphRange(fontstack, range, urlTemplate, requestManager) { const begin = range * 256; const end = begin + 255; const request = await requestManager.transformRequest(urlTemplate.replace("{fontstack}", fontstack).replace("{range}", `${begin}-${end}`), "Glyphs"); const response = await getArrayBuffer(request, new AbortController()); if (!response?.data) throw new Error(`Could not load glyph range. range: ${range}, ${begin}-${end}`); const glyphs = {}; for (const glyph of parseGlyphPbf(response.data)) glyphs[glyph.id] = glyph; return glyphs; } //#endregion //#region node_modules/@mapbox/tiny-sdf/index.js const INF = 0x56bc75e2d63100000; const alphaTable = /* @__PURE__ */ new Float64Array(256); for (let i = 0; i < 256; i++) { const d = .5 - Math.pow(i / 255, 1 / 2.2); alphaTable[i] = d * Math.abs(d); } alphaTable[255] = -0x56bc75e2d63100000; var TinySDF = class { constructor({ fontSize = 24, buffer = 3, radius = 8, cutoff = .25, fontFamily = "sans-serif", fontWeight = "normal", fontStyle = "normal", lang = null } = {}) { this.buffer = buffer; this.radius = radius; this.cutoff = cutoff; this.lang = lang; const size = this.size = fontSize + buffer * 4; const canvas = this._createCanvas(size); const ctx = this.ctx = canvas.getContext("2d", { willReadFrequently: true }); ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`; ctx.textBaseline = "alphabetic"; ctx.textAlign = "left"; ctx.fillStyle = "black"; this.gridOuter = new Float64Array(size * size); this.gridInner = new Float64Array(size * size); this.f = new Float64Array(size); this.z = new Float64Array(size + 1); this.v = new Uint16Array(size); } _createCanvas(size) { if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(size, size); const canvas = document.createElement("canvas"); canvas.width = canvas.height = size; return canvas; } draw(char) { const { width: glyphAdvance, actualBoundingBoxAscent, actualBoundingBoxDescent, actualBoundingBoxLeft, actualBoundingBoxRight } = this.ctx.measureText(char); const glyphTop = Math.ceil(actualBoundingBoxAscent); const glyphLeft = Math.floor(-actualBoundingBoxLeft); const glyphWidth = Math.max(0, Math.min(this.size - this.buffer, Math.ceil(actualBoundingBoxRight) - glyphLeft)); const glyphHeight = Math.max(0, Math.min(this.size - this.buffer, glyphTop + Math.ceil(actualBoundingBoxDescent))); const width = glyphWidth + 2 * this.buffer; const height = glyphHeight + 2 * this.buffer; const len = Math.max(width * height, 0); const data = new Uint8ClampedArray(len); const glyph = { data, width, height, glyphWidth, glyphHeight, glyphTop, glyphLeft, glyphAdvance }; if (glyphWidth === 0 || glyphHeight === 0) return glyph; const { ctx, buffer, gridInner, gridOuter } = this; if (this.lang) ctx.lang = this.lang; ctx.clearRect(buffer, buffer, glyphWidth, glyphHeight); ctx.fillText(char, buffer - glyphLeft, buffer + glyphTop); const imgData = ctx.getImageData(buffer, buffer, glyphWidth, glyphHeight); gridOuter.fill(INF, 0, len); gridInner.fill(0, 0, len); let imgIdx = 3; for (let y = 0; y < glyphHeight; y++) { let j = (y + buffer) * width + buffer; for (let x = 0; x < glyphWidth; x++, imgIdx += 4, j++) { const a = imgData.data[imgIdx]; if (a === 0) continue; const t = alphaTable[a]; gridOuter[j] = Math.max(0, t); gridInner[j] = Math.max(0, -t); } } edt(gridOuter, 0, 0, width, height, width, this.f, this.v, this.z); const pad = Math.min(buffer, 1); edt(gridInner, buffer - pad, buffer - pad, glyphWidth + 2 * pad, glyphHeight + 2 * pad, width, this.f, this.v, this.z); const scale = 255 / this.radius; const base = 255 * (1 - this.cutoff); for (let i = 0; i < len; i++) { const d = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]); data[i] = Math.round(base - scale * d); } return glyph; } }; function edt(data, x0, y0, width, height, gridSize, f, v, z) { for (let x = x0; x < x0 + width; x++) edt1d(data, y0 * gridSize + x, gridSize, height, f, v, z); for (let y = y0; y < y0 + height; y++) edt1d(data, y * gridSize + x0, 1, width, f, v, z); } function edt1d(grid, offset, stride, length, f, v, z) { v[0] = 0; z[0] = -0x56bc75e2d63100000; z[1] = INF; f[0] = grid[offset]; for (let q = 1, k = 0, s = 0; q < length; q++) { f[q] = grid[offset + q * stride]; const q2 = q * q; do { const r = v[k]; s = (f[q] - f[r] + q2 - r * r) / (q - r) / 2; } while (s <= z[k] && --k > -1); k++; v[k] = q; z[k] = s; z[k + 1] = INF; } for (let q = 0, k = 0; q < length; q++) { while (z[k + 1] < q) k++; const r = v[k]; const qr = q - r; grid[offset + q * stride] = f[r] + qr * qr; } } //#endregion //#region src/render/glyph_manager.ts /** * The style specification hard-codes some last resort fonts as a default fontstack. */ const defaultStack = latest.layout_symbol["text-font"].default.join(","); /** * The CSS generic font family closest to `defaultStack`. */ const defaultGenericFontFamily = "sans-serif"; /** * Scale factor for client-generated glyphs. * * Client-generated glyphs are rendered at 2× because CJK glyphs are more detailed than others. */ const textureScale = 2; var GlyphManager = class GlyphManager { static { this.loadGlyphRange = loadGlyphRange; } static { this.TinySDF = TinySDF; } constructor(requestManager, localIdeographFontFamily, lang) { this.requestManager = requestManager; this.localIdeographFontFamily = localIdeographFontFamily; this.entries = {}; this.lang = lang; } setURL(url) { this.url = url; } async getGlyphs(glyphs) { const glyphsPromises = []; for (const stack in glyphs) for (const id of glyphs[stack]) glyphsPromises.push(this._getAndCacheGlyphsPromise(stack, id)); const updatedGlyphs = await Promise.all(glyphsPromises); const result = {}; for (const { stack, id, glyph } of updatedGlyphs) { result[stack] ||= {}; result[stack][id] = glyph && { id: glyph.id, bitmap: glyph.bitmap.clone(), metrics: glyph.metrics }; } return result; } async _getAndCacheGlyphsPromise(stack, id) { this.entries[stack] ??= { glyphs: {}, requests: {}, ranges: {} }; const entry = this.entries[stack]; let glyph = entry.glyphs[id]; if (glyph !== void 0) return { stack, id, glyph }; if (!this.url || this._charUsesLocalIdeographFontFamily(id)) { glyph = entry.glyphs[id] = await this._drawGlyph(entry, stack, id); return { stack, id, glyph }; } return await this._downloadAndCacheRangePromise(stack, id); } async _downloadAndCacheRangePromise(stack, id) { const entry = this.entries[stack]; const range = Math.floor(id / 256); if (entry.ranges[range]) return { stack, id, glyph: null }; entry.requests[range] ||= GlyphManager.loadGlyphRange(stack, range, this.url, this.requestManager); try { const response = await entry.requests[range]; for (const id in response) entry.glyphs[+id] = response[+id]; entry.ranges[range] = true; return { stack, id, glyph: response[id] || null }; } catch (e) { const glyph = entry.glyphs[id] = await this._drawGlyph(entry, stack, id); this._warnOnMissingGlyphRange(glyph, range, id, ensureError(e)); return { stack, id, glyph }; } } _warnOnMissingGlyphRange(glyph, range, id, err) { const begin = range * 256; const end = begin + 255; const codePoint = id.toString(16).padStart(4, "0").toUpperCase(); warnOnce(`Unable to load glyph range ${range}, ${begin}-${end}. Rendering codepoint U+${codePoint} locally instead. ${err}`); } /** * Returns whether the given codepoint should be rendered locally. */ _charUsesLocalIdeographFontFamily(id) { return !!this.localIdeographFontFamily && codePointUsesLocalIdeographFontFamily(id); } /** * Draws a glyph offscreen using TinySDF, creating a TinySDF instance lazily. */ async _drawGlyph(entry, stack, id) { const usesLocalIdeographFontFamily = stack === defaultStack && this.localIdeographFontFamily !== "" && this._charUsesLocalIdeographFontFamily(id); const tinySDFKey = usesLocalIdeographFontFamily ? "ideographTinySDF" : "tinySDF"; entry[tinySDFKey] ||= this._createTinySDF(usesLocalIdeographFontFamily ? this.localIdeographFontFamily : stack); const char = (await entry[tinySDFKey]).draw(String.fromCodePoint(id)); /** * TinySDF's "top" is the distance from the alphabetic baseline to the top of the glyph. * Server-generated fonts specify "top" relative to an origin above the em box (the origin * comes from FreeType, but I'm unclear on exactly how it's derived) * ref: https://github.com/mapbox/sdf-glyph-foundry * * Server fonts don't yet include baseline information, so we can't line up exactly with them * (and they don't line up with each other) * ref: https://github.com/mapbox/node-fontnik/pull/160 * * To approximately align TinySDF glyphs with server-provided glyphs, we use this baseline adjustment * factor calibrated to be in between DIN Pro and Arial Unicode (but closer to Arial Unicode) */ const topAdjustment = 27.5; const leftAdjustment = .5; const isControl = /^\p{gc=Cf}+$/u.test(String.fromCodePoint(id)); return { id, bitmap: new AlphaImage({ width: char.width || 60, height: char.height || 60 }, char.data), metrics: { width: isControl ? 0 : char.glyphWidth / textureScale || 24, height: char.glyphHeight / textureScale || 24, left: char.glyphLeft / textureScale + leftAdjustment || 0, top: char.glyphTop / textureScale - topAdjustment || -8, advance: isControl ? 0 : char.glyphAdvance / textureScale || 24, isDoubleResolution: true } }; } async _createTinySDF(stack) { const fontFamilies = stack ? stack.split(",") : []; fontFamilies.push(defaultGenericFontFamily); const fontFamily = fontFamilies.map((fontName) => /[-\w]+/.test(fontName) ? fontName : `'${CSS.escape(fontName)}'`).join(","); const fontSize = 48; const fontWeight = this._fontWeight(fontFamilies[0]); const fontStyle = this._fontStyle(fontFamilies[0]); if (typeof document !== "undefined" && document.fonts?.load) try { await document.fonts.load(`${fontStyle} ${fontWeight || "normal"} ${fontSize}px ${fontFamily}`); } catch (e) { warnOnce(`Failed to load font "${fontFamily}": ${ensureError(e).message}`); } return new GlyphManager.TinySDF({ fontSize, buffer: 6, radius: 16, cutoff: .25, fontFamily, fontWeight, fontStyle, lang: this.lang }); } /** * Sniffs the font style out of a font family name. */ _fontStyle(fontFamily) { if (/italic/i.test(fontFamily)) return "italic"; else if (/oblique/i.test(fontFamily)) return "oblique"; return "normal"; } /** * Sniffs the font weight out of a font family name. */ _fontWeight(fontFamily) { const weightsByName = { thin: 100, hairline: 100, "extra light": 200, "ultra light": 200, light: 300, normal: 400, regular: 400, medium: 500, semibold: 600, demibold: 600, bold: 700, "extra bold": 800, "ultra bold": 800, black: 900, heavy: 900, "extra black": 950, "ultra black": 950 }; let match; for (const [name, weight] of Object.entries(weightsByName)) if (new RegExp(`\\b${name}\\b`, "i").test(fontFamily)) match = `${weight}`; return match; } destroy() { for (const stack in this.entries) { const entry = this.entries[stack]; entry.tinySDF = null; entry.ideographTinySDF = null; entry.glyphs = {}; entry.requests = {}; entry.ranges = {}; } this.entries = {}; } }; //#endregion //#region src/style/light_properties.g.ts let properties$2; const getProperties$2 = () => properties$2 = properties$2 || new Properties({ "anchor": new DataConstantProperty(latest["light"]["anchor"], "anchor"), "position": new DataConstantProperty(latest["light"]["position"], "position"), "color": new DataConstantProperty(latest["light"]["color"], "color"), "intensity": new DataConstantProperty(latest["light"]["intensity"], "intensity") }); //#endregion //#region src/style/light.ts var Light = class extends Evented { constructor(lightOptions, globalState) { super(); this._transitionable = new Transitionable(getProperties$2(), "light", globalState); this.setLight(lightOptions); this._transitioning = this._transitionable.untransitioned(); } getLight() { return this._transitionable.serialize(); } /** * Get