diff --git a/src/ol/Feature.js b/src/ol/Feature.js index e636bb4b40..9ae3b94303 100644 --- a/src/ol/Feature.js +++ b/src/ol/Feature.js @@ -128,7 +128,9 @@ class Feature extends BaseObject { * @api */ clone() { - const clone = new Feature(this.getProperties()); + const clone = new Feature( + this.hasProperties() ? this.getProperties() : null + ); clone.setGeometryName(this.getGeometryName()); const geometry = this.getGeometry(); if (geometry) { diff --git a/src/ol/Object.js b/src/ol/Object.js index a462129c10..59fb349811 100644 --- a/src/ol/Object.js +++ b/src/ol/Object.js @@ -4,7 +4,7 @@ import Event from './events/Event.js'; import ObjectEventType from './ObjectEventType.js'; import Observable from './Observable.js'; -import {assign} from './obj.js'; +import {assign, isEmpty} from './obj.js'; import {getUid} from './util.js'; /** @@ -95,9 +95,9 @@ class BaseObject extends Observable { /** * @private - * @type {!Object} + * @type {Object} */ - this.values_ = {}; + this.values_ = null; if (opt_values !== undefined) { this.setProperties(opt_values); @@ -112,7 +112,7 @@ class BaseObject extends Observable { */ get(key) { let value; - if (this.values_.hasOwnProperty(key)) { + if (this.values_ && this.values_.hasOwnProperty(key)) { value = this.values_[key]; } return value; @@ -124,7 +124,7 @@ class BaseObject extends Observable { * @api */ getKeys() { - return Object.keys(this.values_); + return (this.values_ && Object.keys(this.values_)) || []; } /** @@ -133,7 +133,14 @@ class BaseObject extends Observable { * @api */ getProperties() { - return assign({}, this.values_); + return (this.values_ && assign({}, this.values_)) || {}; + } + + /** + * @return {boolean} The object has properties. + */ + hasProperties() { + return !!this.values_; } /** @@ -156,11 +163,12 @@ class BaseObject extends Observable { * @api */ set(key, value, opt_silent) { + const values = this.values_ || (this.values_ = {}); if (opt_silent) { - this.values_[key] = value; + values[key] = value; } else { - const oldValue = this.values_[key]; - this.values_[key] = value; + const oldValue = values[key]; + values[key] = value; if (oldValue !== value) { this.notify(key, oldValue); } @@ -187,9 +195,12 @@ class BaseObject extends Observable { * @api */ unset(key, opt_silent) { - if (key in this.values_) { + if (this.values_ && key in this.values_) { const oldValue = this.values_[key]; delete this.values_[key]; + if (isEmpty(this.values_)) { + this.values_ = null; + } if (!opt_silent) { this.notify(key, oldValue); } diff --git a/src/ol/events/Target.js b/src/ol/events/Target.js index dbc845d0dc..e8e4967acd 100644 --- a/src/ol/events/Target.js +++ b/src/ol/events/Target.js @@ -40,21 +40,21 @@ class Target extends Disposable { /** * @private - * @type {!Object} + * @type {Object} */ - this.pendingRemovals_ = {}; + this.pendingRemovals_ = null; /** * @private - * @type {!Object} + * @type {Object} */ - this.dispatching_ = {}; + this.dispatching_ = null; /** * @private - * @type {!Object>} + * @type {Object>} */ - this.listeners_ = {}; + this.listeners_ = null; } /** @@ -65,13 +65,10 @@ class Target extends Disposable { if (!type || !listener) { return; } - let listeners = this.listeners_[type]; - if (!listeners) { - listeners = []; - this.listeners_[type] = listeners; - } - if (listeners.indexOf(listener) === -1) { - listeners.push(listener); + const listeners = this.listeners_ || (this.listeners_ = {}); + const listenersForType = listeners[type] || (listeners[type] = []); + if (listenersForType.indexOf(listener) === -1) { + listenersForType.push(listener); } } @@ -92,14 +89,17 @@ class Target extends Disposable { if (!evt.target) { evt.target = this.eventTarget_ || this; } - const listeners = this.listeners_[type]; + const listeners = this.listeners_ && this.listeners_[type]; let propagate; if (listeners) { - if (!(type in this.dispatching_)) { - this.dispatching_[type] = 0; - this.pendingRemovals_[type] = 0; + const dispatching = this.dispatching_ || (this.dispatching_ = {}); + const pendingRemovals = + this.pendingRemovals_ || (this.pendingRemovals_ = {}); + if (!(type in dispatching)) { + dispatching[type] = 0; + pendingRemovals[type] = 0; } - ++this.dispatching_[type]; + ++dispatching[type]; for (let i = 0, ii = listeners.length; i < ii; ++i) { if ('handleEvent' in listeners[i]) { propagate = /** @type {import("../events.js").ListenerObject} */ (listeners[ @@ -115,14 +115,14 @@ class Target extends Disposable { break; } } - --this.dispatching_[type]; - if (this.dispatching_[type] === 0) { - let pendingRemovals = this.pendingRemovals_[type]; - delete this.pendingRemovals_[type]; - while (pendingRemovals--) { + --dispatching[type]; + if (dispatching[type] === 0) { + let pr = pendingRemovals[type]; + delete pendingRemovals[type]; + while (pr--) { this.removeEventListener(type, VOID); } - delete this.dispatching_[type]; + delete dispatching[type]; } return propagate; } @@ -132,7 +132,7 @@ class Target extends Disposable { * Clean up. */ disposeInternal() { - clear(this.listeners_); + this.listeners_ && clear(this.listeners_); } /** @@ -140,10 +140,10 @@ class Target extends Disposable { * order that they will be called in. * * @param {string} type Type. - * @return {Array} Listeners. + * @return {Array|undefined} Listeners. */ getListeners(type) { - return this.listeners_[type]; + return (this.listeners_ && this.listeners_[type]) || undefined; } /** @@ -152,6 +152,9 @@ class Target extends Disposable { * @return {boolean} Has listeners. */ hasListener(opt_type) { + if (!this.listeners_) { + return false; + } return opt_type ? opt_type in this.listeners_ : Object.keys(this.listeners_).length > 0; @@ -162,11 +165,11 @@ class Target extends Disposable { * @param {import("../events.js").Listener} listener Listener. */ removeEventListener(type, listener) { - const listeners = this.listeners_[type]; + const listeners = this.listeners_ && this.listeners_[type]; if (listeners) { const index = listeners.indexOf(listener); if (index !== -1) { - if (type in this.pendingRemovals_) { + if (this.pendingRemovals_ && type in this.pendingRemovals_) { // make listener a no-op, and remove later in #dispatchEvent() listeners[index] = VOID; ++this.pendingRemovals_[type]; diff --git a/src/ol/format/EsriJSON.js b/src/ol/format/EsriJSON.js index 54294236bd..77973fdf2c 100644 --- a/src/ol/format/EsriJSON.js +++ b/src/ol/format/EsriJSON.js @@ -199,6 +199,11 @@ class EsriJSON extends JSONFeature { writeFeatureObject(feature, opt_options) { opt_options = this.adaptOptions(opt_options); const object = {}; + if (!feature.hasProperties()) { + object['attributes'] = {}; + return object; + } + const properties = feature.getProperties(); const geometry = feature.getGeometry(); if (geometry) { object['geometry'] = writeGeometry(geometry, opt_options); @@ -214,9 +219,8 @@ class EsriJSON extends JSONFeature { ), }); } + delete properties[feature.getGeometryName()]; } - const properties = feature.getProperties(); - delete properties[feature.getGeometryName()]; if (!isEmpty(properties)) { object['attributes'] = properties; } else { diff --git a/src/ol/format/GML2.js b/src/ol/format/GML2.js index cc85f92e4e..0d2cb67be8 100644 --- a/src/ol/format/GML2.js +++ b/src/ol/format/GML2.js @@ -212,29 +212,32 @@ class GML2 extends GMLBase { context.serializers = {}; context.serializers[featureNS] = {}; } - const properties = feature.getProperties(); const keys = []; const values = []; - for (const key in properties) { - const value = properties[key]; - if (value !== null) { - keys.push(key); - values.push(value); - if ( - key == geometryName || - typeof (/** @type {?} */ (value).getSimplifiedGeometry) === 'function' - ) { - if (!(key in context.serializers[featureNS])) { - context.serializers[featureNS][key] = makeChildAppender( - this.writeGeometryElement, - this - ); - } - } else { - if (!(key in context.serializers[featureNS])) { - context.serializers[featureNS][key] = makeChildAppender( - writeStringTextNode - ); + if (feature.hasProperties()) { + const properties = feature.getProperties(); + for (const key in properties) { + const value = properties[key]; + if (value !== null) { + keys.push(key); + values.push(value); + if ( + key == geometryName || + typeof (/** @type {?} */ (value).getSimplifiedGeometry) === + 'function' + ) { + if (!(key in context.serializers[featureNS])) { + context.serializers[featureNS][key] = makeChildAppender( + this.writeGeometryElement, + this + ); + } + } else { + if (!(key in context.serializers[featureNS])) { + context.serializers[featureNS][key] = makeChildAppender( + writeStringTextNode + ); + } } } } diff --git a/src/ol/format/GML3.js b/src/ol/format/GML3.js index 7692d3d23b..fb67e703ee 100644 --- a/src/ol/format/GML3.js +++ b/src/ol/format/GML3.js @@ -843,29 +843,32 @@ class GML3 extends GMLBase { context.serializers = {}; context.serializers[featureNS] = {}; } - const properties = feature.getProperties(); const keys = []; const values = []; - for (const key in properties) { - const value = properties[key]; - if (value !== null) { - keys.push(key); - values.push(value); - if ( - key == geometryName || - typeof (/** @type {?} */ (value).getSimplifiedGeometry) === 'function' - ) { - if (!(key in context.serializers[featureNS])) { - context.serializers[featureNS][key] = makeChildAppender( - this.writeGeometryElement, - this - ); - } - } else { - if (!(key in context.serializers[featureNS])) { - context.serializers[featureNS][key] = makeChildAppender( - writeStringTextNode - ); + if (feature.hasProperties()) { + const properties = feature.getProperties(); + for (const key in properties) { + const value = properties[key]; + if (value !== null) { + keys.push(key); + values.push(value); + if ( + key == geometryName || + typeof (/** @type {?} */ (value).getSimplifiedGeometry) === + 'function' + ) { + if (!(key in context.serializers[featureNS])) { + context.serializers[featureNS][key] = makeChildAppender( + this.writeGeometryElement, + this + ); + } + } else { + if (!(key in context.serializers[featureNS])) { + context.serializers[featureNS][key] = makeChildAppender( + writeStringTextNode + ); + } } } } diff --git a/src/ol/format/GeoJSON.js b/src/ol/format/GeoJSON.js index 93cc1b9a39..583215bdfc 100644 --- a/src/ol/format/GeoJSON.js +++ b/src/ol/format/GeoJSON.js @@ -207,16 +207,22 @@ class GeoJSON extends JSONFeature { object.id = id; } - const geometry = feature.getGeometry(); - if (geometry) { - object.geometry = writeGeometry(geometry, opt_options); + if (!feature.hasProperties()) { + return object; } const properties = feature.getProperties(); - delete properties[feature.getGeometryName()]; + const geometry = feature.getGeometry(); + if (geometry) { + object.geometry = writeGeometry(geometry, opt_options); + + delete properties[feature.getGeometryName()]; + } + if (!isEmpty(properties)) { object.properties = properties; } + return object; } diff --git a/test/spec/ol/events/eventtarget.test.js b/test/spec/ol/events/eventtarget.test.js index 542b38ccea..90bf63ce59 100644 --- a/test/spec/ol/events/eventtarget.test.js +++ b/test/spec/ol/events/eventtarget.test.js @@ -24,9 +24,6 @@ describe('ol.events.EventTarget', function () { expect(eventTarget).to.be.a(EventTarget); expect(eventTarget).to.be.a(Disposable); }); - it('creates an empty listeners_ object', function () { - expect(Object.keys(eventTarget.listeners_)).to.have.length(0); - }); it('accepts a default target', function (done) { const defaultTarget = {}; const target = new EventTarget(defaultTarget); @@ -36,16 +33,21 @@ describe('ol.events.EventTarget', function () { }); target.dispatchEvent('my-event'); }); + it('does not initialize objects in advance', function () { + expect(eventTarget.pendingRemovals_).to.be(null); + expect(eventTarget.dispatching_).to.be(null); + expect(eventTarget.listeners_).to.be(null); + }); }); describe('#hasListener', function () { it('reports any listeners when called without argument', function () { expect(eventTarget.hasListener()).to.be(false); - eventTarget.listeners_['foo'] = [function () {}]; + eventTarget.addEventListener('foo', function () {}); expect(eventTarget.hasListener()).to.be(true); }); it('reports listeners for the type passed as argument', function () { - eventTarget.listeners_['foo'] = [function () {}]; + eventTarget.addEventListener('foo', function () {}); expect(eventTarget.hasListener('foo')).to.be(true); expect(eventTarget.hasListener('bar')).to.be(false); }); diff --git a/test/spec/ol/object.test.js b/test/spec/ol/object.test.js index 353c367e01..b3529b27c2 100644 --- a/test/spec/ol/object.test.js +++ b/test/spec/ol/object.test.js @@ -101,6 +101,22 @@ describe('ol.Object', function () { }); }); + describe('hasProperties', function () { + it('has no properties after creation', function () { + expect(o.hasProperties()).to.eql(false); + }); + + it('has properties after set', function () { + o.set('foo', 1); + expect(o.hasProperties()).to.eql(true); + }); + + it('has no properties after unset all', function () { + o.unset('foo'); + expect(o.hasProperties()).to.eql(false); + }); + }); + describe('notify', function () { let listener1, listener2;