diff --git a/src/ol/CompositeMap.js b/src/ol/CompositeMap.js new file mode 100644 index 0000000000..7050fd96f6 --- /dev/null +++ b/src/ol/CompositeMap.js @@ -0,0 +1,84 @@ +/** + * @module ol/CompositeMap + */ +import PluggableMap from './PluggableMap.js'; +import {defaults as defaultControls} from './control/util.js'; +import {defaults as defaultInteractions} from './interaction.js'; +import {assign} from './obj.js'; +import CompositeMapRenderer from './renderer/Composite.js'; + +/** + * @classdesc + * The map is the core component of OpenLayers. For a map to render, a view, + * one or more layers, and a target container are needed: + * + * import Map from 'ol/Map'; + * import View from 'ol/View'; + * import TileLayer from 'ol/layer/Tile'; + * import OSM from 'ol/source/OSM'; + * + * var map = new Map({ + * view: new View({ + * center: [0, 0], + * zoom: 1 + * }), + * layers: [ + * new TileLayer({ + * source: new OSM() + * }) + * ], + * target: 'map' + * }); + * + * The above snippet creates a map using a {@link module:ol/layer/Tile} to + * display {@link module:ol/source/OSM~OSM} OSM data and render it to a DOM + * element with the id `map`. + * + * The constructor places a viewport container (with CSS class name + * `ol-viewport`) in the target element (see `getViewport()`), and then two + * further elements within the viewport: one with CSS class name + * `ol-overlaycontainer-stopevent` for controls and some overlays, and one with + * CSS class name `ol-overlaycontainer` for other overlays (see the `stopEvent` + * option of {@link module:ol/Overlay~Overlay} for the difference). The map + * itself is placed in a further element within the viewport. + * + * Layers are stored as a {@link module:ol/Collection~Collection} in + * layerGroups. A top-level group is provided by the library. This is what is + * accessed by `getLayerGroup` and `setLayerGroup`. Layers entered in the + * options are added to this group, and `addLayer` and `removeLayer` change the + * layer collection in the group. `getLayers` is a convenience function for + * `getLayerGroup().getLayers()`. Note that {@link module:ol/layer/Group~Group} + * is a subclass of {@link module:ol/layer/Base}, so layers entered in the + * options or added with `addLayer` can be groups, which can contain further + * groups, and so on. + * + * @fires import("./MapBrowserEvent.js").MapBrowserEvent + * @fires import("./MapEvent.js").MapEvent + * @fires module:ol/render/Event~RenderEvent#postcompose + * @fires module:ol/render/Event~RenderEvent#precompose + * @api + */ +class CompositeMap extends PluggableMap { + + /** + * @param {import("./PluggableMap.js").MapOptions} options Map options. + */ + constructor(options) { + options = assign({}, options); + if (!options.controls) { + options.controls = defaultControls(); + } + if (!options.interactions) { + options.interactions = defaultInteractions(); + } + + super(options); + } + + createRenderer() { + return new CompositeMapRenderer(this); + } +} + + +export default CompositeMap; diff --git a/src/ol/renderer/Composite.js b/src/ol/renderer/Composite.js new file mode 100644 index 0000000000..6cbe9c586a --- /dev/null +++ b/src/ol/renderer/Composite.js @@ -0,0 +1,151 @@ +/** + * @module ol/renderer/canvas/Map + */ +import {apply as applyTransform} from '../transform.js'; +import {stableSort} from '../array.js'; +import {CLASS_UNSELECTABLE} from '../css.js'; +import {visibleAtResolution} from '../layer/Layer.js'; +import RenderEvent from '../render/Event.js'; +import RenderEventType from '../render/EventType.js'; +import MapRenderer, {sortByZIndex} from './Map.js'; +import SourceState from '../source/State.js'; +import {replaceChildren} from '../dom.js'; + + +/** + * @classdesc + * Canvas map renderer. + * @api + */ +class CompositeMapRenderer extends MapRenderer { + + /** + * @param {import("../PluggableMap.js").default} map Map. + */ + constructor(map) { + super(map); + + /** + * @private + * @type {HTMLDivElement} + */ + this.element_ = document.createElement('div'); + + this.element_.style.width = '100%'; + this.element_.style.height = '100%'; + this.element_.className = CLASS_UNSELECTABLE; + + const container = map.getViewport(); + container.insertBefore(this.element_, container.firstChild || null); + + /** + * @private + * @type {Array} + */ + this.children_ = []; + + /** + * @private + * @type {boolean} + */ + this.renderedVisible_ = true; + } + + /** + * @param {import("../render/EventType.js").default} type Event type. + * @param {import("../PluggableMap.js").FrameState} frameState Frame state. + */ + dispatchRenderEvent(type, frameState) { + const map = this.getMap(); + if (map.hasListener(type)) { + const event = new RenderEvent(type, undefined, frameState); + map.dispatchEvent(event); + } + } + + /** + * @inheritDoc + */ + renderFrame(frameState) { + if (!frameState) { + if (this.renderedVisible_) { + this.element_.style.display = 'none'; + this.renderedVisible_ = false; + } + return; + } + + this.calculateMatrices2D(frameState); + this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState); + + const layerStatesArray = frameState.layerStatesArray; + stableSort(layerStatesArray, sortByZIndex); + + const rotation = frameState.viewState.rotation; + if (rotation) { + // TODO: apply rotation + } + + const viewResolution = frameState.viewState.resolution; + + this.children_.length = 0; + for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { + const layerState = layerStatesArray[i]; + if (!visibleAtResolution(layerState, viewResolution) || layerState.sourceState != SourceState.READY) { + continue; + } + + const layer = layerState.layer; + const layerRenderer = this.getLayerRenderer(layer); + if (layerRenderer.prepareFrame(frameState, layerState)) { + const element = layerRenderer.renderFrame(frameState, layerState); + // TODO: deal with opacity + this.children_.push(element); + } + } + + replaceChildren(this.element_, this.children_); + + this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState); + + if (!this.renderedVisible_) { + this.element_.style.display = ''; + this.renderedVisible_ = true; + } + + this.scheduleRemoveUnusedLayerRenderers(frameState); + this.scheduleExpireIconCache(frameState); + } + + /** + * @inheritDoc + */ + forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { + let result; + const viewState = frameState.viewState; + const viewResolution = viewState.resolution; + + const layerStates = frameState.layerStatesArray; + const numLayers = layerStates.length; + + const coordinate = applyTransform( + frameState.pixelToCoordinateTransform, pixel.slice()); + + for (let i = numLayers - 1; i >= 0; --i) { + const layerState = layerStates[i]; + const layer = layerState.layer; + if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) { + const layerRenderer = this.getLayerRenderer(layer); + result = layerRenderer.forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg); + if (result) { + return result; + } + } + } + return undefined; + } + +} + + +export default CompositeMapRenderer; diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index c1294826db..06de5216a5 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -1,7 +1,7 @@ /** * @module ol/renderer/Layer */ -import {getUid} from '../util.js'; +import {getUid, abstract} from '../util.js'; import ImageState from '../ImageState.js'; import Observable from '../Observable.js'; import TileState from '../TileState.js'; @@ -26,6 +26,28 @@ class LayerRenderer extends Observable { } + /** + * Determine whether render should be called. + * @abstract + * @param {import("../PluggableMap.js").FrameState} frameState Frame state. + * @param {import("../layer/Layer.js").State} layerState Layer state. + * @return {boolean} Layer is ready to be rendered. + */ + prepareFrame(frameState, layerState) { + return abstract(); + } + + /** + * Render the layer. + * @abstract + * @param {import("../PluggableMap.js").FrameState} frameState Frame state. + * @param {import("../layer/Layer.js").State} layerState Layer state. + * @return {HTMLElement} The rendered element. + */ + renderFrame(frameState, layerState) { + return abstract(); + } + /** * Create a function that adds loaded tiles to the tile lookup. * @param {import("../source/Tile.js").default} source Tile source. @@ -68,6 +90,21 @@ class LayerRenderer extends Observable { */ forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {} + /** + * @abstract + * @param {import("../coordinate.js").Coordinate} coordinate Coordinate. + * @param {import("../PluggableMap.js").FrameState} frameState FrameState. + * @param {number} hitTolerance Hit tolerance in pixels. + * @param {function(this: S, import("../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer + * callback. + * @param {S} thisArg Value to use as `this` when executing `callback`. + * @return {T|undefined} Callback result. + * @template S,T + */ + forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + return abstract(); + } + /** * @return {import("../layer/Layer.js").default} Layer. */ diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index 4136428322..47b0a582ae 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -136,6 +136,25 @@ class CanvasLayerRenderer extends LayerRenderer { this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, context, frameState, opt_transform); } + /** + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @param {import("../../transform.js").Transform=} opt_transform Transform. + * @protected + */ + preRender(frameState, opt_transform) { + // TODO: pre-render event + } + + /** + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @param {import("../../layer/Layer.js").State} layerState Layer state. + * @param {import("../../transform.js").Transform=} opt_transform Transform. + * @protected + */ + postRender(frameState, layerState, opt_transform) { + // TODO: pre-render event + } + /** * @param {CanvasRenderingContext2D} context Context. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. @@ -175,15 +194,6 @@ class CanvasLayerRenderer extends LayerRenderer { abstract(); } - /** - * @abstract - * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. - * @param {import("../../layer/Layer.js").State} layerState Layer state. - * @return {boolean} whether composeFrame should be called. - */ - prepareFrame(frameState, layerState) { - return abstract(); - } } export default CanvasLayerRenderer; diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index ea30d981e2..dcabe4fcbc 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -82,7 +82,6 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { this.context = createCanvasContext2D(); listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); - } /** @@ -225,6 +224,114 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { this.postCompose(context, frameState, layerState, transform); } + /** + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @param {import("../../layer/Layer.js").State} layerState Layer state. + */ + render(frameState, layerState) { + const replayGroup = this.replayGroup_; + if (!replayGroup || replayGroup.isEmpty()) { + return; + } + + const context = this.context; + const canvas = context.canvas; + + const extent = frameState.extent; + const pixelRatio = frameState.pixelRatio; + const viewState = frameState.viewState; + const projection = viewState.projection; + const rotation = viewState.rotation; + const projectionExtent = projection.getExtent(); + const vectorSource = /** @type {import("../../source/Vector.js").default} */ (this.getLayer().getSource()); + + // clipped rendering if layer extent is set + const clipExtent = layerState.extent; + const clipped = clipExtent !== undefined; + if (clipped) { + this.clip(context, frameState, /** @type {import("../../extent.js").Extent} */ (clipExtent)); + } + + if (this.declutterTree_) { + this.declutterTree_.clear(); + } + + // resize and clear + let width = Math.round(frameState.size[0] * pixelRatio); + let height = Math.round(frameState.size[1] * pixelRatio); + if (rotation) { + const size = Math.round(Math.sqrt(width * width + height * height)); + width = height = size; + } + if (canvas.width != width || canvas.height != height) { + canvas.width = width; + canvas.height = height; + canvas.style.width = (width / pixelRatio) + 'px'; + canvas.style.height = (height / pixelRatio) + 'px'; + } else { + context.clearRect(0, 0, width, height); + } + + const viewHints = frameState.viewHints; + const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); + + // TODO: deal with rotation (this should not be necessary) + if (rotation) { + rotateAtOffset(context, -rotation, width / 2, height / 2); + } + + let transform = this.getTransform(frameState, 0); + const skippedFeatureUids = layerState.managed ? frameState.skippedFeatureUids : {}; + replayGroup.replay(context, transform, rotation, skippedFeatureUids, snapToPixel); + + if (vectorSource.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) { + let startX = extent[0]; + const worldWidth = getWidth(projectionExtent); + let world = 0; + let offsetX; + while (startX < projectionExtent[0]) { + --world; + offsetX = worldWidth * world; + transform = this.getTransform(frameState, offsetX); + replayGroup.replay(context, transform, rotation, skippedFeatureUids, snapToPixel); + startX += worldWidth; + } + world = 0; + startX = extent[2]; + while (startX > projectionExtent[2]) { + ++world; + offsetX = worldWidth * world; + transform = this.getTransform(frameState, offsetX); + replayGroup.replay(context, transform, rotation, skippedFeatureUids, snapToPixel); + startX -= worldWidth; + } + } + + // TODO: deal with rotation (this should not be necessary) + if (rotation) { + rotateAtOffset(context, rotation, width / 2, height / 2); + } + + if (this.getLayer().hasListener(RenderEventType.RENDER)) { + this.dispatchRenderEvent(context, frameState, transform); + } + + if (clipped) { + context.restore(); + } + } + + /** + * @inheritDoc + */ + renderFrame(frameState, layerState) { + const transform = this.getTransform(frameState, 0); + this.preRender(frameState, transform); + this.render(frameState, layerState); + this.postRender(frameState, layerState, transform); + return this.context.canvas; + } + /** * @inheritDoc */