diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 81c38e070d..bc75777755 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -5,7 +5,8 @@ import BaseObject from './Object.js'; import Collection from './Collection.js'; import CollectionEventType from './CollectionEventType.js'; import EventType from './events/EventType.js'; -import LayerGroup from './layer/Group.js'; +import Layer from './layer/Layer.js'; +import LayerGroup, {GroupEvent} from './layer/Group.js'; import MapBrowserEvent from './MapBrowserEvent.js'; import MapBrowserEventHandler from './MapBrowserEventHandler.js'; import MapBrowserEventType from './MapBrowserEventType.js'; @@ -143,6 +144,36 @@ import {removeNode} from './dom.js'; * {@link module:ol/Map~Map#setView}. */ +/** + * @param {import("./layer/Base.js").default} layer Layer. + */ +function removeLayerMapProperty(layer) { + if (layer instanceof Layer) { + layer.setMapInternal(null); + return; + } + if (layer instanceof LayerGroup) { + layer.getLayers().forEach(removeLayerMapProperty); + } +} + +/** + * @param {import("./layer/Base.js").default} layer Layer. + * @param {PluggableMap} map Map. + */ +function setLayerMapProperty(layer, map) { + if (layer instanceof Layer) { + layer.setMapInternal(map); + return; + } + if (layer instanceof LayerGroup) { + const layers = layer.getLayers().getArray(); + for (let i = 0, ii = layers.length; i < ii; ++i) { + setLayerMapProperty(layers[i], map); + } + } +} + /** * @fires import("./MapBrowserEvent.js").MapBrowserEvent * @fires import("./MapEvent.js").MapEvent @@ -524,6 +555,14 @@ class PluggableMap extends BaseObject { layers.push(layer); } + /** + * @param {import("./layer/Group.js").GroupEvent} event The layer add event. + * @private + */ + handleLayerAdd_(event) { + setLayerMapProperty(event.layer, this); + } + /** * Add the given overlay to the map. * @param {import("./Overlay.js").default} overlay Overlay. @@ -1287,9 +1326,12 @@ class PluggableMap extends BaseObject { } const layerGroup = this.getLayerGroup(); if (layerGroup) { + this.handleLayerAdd_(new GroupEvent('addlayer', layerGroup)); this.layerGroupPropertyListenerKeys_ = [ listen(layerGroup, ObjectEventType.PROPERTYCHANGE, this.render, this), listen(layerGroup, EventType.CHANGE, this.render, this), + listen(layerGroup, 'addlayer', this.handleLayerAdd_, this), + listen(layerGroup, 'removelayer', this.handleLayerRemove_, this), ]; } this.render(); @@ -1370,6 +1412,14 @@ class PluggableMap extends BaseObject { return layers.remove(layer); } + /** + * @param {import("./layer/Group.js").GroupEvent} event The layer remove event. + * @private + */ + handleLayerRemove_(event) { + removeLayerMapProperty(event.layer); + } + /** * Remove the given overlay from the map. * @param {import("./Overlay.js").default} overlay Overlay. @@ -1490,6 +1540,10 @@ class PluggableMap extends BaseObject { * @api */ setLayerGroup(layerGroup) { + const oldLayerGroup = this.getLayerGroup(); + if (oldLayerGroup) { + this.handleLayerRemove_(new GroupEvent('removelayer', oldLayerGroup)); + } this.set(MapProperty.LAYERGROUP, layerGroup); } diff --git a/src/ol/layer/Group.js b/src/ol/layer/Group.js index 7bdc2674f0..e2be915586 100644 --- a/src/ol/layer/Group.js +++ b/src/ol/layer/Group.js @@ -4,6 +4,7 @@ import BaseLayer from './Base.js'; import Collection from '../Collection.js'; import CollectionEventType from '../CollectionEventType.js'; +import Event from '../events/Event.js'; import EventType from '../events/EventType.js'; import ObjectEventType from '../ObjectEventType.js'; import SourceState from '../source/State.js'; @@ -13,6 +14,33 @@ import {getIntersection} from '../extent.js'; import {getUid} from '../util.js'; import {listen, unlistenByKey} from '../events.js'; +/** + * @typedef {'addlayer'|'removelayer'} EventType + */ + +/** + * @classdesc + * A layer group triggers 'addlayer' and 'removelayer' events when layers are added to or removed from + * the group or one of its child groups. When a layer group is added to or removed from another layer group, + * a single event will be triggered (instead of one per layer in the group added or removed). + */ +export class GroupEvent extends Event { + /** + * @param {EventType} type The event type. + * @param {BaseLayer} layer The layer. + */ + constructor(type, layer) { + super(type); + + /** + * The added or removed layer. + * @type {BaseLayer} + * @api + */ + this.layer = layer; + } +} + /*** * @template Return * @typedef {import("../Observable").OnSignature & @@ -142,18 +170,48 @@ class LayerGroup extends BaseLayer { const layersArray = layers.getArray(); for (let i = 0, ii = layersArray.length; i < ii; i++) { const layer = layersArray[i]; - this.listenerKeys_[getUid(layer)] = [ - listen( - layer, - ObjectEventType.PROPERTYCHANGE, - this.handleLayerChange_, - this - ), - listen(layer, EventType.CHANGE, this.handleLayerChange_, this), - ]; + this.registerLayerListeners_(layer); + this.dispatchEvent(new GroupEvent('addlayer', layer)); + } + this.changed(); + } + + /** + * @param {BaseLayer} layer The layer. + */ + registerLayerListeners_(layer) { + const listenerKeys = [ + listen( + layer, + ObjectEventType.PROPERTYCHANGE, + this.handleLayerChange_, + this + ), + listen(layer, EventType.CHANGE, this.handleLayerChange_, this), + ]; + + if (layer instanceof LayerGroup) { + listenerKeys.push( + listen(layer, 'addlayer', this.handleLayerGroupAdd_, this), + listen(layer, 'removelayer', this.handleLayerGroupRemove_, this) + ); } - this.changed(); + this.listenerKeys_[getUid(layer)] = listenerKeys; + } + + /** + * @param {GroupEvent} event The layer group event. + */ + handleLayerGroupAdd_(event) { + this.dispatchEvent(new GroupEvent('addlayer', event.layer)); + } + + /** + * @param {GroupEvent} event The layer group event. + */ + handleLayerGroupRemove_(event) { + this.dispatchEvent(new GroupEvent('removelayer', event.layer)); } /** @@ -164,15 +222,8 @@ class LayerGroup extends BaseLayer { const layer = /** @type {import("./Base.js").default} */ ( collectionEvent.element ); - this.listenerKeys_[getUid(layer)] = [ - listen( - layer, - ObjectEventType.PROPERTYCHANGE, - this.handleLayerChange_, - this - ), - listen(layer, EventType.CHANGE, this.handleLayerChange_, this), - ]; + this.registerLayerListeners_(layer); + this.dispatchEvent(new GroupEvent('addlayer', layer)); this.changed(); } @@ -187,6 +238,7 @@ class LayerGroup extends BaseLayer { const key = getUid(layer); this.listenerKeys_[key].forEach(unlistenByKey); delete this.listenerKeys_[key]; + this.dispatchEvent(new GroupEvent('removelayer', layer)); this.changed(); } @@ -213,6 +265,14 @@ class LayerGroup extends BaseLayer { * @api */ setLayers(layers) { + const collection = this.getLayers(); + if (collection) { + const currentLayers = collection.getArray(); + for (let i = 0, ii = currentLayers.length; i < ii; ++i) { + this.dispatchEvent(new GroupEvent('removelayer', currentLayers[i])); + } + } + this.set(Property.LAYERS, layers); } diff --git a/src/ol/layer/Layer.js b/src/ol/layer/Layer.js index 31e442e82b..8c9d75fe6e 100644 --- a/src/ol/layer/Layer.js +++ b/src/ol/layer/Layer.js @@ -258,6 +258,22 @@ class Layer extends BaseLayer { } } + /** + * For use inside the library only. + * @param {import("../PluggableMap.js").default} map Map. + */ + setMapInternal(map) { + this.set(LayerProperty.MAP, map); + } + + /** + * For use inside the library only. + * @return {import("../PluggableMap.js").default} Map. + */ + getMapInternal() { + return this.get(LayerProperty.MAP); + } + /** * Sets the layer to be rendered on top of other layers on a map. The map will * not manage this layer in its layers collection, and the callback in diff --git a/src/ol/layer/Property.js b/src/ol/layer/Property.js index 2e165eb921..ae1d81be9f 100644 --- a/src/ol/layer/Property.js +++ b/src/ol/layer/Property.js @@ -15,4 +15,5 @@ export default { MAX_ZOOM: 'maxZoom', MIN_ZOOM: 'minZoom', SOURCE: 'source', + MAP: 'map', }; diff --git a/test/browser/spec/ol/Map.test.js b/test/browser/spec/ol/Map.test.js index 5c436503e4..04c3595a3b 100644 --- a/test/browser/spec/ol/Map.test.js +++ b/test/browser/spec/ol/Map.test.js @@ -8,12 +8,15 @@ import ImageLayer from '../../../../src/ol/layer/Image.js'; import ImageState from '../../../../src/ol/ImageState.js'; import ImageStatic from '../../../../src/ol/source/ImageStatic.js'; import Interaction from '../../../../src/ol/interaction/Interaction.js'; +import Layer from '../../../../src/ol/layer/Layer.js'; +import LayerGroup from '../../../../src/ol/layer/Group.js'; import Map from '../../../../src/ol/Map.js'; import MapBrowserEvent from '../../../../src/ol/MapBrowserEvent.js'; import MapEvent from '../../../../src/ol/MapEvent.js'; import MouseWheelZoom from '../../../../src/ol/interaction/MouseWheelZoom.js'; import Overlay from '../../../../src/ol/Overlay.js'; import PinchZoom from '../../../../src/ol/interaction/PinchZoom.js'; +import Property from '../../../../src/ol/layer/Property.js'; import Select from '../../../../src/ol/interaction/Select.js'; import TileLayer from '../../../../src/ol/layer/Tile.js'; import TileLayerRenderer from '../../../../src/ol/renderer/canvas/TileLayer.js'; @@ -166,6 +169,7 @@ describe('ol/Map', function () { map.addLayer(layer); expect(map.getLayers().item(0)).to.be(layer); + expect(layer.get(Property.MAP)).to.be(map); }); it('throws if a layer is added twice', function () { @@ -180,6 +184,55 @@ describe('ol/Map', function () { }); }); + describe('#removeLayer()', function () { + it('removes a layer from the map', function () { + const map = new Map({}); + const layer = new TileLayer(); + map.addLayer(layer); + + expect(layer.get(Property.MAP)).to.be(map); + map.removeLayer(layer); + expect(layer.get(Property.MAP)).to.be(null); + }); + + it('removes a layer group from the map', function () { + const map = new Map({}); + const layer = new TileLayer(); + const group = new LayerGroup({layers: [layer]}); + map.addLayer(group); + expect(layer.get(Property.MAP)).to.be(map); + + map.removeLayer(group); + expect(layer.get(Property.MAP)).to.be(null); + }); + }); + + describe('#setLayerGroup()', function () { + it('sets the layer group', function () { + const map = new Map({}); + + const layer = new Layer({}); + const group = new LayerGroup({layers: [layer]}); + map.setLayerGroup(group); + + expect(map.getLayerGroup()).to.be(group); + expect(layer.get(Property.MAP)).to.be(map); + }); + + it('removes the map property from old layers', function () { + const oldLayer = new Layer({}); + const map = new Map({layers: [oldLayer]}); + expect(oldLayer.get(Property.MAP)).to.be(map); + + const layer = new Layer({}); + const group = new LayerGroup({layers: [layer]}); + map.setLayerGroup(group); + + expect(layer.get(Property.MAP)).to.be(map); + expect(oldLayer.get(Property.MAP)).to.be(null); + }); + }); + describe('#setLayers()', function () { it('adds an array of layers to the map', function () { const map = new Map({}); @@ -192,12 +245,21 @@ describe('ol/Map', function () { expect(collection.getLength()).to.be(2); expect(collection.item(0)).to.be(layer0); expect(collection.item(1)).to.be(layer1); + expect(layer0.get(Property.MAP)).to.be(map); + expect(layer1.get(Property.MAP)).to.be(map); }); it('clears any existing layers', function () { - const map = new Map({layers: [new TileLayer()]}); + const oldLayer = new TileLayer(); + const map = new Map({layers: [oldLayer]}); + expect(oldLayer.get(Property.MAP)).to.be(map); - map.setLayers([new TileLayer(), new TileLayer()]); + const newLayer1 = new TileLayer(); + const newLayer2 = new TileLayer(); + map.setLayers([newLayer1, newLayer2]); + expect(newLayer1.get(Property.MAP)).to.be(map); + expect(newLayer2.get(Property.MAP)).to.be(map); + expect(oldLayer.get(Property.MAP)).to.be(null); expect(map.getLayers().getLength()).to.be(2); }); diff --git a/test/browser/spec/ol/layer/group.test.js b/test/browser/spec/ol/layer/Group.test.js similarity index 78% rename from test/browser/spec/ol/layer/group.test.js rename to test/browser/spec/ol/layer/Group.test.js index a60788fb5c..c02a0fbff2 100644 --- a/test/browser/spec/ol/layer/group.test.js +++ b/test/browser/spec/ol/layer/Group.test.js @@ -6,7 +6,7 @@ import {assign} from '../../../../../src/ol/obj.js'; import {getIntersection} from '../../../../../src/ol/extent.js'; import {getUid} from '../../../../../src/ol/util.js'; -describe('ol.layer.Group', function () { +describe('ol/layer/Group', function () { function disposeHierarchy(layer) { if (layer instanceof LayerGroup) { layer.getLayers().forEach((l) => disposeHierarchy(l)); @@ -219,6 +219,160 @@ describe('ol.layer.Group', function () { }); }); + describe('addlayer event', () => { + it('is dispatched when a layer is added', (done) => { + const group = new LayerGroup(); + const layer = new Layer({}); + group.on('addlayer', (event) => { + expect(event.layer).to.be(layer); + done(); + }); + + group.getLayers().push(layer); + }); + + it('is dispatched once for each layer added', (done) => { + const group = new LayerGroup(); + const layers = [new Layer({}), new Layer({}), new Layer({})]; + + let count = 0; + group.on('addlayer', (event) => { + expect(event.layer).to.be(layers[count]); + count++; + if (count === layers.length) { + done(); + } + }); + + group.getLayers().extend(layers); + }); + + it('is dispatched when setLayers is called', (done) => { + const group = new LayerGroup(); + + const layers = [new Layer({}), new Layer({}), new Layer({})]; + + let count = 0; + group.on('addlayer', (event) => { + expect(event.layer).to.be(layers[count]); + count++; + if (count === layers.length) { + done(); + } + }); + + group.setLayers(new Collection(layers)); + }); + + it('is dispatched when a layer group is added', (done) => { + const group = new LayerGroup(); + const layer = new LayerGroup(); + group.on('addlayer', (event) => { + expect(event.layer).to.be(layer); + done(); + }); + + group.getLayers().push(layer); + }); + + it('is dispatched for each layer added to a child group', (done) => { + const group = new LayerGroup(); + const child = new LayerGroup(); + group.getLayers().push(child); + + const layer = new Layer({}); + group.on('addlayer', (event) => { + expect(event.layer).to.be(layer); + done(); + }); + + child.getLayers().push(layer); + }); + + it('is dispatched for each layer added to a child group configured at construction', (done) => { + const child = new LayerGroup(); + const group = new LayerGroup({ + layers: [child], + }); + + const layer = new Layer({}); + group.on('addlayer', (event) => { + expect(event.layer).to.be(layer); + done(); + }); + + child.getLayers().push(layer); + }); + + it('is not dispatched for layers added to a child group after the child group is removed', (done) => { + const child = new LayerGroup(); + const group = new LayerGroup({ + layers: [child], + }); + + const layer = new Layer({}); + group.on('addlayer', (event) => { + done(new Error('unexpected addlayer after group removal')); + }); + + group.getLayers().remove(child); + child.getLayers().push(layer); + + setTimeout(done, 10); + }); + }); + + describe('removelayer event', () => { + it('is dispatched when a layer is removed', (done) => { + const layer = new Layer({}); + const group = new LayerGroup({layers: [layer]}); + group.on('removelayer', (event) => { + expect(event.layer).to.be(layer); + done(); + }); + + group.getLayers().remove(layer); + }); + + it('is dispatched when a setLayers is called', (done) => { + const layer = new Layer({}); + const group = new LayerGroup({layers: [layer]}); + group.on('removelayer', (event) => { + expect(event.layer).to.be(layer); + done(); + }); + + group.setLayers(new Collection()); + }); + + it('is dispatched when a layer is removed from a child group', (done) => { + const layer = new Layer({}); + const child = new LayerGroup({layers: [layer]}); + const group = new LayerGroup({layers: [child]}); + group.on('removelayer', (event) => { + expect(event.layer).to.be(layer); + done(); + }); + + child.getLayers().remove(layer); + }); + + it('is not dispatched when a layer is removed from a child group after child group removal', (done) => { + const layer = new Layer({}); + const child = new LayerGroup({layers: [layer]}); + const group = new LayerGroup({layers: [child]}); + group.getLayers().remove(child); + + group.on('removelayer', (event) => { + done(new Error('unexpected removelayer after group removal')); + }); + + child.getLayers().remove(layer); + + setTimeout(done, 10); + }); + }); + describe('#getLayerState', function () { let group; diff --git a/test/browser/spec/ol/layer/layer.test.js b/test/browser/spec/ol/layer/Layer.test.js similarity index 87% rename from test/browser/spec/ol/layer/layer.test.js rename to test/browser/spec/ol/layer/Layer.test.js index e1ba3fbc1a..03cf12f1e2 100644 --- a/test/browser/spec/ol/layer/layer.test.js +++ b/test/browser/spec/ol/layer/Layer.test.js @@ -1,10 +1,12 @@ +import Group from '../../../../../src/ol/layer/Group.js'; import Layer, {inView} from '../../../../../src/ol/layer/Layer.js'; import Map from '../../../../../src/ol/Map.js'; +import Property from '../../../../../src/ol/layer/Property.js'; import RenderEvent from '../../../../../src/ol/render/Event.js'; import Source from '../../../../../src/ol/source/Source.js'; import {get as getProjection} from '../../../../../src/ol/proj.js'; -describe('ol.layer.Layer', function () { +describe('ol/layer/Layer', function () { describe('constructor (defaults)', function () { let layer; @@ -620,6 +622,79 @@ describe('ol.layer.Layer', function () { }); }); + describe('map property', () => { + it('is set when a layer is added to a map', () => { + const map = new Map({}); + const layer = new Layer({}); + map.addLayer(layer); + + expect(layer.get(Property.MAP)).to.be(map); + }); + + it('is set when a layer is added to a map in the constructor', () => { + const layer = new Layer({}); + const map = new Map({layers: [layer]}); + + expect(layer.get(Property.MAP)).to.be(map); + }); + + it('is set when a layer is added to a group', () => { + const layer = new Layer({}); + const group = new Group(); + const map = new Map({}); + map.addLayer(group); + group.getLayers().push(layer); + + expect(layer.get(Property.MAP)).to.be(map); + }); + + it('is set when a layer is added to a group set in the constructor', () => { + const layer = new Layer({}); + const group = new Group(); + const map = new Map({layers: [group]}); + group.getLayers().push(layer); + + expect(layer.get(Property.MAP)).to.be(map); + }); + + it('is set when a layer already added to a group set in the constructor', () => { + const layer = new Layer({}); + const group = new Group({layers: [layer]}); + const map = new Map({layers: [group]}); + + expect(layer.get(Property.MAP)).to.be(map); + }); + + it('is removed when a layer is removed from the map', () => { + const map = new Map({}); + const layer = new Layer({}); + map.addLayer(layer); + expect(layer.get(Property.MAP)).to.be(map); + + map.removeLayer(layer); + expect(layer.get(Property.MAP)).to.be(null); + }); + + it('is removed when a layer added in the constructor is removed from the map', () => { + const layer = new Layer({}); + const map = new Map({layers: [layer]}); + expect(layer.get(Property.MAP)).to.be(map); + + map.removeLayer(layer); + expect(layer.get(Property.MAP)).to.be(null); + }); + + it('is removed when a layer is removed from a group', () => { + const layer = new Layer({}); + const group = new Group({layers: [layer]}); + const map = new Map({layers: [group]}); + expect(layer.get(Property.MAP)).to.be(map); + + group.getLayers().remove(layer); + expect(layer.get(Property.MAP)).to.be(null); + }); + }); + describe('#setMap (unmanaged layer)', function () { let map;