diff --git a/examples/device-orientation.js b/examples/device-orientation.js index b464d2e958..318dc6468d 100644 --- a/examples/device-orientation.js +++ b/examples/device-orientation.js @@ -25,11 +25,13 @@ var deviceOrientation = new ol.DeviceOrientation(); var track = new ol.dom.Input(document.getElementById('track')); track.bindTo('checked', deviceOrientation, 'tracking'); -deviceOrientation.on('change', function(event) { - document.getElementById('alpha').innerHTML = event.target.getAlpha(); - document.getElementById('beta').innerHTML = event.target.getBeta(); - document.getElementById('gamma').innerHTML = event.target.getGamma(); - document.getElementById('heading').innerHTML = event.target.getHeading(); +deviceOrientation.on('propertychange', function(event) { + // event.getKey() is the changed property name + var key = event.getKey(); + var element = document.getElementById(key); + if (element) { + element.innerHTML = deviceOrientation.get(key); + } }); // tilt the map diff --git a/examples/geolocation.js b/examples/geolocation.js index 7fb3fe83f2..fc0d2ac62e 100644 --- a/examples/geolocation.js +++ b/examples/geolocation.js @@ -29,7 +29,7 @@ geolocation.bindTo('projection', map.getView()); var track = new ol.dom.Input(document.getElementById('track')); track.bindTo('checked', geolocation, 'tracking'); -geolocation.on('change', function() { +geolocation.on('propertychange', function() { $('#accuracy').text(geolocation.getAccuracy() + ' [m]'); $('#altitude').text(geolocation.getAltitude() + ' [m]'); $('#altitudeAccuracy').text(geolocation.getAltitudeAccuracy() + ' [m]'); diff --git a/src/ol/layer/layergroup.js b/src/ol/layer/layergroup.js index 50649459f0..4e00e74c5d 100644 --- a/src/ol/layer/layergroup.js +++ b/src/ol/layer/layergroup.js @@ -10,6 +10,7 @@ goog.require('ol.Collection'); goog.require('ol.CollectionEvent'); goog.require('ol.CollectionEventType'); goog.require('ol.Object'); +goog.require('ol.ObjectEventType'); goog.require('ol.layer.Base'); goog.require('ol.source.State'); @@ -104,7 +105,8 @@ ol.layer.Group.prototype.handleLayersChanged_ = function(event) { for (i = 0, ii = layersArray.length; i < ii; i++) { layer = layersArray[i]; this.listenerKeys_[goog.getUid(layer).toString()] = - goog.events.listen(layer, goog.events.EventType.CHANGE, + goog.events.listen(layer, + [ol.ObjectEventType.PROPERTYCHANGE, goog.events.EventType.CHANGE], this.handleLayerChange_, false, this); } } @@ -120,8 +122,8 @@ ol.layer.Group.prototype.handleLayersChanged_ = function(event) { ol.layer.Group.prototype.handleLayersAdd_ = function(collectionEvent) { var layer = /** @type {ol.layer.Base} */ (collectionEvent.getElement()); this.listenerKeys_[goog.getUid(layer).toString()] = goog.events.listen( - layer, goog.events.EventType.CHANGE, this.handleLayerChange_, false, - this); + layer, [ol.ObjectEventType.PROPERTYCHANGE, goog.events.EventType.CHANGE], + this.handleLayerChange_, false, this); this.dispatchChangeEvent(); }; diff --git a/src/ol/map.js b/src/ol/map.js index 69fd635db6..7caf96bcc0 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -39,6 +39,7 @@ goog.require('ol.MapBrowserEventHandler'); goog.require('ol.MapEvent'); goog.require('ol.MapEventType'); goog.require('ol.Object'); +goog.require('ol.ObjectEvent'); goog.require('ol.ObjectEventType'); goog.require('ol.Pixel'); goog.require('ol.PostRenderFunction'); @@ -208,9 +209,9 @@ ol.Map = function(options) { /** * @private - * @type {goog.events.Key} + * @type {Array.} */ - this.layerGroupPropertyListenerKey_ = null; + this.layerGroupPropertyListenerKeys_ = null; /** * @private @@ -888,7 +889,7 @@ ol.Map.prototype.handleViewChanged_ = function() { var view = this.getView(); if (goog.isDefAndNotNull(view)) { this.viewPropertyListenerKey_ = goog.events.listen( - view, ol.ObjectEventType.CHANGE, + view, ol.ObjectEventType.PROPERTYCHANGE, this.handleViewPropertyChanged_, false, this); } this.render(); @@ -899,7 +900,18 @@ ol.Map.prototype.handleViewChanged_ = function() { * @param {goog.events.Event} event Event. * @private */ +ol.Map.prototype.handleLayerGroupMemberChanged_ = function(event) { + goog.asserts.assertInstanceof(event, goog.events.Event); + this.render(); +}; + + +/** + * @param {ol.ObjectEvent} event Event. + * @private + */ ol.Map.prototype.handleLayerGroupPropertyChanged_ = function(event) { + goog.asserts.assertInstanceof(event, ol.ObjectEvent); this.render(); }; @@ -908,15 +920,23 @@ ol.Map.prototype.handleLayerGroupPropertyChanged_ = function(event) { * @private */ ol.Map.prototype.handleLayerGroupChanged_ = function() { - if (!goog.isNull(this.layerGroupPropertyListenerKey_)) { - goog.events.unlistenByKey(this.layerGroupPropertyListenerKey_); - this.layerGroupPropertyListenerKey_ = null; + if (!goog.isNull(this.layerGroupPropertyListenerKeys_)) { + var length = this.layerGroupPropertyListenerKeys_.length; + for (var i = 0; i < length; ++i) { + goog.events.unlistenByKey(this.layerGroupPropertyListenerKeys_[i]); + } + this.layerGroupPropertyListenerKeys_ = null; } var layerGroup = this.getLayerGroup(); if (goog.isDefAndNotNull(layerGroup)) { - this.layerGroupPropertyListenerKey_ = goog.events.listen( - layerGroup, ol.ObjectEventType.CHANGE, - this.handleLayerGroupPropertyChanged_, false, this); + this.layerGroupPropertyListenerKeys_ = [ + goog.events.listen( + layerGroup, ol.ObjectEventType.PROPERTYCHANGE, + this.handleLayerGroupPropertyChanged_, false, this), + goog.events.listen( + layerGroup, goog.events.EventType.CHANGE, + this.handleLayerGroupMemberChanged_, false, this) + ]; } this.render(); }; diff --git a/src/ol/object.exports b/src/ol/object.exports index 89426170a5..3b9b0d2900 100644 --- a/src/ol/object.exports +++ b/src/ol/object.exports @@ -7,3 +7,6 @@ @exportProperty ol.Object.prototype.setValues @exportProperty ol.Object.prototype.unbind @exportProperty ol.Object.prototype.unbindAll + +@exportSymbol ol.ObjectEvent +@exportProperty ol.ObjectEvent.prototype.getKey diff --git a/src/ol/object.js b/src/ol/object.js index 4bca4e75ca..a918f8df06 100644 --- a/src/ol/object.js +++ b/src/ol/object.js @@ -6,10 +6,12 @@ */ goog.provide('ol.Object'); +goog.provide('ol.ObjectEvent'); goog.provide('ol.ObjectEventType'); goog.require('goog.array'); goog.require('goog.events'); +goog.require('goog.events.Event'); goog.require('goog.functions'); goog.require('goog.object'); goog.require('ol.Observable'); @@ -19,7 +21,40 @@ goog.require('ol.Observable'); * @enum {string} */ ol.ObjectEventType = { - CHANGE: 'change' + BEFOREPROPERTYCHANGE: 'beforepropertychange', + PROPERTYCHANGE: 'propertychange' +}; + + + +/** + * Object representing a property change event. + * + * @param {string} type The event type. + * @param {string} key The property name. + * @extends {goog.events.Event} + * @constructor + */ +ol.ObjectEvent = function(type, key) { + goog.base(this, type); + + /** + * The name of the property whose value is changing. + * @type {string} + * @private + */ + this.key_ = key; + +}; +goog.inherits(ol.ObjectEvent, goog.events.Event); + + +/** + * Get the name of the property associated with this event. + * @return {string} Object property name. + */ +ol.ObjectEvent.prototype.getKey = function() { + return this.key_; }; @@ -94,6 +129,13 @@ ol.Object = function(opt_values) { */ this.values_ = {}; + /** + * Lookup of beforechange listener keys. + * @type {Object.} + * @private + */ + this.beforeChangeListeners_ = {}; + if (goog.isDef(opt_values)) { this.setValues(opt_values); } @@ -216,11 +258,20 @@ ol.Object.getSetterName = function(key) { ol.Object.prototype.bindTo = function(key, target, opt_targetKey) { var targetKey = opt_targetKey || key; this.unbind(key); + + // listen for change:targetkey events var eventType = ol.Object.getChangeEventType(targetKey); var listeners = ol.Object.getListeners(this); listeners[key] = goog.events.listen(target, eventType, function() { this.notifyInternal_(key); }, undefined, this); + + // listen for beforechange events and relay if key matches + this.beforeChangeListeners_[key] = goog.events.listen(target, + ol.ObjectEventType.BEFOREPROPERTYCHANGE, + this.createBeforeChangeListener_(key, targetKey), + undefined, this); + var accessor = new ol.ObjectAccessor(target, targetKey); var accessors = ol.Object.getAccessors(this); accessors[key] = accessor; @@ -229,6 +280,30 @@ ol.Object.prototype.bindTo = function(key, target, opt_targetKey) { }; +/** + * Create a listener for beforechange events on a target object. This listener + * will relay events on this object if the event key matches the provided target + * key. + * @param {string} key The key on this object whose value will be changing. + * @param {string} targetKey The key on the target object. + * @return {function(this: ol.Object, ol.ObjectEvent)} Listener. + * @private + */ +ol.Object.prototype.createBeforeChangeListener_ = function(key, targetKey) { + /** + * Conditionally relay beforechange events if event key matches target key. + * @param {ol.ObjectEvent} event The beforechange event from the target. + * @this {ol.Object} + */ + return function(event) { + if (event.getKey() === targetKey) { + this.dispatchEvent( + new ol.ObjectEvent(ol.ObjectEventType.BEFOREPROPERTYCHANGE, key)); + } + }; +}; + + /** * Gets a value. * @param {string} key Key name. @@ -331,7 +406,8 @@ ol.Object.prototype.notify = function(key) { ol.Object.prototype.notifyInternal_ = function(key) { var eventType = ol.Object.getChangeEventType(key); this.dispatchEvent(eventType); - this.dispatchEvent(ol.ObjectEventType.CHANGE); + this.dispatchEvent( + new ol.ObjectEvent(ol.ObjectEventType.PROPERTYCHANGE, key)); }; @@ -342,6 +418,8 @@ ol.Object.prototype.notifyInternal_ = function(key) { * @todo stability experimental */ ol.Object.prototype.set = function(key, value) { + this.dispatchEvent( + new ol.ObjectEvent(ol.ObjectEventType.BEFOREPROPERTYCHANGE, key)); var accessors = ol.Object.getAccessors(this); if (accessors.hasOwnProperty(key)) { var accessor = accessors[key]; @@ -397,6 +475,13 @@ ol.Object.prototype.unbind = function(key) { delete accessors[key]; this.values_[key] = value; } + + // unregister any beforechange listener + var listenerKey = this.beforeChangeListeners_[key]; + if (listenerKey) { + goog.events.unlistenByKey(listenerKey); + delete this.beforeChangeListeners_[key]; + } }; diff --git a/test/spec/ol/layer/layer.test.js b/test/spec/ol/layer/layer.test.js index 8fd23001e3..4d8f1b516c 100644 --- a/test/spec/ol/layer/layer.test.js +++ b/test/spec/ol/layer/layer.test.js @@ -212,7 +212,7 @@ describe('ol.layer.Layer', function() { it('triggers a change event', function() { var listener = sinon.spy(); - layer.on(ol.ObjectEventType.CHANGE, listener); + layer.on(ol.ObjectEventType.PROPERTYCHANGE, listener); layer.setBrightness(0.5); expect(listener.calledOnce).to.be(true); }); @@ -247,7 +247,7 @@ describe('ol.layer.Layer', function() { it('triggers a change event', function() { var listener = sinon.spy(); - layer.on(ol.ObjectEventType.CHANGE, listener); + layer.on(ol.ObjectEventType.PROPERTYCHANGE, listener); layer.setContrast(43); expect(listener.calledOnce).to.be(true); }); @@ -293,7 +293,7 @@ describe('ol.layer.Layer', function() { it('triggers a change event', function() { var listener = sinon.spy(); - layer.on(ol.ObjectEventType.CHANGE, listener); + layer.on(ol.ObjectEventType.PROPERTYCHANGE, listener); layer.setHue(0.5); expect(listener.calledOnce).to.be(true); }); @@ -324,7 +324,7 @@ describe('ol.layer.Layer', function() { it('triggers a change event', function() { var listener = sinon.spy(); - layer.on(ol.ObjectEventType.CHANGE, listener); + layer.on(ol.ObjectEventType.PROPERTYCHANGE, listener); layer.setOpacity(0.4); expect(listener.calledOnce).to.be(true); }); @@ -360,7 +360,7 @@ describe('ol.layer.Layer', function() { it('triggers a change event', function() { var listener = sinon.spy(); - layer.on(ol.ObjectEventType.CHANGE, listener); + layer.on(ol.ObjectEventType.PROPERTYCHANGE, listener); layer.setSaturation(42); expect(listener.calledOnce).to.be(true); }); @@ -393,7 +393,7 @@ describe('ol.layer.Layer', function() { it('fires a change event', function() { var listener = sinon.spy(); - layer.on(ol.ObjectEventType.CHANGE, listener); + layer.on(ol.ObjectEventType.PROPERTYCHANGE, listener); layer.setVisible(false); expect(listener.callCount).to.be(1); diff --git a/test/spec/ol/layer/layergroup.test.js b/test/spec/ol/layer/layergroup.test.js index cefa283374..d3a29cfd15 100644 --- a/test/spec/ol/layer/layergroup.test.js +++ b/test/spec/ol/layer/layergroup.test.js @@ -63,7 +63,7 @@ describe('ol.layer.Group', function() { }); - describe('change event', function() { + describe('generic change event', function() { var layer, group, listener; beforeEach(function() { @@ -84,14 +84,14 @@ describe('ol.layer.Group', function() { }); it('is dispatched by the group when layer opacity changes', function() { - group.on(ol.ObjectEventType.CHANGE, listener); + group.on(goog.events.EventType.CHANGE, listener); layer.setOpacity(0.5); expect(listener.calledOnce).to.be(true); }); it('is dispatched by the group when layer visibility changes', function() { - group.on(ol.ObjectEventType.CHANGE, listener); + group.on(goog.events.EventType.CHANGE, listener); layer.setVisible(false); expect(listener.callCount).to.be(1); @@ -102,6 +102,45 @@ describe('ol.layer.Group', function() { }); + describe('property change event', function() { + + var layer, group, listener; + beforeEach(function() { + layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: 'EPSG:4326' + }) + }); + group = new ol.layer.Group({ + layers: [layer] + }); + listener = sinon.spy(); + }); + + afterEach(function() { + goog.dispose(group); + goog.dispose(layer); + }); + + it('is dispatched by the group when group opacity changes', function() { + group.on(ol.ObjectEventType.PROPERTYCHANGE, listener); + + group.setOpacity(0.5); + expect(listener.calledOnce).to.be(true); + }); + + it('is dispatched by the group when group visibility changes', function() { + group.on(ol.ObjectEventType.PROPERTYCHANGE, listener); + + group.setVisible(false); + expect(listener.callCount).to.be(1); + + group.setVisible(true); + expect(listener.callCount).to.be(2); + }); + + }); + describe('constructor (options)', function() { it('accepts options', function() { @@ -352,6 +391,7 @@ describe('ol.layer.Group', function() { }); goog.require('goog.dispose'); +goog.require('goog.events.EventType'); goog.require('ol.ObjectEventType'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Group'); diff --git a/test/spec/ol/object.test.js b/test/spec/ol/object.test.js index 1c59d6f273..f82bb37a9e 100644 --- a/test/spec/ol/object.test.js +++ b/test/spec/ol/object.test.js @@ -106,7 +106,7 @@ describe('ol.Object', function() { goog.events.listen(o, 'change:k', listener1); listener2 = sinon.spy(); - goog.events.listen(o, 'change', listener2); + goog.events.listen(o, ol.ObjectEventType.PROPERTYCHANGE, listener2); var o2 = new ol.Object(); o2.bindTo('k', o); @@ -121,7 +121,11 @@ describe('ol.Object', function() { it('dispatches generic change events to bound objects', function() { o.notify('k'); - expect(listener2).to.be.called(); + expect(listener2.calledOnce).to.be(true); + var args = listener2.firstCall.args; + expect(args).to.have.length(1); + var event = args[0]; + expect(event.getKey()).to.be('k'); }); it('dispatches events to bound objects', function() { @@ -139,7 +143,7 @@ describe('ol.Object', function() { goog.events.listen(o, 'change:k', listener1); listener2 = sinon.spy(); - goog.events.listen(o, 'change', listener2); + goog.events.listen(o, ol.ObjectEventType.PROPERTYCHANGE, listener2); o2 = new ol.Object(); o2.bindTo('k', o); @@ -157,7 +161,25 @@ describe('ol.Object', function() { it('dispatches generic change events to object', function() { o.set('k', 1); - expect(listener2).to.be.called(); + expect(listener2.calledOnce).to.be(true); + var args = listener2.firstCall.args; + expect(args).to.have.length(1); + var event = args[0]; + expect(event.getKey()).to.be('k'); + }); + + it('dispatches beforechange events to object', function() { + o.set('k', 1); + + var oldValue; + var beforeListener = sinon.spy(function(event) { + oldValue = o2.get(event.getKey()); + }); + o.on(ol.ObjectEventType.BEFOREPROPERTYCHANGE, beforeListener); + + o.set('k', 2); + expect(beforeListener.calledOnce).to.be(true); + expect(oldValue).to.be(1); }); it('dispatches events to bound object', function() { @@ -175,8 +197,80 @@ describe('ol.Object', function() { it('dispatches generic change events to object bound to', function() { o2.set('k', 2); - expect(listener2).to.be.called(); + expect(listener2.calledOnce).to.be(true); + var args = listener2.firstCall.args; + expect(args).to.have.length(1); + var event = args[0]; + expect(event.getKey()).to.be('k'); }); + + it('dispatches beforechange before changing bound objects', function() { + o2.set('k', 1); + + var oldValue; + var beforeListener = sinon.spy(function(event) { + oldValue = o2.get(event.getKey()); + }); + o.on(ol.ObjectEventType.BEFOREPROPERTYCHANGE, beforeListener); + + o2.set('k', 2); + expect(beforeListener.calledOnce).to.be(true); + var args = beforeListener.firstCall.args; + expect(args).to.have.length(1); + var event = args[0]; + expect(event.getKey()).to.be('k'); + + expect(oldValue).to.be(1); + }); + + it('relays beforechange events from bound objects', function() { + var target = new ol.Object({ + foo: 'original value' + }); + var object = new ol.Object(); + object.bindTo('foo', target); + + var oldValue; + var beforeListener = sinon.spy(function(event) { + oldValue = object.get(event.getKey()); + }); + object.on(ol.ObjectEventType.BEFOREPROPERTYCHANGE, beforeListener); + + target.set('foo', 'new value'); + expect(beforeListener.calledOnce).to.be(true); + var args = beforeListener.firstCall.args; + expect(args).to.have.length(1); + var event = args[0]; + expect(event.getKey()).to.be('foo'); + + expect(oldValue).to.be('original value'); + expect(object.get('foo')).to.be('new value'); + }); + + it('relays beforechange events when bound with a new key', function() { + var target = new ol.Object({ + foo: 'original value' + }); + var object = new ol.Object(); + object.bindTo('bar', target, 'foo'); + + var oldValue; + var beforeListener = sinon.spy(function(event) { + oldValue = object.get(event.getKey()); + }); + object.on(ol.ObjectEventType.BEFOREPROPERTYCHANGE, beforeListener); + + target.set('foo', 'new value'); + expect(beforeListener.calledOnce).to.be(true); + var args = beforeListener.firstCall.args; + expect(args).to.have.length(1); + var event = args[0]; + expect(event.getKey()).to.be('bar'); + + expect(oldValue).to.be('original value'); + expect(object.get('bar')).to.be('new value'); + }); + }); describe('bind', function() { @@ -256,6 +350,59 @@ describe('ol.Object', function() { expect(o.get('k')).to.eql(1); expect(o2.get('k')).to.eql(2); }); + + it('stops relaying beforechange events', function() { + var target = new ol.Object({ + foo: 'original value' + }); + var object = new ol.Object(); + object.bindTo('foo', target); + + var listener = sinon.spy(); + object.on(ol.ObjectEventType.BEFOREPROPERTYCHANGE, listener); + + target.set('foo', 'new value'); + expect(listener.calledOnce).to.be(true); + var call = listener.firstCall; + expect(call.args).to.have.length(1); + expect(call.args[0].getKey()).to.be('foo'); + + object.unbind('foo'); + target.set('foo', 'another new value'); + expect(listener.calledOnce).to.be(true); + + expect(object.get('foo')).to.be('new value'); + }); + + it('selectively stops relaying beforechange events', function() { + var target = new ol.Object({ + foo: 'original foo', + bar: 'original bar' + }); + var object = new ol.Object(); + object.bindTo('foo', target); + object.bindTo('bar', target); + + var listener = sinon.spy(); + object.on(ol.ObjectEventType.BEFOREPROPERTYCHANGE, listener); + + target.set('foo', 'new foo'); + expect(listener.calledOnce).to.be(true); + + target.set('bar', 'new bar'); + expect(listener.callCount).to.be(2); + + object.unbind('foo'); + target.set('foo', 'another new foo'); + expect(listener.callCount).to.be(2); + + target.set('bar', 'another new bar'); + expect(listener.callCount).to.be(3); + var lastCall = listener.getCall(2); + expect(lastCall.args).to.have.length(1); + expect(lastCall.args[0].getKey()).to.be('bar'); + }); + }); describe('unbindAll', function() { @@ -567,3 +714,4 @@ describe('ol.Object', function() { goog.require('goog.events'); goog.require('ol.Object'); +goog.require('ol.ObjectEventType');