maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
1,133 lines (1,033 loc) • 153 kB
text/typescript
import {extend, warnOnce, uniqueId, isImageBitmap, type Complete, pick, type Subscription} from '../util/util';
import {browser} from '../util/browser';
import {DOM} from '../util/dom';
import packageJSON from '../../package.json' with {type: 'json'};
import {type GetResourceResponse, getJSON} from '../util/ajax';
import {ImageRequest} from '../util/image_request';
import {RequestManager, ResourceType} from '../util/request_manager';
import {Style, type StyleSwapOptions} from '../style/style';
import {EvaluationParameters} from '../style/evaluation_parameters';
import {Painter} from '../render/painter';
import {Hash} from './hash';
import {HandlerManager} from './handler_manager';
import {Camera, type CameraOptions, type CameraUpdateTransformFunction, type FitBoundsOptions} from './camera';
import {LngLat} from '../geo/lng_lat';
import {LngLatBounds} from '../geo/lng_lat_bounds';
import Point from '@mapbox/point-geometry';
import {AttributionControl, type AttributionControlOptions, defaultAttributionControlOptions} from './control/attribution_control';
import {LogoControl} from './control/logo_control';
import {RGBAImage} from '../util/image';
import {Event, ErrorEvent, type Listener} from '../util/evented';
import {type MapEventType, type MapLayerEventType, MapMouseEvent, type MapSourceDataEvent, type MapStyleDataEvent} from './events';
import {TaskQueue} from '../util/task_queue';
import {throttle} from '../util/throttle';
import {webpSupported} from '../util/webp_supported';
import {PerformanceMarkers, PerformanceUtils} from '../util/performance';
import {type Source} from '../source/source';
import {type StyleLayer} from '../style/style_layer';
import {Terrain} from '../render/terrain';
import {RenderToTexture} from '../render/render_to_texture';
import {config} from '../util/config';
import {defaultLocale} from './default_locale';
import type {RequestTransformFunction} from '../util/request_manager';
import type {LngLatLike} from '../geo/lng_lat';
import type {LngLatBoundsLike} from '../geo/lng_lat_bounds';
import type {AddLayerObject, FeatureIdentifier, StyleOptions, StyleSetterOptions} from '../style/style';
import type {MapDataEvent} from './events';
import type {StyleImage, StyleImageInterface, StyleImageMetadata} from '../style/style_image';
import type {PointLike} from './camera';
import type {ScrollZoomHandler} from './handler/scroll_zoom';
import type {BoxZoomHandler} from './handler/box_zoom';
import type {AroundCenterOptions, TwoFingersTouchPitchHandler} from './handler/two_fingers_touch';
import type {DragRotateHandler} from './handler/shim/drag_rotate';
import type {DragPanHandler, DragPanOptions} from './handler/shim/drag_pan';
import type {CooperativeGesturesHandler, GestureOptions} from './handler/cooperative_gestures';
import type {KeyboardHandler} from './handler/keyboard';
import type {DoubleClickZoomHandler} from './handler/shim/dblclick_zoom';
import type {TwoFingersTouchZoomRotateHandler} from './handler/shim/two_fingers_touch';
import type {TaskID} from '../util/task_queue';
import type {
FilterSpecification,
StyleSpecification,
LightSpecification,
SourceSpecification,
TerrainSpecification,
ProjectionSpecification,
SkySpecification,
} from '@maplibre/maplibre-gl-style-spec';
import type {CanvasSourceSpecification} from '../source/canvas_source';
import type {GeoJSONFeature, MapGeoJSONFeature} from '../util/vectortile_to_geojson';
import type {ControlPosition, IControl} from './control/control';
import type {QueryRenderedFeaturesOptions, QuerySourceFeatureOptions} from '../source/query_features';
import {MercatorTransform} from '../geo/projection/mercator_transform';
import {type ITransform} from '../geo/transform_interface';
import {type ICameraHelper} from '../geo/projection/camera_helper';
import {MercatorCameraHelper} from '../geo/projection/mercator_camera_helper';
import {isAbortError} from '../util/abort_error';
import {isFramebufferNotCompleteError} from '../util/framebuffer_error';
import {createCalculateTileZoomFunction} from '../geo/projection/covering_tiles';
import {CanonicalTileID} from '../source/tile_id';
const version = packageJSON.version;
type WebGLSupportedVersions = 'webgl2' | 'webgl' | undefined;
type WebGLContextAttributesWithType = WebGLContextAttributes & {contextType?: WebGLSupportedVersions};
/**
* The {@link Map} options object.
*/
export type MapOptions = {
/**
* If `true`, the map's position (zoom, center latitude, center longitude, bearing, and pitch) will be synced with the hash fragment of the page's URL.
* For example, `http://path/to/my/page.html#2.59/39.26/53.07/-24.1/60`.
* An additional string may optionally be provided to indicate a parameter-styled hash,
* e.g. http://path/to/my/page.html#map=2.59/39.26/53.07/-24.1/60&foo=bar, where foo
* is a custom parameter and bar is an arbitrary hash distinct from the map hash.
* @defaultValue false
*/
hash?: boolean | string;
/**
* If `false`, no mouse, touch, or keyboard listeners will be attached to the map, so it will not respond to interaction.
* @defaultValue true
*/
interactive?: boolean;
/**
* The HTML element in which MapLibre GL JS will render the map, or the element's string `id`. The specified element must have no children.
*/
container: HTMLElement | string;
/**
* The threshold, measured in degrees, that determines when the map's
* bearing will snap to north. For example, with a `bearingSnap` of 7, if the user rotates
* the map within 7 degrees of north, the map will automatically snap to exact north.
* @defaultValue 7
*/
bearingSnap?: number;
/**
* If set, an {@link AttributionControl} will be added to the map with the provided options.
* To disable the attribution control, pass `false`.
* Note: showing the logo of MapLibre is not required for using MapLibre.
* @defaultValue compact: true, customAttribution: "MapLibre ...".
*/
attributionControl?: false | AttributionControlOptions;
/**
* If `true`, the MapLibre logo will be shown.
*/
maplibreLogo?: boolean;
/**
* A string representing the position of the MapLibre wordmark on the map. Valid options are `top-left`,`top-right`, `bottom-left`, or `bottom-right`.
* @defaultValue 'bottom-left'
*/
logoPosition?: ControlPosition;
/**
* Set of WebGLContextAttributes that are applied to the WebGL context of the map.
* See https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext for more details.
* `contextType` can be set to `webgl2` or `webgl` to force a WebGL version. Not setting it, Maplibre will do it's best to get a suitable context.
* @defaultValue antialias: false, powerPreference: 'high-performance', preserveDrawingBuffer: false, failIfMajorPerformanceCaveat: false, desynchronized: false, contextType: 'webgl2withfallback'
*/
canvasContextAttributes?: WebGLContextAttributesWithType;
/**
* If `false`, the map won't attempt to re-request tiles once they expire per their HTTP `cacheControl`/`expires` headers.
* @defaultValue true
*/
refreshExpiredTiles?: boolean;
/**
* If set, the map will be constrained to the given bounds.
*/
maxBounds?: LngLatBoundsLike;
/**
* If `true`, the "scroll to zoom" interaction is enabled. {@link AroundCenterOptions} are passed as options to {@link ScrollZoomHandler.enable}.
* @defaultValue true
*/
scrollZoom?: boolean | AroundCenterOptions;
/**
* The minimum zoom level of the map (0-24).
* @defaultValue 0
*/
minZoom?: number | null;
/**
* The maximum zoom level of the map (0-24).
* @defaultValue 22
*/
maxZoom?: number | null;
/**
* The minimum pitch of the map (0-180).
* @defaultValue 0
*/
minPitch?: number | null;
/**
* The maximum pitch of the map (0-180).
* @defaultValue 60
*/
maxPitch?: number | null;
/**
* If `true`, the "box zoom" interaction is enabled (see {@link BoxZoomHandler}).
* @defaultValue true
*/
boxZoom?: boolean;
/**
* If `true`, the "drag to rotate" interaction is enabled (see {@link DragRotateHandler}).
* @defaultValue true
*/
dragRotate?: boolean;
/**
* If `true`, the "drag to pan" interaction is enabled. An `Object` value is passed as options to {@link DragPanHandler.enable}.
* @defaultValue true
*/
dragPan?: boolean | DragPanOptions;
/**
* If `true`, keyboard shortcuts are enabled (see {@link KeyboardHandler}).
* @defaultValue true
*/
keyboard?: boolean;
/**
* If `true`, the "double click to zoom" interaction is enabled (see {@link DoubleClickZoomHandler}).
* @defaultValue true
*/
doubleClickZoom?: boolean;
/**
* If `true`, the "pinch to rotate and zoom" interaction is enabled. An `Object` value is passed as options to {@link TwoFingersTouchZoomRotateHandler.enable}.
* @defaultValue true
*/
touchZoomRotate?: boolean | AroundCenterOptions;
/**
* If `true`, the "drag to pitch" interaction is enabled. An `Object` value is passed as options to {@link TwoFingersTouchPitchHandler.enable}.
* @defaultValue true
*/
touchPitch?: boolean | AroundCenterOptions;
/**
* If `true` or set to an options object, the map is only accessible on desktop while holding Command/Ctrl and only accessible on mobile with two fingers. Interacting with the map using normal gestures will trigger an informational screen. With this option enabled, "drag to pitch" requires a three-finger gesture. Cooperative gestures are disabled when a map enters fullscreen using {@link FullscreenControl}.
* @defaultValue false
*/
cooperativeGestures?: GestureOptions;
/**
* If `true`, the map will automatically resize when the browser window resizes.
* @defaultValue true
*/
trackResize?: boolean;
/**
* The initial geographical centerpoint of the map. If `center` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `[0, 0]` Note: MapLibre GL JS uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON.
* @defaultValue [0, 0]
*/
center?: LngLatLike;
/**
* The elevation of the initial geographical centerpoint of the map, in meters above sea level. If `elevation` is not specified in the constructor options, it will default to `0`.
* @defaultValue 0
*/
elevation?: number;
/**
* The initial zoom level of the map. If `zoom` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
* @defaultValue 0
*/
zoom?: number;
/**
* The initial bearing (rotation) of the map, measured in degrees counter-clockwise from north. If `bearing` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
* @defaultValue 0
*/
bearing?: number;
/**
* The initial pitch (tilt) of the map, measured in degrees away from the plane of the screen (0-85). If `pitch` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`. Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project.
* @defaultValue 0
*/
pitch?: number;
/**
* The initial roll angle of the map, measured in degrees counter-clockwise about the camera boresight. If `roll` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
* @defaultValue 0
*/
roll?: number;
/**
* If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`:
*
* - When the map is zoomed out far enough that a single representation of the world does not fill the map's entire
* container, there will be blank space beyond 180 and -180 degrees longitude.
* - Features that cross 180 and -180 degrees longitude will be cut in two (with one portion on the right edge of the
* map and the other on the left edge of the map) at every zoom level.
* @defaultValue true
*/
renderWorldCopies?: boolean;
/**
* The maximum number of tiles stored in the tile cache for a given source. If omitted, the cache will be dynamically sized based on the current viewport which can be set using `maxTileCacheZoomLevels` constructor options.
* @defaultValue null
*/
maxTileCacheSize?: number | null;
/**
* The maximum number of zoom levels for which to store tiles for a given source. Tile cache dynamic size is calculated by multiplying `maxTileCacheZoomLevels` with the approximate number of tiles in the viewport for a given source.
* @defaultValue 5
*/
maxTileCacheZoomLevels?: number;
/**
* A callback run before the Map makes a request for an external URL. The callback can be used to modify the url, set headers, or set the credentials property for cross-origin requests.
* Expected to return an object with a `url` property and optionally `headers` and `credentials` properties.
* @defaultValue null
*/
transformRequest?: RequestTransformFunction | null;
/**
* A callback run before the map's camera is moved due to user input or animation. The callback can be used to modify the new center, zoom, pitch and bearing.
* Expected to return an object containing center, zoom, pitch or bearing values to overwrite.
* @defaultValue null
*/
transformCameraUpdate?: CameraUpdateTransformFunction | null;
/**
* A patch to apply to the default localization table for UI strings, e.g. control tooltips. The `locale` object maps namespaced UI string IDs to translated strings in the target language; see `src/ui/default_locale.js` for an example with all supported string IDs. The object may specify all UI strings (thereby adding support for a new translation) or only a subset of strings (thereby patching the default translation table).
* @defaultValue null
*/
locale?: any;
/**
* Controls the duration of the fade-in/fade-out animation for label collisions after initial map load, in milliseconds. This setting affects all symbol layers. This setting does not affect the duration of runtime styling transitions or raster tile cross-fading.
* @defaultValue 300
*/
fadeDuration?: number;
/**
* If `true`, symbols from multiple sources can collide with each other during collision detection. If `false`, collision detection is run separately for the symbols in each source.
* @defaultValue true
*/
crossSourceCollisions?: boolean;
/**
* If `true`, Resource Timing API information will be collected for requests made by GeoJSON and Vector Tile web workers (this information is normally inaccessible from the main Javascript thread). Information will be returned in a `resourceTiming` property of relevant `data` events.
* @defaultValue false
*/
collectResourceTiming?: boolean;
/**
* The max number of pixels a user can shift the mouse pointer during a click for it to be considered a valid click (as opposed to a mouse drag).
* @defaultValue 3
*/
clickTolerance?: number;
/**
* The initial bounds of the map. If `bounds` is specified, it overrides `center` and `zoom` constructor options.
*/
bounds?: LngLatBoundsLike;
/**
* A {@link FitBoundsOptions} options object to use _only_ when fitting the initial `bounds` provided above.
*/
fitBoundsOptions?: FitBoundsOptions;
/**
* Defines a CSS
* font-family for locally overriding generation of Chinese, Japanese, and Korean characters.
* For these characters, font settings from the map's style will be ignored, except for font-weight keywords (light/regular/medium/bold).
* Set to `false`, to enable font settings from the map's style for these glyph ranges.
* The purpose of this option is to avoid bandwidth-intensive glyph server requests. (See [Use locally generated ideographs](https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs).)
* @defaultValue 'sans-serif'
*/
localIdeographFontFamily?: string | false;
/**
* The map's MapLibre style. This must be a JSON object conforming to
* the schema described in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/),
* or a URL to such JSON.
* When the style is not specified, calling {@link Map.setStyle} is required to render the map.
*/
style?: StyleSpecification | string;
/**
* If `false`, the map's pitch (tilt) control with "drag to rotate" interaction will be disabled.
* @defaultValue true
*/
pitchWithRotate?: boolean;
/**
* If `false`, the map's roll control with "drag to rotate" interaction will be disabled.
* @defaultValue false
*/
rollEnabled?: boolean;
/**
* The pixel ratio.
* The canvas' `width` attribute will be `container.clientWidth * pixelRatio` and its `height` attribute will be `container.clientHeight * pixelRatio`. Defaults to `devicePixelRatio` if not specified.
*/
pixelRatio?: number;
/**
* If false, style validation will be skipped. Useful in production environment.
* @defaultValue true
*/
validateStyle?: boolean;
/**
* The canvas' `width` and `height` max size. The values are passed as an array where the first element is max width and the second element is max height.
* You shouldn't set this above WebGl `MAX_TEXTURE_SIZE`.
* @defaultValue [4096, 4096].
*/
maxCanvasSize?: [number, number];
/**
* Determines whether to cancel, or retain, tiles from the current viewport which are still loading but which belong to a farther (smaller) zoom level than the current one.
* * If `true`, when zooming in, tiles which didn't manage to load for previous zoom levels will become canceled. This might save some computing resources for slower devices, but the map details might appear more abruptly at the end of the zoom.
* * If `false`, when zooming in, the previous zoom level(s) tiles will progressively appear, giving a smoother map details experience. However, more tiles will be rendered in a short period of time.
* @defaultValue true
*/
cancelPendingTileRequestsWhileZooming?: boolean;
/**
* If true, the elevation of the center point will automatically be set to the terrain elevation
* (or zero if terrain is not enabled). If false, the elevation of the center point will default
* to sea level and will not automatically update. Defaults to true. Needs to be set to false to
* keep the camera above ground when pitch \> 90 degrees.
*/
centerClampedToGround?: boolean;
};
export type AddImageOptions = {
};
// This type is used inside map since all properties are assigned a default value.
export type CompleteMapOptions = Complete<MapOptions>;
type DelegatedListener = {
layers: string[];
listener: Listener;
delegates: {[E in keyof MapEventType]?: Delegate<MapEventType[E]>};
};
type Delegate<E extends Event = Event> = (e: E) => void;
const defaultMinZoom = -2;
const defaultMaxZoom = 22;
// the default values, but also the valid range
const defaultMinPitch = 0;
const defaultMaxPitch = 60;
// use this variable to check maxPitch for validity
const maxPitchThreshold = 180;
const defaultOptions: Readonly<Partial<MapOptions>> = {
hash: false,
interactive: true,
bearingSnap: 7,
attributionControl: defaultAttributionControlOptions,
maplibreLogo: false,
refreshExpiredTiles: true,
canvasContextAttributes: {
antialias: false,
preserveDrawingBuffer: false,
powerPreference: 'high-performance',
failIfMajorPerformanceCaveat: false,
desynchronized: false,
contextType: undefined
},
scrollZoom: true,
minZoom: defaultMinZoom,
maxZoom: defaultMaxZoom,
minPitch: defaultMinPitch,
maxPitch: defaultMaxPitch,
boxZoom: true,
dragRotate: true,
dragPan: true,
keyboard: true,
doubleClickZoom: true,
touchZoomRotate: true,
touchPitch: true,
cooperativeGestures: false,
trackResize: true,
center: [0, 0],
elevation: 0,
zoom: 0,
bearing: 0,
pitch: 0,
roll: 0,
renderWorldCopies: true,
maxTileCacheSize: null,
maxTileCacheZoomLevels: config.MAX_TILE_CACHE_ZOOM_LEVELS,
transformRequest: null,
transformCameraUpdate: null,
fadeDuration: 300,
crossSourceCollisions: true,
clickTolerance: 3,
localIdeographFontFamily: 'sans-serif',
pitchWithRotate: true,
rollEnabled: false,
validateStyle: true,
/**Because GL MAX_TEXTURE_SIZE is usually at least 4096px. */
maxCanvasSize: [4096, 4096],
cancelPendingTileRequestsWhileZooming: true,
centerClampedToGround: true
};
/**
* The `Map` object represents the map on your page. It exposes methods
* and properties that enable you to programmatically change the map,
* and fires events as users interact with it.
*
* You create a `Map` by specifying a `container` and other options, see {@link MapOptions} for the full list.
* Then MapLibre GL JS initializes the map on the page and returns your `Map` object.
*
* @group Main
*
* @example
* ```ts
* let map = new Map({
* container: 'map',
* center: [-122.420679, 37.772537],
* zoom: 13,
* style: style_object,
* hash: true,
* transformRequest: (url, resourceType)=> {
* if(resourceType === 'Source' && url.startsWith('http://myHost')) {
* return {
* url: url.replace('http', 'https'),
* headers: { 'my-custom-header': true},
* credentials: 'include' // Include cookies for cross-origin requests
* }
* }
* }
* });
* ```
* @see [Display a map](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-map/)
*/
export class Map extends Camera {
style: Style;
painter: Painter;
handlers: HandlerManager;
_container: HTMLElement;
_canvasContainer: HTMLElement;
_controlContainer: HTMLElement;
_controlPositions: Partial<Record<ControlPosition, HTMLElement>>;
_interactive: boolean;
_showTileBoundaries: boolean;
_showCollisionBoxes: boolean;
_showPadding: boolean;
_showOverdrawInspector: boolean;
_repaint: boolean;
_vertices: boolean;
_canvas: HTMLCanvasElement;
_maxTileCacheSize: number | null;
_maxTileCacheZoomLevels: number;
_frameRequest: AbortController;
_styleDirty: boolean;
_sourcesDirty: boolean;
_placementDirty: boolean;
_loaded: boolean;
_idleTriggered = false;
// accounts for placement finishing as well
_fullyLoaded: boolean;
_trackResize: boolean;
_resizeObserver: ResizeObserver;
_canvasContextAttributes: WebGLContextAttributesWithType;
_refreshExpiredTiles: boolean;
_hash: Hash;
_delegatedListeners: Record<string, DelegatedListener[]>;
_fadeDuration: number;
_crossSourceCollisions: boolean;
_crossFadingFactor = 1;
_collectResourceTiming: boolean;
_renderTaskQueue = new TaskQueue();
_controls: Array<IControl> = [];
_mapId = uniqueId();
_localIdeographFontFamily: string | false;
_validateStyle: boolean;
_requestManager: RequestManager;
_locale: typeof defaultLocale;
_removed: boolean;
_clickTolerance: number;
_overridePixelRatio: number | null | undefined;
_maxCanvasSize: [number, number];
_terrainDataCallback: (e: MapStyleDataEvent | MapSourceDataEvent) => void;
/**
* @internal
* image queue throttling handle. To be used later when clean up
*/
_imageQueueHandle: number;
/**
* The map's {@link ScrollZoomHandler}, which implements zooming in and out with a scroll wheel or trackpad.
* Find more details and examples using `scrollZoom` in the {@link ScrollZoomHandler} section.
*/
scrollZoom: ScrollZoomHandler;
/**
* The map's {@link BoxZoomHandler}, which implements zooming using a drag gesture with the Shift key pressed.
* Find more details and examples using `boxZoom` in the {@link BoxZoomHandler} section.
*/
boxZoom: BoxZoomHandler;
/**
* The map's {@link DragRotateHandler}, which implements rotating the map while dragging with the right
* mouse button or with the Control key pressed. Find more details and examples using `dragRotate`
* in the {@link DragRotateHandler} section.
*/
dragRotate: DragRotateHandler;
/**
* The map's {@link DragPanHandler}, which implements dragging the map with a mouse or touch gesture.
* Find more details and examples using `dragPan` in the {@link DragPanHandler} section.
*/
dragPan: DragPanHandler;
/**
* The map's {@link KeyboardHandler}, which allows the user to zoom, rotate, and pan the map using keyboard
* shortcuts. Find more details and examples using `keyboard` in the {@link KeyboardHandler} section.
*/
keyboard: KeyboardHandler;
/**
* The map's {@link DoubleClickZoomHandler}, which allows the user to zoom by double clicking.
* Find more details and examples using `doubleClickZoom` in the {@link DoubleClickZoomHandler} section.
*/
doubleClickZoom: DoubleClickZoomHandler;
/**
* The map's {@link TwoFingersTouchZoomRotateHandler}, which allows the user to zoom or rotate the map with touch gestures.
* Find more details and examples using `touchZoomRotate` in the {@link TwoFingersTouchZoomRotateHandler} section.
*/
touchZoomRotate: TwoFingersTouchZoomRotateHandler;
/**
* The map's {@link TwoFingersTouchPitchHandler}, which allows the user to pitch the map with touch gestures.
* Find more details and examples using `touchPitch` in the {@link TwoFingersTouchPitchHandler} section.
*/
touchPitch: TwoFingersTouchPitchHandler;
/**
* The map's {@link CooperativeGesturesHandler}, which allows the user to see cooperative gesture info when user tries to zoom in/out.
* Find more details and examples using `cooperativeGestures` in the {@link CooperativeGesturesHandler} section.
*/
cooperativeGestures: CooperativeGesturesHandler;
/**
* The map's property which determines whether to cancel, or retain, tiles from the current viewport which are still loading but which belong to a farther (smaller) zoom level than the current one.
* * If `true`, when zooming in, tiles which didn't manage to load for previous zoom levels will become canceled. This might save some computing resources for slower devices, but the map details might appear more abruptly at the end of the zoom.
* * If `false`, when zooming in, the previous zoom level(s) tiles will progressively appear, giving a smoother map details experience. However, more tiles will be rendered in a short period of time.
* @defaultValue true
*/
cancelPendingTileRequestsWhileZooming: boolean;
constructor(options: MapOptions) {
PerformanceUtils.mark(PerformanceMarkers.create);
const resolvedOptions = {...defaultOptions, ...options, canvasContextAttributes: {
...defaultOptions.canvasContextAttributes,
...options.canvasContextAttributes
}} as CompleteMapOptions;
if (resolvedOptions.minZoom != null && resolvedOptions.maxZoom != null && resolvedOptions.minZoom > resolvedOptions.maxZoom) {
throw new Error('maxZoom must be greater than or equal to minZoom');
}
if (resolvedOptions.minPitch != null && resolvedOptions.maxPitch != null && resolvedOptions.minPitch > resolvedOptions.maxPitch) {
throw new Error('maxPitch must be greater than or equal to minPitch');
}
if (resolvedOptions.minPitch != null && resolvedOptions.minPitch < defaultMinPitch) {
throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`);
}
if (resolvedOptions.maxPitch != null && resolvedOptions.maxPitch > maxPitchThreshold) {
throw new Error(`maxPitch must be less than or equal to ${maxPitchThreshold}`);
}
// For now we will use a temporary MercatorTransform instance.
// Transform specialization will later be set by style when it creates its projection instance.
// When this happens, the new transform will inherit all properties of this temporary transform.
const transform = new MercatorTransform();
const cameraHelper = new MercatorCameraHelper();
if (resolvedOptions.minZoom !== undefined) {
transform.setMinZoom(resolvedOptions.minZoom);
}
if (resolvedOptions.maxZoom !== undefined) {
transform.setMaxZoom(resolvedOptions.maxZoom);
}
if (resolvedOptions.minPitch !== undefined) {
transform.setMinPitch(resolvedOptions.minPitch);
}
if (resolvedOptions.maxPitch !== undefined) {
transform.setMaxPitch(resolvedOptions.maxPitch);
}
if (resolvedOptions.renderWorldCopies !== undefined) {
transform.setRenderWorldCopies(resolvedOptions.renderWorldCopies);
}
super(transform, cameraHelper, {bearingSnap: resolvedOptions.bearingSnap});
this._interactive = resolvedOptions.interactive;
this._maxTileCacheSize = resolvedOptions.maxTileCacheSize;
this._maxTileCacheZoomLevels = resolvedOptions.maxTileCacheZoomLevels;
this._canvasContextAttributes = {...resolvedOptions.canvasContextAttributes};
this._trackResize = resolvedOptions.trackResize === true;
this._bearingSnap = resolvedOptions.bearingSnap;
this._centerClampedToGround = resolvedOptions.centerClampedToGround;
this._refreshExpiredTiles = resolvedOptions.refreshExpiredTiles === true;
this._fadeDuration = resolvedOptions.fadeDuration;
this._crossSourceCollisions = resolvedOptions.crossSourceCollisions === true;
this._collectResourceTiming = resolvedOptions.collectResourceTiming === true;
this._locale = {...defaultLocale, ...resolvedOptions.locale};
this._clickTolerance = resolvedOptions.clickTolerance;
this._overridePixelRatio = resolvedOptions.pixelRatio;
this._maxCanvasSize = resolvedOptions.maxCanvasSize;
this.transformCameraUpdate = resolvedOptions.transformCameraUpdate;
this.cancelPendingTileRequestsWhileZooming = resolvedOptions.cancelPendingTileRequestsWhileZooming === true;
this._imageQueueHandle = ImageRequest.addThrottleControl(() => this.isMoving());
this._requestManager = new RequestManager(resolvedOptions.transformRequest);
if (typeof resolvedOptions.container === 'string') {
this._container = document.getElementById(resolvedOptions.container);
if (!this._container) {
throw new Error(`Container '${resolvedOptions.container}' not found.`);
}
} else if (resolvedOptions.container instanceof HTMLElement) {
this._container = resolvedOptions.container;
} else {
throw new Error('Invalid type: \'container\' must be a String or HTMLElement.');
}
if (resolvedOptions.maxBounds) {
this.setMaxBounds(resolvedOptions.maxBounds);
}
this._setupContainer();
this._setupPainter();
this.on('move', () => this._update(false));
this.on('moveend', () => this._update(false));
this.on('zoom', () => this._update(true));
this.on('terrain', () => {
this.painter.terrainFacilitator.dirty = true;
this._update(true);
});
this.once('idle', () => { this._idleTriggered = true; });
if (typeof window !== 'undefined') {
addEventListener('online', this._onWindowOnline, false);
let initialResizeEventCaptured = false;
const throttledResizeCallback = throttle((entries: ResizeObserverEntry[]) => {
if (this._trackResize && !this._removed) {
this.resize(entries);
this.redraw();
}
}, 50);
this._resizeObserver = new ResizeObserver((entries) => {
if (!initialResizeEventCaptured) {
initialResizeEventCaptured = true;
return;
}
throttledResizeCallback(entries);
});
this._resizeObserver.observe(this._container);
}
this.handlers = new HandlerManager(this, resolvedOptions);
const hashName = (typeof resolvedOptions.hash === 'string' && resolvedOptions.hash) || undefined;
this._hash = resolvedOptions.hash && (new Hash(hashName)).addTo(this);
// don't set position from options if set through hash
if (!this._hash || !this._hash._onHashChange()) {
this.jumpTo({
center: resolvedOptions.center,
elevation: resolvedOptions.elevation,
zoom: resolvedOptions.zoom,
bearing: resolvedOptions.bearing,
pitch: resolvedOptions.pitch,
roll: resolvedOptions.roll
});
if (resolvedOptions.bounds) {
this.resize();
this.fitBounds(resolvedOptions.bounds, extend({}, resolvedOptions.fitBoundsOptions, {duration: 0}));
}
}
// When no style is set or it's using something other than the globe projection, we can constrain the camera.
// When a style is set with other projections though, we can't constrain the camera until the style is loaded
// and the correct transform is used. Otherwise, valid points in the desired projection could be rejected
const shouldConstrainUsingMercatorTransform = typeof resolvedOptions.style === 'string' || !(resolvedOptions.style?.projection?.type === 'globe');
this.resize(null, shouldConstrainUsingMercatorTransform);
this._localIdeographFontFamily = resolvedOptions.localIdeographFontFamily;
this._validateStyle = resolvedOptions.validateStyle;
if (resolvedOptions.style) this.setStyle(resolvedOptions.style, {localIdeographFontFamily: resolvedOptions.localIdeographFontFamily});
if (resolvedOptions.attributionControl)
this.addControl(new AttributionControl(typeof resolvedOptions.attributionControl === 'boolean' ? undefined : resolvedOptions.attributionControl));
if (resolvedOptions.maplibreLogo)
this.addControl(new LogoControl(), resolvedOptions.logoPosition);
this.on('style.load', () => {
// If we didn't constrain the camera before, we do it now
if (!shouldConstrainUsingMercatorTransform) this._resizeTransform();
if (this.transform.unmodified) {
const coercedOptions = pick(this.style.stylesheet, ['center', 'zoom', 'bearing', 'pitch', 'roll']) as CameraOptions;
this.jumpTo(coercedOptions);
}
});
this.on('data', (event: MapDataEvent) => {
this._update(event.dataType === 'style');
this.fire(new Event(`${event.dataType}data`, event));
});
this.on('dataloading', (event: MapDataEvent) => {
this.fire(new Event(`${event.dataType}dataloading`, event));
});
this.on('dataabort', (event: MapDataEvent) => {
this.fire(new Event('sourcedataabort', event));
});
}
/**
* @internal
* Returns a unique number for this map instance which is used for the MapLoadEvent
* to make sure we only fire one event per instantiated map object.
* @returns the uniq map ID
*/
_getMapId() {
return this._mapId;
}
/**
* Sets a global state property that can be retrieved with the [`global-state` expression](https://maplibre.org/maplibre-style-spec/expressions/#global-state).
* If the value is null, it resets the property to its default value defined in the [`state` style property](https://maplibre.org/maplibre-style-spec/root/#state).
*
* Note that changing `global-state` values defined in layout properties is not supported, and will be ignored.
*
* @param propertyName - The name of the state property to set.
* @param value - The value of the state property to set.
*/
setGlobalStateProperty(propertyName: string, value: any) {
this.style.setGlobalStateProperty(propertyName, value);
return this._update(true);
}
/**
* Returns the global map state
*
* @returns The map state object.
*/
getGlobalState(): Record<string, any> {
return this.style.getGlobalState();
}
/**
* Adds an {@link IControl} to the map, calling `control.onAdd(this)`.
*
* An {@link ErrorEvent} will be fired if the image parameter is invalid.
*
* @param control - The {@link IControl} to add.
* @param position - position on the map to which the control will be added.
* Valid values are `'top-left'`, `'top-right'`, `'bottom-left'`, and `'bottom-right'`. Defaults to `'top-right'`.
* @example
* Add zoom and rotation controls to the map.
* ```ts
* map.addControl(new NavigationControl());
* ```
* @see [Display map navigation controls](https://maplibre.org/maplibre-gl-js/docs/examples/display-map-navigation-controls/)
*/
addControl(control: IControl, position?: ControlPosition): Map {
if (position === undefined) {
if (control.getDefaultPosition) {
position = control.getDefaultPosition();
} else {
position = 'top-right';
}
}
if (!control || !control.onAdd) {
return this.fire(new ErrorEvent(new Error(
'Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.')));
}
const controlElement = control.onAdd(this);
this._controls.push(control);
const positionContainer = this._controlPositions[position];
if (position.indexOf('bottom') !== -1) {
positionContainer.insertBefore(controlElement, positionContainer.firstChild);
} else {
positionContainer.appendChild(controlElement);
}
return this;
}
/**
* Removes the control from the map.
*
* An {@link ErrorEvent} will be fired if the image parameter is invalid.
*
* @param control - The {@link IControl} to remove.
* @example
* ```ts
* // Define a new navigation control.
* let navigation = new NavigationControl();
* // Add zoom and rotation controls to the map.
* map.addControl(navigation);
* // Remove zoom and rotation controls from the map.
* map.removeControl(navigation);
* ```
*/
removeControl(control: IControl): Map {
if (!control || !control.onRemove) {
return this.fire(new ErrorEvent(new Error(
'Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.')));
}
const ci = this._controls.indexOf(control);
if (ci > -1) this._controls.splice(ci, 1);
control.onRemove(this);
return this;
}
/**
* Checks if a control exists on the map.
*
* @param control - The {@link IControl} to check.
* @returns true if map contains control.
* @example
* ```ts
* // Define a new navigation control.
* let navigation = new NavigationControl();
* // Add zoom and rotation controls to the map.
* map.addControl(navigation);
* // Check that the navigation control exists on the map.
* map.hasControl(navigation);
* ```
*/
hasControl(control: IControl): boolean {
return this._controls.indexOf(control) > -1;
}
calculateCameraOptionsFromTo(from: LngLat, altitudeFrom: number, to: LngLat, altitudeTo?: number): CameraOptions {
if (altitudeTo == null && this.terrain) {
altitudeTo = this.terrain.getElevationForLngLatZoom(to, this.transform.tileZoom);
}
return super.calculateCameraOptionsFromTo(from, altitudeFrom, to, altitudeTo);
}
/**
* Resizes the map according to the dimensions of its
* `container` element.
*
* Checks if the map container size changed and updates the map if it has changed.
* This method must be called after the map's `container` is resized programmatically
* or when the map is shown after being initially hidden with CSS.
*
* Triggers the following events: `movestart`, `move`, `moveend`, and `resize`.
*
* @param eventData - Additional properties to be passed to `movestart`, `move`, `resize`, and `moveend`
* events that get triggered as a result of resize. This can be useful for differentiating the
* source of an event (for example, user-initiated or programmatically-triggered events).
* @example
* Resize the map when the map container is shown after being initially hidden with CSS.
* ```ts
* let mapDiv = document.getElementById('map');
* if (mapDiv.style.visibility === true) map.resize();
* ```
*/
resize(eventData?: any, constrainTransform = true): Map {
const [width, height] = this._containerDimensions();
const clampedPixelRatio = this._getClampedPixelRatio(width, height);
this._resizeCanvas(width, height, clampedPixelRatio);
this.painter.resize(width, height, clampedPixelRatio);
// check if we've reached GL limits, in that case further clamps pixelRatio
if (this.painter.overLimit()) {
const gl = this.painter.context.gl;
// store updated _maxCanvasSize value
this._maxCanvasSize = [gl.drawingBufferWidth, gl.drawingBufferHeight];
const clampedPixelRatio = this._getClampedPixelRatio(width, height);
this._resizeCanvas(width, height, clampedPixelRatio);
this.painter.resize(width, height, clampedPixelRatio);
}
this._resizeTransform(constrainTransform);
const fireMoving = !this._moving;
if (fireMoving) {
this.stop();
this.fire(new Event('movestart', eventData))
.fire(new Event('move', eventData));
}
this.fire(new Event('resize', eventData));
if (fireMoving) this.fire(new Event('moveend', eventData));
return this;
}
_resizeTransform(constrainTransform = true) {
const [width, height] = this._containerDimensions();
this.transform.resize(width, height, constrainTransform);
this._requestedCameraState?.resize(width, height, constrainTransform);
}
/**
* @internal
* Return the map's pixel ratio eventually scaled down to respect maxCanvasSize.
* Internally you should use this and not getPixelRatio().
*/
_getClampedPixelRatio(width: number, height: number): number {
const {0: maxCanvasWidth, 1: maxCanvasHeight} = this._maxCanvasSize;
const pixelRatio = this.getPixelRatio();
const canvasWidth = width * pixelRatio;
const canvasHeight = height * pixelRatio;
const widthScaleFactor = canvasWidth > maxCanvasWidth ? (maxCanvasWidth / canvasWidth) : 1;
const heightScaleFactor = canvasHeight > maxCanvasHeight ? (maxCanvasHeight / canvasHeight) : 1;
return Math.min(widthScaleFactor, heightScaleFactor) * pixelRatio;
}
/**
* Returns the map's pixel ratio.
* Note that the pixel ratio actually applied may be lower to respect maxCanvasSize.
* @returns The pixel ratio.
*/
getPixelRatio(): number {
return this._overridePixelRatio ?? devicePixelRatio;
}
/**
* Sets the map's pixel ratio. This allows to override `devicePixelRatio`.
* After this call, the canvas' `width` attribute will be `container.clientWidth * pixelRatio`
* and its height attribute will be `container.clientHeight * pixelRatio`.
* Set this to null to disable `devicePixelRatio` override.
* Note that the pixel ratio actually applied may be lower to respect maxCanvasSize.
* @param pixelRatio - The pixel ratio.
*/
setPixelRatio(pixelRatio: number) {
this._overridePixelRatio = pixelRatio;
this.resize();
}
/**
* Returns the map's geographical bounds. When the bearing or pitch is non-zero, the visible region is not
* an axis-aligned rectangle, and the result is the smallest bounds that encompasses the visible region.
* @returns The geographical bounds of the map as {@link LngLatBounds}.
* @example
* ```ts
* let bounds = map.getBounds();
* ```
*/
getBounds(): LngLatBounds {
return this.transform.getBounds();
}
/**
* Returns the maximum geographical bounds the map is constrained to, or `null` if none set.
* @returns The map object.
* @example
* ```ts
* let maxBounds = map.getMaxBounds();
* ```
*/
getMaxBounds(): LngLatBounds | null {
return this.transform.getMaxBounds();
}
/**
* Sets or clears the map's geographical bounds.
*
* Pan and zoom operations are constrained within these bounds.
* If a pan or zoom is performed that would
* display regions outside these bounds, the map will
* instead display a position and zoom level
* as close as possible to the operation's request while still
* remaining within the bounds.
*
* @param bounds - The maximum bounds to set. If `null` or `undefined` is provided, the function removes the map's maximum bounds.
* @example
* Define bounds that conform to the `LngLatBoundsLike` object as set the max bounds.
* ```ts
* let bounds = [
* [-74.04728, 40.68392], // [west, south]
* [-73.91058, 40.87764] // [east, north]
* ];
* map.setMaxBounds(bounds);
* ```
*/
setMaxBounds(bounds?: LngLatBoundsLike | null): Map {
this.transform.setMaxBounds(LngLatBounds.convert(bounds));
return this._update();
}
/**
* Sets or clears the map's minimum zoom level.
* If the map's current zoom level is lower than the new minimum,
* the map will zoom to the new minimum.
*
* It is not always possible to zoom out and reach the set `minZoom`.
* Other factors such as map height may restrict zooming. For example,
* if the map is 512px tall it will not be possible to zoom below zoom 0
* no matter what the `minZoom` is set to.
*
* A {@link ErrorEvent} event will be fired if minZoom is out of bounds.
*
* @param minZoom - The minimum zoom level to set (-2 - 24).
* If `null` or `undefined` is provided, the function removes the current minimum zoom (i.e. sets it to -2).
* @example
* ```ts
* map.setMinZoom(12.25);
* ```
*/
setMinZoom(minZoom?: number | null): Map {
minZoom = minZoom === null || minZoom === undefined ? defaultMinZoom : minZoom;
if (minZoom >= defaultMinZoom && minZoom <= this.transform.maxZoom) {
this.transform.setMinZoom(minZoom);
this._update();
if (this.getZoom() < minZoom) this.setZoom(minZoom);
return this;
} else throw new Error(`minZoom must be between ${defaultMinZoom} and the current maxZoom, inclusive`);
}
/**
* Returns the map's minimum allowable zoom level.
*
* @returns minZoom
* @example
* ```ts
* let minZoom = map.getMinZoom();
* ```
*/
getMinZoom(): number { return this.transform.minZoom; }
/**
* Sets or clears the map's maximum zoom level.
* If the map's current zoom level is higher than the new maximum,
* the map will zoom to the new maximum.
*
* A {@link ErrorEvent} event will be fired if minZoom is out of bounds.
*
* @param maxZoom - The maximum zoom level to set.
* If `null` or `undefined` is provided, the function removes the current maximum zoom (sets it to 22).
* @example
* ```ts
* map.setMaxZoom(18.75);
* ```
*/
setMaxZoom(maxZoom?: number | null): Map {
maxZoom = maxZoom === null || maxZoom === undefined ? defaultMaxZoom : maxZoom;
if (maxZoom >= this.transform.minZoom) {
this.transform.setMaxZoom(maxZoom);
this._update();
if (this.getZoom() > maxZoom) this.setZoom(maxZoom);
return this;
} else throw new Error('maxZoom must be greater than the current minZoom');
}
/**
* Returns the map's maximum allowable zoom level.
*
* @returns The maxZoom
* @example
* ```ts
* let maxZoom = map.getMaxZoom();
* ```
*/
getMaxZoom(): number { return this.transform.maxZoom; }
/**
* Sets or clears the map's minimum pitch.
* If the map's current pitch is lower than the new minimum,
* the ma