UNPKG

ol

Version:

OpenLayers mapping library

1,175 lines (1,174 loc) • 54.7 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); /** * @module ol/PluggableMap */ import Collection from './Collection.js'; import CollectionEventType from './CollectionEventType.js'; import MapBrowserEvent from './MapBrowserEvent.js'; import MapBrowserEventHandler from './MapBrowserEventHandler.js'; import MapBrowserEventType from './MapBrowserEventType.js'; import MapEvent from './MapEvent.js'; import MapEventType from './MapEventType.js'; import MapProperty from './MapProperty.js'; import RenderEventType from './render/EventType.js'; import BaseObject, { getChangeEventType } from './Object.js'; import ObjectEventType from './ObjectEventType.js'; import TileQueue, { getTilePriority } from './TileQueue.js'; import View from './View.js'; import ViewHint from './ViewHint.js'; import { assert } from './asserts.js'; import { removeNode } from './dom.js'; import { listen, unlistenByKey } from './events.js'; import EventType from './events/EventType.js'; import { clone, createOrUpdateEmpty, equals, getForViewAndSize, isEmpty } from './extent.js'; import { TRUE } from './functions.js'; import { DEVICE_PIXEL_RATIO, IMAGE_DECODE, PASSIVE_EVENT_LISTENERS } from './has.js'; import LayerGroup from './layer/Group.js'; import { hasArea } from './size.js'; import { create as createTransform, apply as applyTransform } from './transform.js'; import { toUserCoordinate, fromUserCoordinate } from './proj.js'; /** * State of the current frame. Only `pixelRatio`, `time` and `viewState` should * be used in applications. * @typedef {Object} FrameState * @property {number} pixelRatio The pixel ratio of the frame. * @property {number} time The time when rendering of the frame was requested. * @property {import("./View.js").State} viewState The state of the current view. * @property {boolean} animate * @property {import("./transform.js").Transform} coordinateToPixelTransform * @property {null|import("./extent.js").Extent} extent * @property {Array<DeclutterItems>} declutterItems * @property {number} index * @property {Array<import("./layer/Layer.js").State>} layerStatesArray * @property {number} layerIndex * @property {import("./transform.js").Transform} pixelToCoordinateTransform * @property {Array<PostRenderFunction>} postRenderFunctions * @property {import("./size.js").Size} size * @property {TileQueue} tileQueue * @property {!Object<string, Object<string, boolean>>} usedTiles * @property {Array<number>} viewHints * @property {!Object<string, Object<string, boolean>>} wantedTiles */ /** * @typedef {Object} DeclutterItems * @property {Array<*>} items Declutter items of an executor. * @property {number} opacity Layer opacity. */ /** * @typedef {function(PluggableMap, ?FrameState): any} PostRenderFunction */ /** * @typedef {Object} AtPixelOptions * @property {undefined|function(import("./layer/Layer.js").default): boolean} [layerFilter] Layer filter * function. The filter function will receive one argument, the * {@link module:ol/layer/Layer layer-candidate} and it should return a boolean value. * Only layers which are visible and for which this function returns `true` * will be tested for features. By default, all visible layers will be tested. * @property {number} [hitTolerance=0] Hit-detection tolerance in pixels. Pixels * inside the radius around the given position will be checked for features. * @property {boolean} [checkWrapped=true] Check-Wrapped Will check for for wrapped geometries inside the range of * +/- 1 world width. Works only if a projection is used that can be wrapped. */ /** * @typedef {Object} MapOptionsInternal * @property {Collection<import("./control/Control.js").default>} [controls] * @property {Collection<import("./interaction/Interaction.js").default>} [interactions] * @property {HTMLElement|Document} keyboardEventTarget * @property {Collection<import("./Overlay.js").default>} overlays * @property {Object<string, *>} values */ /** * Object literal with config options for the map. * @typedef {Object} MapOptions * @property {Collection<import("./control/Control.js").default>|Array<import("./control/Control.js").default>} [controls] * Controls initially added to the map. If not specified, * {@link module:ol/control~defaults} is used. * @property {number} [pixelRatio=window.devicePixelRatio] The ratio between * physical pixels and device-independent pixels (dips) on the device. * @property {Collection<import("./interaction/Interaction.js").default>|Array<import("./interaction/Interaction.js").default>} [interactions] * Interactions that are initially added to the map. If not specified, * {@link module:ol/interaction~defaults} is used. * @property {HTMLElement|Document|string} [keyboardEventTarget] The element to * listen to keyboard events on. This determines when the `KeyboardPan` and * `KeyboardZoom` interactions trigger. For example, if this option is set to * `document` the keyboard interactions will always trigger. If this option is * not specified, the element the library listens to keyboard events on is the * map target (i.e. the user-provided div for the map). If this is not * `document`, the target element needs to be focused for key events to be * emitted, requiring that the target element has a `tabindex` attribute. * @property {Array<import("./layer/Base.js").default>|Collection<import("./layer/Base.js").default>|LayerGroup} [layers] * Layers. If this is not defined, a map with no layers will be rendered. Note * that layers are rendered in the order supplied, so if you want, for example, * a vector layer to appear on top of a tile layer, it must come after the tile * layer. * @property {number} [maxTilesLoading=16] Maximum number tiles to load * simultaneously. * @property {number} [moveTolerance=1] The minimum distance in pixels the * cursor must move to be detected as a map move event instead of a click. * Increasing this value can make it easier to click on the map. * @property {Collection<import("./Overlay.js").default>|Array<import("./Overlay.js").default>} [overlays] * Overlays initially added to the map. By default, no overlays are added. * @property {HTMLElement|string} [target] The container for the map, either the * element itself or the `id` of the element. If not specified at construction * time, {@link module:ol/Map~Map#setTarget} must be called for the map to be * rendered. * @property {View} [view] The map's view. No layer sources will be * fetched unless this is specified at construction time or through * {@link module:ol/Map~Map#setView}. */ /** * @fires import("./MapBrowserEvent.js").MapBrowserEvent * @fires import("./MapEvent.js").MapEvent * @fires import("./render/Event.js").default#precompose * @fires import("./render/Event.js").default#postcompose * @fires import("./render/Event.js").default#rendercomplete * @api */ var PluggableMap = /** @class */ (function (_super) { __extends(PluggableMap, _super); /** * @param {MapOptions} options Map options. */ function PluggableMap(options) { var _this = _super.call(this) || this; var optionsInternal = createOptionsInternal(options); /** @private */ _this.boundHandleBrowserEvent_ = _this.handleBrowserEvent.bind(_this); /** * @type {number} * @private */ _this.maxTilesLoading_ = options.maxTilesLoading !== undefined ? options.maxTilesLoading : 16; /** * @private * @type {number} */ _this.pixelRatio_ = options.pixelRatio !== undefined ? options.pixelRatio : DEVICE_PIXEL_RATIO; /** * @private * @type {*} */ _this.postRenderTimeoutHandle_; /** * @private * @type {number|undefined} */ _this.animationDelayKey_; /** * @private */ _this.animationDelay_ = function () { this.animationDelayKey_ = undefined; this.renderFrame_(Date.now()); }.bind(_this); /** * @private * @type {import("./transform.js").Transform} */ _this.coordinateToPixelTransform_ = createTransform(); /** * @private * @type {import("./transform.js").Transform} */ _this.pixelToCoordinateTransform_ = createTransform(); /** * @private * @type {number} */ _this.frameIndex_ = 0; /** * @private * @type {?FrameState} */ _this.frameState_ = null; /** * The extent at the previous 'moveend' event. * @private * @type {import("./extent.js").Extent} */ _this.previousExtent_ = null; /** * @private * @type {?import("./events.js").EventsKey} */ _this.viewPropertyListenerKey_ = null; /** * @private * @type {?import("./events.js").EventsKey} */ _this.viewChangeListenerKey_ = null; /** * @private * @type {?Array<import("./events.js").EventsKey>} */ _this.layerGroupPropertyListenerKeys_ = null; /** * @private * @type {!HTMLElement} */ _this.viewport_ = document.createElement('div'); _this.viewport_.className = 'ol-viewport' + ('ontouchstart' in window ? ' ol-touch' : ''); _this.viewport_.style.position = 'relative'; _this.viewport_.style.overflow = 'hidden'; _this.viewport_.style.width = '100%'; _this.viewport_.style.height = '100%'; /** * @private * @type {!HTMLElement} */ _this.overlayContainer_ = document.createElement('div'); _this.overlayContainer_.style.position = 'absolute'; _this.overlayContainer_.style.zIndex = '0'; _this.overlayContainer_.style.width = '100%'; _this.overlayContainer_.style.height = '100%'; _this.overlayContainer_.className = 'ol-overlaycontainer'; _this.viewport_.appendChild(_this.overlayContainer_); /** * @private * @type {!HTMLElement} */ _this.overlayContainerStopEvent_ = document.createElement('div'); _this.overlayContainerStopEvent_.style.position = 'absolute'; _this.overlayContainerStopEvent_.style.zIndex = '0'; _this.overlayContainerStopEvent_.style.width = '100%'; _this.overlayContainerStopEvent_.style.height = '100%'; _this.overlayContainerStopEvent_.className = 'ol-overlaycontainer-stopevent'; _this.viewport_.appendChild(_this.overlayContainerStopEvent_); /** * @private * @type {MapBrowserEventHandler} */ _this.mapBrowserEventHandler_ = new MapBrowserEventHandler(_this, options.moveTolerance); var handleMapBrowserEvent = _this.handleMapBrowserEvent.bind(_this); for (var key in MapBrowserEventType) { _this.mapBrowserEventHandler_.addEventListener(MapBrowserEventType[key], handleMapBrowserEvent); } /** * @private * @type {HTMLElement|Document} */ _this.keyboardEventTarget_ = optionsInternal.keyboardEventTarget; /** * @private * @type {?Array<import("./events.js").EventsKey>} */ _this.keyHandlerKeys_ = null; var handleBrowserEvent = _this.handleBrowserEvent.bind(_this); _this.viewport_.addEventListener(EventType.CONTEXTMENU, handleBrowserEvent, false); _this.viewport_.addEventListener(EventType.WHEEL, handleBrowserEvent, PASSIVE_EVENT_LISTENERS ? { passive: false } : false); /** * @type {Collection<import("./control/Control.js").default>} * @protected */ _this.controls = optionsInternal.controls || new Collection(); /** * @type {Collection<import("./interaction/Interaction.js").default>} * @protected */ _this.interactions = optionsInternal.interactions || new Collection(); /** * @type {Collection<import("./Overlay.js").default>} * @private */ _this.overlays_ = optionsInternal.overlays; /** * A lookup of overlays by id. * @private * @type {Object<string, import("./Overlay.js").default>} */ _this.overlayIdIndex_ = {}; /** * @type {import("./renderer/Map.js").default} * @private */ _this.renderer_ = null; /** * @type {undefined|function(Event): void} * @private */ _this.handleResize_; /** * @private * @type {!Array<PostRenderFunction>} */ _this.postRenderFunctions_ = []; /** * @private * @type {TileQueue} */ _this.tileQueue_ = new TileQueue(_this.getTilePriority.bind(_this), _this.handleTileChange_.bind(_this)); _this.addEventListener(getChangeEventType(MapProperty.LAYERGROUP), _this.handleLayerGroupChanged_); _this.addEventListener(getChangeEventType(MapProperty.VIEW), _this.handleViewChanged_); _this.addEventListener(getChangeEventType(MapProperty.SIZE), _this.handleSizeChanged_); _this.addEventListener(getChangeEventType(MapProperty.TARGET), _this.handleTargetChanged_); // setProperties will trigger the rendering of the map if the map // is "defined" already. _this.setProperties(optionsInternal.values); _this.controls.forEach( /** * @param {import("./control/Control.js").default} control Control. * @this {PluggableMap} */ function (control) { control.setMap(this); }.bind(_this)); _this.controls.addEventListener(CollectionEventType.ADD, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function (event) { event.element.setMap(this); }.bind(_this)); _this.controls.addEventListener(CollectionEventType.REMOVE, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function (event) { event.element.setMap(null); }.bind(_this)); _this.interactions.forEach( /** * @param {import("./interaction/Interaction.js").default} interaction Interaction. * @this {PluggableMap} */ function (interaction) { interaction.setMap(this); }.bind(_this)); _this.interactions.addEventListener(CollectionEventType.ADD, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function (event) { event.element.setMap(this); }.bind(_this)); _this.interactions.addEventListener(CollectionEventType.REMOVE, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function (event) { event.element.setMap(null); }.bind(_this)); _this.overlays_.forEach(_this.addOverlayInternal_.bind(_this)); _this.overlays_.addEventListener(CollectionEventType.ADD, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function (event) { this.addOverlayInternal_(/** @type {import("./Overlay.js").default} */ (event.element)); }.bind(_this)); _this.overlays_.addEventListener(CollectionEventType.REMOVE, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function (event) { var overlay = /** @type {import("./Overlay.js").default} */ (event.element); var id = overlay.getId(); if (id !== undefined) { delete this.overlayIdIndex_[id.toString()]; } event.element.setMap(null); }.bind(_this)); return _this; } /** * @abstract * @return {import("./renderer/Map.js").default} The map renderer */ PluggableMap.prototype.createRenderer = function () { throw new Error('Use a map type that has a createRenderer method'); }; /** * Add the given control to the map. * @param {import("./control/Control.js").default} control Control. * @api */ PluggableMap.prototype.addControl = function (control) { this.getControls().push(control); }; /** * Add the given interaction to the map. If you want to add an interaction * at another point of the collection use `getInteraction()` and the methods * available on {@link module:ol/Collection~Collection}. This can be used to * stop the event propagation from the handleEvent function. The interactions * get to handle the events in the reverse order of this collection. * @param {import("./interaction/Interaction.js").default} interaction Interaction to add. * @api */ PluggableMap.prototype.addInteraction = function (interaction) { this.getInteractions().push(interaction); }; /** * Adds the given layer to the top of this map. If you want to add a layer * elsewhere in the stack, use `getLayers()` and the methods available on * {@link module:ol/Collection~Collection}. * @param {import("./layer/Base.js").default} layer Layer. * @api */ PluggableMap.prototype.addLayer = function (layer) { var layers = this.getLayerGroup().getLayers(); layers.push(layer); }; /** * Add the given overlay to the map. * @param {import("./Overlay.js").default} overlay Overlay. * @api */ PluggableMap.prototype.addOverlay = function (overlay) { this.getOverlays().push(overlay); }; /** * This deals with map's overlay collection changes. * @param {import("./Overlay.js").default} overlay Overlay. * @private */ PluggableMap.prototype.addOverlayInternal_ = function (overlay) { var id = overlay.getId(); if (id !== undefined) { this.overlayIdIndex_[id.toString()] = overlay; } overlay.setMap(this); }; /** * * @inheritDoc */ PluggableMap.prototype.disposeInternal = function () { this.mapBrowserEventHandler_.dispose(); this.viewport_.removeEventListener(EventType.CONTEXTMENU, this.boundHandleBrowserEvent_); this.viewport_.removeEventListener(EventType.WHEEL, this.boundHandleBrowserEvent_); if (this.handleResize_ !== undefined) { removeEventListener(EventType.RESIZE, this.handleResize_, false); this.handleResize_ = undefined; } this.setTarget(null); _super.prototype.disposeInternal.call(this); }; /** * Detect features that intersect a pixel on the viewport, and execute a * callback with each intersecting feature. Layers included in the detection can * be configured through the `layerFilter` option in `opt_options`. * @param {import("./pixel.js").Pixel} pixel Pixel. * @param {function(this: S, import("./Feature.js").FeatureLike, * import("./layer/Layer.js").default): T} callback Feature callback. The callback will be * called with two arguments. The first argument is one * {@link module:ol/Feature feature} or * {@link module:ol/render/Feature render feature} at the pixel, the second is * the {@link module:ol/layer/Layer layer} of the feature and will be null for * unmanaged layers. To stop detection, callback functions can return a * truthy value. * @param {AtPixelOptions=} opt_options Optional options. * @return {T|undefined} Callback result, i.e. the return value of last * callback execution, or the first truthy callback return value. * @template S,T * @api */ PluggableMap.prototype.forEachFeatureAtPixel = function (pixel, callback, opt_options) { if (!this.frameState_) { return; } var coordinate = this.getCoordinateFromPixelInternal(pixel); opt_options = opt_options !== undefined ? opt_options : {}; var hitTolerance = opt_options.hitTolerance !== undefined ? opt_options.hitTolerance * this.frameState_.pixelRatio : 0; var layerFilter = opt_options.layerFilter !== undefined ? opt_options.layerFilter : TRUE; var checkWrapped = opt_options.checkWrapped !== false; return this.renderer_.forEachFeatureAtCoordinate(coordinate, this.frameState_, hitTolerance, checkWrapped, callback, null, layerFilter, null); }; /** * Get all features that intersect a pixel on the viewport. * @param {import("./pixel.js").Pixel} pixel Pixel. * @param {AtPixelOptions=} opt_options Optional options. * @return {Array<import("./Feature.js").FeatureLike>} The detected features or * an empty array if none were found. * @api */ PluggableMap.prototype.getFeaturesAtPixel = function (pixel, opt_options) { var features = []; this.forEachFeatureAtPixel(pixel, function (feature) { features.push(feature); }, opt_options); return features; }; /** * Detect layers that have a color value at a pixel on the viewport, and * execute a callback with each matching layer. Layers included in the * detection can be configured through `opt_layerFilter`. * * Note: this may give false positives unless the map layers have had different `className` * properties assigned to them. * * @param {import("./pixel.js").Pixel} pixel Pixel. * @param {function(this: S, import("./layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback * Layer callback. This callback will receive two arguments: first is the * {@link module:ol/layer/Layer layer}, second argument is an array representing * [R, G, B, A] pixel values (0 - 255) and will be `null` for layer types * that do not currently support this argument. To stop detection, callback * functions can return a truthy value. * @param {AtPixelOptions=} opt_options Configuration options. * @return {T|undefined} Callback result, i.e. the return value of last * callback execution, or the first truthy callback return value. * @template S,T * @api */ PluggableMap.prototype.forEachLayerAtPixel = function (pixel, callback, opt_options) { if (!this.frameState_) { return; } var options = opt_options || {}; var hitTolerance = options.hitTolerance !== undefined ? options.hitTolerance * this.frameState_.pixelRatio : 0; var layerFilter = options.layerFilter || TRUE; return this.renderer_.forEachLayerAtPixel(pixel, this.frameState_, hitTolerance, callback, layerFilter); }; /** * Detect if features intersect a pixel on the viewport. Layers included in the * detection can be configured through `opt_layerFilter`. * @param {import("./pixel.js").Pixel} pixel Pixel. * @param {AtPixelOptions=} opt_options Optional options. * @return {boolean} Is there a feature at the given pixel? * @api */ PluggableMap.prototype.hasFeatureAtPixel = function (pixel, opt_options) { if (!this.frameState_) { return false; } var coordinate = this.getCoordinateFromPixelInternal(pixel); opt_options = opt_options !== undefined ? opt_options : {}; var layerFilter = opt_options.layerFilter !== undefined ? opt_options.layerFilter : TRUE; var hitTolerance = opt_options.hitTolerance !== undefined ? opt_options.hitTolerance * this.frameState_.pixelRatio : 0; var checkWrapped = opt_options.checkWrapped !== false; return this.renderer_.hasFeatureAtCoordinate(coordinate, this.frameState_, hitTolerance, checkWrapped, layerFilter, null); }; /** * Returns the coordinate in user projection for a browser event. * @param {Event} event Event. * @return {import("./coordinate.js").Coordinate} Coordinate. * @api */ PluggableMap.prototype.getEventCoordinate = function (event) { return this.getCoordinateFromPixel(this.getEventPixel(event)); }; /** * Returns the coordinate in view projection for a browser event. * @param {Event} event Event. * @return {import("./coordinate.js").Coordinate} Coordinate. */ PluggableMap.prototype.getEventCoordinateInternal = function (event) { return this.getCoordinateFromPixelInternal(this.getEventPixel(event)); }; /** * Returns the map pixel position for a browser event relative to the viewport. * @param {Event|TouchEvent} event Event. * @return {import("./pixel.js").Pixel} Pixel. * @api */ PluggableMap.prototype.getEventPixel = function (event) { var viewportPosition = this.viewport_.getBoundingClientRect(); var eventPosition = 'changedTouches' in event ? /** @type {TouchEvent} */ (event).changedTouches[0] : /** @type {MouseEvent} */ (event); return [ eventPosition.clientX - viewportPosition.left, eventPosition.clientY - viewportPosition.top ]; }; /** * Get the target in which this map is rendered. * Note that this returns what is entered as an option or in setTarget: * if that was an element, it returns an element; if a string, it returns that. * @return {HTMLElement|string|undefined} The Element or id of the Element that the * map is rendered in. * @observable * @api */ PluggableMap.prototype.getTarget = function () { return /** @type {HTMLElement|string|undefined} */ (this.get(MapProperty.TARGET)); }; /** * Get the DOM element into which this map is rendered. In contrast to * `getTarget` this method always return an `Element`, or `null` if the * map has no target. * @return {HTMLElement} The element that the map is rendered in. * @api */ PluggableMap.prototype.getTargetElement = function () { var target = this.getTarget(); if (target !== undefined) { return typeof target === 'string' ? document.getElementById(target) : target; } else { return null; } }; /** * Get the coordinate for a given pixel. This returns a coordinate in the * user projection. * @param {import("./pixel.js").Pixel} pixel Pixel position in the map viewport. * @return {import("./coordinate.js").Coordinate} The coordinate for the pixel position. * @api */ PluggableMap.prototype.getCoordinateFromPixel = function (pixel) { return toUserCoordinate(this.getCoordinateFromPixelInternal(pixel), this.getView().getProjection()); }; /** * Get the coordinate for a given pixel. This returns a coordinate in the * map view projection. * @param {import("./pixel.js").Pixel} pixel Pixel position in the map viewport. * @return {import("./coordinate.js").Coordinate} The coordinate for the pixel position. */ PluggableMap.prototype.getCoordinateFromPixelInternal = function (pixel) { var frameState = this.frameState_; if (!frameState) { return null; } else { return applyTransform(frameState.pixelToCoordinateTransform, pixel.slice()); } }; /** * Get the map controls. Modifying this collection changes the controls * associated with the map. * @return {Collection<import("./control/Control.js").default>} Controls. * @api */ PluggableMap.prototype.getControls = function () { return this.controls; }; /** * Get the map overlays. Modifying this collection changes the overlays * associated with the map. * @return {Collection<import("./Overlay.js").default>} Overlays. * @api */ PluggableMap.prototype.getOverlays = function () { return this.overlays_; }; /** * Get an overlay by its identifier (the value returned by overlay.getId()). * Note that the index treats string and numeric identifiers as the same. So * `map.getOverlayById(2)` will return an overlay with id `'2'` or `2`. * @param {string|number} id Overlay identifier. * @return {import("./Overlay.js").default} Overlay. * @api */ PluggableMap.prototype.getOverlayById = function (id) { var overlay = this.overlayIdIndex_[id.toString()]; return overlay !== undefined ? overlay : null; }; /** * Get the map interactions. Modifying this collection changes the interactions * associated with the map. * * Interactions are used for e.g. pan, zoom and rotate. * @return {Collection<import("./interaction/Interaction.js").default>} Interactions. * @api */ PluggableMap.prototype.getInteractions = function () { return this.interactions; }; /** * Get the layergroup associated with this map. * @return {LayerGroup} A layer group containing the layers in this map. * @observable * @api */ PluggableMap.prototype.getLayerGroup = function () { return ( /** @type {LayerGroup} */ (this.get(MapProperty.LAYERGROUP))); }; /** * Get the collection of layers associated with this map. * @return {!Collection<import("./layer/Base.js").default>} Layers. * @api */ PluggableMap.prototype.getLayers = function () { var layers = this.getLayerGroup().getLayers(); return layers; }; /** * @return {boolean} Layers have sources that are still loading. */ PluggableMap.prototype.getLoading = function () { var layerStatesArray = this.getLayerGroup().getLayerStatesArray(); for (var i = 0, ii = layerStatesArray.length; i < ii; ++i) { var layer = layerStatesArray[i].layer; var source = /** @type {import("./layer/Layer.js").default} */ (layer).getSource(); if (source && source.loading) { return true; } } return false; }; /** * Get the pixel for a coordinate. This takes a coordinate in the user * projection and returns the corresponding pixel. * @param {import("./coordinate.js").Coordinate} coordinate A map coordinate. * @return {import("./pixel.js").Pixel} A pixel position in the map viewport. * @api */ PluggableMap.prototype.getPixelFromCoordinate = function (coordinate) { var viewCoordinate = fromUserCoordinate(coordinate, this.getView().getProjection()); return this.getPixelFromCoordinateInternal(viewCoordinate); }; /** * Get the pixel for a coordinate. This takes a coordinate in the map view * projection and returns the corresponding pixel. * @param {import("./coordinate.js").Coordinate} coordinate A map coordinate. * @return {import("./pixel.js").Pixel} A pixel position in the map viewport. */ PluggableMap.prototype.getPixelFromCoordinateInternal = function (coordinate) { var frameState = this.frameState_; if (!frameState) { return null; } else { return applyTransform(frameState.coordinateToPixelTransform, coordinate.slice(0, 2)); } }; /** * Get the map renderer. * @return {import("./renderer/Map.js").default} Renderer */ PluggableMap.prototype.getRenderer = function () { return this.renderer_; }; /** * Get the size of this map. * @return {import("./size.js").Size|undefined} The size in pixels of the map in the DOM. * @observable * @api */ PluggableMap.prototype.getSize = function () { return ( /** @type {import("./size.js").Size|undefined} */ (this.get(MapProperty.SIZE))); }; /** * Get the view associated with this map. A view manages properties such as * center and resolution. * @return {View} The view that controls this map. * @observable * @api */ PluggableMap.prototype.getView = function () { return ( /** @type {View} */ (this.get(MapProperty.VIEW))); }; /** * Get the element that serves as the map viewport. * @return {HTMLElement} Viewport. * @api */ PluggableMap.prototype.getViewport = function () { return this.viewport_; }; /** * Get the element that serves as the container for overlays. Elements added to * this container will let mousedown and touchstart events through to the map, * so clicks and gestures on an overlay will trigger {@link module:ol/MapBrowserEvent~MapBrowserEvent} * events. * @return {!HTMLElement} The map's overlay container. */ PluggableMap.prototype.getOverlayContainer = function () { return this.overlayContainer_; }; /** * Get the element that serves as a container for overlays that don't allow * event propagation. Elements added to this container won't let mousedown and * touchstart events through to the map, so clicks and gestures on an overlay * don't trigger any {@link module:ol/MapBrowserEvent~MapBrowserEvent}. * @return {!HTMLElement} The map's overlay container that stops events. */ PluggableMap.prototype.getOverlayContainerStopEvent = function () { return this.overlayContainerStopEvent_; }; /** * @param {import("./Tile.js").default} tile Tile. * @param {string} tileSourceKey Tile source key. * @param {import("./coordinate.js").Coordinate} tileCenter Tile center. * @param {number} tileResolution Tile resolution. * @return {number} Tile priority. */ PluggableMap.prototype.getTilePriority = function (tile, tileSourceKey, tileCenter, tileResolution) { return getTilePriority(this.frameState_, tile, tileSourceKey, tileCenter, tileResolution); }; /** * @param {Event} browserEvent Browser event. * @param {string=} opt_type Type. */ PluggableMap.prototype.handleBrowserEvent = function (browserEvent, opt_type) { var type = opt_type || browserEvent.type; var mapBrowserEvent = new MapBrowserEvent(type, this, browserEvent); this.handleMapBrowserEvent(mapBrowserEvent); }; /** * @param {MapBrowserEvent} mapBrowserEvent The event to handle. */ PluggableMap.prototype.handleMapBrowserEvent = function (mapBrowserEvent) { if (!this.frameState_) { // With no view defined, we cannot translate pixels into geographical // coordinates so interactions cannot be used. return; } var target = /** @type {Node} */ (mapBrowserEvent.originalEvent.target); if (!mapBrowserEvent.dragging) { if (this.overlayContainerStopEvent_.contains(target) || !(document.body.contains(target) || this.viewport_.getRootNode && this.viewport_.getRootNode().contains(target))) { // Abort if the event target is a child of the container that doesn't allow // event propagation or is no longer in the page. It's possible for the target to no longer // be in the page if it has been removed in an event listener, this might happen in a Control // that recreates it's content based on user interaction either manually or via a render // in something like https://reactjs.org/ return; } } mapBrowserEvent.frameState = this.frameState_; var interactionsArray = this.getInteractions().getArray(); if (this.dispatchEvent(mapBrowserEvent) !== false) { for (var i = interactionsArray.length - 1; i >= 0; i--) { var interaction = interactionsArray[i]; if (!interaction.getActive()) { continue; } var cont = interaction.handleEvent(mapBrowserEvent); if (!cont) { break; } } } }; /** * @protected */ PluggableMap.prototype.handlePostRender = function () { var frameState = this.frameState_; // Manage the tile queue // Image loads are expensive and a limited resource, so try to use them // efficiently: // * When the view is static we allow a large number of parallel tile loads // to complete the frame as quickly as possible. // * When animating or interacting, image loads can cause janks, so we reduce // the maximum number of loads per frame and limit the number of parallel // tile loads to remain reactive to view changes and to reduce the chance of // loading tiles that will quickly disappear from view. var tileQueue = this.tileQueue_; if (!tileQueue.isEmpty()) { var maxTotalLoading = this.maxTilesLoading_; var maxNewLoads = maxTotalLoading; if (frameState) { var hints = frameState.viewHints; if (hints[ViewHint.ANIMATING] || hints[ViewHint.INTERACTING]) { var lowOnFrameBudget = !IMAGE_DECODE && Date.now() - frameState.time > 8; maxTotalLoading = lowOnFrameBudget ? 0 : 8; maxNewLoads = lowOnFrameBudget ? 0 : 2; } } if (tileQueue.getTilesLoading() < maxTotalLoading) { tileQueue.reprioritize(); // FIXME only call if view has changed tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); } } if (frameState && this.hasListener(RenderEventType.RENDERCOMPLETE) && !frameState.animate && !this.tileQueue_.getTilesLoading() && !this.getLoading()) { this.renderer_.dispatchRenderEvent(RenderEventType.RENDERCOMPLETE, frameState); } var postRenderFunctions = this.postRenderFunctions_; for (var i = 0, ii = postRenderFunctions.length; i < ii; ++i) { postRenderFunctions[i](this, frameState); } postRenderFunctions.length = 0; }; /** * @private */ PluggableMap.prototype.handleSizeChanged_ = function () { if (this.getView()) { this.getView().resolveConstraints(0); } this.render(); }; /** * @private */ PluggableMap.prototype.handleTargetChanged_ = function () { // target may be undefined, null, a string or an Element. // If it's a string we convert it to an Element before proceeding. // If it's not now an Element we remove the viewport from the DOM. // If it's an Element we append the viewport element to it. var targetElement; if (this.getTarget()) { targetElement = this.getTargetElement(); } if (this.keyHandlerKeys_) { for (var i = 0, ii = this.keyHandlerKeys_.length; i < ii; ++i) { unlistenByKey(this.keyHandlerKeys_[i]); } this.keyHandlerKeys_ = null; } if (!targetElement) { if (this.renderer_) { clearTimeout(this.postRenderTimeoutHandle_); this.postRenderFunctions_.length = 0; this.renderer_.dispose(); this.renderer_ = null; } if (this.animationDelayKey_) { cancelAnimationFrame(this.animationDelayKey_); this.animationDelayKey_ = undefined; } removeNode(this.viewport_); if (this.handleResize_ !== undefined) { removeEventListener(EventType.RESIZE, this.handleResize_, false); this.handleResize_ = undefined; } } else { targetElement.appendChild(this.viewport_); if (!this.renderer_) { this.renderer_ = this.createRenderer(); } var keyboardEventTarget = !this.keyboardEventTarget_ ? targetElement : this.keyboardEventTarget_; this.keyHandlerKeys_ = [ listen(keyboardEventTarget, EventType.KEYDOWN, this.handleBrowserEvent, this), listen(keyboardEventTarget, EventType.KEYPRESS, this.handleBrowserEvent, this) ]; if (!this.handleResize_) { this.handleResize_ = this.updateSize.bind(this); window.addEventListener(EventType.RESIZE, this.handleResize_, false); } } this.updateSize(); // updateSize calls setSize, so no need to call this.render // ourselves here. }; /** * @private */ PluggableMap.prototype.handleTileChange_ = function () { this.render(); }; /** * @private */ PluggableMap.prototype.handleViewPropertyChanged_ = function () { this.render(); }; /** * @private */ PluggableMap.prototype.handleViewChanged_ = function () { if (this.viewPropertyListenerKey_) { unlistenByKey(this.viewPropertyListenerKey_); this.viewPropertyListenerKey_ = null; } if (this.viewChangeListenerKey_) { unlistenByKey(this.viewChangeListenerKey_); this.viewChangeListenerKey_ = null; } var view = this.getView(); if (view) { this.updateViewportSize_(); this.viewPropertyListenerKey_ = listen(view, ObjectEventType.PROPERTYCHANGE, this.handleViewPropertyChanged_, this); this.viewChangeListenerKey_ = listen(view, EventType.CHANGE, this.handleViewPropertyChanged_, this); view.resolveConstraints(0); } this.render(); }; /** * @private */ PluggableMap.prototype.handleLayerGroupChanged_ = function () { if (this.layerGroupPropertyListenerKeys_) { this.layerGroupPropertyListenerKeys_.forEach(unlistenByKey); this.layerGroupPropertyListenerKeys_ = null; } var layerGroup = this.getLayerGroup(); if (layerGroup) { this.layerGroupPropertyListenerKeys_ = [ listen(layerGroup, ObjectEventType.PROPERTYCHANGE, this.render, this), listen(layerGroup, EventType.CHANGE, this.render, this) ]; } this.render(); }; /** * @return {boolean} Is rendered. */ PluggableMap.prototype.isRendered = function () { return !!this.frameState_; }; /** * Requests an immediate render in a synchronous manner. * @api */ PluggableMap.prototype.renderSync = function () { if (this.animationDelayKey_) { cancelAnimationFrame(this.animationDelayKey_); } this.animationDelay_(); }; /** * Redraws all text after new fonts have loaded */ PluggableMap.prototype.redrawText = function () { var layerStates = this.getLayerGroup().getLayerStatesArray(); for (var i = 0, ii = layerStates.length; i < ii; ++i) { var layer = layerStates[i].layer; if (layer.hasRenderer()) { layer.getRenderer().handleFontsChanged(); } } }; /** * Request a map rendering (at the next animation frame). * @api */ PluggableMap.prototype.render = function () { if (this.renderer_ && this.animationDelayKey_ === undefined) { this.animationDelayKey_ = requestAnimationFrame(this.animationDelay_); } }; /** * Remove the given control from the map. * @param {import("./control/Control.js").default} control Control. * @return {import("./control/Control.js").default|undefined} The removed control (or undefined * if the control was not found). * @api */ PluggableMap.prototype.removeControl = function (control) { return this.getControls().remove(control); }; /** * Remove the given interaction from the map. * @param {import("./interaction/Interaction.js").default} interaction Interaction to remove. * @return {import("./interaction/Interaction.js").default|undefined} The removed interaction (or * undefined if the interaction was not found). * @api */ PluggableMap.prototype.removeInteraction = function (interaction) { return this.getInteractions().remove(interaction); }; /** * Removes the given layer from the map. * @param {import("./layer/Base.js").default} layer Layer. * @return {import("./layer/Base.js").default|undefined} The removed layer (or undefined if the * layer was not found). * @api */ PluggableMap.prototype.removeLayer = function (layer) { var layers = this.getLayerGroup().getLayers(); return layers.remove(layer); }; /** * Remove the given overlay from the map. * @param {import("./Overlay.js").default} overlay Overlay. * @return {import("./Overlay.js").default|undefined} The removed overlay (or undefined * if the overlay was not found). * @api */ PluggableMap.prototype.removeOverlay = function (overlay) { return this.getOverlays().remove(overlay); }; /** * @param {number} time Time. * @private */ PluggableMap.prototype.renderFrame_ = function (time) { var size = this.getSize(); var view = this.getView(); var previousFrameState = this.frameState_; /** @type {?FrameState} */ var frameState = null; if (size !== undefined && hasArea(size) && view && view.isDef()) { var viewHints = view.getHints(this.frameState_ ? this.frameState_.viewHints : undefined); var viewState = view.getState(); frameState = { animate: false, coordinateToPixelTransform: this.coordinateToPixelTransform_, declutterItems: previousFrameState ? previousFrameState.declutterItems : [], extent: getForViewAndSize(viewState.center, viewState.resolution, viewState.rotation, size), index: this.frameIndex_++, layerIndex: 0, layerStatesArray: this.getLayerGroup().getLayerStatesArray(), pixelRatio: this.pixelRatio_, pixelToCoordinateTransform: this.pixelToCoordinateTransform_, postRenderFunctions: [], size: size, tileQueue: this.tileQueue_, time: time, usedTiles: {}, viewState: viewState, viewHints: viewHints, wantedTiles: {} }; } this.frameState_ = frameState; this.renderer_.renderFrame(frameState); if (frameState) { if (frameState.animate) { this.render(); } Array.prototype.push.apply(this.postRenderFunctions_, frameState.postRenderFunctions); if (previousFrameState) { var moveStart = !this.previousExtent_ || (!isEmpty(this.previousExtent_) && !equals(frameState.extent, this.previousExtent_)); if (moveStart) { this.dispatchEvent(new MapEvent(MapEventType.MOVESTART, this, previousFrameState)); this.previousExtent_ = createOrUpdateEmpty(this.previousExtent_); } } var idle = this.previousExtent_ && !frameState.viewHints[ViewHint.ANIMATING] && !frameState.viewHints[ViewHint.INTERACTING] && !equals(frameState.extent, this.previousExtent_); if (idle) { this.dispatchEvent(new MapEvent(MapEventType.MOVEEND, this, frameState)); clone(frameState.extent, this.previousExtent_); } } this.dispatchEvent(new MapEvent(MapEventType.POSTRENDER, this, frameState)); this.postRenderTimeoutHandle_ = setTimeout(this.handlePostRender.bind(this), 0); }; /** * Sets the layergroup of this map. * @param {LayerGroup} layerGroup A layer group containing the layers in this map. * @observable * @api */ PluggableMap.prototype.setLayerGroup = function (layerGroup) { this.set(MapProperty.LAYERGROUP, layerGroup); }; /** * Set the size of this map. * @param {import("./size.js").Size|undefined} size The size in pixels of the map in the DOM. * @observable * @api */ PluggableMap.prototype.setSize = function (size) { this.set(MapProperty.SIZE, size); }; /** * Set the target element to render this map into. * @param {HTMLElement|string|undefined} target The Element or id of the Element * that the map is rendered in. * @observable * @api */ PluggableMap.prototype.setTarget = function (target) { this.set(MapProperty.TARGET, target); };