ol
Version:
OpenLayers mapping library
1,175 lines (1,174 loc) • 54.7 kB
JavaScript
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);
};