UNPKG

maplibre-gl

Version:

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

1,039 lines (931 loc) 137 kB
import {extend, bindAll, warnOnce, uniqueId, isImageBitmap} from '../util/util'; import browser from '../util/browser'; import DOM from '../util/dom'; import packageJSON from '../../package.json' assert {type: 'json'}; import {getImage, GetImageCallback, getJSON, ResourceType} from '../util/ajax'; import {RequestManager} from '../util/request_manager'; import Style from '../style/style'; import EvaluationParameters from '../style/evaluation_parameters'; import Painter from '../render/painter'; import Transform from '../geo/transform'; import Hash from './hash'; import HandlerManager from './handler_manager'; import Camera from './camera'; import LngLat from '../geo/lng_lat'; import LngLatBounds from '../geo/lng_lat_bounds'; import Point from '@mapbox/point-geometry'; import AttributionControl from './control/attribution_control'; import LogoControl from './control/logo_control'; import {supported} from '@mapbox/mapbox-gl-supported'; import {RGBAImage} from '../util/image'; import {Event, ErrorEvent, Listener} from '../util/evented'; import {MapEventType, MapLayerEventType, MapMouseEvent} from './events'; import TaskQueue from '../util/task_queue'; import webpSupported from '../util/webp_supported'; import {PerformanceMarkers, PerformanceUtils} from '../util/performance'; import {setCacheLimits} from '../util/tile_request_cache'; import {Source} from '../source/source'; import StyleLayer from '../style/style_layer'; import type {RequestTransformFunction} from '../util/request_manager'; import type {LngLatLike} from '../geo/lng_lat'; import type {LngLatBoundsLike} from '../geo/lng_lat_bounds'; import type {FeatureIdentifier, StyleOptions, StyleSetterOptions} from '../style/style'; import type {MapEvent, MapDataEvent} from './events'; import type {CustomLayerInterface} from '../style/style_layer/custom_style_layer'; import type {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 {TouchPitchHandler} from './handler/touch_zoom_rotate'; import type DragRotateHandler from './handler/shim/drag_rotate'; import DragPanHandler, {DragPanOptions} from './handler/shim/drag_pan'; import type KeyboardHandler from './handler/keyboard'; import type DoubleClickZoomHandler from './handler/shim/dblclick_zoom'; import type TouchZoomRotateHandler from './handler/shim/touch_zoom_rotate'; import defaultLocale from './default_locale'; import type {TaskID} from '../util/task_queue'; import type {Cancelable} from '../types/cancelable'; import type { LayerSpecification, FilterSpecification, StyleSpecification, LightSpecification, SourceSpecification, TerrainSpecification } from '../style-spec/types.g'; import {Callback} from '../types/callback'; import type {ControlPosition, IControl} from './control/control'; import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson'; const version = packageJSON.version; /* eslint-enable no-use-before-define */ export type MapOptions = { hash?: boolean | string; interactive?: boolean; container: HTMLElement | string; bearingSnap?: number; attributionControl?: boolean; customAttribution?: string | Array<string>; maplibreLogo?: boolean; logoPosition?: ControlPosition; failIfMajorPerformanceCaveat?: boolean; preserveDrawingBuffer?: boolean; antialias?: boolean; refreshExpiredTiles?: boolean; maxBounds?: LngLatBoundsLike; scrollZoom?: boolean; minZoom?: number | null; maxZoom?: number | null; minPitch?: number | null; maxPitch?: number | null; boxZoom?: boolean; dragRotate?: boolean; dragPan?: DragPanOptions | boolean; keyboard?: boolean; doubleClickZoom?: boolean; touchZoomRotate?: boolean; touchPitch?: boolean; cooperativeGestures?: boolean | GestureOptions; trackResize?: boolean; center?: LngLatLike; zoom?: number; bearing?: number; pitch?: number; renderWorldCopies?: boolean; maxTileCacheSize?: number; transformRequest?: RequestTransformFunction; locale?: any; fadeDuration?: number; crossSourceCollisions?: boolean; collectResourceTiming?: boolean; clickTolerance?: number; bounds?: LngLatBoundsLike; fitBoundsOptions?: Object; localIdeographFontFamily?: string; style: StyleSpecification | string; pitchWithRotate?: boolean; pixelRatio?: number; }; export type GestureOptions = { windowsHelpText?: string; macHelpText?: string; mobileHelpText?: string; }; // See article here: https://medium.com/terria/typescript-transforming-optional-properties-to-required-properties-that-may-be-undefined-7482cb4e1585 type Complete<T> = { [P in keyof Required<T>]: Pick<T, P> extends Required<Pick<T, P>> ? T[P] : (T[P] | undefined); } // This type is used inside map since all properties are assigned a default value. export type CompleteMapOptions = Complete<MapOptions>; 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 = 85; const defaultOptions = { center: [0, 0], zoom: 0, bearing: 0, pitch: 0, minZoom: defaultMinZoom, maxZoom: defaultMaxZoom, minPitch: defaultMinPitch, maxPitch: defaultMaxPitch, interactive: true, scrollZoom: true, boxZoom: true, dragRotate: true, dragPan: true, keyboard: true, doubleClickZoom: true, touchZoomRotate: true, touchPitch: true, cooperativeGestures: undefined, bearingSnap: 7, clickTolerance: 3, pitchWithRotate: true, hash: false, attributionControl: true, maplibreLogo: false, failIfMajorPerformanceCaveat: false, preserveDrawingBuffer: false, trackResize: true, renderWorldCopies: true, refreshExpiredTiles: true, maxTileCacheSize: null, localIdeographFontFamily: 'sans-serif', transformRequest: null, fadeDuration: 300, crossSourceCollisions: true } as CompleteMapOptions; /** * 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. * Then MapLibre GL JS initializes the map on the page and returns your `Map` * object. * * @extends Evented * @param {Object} options * @param {HTMLElement|string} options.container 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. * @param {number} [options.minZoom=0] The minimum zoom level of the map (0-24). * @param {number} [options.maxZoom=22] The maximum zoom level of the map (0-24). * @param {number} [options.minPitch=0] The minimum pitch of the map (0-85). 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. * @param {number} [options.maxPitch=60] The maximum pitch of the map (0-85). 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. * @param {Object|string} [options.style] The map's MapLibre style. This must be an a JSON object conforming to * the schema described in the [MapLibre Style Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/), or a URL to * such JSON. * * * @param {(boolean|string)} [options.hash=false] 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. * @param {boolean} [options.interactive=true] If `false`, no mouse, touch, or keyboard listeners will be attached to the map, so it will not respond to interaction. * @param {number} [options.bearingSnap=7] 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. * @param {boolean} [options.pitchWithRotate=true] If `false`, the map's pitch (tilt) control with "drag to rotate" interaction will be disabled. * @param {number} [options.clickTolerance=3] 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). * @param {boolean} [options.attributionControl=true] If `true`, an {@link AttributionControl} will be added to the map. * @param {string | Array<string>} [options.customAttribution] String or strings to show in an {@link AttributionControl}. Only applicable if `options.attributionControl` is `true`. * @param {boolean} [options.maplibreLogo=false] If `true`, the MapLibre logo will be shown. * @param {string} [options.logoPosition='bottom-left'] A string representing the position of the MapLibre wordmark on the map. Valid options are `top-left`,`top-right`, `bottom-left`, `bottom-right`. * @param {boolean} [options.failIfMajorPerformanceCaveat=false] If `true`, map creation will fail if the performance of MapLibre * GL JS would be dramatically worse than expected (i.e. a software renderer would be used). * @param {boolean} [options.preserveDrawingBuffer=false] If `true`, the map's canvas can be exported to a PNG using `map.getCanvas().toDataURL()`. This is `false` by default as a performance optimization. * @param {boolean} [options.antialias] If `true`, the gl context will be created with MSAA antialiasing, which can be useful for antialiasing custom layers. this is `false` by default as a performance optimization. * @param {boolean} [options.refreshExpiredTiles=true] If `false`, the map won't attempt to re-request tiles once they expire per their HTTP `cacheControl`/`expires` headers. * @param {LngLatBoundsLike} [options.maxBounds] If set, the map will be constrained to the given bounds. * @param {boolean|Object} [options.scrollZoom=true] If `true`, the "scroll to zoom" interaction is enabled. An `Object` value is passed as options to {@link ScrollZoomHandler#enable}. * @param {boolean} [options.boxZoom=true] If `true`, the "box zoom" interaction is enabled (see {@link BoxZoomHandler}). * @param {boolean} [options.dragRotate=true] If `true`, the "drag to rotate" interaction is enabled (see {@link DragRotateHandler}). * @param {boolean|Object} [options.dragPan=true] If `true`, the "drag to pan" interaction is enabled. An `Object` value is passed as options to {@link DragPanHandler#enable}. * @param {boolean} [options.keyboard=true] If `true`, keyboard shortcuts are enabled (see {@link KeyboardHandler}). * @param {boolean} [options.doubleClickZoom=true] If `true`, the "double click to zoom" interaction is enabled (see {@link DoubleClickZoomHandler}). * @param {boolean|Object} [options.touchZoomRotate=true] If `true`, the "pinch to rotate and zoom" interaction is enabled. An `Object` value is passed as options to {@link TouchZoomRotateHandler#enable}. * @param {boolean|Object} [options.touchPitch=true] If `true`, the "drag to pitch" interaction is enabled. An `Object` value is passed as options to {@link TouchPitchHandler#enable}. * @param {boolean|GestureOptions} [options.cooperativeGestures=undefined] If `true` or set to an options object, 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. * A valid options object includes the following properties to customize the text on the informational screen. The values below are the defaults. * { * windowsHelpText: "Use Ctrl + scroll to zoom the map", * macHelpText: "Use ⌘ + scroll to zoom the map", * mobileHelpText: "Use two fingers to move the map", * } * @param {boolean} [options.trackResize=true] If `true`, the map will automatically resize when the browser window resizes. * @param {LngLatLike} [options.center=[0, 0]] 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 uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON. * @param {number} [options.zoom=0] 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`. * @param {number} [options.bearing=0] 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`. * @param {number} [options.pitch=0] 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. * @param {LngLatBoundsLike} [options.bounds] The initial bounds of the map. If `bounds` is specified, it overrides `center` and `zoom` constructor options. * @param {Object} [options.fitBoundsOptions] A {@link Map#fitBounds} options object to use _only_ when fitting the initial `bounds` provided above. * @param {boolean} [options.renderWorldCopies=true] 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. * @param {number} [options.maxTileCacheSize=null] 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. * @param {string} [options.localIdeographFontFamily='sans-serif'] Defines a CSS * font-family for locally overriding generation of glyphs in the 'CJK Unified Ideographs', 'Hiragana', 'Katakana' and 'Hangul Syllables' ranges. * In these ranges, 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/example/local-ideographs).) * @param {RequestTransformFunction} [options.transformRequest=null] 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. * @param {boolean} [options.collectResourceTiming=false] 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. * @param {number} [options.fadeDuration=300] Controls the duration of the fade-in/fade-out animation for label collisions, in milliseconds. This setting affects all symbol layers. This setting does not affect the duration of runtime styling transitions or raster tile cross-fading. * @param {boolean} [options.crossSourceCollisions=true] 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. * @param {Object} [options.locale=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). * @param {number} [options.pixelRatio] 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. * @example * var map = new maplibregl.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/example/simple-map/) */ class Map extends Camera { style: Style; painter: Painter; handlers: HandlerManager; _container: HTMLElement; _canvasContainer: HTMLElement; _controlContainer: HTMLElement; _controlPositions: {[_: string]: HTMLElement}; _interactive: boolean; _cooperativeGestures: boolean | GestureOptions; _cooperativeGesturesScreen: HTMLElement; _metaPress: boolean; _showTileBoundaries: boolean; _showCollisionBoxes: boolean; _showPadding: boolean; _showOverdrawInspector: boolean; _repaint: boolean; _vertices: boolean; _canvas: HTMLCanvasElement; _maxTileCacheSize: number; _frame: Cancelable; _styleDirty: boolean; _sourcesDirty: boolean; _placementDirty: boolean; _loaded: boolean; // accounts for placement finishing as well _fullyLoaded: boolean; _trackResize: boolean; _preserveDrawingBuffer: boolean; _failIfMajorPerformanceCaveat: boolean; _antialias: boolean; _refreshExpiredTiles: boolean; _hash: Hash; _delegatedListeners: any; _fadeDuration: number; _crossSourceCollisions: boolean; _crossFadingFactor: number; _collectResourceTiming: boolean; _renderTaskQueue: TaskQueue; _controls: Array<IControl>; _mapId: number; _localIdeographFontFamily: string; _requestManager: RequestManager; _locale: any; _removed: boolean; _clickTolerance: number; _pixelRatio: 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 TouchZoomRotateHandler}, which allows the user to zoom or rotate the map with touch gestures. * Find more details and examples using `touchZoomRotate` in the {@link TouchZoomRotateHandler} section. */ touchZoomRotate: TouchZoomRotateHandler; /** * The map's {@link TouchPitchHandler}, which allows the user to pitch the map with touch gestures. * Find more details and examples using `touchPitch` in the {@link TouchPitchHandler} section. */ touchPitch: TouchPitchHandler; constructor(options: MapOptions) { PerformanceUtils.mark(PerformanceMarkers.create); options = extend({}, defaultOptions, options); if (options.minZoom != null && options.maxZoom != null && options.minZoom > options.maxZoom) { throw new Error('maxZoom must be greater than or equal to minZoom'); } if (options.minPitch != null && options.maxPitch != null && options.minPitch > options.maxPitch) { throw new Error('maxPitch must be greater than or equal to minPitch'); } if (options.minPitch != null && options.minPitch < defaultMinPitch) { throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`); } if (options.maxPitch != null && options.maxPitch > maxPitchThreshold) { throw new Error(`maxPitch must be less than or equal to ${maxPitchThreshold}`); } const transform = new Transform(options.minZoom, options.maxZoom, options.minPitch, options.maxPitch, options.renderWorldCopies); super(transform, {bearingSnap: options.bearingSnap}); this._interactive = options.interactive; this._cooperativeGestures = options.cooperativeGestures; this._maxTileCacheSize = options.maxTileCacheSize; this._failIfMajorPerformanceCaveat = options.failIfMajorPerformanceCaveat; this._preserveDrawingBuffer = options.preserveDrawingBuffer; this._antialias = options.antialias; this._trackResize = options.trackResize; this._bearingSnap = options.bearingSnap; this._refreshExpiredTiles = options.refreshExpiredTiles; this._fadeDuration = options.fadeDuration; this._crossSourceCollisions = options.crossSourceCollisions; this._crossFadingFactor = 1; this._collectResourceTiming = options.collectResourceTiming; this._renderTaskQueue = new TaskQueue(); this._controls = []; this._mapId = uniqueId(); this._locale = extend({}, defaultLocale, options.locale); this._clickTolerance = options.clickTolerance; this._pixelRatio = options.pixelRatio ?? devicePixelRatio; this._requestManager = new RequestManager(options.transformRequest); if (typeof options.container === 'string') { this._container = document.getElementById(options.container); if (!this._container) { throw new Error(`Container '${options.container}' not found.`); } } else if (options.container instanceof HTMLElement) { this._container = options.container; } else { throw new Error('Invalid type: \'container\' must be a String or HTMLElement.'); } if (options.maxBounds) { this.setMaxBounds(options.maxBounds); } bindAll([ '_onWindowOnline', '_onWindowResize', '_onMapScroll', '_contextLost', '_contextRestored' ], this); this._setupContainer(); this._setupPainter(); if (this.painter === undefined) { throw new Error('Failed to initialize WebGL.'); } 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); }); if (typeof window !== 'undefined') { addEventListener('online', this._onWindowOnline, false); addEventListener('resize', this._onWindowResize, false); addEventListener('orientationchange', this._onWindowResize, false); } this.handlers = new HandlerManager(this, options as CompleteMapOptions); if (this._cooperativeGestures) { this._setupCooperativeGestures(); } const hashName = (typeof options.hash === 'string' && options.hash) || undefined; this._hash = options.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: options.center, zoom: options.zoom, bearing: options.bearing, pitch: options.pitch }); if (options.bounds) { this.resize(); this.fitBounds(options.bounds, extend({}, options.fitBoundsOptions, {duration: 0})); } } this.resize(); this._localIdeographFontFamily = options.localIdeographFontFamily; if (options.style) this.setStyle(options.style, {localIdeographFontFamily: options.localIdeographFontFamily}); if (options.attributionControl) this.addControl(new AttributionControl({customAttribution: options.customAttribution})); if (options.maplibreLogo) this.addControl(new LogoControl(), options.logoPosition); this.on('style.load', () => { if (this.transform.unmodified) { this.jumpTo(this.style.stylesheet as any); } }); 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)); }); } /* * 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. * @private * @returns {number} */ _getMapId() { return this._mapId; } /** * Adds an {@link IControl} to the map, calling `control.onAdd(this)`. * * @param {IControl} control The {@link IControl} to add. * @param {string} [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'`. * @returns {Map} `this` * @example * // Add zoom and rotation controls to the map. * map.addControl(new maplibregl.NavigationControl()); * @see [Display map navigation controls](https://maplibre.org/maplibre-gl-js-docs/example/navigation/) */ addControl(control: IControl, position?: ControlPosition) { 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. * * @param {IControl} control The {@link IControl} to remove. * @returns {Map} `this` * @example * // Define a new navigation control. * var navigation = new maplibregl.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) { 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 {IControl} control The {@link IControl} to check. * @returns {boolean} True if map contains control. * @example * // Define a new navigation control. * var navigation = new maplibregl.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) { return this._controls.indexOf(control) > -1; } /** * 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. * * @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). * @returns {Map} `this` * @example * // Resize the map when the map container is shown * // after being initially hidden with CSS. * var mapDiv = document.getElementById('map'); * if (mapDiv.style.visibility === true) map.resize(); */ resize(eventData?: any) { const dimensions = this._containerDimensions(); const width = dimensions[0]; const height = dimensions[1]; this._resizeCanvas(width, height, this.getPixelRatio()); this.transform.resize(width, height); this.painter.resize(width, height, this.getPixelRatio()); 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; } /** * Returns the map's pixel ratio. * @returns {number} The pixel ratio. */ getPixelRatio() { return this._pixelRatio; } /** * 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`. * @param {number} pixelRatio The pixel ratio. */ setPixelRatio(pixelRatio: number) { const [width, height] = this._containerDimensions(); this._pixelRatio = pixelRatio; this._resizeCanvas(width, height, pixelRatio); this.painter.resize(width, height, pixelRatio); } /** * 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 {LngLatBounds} The geographical bounds of the map as {@link LngLatBounds}. * @example * var 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 * var 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 {LngLatBoundsLike | null | undefined} bounds The maximum bounds to set. If `null` or `undefined` is provided, the function removes the map's maximum bounds. * @returns {Map} `this` * @example * // Define bounds that conform to the `LngLatBoundsLike` object. * var bounds = [ * [-74.04728, 40.68392], // [west, south] * [-73.91058, 40.87764] // [east, north] * ]; * // Set the map's max bounds. * map.setMaxBounds(bounds); */ setMaxBounds(bounds?: LngLatBoundsLike | null) { 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. * * @param {number | null | undefined} 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). * @returns {Map} `this` * @example * map.setMinZoom(12.25); */ setMinZoom(minZoom?: number | null) { minZoom = minZoom === null || minZoom === undefined ? defaultMinZoom : minZoom; if (minZoom >= defaultMinZoom && minZoom <= this.transform.maxZoom) { this.transform.minZoom = 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 {number} minZoom * @example * var minZoom = map.getMinZoom(); */ getMinZoom() { 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. * * @param {number | null | undefined} maxZoom The maximum zoom level to set. * If `null` or `undefined` is provided, the function removes the current maximum zoom (sets it to 22). * @returns {Map} `this` * @example * map.setMaxZoom(18.75); */ setMaxZoom(maxZoom?: number | null) { maxZoom = maxZoom === null || maxZoom === undefined ? defaultMaxZoom : maxZoom; if (maxZoom >= this.transform.minZoom) { this.transform.maxZoom = 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 {number} maxZoom * @example * var maxZoom = map.getMaxZoom(); */ getMaxZoom() { 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 map will pitch to the new minimum. * * @param {number | null | undefined} minPitch The minimum pitch to set (0-85). 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. * If `null` or `undefined` is provided, the function removes the current minimum pitch (i.e. sets it to 0). * @returns {Map} `this` */ setMinPitch(minPitch?: number | null) { minPitch = minPitch === null || minPitch === undefined ? defaultMinPitch : minPitch; if (minPitch < defaultMinPitch) { throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`); } if (minPitch >= defaultMinPitch && minPitch <= this.transform.maxPitch) { this.transform.minPitch = minPitch; this._update(); if (this.getPitch() < minPitch) this.setPitch(minPitch); return this; } else throw new Error(`minPitch must be between ${defaultMinPitch} and the current maxPitch, inclusive`); } /** * Returns the map's minimum allowable pitch. * * @returns {number} minPitch */ getMinPitch() { return this.transform.minPitch; } /** * Sets or clears the map's maximum pitch. * If the map's current pitch is higher than the new maximum, * the map will pitch to the new maximum. * * @param {number | null | undefined} maxPitch The maximum pitch to set (0-85). 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. * If `null` or `undefined` is provided, the function removes the current maximum pitch (sets it to 60). * @returns {Map} `this` */ setMaxPitch(maxPitch?: number | null) { maxPitch = maxPitch === null || maxPitch === undefined ? defaultMaxPitch : maxPitch; if (maxPitch > maxPitchThreshold) { throw new Error(`maxPitch must be less than or equal to ${maxPitchThreshold}`); } if (maxPitch >= this.transform.minPitch) { this.transform.maxPitch = maxPitch; this._update(); if (this.getPitch() > maxPitch) this.setPitch(maxPitch); return this; } else throw new Error('maxPitch must be greater than the current minPitch'); } /** * Returns the map's maximum allowable pitch. * * @returns {number} maxPitch */ getMaxPitch() { return this.transform.maxPitch; } /** * Returns the state of `renderWorldCopies`. 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. * @returns {boolean} renderWorldCopies * @example * var worldCopiesRendered = map.getRenderWorldCopies(); * @see [Render world copies](https://maplibre.org/maplibre-gl-js-docs/example/render-world-copies/) */ getRenderWorldCopies() { return this.transform.renderWorldCopies; } /** * Sets the state of `renderWorldCopies`. * * @param {boolean} renderWorldCopies 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. * * `undefined` is treated as `true`, `null` is treated as `false`. * @returns {Map} `this` * @example * map.setRenderWorldCopies(true); * @see [Render world copies](https://maplibre.org/maplibre-gl-js-docs/example/render-world-copies/) */ setRenderWorldCopies(renderWorldCopies?: boolean | null) { this.transform.renderWorldCopies = renderWorldCopies; return this._update(); } /** * Returns a [Point](https://github.com/mapbox/point-geometry) representing pixel coordinates, relative to the map's `container`, * that correspond to the specified geographical location. * * @param {LngLatLike} lnglat The geographical location to project. * @returns {Point} The [Point](https://github.com/mapbox/point-geometry) corresponding to `lnglat`, relative to the map's `container`. * @example * var coordinate = [-122.420679, 37.772537]; * var point = map.project(coordinate); */ project(lnglat: LngLatLike) { return this.transform.locationPoint(LngLat.convert(lnglat), this.style && this.style.terrain); } /** * Returns a {@link LngLat} representing geographical coordinates that correspond * to the specified pixel coordinates. * * @param {PointLike} point The pixel coordinates to unproject. * @returns {LngLat} The {@link LngLat} corresponding to `point`. * @example * map.on('click', function(e) { * // When the map is clicked, get the geographic coordinate. * var coordinate = map.unproject(e.point); * }); */ unproject(point: PointLike) { return this.transform.pointLocation(Point.convert(point), this.style && this.style.terrain); } /** * Returns true if the map is panning, zooming, rotating, or pitching due to a camera animation or user gesture. * @returns {boolean} True if the map is moving. * @example * var isMoving = map.isMoving(); */ isMoving(): boolean { return this._moving || this.handlers.isMoving(); } /** * Returns true if the map is zooming due to a camera animation or user gesture. * @returns {boolean} True if the map is zooming. * @example * var isZooming = map.isZooming(); */ isZooming(): boolean { return this._zooming || this.handlers.isZooming(); } /** * Returns true if the map is rotating due to a camera animation or user gesture. * @returns {boolean} True if the map is rotating. * @example * map.isRotating(); */ isRotating(): boolean { return this._rotating || this.handlers.isRotating(); } _createDelegatedListener(type: MapEvent | string, layerId: string, listener: Listener): { layer: string; listener: Listener; delegates: {[type in keyof MapEventType]?: (e: any) => void}; } { if (type === 'mouseenter' || type === 'mouseover') { let mousein = false; const mousemove = (e) => { const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : []; if (!features.length) { mousein = false; } else if (!mousein) { mousein = true; listener.call(this, new MapMouseEvent(type, this, e.originalEvent, {features})); } }; const mouseout = () => { mousein = false; }; return {layer: layerId, listener, delegates: {mousemove, mouseout}}; } else if (type === 'mouseleave' || type === 'mouseout') { let mousein = false; const mousemove = (e) => { const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : []; if (features.length) { mousein = true; } else if (mousein) { mousein = false; listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); } }; const mouseout = (e) => { if (mousein) { mousein = false; listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); } }; return {layer: layerId, listener, delegates: {mousemove, mouseout}}; } else { const delegate = (e) => { const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : []; if (features.length) { // Here we need to mutate the original event, so that preventDefault works as expected. e.features = features; listener.call(this, e); delete e.features; } }; return {layer: layerId, listener, delegates: {[type]: delegate}}; } } /** * Adds a listener for events of a specified type, optionally limited to features in a specified style layer. * * @param {string} type The event type to listen for. Events compatible with the optional `layerId` parameter are triggered * when the cursor enters a visible portion of the specified layer from outside that layer or outside the map canvas. * * | Event | Compatible with `layerId` | * |-----------------------------------------------------------|---------------------------| * | [`mousedown`](#map.event:mousedown) | yes | * | [`mouseup`](#map.event:mouseup) | yes | * | [`mouseover`](#map.event:mouseover) | yes | * | [`mouseout`](#map.event:mouseout) | yes | * | [`mousemove`](#map.event:mousemove) | yes | * | [`mouseenter`](#map.event:mouseenter) | yes (required) | * | [`mouseleave`](#map.event:mouseleave) | yes (required) | * | [`click`](#map.event:click) | yes | * | [`dblclick`](#map.event:dblclick) | yes | * | [`contextmenu`](#map.event:contextmenu) | yes | * | [`touchstart`](#map.event:touchstart) | yes | * | [`touchend`](#map.event:touchend) | yes | * | [`touchcancel`](#map.event:touchcancel) | yes | * | [`wheel`](#map.event:wheel) | | * | [`resize`](#map.event:resize) | | * | [`remove`](#map.event:remove) | | * | [`touchmove`](#map.event:touchmove) | | * | [`movestart`](#map.event:movestart) | | * | [`move`](#map.event:move) | | * | [`moveend`](#map.event:moveend) | | * | [`dragstart`](#map.event:dragstart) | | * | [`drag`](#map.event:drag) | | * | [`dragend`](#map.event:dragend) | | * | [`zoomstart`](#map.event:zoomstart) |