diff --git a/src/ol/Collection.js b/src/ol/Collection.js index 9dd5a7ff78..b50ed0d40c 100644 --- a/src/ol/Collection.js +++ b/src/ol/Collection.js @@ -65,236 +65,225 @@ inherits(CollectionEvent, Event); * @template T * @api */ -const Collection = function(opt_array, opt_options) { +class Collection { + constructor(opt_array, opt_options) { - BaseObject.call(this); + BaseObject.call(this); - const options = opt_options || {}; + const options = opt_options || {}; + + /** + * @private + * @type {boolean} + */ + this.unique_ = !!options.unique; + + /** + * @private + * @type {!Array.} + */ + this.array_ = opt_array ? opt_array : []; + + if (this.unique_) { + for (let i = 0, ii = this.array_.length; i < ii; ++i) { + this.assertUnique_(this.array_[i], i); + } + } + + this.updateLength_(); + + } /** - * @private - * @type {boolean} + * Remove all elements from the collection. + * @api */ - this.unique_ = !!options.unique; - - /** - * @private - * @type {!Array.} - */ - this.array_ = opt_array ? opt_array : []; - - if (this.unique_) { - for (let i = 0, ii = this.array_.length; i < ii; ++i) { - this.assertUnique_(this.array_[i], i); + clear() { + while (this.getLength() > 0) { + this.pop(); } } - this.updateLength_(); + /** + * Add elements to the collection. This pushes each item in the provided array + * to the end of the collection. + * @param {!Array.} arr Array. + * @return {module:ol/Collection.} This collection. + * @api + */ + extend(arr) { + for (let i = 0, ii = arr.length; i < ii; ++i) { + this.push(arr[i]); + } + return this; + } -}; + /** + * Iterate over each element, calling the provided callback. + * @param {function(T, number, Array.): *} f The function to call + * for every element. This function takes 3 arguments (the element, the + * index and the array). The return value is ignored. + * @api + */ + forEach(f) { + const array = this.array_; + for (let i = 0, ii = array.length; i < ii; ++i) { + f(array[i], i, array); + } + } + + /** + * Get a reference to the underlying Array object. Warning: if the array + * is mutated, no events will be dispatched by the collection, and the + * collection's "length" property won't be in sync with the actual length + * of the array. + * @return {!Array.} Array. + * @api + */ + getArray() { + return this.array_; + } + + /** + * Get the element at the provided index. + * @param {number} index Index. + * @return {T} Element. + * @api + */ + item(index) { + return this.array_[index]; + } + + /** + * Get the length of this collection. + * @return {number} The length of the array. + * @observable + * @api + */ + getLength() { + return /** @type {number} */ (this.get(Property.LENGTH)); + } + + /** + * Insert an element at the provided index. + * @param {number} index Index. + * @param {T} elem Element. + * @api + */ + insertAt(index, elem) { + if (this.unique_) { + this.assertUnique_(elem); + } + this.array_.splice(index, 0, elem); + this.updateLength_(); + this.dispatchEvent( + new CollectionEvent(CollectionEventType.ADD, elem)); + } + + /** + * Remove the last element of the collection and return it. + * Return `undefined` if the collection is empty. + * @return {T|undefined} Element. + * @api + */ + pop() { + return this.removeAt(this.getLength() - 1); + } + + /** + * Insert the provided element at the end of the collection. + * @param {T} elem Element. + * @return {number} New length of the collection. + * @api + */ + push(elem) { + if (this.unique_) { + this.assertUnique_(elem); + } + const n = this.getLength(); + this.insertAt(n, elem); + return this.getLength(); + } + + /** + * Remove the first occurrence of an element from the collection. + * @param {T} elem Element. + * @return {T|undefined} The removed element or undefined if none found. + * @api + */ + remove(elem) { + const arr = this.array_; + for (let i = 0, ii = arr.length; i < ii; ++i) { + if (arr[i] === elem) { + return this.removeAt(i); + } + } + return undefined; + } + + /** + * Remove the element at the provided index and return it. + * Return `undefined` if the collection does not contain this index. + * @param {number} index Index. + * @return {T|undefined} Value. + * @api + */ + removeAt(index) { + const prev = this.array_[index]; + this.array_.splice(index, 1); + this.updateLength_(); + this.dispatchEvent(new CollectionEvent(CollectionEventType.REMOVE, prev)); + return prev; + } + + /** + * Set the element at the provided index. + * @param {number} index Index. + * @param {T} elem Element. + * @api + */ + setAt(index, elem) { + const n = this.getLength(); + if (index < n) { + if (this.unique_) { + this.assertUnique_(elem, index); + } + const prev = this.array_[index]; + this.array_[index] = elem; + this.dispatchEvent( + new CollectionEvent(CollectionEventType.REMOVE, prev)); + this.dispatchEvent( + new CollectionEvent(CollectionEventType.ADD, elem)); + } else { + for (let j = n; j < index; ++j) { + this.insertAt(j, undefined); + } + this.insertAt(index, elem); + } + } + + /** + * @private + */ + updateLength_() { + this.set(Property.LENGTH, this.array_.length); + } + + /** + * @private + * @param {T} elem Element. + * @param {number=} opt_except Optional index to ignore. + */ + assertUnique_(elem, opt_except) { + for (let i = 0, ii = this.array_.length; i < ii; ++i) { + if (this.array_[i] === elem && i !== opt_except) { + throw new AssertionError(58); + } + } + } +} inherits(Collection, BaseObject); -/** - * Remove all elements from the collection. - * @api - */ -Collection.prototype.clear = function() { - while (this.getLength() > 0) { - this.pop(); - } -}; - - -/** - * Add elements to the collection. This pushes each item in the provided array - * to the end of the collection. - * @param {!Array.} arr Array. - * @return {module:ol/Collection.} This collection. - * @api - */ -Collection.prototype.extend = function(arr) { - for (let i = 0, ii = arr.length; i < ii; ++i) { - this.push(arr[i]); - } - return this; -}; - - -/** - * Iterate over each element, calling the provided callback. - * @param {function(T, number, Array.): *} f The function to call - * for every element. This function takes 3 arguments (the element, the - * index and the array). The return value is ignored. - * @api - */ -Collection.prototype.forEach = function(f) { - const array = this.array_; - for (let i = 0, ii = array.length; i < ii; ++i) { - f(array[i], i, array); - } -}; - - -/** - * Get a reference to the underlying Array object. Warning: if the array - * is mutated, no events will be dispatched by the collection, and the - * collection's "length" property won't be in sync with the actual length - * of the array. - * @return {!Array.} Array. - * @api - */ -Collection.prototype.getArray = function() { - return this.array_; -}; - - -/** - * Get the element at the provided index. - * @param {number} index Index. - * @return {T} Element. - * @api - */ -Collection.prototype.item = function(index) { - return this.array_[index]; -}; - - -/** - * Get the length of this collection. - * @return {number} The length of the array. - * @observable - * @api - */ -Collection.prototype.getLength = function() { - return /** @type {number} */ (this.get(Property.LENGTH)); -}; - - -/** - * Insert an element at the provided index. - * @param {number} index Index. - * @param {T} elem Element. - * @api - */ -Collection.prototype.insertAt = function(index, elem) { - if (this.unique_) { - this.assertUnique_(elem); - } - this.array_.splice(index, 0, elem); - this.updateLength_(); - this.dispatchEvent( - new CollectionEvent(CollectionEventType.ADD, elem)); -}; - - -/** - * Remove the last element of the collection and return it. - * Return `undefined` if the collection is empty. - * @return {T|undefined} Element. - * @api - */ -Collection.prototype.pop = function() { - return this.removeAt(this.getLength() - 1); -}; - - -/** - * Insert the provided element at the end of the collection. - * @param {T} elem Element. - * @return {number} New length of the collection. - * @api - */ -Collection.prototype.push = function(elem) { - if (this.unique_) { - this.assertUnique_(elem); - } - const n = this.getLength(); - this.insertAt(n, elem); - return this.getLength(); -}; - - -/** - * Remove the first occurrence of an element from the collection. - * @param {T} elem Element. - * @return {T|undefined} The removed element or undefined if none found. - * @api - */ -Collection.prototype.remove = function(elem) { - const arr = this.array_; - for (let i = 0, ii = arr.length; i < ii; ++i) { - if (arr[i] === elem) { - return this.removeAt(i); - } - } - return undefined; -}; - - -/** - * Remove the element at the provided index and return it. - * Return `undefined` if the collection does not contain this index. - * @param {number} index Index. - * @return {T|undefined} Value. - * @api - */ -Collection.prototype.removeAt = function(index) { - const prev = this.array_[index]; - this.array_.splice(index, 1); - this.updateLength_(); - this.dispatchEvent(new CollectionEvent(CollectionEventType.REMOVE, prev)); - return prev; -}; - - -/** - * Set the element at the provided index. - * @param {number} index Index. - * @param {T} elem Element. - * @api - */ -Collection.prototype.setAt = function(index, elem) { - const n = this.getLength(); - if (index < n) { - if (this.unique_) { - this.assertUnique_(elem, index); - } - const prev = this.array_[index]; - this.array_[index] = elem; - this.dispatchEvent( - new CollectionEvent(CollectionEventType.REMOVE, prev)); - this.dispatchEvent( - new CollectionEvent(CollectionEventType.ADD, elem)); - } else { - for (let j = n; j < index; ++j) { - this.insertAt(j, undefined); - } - this.insertAt(index, elem); - } -}; - - -/** - * @private - */ -Collection.prototype.updateLength_ = function() { - this.set(Property.LENGTH, this.array_.length); -}; - - -/** - * @private - * @param {T} elem Element. - * @param {number=} opt_except Optional index to ignore. - */ -Collection.prototype.assertUnique_ = function(elem, opt_except) { - for (let i = 0, ii = this.array_.length; i < ii; ++i) { - if (this.array_[i] === elem && i !== opt_except) { - throw new AssertionError(58); - } - } -}; - export default Collection; diff --git a/src/ol/Disposable.js b/src/ol/Disposable.js index b3d9ee28ac..4b377d5c5c 100644 --- a/src/ol/Disposable.js +++ b/src/ol/Disposable.js @@ -7,7 +7,17 @@ import {UNDEFINED} from './functions.js'; * Objects that need to clean up after themselves. * @constructor */ -const Disposable = function() {}; +class Disposable { + /** + * Clean up. + */ + dispose() { + if (!this.disposed_) { + this.disposed_ = true; + this.disposeInternal(); + } + } +} /** * The object has already been disposed. @@ -16,16 +26,6 @@ const Disposable = function() {}; */ Disposable.prototype.disposed_ = false; -/** - * Clean up. - */ -Disposable.prototype.dispose = function() { - if (!this.disposed_) { - this.disposed_ = true; - this.disposeInternal(); - } -}; - /** * Extension point for disposable objects. * @protected diff --git a/src/ol/Feature.js b/src/ol/Feature.js index d6d2e025a3..fcd14ebb19 100644 --- a/src/ol/Feature.js +++ b/src/ol/Feature.js @@ -59,229 +59,219 @@ import Style from './style/Style.js'; * associated with a `geometry` key. * @api */ -const Feature = function(opt_geometryOrProperties) { +class Feature { + constructor(opt_geometryOrProperties) { - BaseObject.call(this); + BaseObject.call(this); - /** - * @private - * @type {number|string|undefined} - */ - this.id_ = undefined; + /** + * @private + * @type {number|string|undefined} + */ + this.id_ = undefined; - /** - * @type {string} - * @private - */ - this.geometryName_ = 'geometry'; + /** + * @type {string} + * @private + */ + this.geometryName_ = 'geometry'; - /** - * User provided style. - * @private - * @type {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} - */ - this.style_ = null; + /** + * User provided style. + * @private + * @type {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} + */ + this.style_ = null; - /** - * @private - * @type {module:ol/style/Style~StyleFunction|undefined} - */ - this.styleFunction_ = undefined; + /** + * @private + * @type {module:ol/style/Style~StyleFunction|undefined} + */ + this.styleFunction_ = undefined; - /** - * @private - * @type {?module:ol/events~EventsKey} - */ - this.geometryChangeKey_ = null; + /** + * @private + * @type {?module:ol/events~EventsKey} + */ + this.geometryChangeKey_ = null; - listen( - this, getChangeEventType(this.geometryName_), - this.handleGeometryChanged_, this); + listen( + this, getChangeEventType(this.geometryName_), + this.handleGeometryChanged_, this); - if (opt_geometryOrProperties !== undefined) { - if (opt_geometryOrProperties instanceof Geometry || - !opt_geometryOrProperties) { - const geometry = opt_geometryOrProperties; - this.setGeometry(geometry); - } else { - /** @type {Object.} */ - const properties = opt_geometryOrProperties; - this.setProperties(properties); + if (opt_geometryOrProperties !== undefined) { + if (opt_geometryOrProperties instanceof Geometry || + !opt_geometryOrProperties) { + const geometry = opt_geometryOrProperties; + this.setGeometry(geometry); + } else { + /** @type {Object.} */ + const properties = opt_geometryOrProperties; + this.setProperties(properties); + } } } -}; + + /** + * Clone this feature. If the original feature has a geometry it + * is also cloned. The feature id is not set in the clone. + * @return {module:ol/Feature} The clone. + * @api + */ + clone() { + const clone = new Feature(this.getProperties()); + clone.setGeometryName(this.getGeometryName()); + const geometry = this.getGeometry(); + if (geometry) { + clone.setGeometry(geometry.clone()); + } + const style = this.getStyle(); + if (style) { + clone.setStyle(style); + } + return clone; + } + + /** + * Get the feature's default geometry. A feature may have any number of named + * geometries. The "default" geometry (the one that is rendered by default) is + * set when calling {@link module:ol/Feature~Feature#setGeometry}. + * @return {module:ol/geom/Geometry|undefined} The default geometry for the feature. + * @api + * @observable + */ + getGeometry() { + return ( + /** @type {module:ol/geom/Geometry|undefined} */ (this.get(this.geometryName_)) + ); + } + + /** + * Get the feature identifier. This is a stable identifier for the feature and + * is either set when reading data from a remote source or set explicitly by + * calling {@link module:ol/Feature~Feature#setId}. + * @return {number|string|undefined} Id. + * @api + */ + getId() { + return this.id_; + } + + /** + * Get the name of the feature's default geometry. By default, the default + * geometry is named `geometry`. + * @return {string} Get the property name associated with the default geometry + * for this feature. + * @api + */ + getGeometryName() { + return this.geometryName_; + } + + /** + * Get the feature's style. Will return what was provided to the + * {@link module:ol/Feature~Feature#setStyle} method. + * @return {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} The feature style. + * @api + */ + getStyle() { + return this.style_; + } + + /** + * Get the feature's style function. + * @return {module:ol/style/Style~StyleFunction|undefined} Return a function + * representing the current style of this feature. + * @api + */ + getStyleFunction() { + return this.styleFunction_; + } + + /** + * @private + */ + handleGeometryChange_() { + this.changed(); + } + + /** + * @private + */ + handleGeometryChanged_() { + if (this.geometryChangeKey_) { + unlistenByKey(this.geometryChangeKey_); + this.geometryChangeKey_ = null; + } + const geometry = this.getGeometry(); + if (geometry) { + this.geometryChangeKey_ = listen(geometry, + EventType.CHANGE, this.handleGeometryChange_, this); + } + this.changed(); + } + + /** + * Set the default geometry for the feature. This will update the property + * with the name returned by {@link module:ol/Feature~Feature#getGeometryName}. + * @param {module:ol/geom/Geometry|undefined} geometry The new geometry. + * @api + * @observable + */ + setGeometry(geometry) { + this.set(this.geometryName_, geometry); + } + + /** + * Set the style for the feature. This can be a single style object, an array + * of styles, or a function that takes a resolution and returns an array of + * styles. If it is `null` the feature has no style (a `null` style). + * @param {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} style Style for this feature. + * @api + * @fires module:ol/events/Event~Event#event:change + */ + setStyle(style) { + this.style_ = style; + this.styleFunction_ = !style ? undefined : createStyleFunction(style); + this.changed(); + } + + /** + * Set the feature id. The feature id is considered stable and may be used when + * requesting features or comparing identifiers returned from a remote source. + * The feature id can be used with the + * {@link module:ol/source/Vector~VectorSource#getFeatureById} method. + * @param {number|string|undefined} id The feature id. + * @api + * @fires module:ol/events/Event~Event#event:change + */ + setId(id) { + this.id_ = id; + this.changed(); + } + + /** + * Set the property name to be used when getting the feature's default geometry. + * When calling {@link module:ol/Feature~Feature#getGeometry}, the value of the property with + * this name will be returned. + * @param {string} name The property name of the default geometry. + * @api + */ + setGeometryName(name) { + unlisten( + this, getChangeEventType(this.geometryName_), + this.handleGeometryChanged_, this); + this.geometryName_ = name; + listen( + this, getChangeEventType(this.geometryName_), + this.handleGeometryChanged_, this); + this.handleGeometryChanged_(); + } +} inherits(Feature, BaseObject); -/** - * Clone this feature. If the original feature has a geometry it - * is also cloned. The feature id is not set in the clone. - * @return {module:ol/Feature} The clone. - * @api - */ -Feature.prototype.clone = function() { - const clone = new Feature(this.getProperties()); - clone.setGeometryName(this.getGeometryName()); - const geometry = this.getGeometry(); - if (geometry) { - clone.setGeometry(geometry.clone()); - } - const style = this.getStyle(); - if (style) { - clone.setStyle(style); - } - return clone; -}; - - -/** - * Get the feature's default geometry. A feature may have any number of named - * geometries. The "default" geometry (the one that is rendered by default) is - * set when calling {@link module:ol/Feature~Feature#setGeometry}. - * @return {module:ol/geom/Geometry|undefined} The default geometry for the feature. - * @api - * @observable - */ -Feature.prototype.getGeometry = function() { - return ( - /** @type {module:ol/geom/Geometry|undefined} */ (this.get(this.geometryName_)) - ); -}; - - -/** - * Get the feature identifier. This is a stable identifier for the feature and - * is either set when reading data from a remote source or set explicitly by - * calling {@link module:ol/Feature~Feature#setId}. - * @return {number|string|undefined} Id. - * @api - */ -Feature.prototype.getId = function() { - return this.id_; -}; - - -/** - * Get the name of the feature's default geometry. By default, the default - * geometry is named `geometry`. - * @return {string} Get the property name associated with the default geometry - * for this feature. - * @api - */ -Feature.prototype.getGeometryName = function() { - return this.geometryName_; -}; - - -/** - * Get the feature's style. Will return what was provided to the - * {@link module:ol/Feature~Feature#setStyle} method. - * @return {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} The feature style. - * @api - */ -Feature.prototype.getStyle = function() { - return this.style_; -}; - - -/** - * Get the feature's style function. - * @return {module:ol/style/Style~StyleFunction|undefined} Return a function - * representing the current style of this feature. - * @api - */ -Feature.prototype.getStyleFunction = function() { - return this.styleFunction_; -}; - - -/** - * @private - */ -Feature.prototype.handleGeometryChange_ = function() { - this.changed(); -}; - - -/** - * @private - */ -Feature.prototype.handleGeometryChanged_ = function() { - if (this.geometryChangeKey_) { - unlistenByKey(this.geometryChangeKey_); - this.geometryChangeKey_ = null; - } - const geometry = this.getGeometry(); - if (geometry) { - this.geometryChangeKey_ = listen(geometry, - EventType.CHANGE, this.handleGeometryChange_, this); - } - this.changed(); -}; - - -/** - * Set the default geometry for the feature. This will update the property - * with the name returned by {@link module:ol/Feature~Feature#getGeometryName}. - * @param {module:ol/geom/Geometry|undefined} geometry The new geometry. - * @api - * @observable - */ -Feature.prototype.setGeometry = function(geometry) { - this.set(this.geometryName_, geometry); -}; - - -/** - * Set the style for the feature. This can be a single style object, an array - * of styles, or a function that takes a resolution and returns an array of - * styles. If it is `null` the feature has no style (a `null` style). - * @param {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} style Style for this feature. - * @api - * @fires module:ol/events/Event~Event#event:change - */ -Feature.prototype.setStyle = function(style) { - this.style_ = style; - this.styleFunction_ = !style ? undefined : createStyleFunction(style); - this.changed(); -}; - - -/** - * Set the feature id. The feature id is considered stable and may be used when - * requesting features or comparing identifiers returned from a remote source. - * The feature id can be used with the - * {@link module:ol/source/Vector~VectorSource#getFeatureById} method. - * @param {number|string|undefined} id The feature id. - * @api - * @fires module:ol/events/Event~Event#event:change - */ -Feature.prototype.setId = function(id) { - this.id_ = id; - this.changed(); -}; - - -/** - * Set the property name to be used when getting the feature's default geometry. - * When calling {@link module:ol/Feature~Feature#getGeometry}, the value of the property with - * this name will be returned. - * @param {string} name The property name of the default geometry. - * @api - */ -Feature.prototype.setGeometryName = function(name) { - unlisten( - this, getChangeEventType(this.geometryName_), - this.handleGeometryChanged_, this); - this.geometryName_ = name; - listen( - this, getChangeEventType(this.geometryName_), - this.handleGeometryChanged_, this); - this.handleGeometryChanged_(); -}; - - /** * Convert the provided object into a feature style function. Functions passed * through unchanged. Arrays of module:ol/style/Style or single style objects wrapped diff --git a/src/ol/Geolocation.js b/src/ol/Geolocation.js index 43a1d55fd7..80a3902162 100644 --- a/src/ol/Geolocation.js +++ b/src/ol/Geolocation.js @@ -49,302 +49,289 @@ import {get as getProjection, getTransformFromProjections, identityTransform} fr * @param {module:ol/Geolocation~Options=} opt_options Options. * @api */ -const Geolocation = function(opt_options) { +class Geolocation { + constructor(opt_options) { - BaseObject.call(this); + BaseObject.call(this); - const options = opt_options || {}; + const options = opt_options || {}; - /** - * The unprojected (EPSG:4326) device position. - * @private - * @type {module:ol/coordinate~Coordinate} - */ - this.position_ = null; + /** + * The unprojected (EPSG:4326) device position. + * @private + * @type {module:ol/coordinate~Coordinate} + */ + this.position_ = null; - /** - * @private - * @type {module:ol/proj~TransformFunction} - */ - this.transform_ = identityTransform; + /** + * @private + * @type {module:ol/proj~TransformFunction} + */ + this.transform_ = identityTransform; - /** - * @private - * @type {number|undefined} - */ - this.watchId_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.watchId_ = undefined; - listen( - this, getChangeEventType(GeolocationProperty.PROJECTION), - this.handleProjectionChanged_, this); - listen( - this, getChangeEventType(GeolocationProperty.TRACKING), - this.handleTrackingChanged_, this); + listen( + this, getChangeEventType(GeolocationProperty.PROJECTION), + this.handleProjectionChanged_, this); + listen( + this, getChangeEventType(GeolocationProperty.TRACKING), + this.handleTrackingChanged_, this); + + if (options.projection !== undefined) { + this.setProjection(options.projection); + } + if (options.trackingOptions !== undefined) { + this.setTrackingOptions(options.trackingOptions); + } + + this.setTracking(options.tracking !== undefined ? options.tracking : false); - if (options.projection !== undefined) { - this.setProjection(options.projection); - } - if (options.trackingOptions !== undefined) { - this.setTrackingOptions(options.trackingOptions); } - this.setTracking(options.tracking !== undefined ? options.tracking : false); + /** + * @inheritDoc + */ + disposeInternal() { + this.setTracking(false); + BaseObject.prototype.disposeInternal.call(this); + } -}; + /** + * @private + */ + handleProjectionChanged_() { + const projection = this.getProjection(); + if (projection) { + this.transform_ = getTransformFromProjections( + getProjection('EPSG:4326'), projection); + if (this.position_) { + this.set(GeolocationProperty.POSITION, this.transform_(this.position_)); + } + } + } + + /** + * @private + */ + handleTrackingChanged_() { + if (GEOLOCATION) { + const tracking = this.getTracking(); + if (tracking && this.watchId_ === undefined) { + this.watchId_ = navigator.geolocation.watchPosition( + this.positionChange_.bind(this), + this.positionError_.bind(this), + this.getTrackingOptions()); + } else if (!tracking && this.watchId_ !== undefined) { + navigator.geolocation.clearWatch(this.watchId_); + this.watchId_ = undefined; + } + } + } + + /** + * @private + * @param {GeolocationPosition} position position event. + */ + positionChange_(position) { + const coords = position.coords; + this.set(GeolocationProperty.ACCURACY, coords.accuracy); + this.set(GeolocationProperty.ALTITUDE, + coords.altitude === null ? undefined : coords.altitude); + this.set(GeolocationProperty.ALTITUDE_ACCURACY, + coords.altitudeAccuracy === null ? + undefined : coords.altitudeAccuracy); + this.set(GeolocationProperty.HEADING, coords.heading === null ? + undefined : toRadians(coords.heading)); + if (!this.position_) { + this.position_ = [coords.longitude, coords.latitude]; + } else { + this.position_[0] = coords.longitude; + this.position_[1] = coords.latitude; + } + const projectedPosition = this.transform_(this.position_); + this.set(GeolocationProperty.POSITION, projectedPosition); + this.set(GeolocationProperty.SPEED, + coords.speed === null ? undefined : coords.speed); + const geometry = circularPolygon(this.position_, coords.accuracy); + geometry.applyTransform(this.transform_); + this.set(GeolocationProperty.ACCURACY_GEOMETRY, geometry); + this.changed(); + } + + /** + * Triggered when the Geolocation returns an error. + * @event error + * @api + */ + + /** + * @private + * @param {GeolocationPositionError} error error object. + */ + positionError_(error) { + error.type = EventType.ERROR; + this.setTracking(false); + this.dispatchEvent(/** @type {{type: string, target: undefined}} */ (error)); + } + + /** + * Get the accuracy of the position in meters. + * @return {number|undefined} The accuracy of the position measurement in + * meters. + * @observable + * @api + */ + getAccuracy() { + return /** @type {number|undefined} */ (this.get(GeolocationProperty.ACCURACY)); + } + + /** + * Get a geometry of the position accuracy. + * @return {?module:ol/geom/Polygon} A geometry of the position accuracy. + * @observable + * @api + */ + getAccuracyGeometry() { + return ( + /** @type {?module:ol/geom/Polygon} */ (this.get(GeolocationProperty.ACCURACY_GEOMETRY) || null) + ); + } + + /** + * Get the altitude associated with the position. + * @return {number|undefined} The altitude of the position in meters above mean + * sea level. + * @observable + * @api + */ + getAltitude() { + return /** @type {number|undefined} */ (this.get(GeolocationProperty.ALTITUDE)); + } + + /** + * Get the altitude accuracy of the position. + * @return {number|undefined} The accuracy of the altitude measurement in + * meters. + * @observable + * @api + */ + getAltitudeAccuracy() { + return /** @type {number|undefined} */ (this.get(GeolocationProperty.ALTITUDE_ACCURACY)); + } + + /** + * Get the heading as radians clockwise from North. + * Note: depending on the browser, the heading is only defined if the `enableHighAccuracy` + * is set to `true` in the tracking options. + * @return {number|undefined} The heading of the device in radians from north. + * @observable + * @api + */ + getHeading() { + return /** @type {number|undefined} */ (this.get(GeolocationProperty.HEADING)); + } + + /** + * Get the position of the device. + * @return {module:ol/coordinate~Coordinate|undefined} The current position of the device reported + * in the current projection. + * @observable + * @api + */ + getPosition() { + return ( + /** @type {module:ol/coordinate~Coordinate|undefined} */ (this.get(GeolocationProperty.POSITION)) + ); + } + + /** + * Get the projection associated with the position. + * @return {module:ol/proj/Projection|undefined} The projection the position is + * reported in. + * @observable + * @api + */ + getProjection() { + return ( + /** @type {module:ol/proj/Projection|undefined} */ (this.get(GeolocationProperty.PROJECTION)) + ); + } + + /** + * Get the speed in meters per second. + * @return {number|undefined} The instantaneous speed of the device in meters + * per second. + * @observable + * @api + */ + getSpeed() { + return /** @type {number|undefined} */ (this.get(GeolocationProperty.SPEED)); + } + + /** + * Determine if the device location is being tracked. + * @return {boolean} The device location is being tracked. + * @observable + * @api + */ + getTracking() { + return /** @type {boolean} */ (this.get(GeolocationProperty.TRACKING)); + } + + /** + * Get the tracking options. + * @see http://www.w3.org/TR/geolocation-API/#position-options + * @return {GeolocationPositionOptions|undefined} PositionOptions as defined by + * the [HTML5 Geolocation spec + * ](http://www.w3.org/TR/geolocation-API/#position_options_interface). + * @observable + * @api + */ + getTrackingOptions() { + return /** @type {GeolocationPositionOptions|undefined} */ (this.get(GeolocationProperty.TRACKING_OPTIONS)); + } + + /** + * Set the projection to use for transforming the coordinates. + * @param {module:ol/proj~ProjectionLike} projection The projection the position is + * reported in. + * @observable + * @api + */ + setProjection(projection) { + this.set(GeolocationProperty.PROJECTION, getProjection(projection)); + } + + /** + * Enable or disable tracking. + * @param {boolean} tracking Enable tracking. + * @observable + * @api + */ + setTracking(tracking) { + this.set(GeolocationProperty.TRACKING, tracking); + } + + /** + * Set the tracking options. + * @see http://www.w3.org/TR/geolocation-API/#position-options + * @param {GeolocationPositionOptions} options PositionOptions as defined by the + * [HTML5 Geolocation spec + * ](http://www.w3.org/TR/geolocation-API/#position_options_interface). + * @observable + * @api + */ + setTrackingOptions(options) { + this.set(GeolocationProperty.TRACKING_OPTIONS, options); + } +} inherits(Geolocation, BaseObject); -/** - * @inheritDoc - */ -Geolocation.prototype.disposeInternal = function() { - this.setTracking(false); - BaseObject.prototype.disposeInternal.call(this); -}; - - -/** - * @private - */ -Geolocation.prototype.handleProjectionChanged_ = function() { - const projection = this.getProjection(); - if (projection) { - this.transform_ = getTransformFromProjections( - getProjection('EPSG:4326'), projection); - if (this.position_) { - this.set(GeolocationProperty.POSITION, this.transform_(this.position_)); - } - } -}; - - -/** - * @private - */ -Geolocation.prototype.handleTrackingChanged_ = function() { - if (GEOLOCATION) { - const tracking = this.getTracking(); - if (tracking && this.watchId_ === undefined) { - this.watchId_ = navigator.geolocation.watchPosition( - this.positionChange_.bind(this), - this.positionError_.bind(this), - this.getTrackingOptions()); - } else if (!tracking && this.watchId_ !== undefined) { - navigator.geolocation.clearWatch(this.watchId_); - this.watchId_ = undefined; - } - } -}; - - -/** - * @private - * @param {GeolocationPosition} position position event. - */ -Geolocation.prototype.positionChange_ = function(position) { - const coords = position.coords; - this.set(GeolocationProperty.ACCURACY, coords.accuracy); - this.set(GeolocationProperty.ALTITUDE, - coords.altitude === null ? undefined : coords.altitude); - this.set(GeolocationProperty.ALTITUDE_ACCURACY, - coords.altitudeAccuracy === null ? - undefined : coords.altitudeAccuracy); - this.set(GeolocationProperty.HEADING, coords.heading === null ? - undefined : toRadians(coords.heading)); - if (!this.position_) { - this.position_ = [coords.longitude, coords.latitude]; - } else { - this.position_[0] = coords.longitude; - this.position_[1] = coords.latitude; - } - const projectedPosition = this.transform_(this.position_); - this.set(GeolocationProperty.POSITION, projectedPosition); - this.set(GeolocationProperty.SPEED, - coords.speed === null ? undefined : coords.speed); - const geometry = circularPolygon(this.position_, coords.accuracy); - geometry.applyTransform(this.transform_); - this.set(GeolocationProperty.ACCURACY_GEOMETRY, geometry); - this.changed(); -}; - -/** - * Triggered when the Geolocation returns an error. - * @event error - * @api - */ - -/** - * @private - * @param {GeolocationPositionError} error error object. - */ -Geolocation.prototype.positionError_ = function(error) { - error.type = EventType.ERROR; - this.setTracking(false); - this.dispatchEvent(/** @type {{type: string, target: undefined}} */ (error)); -}; - - -/** - * Get the accuracy of the position in meters. - * @return {number|undefined} The accuracy of the position measurement in - * meters. - * @observable - * @api - */ -Geolocation.prototype.getAccuracy = function() { - return /** @type {number|undefined} */ (this.get(GeolocationProperty.ACCURACY)); -}; - - -/** - * Get a geometry of the position accuracy. - * @return {?module:ol/geom/Polygon} A geometry of the position accuracy. - * @observable - * @api - */ -Geolocation.prototype.getAccuracyGeometry = function() { - return ( - /** @type {?module:ol/geom/Polygon} */ (this.get(GeolocationProperty.ACCURACY_GEOMETRY) || null) - ); -}; - - -/** - * Get the altitude associated with the position. - * @return {number|undefined} The altitude of the position in meters above mean - * sea level. - * @observable - * @api - */ -Geolocation.prototype.getAltitude = function() { - return /** @type {number|undefined} */ (this.get(GeolocationProperty.ALTITUDE)); -}; - - -/** - * Get the altitude accuracy of the position. - * @return {number|undefined} The accuracy of the altitude measurement in - * meters. - * @observable - * @api - */ -Geolocation.prototype.getAltitudeAccuracy = function() { - return /** @type {number|undefined} */ (this.get(GeolocationProperty.ALTITUDE_ACCURACY)); -}; - - -/** - * Get the heading as radians clockwise from North. - * Note: depending on the browser, the heading is only defined if the `enableHighAccuracy` - * is set to `true` in the tracking options. - * @return {number|undefined} The heading of the device in radians from north. - * @observable - * @api - */ -Geolocation.prototype.getHeading = function() { - return /** @type {number|undefined} */ (this.get(GeolocationProperty.HEADING)); -}; - - -/** - * Get the position of the device. - * @return {module:ol/coordinate~Coordinate|undefined} The current position of the device reported - * in the current projection. - * @observable - * @api - */ -Geolocation.prototype.getPosition = function() { - return ( - /** @type {module:ol/coordinate~Coordinate|undefined} */ (this.get(GeolocationProperty.POSITION)) - ); -}; - - -/** - * Get the projection associated with the position. - * @return {module:ol/proj/Projection|undefined} The projection the position is - * reported in. - * @observable - * @api - */ -Geolocation.prototype.getProjection = function() { - return ( - /** @type {module:ol/proj/Projection|undefined} */ (this.get(GeolocationProperty.PROJECTION)) - ); -}; - - -/** - * Get the speed in meters per second. - * @return {number|undefined} The instantaneous speed of the device in meters - * per second. - * @observable - * @api - */ -Geolocation.prototype.getSpeed = function() { - return /** @type {number|undefined} */ (this.get(GeolocationProperty.SPEED)); -}; - - -/** - * Determine if the device location is being tracked. - * @return {boolean} The device location is being tracked. - * @observable - * @api - */ -Geolocation.prototype.getTracking = function() { - return /** @type {boolean} */ (this.get(GeolocationProperty.TRACKING)); -}; - - -/** - * Get the tracking options. - * @see http://www.w3.org/TR/geolocation-API/#position-options - * @return {GeolocationPositionOptions|undefined} PositionOptions as defined by - * the [HTML5 Geolocation spec - * ](http://www.w3.org/TR/geolocation-API/#position_options_interface). - * @observable - * @api - */ -Geolocation.prototype.getTrackingOptions = function() { - return /** @type {GeolocationPositionOptions|undefined} */ (this.get(GeolocationProperty.TRACKING_OPTIONS)); -}; - - -/** - * Set the projection to use for transforming the coordinates. - * @param {module:ol/proj~ProjectionLike} projection The projection the position is - * reported in. - * @observable - * @api - */ -Geolocation.prototype.setProjection = function(projection) { - this.set(GeolocationProperty.PROJECTION, getProjection(projection)); -}; - - -/** - * Enable or disable tracking. - * @param {boolean} tracking Enable tracking. - * @observable - * @api - */ -Geolocation.prototype.setTracking = function(tracking) { - this.set(GeolocationProperty.TRACKING, tracking); -}; - - -/** - * Set the tracking options. - * @see http://www.w3.org/TR/geolocation-API/#position-options - * @param {GeolocationPositionOptions} options PositionOptions as defined by the - * [HTML5 Geolocation spec - * ](http://www.w3.org/TR/geolocation-API/#position_options_interface). - * @observable - * @api - */ -Geolocation.prototype.setTrackingOptions = function(options) { - this.set(GeolocationProperty.TRACKING_OPTIONS, options); -}; export default Geolocation; diff --git a/src/ol/Graticule.js b/src/ol/Graticule.js index 4682476256..375d2df7a0 100644 --- a/src/ol/Graticule.js +++ b/src/ol/Graticule.js @@ -116,616 +116,606 @@ const INTERVALS = [ * @param {module:ol/Graticule~Options=} opt_options Options. * @api */ -const Graticule = function(opt_options) { - const options = opt_options || {}; - - /** - * @type {module:ol/PluggableMap} - * @private - */ - this.map_ = null; - - /** - * @type {?module:ol/events~EventsKey} - * @private - */ - this.postcomposeListenerKey_ = null; - - /** - * @type {module:ol/proj/Projection} - */ - this.projection_ = null; - - /** - * @type {number} - * @private - */ - this.maxLat_ = Infinity; - - /** - * @type {number} - * @private - */ - this.maxLon_ = Infinity; - - /** - * @type {number} - * @private - */ - this.minLat_ = -Infinity; - - /** - * @type {number} - * @private - */ - this.minLon_ = -Infinity; - - /** - * @type {number} - * @private - */ - this.maxLatP_ = Infinity; - - /** - * @type {number} - * @private - */ - this.maxLonP_ = Infinity; - - /** - * @type {number} - * @private - */ - this.minLatP_ = -Infinity; - - /** - * @type {number} - * @private - */ - this.minLonP_ = -Infinity; - - /** - * @type {number} - * @private - */ - this.targetSize_ = options.targetSize !== undefined ? options.targetSize : 100; - - /** - * @type {number} - * @private - */ - this.maxLines_ = options.maxLines !== undefined ? options.maxLines : 100; - - /** - * @type {Array.} - * @private - */ - this.meridians_ = []; - - /** - * @type {Array.} - * @private - */ - this.parallels_ = []; - - /** - * @type {module:ol/style/Stroke} - * @private - */ - this.strokeStyle_ = options.strokeStyle !== undefined ? options.strokeStyle : DEFAULT_STROKE_STYLE; - - /** - * @type {module:ol/proj~TransformFunction|undefined} - * @private - */ - this.fromLonLatTransform_ = undefined; - - /** - * @type {module:ol/proj~TransformFunction|undefined} - * @private - */ - this.toLonLatTransform_ = undefined; - - /** - * @type {module:ol/coordinate~Coordinate} - * @private - */ - this.projectionCenterLonLat_ = null; - - /** - * @type {Array.} - * @private - */ - this.meridiansLabels_ = null; - - /** - * @type {Array.} - * @private - */ - this.parallelsLabels_ = null; - - if (options.showLabels == true) { +class Graticule { + constructor(opt_options) { + const options = opt_options || {}; /** - * @type {null|function(number):string} + * @type {module:ol/PluggableMap} * @private */ - this.lonLabelFormatter_ = options.lonLabelFormatter == undefined ? - degreesToStringHDMS.bind(this, 'EW') : options.lonLabelFormatter; + this.map_ = null; /** - * @type {function(number):string} + * @type {?module:ol/events~EventsKey} * @private */ - this.latLabelFormatter_ = options.latLabelFormatter == undefined ? - degreesToStringHDMS.bind(this, 'NS') : options.latLabelFormatter; - - /** - * Longitude label position in fractions (0..1) of view extent. 0 means - * bottom, 1 means top. - * @type {number} - * @private - */ - this.lonLabelPosition_ = options.lonLabelPosition == undefined ? 0 : - options.lonLabelPosition; - - /** - * Latitude Label position in fractions (0..1) of view extent. 0 means left, 1 - * means right. - * @type {number} - * @private - */ - this.latLabelPosition_ = options.latLabelPosition == undefined ? 1 : - options.latLabelPosition; - - /** - * @type {module:ol/style/Text} - * @private - */ - this.lonLabelStyle_ = options.lonLabelStyle !== undefined ? options.lonLabelStyle : - new Text({ - font: '12px Calibri,sans-serif', - textBaseline: 'bottom', - fill: new Fill({ - color: 'rgba(0,0,0,1)' - }), - stroke: new Stroke({ - color: 'rgba(255,255,255,1)', - width: 3 - }) - }); - - /** - * @type {module:ol/style/Text} - * @private - */ - this.latLabelStyle_ = options.latLabelStyle !== undefined ? options.latLabelStyle : - new Text({ - font: '12px Calibri,sans-serif', - textAlign: 'end', - fill: new Fill({ - color: 'rgba(0,0,0,1)' - }), - stroke: new Stroke({ - color: 'rgba(255,255,255,1)', - width: 3 - }) - }); - - this.meridiansLabels_ = []; - this.parallelsLabels_ = []; - } - - this.setMap(options.map !== undefined ? options.map : null); -}; - - -/** - * @param {number} lon Longitude. - * @param {number} minLat Minimal latitude. - * @param {number} maxLat Maximal latitude. - * @param {number} squaredTolerance Squared tolerance. - * @param {module:ol/extent~Extent} extent Extent. - * @param {number} index Index. - * @return {number} Index. - * @private - */ -Graticule.prototype.addMeridian_ = function(lon, minLat, maxLat, squaredTolerance, extent, index) { - const lineString = this.getMeridian_(lon, minLat, maxLat, squaredTolerance, index); - if (intersects(lineString.getExtent(), extent)) { - if (this.meridiansLabels_) { - const textPoint = this.getMeridianPoint_(lineString, extent, index); - this.meridiansLabels_[index] = { - geom: textPoint, - text: this.lonLabelFormatter_(lon) - }; - } - this.meridians_[index++] = lineString; - } - return index; -}; - -/** - * @param {module:ol/geom/LineString} lineString Meridian - * @param {module:ol/extent~Extent} extent Extent. - * @param {number} index Index. - * @return {module:ol/geom/Point} Meridian point. - * @private - */ -Graticule.prototype.getMeridianPoint_ = function(lineString, extent, index) { - const flatCoordinates = lineString.getFlatCoordinates(); - const clampedBottom = Math.max(extent[1], flatCoordinates[1]); - const clampedTop = Math.min(extent[3], flatCoordinates[flatCoordinates.length - 1]); - const lat = clamp( - extent[1] + Math.abs(extent[1] - extent[3]) * this.lonLabelPosition_, - clampedBottom, clampedTop); - const coordinate = [flatCoordinates[0], lat]; - let point; - if (index in this.meridiansLabels_) { - point = this.meridiansLabels_[index]; - point.setCoordinates(coordinate); - } else { - point = new Point(coordinate); - } - return point; -}; - - -/** - * @param {number} lat Latitude. - * @param {number} minLon Minimal longitude. - * @param {number} maxLon Maximal longitude. - * @param {number} squaredTolerance Squared tolerance. - * @param {module:ol/extent~Extent} extent Extent. - * @param {number} index Index. - * @return {number} Index. - * @private - */ -Graticule.prototype.addParallel_ = function(lat, minLon, maxLon, squaredTolerance, extent, index) { - const lineString = this.getParallel_(lat, minLon, maxLon, squaredTolerance, index); - if (intersects(lineString.getExtent(), extent)) { - if (this.parallelsLabels_) { - const textPoint = this.getParallelPoint_(lineString, extent, index); - this.parallelsLabels_[index] = { - geom: textPoint, - text: this.latLabelFormatter_(lat) - }; - } - this.parallels_[index++] = lineString; - } - return index; -}; - - -/** - * @param {module:ol/geom/LineString} lineString Parallels. - * @param {module:ol/extent~Extent} extent Extent. - * @param {number} index Index. - * @return {module:ol/geom/Point} Parallel point. - * @private - */ -Graticule.prototype.getParallelPoint_ = function(lineString, extent, index) { - const flatCoordinates = lineString.getFlatCoordinates(); - const clampedLeft = Math.max(extent[0], flatCoordinates[0]); - const clampedRight = Math.min(extent[2], flatCoordinates[flatCoordinates.length - 2]); - const lon = clamp( - extent[0] + Math.abs(extent[0] - extent[2]) * this.latLabelPosition_, - clampedLeft, clampedRight); - const coordinate = [lon, flatCoordinates[1]]; - let point; - if (index in this.parallelsLabels_) { - point = this.parallelsLabels_[index]; - point.setCoordinates(coordinate); - } else { - point = new Point(coordinate); - } - return point; -}; - - -/** - * @param {module:ol/extent~Extent} extent Extent. - * @param {module:ol/coordinate~Coordinate} center Center. - * @param {number} resolution Resolution. - * @param {number} squaredTolerance Squared tolerance. - * @private - */ -Graticule.prototype.createGraticule_ = function(extent, center, resolution, squaredTolerance) { - - const interval = this.getInterval_(resolution); - if (interval == -1) { - this.meridians_.length = this.parallels_.length = 0; - if (this.meridiansLabels_) { - this.meridiansLabels_.length = 0; - } - if (this.parallelsLabels_) { - this.parallelsLabels_.length = 0; - } - return; - } - - const centerLonLat = this.toLonLatTransform_(center); - let centerLon = centerLonLat[0]; - let centerLat = centerLonLat[1]; - const maxLines = this.maxLines_; - let cnt, idx, lat, lon; - - let validExtent = [ - Math.max(extent[0], this.minLonP_), - Math.max(extent[1], this.minLatP_), - Math.min(extent[2], this.maxLonP_), - Math.min(extent[3], this.maxLatP_) - ]; - - validExtent = transformExtent(validExtent, this.projection_, 'EPSG:4326'); - const maxLat = validExtent[3]; - const maxLon = validExtent[2]; - const minLat = validExtent[1]; - const minLon = validExtent[0]; - - // Create meridians - - centerLon = Math.floor(centerLon / interval) * interval; - lon = clamp(centerLon, this.minLon_, this.maxLon_); - - idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, 0); - - cnt = 0; - while (lon != this.minLon_ && cnt++ < maxLines) { - lon = Math.max(lon - interval, this.minLon_); - idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx); - } - - lon = clamp(centerLon, this.minLon_, this.maxLon_); - - cnt = 0; - while (lon != this.maxLon_ && cnt++ < maxLines) { - lon = Math.min(lon + interval, this.maxLon_); - idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx); - } - - this.meridians_.length = idx; - if (this.meridiansLabels_) { - this.meridiansLabels_.length = idx; - } - - // Create parallels - - centerLat = Math.floor(centerLat / interval) * interval; - lat = clamp(centerLat, this.minLat_, this.maxLat_); - - idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, 0); - - cnt = 0; - while (lat != this.minLat_ && cnt++ < maxLines) { - lat = Math.max(lat - interval, this.minLat_); - idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, idx); - } - - lat = clamp(centerLat, this.minLat_, this.maxLat_); - - cnt = 0; - while (lat != this.maxLat_ && cnt++ < maxLines) { - lat = Math.min(lat + interval, this.maxLat_); - idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, idx); - } - - this.parallels_.length = idx; - if (this.parallelsLabels_) { - this.parallelsLabels_.length = idx; - } - -}; - - -/** - * @param {number} resolution Resolution. - * @return {number} The interval in degrees. - * @private - */ -Graticule.prototype.getInterval_ = function(resolution) { - const centerLon = this.projectionCenterLonLat_[0]; - const centerLat = this.projectionCenterLonLat_[1]; - let interval = -1; - const target = Math.pow(this.targetSize_ * resolution, 2); - /** @type {Array.} **/ - const p1 = []; - /** @type {Array.} **/ - const p2 = []; - for (let i = 0, ii = INTERVALS.length; i < ii; ++i) { - const delta = INTERVALS[i] / 2; - p1[0] = centerLon - delta; - p1[1] = centerLat - delta; - p2[0] = centerLon + delta; - p2[1] = centerLat + delta; - this.fromLonLatTransform_(p1, p1); - this.fromLonLatTransform_(p2, p2); - const dist = Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2); - if (dist <= target) { - break; - } - interval = INTERVALS[i]; - } - return interval; -}; - - -/** - * Get the map associated with this graticule. - * @return {module:ol/PluggableMap} The map. - * @api - */ -Graticule.prototype.getMap = function() { - return this.map_; -}; - - -/** - * @param {number} lon Longitude. - * @param {number} minLat Minimal latitude. - * @param {number} maxLat Maximal latitude. - * @param {number} squaredTolerance Squared tolerance. - * @return {module:ol/geom/LineString} The meridian line string. - * @param {number} index Index. - * @private - */ -Graticule.prototype.getMeridian_ = function(lon, minLat, maxLat, squaredTolerance, index) { - const flatCoordinates = meridian(lon, minLat, maxLat, this.projection_, squaredTolerance); - let lineString = this.meridians_[index]; - if (!lineString) { - lineString = this.meridians_[index] = new LineString(flatCoordinates, GeometryLayout.XY); - } else { - lineString.setFlatCoordinates(GeometryLayout.XY, flatCoordinates); - lineString.changed(); - } - return lineString; -}; - - -/** - * Get the list of meridians. Meridians are lines of equal longitude. - * @return {Array.} The meridians. - * @api - */ -Graticule.prototype.getMeridians = function() { - return this.meridians_; -}; - - -/** - * @param {number} lat Latitude. - * @param {number} minLon Minimal longitude. - * @param {number} maxLon Maximal longitude. - * @param {number} squaredTolerance Squared tolerance. - * @return {module:ol/geom/LineString} The parallel line string. - * @param {number} index Index. - * @private - */ -Graticule.prototype.getParallel_ = function(lat, minLon, maxLon, squaredTolerance, index) { - const flatCoordinates = parallel(lat, minLon, maxLon, this.projection_, squaredTolerance); - let lineString = this.parallels_[index]; - if (!lineString) { - lineString = new LineString(flatCoordinates, GeometryLayout.XY); - } else { - lineString.setFlatCoordinates(GeometryLayout.XY, flatCoordinates); - lineString.changed(); - } - return lineString; -}; - - -/** - * Get the list of parallels. Parallels are lines of equal latitude. - * @return {Array.} The parallels. - * @api - */ -Graticule.prototype.getParallels = function() { - return this.parallels_; -}; - - -/** - * @param {module:ol/render/Event} e Event. - * @private - */ -Graticule.prototype.handlePostCompose_ = function(e) { - const vectorContext = e.vectorContext; - const frameState = e.frameState; - const extent = frameState.extent; - const viewState = frameState.viewState; - const center = viewState.center; - const projection = viewState.projection; - const resolution = viewState.resolution; - const pixelRatio = frameState.pixelRatio; - const squaredTolerance = - resolution * resolution / (4 * pixelRatio * pixelRatio); - - const updateProjectionInfo = !this.projection_ || - !equivalentProjection(this.projection_, projection); - - if (updateProjectionInfo) { - this.updateProjectionInfo_(projection); - } - - this.createGraticule_(extent, center, resolution, squaredTolerance); - - // Draw the lines - vectorContext.setFillStrokeStyle(null, this.strokeStyle_); - let i, l, line; - for (i = 0, l = this.meridians_.length; i < l; ++i) { - line = this.meridians_[i]; - vectorContext.drawGeometry(line); - } - for (i = 0, l = this.parallels_.length; i < l; ++i) { - line = this.parallels_[i]; - vectorContext.drawGeometry(line); - } - let labelData; - if (this.meridiansLabels_) { - for (i = 0, l = this.meridiansLabels_.length; i < l; ++i) { - labelData = this.meridiansLabels_[i]; - this.lonLabelStyle_.setText(labelData.text); - vectorContext.setTextStyle(this.lonLabelStyle_); - vectorContext.drawGeometry(labelData.geom); - } - } - if (this.parallelsLabels_) { - for (i = 0, l = this.parallelsLabels_.length; i < l; ++i) { - labelData = this.parallelsLabels_[i]; - this.latLabelStyle_.setText(labelData.text); - vectorContext.setTextStyle(this.latLabelStyle_); - vectorContext.drawGeometry(labelData.geom); - } - } -}; - - -/** - * @param {module:ol/proj/Projection} projection Projection. - * @private - */ -Graticule.prototype.updateProjectionInfo_ = function(projection) { - const epsg4326Projection = getProjection('EPSG:4326'); - - const worldExtent = projection.getWorldExtent(); - const worldExtentP = transformExtent(worldExtent, epsg4326Projection, projection); - - this.maxLat_ = worldExtent[3]; - this.maxLon_ = worldExtent[2]; - this.minLat_ = worldExtent[1]; - this.minLon_ = worldExtent[0]; - - this.maxLatP_ = worldExtentP[3]; - this.maxLonP_ = worldExtentP[2]; - this.minLatP_ = worldExtentP[1]; - this.minLonP_ = worldExtentP[0]; - - this.fromLonLatTransform_ = getTransform(epsg4326Projection, projection); - - this.toLonLatTransform_ = getTransform(projection, epsg4326Projection); - - this.projectionCenterLonLat_ = this.toLonLatTransform_(getCenter(projection.getExtent())); - - this.projection_ = projection; -}; - - -/** - * Set the map for this graticule. The graticule will be rendered on the - * provided map. - * @param {module:ol/PluggableMap} map Map. - * @api - */ -Graticule.prototype.setMap = function(map) { - if (this.map_) { - unlistenByKey(this.postcomposeListenerKey_); this.postcomposeListenerKey_ = null; - this.map_.render(); + + /** + * @type {module:ol/proj/Projection} + */ + this.projection_ = null; + + /** + * @type {number} + * @private + */ + this.maxLat_ = Infinity; + + /** + * @type {number} + * @private + */ + this.maxLon_ = Infinity; + + /** + * @type {number} + * @private + */ + this.minLat_ = -Infinity; + + /** + * @type {number} + * @private + */ + this.minLon_ = -Infinity; + + /** + * @type {number} + * @private + */ + this.maxLatP_ = Infinity; + + /** + * @type {number} + * @private + */ + this.maxLonP_ = Infinity; + + /** + * @type {number} + * @private + */ + this.minLatP_ = -Infinity; + + /** + * @type {number} + * @private + */ + this.minLonP_ = -Infinity; + + /** + * @type {number} + * @private + */ + this.targetSize_ = options.targetSize !== undefined ? options.targetSize : 100; + + /** + * @type {number} + * @private + */ + this.maxLines_ = options.maxLines !== undefined ? options.maxLines : 100; + + /** + * @type {Array.} + * @private + */ + this.meridians_ = []; + + /** + * @type {Array.} + * @private + */ + this.parallels_ = []; + + /** + * @type {module:ol/style/Stroke} + * @private + */ + this.strokeStyle_ = options.strokeStyle !== undefined ? options.strokeStyle : DEFAULT_STROKE_STYLE; + + /** + * @type {module:ol/proj~TransformFunction|undefined} + * @private + */ + this.fromLonLatTransform_ = undefined; + + /** + * @type {module:ol/proj~TransformFunction|undefined} + * @private + */ + this.toLonLatTransform_ = undefined; + + /** + * @type {module:ol/coordinate~Coordinate} + * @private + */ + this.projectionCenterLonLat_ = null; + + /** + * @type {Array.} + * @private + */ + this.meridiansLabels_ = null; + + /** + * @type {Array.} + * @private + */ + this.parallelsLabels_ = null; + + if (options.showLabels == true) { + + /** + * @type {null|function(number):string} + * @private + */ + this.lonLabelFormatter_ = options.lonLabelFormatter == undefined ? + degreesToStringHDMS.bind(this, 'EW') : options.lonLabelFormatter; + + /** + * @type {function(number):string} + * @private + */ + this.latLabelFormatter_ = options.latLabelFormatter == undefined ? + degreesToStringHDMS.bind(this, 'NS') : options.latLabelFormatter; + + /** + * Longitude label position in fractions (0..1) of view extent. 0 means + * bottom, 1 means top. + * @type {number} + * @private + */ + this.lonLabelPosition_ = options.lonLabelPosition == undefined ? 0 : + options.lonLabelPosition; + + /** + * Latitude Label position in fractions (0..1) of view extent. 0 means left, 1 + * means right. + * @type {number} + * @private + */ + this.latLabelPosition_ = options.latLabelPosition == undefined ? 1 : + options.latLabelPosition; + + /** + * @type {module:ol/style/Text} + * @private + */ + this.lonLabelStyle_ = options.lonLabelStyle !== undefined ? options.lonLabelStyle : + new Text({ + font: '12px Calibri,sans-serif', + textBaseline: 'bottom', + fill: new Fill({ + color: 'rgba(0,0,0,1)' + }), + stroke: new Stroke({ + color: 'rgba(255,255,255,1)', + width: 3 + }) + }); + + /** + * @type {module:ol/style/Text} + * @private + */ + this.latLabelStyle_ = options.latLabelStyle !== undefined ? options.latLabelStyle : + new Text({ + font: '12px Calibri,sans-serif', + textAlign: 'end', + fill: new Fill({ + color: 'rgba(0,0,0,1)' + }), + stroke: new Stroke({ + color: 'rgba(255,255,255,1)', + width: 3 + }) + }); + + this.meridiansLabels_ = []; + this.parallelsLabels_ = []; + } + + this.setMap(options.map !== undefined ? options.map : null); } - if (map) { - this.postcomposeListenerKey_ = listen(map, RenderEventType.POSTCOMPOSE, this.handlePostCompose_, this); - map.render(); + + /** + * @param {number} lon Longitude. + * @param {number} minLat Minimal latitude. + * @param {number} maxLat Maximal latitude. + * @param {number} squaredTolerance Squared tolerance. + * @param {module:ol/extent~Extent} extent Extent. + * @param {number} index Index. + * @return {number} Index. + * @private + */ + addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, index) { + const lineString = this.getMeridian_(lon, minLat, maxLat, squaredTolerance, index); + if (intersects(lineString.getExtent(), extent)) { + if (this.meridiansLabels_) { + const textPoint = this.getMeridianPoint_(lineString, extent, index); + this.meridiansLabels_[index] = { + geom: textPoint, + text: this.lonLabelFormatter_(lon) + }; + } + this.meridians_[index++] = lineString; + } + return index; } - this.map_ = map; -}; + + /** + * @param {module:ol/geom/LineString} lineString Meridian + * @param {module:ol/extent~Extent} extent Extent. + * @param {number} index Index. + * @return {module:ol/geom/Point} Meridian point. + * @private + */ + getMeridianPoint_(lineString, extent, index) { + const flatCoordinates = lineString.getFlatCoordinates(); + const clampedBottom = Math.max(extent[1], flatCoordinates[1]); + const clampedTop = Math.min(extent[3], flatCoordinates[flatCoordinates.length - 1]); + const lat = clamp( + extent[1] + Math.abs(extent[1] - extent[3]) * this.lonLabelPosition_, + clampedBottom, clampedTop); + const coordinate = [flatCoordinates[0], lat]; + let point; + if (index in this.meridiansLabels_) { + point = this.meridiansLabels_[index]; + point.setCoordinates(coordinate); + } else { + point = new Point(coordinate); + } + return point; + } + + /** + * @param {number} lat Latitude. + * @param {number} minLon Minimal longitude. + * @param {number} maxLon Maximal longitude. + * @param {number} squaredTolerance Squared tolerance. + * @param {module:ol/extent~Extent} extent Extent. + * @param {number} index Index. + * @return {number} Index. + * @private + */ + addParallel_(lat, minLon, maxLon, squaredTolerance, extent, index) { + const lineString = this.getParallel_(lat, minLon, maxLon, squaredTolerance, index); + if (intersects(lineString.getExtent(), extent)) { + if (this.parallelsLabels_) { + const textPoint = this.getParallelPoint_(lineString, extent, index); + this.parallelsLabels_[index] = { + geom: textPoint, + text: this.latLabelFormatter_(lat) + }; + } + this.parallels_[index++] = lineString; + } + return index; + } + + /** + * @param {module:ol/geom/LineString} lineString Parallels. + * @param {module:ol/extent~Extent} extent Extent. + * @param {number} index Index. + * @return {module:ol/geom/Point} Parallel point. + * @private + */ + getParallelPoint_(lineString, extent, index) { + const flatCoordinates = lineString.getFlatCoordinates(); + const clampedLeft = Math.max(extent[0], flatCoordinates[0]); + const clampedRight = Math.min(extent[2], flatCoordinates[flatCoordinates.length - 2]); + const lon = clamp( + extent[0] + Math.abs(extent[0] - extent[2]) * this.latLabelPosition_, + clampedLeft, clampedRight); + const coordinate = [lon, flatCoordinates[1]]; + let point; + if (index in this.parallelsLabels_) { + point = this.parallelsLabels_[index]; + point.setCoordinates(coordinate); + } else { + point = new Point(coordinate); + } + return point; + } + + /** + * @param {module:ol/extent~Extent} extent Extent. + * @param {module:ol/coordinate~Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} squaredTolerance Squared tolerance. + * @private + */ + createGraticule_(extent, center, resolution, squaredTolerance) { + + const interval = this.getInterval_(resolution); + if (interval == -1) { + this.meridians_.length = this.parallels_.length = 0; + if (this.meridiansLabels_) { + this.meridiansLabels_.length = 0; + } + if (this.parallelsLabels_) { + this.parallelsLabels_.length = 0; + } + return; + } + + const centerLonLat = this.toLonLatTransform_(center); + let centerLon = centerLonLat[0]; + let centerLat = centerLonLat[1]; + const maxLines = this.maxLines_; + let cnt, idx, lat, lon; + + let validExtent = [ + Math.max(extent[0], this.minLonP_), + Math.max(extent[1], this.minLatP_), + Math.min(extent[2], this.maxLonP_), + Math.min(extent[3], this.maxLatP_) + ]; + + validExtent = transformExtent(validExtent, this.projection_, 'EPSG:4326'); + const maxLat = validExtent[3]; + const maxLon = validExtent[2]; + const minLat = validExtent[1]; + const minLon = validExtent[0]; + + // Create meridians + + centerLon = Math.floor(centerLon / interval) * interval; + lon = clamp(centerLon, this.minLon_, this.maxLon_); + + idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, 0); + + cnt = 0; + while (lon != this.minLon_ && cnt++ < maxLines) { + lon = Math.max(lon - interval, this.minLon_); + idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx); + } + + lon = clamp(centerLon, this.minLon_, this.maxLon_); + + cnt = 0; + while (lon != this.maxLon_ && cnt++ < maxLines) { + lon = Math.min(lon + interval, this.maxLon_); + idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx); + } + + this.meridians_.length = idx; + if (this.meridiansLabels_) { + this.meridiansLabels_.length = idx; + } + + // Create parallels + + centerLat = Math.floor(centerLat / interval) * interval; + lat = clamp(centerLat, this.minLat_, this.maxLat_); + + idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, 0); + + cnt = 0; + while (lat != this.minLat_ && cnt++ < maxLines) { + lat = Math.max(lat - interval, this.minLat_); + idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, idx); + } + + lat = clamp(centerLat, this.minLat_, this.maxLat_); + + cnt = 0; + while (lat != this.maxLat_ && cnt++ < maxLines) { + lat = Math.min(lat + interval, this.maxLat_); + idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, idx); + } + + this.parallels_.length = idx; + if (this.parallelsLabels_) { + this.parallelsLabels_.length = idx; + } + + } + + /** + * @param {number} resolution Resolution. + * @return {number} The interval in degrees. + * @private + */ + getInterval_(resolution) { + const centerLon = this.projectionCenterLonLat_[0]; + const centerLat = this.projectionCenterLonLat_[1]; + let interval = -1; + const target = Math.pow(this.targetSize_ * resolution, 2); + /** @type {Array.} **/ + const p1 = []; + /** @type {Array.} **/ + const p2 = []; + for (let i = 0, ii = INTERVALS.length; i < ii; ++i) { + const delta = INTERVALS[i] / 2; + p1[0] = centerLon - delta; + p1[1] = centerLat - delta; + p2[0] = centerLon + delta; + p2[1] = centerLat + delta; + this.fromLonLatTransform_(p1, p1); + this.fromLonLatTransform_(p2, p2); + const dist = Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2); + if (dist <= target) { + break; + } + interval = INTERVALS[i]; + } + return interval; + } + + /** + * Get the map associated with this graticule. + * @return {module:ol/PluggableMap} The map. + * @api + */ + getMap() { + return this.map_; + } + + /** + * @param {number} lon Longitude. + * @param {number} minLat Minimal latitude. + * @param {number} maxLat Maximal latitude. + * @param {number} squaredTolerance Squared tolerance. + * @return {module:ol/geom/LineString} The meridian line string. + * @param {number} index Index. + * @private + */ + getMeridian_(lon, minLat, maxLat, squaredTolerance, index) { + const flatCoordinates = meridian(lon, minLat, maxLat, this.projection_, squaredTolerance); + let lineString = this.meridians_[index]; + if (!lineString) { + lineString = this.meridians_[index] = new LineString(flatCoordinates, GeometryLayout.XY); + } else { + lineString.setFlatCoordinates(GeometryLayout.XY, flatCoordinates); + lineString.changed(); + } + return lineString; + } + + /** + * Get the list of meridians. Meridians are lines of equal longitude. + * @return {Array.} The meridians. + * @api + */ + getMeridians() { + return this.meridians_; + } + + /** + * @param {number} lat Latitude. + * @param {number} minLon Minimal longitude. + * @param {number} maxLon Maximal longitude. + * @param {number} squaredTolerance Squared tolerance. + * @return {module:ol/geom/LineString} The parallel line string. + * @param {number} index Index. + * @private + */ + getParallel_(lat, minLon, maxLon, squaredTolerance, index) { + const flatCoordinates = parallel(lat, minLon, maxLon, this.projection_, squaredTolerance); + let lineString = this.parallels_[index]; + if (!lineString) { + lineString = new LineString(flatCoordinates, GeometryLayout.XY); + } else { + lineString.setFlatCoordinates(GeometryLayout.XY, flatCoordinates); + lineString.changed(); + } + return lineString; + } + + /** + * Get the list of parallels. Parallels are lines of equal latitude. + * @return {Array.} The parallels. + * @api + */ + getParallels() { + return this.parallels_; + } + + /** + * @param {module:ol/render/Event} e Event. + * @private + */ + handlePostCompose_(e) { + const vectorContext = e.vectorContext; + const frameState = e.frameState; + const extent = frameState.extent; + const viewState = frameState.viewState; + const center = viewState.center; + const projection = viewState.projection; + const resolution = viewState.resolution; + const pixelRatio = frameState.pixelRatio; + const squaredTolerance = + resolution * resolution / (4 * pixelRatio * pixelRatio); + + const updateProjectionInfo = !this.projection_ || + !equivalentProjection(this.projection_, projection); + + if (updateProjectionInfo) { + this.updateProjectionInfo_(projection); + } + + this.createGraticule_(extent, center, resolution, squaredTolerance); + + // Draw the lines + vectorContext.setFillStrokeStyle(null, this.strokeStyle_); + let i, l, line; + for (i = 0, l = this.meridians_.length; i < l; ++i) { + line = this.meridians_[i]; + vectorContext.drawGeometry(line); + } + for (i = 0, l = this.parallels_.length; i < l; ++i) { + line = this.parallels_[i]; + vectorContext.drawGeometry(line); + } + let labelData; + if (this.meridiansLabels_) { + for (i = 0, l = this.meridiansLabels_.length; i < l; ++i) { + labelData = this.meridiansLabels_[i]; + this.lonLabelStyle_.setText(labelData.text); + vectorContext.setTextStyle(this.lonLabelStyle_); + vectorContext.drawGeometry(labelData.geom); + } + } + if (this.parallelsLabels_) { + for (i = 0, l = this.parallelsLabels_.length; i < l; ++i) { + labelData = this.parallelsLabels_[i]; + this.latLabelStyle_.setText(labelData.text); + vectorContext.setTextStyle(this.latLabelStyle_); + vectorContext.drawGeometry(labelData.geom); + } + } + } + + /** + * @param {module:ol/proj/Projection} projection Projection. + * @private + */ + updateProjectionInfo_(projection) { + const epsg4326Projection = getProjection('EPSG:4326'); + + const worldExtent = projection.getWorldExtent(); + const worldExtentP = transformExtent(worldExtent, epsg4326Projection, projection); + + this.maxLat_ = worldExtent[3]; + this.maxLon_ = worldExtent[2]; + this.minLat_ = worldExtent[1]; + this.minLon_ = worldExtent[0]; + + this.maxLatP_ = worldExtentP[3]; + this.maxLonP_ = worldExtentP[2]; + this.minLatP_ = worldExtentP[1]; + this.minLonP_ = worldExtentP[0]; + + this.fromLonLatTransform_ = getTransform(epsg4326Projection, projection); + + this.toLonLatTransform_ = getTransform(projection, epsg4326Projection); + + this.projectionCenterLonLat_ = this.toLonLatTransform_(getCenter(projection.getExtent())); + + this.projection_ = projection; + } + + /** + * Set the map for this graticule. The graticule will be rendered on the + * provided map. + * @param {module:ol/PluggableMap} map Map. + * @api + */ + setMap(map) { + if (this.map_) { + unlistenByKey(this.postcomposeListenerKey_); + this.postcomposeListenerKey_ = null; + this.map_.render(); + } + if (map) { + this.postcomposeListenerKey_ = listen(map, RenderEventType.POSTCOMPOSE, this.handlePostCompose_, this); + map.render(); + } + this.map_ = map; + } +} + export default Graticule; diff --git a/src/ol/Image.js b/src/ol/Image.js index ac9f4e8f82..30c48ca8bc 100644 --- a/src/ol/Image.js +++ b/src/ol/Image.js @@ -38,122 +38,119 @@ import {getHeight} from './extent.js'; * @param {?string} crossOrigin Cross origin. * @param {module:ol/Image~LoadFunction} imageLoadFunction Image load function. */ -const ImageWrapper = function(extent, resolution, pixelRatio, src, crossOrigin, imageLoadFunction) { +class ImageWrapper { + constructor(extent, resolution, pixelRatio, src, crossOrigin, imageLoadFunction) { - ImageBase.call(this, extent, resolution, pixelRatio, ImageState.IDLE); + ImageBase.call(this, extent, resolution, pixelRatio, ImageState.IDLE); - /** - * @private - * @type {string} - */ - this.src_ = src; + /** + * @private + * @type {string} + */ + this.src_ = src; - /** - * @private - * @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} - */ - this.image_ = new Image(); - if (crossOrigin !== null) { - this.image_.crossOrigin = crossOrigin; - } + /** + * @private + * @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} + */ + this.image_ = new Image(); + if (crossOrigin !== null) { + this.image_.crossOrigin = crossOrigin; + } - /** - * @private - * @type {Array.} - */ - this.imageListenerKeys_ = null; + /** + * @private + * @type {Array.} + */ + this.imageListenerKeys_ = null; - /** - * @protected - * @type {module:ol/ImageState} - */ - this.state = ImageState.IDLE; + /** + * @protected + * @type {module:ol/ImageState} + */ + this.state = ImageState.IDLE; - /** - * @private - * @type {module:ol/Image~LoadFunction} - */ - this.imageLoadFunction_ = imageLoadFunction; + /** + * @private + * @type {module:ol/Image~LoadFunction} + */ + this.imageLoadFunction_ = imageLoadFunction; -}; + } + + /** + * @inheritDoc + * @api + */ + getImage() { + return this.image_; + } + + /** + * Tracks loading or read errors. + * + * @private + */ + handleImageError_() { + this.state = ImageState.ERROR; + this.unlistenImage_(); + this.changed(); + } + + /** + * Tracks successful image load. + * + * @private + */ + handleImageLoad_() { + if (this.resolution === undefined) { + this.resolution = getHeight(this.extent) / this.image_.height; + } + this.state = ImageState.LOADED; + this.unlistenImage_(); + this.changed(); + } + + /** + * Load the image or retry if loading previously failed. + * Loading is taken care of by the tile queue, and calling this method is + * only needed for preloading or for reloading in case of an error. + * @override + * @api + */ + load() { + if (this.state == ImageState.IDLE || this.state == ImageState.ERROR) { + this.state = ImageState.LOADING; + this.changed(); + this.imageListenerKeys_ = [ + listenOnce(this.image_, EventType.ERROR, + this.handleImageError_, this), + listenOnce(this.image_, EventType.LOAD, + this.handleImageLoad_, this) + ]; + this.imageLoadFunction_(this, this.src_); + } + } + + /** + * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image. + */ + setImage(image) { + this.image_ = image; + } + + /** + * Discards event handlers which listen for load completion or errors. + * + * @private + */ + unlistenImage_() { + this.imageListenerKeys_.forEach(unlistenByKey); + this.imageListenerKeys_ = null; + } +} inherits(ImageWrapper, ImageBase); -/** - * @inheritDoc - * @api - */ -ImageWrapper.prototype.getImage = function() { - return this.image_; -}; - - -/** - * Tracks loading or read errors. - * - * @private - */ -ImageWrapper.prototype.handleImageError_ = function() { - this.state = ImageState.ERROR; - this.unlistenImage_(); - this.changed(); -}; - - -/** - * Tracks successful image load. - * - * @private - */ -ImageWrapper.prototype.handleImageLoad_ = function() { - if (this.resolution === undefined) { - this.resolution = getHeight(this.extent) / this.image_.height; - } - this.state = ImageState.LOADED; - this.unlistenImage_(); - this.changed(); -}; - - -/** - * Load the image or retry if loading previously failed. - * Loading is taken care of by the tile queue, and calling this method is - * only needed for preloading or for reloading in case of an error. - * @override - * @api - */ -ImageWrapper.prototype.load = function() { - if (this.state == ImageState.IDLE || this.state == ImageState.ERROR) { - this.state = ImageState.LOADING; - this.changed(); - this.imageListenerKeys_ = [ - listenOnce(this.image_, EventType.ERROR, - this.handleImageError_, this), - listenOnce(this.image_, EventType.LOAD, - this.handleImageLoad_, this) - ]; - this.imageLoadFunction_(this, this.src_); - } -}; - - -/** - * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image. - */ -ImageWrapper.prototype.setImage = function(image) { - this.image_ = image; -}; - - -/** - * Discards event handlers which listen for load completion or errors. - * - * @private - */ -ImageWrapper.prototype.unlistenImage_ = function() { - this.imageListenerKeys_.forEach(unlistenByKey); - this.imageListenerKeys_ = null; -}; - export default ImageWrapper; diff --git a/src/ol/ImageBase.js b/src/ol/ImageBase.js index 1f557906cf..bcc4a7adcc 100644 --- a/src/ol/ImageBase.js +++ b/src/ol/ImageBase.js @@ -14,90 +14,86 @@ import EventType from './events/EventType.js'; * @param {number} pixelRatio Pixel ratio. * @param {module:ol/ImageState} state State. */ -const ImageBase = function(extent, resolution, pixelRatio, state) { +class ImageBase { + constructor(extent, resolution, pixelRatio, state) { - EventTarget.call(this); + EventTarget.call(this); - /** - * @protected - * @type {module:ol/extent~Extent} - */ - this.extent = extent; + /** + * @protected + * @type {module:ol/extent~Extent} + */ + this.extent = extent; - /** - * @private - * @type {number} - */ - this.pixelRatio_ = pixelRatio; + /** + * @private + * @type {number} + */ + this.pixelRatio_ = pixelRatio; - /** - * @protected - * @type {number|undefined} - */ - this.resolution = resolution; + /** + * @protected + * @type {number|undefined} + */ + this.resolution = resolution; - /** - * @protected - * @type {module:ol/ImageState} - */ - this.state = state; + /** + * @protected + * @type {module:ol/ImageState} + */ + this.state = state; -}; + } + + /** + * @protected + */ + changed() { + this.dispatchEvent(EventType.CHANGE); + } + + /** + * @return {module:ol/extent~Extent} Extent. + */ + getExtent() { + return this.extent; + } + + /** + * @abstract + * @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image. + */ + getImage() {} + + /** + * @return {number} PixelRatio. + */ + getPixelRatio() { + return this.pixelRatio_; + } + + /** + * @return {number} Resolution. + */ + getResolution() { + return /** @type {number} */ (this.resolution); + } + + /** + * @return {module:ol/ImageState} State. + */ + getState() { + return this.state; + } + + /** + * Load not yet loaded URI. + * @abstract + */ + load() {} +} inherits(ImageBase, EventTarget); -/** - * @protected - */ -ImageBase.prototype.changed = function() { - this.dispatchEvent(EventType.CHANGE); -}; - - -/** - * @return {module:ol/extent~Extent} Extent. - */ -ImageBase.prototype.getExtent = function() { - return this.extent; -}; - - -/** - * @abstract - * @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image. - */ -ImageBase.prototype.getImage = function() {}; - - -/** - * @return {number} PixelRatio. - */ -ImageBase.prototype.getPixelRatio = function() { - return this.pixelRatio_; -}; - - -/** - * @return {number} Resolution. - */ -ImageBase.prototype.getResolution = function() { - return /** @type {number} */ (this.resolution); -}; - - -/** - * @return {module:ol/ImageState} State. - */ -ImageBase.prototype.getState = function() { - return this.state; -}; - - -/** - * Load not yet loaded URI. - * @abstract - */ -ImageBase.prototype.load = function() {}; - export default ImageBase; diff --git a/src/ol/ImageCanvas.js b/src/ol/ImageCanvas.js index 524d1fa754..989503d928 100644 --- a/src/ol/ImageCanvas.js +++ b/src/ol/ImageCanvas.js @@ -26,77 +26,77 @@ import ImageState from './ImageState.js'; * @param {module:ol/ImageCanvas~Loader=} opt_loader Optional loader function to * support asynchronous canvas drawing. */ -const ImageCanvas = function(extent, resolution, pixelRatio, canvas, opt_loader) { +class ImageCanvas { + constructor(extent, resolution, pixelRatio, canvas, opt_loader) { - /** - * Optional canvas loader function. - * @type {?module:ol/ImageCanvas~Loader} - * @private - */ - this.loader_ = opt_loader !== undefined ? opt_loader : null; + /** + * Optional canvas loader function. + * @type {?module:ol/ImageCanvas~Loader} + * @private + */ + this.loader_ = opt_loader !== undefined ? opt_loader : null; - const state = opt_loader !== undefined ? ImageState.IDLE : ImageState.LOADED; + const state = opt_loader !== undefined ? ImageState.IDLE : ImageState.LOADED; - ImageBase.call(this, extent, resolution, pixelRatio, state); + ImageBase.call(this, extent, resolution, pixelRatio, state); - /** - * @private - * @type {HTMLCanvasElement} - */ - this.canvas_ = canvas; + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = canvas; - /** - * @private - * @type {Error} - */ - this.error_ = null; + /** + * @private + * @type {Error} + */ + this.error_ = null; -}; + } + + /** + * Get any error associated with asynchronous rendering. + * @return {Error} Any error that occurred during rendering. + */ + getError() { + return this.error_; + } + + /** + * Handle async drawing complete. + * @param {Error} err Any error during drawing. + * @private + */ + handleLoad_(err) { + if (err) { + this.error_ = err; + this.state = ImageState.ERROR; + } else { + this.state = ImageState.LOADED; + } + this.changed(); + } + + /** + * @inheritDoc + */ + load() { + if (this.state == ImageState.IDLE) { + this.state = ImageState.LOADING; + this.changed(); + this.loader_(this.handleLoad_.bind(this)); + } + } + + /** + * @return {HTMLCanvasElement} Canvas element. + */ + getImage() { + return this.canvas_; + } +} inherits(ImageCanvas, ImageBase); -/** - * Get any error associated with asynchronous rendering. - * @return {Error} Any error that occurred during rendering. - */ -ImageCanvas.prototype.getError = function() { - return this.error_; -}; - - -/** - * Handle async drawing complete. - * @param {Error} err Any error during drawing. - * @private - */ -ImageCanvas.prototype.handleLoad_ = function(err) { - if (err) { - this.error_ = err; - this.state = ImageState.ERROR; - } else { - this.state = ImageState.LOADED; - } - this.changed(); -}; - - -/** - * @inheritDoc - */ -ImageCanvas.prototype.load = function() { - if (this.state == ImageState.IDLE) { - this.state = ImageState.LOADING; - this.changed(); - this.loader_(this.handleLoad_.bind(this)); - } -}; - - -/** - * @return {HTMLCanvasElement} Canvas element. - */ -ImageCanvas.prototype.getImage = function() { - return this.canvas_; -}; export default ImageCanvas; diff --git a/src/ol/ImageTile.js b/src/ol/ImageTile.js index 2e9748c87d..7ac535885a 100644 --- a/src/ol/ImageTile.js +++ b/src/ol/ImageTile.js @@ -24,149 +24,144 @@ import EventType from './events/EventType.js'; * @param {module:ol/Tile~LoadFunction} tileLoadFunction Tile load function. * @param {module:ol/Tile~Options=} opt_options Tile options. */ -const ImageTile = function(tileCoord, state, src, crossOrigin, tileLoadFunction, opt_options) { +class ImageTile { + constructor(tileCoord, state, src, crossOrigin, tileLoadFunction, opt_options) { - Tile.call(this, tileCoord, state, opt_options); + Tile.call(this, tileCoord, state, opt_options); + + /** + * @private + * @type {?string} + */ + this.crossOrigin_ = crossOrigin; + + /** + * Image URI + * + * @private + * @type {string} + */ + this.src_ = src; + + /** + * @private + * @type {HTMLImageElement|HTMLCanvasElement} + */ + this.image_ = new Image(); + if (crossOrigin !== null) { + this.image_.crossOrigin = crossOrigin; + } + + /** + * @private + * @type {Array.} + */ + this.imageListenerKeys_ = null; + + /** + * @private + * @type {module:ol/Tile~LoadFunction} + */ + this.tileLoadFunction_ = tileLoadFunction; + + } /** - * @private - * @type {?string} + * @inheritDoc */ - this.crossOrigin_ = crossOrigin; + disposeInternal() { + if (this.state == TileState.LOADING) { + this.unlistenImage_(); + this.image_ = getBlankImage(); + } + if (this.interimTile) { + this.interimTile.dispose(); + } + this.state = TileState.ABORT; + this.changed(); + Tile.prototype.disposeInternal.call(this); + } /** - * Image URI + * Get the HTML image element for this tile (may be a Canvas, Image, or Video). + * @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image. + * @api + */ + getImage() { + return this.image_; + } + + /** + * @inheritDoc + */ + getKey() { + return this.src_; + } + + /** + * Tracks loading or read errors. * * @private - * @type {string} */ - this.src_ = src; - - /** - * @private - * @type {HTMLImageElement|HTMLCanvasElement} - */ - this.image_ = new Image(); - if (crossOrigin !== null) { - this.image_.crossOrigin = crossOrigin; - } - - /** - * @private - * @type {Array.} - */ - this.imageListenerKeys_ = null; - - /** - * @private - * @type {module:ol/Tile~LoadFunction} - */ - this.tileLoadFunction_ = tileLoadFunction; - -}; - -inherits(ImageTile, Tile); - - -/** - * @inheritDoc - */ -ImageTile.prototype.disposeInternal = function() { - if (this.state == TileState.LOADING) { + handleImageError_() { + this.state = TileState.ERROR; this.unlistenImage_(); this.image_ = getBlankImage(); + this.changed(); } - if (this.interimTile) { - this.interimTile.dispose(); + + /** + * Tracks successful image load. + * + * @private + */ + handleImageLoad_() { + if (this.image_.naturalWidth && this.image_.naturalHeight) { + this.state = TileState.LOADED; + } else { + this.state = TileState.EMPTY; + } + this.unlistenImage_(); + this.changed(); } - this.state = TileState.ABORT; - this.changed(); - Tile.prototype.disposeInternal.call(this); -}; - -/** - * Get the HTML image element for this tile (may be a Canvas, Image, or Video). - * @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image. - * @api - */ -ImageTile.prototype.getImage = function() { - return this.image_; -}; - - -/** - * @inheritDoc - */ -ImageTile.prototype.getKey = function() { - return this.src_; -}; - - -/** - * Tracks loading or read errors. - * - * @private - */ -ImageTile.prototype.handleImageError_ = function() { - this.state = TileState.ERROR; - this.unlistenImage_(); - this.image_ = getBlankImage(); - this.changed(); -}; - - -/** - * Tracks successful image load. - * - * @private - */ -ImageTile.prototype.handleImageLoad_ = function() { - if (this.image_.naturalWidth && this.image_.naturalHeight) { - this.state = TileState.LOADED; - } else { - this.state = TileState.EMPTY; - } - this.unlistenImage_(); - this.changed(); -}; - - -/** - * @inheritDoc - * @api - */ -ImageTile.prototype.load = function() { - if (this.state == TileState.ERROR) { - this.state = TileState.IDLE; - this.image_ = new Image(); - if (this.crossOrigin_ !== null) { - this.image_.crossOrigin = this.crossOrigin_; + /** + * @inheritDoc + * @api + */ + load() { + if (this.state == TileState.ERROR) { + this.state = TileState.IDLE; + this.image_ = new Image(); + if (this.crossOrigin_ !== null) { + this.image_.crossOrigin = this.crossOrigin_; + } + } + if (this.state == TileState.IDLE) { + this.state = TileState.LOADING; + this.changed(); + this.imageListenerKeys_ = [ + listenOnce(this.image_, EventType.ERROR, + this.handleImageError_, this), + listenOnce(this.image_, EventType.LOAD, + this.handleImageLoad_, this) + ]; + this.tileLoadFunction_(this, this.src_); } } - if (this.state == TileState.IDLE) { - this.state = TileState.LOADING; - this.changed(); - this.imageListenerKeys_ = [ - listenOnce(this.image_, EventType.ERROR, - this.handleImageError_, this), - listenOnce(this.image_, EventType.LOAD, - this.handleImageLoad_, this) - ]; - this.tileLoadFunction_(this, this.src_); + + /** + * Discards event handlers which listen for load completion or errors. + * + * @private + */ + unlistenImage_() { + this.imageListenerKeys_.forEach(unlistenByKey); + this.imageListenerKeys_ = null; } -}; +} - -/** - * Discards event handlers which listen for load completion or errors. - * - * @private - */ -ImageTile.prototype.unlistenImage_ = function() { - this.imageListenerKeys_.forEach(unlistenByKey); - this.imageListenerKeys_ = null; -}; +inherits(ImageTile, Tile); /** diff --git a/src/ol/Kinetic.js b/src/ol/Kinetic.js index 8449e1b690..4b6a5e470c 100644 --- a/src/ol/Kinetic.js +++ b/src/ol/Kinetic.js @@ -14,116 +14,114 @@ * @struct * @api */ -const Kinetic = function(decay, minVelocity, delay) { +class Kinetic { + constructor(decay, minVelocity, delay) { - /** - * @private - * @type {number} - */ - this.decay_ = decay; + /** + * @private + * @type {number} + */ + this.decay_ = decay; - /** - * @private - * @type {number} - */ - this.minVelocity_ = minVelocity; + /** + * @private + * @type {number} + */ + this.minVelocity_ = minVelocity; - /** - * @private - * @type {number} - */ - this.delay_ = delay; + /** + * @private + * @type {number} + */ + this.delay_ = delay; - /** - * @private - * @type {Array.} - */ - this.points_ = []; + /** + * @private + * @type {Array.} + */ + this.points_ = []; - /** - * @private - * @type {number} - */ - this.angle_ = 0; + /** + * @private + * @type {number} + */ + this.angle_ = 0; - /** - * @private - * @type {number} - */ - this.initialVelocity_ = 0; -}; + /** + * @private + * @type {number} + */ + this.initialVelocity_ = 0; + } + /** + * FIXME empty description for jsdoc + */ + begin() { + this.points_.length = 0; + this.angle_ = 0; + this.initialVelocity_ = 0; + } -/** - * FIXME empty description for jsdoc - */ -Kinetic.prototype.begin = function() { - this.points_.length = 0; - this.angle_ = 0; - this.initialVelocity_ = 0; -}; + /** + * @param {number} x X. + * @param {number} y Y. + */ + update(x, y) { + this.points_.push(x, y, Date.now()); + } + /** + * @return {boolean} Whether we should do kinetic animation. + */ + end() { + if (this.points_.length < 6) { + // at least 2 points are required (i.e. there must be at least 6 elements + // in the array) + return false; + } + const delay = Date.now() - this.delay_; + const lastIndex = this.points_.length - 3; + if (this.points_[lastIndex + 2] < delay) { + // the last tracked point is too old, which means that the user stopped + // panning before releasing the map + return false; + } -/** - * @param {number} x X. - * @param {number} y Y. - */ -Kinetic.prototype.update = function(x, y) { - this.points_.push(x, y, Date.now()); -}; + // get the first point which still falls into the delay time + let firstIndex = lastIndex - 3; + while (firstIndex > 0 && this.points_[firstIndex + 2] > delay) { + firstIndex -= 3; + } + const duration = this.points_[lastIndex + 2] - this.points_[firstIndex + 2]; + // we don't want a duration of 0 (divide by zero) + // we also make sure the user panned for a duration of at least one frame + // (1/60s) to compute sane displacement values + if (duration < 1000 / 60) { + return false; + } -/** - * @return {boolean} Whether we should do kinetic animation. - */ -Kinetic.prototype.end = function() { - if (this.points_.length < 6) { - // at least 2 points are required (i.e. there must be at least 6 elements - // in the array) - return false; - } - const delay = Date.now() - this.delay_; - const lastIndex = this.points_.length - 3; - if (this.points_[lastIndex + 2] < delay) { - // the last tracked point is too old, which means that the user stopped - // panning before releasing the map - return false; - } + const dx = this.points_[lastIndex] - this.points_[firstIndex]; + const dy = this.points_[lastIndex + 1] - this.points_[firstIndex + 1]; + this.angle_ = Math.atan2(dy, dx); + this.initialVelocity_ = Math.sqrt(dx * dx + dy * dy) / duration; + return this.initialVelocity_ > this.minVelocity_; + } - // get the first point which still falls into the delay time - let firstIndex = lastIndex - 3; - while (firstIndex > 0 && this.points_[firstIndex + 2] > delay) { - firstIndex -= 3; - } + /** + * @return {number} Total distance travelled (pixels). + */ + getDistance() { + return (this.minVelocity_ - this.initialVelocity_) / this.decay_; + } - const duration = this.points_[lastIndex + 2] - this.points_[firstIndex + 2]; - // we don't want a duration of 0 (divide by zero) - // we also make sure the user panned for a duration of at least one frame - // (1/60s) to compute sane displacement values - if (duration < 1000 / 60) { - return false; - } + /** + * @return {number} Angle of the kinetic panning animation (radians). + */ + getAngle() { + return this.angle_; + } +} - const dx = this.points_[lastIndex] - this.points_[firstIndex]; - const dy = this.points_[lastIndex + 1] - this.points_[firstIndex + 1]; - this.angle_ = Math.atan2(dy, dx); - this.initialVelocity_ = Math.sqrt(dx * dx + dy * dy) / duration; - return this.initialVelocity_ > this.minVelocity_; -}; - - -/** - * @return {number} Total distance travelled (pixels). - */ -Kinetic.prototype.getDistance = function() { - return (this.minVelocity_ - this.initialVelocity_) / this.decay_; -}; - - -/** - * @return {number} Angle of the kinetic panning animation (radians). - */ -Kinetic.prototype.getAngle = function() { - return this.angle_; -}; export default Kinetic; diff --git a/src/ol/Map.js b/src/ol/Map.js index 3fb24d0871..23bb531540 100644 --- a/src/ol/Map.js +++ b/src/ol/Map.js @@ -66,29 +66,31 @@ import CanvasVectorTileLayerRenderer from './renderer/canvas/VectorTileLayer.js' * @fires module:ol/render/Event~RenderEvent#precompose * @api */ -const Map = function(options) { - options = assign({}, options); - if (!options.controls) { - options.controls = defaultControls(); - } - if (!options.interactions) { - options.interactions = defaultInteractions(); +class Map { + constructor(options) { + options = assign({}, options); + if (!options.controls) { + options.controls = defaultControls(); + } + if (!options.interactions) { + options.interactions = defaultInteractions(); + } + + PluggableMap.call(this, options); } - PluggableMap.call(this, options); -}; + createRenderer() { + const renderer = new CanvasMapRenderer(this); + renderer.registerLayerRenderers([ + CanvasImageLayerRenderer, + CanvasTileLayerRenderer, + CanvasVectorLayerRenderer, + CanvasVectorTileLayerRenderer + ]); + return renderer; + } +} inherits(Map, PluggableMap); -Map.prototype.createRenderer = function() { - const renderer = new CanvasMapRenderer(this); - renderer.registerLayerRenderers([ - CanvasImageLayerRenderer, - CanvasTileLayerRenderer, - CanvasVectorLayerRenderer, - CanvasVectorTileLayerRenderer - ]); - return renderer; -}; - export default Map; diff --git a/src/ol/MapBrowserEvent.js b/src/ol/MapBrowserEvent.js index f326ceec64..3043c4e7d5 100644 --- a/src/ol/MapBrowserEvent.js +++ b/src/ol/MapBrowserEvent.js @@ -17,66 +17,68 @@ import MapEvent from './MapEvent.js'; * @param {boolean=} opt_dragging Is the map currently being dragged? * @param {?module:ol/PluggableMap~FrameState=} opt_frameState Frame state. */ -const MapBrowserEvent = function(type, map, browserEvent, opt_dragging, opt_frameState) { +class MapBrowserEvent { + constructor(type, map, browserEvent, opt_dragging, opt_frameState) { - MapEvent.call(this, type, map, opt_frameState); + MapEvent.call(this, type, map, opt_frameState); - /** - * The original browser event. - * @const - * @type {Event} - * @api - */ - this.originalEvent = browserEvent; + /** + * The original browser event. + * @const + * @type {Event} + * @api + */ + this.originalEvent = browserEvent; - /** - * The map pixel relative to the viewport corresponding to the original browser event. - * @type {module:ol~Pixel} - * @api - */ - this.pixel = map.getEventPixel(browserEvent); + /** + * The map pixel relative to the viewport corresponding to the original browser event. + * @type {module:ol~Pixel} + * @api + */ + this.pixel = map.getEventPixel(browserEvent); - /** - * The coordinate in view projection corresponding to the original browser event. - * @type {module:ol/coordinate~Coordinate} - * @api - */ - this.coordinate = map.getCoordinateFromPixel(this.pixel); + /** + * The coordinate in view projection corresponding to the original browser event. + * @type {module:ol/coordinate~Coordinate} + * @api + */ + this.coordinate = map.getCoordinateFromPixel(this.pixel); - /** - * Indicates if the map is currently being dragged. Only set for - * `POINTERDRAG` and `POINTERMOVE` events. Default is `false`. - * - * @type {boolean} - * @api - */ - this.dragging = opt_dragging !== undefined ? opt_dragging : false; + /** + * Indicates if the map is currently being dragged. Only set for + * `POINTERDRAG` and `POINTERMOVE` events. Default is `false`. + * + * @type {boolean} + * @api + */ + this.dragging = opt_dragging !== undefined ? opt_dragging : false; -}; + } + + /** + * Prevents the default browser action. + * @see https://developer.mozilla.org/en-US/docs/Web/API/event.preventDefault + * @override + * @api + */ + preventDefault() { + MapEvent.prototype.preventDefault.call(this); + this.originalEvent.preventDefault(); + } + + /** + * Prevents further propagation of the current event. + * @see https://developer.mozilla.org/en-US/docs/Web/API/event.stopPropagation + * @override + * @api + */ + stopPropagation() { + MapEvent.prototype.stopPropagation.call(this); + this.originalEvent.stopPropagation(); + } +} inherits(MapBrowserEvent, MapEvent); -/** - * Prevents the default browser action. - * @see https://developer.mozilla.org/en-US/docs/Web/API/event.preventDefault - * @override - * @api - */ -MapBrowserEvent.prototype.preventDefault = function() { - MapEvent.prototype.preventDefault.call(this); - this.originalEvent.preventDefault(); -}; - - -/** - * Prevents further propagation of the current event. - * @see https://developer.mozilla.org/en-US/docs/Web/API/event.stopPropagation - * @override - * @api - */ -MapBrowserEvent.prototype.stopPropagation = function() { - MapEvent.prototype.stopPropagation.call(this); - this.originalEvent.stopPropagation(); -}; export default MapBrowserEvent; diff --git a/src/ol/MapBrowserEventHandler.js b/src/ol/MapBrowserEventHandler.js index 6718d1e556..48dc9109a2 100644 --- a/src/ol/MapBrowserEventHandler.js +++ b/src/ol/MapBrowserEventHandler.js @@ -18,319 +18,314 @@ import PointerEventHandler from './pointer/PointerEventHandler.js'; * @constructor * @extends {module:ol/events/EventTarget} */ -const MapBrowserEventHandler = function(map, moveTolerance) { +class MapBrowserEventHandler { + constructor(map, moveTolerance) { - EventTarget.call(this); + EventTarget.call(this); + + /** + * This is the element that we will listen to the real events on. + * @type {module:ol/PluggableMap} + * @private + */ + this.map_ = map; + + /** + * @type {number} + * @private + */ + this.clickTimeoutId_ = 0; + + /** + * @type {boolean} + * @private + */ + this.dragging_ = false; + + /** + * @type {!Array.} + * @private + */ + this.dragListenerKeys_ = []; + + /** + * @type {number} + * @private + */ + this.moveTolerance_ = moveTolerance ? + moveTolerance * DEVICE_PIXEL_RATIO : DEVICE_PIXEL_RATIO; + + /** + * The most recent "down" type event (or null if none have occurred). + * Set on pointerdown. + * @type {module:ol/pointer/PointerEvent} + * @private + */ + this.down_ = null; + + const element = this.map_.getViewport(); + + /** + * @type {number} + * @private + */ + this.activePointers_ = 0; + + /** + * @type {!Object.} + * @private + */ + this.trackedTouches_ = {}; + + /** + * Event handler which generates pointer events for + * the viewport element. + * + * @type {module:ol/pointer/PointerEventHandler} + * @private + */ + this.pointerEventHandler_ = new PointerEventHandler(element); + + /** + * Event handler which generates pointer events for + * the document (used when dragging). + * + * @type {module:ol/pointer/PointerEventHandler} + * @private + */ + this.documentPointerEventHandler_ = null; + + /** + * @type {?module:ol/events~EventsKey} + * @private + */ + this.pointerdownListenerKey_ = listen(this.pointerEventHandler_, + PointerEventType.POINTERDOWN, + this.handlePointerDown_, this); + + /** + * @type {?module:ol/events~EventsKey} + * @private + */ + this.relayedListenerKey_ = listen(this.pointerEventHandler_, + PointerEventType.POINTERMOVE, + this.relayEvent_, this); + + } /** - * This is the element that we will listen to the real events on. - * @type {module:ol/PluggableMap} + * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer + * event. * @private */ - this.map_ = map; + emulateClick_(pointerEvent) { + let newEvent = new MapBrowserPointerEvent( + MapBrowserEventType.CLICK, this.map_, pointerEvent); + this.dispatchEvent(newEvent); + if (this.clickTimeoutId_ !== 0) { + // double-click + clearTimeout(this.clickTimeoutId_); + this.clickTimeoutId_ = 0; + newEvent = new MapBrowserPointerEvent( + MapBrowserEventType.DBLCLICK, this.map_, pointerEvent); + this.dispatchEvent(newEvent); + } else { + // click + this.clickTimeoutId_ = setTimeout(function() { + this.clickTimeoutId_ = 0; + const newEvent = new MapBrowserPointerEvent( + MapBrowserEventType.SINGLECLICK, this.map_, pointerEvent); + this.dispatchEvent(newEvent); + }.bind(this), 250); + } + } /** - * @type {number} - * @private - */ - this.clickTimeoutId_ = 0; - - /** - * @type {boolean} - * @private - */ - this.dragging_ = false; - - /** - * @type {!Array.} - * @private - */ - this.dragListenerKeys_ = []; - - /** - * @type {number} - * @private - */ - this.moveTolerance_ = moveTolerance ? - moveTolerance * DEVICE_PIXEL_RATIO : DEVICE_PIXEL_RATIO; - - /** - * The most recent "down" type event (or null if none have occurred). - * Set on pointerdown. - * @type {module:ol/pointer/PointerEvent} - * @private - */ - this.down_ = null; - - const element = this.map_.getViewport(); - - /** - * @type {number} - * @private - */ - this.activePointers_ = 0; - - /** - * @type {!Object.} - * @private - */ - this.trackedTouches_ = {}; - - /** - * Event handler which generates pointer events for - * the viewport element. + * Keeps track on how many pointers are currently active. * - * @type {module:ol/pointer/PointerEventHandler} + * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer + * event. * @private */ - this.pointerEventHandler_ = new PointerEventHandler(element); + updateActivePointers_(pointerEvent) { + const event = pointerEvent; + + if (event.type == MapBrowserEventType.POINTERUP || + event.type == MapBrowserEventType.POINTERCANCEL) { + delete this.trackedTouches_[event.pointerId]; + } else if (event.type == MapBrowserEventType.POINTERDOWN) { + this.trackedTouches_[event.pointerId] = true; + } + this.activePointers_ = Object.keys(this.trackedTouches_).length; + } /** - * Event handler which generates pointer events for - * the document (used when dragging). - * - * @type {module:ol/pointer/PointerEventHandler} + * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer + * event. * @private */ - this.documentPointerEventHandler_ = null; + handlePointerUp_(pointerEvent) { + this.updateActivePointers_(pointerEvent); + const newEvent = new MapBrowserPointerEvent( + MapBrowserEventType.POINTERUP, this.map_, pointerEvent); + this.dispatchEvent(newEvent); + + // We emulate click events on left mouse button click, touch contact, and pen + // contact. isMouseActionButton returns true in these cases (evt.button is set + // to 0). + // See http://www.w3.org/TR/pointerevents/#button-states + // We only fire click, singleclick, and doubleclick if nobody has called + // event.stopPropagation() or event.preventDefault(). + if (!newEvent.propagationStopped && !this.dragging_ && this.isMouseActionButton_(pointerEvent)) { + this.emulateClick_(this.down_); + } + + if (this.activePointers_ === 0) { + this.dragListenerKeys_.forEach(unlistenByKey); + this.dragListenerKeys_.length = 0; + this.dragging_ = false; + this.down_ = null; + this.documentPointerEventHandler_.dispose(); + this.documentPointerEventHandler_ = null; + } + } /** - * @type {?module:ol/events~EventsKey} + * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer + * event. + * @return {boolean} If the left mouse button was pressed. * @private */ - this.pointerdownListenerKey_ = listen(this.pointerEventHandler_, - PointerEventType.POINTERDOWN, - this.handlePointerDown_, this); + isMouseActionButton_(pointerEvent) { + return pointerEvent.button === 0; + } /** - * @type {?module:ol/events~EventsKey} + * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer + * event. * @private */ - this.relayedListenerKey_ = listen(this.pointerEventHandler_, - PointerEventType.POINTERMOVE, - this.relayEvent_, this); + handlePointerDown_(pointerEvent) { + this.updateActivePointers_(pointerEvent); + const newEvent = new MapBrowserPointerEvent( + MapBrowserEventType.POINTERDOWN, this.map_, pointerEvent); + this.dispatchEvent(newEvent); -}; + this.down_ = pointerEvent; + + if (this.dragListenerKeys_.length === 0) { + /* Set up a pointer event handler on the `document`, + * which is required when the pointer is moved outside + * the viewport when dragging. + */ + this.documentPointerEventHandler_ = + new PointerEventHandler(document); + + this.dragListenerKeys_.push( + listen(this.documentPointerEventHandler_, + MapBrowserEventType.POINTERMOVE, + this.handlePointerMove_, this), + listen(this.documentPointerEventHandler_, + MapBrowserEventType.POINTERUP, + this.handlePointerUp_, this), + /* Note that the listener for `pointercancel is set up on + * `pointerEventHandler_` and not `documentPointerEventHandler_` like + * the `pointerup` and `pointermove` listeners. + * + * The reason for this is the following: `TouchSource.vacuumTouches_()` + * issues `pointercancel` events, when there was no `touchend` for a + * `touchstart`. Now, let's say a first `touchstart` is registered on + * `pointerEventHandler_`. The `documentPointerEventHandler_` is set up. + * But `documentPointerEventHandler_` doesn't know about the first + * `touchstart`. If there is no `touchend` for the `touchstart`, we can + * only receive a `touchcancel` from `pointerEventHandler_`, because it is + * only registered there. + */ + listen(this.pointerEventHandler_, + MapBrowserEventType.POINTERCANCEL, + this.handlePointerUp_, this) + ); + } + } + + /** + * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer + * event. + * @private + */ + handlePointerMove_(pointerEvent) { + // Between pointerdown and pointerup, pointermove events are triggered. + // To avoid a 'false' touchmove event to be dispatched, we test if the pointer + // moved a significant distance. + if (this.isMoving_(pointerEvent)) { + this.dragging_ = true; + const newEvent = new MapBrowserPointerEvent( + MapBrowserEventType.POINTERDRAG, this.map_, pointerEvent, + this.dragging_); + this.dispatchEvent(newEvent); + } + + // Some native android browser triggers mousemove events during small period + // of time. See: https://code.google.com/p/android/issues/detail?id=5491 or + // https://code.google.com/p/android/issues/detail?id=19827 + // ex: Galaxy Tab P3110 + Android 4.1.1 + pointerEvent.preventDefault(); + } + + /** + * Wrap and relay a pointer event. Note that this requires that the type + * string for the MapBrowserPointerEvent matches the PointerEvent type. + * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer + * event. + * @private + */ + relayEvent_(pointerEvent) { + const dragging = !!(this.down_ && this.isMoving_(pointerEvent)); + this.dispatchEvent(new MapBrowserPointerEvent( + pointerEvent.type, this.map_, pointerEvent, dragging)); + } + + /** + * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer + * event. + * @return {boolean} Is moving. + * @private + */ + isMoving_(pointerEvent) { + return this.dragging_ || + Math.abs(pointerEvent.clientX - this.down_.clientX) > this.moveTolerance_ || + Math.abs(pointerEvent.clientY - this.down_.clientY) > this.moveTolerance_; + } + + /** + * @inheritDoc + */ + disposeInternal() { + if (this.relayedListenerKey_) { + unlistenByKey(this.relayedListenerKey_); + this.relayedListenerKey_ = null; + } + if (this.pointerdownListenerKey_) { + unlistenByKey(this.pointerdownListenerKey_); + this.pointerdownListenerKey_ = null; + } + + this.dragListenerKeys_.forEach(unlistenByKey); + this.dragListenerKeys_.length = 0; + + if (this.documentPointerEventHandler_) { + this.documentPointerEventHandler_.dispose(); + this.documentPointerEventHandler_ = null; + } + if (this.pointerEventHandler_) { + this.pointerEventHandler_.dispose(); + this.pointerEventHandler_ = null; + } + EventTarget.prototype.disposeInternal.call(this); + } +} inherits(MapBrowserEventHandler, EventTarget); -/** - * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer - * event. - * @private - */ -MapBrowserEventHandler.prototype.emulateClick_ = function(pointerEvent) { - let newEvent = new MapBrowserPointerEvent( - MapBrowserEventType.CLICK, this.map_, pointerEvent); - this.dispatchEvent(newEvent); - if (this.clickTimeoutId_ !== 0) { - // double-click - clearTimeout(this.clickTimeoutId_); - this.clickTimeoutId_ = 0; - newEvent = new MapBrowserPointerEvent( - MapBrowserEventType.DBLCLICK, this.map_, pointerEvent); - this.dispatchEvent(newEvent); - } else { - // click - this.clickTimeoutId_ = setTimeout(function() { - this.clickTimeoutId_ = 0; - const newEvent = new MapBrowserPointerEvent( - MapBrowserEventType.SINGLECLICK, this.map_, pointerEvent); - this.dispatchEvent(newEvent); - }.bind(this), 250); - } -}; - - -/** - * Keeps track on how many pointers are currently active. - * - * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer - * event. - * @private - */ -MapBrowserEventHandler.prototype.updateActivePointers_ = function(pointerEvent) { - const event = pointerEvent; - - if (event.type == MapBrowserEventType.POINTERUP || - event.type == MapBrowserEventType.POINTERCANCEL) { - delete this.trackedTouches_[event.pointerId]; - } else if (event.type == MapBrowserEventType.POINTERDOWN) { - this.trackedTouches_[event.pointerId] = true; - } - this.activePointers_ = Object.keys(this.trackedTouches_).length; -}; - - -/** - * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer - * event. - * @private - */ -MapBrowserEventHandler.prototype.handlePointerUp_ = function(pointerEvent) { - this.updateActivePointers_(pointerEvent); - const newEvent = new MapBrowserPointerEvent( - MapBrowserEventType.POINTERUP, this.map_, pointerEvent); - this.dispatchEvent(newEvent); - - // We emulate click events on left mouse button click, touch contact, and pen - // contact. isMouseActionButton returns true in these cases (evt.button is set - // to 0). - // See http://www.w3.org/TR/pointerevents/#button-states - // We only fire click, singleclick, and doubleclick if nobody has called - // event.stopPropagation() or event.preventDefault(). - if (!newEvent.propagationStopped && !this.dragging_ && this.isMouseActionButton_(pointerEvent)) { - this.emulateClick_(this.down_); - } - - if (this.activePointers_ === 0) { - this.dragListenerKeys_.forEach(unlistenByKey); - this.dragListenerKeys_.length = 0; - this.dragging_ = false; - this.down_ = null; - this.documentPointerEventHandler_.dispose(); - this.documentPointerEventHandler_ = null; - } -}; - - -/** - * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer - * event. - * @return {boolean} If the left mouse button was pressed. - * @private - */ -MapBrowserEventHandler.prototype.isMouseActionButton_ = function(pointerEvent) { - return pointerEvent.button === 0; -}; - - -/** - * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer - * event. - * @private - */ -MapBrowserEventHandler.prototype.handlePointerDown_ = function(pointerEvent) { - this.updateActivePointers_(pointerEvent); - const newEvent = new MapBrowserPointerEvent( - MapBrowserEventType.POINTERDOWN, this.map_, pointerEvent); - this.dispatchEvent(newEvent); - - this.down_ = pointerEvent; - - if (this.dragListenerKeys_.length === 0) { - /* Set up a pointer event handler on the `document`, - * which is required when the pointer is moved outside - * the viewport when dragging. - */ - this.documentPointerEventHandler_ = - new PointerEventHandler(document); - - this.dragListenerKeys_.push( - listen(this.documentPointerEventHandler_, - MapBrowserEventType.POINTERMOVE, - this.handlePointerMove_, this), - listen(this.documentPointerEventHandler_, - MapBrowserEventType.POINTERUP, - this.handlePointerUp_, this), - /* Note that the listener for `pointercancel is set up on - * `pointerEventHandler_` and not `documentPointerEventHandler_` like - * the `pointerup` and `pointermove` listeners. - * - * The reason for this is the following: `TouchSource.vacuumTouches_()` - * issues `pointercancel` events, when there was no `touchend` for a - * `touchstart`. Now, let's say a first `touchstart` is registered on - * `pointerEventHandler_`. The `documentPointerEventHandler_` is set up. - * But `documentPointerEventHandler_` doesn't know about the first - * `touchstart`. If there is no `touchend` for the `touchstart`, we can - * only receive a `touchcancel` from `pointerEventHandler_`, because it is - * only registered there. - */ - listen(this.pointerEventHandler_, - MapBrowserEventType.POINTERCANCEL, - this.handlePointerUp_, this) - ); - } -}; - - -/** - * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer - * event. - * @private - */ -MapBrowserEventHandler.prototype.handlePointerMove_ = function(pointerEvent) { - // Between pointerdown and pointerup, pointermove events are triggered. - // To avoid a 'false' touchmove event to be dispatched, we test if the pointer - // moved a significant distance. - if (this.isMoving_(pointerEvent)) { - this.dragging_ = true; - const newEvent = new MapBrowserPointerEvent( - MapBrowserEventType.POINTERDRAG, this.map_, pointerEvent, - this.dragging_); - this.dispatchEvent(newEvent); - } - - // Some native android browser triggers mousemove events during small period - // of time. See: https://code.google.com/p/android/issues/detail?id=5491 or - // https://code.google.com/p/android/issues/detail?id=19827 - // ex: Galaxy Tab P3110 + Android 4.1.1 - pointerEvent.preventDefault(); -}; - - -/** - * Wrap and relay a pointer event. Note that this requires that the type - * string for the MapBrowserPointerEvent matches the PointerEvent type. - * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer - * event. - * @private - */ -MapBrowserEventHandler.prototype.relayEvent_ = function(pointerEvent) { - const dragging = !!(this.down_ && this.isMoving_(pointerEvent)); - this.dispatchEvent(new MapBrowserPointerEvent( - pointerEvent.type, this.map_, pointerEvent, dragging)); -}; - - -/** - * @param {module:ol/pointer/PointerEvent} pointerEvent Pointer - * event. - * @return {boolean} Is moving. - * @private - */ -MapBrowserEventHandler.prototype.isMoving_ = function(pointerEvent) { - return this.dragging_ || - Math.abs(pointerEvent.clientX - this.down_.clientX) > this.moveTolerance_ || - Math.abs(pointerEvent.clientY - this.down_.clientY) > this.moveTolerance_; -}; - - -/** - * @inheritDoc - */ -MapBrowserEventHandler.prototype.disposeInternal = function() { - if (this.relayedListenerKey_) { - unlistenByKey(this.relayedListenerKey_); - this.relayedListenerKey_ = null; - } - if (this.pointerdownListenerKey_) { - unlistenByKey(this.pointerdownListenerKey_); - this.pointerdownListenerKey_ = null; - } - - this.dragListenerKeys_.forEach(unlistenByKey); - this.dragListenerKeys_.length = 0; - - if (this.documentPointerEventHandler_) { - this.documentPointerEventHandler_.dispose(); - this.documentPointerEventHandler_ = null; - } - if (this.pointerEventHandler_) { - this.pointerEventHandler_.dispose(); - this.pointerEventHandler_ = null; - } - EventTarget.prototype.disposeInternal.call(this); -}; export default MapBrowserEventHandler; diff --git a/src/ol/Object.js b/src/ol/Object.js index 8dc4adb4b6..39747bbb3e 100644 --- a/src/ol/Object.js +++ b/src/ol/Object.js @@ -87,25 +87,119 @@ inherits(ObjectEvent, Event); * @fires module:ol/Object~ObjectEvent * @api */ -const BaseObject = function(opt_values) { - Observable.call(this); +class BaseObject { + constructor(opt_values) { + Observable.call(this); - // Call {@link module:ol~getUid} to ensure that the order of objects' ids is - // the same as the order in which they were created. This also helps to - // ensure that object properties are always added in the same order, which - // helps many JavaScript engines generate faster code. - getUid(this); + // Call {@link module:ol~getUid} to ensure that the order of objects' ids is + // the same as the order in which they were created. This also helps to + // ensure that object properties are always added in the same order, which + // helps many JavaScript engines generate faster code. + getUid(this); + + /** + * @private + * @type {!Object.} + */ + this.values_ = {}; + + if (opt_values !== undefined) { + this.setProperties(opt_values); + } + } /** - * @private - * @type {!Object.} + * Gets a value. + * @param {string} key Key name. + * @return {*} Value. + * @api */ - this.values_ = {}; - - if (opt_values !== undefined) { - this.setProperties(opt_values); + get(key) { + let value; + if (this.values_.hasOwnProperty(key)) { + value = this.values_[key]; + } + return value; } -}; + + /** + * Get a list of object property names. + * @return {Array.} List of property names. + * @api + */ + getKeys() { + return Object.keys(this.values_); + } + + /** + * Get an object of all property names and values. + * @return {Object.} Object. + * @api + */ + getProperties() { + return assign({}, this.values_); + } + + /** + * @param {string} key Key name. + * @param {*} oldValue Old value. + */ + notify(key, oldValue) { + let eventType; + eventType = getChangeEventType(key); + this.dispatchEvent(new ObjectEvent(eventType, key, oldValue)); + eventType = ObjectEventType.PROPERTYCHANGE; + this.dispatchEvent(new ObjectEvent(eventType, key, oldValue)); + } + + /** + * Sets a value. + * @param {string} key Key name. + * @param {*} value Value. + * @param {boolean=} opt_silent Update without triggering an event. + * @api + */ + set(key, value, opt_silent) { + if (opt_silent) { + this.values_[key] = value; + } else { + const oldValue = this.values_[key]; + this.values_[key] = value; + if (oldValue !== value) { + this.notify(key, oldValue); + } + } + } + + /** + * Sets a collection of key-value pairs. Note that this changes any existing + * properties and adds new ones (it does not remove any existing properties). + * @param {Object.} values Values. + * @param {boolean=} opt_silent Update without triggering an event. + * @api + */ + setProperties(values, opt_silent) { + for (const key in values) { + this.set(key, values[key], opt_silent); + } + } + + /** + * Unsets a property. + * @param {string} key Key name. + * @param {boolean=} opt_silent Unset without triggering an event. + * @api + */ + unset(key, opt_silent) { + if (key in this.values_) { + const oldValue = this.values_[key]; + delete this.values_[key]; + if (!opt_silent) { + this.notify(key, oldValue); + } + } + } +} inherits(BaseObject, Observable); @@ -127,103 +221,4 @@ export function getChangeEventType(key) { } -/** - * Gets a value. - * @param {string} key Key name. - * @return {*} Value. - * @api - */ -BaseObject.prototype.get = function(key) { - let value; - if (this.values_.hasOwnProperty(key)) { - value = this.values_[key]; - } - return value; -}; - - -/** - * Get a list of object property names. - * @return {Array.} List of property names. - * @api - */ -BaseObject.prototype.getKeys = function() { - return Object.keys(this.values_); -}; - - -/** - * Get an object of all property names and values. - * @return {Object.} Object. - * @api - */ -BaseObject.prototype.getProperties = function() { - return assign({}, this.values_); -}; - - -/** - * @param {string} key Key name. - * @param {*} oldValue Old value. - */ -BaseObject.prototype.notify = function(key, oldValue) { - let eventType; - eventType = getChangeEventType(key); - this.dispatchEvent(new ObjectEvent(eventType, key, oldValue)); - eventType = ObjectEventType.PROPERTYCHANGE; - this.dispatchEvent(new ObjectEvent(eventType, key, oldValue)); -}; - - -/** - * Sets a value. - * @param {string} key Key name. - * @param {*} value Value. - * @param {boolean=} opt_silent Update without triggering an event. - * @api - */ -BaseObject.prototype.set = function(key, value, opt_silent) { - if (opt_silent) { - this.values_[key] = value; - } else { - const oldValue = this.values_[key]; - this.values_[key] = value; - if (oldValue !== value) { - this.notify(key, oldValue); - } - } -}; - - -/** - * Sets a collection of key-value pairs. Note that this changes any existing - * properties and adds new ones (it does not remove any existing properties). - * @param {Object.} values Values. - * @param {boolean=} opt_silent Update without triggering an event. - * @api - */ -BaseObject.prototype.setProperties = function(values, opt_silent) { - for (const key in values) { - this.set(key, values[key], opt_silent); - } -}; - - -/** - * Unsets a property. - * @param {string} key Key name. - * @param {boolean=} opt_silent Unset without triggering an event. - * @api - */ -BaseObject.prototype.unset = function(key, opt_silent) { - if (key in this.values_) { - const oldValue = this.values_[key]; - delete this.values_[key]; - if (!opt_silent) { - this.notify(key, oldValue); - } - } -}; - - export default BaseObject; diff --git a/src/ol/Observable.js b/src/ol/Observable.js index a1052e3025..30edbbbc09 100644 --- a/src/ol/Observable.js +++ b/src/ol/Observable.js @@ -20,17 +20,99 @@ import EventType from './events/EventType.js'; * @struct * @api */ -const Observable = function() { +class Observable { + constructor() { - EventTarget.call(this); + EventTarget.call(this); + + /** + * @private + * @type {number} + */ + this.revision_ = 0; + + } /** - * @private - * @type {number} + * Increases the revision counter and dispatches a 'change' event. + * @api */ - this.revision_ = 0; + changed() { + ++this.revision_; + this.dispatchEvent(EventType.CHANGE); + } -}; + /** + * Get the version number for this object. Each time the object is modified, + * its version number will be incremented. + * @return {number} Revision. + * @api + */ + getRevision() { + return this.revision_; + } + + /** + * Listen for a certain type of event. + * @param {string|Array.} type The event type or array of event types. + * @param {function(?): ?} listener The listener function. + * @return {module:ol/events~EventsKey|Array.} Unique key for the listener. If + * called with an array of event types as the first argument, the return + * will be an array of keys. + * @api + */ + on(type, listener) { + if (Array.isArray(type)) { + const len = type.length; + const keys = new Array(len); + for (let i = 0; i < len; ++i) { + keys[i] = listen(this, type[i], listener); + } + return keys; + } else { + return listen(this, /** @type {string} */ (type), listener); + } + } + + /** + * Listen once for a certain type of event. + * @param {string|Array.} type The event type or array of event types. + * @param {function(?): ?} listener The listener function. + * @return {module:ol/events~EventsKey|Array.} Unique key for the listener. If + * called with an array of event types as the first argument, the return + * will be an array of keys. + * @api + */ + once(type, listener) { + if (Array.isArray(type)) { + const len = type.length; + const keys = new Array(len); + for (let i = 0; i < len; ++i) { + keys[i] = listenOnce(this, type[i], listener); + } + return keys; + } else { + return listenOnce(this, /** @type {string} */ (type), listener); + } + } + + /** + * Unlisten for a certain type of event. + * @param {string|Array.} type The event type or array of event types. + * @param {function(?): ?} listener The listener function. + * @api + */ + un(type, listener) { + if (Array.isArray(type)) { + for (let i = 0, ii = type.length; i < ii; ++i) { + unlisten(this, type[i], listener); + } + return; + } else { + unlisten(this, /** @type {string} */ (type), listener); + } + } +} inherits(Observable, EventTarget); @@ -52,16 +134,6 @@ export function unByKey(key) { } -/** - * Increases the revision counter and dispatches a 'change' event. - * @api - */ -Observable.prototype.changed = function() { - ++this.revision_; - this.dispatchEvent(EventType.CHANGE); -}; - - /** * Dispatches an event and calls all listeners listening for events * of this type. The event parameter can either be a string or an @@ -76,77 +148,4 @@ Observable.prototype.changed = function() { Observable.prototype.dispatchEvent; -/** - * Get the version number for this object. Each time the object is modified, - * its version number will be incremented. - * @return {number} Revision. - * @api - */ -Observable.prototype.getRevision = function() { - return this.revision_; -}; - - -/** - * Listen for a certain type of event. - * @param {string|Array.} type The event type or array of event types. - * @param {function(?): ?} listener The listener function. - * @return {module:ol/events~EventsKey|Array.} Unique key for the listener. If - * called with an array of event types as the first argument, the return - * will be an array of keys. - * @api - */ -Observable.prototype.on = function(type, listener) { - if (Array.isArray(type)) { - const len = type.length; - const keys = new Array(len); - for (let i = 0; i < len; ++i) { - keys[i] = listen(this, type[i], listener); - } - return keys; - } else { - return listen(this, /** @type {string} */ (type), listener); - } -}; - - -/** - * Listen once for a certain type of event. - * @param {string|Array.} type The event type or array of event types. - * @param {function(?): ?} listener The listener function. - * @return {module:ol/events~EventsKey|Array.} Unique key for the listener. If - * called with an array of event types as the first argument, the return - * will be an array of keys. - * @api - */ -Observable.prototype.once = function(type, listener) { - if (Array.isArray(type)) { - const len = type.length; - const keys = new Array(len); - for (let i = 0; i < len; ++i) { - keys[i] = listenOnce(this, type[i], listener); - } - return keys; - } else { - return listenOnce(this, /** @type {string} */ (type), listener); - } -}; - - -/** - * Unlisten for a certain type of event. - * @param {string|Array.} type The event type or array of event types. - * @param {function(?): ?} listener The listener function. - * @api - */ -Observable.prototype.un = function(type, listener) { - if (Array.isArray(type)) { - for (let i = 0, ii = type.length; i < ii; ++i) { - unlisten(this, type[i], listener); - } - return; - } else { - unlisten(this, /** @type {string} */ (type), listener); - } -}; export default Observable; diff --git a/src/ol/Overlay.js b/src/ol/Overlay.js index 102fa04685..b5fc6d3d0b 100644 --- a/src/ol/Overlay.js +++ b/src/ol/Overlay.js @@ -98,510 +98,490 @@ const Property = { * @param {module:ol/Overlay~Options} options Overlay options. * @api */ -const Overlay = function(options) { +class Overlay { + constructor(options) { - BaseObject.call(this); + BaseObject.call(this); - /** - * @protected - * @type {module:ol/Overlay~Options} - */ - this.options = options; + /** + * @protected + * @type {module:ol/Overlay~Options} + */ + this.options = options; - /** - * @protected - * @type {number|string|undefined} - */ - this.id = options.id; + /** + * @protected + * @type {number|string|undefined} + */ + this.id = options.id; - /** - * @protected - * @type {boolean} - */ - this.insertFirst = options.insertFirst !== undefined ? - options.insertFirst : true; + /** + * @protected + * @type {boolean} + */ + this.insertFirst = options.insertFirst !== undefined ? + options.insertFirst : true; - /** - * @protected - * @type {boolean} - */ - this.stopEvent = options.stopEvent !== undefined ? options.stopEvent : true; + /** + * @protected + * @type {boolean} + */ + this.stopEvent = options.stopEvent !== undefined ? options.stopEvent : true; - /** - * @protected - * @type {HTMLElement} - */ - this.element = document.createElement('DIV'); - this.element.className = options.className !== undefined ? - options.className : 'ol-overlay-container ' + CLASS_SELECTABLE; - this.element.style.position = 'absolute'; + /** + * @protected + * @type {HTMLElement} + */ + this.element = document.createElement('DIV'); + this.element.className = options.className !== undefined ? + options.className : 'ol-overlay-container ' + CLASS_SELECTABLE; + this.element.style.position = 'absolute'; - /** - * @protected - * @type {boolean} - */ - this.autoPan = options.autoPan !== undefined ? options.autoPan : false; + /** + * @protected + * @type {boolean} + */ + this.autoPan = options.autoPan !== undefined ? options.autoPan : false; - /** - * @protected - * @type {module:ol/Overlay~PanOptions} - */ - this.autoPanAnimation = options.autoPanAnimation || /** @type {module:ol/Overlay~PanOptions} */ ({}); + /** + * @protected + * @type {module:ol/Overlay~PanOptions} + */ + this.autoPanAnimation = options.autoPanAnimation || /** @type {module:ol/Overlay~PanOptions} */ ({}); - /** - * @protected - * @type {number} - */ - this.autoPanMargin = options.autoPanMargin !== undefined ? - options.autoPanMargin : 20; + /** + * @protected + * @type {number} + */ + this.autoPanMargin = options.autoPanMargin !== undefined ? + options.autoPanMargin : 20; - /** - * @protected - * @type {{bottom_: string, - * left_: string, - * right_: string, - * top_: string, - * visible: boolean}} - */ - this.rendered = { - bottom_: '', - left_: '', - right_: '', - top_: '', - visible: true - }; + /** + * @protected + * @type {{bottom_: string, + * left_: string, + * right_: string, + * top_: string, + * visible: boolean}} + */ + this.rendered = { + bottom_: '', + left_: '', + right_: '', + top_: '', + visible: true + }; - /** - * @protected - * @type {?module:ol/events~EventsKey} - */ - this.mapPostrenderListenerKey = null; + /** + * @protected + * @type {?module:ol/events~EventsKey} + */ + this.mapPostrenderListenerKey = null; - listen( - this, getChangeEventType(Property.ELEMENT), - this.handleElementChanged, this); + listen( + this, getChangeEventType(Property.ELEMENT), + this.handleElementChanged, this); - listen( - this, getChangeEventType(Property.MAP), - this.handleMapChanged, this); + listen( + this, getChangeEventType(Property.MAP), + this.handleMapChanged, this); - listen( - this, getChangeEventType(Property.OFFSET), - this.handleOffsetChanged, this); + listen( + this, getChangeEventType(Property.OFFSET), + this.handleOffsetChanged, this); - listen( - this, getChangeEventType(Property.POSITION), - this.handlePositionChanged, this); + listen( + this, getChangeEventType(Property.POSITION), + this.handlePositionChanged, this); - listen( - this, getChangeEventType(Property.POSITIONING), - this.handlePositioningChanged, this); + listen( + this, getChangeEventType(Property.POSITIONING), + this.handlePositioningChanged, this); + + if (options.element !== undefined) { + this.setElement(options.element); + } + + this.setOffset(options.offset !== undefined ? options.offset : [0, 0]); + + this.setPositioning(options.positioning !== undefined ? + /** @type {module:ol/OverlayPositioning} */ (options.positioning) : + OverlayPositioning.TOP_LEFT); + + if (options.position !== undefined) { + this.setPosition(options.position); + } - if (options.element !== undefined) { - this.setElement(options.element); } - this.setOffset(options.offset !== undefined ? options.offset : [0, 0]); - - this.setPositioning(options.positioning !== undefined ? - /** @type {module:ol/OverlayPositioning} */ (options.positioning) : - OverlayPositioning.TOP_LEFT); - - if (options.position !== undefined) { - this.setPosition(options.position); + /** + * Get the DOM element of this overlay. + * @return {HTMLElement|undefined} The Element containing the overlay. + * @observable + * @api + */ + getElement() { + return /** @type {HTMLElement|undefined} */ (this.get(Property.ELEMENT)); } -}; + /** + * Get the overlay identifier which is set on constructor. + * @return {number|string|undefined} Id. + * @api + */ + getId() { + return this.id; + } + + /** + * Get the map associated with this overlay. + * @return {module:ol/PluggableMap|undefined} The map that the + * overlay is part of. + * @observable + * @api + */ + getMap() { + return ( + /** @type {module:ol/PluggableMap|undefined} */ (this.get(Property.MAP)) + ); + } + + /** + * Get the offset of this overlay. + * @return {Array.} The offset. + * @observable + * @api + */ + getOffset() { + return /** @type {Array.} */ (this.get(Property.OFFSET)); + } + + /** + * Get the current position of this overlay. + * @return {module:ol/coordinate~Coordinate|undefined} The spatial point that the overlay is + * anchored at. + * @observable + * @api + */ + getPosition() { + return ( + /** @type {module:ol/coordinate~Coordinate|undefined} */ (this.get(Property.POSITION)) + ); + } + + /** + * Get the current positioning of this overlay. + * @return {module:ol/OverlayPositioning} How the overlay is positioned + * relative to its point on the map. + * @observable + * @api + */ + getPositioning() { + return ( + /** @type {module:ol/OverlayPositioning} */ (this.get(Property.POSITIONING)) + ); + } + + /** + * @protected + */ + handleElementChanged() { + removeChildren(this.element); + const element = this.getElement(); + if (element) { + this.element.appendChild(element); + } + } + + /** + * @protected + */ + handleMapChanged() { + if (this.mapPostrenderListenerKey) { + removeNode(this.element); + unlistenByKey(this.mapPostrenderListenerKey); + this.mapPostrenderListenerKey = null; + } + const map = this.getMap(); + if (map) { + this.mapPostrenderListenerKey = listen(map, + MapEventType.POSTRENDER, this.render, this); + this.updatePixelPosition(); + const container = this.stopEvent ? + map.getOverlayContainerStopEvent() : map.getOverlayContainer(); + if (this.insertFirst) { + container.insertBefore(this.element, container.childNodes[0] || null); + } else { + container.appendChild(this.element); + } + } + } + + /** + * @protected + */ + render() { + this.updatePixelPosition(); + } + + /** + * @protected + */ + handleOffsetChanged() { + this.updatePixelPosition(); + } + + /** + * @protected + */ + handlePositionChanged() { + this.updatePixelPosition(); + if (this.get(Property.POSITION) && this.autoPan) { + this.panIntoView(); + } + } + + /** + * @protected + */ + handlePositioningChanged() { + this.updatePixelPosition(); + } + + /** + * Set the DOM element to be associated with this overlay. + * @param {HTMLElement|undefined} element The Element containing the overlay. + * @observable + * @api + */ + setElement(element) { + this.set(Property.ELEMENT, element); + } + + /** + * Set the map to be associated with this overlay. + * @param {module:ol/PluggableMap|undefined} map The map that the + * overlay is part of. + * @observable + * @api + */ + setMap(map) { + this.set(Property.MAP, map); + } + + /** + * Set the offset for this overlay. + * @param {Array.} offset Offset. + * @observable + * @api + */ + setOffset(offset) { + this.set(Property.OFFSET, offset); + } + + /** + * Set the position for this overlay. If the position is `undefined` the + * overlay is hidden. + * @param {module:ol/coordinate~Coordinate|undefined} position The spatial point that the overlay + * is anchored at. + * @observable + * @api + */ + setPosition(position) { + this.set(Property.POSITION, position); + } + + /** + * Pan the map so that the overlay is entirely visible in the current viewport + * (if necessary). + * @protected + */ + panIntoView() { + const map = this.getMap(); + + if (!map || !map.getTargetElement()) { + return; + } + + const mapRect = this.getRect(map.getTargetElement(), map.getSize()); + const element = this.getElement(); + const overlayRect = this.getRect(element, [outerWidth(element), outerHeight(element)]); + + const margin = this.autoPanMargin; + if (!containsExtent(mapRect, overlayRect)) { + // the overlay is not completely inside the viewport, so pan the map + const offsetLeft = overlayRect[0] - mapRect[0]; + const offsetRight = mapRect[2] - overlayRect[2]; + const offsetTop = overlayRect[1] - mapRect[1]; + const offsetBottom = mapRect[3] - overlayRect[3]; + + const delta = [0, 0]; + if (offsetLeft < 0) { + // move map to the left + delta[0] = offsetLeft - margin; + } else if (offsetRight < 0) { + // move map to the right + delta[0] = Math.abs(offsetRight) + margin; + } + if (offsetTop < 0) { + // move map up + delta[1] = offsetTop - margin; + } else if (offsetBottom < 0) { + // move map down + delta[1] = Math.abs(offsetBottom) + margin; + } + + if (delta[0] !== 0 || delta[1] !== 0) { + const center = /** @type {module:ol/coordinate~Coordinate} */ (map.getView().getCenter()); + const centerPx = map.getPixelFromCoordinate(center); + const newCenterPx = [ + centerPx[0] + delta[0], + centerPx[1] + delta[1] + ]; + + map.getView().animate({ + center: map.getCoordinateFromPixel(newCenterPx), + duration: this.autoPanAnimation.duration, + easing: this.autoPanAnimation.easing + }); + } + } + } + + /** + * Get the extent of an element relative to the document + * @param {HTMLElement|undefined} element The element. + * @param {module:ol/size~Size|undefined} size The size of the element. + * @return {module:ol/extent~Extent} The extent. + * @protected + */ + getRect(element, size) { + const box = element.getBoundingClientRect(); + const offsetX = box.left + window.pageXOffset; + const offsetY = box.top + window.pageYOffset; + return [ + offsetX, + offsetY, + offsetX + size[0], + offsetY + size[1] + ]; + } + + /** + * Set the positioning for this overlay. + * @param {module:ol/OverlayPositioning} positioning how the overlay is + * positioned relative to its point on the map. + * @observable + * @api + */ + setPositioning(positioning) { + this.set(Property.POSITIONING, positioning); + } + + /** + * Modify the visibility of the element. + * @param {boolean} visible Element visibility. + * @protected + */ + setVisible(visible) { + if (this.rendered.visible !== visible) { + this.element.style.display = visible ? '' : 'none'; + this.rendered.visible = visible; + } + } + + /** + * Update pixel position. + * @protected + */ + updatePixelPosition() { + const map = this.getMap(); + const position = this.getPosition(); + if (!map || !map.isRendered() || !position) { + this.setVisible(false); + return; + } + + const pixel = map.getPixelFromCoordinate(position); + const mapSize = map.getSize(); + this.updateRenderedPosition(pixel, mapSize); + } + + /** + * @param {module:ol~Pixel} pixel The pixel location. + * @param {module:ol/size~Size|undefined} mapSize The map size. + * @protected + */ + updateRenderedPosition(pixel, mapSize) { + const style = this.element.style; + const offset = this.getOffset(); + + const positioning = this.getPositioning(); + + this.setVisible(true); + + let offsetX = offset[0]; + let offsetY = offset[1]; + if (positioning == OverlayPositioning.BOTTOM_RIGHT || + positioning == OverlayPositioning.CENTER_RIGHT || + positioning == OverlayPositioning.TOP_RIGHT) { + if (this.rendered.left_ !== '') { + this.rendered.left_ = style.left = ''; + } + const right = Math.round(mapSize[0] - pixel[0] - offsetX) + 'px'; + if (this.rendered.right_ != right) { + this.rendered.right_ = style.right = right; + } + } else { + if (this.rendered.right_ !== '') { + this.rendered.right_ = style.right = ''; + } + if (positioning == OverlayPositioning.BOTTOM_CENTER || + positioning == OverlayPositioning.CENTER_CENTER || + positioning == OverlayPositioning.TOP_CENTER) { + offsetX -= this.element.offsetWidth / 2; + } + const left = Math.round(pixel[0] + offsetX) + 'px'; + if (this.rendered.left_ != left) { + this.rendered.left_ = style.left = left; + } + } + if (positioning == OverlayPositioning.BOTTOM_LEFT || + positioning == OverlayPositioning.BOTTOM_CENTER || + positioning == OverlayPositioning.BOTTOM_RIGHT) { + if (this.rendered.top_ !== '') { + this.rendered.top_ = style.top = ''; + } + const bottom = Math.round(mapSize[1] - pixel[1] - offsetY) + 'px'; + if (this.rendered.bottom_ != bottom) { + this.rendered.bottom_ = style.bottom = bottom; + } + } else { + if (this.rendered.bottom_ !== '') { + this.rendered.bottom_ = style.bottom = ''; + } + if (positioning == OverlayPositioning.CENTER_LEFT || + positioning == OverlayPositioning.CENTER_CENTER || + positioning == OverlayPositioning.CENTER_RIGHT) { + offsetY -= this.element.offsetHeight / 2; + } + const top = Math.round(pixel[1] + offsetY) + 'px'; + if (this.rendered.top_ != top) { + this.rendered.top_ = style.top = top; + } + } + } + + /** + * returns the options this Overlay has been created with + * @return {module:ol/Overlay~Options} overlay options + */ + getOptions() { + return this.options; + } +} inherits(Overlay, BaseObject); -/** - * Get the DOM element of this overlay. - * @return {HTMLElement|undefined} The Element containing the overlay. - * @observable - * @api - */ -Overlay.prototype.getElement = function() { - return /** @type {HTMLElement|undefined} */ (this.get(Property.ELEMENT)); -}; - - -/** - * Get the overlay identifier which is set on constructor. - * @return {number|string|undefined} Id. - * @api - */ -Overlay.prototype.getId = function() { - return this.id; -}; - - -/** - * Get the map associated with this overlay. - * @return {module:ol/PluggableMap|undefined} The map that the - * overlay is part of. - * @observable - * @api - */ -Overlay.prototype.getMap = function() { - return ( - /** @type {module:ol/PluggableMap|undefined} */ (this.get(Property.MAP)) - ); -}; - - -/** - * Get the offset of this overlay. - * @return {Array.} The offset. - * @observable - * @api - */ -Overlay.prototype.getOffset = function() { - return /** @type {Array.} */ (this.get(Property.OFFSET)); -}; - - -/** - * Get the current position of this overlay. - * @return {module:ol/coordinate~Coordinate|undefined} The spatial point that the overlay is - * anchored at. - * @observable - * @api - */ -Overlay.prototype.getPosition = function() { - return ( - /** @type {module:ol/coordinate~Coordinate|undefined} */ (this.get(Property.POSITION)) - ); -}; - - -/** - * Get the current positioning of this overlay. - * @return {module:ol/OverlayPositioning} How the overlay is positioned - * relative to its point on the map. - * @observable - * @api - */ -Overlay.prototype.getPositioning = function() { - return ( - /** @type {module:ol/OverlayPositioning} */ (this.get(Property.POSITIONING)) - ); -}; - - -/** - * @protected - */ -Overlay.prototype.handleElementChanged = function() { - removeChildren(this.element); - const element = this.getElement(); - if (element) { - this.element.appendChild(element); - } -}; - - -/** - * @protected - */ -Overlay.prototype.handleMapChanged = function() { - if (this.mapPostrenderListenerKey) { - removeNode(this.element); - unlistenByKey(this.mapPostrenderListenerKey); - this.mapPostrenderListenerKey = null; - } - const map = this.getMap(); - if (map) { - this.mapPostrenderListenerKey = listen(map, - MapEventType.POSTRENDER, this.render, this); - this.updatePixelPosition(); - const container = this.stopEvent ? - map.getOverlayContainerStopEvent() : map.getOverlayContainer(); - if (this.insertFirst) { - container.insertBefore(this.element, container.childNodes[0] || null); - } else { - container.appendChild(this.element); - } - } -}; - - -/** - * @protected - */ -Overlay.prototype.render = function() { - this.updatePixelPosition(); -}; - - -/** - * @protected - */ -Overlay.prototype.handleOffsetChanged = function() { - this.updatePixelPosition(); -}; - - -/** - * @protected - */ -Overlay.prototype.handlePositionChanged = function() { - this.updatePixelPosition(); - if (this.get(Property.POSITION) && this.autoPan) { - this.panIntoView(); - } -}; - - -/** - * @protected - */ -Overlay.prototype.handlePositioningChanged = function() { - this.updatePixelPosition(); -}; - - -/** - * Set the DOM element to be associated with this overlay. - * @param {HTMLElement|undefined} element The Element containing the overlay. - * @observable - * @api - */ -Overlay.prototype.setElement = function(element) { - this.set(Property.ELEMENT, element); -}; - - -/** - * Set the map to be associated with this overlay. - * @param {module:ol/PluggableMap|undefined} map The map that the - * overlay is part of. - * @observable - * @api - */ -Overlay.prototype.setMap = function(map) { - this.set(Property.MAP, map); -}; - - -/** - * Set the offset for this overlay. - * @param {Array.} offset Offset. - * @observable - * @api - */ -Overlay.prototype.setOffset = function(offset) { - this.set(Property.OFFSET, offset); -}; - - -/** - * Set the position for this overlay. If the position is `undefined` the - * overlay is hidden. - * @param {module:ol/coordinate~Coordinate|undefined} position The spatial point that the overlay - * is anchored at. - * @observable - * @api - */ -Overlay.prototype.setPosition = function(position) { - this.set(Property.POSITION, position); -}; - - -/** - * Pan the map so that the overlay is entirely visible in the current viewport - * (if necessary). - * @protected - */ -Overlay.prototype.panIntoView = function() { - const map = this.getMap(); - - if (!map || !map.getTargetElement()) { - return; - } - - const mapRect = this.getRect(map.getTargetElement(), map.getSize()); - const element = this.getElement(); - const overlayRect = this.getRect(element, [outerWidth(element), outerHeight(element)]); - - const margin = this.autoPanMargin; - if (!containsExtent(mapRect, overlayRect)) { - // the overlay is not completely inside the viewport, so pan the map - const offsetLeft = overlayRect[0] - mapRect[0]; - const offsetRight = mapRect[2] - overlayRect[2]; - const offsetTop = overlayRect[1] - mapRect[1]; - const offsetBottom = mapRect[3] - overlayRect[3]; - - const delta = [0, 0]; - if (offsetLeft < 0) { - // move map to the left - delta[0] = offsetLeft - margin; - } else if (offsetRight < 0) { - // move map to the right - delta[0] = Math.abs(offsetRight) + margin; - } - if (offsetTop < 0) { - // move map up - delta[1] = offsetTop - margin; - } else if (offsetBottom < 0) { - // move map down - delta[1] = Math.abs(offsetBottom) + margin; - } - - if (delta[0] !== 0 || delta[1] !== 0) { - const center = /** @type {module:ol/coordinate~Coordinate} */ (map.getView().getCenter()); - const centerPx = map.getPixelFromCoordinate(center); - const newCenterPx = [ - centerPx[0] + delta[0], - centerPx[1] + delta[1] - ]; - - map.getView().animate({ - center: map.getCoordinateFromPixel(newCenterPx), - duration: this.autoPanAnimation.duration, - easing: this.autoPanAnimation.easing - }); - } - } -}; - - -/** - * Get the extent of an element relative to the document - * @param {HTMLElement|undefined} element The element. - * @param {module:ol/size~Size|undefined} size The size of the element. - * @return {module:ol/extent~Extent} The extent. - * @protected - */ -Overlay.prototype.getRect = function(element, size) { - const box = element.getBoundingClientRect(); - const offsetX = box.left + window.pageXOffset; - const offsetY = box.top + window.pageYOffset; - return [ - offsetX, - offsetY, - offsetX + size[0], - offsetY + size[1] - ]; -}; - - -/** - * Set the positioning for this overlay. - * @param {module:ol/OverlayPositioning} positioning how the overlay is - * positioned relative to its point on the map. - * @observable - * @api - */ -Overlay.prototype.setPositioning = function(positioning) { - this.set(Property.POSITIONING, positioning); -}; - - -/** - * Modify the visibility of the element. - * @param {boolean} visible Element visibility. - * @protected - */ -Overlay.prototype.setVisible = function(visible) { - if (this.rendered.visible !== visible) { - this.element.style.display = visible ? '' : 'none'; - this.rendered.visible = visible; - } -}; - - -/** - * Update pixel position. - * @protected - */ -Overlay.prototype.updatePixelPosition = function() { - const map = this.getMap(); - const position = this.getPosition(); - if (!map || !map.isRendered() || !position) { - this.setVisible(false); - return; - } - - const pixel = map.getPixelFromCoordinate(position); - const mapSize = map.getSize(); - this.updateRenderedPosition(pixel, mapSize); -}; - - -/** - * @param {module:ol~Pixel} pixel The pixel location. - * @param {module:ol/size~Size|undefined} mapSize The map size. - * @protected - */ -Overlay.prototype.updateRenderedPosition = function(pixel, mapSize) { - const style = this.element.style; - const offset = this.getOffset(); - - const positioning = this.getPositioning(); - - this.setVisible(true); - - let offsetX = offset[0]; - let offsetY = offset[1]; - if (positioning == OverlayPositioning.BOTTOM_RIGHT || - positioning == OverlayPositioning.CENTER_RIGHT || - positioning == OverlayPositioning.TOP_RIGHT) { - if (this.rendered.left_ !== '') { - this.rendered.left_ = style.left = ''; - } - const right = Math.round(mapSize[0] - pixel[0] - offsetX) + 'px'; - if (this.rendered.right_ != right) { - this.rendered.right_ = style.right = right; - } - } else { - if (this.rendered.right_ !== '') { - this.rendered.right_ = style.right = ''; - } - if (positioning == OverlayPositioning.BOTTOM_CENTER || - positioning == OverlayPositioning.CENTER_CENTER || - positioning == OverlayPositioning.TOP_CENTER) { - offsetX -= this.element.offsetWidth / 2; - } - const left = Math.round(pixel[0] + offsetX) + 'px'; - if (this.rendered.left_ != left) { - this.rendered.left_ = style.left = left; - } - } - if (positioning == OverlayPositioning.BOTTOM_LEFT || - positioning == OverlayPositioning.BOTTOM_CENTER || - positioning == OverlayPositioning.BOTTOM_RIGHT) { - if (this.rendered.top_ !== '') { - this.rendered.top_ = style.top = ''; - } - const bottom = Math.round(mapSize[1] - pixel[1] - offsetY) + 'px'; - if (this.rendered.bottom_ != bottom) { - this.rendered.bottom_ = style.bottom = bottom; - } - } else { - if (this.rendered.bottom_ !== '') { - this.rendered.bottom_ = style.bottom = ''; - } - if (positioning == OverlayPositioning.CENTER_LEFT || - positioning == OverlayPositioning.CENTER_CENTER || - positioning == OverlayPositioning.CENTER_RIGHT) { - offsetY -= this.element.offsetHeight / 2; - } - const top = Math.round(pixel[1] + offsetY) + 'px'; - if (this.rendered.top_ != top) { - this.rendered.top_ = style.top = top; - } - } -}; - - -/** - * returns the options this Overlay has been created with - * @return {module:ol/Overlay~Options} overlay options - */ -Overlay.prototype.getOptions = function() { - return this.options; -}; - export default Overlay; diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 665c412f37..9e8bb2fbc7 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -140,1255 +140,1204 @@ import {create as createTransform, apply as applyTransform} from './transform.js * @fires module:ol/render/Event~RenderEvent#precompose * @api */ -const PluggableMap = function(options) { +class PluggableMap { + constructor(options) { - BaseObject.call(this); + BaseObject.call(this); - const optionsInternal = createOptionsInternal(options); + const optionsInternal = createOptionsInternal(options); - /** - * @type {number} - * @private - */ - this.maxTilesLoading_ = options.maxTilesLoading !== undefined ? options.maxTilesLoading : 16; - - /** - * @type {boolean} - * @private - */ - this.loadTilesWhileAnimating_ = - options.loadTilesWhileAnimating !== undefined ? - options.loadTilesWhileAnimating : false; - - /** - * @type {boolean} - * @private - */ - this.loadTilesWhileInteracting_ = - options.loadTilesWhileInteracting !== undefined ? - options.loadTilesWhileInteracting : false; - - /** - * @private - * @type {number} - */ - this.pixelRatio_ = options.pixelRatio !== undefined ? - options.pixelRatio : DEVICE_PIXEL_RATIO; - - /** - * @private - * @type {number|undefined} - */ - this.animationDelayKey_; - - /** - * @private - */ - this.animationDelay_ = function() { - this.animationDelayKey_ = undefined; - this.renderFrame_.call(this, Date.now()); - }.bind(this); - - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.coordinateToPixelTransform_ = createTransform(); - - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.pixelToCoordinateTransform_ = createTransform(); - - /** - * @private - * @type {number} - */ - this.frameIndex_ = 0; - - /** - * @private - * @type {?module:ol/PluggableMap~FrameState} - */ - this.frameState_ = null; - - /** - * The extent at the previous 'moveend' event. - * @private - * @type {module:ol/extent~Extent} - */ - this.previousExtent_ = null; - - /** - * @private - * @type {?module:ol/events~EventsKey} - */ - this.viewPropertyListenerKey_ = null; - - /** - * @private - * @type {?module:ol/events~EventsKey} - */ - this.viewChangeListenerKey_ = null; - - /** - * @private - * @type {Array.} - */ - this.layerGroupPropertyListenerKeys_ = null; - - /** - * @private - * @type {!HTMLElement} - */ - this.viewport_ = document.createElement('DIV'); - this.viewport_.className = 'ol-viewport' + (TOUCH ? ' ol-touch' : ''); - this.viewport_.style.position = 'relative'; - this.viewport_.style.overflow = 'hidden'; - this.viewport_.style.width = '100%'; - this.viewport_.style.height = '100%'; - // prevent page zoom on IE >= 10 browsers - this.viewport_.style.msTouchAction = 'none'; - this.viewport_.style.touchAction = 'none'; - - /** - * @private - * @type {!HTMLElement} - */ - this.overlayContainer_ = document.createElement('DIV'); - this.overlayContainer_.className = 'ol-overlaycontainer'; - this.viewport_.appendChild(this.overlayContainer_); - - /** - * @private - * @type {!HTMLElement} - */ - this.overlayContainerStopEvent_ = document.createElement('DIV'); - this.overlayContainerStopEvent_.className = 'ol-overlaycontainer-stopevent'; - const overlayEvents = [ - EventType.CLICK, - EventType.DBLCLICK, - EventType.MOUSEDOWN, - EventType.TOUCHSTART, - EventType.MSPOINTERDOWN, - MapBrowserEventType.POINTERDOWN, - EventType.MOUSEWHEEL, - EventType.WHEEL - ]; - for (let i = 0, ii = overlayEvents.length; i < ii; ++i) { - listen(this.overlayContainerStopEvent_, overlayEvents[i], stopPropagation); - } - this.viewport_.appendChild(this.overlayContainerStopEvent_); - - /** - * @private - * @type {module:ol/MapBrowserEventHandler} - */ - this.mapBrowserEventHandler_ = new MapBrowserEventHandler(this, options.moveTolerance); - for (const key in MapBrowserEventType) { - listen(this.mapBrowserEventHandler_, MapBrowserEventType[key], - this.handleMapBrowserEvent, this); - } - - /** - * @private - * @type {HTMLElement|Document} - */ - this.keyboardEventTarget_ = optionsInternal.keyboardEventTarget; - - /** - * @private - * @type {Array.} - */ - this.keyHandlerKeys_ = null; - - listen(this.viewport_, EventType.CONTEXTMENU, this.handleBrowserEvent, this); - listen(this.viewport_, EventType.WHEEL, this.handleBrowserEvent, this); - listen(this.viewport_, EventType.MOUSEWHEEL, this.handleBrowserEvent, this); - - /** - * @type {module:ol/Collection.} - * @protected - */ - this.controls = optionsInternal.controls || new Collection(); - - /** - * @type {module:ol/Collection.} - * @protected - */ - this.interactions = optionsInternal.interactions || new Collection(); - - /** - * @type {module:ol/Collection.} - * @private - */ - this.overlays_ = optionsInternal.overlays; - - /** - * A lookup of overlays by id. - * @private - * @type {Object.} - */ - this.overlayIdIndex_ = {}; - - /** - * @type {module:ol/renderer/Map} - * @private - */ - this.renderer_ = this.createRenderer(); - - /** - * @type {function(Event)|undefined} - * @private - */ - this.handleResize_; - - /** - * @private - * @type {module:ol/coordinate~Coordinate} - */ - this.focus_ = null; - - /** - * @private - * @type {!Array.} - */ - this.postRenderFunctions_ = []; - - /** - * @private - * @type {module:ol/TileQueue} - */ - this.tileQueue_ = new TileQueue( - this.getTilePriority.bind(this), - this.handleTileChange_.bind(this)); - - /** - * Uids of features to skip at rendering time. - * @type {Object.} - * @private - */ - this.skippedFeatureUids_ = {}; - - listen( - this, getChangeEventType(MapProperty.LAYERGROUP), - this.handleLayerGroupChanged_, this); - listen(this, getChangeEventType(MapProperty.VIEW), - this.handleViewChanged_, this); - listen(this, getChangeEventType(MapProperty.SIZE), - this.handleSizeChanged_, this); - listen(this, getChangeEventType(MapProperty.TARGET), - this.handleTargetChanged_, this); - - // setProperties will trigger the rendering of the map if the map - // is "defined" already. - this.setProperties(optionsInternal.values); - - this.controls.forEach( /** - * @param {module:ol/control/Control} control Control. - * @this {module:ol/PluggableMap} + * @type {number} + * @private */ - (function(control) { - control.setMap(this); - }).bind(this)); + this.maxTilesLoading_ = options.maxTilesLoading !== undefined ? options.maxTilesLoading : 16; - listen(this.controls, CollectionEventType.ADD, /** - * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + * @type {boolean} + * @private */ - function(event) { - event.element.setMap(this); - }, this); + this.loadTilesWhileAnimating_ = + options.loadTilesWhileAnimating !== undefined ? + options.loadTilesWhileAnimating : false; - listen(this.controls, CollectionEventType.REMOVE, /** - * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + * @type {boolean} + * @private */ - function(event) { - event.element.setMap(null); - }, this); + this.loadTilesWhileInteracting_ = + options.loadTilesWhileInteracting !== undefined ? + options.loadTilesWhileInteracting : false; - this.interactions.forEach( /** - * @param {module:ol/interaction/Interaction} interaction Interaction. - * @this {module:ol/PluggableMap} + * @private + * @type {number} */ - (function(interaction) { - interaction.setMap(this); - }).bind(this)); + this.pixelRatio_ = options.pixelRatio !== undefined ? + options.pixelRatio : DEVICE_PIXEL_RATIO; - listen(this.interactions, CollectionEventType.ADD, /** - * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + * @private + * @type {number|undefined} */ - function(event) { - event.element.setMap(this); - }, this); + this.animationDelayKey_; - listen(this.interactions, CollectionEventType.REMOVE, /** - * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + * @private */ - function(event) { - event.element.setMap(null); - }, this); + this.animationDelay_ = function() { + this.animationDelayKey_ = undefined; + this.renderFrame_.call(this, Date.now()); + }.bind(this); - this.overlays_.forEach(this.addOverlayInternal_.bind(this)); - - listen(this.overlays_, CollectionEventType.ADD, /** - * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + * @private + * @type {module:ol/transform~Transform} */ - function(event) { - this.addOverlayInternal_(/** @type {module:ol/Overlay} */ (event.element)); - }, this); + this.coordinateToPixelTransform_ = createTransform(); - listen(this.overlays_, CollectionEventType.REMOVE, /** - * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + * @private + * @type {module:ol/transform~Transform} */ - function(event) { - const overlay = /** @type {module:ol/Overlay} */ (event.element); - const id = overlay.getId(); - if (id !== undefined) { - delete this.overlayIdIndex_[id.toString()]; - } - event.element.setMap(null); - }, this); + this.pixelToCoordinateTransform_ = createTransform(); -}; + /** + * @private + * @type {number} + */ + this.frameIndex_ = 0; -inherits(PluggableMap, BaseObject); + /** + * @private + * @type {?module:ol/PluggableMap~FrameState} + */ + this.frameState_ = null; + /** + * The extent at the previous 'moveend' event. + * @private + * @type {module:ol/extent~Extent} + */ + this.previousExtent_ = null; -PluggableMap.prototype.createRenderer = function() { - throw new Error('Use a map type that has a createRenderer method'); -}; + /** + * @private + * @type {?module:ol/events~EventsKey} + */ + this.viewPropertyListenerKey_ = null; + /** + * @private + * @type {?module:ol/events~EventsKey} + */ + this.viewChangeListenerKey_ = null; -/** - * Add the given control to the map. - * @param {module:ol/control/Control} control Control. - * @api - */ -PluggableMap.prototype.addControl = function(control) { - this.getControls().push(control); -}; + /** + * @private + * @type {Array.} + */ + this.layerGroupPropertyListenerKeys_ = null; + /** + * @private + * @type {!HTMLElement} + */ + this.viewport_ = document.createElement('DIV'); + this.viewport_.className = 'ol-viewport' + (TOUCH ? ' ol-touch' : ''); + this.viewport_.style.position = 'relative'; + this.viewport_.style.overflow = 'hidden'; + this.viewport_.style.width = '100%'; + this.viewport_.style.height = '100%'; + // prevent page zoom on IE >= 10 browsers + this.viewport_.style.msTouchAction = 'none'; + this.viewport_.style.touchAction = 'none'; -/** - * Add the given interaction to the map. - * @param {module:ol/interaction/Interaction} interaction Interaction to add. - * @api - */ -PluggableMap.prototype.addInteraction = function(interaction) { - this.getInteractions().push(interaction); -}; + /** + * @private + * @type {!HTMLElement} + */ + this.overlayContainer_ = document.createElement('DIV'); + this.overlayContainer_.className = 'ol-overlaycontainer'; + this.viewport_.appendChild(this.overlayContainer_); - -/** - * 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 {module:ol/layer/Base} layer Layer. - * @api - */ -PluggableMap.prototype.addLayer = function(layer) { - const layers = this.getLayerGroup().getLayers(); - layers.push(layer); -}; - - -/** - * Add the given overlay to the map. - * @param {module:ol/Overlay} overlay Overlay. - * @api - */ -PluggableMap.prototype.addOverlay = function(overlay) { - this.getOverlays().push(overlay); -}; - - -/** - * This deals with map's overlay collection changes. - * @param {module:ol/Overlay} overlay Overlay. - * @private - */ -PluggableMap.prototype.addOverlayInternal_ = function(overlay) { - const id = overlay.getId(); - if (id !== undefined) { - this.overlayIdIndex_[id.toString()] = overlay; - } - overlay.setMap(this); -}; - - -/** - * - * @inheritDoc - */ -PluggableMap.prototype.disposeInternal = function() { - this.mapBrowserEventHandler_.dispose(); - unlisten(this.viewport_, EventType.CONTEXTMENU, this.handleBrowserEvent, this); - unlisten(this.viewport_, EventType.WHEEL, this.handleBrowserEvent, this); - unlisten(this.viewport_, EventType.MOUSEWHEEL, this.handleBrowserEvent, this); - if (this.handleResize_ !== undefined) { - removeEventListener(EventType.RESIZE, this.handleResize_, false); - this.handleResize_ = undefined; - } - if (this.animationDelayKey_) { - cancelAnimationFrame(this.animationDelayKey_); - this.animationDelayKey_ = undefined; - } - this.setTarget(null); - BaseObject.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 {module:ol~Pixel} pixel Pixel. - * @param {function(this: S, (module:ol/Feature|module:ol/render/Feature), - * module:ol/layer/Layer): 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 {module:ol/PluggableMap~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; - } - const coordinate = this.getCoordinateFromPixel(pixel); - opt_options = opt_options !== undefined ? opt_options : {}; - const hitTolerance = opt_options.hitTolerance !== undefined ? - opt_options.hitTolerance * this.frameState_.pixelRatio : 0; - const layerFilter = opt_options.layerFilter !== undefined ? - opt_options.layerFilter : TRUE; - return this.renderer_.forEachFeatureAtCoordinate( - coordinate, this.frameState_, hitTolerance, callback, null, - layerFilter, null); -}; - - -/** - * Get all features that intersect a pixel on the viewport. - * @param {module:ol~Pixel} pixel Pixel. - * @param {module:ol/PluggableMap~AtPixelOptions=} opt_options Optional options. - * @return {Array.} The detected features or - * `null` if none were found. - * @api - */ -PluggableMap.prototype.getFeaturesAtPixel = function(pixel, opt_options) { - let features = null; - this.forEachFeatureAtPixel(pixel, function(feature) { - if (!features) { - features = []; + /** + * @private + * @type {!HTMLElement} + */ + this.overlayContainerStopEvent_ = document.createElement('DIV'); + this.overlayContainerStopEvent_.className = 'ol-overlaycontainer-stopevent'; + const overlayEvents = [ + EventType.CLICK, + EventType.DBLCLICK, + EventType.MOUSEDOWN, + EventType.TOUCHSTART, + EventType.MSPOINTERDOWN, + MapBrowserEventType.POINTERDOWN, + EventType.MOUSEWHEEL, + EventType.WHEEL + ]; + for (let i = 0, ii = overlayEvents.length; i < ii; ++i) { + listen(this.overlayContainerStopEvent_, overlayEvents[i], stopPropagation); } - features.push(feature); - }, opt_options); - return features; -}; + this.viewport_.appendChild(this.overlayContainerStopEvent_); -/** - * 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`. - * @param {module:ol~Pixel} pixel Pixel. - * @param {function(this: S, module:ol/layer/Layer, (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 {module:ol/PluggableMap~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; - } - const options = opt_options || {}; - const hitTolerance = options.hitTolerance !== undefined ? - opt_options.hitTolerance * this.frameState_.pixelRatio : 0; - const layerFilter = options.layerFilter || TRUE; - return this.renderer_.forEachLayerAtPixel( - pixel, this.frameState_, hitTolerance, callback, null, layerFilter, null); -}; - - -/** - * Detect if features intersect a pixel on the viewport. Layers included in the - * detection can be configured through `opt_layerFilter`. - * @param {module:ol~Pixel} pixel Pixel. - * @param {module:ol/PluggableMap~AtPixelOptions=} opt_options Optional options. - * @return {boolean} Is there a feature at the given pixel? - * @template U - * @api - */ -PluggableMap.prototype.hasFeatureAtPixel = function(pixel, opt_options) { - if (!this.frameState_) { - return false; - } - const coordinate = this.getCoordinateFromPixel(pixel); - opt_options = opt_options !== undefined ? opt_options : {}; - const layerFilter = opt_options.layerFilter !== undefined ? opt_options.layerFilter : TRUE; - const hitTolerance = opt_options.hitTolerance !== undefined ? - opt_options.hitTolerance * this.frameState_.pixelRatio : 0; - return this.renderer_.hasFeatureAtCoordinate( - coordinate, this.frameState_, hitTolerance, layerFilter, null); -}; - - -/** - * Returns the coordinate in view projection for a browser event. - * @param {Event} event Event. - * @return {module:ol/coordinate~Coordinate} Coordinate. - * @api - */ -PluggableMap.prototype.getEventCoordinate = function(event) { - return this.getCoordinateFromPixel(this.getEventPixel(event)); -}; - - -/** - * Returns the map pixel position for a browser event relative to the viewport. - * @param {Event} event Event. - * @return {module:ol~Pixel} Pixel. - * @api - */ -PluggableMap.prototype.getEventPixel = function(event) { - const viewportPosition = this.viewport_.getBoundingClientRect(); - const eventPosition = event.changedTouches ? event.changedTouches[0] : 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() { - const 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 - * map view projection. - * @param {module:ol~Pixel} pixel Pixel position in the map viewport. - * @return {module:ol/coordinate~Coordinate} The coordinate for the pixel position. - * @api - */ -PluggableMap.prototype.getCoordinateFromPixel = function(pixel) { - const 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 {module:ol/Collection.} Controls. - * @api - */ -PluggableMap.prototype.getControls = function() { - return this.controls; -}; - - -/** - * Get the map overlays. Modifying this collection changes the overlays - * associated with the map. - * @return {module:ol/Collection.} 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 {module:ol/Overlay} Overlay. - * @api - */ -PluggableMap.prototype.getOverlayById = function(id) { - const 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 {module:ol/Collection.} Interactions. - * @api - */ -PluggableMap.prototype.getInteractions = function() { - return this.interactions; -}; - - -/** - * Get the layergroup associated with this map. - * @return {module:ol/layer/Group} A layer group containing the layers in this map. - * @observable - * @api - */ -PluggableMap.prototype.getLayerGroup = function() { - return ( - /** @type {module:ol/layer/Group} */ (this.get(MapProperty.LAYERGROUP)) - ); -}; - - -/** - * Get the collection of layers associated with this map. - * @return {!module:ol/Collection.} Layers. - * @api - */ -PluggableMap.prototype.getLayers = function() { - const layers = this.getLayerGroup().getLayers(); - return layers; -}; - - -/** - * Get the pixel for a coordinate. This takes a coordinate in the map view - * projection and returns the corresponding pixel. - * @param {module:ol/coordinate~Coordinate} coordinate A map coordinate. - * @return {module:ol~Pixel} A pixel position in the map viewport. - * @api - */ -PluggableMap.prototype.getPixelFromCoordinate = function(coordinate) { - const frameState = this.frameState_; - if (!frameState) { - return null; - } else { - return applyTransform(frameState.coordinateToPixelTransform, coordinate.slice(0, 2)); - } -}; - - -/** - * Get the map renderer. - * @return {module:ol/renderer/Map} Renderer - */ -PluggableMap.prototype.getRenderer = function() { - return this.renderer_; -}; - - -/** - * Get the size of this map. - * @return {module:ol/size~Size|undefined} The size in pixels of the map in the DOM. - * @observable - * @api - */ -PluggableMap.prototype.getSize = function() { - return ( - /** @type {module:ol/size~Size|undefined} */ (this.get(MapProperty.SIZE)) - ); -}; - - -/** - * Get the view associated with this map. A view manages properties such as - * center and resolution. - * @return {module:ol/View} The view that controls this map. - * @observable - * @api - */ -PluggableMap.prototype.getView = function() { - return ( - /** @type {module:ol/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 {module:ol/Tile} tile Tile. - * @param {string} tileSourceKey Tile source key. - * @param {module:ol/coordinate~Coordinate} tileCenter Tile center. - * @param {number} tileResolution Tile resolution. - * @return {number} Tile priority. - */ -PluggableMap.prototype.getTilePriority = function(tile, tileSourceKey, tileCenter, tileResolution) { - // Filter out tiles at higher zoom levels than the current zoom level, or that - // are outside the visible extent. - const frameState = this.frameState_; - if (!frameState || !(tileSourceKey in frameState.wantedTiles)) { - return DROP; - } - if (!frameState.wantedTiles[tileSourceKey][tile.getKey()]) { - return DROP; - } - // Prioritize the highest zoom level tiles closest to the focus. - // Tiles at higher zoom levels are prioritized using Math.log(tileResolution). - // Within a zoom level, tiles are prioritized by the distance in pixels - // between the center of the tile and the focus. The factor of 65536 means - // that the prioritization should behave as desired for tiles up to - // 65536 * Math.log(2) = 45426 pixels from the focus. - const deltaX = tileCenter[0] - frameState.focus[0]; - const deltaY = tileCenter[1] - frameState.focus[1]; - return 65536 * Math.log(tileResolution) + - Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution; -}; - - -/** - * @param {Event} browserEvent Browser event. - * @param {string=} opt_type Type. - */ -PluggableMap.prototype.handleBrowserEvent = function(browserEvent, opt_type) { - const type = opt_type || browserEvent.type; - const mapBrowserEvent = new MapBrowserEvent(type, this, browserEvent); - this.handleMapBrowserEvent(mapBrowserEvent); -}; - - -/** - * @param {module:ol/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; - } - this.focus_ = mapBrowserEvent.coordinate; - mapBrowserEvent.frameState = this.frameState_; - const interactionsArray = this.getInteractions().getArray(); - if (this.dispatchEvent(mapBrowserEvent) !== false) { - for (let i = interactionsArray.length - 1; i >= 0; i--) { - const interaction = interactionsArray[i]; - if (!interaction.getActive()) { - continue; - } - const cont = interaction.handleEvent(mapBrowserEvent); - if (!cont) { - break; - } + /** + * @private + * @type {module:ol/MapBrowserEventHandler} + */ + this.mapBrowserEventHandler_ = new MapBrowserEventHandler(this, options.moveTolerance); + for (const key in MapBrowserEventType) { + listen(this.mapBrowserEventHandler_, MapBrowserEventType[key], + this.handleMapBrowserEvent, this); } - } -}; + /** + * @private + * @type {HTMLElement|Document} + */ + this.keyboardEventTarget_ = optionsInternal.keyboardEventTarget; -/** - * @protected - */ -PluggableMap.prototype.handlePostRender = function() { - - const 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. - const tileQueue = this.tileQueue_; - if (!tileQueue.isEmpty()) { - let maxTotalLoading = this.maxTilesLoading_; - let maxNewLoads = maxTotalLoading; - if (frameState) { - const hints = frameState.viewHints; - if (hints[ViewHint.ANIMATING]) { - maxTotalLoading = this.loadTilesWhileAnimating_ ? 8 : 0; - maxNewLoads = 2; - } - if (hints[ViewHint.INTERACTING]) { - maxTotalLoading = this.loadTilesWhileInteracting_ ? 8 : 0; - maxNewLoads = 2; - } - } - if (tileQueue.getTilesLoading() < maxTotalLoading) { - tileQueue.reprioritize(); // FIXME only call if view has changed - tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); - } - } - - const postRenderFunctions = this.postRenderFunctions_; - for (let i = 0, ii = postRenderFunctions.length; i < ii; ++i) { - postRenderFunctions[i](this, frameState); - } - postRenderFunctions.length = 0; -}; - - -/** - * @private - */ -PluggableMap.prototype.handleSizeChanged_ = function() { - 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. - - let targetElement; - if (this.getTarget()) { - targetElement = this.getTargetElement(); - } - - if (this.keyHandlerKeys_) { - for (let i = 0, ii = this.keyHandlerKeys_.length; i < ii; ++i) { - unlistenByKey(this.keyHandlerKeys_[i]); - } + /** + * @private + * @type {Array.} + */ this.keyHandlerKeys_ = null; + + listen(this.viewport_, EventType.CONTEXTMENU, this.handleBrowserEvent, this); + listen(this.viewport_, EventType.WHEEL, this.handleBrowserEvent, this); + listen(this.viewport_, EventType.MOUSEWHEEL, this.handleBrowserEvent, this); + + /** + * @type {module:ol/Collection.} + * @protected + */ + this.controls = optionsInternal.controls || new Collection(); + + /** + * @type {module:ol/Collection.} + * @protected + */ + this.interactions = optionsInternal.interactions || new Collection(); + + /** + * @type {module:ol/Collection.} + * @private + */ + this.overlays_ = optionsInternal.overlays; + + /** + * A lookup of overlays by id. + * @private + * @type {Object.} + */ + this.overlayIdIndex_ = {}; + + /** + * @type {module:ol/renderer/Map} + * @private + */ + this.renderer_ = this.createRenderer(); + + /** + * @type {function(Event)|undefined} + * @private + */ + this.handleResize_; + + /** + * @private + * @type {module:ol/coordinate~Coordinate} + */ + this.focus_ = null; + + /** + * @private + * @type {!Array.} + */ + this.postRenderFunctions_ = []; + + /** + * @private + * @type {module:ol/TileQueue} + */ + this.tileQueue_ = new TileQueue( + this.getTilePriority.bind(this), + this.handleTileChange_.bind(this)); + + /** + * Uids of features to skip at rendering time. + * @type {Object.} + * @private + */ + this.skippedFeatureUids_ = {}; + + listen( + this, getChangeEventType(MapProperty.LAYERGROUP), + this.handleLayerGroupChanged_, this); + listen(this, getChangeEventType(MapProperty.VIEW), + this.handleViewChanged_, this); + listen(this, getChangeEventType(MapProperty.SIZE), + this.handleSizeChanged_, this); + listen(this, getChangeEventType(MapProperty.TARGET), + this.handleTargetChanged_, this); + + // setProperties will trigger the rendering of the map if the map + // is "defined" already. + this.setProperties(optionsInternal.values); + + this.controls.forEach( + /** + * @param {module:ol/control/Control} control Control. + * @this {module:ol/PluggableMap} + */ + (function(control) { + control.setMap(this); + }).bind(this)); + + listen(this.controls, CollectionEventType.ADD, + /** + * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + */ + function(event) { + event.element.setMap(this); + }, this); + + listen(this.controls, CollectionEventType.REMOVE, + /** + * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + */ + function(event) { + event.element.setMap(null); + }, this); + + this.interactions.forEach( + /** + * @param {module:ol/interaction/Interaction} interaction Interaction. + * @this {module:ol/PluggableMap} + */ + (function(interaction) { + interaction.setMap(this); + }).bind(this)); + + listen(this.interactions, CollectionEventType.ADD, + /** + * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + */ + function(event) { + event.element.setMap(this); + }, this); + + listen(this.interactions, CollectionEventType.REMOVE, + /** + * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + */ + function(event) { + event.element.setMap(null); + }, this); + + this.overlays_.forEach(this.addOverlayInternal_.bind(this)); + + listen(this.overlays_, CollectionEventType.ADD, + /** + * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + */ + function(event) { + this.addOverlayInternal_(/** @type {module:ol/Overlay} */ (event.element)); + }, this); + + listen(this.overlays_, CollectionEventType.REMOVE, + /** + * @param {module:ol/Collection~CollectionEvent} event CollectionEvent. + */ + function(event) { + const overlay = /** @type {module:ol/Overlay} */ (event.element); + const id = overlay.getId(); + if (id !== undefined) { + delete this.overlayIdIndex_[id.toString()]; + } + event.element.setMap(null); + }, this); + } - if (!targetElement) { - this.renderer_.removeLayerRenderers(); - removeNode(this.viewport_); + createRenderer() { + throw new Error('Use a map type that has a createRenderer method'); + } + + /** + * Add the given control to the map. + * @param {module:ol/control/Control} control Control. + * @api + */ + addControl(control) { + this.getControls().push(control); + } + + /** + * Add the given interaction to the map. + * @param {module:ol/interaction/Interaction} interaction Interaction to add. + * @api + */ + addInteraction(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 {module:ol/layer/Base} layer Layer. + * @api + */ + addLayer(layer) { + const layers = this.getLayerGroup().getLayers(); + layers.push(layer); + } + + /** + * Add the given overlay to the map. + * @param {module:ol/Overlay} overlay Overlay. + * @api + */ + addOverlay(overlay) { + this.getOverlays().push(overlay); + } + + /** + * This deals with map's overlay collection changes. + * @param {module:ol/Overlay} overlay Overlay. + * @private + */ + addOverlayInternal_(overlay) { + const id = overlay.getId(); + if (id !== undefined) { + this.overlayIdIndex_[id.toString()] = overlay; + } + overlay.setMap(this); + } + + /** + * + * @inheritDoc + */ + disposeInternal() { + this.mapBrowserEventHandler_.dispose(); + unlisten(this.viewport_, EventType.CONTEXTMENU, this.handleBrowserEvent, this); + unlisten(this.viewport_, EventType.WHEEL, this.handleBrowserEvent, this); + unlisten(this.viewport_, EventType.MOUSEWHEEL, this.handleBrowserEvent, this); if (this.handleResize_ !== undefined) { removeEventListener(EventType.RESIZE, this.handleResize_, false); this.handleResize_ = undefined; } - } else { - targetElement.appendChild(this.viewport_); - - const 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); - addEventListener(EventType.RESIZE, this.handleResize_, false); + if (this.animationDelayKey_) { + cancelAnimationFrame(this.animationDelayKey_); + this.animationDelayKey_ = undefined; } + this.setTarget(null); + BaseObject.prototype.disposeInternal.call(this); } - 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; + /** + * 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 {module:ol~Pixel} pixel Pixel. + * @param {function(this: S, (module:ol/Feature|module:ol/render/Feature), + * module:ol/layer/Layer): 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 {module:ol/PluggableMap~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 + */ + forEachFeatureAtPixel(pixel, callback, opt_options) { + if (!this.frameState_) { + return; + } + const coordinate = this.getCoordinateFromPixel(pixel); + opt_options = opt_options !== undefined ? opt_options : {}; + const hitTolerance = opt_options.hitTolerance !== undefined ? + opt_options.hitTolerance * this.frameState_.pixelRatio : 0; + const layerFilter = opt_options.layerFilter !== undefined ? + opt_options.layerFilter : TRUE; + return this.renderer_.forEachFeatureAtCoordinate( + coordinate, this.frameState_, hitTolerance, callback, null, + layerFilter, null); } - if (this.viewChangeListenerKey_) { - unlistenByKey(this.viewChangeListenerKey_); - this.viewChangeListenerKey_ = null; - } - const view = this.getView(); - if (view) { - this.viewport_.setAttribute('data-view', getUid(view)); - this.viewPropertyListenerKey_ = listen( - view, ObjectEventType.PROPERTYCHANGE, - this.handleViewPropertyChanged_, this); - this.viewChangeListenerKey_ = listen( - view, EventType.CHANGE, - this.handleViewPropertyChanged_, this); - } - this.render(); -}; - -/** - * @private - */ -PluggableMap.prototype.handleLayerGroupChanged_ = function() { - if (this.layerGroupPropertyListenerKeys_) { - this.layerGroupPropertyListenerKeys_.forEach(unlistenByKey); - this.layerGroupPropertyListenerKeys_ = null; + /** + * Get all features that intersect a pixel on the viewport. + * @param {module:ol~Pixel} pixel Pixel. + * @param {module:ol/PluggableMap~AtPixelOptions=} opt_options Optional options. + * @return {Array.} The detected features or + * `null` if none were found. + * @api + */ + getFeaturesAtPixel(pixel, opt_options) { + let features = null; + this.forEachFeatureAtPixel(pixel, function(feature) { + if (!features) { + features = []; + } + features.push(feature); + }, opt_options); + return features; } - const layerGroup = this.getLayerGroup(); - if (layerGroup) { - this.layerGroupPropertyListenerKeys_ = [ - listen( - layerGroup, ObjectEventType.PROPERTYCHANGE, - this.render, this), - listen( - layerGroup, EventType.CHANGE, - this.render, this) + + /** + * 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`. + * @param {module:ol~Pixel} pixel Pixel. + * @param {function(this: S, module:ol/layer/Layer, (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 {module:ol/PluggableMap~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 + */ + forEachLayerAtPixel(pixel, callback, opt_options) { + if (!this.frameState_) { + return; + } + const options = opt_options || {}; + const hitTolerance = options.hitTolerance !== undefined ? + opt_options.hitTolerance * this.frameState_.pixelRatio : 0; + const layerFilter = options.layerFilter || TRUE; + return this.renderer_.forEachLayerAtPixel( + pixel, this.frameState_, hitTolerance, callback, null, layerFilter, null); + } + + /** + * Detect if features intersect a pixel on the viewport. Layers included in the + * detection can be configured through `opt_layerFilter`. + * @param {module:ol~Pixel} pixel Pixel. + * @param {module:ol/PluggableMap~AtPixelOptions=} opt_options Optional options. + * @return {boolean} Is there a feature at the given pixel? + * @template U + * @api + */ + hasFeatureAtPixel(pixel, opt_options) { + if (!this.frameState_) { + return false; + } + const coordinate = this.getCoordinateFromPixel(pixel); + opt_options = opt_options !== undefined ? opt_options : {}; + const layerFilter = opt_options.layerFilter !== undefined ? opt_options.layerFilter : TRUE; + const hitTolerance = opt_options.hitTolerance !== undefined ? + opt_options.hitTolerance * this.frameState_.pixelRatio : 0; + return this.renderer_.hasFeatureAtCoordinate( + coordinate, this.frameState_, hitTolerance, layerFilter, null); + } + + /** + * Returns the coordinate in view projection for a browser event. + * @param {Event} event Event. + * @return {module:ol/coordinate~Coordinate} Coordinate. + * @api + */ + getEventCoordinate(event) { + return this.getCoordinateFromPixel(this.getEventPixel(event)); + } + + /** + * Returns the map pixel position for a browser event relative to the viewport. + * @param {Event} event Event. + * @return {module:ol~Pixel} Pixel. + * @api + */ + getEventPixel(event) { + const viewportPosition = this.viewport_.getBoundingClientRect(); + const eventPosition = event.changedTouches ? event.changedTouches[0] : event; + return [ + eventPosition.clientX - viewportPosition.left, + eventPosition.clientY - viewportPosition.top ]; } - 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_); + /** + * 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 + */ + getTarget() { + return /** @type {HTMLElement|string|undefined} */ (this.get(MapProperty.TARGET)); } - this.animationDelay_(); -}; - -/** - * Request a map rendering (at the next animation frame). - * @api - */ -PluggableMap.prototype.render = function() { - if (this.animationDelayKey_ === undefined) { - this.animationDelayKey_ = requestAnimationFrame(this.animationDelay_); - } -}; - - -/** - * Remove the given control from the map. - * @param {module:ol/control/Control} control Control. - * @return {module:ol/control/Control|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 {module:ol/interaction/Interaction} interaction Interaction to remove. - * @return {module:ol/interaction/Interaction|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 {module:ol/layer/Base} layer Layer. - * @return {module:ol/layer/Base|undefined} The removed layer (or undefined if the - * layer was not found). - * @api - */ -PluggableMap.prototype.removeLayer = function(layer) { - const layers = this.getLayerGroup().getLayers(); - return layers.remove(layer); -}; - - -/** - * Remove the given overlay from the map. - * @param {module:ol/Overlay} overlay Overlay. - * @return {module:ol/Overlay|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) { - let viewState; - - const size = this.getSize(); - const view = this.getView(); - const extent = createEmpty(); - const previousFrameState = this.frameState_; - /** @type {?module:ol/PluggableMap~FrameState} */ - let frameState = null; - if (size !== undefined && hasArea(size) && view && view.isDef()) { - const viewHints = view.getHints(this.frameState_ ? this.frameState_.viewHints : undefined); - const layerStatesArray = this.getLayerGroup().getLayerStatesArray(); - const layerStates = {}; - for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { - layerStates[getUid(layerStatesArray[i].layer)] = layerStatesArray[i]; + /** + * 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 + */ + getTargetElement() { + const target = this.getTarget(); + if (target !== undefined) { + return typeof target === 'string' ? document.getElementById(target) : target; + } else { + return null; } - viewState = view.getState(); - let focus = this.focus_; - if (!focus) { - focus = viewState.center; - const pixelResolution = viewState.resolution / this.pixelRatio_; - focus[0] = Math.round(focus[0] / pixelResolution) * pixelResolution; - focus[1] = Math.round(focus[1] / pixelResolution) * pixelResolution; - } - frameState = /** @type {module:ol/PluggableMap~FrameState} */ ({ - animate: false, - coordinateToPixelTransform: this.coordinateToPixelTransform_, - extent: extent, - focus: focus, - index: this.frameIndex_++, - layerStates: layerStates, - layerStatesArray: layerStatesArray, - pixelRatio: this.pixelRatio_, - pixelToCoordinateTransform: this.pixelToCoordinateTransform_, - postRenderFunctions: [], - size: size, - skippedFeatureUids: this.skippedFeatureUids_, - tileQueue: this.tileQueue_, - time: time, - usedTiles: {}, - viewState: viewState, - viewHints: viewHints, - wantedTiles: {} - }); } - if (frameState) { - frameState.extent = getForViewAndSize(viewState.center, - viewState.resolution, viewState.rotation, frameState.size, extent); + /** + * Get the coordinate for a given pixel. This returns a coordinate in the + * map view projection. + * @param {module:ol~Pixel} pixel Pixel position in the map viewport. + * @return {module:ol/coordinate~Coordinate} The coordinate for the pixel position. + * @api + */ + getCoordinateFromPixel(pixel) { + const frameState = this.frameState_; + if (!frameState) { + return null; + } else { + return applyTransform(frameState.pixelToCoordinateTransform, pixel.slice()); + } } - this.frameState_ = frameState; - this.renderer_.renderFrame(frameState); + /** + * Get the map controls. Modifying this collection changes the controls + * associated with the map. + * @return {module:ol/Collection.} Controls. + * @api + */ + getControls() { + return this.controls; + } - if (frameState) { - if (frameState.animate) { - this.render(); + /** + * Get the map overlays. Modifying this collection changes the overlays + * associated with the map. + * @return {module:ol/Collection.} Overlays. + * @api + */ + getOverlays() { + 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 {module:ol/Overlay} Overlay. + * @api + */ + getOverlayById(id) { + const 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 {module:ol/Collection.} Interactions. + * @api + */ + getInteractions() { + return this.interactions; + } + + /** + * Get the layergroup associated with this map. + * @return {module:ol/layer/Group} A layer group containing the layers in this map. + * @observable + * @api + */ + getLayerGroup() { + return ( + /** @type {module:ol/layer/Group} */ (this.get(MapProperty.LAYERGROUP)) + ); + } + + /** + * Get the collection of layers associated with this map. + * @return {!module:ol/Collection.} Layers. + * @api + */ + getLayers() { + const layers = this.getLayerGroup().getLayers(); + return layers; + } + + /** + * Get the pixel for a coordinate. This takes a coordinate in the map view + * projection and returns the corresponding pixel. + * @param {module:ol/coordinate~Coordinate} coordinate A map coordinate. + * @return {module:ol~Pixel} A pixel position in the map viewport. + * @api + */ + getPixelFromCoordinate(coordinate) { + const frameState = this.frameState_; + if (!frameState) { + return null; + } else { + return applyTransform(frameState.coordinateToPixelTransform, coordinate.slice(0, 2)); } - Array.prototype.push.apply(this.postRenderFunctions_, frameState.postRenderFunctions); + } - if (previousFrameState) { - const 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_); + /** + * Get the map renderer. + * @return {module:ol/renderer/Map} Renderer + */ + getRenderer() { + return this.renderer_; + } + + /** + * Get the size of this map. + * @return {module:ol/size~Size|undefined} The size in pixels of the map in the DOM. + * @observable + * @api + */ + getSize() { + return ( + /** @type {module:ol/size~Size|undefined} */ (this.get(MapProperty.SIZE)) + ); + } + + /** + * Get the view associated with this map. A view manages properties such as + * center and resolution. + * @return {module:ol/View} The view that controls this map. + * @observable + * @api + */ + getView() { + return ( + /** @type {module:ol/View} */ (this.get(MapProperty.VIEW)) + ); + } + + /** + * Get the element that serves as the map viewport. + * @return {HTMLElement} Viewport. + * @api + */ + getViewport() { + 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. + */ + getOverlayContainer() { + 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. + */ + getOverlayContainerStopEvent() { + return this.overlayContainerStopEvent_; + } + + /** + * @param {module:ol/Tile} tile Tile. + * @param {string} tileSourceKey Tile source key. + * @param {module:ol/coordinate~Coordinate} tileCenter Tile center. + * @param {number} tileResolution Tile resolution. + * @return {number} Tile priority. + */ + getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) { + // Filter out tiles at higher zoom levels than the current zoom level, or that + // are outside the visible extent. + const frameState = this.frameState_; + if (!frameState || !(tileSourceKey in frameState.wantedTiles)) { + return DROP; + } + if (!frameState.wantedTiles[tileSourceKey][tile.getKey()]) { + return DROP; + } + // Prioritize the highest zoom level tiles closest to the focus. + // Tiles at higher zoom levels are prioritized using Math.log(tileResolution). + // Within a zoom level, tiles are prioritized by the distance in pixels + // between the center of the tile and the focus. The factor of 65536 means + // that the prioritization should behave as desired for tiles up to + // 65536 * Math.log(2) = 45426 pixels from the focus. + const deltaX = tileCenter[0] - frameState.focus[0]; + const deltaY = tileCenter[1] - frameState.focus[1]; + return 65536 * Math.log(tileResolution) + + Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution; + } + + /** + * @param {Event} browserEvent Browser event. + * @param {string=} opt_type Type. + */ + handleBrowserEvent(browserEvent, opt_type) { + const type = opt_type || browserEvent.type; + const mapBrowserEvent = new MapBrowserEvent(type, this, browserEvent); + this.handleMapBrowserEvent(mapBrowserEvent); + } + + /** + * @param {module:ol/MapBrowserEvent} mapBrowserEvent The event to handle. + */ + handleMapBrowserEvent(mapBrowserEvent) { + if (!this.frameState_) { + // With no view defined, we cannot translate pixels into geographical + // coordinates so interactions cannot be used. + return; + } + this.focus_ = mapBrowserEvent.coordinate; + mapBrowserEvent.frameState = this.frameState_; + const interactionsArray = this.getInteractions().getArray(); + if (this.dispatchEvent(mapBrowserEvent) !== false) { + for (let i = interactionsArray.length - 1; i >= 0; i--) { + const interaction = interactionsArray[i]; + if (!interaction.getActive()) { + continue; + } + const cont = interaction.handleEvent(mapBrowserEvent); + if (!cont) { + break; + } + } + } + } + + /** + * @protected + */ + handlePostRender() { + + const 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. + const tileQueue = this.tileQueue_; + if (!tileQueue.isEmpty()) { + let maxTotalLoading = this.maxTilesLoading_; + let maxNewLoads = maxTotalLoading; + if (frameState) { + const hints = frameState.viewHints; + if (hints[ViewHint.ANIMATING]) { + maxTotalLoading = this.loadTilesWhileAnimating_ ? 8 : 0; + maxNewLoads = 2; + } + if (hints[ViewHint.INTERACTING]) { + maxTotalLoading = this.loadTilesWhileInteracting_ ? 8 : 0; + maxNewLoads = 2; + } + } + if (tileQueue.getTilesLoading() < maxTotalLoading) { + tileQueue.reprioritize(); // FIXME only call if view has changed + tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); } } - const idle = this.previousExtent_ && - !frameState.viewHints[ViewHint.ANIMATING] && - !frameState.viewHints[ViewHint.INTERACTING] && - !equals(frameState.extent, this.previousExtent_); + const postRenderFunctions = this.postRenderFunctions_; + for (let i = 0, ii = postRenderFunctions.length; i < ii; ++i) { + postRenderFunctions[i](this, frameState); + } + postRenderFunctions.length = 0; + } - if (idle) { - this.dispatchEvent(new MapEvent(MapEventType.MOVEEND, this, frameState)); - clone(frameState.extent, this.previousExtent_); + /** + * @private + */ + handleSizeChanged_() { + this.render(); + } + + /** + * @private + */ + handleTargetChanged_() { + // 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. + + let targetElement; + if (this.getTarget()) { + targetElement = this.getTargetElement(); + } + + if (this.keyHandlerKeys_) { + for (let i = 0, ii = this.keyHandlerKeys_.length; i < ii; ++i) { + unlistenByKey(this.keyHandlerKeys_[i]); + } + this.keyHandlerKeys_ = null; + } + + if (!targetElement) { + this.renderer_.removeLayerRenderers(); + removeNode(this.viewport_); + if (this.handleResize_ !== undefined) { + removeEventListener(EventType.RESIZE, this.handleResize_, false); + this.handleResize_ = undefined; + } + } else { + targetElement.appendChild(this.viewport_); + + const 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); + addEventListener(EventType.RESIZE, this.handleResize_, false); + } + } + + this.updateSize(); + // updateSize calls setSize, so no need to call this.render + // ourselves here. + } + + /** + * @private + */ + handleTileChange_() { + this.render(); + } + + /** + * @private + */ + handleViewPropertyChanged_() { + this.render(); + } + + /** + * @private + */ + handleViewChanged_() { + if (this.viewPropertyListenerKey_) { + unlistenByKey(this.viewPropertyListenerKey_); + this.viewPropertyListenerKey_ = null; + } + if (this.viewChangeListenerKey_) { + unlistenByKey(this.viewChangeListenerKey_); + this.viewChangeListenerKey_ = null; + } + const view = this.getView(); + if (view) { + this.viewport_.setAttribute('data-view', getUid(view)); + this.viewPropertyListenerKey_ = listen( + view, ObjectEventType.PROPERTYCHANGE, + this.handleViewPropertyChanged_, this); + this.viewChangeListenerKey_ = listen( + view, EventType.CHANGE, + this.handleViewPropertyChanged_, this); + } + this.render(); + } + + /** + * @private + */ + handleLayerGroupChanged_() { + if (this.layerGroupPropertyListenerKeys_) { + this.layerGroupPropertyListenerKeys_.forEach(unlistenByKey); + this.layerGroupPropertyListenerKeys_ = null; + } + const 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. + */ + isRendered() { + return !!this.frameState_; + } + + /** + * Requests an immediate render in a synchronous manner. + * @api + */ + renderSync() { + if (this.animationDelayKey_) { + cancelAnimationFrame(this.animationDelayKey_); + } + this.animationDelay_(); + } + + /** + * Request a map rendering (at the next animation frame). + * @api + */ + render() { + if (this.animationDelayKey_ === undefined) { + this.animationDelayKey_ = requestAnimationFrame(this.animationDelay_); } } - this.dispatchEvent(new MapEvent(MapEventType.POSTRENDER, this, frameState)); - - setTimeout(this.handlePostRender.bind(this), 0); - -}; - - -/** - * Sets the layergroup of this map. - * @param {module:ol/layer/Group} 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 {module:ol/size~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); -}; - - -/** - * Set the view for this map. - * @param {module:ol/View} view The view that controls this map. - * @observable - * @api - */ -PluggableMap.prototype.setView = function(view) { - this.set(MapProperty.VIEW, view); -}; - - -/** - * @param {module:ol/Feature} feature Feature. - */ -PluggableMap.prototype.skipFeature = function(feature) { - const featureUid = getUid(feature).toString(); - this.skippedFeatureUids_[featureUid] = true; - this.render(); -}; - - -/** - * Force a recalculation of the map viewport size. This should be called when - * third-party code changes the size of the map viewport. - * @api - */ -PluggableMap.prototype.updateSize = function() { - const targetElement = this.getTargetElement(); - - if (!targetElement) { - this.setSize(undefined); - } else { - const computedStyle = getComputedStyle(targetElement); - this.setSize([ - targetElement.offsetWidth - - parseFloat(computedStyle['borderLeftWidth']) - - parseFloat(computedStyle['paddingLeft']) - - parseFloat(computedStyle['paddingRight']) - - parseFloat(computedStyle['borderRightWidth']), - targetElement.offsetHeight - - parseFloat(computedStyle['borderTopWidth']) - - parseFloat(computedStyle['paddingTop']) - - parseFloat(computedStyle['paddingBottom']) - - parseFloat(computedStyle['borderBottomWidth']) - ]); + /** + * Remove the given control from the map. + * @param {module:ol/control/Control} control Control. + * @return {module:ol/control/Control|undefined} The removed control (or undefined + * if the control was not found). + * @api + */ + removeControl(control) { + return this.getControls().remove(control); } -}; + /** + * Remove the given interaction from the map. + * @param {module:ol/interaction/Interaction} interaction Interaction to remove. + * @return {module:ol/interaction/Interaction|undefined} The removed interaction (or + * undefined if the interaction was not found). + * @api + */ + removeInteraction(interaction) { + return this.getInteractions().remove(interaction); + } -/** - * @param {module:ol/Feature} feature Feature. - */ -PluggableMap.prototype.unskipFeature = function(feature) { - const featureUid = getUid(feature).toString(); - delete this.skippedFeatureUids_[featureUid]; - this.render(); -}; + /** + * Removes the given layer from the map. + * @param {module:ol/layer/Base} layer Layer. + * @return {module:ol/layer/Base|undefined} The removed layer (or undefined if the + * layer was not found). + * @api + */ + removeLayer(layer) { + const layers = this.getLayerGroup().getLayers(); + return layers.remove(layer); + } + + /** + * Remove the given overlay from the map. + * @param {module:ol/Overlay} overlay Overlay. + * @return {module:ol/Overlay|undefined} The removed overlay (or undefined + * if the overlay was not found). + * @api + */ + removeOverlay(overlay) { + return this.getOverlays().remove(overlay); + } + + /** + * @param {number} time Time. + * @private + */ + renderFrame_(time) { + let viewState; + + const size = this.getSize(); + const view = this.getView(); + const extent = createEmpty(); + const previousFrameState = this.frameState_; + /** @type {?module:ol/PluggableMap~FrameState} */ + let frameState = null; + if (size !== undefined && hasArea(size) && view && view.isDef()) { + const viewHints = view.getHints(this.frameState_ ? this.frameState_.viewHints : undefined); + const layerStatesArray = this.getLayerGroup().getLayerStatesArray(); + const layerStates = {}; + for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { + layerStates[getUid(layerStatesArray[i].layer)] = layerStatesArray[i]; + } + viewState = view.getState(); + let focus = this.focus_; + if (!focus) { + focus = viewState.center; + const pixelResolution = viewState.resolution / this.pixelRatio_; + focus[0] = Math.round(focus[0] / pixelResolution) * pixelResolution; + focus[1] = Math.round(focus[1] / pixelResolution) * pixelResolution; + } + frameState = /** @type {module:ol/PluggableMap~FrameState} */ ({ + animate: false, + coordinateToPixelTransform: this.coordinateToPixelTransform_, + extent: extent, + focus: focus, + index: this.frameIndex_++, + layerStates: layerStates, + layerStatesArray: layerStatesArray, + pixelRatio: this.pixelRatio_, + pixelToCoordinateTransform: this.pixelToCoordinateTransform_, + postRenderFunctions: [], + size: size, + skippedFeatureUids: this.skippedFeatureUids_, + tileQueue: this.tileQueue_, + time: time, + usedTiles: {}, + viewState: viewState, + viewHints: viewHints, + wantedTiles: {} + }); + } + + if (frameState) { + frameState.extent = getForViewAndSize(viewState.center, + viewState.resolution, viewState.rotation, frameState.size, extent); + } + + this.frameState_ = frameState; + this.renderer_.renderFrame(frameState); + + if (frameState) { + if (frameState.animate) { + this.render(); + } + Array.prototype.push.apply(this.postRenderFunctions_, frameState.postRenderFunctions); + + if (previousFrameState) { + const 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_); + } + } + + const 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)); + + setTimeout(this.handlePostRender.bind(this), 0); + + } + + /** + * Sets the layergroup of this map. + * @param {module:ol/layer/Group} layerGroup A layer group containing the layers in this map. + * @observable + * @api + */ + setLayerGroup(layerGroup) { + this.set(MapProperty.LAYERGROUP, layerGroup); + } + + /** + * Set the size of this map. + * @param {module:ol/size~Size|undefined} size The size in pixels of the map in the DOM. + * @observable + * @api + */ + setSize(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 + */ + setTarget(target) { + this.set(MapProperty.TARGET, target); + } + + /** + * Set the view for this map. + * @param {module:ol/View} view The view that controls this map. + * @observable + * @api + */ + setView(view) { + this.set(MapProperty.VIEW, view); + } + + /** + * @param {module:ol/Feature} feature Feature. + */ + skipFeature(feature) { + const featureUid = getUid(feature).toString(); + this.skippedFeatureUids_[featureUid] = true; + this.render(); + } + + /** + * Force a recalculation of the map viewport size. This should be called when + * third-party code changes the size of the map viewport. + * @api + */ + updateSize() { + const targetElement = this.getTargetElement(); + + if (!targetElement) { + this.setSize(undefined); + } else { + const computedStyle = getComputedStyle(targetElement); + this.setSize([ + targetElement.offsetWidth - + parseFloat(computedStyle['borderLeftWidth']) - + parseFloat(computedStyle['paddingLeft']) - + parseFloat(computedStyle['paddingRight']) - + parseFloat(computedStyle['borderRightWidth']), + targetElement.offsetHeight - + parseFloat(computedStyle['borderTopWidth']) - + parseFloat(computedStyle['paddingTop']) - + parseFloat(computedStyle['paddingBottom']) - + parseFloat(computedStyle['borderBottomWidth']) + ]); + } + } + + /** + * @param {module:ol/Feature} feature Feature. + */ + unskipFeature(feature) { + const featureUid = getUid(feature).toString(); + delete this.skippedFeatureUids_[featureUid]; + this.render(); + } +} + +inherits(PluggableMap, BaseObject); /** diff --git a/src/ol/control/Attribution.js b/src/ol/control/Attribution.js index 2f000ed2c9..2699c0d489 100644 --- a/src/ol/control/Attribution.js +++ b/src/ol/control/Attribution.js @@ -47,163 +47,272 @@ import {visibleAtResolution} from '../layer/Layer.js'; * @param {module:ol/control/Attribution~Options=} opt_options Attribution options. * @api */ -const Attribution = function(opt_options) { +class Attribution { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - /** - * @private - * @type {Element} - */ - this.ulElement_ = document.createElement('UL'); - - /** - * @private - * @type {boolean} - */ - this.collapsed_ = options.collapsed !== undefined ? options.collapsed : true; - - /** - * @private - * @type {boolean} - */ - this.collapsible_ = options.collapsible !== undefined ? - options.collapsible : true; - - if (!this.collapsible_) { - this.collapsed_ = false; - } - - const className = options.className !== undefined ? options.className : 'ol-attribution'; - - const tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Attributions'; - - const collapseLabel = options.collapseLabel !== undefined ? options.collapseLabel : '\u00BB'; - - if (typeof collapseLabel === 'string') { /** * @private * @type {Element} */ - this.collapseLabel_ = document.createElement('span'); - this.collapseLabel_.textContent = collapseLabel; - } else { - this.collapseLabel_ = collapseLabel; - } + this.ulElement_ = document.createElement('UL'); - const label = options.label !== undefined ? options.label : 'i'; - - if (typeof label === 'string') { /** * @private - * @type {Element} + * @type {boolean} */ - this.label_ = document.createElement('span'); - this.label_.textContent = label; - } else { - this.label_ = label; + this.collapsed_ = options.collapsed !== undefined ? options.collapsed : true; + + /** + * @private + * @type {boolean} + */ + this.collapsible_ = options.collapsible !== undefined ? + options.collapsible : true; + + if (!this.collapsible_) { + this.collapsed_ = false; + } + + const className = options.className !== undefined ? options.className : 'ol-attribution'; + + const tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Attributions'; + + const collapseLabel = options.collapseLabel !== undefined ? options.collapseLabel : '\u00BB'; + + if (typeof collapseLabel === 'string') { + /** + * @private + * @type {Element} + */ + this.collapseLabel_ = document.createElement('span'); + this.collapseLabel_.textContent = collapseLabel; + } else { + this.collapseLabel_ = collapseLabel; + } + + const label = options.label !== undefined ? options.label : 'i'; + + if (typeof label === 'string') { + /** + * @private + * @type {Element} + */ + this.label_ = document.createElement('span'); + this.label_.textContent = label; + } else { + this.label_ = label; + } + + + const activeLabel = (this.collapsible_ && !this.collapsed_) ? + this.collapseLabel_ : this.label_; + const button = document.createElement('button'); + button.setAttribute('type', 'button'); + button.title = tipLabel; + button.appendChild(activeLabel); + + listen(button, EventType.CLICK, this.handleClick_, this); + + const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL + + (this.collapsed_ && this.collapsible_ ? ' ' + CLASS_COLLAPSED : '') + + (this.collapsible_ ? '' : ' ol-uncollapsible'); + const element = document.createElement('div'); + element.className = cssClasses; + element.appendChild(this.ulElement_); + element.appendChild(button); + + Control.call(this, { + element: element, + render: options.render || render, + target: options.target + }); + + /** + * A list of currently rendered resolutions. + * @type {Array.} + * @private + */ + this.renderedAttributions_ = []; + + /** + * @private + * @type {boolean} + */ + this.renderedVisible_ = true; + } - - const activeLabel = (this.collapsible_ && !this.collapsed_) ? - this.collapseLabel_ : this.label_; - const button = document.createElement('button'); - button.setAttribute('type', 'button'); - button.title = tipLabel; - button.appendChild(activeLabel); - - listen(button, EventType.CLICK, this.handleClick_, this); - - const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL + - (this.collapsed_ && this.collapsible_ ? ' ' + CLASS_COLLAPSED : '') + - (this.collapsible_ ? '' : ' ol-uncollapsible'); - const element = document.createElement('div'); - element.className = cssClasses; - element.appendChild(this.ulElement_); - element.appendChild(button); - - Control.call(this, { - element: element, - render: options.render || render, - target: options.target - }); - /** - * A list of currently rendered resolutions. - * @type {Array.} + * Get a list of visible attributions. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @return {Array.} Attributions. * @private */ - this.renderedAttributions_ = []; + getSourceAttributions_(frameState) { + /** + * Used to determine if an attribution already exists. + * @type {!Object.} + */ + const lookup = {}; - /** - * @private - * @type {boolean} - */ - this.renderedVisible_ = true; + /** + * A list of visible attributions. + * @type {Array.} + */ + const visibleAttributions = []; -}; + const layerStatesArray = frameState.layerStatesArray; + const resolution = frameState.viewState.resolution; + for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { + const layerState = layerStatesArray[i]; + if (!visibleAtResolution(layerState, resolution)) { + continue; + } -inherits(Attribution, Control); + const source = layerState.layer.getSource(); + if (!source) { + continue; + } + const attributionGetter = source.getAttributions(); + if (!attributionGetter) { + continue; + } -/** - * Get a list of visible attributions. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @return {Array.} Attributions. - * @private - */ -Attribution.prototype.getSourceAttributions_ = function(frameState) { - /** - * Used to determine if an attribution already exists. - * @type {!Object.} - */ - const lookup = {}; + const attributions = attributionGetter(frameState); + if (!attributions) { + continue; + } - /** - * A list of visible attributions. - * @type {Array.} - */ - const visibleAttributions = []; - - const layerStatesArray = frameState.layerStatesArray; - const resolution = frameState.viewState.resolution; - for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { - const layerState = layerStatesArray[i]; - if (!visibleAtResolution(layerState, resolution)) { - continue; - } - - const source = layerState.layer.getSource(); - if (!source) { - continue; - } - - const attributionGetter = source.getAttributions(); - if (!attributionGetter) { - continue; - } - - const attributions = attributionGetter(frameState); - if (!attributions) { - continue; - } - - if (Array.isArray(attributions)) { - for (let j = 0, jj = attributions.length; j < jj; ++j) { - if (!(attributions[j] in lookup)) { - visibleAttributions.push(attributions[j]); - lookup[attributions[j]] = true; + if (Array.isArray(attributions)) { + for (let j = 0, jj = attributions.length; j < jj; ++j) { + if (!(attributions[j] in lookup)) { + visibleAttributions.push(attributions[j]); + lookup[attributions[j]] = true; + } + } + } else { + if (!(attributions in lookup)) { + visibleAttributions.push(attributions); + lookup[attributions] = true; } } - } else { - if (!(attributions in lookup)) { - visibleAttributions.push(attributions); - lookup[attributions] = true; + } + return visibleAttributions; + } + + /** + * @private + * @param {?module:ol/PluggableMap~FrameState} frameState Frame state. + */ + updateElement_(frameState) { + if (!frameState) { + if (this.renderedVisible_) { + this.element.style.display = 'none'; + this.renderedVisible_ = false; } + return; + } + + const attributions = this.getSourceAttributions_(frameState); + + const visible = attributions.length > 0; + if (this.renderedVisible_ != visible) { + this.element.style.display = visible ? '' : 'none'; + this.renderedVisible_ = visible; + } + + if (equals(attributions, this.renderedAttributions_)) { + return; + } + + removeChildren(this.ulElement_); + + // append the attributions + for (let i = 0, ii = attributions.length; i < ii; ++i) { + const element = document.createElement('LI'); + element.innerHTML = attributions[i]; + this.ulElement_.appendChild(element); + } + + this.renderedAttributions_ = attributions; + } + + /** + * @param {MouseEvent} event The event to handle + * @private + */ + handleClick_(event) { + event.preventDefault(); + this.handleToggle_(); + } + + /** + * @private + */ + handleToggle_() { + this.element.classList.toggle(CLASS_COLLAPSED); + if (this.collapsed_) { + replaceNode(this.collapseLabel_, this.label_); + } else { + replaceNode(this.label_, this.collapseLabel_); + } + this.collapsed_ = !this.collapsed_; + } + + /** + * Return `true` if the attribution is collapsible, `false` otherwise. + * @return {boolean} True if the widget is collapsible. + * @api + */ + getCollapsible() { + return this.collapsible_; + } + + /** + * Set whether the attribution should be collapsible. + * @param {boolean} collapsible True if the widget is collapsible. + * @api + */ + setCollapsible(collapsible) { + if (this.collapsible_ === collapsible) { + return; + } + this.collapsible_ = collapsible; + this.element.classList.toggle('ol-uncollapsible'); + if (!collapsible && this.collapsed_) { + this.handleToggle_(); } } - return visibleAttributions; -}; + + /** + * Collapse or expand the attribution according to the passed parameter. Will + * not do anything if the attribution isn't collapsible or if the current + * collapsed state is already the one requested. + * @param {boolean} collapsed True if the widget is collapsed. + * @api + */ + setCollapsed(collapsed) { + if (!this.collapsible_ || this.collapsed_ === collapsed) { + return; + } + this.handleToggle_(); + } + + /** + * Return `true` when the attribution is currently collapsed or `false` + * otherwise. + * @return {boolean} True if the widget is collapsed. + * @api + */ + getCollapsed() { + return this.collapsed_; + } +} + +inherits(Attribution, Control); /** @@ -217,117 +326,4 @@ export function render(mapEvent) { } -/** - * @private - * @param {?module:ol/PluggableMap~FrameState} frameState Frame state. - */ -Attribution.prototype.updateElement_ = function(frameState) { - if (!frameState) { - if (this.renderedVisible_) { - this.element.style.display = 'none'; - this.renderedVisible_ = false; - } - return; - } - - const attributions = this.getSourceAttributions_(frameState); - - const visible = attributions.length > 0; - if (this.renderedVisible_ != visible) { - this.element.style.display = visible ? '' : 'none'; - this.renderedVisible_ = visible; - } - - if (equals(attributions, this.renderedAttributions_)) { - return; - } - - removeChildren(this.ulElement_); - - // append the attributions - for (let i = 0, ii = attributions.length; i < ii; ++i) { - const element = document.createElement('LI'); - element.innerHTML = attributions[i]; - this.ulElement_.appendChild(element); - } - - this.renderedAttributions_ = attributions; -}; - - -/** - * @param {MouseEvent} event The event to handle - * @private - */ -Attribution.prototype.handleClick_ = function(event) { - event.preventDefault(); - this.handleToggle_(); -}; - - -/** - * @private - */ -Attribution.prototype.handleToggle_ = function() { - this.element.classList.toggle(CLASS_COLLAPSED); - if (this.collapsed_) { - replaceNode(this.collapseLabel_, this.label_); - } else { - replaceNode(this.label_, this.collapseLabel_); - } - this.collapsed_ = !this.collapsed_; -}; - - -/** - * Return `true` if the attribution is collapsible, `false` otherwise. - * @return {boolean} True if the widget is collapsible. - * @api - */ -Attribution.prototype.getCollapsible = function() { - return this.collapsible_; -}; - - -/** - * Set whether the attribution should be collapsible. - * @param {boolean} collapsible True if the widget is collapsible. - * @api - */ -Attribution.prototype.setCollapsible = function(collapsible) { - if (this.collapsible_ === collapsible) { - return; - } - this.collapsible_ = collapsible; - this.element.classList.toggle('ol-uncollapsible'); - if (!collapsible && this.collapsed_) { - this.handleToggle_(); - } -}; - - -/** - * Collapse or expand the attribution according to the passed parameter. Will - * not do anything if the attribution isn't collapsible or if the current - * collapsed state is already the one requested. - * @param {boolean} collapsed True if the widget is collapsed. - * @api - */ -Attribution.prototype.setCollapsed = function(collapsed) { - if (!this.collapsible_ || this.collapsed_ === collapsed) { - return; - } - this.handleToggle_(); -}; - - -/** - * Return `true` when the attribution is currently collapsed or `false` - * otherwise. - * @return {boolean} True if the widget is collapsed. - * @api - */ -Attribution.prototype.getCollapsed = function() { - return this.collapsed_; -}; export default Attribution; diff --git a/src/ol/control/Control.js b/src/ol/control/Control.js index 0d63ca7b6c..4ce8985d46 100644 --- a/src/ol/control/Control.js +++ b/src/ol/control/Control.js @@ -49,108 +49,108 @@ import {listen, unlistenByKey} from '../events.js'; * @param {module:ol/control/Control~Options} options Control options. * @api */ -const Control = function(options) { +class Control { + constructor(options) { - BaseObject.call(this); + BaseObject.call(this); - /** - * @protected - * @type {Element} - */ - this.element = options.element ? options.element : null; + /** + * @protected + * @type {Element} + */ + this.element = options.element ? options.element : null; - /** - * @private - * @type {Element} - */ - this.target_ = null; + /** + * @private + * @type {Element} + */ + this.target_ = null; - /** - * @private - * @type {module:ol/PluggableMap} - */ - this.map_ = null; + /** + * @private + * @type {module:ol/PluggableMap} + */ + this.map_ = null; - /** - * @protected - * @type {!Array.} - */ - this.listenerKeys = []; + /** + * @protected + * @type {!Array.} + */ + this.listenerKeys = []; - /** - * @type {function(module:ol/MapEvent)} - */ - this.render = options.render ? options.render : UNDEFINED; + /** + * @type {function(module:ol/MapEvent)} + */ + this.render = options.render ? options.render : UNDEFINED; - if (options.target) { - this.setTarget(options.target); - } + if (options.target) { + this.setTarget(options.target); + } -}; + } + + /** + * @inheritDoc + */ + disposeInternal() { + removeNode(this.element); + BaseObject.prototype.disposeInternal.call(this); + } + + /** + * Get the map associated with this control. + * @return {module:ol/PluggableMap} Map. + * @api + */ + getMap() { + return this.map_; + } + + /** + * Remove the control from its current map and attach it to the new map. + * Subclasses may set up event handlers to get notified about changes to + * the map here. + * @param {module:ol/PluggableMap} map Map. + * @api + */ + setMap(map) { + if (this.map_) { + removeNode(this.element); + } + for (let i = 0, ii = this.listenerKeys.length; i < ii; ++i) { + unlistenByKey(this.listenerKeys[i]); + } + this.listenerKeys.length = 0; + this.map_ = map; + if (this.map_) { + const target = this.target_ ? + this.target_ : map.getOverlayContainerStopEvent(); + target.appendChild(this.element); + if (this.render !== UNDEFINED) { + this.listenerKeys.push(listen(map, + MapEventType.POSTRENDER, this.render, this)); + } + map.render(); + } + } + + /** + * This function is used to set a target element for the control. It has no + * effect if it is called after the control has been added to the map (i.e. + * after `setMap` is called on the control). If no `target` is set in the + * options passed to the control constructor and if `setTarget` is not called + * then the control is added to the map's overlay container. + * @param {Element|string} target Target. + * @api + */ + setTarget(target) { + this.target_ = typeof target === 'string' ? + document.getElementById(target) : + target; + } +} inherits(Control, BaseObject); -/** - * @inheritDoc - */ -Control.prototype.disposeInternal = function() { - removeNode(this.element); - BaseObject.prototype.disposeInternal.call(this); -}; - - -/** - * Get the map associated with this control. - * @return {module:ol/PluggableMap} Map. - * @api - */ -Control.prototype.getMap = function() { - return this.map_; -}; - - -/** - * Remove the control from its current map and attach it to the new map. - * Subclasses may set up event handlers to get notified about changes to - * the map here. - * @param {module:ol/PluggableMap} map Map. - * @api - */ -Control.prototype.setMap = function(map) { - if (this.map_) { - removeNode(this.element); - } - for (let i = 0, ii = this.listenerKeys.length; i < ii; ++i) { - unlistenByKey(this.listenerKeys[i]); - } - this.listenerKeys.length = 0; - this.map_ = map; - if (this.map_) { - const target = this.target_ ? - this.target_ : map.getOverlayContainerStopEvent(); - target.appendChild(this.element); - if (this.render !== UNDEFINED) { - this.listenerKeys.push(listen(map, - MapEventType.POSTRENDER, this.render, this)); - } - map.render(); - } -}; - - -/** - * This function is used to set a target element for the control. It has no - * effect if it is called after the control has been added to the map (i.e. - * after `setMap` is called on the control). If no `target` is set in the - * options passed to the control constructor and if `setTarget` is not called - * then the control is added to the map's overlay container. - * @param {Element|string} target Target. - * @api - */ -Control.prototype.setTarget = function(target) { - this.target_ = typeof target === 'string' ? - document.getElementById(target) : - target; -}; export default Control; diff --git a/src/ol/control/FullScreen.js b/src/ol/control/FullScreen.js index 74b0459f40..2abd9ac2d9 100644 --- a/src/ol/control/FullScreen.js +++ b/src/ol/control/FullScreen.js @@ -67,149 +67,148 @@ const getChangeType = (function() { * @param {module:ol/control/FullScreen~Options=} opt_options Options. * @api */ -const FullScreen = function(opt_options) { +class FullScreen { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; + + /** + * @private + * @type {string} + */ + this.cssClassName_ = options.className !== undefined ? options.className : + 'ol-full-screen'; + + const label = options.label !== undefined ? options.label : '\u2922'; + + /** + * @private + * @type {Element} + */ + this.labelNode_ = typeof label === 'string' ? + document.createTextNode(label) : label; + + const labelActive = options.labelActive !== undefined ? options.labelActive : '\u00d7'; + + /** + * @private + * @type {Element} + */ + this.labelActiveNode_ = typeof labelActive === 'string' ? + document.createTextNode(labelActive) : labelActive; + + const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen'; + const button = document.createElement('button'); + button.className = this.cssClassName_ + '-' + isFullScreen(); + button.setAttribute('type', 'button'); + button.title = tipLabel; + button.appendChild(this.labelNode_); + + listen(button, EventType.CLICK, + this.handleClick_, this); + + const cssClasses = this.cssClassName_ + ' ' + CLASS_UNSELECTABLE + + ' ' + CLASS_CONTROL + ' ' + + (!isFullScreenSupported() ? CLASS_UNSUPPORTED : ''); + const element = document.createElement('div'); + element.className = cssClasses; + element.appendChild(button); + + Control.call(this, { + element: element, + target: options.target + }); + + /** + * @private + * @type {boolean} + */ + this.keys_ = options.keys !== undefined ? options.keys : false; + + /** + * @private + * @type {Element|string|undefined} + */ + this.source_ = options.source; + + } + + /** + * @param {MouseEvent} event The event to handle + * @private + */ + handleClick_(event) { + event.preventDefault(); + this.handleFullScreen_(); + } /** * @private - * @type {string} */ - this.cssClassName_ = options.className !== undefined ? options.className : - 'ol-full-screen'; + handleFullScreen_() { + if (!isFullScreenSupported()) { + return; + } + const map = this.getMap(); + if (!map) { + return; + } + if (isFullScreen()) { + exitFullScreen(); + } else { + let element; + if (this.source_) { + element = typeof this.source_ === 'string' ? + document.getElementById(this.source_) : + this.source_; + } else { + element = map.getTargetElement(); + } + if (this.keys_) { + requestFullScreenWithKeys(element); - const label = options.label !== undefined ? options.label : '\u2922'; + } else { + requestFullScreen(element); + } + } + } /** * @private - * @type {Element} */ - this.labelNode_ = typeof label === 'string' ? - document.createTextNode(label) : label; - - const labelActive = options.labelActive !== undefined ? options.labelActive : '\u00d7'; + handleFullScreenChange_() { + const button = this.element.firstElementChild; + const map = this.getMap(); + if (isFullScreen()) { + button.className = this.cssClassName_ + '-true'; + replaceNode(this.labelActiveNode_, this.labelNode_); + } else { + button.className = this.cssClassName_ + '-false'; + replaceNode(this.labelNode_, this.labelActiveNode_); + } + if (map) { + map.updateSize(); + } + } /** - * @private - * @type {Element} + * @inheritDoc + * @api */ - this.labelActiveNode_ = typeof labelActive === 'string' ? - document.createTextNode(labelActive) : labelActive; - - const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen'; - const button = document.createElement('button'); - button.className = this.cssClassName_ + '-' + isFullScreen(); - button.setAttribute('type', 'button'); - button.title = tipLabel; - button.appendChild(this.labelNode_); - - listen(button, EventType.CLICK, - this.handleClick_, this); - - const cssClasses = this.cssClassName_ + ' ' + CLASS_UNSELECTABLE + - ' ' + CLASS_CONTROL + ' ' + - (!isFullScreenSupported() ? CLASS_UNSUPPORTED : ''); - const element = document.createElement('div'); - element.className = cssClasses; - element.appendChild(button); - - Control.call(this, { - element: element, - target: options.target - }); - - /** - * @private - * @type {boolean} - */ - this.keys_ = options.keys !== undefined ? options.keys : false; - - /** - * @private - * @type {Element|string|undefined} - */ - this.source_ = options.source; - -}; + setMap(map) { + Control.prototype.setMap.call(this, map); + if (map) { + this.listenerKeys.push(listen(document, + getChangeType(), + this.handleFullScreenChange_, this) + ); + } + } +} inherits(FullScreen, Control); -/** - * @param {MouseEvent} event The event to handle - * @private - */ -FullScreen.prototype.handleClick_ = function(event) { - event.preventDefault(); - this.handleFullScreen_(); -}; - - -/** - * @private - */ -FullScreen.prototype.handleFullScreen_ = function() { - if (!isFullScreenSupported()) { - return; - } - const map = this.getMap(); - if (!map) { - return; - } - if (isFullScreen()) { - exitFullScreen(); - } else { - let element; - if (this.source_) { - element = typeof this.source_ === 'string' ? - document.getElementById(this.source_) : - this.source_; - } else { - element = map.getTargetElement(); - } - if (this.keys_) { - requestFullScreenWithKeys(element); - - } else { - requestFullScreen(element); - } - } -}; - - -/** - * @private - */ -FullScreen.prototype.handleFullScreenChange_ = function() { - const button = this.element.firstElementChild; - const map = this.getMap(); - if (isFullScreen()) { - button.className = this.cssClassName_ + '-true'; - replaceNode(this.labelActiveNode_, this.labelNode_); - } else { - button.className = this.cssClassName_ + '-false'; - replaceNode(this.labelNode_, this.labelActiveNode_); - } - if (map) { - map.updateSize(); - } -}; - - -/** - * @inheritDoc - * @api - */ -FullScreen.prototype.setMap = function(map) { - Control.prototype.setMap.call(this, map); - if (map) { - this.listenerKeys.push(listen(document, - getChangeType(), - this.handleFullScreenChange_, this) - ); - } -}; - /** * @return {boolean} Fullscreen is supported by the current platform. */ diff --git a/src/ol/control/MousePosition.js b/src/ol/control/MousePosition.js index 811d7b7170..b4c17da69c 100644 --- a/src/ol/control/MousePosition.js +++ b/src/ol/control/MousePosition.js @@ -52,67 +52,197 @@ const COORDINATE_FORMAT = 'coordinateFormat'; * options. * @api */ -const MousePosition = function(opt_options) { +class MousePosition { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - const element = document.createElement('DIV'); - element.className = options.className !== undefined ? options.className : 'ol-mouse-position'; + const element = document.createElement('DIV'); + element.className = options.className !== undefined ? options.className : 'ol-mouse-position'; - Control.call(this, { - element: element, - render: options.render || render, - target: options.target - }); + Control.call(this, { + element: element, + render: options.render || render, + target: options.target + }); - listen(this, - getChangeEventType(PROJECTION), - this.handleProjectionChanged_, this); + listen(this, + getChangeEventType(PROJECTION), + this.handleProjectionChanged_, this); + + if (options.coordinateFormat) { + this.setCoordinateFormat(options.coordinateFormat); + } + if (options.projection) { + this.setProjection(options.projection); + } + + /** + * @private + * @type {string} + */ + this.undefinedHTML_ = 'undefinedHTML' in options ? options.undefinedHTML : ' '; + + /** + * @private + * @type {boolean} + */ + this.renderOnMouseOut_ = !!this.undefinedHTML_; + + /** + * @private + * @type {string} + */ + this.renderedHTML_ = element.innerHTML; + + /** + * @private + * @type {module:ol/proj/Projection} + */ + this.mapProjection_ = null; + + /** + * @private + * @type {?module:ol/proj~TransformFunction} + */ + this.transform_ = null; + + /** + * @private + * @type {module:ol~Pixel} + */ + this.lastMouseMovePixel_ = null; - if (options.coordinateFormat) { - this.setCoordinateFormat(options.coordinateFormat); - } - if (options.projection) { - this.setProjection(options.projection); } /** * @private - * @type {string} */ - this.undefinedHTML_ = 'undefinedHTML' in options ? options.undefinedHTML : ' '; + handleProjectionChanged_() { + this.transform_ = null; + } /** - * @private - * @type {boolean} + * Return the coordinate format type used to render the current position or + * undefined. + * @return {module:ol/coordinate~CoordinateFormat|undefined} The format to render the current + * position in. + * @observable + * @api */ - this.renderOnMouseOut_ = !!this.undefinedHTML_; + getCoordinateFormat() { + return ( + /** @type {module:ol/coordinate~CoordinateFormat|undefined} */ (this.get(COORDINATE_FORMAT)) + ); + } /** - * @private - * @type {string} + * Return the projection that is used to report the mouse position. + * @return {module:ol/proj/Projection|undefined} The projection to report mouse + * position in. + * @observable + * @api */ - this.renderedHTML_ = element.innerHTML; + getProjection() { + return ( + /** @type {module:ol/proj/Projection|undefined} */ (this.get(PROJECTION)) + ); + } /** - * @private - * @type {module:ol/proj/Projection} + * @param {Event} event Browser event. + * @protected */ - this.mapProjection_ = null; + handleMouseMove(event) { + const map = this.getMap(); + this.lastMouseMovePixel_ = map.getEventPixel(event); + this.updateHTML_(this.lastMouseMovePixel_); + } /** - * @private - * @type {?module:ol/proj~TransformFunction} + * @param {Event} event Browser event. + * @protected */ - this.transform_ = null; + handleMouseOut(event) { + this.updateHTML_(null); + this.lastMouseMovePixel_ = null; + } /** - * @private - * @type {module:ol~Pixel} + * @inheritDoc + * @api */ - this.lastMouseMovePixel_ = null; + setMap(map) { + Control.prototype.setMap.call(this, map); + if (map) { + const viewport = map.getViewport(); + this.listenerKeys.push( + listen(viewport, EventType.MOUSEMOVE, this.handleMouseMove, this) + ); + if (this.renderOnMouseOut_) { + this.listenerKeys.push( + listen(viewport, EventType.MOUSEOUT, this.handleMouseOut, this) + ); + } + } + } -}; + /** + * Set the coordinate format type used to render the current position. + * @param {module:ol/coordinate~CoordinateFormat} format The format to render the current + * position in. + * @observable + * @api + */ + setCoordinateFormat(format) { + this.set(COORDINATE_FORMAT, format); + } + + /** + * Set the projection that is used to report the mouse position. + * @param {module:ol/proj~ProjectionLike} projection The projection to report mouse + * position in. + * @observable + * @api + */ + setProjection(projection) { + this.set(PROJECTION, getProjection(projection)); + } + + /** + * @param {?module:ol~Pixel} pixel Pixel. + * @private + */ + updateHTML_(pixel) { + let html = this.undefinedHTML_; + if (pixel && this.mapProjection_) { + if (!this.transform_) { + const projection = this.getProjection(); + if (projection) { + this.transform_ = getTransformFromProjections( + this.mapProjection_, projection); + } else { + this.transform_ = identityTransform; + } + } + const map = this.getMap(); + const coordinate = map.getCoordinateFromPixel(pixel); + if (coordinate) { + this.transform_(coordinate, coordinate); + const coordinateFormat = this.getCoordinateFormat(); + if (coordinateFormat) { + html = coordinateFormat(coordinate); + } else { + html = coordinate.toString(); + } + } + } + if (!this.renderedHTML_ || html !== this.renderedHTML_) { + this.element.innerHTML = html; + this.renderedHTML_ = html; + } + } +} inherits(MousePosition, Control); @@ -137,141 +267,4 @@ export function render(mapEvent) { } -/** - * @private - */ -MousePosition.prototype.handleProjectionChanged_ = function() { - this.transform_ = null; -}; - - -/** - * Return the coordinate format type used to render the current position or - * undefined. - * @return {module:ol/coordinate~CoordinateFormat|undefined} The format to render the current - * position in. - * @observable - * @api - */ -MousePosition.prototype.getCoordinateFormat = function() { - return ( - /** @type {module:ol/coordinate~CoordinateFormat|undefined} */ (this.get(COORDINATE_FORMAT)) - ); -}; - - -/** - * Return the projection that is used to report the mouse position. - * @return {module:ol/proj/Projection|undefined} The projection to report mouse - * position in. - * @observable - * @api - */ -MousePosition.prototype.getProjection = function() { - return ( - /** @type {module:ol/proj/Projection|undefined} */ (this.get(PROJECTION)) - ); -}; - - -/** - * @param {Event} event Browser event. - * @protected - */ -MousePosition.prototype.handleMouseMove = function(event) { - const map = this.getMap(); - this.lastMouseMovePixel_ = map.getEventPixel(event); - this.updateHTML_(this.lastMouseMovePixel_); -}; - - -/** - * @param {Event} event Browser event. - * @protected - */ -MousePosition.prototype.handleMouseOut = function(event) { - this.updateHTML_(null); - this.lastMouseMovePixel_ = null; -}; - - -/** - * @inheritDoc - * @api - */ -MousePosition.prototype.setMap = function(map) { - Control.prototype.setMap.call(this, map); - if (map) { - const viewport = map.getViewport(); - this.listenerKeys.push( - listen(viewport, EventType.MOUSEMOVE, this.handleMouseMove, this) - ); - if (this.renderOnMouseOut_) { - this.listenerKeys.push( - listen(viewport, EventType.MOUSEOUT, this.handleMouseOut, this) - ); - } - } -}; - - -/** - * Set the coordinate format type used to render the current position. - * @param {module:ol/coordinate~CoordinateFormat} format The format to render the current - * position in. - * @observable - * @api - */ -MousePosition.prototype.setCoordinateFormat = function(format) { - this.set(COORDINATE_FORMAT, format); -}; - - -/** - * Set the projection that is used to report the mouse position. - * @param {module:ol/proj~ProjectionLike} projection The projection to report mouse - * position in. - * @observable - * @api - */ -MousePosition.prototype.setProjection = function(projection) { - this.set(PROJECTION, getProjection(projection)); -}; - - -/** - * @param {?module:ol~Pixel} pixel Pixel. - * @private - */ -MousePosition.prototype.updateHTML_ = function(pixel) { - let html = this.undefinedHTML_; - if (pixel && this.mapProjection_) { - if (!this.transform_) { - const projection = this.getProjection(); - if (projection) { - this.transform_ = getTransformFromProjections( - this.mapProjection_, projection); - } else { - this.transform_ = identityTransform; - } - } - const map = this.getMap(); - const coordinate = map.getCoordinateFromPixel(pixel); - if (coordinate) { - this.transform_(coordinate, coordinate); - const coordinateFormat = this.getCoordinateFormat(); - if (coordinateFormat) { - html = coordinateFormat(coordinate); - } else { - html = coordinate.toString(); - } - } - } - if (!this.renderedHTML_ || html !== this.renderedHTML_) { - this.element.innerHTML = html; - this.renderedHTML_ = html; - } -}; - - export default MousePosition; diff --git a/src/ol/control/OverviewMap.js b/src/ol/control/OverviewMap.js index 6241ff3374..9849dcb538 100644 --- a/src/ol/control/OverviewMap.js +++ b/src/ol/control/OverviewMap.js @@ -66,258 +66,506 @@ const MIN_RATIO = 0.1; * @param {module:ol/control/OverviewMap~Options=} opt_options OverviewMap options. * @api */ -const OverviewMap = function(opt_options) { +class OverviewMap { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - /** - * @type {boolean} - * @private - */ - this.collapsed_ = options.collapsed !== undefined ? options.collapsed : true; + /** + * @type {boolean} + * @private + */ + this.collapsed_ = options.collapsed !== undefined ? options.collapsed : true; - /** - * @private - * @type {boolean} - */ - this.collapsible_ = options.collapsible !== undefined ? - options.collapsible : true; - - if (!this.collapsible_) { - this.collapsed_ = false; - } - - const className = options.className !== undefined ? options.className : 'ol-overviewmap'; - - const tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Overview map'; - - const collapseLabel = options.collapseLabel !== undefined ? options.collapseLabel : '\u00AB'; - - if (typeof collapseLabel === 'string') { /** * @private - * @type {Element} + * @type {boolean} */ - this.collapseLabel_ = document.createElement('span'); - this.collapseLabel_.textContent = collapseLabel; - } else { - this.collapseLabel_ = collapseLabel; - } + this.collapsible_ = options.collapsible !== undefined ? + options.collapsible : true; - const label = options.label !== undefined ? options.label : '\u00BB'; + if (!this.collapsible_) { + this.collapsed_ = false; + } + const className = options.className !== undefined ? options.className : 'ol-overviewmap'; - if (typeof label === 'string') { - /** - * @private - * @type {Element} - */ - this.label_ = document.createElement('span'); - this.label_.textContent = label; - } else { - this.label_ = label; - } + const tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Overview map'; - const activeLabel = (this.collapsible_ && !this.collapsed_) ? - this.collapseLabel_ : this.label_; - const button = document.createElement('button'); - button.setAttribute('type', 'button'); - button.title = tipLabel; - button.appendChild(activeLabel); + const collapseLabel = options.collapseLabel !== undefined ? options.collapseLabel : '\u00AB'; - listen(button, EventType.CLICK, - this.handleClick_, this); - - /** - * @type {Element} - * @private - */ - this.ovmapDiv_ = document.createElement('DIV'); - this.ovmapDiv_.className = 'ol-overviewmap-map'; - - /** - * @type {module:ol/Map} - * @private - */ - this.ovmap_ = new Map({ - controls: new Collection(), - interactions: new Collection(), - view: options.view - }); - const ovmap = this.ovmap_; - - if (options.layers) { - options.layers.forEach( + if (typeof collapseLabel === 'string') { /** - * @param {module:ol/layer/Layer} layer Layer. + * @private + * @type {Element} */ - (function(layer) { - ovmap.addLayer(layer); - }).bind(this)); - } + this.collapseLabel_ = document.createElement('span'); + this.collapseLabel_.textContent = collapseLabel; + } else { + this.collapseLabel_ = collapseLabel; + } - const box = document.createElement('DIV'); - box.className = 'ol-overviewmap-box'; - box.style.boxSizing = 'border-box'; + const label = options.label !== undefined ? options.label : '\u00BB'; + + + if (typeof label === 'string') { + /** + * @private + * @type {Element} + */ + this.label_ = document.createElement('span'); + this.label_.textContent = label; + } else { + this.label_ = label; + } + + const activeLabel = (this.collapsible_ && !this.collapsed_) ? + this.collapseLabel_ : this.label_; + const button = document.createElement('button'); + button.setAttribute('type', 'button'); + button.title = tipLabel; + button.appendChild(activeLabel); + + listen(button, EventType.CLICK, + this.handleClick_, this); + + /** + * @type {Element} + * @private + */ + this.ovmapDiv_ = document.createElement('DIV'); + this.ovmapDiv_.className = 'ol-overviewmap-map'; + + /** + * @type {module:ol/Map} + * @private + */ + this.ovmap_ = new Map({ + controls: new Collection(), + interactions: new Collection(), + view: options.view + }); + const ovmap = this.ovmap_; + + if (options.layers) { + options.layers.forEach( + /** + * @param {module:ol/layer/Layer} layer Layer. + */ + (function(layer) { + ovmap.addLayer(layer); + }).bind(this)); + } + + const box = document.createElement('DIV'); + box.className = 'ol-overviewmap-box'; + box.style.boxSizing = 'border-box'; + + /** + * @type {module:ol/Overlay} + * @private + */ + this.boxOverlay_ = new Overlay({ + position: [0, 0], + positioning: OverlayPositioning.BOTTOM_LEFT, + element: box + }); + this.ovmap_.addOverlay(this.boxOverlay_); + + const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL + + (this.collapsed_ && this.collapsible_ ? ' ' + CLASS_COLLAPSED : '') + + (this.collapsible_ ? '' : ' ol-uncollapsible'); + const element = document.createElement('div'); + element.className = cssClasses; + element.appendChild(this.ovmapDiv_); + element.appendChild(button); + + Control.call(this, { + element: element, + render: options.render || render, + target: options.target + }); + + /* Interactive map */ + + const scope = this; + + const overlay = this.boxOverlay_; + const overlayBox = this.boxOverlay_.getElement(); + + /* Functions definition */ + + const computeDesiredMousePosition = function(mousePosition) { + return { + clientX: mousePosition.clientX - (overlayBox.offsetWidth / 2), + clientY: mousePosition.clientY + (overlayBox.offsetHeight / 2) + }; + }; + + const move = function(event) { + const coordinates = ovmap.getEventCoordinate(computeDesiredMousePosition(event)); + + overlay.setPosition(coordinates); + }; + + const endMoving = function(event) { + const coordinates = ovmap.getEventCoordinate(event); + + scope.getMap().getView().setCenter(coordinates); + + window.removeEventListener('mousemove', move); + window.removeEventListener('mouseup', endMoving); + }; + + /* Binding */ + + overlayBox.addEventListener('mousedown', function() { + window.addEventListener('mousemove', move); + window.addEventListener('mouseup', endMoving); + }); + } /** - * @type {module:ol/Overlay} - * @private + * @inheritDoc + * @api */ - this.boxOverlay_ = new Overlay({ - position: [0, 0], - positioning: OverlayPositioning.BOTTOM_LEFT, - element: box - }); - this.ovmap_.addOverlay(this.boxOverlay_); - - const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL + - (this.collapsed_ && this.collapsible_ ? ' ' + CLASS_COLLAPSED : '') + - (this.collapsible_ ? '' : ' ol-uncollapsible'); - const element = document.createElement('div'); - element.className = cssClasses; - element.appendChild(this.ovmapDiv_); - element.appendChild(button); - - Control.call(this, { - element: element, - render: options.render || render, - target: options.target - }); - - /* Interactive map */ - - const scope = this; - - const overlay = this.boxOverlay_; - const overlayBox = this.boxOverlay_.getElement(); - - /* Functions definition */ - - const computeDesiredMousePosition = function(mousePosition) { - return { - clientX: mousePosition.clientX - (overlayBox.offsetWidth / 2), - clientY: mousePosition.clientY + (overlayBox.offsetHeight / 2) - }; - }; - - const move = function(event) { - const coordinates = ovmap.getEventCoordinate(computeDesiredMousePosition(event)); - - overlay.setPosition(coordinates); - }; - - const endMoving = function(event) { - const coordinates = ovmap.getEventCoordinate(event); - - scope.getMap().getView().setCenter(coordinates); - - window.removeEventListener('mousemove', move); - window.removeEventListener('mouseup', endMoving); - }; - - /* Binding */ - - overlayBox.addEventListener('mousedown', function() { - window.addEventListener('mousemove', move); - window.addEventListener('mouseup', endMoving); - }); -}; - -inherits(OverviewMap, Control); - - -/** - * @inheritDoc - * @api - */ -OverviewMap.prototype.setMap = function(map) { - const oldMap = this.getMap(); - if (map === oldMap) { - return; - } - if (oldMap) { - const oldView = oldMap.getView(); - if (oldView) { - this.unbindView_(oldView); + setMap(map) { + const oldMap = this.getMap(); + if (map === oldMap) { + return; } - this.ovmap_.setTarget(null); - } - Control.prototype.setMap.call(this, map); - - if (map) { - this.ovmap_.setTarget(this.ovmapDiv_); - this.listenerKeys.push(listen( - map, ObjectEventType.PROPERTYCHANGE, - this.handleMapPropertyChange_, this)); - - // TODO: to really support map switching, this would need to be reworked - if (this.ovmap_.getLayers().getLength() === 0) { - this.ovmap_.setLayerGroup(map.getLayerGroup()); + if (oldMap) { + const oldView = oldMap.getView(); + if (oldView) { + this.unbindView_(oldView); + } + this.ovmap_.setTarget(null); } + Control.prototype.setMap.call(this, map); - const view = map.getView(); - if (view) { - this.bindView_(view); - if (view.isDef()) { - this.ovmap_.updateSize(); - this.resetExtent_(); + if (map) { + this.ovmap_.setTarget(this.ovmapDiv_); + this.listenerKeys.push(listen( + map, ObjectEventType.PROPERTYCHANGE, + this.handleMapPropertyChange_, this)); + + // TODO: to really support map switching, this would need to be reworked + if (this.ovmap_.getLayers().getLength() === 0) { + this.ovmap_.setLayerGroup(map.getLayerGroup()); + } + + const view = map.getView(); + if (view) { + this.bindView_(view); + if (view.isDef()) { + this.ovmap_.updateSize(); + this.resetExtent_(); + } } } } -}; - -/** - * Handle map property changes. This only deals with changes to the map's view. - * @param {module:ol/Object~ObjectEvent} event The propertychange event. - * @private - */ -OverviewMap.prototype.handleMapPropertyChange_ = function(event) { - if (event.key === MapProperty.VIEW) { - const oldView = /** @type {module:ol/View} */ (event.oldValue); - if (oldView) { - this.unbindView_(oldView); + /** + * Handle map property changes. This only deals with changes to the map's view. + * @param {module:ol/Object~ObjectEvent} event The propertychange event. + * @private + */ + handleMapPropertyChange_(event) { + if (event.key === MapProperty.VIEW) { + const oldView = /** @type {module:ol/View} */ (event.oldValue); + if (oldView) { + this.unbindView_(oldView); + } + const newView = this.getMap().getView(); + this.bindView_(newView); } - const newView = this.getMap().getView(); - this.bindView_(newView); } -}; + /** + * Register listeners for view property changes. + * @param {module:ol/View} view The view. + * @private + */ + bindView_(view) { + listen(view, + getChangeEventType(ViewProperty.ROTATION), + this.handleRotationChanged_, this); + } -/** - * Register listeners for view property changes. - * @param {module:ol/View} view The view. - * @private - */ -OverviewMap.prototype.bindView_ = function(view) { - listen(view, - getChangeEventType(ViewProperty.ROTATION), - this.handleRotationChanged_, this); -}; + /** + * Unregister listeners for view property changes. + * @param {module:ol/View} view The view. + * @private + */ + unbindView_(view) { + unlisten(view, + getChangeEventType(ViewProperty.ROTATION), + this.handleRotationChanged_, this); + } + /** + * Handle rotation changes to the main map. + * TODO: This should rotate the extent rectrangle instead of the + * overview map's view. + * @private + */ + handleRotationChanged_() { + this.ovmap_.getView().setRotation(this.getMap().getView().getRotation()); + } -/** - * Unregister listeners for view property changes. - * @param {module:ol/View} view The view. - * @private - */ -OverviewMap.prototype.unbindView_ = function(view) { - unlisten(view, - getChangeEventType(ViewProperty.ROTATION), - this.handleRotationChanged_, this); -}; + /** + * Reset the overview map extent if the box size (width or + * height) is less than the size of the overview map size times minRatio + * or is greater than the size of the overview size times maxRatio. + * + * If the map extent was not reset, the box size can fits in the defined + * ratio sizes. This method then checks if is contained inside the overview + * map current extent. If not, recenter the overview map to the current + * main map center location. + * @private + */ + validateExtent_() { + const map = this.getMap(); + const ovmap = this.ovmap_; + if (!map.isRendered() || !ovmap.isRendered()) { + return; + } -/** - * Handle rotation changes to the main map. - * TODO: This should rotate the extent rectrangle instead of the - * overview map's view. - * @private - */ -OverviewMap.prototype.handleRotationChanged_ = function() { - this.ovmap_.getView().setRotation(this.getMap().getView().getRotation()); -}; + const mapSize = /** @type {module:ol/size~Size} */ (map.getSize()); + + const view = map.getView(); + const extent = view.calculateExtent(mapSize); + + const ovmapSize = /** @type {module:ol/size~Size} */ (ovmap.getSize()); + + const ovview = ovmap.getView(); + const ovextent = ovview.calculateExtent(ovmapSize); + + const topLeftPixel = + ovmap.getPixelFromCoordinate(getTopLeft(extent)); + const bottomRightPixel = + ovmap.getPixelFromCoordinate(getBottomRight(extent)); + + const boxWidth = Math.abs(topLeftPixel[0] - bottomRightPixel[0]); + const boxHeight = Math.abs(topLeftPixel[1] - bottomRightPixel[1]); + + const ovmapWidth = ovmapSize[0]; + const ovmapHeight = ovmapSize[1]; + + if (boxWidth < ovmapWidth * MIN_RATIO || + boxHeight < ovmapHeight * MIN_RATIO || + boxWidth > ovmapWidth * MAX_RATIO || + boxHeight > ovmapHeight * MAX_RATIO) { + this.resetExtent_(); + } else if (!containsExtent(ovextent, extent)) { + this.recenter_(); + } + } + + /** + * Reset the overview map extent to half calculated min and max ratio times + * the extent of the main map. + * @private + */ + resetExtent_() { + if (MAX_RATIO === 0 || MIN_RATIO === 0) { + return; + } + + const map = this.getMap(); + const ovmap = this.ovmap_; + + const mapSize = /** @type {module:ol/size~Size} */ (map.getSize()); + + const view = map.getView(); + const extent = view.calculateExtent(mapSize); + + const ovview = ovmap.getView(); + + // get how many times the current map overview could hold different + // box sizes using the min and max ratio, pick the step in the middle used + // to calculate the extent from the main map to set it to the overview map, + const steps = Math.log( + MAX_RATIO / MIN_RATIO) / Math.LN2; + const ratio = 1 / (Math.pow(2, steps / 2) * MIN_RATIO); + scaleFromCenter(extent, ratio); + ovview.fit(extent); + } + + /** + * Set the center of the overview map to the map center without changing its + * resolution. + * @private + */ + recenter_() { + const map = this.getMap(); + const ovmap = this.ovmap_; + + const view = map.getView(); + + const ovview = ovmap.getView(); + + ovview.setCenter(view.getCenter()); + } + + /** + * Update the box using the main map extent + * @private + */ + updateBox_() { + const map = this.getMap(); + const ovmap = this.ovmap_; + + if (!map.isRendered() || !ovmap.isRendered()) { + return; + } + + const mapSize = /** @type {module:ol/size~Size} */ (map.getSize()); + + const view = map.getView(); + + const ovview = ovmap.getView(); + + const rotation = view.getRotation(); + + const overlay = this.boxOverlay_; + const box = this.boxOverlay_.getElement(); + const extent = view.calculateExtent(mapSize); + const ovresolution = ovview.getResolution(); + const bottomLeft = getBottomLeft(extent); + const topRight = getTopRight(extent); + + // set position using bottom left coordinates + const rotateBottomLeft = this.calculateCoordinateRotate_(rotation, bottomLeft); + overlay.setPosition(rotateBottomLeft); + + // set box size calculated from map extent size and overview map resolution + if (box) { + box.style.width = Math.abs((bottomLeft[0] - topRight[0]) / ovresolution) + 'px'; + box.style.height = Math.abs((topRight[1] - bottomLeft[1]) / ovresolution) + 'px'; + } + } + + /** + * @param {number} rotation Target rotation. + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @return {module:ol/coordinate~Coordinate|undefined} Coordinate for rotation and center anchor. + * @private + */ + calculateCoordinateRotate_(rotation, coordinate) { + let coordinateRotate; + + const map = this.getMap(); + const view = map.getView(); + + const currentCenter = view.getCenter(); + + if (currentCenter) { + coordinateRotate = [ + coordinate[0] - currentCenter[0], + coordinate[1] - currentCenter[1] + ]; + rotateCoordinate(coordinateRotate, rotation); + addCoordinate(coordinateRotate, currentCenter); + } + return coordinateRotate; + } + + /** + * @param {MouseEvent} event The event to handle + * @private + */ + handleClick_(event) { + event.preventDefault(); + this.handleToggle_(); + } + + /** + * @private + */ + handleToggle_() { + this.element.classList.toggle(CLASS_COLLAPSED); + if (this.collapsed_) { + replaceNode(this.collapseLabel_, this.label_); + } else { + replaceNode(this.label_, this.collapseLabel_); + } + this.collapsed_ = !this.collapsed_; + + // manage overview map if it had not been rendered before and control + // is expanded + const ovmap = this.ovmap_; + if (!this.collapsed_ && !ovmap.isRendered()) { + ovmap.updateSize(); + this.resetExtent_(); + listenOnce(ovmap, MapEventType.POSTRENDER, + function(event) { + this.updateBox_(); + }, + this); + } + } + + /** + * Return `true` if the overview map is collapsible, `false` otherwise. + * @return {boolean} True if the widget is collapsible. + * @api + */ + getCollapsible() { + return this.collapsible_; + } + + /** + * Set whether the overview map should be collapsible. + * @param {boolean} collapsible True if the widget is collapsible. + * @api + */ + setCollapsible(collapsible) { + if (this.collapsible_ === collapsible) { + return; + } + this.collapsible_ = collapsible; + this.element.classList.toggle('ol-uncollapsible'); + if (!collapsible && this.collapsed_) { + this.handleToggle_(); + } + } + + /** + * Collapse or expand the overview map according to the passed parameter. Will + * not do anything if the overview map isn't collapsible or if the current + * collapsed state is already the one requested. + * @param {boolean} collapsed True if the widget is collapsed. + * @api + */ + setCollapsed(collapsed) { + if (!this.collapsible_ || this.collapsed_ === collapsed) { + return; + } + this.handleToggle_(); + } + + /** + * Determine if the overview map is collapsed. + * @return {boolean} The overview map is collapsed. + * @api + */ + getCollapsed() { + return this.collapsed_; + } + + /** + * Return the overview map. + * @return {module:ol/PluggableMap} Overview map. + * @api + */ + getOverviewMap() { + return this.ovmap_; + } +} + +inherits(OverviewMap, Control); /** @@ -332,266 +580,4 @@ export function render(mapEvent) { } -/** - * Reset the overview map extent if the box size (width or - * height) is less than the size of the overview map size times minRatio - * or is greater than the size of the overview size times maxRatio. - * - * If the map extent was not reset, the box size can fits in the defined - * ratio sizes. This method then checks if is contained inside the overview - * map current extent. If not, recenter the overview map to the current - * main map center location. - * @private - */ -OverviewMap.prototype.validateExtent_ = function() { - const map = this.getMap(); - const ovmap = this.ovmap_; - - if (!map.isRendered() || !ovmap.isRendered()) { - return; - } - - const mapSize = /** @type {module:ol/size~Size} */ (map.getSize()); - - const view = map.getView(); - const extent = view.calculateExtent(mapSize); - - const ovmapSize = /** @type {module:ol/size~Size} */ (ovmap.getSize()); - - const ovview = ovmap.getView(); - const ovextent = ovview.calculateExtent(ovmapSize); - - const topLeftPixel = - ovmap.getPixelFromCoordinate(getTopLeft(extent)); - const bottomRightPixel = - ovmap.getPixelFromCoordinate(getBottomRight(extent)); - - const boxWidth = Math.abs(topLeftPixel[0] - bottomRightPixel[0]); - const boxHeight = Math.abs(topLeftPixel[1] - bottomRightPixel[1]); - - const ovmapWidth = ovmapSize[0]; - const ovmapHeight = ovmapSize[1]; - - if (boxWidth < ovmapWidth * MIN_RATIO || - boxHeight < ovmapHeight * MIN_RATIO || - boxWidth > ovmapWidth * MAX_RATIO || - boxHeight > ovmapHeight * MAX_RATIO) { - this.resetExtent_(); - } else if (!containsExtent(ovextent, extent)) { - this.recenter_(); - } -}; - - -/** - * Reset the overview map extent to half calculated min and max ratio times - * the extent of the main map. - * @private - */ -OverviewMap.prototype.resetExtent_ = function() { - if (MAX_RATIO === 0 || MIN_RATIO === 0) { - return; - } - - const map = this.getMap(); - const ovmap = this.ovmap_; - - const mapSize = /** @type {module:ol/size~Size} */ (map.getSize()); - - const view = map.getView(); - const extent = view.calculateExtent(mapSize); - - const ovview = ovmap.getView(); - - // get how many times the current map overview could hold different - // box sizes using the min and max ratio, pick the step in the middle used - // to calculate the extent from the main map to set it to the overview map, - const steps = Math.log( - MAX_RATIO / MIN_RATIO) / Math.LN2; - const ratio = 1 / (Math.pow(2, steps / 2) * MIN_RATIO); - scaleFromCenter(extent, ratio); - ovview.fit(extent); -}; - - -/** - * Set the center of the overview map to the map center without changing its - * resolution. - * @private - */ -OverviewMap.prototype.recenter_ = function() { - const map = this.getMap(); - const ovmap = this.ovmap_; - - const view = map.getView(); - - const ovview = ovmap.getView(); - - ovview.setCenter(view.getCenter()); -}; - - -/** - * Update the box using the main map extent - * @private - */ -OverviewMap.prototype.updateBox_ = function() { - const map = this.getMap(); - const ovmap = this.ovmap_; - - if (!map.isRendered() || !ovmap.isRendered()) { - return; - } - - const mapSize = /** @type {module:ol/size~Size} */ (map.getSize()); - - const view = map.getView(); - - const ovview = ovmap.getView(); - - const rotation = view.getRotation(); - - const overlay = this.boxOverlay_; - const box = this.boxOverlay_.getElement(); - const extent = view.calculateExtent(mapSize); - const ovresolution = ovview.getResolution(); - const bottomLeft = getBottomLeft(extent); - const topRight = getTopRight(extent); - - // set position using bottom left coordinates - const rotateBottomLeft = this.calculateCoordinateRotate_(rotation, bottomLeft); - overlay.setPosition(rotateBottomLeft); - - // set box size calculated from map extent size and overview map resolution - if (box) { - box.style.width = Math.abs((bottomLeft[0] - topRight[0]) / ovresolution) + 'px'; - box.style.height = Math.abs((topRight[1] - bottomLeft[1]) / ovresolution) + 'px'; - } -}; - - -/** - * @param {number} rotation Target rotation. - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @return {module:ol/coordinate~Coordinate|undefined} Coordinate for rotation and center anchor. - * @private - */ -OverviewMap.prototype.calculateCoordinateRotate_ = function( - rotation, coordinate) { - let coordinateRotate; - - const map = this.getMap(); - const view = map.getView(); - - const currentCenter = view.getCenter(); - - if (currentCenter) { - coordinateRotate = [ - coordinate[0] - currentCenter[0], - coordinate[1] - currentCenter[1] - ]; - rotateCoordinate(coordinateRotate, rotation); - addCoordinate(coordinateRotate, currentCenter); - } - return coordinateRotate; -}; - - -/** - * @param {MouseEvent} event The event to handle - * @private - */ -OverviewMap.prototype.handleClick_ = function(event) { - event.preventDefault(); - this.handleToggle_(); -}; - - -/** - * @private - */ -OverviewMap.prototype.handleToggle_ = function() { - this.element.classList.toggle(CLASS_COLLAPSED); - if (this.collapsed_) { - replaceNode(this.collapseLabel_, this.label_); - } else { - replaceNode(this.label_, this.collapseLabel_); - } - this.collapsed_ = !this.collapsed_; - - // manage overview map if it had not been rendered before and control - // is expanded - const ovmap = this.ovmap_; - if (!this.collapsed_ && !ovmap.isRendered()) { - ovmap.updateSize(); - this.resetExtent_(); - listenOnce(ovmap, MapEventType.POSTRENDER, - function(event) { - this.updateBox_(); - }, - this); - } -}; - - -/** - * Return `true` if the overview map is collapsible, `false` otherwise. - * @return {boolean} True if the widget is collapsible. - * @api - */ -OverviewMap.prototype.getCollapsible = function() { - return this.collapsible_; -}; - - -/** - * Set whether the overview map should be collapsible. - * @param {boolean} collapsible True if the widget is collapsible. - * @api - */ -OverviewMap.prototype.setCollapsible = function(collapsible) { - if (this.collapsible_ === collapsible) { - return; - } - this.collapsible_ = collapsible; - this.element.classList.toggle('ol-uncollapsible'); - if (!collapsible && this.collapsed_) { - this.handleToggle_(); - } -}; - - -/** - * Collapse or expand the overview map according to the passed parameter. Will - * not do anything if the overview map isn't collapsible or if the current - * collapsed state is already the one requested. - * @param {boolean} collapsed True if the widget is collapsed. - * @api - */ -OverviewMap.prototype.setCollapsed = function(collapsed) { - if (!this.collapsible_ || this.collapsed_ === collapsed) { - return; - } - this.handleToggle_(); -}; - - -/** - * Determine if the overview map is collapsed. - * @return {boolean} The overview map is collapsed. - * @api - */ -OverviewMap.prototype.getCollapsed = function() { - return this.collapsed_; -}; - - -/** - * Return the overview map. - * @return {module:ol/PluggableMap} Overview map. - * @api - */ -OverviewMap.prototype.getOverviewMap = function() { - return this.ovmap_; -}; export default OverviewMap; diff --git a/src/ol/control/Rotate.js b/src/ol/control/Rotate.js index 8d73e37ab4..6d8336827c 100644 --- a/src/ol/control/Rotate.js +++ b/src/ol/control/Rotate.js @@ -38,117 +38,117 @@ import {inherits} from '../util.js'; * @param {module:ol/control/Rotate~Options=} opt_options Rotate options. * @api */ -const Rotate = function(opt_options) { +class Rotate { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - const className = options.className !== undefined ? options.className : 'ol-rotate'; + const className = options.className !== undefined ? options.className : 'ol-rotate'; - const label = options.label !== undefined ? options.label : '\u21E7'; + const label = options.label !== undefined ? options.label : '\u21E7'; - /** - * @type {Element} - * @private - */ - this.label_ = null; + /** + * @type {Element} + * @private + */ + this.label_ = null; - if (typeof label === 'string') { - this.label_ = document.createElement('span'); - this.label_.className = 'ol-compass'; - this.label_.textContent = label; - } else { - this.label_ = label; - this.label_.classList.add('ol-compass'); - } - - const tipLabel = options.tipLabel ? options.tipLabel : 'Reset rotation'; - - const button = document.createElement('button'); - button.className = className + '-reset'; - button.setAttribute('type', 'button'); - button.title = tipLabel; - button.appendChild(this.label_); - - listen(button, EventType.CLICK, - Rotate.prototype.handleClick_, this); - - const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL; - const element = document.createElement('div'); - element.className = cssClasses; - element.appendChild(button); - - this.callResetNorth_ = options.resetNorth ? options.resetNorth : undefined; - - Control.call(this, { - element: element, - render: options.render || render, - target: options.target - }); - - /** - * @type {number} - * @private - */ - this.duration_ = options.duration !== undefined ? options.duration : 250; - - /** - * @type {boolean} - * @private - */ - this.autoHide_ = options.autoHide !== undefined ? options.autoHide : true; - - /** - * @private - * @type {number|undefined} - */ - this.rotation_ = undefined; - - if (this.autoHide_) { - this.element.classList.add(CLASS_HIDDEN); - } - -}; - -inherits(Rotate, Control); - - -/** - * @param {MouseEvent} event The event to handle - * @private - */ -Rotate.prototype.handleClick_ = function(event) { - event.preventDefault(); - if (this.callResetNorth_ !== undefined) { - this.callResetNorth_(); - } else { - this.resetNorth_(); - } -}; - - -/** - * @private - */ -Rotate.prototype.resetNorth_ = function() { - const map = this.getMap(); - const view = map.getView(); - if (!view) { - // the map does not have a view, so we can't act - // upon it - return; - } - if (view.getRotation() !== undefined) { - if (this.duration_ > 0) { - view.animate({ - rotation: 0, - duration: this.duration_, - easing: easeOut - }); + if (typeof label === 'string') { + this.label_ = document.createElement('span'); + this.label_.className = 'ol-compass'; + this.label_.textContent = label; } else { - view.setRotation(0); + this.label_ = label; + this.label_.classList.add('ol-compass'); + } + + const tipLabel = options.tipLabel ? options.tipLabel : 'Reset rotation'; + + const button = document.createElement('button'); + button.className = className + '-reset'; + button.setAttribute('type', 'button'); + button.title = tipLabel; + button.appendChild(this.label_); + + listen(button, EventType.CLICK, + Rotate.prototype.handleClick_, this); + + const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL; + const element = document.createElement('div'); + element.className = cssClasses; + element.appendChild(button); + + this.callResetNorth_ = options.resetNorth ? options.resetNorth : undefined; + + Control.call(this, { + element: element, + render: options.render || render, + target: options.target + }); + + /** + * @type {number} + * @private + */ + this.duration_ = options.duration !== undefined ? options.duration : 250; + + /** + * @type {boolean} + * @private + */ + this.autoHide_ = options.autoHide !== undefined ? options.autoHide : true; + + /** + * @private + * @type {number|undefined} + */ + this.rotation_ = undefined; + + if (this.autoHide_) { + this.element.classList.add(CLASS_HIDDEN); + } + + } + + /** + * @param {MouseEvent} event The event to handle + * @private + */ + handleClick_(event) { + event.preventDefault(); + if (this.callResetNorth_ !== undefined) { + this.callResetNorth_(); + } else { + this.resetNorth_(); } } -}; + + /** + * @private + */ + resetNorth_() { + const map = this.getMap(); + const view = map.getView(); + if (!view) { + // the map does not have a view, so we can't act + // upon it + return; + } + if (view.getRotation() !== undefined) { + if (this.duration_ > 0) { + view.animate({ + rotation: 0, + duration: this.duration_, + easing: easeOut + }); + } else { + view.setRotation(0); + } + } + } +} + +inherits(Rotate, Control); /** diff --git a/src/ol/control/ScaleLine.js b/src/ol/control/ScaleLine.js index 732cb73419..1b6000e377 100644 --- a/src/ol/control/ScaleLine.js +++ b/src/ol/control/ScaleLine.js @@ -64,89 +64,229 @@ const LEADING_DIGITS = [1, 2, 5]; * @param {module:ol/control/ScaleLine~Options=} opt_options Scale line options. * @api */ -const ScaleLine = function(opt_options) { +class ScaleLine { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - const className = options.className !== undefined ? options.className : 'ol-scale-line'; + const className = options.className !== undefined ? options.className : 'ol-scale-line'; + + /** + * @private + * @type {HTMLElement} + */ + this.innerElement_ = document.createElement('DIV'); + this.innerElement_.className = className + '-inner'; + + /** + * @private + * @type {HTMLElement} + */ + this.element_ = document.createElement('DIV'); + this.element_.className = className + ' ' + CLASS_UNSELECTABLE; + this.element_.appendChild(this.innerElement_); + + /** + * @private + * @type {?module:ol/View~State} + */ + this.viewState_ = null; + + /** + * @private + * @type {number} + */ + this.minWidth_ = options.minWidth !== undefined ? options.minWidth : 64; + + /** + * @private + * @type {boolean} + */ + this.renderedVisible_ = false; + + /** + * @private + * @type {number|undefined} + */ + this.renderedWidth_ = undefined; + + /** + * @private + * @type {string} + */ + this.renderedHTML_ = ''; + + Control.call(this, { + element: this.element_, + render: options.render || render, + target: options.target + }); + + listen( + this, getChangeEventType(UNITS_PROP), + this.handleUnitsChanged_, this); + + this.setUnits(/** @type {module:ol/control/ScaleLine~Units} */ (options.units) || + Units.METRIC); + + } + + /** + * Return the units to use in the scale line. + * @return {module:ol/control/ScaleLine~Units|undefined} The units + * to use in the scale line. + * @observable + * @api + */ + getUnits() { + return ( + /** @type {module:ol/control/ScaleLine~Units|undefined} */ (this.get(UNITS_PROP)) + ); + } /** * @private - * @type {HTMLElement} */ - this.innerElement_ = document.createElement('DIV'); - this.innerElement_.className = className + '-inner'; + handleUnitsChanged_() { + this.updateElement_(); + } + + /** + * Set the units to use in the scale line. + * @param {module:ol/control/ScaleLine~Units} units The units to use in the scale line. + * @observable + * @api + */ + setUnits(units) { + this.set(UNITS_PROP, units); + } /** * @private - * @type {HTMLElement} */ - this.element_ = document.createElement('DIV'); - this.element_.className = className + ' ' + CLASS_UNSELECTABLE; - this.element_.appendChild(this.innerElement_); + updateElement_() { + const viewState = this.viewState_; - /** - * @private - * @type {?module:ol/View~State} - */ - this.viewState_ = null; + if (!viewState) { + if (this.renderedVisible_) { + this.element_.style.display = 'none'; + this.renderedVisible_ = false; + } + return; + } - /** - * @private - * @type {number} - */ - this.minWidth_ = options.minWidth !== undefined ? options.minWidth : 64; + const center = viewState.center; + const projection = viewState.projection; + const units = this.getUnits(); + const pointResolutionUnits = units == Units.DEGREES ? + ProjUnits.DEGREES : + ProjUnits.METERS; + let pointResolution = + getPointResolution(projection, viewState.resolution, center, pointResolutionUnits); + if (projection.getUnits() != ProjUnits.DEGREES && projection.getMetersPerUnit() + && pointResolutionUnits == ProjUnits.METERS) { + pointResolution *= projection.getMetersPerUnit(); + } - /** - * @private - * @type {boolean} - */ - this.renderedVisible_ = false; + let nominalCount = this.minWidth_ * pointResolution; + let suffix = ''; + if (units == Units.DEGREES) { + const metersPerDegree = METERS_PER_UNIT[ProjUnits.DEGREES]; + if (projection.getUnits() == ProjUnits.DEGREES) { + nominalCount *= metersPerDegree; + } else { + pointResolution /= metersPerDegree; + } + if (nominalCount < metersPerDegree / 60) { + suffix = '\u2033'; // seconds + pointResolution *= 3600; + } else if (nominalCount < metersPerDegree) { + suffix = '\u2032'; // minutes + pointResolution *= 60; + } else { + suffix = '\u00b0'; // degrees + } + } else if (units == Units.IMPERIAL) { + if (nominalCount < 0.9144) { + suffix = 'in'; + pointResolution /= 0.0254; + } else if (nominalCount < 1609.344) { + suffix = 'ft'; + pointResolution /= 0.3048; + } else { + suffix = 'mi'; + pointResolution /= 1609.344; + } + } else if (units == Units.NAUTICAL) { + pointResolution /= 1852; + suffix = 'nm'; + } else if (units == Units.METRIC) { + if (nominalCount < 0.001) { + suffix = 'μm'; + pointResolution *= 1000000; + } else if (nominalCount < 1) { + suffix = 'mm'; + pointResolution *= 1000; + } else if (nominalCount < 1000) { + suffix = 'm'; + } else { + suffix = 'km'; + pointResolution /= 1000; + } + } else if (units == Units.US) { + if (nominalCount < 0.9144) { + suffix = 'in'; + pointResolution *= 39.37; + } else if (nominalCount < 1609.344) { + suffix = 'ft'; + pointResolution /= 0.30480061; + } else { + suffix = 'mi'; + pointResolution /= 1609.3472; + } + } else { + assert(false, 33); // Invalid units + } - /** - * @private - * @type {number|undefined} - */ - this.renderedWidth_ = undefined; + let i = 3 * Math.floor( + Math.log(this.minWidth_ * pointResolution) / Math.log(10)); + let count, width; + while (true) { + count = LEADING_DIGITS[((i % 3) + 3) % 3] * + Math.pow(10, Math.floor(i / 3)); + width = Math.round(count / pointResolution); + if (isNaN(width)) { + this.element_.style.display = 'none'; + this.renderedVisible_ = false; + return; + } else if (width >= this.minWidth_) { + break; + } + ++i; + } - /** - * @private - * @type {string} - */ - this.renderedHTML_ = ''; + const html = count + ' ' + suffix; + if (this.renderedHTML_ != html) { + this.innerElement_.innerHTML = html; + this.renderedHTML_ = html; + } - Control.call(this, { - element: this.element_, - render: options.render || render, - target: options.target - }); + if (this.renderedWidth_ != width) { + this.innerElement_.style.width = width + 'px'; + this.renderedWidth_ = width; + } - listen( - this, getChangeEventType(UNITS_PROP), - this.handleUnitsChanged_, this); + if (!this.renderedVisible_) { + this.element_.style.display = ''; + this.renderedVisible_ = true; + } - this.setUnits(/** @type {module:ol/control/ScaleLine~Units} */ (options.units) || - Units.METRIC); - -}; + } +} inherits(ScaleLine, Control); -/** - * Return the units to use in the scale line. - * @return {module:ol/control/ScaleLine~Units|undefined} The units - * to use in the scale line. - * @observable - * @api - */ -ScaleLine.prototype.getUnits = function() { - return ( - /** @type {module:ol/control/ScaleLine~Units|undefined} */ (this.get(UNITS_PROP)) - ); -}; - - /** * Update the scale line element. * @param {module:ol/MapEvent} mapEvent Map event. @@ -164,145 +304,4 @@ export function render(mapEvent) { } -/** - * @private - */ -ScaleLine.prototype.handleUnitsChanged_ = function() { - this.updateElement_(); -}; - - -/** - * Set the units to use in the scale line. - * @param {module:ol/control/ScaleLine~Units} units The units to use in the scale line. - * @observable - * @api - */ -ScaleLine.prototype.setUnits = function(units) { - this.set(UNITS_PROP, units); -}; - - -/** - * @private - */ -ScaleLine.prototype.updateElement_ = function() { - const viewState = this.viewState_; - - if (!viewState) { - if (this.renderedVisible_) { - this.element_.style.display = 'none'; - this.renderedVisible_ = false; - } - return; - } - - const center = viewState.center; - const projection = viewState.projection; - const units = this.getUnits(); - const pointResolutionUnits = units == Units.DEGREES ? - ProjUnits.DEGREES : - ProjUnits.METERS; - let pointResolution = - getPointResolution(projection, viewState.resolution, center, pointResolutionUnits); - if (projection.getUnits() != ProjUnits.DEGREES && projection.getMetersPerUnit() - && pointResolutionUnits == ProjUnits.METERS) { - pointResolution *= projection.getMetersPerUnit(); - } - - let nominalCount = this.minWidth_ * pointResolution; - let suffix = ''; - if (units == Units.DEGREES) { - const metersPerDegree = METERS_PER_UNIT[ProjUnits.DEGREES]; - if (projection.getUnits() == ProjUnits.DEGREES) { - nominalCount *= metersPerDegree; - } else { - pointResolution /= metersPerDegree; - } - if (nominalCount < metersPerDegree / 60) { - suffix = '\u2033'; // seconds - pointResolution *= 3600; - } else if (nominalCount < metersPerDegree) { - suffix = '\u2032'; // minutes - pointResolution *= 60; - } else { - suffix = '\u00b0'; // degrees - } - } else if (units == Units.IMPERIAL) { - if (nominalCount < 0.9144) { - suffix = 'in'; - pointResolution /= 0.0254; - } else if (nominalCount < 1609.344) { - suffix = 'ft'; - pointResolution /= 0.3048; - } else { - suffix = 'mi'; - pointResolution /= 1609.344; - } - } else if (units == Units.NAUTICAL) { - pointResolution /= 1852; - suffix = 'nm'; - } else if (units == Units.METRIC) { - if (nominalCount < 0.001) { - suffix = 'μm'; - pointResolution *= 1000000; - } else if (nominalCount < 1) { - suffix = 'mm'; - pointResolution *= 1000; - } else if (nominalCount < 1000) { - suffix = 'm'; - } else { - suffix = 'km'; - pointResolution /= 1000; - } - } else if (units == Units.US) { - if (nominalCount < 0.9144) { - suffix = 'in'; - pointResolution *= 39.37; - } else if (nominalCount < 1609.344) { - suffix = 'ft'; - pointResolution /= 0.30480061; - } else { - suffix = 'mi'; - pointResolution /= 1609.3472; - } - } else { - assert(false, 33); // Invalid units - } - - let i = 3 * Math.floor( - Math.log(this.minWidth_ * pointResolution) / Math.log(10)); - let count, width; - while (true) { - count = LEADING_DIGITS[((i % 3) + 3) % 3] * - Math.pow(10, Math.floor(i / 3)); - width = Math.round(count / pointResolution); - if (isNaN(width)) { - this.element_.style.display = 'none'; - this.renderedVisible_ = false; - return; - } else if (width >= this.minWidth_) { - break; - } - ++i; - } - - const html = count + ' ' + suffix; - if (this.renderedHTML_ != html) { - this.innerElement_.innerHTML = html; - this.renderedHTML_ = html; - } - - if (this.renderedWidth_ != width) { - this.innerElement_.style.width = width + 'px'; - this.renderedWidth_ = width; - } - - if (!this.renderedVisible_) { - this.element_.style.display = ''; - this.renderedVisible_ = true; - } - -}; - export default ScaleLine; diff --git a/src/ol/control/Zoom.js b/src/ol/control/Zoom.js index 235e216d80..e8ecbe4f65 100644 --- a/src/ol/control/Zoom.js +++ b/src/ol/control/Zoom.js @@ -36,104 +36,106 @@ import {easeOut} from '../easing.js'; * @param {module:ol/control/Zoom~Options=} opt_options Zoom options. * @api */ -const Zoom = function(opt_options) { +class Zoom { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - const className = options.className !== undefined ? options.className : 'ol-zoom'; + const className = options.className !== undefined ? options.className : 'ol-zoom'; - const delta = options.delta !== undefined ? options.delta : 1; + const delta = options.delta !== undefined ? options.delta : 1; - const zoomInLabel = options.zoomInLabel !== undefined ? options.zoomInLabel : '+'; - const zoomOutLabel = options.zoomOutLabel !== undefined ? options.zoomOutLabel : '\u2212'; + const zoomInLabel = options.zoomInLabel !== undefined ? options.zoomInLabel : '+'; + const zoomOutLabel = options.zoomOutLabel !== undefined ? options.zoomOutLabel : '\u2212'; - const zoomInTipLabel = options.zoomInTipLabel !== undefined ? - options.zoomInTipLabel : 'Zoom in'; - const zoomOutTipLabel = options.zoomOutTipLabel !== undefined ? - options.zoomOutTipLabel : 'Zoom out'; + const zoomInTipLabel = options.zoomInTipLabel !== undefined ? + options.zoomInTipLabel : 'Zoom in'; + const zoomOutTipLabel = options.zoomOutTipLabel !== undefined ? + options.zoomOutTipLabel : 'Zoom out'; - const inElement = document.createElement('button'); - inElement.className = className + '-in'; - inElement.setAttribute('type', 'button'); - inElement.title = zoomInTipLabel; - inElement.appendChild( - typeof zoomInLabel === 'string' ? document.createTextNode(zoomInLabel) : zoomInLabel - ); + const inElement = document.createElement('button'); + inElement.className = className + '-in'; + inElement.setAttribute('type', 'button'); + inElement.title = zoomInTipLabel; + inElement.appendChild( + typeof zoomInLabel === 'string' ? document.createTextNode(zoomInLabel) : zoomInLabel + ); - listen(inElement, EventType.CLICK, - Zoom.prototype.handleClick_.bind(this, delta)); + listen(inElement, EventType.CLICK, + Zoom.prototype.handleClick_.bind(this, delta)); - const outElement = document.createElement('button'); - outElement.className = className + '-out'; - outElement.setAttribute('type', 'button'); - outElement.title = zoomOutTipLabel; - outElement.appendChild( - typeof zoomOutLabel === 'string' ? document.createTextNode(zoomOutLabel) : zoomOutLabel - ); + const outElement = document.createElement('button'); + outElement.className = className + '-out'; + outElement.setAttribute('type', 'button'); + outElement.title = zoomOutTipLabel; + outElement.appendChild( + typeof zoomOutLabel === 'string' ? document.createTextNode(zoomOutLabel) : zoomOutLabel + ); - listen(outElement, EventType.CLICK, - Zoom.prototype.handleClick_.bind(this, -delta)); + listen(outElement, EventType.CLICK, + Zoom.prototype.handleClick_.bind(this, -delta)); - const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL; - const element = document.createElement('div'); - element.className = cssClasses; - element.appendChild(inElement); - element.appendChild(outElement); + const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL; + const element = document.createElement('div'); + element.className = cssClasses; + element.appendChild(inElement); + element.appendChild(outElement); - Control.call(this, { - element: element, - target: options.target - }); + Control.call(this, { + element: element, + target: options.target + }); + + /** + * @type {number} + * @private + */ + this.duration_ = options.duration !== undefined ? options.duration : 250; + + } /** - * @type {number} + * @param {number} delta Zoom delta. + * @param {MouseEvent} event The event to handle * @private */ - this.duration_ = options.duration !== undefined ? options.duration : 250; + handleClick_(delta, event) { + event.preventDefault(); + this.zoomByDelta_(delta); + } -}; + /** + * @param {number} delta Zoom delta. + * @private + */ + zoomByDelta_(delta) { + const map = this.getMap(); + const view = map.getView(); + if (!view) { + // the map does not have a view, so we can't act + // upon it + return; + } + const currentResolution = view.getResolution(); + if (currentResolution) { + const newResolution = view.constrainResolution(currentResolution, delta); + if (this.duration_ > 0) { + if (view.getAnimating()) { + view.cancelAnimations(); + } + view.animate({ + resolution: newResolution, + duration: this.duration_, + easing: easeOut + }); + } else { + view.setResolution(newResolution); + } + } + } +} inherits(Zoom, Control); -/** - * @param {number} delta Zoom delta. - * @param {MouseEvent} event The event to handle - * @private - */ -Zoom.prototype.handleClick_ = function(delta, event) { - event.preventDefault(); - this.zoomByDelta_(delta); -}; - - -/** - * @param {number} delta Zoom delta. - * @private - */ -Zoom.prototype.zoomByDelta_ = function(delta) { - const map = this.getMap(); - const view = map.getView(); - if (!view) { - // the map does not have a view, so we can't act - // upon it - return; - } - const currentResolution = view.getResolution(); - if (currentResolution) { - const newResolution = view.constrainResolution(currentResolution, delta); - if (this.duration_ > 0) { - if (view.getAnimating()) { - view.cancelAnimations(); - } - view.animate({ - resolution: newResolution, - duration: this.duration_, - easing: easeOut - }); - } else { - view.setResolution(newResolution); - } - } -}; export default Zoom; diff --git a/src/ol/control/ZoomSlider.js b/src/ol/control/ZoomSlider.js index 982e946c08..9cea621fcb 100644 --- a/src/ol/control/ZoomSlider.js +++ b/src/ol/control/ZoomSlider.js @@ -47,164 +47,303 @@ const Direction = { * @param {module:ol/control/ZoomSlider~Options=} opt_options Zoom slider options. * @api */ -const ZoomSlider = function(opt_options) { +class ZoomSlider { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; + + /** + * Will hold the current resolution of the view. + * + * @type {number|undefined} + * @private + */ + this.currentResolution_ = undefined; + + /** + * The direction of the slider. Will be determined from actual display of the + * container and defaults to Direction.VERTICAL. + * + * @type {Direction} + * @private + */ + this.direction_ = Direction.VERTICAL; + + /** + * @type {boolean} + * @private + */ + this.dragging_; + + /** + * @type {number} + * @private + */ + this.heightLimit_ = 0; + + /** + * @type {number} + * @private + */ + this.widthLimit_ = 0; + + /** + * @type {number|undefined} + * @private + */ + this.previousX_; + + /** + * @type {number|undefined} + * @private + */ + this.previousY_; + + /** + * The calculated thumb size (border box plus margins). Set when initSlider_ + * is called. + * @type {module:ol/size~Size} + * @private + */ + this.thumbSize_ = null; + + /** + * Whether the slider is initialized. + * @type {boolean} + * @private + */ + this.sliderInitialized_ = false; + + /** + * @type {number} + * @private + */ + this.duration_ = options.duration !== undefined ? options.duration : 200; + + const className = options.className !== undefined ? options.className : 'ol-zoomslider'; + const thumbElement = document.createElement('button'); + thumbElement.setAttribute('type', 'button'); + thumbElement.className = className + '-thumb ' + CLASS_UNSELECTABLE; + const containerElement = document.createElement('div'); + containerElement.className = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL; + containerElement.appendChild(thumbElement); + /** + * @type {module:ol/pointer/PointerEventHandler} + * @private + */ + this.dragger_ = new PointerEventHandler(containerElement); + + listen(this.dragger_, PointerEventType.POINTERDOWN, + this.handleDraggerStart_, this); + listen(this.dragger_, PointerEventType.POINTERMOVE, + this.handleDraggerDrag_, this); + listen(this.dragger_, PointerEventType.POINTERUP, + this.handleDraggerEnd_, this); + + listen(containerElement, EventType.CLICK, this.handleContainerClick_, this); + listen(thumbElement, EventType.CLICK, stopPropagation); + + Control.call(this, { + element: containerElement, + render: options.render || render + }); + } /** - * Will hold the current resolution of the view. + * @inheritDoc + */ + disposeInternal() { + this.dragger_.dispose(); + Control.prototype.disposeInternal.call(this); + } + + /** + * @inheritDoc + */ + setMap(map) { + Control.prototype.setMap.call(this, map); + if (map) { + map.render(); + } + } + + /** + * Initializes the slider element. This will determine and set this controls + * direction_ and also constrain the dragging of the thumb to always be within + * the bounds of the container. * - * @type {number|undefined} * @private */ - this.currentResolution_ = undefined; + initSlider_() { + const container = this.element; + const containerSize = { + width: container.offsetWidth, height: container.offsetHeight + }; + + const thumb = container.firstElementChild; + const computedStyle = getComputedStyle(thumb); + const thumbWidth = thumb.offsetWidth + + parseFloat(computedStyle['marginRight']) + + parseFloat(computedStyle['marginLeft']); + const thumbHeight = thumb.offsetHeight + + parseFloat(computedStyle['marginTop']) + + parseFloat(computedStyle['marginBottom']); + this.thumbSize_ = [thumbWidth, thumbHeight]; + + if (containerSize.width > containerSize.height) { + this.direction_ = Direction.HORIZONTAL; + this.widthLimit_ = containerSize.width - thumbWidth; + } else { + this.direction_ = Direction.VERTICAL; + this.heightLimit_ = containerSize.height - thumbHeight; + } + this.sliderInitialized_ = true; + } /** - * The direction of the slider. Will be determined from actual display of the - * container and defaults to Direction.VERTICAL. + * @param {MouseEvent} event The browser event to handle. + * @private + */ + handleContainerClick_(event) { + const view = this.getMap().getView(); + + const relativePosition = this.getRelativePosition_( + event.offsetX - this.thumbSize_[0] / 2, + event.offsetY - this.thumbSize_[1] / 2); + + const resolution = this.getResolutionForPosition_(relativePosition); + + view.animate({ + resolution: view.constrainResolution(resolution), + duration: this.duration_, + easing: easeOut + }); + } + + /** + * Handle dragger start events. + * @param {module:ol/pointer/PointerEvent} event The drag event. + * @private + */ + handleDraggerStart_(event) { + if (!this.dragging_ && event.originalEvent.target === this.element.firstElementChild) { + this.getMap().getView().setHint(ViewHint.INTERACTING, 1); + this.previousX_ = event.clientX; + this.previousY_ = event.clientY; + this.dragging_ = true; + } + } + + /** + * Handle dragger drag events. * - * @type {Direction} + * @param {module:ol/pointer/PointerEvent|Event} event The drag event. * @private */ - this.direction_ = Direction.VERTICAL; + handleDraggerDrag_(event) { + if (this.dragging_) { + const element = this.element.firstElementChild; + const deltaX = event.clientX - this.previousX_ + parseInt(element.style.left, 10); + const deltaY = event.clientY - this.previousY_ + parseInt(element.style.top, 10); + const relativePosition = this.getRelativePosition_(deltaX, deltaY); + this.currentResolution_ = this.getResolutionForPosition_(relativePosition); + this.getMap().getView().setResolution(this.currentResolution_); + this.setThumbPosition_(this.currentResolution_); + this.previousX_ = event.clientX; + this.previousY_ = event.clientY; + } + } /** - * @type {boolean} + * Handle dragger end events. + * @param {module:ol/pointer/PointerEvent|Event} event The drag event. * @private */ - this.dragging_; + handleDraggerEnd_(event) { + if (this.dragging_) { + const view = this.getMap().getView(); + view.setHint(ViewHint.INTERACTING, -1); + + view.animate({ + resolution: view.constrainResolution(this.currentResolution_), + duration: this.duration_, + easing: easeOut + }); + + this.dragging_ = false; + this.previousX_ = undefined; + this.previousY_ = undefined; + } + } /** - * @type {number} + * Positions the thumb inside its container according to the given resolution. + * + * @param {number} res The res. * @private */ - this.heightLimit_ = 0; + setThumbPosition_(res) { + const position = this.getPositionForResolution_(res); + const thumb = this.element.firstElementChild; + + if (this.direction_ == Direction.HORIZONTAL) { + thumb.style.left = this.widthLimit_ * position + 'px'; + } else { + thumb.style.top = this.heightLimit_ * position + 'px'; + } + } /** - * @type {number} + * Calculates the relative position of the thumb given x and y offsets. The + * relative position scales from 0 to 1. The x and y offsets are assumed to be + * in pixel units within the dragger limits. + * + * @param {number} x Pixel position relative to the left of the slider. + * @param {number} y Pixel position relative to the top of the slider. + * @return {number} The relative position of the thumb. * @private */ - this.widthLimit_ = 0; + getRelativePosition_(x, y) { + let amount; + if (this.direction_ === Direction.HORIZONTAL) { + amount = x / this.widthLimit_; + } else { + amount = y / this.heightLimit_; + } + return clamp(amount, 0, 1); + } /** - * @type {number|undefined} + * Calculates the corresponding resolution of the thumb given its relative + * position (where 0 is the minimum and 1 is the maximum). + * + * @param {number} position The relative position of the thumb. + * @return {number} The corresponding resolution. * @private */ - this.previousX_; + getResolutionForPosition_(position) { + const fn = this.getMap().getView().getResolutionForValueFunction(); + return fn(1 - position); + } /** - * @type {number|undefined} + * Determines the relative position of the slider for the given resolution. A + * relative position of 0 corresponds to the minimum view resolution. A + * relative position of 1 corresponds to the maximum view resolution. + * + * @param {number} res The resolution. + * @return {number} The relative position value (between 0 and 1). * @private */ - this.previousY_; - - /** - * The calculated thumb size (border box plus margins). Set when initSlider_ - * is called. - * @type {module:ol/size~Size} - * @private - */ - this.thumbSize_ = null; - - /** - * Whether the slider is initialized. - * @type {boolean} - * @private - */ - this.sliderInitialized_ = false; - - /** - * @type {number} - * @private - */ - this.duration_ = options.duration !== undefined ? options.duration : 200; - - const className = options.className !== undefined ? options.className : 'ol-zoomslider'; - const thumbElement = document.createElement('button'); - thumbElement.setAttribute('type', 'button'); - thumbElement.className = className + '-thumb ' + CLASS_UNSELECTABLE; - const containerElement = document.createElement('div'); - containerElement.className = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL; - containerElement.appendChild(thumbElement); - /** - * @type {module:ol/pointer/PointerEventHandler} - * @private - */ - this.dragger_ = new PointerEventHandler(containerElement); - - listen(this.dragger_, PointerEventType.POINTERDOWN, - this.handleDraggerStart_, this); - listen(this.dragger_, PointerEventType.POINTERMOVE, - this.handleDraggerDrag_, this); - listen(this.dragger_, PointerEventType.POINTERUP, - this.handleDraggerEnd_, this); - - listen(containerElement, EventType.CLICK, this.handleContainerClick_, this); - listen(thumbElement, EventType.CLICK, stopPropagation); - - Control.call(this, { - element: containerElement, - render: options.render || render - }); -}; + getPositionForResolution_(res) { + const fn = this.getMap().getView().getValueForResolutionFunction(); + return 1 - fn(res); + } +} inherits(ZoomSlider, Control); -/** - * @inheritDoc - */ -ZoomSlider.prototype.disposeInternal = function() { - this.dragger_.dispose(); - Control.prototype.disposeInternal.call(this); -}; - - -/** - * @inheritDoc - */ -ZoomSlider.prototype.setMap = function(map) { - Control.prototype.setMap.call(this, map); - if (map) { - map.render(); - } -}; - - -/** - * Initializes the slider element. This will determine and set this controls - * direction_ and also constrain the dragging of the thumb to always be within - * the bounds of the container. - * - * @private - */ -ZoomSlider.prototype.initSlider_ = function() { - const container = this.element; - const containerSize = { - width: container.offsetWidth, height: container.offsetHeight - }; - - const thumb = container.firstElementChild; - const computedStyle = getComputedStyle(thumb); - const thumbWidth = thumb.offsetWidth + - parseFloat(computedStyle['marginRight']) + - parseFloat(computedStyle['marginLeft']); - const thumbHeight = thumb.offsetHeight + - parseFloat(computedStyle['marginTop']) + - parseFloat(computedStyle['marginBottom']); - this.thumbSize_ = [thumbWidth, thumbHeight]; - - if (containerSize.width > containerSize.height) { - this.direction_ = Direction.HORIZONTAL; - this.widthLimit_ = containerSize.width - thumbWidth; - } else { - this.direction_ = Direction.VERTICAL; - this.heightLimit_ = containerSize.height - thumbHeight; - } - this.sliderInitialized_ = true; -}; - - /** * Update the zoomslider element. * @param {module:ol/MapEvent} mapEvent Map event. @@ -226,151 +365,4 @@ export function render(mapEvent) { } -/** - * @param {MouseEvent} event The browser event to handle. - * @private - */ -ZoomSlider.prototype.handleContainerClick_ = function(event) { - const view = this.getMap().getView(); - - const relativePosition = this.getRelativePosition_( - event.offsetX - this.thumbSize_[0] / 2, - event.offsetY - this.thumbSize_[1] / 2); - - const resolution = this.getResolutionForPosition_(relativePosition); - - view.animate({ - resolution: view.constrainResolution(resolution), - duration: this.duration_, - easing: easeOut - }); -}; - - -/** - * Handle dragger start events. - * @param {module:ol/pointer/PointerEvent} event The drag event. - * @private - */ -ZoomSlider.prototype.handleDraggerStart_ = function(event) { - if (!this.dragging_ && event.originalEvent.target === this.element.firstElementChild) { - this.getMap().getView().setHint(ViewHint.INTERACTING, 1); - this.previousX_ = event.clientX; - this.previousY_ = event.clientY; - this.dragging_ = true; - } -}; - - -/** - * Handle dragger drag events. - * - * @param {module:ol/pointer/PointerEvent|Event} event The drag event. - * @private - */ -ZoomSlider.prototype.handleDraggerDrag_ = function(event) { - if (this.dragging_) { - const element = this.element.firstElementChild; - const deltaX = event.clientX - this.previousX_ + parseInt(element.style.left, 10); - const deltaY = event.clientY - this.previousY_ + parseInt(element.style.top, 10); - const relativePosition = this.getRelativePosition_(deltaX, deltaY); - this.currentResolution_ = this.getResolutionForPosition_(relativePosition); - this.getMap().getView().setResolution(this.currentResolution_); - this.setThumbPosition_(this.currentResolution_); - this.previousX_ = event.clientX; - this.previousY_ = event.clientY; - } -}; - - -/** - * Handle dragger end events. - * @param {module:ol/pointer/PointerEvent|Event} event The drag event. - * @private - */ -ZoomSlider.prototype.handleDraggerEnd_ = function(event) { - if (this.dragging_) { - const view = this.getMap().getView(); - view.setHint(ViewHint.INTERACTING, -1); - - view.animate({ - resolution: view.constrainResolution(this.currentResolution_), - duration: this.duration_, - easing: easeOut - }); - - this.dragging_ = false; - this.previousX_ = undefined; - this.previousY_ = undefined; - } -}; - - -/** - * Positions the thumb inside its container according to the given resolution. - * - * @param {number} res The res. - * @private - */ -ZoomSlider.prototype.setThumbPosition_ = function(res) { - const position = this.getPositionForResolution_(res); - const thumb = this.element.firstElementChild; - - if (this.direction_ == Direction.HORIZONTAL) { - thumb.style.left = this.widthLimit_ * position + 'px'; - } else { - thumb.style.top = this.heightLimit_ * position + 'px'; - } -}; - - -/** - * Calculates the relative position of the thumb given x and y offsets. The - * relative position scales from 0 to 1. The x and y offsets are assumed to be - * in pixel units within the dragger limits. - * - * @param {number} x Pixel position relative to the left of the slider. - * @param {number} y Pixel position relative to the top of the slider. - * @return {number} The relative position of the thumb. - * @private - */ -ZoomSlider.prototype.getRelativePosition_ = function(x, y) { - let amount; - if (this.direction_ === Direction.HORIZONTAL) { - amount = x / this.widthLimit_; - } else { - amount = y / this.heightLimit_; - } - return clamp(amount, 0, 1); -}; - - -/** - * Calculates the corresponding resolution of the thumb given its relative - * position (where 0 is the minimum and 1 is the maximum). - * - * @param {number} position The relative position of the thumb. - * @return {number} The corresponding resolution. - * @private - */ -ZoomSlider.prototype.getResolutionForPosition_ = function(position) { - const fn = this.getMap().getView().getResolutionForValueFunction(); - return fn(1 - position); -}; - - -/** - * Determines the relative position of the slider for the given resolution. A - * relative position of 0 corresponds to the minimum view resolution. A - * relative position of 1 corresponds to the maximum view resolution. - * - * @param {number} res The resolution. - * @return {number} The relative position value (between 0 and 1). - * @private - */ -ZoomSlider.prototype.getPositionForResolution_ = function(res) { - const fn = this.getMap().getView().getValueForResolutionFunction(); - return 1 - fn(res); -}; - export default ZoomSlider; diff --git a/src/ol/control/ZoomToExtent.js b/src/ol/control/ZoomToExtent.js index 49c8cf1b8b..cafa6d7190 100644 --- a/src/ol/control/ZoomToExtent.js +++ b/src/ol/control/ZoomToExtent.js @@ -31,59 +31,61 @@ import {CLASS_CONTROL, CLASS_UNSELECTABLE} from '../css.js'; * @param {module:ol/control/ZoomToExtent~Options=} opt_options Options. * @api */ -const ZoomToExtent = function(opt_options) { - const options = opt_options ? opt_options : {}; +class ZoomToExtent { + constructor(opt_options) { + const options = opt_options ? opt_options : {}; - /** - * @type {module:ol/extent~Extent} - * @protected - */ - this.extent = options.extent ? options.extent : null; + /** + * @type {module:ol/extent~Extent} + * @protected + */ + this.extent = options.extent ? options.extent : null; - const className = options.className !== undefined ? options.className : 'ol-zoom-extent'; + const className = options.className !== undefined ? options.className : 'ol-zoom-extent'; - const label = options.label !== undefined ? options.label : 'E'; - const tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Fit to extent'; - const button = document.createElement('button'); - button.setAttribute('type', 'button'); - button.title = tipLabel; - button.appendChild( - typeof label === 'string' ? document.createTextNode(label) : label - ); + const label = options.label !== undefined ? options.label : 'E'; + const tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Fit to extent'; + const button = document.createElement('button'); + button.setAttribute('type', 'button'); + button.title = tipLabel; + button.appendChild( + typeof label === 'string' ? document.createTextNode(label) : label + ); - listen(button, EventType.CLICK, this.handleClick_, this); + listen(button, EventType.CLICK, this.handleClick_, this); - const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL; - const element = document.createElement('div'); - element.className = cssClasses; - element.appendChild(button); + const cssClasses = className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL; + const element = document.createElement('div'); + element.className = cssClasses; + element.appendChild(button); - Control.call(this, { - element: element, - target: options.target - }); -}; + Control.call(this, { + element: element, + target: options.target + }); + } + + /** + * @param {MouseEvent} event The event to handle + * @private + */ + handleClick_(event) { + event.preventDefault(); + this.handleZoomToExtent(); + } + + /** + * @protected + */ + handleZoomToExtent() { + const map = this.getMap(); + const view = map.getView(); + const extent = !this.extent ? view.getProjection().getExtent() : this.extent; + view.fit(extent); + } +} inherits(ZoomToExtent, Control); -/** - * @param {MouseEvent} event The event to handle - * @private - */ -ZoomToExtent.prototype.handleClick_ = function(event) { - event.preventDefault(); - this.handleZoomToExtent(); -}; - - -/** - * @protected - */ -ZoomToExtent.prototype.handleZoomToExtent = function() { - const map = this.getMap(); - const view = map.getView(); - const extent = !this.extent ? view.getProjection().getExtent() : this.extent; - view.fit(extent); -}; export default ZoomToExtent; diff --git a/src/ol/events/EventTarget.js b/src/ol/events/EventTarget.js index 1eaae0d5a0..e7cbaa9db4 100644 --- a/src/ol/events/EventTarget.js +++ b/src/ol/events/EventTarget.js @@ -31,137 +31,135 @@ import Event from '../events/Event.js'; * @constructor * @extends {module:ol/Disposable} */ -const EventTarget = function() { +class EventTarget { + constructor() { - Disposable.call(this); + Disposable.call(this); + + /** + * @private + * @type {!Object.} + */ + this.pendingRemovals_ = {}; + + /** + * @private + * @type {!Object.} + */ + this.dispatching_ = {}; + + /** + * @private + * @type {!Object.>} + */ + this.listeners_ = {}; + + } /** - * @private - * @type {!Object.} + * @param {string} type Type. + * @param {module:ol/events~ListenerFunction} listener Listener. */ - this.pendingRemovals_ = {}; + addEventListener(type, listener) { + let listeners = this.listeners_[type]; + if (!listeners) { + listeners = this.listeners_[type] = []; + } + if (listeners.indexOf(listener) === -1) { + listeners.push(listener); + } + } /** - * @private - * @type {!Object.} + * @param {{type: string, + * target: (EventTarget|module:ol/events/EventTarget|undefined)}|module:ol/events/Event| + * string} event Event or event type. + * @return {boolean|undefined} `false` if anyone called preventDefault on the + * event object or if any of the listeners returned false. */ - this.dispatching_ = {}; + dispatchEvent(event) { + const evt = typeof event === 'string' ? new Event(event) : event; + const type = evt.type; + evt.target = this; + const listeners = this.listeners_[type]; + let propagate; + if (listeners) { + if (!(type in this.dispatching_)) { + this.dispatching_[type] = 0; + this.pendingRemovals_[type] = 0; + } + ++this.dispatching_[type]; + for (let i = 0, ii = listeners.length; i < ii; ++i) { + if (listeners[i].call(this, evt) === false || evt.propagationStopped) { + propagate = false; + break; + } + } + --this.dispatching_[type]; + if (this.dispatching_[type] === 0) { + let pendingRemovals = this.pendingRemovals_[type]; + delete this.pendingRemovals_[type]; + while (pendingRemovals--) { + this.removeEventListener(type, UNDEFINED); + } + delete this.dispatching_[type]; + } + return propagate; + } + } /** - * @private - * @type {!Object.>} + * @inheritDoc */ - this.listeners_ = {}; + disposeInternal() { + unlistenAll(this); + } -}; + /** + * Get the listeners for a specified event type. Listeners are returned in the + * order that they will be called in. + * + * @param {string} type Type. + * @return {Array.} Listeners. + */ + getListeners(type) { + return this.listeners_[type]; + } + + /** + * @param {string=} opt_type Type. If not provided, + * `true` will be returned if this EventTarget has any listeners. + * @return {boolean} Has listeners. + */ + hasListener(opt_type) { + return opt_type ? + opt_type in this.listeners_ : + Object.keys(this.listeners_).length > 0; + } + + /** + * @param {string} type Type. + * @param {module:ol/events~ListenerFunction} listener Listener. + */ + removeEventListener(type, listener) { + const listeners = this.listeners_[type]; + if (listeners) { + const index = listeners.indexOf(listener); + if (type in this.pendingRemovals_) { + // make listener a no-op, and remove later in #dispatchEvent() + listeners[index] = UNDEFINED; + ++this.pendingRemovals_[type]; + } else { + listeners.splice(index, 1); + if (listeners.length === 0) { + delete this.listeners_[type]; + } + } + } + } +} inherits(EventTarget, Disposable); -/** - * @param {string} type Type. - * @param {module:ol/events~ListenerFunction} listener Listener. - */ -EventTarget.prototype.addEventListener = function(type, listener) { - let listeners = this.listeners_[type]; - if (!listeners) { - listeners = this.listeners_[type] = []; - } - if (listeners.indexOf(listener) === -1) { - listeners.push(listener); - } -}; - - -/** - * @param {{type: string, - * target: (EventTarget|module:ol/events/EventTarget|undefined)}|module:ol/events/Event| - * string} event Event or event type. - * @return {boolean|undefined} `false` if anyone called preventDefault on the - * event object or if any of the listeners returned false. - */ -EventTarget.prototype.dispatchEvent = function(event) { - const evt = typeof event === 'string' ? new Event(event) : event; - const type = evt.type; - evt.target = this; - const listeners = this.listeners_[type]; - let propagate; - if (listeners) { - if (!(type in this.dispatching_)) { - this.dispatching_[type] = 0; - this.pendingRemovals_[type] = 0; - } - ++this.dispatching_[type]; - for (let i = 0, ii = listeners.length; i < ii; ++i) { - if (listeners[i].call(this, evt) === false || evt.propagationStopped) { - propagate = false; - break; - } - } - --this.dispatching_[type]; - if (this.dispatching_[type] === 0) { - let pendingRemovals = this.pendingRemovals_[type]; - delete this.pendingRemovals_[type]; - while (pendingRemovals--) { - this.removeEventListener(type, UNDEFINED); - } - delete this.dispatching_[type]; - } - return propagate; - } -}; - - -/** - * @inheritDoc - */ -EventTarget.prototype.disposeInternal = function() { - unlistenAll(this); -}; - - -/** - * Get the listeners for a specified event type. Listeners are returned in the - * order that they will be called in. - * - * @param {string} type Type. - * @return {Array.} Listeners. - */ -EventTarget.prototype.getListeners = function(type) { - return this.listeners_[type]; -}; - - -/** - * @param {string=} opt_type Type. If not provided, - * `true` will be returned if this EventTarget has any listeners. - * @return {boolean} Has listeners. - */ -EventTarget.prototype.hasListener = function(opt_type) { - return opt_type ? - opt_type in this.listeners_ : - Object.keys(this.listeners_).length > 0; -}; - - -/** - * @param {string} type Type. - * @param {module:ol/events~ListenerFunction} listener Listener. - */ -EventTarget.prototype.removeEventListener = function(type, listener) { - const listeners = this.listeners_[type]; - if (listeners) { - const index = listeners.indexOf(listener); - if (type in this.pendingRemovals_) { - // make listener a no-op, and remove later in #dispatchEvent() - listeners[index] = UNDEFINED; - ++this.pendingRemovals_[type]; - } else { - listeners.splice(index, 1); - if (listeners.length === 0) { - delete this.listeners_[type]; - } - } - } -}; export default EventTarget; diff --git a/src/ol/format/EsriJSON.js b/src/ol/format/EsriJSON.js index 0ea7cd55a5..4b0ad55729 100644 --- a/src/ol/format/EsriJSON.js +++ b/src/ol/format/EsriJSON.js @@ -63,20 +63,148 @@ GEOMETRY_WRITERS[GeometryType.MULTI_POLYGON] = writeMultiPolygonGeometry; * @param {module:ol/format/EsriJSON~Options=} opt_options Options. * @api */ -const EsriJSON = function(opt_options) { +class EsriJSON { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - JSONFeature.call(this); + JSONFeature.call(this); + + /** + * Name of the geometry attribute for features. + * @type {string|undefined} + * @private + */ + this.geometryName_ = options.geometryName; + + } /** - * Name of the geometry attribute for features. - * @type {string|undefined} - * @private + * @inheritDoc */ - this.geometryName_ = options.geometryName; + readFeatureFromObject(object, opt_options) { + const esriJSONFeature = /** @type {EsriJSONFeature} */ (object); + const geometry = readGeometry(esriJSONFeature.geometry, opt_options); + const feature = new Feature(); + if (this.geometryName_) { + feature.setGeometryName(this.geometryName_); + } + feature.setGeometry(geometry); + if (opt_options && opt_options.idField && + esriJSONFeature.attributes[opt_options.idField]) { + feature.setId(/** @type {number} */(esriJSONFeature.attributes[opt_options.idField])); + } + if (esriJSONFeature.attributes) { + feature.setProperties(esriJSONFeature.attributes); + } + return feature; + } -}; + /** + * @inheritDoc + */ + readFeaturesFromObject(object, opt_options) { + const esriJSONObject = /** @type {EsriJSONObject} */ (object); + const options = opt_options ? opt_options : {}; + if (esriJSONObject.features) { + const esriJSONFeatureCollection = /** @type {EsriJSONFeatureCollection} */ (object); + /** @type {Array.} */ + const features = []; + const esriJSONFeatures = esriJSONFeatureCollection.features; + options.idField = object.objectIdFieldName; + for (let i = 0, ii = esriJSONFeatures.length; i < ii; ++i) { + features.push(this.readFeatureFromObject(esriJSONFeatures[i], options)); + } + return features; + } else { + return [this.readFeatureFromObject(object, options)]; + } + } + + /** + * @inheritDoc + */ + readGeometryFromObject(object, opt_options) { + return readGeometry(/** @type {EsriJSONGeometry} */(object), opt_options); + } + + /** + * @inheritDoc + */ + readProjectionFromObject(object) { + const esriJSONObject = /** @type {EsriJSONObject} */ (object); + if (esriJSONObject.spatialReference && esriJSONObject.spatialReference.wkid) { + const crs = esriJSONObject.spatialReference.wkid; + return getProjection('EPSG:' + crs); + } else { + return null; + } + } + + /** + * Encode a geometry as a EsriJSON object. + * + * @param {module:ol/geom/Geometry} geometry Geometry. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {EsriJSONGeometry} Object. + * @override + * @api + */ + writeGeometryObject(geometry, opt_options) { + return writeGeometry(geometry, this.adaptOptions(opt_options)); + } + + /** + * Encode a feature as a esriJSON Feature object. + * + * @param {module:ol/Feature} feature Feature. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {Object} Object. + * @override + * @api + */ + writeFeatureObject(feature, opt_options) { + opt_options = this.adaptOptions(opt_options); + const object = {}; + const geometry = feature.getGeometry(); + if (geometry) { + object['geometry'] = writeGeometry(geometry, opt_options); + if (opt_options && opt_options.featureProjection) { + object['geometry']['spatialReference'] = /** @type {EsriJSONCRS} */({ + wkid: getProjection(opt_options.featureProjection).getCode().split(':').pop() + }); + } + } + const properties = feature.getProperties(); + delete properties[feature.getGeometryName()]; + if (!isEmpty(properties)) { + object['attributes'] = properties; + } else { + object['attributes'] = {}; + } + return object; + } + + /** + * Encode an array of features as a EsriJSON object. + * + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {Object} EsriJSON Object. + * @override + * @api + */ + writeFeaturesObject(features, opt_options) { + opt_options = this.adaptOptions(opt_options); + const objects = []; + for (let i = 0, ii = features.length; i < ii; ++i) { + objects.push(this.writeFeatureObject(features[i], opt_options)); + } + return /** @type {EsriJSONFeatureCollection} */ ({ + 'features': objects + }); + } +} inherits(EsriJSON, JSONFeature); @@ -439,50 +567,6 @@ EsriJSON.prototype.readFeature; EsriJSON.prototype.readFeatures; -/** - * @inheritDoc - */ -EsriJSON.prototype.readFeatureFromObject = function(object, opt_options) { - const esriJSONFeature = /** @type {EsriJSONFeature} */ (object); - const geometry = readGeometry(esriJSONFeature.geometry, opt_options); - const feature = new Feature(); - if (this.geometryName_) { - feature.setGeometryName(this.geometryName_); - } - feature.setGeometry(geometry); - if (opt_options && opt_options.idField && - esriJSONFeature.attributes[opt_options.idField]) { - feature.setId(/** @type {number} */(esriJSONFeature.attributes[opt_options.idField])); - } - if (esriJSONFeature.attributes) { - feature.setProperties(esriJSONFeature.attributes); - } - return feature; -}; - - -/** - * @inheritDoc - */ -EsriJSON.prototype.readFeaturesFromObject = function(object, opt_options) { - const esriJSONObject = /** @type {EsriJSONObject} */ (object); - const options = opt_options ? opt_options : {}; - if (esriJSONObject.features) { - const esriJSONFeatureCollection = /** @type {EsriJSONFeatureCollection} */ (object); - /** @type {Array.} */ - const features = []; - const esriJSONFeatures = esriJSONFeatureCollection.features; - options.idField = object.objectIdFieldName; - for (let i = 0, ii = esriJSONFeatures.length; i < ii; ++i) { - features.push(this.readFeatureFromObject(esriJSONFeatures[i], options)); - } - return features; - } else { - return [this.readFeatureFromObject(object, options)]; - } -}; - - /** * Read a geometry from a EsriJSON source. * @@ -495,14 +579,6 @@ EsriJSON.prototype.readFeaturesFromObject = function(object, opt_options) { EsriJSON.prototype.readGeometry; -/** - * @inheritDoc - */ -EsriJSON.prototype.readGeometryFromObject = function(object, opt_options) { - return readGeometry(/** @type {EsriJSONGeometry} */(object), opt_options); -}; - - /** * Read the projection from a EsriJSON source. * @@ -514,20 +590,6 @@ EsriJSON.prototype.readGeometryFromObject = function(object, opt_options) { EsriJSON.prototype.readProjection; -/** - * @inheritDoc - */ -EsriJSON.prototype.readProjectionFromObject = function(object) { - const esriJSONObject = /** @type {EsriJSONObject} */ (object); - if (esriJSONObject.spatialReference && esriJSONObject.spatialReference.wkid) { - const crs = esriJSONObject.spatialReference.wkid; - return getProjection('EPSG:' + crs); - } else { - return null; - } -}; - - /** * @param {module:ol/geom/Geometry} geometry Geometry. * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. @@ -552,20 +614,6 @@ function writeGeometry(geometry, opt_options) { EsriJSON.prototype.writeGeometry; -/** - * Encode a geometry as a EsriJSON object. - * - * @param {module:ol/geom/Geometry} geometry Geometry. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {EsriJSONGeometry} Object. - * @override - * @api - */ -EsriJSON.prototype.writeGeometryObject = function(geometry, opt_options) { - return writeGeometry(geometry, this.adaptOptions(opt_options)); -}; - - /** * Encode a feature as a EsriJSON Feature string. * @@ -578,38 +626,6 @@ EsriJSON.prototype.writeGeometryObject = function(geometry, opt_options) { EsriJSON.prototype.writeFeature; -/** - * Encode a feature as a esriJSON Feature object. - * - * @param {module:ol/Feature} feature Feature. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {Object} Object. - * @override - * @api - */ -EsriJSON.prototype.writeFeatureObject = function(feature, opt_options) { - opt_options = this.adaptOptions(opt_options); - const object = {}; - const geometry = feature.getGeometry(); - if (geometry) { - object['geometry'] = writeGeometry(geometry, opt_options); - if (opt_options && opt_options.featureProjection) { - object['geometry']['spatialReference'] = /** @type {EsriJSONCRS} */({ - wkid: getProjection(opt_options.featureProjection).getCode().split(':').pop() - }); - } - } - const properties = feature.getProperties(); - delete properties[feature.getGeometryName()]; - if (!isEmpty(properties)) { - object['attributes'] = properties; - } else { - object['attributes'] = {}; - } - return object; -}; - - /** * Encode an array of features as EsriJSON. * @@ -622,24 +638,4 @@ EsriJSON.prototype.writeFeatureObject = function(feature, opt_options) { EsriJSON.prototype.writeFeatures; -/** - * Encode an array of features as a EsriJSON object. - * - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {Object} EsriJSON Object. - * @override - * @api - */ -EsriJSON.prototype.writeFeaturesObject = function(features, opt_options) { - opt_options = this.adaptOptions(opt_options); - const objects = []; - for (let i = 0, ii = features.length; i < ii; ++i) { - objects.push(this.writeFeatureObject(features[i], opt_options)); - } - return /** @type {EsriJSONFeatureCollection} */ ({ - 'features': objects - }); -}; - export default EsriJSON; diff --git a/src/ol/format/Feature.js b/src/ol/format/Feature.js index 6c770c93a9..089cbc518b 100644 --- a/src/ol/format/Feature.js +++ b/src/ol/format/Feature.js @@ -60,150 +60,141 @@ import {get as getProjection, equivalent as equivalentProjection, transformExten * @abstract * @api */ -const FeatureFormat = function() { +class FeatureFormat { + constructor() { - /** - * @protected - * @type {module:ol/proj/Projection} - */ - this.dataProjection = null; + /** + * @protected + * @type {module:ol/proj/Projection} + */ + this.dataProjection = null; - /** - * @protected - * @type {module:ol/proj/Projection} - */ - this.defaultFeatureProjection = null; + /** + * @protected + * @type {module:ol/proj/Projection} + */ + this.defaultFeatureProjection = null; -}; + } + /** + * Adds the data projection to the read options. + * @param {Document|Node|Object|string} source Source. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. + * @return {module:ol/format/Feature~ReadOptions|undefined} Options. + * @protected + */ + getReadOptions(source, opt_options) { + let options; + if (opt_options) { + options = { + dataProjection: opt_options.dataProjection ? + opt_options.dataProjection : this.readProjection(source), + featureProjection: opt_options.featureProjection + }; + } + return this.adaptOptions(options); + } -/** - * Adds the data projection to the read options. - * @param {Document|Node|Object|string} source Source. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. - * @return {module:ol/format/Feature~ReadOptions|undefined} Options. - * @protected - */ -FeatureFormat.prototype.getReadOptions = function(source, opt_options) { - let options; - if (opt_options) { - options = { - dataProjection: opt_options.dataProjection ? - opt_options.dataProjection : this.readProjection(source), - featureProjection: opt_options.featureProjection - }; - } - return this.adaptOptions(options); -}; + /** + * Sets the `dataProjection` on the options, if no `dataProjection` + * is set. + * @param {module:ol/format/Feature~WriteOptions|module:ol/format/Feature~ReadOptions|undefined} options + * Options. + * @protected + * @return {module:ol/format/Feature~WriteOptions|module:ol/format/Feature~ReadOptions|undefined} + * Updated options. + */ + adaptOptions(options) { + return assign({ + dataProjection: this.dataProjection, + featureProjection: this.defaultFeatureProjection + }, options); + } + /** + * Get the extent from the source of the last {@link readFeatures} call. + * @return {module:ol/extent~Extent} Tile extent. + */ + getLastExtent() { + return null; + } -/** - * Sets the `dataProjection` on the options, if no `dataProjection` - * is set. - * @param {module:ol/format/Feature~WriteOptions|module:ol/format/Feature~ReadOptions|undefined} options - * Options. - * @protected - * @return {module:ol/format/Feature~WriteOptions|module:ol/format/Feature~ReadOptions|undefined} - * Updated options. - */ -FeatureFormat.prototype.adaptOptions = function(options) { - return assign({ - dataProjection: this.dataProjection, - featureProjection: this.defaultFeatureProjection - }, options); -}; + /** + * @abstract + * @return {module:ol/format/FormatType} Format. + */ + getType() {} + /** + * Read a single feature from a source. + * + * @abstract + * @param {Document|Node|Object|string} source Source. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @return {module:ol/Feature} Feature. + */ + readFeature(source, opt_options) {} -/** - * Get the extent from the source of the last {@link readFeatures} call. - * @return {module:ol/extent~Extent} Tile extent. - */ -FeatureFormat.prototype.getLastExtent = function() { - return null; -}; + /** + * Read all features from a source. + * + * @abstract + * @param {Document|Node|ArrayBuffer|Object|string} source Source. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @return {Array.} Features. + */ + readFeatures(source, opt_options) {} + /** + * Read a single geometry from a source. + * + * @abstract + * @param {Document|Node|Object|string} source Source. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @return {module:ol/geom/Geometry} Geometry. + */ + readGeometry(source, opt_options) {} -/** - * @abstract - * @return {module:ol/format/FormatType} Format. - */ -FeatureFormat.prototype.getType = function() {}; + /** + * Read the projection from a source. + * + * @abstract + * @param {Document|Node|Object|string} source Source. + * @return {module:ol/proj/Projection} Projection. + */ + readProjection(source) {} + /** + * Encode a feature in this format. + * + * @abstract + * @param {module:ol/Feature} feature Feature. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {string} Result. + */ + writeFeature(feature, opt_options) {} -/** - * Read a single feature from a source. - * - * @abstract - * @param {Document|Node|Object|string} source Source. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @return {module:ol/Feature} Feature. - */ -FeatureFormat.prototype.readFeature = function(source, opt_options) {}; + /** + * Encode an array of features in this format. + * + * @abstract + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {string} Result. + */ + writeFeatures(features, opt_options) {} - -/** - * Read all features from a source. - * - * @abstract - * @param {Document|Node|ArrayBuffer|Object|string} source Source. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @return {Array.} Features. - */ -FeatureFormat.prototype.readFeatures = function(source, opt_options) {}; - - -/** - * Read a single geometry from a source. - * - * @abstract - * @param {Document|Node|Object|string} source Source. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @return {module:ol/geom/Geometry} Geometry. - */ -FeatureFormat.prototype.readGeometry = function(source, opt_options) {}; - - -/** - * Read the projection from a source. - * - * @abstract - * @param {Document|Node|Object|string} source Source. - * @return {module:ol/proj/Projection} Projection. - */ -FeatureFormat.prototype.readProjection = function(source) {}; - - -/** - * Encode a feature in this format. - * - * @abstract - * @param {module:ol/Feature} feature Feature. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {string} Result. - */ -FeatureFormat.prototype.writeFeature = function(feature, opt_options) {}; - - -/** - * Encode an array of features in this format. - * - * @abstract - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {string} Result. - */ -FeatureFormat.prototype.writeFeatures = function(features, opt_options) {}; - - -/** - * Write a single geometry in this format. - * - * @abstract - * @param {module:ol/geom/Geometry} geometry Geometry. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {string} Result. - */ -FeatureFormat.prototype.writeGeometry = function(geometry, opt_options) {}; + /** + * Write a single geometry in this format. + * + * @abstract + * @param {module:ol/geom/Geometry} geometry Geometry. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {string} Result. + */ + writeGeometry(geometry, opt_options) {} +} export default FeatureFormat; diff --git a/src/ol/format/GML2.js b/src/ol/format/GML2.js index 0cdd72c3ea..b16365f1e0 100644 --- a/src/ol/format/GML2.js +++ b/src/ol/format/GML2.js @@ -30,559 +30,550 @@ const schemaLocation = GMLNS + ' http://schemas.opengis.net/gml/2.1.2/feature.xs * @extends {module:ol/format/GMLBase} * @api */ -const GML2 = function(opt_options) { - const options = /** @type {module:ol/format/GMLBase~Options} */ - (opt_options ? opt_options : {}); +class GML2 { + constructor(opt_options) { + const options = /** @type {module:ol/format/GMLBase~Options} */ + (opt_options ? opt_options : {}); - GMLBase.call(this, options); + GMLBase.call(this, options); - this.FEATURE_COLLECTION_PARSERS[GMLNS][ - 'featureMember'] = - makeArrayPusher(GMLBase.prototype.readFeaturesInternal); + this.FEATURE_COLLECTION_PARSERS[GMLNS][ + 'featureMember'] = + makeArrayPusher(GMLBase.prototype.readFeaturesInternal); + + /** + * @inheritDoc + */ + this.schemaLocation = options.schemaLocation ? + options.schemaLocation : schemaLocation; + + } /** - * @inheritDoc + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.|undefined} Flat coordinates. */ - this.schemaLocation = options.schemaLocation ? - options.schemaLocation : schemaLocation; - -}; - -inherits(GML2, GMLBase); - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.|undefined} Flat coordinates. - */ -GML2.prototype.readFlatCoordinates_ = function(node, objectStack) { - const s = getAllTextContent(node, false).replace(/^\s*|\s*$/g, ''); - const context = /** @type {module:ol/xml~NodeStackItem} */ (objectStack[0]); - const containerSrs = context['srsName']; - let axisOrientation = 'enu'; - if (containerSrs) { - const proj = getProjection(containerSrs); - if (proj) { - axisOrientation = proj.getAxisOrientation(); + readFlatCoordinates_(node, objectStack) { + const s = getAllTextContent(node, false).replace(/^\s*|\s*$/g, ''); + const context = /** @type {module:ol/xml~NodeStackItem} */ (objectStack[0]); + const containerSrs = context['srsName']; + let axisOrientation = 'enu'; + if (containerSrs) { + const proj = getProjection(containerSrs); + if (proj) { + axisOrientation = proj.getAxisOrientation(); + } } - } - const coordsGroups = s.trim().split(/\s+/); - const flatCoordinates = []; - for (let i = 0, ii = coordsGroups.length; i < ii; i++) { - const coords = coordsGroups[i].split(/,+/); - const x = parseFloat(coords[0]); - const y = parseFloat(coords[1]); - const z = (coords.length === 3) ? parseFloat(coords[2]) : 0; - if (axisOrientation.substr(0, 2) === 'en') { - flatCoordinates.push(x, y, z); - } else { - flatCoordinates.push(y, x, z); - } - } - return flatCoordinates; -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {module:ol/extent~Extent|undefined} Envelope. - */ -GML2.prototype.readBox_ = function(node, objectStack) { - /** @type {Array.} */ - const flatCoordinates = pushParseAndPop([null], - this.BOX_PARSERS_, node, objectStack, this); - return createOrUpdate(flatCoordinates[1][0], - flatCoordinates[1][1], flatCoordinates[1][3], - flatCoordinates[1][4]); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GML2.prototype.innerBoundaryIsParser_ = function(node, objectStack) { - /** @type {Array.|undefined} */ - const flatLinearRing = pushParseAndPop(undefined, - this.RING_PARSERS, node, objectStack, this); - if (flatLinearRing) { - const flatLinearRings = /** @type {Array.>} */ - (objectStack[objectStack.length - 1]); - flatLinearRings.push(flatLinearRing); - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GML2.prototype.outerBoundaryIsParser_ = function(node, objectStack) { - /** @type {Array.|undefined} */ - const flatLinearRing = pushParseAndPop(undefined, - this.RING_PARSERS, node, objectStack, this); - if (flatLinearRing) { - const flatLinearRings = /** @type {Array.>} */ - (objectStack[objectStack.length - 1]); - flatLinearRings[0] = flatLinearRing; - } -}; - - -/** - * @const - * @param {*} value Value. - * @param {Array.<*>} objectStack Object stack. - * @param {string=} opt_nodeName Node name. - * @return {Node|undefined} Node. - * @private - */ -GML2.prototype.GEOMETRY_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) { - const context = objectStack[objectStack.length - 1]; - const multiSurface = context['multiSurface']; - const surface = context['surface']; - const multiCurve = context['multiCurve']; - let nodeName; - if (!Array.isArray(value)) { - nodeName = /** @type {module:ol/geom/Geometry} */ (value).getType(); - if (nodeName === 'MultiPolygon' && multiSurface === true) { - nodeName = 'MultiSurface'; - } else if (nodeName === 'Polygon' && surface === true) { - nodeName = 'Surface'; - } else if (nodeName === 'MultiLineString' && multiCurve === true) { - nodeName = 'MultiCurve'; - } - } else { - nodeName = 'Envelope'; - } - return createElementNS('http://www.opengis.net/gml', - nodeName); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/Feature} feature Feature. - * @param {Array.<*>} objectStack Node stack. - */ -GML2.prototype.writeFeatureElement = function(node, feature, objectStack) { - const fid = feature.getId(); - if (fid) { - node.setAttribute('fid', fid); - } - const context = /** @type {Object} */ (objectStack[objectStack.length - 1]); - const featureNS = context['featureNS']; - const geometryName = feature.getGeometryName(); - if (!context.serializers) { - 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 || value instanceof Geometry) { - if (!(key in context.serializers[featureNS])) { - context.serializers[featureNS][key] = makeChildAppender( - this.writeGeometryElement, this); - } + const coordsGroups = s.trim().split(/\s+/); + const flatCoordinates = []; + for (let i = 0, ii = coordsGroups.length; i < ii; i++) { + const coords = coordsGroups[i].split(/,+/); + const x = parseFloat(coords[0]); + const y = parseFloat(coords[1]); + const z = (coords.length === 3) ? parseFloat(coords[2]) : 0; + if (axisOrientation.substr(0, 2) === 'en') { + flatCoordinates.push(x, y, z); } else { - if (!(key in context.serializers[featureNS])) { - context.serializers[featureNS][key] = makeChildAppender(writeStringTextNode); + flatCoordinates.push(y, x, z); + } + } + return flatCoordinates; + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {module:ol/extent~Extent|undefined} Envelope. + */ + readBox_(node, objectStack) { + /** @type {Array.} */ + const flatCoordinates = pushParseAndPop([null], + this.BOX_PARSERS_, node, objectStack, this); + return createOrUpdate(flatCoordinates[1][0], + flatCoordinates[1][1], flatCoordinates[1][3], + flatCoordinates[1][4]); + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + innerBoundaryIsParser_(node, objectStack) { + /** @type {Array.|undefined} */ + const flatLinearRing = pushParseAndPop(undefined, + this.RING_PARSERS, node, objectStack, this); + if (flatLinearRing) { + const flatLinearRings = /** @type {Array.>} */ + (objectStack[objectStack.length - 1]); + flatLinearRings.push(flatLinearRing); + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + outerBoundaryIsParser_(node, objectStack) { + /** @type {Array.|undefined} */ + const flatLinearRing = pushParseAndPop(undefined, + this.RING_PARSERS, node, objectStack, this); + if (flatLinearRing) { + const flatLinearRings = /** @type {Array.>} */ + (objectStack[objectStack.length - 1]); + flatLinearRings[0] = flatLinearRing; + } + } + + /** + * @const + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node|undefined} Node. + * @private + */ + GEOMETRY_NODE_FACTORY_(value, objectStack, opt_nodeName) { + const context = objectStack[objectStack.length - 1]; + const multiSurface = context['multiSurface']; + const surface = context['surface']; + const multiCurve = context['multiCurve']; + let nodeName; + if (!Array.isArray(value)) { + nodeName = /** @type {module:ol/geom/Geometry} */ (value).getType(); + if (nodeName === 'MultiPolygon' && multiSurface === true) { + nodeName = 'MultiSurface'; + } else if (nodeName === 'Polygon' && surface === true) { + nodeName = 'Surface'; + } else if (nodeName === 'MultiLineString' && multiCurve === true) { + nodeName = 'MultiCurve'; + } + } else { + nodeName = 'Envelope'; + } + return createElementNS('http://www.opengis.net/gml', + nodeName); + } + + /** + * @param {Node} node Node. + * @param {module:ol/Feature} feature Feature. + * @param {Array.<*>} objectStack Node stack. + */ + writeFeatureElement(node, feature, objectStack) { + const fid = feature.getId(); + if (fid) { + node.setAttribute('fid', fid); + } + const context = /** @type {Object} */ (objectStack[objectStack.length - 1]); + const featureNS = context['featureNS']; + const geometryName = feature.getGeometryName(); + if (!context.serializers) { + 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 || value instanceof Geometry) { + 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); + } } } } + const item = assign({}, context); + item.node = node; + pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ + (item), context.serializers, + makeSimpleNodeFactory(undefined, featureNS), + values, + objectStack, keys); } - const item = assign({}, context); - item.node = node; - pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ - (item), context.serializers, - makeSimpleNodeFactory(undefined, featureNS), - values, - objectStack, keys); -}; - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LineString} geometry LineString geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeCurveOrLineString_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const srsName = context['srsName']; - if (node.nodeName !== 'LineStringSegment' && srsName) { - node.setAttribute('srsName', srsName); + /** + * @param {Node} node Node. + * @param {module:ol/geom/LineString} geometry LineString geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeCurveOrLineString_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const srsName = context['srsName']; + if (node.nodeName !== 'LineStringSegment' && srsName) { + node.setAttribute('srsName', srsName); + } + if (node.nodeName === 'LineString' || + node.nodeName === 'LineStringSegment') { + const coordinates = this.createCoordinatesNode_(node.namespaceURI); + node.appendChild(coordinates); + this.writeCoordinates_(coordinates, geometry, objectStack); + } else if (node.nodeName === 'Curve') { + const segments = createElementNS(node.namespaceURI, 'segments'); + node.appendChild(segments); + this.writeCurveSegments_(segments, + geometry, objectStack); + } } - if (node.nodeName === 'LineString' || - node.nodeName === 'LineStringSegment') { - const coordinates = this.createCoordinatesNode_(node.namespaceURI); - node.appendChild(coordinates); - this.writeCoordinates_(coordinates, geometry, objectStack); - } else if (node.nodeName === 'Curve') { - const segments = createElementNS(node.namespaceURI, 'segments'); - node.appendChild(segments); - this.writeCurveSegments_(segments, - geometry, objectStack); + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LineString} line LineString geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeLineStringOrCurveMember_(node, line, objectStack) { + const child = this.GEOMETRY_NODE_FACTORY_(line, objectStack); + if (child) { + node.appendChild(child); + this.writeCurveOrLineString_(child, line, objectStack); + } } -}; + /** + * @param {Node} node Node. + * @param {module:ol/geom/MultiLineString} geometry MultiLineString geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeMultiCurveOrLineString_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + const curve = context['curve']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const lines = geometry.getLineStrings(); + pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, curve: curve}, + this.LINESTRINGORCURVEMEMBER_SERIALIZERS_, + this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, lines, + objectStack, undefined, this); + } -/** - * @param {Node} node Node. - * @param {module:ol/geom/LineString} line LineString geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeLineStringOrCurveMember_ = function(node, line, objectStack) { - const child = this.GEOMETRY_NODE_FACTORY_(line, objectStack); - if (child) { + /** + * @param {Node} node Node. + * @param {module:ol/geom/Geometry|module:ol/extent~Extent} geometry Geometry. + * @param {Array.<*>} objectStack Node stack. + */ + writeGeometryElement(node, geometry, objectStack) { + const context = /** @type {module:ol/format/Feature~WriteOptions} */ (objectStack[objectStack.length - 1]); + const item = assign({}, context); + item.node = node; + let value; + if (Array.isArray(geometry)) { + if (context.dataProjection) { + value = transformExtent( + geometry, context.featureProjection, context.dataProjection); + } else { + value = geometry; + } + } else { + value = transformWithOptions(/** @type {module:ol/geom/Geometry} */ (geometry), true, context); + } + pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ + (item), this.GEOMETRY_SERIALIZERS_, + this.GEOMETRY_NODE_FACTORY_, [value], + objectStack, undefined, this); + } + + /** + * @param {string} namespaceURI XML namespace. + * @returns {Node} coordinates node. + * @private + */ + createCoordinatesNode_(namespaceURI) { + const coordinates = createElementNS(namespaceURI, 'coordinates'); + coordinates.setAttribute('decimal', '.'); + coordinates.setAttribute('cs', ','); + coordinates.setAttribute('ts', ' '); + + return coordinates; + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LineString|module:ol/geom/LinearRing} value Geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeCoordinates_(node, value, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + // only 2d for simple features profile + const points = value.getCoordinates(); + const len = points.length; + const parts = new Array(len); + for (let i = 0; i < len; ++i) { + const point = points[i]; + parts[i] = this.getCoords_(point, srsName, hasZ); + } + writeStringTextNode(node, parts.join(' ')); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LineString} line LineString geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeCurveSegments_(node, line, objectStack) { + const child = createElementNS(node.namespaceURI, 'LineStringSegment'); node.appendChild(child); this.writeCurveOrLineString_(child, line, objectStack); } -}; - -/** - * @param {Node} node Node. - * @param {module:ol/geom/MultiLineString} geometry MultiLineString geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeMultiCurveOrLineString_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - const curve = context['curve']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const lines = geometry.getLineStrings(); - pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, curve: curve}, - this.LINESTRINGORCURVEMEMBER_SERIALIZERS_, - this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, lines, - objectStack, undefined, this); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Geometry|module:ol/extent~Extent} geometry Geometry. - * @param {Array.<*>} objectStack Node stack. - */ -GML2.prototype.writeGeometryElement = function(node, geometry, objectStack) { - const context = /** @type {module:ol/format/Feature~WriteOptions} */ (objectStack[objectStack.length - 1]); - const item = assign({}, context); - item.node = node; - let value; - if (Array.isArray(geometry)) { - if (context.dataProjection) { - value = transformExtent( - geometry, context.featureProjection, context.dataProjection); - } else { - value = geometry; + /** + * @param {Node} node Node. + * @param {module:ol/geom/Polygon} geometry Polygon geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeSurfaceOrPolygon_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + if (node.nodeName !== 'PolygonPatch' && srsName) { + node.setAttribute('srsName', srsName); + } + if (node.nodeName === 'Polygon' || node.nodeName === 'PolygonPatch') { + const rings = geometry.getLinearRings(); + pushSerializeAndPop( + {node: node, hasZ: hasZ, srsName: srsName}, + this.RING_SERIALIZERS_, + this.RING_NODE_FACTORY_, + rings, objectStack, undefined, this); + } else if (node.nodeName === 'Surface') { + const patches = createElementNS(node.namespaceURI, 'patches'); + node.appendChild(patches); + this.writeSurfacePatches_( + patches, geometry, objectStack); } - } else { - value = transformWithOptions(/** @type {module:ol/geom/Geometry} */ (geometry), true, context); - } - pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ - (item), this.GEOMETRY_SERIALIZERS_, - this.GEOMETRY_NODE_FACTORY_, [value], - objectStack, undefined, this); -}; - - -/** - * @param {string} namespaceURI XML namespace. - * @returns {Node} coordinates node. - * @private - */ -GML2.prototype.createCoordinatesNode_ = function(namespaceURI) { - const coordinates = createElementNS(namespaceURI, 'coordinates'); - coordinates.setAttribute('decimal', '.'); - coordinates.setAttribute('cs', ','); - coordinates.setAttribute('ts', ' '); - - return coordinates; -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LineString|module:ol/geom/LinearRing} value Geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeCoordinates_ = function(node, value, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - // only 2d for simple features profile - const points = value.getCoordinates(); - const len = points.length; - const parts = new Array(len); - for (let i = 0; i < len; ++i) { - const point = points[i]; - parts[i] = this.getCoords_(point, srsName, hasZ); - } - writeStringTextNode(node, parts.join(' ')); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LineString} line LineString geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeCurveSegments_ = function(node, line, objectStack) { - const child = createElementNS(node.namespaceURI, 'LineStringSegment'); - node.appendChild(child); - this.writeCurveOrLineString_(child, line, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Polygon} geometry Polygon geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeSurfaceOrPolygon_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - if (node.nodeName !== 'PolygonPatch' && srsName) { - node.setAttribute('srsName', srsName); - } - if (node.nodeName === 'Polygon' || node.nodeName === 'PolygonPatch') { - const rings = geometry.getLinearRings(); - pushSerializeAndPop( - {node: node, hasZ: hasZ, srsName: srsName}, - this.RING_SERIALIZERS_, - this.RING_NODE_FACTORY_, - rings, objectStack, undefined, this); - } else if (node.nodeName === 'Surface') { - const patches = createElementNS(node.namespaceURI, 'patches'); - node.appendChild(patches); - this.writeSurfacePatches_( - patches, geometry, objectStack); - } -}; - - -/** - * @param {*} value Value. - * @param {Array.<*>} objectStack Object stack. - * @param {string=} opt_nodeName Node name. - * @return {Node} Node. - * @private - */ -GML2.prototype.RING_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) { - const context = objectStack[objectStack.length - 1]; - const parentNode = context.node; - const exteriorWritten = context['exteriorWritten']; - if (exteriorWritten === undefined) { - context['exteriorWritten'] = true; - } - return createElementNS(parentNode.namespaceURI, - exteriorWritten !== undefined ? 'innerBoundaryIs' : 'outerBoundaryIs'); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Polygon} polygon Polygon geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeSurfacePatches_ = function(node, polygon, objectStack) { - const child = createElementNS(node.namespaceURI, 'PolygonPatch'); - node.appendChild(child); - this.writeSurfaceOrPolygon_(child, polygon, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LinearRing} ring LinearRing geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeRing_ = function(node, ring, objectStack) { - const linearRing = createElementNS(node.namespaceURI, 'LinearRing'); - node.appendChild(linearRing); - this.writeLinearRing_(linearRing, ring, objectStack); -}; - - -/** - * @param {Array.} point Point geometry. - * @param {string=} opt_srsName Optional srsName - * @param {boolean=} opt_hasZ whether the geometry has a Z coordinate (is 3D) or not. - * @return {string} The coords string. - * @private - */ -GML2.prototype.getCoords_ = function(point, opt_srsName, opt_hasZ) { - let axisOrientation = 'enu'; - if (opt_srsName) { - axisOrientation = getProjection(opt_srsName).getAxisOrientation(); - } - let coords = ((axisOrientation.substr(0, 2) === 'en') ? - point[0] + ',' + point[1] : - point[1] + ',' + point[0]); - if (opt_hasZ) { - // For newly created points, Z can be undefined. - const z = point[2] || 0; - coords += ',' + z; } - return coords; -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Point} geometry Point geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writePoint_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - if (srsName) { - node.setAttribute('srsName', srsName); + /** + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node} Node. + * @private + */ + RING_NODE_FACTORY_(value, objectStack, opt_nodeName) { + const context = objectStack[objectStack.length - 1]; + const parentNode = context.node; + const exteriorWritten = context['exteriorWritten']; + if (exteriorWritten === undefined) { + context['exteriorWritten'] = true; + } + return createElementNS(parentNode.namespaceURI, + exteriorWritten !== undefined ? 'innerBoundaryIs' : 'outerBoundaryIs'); } - const coordinates = this.createCoordinatesNode_(node.namespaceURI); - node.appendChild(coordinates); - const point = geometry.getCoordinates(); - const coord = this.getCoords_(point, srsName, hasZ); - writeStringTextNode(coordinates, coord); -}; - -/** - * @param {Node} node Node. - * @param {module:ol/geom/MultiPoint} geometry MultiPoint geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeMultiPoint_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const points = geometry.getPoints(); - pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName}, - this.POINTMEMBER_SERIALIZERS_, - makeSimpleNodeFactory('pointMember'), points, - objectStack, undefined, this); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Point} point Point geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writePointMember_ = function(node, point, objectStack) { - const child = createElementNS(node.namespaceURI, 'Point'); - node.appendChild(child); - this.writePoint_(child, point, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LinearRing} geometry LinearRing geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeLinearRing_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const srsName = context['srsName']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const coordinates = this.createCoordinatesNode_(node.namespaceURI); - node.appendChild(coordinates); - this.writeCoordinates_(coordinates, geometry, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/MultiPolygon} geometry MultiPolygon geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeMultiSurfaceOrPolygon_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - const surface = context['surface']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const polygons = geometry.getPolygons(); - pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, surface: surface}, - this.SURFACEORPOLYGONMEMBER_SERIALIZERS_, - this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, polygons, - objectStack, undefined, this); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Polygon} polygon Polygon geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeSurfaceOrPolygonMember_ = function(node, polygon, objectStack) { - const child = this.GEOMETRY_NODE_FACTORY_( - polygon, objectStack); - if (child) { + /** + * @param {Node} node Node. + * @param {module:ol/geom/Polygon} polygon Polygon geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeSurfacePatches_(node, polygon, objectStack) { + const child = createElementNS(node.namespaceURI, 'PolygonPatch'); node.appendChild(child); this.writeSurfaceOrPolygon_(child, polygon, objectStack); } -}; - -/** - * @param {Node} node Node. - * @param {module:ol/extent~Extent} extent Extent. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML2.prototype.writeEnvelope = function(node, extent, objectStack) { - const context = objectStack[objectStack.length - 1]; - const srsName = context['srsName']; - if (srsName) { - node.setAttribute('srsName', srsName); + /** + * @param {Node} node Node. + * @param {module:ol/geom/LinearRing} ring LinearRing geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeRing_(node, ring, objectStack) { + const linearRing = createElementNS(node.namespaceURI, 'LinearRing'); + node.appendChild(linearRing); + this.writeLinearRing_(linearRing, ring, objectStack); } - const keys = ['lowerCorner', 'upperCorner']; - const values = [extent[0] + ' ' + extent[1], extent[2] + ' ' + extent[3]]; - pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ - ({node: node}), this.ENVELOPE_SERIALIZERS_, - OBJECT_PROPERTY_NODE_FACTORY, - values, - objectStack, keys, this); -}; + + /** + * @param {Array.} point Point geometry. + * @param {string=} opt_srsName Optional srsName + * @param {boolean=} opt_hasZ whether the geometry has a Z coordinate (is 3D) or not. + * @return {string} The coords string. + * @private + */ + getCoords_(point, opt_srsName, opt_hasZ) { + let axisOrientation = 'enu'; + if (opt_srsName) { + axisOrientation = getProjection(opt_srsName).getAxisOrientation(); + } + let coords = ((axisOrientation.substr(0, 2) === 'en') ? + point[0] + ',' + point[1] : + point[1] + ',' + point[0]); + if (opt_hasZ) { + // For newly created points, Z can be undefined. + const z = point[2] || 0; + coords += ',' + z; + } + + return coords; + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Point} geometry Point geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writePoint_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const coordinates = this.createCoordinatesNode_(node.namespaceURI); + node.appendChild(coordinates); + const point = geometry.getCoordinates(); + const coord = this.getCoords_(point, srsName, hasZ); + writeStringTextNode(coordinates, coord); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/MultiPoint} geometry MultiPoint geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeMultiPoint_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const points = geometry.getPoints(); + pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName}, + this.POINTMEMBER_SERIALIZERS_, + makeSimpleNodeFactory('pointMember'), points, + objectStack, undefined, this); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Point} point Point geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writePointMember_(node, point, objectStack) { + const child = createElementNS(node.namespaceURI, 'Point'); + node.appendChild(child); + this.writePoint_(child, point, objectStack); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LinearRing} geometry LinearRing geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeLinearRing_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const srsName = context['srsName']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const coordinates = this.createCoordinatesNode_(node.namespaceURI); + node.appendChild(coordinates); + this.writeCoordinates_(coordinates, geometry, objectStack); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/MultiPolygon} geometry MultiPolygon geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeMultiSurfaceOrPolygon_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + const surface = context['surface']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const polygons = geometry.getPolygons(); + pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, surface: surface}, + this.SURFACEORPOLYGONMEMBER_SERIALIZERS_, + this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, polygons, + objectStack, undefined, this); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Polygon} polygon Polygon geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeSurfaceOrPolygonMember_(node, polygon, objectStack) { + const child = this.GEOMETRY_NODE_FACTORY_( + polygon, objectStack); + if (child) { + node.appendChild(child); + this.writeSurfaceOrPolygon_(child, polygon, objectStack); + } + } + + /** + * @param {Node} node Node. + * @param {module:ol/extent~Extent} extent Extent. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeEnvelope(node, extent, objectStack) { + const context = objectStack[objectStack.length - 1]; + const srsName = context['srsName']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const keys = ['lowerCorner', 'upperCorner']; + const values = [extent[0] + ' ' + extent[1], extent[2] + ' ' + extent[3]]; + pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ + ({node: node}), this.ENVELOPE_SERIALIZERS_, + OBJECT_PROPERTY_NODE_FACTORY, + values, + objectStack, keys, this); + } + + /** + * @const + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node|undefined} Node. + * @private + */ + MULTIGEOMETRY_MEMBER_NODE_FACTORY_(value, objectStack, opt_nodeName) { + const parentNode = objectStack[objectStack.length - 1].node; + return createElementNS('http://www.opengis.net/gml', + MULTIGEOMETRY_TO_MEMBER_NODENAME[parentNode.nodeName]); + } +} + +inherits(GML2, GMLBase); /** @@ -597,21 +588,6 @@ const MULTIGEOMETRY_TO_MEMBER_NODENAME = { }; -/** - * @const - * @param {*} value Value. - * @param {Array.<*>} objectStack Object stack. - * @param {string=} opt_nodeName Node name. - * @return {Node|undefined} Node. - * @private - */ -GML2.prototype.MULTIGEOMETRY_MEMBER_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) { - const parentNode = objectStack[objectStack.length - 1].node; - return createElementNS('http://www.opengis.net/gml', - MULTIGEOMETRY_TO_MEMBER_NODENAME[parentNode.nodeName]); -}; - - /** * @const * @type {Object.>} diff --git a/src/ol/format/GML3.js b/src/ol/format/GML3.js index 78887c527e..34de9e9d86 100644 --- a/src/ol/format/GML3.js +++ b/src/ol/format/GML3.js @@ -42,348 +42,874 @@ const schemaLocation = GMLNS + * @extends {module:ol/format/GMLBase} * @api */ -const GML3 = function(opt_options) { - const options = /** @type {module:ol/format/GMLBase~Options} */ - (opt_options ? opt_options : {}); +class GML3 { + constructor(opt_options) { + const options = /** @type {module:ol/format/GMLBase~Options} */ + (opt_options ? opt_options : {}); - GMLBase.call(this, options); + GMLBase.call(this, options); + + /** + * @private + * @type {boolean} + */ + this.surface_ = options.surface !== undefined ? options.surface : false; + + /** + * @private + * @type {boolean} + */ + this.curve_ = options.curve !== undefined ? options.curve : false; + + /** + * @private + * @type {boolean} + */ + this.multiCurve_ = options.multiCurve !== undefined ? + options.multiCurve : true; + + /** + * @private + * @type {boolean} + */ + this.multiSurface_ = options.multiSurface !== undefined ? + options.multiSurface : true; + + /** + * @inheritDoc + */ + this.schemaLocation = options.schemaLocation ? + options.schemaLocation : schemaLocation; + + /** + * @private + * @type {boolean} + */ + this.hasZ = options.hasZ !== undefined ? + options.hasZ : false; + + } /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. * @private - * @type {boolean} + * @return {module:ol/geom/MultiLineString|undefined} MultiLineString. */ - this.surface_ = options.surface !== undefined ? options.surface : false; + readMultiCurve_(node, objectStack) { + /** @type {Array.} */ + const lineStrings = pushParseAndPop([], + this.MULTICURVE_PARSERS_, node, objectStack, this); + if (lineStrings) { + const multiLineString = new MultiLineString(lineStrings); + return multiLineString; + } else { + return undefined; + } + } /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. * @private - * @type {boolean} + * @return {module:ol/geom/MultiPolygon|undefined} MultiPolygon. */ - this.curve_ = options.curve !== undefined ? options.curve : false; + readMultiSurface_(node, objectStack) { + /** @type {Array.} */ + const polygons = pushParseAndPop([], + this.MULTISURFACE_PARSERS_, node, objectStack, this); + if (polygons) { + return new MultiPolygon(polygons); + } + } /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. * @private - * @type {boolean} */ - this.multiCurve_ = options.multiCurve !== undefined ? - options.multiCurve : true; + curveMemberParser_(node, objectStack) { + parseNode(this.CURVEMEMBER_PARSERS_, node, objectStack, this); + } /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. * @private - * @type {boolean} */ - this.multiSurface_ = options.multiSurface !== undefined ? - options.multiSurface : true; - - /** - * @inheritDoc - */ - this.schemaLocation = options.schemaLocation ? - options.schemaLocation : schemaLocation; + surfaceMemberParser_(node, objectStack) { + parseNode(this.SURFACEMEMBER_PARSERS_, + node, objectStack, this); + } /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. * @private - * @type {boolean} + * @return {Array.<(Array.)>|undefined} flat coordinates. */ - this.hasZ = options.hasZ !== undefined ? - options.hasZ : false; + readPatch_(node, objectStack) { + return pushParseAndPop([null], + this.PATCHES_PARSERS_, node, objectStack, this); + } -}; + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.|undefined} flat coordinates. + */ + readSegment_(node, objectStack) { + return pushParseAndPop([null], + this.SEGMENTS_PARSERS_, node, objectStack, this); + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.<(Array.)>|undefined} flat coordinates. + */ + readPolygonPatch_(node, objectStack) { + return pushParseAndPop([null], + this.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack, this); + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.|undefined} flat coordinates. + */ + readLineStringSegment_(node, objectStack) { + return pushParseAndPop([null], + this.GEOMETRY_FLAT_COORDINATES_PARSERS_, + node, objectStack, this); + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + interiorParser_(node, objectStack) { + /** @type {Array.|undefined} */ + const flatLinearRing = pushParseAndPop(undefined, + this.RING_PARSERS, node, objectStack, this); + if (flatLinearRing) { + const flatLinearRings = /** @type {Array.>} */ + (objectStack[objectStack.length - 1]); + flatLinearRings.push(flatLinearRing); + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + exteriorParser_(node, objectStack) { + /** @type {Array.|undefined} */ + const flatLinearRing = pushParseAndPop(undefined, + this.RING_PARSERS, node, objectStack, this); + if (flatLinearRing) { + const flatLinearRings = /** @type {Array.>} */ + (objectStack[objectStack.length - 1]); + flatLinearRings[0] = flatLinearRing; + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {module:ol/geom/Polygon|undefined} Polygon. + */ + readSurface_(node, objectStack) { + /** @type {Array.>} */ + const flatLinearRings = pushParseAndPop([null], + this.SURFACE_PARSERS_, node, objectStack, this); + if (flatLinearRings && flatLinearRings[0]) { + const flatCoordinates = flatLinearRings[0]; + const ends = [flatCoordinates.length]; + let i, ii; + for (i = 1, ii = flatLinearRings.length; i < ii; ++i) { + extend(flatCoordinates, flatLinearRings[i]); + ends.push(flatCoordinates.length); + } + return new Polygon(flatCoordinates, GeometryLayout.XYZ, ends); + } else { + return undefined; + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {module:ol/geom/LineString|undefined} LineString. + */ + readCurve_(node, objectStack) { + /** @type {Array.} */ + const flatCoordinates = pushParseAndPop([null], + this.CURVE_PARSERS_, node, objectStack, this); + if (flatCoordinates) { + const lineString = new LineString(flatCoordinates, GeometryLayout.XYZ); + return lineString; + } else { + return undefined; + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {module:ol/extent~Extent|undefined} Envelope. + */ + readEnvelope_(node, objectStack) { + /** @type {Array.} */ + const flatCoordinates = pushParseAndPop([null], + this.ENVELOPE_PARSERS_, node, objectStack, this); + return createOrUpdate(flatCoordinates[1][0], + flatCoordinates[1][1], flatCoordinates[2][0], + flatCoordinates[2][1]); + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.|undefined} Flat coordinates. + */ + readFlatPos_(node, objectStack) { + let s = getAllTextContent(node, false); + const re = /^\s*([+\-]?\d*\.?\d+(?:[eE][+\-]?\d+)?)\s*/; + /** @type {Array.} */ + const flatCoordinates = []; + let m; + while ((m = re.exec(s))) { + flatCoordinates.push(parseFloat(m[1])); + s = s.substr(m[0].length); + } + if (s !== '') { + return undefined; + } + const context = objectStack[0]; + const containerSrs = context['srsName']; + let axisOrientation = 'enu'; + if (containerSrs) { + const proj = getProjection(containerSrs); + axisOrientation = proj.getAxisOrientation(); + } + if (axisOrientation === 'neu') { + let i, ii; + for (i = 0, ii = flatCoordinates.length; i < ii; i += 3) { + const y = flatCoordinates[i]; + const x = flatCoordinates[i + 1]; + flatCoordinates[i] = x; + flatCoordinates[i + 1] = y; + } + } + const len = flatCoordinates.length; + if (len == 2) { + flatCoordinates.push(0); + } + if (len === 0) { + return undefined; + } + return flatCoordinates; + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.|undefined} Flat coordinates. + */ + readFlatPosList_(node, objectStack) { + const s = getAllTextContent(node, false).replace(/^\s*|\s*$/g, ''); + const context = objectStack[0]; + const containerSrs = context['srsName']; + const contextDimension = context['srsDimension']; + let axisOrientation = 'enu'; + if (containerSrs) { + const proj = getProjection(containerSrs); + axisOrientation = proj.getAxisOrientation(); + } + const coords = s.split(/\s+/); + // The "dimension" attribute is from the GML 3.0.1 spec. + let dim = 2; + if (node.getAttribute('srsDimension')) { + dim = readNonNegativeIntegerString( + node.getAttribute('srsDimension')); + } else if (node.getAttribute('dimension')) { + dim = readNonNegativeIntegerString( + node.getAttribute('dimension')); + } else if (node.parentNode.getAttribute('srsDimension')) { + dim = readNonNegativeIntegerString( + node.parentNode.getAttribute('srsDimension')); + } else if (contextDimension) { + dim = readNonNegativeIntegerString(contextDimension); + } + let x, y, z; + const flatCoordinates = []; + for (let i = 0, ii = coords.length; i < ii; i += dim) { + x = parseFloat(coords[i]); + y = parseFloat(coords[i + 1]); + z = (dim === 3) ? parseFloat(coords[i + 2]) : 0; + if (axisOrientation.substr(0, 2) === 'en') { + flatCoordinates.push(x, y, z); + } else { + flatCoordinates.push(y, x, z); + } + } + return flatCoordinates; + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Point} value Point geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writePos_(node, value, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsDimension = hasZ ? 3 : 2; + node.setAttribute('srsDimension', srsDimension); + const srsName = context['srsName']; + let axisOrientation = 'enu'; + if (srsName) { + axisOrientation = getProjection(srsName).getAxisOrientation(); + } + const point = value.getCoordinates(); + let coords; + // only 2d for simple features profile + if (axisOrientation.substr(0, 2) === 'en') { + coords = (point[0] + ' ' + point[1]); + } else { + coords = (point[1] + ' ' + point[0]); + } + if (hasZ) { + // For newly created points, Z can be undefined. + const z = point[2] || 0; + coords += ' ' + z; + } + writeStringTextNode(node, coords); + } + + /** + * @param {Array.} point Point geometry. + * @param {string=} opt_srsName Optional srsName + * @param {boolean=} opt_hasZ whether the geometry has a Z coordinate (is 3D) or not. + * @return {string} The coords string. + * @private + */ + getCoords_(point, opt_srsName, opt_hasZ) { + let axisOrientation = 'enu'; + if (opt_srsName) { + axisOrientation = getProjection(opt_srsName).getAxisOrientation(); + } + let coords = ((axisOrientation.substr(0, 2) === 'en') ? + point[0] + ' ' + point[1] : + point[1] + ' ' + point[0]); + if (opt_hasZ) { + // For newly created points, Z can be undefined. + const z = point[2] || 0; + coords += ' ' + z; + } + + return coords; + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LineString|module:ol/geom/LinearRing} value Geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writePosList_(node, value, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsDimension = hasZ ? 3 : 2; + node.setAttribute('srsDimension', srsDimension); + const srsName = context['srsName']; + // only 2d for simple features profile + const points = value.getCoordinates(); + const len = points.length; + const parts = new Array(len); + let point; + for (let i = 0; i < len; ++i) { + point = points[i]; + parts[i] = this.getCoords_(point, srsName, hasZ); + } + writeStringTextNode(node, parts.join(' ')); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Point} geometry Point geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writePoint_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const srsName = context['srsName']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const pos = createElementNS(node.namespaceURI, 'pos'); + node.appendChild(pos); + this.writePos_(pos, geometry, objectStack); + } + + /** + * @param {Node} node Node. + * @param {module:ol/extent~Extent} extent Extent. + * @param {Array.<*>} objectStack Node stack. + */ + writeEnvelope(node, extent, objectStack) { + const context = objectStack[objectStack.length - 1]; + const srsName = context['srsName']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const keys = ['lowerCorner', 'upperCorner']; + const values = [extent[0] + ' ' + extent[1], extent[2] + ' ' + extent[3]]; + pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ + ({node: node}), this.ENVELOPE_SERIALIZERS_, + OBJECT_PROPERTY_NODE_FACTORY, + values, + objectStack, keys, this); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LinearRing} geometry LinearRing geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeLinearRing_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const srsName = context['srsName']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const posList = createElementNS(node.namespaceURI, 'posList'); + node.appendChild(posList); + this.writePosList_(posList, geometry, objectStack); + } + + /** + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node} Node. + * @private + */ + RING_NODE_FACTORY_(value, objectStack, opt_nodeName) { + const context = objectStack[objectStack.length - 1]; + const parentNode = context.node; + const exteriorWritten = context['exteriorWritten']; + if (exteriorWritten === undefined) { + context['exteriorWritten'] = true; + } + return createElementNS(parentNode.namespaceURI, + exteriorWritten !== undefined ? 'interior' : 'exterior'); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Polygon} geometry Polygon geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeSurfaceOrPolygon_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + if (node.nodeName !== 'PolygonPatch' && srsName) { + node.setAttribute('srsName', srsName); + } + if (node.nodeName === 'Polygon' || node.nodeName === 'PolygonPatch') { + const rings = geometry.getLinearRings(); + pushSerializeAndPop( + {node: node, hasZ: hasZ, srsName: srsName}, + this.RING_SERIALIZERS_, + this.RING_NODE_FACTORY_, + rings, objectStack, undefined, this); + } else if (node.nodeName === 'Surface') { + const patches = createElementNS(node.namespaceURI, 'patches'); + node.appendChild(patches); + this.writeSurfacePatches_( + patches, geometry, objectStack); + } + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LineString} geometry LineString geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeCurveOrLineString_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const srsName = context['srsName']; + if (node.nodeName !== 'LineStringSegment' && srsName) { + node.setAttribute('srsName', srsName); + } + if (node.nodeName === 'LineString' || + node.nodeName === 'LineStringSegment') { + const posList = createElementNS(node.namespaceURI, 'posList'); + node.appendChild(posList); + this.writePosList_(posList, geometry, objectStack); + } else if (node.nodeName === 'Curve') { + const segments = createElementNS(node.namespaceURI, 'segments'); + node.appendChild(segments); + this.writeCurveSegments_(segments, + geometry, objectStack); + } + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/MultiPolygon} geometry MultiPolygon geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeMultiSurfaceOrPolygon_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + const surface = context['surface']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const polygons = geometry.getPolygons(); + pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, surface: surface}, + this.SURFACEORPOLYGONMEMBER_SERIALIZERS_, + this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, polygons, + objectStack, undefined, this); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/MultiPoint} geometry MultiPoint geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeMultiPoint_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const srsName = context['srsName']; + const hasZ = context['hasZ']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const points = geometry.getPoints(); + pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName}, + this.POINTMEMBER_SERIALIZERS_, + makeSimpleNodeFactory('pointMember'), points, + objectStack, undefined, this); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/MultiLineString} geometry MultiLineString geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeMultiCurveOrLineString_(node, geometry, objectStack) { + const context = objectStack[objectStack.length - 1]; + const hasZ = context['hasZ']; + const srsName = context['srsName']; + const curve = context['curve']; + if (srsName) { + node.setAttribute('srsName', srsName); + } + const lines = geometry.getLineStrings(); + pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, curve: curve}, + this.LINESTRINGORCURVEMEMBER_SERIALIZERS_, + this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, lines, + objectStack, undefined, this); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LinearRing} ring LinearRing geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeRing_(node, ring, objectStack) { + const linearRing = createElementNS(node.namespaceURI, 'LinearRing'); + node.appendChild(linearRing); + this.writeLinearRing_(linearRing, ring, objectStack); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Polygon} polygon Polygon geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeSurfaceOrPolygonMember_(node, polygon, objectStack) { + const child = this.GEOMETRY_NODE_FACTORY_( + polygon, objectStack); + if (child) { + node.appendChild(child); + this.writeSurfaceOrPolygon_(child, polygon, objectStack); + } + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Point} point Point geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writePointMember_(node, point, objectStack) { + const child = createElementNS(node.namespaceURI, 'Point'); + node.appendChild(child); + this.writePoint_(child, point, objectStack); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LineString} line LineString geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeLineStringOrCurveMember_(node, line, objectStack) { + const child = this.GEOMETRY_NODE_FACTORY_(line, objectStack); + if (child) { + node.appendChild(child); + this.writeCurveOrLineString_(child, line, objectStack); + } + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Polygon} polygon Polygon geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeSurfacePatches_(node, polygon, objectStack) { + const child = createElementNS(node.namespaceURI, 'PolygonPatch'); + node.appendChild(child); + this.writeSurfaceOrPolygon_(child, polygon, objectStack); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/LineString} line LineString geometry. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeCurveSegments_(node, line, objectStack) { + const child = createElementNS(node.namespaceURI, + 'LineStringSegment'); + node.appendChild(child); + this.writeCurveOrLineString_(child, line, objectStack); + } + + /** + * @param {Node} node Node. + * @param {module:ol/geom/Geometry|module:ol/extent~Extent} geometry Geometry. + * @param {Array.<*>} objectStack Node stack. + */ + writeGeometryElement(node, geometry, objectStack) { + const context = /** @type {module:ol/format/Feature~WriteOptions} */ (objectStack[objectStack.length - 1]); + const item = assign({}, context); + item.node = node; + let value; + if (Array.isArray(geometry)) { + if (context.dataProjection) { + value = transformExtent( + geometry, context.featureProjection, context.dataProjection); + } else { + value = geometry; + } + } else { + value = transformWithOptions(/** @type {module:ol/geom/Geometry} */ (geometry), true, context); + } + pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ + (item), this.GEOMETRY_SERIALIZERS_, + this.GEOMETRY_NODE_FACTORY_, [value], + objectStack, undefined, this); + } + + /** + * @param {Node} node Node. + * @param {module:ol/Feature} feature Feature. + * @param {Array.<*>} objectStack Node stack. + */ + writeFeatureElement(node, feature, objectStack) { + const fid = feature.getId(); + if (fid) { + node.setAttribute('fid', fid); + } + const context = /** @type {Object} */ (objectStack[objectStack.length - 1]); + const featureNS = context['featureNS']; + const geometryName = feature.getGeometryName(); + if (!context.serializers) { + 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 || value instanceof Geometry) { + 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); + } + } + } + } + const item = assign({}, context); + item.node = node; + pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ + (item), context.serializers, + makeSimpleNodeFactory(undefined, featureNS), + values, + objectStack, keys); + } + + /** + * @param {Node} node Node. + * @param {Array.} features Features. + * @param {Array.<*>} objectStack Node stack. + * @private + */ + writeFeatureMembers_(node, features, objectStack) { + const context = /** @type {Object} */ (objectStack[objectStack.length - 1]); + const featureType = context['featureType']; + const featureNS = context['featureNS']; + const serializers = {}; + serializers[featureNS] = {}; + serializers[featureNS][featureType] = makeChildAppender( + this.writeFeatureElement, this); + const item = assign({}, context); + item.node = node; + pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ + (item), + serializers, + makeSimpleNodeFactory(featureType, featureNS), features, + objectStack); + } + + /** + * @const + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node|undefined} Node. + * @private + */ + MULTIGEOMETRY_MEMBER_NODE_FACTORY_(value, objectStack, opt_nodeName) { + const parentNode = objectStack[objectStack.length - 1].node; + return createElementNS('http://www.opengis.net/gml', + MULTIGEOMETRY_TO_MEMBER_NODENAME[parentNode.nodeName]); + } + + /** + * @const + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node|undefined} Node. + * @private + */ + GEOMETRY_NODE_FACTORY_(value, objectStack, opt_nodeName) { + const context = objectStack[objectStack.length - 1]; + const multiSurface = context['multiSurface']; + const surface = context['surface']; + const curve = context['curve']; + const multiCurve = context['multiCurve']; + let nodeName; + if (!Array.isArray(value)) { + nodeName = /** @type {module:ol/geom/Geometry} */ (value).getType(); + if (nodeName === 'MultiPolygon' && multiSurface === true) { + nodeName = 'MultiSurface'; + } else if (nodeName === 'Polygon' && surface === true) { + nodeName = 'Surface'; + } else if (nodeName === 'LineString' && curve === true) { + nodeName = 'Curve'; + } else if (nodeName === 'MultiLineString' && multiCurve === true) { + nodeName = 'MultiCurve'; + } + } else { + nodeName = 'Envelope'; + } + return createElementNS('http://www.opengis.net/gml', + nodeName); + } + + /** + * Encode a geometry in GML 3.1.1 Simple Features. + * + * @param {module:ol/geom/Geometry} geometry Geometry. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. + * @return {Node} Node. + * @override + * @api + */ + writeGeometryNode(geometry, opt_options) { + opt_options = this.adaptOptions(opt_options); + const geom = createElementNS('http://www.opengis.net/gml', 'geom'); + const context = {node: geom, hasZ: this.hasZ, srsName: this.srsName, + curve: this.curve_, surface: this.surface_, + multiSurface: this.multiSurface_, multiCurve: this.multiCurve_}; + if (opt_options) { + assign(context, opt_options); + } + this.writeGeometryElement(geom, geometry, [context]); + return geom; + } + + /** + * Encode an array of features in the GML 3.1.1 format as an XML node. + * + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. + * @return {Node} Node. + * @override + * @api + */ + writeFeaturesNode(features, opt_options) { + opt_options = this.adaptOptions(opt_options); + const node = createElementNS('http://www.opengis.net/gml', 'featureMembers'); + node.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', this.schemaLocation); + const context = { + srsName: this.srsName, + hasZ: this.hasZ, + curve: this.curve_, + surface: this.surface_, + multiSurface: this.multiSurface_, + multiCurve: this.multiCurve_, + featureNS: this.featureNS, + featureType: this.featureType + }; + if (opt_options) { + assign(context, opt_options); + } + this.writeFeatureMembers_(node, features, [context]); + return node; + } +} inherits(GML3, GMLBase); -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {module:ol/geom/MultiLineString|undefined} MultiLineString. - */ -GML3.prototype.readMultiCurve_ = function(node, objectStack) { - /** @type {Array.} */ - const lineStrings = pushParseAndPop([], - this.MULTICURVE_PARSERS_, node, objectStack, this); - if (lineStrings) { - const multiLineString = new MultiLineString(lineStrings); - return multiLineString; - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {module:ol/geom/MultiPolygon|undefined} MultiPolygon. - */ -GML3.prototype.readMultiSurface_ = function(node, objectStack) { - /** @type {Array.} */ - const polygons = pushParseAndPop([], - this.MULTISURFACE_PARSERS_, node, objectStack, this); - if (polygons) { - return new MultiPolygon(polygons); - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GML3.prototype.curveMemberParser_ = function(node, objectStack) { - parseNode(this.CURVEMEMBER_PARSERS_, node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GML3.prototype.surfaceMemberParser_ = function(node, objectStack) { - parseNode(this.SURFACEMEMBER_PARSERS_, - node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.<(Array.)>|undefined} flat coordinates. - */ -GML3.prototype.readPatch_ = function(node, objectStack) { - return pushParseAndPop([null], - this.PATCHES_PARSERS_, node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.|undefined} flat coordinates. - */ -GML3.prototype.readSegment_ = function(node, objectStack) { - return pushParseAndPop([null], - this.SEGMENTS_PARSERS_, node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.<(Array.)>|undefined} flat coordinates. - */ -GML3.prototype.readPolygonPatch_ = function(node, objectStack) { - return pushParseAndPop([null], - this.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.|undefined} flat coordinates. - */ -GML3.prototype.readLineStringSegment_ = function(node, objectStack) { - return pushParseAndPop([null], - this.GEOMETRY_FLAT_COORDINATES_PARSERS_, - node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GML3.prototype.interiorParser_ = function(node, objectStack) { - /** @type {Array.|undefined} */ - const flatLinearRing = pushParseAndPop(undefined, - this.RING_PARSERS, node, objectStack, this); - if (flatLinearRing) { - const flatLinearRings = /** @type {Array.>} */ - (objectStack[objectStack.length - 1]); - flatLinearRings.push(flatLinearRing); - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GML3.prototype.exteriorParser_ = function(node, objectStack) { - /** @type {Array.|undefined} */ - const flatLinearRing = pushParseAndPop(undefined, - this.RING_PARSERS, node, objectStack, this); - if (flatLinearRing) { - const flatLinearRings = /** @type {Array.>} */ - (objectStack[objectStack.length - 1]); - flatLinearRings[0] = flatLinearRing; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {module:ol/geom/Polygon|undefined} Polygon. - */ -GML3.prototype.readSurface_ = function(node, objectStack) { - /** @type {Array.>} */ - const flatLinearRings = pushParseAndPop([null], - this.SURFACE_PARSERS_, node, objectStack, this); - if (flatLinearRings && flatLinearRings[0]) { - const flatCoordinates = flatLinearRings[0]; - const ends = [flatCoordinates.length]; - let i, ii; - for (i = 1, ii = flatLinearRings.length; i < ii; ++i) { - extend(flatCoordinates, flatLinearRings[i]); - ends.push(flatCoordinates.length); - } - return new Polygon(flatCoordinates, GeometryLayout.XYZ, ends); - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {module:ol/geom/LineString|undefined} LineString. - */ -GML3.prototype.readCurve_ = function(node, objectStack) { - /** @type {Array.} */ - const flatCoordinates = pushParseAndPop([null], - this.CURVE_PARSERS_, node, objectStack, this); - if (flatCoordinates) { - const lineString = new LineString(flatCoordinates, GeometryLayout.XYZ); - return lineString; - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {module:ol/extent~Extent|undefined} Envelope. - */ -GML3.prototype.readEnvelope_ = function(node, objectStack) { - /** @type {Array.} */ - const flatCoordinates = pushParseAndPop([null], - this.ENVELOPE_PARSERS_, node, objectStack, this); - return createOrUpdate(flatCoordinates[1][0], - flatCoordinates[1][1], flatCoordinates[2][0], - flatCoordinates[2][1]); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.|undefined} Flat coordinates. - */ -GML3.prototype.readFlatPos_ = function(node, objectStack) { - let s = getAllTextContent(node, false); - const re = /^\s*([+\-]?\d*\.?\d+(?:[eE][+\-]?\d+)?)\s*/; - /** @type {Array.} */ - const flatCoordinates = []; - let m; - while ((m = re.exec(s))) { - flatCoordinates.push(parseFloat(m[1])); - s = s.substr(m[0].length); - } - if (s !== '') { - return undefined; - } - const context = objectStack[0]; - const containerSrs = context['srsName']; - let axisOrientation = 'enu'; - if (containerSrs) { - const proj = getProjection(containerSrs); - axisOrientation = proj.getAxisOrientation(); - } - if (axisOrientation === 'neu') { - let i, ii; - for (i = 0, ii = flatCoordinates.length; i < ii; i += 3) { - const y = flatCoordinates[i]; - const x = flatCoordinates[i + 1]; - flatCoordinates[i] = x; - flatCoordinates[i + 1] = y; - } - } - const len = flatCoordinates.length; - if (len == 2) { - flatCoordinates.push(0); - } - if (len === 0) { - return undefined; - } - return flatCoordinates; -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.|undefined} Flat coordinates. - */ -GML3.prototype.readFlatPosList_ = function(node, objectStack) { - const s = getAllTextContent(node, false).replace(/^\s*|\s*$/g, ''); - const context = objectStack[0]; - const containerSrs = context['srsName']; - const contextDimension = context['srsDimension']; - let axisOrientation = 'enu'; - if (containerSrs) { - const proj = getProjection(containerSrs); - axisOrientation = proj.getAxisOrientation(); - } - const coords = s.split(/\s+/); - // The "dimension" attribute is from the GML 3.0.1 spec. - let dim = 2; - if (node.getAttribute('srsDimension')) { - dim = readNonNegativeIntegerString( - node.getAttribute('srsDimension')); - } else if (node.getAttribute('dimension')) { - dim = readNonNegativeIntegerString( - node.getAttribute('dimension')); - } else if (node.parentNode.getAttribute('srsDimension')) { - dim = readNonNegativeIntegerString( - node.parentNode.getAttribute('srsDimension')); - } else if (contextDimension) { - dim = readNonNegativeIntegerString(contextDimension); - } - let x, y, z; - const flatCoordinates = []; - for (let i = 0, ii = coords.length; i < ii; i += dim) { - x = parseFloat(coords[i]); - y = parseFloat(coords[i + 1]); - z = (dim === 3) ? parseFloat(coords[i + 2]) : 0; - if (axisOrientation.substr(0, 2) === 'en') { - flatCoordinates.push(x, y, z); - } else { - flatCoordinates.push(y, x, z); - } - } - return flatCoordinates; -}; - - /** * @const * @type {Object.>} @@ -562,467 +1088,6 @@ GML3.prototype.SEGMENTS_PARSERS_ = { }; -/** - * @param {Node} node Node. - * @param {module:ol/geom/Point} value Point geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writePos_ = function(node, value, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsDimension = hasZ ? 3 : 2; - node.setAttribute('srsDimension', srsDimension); - const srsName = context['srsName']; - let axisOrientation = 'enu'; - if (srsName) { - axisOrientation = getProjection(srsName).getAxisOrientation(); - } - const point = value.getCoordinates(); - let coords; - // only 2d for simple features profile - if (axisOrientation.substr(0, 2) === 'en') { - coords = (point[0] + ' ' + point[1]); - } else { - coords = (point[1] + ' ' + point[0]); - } - if (hasZ) { - // For newly created points, Z can be undefined. - const z = point[2] || 0; - coords += ' ' + z; - } - writeStringTextNode(node, coords); -}; - - -/** - * @param {Array.} point Point geometry. - * @param {string=} opt_srsName Optional srsName - * @param {boolean=} opt_hasZ whether the geometry has a Z coordinate (is 3D) or not. - * @return {string} The coords string. - * @private - */ -GML3.prototype.getCoords_ = function(point, opt_srsName, opt_hasZ) { - let axisOrientation = 'enu'; - if (opt_srsName) { - axisOrientation = getProjection(opt_srsName).getAxisOrientation(); - } - let coords = ((axisOrientation.substr(0, 2) === 'en') ? - point[0] + ' ' + point[1] : - point[1] + ' ' + point[0]); - if (opt_hasZ) { - // For newly created points, Z can be undefined. - const z = point[2] || 0; - coords += ' ' + z; - } - - return coords; -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LineString|module:ol/geom/LinearRing} value Geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writePosList_ = function(node, value, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsDimension = hasZ ? 3 : 2; - node.setAttribute('srsDimension', srsDimension); - const srsName = context['srsName']; - // only 2d for simple features profile - const points = value.getCoordinates(); - const len = points.length; - const parts = new Array(len); - let point; - for (let i = 0; i < len; ++i) { - point = points[i]; - parts[i] = this.getCoords_(point, srsName, hasZ); - } - writeStringTextNode(node, parts.join(' ')); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Point} geometry Point geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writePoint_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const srsName = context['srsName']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const pos = createElementNS(node.namespaceURI, 'pos'); - node.appendChild(pos); - this.writePos_(pos, geometry, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/extent~Extent} extent Extent. - * @param {Array.<*>} objectStack Node stack. - */ -GML3.prototype.writeEnvelope = function(node, extent, objectStack) { - const context = objectStack[objectStack.length - 1]; - const srsName = context['srsName']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const keys = ['lowerCorner', 'upperCorner']; - const values = [extent[0] + ' ' + extent[1], extent[2] + ' ' + extent[3]]; - pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ - ({node: node}), this.ENVELOPE_SERIALIZERS_, - OBJECT_PROPERTY_NODE_FACTORY, - values, - objectStack, keys, this); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LinearRing} geometry LinearRing geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeLinearRing_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const srsName = context['srsName']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const posList = createElementNS(node.namespaceURI, 'posList'); - node.appendChild(posList); - this.writePosList_(posList, geometry, objectStack); -}; - - -/** - * @param {*} value Value. - * @param {Array.<*>} objectStack Object stack. - * @param {string=} opt_nodeName Node name. - * @return {Node} Node. - * @private - */ -GML3.prototype.RING_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) { - const context = objectStack[objectStack.length - 1]; - const parentNode = context.node; - const exteriorWritten = context['exteriorWritten']; - if (exteriorWritten === undefined) { - context['exteriorWritten'] = true; - } - return createElementNS(parentNode.namespaceURI, - exteriorWritten !== undefined ? 'interior' : 'exterior'); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Polygon} geometry Polygon geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeSurfaceOrPolygon_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - if (node.nodeName !== 'PolygonPatch' && srsName) { - node.setAttribute('srsName', srsName); - } - if (node.nodeName === 'Polygon' || node.nodeName === 'PolygonPatch') { - const rings = geometry.getLinearRings(); - pushSerializeAndPop( - {node: node, hasZ: hasZ, srsName: srsName}, - this.RING_SERIALIZERS_, - this.RING_NODE_FACTORY_, - rings, objectStack, undefined, this); - } else if (node.nodeName === 'Surface') { - const patches = createElementNS(node.namespaceURI, 'patches'); - node.appendChild(patches); - this.writeSurfacePatches_( - patches, geometry, objectStack); - } -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LineString} geometry LineString geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeCurveOrLineString_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const srsName = context['srsName']; - if (node.nodeName !== 'LineStringSegment' && srsName) { - node.setAttribute('srsName', srsName); - } - if (node.nodeName === 'LineString' || - node.nodeName === 'LineStringSegment') { - const posList = createElementNS(node.namespaceURI, 'posList'); - node.appendChild(posList); - this.writePosList_(posList, geometry, objectStack); - } else if (node.nodeName === 'Curve') { - const segments = createElementNS(node.namespaceURI, 'segments'); - node.appendChild(segments); - this.writeCurveSegments_(segments, - geometry, objectStack); - } -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/MultiPolygon} geometry MultiPolygon geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeMultiSurfaceOrPolygon_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - const surface = context['surface']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const polygons = geometry.getPolygons(); - pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, surface: surface}, - this.SURFACEORPOLYGONMEMBER_SERIALIZERS_, - this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, polygons, - objectStack, undefined, this); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/MultiPoint} geometry MultiPoint geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeMultiPoint_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const srsName = context['srsName']; - const hasZ = context['hasZ']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const points = geometry.getPoints(); - pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName}, - this.POINTMEMBER_SERIALIZERS_, - makeSimpleNodeFactory('pointMember'), points, - objectStack, undefined, this); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/MultiLineString} geometry MultiLineString geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeMultiCurveOrLineString_ = function(node, geometry, objectStack) { - const context = objectStack[objectStack.length - 1]; - const hasZ = context['hasZ']; - const srsName = context['srsName']; - const curve = context['curve']; - if (srsName) { - node.setAttribute('srsName', srsName); - } - const lines = geometry.getLineStrings(); - pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, curve: curve}, - this.LINESTRINGORCURVEMEMBER_SERIALIZERS_, - this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, lines, - objectStack, undefined, this); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LinearRing} ring LinearRing geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeRing_ = function(node, ring, objectStack) { - const linearRing = createElementNS(node.namespaceURI, 'LinearRing'); - node.appendChild(linearRing); - this.writeLinearRing_(linearRing, ring, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Polygon} polygon Polygon geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeSurfaceOrPolygonMember_ = function(node, polygon, objectStack) { - const child = this.GEOMETRY_NODE_FACTORY_( - polygon, objectStack); - if (child) { - node.appendChild(child); - this.writeSurfaceOrPolygon_(child, polygon, objectStack); - } -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Point} point Point geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writePointMember_ = function(node, point, objectStack) { - const child = createElementNS(node.namespaceURI, 'Point'); - node.appendChild(child); - this.writePoint_(child, point, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LineString} line LineString geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeLineStringOrCurveMember_ = function(node, line, objectStack) { - const child = this.GEOMETRY_NODE_FACTORY_(line, objectStack); - if (child) { - node.appendChild(child); - this.writeCurveOrLineString_(child, line, objectStack); - } -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Polygon} polygon Polygon geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeSurfacePatches_ = function(node, polygon, objectStack) { - const child = createElementNS(node.namespaceURI, 'PolygonPatch'); - node.appendChild(child); - this.writeSurfaceOrPolygon_(child, polygon, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/LineString} line LineString geometry. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeCurveSegments_ = function(node, line, objectStack) { - const child = createElementNS(node.namespaceURI, - 'LineStringSegment'); - node.appendChild(child); - this.writeCurveOrLineString_(child, line, objectStack); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/geom/Geometry|module:ol/extent~Extent} geometry Geometry. - * @param {Array.<*>} objectStack Node stack. - */ -GML3.prototype.writeGeometryElement = function(node, geometry, objectStack) { - const context = /** @type {module:ol/format/Feature~WriteOptions} */ (objectStack[objectStack.length - 1]); - const item = assign({}, context); - item.node = node; - let value; - if (Array.isArray(geometry)) { - if (context.dataProjection) { - value = transformExtent( - geometry, context.featureProjection, context.dataProjection); - } else { - value = geometry; - } - } else { - value = transformWithOptions(/** @type {module:ol/geom/Geometry} */ (geometry), true, context); - } - pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ - (item), this.GEOMETRY_SERIALIZERS_, - this.GEOMETRY_NODE_FACTORY_, [value], - objectStack, undefined, this); -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/Feature} feature Feature. - * @param {Array.<*>} objectStack Node stack. - */ -GML3.prototype.writeFeatureElement = function(node, feature, objectStack) { - const fid = feature.getId(); - if (fid) { - node.setAttribute('fid', fid); - } - const context = /** @type {Object} */ (objectStack[objectStack.length - 1]); - const featureNS = context['featureNS']; - const geometryName = feature.getGeometryName(); - if (!context.serializers) { - 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 || value instanceof Geometry) { - 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); - } - } - } - } - const item = assign({}, context); - item.node = node; - pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ - (item), context.serializers, - makeSimpleNodeFactory(undefined, featureNS), - values, - objectStack, keys); -}; - - -/** - * @param {Node} node Node. - * @param {Array.} features Features. - * @param {Array.<*>} objectStack Node stack. - * @private - */ -GML3.prototype.writeFeatureMembers_ = function(node, features, objectStack) { - const context = /** @type {Object} */ (objectStack[objectStack.length - 1]); - const featureType = context['featureType']; - const featureNS = context['featureNS']; - const serializers = {}; - serializers[featureNS] = {}; - serializers[featureNS][featureType] = makeChildAppender( - this.writeFeatureElement, this); - const item = assign({}, context); - item.node = node; - pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ - (item), - serializers, - makeSimpleNodeFactory(featureType, featureNS), features, - objectStack); -}; - - /** * @const * @type {Object.} @@ -1035,78 +1100,6 @@ const MULTIGEOMETRY_TO_MEMBER_NODENAME = { }; -/** - * @const - * @param {*} value Value. - * @param {Array.<*>} objectStack Object stack. - * @param {string=} opt_nodeName Node name. - * @return {Node|undefined} Node. - * @private - */ -GML3.prototype.MULTIGEOMETRY_MEMBER_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) { - const parentNode = objectStack[objectStack.length - 1].node; - return createElementNS('http://www.opengis.net/gml', - MULTIGEOMETRY_TO_MEMBER_NODENAME[parentNode.nodeName]); -}; - - -/** - * @const - * @param {*} value Value. - * @param {Array.<*>} objectStack Object stack. - * @param {string=} opt_nodeName Node name. - * @return {Node|undefined} Node. - * @private - */ -GML3.prototype.GEOMETRY_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) { - const context = objectStack[objectStack.length - 1]; - const multiSurface = context['multiSurface']; - const surface = context['surface']; - const curve = context['curve']; - const multiCurve = context['multiCurve']; - let nodeName; - if (!Array.isArray(value)) { - nodeName = /** @type {module:ol/geom/Geometry} */ (value).getType(); - if (nodeName === 'MultiPolygon' && multiSurface === true) { - nodeName = 'MultiSurface'; - } else if (nodeName === 'Polygon' && surface === true) { - nodeName = 'Surface'; - } else if (nodeName === 'LineString' && curve === true) { - nodeName = 'Curve'; - } else if (nodeName === 'MultiLineString' && multiCurve === true) { - nodeName = 'MultiCurve'; - } - } else { - nodeName = 'Envelope'; - } - return createElementNS('http://www.opengis.net/gml', - nodeName); -}; - - -/** - * Encode a geometry in GML 3.1.1 Simple Features. - * - * @param {module:ol/geom/Geometry} geometry Geometry. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. - * @return {Node} Node. - * @override - * @api - */ -GML3.prototype.writeGeometryNode = function(geometry, opt_options) { - opt_options = this.adaptOptions(opt_options); - const geom = createElementNS('http://www.opengis.net/gml', 'geom'); - const context = {node: geom, hasZ: this.hasZ, srsName: this.srsName, - curve: this.curve_, surface: this.surface_, - multiSurface: this.multiSurface_, multiCurve: this.multiCurve_}; - if (opt_options) { - assign(context, opt_options); - } - this.writeGeometryElement(geom, geometry, [context]); - return geom; -}; - - /** * Encode an array of features in GML 3.1.1 Simple Features. * @@ -1119,37 +1112,6 @@ GML3.prototype.writeGeometryNode = function(geometry, opt_options) { GML3.prototype.writeFeatures; -/** - * Encode an array of features in the GML 3.1.1 format as an XML node. - * - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. - * @return {Node} Node. - * @override - * @api - */ -GML3.prototype.writeFeaturesNode = function(features, opt_options) { - opt_options = this.adaptOptions(opt_options); - const node = createElementNS('http://www.opengis.net/gml', 'featureMembers'); - node.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', this.schemaLocation); - const context = { - srsName: this.srsName, - hasZ: this.hasZ, - curve: this.curve_, - surface: this.surface_, - multiSurface: this.multiSurface_, - multiCurve: this.multiCurve_, - featureNS: this.featureNS, - featureType: this.featureType - }; - if (opt_options) { - assign(context, opt_options); - } - this.writeFeatureMembers_(node, features, [context]); - return node; -}; - - /** * @type {Object.>} * @private diff --git a/src/ol/format/GMLBase.js b/src/ol/format/GMLBase.js index 4965f4782c..6761aac1a6 100644 --- a/src/ol/format/GMLBase.js +++ b/src/ol/format/GMLBase.js @@ -75,44 +75,385 @@ export const GMLNS = 'http://www.opengis.net/gml'; * Optional configuration object. * @extends {module:ol/format/XMLFeature} */ -const GMLBase = function(opt_options) { - const options = /** @type {module:ol/format/GMLBase~Options} */ (opt_options ? opt_options : {}); +class GMLBase { + constructor(opt_options) { + const options = /** @type {module:ol/format/GMLBase~Options} */ (opt_options ? opt_options : {}); + + /** + * @protected + * @type {Array.|string|undefined} + */ + this.featureType = options.featureType; + + /** + * @protected + * @type {Object.|string|undefined} + */ + this.featureNS = options.featureNS; + + /** + * @protected + * @type {string} + */ + this.srsName = options.srsName; + + /** + * @protected + * @type {string} + */ + this.schemaLocation = ''; + + /** + * @type {Object.>} + */ + this.FEATURE_COLLECTION_PARSERS = {}; + this.FEATURE_COLLECTION_PARSERS[GMLNS] = { + 'featureMember': makeReplacer(GMLBase.prototype.readFeaturesInternal), + 'featureMembers': makeReplacer(GMLBase.prototype.readFeaturesInternal) + }; + + XMLFeature.call(this); + } /** - * @protected - * @type {Array.|string|undefined} + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {Array. | undefined} Features. */ - this.featureType = options.featureType; + readFeaturesInternal(node, objectStack) { + const localName = node.localName; + let features = null; + if (localName == 'FeatureCollection') { + if (node.namespaceURI === 'http://www.opengis.net/wfs') { + features = pushParseAndPop([], + this.FEATURE_COLLECTION_PARSERS, node, + objectStack, this); + } else { + features = pushParseAndPop(null, + this.FEATURE_COLLECTION_PARSERS, node, + objectStack, this); + } + } else if (localName == 'featureMembers' || localName == 'featureMember') { + const context = objectStack[0]; + let featureType = context['featureType']; + let featureNS = context['featureNS']; + const prefix = 'p'; + const defaultPrefix = 'p0'; + if (!featureType && node.childNodes) { + featureType = [], featureNS = {}; + for (let i = 0, ii = node.childNodes.length; i < ii; ++i) { + const child = node.childNodes[i]; + if (child.nodeType === 1) { + const ft = child.nodeName.split(':').pop(); + if (featureType.indexOf(ft) === -1) { + let key = ''; + let count = 0; + const uri = child.namespaceURI; + for (const candidate in featureNS) { + if (featureNS[candidate] === uri) { + key = candidate; + break; + } + ++count; + } + if (!key) { + key = prefix + count; + featureNS[key] = uri; + } + featureType.push(key + ':' + ft); + } + } + } + if (localName != 'featureMember') { + // recheck featureType for each featureMember + context['featureType'] = featureType; + context['featureNS'] = featureNS; + } + } + if (typeof featureNS === 'string') { + const ns = featureNS; + featureNS = {}; + featureNS[defaultPrefix] = ns; + } + const parsersNS = {}; + const featureTypes = Array.isArray(featureType) ? featureType : [featureType]; + for (const p in featureNS) { + const parsers = {}; + for (let i = 0, ii = featureTypes.length; i < ii; ++i) { + const featurePrefix = featureTypes[i].indexOf(':') === -1 ? + defaultPrefix : featureTypes[i].split(':')[0]; + if (featurePrefix === p) { + parsers[featureTypes[i].split(':').pop()] = + (localName == 'featureMembers') ? + makeArrayPusher(this.readFeatureElement, this) : + makeReplacer(this.readFeatureElement, this); + } + } + parsersNS[featureNS[p]] = parsers; + } + if (localName == 'featureMember') { + features = pushParseAndPop(undefined, parsersNS, node, objectStack); + } else { + features = pushParseAndPop([], parsersNS, node, objectStack); + } + } + if (features === null) { + features = []; + } + return features; + } /** - * @protected - * @type {Object.|string|undefined} + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/geom/Geometry|undefined} Geometry. */ - this.featureNS = options.featureNS; + readGeometryElement(node, objectStack) { + const context = /** @type {Object} */ (objectStack[0]); + context['srsName'] = node.firstElementChild.getAttribute('srsName'); + context['srsDimension'] = node.firstElementChild.getAttribute('srsDimension'); + /** @type {module:ol/geom/Geometry} */ + const geometry = pushParseAndPop(null, this.GEOMETRY_PARSERS_, node, objectStack, this); + if (geometry) { + return ( + /** @type {module:ol/geom/Geometry} */ (transformWithOptions(geometry, false, context)) + ); + } else { + return undefined; + } + } /** - * @protected - * @type {string} + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/Feature} Feature. */ - this.srsName = options.srsName; + readFeatureElement(node, objectStack) { + let n; + const fid = node.getAttribute('fid') || getAttributeNS(node, GMLNS, 'id'); + const values = {}; + let geometryName; + for (n = node.firstElementChild; n; n = n.nextElementSibling) { + const localName = n.localName; + // Assume attribute elements have one child node and that the child + // is a text or CDATA node (to be treated as text). + // Otherwise assume it is a geometry node. + if (n.childNodes.length === 0 || + (n.childNodes.length === 1 && + (n.firstChild.nodeType === 3 || n.firstChild.nodeType === 4))) { + let value = getAllTextContent(n, false); + if (ONLY_WHITESPACE_RE.test(value)) { + value = undefined; + } + values[localName] = value; + } else { + // boundedBy is an extent and must not be considered as a geometry + if (localName !== 'boundedBy') { + geometryName = localName; + } + values[localName] = this.readGeometryElement(n, objectStack); + } + } + const feature = new Feature(values); + if (geometryName) { + feature.setGeometryName(geometryName); + } + if (fid) { + feature.setId(fid); + } + return feature; + } /** - * @protected - * @type {string} + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/geom/Point|undefined} Point. */ - this.schemaLocation = ''; + readPoint(node, objectStack) { + const flatCoordinates = this.readFlatCoordinatesFromNode_(node, objectStack); + if (flatCoordinates) { + return new Point(flatCoordinates, GeometryLayout.XYZ); + } + } /** - * @type {Object.>} + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/geom/MultiPoint|undefined} MultiPoint. */ - this.FEATURE_COLLECTION_PARSERS = {}; - this.FEATURE_COLLECTION_PARSERS[GMLNS] = { - 'featureMember': makeReplacer(GMLBase.prototype.readFeaturesInternal), - 'featureMembers': makeReplacer(GMLBase.prototype.readFeaturesInternal) - }; + readMultiPoint(node, objectStack) { + /** @type {Array.>} */ + const coordinates = pushParseAndPop([], + this.MULTIPOINT_PARSERS_, node, objectStack, this); + if (coordinates) { + return new MultiPoint(coordinates); + } else { + return undefined; + } + } - XMLFeature.call(this); -}; + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/geom/MultiLineString|undefined} MultiLineString. + */ + readMultiLineString(node, objectStack) { + /** @type {Array.} */ + const lineStrings = pushParseAndPop([], + this.MULTILINESTRING_PARSERS_, node, objectStack, this); + if (lineStrings) { + return new MultiLineString(lineStrings); + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/geom/MultiPolygon|undefined} MultiPolygon. + */ + readMultiPolygon(node, objectStack) { + /** @type {Array.} */ + const polygons = pushParseAndPop([], this.MULTIPOLYGON_PARSERS_, node, objectStack, this); + if (polygons) { + return new MultiPolygon(polygons); + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + pointMemberParser_(node, objectStack) { + parseNode(this.POINTMEMBER_PARSERS_, node, objectStack, this); + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + lineStringMemberParser_(node, objectStack) { + parseNode(this.LINESTRINGMEMBER_PARSERS_, node, objectStack, this); + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + polygonMemberParser_(node, objectStack) { + parseNode(this.POLYGONMEMBER_PARSERS_, node, objectStack, this); + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/geom/LineString|undefined} LineString. + */ + readLineString(node, objectStack) { + const flatCoordinates = this.readFlatCoordinatesFromNode_(node, objectStack); + if (flatCoordinates) { + const lineString = new LineString(flatCoordinates, GeometryLayout.XYZ); + return lineString; + } else { + return undefined; + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.|undefined} LinearRing flat coordinates. + */ + readFlatLinearRing_(node, objectStack) { + const ring = pushParseAndPop(null, + this.GEOMETRY_FLAT_COORDINATES_PARSERS_, node, + objectStack, this); + if (ring) { + return ring; + } else { + return undefined; + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/geom/LinearRing|undefined} LinearRing. + */ + readLinearRing(node, objectStack) { + const flatCoordinates = this.readFlatCoordinatesFromNode_(node, objectStack); + if (flatCoordinates) { + return new LinearRing(flatCoordinates, GeometryLayout.XYZ); + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {module:ol/geom/Polygon|undefined} Polygon. + */ + readPolygon(node, objectStack) { + /** @type {Array.>} */ + const flatLinearRings = pushParseAndPop([null], + this.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack, this); + if (flatLinearRings && flatLinearRings[0]) { + const flatCoordinates = flatLinearRings[0]; + const ends = [flatCoordinates.length]; + let i, ii; + for (i = 1, ii = flatLinearRings.length; i < ii; ++i) { + extend(flatCoordinates, flatLinearRings[i]); + ends.push(flatCoordinates.length); + } + return new Polygon(flatCoordinates, GeometryLayout.XYZ, ends); + } else { + return undefined; + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.} Flat coordinates. + */ + readFlatCoordinatesFromNode_(node, objectStack) { + return pushParseAndPop(null, this.GEOMETRY_FLAT_COORDINATES_PARSERS_, node, objectStack, this); + } + + /** + * @inheritDoc + */ + readGeometryFromNode(node, opt_options) { + const geometry = this.readGeometryElement(node, + [this.getReadOptions(node, opt_options ? opt_options : {})]); + return geometry ? geometry : null; + } + + /** + * @inheritDoc + */ + readFeaturesFromNode(node, opt_options) { + const options = { + featureType: this.featureType, + featureNS: this.featureNS + }; + if (opt_options) { + assign(options, this.getReadOptions(node, opt_options)); + } + const features = this.readFeaturesInternal(node, [options]); + return features || []; + } + + /** + * @inheritDoc + */ + readProjectionFromNode(node) { + return getProjection(this.srsName ? this.srsName : node.firstElementChild.getAttribute('srsName')); + } +} inherits(GMLBase, XMLFeature); @@ -131,329 +472,6 @@ inherits(GMLBase, XMLFeature); const ONLY_WHITESPACE_RE = /^[\s\xa0]*$/; -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {Array. | undefined} Features. - */ -GMLBase.prototype.readFeaturesInternal = function(node, objectStack) { - const localName = node.localName; - let features = null; - if (localName == 'FeatureCollection') { - if (node.namespaceURI === 'http://www.opengis.net/wfs') { - features = pushParseAndPop([], - this.FEATURE_COLLECTION_PARSERS, node, - objectStack, this); - } else { - features = pushParseAndPop(null, - this.FEATURE_COLLECTION_PARSERS, node, - objectStack, this); - } - } else if (localName == 'featureMembers' || localName == 'featureMember') { - const context = objectStack[0]; - let featureType = context['featureType']; - let featureNS = context['featureNS']; - const prefix = 'p'; - const defaultPrefix = 'p0'; - if (!featureType && node.childNodes) { - featureType = [], featureNS = {}; - for (let i = 0, ii = node.childNodes.length; i < ii; ++i) { - const child = node.childNodes[i]; - if (child.nodeType === 1) { - const ft = child.nodeName.split(':').pop(); - if (featureType.indexOf(ft) === -1) { - let key = ''; - let count = 0; - const uri = child.namespaceURI; - for (const candidate in featureNS) { - if (featureNS[candidate] === uri) { - key = candidate; - break; - } - ++count; - } - if (!key) { - key = prefix + count; - featureNS[key] = uri; - } - featureType.push(key + ':' + ft); - } - } - } - if (localName != 'featureMember') { - // recheck featureType for each featureMember - context['featureType'] = featureType; - context['featureNS'] = featureNS; - } - } - if (typeof featureNS === 'string') { - const ns = featureNS; - featureNS = {}; - featureNS[defaultPrefix] = ns; - } - const parsersNS = {}; - const featureTypes = Array.isArray(featureType) ? featureType : [featureType]; - for (const p in featureNS) { - const parsers = {}; - for (let i = 0, ii = featureTypes.length; i < ii; ++i) { - const featurePrefix = featureTypes[i].indexOf(':') === -1 ? - defaultPrefix : featureTypes[i].split(':')[0]; - if (featurePrefix === p) { - parsers[featureTypes[i].split(':').pop()] = - (localName == 'featureMembers') ? - makeArrayPusher(this.readFeatureElement, this) : - makeReplacer(this.readFeatureElement, this); - } - } - parsersNS[featureNS[p]] = parsers; - } - if (localName == 'featureMember') { - features = pushParseAndPop(undefined, parsersNS, node, objectStack); - } else { - features = pushParseAndPop([], parsersNS, node, objectStack); - } - } - if (features === null) { - features = []; - } - return features; -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/geom/Geometry|undefined} Geometry. - */ -GMLBase.prototype.readGeometryElement = function(node, objectStack) { - const context = /** @type {Object} */ (objectStack[0]); - context['srsName'] = node.firstElementChild.getAttribute('srsName'); - context['srsDimension'] = node.firstElementChild.getAttribute('srsDimension'); - /** @type {module:ol/geom/Geometry} */ - const geometry = pushParseAndPop(null, this.GEOMETRY_PARSERS_, node, objectStack, this); - if (geometry) { - return ( - /** @type {module:ol/geom/Geometry} */ (transformWithOptions(geometry, false, context)) - ); - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/Feature} Feature. - */ -GMLBase.prototype.readFeatureElement = function(node, objectStack) { - let n; - const fid = node.getAttribute('fid') || getAttributeNS(node, GMLNS, 'id'); - const values = {}; - let geometryName; - for (n = node.firstElementChild; n; n = n.nextElementSibling) { - const localName = n.localName; - // Assume attribute elements have one child node and that the child - // is a text or CDATA node (to be treated as text). - // Otherwise assume it is a geometry node. - if (n.childNodes.length === 0 || - (n.childNodes.length === 1 && - (n.firstChild.nodeType === 3 || n.firstChild.nodeType === 4))) { - let value = getAllTextContent(n, false); - if (ONLY_WHITESPACE_RE.test(value)) { - value = undefined; - } - values[localName] = value; - } else { - // boundedBy is an extent and must not be considered as a geometry - if (localName !== 'boundedBy') { - geometryName = localName; - } - values[localName] = this.readGeometryElement(n, objectStack); - } - } - const feature = new Feature(values); - if (geometryName) { - feature.setGeometryName(geometryName); - } - if (fid) { - feature.setId(fid); - } - return feature; -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/geom/Point|undefined} Point. - */ -GMLBase.prototype.readPoint = function(node, objectStack) { - const flatCoordinates = this.readFlatCoordinatesFromNode_(node, objectStack); - if (flatCoordinates) { - return new Point(flatCoordinates, GeometryLayout.XYZ); - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/geom/MultiPoint|undefined} MultiPoint. - */ -GMLBase.prototype.readMultiPoint = function(node, objectStack) { - /** @type {Array.>} */ - const coordinates = pushParseAndPop([], - this.MULTIPOINT_PARSERS_, node, objectStack, this); - if (coordinates) { - return new MultiPoint(coordinates); - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/geom/MultiLineString|undefined} MultiLineString. - */ -GMLBase.prototype.readMultiLineString = function(node, objectStack) { - /** @type {Array.} */ - const lineStrings = pushParseAndPop([], - this.MULTILINESTRING_PARSERS_, node, objectStack, this); - if (lineStrings) { - return new MultiLineString(lineStrings); - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/geom/MultiPolygon|undefined} MultiPolygon. - */ -GMLBase.prototype.readMultiPolygon = function(node, objectStack) { - /** @type {Array.} */ - const polygons = pushParseAndPop([], this.MULTIPOLYGON_PARSERS_, node, objectStack, this); - if (polygons) { - return new MultiPolygon(polygons); - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GMLBase.prototype.pointMemberParser_ = function(node, objectStack) { - parseNode(this.POINTMEMBER_PARSERS_, node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GMLBase.prototype.lineStringMemberParser_ = function(node, objectStack) { - parseNode(this.LINESTRINGMEMBER_PARSERS_, node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -GMLBase.prototype.polygonMemberParser_ = function(node, objectStack) { - parseNode(this.POLYGONMEMBER_PARSERS_, node, objectStack, this); -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/geom/LineString|undefined} LineString. - */ -GMLBase.prototype.readLineString = function(node, objectStack) { - const flatCoordinates = this.readFlatCoordinatesFromNode_(node, objectStack); - if (flatCoordinates) { - const lineString = new LineString(flatCoordinates, GeometryLayout.XYZ); - return lineString; - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.|undefined} LinearRing flat coordinates. - */ -GMLBase.prototype.readFlatLinearRing_ = function(node, objectStack) { - const ring = pushParseAndPop(null, - this.GEOMETRY_FLAT_COORDINATES_PARSERS_, node, - objectStack, this); - if (ring) { - return ring; - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/geom/LinearRing|undefined} LinearRing. - */ -GMLBase.prototype.readLinearRing = function(node, objectStack) { - const flatCoordinates = this.readFlatCoordinatesFromNode_(node, objectStack); - if (flatCoordinates) { - return new LinearRing(flatCoordinates, GeometryLayout.XYZ); - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {module:ol/geom/Polygon|undefined} Polygon. - */ -GMLBase.prototype.readPolygon = function(node, objectStack) { - /** @type {Array.>} */ - const flatLinearRings = pushParseAndPop([null], - this.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack, this); - if (flatLinearRings && flatLinearRings[0]) { - const flatCoordinates = flatLinearRings[0]; - const ends = [flatCoordinates.length]; - let i, ii; - for (i = 1, ii = flatLinearRings.length; i < ii; ++i) { - extend(flatCoordinates, flatLinearRings[i]); - ends.push(flatCoordinates.length); - } - return new Polygon(flatCoordinates, GeometryLayout.XYZ, ends); - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.} Flat coordinates. - */ -GMLBase.prototype.readFlatCoordinatesFromNode_ = function(node, objectStack) { - return pushParseAndPop(null, this.GEOMETRY_FLAT_COORDINATES_PARSERS_, node, objectStack, this); -}; - - /** * @const * @type {Object.>} @@ -541,16 +559,6 @@ GMLBase.prototype.RING_PARSERS = { }; -/** - * @inheritDoc - */ -GMLBase.prototype.readGeometryFromNode = function(node, opt_options) { - const geometry = this.readGeometryElement(node, - [this.getReadOptions(node, opt_options ? opt_options : {})]); - return geometry ? geometry : null; -}; - - /** * Read all features from a GML FeatureCollection. * @@ -563,26 +571,4 @@ GMLBase.prototype.readGeometryFromNode = function(node, opt_options) { GMLBase.prototype.readFeatures; -/** - * @inheritDoc - */ -GMLBase.prototype.readFeaturesFromNode = function(node, opt_options) { - const options = { - featureType: this.featureType, - featureNS: this.featureNS - }; - if (opt_options) { - assign(options, this.getReadOptions(node, opt_options)); - } - const features = this.readFeaturesInternal(node, [options]); - return features || []; -}; - - -/** - * @inheritDoc - */ -GMLBase.prototype.readProjectionFromNode = function(node) { - return getProjection(this.srsName ? this.srsName : node.firstElementChild.getAttribute('srsName')); -}; export default GMLBase; diff --git a/src/ol/format/GPX.js b/src/ol/format/GPX.js index c2c696a0cb..dbcc8fd9e2 100644 --- a/src/ol/format/GPX.js +++ b/src/ol/format/GPX.js @@ -43,23 +43,109 @@ import {createElementNS, makeArrayPusher, makeArraySerializer, makeChildAppender * @param {module:ol/format/GPX~Options=} opt_options Options. * @api */ -const GPX = function(opt_options) { +class GPX { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - XMLFeature.call(this); + XMLFeature.call(this); + + /** + * @inheritDoc + */ + this.dataProjection = getProjection('EPSG:4326'); + + /** + * @type {function(module:ol/Feature, Node)|undefined} + * @private + */ + this.readExtensions_ = options.readExtensions; + } + + /** + * @param {Array.} features List of features. + * @private + */ + handleReadExtensions_(features) { + if (!features) { + features = []; + } + for (let i = 0, ii = features.length; i < ii; ++i) { + const feature = features[i]; + if (this.readExtensions_) { + const extensionsNode = feature.get('extensionsNode_') || null; + this.readExtensions_(feature, extensionsNode); + } + feature.set('extensionsNode_', undefined); + } + } /** * @inheritDoc */ - this.dataProjection = getProjection('EPSG:4326'); + readFeatureFromNode(node, opt_options) { + if (!includes(NAMESPACE_URIS, node.namespaceURI)) { + return null; + } + const featureReader = FEATURE_READER[node.localName]; + if (!featureReader) { + return null; + } + const feature = featureReader(node, [this.getReadOptions(node, opt_options)]); + if (!feature) { + return null; + } + this.handleReadExtensions_([feature]); + return feature; + } /** - * @type {function(module:ol/Feature, Node)|undefined} - * @private + * @inheritDoc */ - this.readExtensions_ = options.readExtensions; -}; + readFeaturesFromNode(node, opt_options) { + if (!includes(NAMESPACE_URIS, node.namespaceURI)) { + return []; + } + if (node.localName == 'gpx') { + /** @type {Array.} */ + const features = pushParseAndPop([], GPX_PARSERS, + node, [this.getReadOptions(node, opt_options)]); + if (features) { + this.handleReadExtensions_(features); + return features; + } else { + return []; + } + } + return []; + } + + /** + * Encode an array of features in the GPX format as an XML node. + * LineString geometries are output as routes (``), and MultiLineString + * as tracks (``). + * + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. + * @return {Node} Node. + * @override + * @api + */ + writeFeaturesNode(features, opt_options) { + opt_options = this.adaptOptions(opt_options); + //FIXME Serialize metadata + const gpx = createElementNS('http://www.topografix.com/GPX/1/1', 'gpx'); + const xmlnsUri = 'http://www.w3.org/2000/xmlns/'; + gpx.setAttributeNS(xmlnsUri, 'xmlns:xsi', XML_SCHEMA_INSTANCE_URI); + gpx.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', SCHEMA_LOCATION); + gpx.setAttribute('version', '1.1'); + gpx.setAttribute('creator', 'OpenLayers'); + + pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ + ({node: gpx}), GPX_SERIALIZERS, GPX_NODE_FACTORY, features, [opt_options]); + return gpx; + } +} inherits(GPX, XMLFeature); @@ -614,25 +700,6 @@ function readWpt(node, objectStack) { } -/** - * @param {Array.} features List of features. - * @private - */ -GPX.prototype.handleReadExtensions_ = function(features) { - if (!features) { - features = []; - } - for (let i = 0, ii = features.length; i < ii; ++i) { - const feature = features[i]; - if (this.readExtensions_) { - const extensionsNode = feature.get('extensionsNode_') || null; - this.readExtensions_(feature, extensionsNode); - } - feature.set('extensionsNode_', undefined); - } -}; - - /** * Read the first feature from a GPX source. * Routes (``) are converted into LineString geometries, and tracks (``) @@ -647,26 +714,6 @@ GPX.prototype.handleReadExtensions_ = function(features) { GPX.prototype.readFeature; -/** - * @inheritDoc - */ -GPX.prototype.readFeatureFromNode = function(node, opt_options) { - if (!includes(NAMESPACE_URIS, node.namespaceURI)) { - return null; - } - const featureReader = FEATURE_READER[node.localName]; - if (!featureReader) { - return null; - } - const feature = featureReader(node, [this.getReadOptions(node, opt_options)]); - if (!feature) { - return null; - } - this.handleReadExtensions_([feature]); - return feature; -}; - - /** * Read all features from a GPX source. * Routes (``) are converted into LineString geometries, and tracks (``) @@ -681,28 +728,6 @@ GPX.prototype.readFeatureFromNode = function(node, opt_options) { GPX.prototype.readFeatures; -/** - * @inheritDoc - */ -GPX.prototype.readFeaturesFromNode = function(node, opt_options) { - if (!includes(NAMESPACE_URIS, node.namespaceURI)) { - return []; - } - if (node.localName == 'gpx') { - /** @type {Array.} */ - const features = pushParseAndPop([], GPX_PARSERS, - node, [this.getReadOptions(node, opt_options)]); - if (features) { - this.handleReadExtensions_(features); - return features; - } else { - return []; - } - } - return []; -}; - - /** * Read the projection from a GPX source. * @@ -874,29 +899,4 @@ function writeWpt(node, feature, objectStack) { GPX.prototype.writeFeatures; -/** - * Encode an array of features in the GPX format as an XML node. - * LineString geometries are output as routes (``), and MultiLineString - * as tracks (``). - * - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. - * @return {Node} Node. - * @override - * @api - */ -GPX.prototype.writeFeaturesNode = function(features, opt_options) { - opt_options = this.adaptOptions(opt_options); - //FIXME Serialize metadata - const gpx = createElementNS('http://www.topografix.com/GPX/1/1', 'gpx'); - const xmlnsUri = 'http://www.w3.org/2000/xmlns/'; - gpx.setAttributeNS(xmlnsUri, 'xmlns:xsi', XML_SCHEMA_INSTANCE_URI); - gpx.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', SCHEMA_LOCATION); - gpx.setAttribute('version', '1.1'); - gpx.setAttribute('creator', 'OpenLayers'); - - pushSerializeAndPop(/** @type {module:ol/xml~NodeStackItem} */ - ({node: gpx}), GPX_SERIALIZERS, GPX_NODE_FACTORY, features, [opt_options]); - return gpx; -}; export default GPX; diff --git a/src/ol/format/GeoJSON.js b/src/ol/format/GeoJSON.js index dadfc2ab1e..f3392fdfdf 100644 --- a/src/ol/format/GeoJSON.js +++ b/src/ol/format/GeoJSON.js @@ -42,38 +42,191 @@ import {get as getProjection} from '../proj.js'; * @param {module:ol/format/GeoJSON~Options=} opt_options Options. * @api */ -const GeoJSON = function(opt_options) { +class GeoJSON { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - JSONFeature.call(this); + JSONFeature.call(this); + + /** + * @inheritDoc + */ + this.dataProjection = getProjection( + options.dataProjection ? + options.dataProjection : 'EPSG:4326'); + + if (options.featureProjection) { + this.defaultFeatureProjection = getProjection(options.featureProjection); + } + + /** + * Name of the geometry attribute for features. + * @type {string|undefined} + * @private + */ + this.geometryName_ = options.geometryName; + + /** + * Look for the geometry name in the feature GeoJSON + * @type {boolean|undefined} + * @private + */ + this.extractGeometryName_ = options.extractGeometryName; + + } /** * @inheritDoc */ - this.dataProjection = getProjection( - options.dataProjection ? - options.dataProjection : 'EPSG:4326'); + readFeatureFromObject(object, opt_options) { + /** + * @type {GeoJSONFeature} + */ + let geoJSONFeature = null; + if (object.type === 'Feature') { + geoJSONFeature = /** @type {GeoJSONFeature} */ (object); + } else { + geoJSONFeature = /** @type {GeoJSONFeature} */ ({ + type: 'Feature', + geometry: /** @type {GeoJSONGeometry|GeoJSONGeometryCollection} */ (object) + }); + } - if (options.featureProjection) { - this.defaultFeatureProjection = getProjection(options.featureProjection); + const geometry = readGeometry(geoJSONFeature.geometry, opt_options); + const feature = new Feature(); + if (this.geometryName_) { + feature.setGeometryName(this.geometryName_); + } else if (this.extractGeometryName_ && geoJSONFeature.geometry_name !== undefined) { + feature.setGeometryName(geoJSONFeature.geometry_name); + } + feature.setGeometry(geometry); + if (geoJSONFeature.id !== undefined) { + feature.setId(geoJSONFeature.id); + } + if (geoJSONFeature.properties) { + feature.setProperties(geoJSONFeature.properties); + } + return feature; } /** - * Name of the geometry attribute for features. - * @type {string|undefined} - * @private + * @inheritDoc */ - this.geometryName_ = options.geometryName; + readFeaturesFromObject(object, opt_options) { + const geoJSONObject = /** @type {GeoJSONObject} */ (object); + /** @type {Array.} */ + let features = null; + if (geoJSONObject.type === 'FeatureCollection') { + const geoJSONFeatureCollection = /** @type {GeoJSONFeatureCollection} */ (object); + features = []; + const geoJSONFeatures = geoJSONFeatureCollection.features; + for (let i = 0, ii = geoJSONFeatures.length; i < ii; ++i) { + features.push(this.readFeatureFromObject(geoJSONFeatures[i], opt_options)); + } + } else { + features = [this.readFeatureFromObject(object, opt_options)]; + } + return features; + } /** - * Look for the geometry name in the feature GeoJSON - * @type {boolean|undefined} - * @private + * @inheritDoc */ - this.extractGeometryName_ = options.extractGeometryName; + readGeometryFromObject(object, opt_options) { + return readGeometry(/** @type {GeoJSONGeometry} */ (object), opt_options); + } -}; + /** + * @inheritDoc + */ + readProjectionFromObject(object) { + const geoJSONObject = /** @type {GeoJSONObject} */ (object); + const crs = geoJSONObject.crs; + let projection; + if (crs) { + if (crs.type == 'name') { + projection = getProjection(crs.properties.name); + } else { + assert(false, 36); // Unknown SRS type + } + } else { + projection = this.dataProjection; + } + return ( + /** @type {module:ol/proj/Projection} */ (projection) + ); + } + + /** + * Encode a feature as a GeoJSON Feature object. + * + * @param {module:ol/Feature} feature Feature. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {GeoJSONFeature} Object. + * @override + * @api + */ + writeFeatureObject(feature, opt_options) { + opt_options = this.adaptOptions(opt_options); + + const object = /** @type {GeoJSONFeature} */ ({ + 'type': 'Feature' + }); + const id = feature.getId(); + if (id !== undefined) { + object.id = id; + } + const geometry = feature.getGeometry(); + if (geometry) { + object.geometry = writeGeometry(geometry, opt_options); + } else { + object.geometry = null; + } + const properties = feature.getProperties(); + delete properties[feature.getGeometryName()]; + if (!isEmpty(properties)) { + object.properties = properties; + } else { + object.properties = null; + } + return object; + } + + /** + * Encode an array of features as a GeoJSON object. + * + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {GeoJSONFeatureCollection} GeoJSON Object. + * @override + * @api + */ + writeFeaturesObject(features, opt_options) { + opt_options = this.adaptOptions(opt_options); + const objects = []; + for (let i = 0, ii = features.length; i < ii; ++i) { + objects.push(this.writeFeatureObject(features[i], opt_options)); + } + return /** @type {GeoJSONFeatureCollection} */ ({ + type: 'FeatureCollection', + features: objects + }); + } + + /** + * Encode a geometry as a GeoJSON object. + * + * @param {module:ol/geom/Geometry} geometry Geometry. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {GeoJSONGeometry|GeoJSONGeometryCollection} Object. + * @override + * @api + */ + writeGeometryObject(geometry, opt_options) { + return writeGeometry(geometry, this.adaptOptions(opt_options)); + } +} inherits(GeoJSON, JSONFeature); @@ -354,62 +507,6 @@ GeoJSON.prototype.readFeature; GeoJSON.prototype.readFeatures; -/** - * @inheritDoc - */ -GeoJSON.prototype.readFeatureFromObject = function(object, opt_options) { - /** - * @type {GeoJSONFeature} - */ - let geoJSONFeature = null; - if (object.type === 'Feature') { - geoJSONFeature = /** @type {GeoJSONFeature} */ (object); - } else { - geoJSONFeature = /** @type {GeoJSONFeature} */ ({ - type: 'Feature', - geometry: /** @type {GeoJSONGeometry|GeoJSONGeometryCollection} */ (object) - }); - } - - const geometry = readGeometry(geoJSONFeature.geometry, opt_options); - const feature = new Feature(); - if (this.geometryName_) { - feature.setGeometryName(this.geometryName_); - } else if (this.extractGeometryName_ && geoJSONFeature.geometry_name !== undefined) { - feature.setGeometryName(geoJSONFeature.geometry_name); - } - feature.setGeometry(geometry); - if (geoJSONFeature.id !== undefined) { - feature.setId(geoJSONFeature.id); - } - if (geoJSONFeature.properties) { - feature.setProperties(geoJSONFeature.properties); - } - return feature; -}; - - -/** - * @inheritDoc - */ -GeoJSON.prototype.readFeaturesFromObject = function(object, opt_options) { - const geoJSONObject = /** @type {GeoJSONObject} */ (object); - /** @type {Array.} */ - let features = null; - if (geoJSONObject.type === 'FeatureCollection') { - const geoJSONFeatureCollection = /** @type {GeoJSONFeatureCollection} */ (object); - features = []; - const geoJSONFeatures = geoJSONFeatureCollection.features; - for (let i = 0, ii = geoJSONFeatures.length; i < ii; ++i) { - features.push(this.readFeatureFromObject(geoJSONFeatures[i], opt_options)); - } - } else { - features = [this.readFeatureFromObject(object, opt_options)]; - } - return features; -}; - - /** * Read a geometry from a GeoJSON source. * @@ -422,14 +519,6 @@ GeoJSON.prototype.readFeaturesFromObject = function(object, opt_options) { GeoJSON.prototype.readGeometry; -/** - * @inheritDoc - */ -GeoJSON.prototype.readGeometryFromObject = function(object, opt_options) { - return readGeometry(/** @type {GeoJSONGeometry} */ (object), opt_options); -}; - - /** * Read the projection from a GeoJSON source. * @@ -441,28 +530,6 @@ GeoJSON.prototype.readGeometryFromObject = function(object, opt_options) { GeoJSON.prototype.readProjection; -/** - * @inheritDoc - */ -GeoJSON.prototype.readProjectionFromObject = function(object) { - const geoJSONObject = /** @type {GeoJSONObject} */ (object); - const crs = geoJSONObject.crs; - let projection; - if (crs) { - if (crs.type == 'name') { - projection = getProjection(crs.properties.name); - } else { - assert(false, 36); // Unknown SRS type - } - } else { - projection = this.dataProjection; - } - return ( - /** @type {module:ol/proj/Projection} */ (projection) - ); -}; - - /** * Encode a feature as a GeoJSON Feature string. * @@ -476,42 +543,6 @@ GeoJSON.prototype.readProjectionFromObject = function(object) { GeoJSON.prototype.writeFeature; -/** - * Encode a feature as a GeoJSON Feature object. - * - * @param {module:ol/Feature} feature Feature. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {GeoJSONFeature} Object. - * @override - * @api - */ -GeoJSON.prototype.writeFeatureObject = function(feature, opt_options) { - opt_options = this.adaptOptions(opt_options); - - const object = /** @type {GeoJSONFeature} */ ({ - 'type': 'Feature' - }); - const id = feature.getId(); - if (id !== undefined) { - object.id = id; - } - const geometry = feature.getGeometry(); - if (geometry) { - object.geometry = writeGeometry(geometry, opt_options); - } else { - object.geometry = null; - } - const properties = feature.getProperties(); - delete properties[feature.getGeometryName()]; - if (!isEmpty(properties)) { - object.properties = properties; - } else { - object.properties = null; - } - return object; -}; - - /** * Encode an array of features as GeoJSON. * @@ -524,28 +555,6 @@ GeoJSON.prototype.writeFeatureObject = function(feature, opt_options) { GeoJSON.prototype.writeFeatures; -/** - * Encode an array of features as a GeoJSON object. - * - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {GeoJSONFeatureCollection} GeoJSON Object. - * @override - * @api - */ -GeoJSON.prototype.writeFeaturesObject = function(features, opt_options) { - opt_options = this.adaptOptions(opt_options); - const objects = []; - for (let i = 0, ii = features.length; i < ii; ++i) { - objects.push(this.writeFeatureObject(features[i], opt_options)); - } - return /** @type {GeoJSONFeatureCollection} */ ({ - type: 'FeatureCollection', - features: objects - }); -}; - - /** * Encode a geometry as a GeoJSON string. * @@ -558,16 +567,4 @@ GeoJSON.prototype.writeFeaturesObject = function(features, opt_options) { GeoJSON.prototype.writeGeometry; -/** - * Encode a geometry as a GeoJSON object. - * - * @param {module:ol/geom/Geometry} geometry Geometry. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {GeoJSONGeometry|GeoJSONGeometryCollection} Object. - * @override - * @api - */ -GeoJSON.prototype.writeGeometryObject = function(geometry, opt_options) { - return writeGeometry(geometry, this.adaptOptions(opt_options)); -}; export default GeoJSON; diff --git a/src/ol/format/IGC.js b/src/ol/format/IGC.js index 3730fce6ac..e67a08dc9e 100644 --- a/src/ol/format/IGC.js +++ b/src/ol/format/IGC.js @@ -36,23 +36,136 @@ const IGCZ = { * @param {module:ol/format/IGC~Options=} opt_options Options. * @api */ -const IGC = function(opt_options) { +class IGC { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - TextFeature.call(this); + TextFeature.call(this); + + /** + * @inheritDoc + */ + this.dataProjection = getProjection('EPSG:4326'); + + /** + * @private + * @type {IGCZ} + */ + this.altitudeMode_ = options.altitudeMode ? options.altitudeMode : IGCZ.NONE; + } /** * @inheritDoc */ - this.dataProjection = getProjection('EPSG:4326'); + readFeatureFromText(text, opt_options) { + const altitudeMode = this.altitudeMode_; + const lines = text.split(NEWLINE_RE); + /** @type {Object.} */ + const properties = {}; + const flatCoordinates = []; + let year = 2000; + let month = 0; + let day = 1; + let lastDateTime = -1; + let i, ii; + for (i = 0, ii = lines.length; i < ii; ++i) { + const line = lines[i]; + let m; + if (line.charAt(0) == 'B') { + m = B_RECORD_RE.exec(line); + if (m) { + const hour = parseInt(m[1], 10); + const minute = parseInt(m[2], 10); + const second = parseInt(m[3], 10); + let y = parseInt(m[4], 10) + parseInt(m[5], 10) / 60000; + if (m[6] == 'S') { + y = -y; + } + let x = parseInt(m[7], 10) + parseInt(m[8], 10) / 60000; + if (m[9] == 'W') { + x = -x; + } + flatCoordinates.push(x, y); + if (altitudeMode != IGCZ.NONE) { + let z; + if (altitudeMode == IGCZ.GPS) { + z = parseInt(m[11], 10); + } else if (altitudeMode == IGCZ.BAROMETRIC) { + z = parseInt(m[12], 10); + } else { + z = 0; + } + flatCoordinates.push(z); + } + let dateTime = Date.UTC(year, month, day, hour, minute, second); + // Detect UTC midnight wrap around. + if (dateTime < lastDateTime) { + dateTime = Date.UTC(year, month, day + 1, hour, minute, second); + } + flatCoordinates.push(dateTime / 1000); + lastDateTime = dateTime; + } + } else if (line.charAt(0) == 'H') { + m = HFDTE_RECORD_RE.exec(line); + if (m) { + day = parseInt(m[1], 10); + month = parseInt(m[2], 10) - 1; + year = 2000 + parseInt(m[3], 10); + } else { + m = H_RECORD_RE.exec(line); + if (m) { + properties[m[1]] = m[2].trim(); + } + } + } + } + if (flatCoordinates.length === 0) { + return null; + } + const layout = altitudeMode == IGCZ.NONE ? GeometryLayout.XYM : GeometryLayout.XYZM; + const lineString = new LineString(flatCoordinates, layout); + const feature = new Feature(transformWithOptions(lineString, false, opt_options)); + feature.setProperties(properties); + return feature; + } /** - * @private - * @type {IGCZ} + * @inheritDoc */ - this.altitudeMode_ = options.altitudeMode ? options.altitudeMode : IGCZ.NONE; -}; + readFeaturesFromText(text, opt_options) { + const feature = this.readFeatureFromText(text, opt_options); + if (feature) { + return [feature]; + } else { + return []; + } + } + + /** + * Not implemented. + * @inheritDoc + */ + writeFeatureText(feature, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + writeFeaturesText(features, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + writeGeometryText(geometry, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + readGeometryFromText(text, opt_options) {} +} inherits(IGC, TextFeature); @@ -100,82 +213,6 @@ const NEWLINE_RE = /\r\n|\r|\n/; IGC.prototype.readFeature; -/** - * @inheritDoc - */ -IGC.prototype.readFeatureFromText = function(text, opt_options) { - const altitudeMode = this.altitudeMode_; - const lines = text.split(NEWLINE_RE); - /** @type {Object.} */ - const properties = {}; - const flatCoordinates = []; - let year = 2000; - let month = 0; - let day = 1; - let lastDateTime = -1; - let i, ii; - for (i = 0, ii = lines.length; i < ii; ++i) { - const line = lines[i]; - let m; - if (line.charAt(0) == 'B') { - m = B_RECORD_RE.exec(line); - if (m) { - const hour = parseInt(m[1], 10); - const minute = parseInt(m[2], 10); - const second = parseInt(m[3], 10); - let y = parseInt(m[4], 10) + parseInt(m[5], 10) / 60000; - if (m[6] == 'S') { - y = -y; - } - let x = parseInt(m[7], 10) + parseInt(m[8], 10) / 60000; - if (m[9] == 'W') { - x = -x; - } - flatCoordinates.push(x, y); - if (altitudeMode != IGCZ.NONE) { - let z; - if (altitudeMode == IGCZ.GPS) { - z = parseInt(m[11], 10); - } else if (altitudeMode == IGCZ.BAROMETRIC) { - z = parseInt(m[12], 10); - } else { - z = 0; - } - flatCoordinates.push(z); - } - let dateTime = Date.UTC(year, month, day, hour, minute, second); - // Detect UTC midnight wrap around. - if (dateTime < lastDateTime) { - dateTime = Date.UTC(year, month, day + 1, hour, minute, second); - } - flatCoordinates.push(dateTime / 1000); - lastDateTime = dateTime; - } - } else if (line.charAt(0) == 'H') { - m = HFDTE_RECORD_RE.exec(line); - if (m) { - day = parseInt(m[1], 10); - month = parseInt(m[2], 10) - 1; - year = 2000 + parseInt(m[3], 10); - } else { - m = H_RECORD_RE.exec(line); - if (m) { - properties[m[1]] = m[2].trim(); - } - } - } - } - if (flatCoordinates.length === 0) { - return null; - } - const layout = altitudeMode == IGCZ.NONE ? GeometryLayout.XYM : GeometryLayout.XYZM; - const lineString = new LineString(flatCoordinates, layout); - const feature = new Feature(transformWithOptions(lineString, false, opt_options)); - feature.setProperties(properties); - return feature; -}; - - /** * Read the feature from the source. As IGC sources contain a single * feature, this will return the feature in an array. @@ -189,19 +226,6 @@ IGC.prototype.readFeatureFromText = function(text, opt_options) { IGC.prototype.readFeatures; -/** - * @inheritDoc - */ -IGC.prototype.readFeaturesFromText = function(text, opt_options) { - const feature = this.readFeatureFromText(text, opt_options); - if (feature) { - return [feature]; - } else { - return []; - } -}; - - /** * Read the projection from the IGC source. * @@ -213,30 +237,4 @@ IGC.prototype.readFeaturesFromText = function(text, opt_options) { IGC.prototype.readProjection; -/** - * Not implemented. - * @inheritDoc - */ -IGC.prototype.writeFeatureText = function(feature, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -IGC.prototype.writeFeaturesText = function(features, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -IGC.prototype.writeGeometryText = function(geometry, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -IGC.prototype.readGeometryFromText = function(text, opt_options) {}; export default IGC; diff --git a/src/ol/format/JSONFeature.js b/src/ol/format/JSONFeature.js index 35e1033969..5a55a3f44c 100644 --- a/src/ol/format/JSONFeature.js +++ b/src/ol/format/JSONFeature.js @@ -15,9 +15,129 @@ import FormatType from '../format/FormatType.js'; * @abstract * @extends {module:ol/format/Feature} */ -const JSONFeature = function() { - FeatureFormat.call(this); -}; +class JSONFeature { + constructor() { + FeatureFormat.call(this); + } + + /** + * @inheritDoc + */ + getType() { + return FormatType.JSON; + } + + /** + * @inheritDoc + */ + readFeature(source, opt_options) { + return this.readFeatureFromObject( + getObject(source), this.getReadOptions(source, opt_options)); + } + + /** + * @inheritDoc + */ + readFeatures(source, opt_options) { + return this.readFeaturesFromObject( + getObject(source), this.getReadOptions(source, opt_options)); + } + + /** + * @abstract + * @param {Object} object Object. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @protected + * @return {module:ol/Feature} Feature. + */ + readFeatureFromObject(object, opt_options) {} + + /** + * @abstract + * @param {Object} object Object. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @protected + * @return {Array.} Features. + */ + readFeaturesFromObject(object, opt_options) {} + + /** + * @inheritDoc + */ + readGeometry(source, opt_options) { + return this.readGeometryFromObject( + getObject(source), this.getReadOptions(source, opt_options)); + } + + /** + * @abstract + * @param {Object} object Object. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @protected + * @return {module:ol/geom/Geometry} Geometry. + */ + readGeometryFromObject(object, opt_options) {} + + /** + * @inheritDoc + */ + readProjection(source) { + return this.readProjectionFromObject(getObject(source)); + } + + /** + * @abstract + * @param {Object} object Object. + * @protected + * @return {module:ol/proj/Projection} Projection. + */ + readProjectionFromObject(object) {} + + /** + * @inheritDoc + */ + writeFeature(feature, opt_options) { + return JSON.stringify(this.writeFeatureObject(feature, opt_options)); + } + + /** + * @abstract + * @param {module:ol/Feature} feature Feature. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {Object} Object. + */ + writeFeatureObject(feature, opt_options) {} + + /** + * @inheritDoc + */ + writeFeatures(features, opt_options) { + return JSON.stringify(this.writeFeaturesObject(features, opt_options)); + } + + /** + * @abstract + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {Object} Object. + */ + writeFeaturesObject(features, opt_options) {} + + /** + * @inheritDoc + */ + writeGeometry(geometry, opt_options) { + return JSON.stringify(this.writeGeometryObject(geometry, opt_options)); + } + + /** + * @abstract + * @param {module:ol/geom/Geometry} geometry Geometry. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @return {Object} Object. + */ + writeGeometryObject(geometry, opt_options) {} +} inherits(JSONFeature, FeatureFormat); @@ -38,135 +158,4 @@ function getObject(source) { } -/** - * @inheritDoc - */ -JSONFeature.prototype.getType = function() { - return FormatType.JSON; -}; - - -/** - * @inheritDoc - */ -JSONFeature.prototype.readFeature = function(source, opt_options) { - return this.readFeatureFromObject( - getObject(source), this.getReadOptions(source, opt_options)); -}; - - -/** - * @inheritDoc - */ -JSONFeature.prototype.readFeatures = function(source, opt_options) { - return this.readFeaturesFromObject( - getObject(source), this.getReadOptions(source, opt_options)); -}; - - -/** - * @abstract - * @param {Object} object Object. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @protected - * @return {module:ol/Feature} Feature. - */ -JSONFeature.prototype.readFeatureFromObject = function(object, opt_options) {}; - - -/** - * @abstract - * @param {Object} object Object. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @protected - * @return {Array.} Features. - */ -JSONFeature.prototype.readFeaturesFromObject = function(object, opt_options) {}; - - -/** - * @inheritDoc - */ -JSONFeature.prototype.readGeometry = function(source, opt_options) { - return this.readGeometryFromObject( - getObject(source), this.getReadOptions(source, opt_options)); -}; - - -/** - * @abstract - * @param {Object} object Object. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @protected - * @return {module:ol/geom/Geometry} Geometry. - */ -JSONFeature.prototype.readGeometryFromObject = function(object, opt_options) {}; - - -/** - * @inheritDoc - */ -JSONFeature.prototype.readProjection = function(source) { - return this.readProjectionFromObject(getObject(source)); -}; - - -/** - * @abstract - * @param {Object} object Object. - * @protected - * @return {module:ol/proj/Projection} Projection. - */ -JSONFeature.prototype.readProjectionFromObject = function(object) {}; - - -/** - * @inheritDoc - */ -JSONFeature.prototype.writeFeature = function(feature, opt_options) { - return JSON.stringify(this.writeFeatureObject(feature, opt_options)); -}; - - -/** - * @abstract - * @param {module:ol/Feature} feature Feature. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {Object} Object. - */ -JSONFeature.prototype.writeFeatureObject = function(feature, opt_options) {}; - - -/** - * @inheritDoc - */ -JSONFeature.prototype.writeFeatures = function(features, opt_options) { - return JSON.stringify(this.writeFeaturesObject(features, opt_options)); -}; - - -/** - * @abstract - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {Object} Object. - */ -JSONFeature.prototype.writeFeaturesObject = function(features, opt_options) {}; - - -/** - * @inheritDoc - */ -JSONFeature.prototype.writeGeometry = function(geometry, opt_options) { - return JSON.stringify(this.writeGeometryObject(geometry, opt_options)); -}; - - -/** - * @abstract - * @param {module:ol/geom/Geometry} geometry Geometry. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @return {Object} Object. - */ -JSONFeature.prototype.writeGeometryObject = function(geometry, opt_options) {}; export default JSONFeature; diff --git a/src/ol/format/KML.js b/src/ol/format/KML.js index 215e93c280..55385b8419 100644 --- a/src/ol/format/KML.js +++ b/src/ol/format/KML.js @@ -259,56 +259,456 @@ function createStyleDefaults() { * @param {module:ol/format/KML~Options=} opt_options Options. * @api */ -const KML = function(opt_options) { +class KML { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - XMLFeature.call(this); + XMLFeature.call(this); - if (!DEFAULT_STYLE_ARRAY) { - createStyleDefaults(); + if (!DEFAULT_STYLE_ARRAY) { + createStyleDefaults(); + } + + /** + * @inheritDoc + */ + this.dataProjection = getProjection('EPSG:4326'); + + /** + * @private + * @type {Array.} + */ + this.defaultStyle_ = options.defaultStyle ? + options.defaultStyle : DEFAULT_STYLE_ARRAY; + + /** + * @private + * @type {boolean} + */ + this.extractStyles_ = options.extractStyles !== undefined ? + options.extractStyles : true; + + /** + * @private + * @type {boolean} + */ + this.writeStyles_ = options.writeStyles !== undefined ? + options.writeStyles : true; + + /** + * @private + * @type {!Object.|string)>} + */ + this.sharedStyles_ = {}; + + /** + * @private + * @type {boolean} + */ + this.showPointNames_ = options.showPointNames !== undefined ? + options.showPointNames : true; + + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {Array.|undefined} Features. + */ + readDocumentOrFolder_(node, objectStack) { + // FIXME use scope somehow + const parsersNS = makeStructureNS( + NAMESPACE_URIS, { + 'Document': makeArrayExtender(this.readDocumentOrFolder_, this), + 'Folder': makeArrayExtender(this.readDocumentOrFolder_, this), + 'Placemark': makeArrayPusher(this.readPlacemark_, this), + 'Style': this.readSharedStyle_.bind(this), + 'StyleMap': this.readSharedStyleMap_.bind(this) + }); + /** @type {Array.} */ + const features = pushParseAndPop([], parsersNS, node, objectStack, this); + if (features) { + return features; + } else { + return undefined; + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + * @return {module:ol/Feature|undefined} Feature. + */ + readPlacemark_(node, objectStack) { + const object = pushParseAndPop({'geometry': null}, + PLACEMARK_PARSERS, node, objectStack); + if (!object) { + return undefined; + } + const feature = new Feature(); + const id = node.getAttribute('id'); + if (id !== null) { + feature.setId(id); + } + const options = /** @type {module:ol/format/Feature~ReadOptions} */ (objectStack[0]); + + const geometry = object['geometry']; + if (geometry) { + transformWithOptions(geometry, false, options); + } + feature.setGeometry(geometry); + delete object['geometry']; + + if (this.extractStyles_) { + const style = object['Style']; + const styleUrl = object['styleUrl']; + const styleFunction = createFeatureStyleFunction( + style, styleUrl, this.defaultStyle_, this.sharedStyles_, + this.showPointNames_); + feature.setStyle(styleFunction); + } + delete object['Style']; + // we do not remove the styleUrl property from the object, so it + // gets stored on feature when setProperties is called + + feature.setProperties(object); + + return feature; + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + readSharedStyle_(node, objectStack) { + const id = node.getAttribute('id'); + if (id !== null) { + const style = readStyle(node, objectStack); + if (style) { + let styleUri; + let baseURI = node.baseURI; + if (!baseURI || baseURI == 'about:blank') { + baseURI = window.location.href; + } + if (baseURI) { + const url = new URL('#' + id, baseURI); + styleUri = url.href; + } else { + styleUri = '#' + id; + } + this.sharedStyles_[styleUri] = style; + } + } + } + + /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @private + */ + readSharedStyleMap_(node, objectStack) { + const id = node.getAttribute('id'); + if (id === null) { + return; + } + const styleMapValue = readStyleMapValue(node, objectStack); + if (!styleMapValue) { + return; + } + let styleUri; + let baseURI = node.baseURI; + if (!baseURI || baseURI == 'about:blank') { + baseURI = window.location.href; + } + if (baseURI) { + const url = new URL('#' + id, baseURI); + styleUri = url.href; + } else { + styleUri = '#' + id; + } + this.sharedStyles_[styleUri] = styleMapValue; } /** * @inheritDoc */ - this.dataProjection = getProjection('EPSG:4326'); + readFeatureFromNode(node, opt_options) { + if (!includes(NAMESPACE_URIS, node.namespaceURI)) { + return null; + } + const feature = this.readPlacemark_( + node, [this.getReadOptions(node, opt_options)]); + if (feature) { + return feature; + } else { + return null; + } + } /** - * @private - * @type {Array.} + * @inheritDoc */ - this.defaultStyle_ = options.defaultStyle ? - options.defaultStyle : DEFAULT_STYLE_ARRAY; + readFeaturesFromNode(node, opt_options) { + if (!includes(NAMESPACE_URIS, node.namespaceURI)) { + return []; + } + let features; + const localName = node.localName; + if (localName == 'Document' || localName == 'Folder') { + features = this.readDocumentOrFolder_( + node, [this.getReadOptions(node, opt_options)]); + if (features) { + return features; + } else { + return []; + } + } else if (localName == 'Placemark') { + const feature = this.readPlacemark_( + node, [this.getReadOptions(node, opt_options)]); + if (feature) { + return [feature]; + } else { + return []; + } + } else if (localName == 'kml') { + features = []; + for (let n = node.firstElementChild; n; n = n.nextElementSibling) { + const fs = this.readFeaturesFromNode(n, opt_options); + if (fs) { + extend(features, fs); + } + } + return features; + } else { + return []; + } + } /** - * @private - * @type {boolean} + * Read the name of the KML. + * + * @param {Document|Node|string} source Source. + * @return {string|undefined} Name. + * @api */ - this.extractStyles_ = options.extractStyles !== undefined ? - options.extractStyles : true; + readName(source) { + if (isDocument(source)) { + return this.readNameFromDocument(/** @type {Document} */ (source)); + } else if (isNode(source)) { + return this.readNameFromNode(/** @type {Node} */ (source)); + } else if (typeof source === 'string') { + const doc = parse(source); + return this.readNameFromDocument(doc); + } else { + return undefined; + } + } /** - * @private - * @type {boolean} + * @param {Document} doc Document. + * @return {string|undefined} Name. */ - this.writeStyles_ = options.writeStyles !== undefined ? - options.writeStyles : true; + readNameFromDocument(doc) { + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + const name = this.readNameFromNode(n); + if (name) { + return name; + } + } + } + return undefined; + } /** - * @private - * @type {!Object.|string)>} + * @param {Node} node Node. + * @return {string|undefined} Name. */ - this.sharedStyles_ = {}; + readNameFromNode(node) { + for (let n = node.firstElementChild; n; n = n.nextElementSibling) { + if (includes(NAMESPACE_URIS, n.namespaceURI) && + n.localName == 'name') { + return readString(n); + } + } + for (let n = node.firstElementChild; n; n = n.nextElementSibling) { + const localName = n.localName; + if (includes(NAMESPACE_URIS, n.namespaceURI) && + (localName == 'Document' || + localName == 'Folder' || + localName == 'Placemark' || + localName == 'kml')) { + const name = this.readNameFromNode(n); + if (name) { + return name; + } + } + } + return undefined; + } /** - * @private - * @type {boolean} + * Read the network links of the KML. + * + * @param {Document|Node|string} source Source. + * @return {Array.} Network links. + * @api */ - this.showPointNames_ = options.showPointNames !== undefined ? - options.showPointNames : true; + readNetworkLinks(source) { + const networkLinks = []; + if (isDocument(source)) { + extend(networkLinks, this.readNetworkLinksFromDocument( + /** @type {Document} */ (source))); + } else if (isNode(source)) { + extend(networkLinks, this.readNetworkLinksFromNode( + /** @type {Node} */ (source))); + } else if (typeof source === 'string') { + const doc = parse(source); + extend(networkLinks, this.readNetworkLinksFromDocument(doc)); + } + return networkLinks; + } -}; + /** + * @param {Document} doc Document. + * @return {Array.} Network links. + */ + readNetworkLinksFromDocument(doc) { + const networkLinks = []; + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + extend(networkLinks, this.readNetworkLinksFromNode(n)); + } + } + return networkLinks; + } + + /** + * @param {Node} node Node. + * @return {Array.} Network links. + */ + readNetworkLinksFromNode(node) { + const networkLinks = []; + for (let n = node.firstElementChild; n; n = n.nextElementSibling) { + if (includes(NAMESPACE_URIS, n.namespaceURI) && + n.localName == 'NetworkLink') { + const obj = pushParseAndPop({}, NETWORK_LINK_PARSERS, + n, []); + networkLinks.push(obj); + } + } + for (let n = node.firstElementChild; n; n = n.nextElementSibling) { + const localName = n.localName; + if (includes(NAMESPACE_URIS, n.namespaceURI) && + (localName == 'Document' || + localName == 'Folder' || + localName == 'kml')) { + extend(networkLinks, this.readNetworkLinksFromNode(n)); + } + } + return networkLinks; + } + + /** + * Read the regions of the KML. + * + * @param {Document|Node|string} source Source. + * @return {Array.} Regions. + * @api + */ + readRegion(source) { + const regions = []; + if (isDocument(source)) { + extend(regions, this.readRegionFromDocument( + /** @type {Document} */ (source))); + } else if (isNode(source)) { + extend(regions, this.readRegionFromNode( + /** @type {Node} */ (source))); + } else if (typeof source === 'string') { + const doc = parse(source); + extend(regions, this.readRegionFromDocument(doc)); + } + return regions; + } + + /** + * @param {Document} doc Document. + * @return {Array.} Region. + */ + readRegionFromDocument(doc) { + const regions = []; + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + extend(regions, this.readRegionFromNode(n)); + } + } + return regions; + } + + /** + * @param {Node} node Node. + * @return {Array.} Region. + * @api + */ + readRegionFromNode(node) { + const regions = []; + for (let n = node.firstElementChild; n; n = n.nextElementSibling) { + if (includes(NAMESPACE_URIS, n.namespaceURI) && + n.localName == 'Region') { + const obj = pushParseAndPop({}, REGION_PARSERS, + n, []); + regions.push(obj); + } + } + for (let n = node.firstElementChild; n; n = n.nextElementSibling) { + const localName = n.localName; + if (includes(NAMESPACE_URIS, n.namespaceURI) && + (localName == 'Document' || + localName == 'Folder' || + localName == 'kml')) { + extend(regions, this.readRegionFromNode(n)); + } + } + return regions; + } + + /** + * Encode an array of features in the KML format as an XML node. GeometryCollections, + * MultiPoints, MultiLineStrings, and MultiPolygons are output as MultiGeometries. + * + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. + * @return {Node} Node. + * @override + * @api + */ + writeFeaturesNode(features, opt_options) { + opt_options = this.adaptOptions(opt_options); + const kml = createElementNS(NAMESPACE_URIS[4], 'kml'); + const xmlnsUri = 'http://www.w3.org/2000/xmlns/'; + kml.setAttributeNS(xmlnsUri, 'xmlns:gx', GX_NAMESPACE_URIS[0]); + kml.setAttributeNS(xmlnsUri, 'xmlns:xsi', XML_SCHEMA_INSTANCE_URI); + kml.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', SCHEMA_LOCATION); + + const /** @type {module:ol/xml~NodeStackItem} */ context = {node: kml}; + const properties = {}; + if (features.length > 1) { + properties['Document'] = features; + } else if (features.length == 1) { + properties['Placemark'] = features[0]; + } + const orderedKeys = KML_SEQUENCE[kml.namespaceURI]; + const values = makeSequence(properties, orderedKeys); + pushSerializeAndPop(context, KML_SERIALIZERS, + OBJECT_PROPERTY_NODE_FACTORY, values, [opt_options], orderedKeys, + this); + return kml; + } +} inherits(KML, XMLFeature); @@ -1643,132 +2043,6 @@ const PLACEMARK_PARSERS = makeStructureNS( )); -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {Array.|undefined} Features. - */ -KML.prototype.readDocumentOrFolder_ = function(node, objectStack) { - // FIXME use scope somehow - const parsersNS = makeStructureNS( - NAMESPACE_URIS, { - 'Document': makeArrayExtender(this.readDocumentOrFolder_, this), - 'Folder': makeArrayExtender(this.readDocumentOrFolder_, this), - 'Placemark': makeArrayPusher(this.readPlacemark_, this), - 'Style': this.readSharedStyle_.bind(this), - 'StyleMap': this.readSharedStyleMap_.bind(this) - }); - /** @type {Array.} */ - const features = pushParseAndPop([], parsersNS, node, objectStack, this); - if (features) { - return features; - } else { - return undefined; - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - * @return {module:ol/Feature|undefined} Feature. - */ -KML.prototype.readPlacemark_ = function(node, objectStack) { - const object = pushParseAndPop({'geometry': null}, - PLACEMARK_PARSERS, node, objectStack); - if (!object) { - return undefined; - } - const feature = new Feature(); - const id = node.getAttribute('id'); - if (id !== null) { - feature.setId(id); - } - const options = /** @type {module:ol/format/Feature~ReadOptions} */ (objectStack[0]); - - const geometry = object['geometry']; - if (geometry) { - transformWithOptions(geometry, false, options); - } - feature.setGeometry(geometry); - delete object['geometry']; - - if (this.extractStyles_) { - const style = object['Style']; - const styleUrl = object['styleUrl']; - const styleFunction = createFeatureStyleFunction( - style, styleUrl, this.defaultStyle_, this.sharedStyles_, - this.showPointNames_); - feature.setStyle(styleFunction); - } - delete object['Style']; - // we do not remove the styleUrl property from the object, so it - // gets stored on feature when setProperties is called - - feature.setProperties(object); - - return feature; -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -KML.prototype.readSharedStyle_ = function(node, objectStack) { - const id = node.getAttribute('id'); - if (id !== null) { - const style = readStyle(node, objectStack); - if (style) { - let styleUri; - let baseURI = node.baseURI; - if (!baseURI || baseURI == 'about:blank') { - baseURI = window.location.href; - } - if (baseURI) { - const url = new URL('#' + id, baseURI); - styleUri = url.href; - } else { - styleUri = '#' + id; - } - this.sharedStyles_[styleUri] = style; - } - } -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @private - */ -KML.prototype.readSharedStyleMap_ = function(node, objectStack) { - const id = node.getAttribute('id'); - if (id === null) { - return; - } - const styleMapValue = readStyleMapValue(node, objectStack); - if (!styleMapValue) { - return; - } - let styleUri; - let baseURI = node.baseURI; - if (!baseURI || baseURI == 'about:blank') { - baseURI = window.location.href; - } - if (baseURI) { - const url = new URL('#' + id, baseURI); - styleUri = url.href; - } else { - styleUri = '#' + id; - } - this.sharedStyles_[styleUri] = styleMapValue; -}; - - /** * Read the first feature from a KML source. MultiGeometries are converted into * GeometryCollections if they are a mix of geometry types, and into MultiPoint/ @@ -1783,23 +2057,6 @@ KML.prototype.readSharedStyleMap_ = function(node, objectStack) { KML.prototype.readFeature; -/** - * @inheritDoc - */ -KML.prototype.readFeatureFromNode = function(node, opt_options) { - if (!includes(NAMESPACE_URIS, node.namespaceURI)) { - return null; - } - const feature = this.readPlacemark_( - node, [this.getReadOptions(node, opt_options)]); - if (feature) { - return feature; - } else { - return null; - } -}; - - /** * Read all features from a KML source. MultiGeometries are converted into * GeometryCollections if they are a mix of geometry types, and into MultiPoint/ @@ -1814,243 +2071,6 @@ KML.prototype.readFeatureFromNode = function(node, opt_options) { KML.prototype.readFeatures; -/** - * @inheritDoc - */ -KML.prototype.readFeaturesFromNode = function(node, opt_options) { - if (!includes(NAMESPACE_URIS, node.namespaceURI)) { - return []; - } - let features; - const localName = node.localName; - if (localName == 'Document' || localName == 'Folder') { - features = this.readDocumentOrFolder_( - node, [this.getReadOptions(node, opt_options)]); - if (features) { - return features; - } else { - return []; - } - } else if (localName == 'Placemark') { - const feature = this.readPlacemark_( - node, [this.getReadOptions(node, opt_options)]); - if (feature) { - return [feature]; - } else { - return []; - } - } else if (localName == 'kml') { - features = []; - for (let n = node.firstElementChild; n; n = n.nextElementSibling) { - const fs = this.readFeaturesFromNode(n, opt_options); - if (fs) { - extend(features, fs); - } - } - return features; - } else { - return []; - } -}; - - -/** - * Read the name of the KML. - * - * @param {Document|Node|string} source Source. - * @return {string|undefined} Name. - * @api - */ -KML.prototype.readName = function(source) { - if (isDocument(source)) { - return this.readNameFromDocument(/** @type {Document} */ (source)); - } else if (isNode(source)) { - return this.readNameFromNode(/** @type {Node} */ (source)); - } else if (typeof source === 'string') { - const doc = parse(source); - return this.readNameFromDocument(doc); - } else { - return undefined; - } -}; - - -/** - * @param {Document} doc Document. - * @return {string|undefined} Name. - */ -KML.prototype.readNameFromDocument = function(doc) { - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - const name = this.readNameFromNode(n); - if (name) { - return name; - } - } - } - return undefined; -}; - - -/** - * @param {Node} node Node. - * @return {string|undefined} Name. - */ -KML.prototype.readNameFromNode = function(node) { - for (let n = node.firstElementChild; n; n = n.nextElementSibling) { - if (includes(NAMESPACE_URIS, n.namespaceURI) && - n.localName == 'name') { - return readString(n); - } - } - for (let n = node.firstElementChild; n; n = n.nextElementSibling) { - const localName = n.localName; - if (includes(NAMESPACE_URIS, n.namespaceURI) && - (localName == 'Document' || - localName == 'Folder' || - localName == 'Placemark' || - localName == 'kml')) { - const name = this.readNameFromNode(n); - if (name) { - return name; - } - } - } - return undefined; -}; - - -/** - * Read the network links of the KML. - * - * @param {Document|Node|string} source Source. - * @return {Array.} Network links. - * @api - */ -KML.prototype.readNetworkLinks = function(source) { - const networkLinks = []; - if (isDocument(source)) { - extend(networkLinks, this.readNetworkLinksFromDocument( - /** @type {Document} */ (source))); - } else if (isNode(source)) { - extend(networkLinks, this.readNetworkLinksFromNode( - /** @type {Node} */ (source))); - } else if (typeof source === 'string') { - const doc = parse(source); - extend(networkLinks, this.readNetworkLinksFromDocument(doc)); - } - return networkLinks; -}; - - -/** - * @param {Document} doc Document. - * @return {Array.} Network links. - */ -KML.prototype.readNetworkLinksFromDocument = function(doc) { - const networkLinks = []; - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - extend(networkLinks, this.readNetworkLinksFromNode(n)); - } - } - return networkLinks; -}; - - -/** - * @param {Node} node Node. - * @return {Array.} Network links. - */ -KML.prototype.readNetworkLinksFromNode = function(node) { - const networkLinks = []; - for (let n = node.firstElementChild; n; n = n.nextElementSibling) { - if (includes(NAMESPACE_URIS, n.namespaceURI) && - n.localName == 'NetworkLink') { - const obj = pushParseAndPop({}, NETWORK_LINK_PARSERS, - n, []); - networkLinks.push(obj); - } - } - for (let n = node.firstElementChild; n; n = n.nextElementSibling) { - const localName = n.localName; - if (includes(NAMESPACE_URIS, n.namespaceURI) && - (localName == 'Document' || - localName == 'Folder' || - localName == 'kml')) { - extend(networkLinks, this.readNetworkLinksFromNode(n)); - } - } - return networkLinks; -}; - - -/** - * Read the regions of the KML. - * - * @param {Document|Node|string} source Source. - * @return {Array.} Regions. - * @api - */ -KML.prototype.readRegion = function(source) { - const regions = []; - if (isDocument(source)) { - extend(regions, this.readRegionFromDocument( - /** @type {Document} */ (source))); - } else if (isNode(source)) { - extend(regions, this.readRegionFromNode( - /** @type {Node} */ (source))); - } else if (typeof source === 'string') { - const doc = parse(source); - extend(regions, this.readRegionFromDocument(doc)); - } - return regions; -}; - - -/** - * @param {Document} doc Document. - * @return {Array.} Region. - */ -KML.prototype.readRegionFromDocument = function(doc) { - const regions = []; - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - extend(regions, this.readRegionFromNode(n)); - } - } - return regions; -}; - - -/** - * @param {Node} node Node. - * @return {Array.} Region. - * @api - */ -KML.prototype.readRegionFromNode = function(node) { - const regions = []; - for (let n = node.firstElementChild; n; n = n.nextElementSibling) { - if (includes(NAMESPACE_URIS, n.namespaceURI) && - n.localName == 'Region') { - const obj = pushParseAndPop({}, REGION_PARSERS, - n, []); - regions.push(obj); - } - } - for (let n = node.firstElementChild; n; n = n.nextElementSibling) { - const localName = n.localName; - if (includes(NAMESPACE_URIS, n.namespaceURI) && - (localName == 'Document' || - localName == 'Folder' || - localName == 'kml')) { - extend(regions, this.readRegionFromNode(n)); - } - } - return regions; -}; - - /** * Read the projection from a KML source. * @@ -2955,37 +2975,4 @@ const KML_SERIALIZERS = makeStructureNS( KML.prototype.writeFeatures; -/** - * Encode an array of features in the KML format as an XML node. GeometryCollections, - * MultiPoints, MultiLineStrings, and MultiPolygons are output as MultiGeometries. - * - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. - * @return {Node} Node. - * @override - * @api - */ -KML.prototype.writeFeaturesNode = function(features, opt_options) { - opt_options = this.adaptOptions(opt_options); - const kml = createElementNS(NAMESPACE_URIS[4], 'kml'); - const xmlnsUri = 'http://www.w3.org/2000/xmlns/'; - kml.setAttributeNS(xmlnsUri, 'xmlns:gx', GX_NAMESPACE_URIS[0]); - kml.setAttributeNS(xmlnsUri, 'xmlns:xsi', XML_SCHEMA_INSTANCE_URI); - kml.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', SCHEMA_LOCATION); - - const /** @type {module:ol/xml~NodeStackItem} */ context = {node: kml}; - const properties = {}; - if (features.length > 1) { - properties['Document'] = features; - } else if (features.length == 1) { - properties['Placemark'] = features[0]; - } - const orderedKeys = KML_SEQUENCE[kml.namespaceURI]; - const values = makeSequence(properties, orderedKeys); - pushSerializeAndPop(context, KML_SERIALIZERS, - OBJECT_PROPERTY_NODE_FACTORY, values, [opt_options], orderedKeys, - this); - return kml; -}; - export default KML; diff --git a/src/ol/format/MVT.js b/src/ol/format/MVT.js index 618b5aea81..2e09508cbc 100644 --- a/src/ol/format/MVT.js +++ b/src/ol/format/MVT.js @@ -47,54 +47,276 @@ import RenderFeature from '../render/Feature.js'; * @param {module:ol/format/MVT~Options=} opt_options Options. * @api */ -const MVT = function(opt_options) { +class MVT { + constructor(opt_options) { - FeatureFormat.call(this); + FeatureFormat.call(this); - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; + + /** + * @type {module:ol/proj/Projection} + */ + this.dataProjection = new Projection({ + code: '', + units: Units.TILE_PIXELS + }); + + /** + * @private + * @type {function((module:ol/geom/Geometry|Object.)=)| + * function(module:ol/geom/GeometryType,Array., + * (Array.|Array.>),Object.,number)} + */ + this.featureClass_ = options.featureClass ? + options.featureClass : RenderFeature; + + /** + * @private + * @type {string|undefined} + */ + this.geometryName_ = options.geometryName; + + /** + * @private + * @type {string} + */ + this.layerName_ = options.layerName ? options.layerName : 'layer'; + + /** + * @private + * @type {Array.} + */ + this.layers_ = options.layers ? options.layers : null; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.extent_ = null; + + } /** - * @type {module:ol/proj/Projection} + * Read the raw geometry from the pbf offset stored in a raw feature's geometry + * property. + * @suppress {missingProperties} + * @param {Object} pbf PBF. + * @param {Object} feature Raw feature. + * @param {Array.} flatCoordinates Array to store flat coordinates in. + * @param {Array.} ends Array to store ends in. + * @private */ - this.dataProjection = new Projection({ - code: '', - units: Units.TILE_PIXELS - }); + readRawGeometry_(pbf, feature, flatCoordinates, ends) { + pbf.pos = feature.geometry; + + const end = pbf.readVarint() + pbf.pos; + let cmd = 1; + let length = 0; + let x = 0; + let y = 0; + let coordsLen = 0; + let currentEnd = 0; + + while (pbf.pos < end) { + if (!length) { + const cmdLen = pbf.readVarint(); + cmd = cmdLen & 0x7; + length = cmdLen >> 3; + } + + length--; + + if (cmd === 1 || cmd === 2) { + x += pbf.readSVarint(); + y += pbf.readSVarint(); + + if (cmd === 1) { // moveTo + if (coordsLen > currentEnd) { + ends.push(coordsLen); + currentEnd = coordsLen; + } + } + + flatCoordinates.push(x, y); + coordsLen += 2; + + } else if (cmd === 7) { + + if (coordsLen > currentEnd) { + // close polygon + flatCoordinates.push( + flatCoordinates[currentEnd], flatCoordinates[currentEnd + 1]); + coordsLen += 2; + } + + } else { + assert(false, 59); // Invalid command found in the PBF + } + } + + if (coordsLen > currentEnd) { + ends.push(coordsLen); + currentEnd = coordsLen; + } + + } /** * @private - * @type {function((module:ol/geom/Geometry|Object.)=)| - * function(module:ol/geom/GeometryType,Array., - * (Array.|Array.>),Object.,number)} + * @param {Object} pbf PBF + * @param {Object} rawFeature Raw Mapbox feature. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @return {module:ol/Feature|module:ol/render/Feature} Feature. */ - this.featureClass_ = options.featureClass ? - options.featureClass : RenderFeature; + createFeature_(pbf, rawFeature, opt_options) { + const type = rawFeature.type; + if (type === 0) { + return null; + } + + let feature; + const id = rawFeature.id; + const values = rawFeature.properties; + values[this.layerName_] = rawFeature.layer.name; + + const flatCoordinates = []; + const ends = []; + this.readRawGeometry_(pbf, rawFeature, flatCoordinates, ends); + + const geometryType = getGeometryType(type, ends.length); + + if (this.featureClass_ === RenderFeature) { + feature = new this.featureClass_(geometryType, flatCoordinates, ends, values, id); + } else { + let geom; + if (geometryType == GeometryType.POLYGON) { + const endss = []; + let offset = 0; + let prevEndIndex = 0; + for (let i = 0, ii = ends.length; i < ii; ++i) { + const end = ends[i]; + if (!linearRingIsClockwise(flatCoordinates, offset, end, 2)) { + endss.push(ends.slice(prevEndIndex, i)); + prevEndIndex = i; + } + offset = end; + } + if (endss.length > 1) { + geom = new MultiPolygon(flatCoordinates, GeometryLayout.XY, endss); + } else { + geom = new Polygon(flatCoordinates, GeometryLayout.XY, ends); + } + } else { + geom = geometryType === GeometryType.POINT ? new Point(flatCoordinates, GeometryLayout.XY) : + geometryType === GeometryType.LINE_STRING ? new LineString(flatCoordinates, GeometryLayout.XY) : + geometryType === GeometryType.POLYGON ? new Polygon(flatCoordinates, GeometryLayout.XY, ends) : + geometryType === GeometryType.MULTI_POINT ? new MultiPoint(flatCoordinates, GeometryLayout.XY) : + geometryType === GeometryType.MULTI_LINE_STRING ? new MultiLineString(flatCoordinates, GeometryLayout.XY, ends) : + null; + } + feature = new this.featureClass_(); + if (this.geometryName_) { + feature.setGeometryName(this.geometryName_); + } + const geometry = transformWithOptions(geom, false, this.adaptOptions(opt_options)); + feature.setGeometry(geometry); + feature.setId(id); + feature.setProperties(values); + } + + return feature; + } /** - * @private - * @type {string|undefined} + * @inheritDoc + * @api */ - this.geometryName_ = options.geometryName; + getLastExtent() { + return this.extent_; + } /** - * @private - * @type {string} + * @inheritDoc */ - this.layerName_ = options.layerName ? options.layerName : 'layer'; + getType() { + return FormatType.ARRAY_BUFFER; + } /** - * @private - * @type {Array.} + * @inheritDoc + * @api */ - this.layers_ = options.layers ? options.layers : null; + readFeatures(source, opt_options) { + const layers = this.layers_; + + const pbf = new PBF(/** @type {ArrayBuffer} */ (source)); + const pbfLayers = pbf.readFields(layersPBFReader, {}); + /** @type {Array.} */ + const features = []; + for (const name in pbfLayers) { + if (layers && layers.indexOf(name) == -1) { + continue; + } + const pbfLayer = pbfLayers[name]; + + for (let i = 0, ii = pbfLayer.length; i < ii; ++i) { + const rawFeature = readRawFeature(pbf, pbfLayer, i); + features.push(this.createFeature_(pbf, rawFeature)); + } + this.extent_ = pbfLayer ? [0, 0, pbfLayer.extent, pbfLayer.extent] : null; + } + + return features; + } /** - * @private - * @type {module:ol/extent~Extent} + * @inheritDoc + * @api */ - this.extent_ = null; + readProjection(source) { + return this.dataProjection; + } -}; + /** + * Sets the layers that features will be read from. + * @param {Array.} layers Layers. + * @api + */ + setLayers(layers) { + this.layers_ = layers; + } + + /** + * Not implemented. + * @override + */ + readFeature() {} + + /** + * Not implemented. + * @override + */ + readGeometry() {} + + /** + * Not implemented. + * @override + */ + writeFeature() {} + + /** + * Not implemented. + * @override + */ + writeGeometry() {} + + /** + * Not implemented. + * @override + */ + writeFeatures() {} +} inherits(MVT, FeatureFormat); @@ -201,72 +423,6 @@ function readRawFeature(pbf, layer, i) { } -/** - * Read the raw geometry from the pbf offset stored in a raw feature's geometry - * property. - * @suppress {missingProperties} - * @param {Object} pbf PBF. - * @param {Object} feature Raw feature. - * @param {Array.} flatCoordinates Array to store flat coordinates in. - * @param {Array.} ends Array to store ends in. - * @private - */ -MVT.prototype.readRawGeometry_ = function(pbf, feature, flatCoordinates, ends) { - pbf.pos = feature.geometry; - - const end = pbf.readVarint() + pbf.pos; - let cmd = 1; - let length = 0; - let x = 0; - let y = 0; - let coordsLen = 0; - let currentEnd = 0; - - while (pbf.pos < end) { - if (!length) { - const cmdLen = pbf.readVarint(); - cmd = cmdLen & 0x7; - length = cmdLen >> 3; - } - - length--; - - if (cmd === 1 || cmd === 2) { - x += pbf.readSVarint(); - y += pbf.readSVarint(); - - if (cmd === 1) { // moveTo - if (coordsLen > currentEnd) { - ends.push(coordsLen); - currentEnd = coordsLen; - } - } - - flatCoordinates.push(x, y); - coordsLen += 2; - - } else if (cmd === 7) { - - if (coordsLen > currentEnd) { - // close polygon - flatCoordinates.push( - flatCoordinates[currentEnd], flatCoordinates[currentEnd + 1]); - coordsLen += 2; - } - - } else { - assert(false, 59); // Invalid command found in the PBF - } - } - - if (coordsLen > currentEnd) { - ends.push(coordsLen); - currentEnd = coordsLen; - } - -}; - - /** * @suppress {missingProperties} * @param {number} type The raw feature's geometry type @@ -292,168 +448,4 @@ function getGeometryType(type, numEnds) { return geometryType; } -/** - * @private - * @param {Object} pbf PBF - * @param {Object} rawFeature Raw Mapbox feature. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @return {module:ol/Feature|module:ol/render/Feature} Feature. - */ -MVT.prototype.createFeature_ = function(pbf, rawFeature, opt_options) { - const type = rawFeature.type; - if (type === 0) { - return null; - } - - let feature; - const id = rawFeature.id; - const values = rawFeature.properties; - values[this.layerName_] = rawFeature.layer.name; - - const flatCoordinates = []; - const ends = []; - this.readRawGeometry_(pbf, rawFeature, flatCoordinates, ends); - - const geometryType = getGeometryType(type, ends.length); - - if (this.featureClass_ === RenderFeature) { - feature = new this.featureClass_(geometryType, flatCoordinates, ends, values, id); - } else { - let geom; - if (geometryType == GeometryType.POLYGON) { - const endss = []; - let offset = 0; - let prevEndIndex = 0; - for (let i = 0, ii = ends.length; i < ii; ++i) { - const end = ends[i]; - if (!linearRingIsClockwise(flatCoordinates, offset, end, 2)) { - endss.push(ends.slice(prevEndIndex, i)); - prevEndIndex = i; - } - offset = end; - } - if (endss.length > 1) { - geom = new MultiPolygon(flatCoordinates, GeometryLayout.XY, endss); - } else { - geom = new Polygon(flatCoordinates, GeometryLayout.XY, ends); - } - } else { - geom = geometryType === GeometryType.POINT ? new Point(flatCoordinates, GeometryLayout.XY) : - geometryType === GeometryType.LINE_STRING ? new LineString(flatCoordinates, GeometryLayout.XY) : - geometryType === GeometryType.POLYGON ? new Polygon(flatCoordinates, GeometryLayout.XY, ends) : - geometryType === GeometryType.MULTI_POINT ? new MultiPoint(flatCoordinates, GeometryLayout.XY) : - geometryType === GeometryType.MULTI_LINE_STRING ? new MultiLineString(flatCoordinates, GeometryLayout.XY, ends) : - null; - } - feature = new this.featureClass_(); - if (this.geometryName_) { - feature.setGeometryName(this.geometryName_); - } - const geometry = transformWithOptions(geom, false, this.adaptOptions(opt_options)); - feature.setGeometry(geometry); - feature.setId(id); - feature.setProperties(values); - } - - return feature; -}; - - -/** - * @inheritDoc - * @api - */ -MVT.prototype.getLastExtent = function() { - return this.extent_; -}; - - -/** - * @inheritDoc - */ -MVT.prototype.getType = function() { - return FormatType.ARRAY_BUFFER; -}; - - -/** - * @inheritDoc - * @api - */ -MVT.prototype.readFeatures = function(source, opt_options) { - const layers = this.layers_; - - const pbf = new PBF(/** @type {ArrayBuffer} */ (source)); - const pbfLayers = pbf.readFields(layersPBFReader, {}); - /** @type {Array.} */ - const features = []; - for (const name in pbfLayers) { - if (layers && layers.indexOf(name) == -1) { - continue; - } - const pbfLayer = pbfLayers[name]; - - for (let i = 0, ii = pbfLayer.length; i < ii; ++i) { - const rawFeature = readRawFeature(pbf, pbfLayer, i); - features.push(this.createFeature_(pbf, rawFeature)); - } - this.extent_ = pbfLayer ? [0, 0, pbfLayer.extent, pbfLayer.extent] : null; - } - - return features; -}; - - -/** - * @inheritDoc - * @api - */ -MVT.prototype.readProjection = function(source) { - return this.dataProjection; -}; - - -/** - * Sets the layers that features will be read from. - * @param {Array.} layers Layers. - * @api - */ -MVT.prototype.setLayers = function(layers) { - this.layers_ = layers; -}; - - -/** - * Not implemented. - * @override - */ -MVT.prototype.readFeature = function() {}; - - -/** - * Not implemented. - * @override - */ -MVT.prototype.readGeometry = function() {}; - - -/** - * Not implemented. - * @override - */ -MVT.prototype.writeFeature = function() {}; - - -/** - * Not implemented. - * @override - */ -MVT.prototype.writeGeometry = function() {}; - - -/** - * Not implemented. - * @override - */ -MVT.prototype.writeFeatures = function() {}; export default MVT; diff --git a/src/ol/format/OSMXML.js b/src/ol/format/OSMXML.js index cb43d9ca99..57cb4d88cb 100644 --- a/src/ol/format/OSMXML.js +++ b/src/ol/format/OSMXML.js @@ -24,14 +24,74 @@ import {pushParseAndPop, makeStructureNS} from '../xml.js'; * @extends {module:ol/format/XMLFeature} * @api */ -const OSMXML = function() { - XMLFeature.call(this); +class OSMXML { + constructor() { + XMLFeature.call(this); + + /** + * @inheritDoc + */ + this.dataProjection = getProjection('EPSG:4326'); + } /** * @inheritDoc */ - this.dataProjection = getProjection('EPSG:4326'); -}; + readFeaturesFromNode(node, opt_options) { + const options = this.getReadOptions(node, opt_options); + if (node.localName == 'osm') { + const state = pushParseAndPop({ + nodes: {}, + ways: [], + features: [] + }, PARSERS, node, [options]); + // parse nodes in ways + for (let j = 0; j < state.ways.length; j++) { + const values = /** @type {Object} */ (state.ways[j]); + /** @type {Array.} */ + const flatCoordinates = []; + for (let i = 0, ii = values.ndrefs.length; i < ii; i++) { + const point = state.nodes[values.ndrefs[i]]; + extend(flatCoordinates, point); + } + let geometry; + if (values.ndrefs[0] == values.ndrefs[values.ndrefs.length - 1]) { + // closed way + geometry = new Polygon(flatCoordinates, GeometryLayout.XY, [flatCoordinates.length]); + } else { + geometry = new LineString(flatCoordinates, GeometryLayout.XY); + } + transformWithOptions(geometry, false, options); + const feature = new Feature(geometry); + feature.setId(values.id); + feature.setProperties(values.tags); + state.features.push(feature); + } + if (state.features) { + return state.features; + } + } + return []; + } + + /** + * Not implemented. + * @inheritDoc + */ + writeFeatureNode(feature, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + writeFeaturesNode(features, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + writeGeometryNode(geometry, opt_options) {} +} inherits(OSMXML, XMLFeature); @@ -152,47 +212,6 @@ function readTag(node, objectStack) { OSMXML.prototype.readFeatures; -/** - * @inheritDoc - */ -OSMXML.prototype.readFeaturesFromNode = function(node, opt_options) { - const options = this.getReadOptions(node, opt_options); - if (node.localName == 'osm') { - const state = pushParseAndPop({ - nodes: {}, - ways: [], - features: [] - }, PARSERS, node, [options]); - // parse nodes in ways - for (let j = 0; j < state.ways.length; j++) { - const values = /** @type {Object} */ (state.ways[j]); - /** @type {Array.} */ - const flatCoordinates = []; - for (let i = 0, ii = values.ndrefs.length; i < ii; i++) { - const point = state.nodes[values.ndrefs[i]]; - extend(flatCoordinates, point); - } - let geometry; - if (values.ndrefs[0] == values.ndrefs[values.ndrefs.length - 1]) { - // closed way - geometry = new Polygon(flatCoordinates, GeometryLayout.XY, [flatCoordinates.length]); - } else { - geometry = new LineString(flatCoordinates, GeometryLayout.XY); - } - transformWithOptions(geometry, false, options); - const feature = new Feature(geometry); - feature.setId(values.id); - feature.setProperties(values.tags); - state.features.push(feature); - } - if (state.features) { - return state.features; - } - } - return []; -}; - - /** * Read the projection from an OSM source. * @@ -204,23 +223,4 @@ OSMXML.prototype.readFeaturesFromNode = function(node, opt_options) { OSMXML.prototype.readProjection; -/** - * Not implemented. - * @inheritDoc - */ -OSMXML.prototype.writeFeatureNode = function(feature, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -OSMXML.prototype.writeFeaturesNode = function(features, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -OSMXML.prototype.writeGeometryNode = function(geometry, opt_options) {}; export default OSMXML; diff --git a/src/ol/format/OWS.js b/src/ol/format/OWS.js index d78b893fdc..f92b7b6559 100644 --- a/src/ol/format/OWS.js +++ b/src/ol/format/OWS.js @@ -11,9 +11,32 @@ import {makeObjectPropertyPusher, makeObjectPropertySetter, makeStructureNS, pus * @constructor * @extends {module:ol/format/XML} */ -const OWS = function() { - XML.call(this); -}; +class OWS { + constructor() { + XML.call(this); + } + + /** + * @inheritDoc + */ + readFromDocument(doc) { + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + return this.readFromNode(n); + } + } + return null; + } + + /** + * @inheritDoc + */ + readFromNode(node) { + const owsObject = pushParseAndPop({}, + PARSERS, node, []); + return owsObject ? owsObject : null; + } +} inherits(OWS, XML); @@ -187,29 +210,6 @@ const SERVICE_PROVIDER_PARSERS = }); -/** - * @inheritDoc - */ -OWS.prototype.readFromDocument = function(doc) { - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - return this.readFromNode(n); - } - } - return null; -}; - - -/** - * @inheritDoc - */ -OWS.prototype.readFromNode = function(node) { - const owsObject = pushParseAndPop({}, - PARSERS, node, []); - return owsObject ? owsObject : null; -}; - - /** * @param {Node} node Node. * @param {Array.<*>} objectStack Object stack. diff --git a/src/ol/format/Polyline.js b/src/ol/format/Polyline.js index ed7267de69..d7f57cee60 100644 --- a/src/ol/format/Polyline.js +++ b/src/ol/format/Polyline.js @@ -32,30 +32,98 @@ import {get as getProjection} from '../proj.js'; * @param {module:ol/format/Polyline~Options=} opt_options Optional configuration object. * @api */ -const Polyline = function(opt_options) { +class Polyline { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - TextFeature.call(this); + TextFeature.call(this); + + /** + * @inheritDoc + */ + this.dataProjection = getProjection('EPSG:4326'); + + /** + * @private + * @type {number} + */ + this.factor_ = options.factor ? options.factor : 1e5; + + /** + * @private + * @type {module:ol/geom/GeometryLayout} + */ + this.geometryLayout_ = options.geometryLayout ? + options.geometryLayout : GeometryLayout.XY; + } /** * @inheritDoc */ - this.dataProjection = getProjection('EPSG:4326'); + readFeatureFromText(text, opt_options) { + const geometry = this.readGeometryFromText(text, opt_options); + return new Feature(geometry); + } /** - * @private - * @type {number} + * @inheritDoc */ - this.factor_ = options.factor ? options.factor : 1e5; + readFeaturesFromText(text, opt_options) { + const feature = this.readFeatureFromText(text, opt_options); + return [feature]; + } /** - * @private - * @type {module:ol/geom/GeometryLayout} + * @inheritDoc */ - this.geometryLayout_ = options.geometryLayout ? - options.geometryLayout : GeometryLayout.XY; -}; + readGeometryFromText(text, opt_options) { + const stride = getStrideForLayout(this.geometryLayout_); + const flatCoordinates = decodeDeltas(text, stride, this.factor_); + flipXY(flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates); + const coordinates = inflateCoordinates(flatCoordinates, 0, flatCoordinates.length, stride); + + return ( + /** @type {module:ol/geom/Geometry} */ (transformWithOptions( + new LineString(coordinates, this.geometryLayout_), + false, + this.adaptOptions(opt_options) + )) + ); + } + + /** + * @inheritDoc + */ + writeFeatureText(feature, opt_options) { + const geometry = feature.getGeometry(); + if (geometry) { + return this.writeGeometryText(geometry, opt_options); + } else { + assert(false, 40); // Expected `feature` to have a geometry + return ''; + } + } + + /** + * @inheritDoc + */ + writeFeaturesText(features, opt_options) { + return this.writeFeatureText(features[0], opt_options); + } + + /** + * @inheritDoc + */ + writeGeometryText(geometry, opt_options) { + geometry = /** @type {module:ol/geom/LineString} */ + (transformWithOptions(geometry, true, this.adaptOptions(opt_options))); + const flatCoordinates = geometry.getFlatCoordinates(); + const stride = geometry.getStride(); + flipXY(flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates); + return encodeDeltas(flatCoordinates, stride, this.factor_); + } +} inherits(Polyline, TextFeature); @@ -277,15 +345,6 @@ export function encodeUnsignedInteger(num) { Polyline.prototype.readFeature; -/** - * @inheritDoc - */ -Polyline.prototype.readFeatureFromText = function(text, opt_options) { - const geometry = this.readGeometryFromText(text, opt_options); - return new Feature(geometry); -}; - - /** * Read the feature from the source. As Polyline sources contain a single * feature, this will return the feature in an array. @@ -299,15 +358,6 @@ Polyline.prototype.readFeatureFromText = function(text, opt_options) { Polyline.prototype.readFeatures; -/** - * @inheritDoc - */ -Polyline.prototype.readFeaturesFromText = function(text, opt_options) { - const feature = this.readFeatureFromText(text, opt_options); - return [feature]; -}; - - /** * Read the geometry from the source. * @@ -320,25 +370,6 @@ Polyline.prototype.readFeaturesFromText = function(text, opt_options) { Polyline.prototype.readGeometry; -/** - * @inheritDoc - */ -Polyline.prototype.readGeometryFromText = function(text, opt_options) { - const stride = getStrideForLayout(this.geometryLayout_); - const flatCoordinates = decodeDeltas(text, stride, this.factor_); - flipXY(flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates); - const coordinates = inflateCoordinates(flatCoordinates, 0, flatCoordinates.length, stride); - - return ( - /** @type {module:ol/geom/Geometry} */ (transformWithOptions( - new LineString(coordinates, this.geometryLayout_), - false, - this.adaptOptions(opt_options) - )) - ); -}; - - /** * Read the projection from a Polyline source. * @@ -350,28 +381,6 @@ Polyline.prototype.readGeometryFromText = function(text, opt_options) { Polyline.prototype.readProjection; -/** - * @inheritDoc - */ -Polyline.prototype.writeFeatureText = function(feature, opt_options) { - const geometry = feature.getGeometry(); - if (geometry) { - return this.writeGeometryText(geometry, opt_options); - } else { - assert(false, 40); // Expected `feature` to have a geometry - return ''; - } -}; - - -/** - * @inheritDoc - */ -Polyline.prototype.writeFeaturesText = function(features, opt_options) { - return this.writeFeatureText(features[0], opt_options); -}; - - /** * Write a single geometry in Polyline format. * @@ -384,15 +393,4 @@ Polyline.prototype.writeFeaturesText = function(features, opt_options) { Polyline.prototype.writeGeometry; -/** - * @inheritDoc - */ -Polyline.prototype.writeGeometryText = function(geometry, opt_options) { - geometry = /** @type {module:ol/geom/LineString} */ - (transformWithOptions(geometry, true, this.adaptOptions(opt_options))); - const flatCoordinates = geometry.getFlatCoordinates(); - const stride = geometry.getStride(); - flipXY(flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates); - return encodeDeltas(flatCoordinates, stride, this.factor_); -}; export default Polyline; diff --git a/src/ol/format/TextFeature.js b/src/ol/format/TextFeature.js index 33d6eac33d..801becd8b4 100644 --- a/src/ol/format/TextFeature.js +++ b/src/ol/format/TextFeature.js @@ -15,9 +15,130 @@ import FormatType from '../format/FormatType.js'; * @abstract * @extends {module:ol/format/Feature} */ -const TextFeature = function() { - FeatureFormat.call(this); -}; +class TextFeature { + constructor() { + FeatureFormat.call(this); + } + + /** + * @inheritDoc + */ + getType() { + return FormatType.TEXT; + } + + /** + * @inheritDoc + */ + readFeature(source, opt_options) { + return this.readFeatureFromText(getText(source), this.adaptOptions(opt_options)); + } + + /** + * @abstract + * @param {string} text Text. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @protected + * @return {module:ol/Feature} Feature. + */ + readFeatureFromText(text, opt_options) {} + + /** + * @inheritDoc + */ + readFeatures(source, opt_options) { + return this.readFeaturesFromText(getText(source), this.adaptOptions(opt_options)); + } + + /** + * @abstract + * @param {string} text Text. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @protected + * @return {Array.} Features. + */ + readFeaturesFromText(text, opt_options) {} + + /** + * @inheritDoc + */ + readGeometry(source, opt_options) { + return this.readGeometryFromText(getText(source), this.adaptOptions(opt_options)); + } + + /** + * @abstract + * @param {string} text Text. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. + * @protected + * @return {module:ol/geom/Geometry} Geometry. + */ + readGeometryFromText(text, opt_options) {} + + /** + * @inheritDoc + */ + readProjection(source) { + return this.readProjectionFromText(getText(source)); + } + + /** + * @param {string} text Text. + * @protected + * @return {module:ol/proj/Projection} Projection. + */ + readProjectionFromText(text) { + return this.dataProjection; + } + + /** + * @inheritDoc + */ + writeFeature(feature, opt_options) { + return this.writeFeatureText(feature, this.adaptOptions(opt_options)); + } + + /** + * @abstract + * @param {module:ol/Feature} feature Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @protected + * @return {string} Text. + */ + writeFeatureText(feature, opt_options) {} + + /** + * @inheritDoc + */ + writeFeatures(features, opt_options) { + return this.writeFeaturesText(features, this.adaptOptions(opt_options)); + } + + /** + * @abstract + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @protected + * @return {string} Text. + */ + writeFeaturesText(features, opt_options) {} + + /** + * @inheritDoc + */ + writeGeometry(geometry, opt_options) { + return this.writeGeometryText(geometry, this.adaptOptions(opt_options)); + } + + /** + * @abstract + * @param {module:ol/geom/Geometry} geometry Geometry. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. + * @protected + * @return {string} Text. + */ + writeGeometryText(geometry, opt_options) {} +} inherits(TextFeature, FeatureFormat); @@ -35,136 +156,4 @@ function getText(source) { } -/** - * @inheritDoc - */ -TextFeature.prototype.getType = function() { - return FormatType.TEXT; -}; - - -/** - * @inheritDoc - */ -TextFeature.prototype.readFeature = function(source, opt_options) { - return this.readFeatureFromText(getText(source), this.adaptOptions(opt_options)); -}; - - -/** - * @abstract - * @param {string} text Text. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @protected - * @return {module:ol/Feature} Feature. - */ -TextFeature.prototype.readFeatureFromText = function(text, opt_options) {}; - - -/** - * @inheritDoc - */ -TextFeature.prototype.readFeatures = function(source, opt_options) { - return this.readFeaturesFromText(getText(source), this.adaptOptions(opt_options)); -}; - - -/** - * @abstract - * @param {string} text Text. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @protected - * @return {Array.} Features. - */ -TextFeature.prototype.readFeaturesFromText = function(text, opt_options) {}; - - -/** - * @inheritDoc - */ -TextFeature.prototype.readGeometry = function(source, opt_options) { - return this.readGeometryFromText(getText(source), this.adaptOptions(opt_options)); -}; - - -/** - * @abstract - * @param {string} text Text. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Read options. - * @protected - * @return {module:ol/geom/Geometry} Geometry. - */ -TextFeature.prototype.readGeometryFromText = function(text, opt_options) {}; - - -/** - * @inheritDoc - */ -TextFeature.prototype.readProjection = function(source) { - return this.readProjectionFromText(getText(source)); -}; - - -/** - * @param {string} text Text. - * @protected - * @return {module:ol/proj/Projection} Projection. - */ -TextFeature.prototype.readProjectionFromText = function(text) { - return this.dataProjection; -}; - - -/** - * @inheritDoc - */ -TextFeature.prototype.writeFeature = function(feature, opt_options) { - return this.writeFeatureText(feature, this.adaptOptions(opt_options)); -}; - - -/** - * @abstract - * @param {module:ol/Feature} feature Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @protected - * @return {string} Text. - */ -TextFeature.prototype.writeFeatureText = function(feature, opt_options) {}; - - -/** - * @inheritDoc - */ -TextFeature.prototype.writeFeatures = function(features, opt_options) { - return this.writeFeaturesText(features, this.adaptOptions(opt_options)); -}; - - -/** - * @abstract - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @protected - * @return {string} Text. - */ -TextFeature.prototype.writeFeaturesText = function(features, opt_options) {}; - - -/** - * @inheritDoc - */ -TextFeature.prototype.writeGeometry = function(geometry, opt_options) { - return this.writeGeometryText(geometry, this.adaptOptions(opt_options)); -}; - - -/** - * @abstract - * @param {module:ol/geom/Geometry} geometry Geometry. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Write options. - * @protected - * @return {string} Text. - */ -TextFeature.prototype.writeGeometryText = function(geometry, opt_options) {}; export default TextFeature; diff --git a/src/ol/format/TopoJSON.js b/src/ol/format/TopoJSON.js index b2b761e524..5ed2121b70 100644 --- a/src/ol/format/TopoJSON.js +++ b/src/ol/format/TopoJSON.js @@ -48,32 +48,112 @@ import {get as getProjection} from '../proj.js'; * @param {module:ol/format/TopoJSON~Options=} opt_options Options. * @api */ -const TopoJSON = function(opt_options) { +class TopoJSON { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - JSONFeature.call(this); + JSONFeature.call(this); - /** - * @private - * @type {string|undefined} - */ - this.layerName_ = options.layerName; + /** + * @private + * @type {string|undefined} + */ + this.layerName_ = options.layerName; - /** - * @private - * @type {Array.} - */ - this.layers_ = options.layers ? options.layers : null; + /** + * @private + * @type {Array.} + */ + this.layers_ = options.layers ? options.layers : null; + + /** + * @inheritDoc + */ + this.dataProjection = getProjection( + options.dataProjection ? + options.dataProjection : 'EPSG:4326'); + + } /** * @inheritDoc */ - this.dataProjection = getProjection( - options.dataProjection ? - options.dataProjection : 'EPSG:4326'); + readFeaturesFromObject(object, opt_options) { + if (object.type == 'Topology') { + const topoJSONTopology = /** @type {TopoJSONTopology} */ (object); + let transform, scale = null, translate = null; + if (topoJSONTopology.transform) { + transform = topoJSONTopology.transform; + scale = transform.scale; + translate = transform.translate; + } + const arcs = topoJSONTopology.arcs; + if (transform) { + transformArcs(arcs, scale, translate); + } + /** @type {Array.} */ + const features = []; + const topoJSONFeatures = topoJSONTopology.objects; + const property = this.layerName_; + let feature; + for (const objectName in topoJSONFeatures) { + if (this.layers_ && this.layers_.indexOf(objectName) == -1) { + continue; + } + if (topoJSONFeatures[objectName].type === 'GeometryCollection') { + feature = /** @type {TopoJSONGeometryCollection} */ (topoJSONFeatures[objectName]); + features.push.apply(features, readFeaturesFromGeometryCollection( + feature, arcs, scale, translate, property, objectName, opt_options)); + } else { + feature = /** @type {TopoJSONGeometry} */ (topoJSONFeatures[objectName]); + features.push(readFeatureFromGeometry( + feature, arcs, scale, translate, property, objectName, opt_options)); + } + } + return features; + } else { + return []; + } + } -}; + /** + * @inheritDoc + */ + readProjectionFromObject(object) { + return this.dataProjection; + } + + /** + * Not implemented. + * @inheritDoc + */ + writeFeatureObject(feature, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + writeFeaturesObject(features, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + writeGeometryObject(geometry, opt_options) {} + + /** + * Not implemented. + * @override + */ + readGeometryFromObject() {} + + /** + * Not implemented. + * @override + */ + readFeatureFromObject() {} +} inherits(TopoJSON, JSONFeature); @@ -309,48 +389,6 @@ function readFeatureFromGeometry(object, arcs, scale, translate, property, name, TopoJSON.prototype.readFeatures; -/** - * @inheritDoc - */ -TopoJSON.prototype.readFeaturesFromObject = function(object, opt_options) { - if (object.type == 'Topology') { - const topoJSONTopology = /** @type {TopoJSONTopology} */ (object); - let transform, scale = null, translate = null; - if (topoJSONTopology.transform) { - transform = topoJSONTopology.transform; - scale = transform.scale; - translate = transform.translate; - } - const arcs = topoJSONTopology.arcs; - if (transform) { - transformArcs(arcs, scale, translate); - } - /** @type {Array.} */ - const features = []; - const topoJSONFeatures = topoJSONTopology.objects; - const property = this.layerName_; - let feature; - for (const objectName in topoJSONFeatures) { - if (this.layers_ && this.layers_.indexOf(objectName) == -1) { - continue; - } - if (topoJSONFeatures[objectName].type === 'GeometryCollection') { - feature = /** @type {TopoJSONGeometryCollection} */ (topoJSONFeatures[objectName]); - features.push.apply(features, readFeaturesFromGeometryCollection( - feature, arcs, scale, translate, property, objectName, opt_options)); - } else { - feature = /** @type {TopoJSONGeometry} */ (topoJSONFeatures[objectName]); - features.push(readFeatureFromGeometry( - feature, arcs, scale, translate, property, objectName, opt_options)); - } - } - return features; - } else { - return []; - } -}; - - /** * Apply a linear transform to array of arcs. The provided array of arcs is * modified in place. @@ -412,45 +450,4 @@ function transformVertex(vertex, scale, translate) { TopoJSON.prototype.readProjection; -/** - * @inheritDoc - */ -TopoJSON.prototype.readProjectionFromObject = function(object) { - return this.dataProjection; -}; - - -/** - * Not implemented. - * @inheritDoc - */ -TopoJSON.prototype.writeFeatureObject = function(feature, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -TopoJSON.prototype.writeFeaturesObject = function(features, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -TopoJSON.prototype.writeGeometryObject = function(geometry, opt_options) {}; - - -/** - * Not implemented. - * @override - */ -TopoJSON.prototype.readGeometryFromObject = function() {}; - - -/** - * Not implemented. - * @override - */ -TopoJSON.prototype.readFeatureFromObject = function() {}; export default TopoJSON; diff --git a/src/ol/format/WFS.js b/src/ol/format/WFS.js index 0e6c27f86b..045a483317 100644 --- a/src/ol/format/WFS.js +++ b/src/ol/format/WFS.js @@ -143,57 +143,338 @@ const DEFAULT_VERSION = '1.1.0'; * @extends {module:ol/format/XMLFeature} * @api */ -const WFS = function(opt_options) { - const options = opt_options ? opt_options : {}; +class WFS { + constructor(opt_options) { + const options = opt_options ? opt_options : {}; + + /** + * @private + * @type {Array.|string|undefined} + */ + this.featureType_ = options.featureType; + + /** + * @private + * @type {Object.|string|undefined} + */ + this.featureNS_ = options.featureNS; + + /** + * @private + * @type {module:ol/format/GMLBase} + */ + this.gmlFormat_ = options.gmlFormat ? + options.gmlFormat : new GML3(); + + /** + * @private + * @type {string} + */ + this.schemaLocation_ = options.schemaLocation ? + options.schemaLocation : SCHEMA_LOCATIONS[DEFAULT_VERSION]; + + XMLFeature.call(this); + } /** - * @private - * @type {Array.|string|undefined} + * @return {Array.|string|undefined} featureType */ - this.featureType_ = options.featureType; + getFeatureType() { + return this.featureType_; + } /** - * @private - * @type {Object.|string|undefined} + * @param {Array.|string|undefined} featureType Feature type(s) to parse. */ - this.featureNS_ = options.featureNS; + setFeatureType(featureType) { + this.featureType_ = featureType; + } /** - * @private - * @type {module:ol/format/GMLBase} + * @inheritDoc */ - this.gmlFormat_ = options.gmlFormat ? - options.gmlFormat : new GML3(); + readFeaturesFromNode(node, opt_options) { + const context = /** @type {module:ol/xml~NodeStackItem} */ ({ + 'featureType': this.featureType_, + 'featureNS': this.featureNS_ + }); + assign(context, this.getReadOptions(node, opt_options ? opt_options : {})); + const objectStack = [context]; + this.gmlFormat_.FEATURE_COLLECTION_PARSERS[GMLNS][ + 'featureMember'] = + makeArrayPusher(GMLBase.prototype.readFeaturesInternal); + let features = pushParseAndPop([], + this.gmlFormat_.FEATURE_COLLECTION_PARSERS, node, + objectStack, this.gmlFormat_); + if (!features) { + features = []; + } + return features; + } /** - * @private - * @type {string} + * Read transaction response of the source. + * + * @param {Document|Node|Object|string} source Source. + * @return {module:ol/format/WFS~TransactionResponse|undefined} Transaction response. + * @api */ - this.schemaLocation_ = options.schemaLocation ? - options.schemaLocation : SCHEMA_LOCATIONS[DEFAULT_VERSION]; + readTransactionResponse(source) { + if (isDocument(source)) { + return this.readTransactionResponseFromDocument( + /** @type {Document} */ (source)); + } else if (isNode(source)) { + return this.readTransactionResponseFromNode(/** @type {Node} */ (source)); + } else if (typeof source === 'string') { + const doc = parse(source); + return this.readTransactionResponseFromDocument(doc); + } else { + return undefined; + } + } - XMLFeature.call(this); -}; + /** + * Read feature collection metadata of the source. + * + * @param {Document|Node|Object|string} source Source. + * @return {module:ol/format/WFS~FeatureCollectionMetadata|undefined} + * FeatureCollection metadata. + * @api + */ + readFeatureCollectionMetadata(source) { + if (isDocument(source)) { + return this.readFeatureCollectionMetadataFromDocument( + /** @type {Document} */ (source)); + } else if (isNode(source)) { + return this.readFeatureCollectionMetadataFromNode( + /** @type {Node} */ (source)); + } else if (typeof source === 'string') { + const doc = parse(source); + return this.readFeatureCollectionMetadataFromDocument(doc); + } else { + return undefined; + } + } + + /** + * @param {Document} doc Document. + * @return {module:ol/format/WFS~FeatureCollectionMetadata|undefined} + * FeatureCollection metadata. + */ + readFeatureCollectionMetadataFromDocument(doc) { + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + return this.readFeatureCollectionMetadataFromNode(n); + } + } + return undefined; + } + + /** + * @param {Node} node Node. + * @return {module:ol/format/WFS~FeatureCollectionMetadata|undefined} + * FeatureCollection metadata. + */ + readFeatureCollectionMetadataFromNode(node) { + const result = {}; + const value = readNonNegativeIntegerString( + node.getAttribute('numberOfFeatures')); + result['numberOfFeatures'] = value; + return pushParseAndPop( + /** @type {module:ol/format/WFS~FeatureCollectionMetadata} */ (result), + FEATURE_COLLECTION_PARSERS, node, [], this.gmlFormat_); + } + + /** + * @param {Document} doc Document. + * @return {module:ol/format/WFS~TransactionResponse|undefined} Transaction response. + */ + readTransactionResponseFromDocument(doc) { + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + return this.readTransactionResponseFromNode(n); + } + } + return undefined; + } + + /** + * @param {Node} node Node. + * @return {module:ol/format/WFS~TransactionResponse|undefined} Transaction response. + */ + readTransactionResponseFromNode(node) { + return pushParseAndPop( + /** @type {module:ol/format/WFS~TransactionResponse} */({}), + TRANSACTION_RESPONSE_PARSERS, node, []); + } + + /** + * Encode format as WFS `GetFeature` and return the Node. + * + * @param {module:ol/format/WFS~WriteGetFeatureOptions} options Options. + * @return {Node} Result. + * @api + */ + writeGetFeature(options) { + const node = createElementNS(WFSNS, 'GetFeature'); + node.setAttribute('service', 'WFS'); + node.setAttribute('version', '1.1.0'); + let filter; + if (options) { + if (options.handle) { + node.setAttribute('handle', options.handle); + } + if (options.outputFormat) { + node.setAttribute('outputFormat', options.outputFormat); + } + if (options.maxFeatures !== undefined) { + node.setAttribute('maxFeatures', options.maxFeatures); + } + if (options.resultType) { + node.setAttribute('resultType', options.resultType); + } + if (options.startIndex !== undefined) { + node.setAttribute('startIndex', options.startIndex); + } + if (options.count !== undefined) { + node.setAttribute('count', options.count); + } + filter = options.filter; + if (options.bbox) { + assert(options.geometryName, + 12); // `options.geometryName` must also be provided when `options.bbox` is set + const bbox = bboxFilter( + /** @type {string} */ (options.geometryName), options.bbox, options.srsName); + if (filter) { + // if bbox and filter are both set, combine the two into a single filter + filter = andFilter(filter, bbox); + } else { + filter = bbox; + } + } + } + node.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', this.schemaLocation_); + /** @type {module:ol/xml~NodeStackItem} */ + const context = { + node: node, + 'srsName': options.srsName, + 'featureNS': options.featureNS ? options.featureNS : this.featureNS_, + 'featurePrefix': options.featurePrefix, + 'geometryName': options.geometryName, + 'filter': filter, + 'propertyNames': options.propertyNames ? options.propertyNames : [] + }; + assert(Array.isArray(options.featureTypes), + 11); // `options.featureTypes` should be an Array + writeGetFeature(node, /** @type {!Array.} */ (options.featureTypes), [context]); + return node; + } + + /** + * Encode format as WFS `Transaction` and return the Node. + * + * @param {Array.} inserts The features to insert. + * @param {Array.} updates The features to update. + * @param {Array.} deletes The features to delete. + * @param {module:ol/format/WFS~WriteTransactionOptions} options Write options. + * @return {Node} Result. + * @api + */ + writeTransaction(inserts, updates, deletes, options) { + const objectStack = []; + const node = createElementNS(WFSNS, 'Transaction'); + const version = options.version ? options.version : DEFAULT_VERSION; + const gmlVersion = version === '1.0.0' ? 2 : 3; + node.setAttribute('service', 'WFS'); + node.setAttribute('version', version); + let baseObj; + /** @type {module:ol/xml~NodeStackItem} */ + let obj; + if (options) { + baseObj = options.gmlOptions ? options.gmlOptions : {}; + if (options.handle) { + node.setAttribute('handle', options.handle); + } + } + const schemaLocation = SCHEMA_LOCATIONS[version]; + node.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', schemaLocation); + const featurePrefix = options.featurePrefix ? options.featurePrefix : FEATURE_PREFIX; + if (inserts) { + obj = {node: node, 'featureNS': options.featureNS, + 'featureType': options.featureType, 'featurePrefix': featurePrefix, + 'gmlVersion': gmlVersion, 'hasZ': options.hasZ, 'srsName': options.srsName}; + assign(obj, baseObj); + pushSerializeAndPop(obj, + TRANSACTION_SERIALIZERS, + makeSimpleNodeFactory('Insert'), inserts, + objectStack); + } + if (updates) { + obj = {node: node, 'featureNS': options.featureNS, + 'featureType': options.featureType, 'featurePrefix': featurePrefix, + 'gmlVersion': gmlVersion, 'hasZ': options.hasZ, 'srsName': options.srsName}; + assign(obj, baseObj); + pushSerializeAndPop(obj, + TRANSACTION_SERIALIZERS, + makeSimpleNodeFactory('Update'), updates, + objectStack); + } + if (deletes) { + pushSerializeAndPop({node: node, 'featureNS': options.featureNS, + 'featureType': options.featureType, 'featurePrefix': featurePrefix, + 'gmlVersion': gmlVersion, 'srsName': options.srsName}, + TRANSACTION_SERIALIZERS, + makeSimpleNodeFactory('Delete'), deletes, + objectStack); + } + if (options.nativeElements) { + pushSerializeAndPop({node: node, 'featureNS': options.featureNS, + 'featureType': options.featureType, 'featurePrefix': featurePrefix, + 'gmlVersion': gmlVersion, 'srsName': options.srsName}, + TRANSACTION_SERIALIZERS, + makeSimpleNodeFactory('Native'), options.nativeElements, + objectStack); + } + return node; + } + + /** + * @inheritDoc + */ + readProjectionFromDocument(doc) { + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + return this.readProjectionFromNode(n); + } + } + return null; + } + + /** + * @inheritDoc + */ + readProjectionFromNode(node) { + if (node.firstElementChild && + node.firstElementChild.firstElementChild) { + node = node.firstElementChild.firstElementChild; + for (let n = node.firstElementChild; n; n = n.nextElementSibling) { + if (!(n.childNodes.length === 0 || + (n.childNodes.length === 1 && + n.firstChild.nodeType === 3))) { + const objectStack = [{}]; + this.gmlFormat_.readGeometryElement(n, objectStack); + return getProjection(objectStack.pop().srsName); + } + } + } + + return null; + } +} inherits(WFS, XMLFeature); -/** - * @return {Array.|string|undefined} featureType - */ -WFS.prototype.getFeatureType = function() { - return this.featureType_; -}; - - -/** - * @param {Array.|string|undefined} featureType Feature type(s) to parse. - */ -WFS.prototype.setFeatureType = function(featureType) { - this.featureType_ = featureType; -}; - - /** * Read all features from a WFS FeatureCollection. * @@ -206,90 +487,6 @@ WFS.prototype.setFeatureType = function(featureType) { WFS.prototype.readFeatures; -/** - * @inheritDoc - */ -WFS.prototype.readFeaturesFromNode = function(node, opt_options) { - const context = /** @type {module:ol/xml~NodeStackItem} */ ({ - 'featureType': this.featureType_, - 'featureNS': this.featureNS_ - }); - assign(context, this.getReadOptions(node, opt_options ? opt_options : {})); - const objectStack = [context]; - this.gmlFormat_.FEATURE_COLLECTION_PARSERS[GMLNS][ - 'featureMember'] = - makeArrayPusher(GMLBase.prototype.readFeaturesInternal); - let features = pushParseAndPop([], - this.gmlFormat_.FEATURE_COLLECTION_PARSERS, node, - objectStack, this.gmlFormat_); - if (!features) { - features = []; - } - return features; -}; - - -/** - * Read transaction response of the source. - * - * @param {Document|Node|Object|string} source Source. - * @return {module:ol/format/WFS~TransactionResponse|undefined} Transaction response. - * @api - */ -WFS.prototype.readTransactionResponse = function(source) { - if (isDocument(source)) { - return this.readTransactionResponseFromDocument( - /** @type {Document} */ (source)); - } else if (isNode(source)) { - return this.readTransactionResponseFromNode(/** @type {Node} */ (source)); - } else if (typeof source === 'string') { - const doc = parse(source); - return this.readTransactionResponseFromDocument(doc); - } else { - return undefined; - } -}; - - -/** - * Read feature collection metadata of the source. - * - * @param {Document|Node|Object|string} source Source. - * @return {module:ol/format/WFS~FeatureCollectionMetadata|undefined} - * FeatureCollection metadata. - * @api - */ -WFS.prototype.readFeatureCollectionMetadata = function(source) { - if (isDocument(source)) { - return this.readFeatureCollectionMetadataFromDocument( - /** @type {Document} */ (source)); - } else if (isNode(source)) { - return this.readFeatureCollectionMetadataFromNode( - /** @type {Node} */ (source)); - } else if (typeof source === 'string') { - const doc = parse(source); - return this.readFeatureCollectionMetadataFromDocument(doc); - } else { - return undefined; - } -}; - - -/** - * @param {Document} doc Document. - * @return {module:ol/format/WFS~FeatureCollectionMetadata|undefined} - * FeatureCollection metadata. - */ -WFS.prototype.readFeatureCollectionMetadataFromDocument = function(doc) { - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - return this.readFeatureCollectionMetadataFromNode(n); - } - } - return undefined; -}; - - /** * @const * @type {Object.>} @@ -302,22 +499,6 @@ const FEATURE_COLLECTION_PARSERS = { }; -/** - * @param {Node} node Node. - * @return {module:ol/format/WFS~FeatureCollectionMetadata|undefined} - * FeatureCollection metadata. - */ -WFS.prototype.readFeatureCollectionMetadataFromNode = function(node) { - const result = {}; - const value = readNonNegativeIntegerString( - node.getAttribute('numberOfFeatures')); - result['numberOfFeatures'] = value; - return pushParseAndPop( - /** @type {module:ol/format/WFS~FeatureCollectionMetadata} */ (result), - FEATURE_COLLECTION_PARSERS, node, [], this.gmlFormat_); -}; - - /** * @const * @type {Object.>} @@ -400,31 +581,6 @@ const TRANSACTION_RESPONSE_PARSERS = { }; -/** - * @param {Document} doc Document. - * @return {module:ol/format/WFS~TransactionResponse|undefined} Transaction response. - */ -WFS.prototype.readTransactionResponseFromDocument = function(doc) { - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - return this.readTransactionResponseFromNode(n); - } - } - return undefined; -}; - - -/** - * @param {Node} node Node. - * @return {module:ol/format/WFS~TransactionResponse|undefined} Transaction response. - */ -WFS.prototype.readTransactionResponseFromNode = function(node) { - return pushParseAndPop( - /** @type {module:ol/format/WFS~TransactionResponse} */({}), - TRANSACTION_RESPONSE_PARSERS, node, []); -}; - - /** * @type {Object.>} */ @@ -942,138 +1098,6 @@ function writeGetFeature(node, featureTypes, objectStack) { } -/** - * Encode format as WFS `GetFeature` and return the Node. - * - * @param {module:ol/format/WFS~WriteGetFeatureOptions} options Options. - * @return {Node} Result. - * @api - */ -WFS.prototype.writeGetFeature = function(options) { - const node = createElementNS(WFSNS, 'GetFeature'); - node.setAttribute('service', 'WFS'); - node.setAttribute('version', '1.1.0'); - let filter; - if (options) { - if (options.handle) { - node.setAttribute('handle', options.handle); - } - if (options.outputFormat) { - node.setAttribute('outputFormat', options.outputFormat); - } - if (options.maxFeatures !== undefined) { - node.setAttribute('maxFeatures', options.maxFeatures); - } - if (options.resultType) { - node.setAttribute('resultType', options.resultType); - } - if (options.startIndex !== undefined) { - node.setAttribute('startIndex', options.startIndex); - } - if (options.count !== undefined) { - node.setAttribute('count', options.count); - } - filter = options.filter; - if (options.bbox) { - assert(options.geometryName, - 12); // `options.geometryName` must also be provided when `options.bbox` is set - const bbox = bboxFilter( - /** @type {string} */ (options.geometryName), options.bbox, options.srsName); - if (filter) { - // if bbox and filter are both set, combine the two into a single filter - filter = andFilter(filter, bbox); - } else { - filter = bbox; - } - } - } - node.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', this.schemaLocation_); - /** @type {module:ol/xml~NodeStackItem} */ - const context = { - node: node, - 'srsName': options.srsName, - 'featureNS': options.featureNS ? options.featureNS : this.featureNS_, - 'featurePrefix': options.featurePrefix, - 'geometryName': options.geometryName, - 'filter': filter, - 'propertyNames': options.propertyNames ? options.propertyNames : [] - }; - assert(Array.isArray(options.featureTypes), - 11); // `options.featureTypes` should be an Array - writeGetFeature(node, /** @type {!Array.} */ (options.featureTypes), [context]); - return node; -}; - - -/** - * Encode format as WFS `Transaction` and return the Node. - * - * @param {Array.} inserts The features to insert. - * @param {Array.} updates The features to update. - * @param {Array.} deletes The features to delete. - * @param {module:ol/format/WFS~WriteTransactionOptions} options Write options. - * @return {Node} Result. - * @api - */ -WFS.prototype.writeTransaction = function(inserts, updates, deletes, options) { - const objectStack = []; - const node = createElementNS(WFSNS, 'Transaction'); - const version = options.version ? options.version : DEFAULT_VERSION; - const gmlVersion = version === '1.0.0' ? 2 : 3; - node.setAttribute('service', 'WFS'); - node.setAttribute('version', version); - let baseObj; - /** @type {module:ol/xml~NodeStackItem} */ - let obj; - if (options) { - baseObj = options.gmlOptions ? options.gmlOptions : {}; - if (options.handle) { - node.setAttribute('handle', options.handle); - } - } - const schemaLocation = SCHEMA_LOCATIONS[version]; - node.setAttributeNS(XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', schemaLocation); - const featurePrefix = options.featurePrefix ? options.featurePrefix : FEATURE_PREFIX; - if (inserts) { - obj = {node: node, 'featureNS': options.featureNS, - 'featureType': options.featureType, 'featurePrefix': featurePrefix, - 'gmlVersion': gmlVersion, 'hasZ': options.hasZ, 'srsName': options.srsName}; - assign(obj, baseObj); - pushSerializeAndPop(obj, - TRANSACTION_SERIALIZERS, - makeSimpleNodeFactory('Insert'), inserts, - objectStack); - } - if (updates) { - obj = {node: node, 'featureNS': options.featureNS, - 'featureType': options.featureType, 'featurePrefix': featurePrefix, - 'gmlVersion': gmlVersion, 'hasZ': options.hasZ, 'srsName': options.srsName}; - assign(obj, baseObj); - pushSerializeAndPop(obj, - TRANSACTION_SERIALIZERS, - makeSimpleNodeFactory('Update'), updates, - objectStack); - } - if (deletes) { - pushSerializeAndPop({node: node, 'featureNS': options.featureNS, - 'featureType': options.featureType, 'featurePrefix': featurePrefix, - 'gmlVersion': gmlVersion, 'srsName': options.srsName}, - TRANSACTION_SERIALIZERS, - makeSimpleNodeFactory('Delete'), deletes, - objectStack); - } - if (options.nativeElements) { - pushSerializeAndPop({node: node, 'featureNS': options.featureNS, - 'featureType': options.featureType, 'featurePrefix': featurePrefix, - 'gmlVersion': gmlVersion, 'srsName': options.srsName}, - TRANSACTION_SERIALIZERS, - makeSimpleNodeFactory('Native'), options.nativeElements, - objectStack); - } - return node; -}; - - /** * Read the projection from a WFS source. * @@ -1085,37 +1109,4 @@ WFS.prototype.writeTransaction = function(inserts, updates, deletes, options) { WFS.prototype.readProjection; -/** - * @inheritDoc - */ -WFS.prototype.readProjectionFromDocument = function(doc) { - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - return this.readProjectionFromNode(n); - } - } - return null; -}; - - -/** - * @inheritDoc - */ -WFS.prototype.readProjectionFromNode = function(node) { - if (node.firstElementChild && - node.firstElementChild.firstElementChild) { - node = node.firstElementChild.firstElementChild; - for (let n = node.firstElementChild; n; n = n.nextElementSibling) { - if (!(n.childNodes.length === 0 || - (n.childNodes.length === 1 && - n.firstChild.nodeType === 3))) { - const objectStack = [{}]; - this.gmlFormat_.readGeometryElement(n, objectStack); - return getProjection(objectStack.pop().srsName); - } - } - } - - return null; -}; export default WFS; diff --git a/src/ol/format/WKT.js b/src/ol/format/WKT.js index fb3f33fc8f..5e149705ab 100644 --- a/src/ol/format/WKT.js +++ b/src/ol/format/WKT.js @@ -77,460 +77,469 @@ const TokenType = { * @param {string} wkt WKT string. * @constructor */ -const Lexer = function(wkt) { +class Lexer { + constructor(wkt) { - /** - * @type {string} - */ - this.wkt = wkt; + /** + * @type {string} + */ + this.wkt = wkt; - /** - * @type {number} - * @private - */ - this.index_ = -1; -}; - - -/** - * @param {string} c Character. - * @return {boolean} Whether the character is alphabetic. - * @private - */ -Lexer.prototype.isAlpha_ = function(c) { - return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; -}; - - -/** - * @param {string} c Character. - * @param {boolean=} opt_decimal Whether the string number - * contains a dot, i.e. is a decimal number. - * @return {boolean} Whether the character is numeric. - * @private - */ -Lexer.prototype.isNumeric_ = function(c, opt_decimal) { - const decimal = opt_decimal !== undefined ? opt_decimal : false; - return c >= '0' && c <= '9' || c == '.' && !decimal; -}; - - -/** - * @param {string} c Character. - * @return {boolean} Whether the character is whitespace. - * @private - */ -Lexer.prototype.isWhiteSpace_ = function(c) { - return c == ' ' || c == '\t' || c == '\r' || c == '\n'; -}; - - -/** - * @return {string} Next string character. - * @private - */ -Lexer.prototype.nextChar_ = function() { - return this.wkt.charAt(++this.index_); -}; - - -/** - * Fetch and return the next token. - * @return {!module:ol/format/WKT~Token} Next string token. - */ -Lexer.prototype.nextToken = function() { - const c = this.nextChar_(); - const token = {position: this.index_, value: c}; - - if (c == '(') { - token.type = TokenType.LEFT_PAREN; - } else if (c == ',') { - token.type = TokenType.COMMA; - } else if (c == ')') { - token.type = TokenType.RIGHT_PAREN; - } else if (this.isNumeric_(c) || c == '-') { - token.type = TokenType.NUMBER; - token.value = this.readNumber_(); - } else if (this.isAlpha_(c)) { - token.type = TokenType.TEXT; - token.value = this.readText_(); - } else if (this.isWhiteSpace_(c)) { - return this.nextToken(); - } else if (c === '') { - token.type = TokenType.EOF; - } else { - throw new Error('Unexpected character: ' + c); + /** + * @type {number} + * @private + */ + this.index_ = -1; } - return token; -}; + /** + * @param {string} c Character. + * @return {boolean} Whether the character is alphabetic. + * @private + */ + isAlpha_(c) { + return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; + } + /** + * @param {string} c Character. + * @param {boolean=} opt_decimal Whether the string number + * contains a dot, i.e. is a decimal number. + * @return {boolean} Whether the character is numeric. + * @private + */ + isNumeric_(c, opt_decimal) { + const decimal = opt_decimal !== undefined ? opt_decimal : false; + return c >= '0' && c <= '9' || c == '.' && !decimal; + } -/** - * @return {number} Numeric token value. - * @private - */ -Lexer.prototype.readNumber_ = function() { - let c; - const index = this.index_; - let decimal = false; - let scientificNotation = false; - do { - if (c == '.') { - decimal = true; - } else if (c == 'e' || c == 'E') { - scientificNotation = true; + /** + * @param {string} c Character. + * @return {boolean} Whether the character is whitespace. + * @private + */ + isWhiteSpace_(c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; + } + + /** + * @return {string} Next string character. + * @private + */ + nextChar_() { + return this.wkt.charAt(++this.index_); + } + + /** + * Fetch and return the next token. + * @return {!module:ol/format/WKT~Token} Next string token. + */ + nextToken() { + const c = this.nextChar_(); + const token = {position: this.index_, value: c}; + + if (c == '(') { + token.type = TokenType.LEFT_PAREN; + } else if (c == ',') { + token.type = TokenType.COMMA; + } else if (c == ')') { + token.type = TokenType.RIGHT_PAREN; + } else if (this.isNumeric_(c) || c == '-') { + token.type = TokenType.NUMBER; + token.value = this.readNumber_(); + } else if (this.isAlpha_(c)) { + token.type = TokenType.TEXT; + token.value = this.readText_(); + } else if (this.isWhiteSpace_(c)) { + return this.nextToken(); + } else if (c === '') { + token.type = TokenType.EOF; + } else { + throw new Error('Unexpected character: ' + c); } - c = this.nextChar_(); - } while ( - this.isNumeric_(c, decimal) || - // if we haven't detected a scientific number before, 'e' or 'E' - // hint that we should continue to read - !scientificNotation && (c == 'e' || c == 'E') || - // once we know that we have a scientific number, both '-' and '+' - // are allowed - scientificNotation && (c == '-' || c == '+') - ); - return parseFloat(this.wkt.substring(index, this.index_--)); -}; + return token; + } -/** - * @return {string} String token value. - * @private - */ -Lexer.prototype.readText_ = function() { - let c; - const index = this.index_; - do { - c = this.nextChar_(); - } while (this.isAlpha_(c)); - return this.wkt.substring(index, this.index_--).toUpperCase(); -}; + /** + * @return {number} Numeric token value. + * @private + */ + readNumber_() { + let c; + const index = this.index_; + let decimal = false; + let scientificNotation = false; + do { + if (c == '.') { + decimal = true; + } else if (c == 'e' || c == 'E') { + scientificNotation = true; + } + c = this.nextChar_(); + } while ( + this.isNumeric_(c, decimal) || + // if we haven't detected a scientific number before, 'e' or 'E' + // hint that we should continue to read + !scientificNotation && (c == 'e' || c == 'E') || + // once we know that we have a scientific number, both '-' and '+' + // are allowed + scientificNotation && (c == '-' || c == '+') + ); + return parseFloat(this.wkt.substring(index, this.index_--)); + } + /** + * @return {string} String token value. + * @private + */ + readText_() { + let c; + const index = this.index_; + do { + c = this.nextChar_(); + } while (this.isAlpha_(c)); + return this.wkt.substring(index, this.index_--).toUpperCase(); + } +} /** * Class to parse the tokens from the WKT string. * @param {module:ol/format/WKT~Lexer} lexer The lexer. * @constructor */ -const Parser = function(lexer) { +class Parser { + constructor(lexer) { - /** - * @type {module:ol/format/WKT~Lexer} - * @private - */ - this.lexer_ = lexer; + /** + * @type {module:ol/format/WKT~Lexer} + * @private + */ + this.lexer_ = lexer; - /** - * @type {module:ol/format/WKT~Token} - * @private - */ - this.token_; + /** + * @type {module:ol/format/WKT~Token} + * @private + */ + this.token_; - /** - * @type {module:ol/geom/GeometryLayout} - * @private - */ - this.layout_ = GeometryLayout.XY; -}; - - -/** - * Fetch the next token form the lexer and replace the active token. - * @private - */ -Parser.prototype.consume_ = function() { - this.token_ = this.lexer_.nextToken(); -}; - -/** - * Tests if the given type matches the type of the current token. - * @param {module:ol/format/WKT~TokenType} type Token type. - * @return {boolean} Whether the token matches the given type. - */ -Parser.prototype.isTokenType = function(type) { - const isMatch = this.token_.type == type; - return isMatch; -}; - - -/** - * If the given type matches the current token, consume it. - * @param {module:ol/format/WKT~TokenType} type Token type. - * @return {boolean} Whether the token matches the given type. - */ -Parser.prototype.match = function(type) { - const isMatch = this.isTokenType(type); - if (isMatch) { - this.consume_(); + /** + * @type {module:ol/geom/GeometryLayout} + * @private + */ + this.layout_ = GeometryLayout.XY; } - return isMatch; -}; + /** + * Fetch the next token form the lexer and replace the active token. + * @private + */ + consume_() { + this.token_ = this.lexer_.nextToken(); + } -/** - * Try to parse the tokens provided by the lexer. - * @return {module:ol/geom/Geometry} The geometry. - */ -Parser.prototype.parse = function() { - this.consume_(); - const geometry = this.parseGeometry_(); - return geometry; -}; + /** + * Tests if the given type matches the type of the current token. + * @param {module:ol/format/WKT~TokenType} type Token type. + * @return {boolean} Whether the token matches the given type. + */ + isTokenType(type) { + const isMatch = this.token_.type == type; + return isMatch; + } - -/** - * Try to parse the dimensional info. - * @return {module:ol/geom/GeometryLayout} The layout. - * @private - */ -Parser.prototype.parseGeometryLayout_ = function() { - let layout = GeometryLayout.XY; - const dimToken = this.token_; - if (this.isTokenType(TokenType.TEXT)) { - const dimInfo = dimToken.value; - if (dimInfo === Z) { - layout = GeometryLayout.XYZ; - } else if (dimInfo === M) { - layout = GeometryLayout.XYM; - } else if (dimInfo === ZM) { - layout = GeometryLayout.XYZM; - } - if (layout !== GeometryLayout.XY) { + /** + * If the given type matches the current token, consume it. + * @param {module:ol/format/WKT~TokenType} type Token type. + * @return {boolean} Whether the token matches the given type. + */ + match(type) { + const isMatch = this.isTokenType(type); + if (isMatch) { this.consume_(); } + return isMatch; } - return layout; -}; + /** + * Try to parse the tokens provided by the lexer. + * @return {module:ol/geom/Geometry} The geometry. + */ + parse() { + this.consume_(); + const geometry = this.parseGeometry_(); + return geometry; + } -/** - * @return {!Array.} A collection of geometries. - * @private - */ -Parser.prototype.parseGeometryCollectionText_ = function() { - if (this.match(TokenType.LEFT_PAREN)) { - const geometries = []; - do { - geometries.push(this.parseGeometry_()); - } while (this.match(TokenType.COMMA)); - if (this.match(TokenType.RIGHT_PAREN)) { - return geometries; + /** + * Try to parse the dimensional info. + * @return {module:ol/geom/GeometryLayout} The layout. + * @private + */ + parseGeometryLayout_() { + let layout = GeometryLayout.XY; + const dimToken = this.token_; + if (this.isTokenType(TokenType.TEXT)) { + const dimInfo = dimToken.value; + if (dimInfo === Z) { + layout = GeometryLayout.XYZ; + } else if (dimInfo === M) { + layout = GeometryLayout.XYM; + } else if (dimInfo === ZM) { + layout = GeometryLayout.XYZM; + } + if (layout !== GeometryLayout.XY) { + this.consume_(); + } } - } else if (this.isEmptyGeometry_()) { - return []; + return layout; } - throw new Error(this.formatErrorMessage_()); -}; + /** + * @return {!Array.} A collection of geometries. + * @private + */ + parseGeometryCollectionText_() { + if (this.match(TokenType.LEFT_PAREN)) { + const geometries = []; + do { + geometries.push(this.parseGeometry_()); + } while (this.match(TokenType.COMMA)); + if (this.match(TokenType.RIGHT_PAREN)) { + return geometries; + } + } else if (this.isEmptyGeometry_()) { + return []; + } + throw new Error(this.formatErrorMessage_()); + } -/** - * @return {Array.} All values in a point. - * @private - */ -Parser.prototype.parsePointText_ = function() { - if (this.match(TokenType.LEFT_PAREN)) { - const coordinates = this.parsePoint_(); - if (this.match(TokenType.RIGHT_PAREN)) { + /** + * @return {Array.} All values in a point. + * @private + */ + parsePointText_() { + if (this.match(TokenType.LEFT_PAREN)) { + const coordinates = this.parsePoint_(); + if (this.match(TokenType.RIGHT_PAREN)) { + return coordinates; + } + } else if (this.isEmptyGeometry_()) { + return null; + } + throw new Error(this.formatErrorMessage_()); + } + + /** + * @return {!Array.>} All points in a linestring. + * @private + */ + parseLineStringText_() { + if (this.match(TokenType.LEFT_PAREN)) { + const coordinates = this.parsePointList_(); + if (this.match(TokenType.RIGHT_PAREN)) { + return coordinates; + } + } else if (this.isEmptyGeometry_()) { + return []; + } + throw new Error(this.formatErrorMessage_()); + } + + /** + * @return {!Array.>} All points in a polygon. + * @private + */ + parsePolygonText_() { + if (this.match(TokenType.LEFT_PAREN)) { + const coordinates = this.parseLineStringTextList_(); + if (this.match(TokenType.RIGHT_PAREN)) { + return coordinates; + } + } else if (this.isEmptyGeometry_()) { + return []; + } + throw new Error(this.formatErrorMessage_()); + } + + /** + * @return {!Array.>} All points in a multipoint. + * @private + */ + parseMultiPointText_() { + if (this.match(TokenType.LEFT_PAREN)) { + let coordinates; + if (this.token_.type == TokenType.LEFT_PAREN) { + coordinates = this.parsePointTextList_(); + } else { + coordinates = this.parsePointList_(); + } + if (this.match(TokenType.RIGHT_PAREN)) { + return coordinates; + } + } else if (this.isEmptyGeometry_()) { + return []; + } + throw new Error(this.formatErrorMessage_()); + } + + /** + * @return {!Array.>} All linestring points + * in a multilinestring. + * @private + */ + parseMultiLineStringText_() { + if (this.match(TokenType.LEFT_PAREN)) { + const coordinates = this.parseLineStringTextList_(); + if (this.match(TokenType.RIGHT_PAREN)) { + return coordinates; + } + } else if (this.isEmptyGeometry_()) { + return []; + } + throw new Error(this.formatErrorMessage_()); + } + + /** + * @return {!Array.>} All polygon points in a multipolygon. + * @private + */ + parseMultiPolygonText_() { + if (this.match(TokenType.LEFT_PAREN)) { + const coordinates = this.parsePolygonTextList_(); + if (this.match(TokenType.RIGHT_PAREN)) { + return coordinates; + } + } else if (this.isEmptyGeometry_()) { + return []; + } + throw new Error(this.formatErrorMessage_()); + } + + /** + * @return {!Array.} A point. + * @private + */ + parsePoint_() { + const coordinates = []; + const dimensions = this.layout_.length; + for (let i = 0; i < dimensions; ++i) { + const token = this.token_; + if (this.match(TokenType.NUMBER)) { + coordinates.push(token.value); + } else { + break; + } + } + if (coordinates.length == dimensions) { return coordinates; } - } else if (this.isEmptyGeometry_()) { - return null; + throw new Error(this.formatErrorMessage_()); } - throw new Error(this.formatErrorMessage_()); -}; - -/** - * @return {!Array.>} All points in a linestring. - * @private - */ -Parser.prototype.parseLineStringText_ = function() { - if (this.match(TokenType.LEFT_PAREN)) { - const coordinates = this.parsePointList_(); - if (this.match(TokenType.RIGHT_PAREN)) { - return coordinates; + /** + * @return {!Array.>} An array of points. + * @private + */ + parsePointList_() { + const coordinates = [this.parsePoint_()]; + while (this.match(TokenType.COMMA)) { + coordinates.push(this.parsePoint_()); } - } else if (this.isEmptyGeometry_()) { - return []; - } - throw new Error(this.formatErrorMessage_()); -}; - - -/** - * @return {!Array.>} All points in a polygon. - * @private - */ -Parser.prototype.parsePolygonText_ = function() { - if (this.match(TokenType.LEFT_PAREN)) { - const coordinates = this.parseLineStringTextList_(); - if (this.match(TokenType.RIGHT_PAREN)) { - return coordinates; - } - } else if (this.isEmptyGeometry_()) { - return []; - } - throw new Error(this.formatErrorMessage_()); -}; - - -/** - * @return {!Array.>} All points in a multipoint. - * @private - */ -Parser.prototype.parseMultiPointText_ = function() { - if (this.match(TokenType.LEFT_PAREN)) { - let coordinates; - if (this.token_.type == TokenType.LEFT_PAREN) { - coordinates = this.parsePointTextList_(); - } else { - coordinates = this.parsePointList_(); - } - if (this.match(TokenType.RIGHT_PAREN)) { - return coordinates; - } - } else if (this.isEmptyGeometry_()) { - return []; - } - throw new Error(this.formatErrorMessage_()); -}; - - -/** - * @return {!Array.>} All linestring points - * in a multilinestring. - * @private - */ -Parser.prototype.parseMultiLineStringText_ = function() { - if (this.match(TokenType.LEFT_PAREN)) { - const coordinates = this.parseLineStringTextList_(); - if (this.match(TokenType.RIGHT_PAREN)) { - return coordinates; - } - } else if (this.isEmptyGeometry_()) { - return []; - } - throw new Error(this.formatErrorMessage_()); -}; - - -/** - * @return {!Array.>} All polygon points in a multipolygon. - * @private - */ -Parser.prototype.parseMultiPolygonText_ = function() { - if (this.match(TokenType.LEFT_PAREN)) { - const coordinates = this.parsePolygonTextList_(); - if (this.match(TokenType.RIGHT_PAREN)) { - return coordinates; - } - } else if (this.isEmptyGeometry_()) { - return []; - } - throw new Error(this.formatErrorMessage_()); -}; - - -/** - * @return {!Array.} A point. - * @private - */ -Parser.prototype.parsePoint_ = function() { - const coordinates = []; - const dimensions = this.layout_.length; - for (let i = 0; i < dimensions; ++i) { - const token = this.token_; - if (this.match(TokenType.NUMBER)) { - coordinates.push(token.value); - } else { - break; - } - } - if (coordinates.length == dimensions) { return coordinates; } - throw new Error(this.formatErrorMessage_()); -}; - -/** - * @return {!Array.>} An array of points. - * @private - */ -Parser.prototype.parsePointList_ = function() { - const coordinates = [this.parsePoint_()]; - while (this.match(TokenType.COMMA)) { - coordinates.push(this.parsePoint_()); + /** + * @return {!Array.>} An array of points. + * @private + */ + parsePointTextList_() { + const coordinates = [this.parsePointText_()]; + while (this.match(TokenType.COMMA)) { + coordinates.push(this.parsePointText_()); + } + return coordinates; } - return coordinates; -}; - -/** - * @return {!Array.>} An array of points. - * @private - */ -Parser.prototype.parsePointTextList_ = function() { - const coordinates = [this.parsePointText_()]; - while (this.match(TokenType.COMMA)) { - coordinates.push(this.parsePointText_()); + /** + * @return {!Array.>} An array of points. + * @private + */ + parseLineStringTextList_() { + const coordinates = [this.parseLineStringText_()]; + while (this.match(TokenType.COMMA)) { + coordinates.push(this.parseLineStringText_()); + } + return coordinates; } - return coordinates; -}; - -/** - * @return {!Array.>} An array of points. - * @private - */ -Parser.prototype.parseLineStringTextList_ = function() { - const coordinates = [this.parseLineStringText_()]; - while (this.match(TokenType.COMMA)) { - coordinates.push(this.parseLineStringText_()); + /** + * @return {!Array.>} An array of points. + * @private + */ + parsePolygonTextList_() { + const coordinates = [this.parsePolygonText_()]; + while (this.match(TokenType.COMMA)) { + coordinates.push(this.parsePolygonText_()); + } + return coordinates; } - return coordinates; -}; - -/** - * @return {!Array.>} An array of points. - * @private - */ -Parser.prototype.parsePolygonTextList_ = function() { - const coordinates = [this.parsePolygonText_()]; - while (this.match(TokenType.COMMA)) { - coordinates.push(this.parsePolygonText_()); + /** + * @return {boolean} Whether the token implies an empty geometry. + * @private + */ + isEmptyGeometry_() { + const isEmpty = this.isTokenType(TokenType.TEXT) && + this.token_.value == EMPTY; + if (isEmpty) { + this.consume_(); + } + return isEmpty; } - return coordinates; -}; - -/** - * @return {boolean} Whether the token implies an empty geometry. - * @private - */ -Parser.prototype.isEmptyGeometry_ = function() { - const isEmpty = this.isTokenType(TokenType.TEXT) && - this.token_.value == EMPTY; - if (isEmpty) { - this.consume_(); + /** + * Create an error message for an unexpected token error. + * @return {string} Error message. + * @private + */ + formatErrorMessage_() { + return 'Unexpected `' + this.token_.value + '` at position ' + + this.token_.position + ' in `' + this.lexer_.wkt + '`'; } - return isEmpty; -}; - - -/** - * Create an error message for an unexpected token error. - * @return {string} Error message. - * @private - */ -Parser.prototype.formatErrorMessage_ = function() { - return 'Unexpected `' + this.token_.value + '` at position ' + - this.token_.position + ' in `' + this.lexer_.wkt + '`'; -}; + /** + * @return {!module:ol/geom/Geometry} The geometry. + * @private + */ + parseGeometry_() { + const token = this.token_; + if (this.match(TokenType.TEXT)) { + const geomType = token.value; + this.layout_ = this.parseGeometryLayout_(); + if (geomType == GeometryType.GEOMETRY_COLLECTION.toUpperCase()) { + const geometries = this.parseGeometryCollectionText_(); + return new GeometryCollection(geometries); + } else { + const parser = GeometryParser[geomType]; + const ctor = GeometryConstructor[geomType]; + if (!parser || !ctor) { + throw new Error('Invalid geometry type: ' + geomType); + } + let coordinates = parser.call(this); + if (!coordinates) { + if (ctor === GeometryConstructor[GeometryType.POINT]) { + coordinates = [NaN, NaN]; + } else { + coordinates = []; + } + } + return new ctor(coordinates, this.layout_); + } + } + throw new Error(this.formatErrorMessage_()); + } +} /** * @classdesc @@ -542,21 +551,119 @@ Parser.prototype.formatErrorMessage_ = function() { * @param {module:ol/format/WKT~Options=} opt_options Options. * @api */ -const WKT = function(opt_options) { +class WKT { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - TextFeature.call(this); + TextFeature.call(this); + + /** + * Split GeometryCollection into multiple features. + * @type {boolean} + * @private + */ + this.splitCollection_ = options.splitCollection !== undefined ? + options.splitCollection : false; + + } /** - * Split GeometryCollection into multiple features. - * @type {boolean} + * Parse a WKT string. + * @param {string} wkt WKT string. + * @return {module:ol/geom/Geometry|undefined} + * The geometry created. * @private */ - this.splitCollection_ = options.splitCollection !== undefined ? - options.splitCollection : false; + parse_(wkt) { + const lexer = new Lexer(wkt); + const parser = new Parser(lexer); + return parser.parse(); + } -}; + /** + * @inheritDoc + */ + readFeatureFromText(text, opt_options) { + const geom = this.readGeometryFromText(text, opt_options); + if (geom) { + const feature = new Feature(); + feature.setGeometry(geom); + return feature; + } + return null; + } + + /** + * @inheritDoc + */ + readFeaturesFromText(text, opt_options) { + let geometries = []; + const geometry = this.readGeometryFromText(text, opt_options); + if (this.splitCollection_ && + geometry.getType() == GeometryType.GEOMETRY_COLLECTION) { + geometries = (/** @type {module:ol/geom/GeometryCollection} */ (geometry)) + .getGeometriesArray(); + } else { + geometries = [geometry]; + } + const features = []; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + const feature = new Feature(); + feature.setGeometry(geometries[i]); + features.push(feature); + } + return features; + } + + /** + * @inheritDoc + */ + readGeometryFromText(text, opt_options) { + const geometry = this.parse_(text); + if (geometry) { + return ( + /** @type {module:ol/geom/Geometry} */ (transformWithOptions(geometry, false, opt_options)) + ); + } else { + return null; + } + } + + /** + * @inheritDoc + */ + writeFeatureText(feature, opt_options) { + const geometry = feature.getGeometry(); + if (geometry) { + return this.writeGeometryText(geometry, opt_options); + } + return ''; + } + + /** + * @inheritDoc + */ + writeFeaturesText(features, opt_options) { + if (features.length == 1) { + return this.writeFeatureText(features[0], opt_options); + } + const geometries = []; + for (let i = 0, ii = features.length; i < ii; ++i) { + geometries.push(features[i].getGeometry()); + } + const collection = new GeometryCollection(geometries); + return this.writeGeometryText(collection, opt_options); + } + + /** + * @inheritDoc + */ + writeGeometryText(geometry, opt_options) { + return encode(/** @type {module:ol/geom/Geometry} */ ( + transformWithOptions(geometry, true, opt_options))); + } +} inherits(WKT, TextFeature); @@ -712,20 +819,6 @@ function encode(geom) { } -/** - * Parse a WKT string. - * @param {string} wkt WKT string. - * @return {module:ol/geom/Geometry|undefined} - * The geometry created. - * @private - */ -WKT.prototype.parse_ = function(wkt) { - const lexer = new Lexer(wkt); - const parser = new Parser(lexer); - return parser.parse(); -}; - - /** * Read a feature from a WKT source. * @@ -738,20 +831,6 @@ WKT.prototype.parse_ = function(wkt) { WKT.prototype.readFeature; -/** - * @inheritDoc - */ -WKT.prototype.readFeatureFromText = function(text, opt_options) { - const geom = this.readGeometryFromText(text, opt_options); - if (geom) { - const feature = new Feature(); - feature.setGeometry(geom); - return feature; - } - return null; -}; - - /** * Read all features from a WKT source. * @@ -764,29 +843,6 @@ WKT.prototype.readFeatureFromText = function(text, opt_options) { WKT.prototype.readFeatures; -/** - * @inheritDoc - */ -WKT.prototype.readFeaturesFromText = function(text, opt_options) { - let geometries = []; - const geometry = this.readGeometryFromText(text, opt_options); - if (this.splitCollection_ && - geometry.getType() == GeometryType.GEOMETRY_COLLECTION) { - geometries = (/** @type {module:ol/geom/GeometryCollection} */ (geometry)) - .getGeometriesArray(); - } else { - geometries = [geometry]; - } - const features = []; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - const feature = new Feature(); - feature.setGeometry(geometries[i]); - features.push(feature); - } - return features; -}; - - /** * Read a single geometry from a WKT source. * @@ -799,21 +855,6 @@ WKT.prototype.readFeaturesFromText = function(text, opt_options) { WKT.prototype.readGeometry; -/** - * @inheritDoc - */ -WKT.prototype.readGeometryFromText = function(text, opt_options) { - const geometry = this.parse_(text); - if (geometry) { - return ( - /** @type {module:ol/geom/Geometry} */ (transformWithOptions(geometry, false, opt_options)) - ); - } else { - return null; - } -}; - - /** * @enum {function (new:module:ol/geom/Geometry, Array, module:ol/geom/GeometryLayout)} */ @@ -840,39 +881,6 @@ const GeometryParser = { }; -/** - * @return {!module:ol/geom/Geometry} The geometry. - * @private - */ -Parser.prototype.parseGeometry_ = function() { - const token = this.token_; - if (this.match(TokenType.TEXT)) { - const geomType = token.value; - this.layout_ = this.parseGeometryLayout_(); - if (geomType == GeometryType.GEOMETRY_COLLECTION.toUpperCase()) { - const geometries = this.parseGeometryCollectionText_(); - return new GeometryCollection(geometries); - } else { - const parser = GeometryParser[geomType]; - const ctor = GeometryConstructor[geomType]; - if (!parser || !ctor) { - throw new Error('Invalid geometry type: ' + geomType); - } - let coordinates = parser.call(this); - if (!coordinates) { - if (ctor === GeometryConstructor[GeometryType.POINT]) { - coordinates = [NaN, NaN]; - } else { - coordinates = []; - } - } - return new ctor(coordinates, this.layout_); - } - } - throw new Error(this.formatErrorMessage_()); -}; - - /** * Encode a feature as a WKT string. * @@ -885,18 +893,6 @@ Parser.prototype.parseGeometry_ = function() { WKT.prototype.writeFeature; -/** - * @inheritDoc - */ -WKT.prototype.writeFeatureText = function(feature, opt_options) { - const geometry = feature.getGeometry(); - if (geometry) { - return this.writeGeometryText(geometry, opt_options); - } - return ''; -}; - - /** * Encode an array of features as a WKT string. * @@ -909,22 +905,6 @@ WKT.prototype.writeFeatureText = function(feature, opt_options) { WKT.prototype.writeFeatures; -/** - * @inheritDoc - */ -WKT.prototype.writeFeaturesText = function(features, opt_options) { - if (features.length == 1) { - return this.writeFeatureText(features[0], opt_options); - } - const geometries = []; - for (let i = 0, ii = features.length; i < ii; ++i) { - geometries.push(features[i].getGeometry()); - } - const collection = new GeometryCollection(geometries); - return this.writeGeometryText(collection, opt_options); -}; - - /** * Write a single geometry as a WKT string. * @@ -937,13 +917,4 @@ WKT.prototype.writeFeaturesText = function(features, opt_options) { WKT.prototype.writeGeometry; -/** - * @inheritDoc - */ -WKT.prototype.writeGeometryText = function(geometry, opt_options) { - return encode(/** @type {module:ol/geom/Geometry} */ ( - transformWithOptions(geometry, true, opt_options))); -}; - - export default WKT; diff --git a/src/ol/format/WMSCapabilities.js b/src/ol/format/WMSCapabilities.js index 59e43e9aa7..b87177196a 100644 --- a/src/ol/format/WMSCapabilities.js +++ b/src/ol/format/WMSCapabilities.js @@ -17,15 +17,40 @@ import {makeArrayPusher, makeObjectPropertyPusher, makeObjectPropertySetter, * @extends {module:ol/format/XML} * @api */ -const WMSCapabilities = function() { +class WMSCapabilities { + constructor() { - XML.call(this); + XML.call(this); + + /** + * @type {string|undefined} + */ + this.version = undefined; + } /** - * @type {string|undefined} + * @inheritDoc */ - this.version = undefined; -}; + readFromDocument(doc) { + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + return this.readFromNode(n); + } + } + return null; + } + + /** + * @inheritDoc + */ + readFromNode(node) { + this.version = node.getAttribute('version').trim(); + const wmsCapabilityObject = pushParseAndPop({ + 'version': this.version + }, PARSERS, node, []); + return wmsCapabilityObject ? wmsCapabilityObject : null; + } +} inherits(WMSCapabilities, XML); @@ -277,31 +302,6 @@ const KEYWORDLIST_PARSERS = makeStructureNS( WMSCapabilities.prototype.read; -/** - * @inheritDoc - */ -WMSCapabilities.prototype.readFromDocument = function(doc) { - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - return this.readFromNode(n); - } - } - return null; -}; - - -/** - * @inheritDoc - */ -WMSCapabilities.prototype.readFromNode = function(node) { - this.version = node.getAttribute('version').trim(); - const wmsCapabilityObject = pushParseAndPop({ - 'version': this.version - }, PARSERS, node, []); - return wmsCapabilityObject ? wmsCapabilityObject : null; -}; - - /** * @param {Node} node Node. * @param {Array.<*>} objectStack Object stack. diff --git a/src/ol/format/WMSGetFeatureInfo.js b/src/ol/format/WMSGetFeatureInfo.js index 4591bbdc8a..7f84c1f99f 100644 --- a/src/ol/format/WMSGetFeatureInfo.js +++ b/src/ol/format/WMSGetFeatureInfo.js @@ -25,32 +25,136 @@ import {makeArrayPusher, makeStructureNS, pushParseAndPop} from '../xml.js'; * @param {module:ol/format/WMSGetFeatureInfo~Options=} opt_options Options. * @api */ -const WMSGetFeatureInfo = function(opt_options) { +class WMSGetFeatureInfo { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; + + /** + * @private + * @type {string} + */ + this.featureNS_ = 'http://mapserver.gis.umn.edu/mapserver'; + + + /** + * @private + * @type {module:ol/format/GML2} + */ + this.gmlFormat_ = new GML2(); + + + /** + * @private + * @type {Array.} + */ + this.layers_ = options.layers ? options.layers : null; + + XMLFeature.call(this); + } /** - * @private - * @type {string} + * @return {Array.} layers */ - this.featureNS_ = 'http://mapserver.gis.umn.edu/mapserver'; - + getLayers() { + return this.layers_; + } /** - * @private - * @type {module:ol/format/GML2} + * @param {Array.} layers Layers to parse. */ - this.gmlFormat_ = new GML2(); - + setLayers(layers) { + this.layers_ = layers; + } /** + * @param {Node} node Node. + * @param {Array.<*>} objectStack Object stack. + * @return {Array.} Features. * @private - * @type {Array.} */ - this.layers_ = options.layers ? options.layers : null; + readFeatures_(node, objectStack) { + node.setAttribute('namespaceURI', this.featureNS_); + const localName = node.localName; + /** @type {Array.} */ + let features = []; + if (node.childNodes.length === 0) { + return features; + } + if (localName == 'msGMLOutput') { + for (let i = 0, ii = node.childNodes.length; i < ii; i++) { + const layer = node.childNodes[i]; + if (layer.nodeType !== Node.ELEMENT_NODE) { + continue; + } + const context = objectStack[0]; - XMLFeature.call(this); -}; + const toRemove = layerIdentifier; + const layerName = layer.localName.replace(toRemove, ''); + + if (this.layers_ && !includes(this.layers_, layerName)) { + continue; + } + + const featureType = layerName + + featureIdentifier; + + context['featureType'] = featureType; + context['featureNS'] = this.featureNS_; + + const parsers = {}; + parsers[featureType] = makeArrayPusher( + this.gmlFormat_.readFeatureElement, this.gmlFormat_); + const parsersNS = makeStructureNS( + [context['featureNS'], null], parsers); + layer.setAttribute('namespaceURI', this.featureNS_); + const layerFeatures = pushParseAndPop( + [], parsersNS, layer, objectStack, this.gmlFormat_); + if (layerFeatures) { + extend(features, layerFeatures); + } + } + } + if (localName == 'FeatureCollection') { + const gmlFeatures = pushParseAndPop([], + this.gmlFormat_.FEATURE_COLLECTION_PARSERS, node, + [{}], this.gmlFormat_); + if (gmlFeatures) { + features = gmlFeatures; + } + } + return features; + } + + /** + * @inheritDoc + */ + readFeaturesFromNode(node, opt_options) { + const options = {}; + if (opt_options) { + assign(options, this.getReadOptions(node, opt_options)); + } + return this.readFeatures_(node, [options]); + } + + /** + * Not implemented. + * @inheritDoc + */ + writeFeatureNode(feature, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + writeFeaturesNode(features, opt_options) {} + + /** + * Not implemented. + * @inheritDoc + */ + writeGeometryNode(geometry, opt_options) {} +} inherits(WMSGetFeatureInfo, XMLFeature); @@ -69,82 +173,6 @@ const featureIdentifier = '_feature'; const layerIdentifier = '_layer'; -/** - * @return {Array.} layers - */ -WMSGetFeatureInfo.prototype.getLayers = function() { - return this.layers_; -}; - - -/** - * @param {Array.} layers Layers to parse. - */ -WMSGetFeatureInfo.prototype.setLayers = function(layers) { - this.layers_ = layers; -}; - - -/** - * @param {Node} node Node. - * @param {Array.<*>} objectStack Object stack. - * @return {Array.} Features. - * @private - */ -WMSGetFeatureInfo.prototype.readFeatures_ = function(node, objectStack) { - node.setAttribute('namespaceURI', this.featureNS_); - const localName = node.localName; - /** @type {Array.} */ - let features = []; - if (node.childNodes.length === 0) { - return features; - } - if (localName == 'msGMLOutput') { - for (let i = 0, ii = node.childNodes.length; i < ii; i++) { - const layer = node.childNodes[i]; - if (layer.nodeType !== Node.ELEMENT_NODE) { - continue; - } - const context = objectStack[0]; - - const toRemove = layerIdentifier; - const layerName = layer.localName.replace(toRemove, ''); - - if (this.layers_ && !includes(this.layers_, layerName)) { - continue; - } - - const featureType = layerName + - featureIdentifier; - - context['featureType'] = featureType; - context['featureNS'] = this.featureNS_; - - const parsers = {}; - parsers[featureType] = makeArrayPusher( - this.gmlFormat_.readFeatureElement, this.gmlFormat_); - const parsersNS = makeStructureNS( - [context['featureNS'], null], parsers); - layer.setAttribute('namespaceURI', this.featureNS_); - const layerFeatures = pushParseAndPop( - [], parsersNS, layer, objectStack, this.gmlFormat_); - if (layerFeatures) { - extend(features, layerFeatures); - } - } - } - if (localName == 'FeatureCollection') { - const gmlFeatures = pushParseAndPop([], - this.gmlFormat_.FEATURE_COLLECTION_PARSERS, node, - [{}], this.gmlFormat_); - if (gmlFeatures) { - features = gmlFeatures; - } - } - return features; -}; - - /** * Read all features from a WMSGetFeatureInfo response. * @@ -157,35 +185,4 @@ WMSGetFeatureInfo.prototype.readFeatures_ = function(node, objectStack) { WMSGetFeatureInfo.prototype.readFeatures; -/** - * @inheritDoc - */ -WMSGetFeatureInfo.prototype.readFeaturesFromNode = function(node, opt_options) { - const options = {}; - if (opt_options) { - assign(options, this.getReadOptions(node, opt_options)); - } - return this.readFeatures_(node, [options]); -}; - - -/** - * Not implemented. - * @inheritDoc - */ -WMSGetFeatureInfo.prototype.writeFeatureNode = function(feature, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -WMSGetFeatureInfo.prototype.writeFeaturesNode = function(features, opt_options) {}; - - -/** - * Not implemented. - * @inheritDoc - */ -WMSGetFeatureInfo.prototype.writeGeometryNode = function(geometry, opt_options) {}; export default WMSGetFeatureInfo; diff --git a/src/ol/format/WMTSCapabilities.js b/src/ol/format/WMTSCapabilities.js index 30463e93d7..fa46d1f218 100644 --- a/src/ol/format/WMTSCapabilities.js +++ b/src/ol/format/WMTSCapabilities.js @@ -18,15 +18,43 @@ import {pushParseAndPop, makeStructureNS, * @extends {module:ol/format/XML} * @api */ -const WMTSCapabilities = function() { - XML.call(this); +class WMTSCapabilities { + constructor() { + XML.call(this); + + /** + * @type {module:ol/format/OWS} + * @private + */ + this.owsParser_ = new OWS(); + } /** - * @type {module:ol/format/OWS} - * @private + * @inheritDoc */ - this.owsParser_ = new OWS(); -}; + readFromDocument(doc) { + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + return this.readFromNode(n); + } + } + return null; + } + + /** + * @inheritDoc + */ + readFromNode(node) { + const version = node.getAttribute('version').trim(); + let WMTSCapabilityObject = this.owsParser_.readFromNode(node); + if (!WMTSCapabilityObject) { + return null; + } + WMTSCapabilityObject['version'] = version; + WMTSCapabilityObject = pushParseAndPop(WMTSCapabilityObject, PARSERS, node, []); + return WMTSCapabilityObject ? WMTSCapabilityObject : null; + } +} inherits(WMTSCapabilities, XML); @@ -204,34 +232,6 @@ const TM_PARSERS = makeStructureNS( WMTSCapabilities.prototype.read; -/** - * @inheritDoc - */ -WMTSCapabilities.prototype.readFromDocument = function(doc) { - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - return this.readFromNode(n); - } - } - return null; -}; - - -/** - * @inheritDoc - */ -WMTSCapabilities.prototype.readFromNode = function(node) { - const version = node.getAttribute('version').trim(); - let WMTSCapabilityObject = this.owsParser_.readFromNode(node); - if (!WMTSCapabilityObject) { - return null; - } - WMTSCapabilityObject['version'] = version; - WMTSCapabilityObject = pushParseAndPop(WMTSCapabilityObject, PARSERS, node, []); - return WMTSCapabilityObject ? WMTSCapabilityObject : null; -}; - - /** * @param {Node} node Node. * @param {Array.<*>} objectStack Object stack. diff --git a/src/ol/format/XML.js b/src/ol/format/XML.js index 4f828ae17e..1dd06cfd9f 100644 --- a/src/ol/format/XML.js +++ b/src/ol/format/XML.js @@ -11,40 +11,37 @@ import {isDocument, isNode, parse} from '../xml.js'; * @abstract * @struct */ -const XML = function() { -}; +class XML { + /** + * @param {Document|Node|string} source Source. + * @return {Object} The parsed result. + */ + read(source) { + if (isDocument(source)) { + return this.readFromDocument(/** @type {Document} */ (source)); + } else if (isNode(source)) { + return this.readFromNode(/** @type {Node} */ (source)); + } else if (typeof source === 'string') { + const doc = parse(source); + return this.readFromDocument(doc); + } else { + return null; + } + } + /** + * @abstract + * @param {Document} doc Document. + * @return {Object} Object + */ + readFromDocument(doc) {} -/** - * @param {Document|Node|string} source Source. - * @return {Object} The parsed result. - */ -XML.prototype.read = function(source) { - if (isDocument(source)) { - return this.readFromDocument(/** @type {Document} */ (source)); - } else if (isNode(source)) { - return this.readFromNode(/** @type {Node} */ (source)); - } else if (typeof source === 'string') { - const doc = parse(source); - return this.readFromDocument(doc); - } else { - return null; - } -}; + /** + * @abstract + * @param {Node} node Node. + * @return {Object} Object + */ + readFromNode(node) {} +} - -/** - * @abstract - * @param {Document} doc Document. - * @return {Object} Object - */ -XML.prototype.readFromDocument = function(doc) {}; - - -/** - * @abstract - * @param {Node} node Node. - * @return {Object} Object - */ -XML.prototype.readFromNode = function(node) {}; export default XML; diff --git a/src/ol/format/XMLFeature.js b/src/ol/format/XMLFeature.js index ec3adb5bf1..5365257d39 100644 --- a/src/ol/format/XMLFeature.js +++ b/src/ol/format/XMLFeature.js @@ -17,247 +17,232 @@ import {isDocument, isNode, parse} from '../xml.js'; * @abstract * @extends {module:ol/format/Feature} */ -const XMLFeature = function() { +class XMLFeature { + constructor() { + + /** + * @type {XMLSerializer} + * @private + */ + this.xmlSerializer_ = new XMLSerializer(); + + FeatureFormat.call(this); + } /** - * @type {XMLSerializer} - * @private + * @inheritDoc */ - this.xmlSerializer_ = new XMLSerializer(); + getType() { + return FormatType.XML; + } - FeatureFormat.call(this); -}; + /** + * @inheritDoc + */ + readFeature(source, opt_options) { + if (isDocument(source)) { + return this.readFeatureFromDocument(/** @type {Document} */ (source), opt_options); + } else if (isNode(source)) { + return this.readFeatureFromNode(/** @type {Node} */ (source), opt_options); + } else if (typeof source === 'string') { + const doc = parse(source); + return this.readFeatureFromDocument(doc, opt_options); + } else { + return null; + } + } + + /** + * @param {Document} doc Document. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. + * @return {module:ol/Feature} Feature. + */ + readFeatureFromDocument(doc, opt_options) { + const features = this.readFeaturesFromDocument(doc, opt_options); + if (features.length > 0) { + return features[0]; + } else { + return null; + } + } + + /** + * @param {Node} node Node. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. + * @return {module:ol/Feature} Feature. + */ + readFeatureFromNode(node, opt_options) { + return null; // not implemented + } + + /** + * @inheritDoc + */ + readFeatures(source, opt_options) { + if (isDocument(source)) { + return this.readFeaturesFromDocument( + /** @type {Document} */ (source), opt_options); + } else if (isNode(source)) { + return this.readFeaturesFromNode(/** @type {Node} */ (source), opt_options); + } else if (typeof source === 'string') { + const doc = parse(source); + return this.readFeaturesFromDocument(doc, opt_options); + } else { + return []; + } + } + + /** + * @param {Document} doc Document. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. + * @protected + * @return {Array.} Features. + */ + readFeaturesFromDocument(doc, opt_options) { + /** @type {Array.} */ + const features = []; + for (let n = doc.firstChild; n; n = n.nextSibling) { + if (n.nodeType == Node.ELEMENT_NODE) { + extend(features, this.readFeaturesFromNode(n, opt_options)); + } + } + return features; + } + + /** + * @abstract + * @param {Node} node Node. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. + * @protected + * @return {Array.} Features. + */ + readFeaturesFromNode(node, opt_options) {} + + /** + * @inheritDoc + */ + readGeometry(source, opt_options) { + if (isDocument(source)) { + return this.readGeometryFromDocument( + /** @type {Document} */ (source), opt_options); + } else if (isNode(source)) { + return this.readGeometryFromNode(/** @type {Node} */ (source), opt_options); + } else if (typeof source === 'string') { + const doc = parse(source); + return this.readGeometryFromDocument(doc, opt_options); + } else { + return null; + } + } + + /** + * @param {Document} doc Document. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. + * @protected + * @return {module:ol/geom/Geometry} Geometry. + */ + readGeometryFromDocument(doc, opt_options) { + return null; // not implemented + } + + /** + * @param {Node} node Node. + * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. + * @protected + * @return {module:ol/geom/Geometry} Geometry. + */ + readGeometryFromNode(node, opt_options) { + return null; // not implemented + } + + /** + * @inheritDoc + */ + readProjection(source) { + if (isDocument(source)) { + return this.readProjectionFromDocument(/** @type {Document} */ (source)); + } else if (isNode(source)) { + return this.readProjectionFromNode(/** @type {Node} */ (source)); + } else if (typeof source === 'string') { + const doc = parse(source); + return this.readProjectionFromDocument(doc); + } else { + return null; + } + } + + /** + * @param {Document} doc Document. + * @protected + * @return {module:ol/proj/Projection} Projection. + */ + readProjectionFromDocument(doc) { + return this.dataProjection; + } + + /** + * @param {Node} node Node. + * @protected + * @return {module:ol/proj/Projection} Projection. + */ + readProjectionFromNode(node) { + return this.dataProjection; + } + + /** + * @inheritDoc + */ + writeFeature(feature, opt_options) { + const node = this.writeFeatureNode(feature, opt_options); + return this.xmlSerializer_.serializeToString(node); + } + + /** + * @param {module:ol/Feature} feature Feature. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. + * @protected + * @return {Node} Node. + */ + writeFeatureNode(feature, opt_options) { + return null; // not implemented + } + + /** + * @inheritDoc + */ + writeFeatures(features, opt_options) { + const node = this.writeFeaturesNode(features, opt_options); + return this.xmlSerializer_.serializeToString(node); + } + + /** + * @param {Array.} features Features. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. + * @return {Node} Node. + */ + writeFeaturesNode(features, opt_options) { + return null; // not implemented + } + + /** + * @inheritDoc + */ + writeGeometry(geometry, opt_options) { + const node = this.writeGeometryNode(geometry, opt_options); + return this.xmlSerializer_.serializeToString(node); + } + + /** + * @param {module:ol/geom/Geometry} geometry Geometry. + * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. + * @return {Node} Node. + */ + writeGeometryNode(geometry, opt_options) { + return null; // not implemented + } +} inherits(XMLFeature, FeatureFormat); -/** - * @inheritDoc - */ -XMLFeature.prototype.getType = function() { - return FormatType.XML; -}; - - -/** - * @inheritDoc - */ -XMLFeature.prototype.readFeature = function(source, opt_options) { - if (isDocument(source)) { - return this.readFeatureFromDocument(/** @type {Document} */ (source), opt_options); - } else if (isNode(source)) { - return this.readFeatureFromNode(/** @type {Node} */ (source), opt_options); - } else if (typeof source === 'string') { - const doc = parse(source); - return this.readFeatureFromDocument(doc, opt_options); - } else { - return null; - } -}; - - -/** - * @param {Document} doc Document. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. - * @return {module:ol/Feature} Feature. - */ -XMLFeature.prototype.readFeatureFromDocument = function(doc, opt_options) { - const features = this.readFeaturesFromDocument(doc, opt_options); - if (features.length > 0) { - return features[0]; - } else { - return null; - } -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. - * @return {module:ol/Feature} Feature. - */ -XMLFeature.prototype.readFeatureFromNode = function(node, opt_options) { - return null; // not implemented -}; - - -/** - * @inheritDoc - */ -XMLFeature.prototype.readFeatures = function(source, opt_options) { - if (isDocument(source)) { - return this.readFeaturesFromDocument( - /** @type {Document} */ (source), opt_options); - } else if (isNode(source)) { - return this.readFeaturesFromNode(/** @type {Node} */ (source), opt_options); - } else if (typeof source === 'string') { - const doc = parse(source); - return this.readFeaturesFromDocument(doc, opt_options); - } else { - return []; - } -}; - - -/** - * @param {Document} doc Document. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. - * @protected - * @return {Array.} Features. - */ -XMLFeature.prototype.readFeaturesFromDocument = function(doc, opt_options) { - /** @type {Array.} */ - const features = []; - for (let n = doc.firstChild; n; n = n.nextSibling) { - if (n.nodeType == Node.ELEMENT_NODE) { - extend(features, this.readFeaturesFromNode(n, opt_options)); - } - } - return features; -}; - - -/** - * @abstract - * @param {Node} node Node. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. - * @protected - * @return {Array.} Features. - */ -XMLFeature.prototype.readFeaturesFromNode = function(node, opt_options) {}; - - -/** - * @inheritDoc - */ -XMLFeature.prototype.readGeometry = function(source, opt_options) { - if (isDocument(source)) { - return this.readGeometryFromDocument( - /** @type {Document} */ (source), opt_options); - } else if (isNode(source)) { - return this.readGeometryFromNode(/** @type {Node} */ (source), opt_options); - } else if (typeof source === 'string') { - const doc = parse(source); - return this.readGeometryFromDocument(doc, opt_options); - } else { - return null; - } -}; - - -/** - * @param {Document} doc Document. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. - * @protected - * @return {module:ol/geom/Geometry} Geometry. - */ -XMLFeature.prototype.readGeometryFromDocument = function(doc, opt_options) { - return null; // not implemented -}; - - -/** - * @param {Node} node Node. - * @param {module:ol/format/Feature~ReadOptions=} opt_options Options. - * @protected - * @return {module:ol/geom/Geometry} Geometry. - */ -XMLFeature.prototype.readGeometryFromNode = function(node, opt_options) { - return null; // not implemented -}; - - -/** - * @inheritDoc - */ -XMLFeature.prototype.readProjection = function(source) { - if (isDocument(source)) { - return this.readProjectionFromDocument(/** @type {Document} */ (source)); - } else if (isNode(source)) { - return this.readProjectionFromNode(/** @type {Node} */ (source)); - } else if (typeof source === 'string') { - const doc = parse(source); - return this.readProjectionFromDocument(doc); - } else { - return null; - } -}; - - -/** - * @param {Document} doc Document. - * @protected - * @return {module:ol/proj/Projection} Projection. - */ -XMLFeature.prototype.readProjectionFromDocument = function(doc) { - return this.dataProjection; -}; - - -/** - * @param {Node} node Node. - * @protected - * @return {module:ol/proj/Projection} Projection. - */ -XMLFeature.prototype.readProjectionFromNode = function(node) { - return this.dataProjection; -}; - - -/** - * @inheritDoc - */ -XMLFeature.prototype.writeFeature = function(feature, opt_options) { - const node = this.writeFeatureNode(feature, opt_options); - return this.xmlSerializer_.serializeToString(node); -}; - - -/** - * @param {module:ol/Feature} feature Feature. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. - * @protected - * @return {Node} Node. - */ -XMLFeature.prototype.writeFeatureNode = function(feature, opt_options) { - return null; // not implemented -}; - - -/** - * @inheritDoc - */ -XMLFeature.prototype.writeFeatures = function(features, opt_options) { - const node = this.writeFeaturesNode(features, opt_options); - return this.xmlSerializer_.serializeToString(node); -}; - - -/** - * @param {Array.} features Features. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. - * @return {Node} Node. - */ -XMLFeature.prototype.writeFeaturesNode = function(features, opt_options) { - return null; // not implemented -}; - - -/** - * @inheritDoc - */ -XMLFeature.prototype.writeGeometry = function(geometry, opt_options) { - const node = this.writeGeometryNode(geometry, opt_options); - return this.xmlSerializer_.serializeToString(node); -}; - - -/** - * @param {module:ol/geom/Geometry} geometry Geometry. - * @param {module:ol/format/Feature~WriteOptions=} opt_options Options. - * @return {Node} Node. - */ -XMLFeature.prototype.writeGeometryNode = function(geometry, opt_options) { - return null; // not implemented -}; export default XMLFeature; diff --git a/src/ol/format/filter/Filter.js b/src/ol/format/filter/Filter.js index 7ec8c2f36e..0f6f538124 100644 --- a/src/ol/format/filter/Filter.js +++ b/src/ol/format/filter/Filter.js @@ -13,21 +13,23 @@ * @param {!string} tagName The XML tag name for this filter. * @struct */ -const Filter = function(tagName) { +class Filter { + constructor(tagName) { - /** - * @private - * @type {!string} - */ - this.tagName_ = tagName; -}; + /** + * @private + * @type {!string} + */ + this.tagName_ = tagName; + } -/** - * The XML tag name for a filter. - * @returns {!string} Name. - */ -Filter.prototype.getTagName = function() { - return this.tagName_; -}; + /** + * The XML tag name for a filter. + * @returns {!string} Name. + */ + getTagName() { + return this.tagName_; + } +} export default Filter; diff --git a/src/ol/geom/Circle.js b/src/ol/geom/Circle.js index 2864866be9..a3528fd277 100644 --- a/src/ol/geom/Circle.js +++ b/src/ol/geom/Circle.js @@ -20,213 +20,201 @@ import {deflateCoordinate} from '../geom/flat/deflate.js'; * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. * @api */ -const Circle = function(center, opt_radius, opt_layout) { - SimpleGeometry.call(this); - if (opt_layout !== undefined && opt_radius === undefined) { - this.setFlatCoordinates(opt_layout, center); - } else { - const radius = opt_radius ? opt_radius : 0; - this.setCenterAndRadius(center, radius, opt_layout); +class Circle { + constructor(center, opt_radius, opt_layout) { + SimpleGeometry.call(this); + if (opt_layout !== undefined && opt_radius === undefined) { + this.setFlatCoordinates(opt_layout, center); + } else { + const radius = opt_radius ? opt_radius : 0; + this.setCenterAndRadius(center, radius, opt_layout); + } } -}; + + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/Circle} Clone. + * @override + * @api + */ + clone() { + return new Circle(this.flatCoordinates.slice(), undefined, this.layout); + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + const flatCoordinates = this.flatCoordinates; + const dx = x - flatCoordinates[0]; + const dy = y - flatCoordinates[1]; + const squaredDistance = dx * dx + dy * dy; + if (squaredDistance < minSquaredDistance) { + if (squaredDistance === 0) { + for (let i = 0; i < this.stride; ++i) { + closestPoint[i] = flatCoordinates[i]; + } + } else { + const delta = this.getRadius() / Math.sqrt(squaredDistance); + closestPoint[0] = flatCoordinates[0] + delta * dx; + closestPoint[1] = flatCoordinates[1] + delta * dy; + for (let i = 2; i < this.stride; ++i) { + closestPoint[i] = flatCoordinates[i]; + } + } + closestPoint.length = this.stride; + return squaredDistance; + } else { + return minSquaredDistance; + } + } + + /** + * @inheritDoc + */ + containsXY(x, y) { + const flatCoordinates = this.flatCoordinates; + const dx = x - flatCoordinates[0]; + const dy = y - flatCoordinates[1]; + return dx * dx + dy * dy <= this.getRadiusSquared_(); + } + + /** + * Return the center of the circle as {@link module:ol/coordinate~Coordinate coordinate}. + * @return {module:ol/coordinate~Coordinate} Center. + * @api + */ + getCenter() { + return this.flatCoordinates.slice(0, this.stride); + } + + /** + * @inheritDoc + */ + computeExtent(extent) { + const flatCoordinates = this.flatCoordinates; + const radius = flatCoordinates[this.stride] - flatCoordinates[0]; + return createOrUpdate( + flatCoordinates[0] - radius, flatCoordinates[1] - radius, + flatCoordinates[0] + radius, flatCoordinates[1] + radius, + extent); + } + + /** + * Return the radius of the circle. + * @return {number} Radius. + * @api + */ + getRadius() { + return Math.sqrt(this.getRadiusSquared_()); + } + + /** + * @private + * @return {number} Radius squared. + */ + getRadiusSquared_() { + const dx = this.flatCoordinates[this.stride] - this.flatCoordinates[0]; + const dy = this.flatCoordinates[this.stride + 1] - this.flatCoordinates[1]; + return dx * dx + dy * dy; + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.CIRCLE; + } + + /** + * @inheritDoc + * @api + */ + intersectsExtent(extent) { + const circleExtent = this.getExtent(); + if (intersects(extent, circleExtent)) { + const center = this.getCenter(); + + if (extent[0] <= center[0] && extent[2] >= center[0]) { + return true; + } + if (extent[1] <= center[1] && extent[3] >= center[1]) { + return true; + } + + return forEachCorner(extent, this.intersectsCoordinate, this); + } + return false; + + } + + /** + * Set the center of the circle as {@link module:ol/coordinate~Coordinate coordinate}. + * @param {module:ol/coordinate~Coordinate} center Center. + * @api + */ + setCenter(center) { + const stride = this.stride; + const radius = this.flatCoordinates[stride] - this.flatCoordinates[0]; + const flatCoordinates = center.slice(); + flatCoordinates[stride] = flatCoordinates[0] + radius; + for (let i = 1; i < stride; ++i) { + flatCoordinates[stride + i] = center[i]; + } + this.setFlatCoordinates(this.layout, flatCoordinates); + this.changed(); + } + + /** + * Set the center (as {@link module:ol/coordinate~Coordinate coordinate}) and the radius (as + * number) of the circle. + * @param {!module:ol/coordinate~Coordinate} center Center. + * @param {number} radius Radius. + * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. + * @api + */ + setCenterAndRadius(center, radius, opt_layout) { + this.setLayout(opt_layout, center, 0); + if (!this.flatCoordinates) { + this.flatCoordinates = []; + } + /** @type {Array.} */ + const flatCoordinates = this.flatCoordinates; + let offset = deflateCoordinate( + flatCoordinates, 0, center, this.stride); + flatCoordinates[offset++] = flatCoordinates[0] + radius; + for (let i = 1, ii = this.stride; i < ii; ++i) { + flatCoordinates[offset++] = flatCoordinates[i]; + } + flatCoordinates.length = offset; + this.changed(); + } + + /** + * @inheritDoc + */ + getCoordinates() {} + + /** + * @inheritDoc + */ + setCoordinates(coordinates, opt_layout) {} + + /** + * Set the radius of the circle. The radius is in the units of the projection. + * @param {number} radius Radius. + * @api + */ + setRadius(radius) { + this.flatCoordinates[this.stride] = this.flatCoordinates[0] + radius; + this.changed(); + } +} inherits(Circle, SimpleGeometry); -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/Circle} Clone. - * @override - * @api - */ -Circle.prototype.clone = function() { - return new Circle(this.flatCoordinates.slice(), undefined, this.layout); -}; - - -/** - * @inheritDoc - */ -Circle.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - const flatCoordinates = this.flatCoordinates; - const dx = x - flatCoordinates[0]; - const dy = y - flatCoordinates[1]; - const squaredDistance = dx * dx + dy * dy; - if (squaredDistance < minSquaredDistance) { - if (squaredDistance === 0) { - for (let i = 0; i < this.stride; ++i) { - closestPoint[i] = flatCoordinates[i]; - } - } else { - const delta = this.getRadius() / Math.sqrt(squaredDistance); - closestPoint[0] = flatCoordinates[0] + delta * dx; - closestPoint[1] = flatCoordinates[1] + delta * dy; - for (let i = 2; i < this.stride; ++i) { - closestPoint[i] = flatCoordinates[i]; - } - } - closestPoint.length = this.stride; - return squaredDistance; - } else { - return minSquaredDistance; - } -}; - - -/** - * @inheritDoc - */ -Circle.prototype.containsXY = function(x, y) { - const flatCoordinates = this.flatCoordinates; - const dx = x - flatCoordinates[0]; - const dy = y - flatCoordinates[1]; - return dx * dx + dy * dy <= this.getRadiusSquared_(); -}; - - -/** - * Return the center of the circle as {@link module:ol/coordinate~Coordinate coordinate}. - * @return {module:ol/coordinate~Coordinate} Center. - * @api - */ -Circle.prototype.getCenter = function() { - return this.flatCoordinates.slice(0, this.stride); -}; - - -/** - * @inheritDoc - */ -Circle.prototype.computeExtent = function(extent) { - const flatCoordinates = this.flatCoordinates; - const radius = flatCoordinates[this.stride] - flatCoordinates[0]; - return createOrUpdate( - flatCoordinates[0] - radius, flatCoordinates[1] - radius, - flatCoordinates[0] + radius, flatCoordinates[1] + radius, - extent); -}; - - -/** - * Return the radius of the circle. - * @return {number} Radius. - * @api - */ -Circle.prototype.getRadius = function() { - return Math.sqrt(this.getRadiusSquared_()); -}; - - -/** - * @private - * @return {number} Radius squared. - */ -Circle.prototype.getRadiusSquared_ = function() { - const dx = this.flatCoordinates[this.stride] - this.flatCoordinates[0]; - const dy = this.flatCoordinates[this.stride + 1] - this.flatCoordinates[1]; - return dx * dx + dy * dy; -}; - - -/** - * @inheritDoc - * @api - */ -Circle.prototype.getType = function() { - return GeometryType.CIRCLE; -}; - - -/** - * @inheritDoc - * @api - */ -Circle.prototype.intersectsExtent = function(extent) { - const circleExtent = this.getExtent(); - if (intersects(extent, circleExtent)) { - const center = this.getCenter(); - - if (extent[0] <= center[0] && extent[2] >= center[0]) { - return true; - } - if (extent[1] <= center[1] && extent[3] >= center[1]) { - return true; - } - - return forEachCorner(extent, this.intersectsCoordinate, this); - } - return false; - -}; - - -/** - * Set the center of the circle as {@link module:ol/coordinate~Coordinate coordinate}. - * @param {module:ol/coordinate~Coordinate} center Center. - * @api - */ -Circle.prototype.setCenter = function(center) { - const stride = this.stride; - const radius = this.flatCoordinates[stride] - this.flatCoordinates[0]; - const flatCoordinates = center.slice(); - flatCoordinates[stride] = flatCoordinates[0] + radius; - for (let i = 1; i < stride; ++i) { - flatCoordinates[stride + i] = center[i]; - } - this.setFlatCoordinates(this.layout, flatCoordinates); - this.changed(); -}; - - -/** - * Set the center (as {@link module:ol/coordinate~Coordinate coordinate}) and the radius (as - * number) of the circle. - * @param {!module:ol/coordinate~Coordinate} center Center. - * @param {number} radius Radius. - * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. - * @api - */ -Circle.prototype.setCenterAndRadius = function(center, radius, opt_layout) { - this.setLayout(opt_layout, center, 0); - if (!this.flatCoordinates) { - this.flatCoordinates = []; - } - /** @type {Array.} */ - const flatCoordinates = this.flatCoordinates; - let offset = deflateCoordinate( - flatCoordinates, 0, center, this.stride); - flatCoordinates[offset++] = flatCoordinates[0] + radius; - for (let i = 1, ii = this.stride; i < ii; ++i) { - flatCoordinates[offset++] = flatCoordinates[i]; - } - flatCoordinates.length = offset; - this.changed(); -}; - - -/** - * @inheritDoc - */ -Circle.prototype.getCoordinates = function() {}; - - -/** - * @inheritDoc - */ -Circle.prototype.setCoordinates = function(coordinates, opt_layout) {}; - - -/** - * Set the radius of the circle. The radius is in the units of the projection. - * @param {number} radius Radius. - * @api - */ -Circle.prototype.setRadius = function(radius) { - this.flatCoordinates[this.stride] = this.flatCoordinates[0] + radius; - this.changed(); -}; - - /** * Transform each coordinate of the circle from one coordinate reference system * to another. The geometry is modified in place. diff --git a/src/ol/geom/Geometry.js b/src/ol/geom/Geometry.js index 9389ecc0ed..a71d3ca775 100644 --- a/src/ol/geom/Geometry.js +++ b/src/ol/geom/Geometry.js @@ -25,41 +25,225 @@ import {create as createTransform, compose as composeTransform} from '../transfo * @extends {module:ol/Object} * @api */ -const Geometry = function() { +class Geometry { + constructor() { - BaseObject.call(this); + BaseObject.call(this); - /** - * @private - * @type {module:ol/extent~Extent} - */ - this.extent_ = createEmpty(); + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.extent_ = createEmpty(); - /** - * @private - * @type {number} - */ - this.extentRevision_ = -1; + /** + * @private + * @type {number} + */ + this.extentRevision_ = -1; - /** - * @protected - * @type {Object.} - */ - this.simplifiedGeometryCache = {}; + /** + * @protected + * @type {Object.} + */ + this.simplifiedGeometryCache = {}; - /** - * @protected - * @type {number} - */ - this.simplifiedGeometryMaxMinSquaredTolerance = 0; + /** + * @protected + * @type {number} + */ + this.simplifiedGeometryMaxMinSquaredTolerance = 0; - /** - * @protected - * @type {number} - */ - this.simplifiedGeometryRevision = 0; + /** + * @protected + * @type {number} + */ + this.simplifiedGeometryRevision = 0; -}; + } + + /** + * Make a complete copy of the geometry. + * @abstract + * @return {!module:ol/geom/Geometry} Clone. + */ + clone() {} + + /** + * @abstract + * @param {number} x X. + * @param {number} y Y. + * @param {module:ol/coordinate~Coordinate} closestPoint Closest point. + * @param {number} minSquaredDistance Minimum squared distance. + * @return {number} Minimum squared distance. + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) {} + + /** + * Return the closest point of the geometry to the passed point as + * {@link module:ol/coordinate~Coordinate coordinate}. + * @param {module:ol/coordinate~Coordinate} point Point. + * @param {module:ol/coordinate~Coordinate=} opt_closestPoint Closest point. + * @return {module:ol/coordinate~Coordinate} Closest point. + * @api + */ + getClosestPoint(point, opt_closestPoint) { + const closestPoint = opt_closestPoint ? opt_closestPoint : [NaN, NaN]; + this.closestPointXY(point[0], point[1], closestPoint, Infinity); + return closestPoint; + } + + /** + * Returns true if this geometry includes the specified coordinate. If the + * coordinate is on the boundary of the geometry, returns false. + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @return {boolean} Contains coordinate. + * @api + */ + intersectsCoordinate(coordinate) { + return this.containsXY(coordinate[0], coordinate[1]); + } + + /** + * @abstract + * @param {module:ol/extent~Extent} extent Extent. + * @protected + * @return {module:ol/extent~Extent} extent Extent. + */ + computeExtent(extent) {} + + /** + * Get the extent of the geometry. + * @param {module:ol/extent~Extent=} opt_extent Extent. + * @return {module:ol/extent~Extent} extent Extent. + * @api + */ + getExtent(opt_extent) { + if (this.extentRevision_ != this.getRevision()) { + this.extent_ = this.computeExtent(this.extent_); + this.extentRevision_ = this.getRevision(); + } + return returnOrUpdate(this.extent_, opt_extent); + } + + /** + * Rotate the geometry around a given coordinate. This modifies the geometry + * coordinates in place. + * @abstract + * @param {number} angle Rotation angle in radians. + * @param {module:ol/coordinate~Coordinate} anchor The rotation center. + * @api + */ + rotate(angle, anchor) {} + + /** + * Scale the geometry (with an optional origin). This modifies the geometry + * coordinates in place. + * @abstract + * @param {number} sx The scaling factor in the x-direction. + * @param {number=} opt_sy The scaling factor in the y-direction (defaults to + * sx). + * @param {module:ol/coordinate~Coordinate=} opt_anchor The scale origin (defaults to the center + * of the geometry extent). + * @api + */ + scale(sx, opt_sy, opt_anchor) {} + + /** + * Create a simplified version of this geometry. For linestrings, this uses + * the the {@link + * https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm + * Douglas Peucker} algorithm. For polygons, a quantization-based + * simplification is used to preserve topology. + * @function + * @param {number} tolerance The tolerance distance for simplification. + * @return {module:ol/geom/Geometry} A new, simplified version of the original + * geometry. + * @api + */ + simplify(tolerance) { + return this.getSimplifiedGeometry(tolerance * tolerance); + } + + /** + * Create a simplified version of this geometry using the Douglas Peucker + * algorithm. + * @see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm + * @abstract + * @param {number} squaredTolerance Squared tolerance. + * @return {module:ol/geom/Geometry} Simplified geometry. + */ + getSimplifiedGeometry(squaredTolerance) {} + + /** + * Get the type of this geometry. + * @abstract + * @return {module:ol/geom/GeometryType} Geometry type. + */ + getType() {} + + /** + * Apply a transform function to each coordinate of the geometry. + * The geometry is modified in place. + * If you do not want the geometry modified in place, first `clone()` it and + * then use this function on the clone. + * @abstract + * @param {module:ol/proj~TransformFunction} transformFn Transform. + */ + applyTransform(transformFn) {} + + /** + * Test if the geometry and the passed extent intersect. + * @abstract + * @param {module:ol/extent~Extent} extent Extent. + * @return {boolean} `true` if the geometry and the extent intersect. + */ + intersectsExtent(extent) {} + + /** + * Translate the geometry. This modifies the geometry coordinates in place. If + * instead you want a new geometry, first `clone()` this geometry. + * @abstract + * @param {number} deltaX Delta X. + * @param {number} deltaY Delta Y. + */ + translate(deltaX, deltaY) {} + + /** + * Transform each coordinate of the geometry from one coordinate reference + * system to another. The geometry is modified in place. + * For example, a line will be transformed to a line and a circle to a circle. + * If you do not want the geometry modified in place, first `clone()` it and + * then use this function on the clone. + * + * @param {module:ol/proj~ProjectionLike} source The current projection. Can be a + * string identifier or a {@link module:ol/proj/Projection~Projection} object. + * @param {module:ol/proj~ProjectionLike} destination The desired projection. Can be a + * string identifier or a {@link module:ol/proj/Projection~Projection} object. + * @return {module:ol/geom/Geometry} This geometry. Note that original geometry is + * modified in place. + * @api + */ + transform(source, destination) { + source = getProjection(source); + const transformFn = source.getUnits() == Units.TILE_PIXELS ? + function(inCoordinates, outCoordinates, stride) { + const pixelExtent = source.getExtent(); + const projectedExtent = source.getWorldExtent(); + const scale = getHeight(projectedExtent) / getHeight(pixelExtent); + composeTransform(tmpTransform, + projectedExtent[0], projectedExtent[3], + scale, -scale, 0, + 0, 0); + transform2D(inCoordinates, 0, inCoordinates.length, stride, + tmpTransform, outCoordinates); + return getTransform(source, destination)(inCoordinates, outCoordinates, stride); + } : + getTransform(source, destination); + this.applyTransform(transformFn); + return this; + } +} inherits(Geometry, BaseObject); @@ -70,61 +254,6 @@ inherits(Geometry, BaseObject); const tmpTransform = createTransform(); -/** - * Make a complete copy of the geometry. - * @abstract - * @return {!module:ol/geom/Geometry} Clone. - */ -Geometry.prototype.clone = function() {}; - - -/** - * @abstract - * @param {number} x X. - * @param {number} y Y. - * @param {module:ol/coordinate~Coordinate} closestPoint Closest point. - * @param {number} minSquaredDistance Minimum squared distance. - * @return {number} Minimum squared distance. - */ -Geometry.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {}; - - -/** - * Return the closest point of the geometry to the passed point as - * {@link module:ol/coordinate~Coordinate coordinate}. - * @param {module:ol/coordinate~Coordinate} point Point. - * @param {module:ol/coordinate~Coordinate=} opt_closestPoint Closest point. - * @return {module:ol/coordinate~Coordinate} Closest point. - * @api - */ -Geometry.prototype.getClosestPoint = function(point, opt_closestPoint) { - const closestPoint = opt_closestPoint ? opt_closestPoint : [NaN, NaN]; - this.closestPointXY(point[0], point[1], closestPoint, Infinity); - return closestPoint; -}; - - -/** - * Returns true if this geometry includes the specified coordinate. If the - * coordinate is on the boundary of the geometry, returns false. - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @return {boolean} Contains coordinate. - * @api - */ -Geometry.prototype.intersectsCoordinate = function(coordinate) { - return this.containsXY(coordinate[0], coordinate[1]); -}; - - -/** - * @abstract - * @param {module:ol/extent~Extent} extent Extent. - * @protected - * @return {module:ol/extent~Extent} extent Extent. - */ -Geometry.prototype.computeExtent = function(extent) {}; - - /** * @param {number} x X. * @param {number} y Y. @@ -133,144 +262,4 @@ Geometry.prototype.computeExtent = function(extent) {}; Geometry.prototype.containsXY = FALSE; -/** - * Get the extent of the geometry. - * @param {module:ol/extent~Extent=} opt_extent Extent. - * @return {module:ol/extent~Extent} extent Extent. - * @api - */ -Geometry.prototype.getExtent = function(opt_extent) { - if (this.extentRevision_ != this.getRevision()) { - this.extent_ = this.computeExtent(this.extent_); - this.extentRevision_ = this.getRevision(); - } - return returnOrUpdate(this.extent_, opt_extent); -}; - - -/** - * Rotate the geometry around a given coordinate. This modifies the geometry - * coordinates in place. - * @abstract - * @param {number} angle Rotation angle in radians. - * @param {module:ol/coordinate~Coordinate} anchor The rotation center. - * @api - */ -Geometry.prototype.rotate = function(angle, anchor) {}; - - -/** - * Scale the geometry (with an optional origin). This modifies the geometry - * coordinates in place. - * @abstract - * @param {number} sx The scaling factor in the x-direction. - * @param {number=} opt_sy The scaling factor in the y-direction (defaults to - * sx). - * @param {module:ol/coordinate~Coordinate=} opt_anchor The scale origin (defaults to the center - * of the geometry extent). - * @api - */ -Geometry.prototype.scale = function(sx, opt_sy, opt_anchor) {}; - - -/** - * Create a simplified version of this geometry. For linestrings, this uses - * the the {@link - * https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm - * Douglas Peucker} algorithm. For polygons, a quantization-based - * simplification is used to preserve topology. - * @function - * @param {number} tolerance The tolerance distance for simplification. - * @return {module:ol/geom/Geometry} A new, simplified version of the original - * geometry. - * @api - */ -Geometry.prototype.simplify = function(tolerance) { - return this.getSimplifiedGeometry(tolerance * tolerance); -}; - - -/** - * Create a simplified version of this geometry using the Douglas Peucker - * algorithm. - * @see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm - * @abstract - * @param {number} squaredTolerance Squared tolerance. - * @return {module:ol/geom/Geometry} Simplified geometry. - */ -Geometry.prototype.getSimplifiedGeometry = function(squaredTolerance) {}; - - -/** - * Get the type of this geometry. - * @abstract - * @return {module:ol/geom/GeometryType} Geometry type. - */ -Geometry.prototype.getType = function() {}; - - -/** - * Apply a transform function to each coordinate of the geometry. - * The geometry is modified in place. - * If you do not want the geometry modified in place, first `clone()` it and - * then use this function on the clone. - * @abstract - * @param {module:ol/proj~TransformFunction} transformFn Transform. - */ -Geometry.prototype.applyTransform = function(transformFn) {}; - - -/** - * Test if the geometry and the passed extent intersect. - * @abstract - * @param {module:ol/extent~Extent} extent Extent. - * @return {boolean} `true` if the geometry and the extent intersect. - */ -Geometry.prototype.intersectsExtent = function(extent) {}; - - -/** - * Translate the geometry. This modifies the geometry coordinates in place. If - * instead you want a new geometry, first `clone()` this geometry. - * @abstract - * @param {number} deltaX Delta X. - * @param {number} deltaY Delta Y. - */ -Geometry.prototype.translate = function(deltaX, deltaY) {}; - - -/** - * Transform each coordinate of the geometry from one coordinate reference - * system to another. The geometry is modified in place. - * For example, a line will be transformed to a line and a circle to a circle. - * If you do not want the geometry modified in place, first `clone()` it and - * then use this function on the clone. - * - * @param {module:ol/proj~ProjectionLike} source The current projection. Can be a - * string identifier or a {@link module:ol/proj/Projection~Projection} object. - * @param {module:ol/proj~ProjectionLike} destination The desired projection. Can be a - * string identifier or a {@link module:ol/proj/Projection~Projection} object. - * @return {module:ol/geom/Geometry} This geometry. Note that original geometry is - * modified in place. - * @api - */ -Geometry.prototype.transform = function(source, destination) { - source = getProjection(source); - const transformFn = source.getUnits() == Units.TILE_PIXELS ? - function(inCoordinates, outCoordinates, stride) { - const pixelExtent = source.getExtent(); - const projectedExtent = source.getWorldExtent(); - const scale = getHeight(projectedExtent) / getHeight(pixelExtent); - composeTransform(tmpTransform, - projectedExtent[0], projectedExtent[3], - scale, -scale, 0, - 0, 0); - transform2D(inCoordinates, 0, inCoordinates.length, stride, - tmpTransform, outCoordinates); - return getTransform(source, destination)(inCoordinates, outCoordinates, stride); - } : - getTransform(source, destination); - this.applyTransform(transformFn); - return this; -}; export default Geometry; diff --git a/src/ol/geom/GeometryCollection.js b/src/ol/geom/GeometryCollection.js index 6e4a1ee673..d7a85c4a6e 100644 --- a/src/ol/geom/GeometryCollection.js +++ b/src/ol/geom/GeometryCollection.js @@ -18,18 +18,268 @@ import {clear} from '../obj.js'; * @param {Array.=} opt_geometries Geometries. * @api */ -const GeometryCollection = function(opt_geometries) { +class GeometryCollection { + constructor(opt_geometries) { - Geometry.call(this); + Geometry.call(this); + + /** + * @private + * @type {Array.} + */ + this.geometries_ = opt_geometries ? opt_geometries : null; + + this.listenGeometriesChange_(); + } /** * @private - * @type {Array.} */ - this.geometries_ = opt_geometries ? opt_geometries : null; + unlistenGeometriesChange_() { + if (!this.geometries_) { + return; + } + for (let i = 0, ii = this.geometries_.length; i < ii; ++i) { + unlisten( + this.geometries_[i], EventType.CHANGE, + this.changed, this); + } + } - this.listenGeometriesChange_(); -}; + /** + * @private + */ + listenGeometriesChange_() { + if (!this.geometries_) { + return; + } + for (let i = 0, ii = this.geometries_.length; i < ii; ++i) { + listen( + this.geometries_[i], EventType.CHANGE, + this.changed, this); + } + } + + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/GeometryCollection} Clone. + * @override + * @api + */ + clone() { + const geometryCollection = new GeometryCollection(null); + geometryCollection.setGeometries(this.geometries_); + return geometryCollection; + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { + return minSquaredDistance; + } + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + minSquaredDistance = geometries[i].closestPointXY( + x, y, closestPoint, minSquaredDistance); + } + return minSquaredDistance; + } + + /** + * @inheritDoc + */ + containsXY(x, y) { + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + if (geometries[i].containsXY(x, y)) { + return true; + } + } + return false; + } + + /** + * @inheritDoc + */ + computeExtent(extent) { + createOrUpdateEmpty(extent); + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + extend(extent, geometries[i].getExtent()); + } + return extent; + } + + /** + * Return the geometries that make up this geometry collection. + * @return {Array.} Geometries. + * @api + */ + getGeometries() { + return cloneGeometries(this.geometries_); + } + + /** + * @return {Array.} Geometries. + */ + getGeometriesArray() { + return this.geometries_; + } + + /** + * @inheritDoc + */ + getSimplifiedGeometry(squaredTolerance) { + if (this.simplifiedGeometryRevision != this.getRevision()) { + clear(this.simplifiedGeometryCache); + this.simplifiedGeometryMaxMinSquaredTolerance = 0; + this.simplifiedGeometryRevision = this.getRevision(); + } + if (squaredTolerance < 0 || + (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 && + squaredTolerance < this.simplifiedGeometryMaxMinSquaredTolerance)) { + return this; + } + const key = squaredTolerance.toString(); + if (this.simplifiedGeometryCache.hasOwnProperty(key)) { + return this.simplifiedGeometryCache[key]; + } else { + const simplifiedGeometries = []; + const geometries = this.geometries_; + let simplified = false; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + const geometry = geometries[i]; + const simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance); + simplifiedGeometries.push(simplifiedGeometry); + if (simplifiedGeometry !== geometry) { + simplified = true; + } + } + if (simplified) { + const simplifiedGeometryCollection = new GeometryCollection(null); + simplifiedGeometryCollection.setGeometriesArray(simplifiedGeometries); + this.simplifiedGeometryCache[key] = simplifiedGeometryCollection; + return simplifiedGeometryCollection; + } else { + this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance; + return this; + } + } + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.GEOMETRY_COLLECTION; + } + + /** + * @inheritDoc + * @api + */ + intersectsExtent(extent) { + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + if (geometries[i].intersectsExtent(extent)) { + return true; + } + } + return false; + } + + /** + * @return {boolean} Is empty. + */ + isEmpty() { + return this.geometries_.length === 0; + } + + /** + * @inheritDoc + * @api + */ + rotate(angle, anchor) { + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + geometries[i].rotate(angle, anchor); + } + this.changed(); + } + + /** + * @inheritDoc + * @api + */ + scale(sx, opt_sy, opt_anchor) { + let anchor = opt_anchor; + if (!anchor) { + anchor = getCenter(this.getExtent()); + } + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + geometries[i].scale(sx, opt_sy, anchor); + } + this.changed(); + } + + /** + * Set the geometries that make up this geometry collection. + * @param {Array.} geometries Geometries. + * @api + */ + setGeometries(geometries) { + this.setGeometriesArray(cloneGeometries(geometries)); + } + + /** + * @param {Array.} geometries Geometries. + */ + setGeometriesArray(geometries) { + this.unlistenGeometriesChange_(); + this.geometries_ = geometries; + this.listenGeometriesChange_(); + this.changed(); + } + + /** + * @inheritDoc + * @api + */ + applyTransform(transformFn) { + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + geometries[i].applyTransform(transformFn); + } + this.changed(); + } + + /** + * Translate the geometry. + * @param {number} deltaX Delta X. + * @param {number} deltaY Delta Y. + * @override + * @api + */ + translate(deltaX, deltaY) { + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + geometries[i].translate(deltaX, deltaY); + } + this.changed(); + } + + /** + * @inheritDoc + */ + disposeInternal() { + this.unlistenGeometriesChange_(); + Geometry.prototype.disposeInternal.call(this); + } +} inherits(GeometryCollection, Geometry); @@ -47,269 +297,4 @@ function cloneGeometries(geometries) { } -/** - * @private - */ -GeometryCollection.prototype.unlistenGeometriesChange_ = function() { - if (!this.geometries_) { - return; - } - for (let i = 0, ii = this.geometries_.length; i < ii; ++i) { - unlisten( - this.geometries_[i], EventType.CHANGE, - this.changed, this); - } -}; - - -/** - * @private - */ -GeometryCollection.prototype.listenGeometriesChange_ = function() { - if (!this.geometries_) { - return; - } - for (let i = 0, ii = this.geometries_.length; i < ii; ++i) { - listen( - this.geometries_[i], EventType.CHANGE, - this.changed, this); - } -}; - - -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/GeometryCollection} Clone. - * @override - * @api - */ -GeometryCollection.prototype.clone = function() { - const geometryCollection = new GeometryCollection(null); - geometryCollection.setGeometries(this.geometries_); - return geometryCollection; -}; - - -/** - * @inheritDoc - */ -GeometryCollection.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { - return minSquaredDistance; - } - const geometries = this.geometries_; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - minSquaredDistance = geometries[i].closestPointXY( - x, y, closestPoint, minSquaredDistance); - } - return minSquaredDistance; -}; - - -/** - * @inheritDoc - */ -GeometryCollection.prototype.containsXY = function(x, y) { - const geometries = this.geometries_; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - if (geometries[i].containsXY(x, y)) { - return true; - } - } - return false; -}; - - -/** - * @inheritDoc - */ -GeometryCollection.prototype.computeExtent = function(extent) { - createOrUpdateEmpty(extent); - const geometries = this.geometries_; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - extend(extent, geometries[i].getExtent()); - } - return extent; -}; - - -/** - * Return the geometries that make up this geometry collection. - * @return {Array.} Geometries. - * @api - */ -GeometryCollection.prototype.getGeometries = function() { - return cloneGeometries(this.geometries_); -}; - - -/** - * @return {Array.} Geometries. - */ -GeometryCollection.prototype.getGeometriesArray = function() { - return this.geometries_; -}; - - -/** - * @inheritDoc - */ -GeometryCollection.prototype.getSimplifiedGeometry = function(squaredTolerance) { - if (this.simplifiedGeometryRevision != this.getRevision()) { - clear(this.simplifiedGeometryCache); - this.simplifiedGeometryMaxMinSquaredTolerance = 0; - this.simplifiedGeometryRevision = this.getRevision(); - } - if (squaredTolerance < 0 || - (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 && - squaredTolerance < this.simplifiedGeometryMaxMinSquaredTolerance)) { - return this; - } - const key = squaredTolerance.toString(); - if (this.simplifiedGeometryCache.hasOwnProperty(key)) { - return this.simplifiedGeometryCache[key]; - } else { - const simplifiedGeometries = []; - const geometries = this.geometries_; - let simplified = false; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - const geometry = geometries[i]; - const simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance); - simplifiedGeometries.push(simplifiedGeometry); - if (simplifiedGeometry !== geometry) { - simplified = true; - } - } - if (simplified) { - const simplifiedGeometryCollection = new GeometryCollection(null); - simplifiedGeometryCollection.setGeometriesArray(simplifiedGeometries); - this.simplifiedGeometryCache[key] = simplifiedGeometryCollection; - return simplifiedGeometryCollection; - } else { - this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance; - return this; - } - } -}; - - -/** - * @inheritDoc - * @api - */ -GeometryCollection.prototype.getType = function() { - return GeometryType.GEOMETRY_COLLECTION; -}; - - -/** - * @inheritDoc - * @api - */ -GeometryCollection.prototype.intersectsExtent = function(extent) { - const geometries = this.geometries_; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - if (geometries[i].intersectsExtent(extent)) { - return true; - } - } - return false; -}; - - -/** - * @return {boolean} Is empty. - */ -GeometryCollection.prototype.isEmpty = function() { - return this.geometries_.length === 0; -}; - - -/** - * @inheritDoc - * @api - */ -GeometryCollection.prototype.rotate = function(angle, anchor) { - const geometries = this.geometries_; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - geometries[i].rotate(angle, anchor); - } - this.changed(); -}; - - -/** - * @inheritDoc - * @api - */ -GeometryCollection.prototype.scale = function(sx, opt_sy, opt_anchor) { - let anchor = opt_anchor; - if (!anchor) { - anchor = getCenter(this.getExtent()); - } - const geometries = this.geometries_; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - geometries[i].scale(sx, opt_sy, anchor); - } - this.changed(); -}; - - -/** - * Set the geometries that make up this geometry collection. - * @param {Array.} geometries Geometries. - * @api - */ -GeometryCollection.prototype.setGeometries = function(geometries) { - this.setGeometriesArray(cloneGeometries(geometries)); -}; - - -/** - * @param {Array.} geometries Geometries. - */ -GeometryCollection.prototype.setGeometriesArray = function(geometries) { - this.unlistenGeometriesChange_(); - this.geometries_ = geometries; - this.listenGeometriesChange_(); - this.changed(); -}; - - -/** - * @inheritDoc - * @api - */ -GeometryCollection.prototype.applyTransform = function(transformFn) { - const geometries = this.geometries_; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - geometries[i].applyTransform(transformFn); - } - this.changed(); -}; - - -/** - * Translate the geometry. - * @param {number} deltaX Delta X. - * @param {number} deltaY Delta Y. - * @override - * @api - */ -GeometryCollection.prototype.translate = function(deltaX, deltaY) { - const geometries = this.geometries_; - for (let i = 0, ii = geometries.length; i < ii; ++i) { - geometries[i].translate(deltaX, deltaY); - } - this.changed(); -}; - - -/** - * @inheritDoc - */ -GeometryCollection.prototype.disposeInternal = function() { - this.unlistenGeometriesChange_(); - Geometry.prototype.disposeInternal.call(this); -}; export default GeometryCollection; diff --git a/src/ol/geom/LineString.js b/src/ol/geom/LineString.js index 5154767dec..69faab0d62 100644 --- a/src/ol/geom/LineString.js +++ b/src/ol/geom/LineString.js @@ -28,229 +28,219 @@ import {douglasPeucker} from '../geom/flat/simplify.js'; * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. * @api */ -const LineString = function(coordinates, opt_layout) { +class LineString { + constructor(coordinates, opt_layout) { - SimpleGeometry.call(this); + SimpleGeometry.call(this); - /** - * @private - * @type {module:ol/coordinate~Coordinate} - */ - this.flatMidpoint_ = null; + /** + * @private + * @type {module:ol/coordinate~Coordinate} + */ + this.flatMidpoint_ = null; - /** - * @private - * @type {number} - */ - this.flatMidpointRevision_ = -1; + /** + * @private + * @type {number} + */ + this.flatMidpointRevision_ = -1; - /** - * @private - * @type {number} - */ - this.maxDelta_ = -1; + /** + * @private + * @type {number} + */ + this.maxDelta_ = -1; - /** - * @private - * @type {number} - */ - this.maxDeltaRevision_ = -1; + /** + * @private + * @type {number} + */ + this.maxDeltaRevision_ = -1; + + if (opt_layout !== undefined && !Array.isArray(coordinates[0])) { + this.setFlatCoordinates(opt_layout, coordinates); + } else { + this.setCoordinates(coordinates, opt_layout); + } - if (opt_layout !== undefined && !Array.isArray(coordinates[0])) { - this.setFlatCoordinates(opt_layout, coordinates); - } else { - this.setCoordinates(coordinates, opt_layout); } -}; + /** + * Append the passed coordinate to the coordinates of the linestring. + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @api + */ + appendCoordinate(coordinate) { + if (!this.flatCoordinates) { + this.flatCoordinates = coordinate.slice(); + } else { + extend(this.flatCoordinates, coordinate); + } + this.changed(); + } + + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/LineString} Clone. + * @override + * @api + */ + clone() { + return new LineString(this.flatCoordinates.slice(), this.layout); + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { + return minSquaredDistance; + } + if (this.maxDeltaRevision_ != this.getRevision()) { + this.maxDelta_ = Math.sqrt(maxSquaredDelta( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0)); + this.maxDeltaRevision_ = this.getRevision(); + } + return assignClosestPoint( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, + this.maxDelta_, false, x, y, closestPoint, minSquaredDistance); + } + + /** + * Iterate over each segment, calling the provided callback. + * If the callback returns a truthy value the function returns that + * value immediately. Otherwise the function returns `false`. + * + * @param {function(this: S, module:ol/coordinate~Coordinate, module:ol/coordinate~Coordinate): T} callback Function + * called for each segment. + * @return {T|boolean} Value. + * @template T,S + * @api + */ + forEachSegment(callback) { + return forEachSegment(this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, callback); + } + + /** + * Returns the coordinate at `m` using linear interpolation, or `null` if no + * such coordinate exists. + * + * `opt_extrapolate` controls extrapolation beyond the range of Ms in the + * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first + * M will return the first coordinate and Ms greater than the last M will + * return the last coordinate. + * + * @param {number} m M. + * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`. + * @return {module:ol/coordinate~Coordinate} Coordinate. + * @api + */ + getCoordinateAtM(m, opt_extrapolate) { + if (this.layout != GeometryLayout.XYM && + this.layout != GeometryLayout.XYZM) { + return null; + } + const extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false; + return lineStringCoordinateAtM(this.flatCoordinates, 0, + this.flatCoordinates.length, this.stride, m, extrapolate); + } + + /** + * Return the coordinates of the linestring. + * @return {Array.} Coordinates. + * @override + * @api + */ + getCoordinates() { + return inflateCoordinates( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); + } + + /** + * Return the coordinate at the provided fraction along the linestring. + * The `fraction` is a number between 0 and 1, where 0 is the start of the + * linestring and 1 is the end. + * @param {number} fraction Fraction. + * @param {module:ol/coordinate~Coordinate=} opt_dest Optional coordinate whose values will + * be modified. If not provided, a new coordinate will be returned. + * @return {module:ol/coordinate~Coordinate} Coordinate of the interpolated point. + * @api + */ + getCoordinateAt(fraction, opt_dest) { + return interpolatePoint( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, + fraction, opt_dest); + } + + /** + * Return the length of the linestring on projected plane. + * @return {number} Length (on projected plane). + * @api + */ + getLength() { + return lineStringLength( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); + } + + /** + * @return {Array.} Flat midpoint. + */ + getFlatMidpoint() { + if (this.flatMidpointRevision_ != this.getRevision()) { + this.flatMidpoint_ = this.getCoordinateAt(0.5, this.flatMidpoint_); + this.flatMidpointRevision_ = this.getRevision(); + } + return this.flatMidpoint_; + } + + /** + * @inheritDoc + */ + getSimplifiedGeometryInternal(squaredTolerance) { + const simplifiedFlatCoordinates = []; + simplifiedFlatCoordinates.length = douglasPeucker( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, + squaredTolerance, simplifiedFlatCoordinates, 0); + return new LineString(simplifiedFlatCoordinates, GeometryLayout.XY); + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.LINE_STRING; + } + + /** + * @inheritDoc + * @api + */ + intersectsExtent(extent) { + return intersectsLineString( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, + extent); + } + + /** + * Set the coordinates of the linestring. + * @param {!Array.} coordinates Coordinates. + * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. + * @override + * @api + */ + setCoordinates(coordinates, opt_layout) { + this.setLayout(opt_layout, coordinates, 1); + if (!this.flatCoordinates) { + this.flatCoordinates = []; + } + this.flatCoordinates.length = deflateCoordinates( + this.flatCoordinates, 0, coordinates, this.stride); + this.changed(); + } +} inherits(LineString, SimpleGeometry); -/** - * Append the passed coordinate to the coordinates of the linestring. - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @api - */ -LineString.prototype.appendCoordinate = function(coordinate) { - if (!this.flatCoordinates) { - this.flatCoordinates = coordinate.slice(); - } else { - extend(this.flatCoordinates, coordinate); - } - this.changed(); -}; - - -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/LineString} Clone. - * @override - * @api - */ -LineString.prototype.clone = function() { - return new LineString(this.flatCoordinates.slice(), this.layout); -}; - - -/** - * @inheritDoc - */ -LineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { - return minSquaredDistance; - } - if (this.maxDeltaRevision_ != this.getRevision()) { - this.maxDelta_ = Math.sqrt(maxSquaredDelta( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0)); - this.maxDeltaRevision_ = this.getRevision(); - } - return assignClosestPoint( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, - this.maxDelta_, false, x, y, closestPoint, minSquaredDistance); -}; - - -/** - * Iterate over each segment, calling the provided callback. - * If the callback returns a truthy value the function returns that - * value immediately. Otherwise the function returns `false`. - * - * @param {function(this: S, module:ol/coordinate~Coordinate, module:ol/coordinate~Coordinate): T} callback Function - * called for each segment. - * @return {T|boolean} Value. - * @template T,S - * @api - */ -LineString.prototype.forEachSegment = function(callback) { - return forEachSegment(this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, callback); -}; - - -/** - * Returns the coordinate at `m` using linear interpolation, or `null` if no - * such coordinate exists. - * - * `opt_extrapolate` controls extrapolation beyond the range of Ms in the - * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first - * M will return the first coordinate and Ms greater than the last M will - * return the last coordinate. - * - * @param {number} m M. - * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`. - * @return {module:ol/coordinate~Coordinate} Coordinate. - * @api - */ -LineString.prototype.getCoordinateAtM = function(m, opt_extrapolate) { - if (this.layout != GeometryLayout.XYM && - this.layout != GeometryLayout.XYZM) { - return null; - } - const extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false; - return lineStringCoordinateAtM(this.flatCoordinates, 0, - this.flatCoordinates.length, this.stride, m, extrapolate); -}; - - -/** - * Return the coordinates of the linestring. - * @return {Array.} Coordinates. - * @override - * @api - */ -LineString.prototype.getCoordinates = function() { - return inflateCoordinates( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); -}; - - -/** - * Return the coordinate at the provided fraction along the linestring. - * The `fraction` is a number between 0 and 1, where 0 is the start of the - * linestring and 1 is the end. - * @param {number} fraction Fraction. - * @param {module:ol/coordinate~Coordinate=} opt_dest Optional coordinate whose values will - * be modified. If not provided, a new coordinate will be returned. - * @return {module:ol/coordinate~Coordinate} Coordinate of the interpolated point. - * @api - */ -LineString.prototype.getCoordinateAt = function(fraction, opt_dest) { - return interpolatePoint( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, - fraction, opt_dest); -}; - - -/** - * Return the length of the linestring on projected plane. - * @return {number} Length (on projected plane). - * @api - */ -LineString.prototype.getLength = function() { - return lineStringLength( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); -}; - - -/** - * @return {Array.} Flat midpoint. - */ -LineString.prototype.getFlatMidpoint = function() { - if (this.flatMidpointRevision_ != this.getRevision()) { - this.flatMidpoint_ = this.getCoordinateAt(0.5, this.flatMidpoint_); - this.flatMidpointRevision_ = this.getRevision(); - } - return this.flatMidpoint_; -}; - - -/** - * @inheritDoc - */ -LineString.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) { - const simplifiedFlatCoordinates = []; - simplifiedFlatCoordinates.length = douglasPeucker( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, - squaredTolerance, simplifiedFlatCoordinates, 0); - return new LineString(simplifiedFlatCoordinates, GeometryLayout.XY); -}; - - -/** - * @inheritDoc - * @api - */ -LineString.prototype.getType = function() { - return GeometryType.LINE_STRING; -}; - - -/** - * @inheritDoc - * @api - */ -LineString.prototype.intersectsExtent = function(extent) { - return intersectsLineString( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, - extent); -}; - - -/** - * Set the coordinates of the linestring. - * @param {!Array.} coordinates Coordinates. - * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. - * @override - * @api - */ -LineString.prototype.setCoordinates = function(coordinates, opt_layout) { - this.setLayout(opt_layout, coordinates, 1); - if (!this.flatCoordinates) { - this.flatCoordinates = []; - } - this.flatCoordinates.length = deflateCoordinates( - this.flatCoordinates, 0, coordinates, this.stride); - this.changed(); -}; - export default LineString; diff --git a/src/ol/geom/LinearRing.js b/src/ol/geom/LinearRing.js index dadfe3ac2f..9cb2c5149f 100644 --- a/src/ol/geom/LinearRing.js +++ b/src/ol/geom/LinearRing.js @@ -25,125 +25,121 @@ import {douglasPeucker} from '../geom/flat/simplify.js'; * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. * @api */ -const LinearRing = function(coordinates, opt_layout) { +class LinearRing { + constructor(coordinates, opt_layout) { - SimpleGeometry.call(this); + SimpleGeometry.call(this); - /** - * @private - * @type {number} - */ - this.maxDelta_ = -1; + /** + * @private + * @type {number} + */ + this.maxDelta_ = -1; - /** - * @private - * @type {number} - */ - this.maxDeltaRevision_ = -1; + /** + * @private + * @type {number} + */ + this.maxDeltaRevision_ = -1; + + if (opt_layout !== undefined && !Array.isArray(coordinates[0])) { + this.setFlatCoordinates(opt_layout, coordinates); + } else { + this.setCoordinates(coordinates, opt_layout); + } - if (opt_layout !== undefined && !Array.isArray(coordinates[0])) { - this.setFlatCoordinates(opt_layout, coordinates); - } else { - this.setCoordinates(coordinates, opt_layout); } -}; + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/LinearRing} Clone. + * @override + * @api + */ + clone() { + return new LinearRing(this.flatCoordinates.slice(), this.layout); + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { + return minSquaredDistance; + } + if (this.maxDeltaRevision_ != this.getRevision()) { + this.maxDelta_ = Math.sqrt(maxSquaredDelta( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0)); + this.maxDeltaRevision_ = this.getRevision(); + } + return assignClosestPoint( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, + this.maxDelta_, true, x, y, closestPoint, minSquaredDistance); + } + + /** + * Return the area of the linear ring on projected plane. + * @return {number} Area (on projected plane). + * @api + */ + getArea() { + return linearRingArea(this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); + } + + /** + * Return the coordinates of the linear ring. + * @return {Array.} Coordinates. + * @override + * @api + */ + getCoordinates() { + return inflateCoordinates( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); + } + + /** + * @inheritDoc + */ + getSimplifiedGeometryInternal(squaredTolerance) { + const simplifiedFlatCoordinates = []; + simplifiedFlatCoordinates.length = douglasPeucker( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, + squaredTolerance, simplifiedFlatCoordinates, 0); + return new LinearRing(simplifiedFlatCoordinates, GeometryLayout.XY); + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.LINEAR_RING; + } + + /** + * @inheritDoc + */ + intersectsExtent(extent) {} + + /** + * Set the coordinates of the linear ring. + * @param {!Array.} coordinates Coordinates. + * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. + * @override + * @api + */ + setCoordinates(coordinates, opt_layout) { + this.setLayout(opt_layout, coordinates, 1); + if (!this.flatCoordinates) { + this.flatCoordinates = []; + } + this.flatCoordinates.length = deflateCoordinates( + this.flatCoordinates, 0, coordinates, this.stride); + this.changed(); + } +} inherits(LinearRing, SimpleGeometry); -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/LinearRing} Clone. - * @override - * @api - */ -LinearRing.prototype.clone = function() { - return new LinearRing(this.flatCoordinates.slice(), this.layout); -}; - - -/** - * @inheritDoc - */ -LinearRing.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { - return minSquaredDistance; - } - if (this.maxDeltaRevision_ != this.getRevision()) { - this.maxDelta_ = Math.sqrt(maxSquaredDelta( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0)); - this.maxDeltaRevision_ = this.getRevision(); - } - return assignClosestPoint( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, - this.maxDelta_, true, x, y, closestPoint, minSquaredDistance); -}; - - -/** - * Return the area of the linear ring on projected plane. - * @return {number} Area (on projected plane). - * @api - */ -LinearRing.prototype.getArea = function() { - return linearRingArea(this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); -}; - - -/** - * Return the coordinates of the linear ring. - * @return {Array.} Coordinates. - * @override - * @api - */ -LinearRing.prototype.getCoordinates = function() { - return inflateCoordinates( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); -}; - - -/** - * @inheritDoc - */ -LinearRing.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) { - const simplifiedFlatCoordinates = []; - simplifiedFlatCoordinates.length = douglasPeucker( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, - squaredTolerance, simplifiedFlatCoordinates, 0); - return new LinearRing(simplifiedFlatCoordinates, GeometryLayout.XY); -}; - - -/** - * @inheritDoc - * @api - */ -LinearRing.prototype.getType = function() { - return GeometryType.LINEAR_RING; -}; - - -/** - * @inheritDoc - */ -LinearRing.prototype.intersectsExtent = function(extent) {}; - - -/** - * Set the coordinates of the linear ring. - * @param {!Array.} coordinates Coordinates. - * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. - * @override - * @api - */ -LinearRing.prototype.setCoordinates = function(coordinates, opt_layout) { - this.setLayout(opt_layout, coordinates, 1); - if (!this.flatCoordinates) { - this.flatCoordinates = []; - } - this.flatCoordinates.length = deflateCoordinates( - this.flatCoordinates, 0, coordinates, this.stride); - this.changed(); -}; export default LinearRing; diff --git a/src/ol/geom/MultiLineString.js b/src/ol/geom/MultiLineString.js index fcff60af99..ab9e92b952 100644 --- a/src/ol/geom/MultiLineString.js +++ b/src/ol/geom/MultiLineString.js @@ -28,258 +28,249 @@ import {douglasPeuckerArray} from '../geom/flat/simplify.js'; * @param {Array.} opt_ends Flat coordinate ends for internal use. * @api */ -const MultiLineString = function(coordinates, opt_layout, opt_ends) { +class MultiLineString { + constructor(coordinates, opt_layout, opt_ends) { - SimpleGeometry.call(this); + SimpleGeometry.call(this); - /** - * @type {Array.} - * @private - */ - this.ends_ = []; + /** + * @type {Array.} + * @private + */ + this.ends_ = []; - /** - * @private - * @type {number} - */ - this.maxDelta_ = -1; + /** + * @private + * @type {number} + */ + this.maxDelta_ = -1; - /** - * @private - * @type {number} - */ - this.maxDeltaRevision_ = -1; + /** + * @private + * @type {number} + */ + this.maxDeltaRevision_ = -1; - if (Array.isArray(coordinates[0])) { - this.setCoordinates(coordinates, opt_layout); - } else if (opt_layout !== undefined && opt_ends) { - this.setFlatCoordinates(opt_layout, coordinates); - this.ends_ = opt_ends; - } else { - let layout = this.getLayout(); - const flatCoordinates = []; - const ends = []; - for (let i = 0, ii = coordinates.length; i < ii; ++i) { - const lineString = coordinates[i]; - if (i === 0) { - layout = lineString.getLayout(); + if (Array.isArray(coordinates[0])) { + this.setCoordinates(coordinates, opt_layout); + } else if (opt_layout !== undefined && opt_ends) { + this.setFlatCoordinates(opt_layout, coordinates); + this.ends_ = opt_ends; + } else { + let layout = this.getLayout(); + const flatCoordinates = []; + const ends = []; + for (let i = 0, ii = coordinates.length; i < ii; ++i) { + const lineString = coordinates[i]; + if (i === 0) { + layout = lineString.getLayout(); + } + extend(flatCoordinates, lineString.getFlatCoordinates()); + ends.push(flatCoordinates.length); } - extend(flatCoordinates, lineString.getFlatCoordinates()); - ends.push(flatCoordinates.length); + this.setFlatCoordinates(layout, flatCoordinates); + this.ends_ = ends; } - this.setFlatCoordinates(layout, flatCoordinates); - this.ends_ = ends; + } -}; + /** + * Append the passed linestring to the multilinestring. + * @param {module:ol/geom/LineString} lineString LineString. + * @api + */ + appendLineString(lineString) { + if (!this.flatCoordinates) { + this.flatCoordinates = lineString.getFlatCoordinates().slice(); + } else { + extend(this.flatCoordinates, lineString.getFlatCoordinates().slice()); + } + this.ends_.push(this.flatCoordinates.length); + this.changed(); + } + + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/MultiLineString} Clone. + * @override + * @api + */ + clone() { + return new MultiLineString(this.flatCoordinates.slice(), this.layout, this.ends_.slice()); + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { + return minSquaredDistance; + } + if (this.maxDeltaRevision_ != this.getRevision()) { + this.maxDelta_ = Math.sqrt(arrayMaxSquaredDelta( + this.flatCoordinates, 0, this.ends_, this.stride, 0)); + this.maxDeltaRevision_ = this.getRevision(); + } + return assignClosestArrayPoint( + this.flatCoordinates, 0, this.ends_, this.stride, + this.maxDelta_, false, x, y, closestPoint, minSquaredDistance); + } + + /** + * Returns the coordinate at `m` using linear interpolation, or `null` if no + * such coordinate exists. + * + * `opt_extrapolate` controls extrapolation beyond the range of Ms in the + * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first + * M will return the first coordinate and Ms greater than the last M will + * return the last coordinate. + * + * `opt_interpolate` controls interpolation between consecutive LineStrings + * within the MultiLineString. If `opt_interpolate` is `true` the coordinates + * will be linearly interpolated between the last coordinate of one LineString + * and the first coordinate of the next LineString. If `opt_interpolate` is + * `false` then the function will return `null` for Ms falling between + * LineStrings. + * + * @param {number} m M. + * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`. + * @param {boolean=} opt_interpolate Interpolate. Default is `false`. + * @return {module:ol/coordinate~Coordinate} Coordinate. + * @api + */ + getCoordinateAtM(m, opt_extrapolate, opt_interpolate) { + if ((this.layout != GeometryLayout.XYM && + this.layout != GeometryLayout.XYZM) || + this.flatCoordinates.length === 0) { + return null; + } + const extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false; + const interpolate = opt_interpolate !== undefined ? opt_interpolate : false; + return lineStringsCoordinateAtM(this.flatCoordinates, 0, + this.ends_, this.stride, m, extrapolate, interpolate); + } + + /** + * Return the coordinates of the multilinestring. + * @return {Array.>} Coordinates. + * @override + * @api + */ + getCoordinates() { + return inflateCoordinatesArray( + this.flatCoordinates, 0, this.ends_, this.stride); + } + + /** + * @return {Array.} Ends. + */ + getEnds() { + return this.ends_; + } + + /** + * Return the linestring at the specified index. + * @param {number} index Index. + * @return {module:ol/geom/LineString} LineString. + * @api + */ + getLineString(index) { + if (index < 0 || this.ends_.length <= index) { + return null; + } + return new LineString(this.flatCoordinates.slice( + index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]), this.layout); + } + + /** + * Return the linestrings of this multilinestring. + * @return {Array.} LineStrings. + * @api + */ + getLineStrings() { + const flatCoordinates = this.flatCoordinates; + const ends = this.ends_; + const layout = this.layout; + /** @type {Array.} */ + const lineStrings = []; + let offset = 0; + for (let i = 0, ii = ends.length; i < ii; ++i) { + const end = ends[i]; + const lineString = new LineString(flatCoordinates.slice(offset, end), layout); + lineStrings.push(lineString); + offset = end; + } + return lineStrings; + } + + /** + * @return {Array.} Flat midpoints. + */ + getFlatMidpoints() { + const midpoints = []; + const flatCoordinates = this.flatCoordinates; + let offset = 0; + const ends = this.ends_; + const stride = this.stride; + for (let i = 0, ii = ends.length; i < ii; ++i) { + const end = ends[i]; + const midpoint = interpolatePoint( + flatCoordinates, offset, end, stride, 0.5); + extend(midpoints, midpoint); + offset = end; + } + return midpoints; + } + + /** + * @inheritDoc + */ + getSimplifiedGeometryInternal(squaredTolerance) { + const simplifiedFlatCoordinates = []; + const simplifiedEnds = []; + simplifiedFlatCoordinates.length = douglasPeuckerArray( + this.flatCoordinates, 0, this.ends_, this.stride, squaredTolerance, + simplifiedFlatCoordinates, 0, simplifiedEnds); + return new MultiLineString(simplifiedFlatCoordinates, GeometryLayout.XY, simplifiedEnds); + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.MULTI_LINE_STRING; + } + + /** + * @inheritDoc + * @api + */ + intersectsExtent(extent) { + return intersectsLineStringArray( + this.flatCoordinates, 0, this.ends_, this.stride, extent); + } + + /** + * Set the coordinates of the multilinestring. + * @param {!Array.>} coordinates Coordinates. + * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. + * @override + * @api + */ + setCoordinates(coordinates, opt_layout) { + this.setLayout(opt_layout, coordinates, 2); + if (!this.flatCoordinates) { + this.flatCoordinates = []; + } + const ends = deflateCoordinatesArray( + this.flatCoordinates, 0, coordinates, this.stride, this.ends_); + this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1]; + this.changed(); + } +} inherits(MultiLineString, SimpleGeometry); -/** - * Append the passed linestring to the multilinestring. - * @param {module:ol/geom/LineString} lineString LineString. - * @api - */ -MultiLineString.prototype.appendLineString = function(lineString) { - if (!this.flatCoordinates) { - this.flatCoordinates = lineString.getFlatCoordinates().slice(); - } else { - extend(this.flatCoordinates, lineString.getFlatCoordinates().slice()); - } - this.ends_.push(this.flatCoordinates.length); - this.changed(); -}; - - -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/MultiLineString} Clone. - * @override - * @api - */ -MultiLineString.prototype.clone = function() { - return new MultiLineString(this.flatCoordinates.slice(), this.layout, this.ends_.slice()); -}; - - -/** - * @inheritDoc - */ -MultiLineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { - return minSquaredDistance; - } - if (this.maxDeltaRevision_ != this.getRevision()) { - this.maxDelta_ = Math.sqrt(arrayMaxSquaredDelta( - this.flatCoordinates, 0, this.ends_, this.stride, 0)); - this.maxDeltaRevision_ = this.getRevision(); - } - return assignClosestArrayPoint( - this.flatCoordinates, 0, this.ends_, this.stride, - this.maxDelta_, false, x, y, closestPoint, minSquaredDistance); -}; - - -/** - * Returns the coordinate at `m` using linear interpolation, or `null` if no - * such coordinate exists. - * - * `opt_extrapolate` controls extrapolation beyond the range of Ms in the - * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first - * M will return the first coordinate and Ms greater than the last M will - * return the last coordinate. - * - * `opt_interpolate` controls interpolation between consecutive LineStrings - * within the MultiLineString. If `opt_interpolate` is `true` the coordinates - * will be linearly interpolated between the last coordinate of one LineString - * and the first coordinate of the next LineString. If `opt_interpolate` is - * `false` then the function will return `null` for Ms falling between - * LineStrings. - * - * @param {number} m M. - * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`. - * @param {boolean=} opt_interpolate Interpolate. Default is `false`. - * @return {module:ol/coordinate~Coordinate} Coordinate. - * @api - */ -MultiLineString.prototype.getCoordinateAtM = function(m, opt_extrapolate, opt_interpolate) { - if ((this.layout != GeometryLayout.XYM && - this.layout != GeometryLayout.XYZM) || - this.flatCoordinates.length === 0) { - return null; - } - const extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false; - const interpolate = opt_interpolate !== undefined ? opt_interpolate : false; - return lineStringsCoordinateAtM(this.flatCoordinates, 0, - this.ends_, this.stride, m, extrapolate, interpolate); -}; - - -/** - * Return the coordinates of the multilinestring. - * @return {Array.>} Coordinates. - * @override - * @api - */ -MultiLineString.prototype.getCoordinates = function() { - return inflateCoordinatesArray( - this.flatCoordinates, 0, this.ends_, this.stride); -}; - - -/** - * @return {Array.} Ends. - */ -MultiLineString.prototype.getEnds = function() { - return this.ends_; -}; - - -/** - * Return the linestring at the specified index. - * @param {number} index Index. - * @return {module:ol/geom/LineString} LineString. - * @api - */ -MultiLineString.prototype.getLineString = function(index) { - if (index < 0 || this.ends_.length <= index) { - return null; - } - return new LineString(this.flatCoordinates.slice( - index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]), this.layout); -}; - - -/** - * Return the linestrings of this multilinestring. - * @return {Array.} LineStrings. - * @api - */ -MultiLineString.prototype.getLineStrings = function() { - const flatCoordinates = this.flatCoordinates; - const ends = this.ends_; - const layout = this.layout; - /** @type {Array.} */ - const lineStrings = []; - let offset = 0; - for (let i = 0, ii = ends.length; i < ii; ++i) { - const end = ends[i]; - const lineString = new LineString(flatCoordinates.slice(offset, end), layout); - lineStrings.push(lineString); - offset = end; - } - return lineStrings; -}; - - -/** - * @return {Array.} Flat midpoints. - */ -MultiLineString.prototype.getFlatMidpoints = function() { - const midpoints = []; - const flatCoordinates = this.flatCoordinates; - let offset = 0; - const ends = this.ends_; - const stride = this.stride; - for (let i = 0, ii = ends.length; i < ii; ++i) { - const end = ends[i]; - const midpoint = interpolatePoint( - flatCoordinates, offset, end, stride, 0.5); - extend(midpoints, midpoint); - offset = end; - } - return midpoints; -}; - - -/** - * @inheritDoc - */ -MultiLineString.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) { - const simplifiedFlatCoordinates = []; - const simplifiedEnds = []; - simplifiedFlatCoordinates.length = douglasPeuckerArray( - this.flatCoordinates, 0, this.ends_, this.stride, squaredTolerance, - simplifiedFlatCoordinates, 0, simplifiedEnds); - return new MultiLineString(simplifiedFlatCoordinates, GeometryLayout.XY, simplifiedEnds); -}; - - -/** - * @inheritDoc - * @api - */ -MultiLineString.prototype.getType = function() { - return GeometryType.MULTI_LINE_STRING; -}; - - -/** - * @inheritDoc - * @api - */ -MultiLineString.prototype.intersectsExtent = function(extent) { - return intersectsLineStringArray( - this.flatCoordinates, 0, this.ends_, this.stride, extent); -}; - - -/** - * Set the coordinates of the multilinestring. - * @param {!Array.>} coordinates Coordinates. - * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. - * @override - * @api - */ -MultiLineString.prototype.setCoordinates = function(coordinates, opt_layout) { - this.setLayout(opt_layout, coordinates, 2); - if (!this.flatCoordinates) { - this.flatCoordinates = []; - } - const ends = deflateCoordinatesArray( - this.flatCoordinates, 0, coordinates, this.stride, this.ends_); - this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1]; - this.changed(); -}; export default MultiLineString; diff --git a/src/ol/geom/MultiPoint.js b/src/ol/geom/MultiPoint.js index 9ae1828b75..b366817415 100644 --- a/src/ol/geom/MultiPoint.js +++ b/src/ol/geom/MultiPoint.js @@ -23,157 +23,152 @@ import {squaredDistance as squaredDx} from '../math.js'; * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. * @api */ -const MultiPoint = function(coordinates, opt_layout) { - SimpleGeometry.call(this); - if (opt_layout && !Array.isArray(coordinates[0])) { - this.setFlatCoordinates(opt_layout, coordinates); - } else { - this.setCoordinates(coordinates, opt_layout); +class MultiPoint { + constructor(coordinates, opt_layout) { + SimpleGeometry.call(this); + if (opt_layout && !Array.isArray(coordinates[0])) { + this.setFlatCoordinates(opt_layout, coordinates); + } else { + this.setCoordinates(coordinates, opt_layout); + } } -}; + + /** + * Append the passed point to this multipoint. + * @param {module:ol/geom/Point} point Point. + * @api + */ + appendPoint(point) { + if (!this.flatCoordinates) { + this.flatCoordinates = point.getFlatCoordinates().slice(); + } else { + extend(this.flatCoordinates, point.getFlatCoordinates()); + } + this.changed(); + } + + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/MultiPoint} Clone. + * @override + * @api + */ + clone() { + const multiPoint = new MultiPoint(this.flatCoordinates.slice(), this.layout); + return multiPoint; + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { + return minSquaredDistance; + } + const flatCoordinates = this.flatCoordinates; + const stride = this.stride; + for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) { + const squaredDistance = squaredDx( + x, y, flatCoordinates[i], flatCoordinates[i + 1]); + if (squaredDistance < minSquaredDistance) { + minSquaredDistance = squaredDistance; + for (let j = 0; j < stride; ++j) { + closestPoint[j] = flatCoordinates[i + j]; + } + closestPoint.length = stride; + } + } + return minSquaredDistance; + } + + /** + * Return the coordinates of the multipoint. + * @return {Array.} Coordinates. + * @override + * @api + */ + getCoordinates() { + return inflateCoordinates( + this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); + } + + /** + * Return the point at the specified index. + * @param {number} index Index. + * @return {module:ol/geom/Point} Point. + * @api + */ + getPoint(index) { + const n = !this.flatCoordinates ? 0 : this.flatCoordinates.length / this.stride; + if (index < 0 || n <= index) { + return null; + } + return new Point(this.flatCoordinates.slice( + index * this.stride, (index + 1) * this.stride), this.layout); + } + + /** + * Return the points of this multipoint. + * @return {Array.} Points. + * @api + */ + getPoints() { + const flatCoordinates = this.flatCoordinates; + const layout = this.layout; + const stride = this.stride; + /** @type {Array.} */ + const points = []; + for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) { + const point = new Point(flatCoordinates.slice(i, i + stride), layout); + points.push(point); + } + return points; + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.MULTI_POINT; + } + + /** + * @inheritDoc + * @api + */ + intersectsExtent(extent) { + const flatCoordinates = this.flatCoordinates; + const stride = this.stride; + for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) { + const x = flatCoordinates[i]; + const y = flatCoordinates[i + 1]; + if (containsXY(extent, x, y)) { + return true; + } + } + return false; + } + + /** + * Set the coordinates of the multipoint. + * @param {!Array.} coordinates Coordinates. + * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. + * @override + * @api + */ + setCoordinates(coordinates, opt_layout) { + this.setLayout(opt_layout, coordinates, 1); + if (!this.flatCoordinates) { + this.flatCoordinates = []; + } + this.flatCoordinates.length = deflateCoordinates( + this.flatCoordinates, 0, coordinates, this.stride); + this.changed(); + } +} inherits(MultiPoint, SimpleGeometry); -/** - * Append the passed point to this multipoint. - * @param {module:ol/geom/Point} point Point. - * @api - */ -MultiPoint.prototype.appendPoint = function(point) { - if (!this.flatCoordinates) { - this.flatCoordinates = point.getFlatCoordinates().slice(); - } else { - extend(this.flatCoordinates, point.getFlatCoordinates()); - } - this.changed(); -}; - - -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/MultiPoint} Clone. - * @override - * @api - */ -MultiPoint.prototype.clone = function() { - const multiPoint = new MultiPoint(this.flatCoordinates.slice(), this.layout); - return multiPoint; -}; - - -/** - * @inheritDoc - */ -MultiPoint.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { - return minSquaredDistance; - } - const flatCoordinates = this.flatCoordinates; - const stride = this.stride; - for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) { - const squaredDistance = squaredDx( - x, y, flatCoordinates[i], flatCoordinates[i + 1]); - if (squaredDistance < minSquaredDistance) { - minSquaredDistance = squaredDistance; - for (let j = 0; j < stride; ++j) { - closestPoint[j] = flatCoordinates[i + j]; - } - closestPoint.length = stride; - } - } - return minSquaredDistance; -}; - - -/** - * Return the coordinates of the multipoint. - * @return {Array.} Coordinates. - * @override - * @api - */ -MultiPoint.prototype.getCoordinates = function() { - return inflateCoordinates( - this.flatCoordinates, 0, this.flatCoordinates.length, this.stride); -}; - - -/** - * Return the point at the specified index. - * @param {number} index Index. - * @return {module:ol/geom/Point} Point. - * @api - */ -MultiPoint.prototype.getPoint = function(index) { - const n = !this.flatCoordinates ? 0 : this.flatCoordinates.length / this.stride; - if (index < 0 || n <= index) { - return null; - } - return new Point(this.flatCoordinates.slice( - index * this.stride, (index + 1) * this.stride), this.layout); -}; - - -/** - * Return the points of this multipoint. - * @return {Array.} Points. - * @api - */ -MultiPoint.prototype.getPoints = function() { - const flatCoordinates = this.flatCoordinates; - const layout = this.layout; - const stride = this.stride; - /** @type {Array.} */ - const points = []; - for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) { - const point = new Point(flatCoordinates.slice(i, i + stride), layout); - points.push(point); - } - return points; -}; - - -/** - * @inheritDoc - * @api - */ -MultiPoint.prototype.getType = function() { - return GeometryType.MULTI_POINT; -}; - - -/** - * @inheritDoc - * @api - */ -MultiPoint.prototype.intersectsExtent = function(extent) { - const flatCoordinates = this.flatCoordinates; - const stride = this.stride; - for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) { - const x = flatCoordinates[i]; - const y = flatCoordinates[i + 1]; - if (containsXY(extent, x, y)) { - return true; - } - } - return false; -}; - - -/** - * Set the coordinates of the multipoint. - * @param {!Array.} coordinates Coordinates. - * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. - * @override - * @api - */ -MultiPoint.prototype.setCoordinates = function(coordinates, opt_layout) { - this.setLayout(opt_layout, coordinates, 1); - if (!this.flatCoordinates) { - this.flatCoordinates = []; - } - this.flatCoordinates.length = deflateCoordinates( - this.flatCoordinates, 0, coordinates, this.stride); - this.changed(); -}; export default MultiPoint; diff --git a/src/ol/geom/MultiPolygon.js b/src/ol/geom/MultiPolygon.js index 990dd159c2..a5bb32c476 100644 --- a/src/ol/geom/MultiPolygon.js +++ b/src/ol/geom/MultiPolygon.js @@ -34,360 +34,346 @@ import {quantizeMultiArray} from '../geom/flat/simplify.js'; * coordinates. * @api */ -const MultiPolygon = function(coordinates, opt_layout, opt_endss) { +class MultiPolygon { + constructor(coordinates, opt_layout, opt_endss) { - SimpleGeometry.call(this); + SimpleGeometry.call(this); - /** - * @type {Array.>} - * @private - */ - this.endss_ = []; + /** + * @type {Array.>} + * @private + */ + this.endss_ = []; - /** - * @private - * @type {number} - */ - this.flatInteriorPointsRevision_ = -1; + /** + * @private + * @type {number} + */ + this.flatInteriorPointsRevision_ = -1; - /** - * @private - * @type {Array.} - */ - this.flatInteriorPoints_ = null; + /** + * @private + * @type {Array.} + */ + this.flatInteriorPoints_ = null; - /** - * @private - * @type {number} - */ - this.maxDelta_ = -1; + /** + * @private + * @type {number} + */ + this.maxDelta_ = -1; - /** - * @private - * @type {number} - */ - this.maxDeltaRevision_ = -1; + /** + * @private + * @type {number} + */ + this.maxDeltaRevision_ = -1; - /** - * @private - * @type {number} - */ - this.orientedRevision_ = -1; + /** + * @private + * @type {number} + */ + this.orientedRevision_ = -1; - /** - * @private - * @type {Array.} - */ - this.orientedFlatCoordinates_ = null; + /** + * @private + * @type {Array.} + */ + this.orientedFlatCoordinates_ = null; - if (!opt_endss && !Array.isArray(coordinates[0])) { - let layout = this.getLayout(); - const flatCoordinates = []; - const endss = []; - for (let i = 0, ii = coordinates.length; i < ii; ++i) { - const polygon = coordinates[i]; - if (i === 0) { - layout = polygon.getLayout(); + if (!opt_endss && !Array.isArray(coordinates[0])) { + let layout = this.getLayout(); + const flatCoordinates = []; + const endss = []; + for (let i = 0, ii = coordinates.length; i < ii; ++i) { + const polygon = coordinates[i]; + if (i === 0) { + layout = polygon.getLayout(); + } + const offset = flatCoordinates.length; + const ends = polygon.getEnds(); + for (let j = 0, jj = ends.length; j < jj; ++j) { + ends[j] += offset; + } + extend(flatCoordinates, polygon.getFlatCoordinates()); + endss.push(ends); } - const offset = flatCoordinates.length; - const ends = polygon.getEnds(); - for (let j = 0, jj = ends.length; j < jj; ++j) { - ends[j] += offset; - } - extend(flatCoordinates, polygon.getFlatCoordinates()); - endss.push(ends); + opt_layout = layout; + coordinates = flatCoordinates; + opt_endss = endss; } - opt_layout = layout; - coordinates = flatCoordinates; - opt_endss = endss; - } - if (opt_layout !== undefined && opt_endss) { - this.setFlatCoordinates(opt_layout, coordinates); - this.endss_ = opt_endss; - } else { - this.setCoordinates(coordinates, opt_layout); + if (opt_layout !== undefined && opt_endss) { + this.setFlatCoordinates(opt_layout, coordinates); + this.endss_ = opt_endss; + } else { + this.setCoordinates(coordinates, opt_layout); + } + } -}; + /** + * Append the passed polygon to this multipolygon. + * @param {module:ol/geom/Polygon} polygon Polygon. + * @api + */ + appendPolygon(polygon) { + /** @type {Array.} */ + let ends; + if (!this.flatCoordinates) { + this.flatCoordinates = polygon.getFlatCoordinates().slice(); + ends = polygon.getEnds().slice(); + this.endss_.push(); + } else { + const offset = this.flatCoordinates.length; + extend(this.flatCoordinates, polygon.getFlatCoordinates()); + ends = polygon.getEnds().slice(); + for (let i = 0, ii = ends.length; i < ii; ++i) { + ends[i] += offset; + } + } + this.endss_.push(ends); + this.changed(); + } + + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/MultiPolygon} Clone. + * @override + * @api + */ + clone() { + const len = this.endss_.length; + const newEndss = new Array(len); + for (let i = 0; i < len; ++i) { + newEndss[i] = this.endss_[i].slice(); + } + + return new MultiPolygon( + this.flatCoordinates.slice(), this.layout, newEndss); + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { + return minSquaredDistance; + } + if (this.maxDeltaRevision_ != this.getRevision()) { + this.maxDelta_ = Math.sqrt(multiArrayMaxSquaredDelta( + this.flatCoordinates, 0, this.endss_, this.stride, 0)); + this.maxDeltaRevision_ = this.getRevision(); + } + return assignClosestMultiArrayPoint( + this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, + this.maxDelta_, true, x, y, closestPoint, minSquaredDistance); + } + + /** + * @inheritDoc + */ + containsXY(x, y) { + return linearRingssContainsXY(this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, x, y); + } + + /** + * Return the area of the multipolygon on projected plane. + * @return {number} Area (on projected plane). + * @api + */ + getArea() { + return linearRingssArea(this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride); + } + + /** + * Get the coordinate array for this geometry. This array has the structure + * of a GeoJSON coordinate array for multi-polygons. + * + * @param {boolean=} opt_right Orient coordinates according to the right-hand + * rule (counter-clockwise for exterior and clockwise for interior rings). + * If `false`, coordinates will be oriented according to the left-hand rule + * (clockwise for exterior and counter-clockwise for interior rings). + * By default, coordinate orientation will depend on how the geometry was + * constructed. + * @return {Array.>>} Coordinates. + * @override + * @api + */ + getCoordinates(opt_right) { + let flatCoordinates; + if (opt_right !== undefined) { + flatCoordinates = this.getOrientedFlatCoordinates().slice(); + orientLinearRingsArray( + flatCoordinates, 0, this.endss_, this.stride, opt_right); + } else { + flatCoordinates = this.flatCoordinates; + } + + return inflateMultiCoordinatesArray( + flatCoordinates, 0, this.endss_, this.stride); + } + + /** + * @return {Array.>} Endss. + */ + getEndss() { + return this.endss_; + } + + /** + * @return {Array.} Flat interior points. + */ + getFlatInteriorPoints() { + if (this.flatInteriorPointsRevision_ != this.getRevision()) { + const flatCenters = linearRingssCenter( + this.flatCoordinates, 0, this.endss_, this.stride); + this.flatInteriorPoints_ = getInteriorPointsOfMultiArray( + this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, + flatCenters); + this.flatInteriorPointsRevision_ = this.getRevision(); + } + return this.flatInteriorPoints_; + } + + /** + * Return the interior points as {@link module:ol/geom/MultiPoint multipoint}. + * @return {module:ol/geom/MultiPoint} Interior points as XYM coordinates, where M is + * the length of the horizontal intersection that the point belongs to. + * @api + */ + getInteriorPoints() { + return new MultiPoint(this.getFlatInteriorPoints().slice(), GeometryLayout.XYM); + } + + /** + * @return {Array.} Oriented flat coordinates. + */ + getOrientedFlatCoordinates() { + if (this.orientedRevision_ != this.getRevision()) { + const flatCoordinates = this.flatCoordinates; + if (linearRingsAreOriented( + flatCoordinates, 0, this.endss_, this.stride)) { + this.orientedFlatCoordinates_ = flatCoordinates; + } else { + this.orientedFlatCoordinates_ = flatCoordinates.slice(); + this.orientedFlatCoordinates_.length = + orientLinearRingsArray( + this.orientedFlatCoordinates_, 0, this.endss_, this.stride); + } + this.orientedRevision_ = this.getRevision(); + } + return this.orientedFlatCoordinates_; + } + + /** + * @inheritDoc + */ + getSimplifiedGeometryInternal(squaredTolerance) { + const simplifiedFlatCoordinates = []; + const simplifiedEndss = []; + simplifiedFlatCoordinates.length = quantizeMultiArray( + this.flatCoordinates, 0, this.endss_, this.stride, + Math.sqrt(squaredTolerance), + simplifiedFlatCoordinates, 0, simplifiedEndss); + return new MultiPolygon(simplifiedFlatCoordinates, GeometryLayout.XY, simplifiedEndss); + } + + /** + * Return the polygon at the specified index. + * @param {number} index Index. + * @return {module:ol/geom/Polygon} Polygon. + * @api + */ + getPolygon(index) { + if (index < 0 || this.endss_.length <= index) { + return null; + } + let offset; + if (index === 0) { + offset = 0; + } else { + const prevEnds = this.endss_[index - 1]; + offset = prevEnds[prevEnds.length - 1]; + } + const ends = this.endss_[index].slice(); + const end = ends[ends.length - 1]; + if (offset !== 0) { + for (let i = 0, ii = ends.length; i < ii; ++i) { + ends[i] -= offset; + } + } + return new Polygon(this.flatCoordinates.slice(offset, end), this.layout, ends); + } + + /** + * Return the polygons of this multipolygon. + * @return {Array.} Polygons. + * @api + */ + getPolygons() { + const layout = this.layout; + const flatCoordinates = this.flatCoordinates; + const endss = this.endss_; + const polygons = []; + let offset = 0; + for (let i = 0, ii = endss.length; i < ii; ++i) { + const ends = endss[i].slice(); + const end = ends[ends.length - 1]; + if (offset !== 0) { + for (let j = 0, jj = ends.length; j < jj; ++j) { + ends[j] -= offset; + } + } + const polygon = new Polygon(flatCoordinates.slice(offset, end), layout, ends); + polygons.push(polygon); + offset = end; + } + return polygons; + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.MULTI_POLYGON; + } + + /** + * @inheritDoc + * @api + */ + intersectsExtent(extent) { + return intersectsLinearRingMultiArray( + this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, extent); + } + + /** + * Set the coordinates of the multipolygon. + * @param {!Array.>>} coordinates Coordinates. + * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. + * @override + * @api + */ + setCoordinates(coordinates, opt_layout) { + this.setLayout(opt_layout, coordinates, 3); + if (!this.flatCoordinates) { + this.flatCoordinates = []; + } + const endss = deflateMultiCoordinatesArray( + this.flatCoordinates, 0, coordinates, this.stride, this.endss_); + if (endss.length === 0) { + this.flatCoordinates.length = 0; + } else { + const lastEnds = endss[endss.length - 1]; + this.flatCoordinates.length = lastEnds.length === 0 ? + 0 : lastEnds[lastEnds.length - 1]; + } + this.changed(); + } +} inherits(MultiPolygon, SimpleGeometry); -/** - * Append the passed polygon to this multipolygon. - * @param {module:ol/geom/Polygon} polygon Polygon. - * @api - */ -MultiPolygon.prototype.appendPolygon = function(polygon) { - /** @type {Array.} */ - let ends; - if (!this.flatCoordinates) { - this.flatCoordinates = polygon.getFlatCoordinates().slice(); - ends = polygon.getEnds().slice(); - this.endss_.push(); - } else { - const offset = this.flatCoordinates.length; - extend(this.flatCoordinates, polygon.getFlatCoordinates()); - ends = polygon.getEnds().slice(); - for (let i = 0, ii = ends.length; i < ii; ++i) { - ends[i] += offset; - } - } - this.endss_.push(ends); - this.changed(); -}; - - -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/MultiPolygon} Clone. - * @override - * @api - */ -MultiPolygon.prototype.clone = function() { - const len = this.endss_.length; - const newEndss = new Array(len); - for (let i = 0; i < len; ++i) { - newEndss[i] = this.endss_[i].slice(); - } - - return new MultiPolygon( - this.flatCoordinates.slice(), this.layout, newEndss); -}; - - -/** - * @inheritDoc - */ -MultiPolygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { - return minSquaredDistance; - } - if (this.maxDeltaRevision_ != this.getRevision()) { - this.maxDelta_ = Math.sqrt(multiArrayMaxSquaredDelta( - this.flatCoordinates, 0, this.endss_, this.stride, 0)); - this.maxDeltaRevision_ = this.getRevision(); - } - return assignClosestMultiArrayPoint( - this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, - this.maxDelta_, true, x, y, closestPoint, minSquaredDistance); -}; - - -/** - * @inheritDoc - */ -MultiPolygon.prototype.containsXY = function(x, y) { - return linearRingssContainsXY(this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, x, y); -}; - - -/** - * Return the area of the multipolygon on projected plane. - * @return {number} Area (on projected plane). - * @api - */ -MultiPolygon.prototype.getArea = function() { - return linearRingssArea(this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride); -}; - - -/** - * Get the coordinate array for this geometry. This array has the structure - * of a GeoJSON coordinate array for multi-polygons. - * - * @param {boolean=} opt_right Orient coordinates according to the right-hand - * rule (counter-clockwise for exterior and clockwise for interior rings). - * If `false`, coordinates will be oriented according to the left-hand rule - * (clockwise for exterior and counter-clockwise for interior rings). - * By default, coordinate orientation will depend on how the geometry was - * constructed. - * @return {Array.>>} Coordinates. - * @override - * @api - */ -MultiPolygon.prototype.getCoordinates = function(opt_right) { - let flatCoordinates; - if (opt_right !== undefined) { - flatCoordinates = this.getOrientedFlatCoordinates().slice(); - orientLinearRingsArray( - flatCoordinates, 0, this.endss_, this.stride, opt_right); - } else { - flatCoordinates = this.flatCoordinates; - } - - return inflateMultiCoordinatesArray( - flatCoordinates, 0, this.endss_, this.stride); -}; - - -/** - * @return {Array.>} Endss. - */ -MultiPolygon.prototype.getEndss = function() { - return this.endss_; -}; - - -/** - * @return {Array.} Flat interior points. - */ -MultiPolygon.prototype.getFlatInteriorPoints = function() { - if (this.flatInteriorPointsRevision_ != this.getRevision()) { - const flatCenters = linearRingssCenter( - this.flatCoordinates, 0, this.endss_, this.stride); - this.flatInteriorPoints_ = getInteriorPointsOfMultiArray( - this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, - flatCenters); - this.flatInteriorPointsRevision_ = this.getRevision(); - } - return this.flatInteriorPoints_; -}; - - -/** - * Return the interior points as {@link module:ol/geom/MultiPoint multipoint}. - * @return {module:ol/geom/MultiPoint} Interior points as XYM coordinates, where M is - * the length of the horizontal intersection that the point belongs to. - * @api - */ -MultiPolygon.prototype.getInteriorPoints = function() { - return new MultiPoint(this.getFlatInteriorPoints().slice(), GeometryLayout.XYM); -}; - - -/** - * @return {Array.} Oriented flat coordinates. - */ -MultiPolygon.prototype.getOrientedFlatCoordinates = function() { - if (this.orientedRevision_ != this.getRevision()) { - const flatCoordinates = this.flatCoordinates; - if (linearRingsAreOriented( - flatCoordinates, 0, this.endss_, this.stride)) { - this.orientedFlatCoordinates_ = flatCoordinates; - } else { - this.orientedFlatCoordinates_ = flatCoordinates.slice(); - this.orientedFlatCoordinates_.length = - orientLinearRingsArray( - this.orientedFlatCoordinates_, 0, this.endss_, this.stride); - } - this.orientedRevision_ = this.getRevision(); - } - return this.orientedFlatCoordinates_; -}; - - -/** - * @inheritDoc - */ -MultiPolygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) { - const simplifiedFlatCoordinates = []; - const simplifiedEndss = []; - simplifiedFlatCoordinates.length = quantizeMultiArray( - this.flatCoordinates, 0, this.endss_, this.stride, - Math.sqrt(squaredTolerance), - simplifiedFlatCoordinates, 0, simplifiedEndss); - return new MultiPolygon(simplifiedFlatCoordinates, GeometryLayout.XY, simplifiedEndss); -}; - - -/** - * Return the polygon at the specified index. - * @param {number} index Index. - * @return {module:ol/geom/Polygon} Polygon. - * @api - */ -MultiPolygon.prototype.getPolygon = function(index) { - if (index < 0 || this.endss_.length <= index) { - return null; - } - let offset; - if (index === 0) { - offset = 0; - } else { - const prevEnds = this.endss_[index - 1]; - offset = prevEnds[prevEnds.length - 1]; - } - const ends = this.endss_[index].slice(); - const end = ends[ends.length - 1]; - if (offset !== 0) { - for (let i = 0, ii = ends.length; i < ii; ++i) { - ends[i] -= offset; - } - } - return new Polygon(this.flatCoordinates.slice(offset, end), this.layout, ends); -}; - - -/** - * Return the polygons of this multipolygon. - * @return {Array.} Polygons. - * @api - */ -MultiPolygon.prototype.getPolygons = function() { - const layout = this.layout; - const flatCoordinates = this.flatCoordinates; - const endss = this.endss_; - const polygons = []; - let offset = 0; - for (let i = 0, ii = endss.length; i < ii; ++i) { - const ends = endss[i].slice(); - const end = ends[ends.length - 1]; - if (offset !== 0) { - for (let j = 0, jj = ends.length; j < jj; ++j) { - ends[j] -= offset; - } - } - const polygon = new Polygon(flatCoordinates.slice(offset, end), layout, ends); - polygons.push(polygon); - offset = end; - } - return polygons; -}; - - -/** - * @inheritDoc - * @api - */ -MultiPolygon.prototype.getType = function() { - return GeometryType.MULTI_POLYGON; -}; - - -/** - * @inheritDoc - * @api - */ -MultiPolygon.prototype.intersectsExtent = function(extent) { - return intersectsLinearRingMultiArray( - this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, extent); -}; - - -/** - * Set the coordinates of the multipolygon. - * @param {!Array.>>} coordinates Coordinates. - * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. - * @override - * @api - */ -MultiPolygon.prototype.setCoordinates = function(coordinates, opt_layout) { - this.setLayout(opt_layout, coordinates, 3); - if (!this.flatCoordinates) { - this.flatCoordinates = []; - } - const endss = deflateMultiCoordinatesArray( - this.flatCoordinates, 0, coordinates, this.stride, this.endss_); - if (endss.length === 0) { - this.flatCoordinates.length = 0; - } else { - const lastEnds = endss[endss.length - 1]; - this.flatCoordinates.length = lastEnds.length === 0 ? - 0 : lastEnds[lastEnds.length - 1]; - } - this.changed(); -}; - - export default MultiPolygon; diff --git a/src/ol/geom/Point.js b/src/ol/geom/Point.js index dd1548189e..e93b7a5514 100644 --- a/src/ol/geom/Point.js +++ b/src/ol/geom/Point.js @@ -18,94 +18,90 @@ import {squaredDistance as squaredDx} from '../math.js'; * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. * @api */ -const Point = function(coordinates, opt_layout) { - SimpleGeometry.call(this); - this.setCoordinates(coordinates, opt_layout); -}; +class Point { + constructor(coordinates, opt_layout) { + SimpleGeometry.call(this); + this.setCoordinates(coordinates, opt_layout); + } + + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/Point} Clone. + * @override + * @api + */ + clone() { + const point = new Point(this.flatCoordinates.slice(), this.layout); + return point; + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + const flatCoordinates = this.flatCoordinates; + const squaredDistance = squaredDx(x, y, flatCoordinates[0], flatCoordinates[1]); + if (squaredDistance < minSquaredDistance) { + const stride = this.stride; + for (let i = 0; i < stride; ++i) { + closestPoint[i] = flatCoordinates[i]; + } + closestPoint.length = stride; + return squaredDistance; + } else { + return minSquaredDistance; + } + } + + /** + * Return the coordinate of the point. + * @return {module:ol/coordinate~Coordinate} Coordinates. + * @override + * @api + */ + getCoordinates() { + return !this.flatCoordinates ? [] : this.flatCoordinates.slice(); + } + + /** + * @inheritDoc + */ + computeExtent(extent) { + return createOrUpdateFromCoordinate(this.flatCoordinates, extent); + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.POINT; + } + + /** + * @inheritDoc + * @api + */ + intersectsExtent(extent) { + return containsXY(extent, this.flatCoordinates[0], this.flatCoordinates[1]); + } + + /** + * @inheritDoc + * @api + */ + setCoordinates(coordinates, opt_layout) { + this.setLayout(opt_layout, coordinates, 0); + if (!this.flatCoordinates) { + this.flatCoordinates = []; + } + this.flatCoordinates.length = deflateCoordinate( + this.flatCoordinates, 0, coordinates, this.stride); + this.changed(); + } +} inherits(Point, SimpleGeometry); -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/Point} Clone. - * @override - * @api - */ -Point.prototype.clone = function() { - const point = new Point(this.flatCoordinates.slice(), this.layout); - return point; -}; - - -/** - * @inheritDoc - */ -Point.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - const flatCoordinates = this.flatCoordinates; - const squaredDistance = squaredDx(x, y, flatCoordinates[0], flatCoordinates[1]); - if (squaredDistance < minSquaredDistance) { - const stride = this.stride; - for (let i = 0; i < stride; ++i) { - closestPoint[i] = flatCoordinates[i]; - } - closestPoint.length = stride; - return squaredDistance; - } else { - return minSquaredDistance; - } -}; - - -/** - * Return the coordinate of the point. - * @return {module:ol/coordinate~Coordinate} Coordinates. - * @override - * @api - */ -Point.prototype.getCoordinates = function() { - return !this.flatCoordinates ? [] : this.flatCoordinates.slice(); -}; - - -/** - * @inheritDoc - */ -Point.prototype.computeExtent = function(extent) { - return createOrUpdateFromCoordinate(this.flatCoordinates, extent); -}; - - -/** - * @inheritDoc - * @api - */ -Point.prototype.getType = function() { - return GeometryType.POINT; -}; - - -/** - * @inheritDoc - * @api - */ -Point.prototype.intersectsExtent = function(extent) { - return containsXY(extent, this.flatCoordinates[0], this.flatCoordinates[1]); -}; - - -/** - * @inheritDoc - * @api - */ -Point.prototype.setCoordinates = function(coordinates, opt_layout) { - this.setLayout(opt_layout, coordinates, 0); - if (!this.flatCoordinates) { - this.flatCoordinates = []; - } - this.flatCoordinates.length = deflateCoordinate( - this.flatCoordinates, 0, coordinates, this.stride); - this.changed(); -}; - export default Point; diff --git a/src/ol/geom/Polygon.js b/src/ol/geom/Polygon.js index eeca8df751..87fa4215c0 100644 --- a/src/ol/geom/Polygon.js +++ b/src/ol/geom/Polygon.js @@ -39,314 +39,300 @@ import {modulo} from '../math.js'; * coordinates). * @api */ -const Polygon = function(coordinates, opt_layout, opt_ends) { +class Polygon { + constructor(coordinates, opt_layout, opt_ends) { - SimpleGeometry.call(this); + SimpleGeometry.call(this); - /** - * @type {Array.} - * @private - */ - this.ends_ = []; + /** + * @type {Array.} + * @private + */ + this.ends_ = []; - /** - * @private - * @type {number} - */ - this.flatInteriorPointRevision_ = -1; + /** + * @private + * @type {number} + */ + this.flatInteriorPointRevision_ = -1; - /** - * @private - * @type {module:ol/coordinate~Coordinate} - */ - this.flatInteriorPoint_ = null; + /** + * @private + * @type {module:ol/coordinate~Coordinate} + */ + this.flatInteriorPoint_ = null; - /** - * @private - * @type {number} - */ - this.maxDelta_ = -1; + /** + * @private + * @type {number} + */ + this.maxDelta_ = -1; - /** - * @private - * @type {number} - */ - this.maxDeltaRevision_ = -1; + /** + * @private + * @type {number} + */ + this.maxDeltaRevision_ = -1; - /** - * @private - * @type {number} - */ - this.orientedRevision_ = -1; + /** + * @private + * @type {number} + */ + this.orientedRevision_ = -1; - /** - * @private - * @type {Array.} - */ - this.orientedFlatCoordinates_ = null; + /** + * @private + * @type {Array.} + */ + this.orientedFlatCoordinates_ = null; + + if (opt_layout !== undefined && opt_ends) { + this.setFlatCoordinates(opt_layout, coordinates); + this.ends_ = opt_ends; + } else { + this.setCoordinates(coordinates, opt_layout); + } - if (opt_layout !== undefined && opt_ends) { - this.setFlatCoordinates(opt_layout, coordinates); - this.ends_ = opt_ends; - } else { - this.setCoordinates(coordinates, opt_layout); } -}; + /** + * Append the passed linear ring to this polygon. + * @param {module:ol/geom/LinearRing} linearRing Linear ring. + * @api + */ + appendLinearRing(linearRing) { + if (!this.flatCoordinates) { + this.flatCoordinates = linearRing.getFlatCoordinates().slice(); + } else { + extend(this.flatCoordinates, linearRing.getFlatCoordinates()); + } + this.ends_.push(this.flatCoordinates.length); + this.changed(); + } + + /** + * Make a complete copy of the geometry. + * @return {!module:ol/geom/Polygon} Clone. + * @override + * @api + */ + clone() { + return new Polygon(this.flatCoordinates.slice(), this.layout, this.ends_.slice()); + } + + /** + * @inheritDoc + */ + closestPointXY(x, y, closestPoint, minSquaredDistance) { + if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { + return minSquaredDistance; + } + if (this.maxDeltaRevision_ != this.getRevision()) { + this.maxDelta_ = Math.sqrt(arrayMaxSquaredDelta( + this.flatCoordinates, 0, this.ends_, this.stride, 0)); + this.maxDeltaRevision_ = this.getRevision(); + } + return assignClosestArrayPoint( + this.flatCoordinates, 0, this.ends_, this.stride, + this.maxDelta_, true, x, y, closestPoint, minSquaredDistance); + } + + /** + * @inheritDoc + */ + containsXY(x, y) { + return linearRingsContainsXY(this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, x, y); + } + + /** + * Return the area of the polygon on projected plane. + * @return {number} Area (on projected plane). + * @api + */ + getArea() { + return linearRingsArea(this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride); + } + + /** + * Get the coordinate array for this geometry. This array has the structure + * of a GeoJSON coordinate array for polygons. + * + * @param {boolean=} opt_right Orient coordinates according to the right-hand + * rule (counter-clockwise for exterior and clockwise for interior rings). + * If `false`, coordinates will be oriented according to the left-hand rule + * (clockwise for exterior and counter-clockwise for interior rings). + * By default, coordinate orientation will depend on how the geometry was + * constructed. + * @return {Array.>} Coordinates. + * @override + * @api + */ + getCoordinates(opt_right) { + let flatCoordinates; + if (opt_right !== undefined) { + flatCoordinates = this.getOrientedFlatCoordinates().slice(); + orientLinearRings( + flatCoordinates, 0, this.ends_, this.stride, opt_right); + } else { + flatCoordinates = this.flatCoordinates; + } + + return inflateCoordinatesArray( + flatCoordinates, 0, this.ends_, this.stride); + } + + /** + * @return {Array.} Ends. + */ + getEnds() { + return this.ends_; + } + + /** + * @return {Array.} Interior point. + */ + getFlatInteriorPoint() { + if (this.flatInteriorPointRevision_ != this.getRevision()) { + const flatCenter = getCenter(this.getExtent()); + this.flatInteriorPoint_ = getInteriorPointOfArray( + this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, + flatCenter, 0); + this.flatInteriorPointRevision_ = this.getRevision(); + } + return this.flatInteriorPoint_; + } + + /** + * Return an interior point of the polygon. + * @return {module:ol/geom/Point} Interior point as XYM coordinate, where M is the + * length of the horizontal intersection that the point belongs to. + * @api + */ + getInteriorPoint() { + return new Point(this.getFlatInteriorPoint(), GeometryLayout.XYM); + } + + /** + * Return the number of rings of the polygon, this includes the exterior + * ring and any interior rings. + * + * @return {number} Number of rings. + * @api + */ + getLinearRingCount() { + return this.ends_.length; + } + + /** + * Return the Nth linear ring of the polygon geometry. Return `null` if the + * given index is out of range. + * The exterior linear ring is available at index `0` and the interior rings + * at index `1` and beyond. + * + * @param {number} index Index. + * @return {module:ol/geom/LinearRing} Linear ring. + * @api + */ + getLinearRing(index) { + if (index < 0 || this.ends_.length <= index) { + return null; + } + return new LinearRing(this.flatCoordinates.slice( + index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]), this.layout); + } + + /** + * Return the linear rings of the polygon. + * @return {Array.} Linear rings. + * @api + */ + getLinearRings() { + const layout = this.layout; + const flatCoordinates = this.flatCoordinates; + const ends = this.ends_; + const linearRings = []; + let offset = 0; + for (let i = 0, ii = ends.length; i < ii; ++i) { + const end = ends[i]; + const linearRing = new LinearRing(flatCoordinates.slice(offset, end), layout); + linearRings.push(linearRing); + offset = end; + } + return linearRings; + } + + /** + * @return {Array.} Oriented flat coordinates. + */ + getOrientedFlatCoordinates() { + if (this.orientedRevision_ != this.getRevision()) { + const flatCoordinates = this.flatCoordinates; + if (linearRingIsOriented( + flatCoordinates, 0, this.ends_, this.stride)) { + this.orientedFlatCoordinates_ = flatCoordinates; + } else { + this.orientedFlatCoordinates_ = flatCoordinates.slice(); + this.orientedFlatCoordinates_.length = + orientLinearRings( + this.orientedFlatCoordinates_, 0, this.ends_, this.stride); + } + this.orientedRevision_ = this.getRevision(); + } + return this.orientedFlatCoordinates_; + } + + /** + * @inheritDoc + */ + getSimplifiedGeometryInternal(squaredTolerance) { + const simplifiedFlatCoordinates = []; + const simplifiedEnds = []; + simplifiedFlatCoordinates.length = quantizeArray( + this.flatCoordinates, 0, this.ends_, this.stride, + Math.sqrt(squaredTolerance), + simplifiedFlatCoordinates, 0, simplifiedEnds); + return new Polygon(simplifiedFlatCoordinates, GeometryLayout.XY, simplifiedEnds); + } + + /** + * @inheritDoc + * @api + */ + getType() { + return GeometryType.POLYGON; + } + + /** + * @inheritDoc + * @api + */ + intersectsExtent(extent) { + return intersectsLinearRingArray( + this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, extent); + } + + /** + * Set the coordinates of the polygon. + * @param {!Array.>} coordinates Coordinates. + * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. + * @override + * @api + */ + setCoordinates(coordinates, opt_layout) { + this.setLayout(opt_layout, coordinates, 2); + if (!this.flatCoordinates) { + this.flatCoordinates = []; + } + const ends = deflateCoordinatesArray( + this.flatCoordinates, 0, coordinates, this.stride, this.ends_); + this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1]; + this.changed(); + } +} inherits(Polygon, SimpleGeometry); -/** - * Append the passed linear ring to this polygon. - * @param {module:ol/geom/LinearRing} linearRing Linear ring. - * @api - */ -Polygon.prototype.appendLinearRing = function(linearRing) { - if (!this.flatCoordinates) { - this.flatCoordinates = linearRing.getFlatCoordinates().slice(); - } else { - extend(this.flatCoordinates, linearRing.getFlatCoordinates()); - } - this.ends_.push(this.flatCoordinates.length); - this.changed(); -}; - - -/** - * Make a complete copy of the geometry. - * @return {!module:ol/geom/Polygon} Clone. - * @override - * @api - */ -Polygon.prototype.clone = function() { - return new Polygon(this.flatCoordinates.slice(), this.layout, this.ends_.slice()); -}; - - -/** - * @inheritDoc - */ -Polygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) { - if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) { - return minSquaredDistance; - } - if (this.maxDeltaRevision_ != this.getRevision()) { - this.maxDelta_ = Math.sqrt(arrayMaxSquaredDelta( - this.flatCoordinates, 0, this.ends_, this.stride, 0)); - this.maxDeltaRevision_ = this.getRevision(); - } - return assignClosestArrayPoint( - this.flatCoordinates, 0, this.ends_, this.stride, - this.maxDelta_, true, x, y, closestPoint, minSquaredDistance); -}; - - -/** - * @inheritDoc - */ -Polygon.prototype.containsXY = function(x, y) { - return linearRingsContainsXY(this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, x, y); -}; - - -/** - * Return the area of the polygon on projected plane. - * @return {number} Area (on projected plane). - * @api - */ -Polygon.prototype.getArea = function() { - return linearRingsArea(this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride); -}; - - -/** - * Get the coordinate array for this geometry. This array has the structure - * of a GeoJSON coordinate array for polygons. - * - * @param {boolean=} opt_right Orient coordinates according to the right-hand - * rule (counter-clockwise for exterior and clockwise for interior rings). - * If `false`, coordinates will be oriented according to the left-hand rule - * (clockwise for exterior and counter-clockwise for interior rings). - * By default, coordinate orientation will depend on how the geometry was - * constructed. - * @return {Array.>} Coordinates. - * @override - * @api - */ -Polygon.prototype.getCoordinates = function(opt_right) { - let flatCoordinates; - if (opt_right !== undefined) { - flatCoordinates = this.getOrientedFlatCoordinates().slice(); - orientLinearRings( - flatCoordinates, 0, this.ends_, this.stride, opt_right); - } else { - flatCoordinates = this.flatCoordinates; - } - - return inflateCoordinatesArray( - flatCoordinates, 0, this.ends_, this.stride); -}; - - -/** - * @return {Array.} Ends. - */ -Polygon.prototype.getEnds = function() { - return this.ends_; -}; - - -/** - * @return {Array.} Interior point. - */ -Polygon.prototype.getFlatInteriorPoint = function() { - if (this.flatInteriorPointRevision_ != this.getRevision()) { - const flatCenter = getCenter(this.getExtent()); - this.flatInteriorPoint_ = getInteriorPointOfArray( - this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, - flatCenter, 0); - this.flatInteriorPointRevision_ = this.getRevision(); - } - return this.flatInteriorPoint_; -}; - - -/** - * Return an interior point of the polygon. - * @return {module:ol/geom/Point} Interior point as XYM coordinate, where M is the - * length of the horizontal intersection that the point belongs to. - * @api - */ -Polygon.prototype.getInteriorPoint = function() { - return new Point(this.getFlatInteriorPoint(), GeometryLayout.XYM); -}; - - -/** - * Return the number of rings of the polygon, this includes the exterior - * ring and any interior rings. - * - * @return {number} Number of rings. - * @api - */ -Polygon.prototype.getLinearRingCount = function() { - return this.ends_.length; -}; - - -/** - * Return the Nth linear ring of the polygon geometry. Return `null` if the - * given index is out of range. - * The exterior linear ring is available at index `0` and the interior rings - * at index `1` and beyond. - * - * @param {number} index Index. - * @return {module:ol/geom/LinearRing} Linear ring. - * @api - */ -Polygon.prototype.getLinearRing = function(index) { - if (index < 0 || this.ends_.length <= index) { - return null; - } - return new LinearRing(this.flatCoordinates.slice( - index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]), this.layout); -}; - - -/** - * Return the linear rings of the polygon. - * @return {Array.} Linear rings. - * @api - */ -Polygon.prototype.getLinearRings = function() { - const layout = this.layout; - const flatCoordinates = this.flatCoordinates; - const ends = this.ends_; - const linearRings = []; - let offset = 0; - for (let i = 0, ii = ends.length; i < ii; ++i) { - const end = ends[i]; - const linearRing = new LinearRing(flatCoordinates.slice(offset, end), layout); - linearRings.push(linearRing); - offset = end; - } - return linearRings; -}; - - -/** - * @return {Array.} Oriented flat coordinates. - */ -Polygon.prototype.getOrientedFlatCoordinates = function() { - if (this.orientedRevision_ != this.getRevision()) { - const flatCoordinates = this.flatCoordinates; - if (linearRingIsOriented( - flatCoordinates, 0, this.ends_, this.stride)) { - this.orientedFlatCoordinates_ = flatCoordinates; - } else { - this.orientedFlatCoordinates_ = flatCoordinates.slice(); - this.orientedFlatCoordinates_.length = - orientLinearRings( - this.orientedFlatCoordinates_, 0, this.ends_, this.stride); - } - this.orientedRevision_ = this.getRevision(); - } - return this.orientedFlatCoordinates_; -}; - - -/** - * @inheritDoc - */ -Polygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) { - const simplifiedFlatCoordinates = []; - const simplifiedEnds = []; - simplifiedFlatCoordinates.length = quantizeArray( - this.flatCoordinates, 0, this.ends_, this.stride, - Math.sqrt(squaredTolerance), - simplifiedFlatCoordinates, 0, simplifiedEnds); - return new Polygon(simplifiedFlatCoordinates, GeometryLayout.XY, simplifiedEnds); -}; - - -/** - * @inheritDoc - * @api - */ -Polygon.prototype.getType = function() { - return GeometryType.POLYGON; -}; - - -/** - * @inheritDoc - * @api - */ -Polygon.prototype.intersectsExtent = function(extent) { - return intersectsLinearRingArray( - this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, extent); -}; - - -/** - * Set the coordinates of the polygon. - * @param {!Array.>} coordinates Coordinates. - * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. - * @override - * @api - */ -Polygon.prototype.setCoordinates = function(coordinates, opt_layout) { - this.setLayout(opt_layout, coordinates, 2); - if (!this.flatCoordinates) { - this.flatCoordinates = []; - } - const ends = deflateCoordinatesArray( - this.flatCoordinates, 0, coordinates, this.stride, this.ends_); - this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1]; - this.changed(); -}; - export default Polygon; diff --git a/src/ol/geom/SimpleGeometry.js b/src/ol/geom/SimpleGeometry.js index 52edc56423..e456a5e2ef 100644 --- a/src/ol/geom/SimpleGeometry.js +++ b/src/ol/geom/SimpleGeometry.js @@ -19,29 +19,243 @@ import {clear} from '../obj.js'; * @extends {module:ol/geom/Geometry} * @api */ -const SimpleGeometry = function() { +class SimpleGeometry { + constructor() { - Geometry.call(this); + Geometry.call(this); + + /** + * @protected + * @type {module:ol/geom/GeometryLayout} + */ + this.layout = GeometryLayout.XY; + + /** + * @protected + * @type {number} + */ + this.stride = 2; + + /** + * @protected + * @type {Array.} + */ + this.flatCoordinates = null; + + } /** - * @protected - * @type {module:ol/geom/GeometryLayout} + * @inheritDoc */ - this.layout = GeometryLayout.XY; + computeExtent(extent) { + return createOrUpdateFromFlatCoordinates(this.flatCoordinates, + 0, this.flatCoordinates.length, this.stride, extent); + } /** - * @protected - * @type {number} + * @abstract + * @return {Array} Coordinates. */ - this.stride = 2; + getCoordinates() {} /** - * @protected - * @type {Array.} + * Return the first coordinate of the geometry. + * @return {module:ol/coordinate~Coordinate} First coordinate. + * @api */ - this.flatCoordinates = null; + getFirstCoordinate() { + return this.flatCoordinates.slice(0, this.stride); + } -}; + /** + * @return {Array.} Flat coordinates. + */ + getFlatCoordinates() { + return this.flatCoordinates; + } + + /** + * Return the last coordinate of the geometry. + * @return {module:ol/coordinate~Coordinate} Last point. + * @api + */ + getLastCoordinate() { + return this.flatCoordinates.slice(this.flatCoordinates.length - this.stride); + } + + /** + * Return the {@link module:ol/geom/GeometryLayout~GeometryLayout layout} of the geometry. + * @return {module:ol/geom/GeometryLayout} Layout. + * @api + */ + getLayout() { + return this.layout; + } + + /** + * @inheritDoc + */ + getSimplifiedGeometry(squaredTolerance) { + if (this.simplifiedGeometryRevision != this.getRevision()) { + clear(this.simplifiedGeometryCache); + this.simplifiedGeometryMaxMinSquaredTolerance = 0; + this.simplifiedGeometryRevision = this.getRevision(); + } + // If squaredTolerance is negative or if we know that simplification will not + // have any effect then just return this. + if (squaredTolerance < 0 || + (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 && + squaredTolerance <= this.simplifiedGeometryMaxMinSquaredTolerance)) { + return this; + } + const key = squaredTolerance.toString(); + if (this.simplifiedGeometryCache.hasOwnProperty(key)) { + return this.simplifiedGeometryCache[key]; + } else { + const simplifiedGeometry = + this.getSimplifiedGeometryInternal(squaredTolerance); + const simplifiedFlatCoordinates = simplifiedGeometry.getFlatCoordinates(); + if (simplifiedFlatCoordinates.length < this.flatCoordinates.length) { + this.simplifiedGeometryCache[key] = simplifiedGeometry; + return simplifiedGeometry; + } else { + // Simplification did not actually remove any coordinates. We now know + // that any calls to getSimplifiedGeometry with a squaredTolerance less + // than or equal to the current squaredTolerance will also not have any + // effect. This allows us to short circuit simplification (saving CPU + // cycles) and prevents the cache of simplified geometries from filling + // up with useless identical copies of this geometry (saving memory). + this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance; + return this; + } + } + } + + /** + * @param {number} squaredTolerance Squared tolerance. + * @return {module:ol/geom/SimpleGeometry} Simplified geometry. + * @protected + */ + getSimplifiedGeometryInternal(squaredTolerance) { + return this; + } + + /** + * @return {number} Stride. + */ + getStride() { + return this.stride; + } + + /** + * @param {module:ol/geom/GeometryLayout} layout Layout. + * @param {Array.} flatCoordinates Flat coordinates. + */ + setFlatCoordinates(layout, flatCoordinates) { + this.stride = getStrideForLayout(layout); + this.layout = layout; + this.flatCoordinates = flatCoordinates; + } + + /** + * @abstract + * @param {!Array} coordinates Coordinates. + * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. + */ + setCoordinates(coordinates, opt_layout) {} + + /** + * @param {module:ol/geom/GeometryLayout|undefined} layout Layout. + * @param {Array} coordinates Coordinates. + * @param {number} nesting Nesting. + * @protected + */ + setLayout(layout, coordinates, nesting) { + /** @type {number} */ + let stride; + if (layout) { + stride = getStrideForLayout(layout); + } else { + for (let i = 0; i < nesting; ++i) { + if (coordinates.length === 0) { + this.layout = GeometryLayout.XY; + this.stride = 2; + return; + } else { + coordinates = /** @type {Array} */ (coordinates[0]); + } + } + stride = coordinates.length; + layout = getLayoutForStride(stride); + } + this.layout = layout; + this.stride = stride; + } + + /** + * @inheritDoc + * @api + */ + applyTransform(transformFn) { + if (this.flatCoordinates) { + transformFn(this.flatCoordinates, this.flatCoordinates, this.stride); + this.changed(); + } + } + + /** + * @inheritDoc + * @api + */ + rotate(angle, anchor) { + const flatCoordinates = this.getFlatCoordinates(); + if (flatCoordinates) { + const stride = this.getStride(); + rotate( + flatCoordinates, 0, flatCoordinates.length, + stride, angle, anchor, flatCoordinates); + this.changed(); + } + } + + /** + * @inheritDoc + * @api + */ + scale(sx, opt_sy, opt_anchor) { + let sy = opt_sy; + if (sy === undefined) { + sy = sx; + } + let anchor = opt_anchor; + if (!anchor) { + anchor = getCenter(this.getExtent()); + } + const flatCoordinates = this.getFlatCoordinates(); + if (flatCoordinates) { + const stride = this.getStride(); + scale( + flatCoordinates, 0, flatCoordinates.length, + stride, sx, sy, anchor, flatCoordinates); + this.changed(); + } + } + + /** + * @inheritDoc + * @api + */ + translate(deltaX, deltaY) { + const flatCoordinates = this.getFlatCoordinates(); + if (flatCoordinates) { + const stride = this.getStride(); + translate( + flatCoordinates, 0, flatCoordinates.length, stride, + deltaX, deltaY, flatCoordinates); + this.changed(); + } + } +} inherits(SimpleGeometry, Geometry); @@ -88,234 +302,6 @@ export function getStrideForLayout(layout) { SimpleGeometry.prototype.containsXY = FALSE; -/** - * @inheritDoc - */ -SimpleGeometry.prototype.computeExtent = function(extent) { - return createOrUpdateFromFlatCoordinates(this.flatCoordinates, - 0, this.flatCoordinates.length, this.stride, extent); -}; - - -/** - * @abstract - * @return {Array} Coordinates. - */ -SimpleGeometry.prototype.getCoordinates = function() {}; - - -/** - * Return the first coordinate of the geometry. - * @return {module:ol/coordinate~Coordinate} First coordinate. - * @api - */ -SimpleGeometry.prototype.getFirstCoordinate = function() { - return this.flatCoordinates.slice(0, this.stride); -}; - - -/** - * @return {Array.} Flat coordinates. - */ -SimpleGeometry.prototype.getFlatCoordinates = function() { - return this.flatCoordinates; -}; - - -/** - * Return the last coordinate of the geometry. - * @return {module:ol/coordinate~Coordinate} Last point. - * @api - */ -SimpleGeometry.prototype.getLastCoordinate = function() { - return this.flatCoordinates.slice(this.flatCoordinates.length - this.stride); -}; - - -/** - * Return the {@link module:ol/geom/GeometryLayout~GeometryLayout layout} of the geometry. - * @return {module:ol/geom/GeometryLayout} Layout. - * @api - */ -SimpleGeometry.prototype.getLayout = function() { - return this.layout; -}; - - -/** - * @inheritDoc - */ -SimpleGeometry.prototype.getSimplifiedGeometry = function(squaredTolerance) { - if (this.simplifiedGeometryRevision != this.getRevision()) { - clear(this.simplifiedGeometryCache); - this.simplifiedGeometryMaxMinSquaredTolerance = 0; - this.simplifiedGeometryRevision = this.getRevision(); - } - // If squaredTolerance is negative or if we know that simplification will not - // have any effect then just return this. - if (squaredTolerance < 0 || - (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 && - squaredTolerance <= this.simplifiedGeometryMaxMinSquaredTolerance)) { - return this; - } - const key = squaredTolerance.toString(); - if (this.simplifiedGeometryCache.hasOwnProperty(key)) { - return this.simplifiedGeometryCache[key]; - } else { - const simplifiedGeometry = - this.getSimplifiedGeometryInternal(squaredTolerance); - const simplifiedFlatCoordinates = simplifiedGeometry.getFlatCoordinates(); - if (simplifiedFlatCoordinates.length < this.flatCoordinates.length) { - this.simplifiedGeometryCache[key] = simplifiedGeometry; - return simplifiedGeometry; - } else { - // Simplification did not actually remove any coordinates. We now know - // that any calls to getSimplifiedGeometry with a squaredTolerance less - // than or equal to the current squaredTolerance will also not have any - // effect. This allows us to short circuit simplification (saving CPU - // cycles) and prevents the cache of simplified geometries from filling - // up with useless identical copies of this geometry (saving memory). - this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance; - return this; - } - } -}; - - -/** - * @param {number} squaredTolerance Squared tolerance. - * @return {module:ol/geom/SimpleGeometry} Simplified geometry. - * @protected - */ -SimpleGeometry.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) { - return this; -}; - - -/** - * @return {number} Stride. - */ -SimpleGeometry.prototype.getStride = function() { - return this.stride; -}; - - -/** - * @param {module:ol/geom/GeometryLayout} layout Layout. - * @param {Array.} flatCoordinates Flat coordinates. - */ -SimpleGeometry.prototype.setFlatCoordinates = function(layout, flatCoordinates) { - this.stride = getStrideForLayout(layout); - this.layout = layout; - this.flatCoordinates = flatCoordinates; -}; - - -/** - * @abstract - * @param {!Array} coordinates Coordinates. - * @param {module:ol/geom/GeometryLayout=} opt_layout Layout. - */ -SimpleGeometry.prototype.setCoordinates = function(coordinates, opt_layout) {}; - - -/** - * @param {module:ol/geom/GeometryLayout|undefined} layout Layout. - * @param {Array} coordinates Coordinates. - * @param {number} nesting Nesting. - * @protected - */ -SimpleGeometry.prototype.setLayout = function(layout, coordinates, nesting) { - /** @type {number} */ - let stride; - if (layout) { - stride = getStrideForLayout(layout); - } else { - for (let i = 0; i < nesting; ++i) { - if (coordinates.length === 0) { - this.layout = GeometryLayout.XY; - this.stride = 2; - return; - } else { - coordinates = /** @type {Array} */ (coordinates[0]); - } - } - stride = coordinates.length; - layout = getLayoutForStride(stride); - } - this.layout = layout; - this.stride = stride; -}; - - -/** - * @inheritDoc - * @api - */ -SimpleGeometry.prototype.applyTransform = function(transformFn) { - if (this.flatCoordinates) { - transformFn(this.flatCoordinates, this.flatCoordinates, this.stride); - this.changed(); - } -}; - - -/** - * @inheritDoc - * @api - */ -SimpleGeometry.prototype.rotate = function(angle, anchor) { - const flatCoordinates = this.getFlatCoordinates(); - if (flatCoordinates) { - const stride = this.getStride(); - rotate( - flatCoordinates, 0, flatCoordinates.length, - stride, angle, anchor, flatCoordinates); - this.changed(); - } -}; - - -/** - * @inheritDoc - * @api - */ -SimpleGeometry.prototype.scale = function(sx, opt_sy, opt_anchor) { - let sy = opt_sy; - if (sy === undefined) { - sy = sx; - } - let anchor = opt_anchor; - if (!anchor) { - anchor = getCenter(this.getExtent()); - } - const flatCoordinates = this.getFlatCoordinates(); - if (flatCoordinates) { - const stride = this.getStride(); - scale( - flatCoordinates, 0, flatCoordinates.length, - stride, sx, sy, anchor, flatCoordinates); - this.changed(); - } -}; - - -/** - * @inheritDoc - * @api - */ -SimpleGeometry.prototype.translate = function(deltaX, deltaY) { - const flatCoordinates = this.getFlatCoordinates(); - if (flatCoordinates) { - const stride = this.getStride(); - translate( - flatCoordinates, 0, flatCoordinates.length, stride, - deltaX, deltaY, flatCoordinates); - this.changed(); - } -}; - - /** * @param {module:ol/geom/SimpleGeometry} simpleGeometry Simple geometry. * @param {module:ol/transform~Transform} transform Transform. diff --git a/src/ol/interaction/DragAndDrop.js b/src/ol/interaction/DragAndDrop.js index 42db71deb2..c34e1cf5e6 100644 --- a/src/ol/interaction/DragAndDrop.js +++ b/src/ol/interaction/DragAndDrop.js @@ -89,47 +89,156 @@ inherits(DragAndDropEvent, Event); * @param {module:ol/interaction/DragAndDrop~Options=} opt_options Options. * @api */ -const DragAndDrop = function(opt_options) { +class DragAndDrop { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - Interaction.call(this, { - handleEvent: TRUE - }); + Interaction.call(this, { + handleEvent: TRUE + }); + + /** + * @private + * @type {Array.} + */ + this.formatConstructors_ = options.formatConstructors ? + options.formatConstructors : []; + + /** + * @private + * @type {module:ol/proj/Projection} + */ + this.projection_ = options.projection ? + getProjection(options.projection) : null; + + /** + * @private + * @type {Array.} + */ + this.dropListenKeys_ = null; + + /** + * @private + * @type {module:ol/source/Vector} + */ + this.source_ = options.source || null; + + /** + * @private + * @type {Element} + */ + this.target = options.target ? options.target : null; + + } + + /** + * @param {File} file File. + * @param {Event} event Load event. + * @private + */ + handleResult_(file, event) { + const result = event.target.result; + const map = this.getMap(); + let projection = this.projection_; + if (!projection) { + const view = map.getView(); + projection = view.getProjection(); + } + + const formatConstructors = this.formatConstructors_; + let features = []; + for (let i = 0, ii = formatConstructors.length; i < ii; ++i) { + /** + * Avoid "cannot instantiate abstract class" error. + * @type {Function} + */ + const formatConstructor = formatConstructors[i]; + /** + * @type {module:ol/format/Feature} + */ + const format = new formatConstructor(); + features = this.tryReadFeatures_(format, result, { + featureProjection: projection + }); + if (features && features.length > 0) { + break; + } + } + if (this.source_) { + this.source_.clear(); + this.source_.addFeatures(features); + } + this.dispatchEvent( + new DragAndDropEvent( + DragAndDropEventType.ADD_FEATURES, file, + features, projection)); + } /** * @private - * @type {Array.} */ - this.formatConstructors_ = options.formatConstructors ? - options.formatConstructors : []; + registerListeners_() { + const map = this.getMap(); + if (map) { + const dropArea = this.target ? this.target : map.getViewport(); + this.dropListenKeys_ = [ + listen(dropArea, EventType.DROP, handleDrop, this), + listen(dropArea, EventType.DRAGENTER, handleStop, this), + listen(dropArea, EventType.DRAGOVER, handleStop, this), + listen(dropArea, EventType.DROP, handleStop, this) + ]; + } + } + + /** + * @inheritDoc + */ + setActive(active) { + Interaction.prototype.setActive.call(this, active); + if (active) { + this.registerListeners_(); + } else { + this.unregisterListeners_(); + } + } + + /** + * @inheritDoc + */ + setMap(map) { + this.unregisterListeners_(); + Interaction.prototype.setMap.call(this, map); + if (this.getActive()) { + this.registerListeners_(); + } + } + + /** + * @param {module:ol/format/Feature} format Format. + * @param {string} text Text. + * @param {module:ol/format/Feature~ReadOptions} options Read options. + * @private + * @return {Array.} Features. + */ + tryReadFeatures_(format, text, options) { + try { + return format.readFeatures(text, options); + } catch (e) { + return null; + } + } /** * @private - * @type {module:ol/proj/Projection} */ - this.projection_ = options.projection ? - getProjection(options.projection) : null; - - /** - * @private - * @type {Array.} - */ - this.dropListenKeys_ = null; - - /** - * @private - * @type {module:ol/source/Vector} - */ - this.source_ = options.source || null; - - /** - * @private - * @type {Element} - */ - this.target = options.target ? options.target : null; - -}; + unregisterListeners_() { + if (this.dropListenKeys_) { + this.dropListenKeys_.forEach(unlistenByKey); + this.dropListenKeys_ = null; + } + } +} inherits(DragAndDrop, Interaction); @@ -159,117 +268,4 @@ function handleStop(event) { } -/** - * @param {File} file File. - * @param {Event} event Load event. - * @private - */ -DragAndDrop.prototype.handleResult_ = function(file, event) { - const result = event.target.result; - const map = this.getMap(); - let projection = this.projection_; - if (!projection) { - const view = map.getView(); - projection = view.getProjection(); - } - - const formatConstructors = this.formatConstructors_; - let features = []; - for (let i = 0, ii = formatConstructors.length; i < ii; ++i) { - /** - * Avoid "cannot instantiate abstract class" error. - * @type {Function} - */ - const formatConstructor = formatConstructors[i]; - /** - * @type {module:ol/format/Feature} - */ - const format = new formatConstructor(); - features = this.tryReadFeatures_(format, result, { - featureProjection: projection - }); - if (features && features.length > 0) { - break; - } - } - if (this.source_) { - this.source_.clear(); - this.source_.addFeatures(features); - } - this.dispatchEvent( - new DragAndDropEvent( - DragAndDropEventType.ADD_FEATURES, file, - features, projection)); -}; - - -/** - * @private - */ -DragAndDrop.prototype.registerListeners_ = function() { - const map = this.getMap(); - if (map) { - const dropArea = this.target ? this.target : map.getViewport(); - this.dropListenKeys_ = [ - listen(dropArea, EventType.DROP, handleDrop, this), - listen(dropArea, EventType.DRAGENTER, handleStop, this), - listen(dropArea, EventType.DRAGOVER, handleStop, this), - listen(dropArea, EventType.DROP, handleStop, this) - ]; - } -}; - - -/** - * @inheritDoc - */ -DragAndDrop.prototype.setActive = function(active) { - Interaction.prototype.setActive.call(this, active); - if (active) { - this.registerListeners_(); - } else { - this.unregisterListeners_(); - } -}; - - -/** - * @inheritDoc - */ -DragAndDrop.prototype.setMap = function(map) { - this.unregisterListeners_(); - Interaction.prototype.setMap.call(this, map); - if (this.getActive()) { - this.registerListeners_(); - } -}; - - -/** - * @param {module:ol/format/Feature} format Format. - * @param {string} text Text. - * @param {module:ol/format/Feature~ReadOptions} options Read options. - * @private - * @return {Array.} Features. - */ -DragAndDrop.prototype.tryReadFeatures_ = function(format, text, options) { - try { - return format.readFeatures(text, options); - } catch (e) { - return null; - } -}; - - -/** - * @private - */ -DragAndDrop.prototype.unregisterListeners_ = function() { - if (this.dropListenKeys_) { - this.dropListenKeys_.forEach(unlistenByKey); - this.dropListenKeys_ = null; - } -}; - - export default DragAndDrop; diff --git a/src/ol/interaction/DragBox.js b/src/ol/interaction/DragBox.js index e5f38f5e32..6befab8280 100644 --- a/src/ol/interaction/DragBox.js +++ b/src/ol/interaction/DragBox.js @@ -110,47 +110,58 @@ inherits(DragBoxEvent, Event); * @param {module:ol/interaction/DragBox~Options=} opt_options Options. * @api */ -const DragBox = function(opt_options) { +class DragBox { + constructor(opt_options) { - PointerInteraction.call(this, { - handleDownEvent: handleDownEvent, - handleDragEvent: handleDragEvent, - handleUpEvent: handleUpEvent - }); + PointerInteraction.call(this, { + handleDownEvent: handleDownEvent, + handleDragEvent: handleDragEvent, + handleUpEvent: handleUpEvent + }); - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - /** - * @type {module:ol/render/Box} - * @private - */ - this.box_ = new RenderBox(options.className || 'ol-dragbox'); + /** + * @type {module:ol/render/Box} + * @private + */ + this.box_ = new RenderBox(options.className || 'ol-dragbox'); - /** - * @type {number} - * @private - */ - this.minArea_ = options.minArea !== undefined ? options.minArea : 64; + /** + * @type {number} + * @private + */ + this.minArea_ = options.minArea !== undefined ? options.minArea : 64; - /** - * @type {module:ol~Pixel} - * @private - */ - this.startPixel_ = null; + /** + * @type {module:ol~Pixel} + * @private + */ + this.startPixel_ = null; - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.condition_ = options.condition ? options.condition : always; + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.condition_ = options.condition ? options.condition : always; - /** - * @private - * @type {module:ol/interaction/DragBox~EndCondition} - */ - this.boxEndCondition_ = options.boxEndCondition ? - options.boxEndCondition : defaultBoxEndCondition; -}; + /** + * @private + * @type {module:ol/interaction/DragBox~EndCondition} + */ + this.boxEndCondition_ = options.boxEndCondition ? + options.boxEndCondition : defaultBoxEndCondition; + } + + /** + * Returns geometry of last drawn box. + * @return {module:ol/geom/Polygon} Geometry. + * @api + */ + getGeometry() { + return this.box_.getGeometry(); + } +} inherits(DragBox, PointerInteraction); @@ -188,16 +199,6 @@ function handleDragEvent(mapBrowserEvent) { } -/** - * Returns geometry of last drawn box. - * @return {module:ol/geom/Polygon} Geometry. - * @api - */ -DragBox.prototype.getGeometry = function() { - return this.box_.getGeometry(); -}; - - /** * To be overridden by child classes. * FIXME: use constructor option instead of relying on overriding. diff --git a/src/ol/interaction/DragZoom.js b/src/ol/interaction/DragZoom.js index eb7e312b93..ff0b2ade1e 100644 --- a/src/ol/interaction/DragZoom.js +++ b/src/ol/interaction/DragZoom.js @@ -35,68 +35,71 @@ import DragBox from '../interaction/DragBox.js'; * @param {module:ol/interaction/DragZoom~Options=} opt_options Options. * @api */ -const DragZoom = function(opt_options) { - const options = opt_options ? opt_options : {}; +class DragZoom { + constructor(opt_options) { + const options = opt_options ? opt_options : {}; - const condition = options.condition ? options.condition : shiftKeyOnly; + const condition = options.condition ? options.condition : shiftKeyOnly; + + /** + * @private + * @type {number} + */ + this.duration_ = options.duration !== undefined ? options.duration : 200; + + /** + * @private + * @type {boolean} + */ + this.out_ = options.out !== undefined ? options.out : false; + + DragBox.call(this, { + condition: condition, + className: options.className || 'ol-dragzoom' + }); + + } /** - * @private - * @type {number} + * @inheritDoc */ - this.duration_ = options.duration !== undefined ? options.duration : 200; + onBoxEnd() { + const map = this.getMap(); - /** - * @private - * @type {boolean} - */ - this.out_ = options.out !== undefined ? options.out : false; + const view = /** @type {!module:ol/View} */ (map.getView()); - DragBox.call(this, { - condition: condition, - className: options.className || 'ol-dragzoom' - }); + const size = /** @type {!module:ol/size~Size} */ (map.getSize()); -}; + let extent = this.getGeometry().getExtent(); + + if (this.out_) { + const mapExtent = view.calculateExtent(size); + const boxPixelExtent = createOrUpdateFromCoordinates([ + map.getPixelFromCoordinate(getBottomLeft(extent)), + map.getPixelFromCoordinate(getTopRight(extent))]); + const factor = view.getResolutionForExtent(boxPixelExtent, size); + + scaleFromCenter(mapExtent, 1 / factor); + extent = mapExtent; + } + + const resolution = view.constrainResolution( + view.getResolutionForExtent(extent, size)); + + let center = getCenter(extent); + center = view.constrainCenter(center); + + view.animate({ + resolution: resolution, + center: center, + duration: this.duration_, + easing: easeOut + }); + + } +} inherits(DragZoom, DragBox); -/** - * @inheritDoc - */ -DragZoom.prototype.onBoxEnd = function() { - const map = this.getMap(); - - const view = /** @type {!module:ol/View} */ (map.getView()); - - const size = /** @type {!module:ol/size~Size} */ (map.getSize()); - - let extent = this.getGeometry().getExtent(); - - if (this.out_) { - const mapExtent = view.calculateExtent(size); - const boxPixelExtent = createOrUpdateFromCoordinates([ - map.getPixelFromCoordinate(getBottomLeft(extent)), - map.getPixelFromCoordinate(getTopRight(extent))]); - const factor = view.getResolutionForExtent(boxPixelExtent, size); - - scaleFromCenter(mapExtent, 1 / factor); - extent = mapExtent; - } - - const resolution = view.constrainResolution( - view.getResolutionForExtent(extent, size)); - - let center = getCenter(extent); - center = view.constrainCenter(center); - - view.animate({ - resolution: resolution, - center: center, - duration: this.duration_, - easing: easeOut - }); - -}; export default DragZoom; diff --git a/src/ol/interaction/Draw.js b/src/ol/interaction/Draw.js index cd42fe7137..e30dd6d24b 100644 --- a/src/ol/interaction/Draw.js +++ b/src/ol/interaction/Draw.js @@ -160,277 +160,642 @@ inherits(DrawEvent, Event); * @param {module:ol/interaction/Draw~Options} options Options. * @api */ -const Draw = function(options) { +class Draw { + constructor(options) { - PointerInteraction.call(this, { - handleDownEvent: handleDownEvent, - handleEvent: handleEvent, - handleUpEvent: handleUpEvent - }); + PointerInteraction.call(this, { + handleDownEvent: handleDownEvent, + handleEvent: handleEvent, + handleUpEvent: handleUpEvent + }); - /** - * @type {boolean} - * @private - */ - this.shouldHandle_ = false; + /** + * @type {boolean} + * @private + */ + this.shouldHandle_ = false; - /** - * @type {module:ol~Pixel} - * @private - */ - this.downPx_ = null; + /** + * @type {module:ol~Pixel} + * @private + */ + this.downPx_ = null; - /** - * @type {number|undefined} - * @private - */ - this.downTimeout_; + /** + * @type {number|undefined} + * @private + */ + this.downTimeout_; - /** - * @type {number|undefined} - * @private - */ - this.lastDragTime_; + /** + * @type {number|undefined} + * @private + */ + this.lastDragTime_; - /** - * @type {boolean} - * @private - */ - this.freehand_ = false; + /** + * @type {boolean} + * @private + */ + this.freehand_ = false; - /** - * Target source for drawn features. - * @type {module:ol/source/Vector} - * @private - */ - this.source_ = options.source ? options.source : null; + /** + * Target source for drawn features. + * @type {module:ol/source/Vector} + * @private + */ + this.source_ = options.source ? options.source : null; - /** - * Target collection for drawn features. - * @type {module:ol/Collection.} - * @private - */ - this.features_ = options.features ? options.features : null; + /** + * Target collection for drawn features. + * @type {module:ol/Collection.} + * @private + */ + this.features_ = options.features ? options.features : null; - /** - * Pixel distance for snapping. - * @type {number} - * @private - */ - this.snapTolerance_ = options.snapTolerance ? options.snapTolerance : 12; + /** + * Pixel distance for snapping. + * @type {number} + * @private + */ + this.snapTolerance_ = options.snapTolerance ? options.snapTolerance : 12; - /** - * Geometry type. - * @type {module:ol/geom/GeometryType} - * @private - */ - this.type_ = /** @type {module:ol/geom/GeometryType} */ (options.type); + /** + * Geometry type. + * @type {module:ol/geom/GeometryType} + * @private + */ + this.type_ = /** @type {module:ol/geom/GeometryType} */ (options.type); - /** - * Drawing mode (derived from geometry type. - * @type {module:ol/interaction/Draw~Mode} - * @private - */ - this.mode_ = getMode(this.type_); + /** + * Drawing mode (derived from geometry type. + * @type {module:ol/interaction/Draw~Mode} + * @private + */ + this.mode_ = getMode(this.type_); - /** - * Stop click, singleclick, and doubleclick events from firing during drawing. - * Default is `false`. - * @type {boolean} - * @private - */ - this.stopClick_ = !!options.stopClick; + /** + * Stop click, singleclick, and doubleclick events from firing during drawing. + * Default is `false`. + * @type {boolean} + * @private + */ + this.stopClick_ = !!options.stopClick; - /** - * The number of points that must be drawn before a polygon ring or line - * string can be finished. The default is 3 for polygon rings and 2 for - * line strings. - * @type {number} - * @private - */ - this.minPoints_ = options.minPoints ? - options.minPoints : - (this.mode_ === Mode.POLYGON ? 3 : 2); + /** + * The number of points that must be drawn before a polygon ring or line + * string can be finished. The default is 3 for polygon rings and 2 for + * line strings. + * @type {number} + * @private + */ + this.minPoints_ = options.minPoints ? + options.minPoints : + (this.mode_ === Mode.POLYGON ? 3 : 2); - /** - * The number of points that can be drawn before a polygon ring or line string - * is finished. The default is no restriction. - * @type {number} - * @private - */ - this.maxPoints_ = options.maxPoints ? options.maxPoints : Infinity; + /** + * The number of points that can be drawn before a polygon ring or line string + * is finished. The default is no restriction. + * @type {number} + * @private + */ + this.maxPoints_ = options.maxPoints ? options.maxPoints : Infinity; - /** - * A function to decide if a potential finish coordinate is permissible - * @private - * @type {module:ol/events/condition~Condition} - */ - this.finishCondition_ = options.finishCondition ? options.finishCondition : TRUE; + /** + * A function to decide if a potential finish coordinate is permissible + * @private + * @type {module:ol/events/condition~Condition} + */ + this.finishCondition_ = options.finishCondition ? options.finishCondition : TRUE; - let geometryFunction = options.geometryFunction; - if (!geometryFunction) { - if (this.type_ === GeometryType.CIRCLE) { - /** - * @param {!Array.} coordinates - * The coordinates. - * @param {module:ol/geom/SimpleGeometry=} opt_geometry Optional geometry. - * @return {module:ol/geom/SimpleGeometry} A geometry. - */ - geometryFunction = function(coordinates, opt_geometry) { - const circle = opt_geometry ? /** @type {module:ol/geom/Circle} */ (opt_geometry) : - new Circle([NaN, NaN]); - const squaredLength = squaredCoordinateDistance( - coordinates[0], coordinates[1]); - circle.setCenterAndRadius(coordinates[0], Math.sqrt(squaredLength)); - return circle; - }; - } else { - let Constructor; - const mode = this.mode_; - if (mode === Mode.POINT) { - Constructor = Point; - } else if (mode === Mode.LINE_STRING) { - Constructor = LineString; - } else if (mode === Mode.POLYGON) { - Constructor = Polygon; - } - /** - * @param {!Array.} coordinates - * The coordinates. - * @param {module:ol/geom/SimpleGeometry=} opt_geometry Optional geometry. - * @return {module:ol/geom/SimpleGeometry} A geometry. - */ - geometryFunction = function(coordinates, opt_geometry) { - let geometry = opt_geometry; - if (geometry) { - if (mode === Mode.POLYGON) { - if (coordinates[0].length) { - // Add a closing coordinate to match the first - geometry.setCoordinates([coordinates[0].concat([coordinates[0][0]])]); + let geometryFunction = options.geometryFunction; + if (!geometryFunction) { + if (this.type_ === GeometryType.CIRCLE) { + /** + * @param {!Array.} coordinates + * The coordinates. + * @param {module:ol/geom/SimpleGeometry=} opt_geometry Optional geometry. + * @return {module:ol/geom/SimpleGeometry} A geometry. + */ + geometryFunction = function(coordinates, opt_geometry) { + const circle = opt_geometry ? /** @type {module:ol/geom/Circle} */ (opt_geometry) : + new Circle([NaN, NaN]); + const squaredLength = squaredCoordinateDistance( + coordinates[0], coordinates[1]); + circle.setCenterAndRadius(coordinates[0], Math.sqrt(squaredLength)); + return circle; + }; + } else { + let Constructor; + const mode = this.mode_; + if (mode === Mode.POINT) { + Constructor = Point; + } else if (mode === Mode.LINE_STRING) { + Constructor = LineString; + } else if (mode === Mode.POLYGON) { + Constructor = Polygon; + } + /** + * @param {!Array.} coordinates + * The coordinates. + * @param {module:ol/geom/SimpleGeometry=} opt_geometry Optional geometry. + * @return {module:ol/geom/SimpleGeometry} A geometry. + */ + geometryFunction = function(coordinates, opt_geometry) { + let geometry = opt_geometry; + if (geometry) { + if (mode === Mode.POLYGON) { + if (coordinates[0].length) { + // Add a closing coordinate to match the first + geometry.setCoordinates([coordinates[0].concat([coordinates[0][0]])]); + } else { + geometry.setCoordinates([]); + } } else { - geometry.setCoordinates([]); + geometry.setCoordinates(coordinates); } } else { - geometry.setCoordinates(coordinates); + geometry = new Constructor(coordinates); + } + return geometry; + }; + } + } + + /** + * @type {module:ol/interaction/Draw~GeometryFunction} + * @private + */ + this.geometryFunction_ = geometryFunction; + + /** + * @type {number} + * @private + */ + this.dragVertexDelay_ = options.dragVertexDelay !== undefined ? options.dragVertexDelay : 500; + + /** + * Finish coordinate for the feature (first point for polygons, last point for + * linestrings). + * @type {module:ol/coordinate~Coordinate} + * @private + */ + this.finishCoordinate_ = null; + + /** + * Sketch feature. + * @type {module:ol/Feature} + * @private + */ + this.sketchFeature_ = null; + + /** + * Sketch point. + * @type {module:ol/Feature} + * @private + */ + this.sketchPoint_ = null; + + /** + * Sketch coordinates. Used when drawing a line or polygon. + * @type {module:ol/coordinate~Coordinate|Array.|Array.>} + * @private + */ + this.sketchCoords_ = null; + + /** + * Sketch line. Used when drawing polygon. + * @type {module:ol/Feature} + * @private + */ + this.sketchLine_ = null; + + /** + * Sketch line coordinates. Used when drawing a polygon or circle. + * @type {Array.} + * @private + */ + this.sketchLineCoords_ = null; + + /** + * Squared tolerance for handling up events. If the squared distance + * between a down and up event is greater than this tolerance, up events + * will not be handled. + * @type {number} + * @private + */ + this.squaredClickTolerance_ = options.clickTolerance ? + options.clickTolerance * options.clickTolerance : 36; + + /** + * Draw overlay where our sketch features are drawn. + * @type {module:ol/layer/Vector} + * @private + */ + this.overlay_ = new VectorLayer({ + source: new VectorSource({ + useSpatialIndex: false, + wrapX: options.wrapX ? options.wrapX : false + }), + style: options.style ? options.style : + getDefaultStyleFunction(), + updateWhileInteracting: true + }); + + /** + * Name of the geometry attribute for newly created features. + * @type {string|undefined} + * @private + */ + this.geometryName_ = options.geometryName; + + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.condition_ = options.condition ? options.condition : noModifierKeys; + + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.freehandCondition_; + if (options.freehand) { + this.freehandCondition_ = always; + } else { + this.freehandCondition_ = options.freehandCondition ? + options.freehandCondition : shiftKeyOnly; + } + + listen(this, + getChangeEventType(InteractionProperty.ACTIVE), + this.updateState_, this); + + } + + /** + * @inheritDoc + */ + setMap(map) { + PointerInteraction.prototype.setMap.call(this, map); + this.updateState_(); + } + + /** + * Handle move events. + * @param {module:ol/MapBrowserEvent} event A move event. + * @return {boolean} Pass the event to other interactions. + * @private + */ + handlePointerMove_(event) { + if (this.downPx_ && + ((!this.freehand_ && this.shouldHandle_) || + (this.freehand_ && !this.shouldHandle_))) { + const downPx = this.downPx_; + const clickPx = event.pixel; + const dx = downPx[0] - clickPx[0]; + const dy = downPx[1] - clickPx[1]; + const squaredDistance = dx * dx + dy * dy; + this.shouldHandle_ = this.freehand_ ? + squaredDistance > this.squaredClickTolerance_ : + squaredDistance <= this.squaredClickTolerance_; + if (!this.shouldHandle_) { + return true; + } + } + + if (this.finishCoordinate_) { + this.modifyDrawing_(event); + } else { + this.createOrUpdateSketchPoint_(event); + } + return true; + } + + /** + * Determine if an event is within the snapping tolerance of the start coord. + * @param {module:ol/MapBrowserEvent} event Event. + * @return {boolean} The event is within the snapping tolerance of the start. + * @private + */ + atFinish_(event) { + let at = false; + if (this.sketchFeature_) { + let potentiallyDone = false; + let potentiallyFinishCoordinates = [this.finishCoordinate_]; + if (this.mode_ === Mode.LINE_STRING) { + potentiallyDone = this.sketchCoords_.length > this.minPoints_; + } else if (this.mode_ === Mode.POLYGON) { + potentiallyDone = this.sketchCoords_[0].length > + this.minPoints_; + potentiallyFinishCoordinates = [this.sketchCoords_[0][0], + this.sketchCoords_[0][this.sketchCoords_[0].length - 2]]; + } + if (potentiallyDone) { + const map = event.map; + for (let i = 0, ii = potentiallyFinishCoordinates.length; i < ii; i++) { + const finishCoordinate = potentiallyFinishCoordinates[i]; + const finishPixel = map.getPixelFromCoordinate(finishCoordinate); + const pixel = event.pixel; + const dx = pixel[0] - finishPixel[0]; + const dy = pixel[1] - finishPixel[1]; + const snapTolerance = this.freehand_ ? 1 : this.snapTolerance_; + at = Math.sqrt(dx * dx + dy * dy) <= snapTolerance; + if (at) { + this.finishCoordinate_ = finishCoordinate; + break; } - } else { - geometry = new Constructor(coordinates); } - return geometry; - }; + } + } + return at; + } + + /** + * @param {module:ol/MapBrowserEvent} event Event. + * @private + */ + createOrUpdateSketchPoint_(event) { + const coordinates = event.coordinate.slice(); + if (!this.sketchPoint_) { + this.sketchPoint_ = new Feature(new Point(coordinates)); + this.updateSketchFeatures_(); + } else { + const sketchPointGeom = /** @type {module:ol/geom/Point} */ (this.sketchPoint_.getGeometry()); + sketchPointGeom.setCoordinates(coordinates); } } /** - * @type {module:ol/interaction/Draw~GeometryFunction} + * Start the drawing. + * @param {module:ol/MapBrowserEvent} event Event. * @private */ - this.geometryFunction_ = geometryFunction; - - /** - * @type {number} - * @private - */ - this.dragVertexDelay_ = options.dragVertexDelay !== undefined ? options.dragVertexDelay : 500; - - /** - * Finish coordinate for the feature (first point for polygons, last point for - * linestrings). - * @type {module:ol/coordinate~Coordinate} - * @private - */ - this.finishCoordinate_ = null; - - /** - * Sketch feature. - * @type {module:ol/Feature} - * @private - */ - this.sketchFeature_ = null; - - /** - * Sketch point. - * @type {module:ol/Feature} - * @private - */ - this.sketchPoint_ = null; - - /** - * Sketch coordinates. Used when drawing a line or polygon. - * @type {module:ol/coordinate~Coordinate|Array.|Array.>} - * @private - */ - this.sketchCoords_ = null; - - /** - * Sketch line. Used when drawing polygon. - * @type {module:ol/Feature} - * @private - */ - this.sketchLine_ = null; - - /** - * Sketch line coordinates. Used when drawing a polygon or circle. - * @type {Array.} - * @private - */ - this.sketchLineCoords_ = null; - - /** - * Squared tolerance for handling up events. If the squared distance - * between a down and up event is greater than this tolerance, up events - * will not be handled. - * @type {number} - * @private - */ - this.squaredClickTolerance_ = options.clickTolerance ? - options.clickTolerance * options.clickTolerance : 36; - - /** - * Draw overlay where our sketch features are drawn. - * @type {module:ol/layer/Vector} - * @private - */ - this.overlay_ = new VectorLayer({ - source: new VectorSource({ - useSpatialIndex: false, - wrapX: options.wrapX ? options.wrapX : false - }), - style: options.style ? options.style : - getDefaultStyleFunction(), - updateWhileInteracting: true - }); - - /** - * Name of the geometry attribute for newly created features. - * @type {string|undefined} - * @private - */ - this.geometryName_ = options.geometryName; - - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.condition_ = options.condition ? options.condition : noModifierKeys; - - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.freehandCondition_; - if (options.freehand) { - this.freehandCondition_ = always; - } else { - this.freehandCondition_ = options.freehandCondition ? - options.freehandCondition : shiftKeyOnly; + startDrawing_(event) { + const start = event.coordinate; + this.finishCoordinate_ = start; + if (this.mode_ === Mode.POINT) { + this.sketchCoords_ = start.slice(); + } else if (this.mode_ === Mode.POLYGON) { + this.sketchCoords_ = [[start.slice(), start.slice()]]; + this.sketchLineCoords_ = this.sketchCoords_[0]; + } else { + this.sketchCoords_ = [start.slice(), start.slice()]; + } + if (this.sketchLineCoords_) { + this.sketchLine_ = new Feature( + new LineString(this.sketchLineCoords_)); + } + const geometry = this.geometryFunction_(this.sketchCoords_); + this.sketchFeature_ = new Feature(); + if (this.geometryName_) { + this.sketchFeature_.setGeometryName(this.geometryName_); + } + this.sketchFeature_.setGeometry(geometry); + this.updateSketchFeatures_(); + this.dispatchEvent(new DrawEvent(DrawEventType.DRAWSTART, this.sketchFeature_)); } - listen(this, - getChangeEventType(InteractionProperty.ACTIVE), - this.updateState_, this); + /** + * Modify the drawing. + * @param {module:ol/MapBrowserEvent} event Event. + * @private + */ + modifyDrawing_(event) { + let coordinate = event.coordinate; + const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (this.sketchFeature_.getGeometry()); + let coordinates, last; + if (this.mode_ === Mode.POINT) { + last = this.sketchCoords_; + } else if (this.mode_ === Mode.POLYGON) { + coordinates = this.sketchCoords_[0]; + last = coordinates[coordinates.length - 1]; + if (this.atFinish_(event)) { + // snap to finish + coordinate = this.finishCoordinate_.slice(); + } + } else { + coordinates = this.sketchCoords_; + last = coordinates[coordinates.length - 1]; + } + last[0] = coordinate[0]; + last[1] = coordinate[1]; + this.geometryFunction_(/** @type {!Array.} */ (this.sketchCoords_), geometry); + if (this.sketchPoint_) { + const sketchPointGeom = /** @type {module:ol/geom/Point} */ (this.sketchPoint_.getGeometry()); + sketchPointGeom.setCoordinates(coordinate); + } + let sketchLineGeom; + if (geometry instanceof Polygon && + this.mode_ !== Mode.POLYGON) { + if (!this.sketchLine_) { + this.sketchLine_ = new Feature(); + } + const ring = geometry.getLinearRing(0); + sketchLineGeom = /** @type {module:ol/geom/LineString} */ (this.sketchLine_.getGeometry()); + if (!sketchLineGeom) { + sketchLineGeom = new LineString(ring.getFlatCoordinates(), ring.getLayout()); + this.sketchLine_.setGeometry(sketchLineGeom); + } else { + sketchLineGeom.setFlatCoordinates( + ring.getLayout(), ring.getFlatCoordinates()); + sketchLineGeom.changed(); + } + } else if (this.sketchLineCoords_) { + sketchLineGeom = /** @type {module:ol/geom/LineString} */ (this.sketchLine_.getGeometry()); + sketchLineGeom.setCoordinates(this.sketchLineCoords_); + } + this.updateSketchFeatures_(); + } -}; + /** + * Add a new coordinate to the drawing. + * @param {module:ol/MapBrowserEvent} event Event. + * @private + */ + addToDrawing_(event) { + const coordinate = event.coordinate; + const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (this.sketchFeature_.getGeometry()); + let done; + let coordinates; + if (this.mode_ === Mode.LINE_STRING) { + this.finishCoordinate_ = coordinate.slice(); + coordinates = this.sketchCoords_; + if (coordinates.length >= this.maxPoints_) { + if (this.freehand_) { + coordinates.pop(); + } else { + done = true; + } + } + coordinates.push(coordinate.slice()); + this.geometryFunction_(coordinates, geometry); + } else if (this.mode_ === Mode.POLYGON) { + coordinates = this.sketchCoords_[0]; + if (coordinates.length >= this.maxPoints_) { + if (this.freehand_) { + coordinates.pop(); + } else { + done = true; + } + } + coordinates.push(coordinate.slice()); + if (done) { + this.finishCoordinate_ = coordinates[0]; + } + this.geometryFunction_(this.sketchCoords_, geometry); + } + this.updateSketchFeatures_(); + if (done) { + this.finishDrawing(); + } + } + + /** + * Remove last point of the feature currently being drawn. + * @api + */ + removeLastPoint() { + if (!this.sketchFeature_) { + return; + } + const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (this.sketchFeature_.getGeometry()); + let coordinates, sketchLineGeom; + if (this.mode_ === Mode.LINE_STRING) { + coordinates = this.sketchCoords_; + coordinates.splice(-2, 1); + this.geometryFunction_(coordinates, geometry); + if (coordinates.length >= 2) { + this.finishCoordinate_ = coordinates[coordinates.length - 2].slice(); + } + } else if (this.mode_ === Mode.POLYGON) { + coordinates = this.sketchCoords_[0]; + coordinates.splice(-2, 1); + sketchLineGeom = /** @type {module:ol/geom/LineString} */ (this.sketchLine_.getGeometry()); + sketchLineGeom.setCoordinates(coordinates); + this.geometryFunction_(this.sketchCoords_, geometry); + } + + if (coordinates.length === 0) { + this.finishCoordinate_ = null; + } + + this.updateSketchFeatures_(); + } + + /** + * Stop drawing and add the sketch feature to the target layer. + * The {@link module:ol/interaction/Draw~DrawEventType.DRAWEND} event is + * dispatched before inserting the feature. + * @api + */ + finishDrawing() { + const sketchFeature = this.abortDrawing_(); + if (!sketchFeature) { + return; + } + let coordinates = this.sketchCoords_; + const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (sketchFeature.getGeometry()); + if (this.mode_ === Mode.LINE_STRING) { + // remove the redundant last point + coordinates.pop(); + this.geometryFunction_(coordinates, geometry); + } else if (this.mode_ === Mode.POLYGON) { + // remove the redundant last point in ring + coordinates[0].pop(); + this.geometryFunction_(coordinates, geometry); + coordinates = geometry.getCoordinates(); + } + + // cast multi-part geometries + if (this.type_ === GeometryType.MULTI_POINT) { + sketchFeature.setGeometry(new MultiPoint([coordinates])); + } else if (this.type_ === GeometryType.MULTI_LINE_STRING) { + sketchFeature.setGeometry(new MultiLineString([coordinates])); + } else if (this.type_ === GeometryType.MULTI_POLYGON) { + sketchFeature.setGeometry(new MultiPolygon([coordinates])); + } + + // First dispatch event to allow full set up of feature + this.dispatchEvent(new DrawEvent(DrawEventType.DRAWEND, sketchFeature)); + + // Then insert feature + if (this.features_) { + this.features_.push(sketchFeature); + } + if (this.source_) { + this.source_.addFeature(sketchFeature); + } + } + + /** + * Stop drawing without adding the sketch feature to the target layer. + * @return {module:ol/Feature} The sketch feature (or null if none). + * @private + */ + abortDrawing_() { + this.finishCoordinate_ = null; + const sketchFeature = this.sketchFeature_; + if (sketchFeature) { + this.sketchFeature_ = null; + this.sketchPoint_ = null; + this.sketchLine_ = null; + this.overlay_.getSource().clear(true); + } + return sketchFeature; + } + + /** + * Extend an existing geometry by adding additional points. This only works + * on features with `LineString` geometries, where the interaction will + * extend lines by adding points to the end of the coordinates array. + * @param {!module:ol/Feature} feature Feature to be extended. + * @api + */ + extend(feature) { + const geometry = feature.getGeometry(); + const lineString = /** @type {module:ol/geom/LineString} */ (geometry); + this.sketchFeature_ = feature; + this.sketchCoords_ = lineString.getCoordinates(); + const last = this.sketchCoords_[this.sketchCoords_.length - 1]; + this.finishCoordinate_ = last.slice(); + this.sketchCoords_.push(last.slice()); + this.updateSketchFeatures_(); + this.dispatchEvent(new DrawEvent(DrawEventType.DRAWSTART, this.sketchFeature_)); + } + + /** + * Redraw the sketch features. + * @private + */ + updateSketchFeatures_() { + const sketchFeatures = []; + if (this.sketchFeature_) { + sketchFeatures.push(this.sketchFeature_); + } + if (this.sketchLine_) { + sketchFeatures.push(this.sketchLine_); + } + if (this.sketchPoint_) { + sketchFeatures.push(this.sketchPoint_); + } + const overlaySource = this.overlay_.getSource(); + overlaySource.clear(true); + overlaySource.addFeatures(sketchFeatures); + } + + /** + * @private + */ + updateState_() { + const map = this.getMap(); + const active = this.getActive(); + if (!map || !active) { + this.abortDrawing_(); + } + this.overlay_.setMap(active ? map : null); + } +} inherits(Draw, PointerInteraction); @@ -446,15 +811,6 @@ function getDefaultStyleFunction() { } -/** - * @inheritDoc - */ -Draw.prototype.setMap = function(map) { - PointerInteraction.prototype.setMap.call(this, map); - this.updateState_(); -}; - - /** * Handles the {@link module:ol/MapBrowserEvent map browser event} and may actually * draw or finish the drawing. @@ -581,379 +937,12 @@ function handleUpEvent(event) { } -/** - * Handle move events. - * @param {module:ol/MapBrowserEvent} event A move event. - * @return {boolean} Pass the event to other interactions. - * @private - */ -Draw.prototype.handlePointerMove_ = function(event) { - if (this.downPx_ && - ((!this.freehand_ && this.shouldHandle_) || - (this.freehand_ && !this.shouldHandle_))) { - const downPx = this.downPx_; - const clickPx = event.pixel; - const dx = downPx[0] - clickPx[0]; - const dy = downPx[1] - clickPx[1]; - const squaredDistance = dx * dx + dy * dy; - this.shouldHandle_ = this.freehand_ ? - squaredDistance > this.squaredClickTolerance_ : - squaredDistance <= this.squaredClickTolerance_; - if (!this.shouldHandle_) { - return true; - } - } - - if (this.finishCoordinate_) { - this.modifyDrawing_(event); - } else { - this.createOrUpdateSketchPoint_(event); - } - return true; -}; - - -/** - * Determine if an event is within the snapping tolerance of the start coord. - * @param {module:ol/MapBrowserEvent} event Event. - * @return {boolean} The event is within the snapping tolerance of the start. - * @private - */ -Draw.prototype.atFinish_ = function(event) { - let at = false; - if (this.sketchFeature_) { - let potentiallyDone = false; - let potentiallyFinishCoordinates = [this.finishCoordinate_]; - if (this.mode_ === Mode.LINE_STRING) { - potentiallyDone = this.sketchCoords_.length > this.minPoints_; - } else if (this.mode_ === Mode.POLYGON) { - potentiallyDone = this.sketchCoords_[0].length > - this.minPoints_; - potentiallyFinishCoordinates = [this.sketchCoords_[0][0], - this.sketchCoords_[0][this.sketchCoords_[0].length - 2]]; - } - if (potentiallyDone) { - const map = event.map; - for (let i = 0, ii = potentiallyFinishCoordinates.length; i < ii; i++) { - const finishCoordinate = potentiallyFinishCoordinates[i]; - const finishPixel = map.getPixelFromCoordinate(finishCoordinate); - const pixel = event.pixel; - const dx = pixel[0] - finishPixel[0]; - const dy = pixel[1] - finishPixel[1]; - const snapTolerance = this.freehand_ ? 1 : this.snapTolerance_; - at = Math.sqrt(dx * dx + dy * dy) <= snapTolerance; - if (at) { - this.finishCoordinate_ = finishCoordinate; - break; - } - } - } - } - return at; -}; - - -/** - * @param {module:ol/MapBrowserEvent} event Event. - * @private - */ -Draw.prototype.createOrUpdateSketchPoint_ = function(event) { - const coordinates = event.coordinate.slice(); - if (!this.sketchPoint_) { - this.sketchPoint_ = new Feature(new Point(coordinates)); - this.updateSketchFeatures_(); - } else { - const sketchPointGeom = /** @type {module:ol/geom/Point} */ (this.sketchPoint_.getGeometry()); - sketchPointGeom.setCoordinates(coordinates); - } -}; - - -/** - * Start the drawing. - * @param {module:ol/MapBrowserEvent} event Event. - * @private - */ -Draw.prototype.startDrawing_ = function(event) { - const start = event.coordinate; - this.finishCoordinate_ = start; - if (this.mode_ === Mode.POINT) { - this.sketchCoords_ = start.slice(); - } else if (this.mode_ === Mode.POLYGON) { - this.sketchCoords_ = [[start.slice(), start.slice()]]; - this.sketchLineCoords_ = this.sketchCoords_[0]; - } else { - this.sketchCoords_ = [start.slice(), start.slice()]; - } - if (this.sketchLineCoords_) { - this.sketchLine_ = new Feature( - new LineString(this.sketchLineCoords_)); - } - const geometry = this.geometryFunction_(this.sketchCoords_); - this.sketchFeature_ = new Feature(); - if (this.geometryName_) { - this.sketchFeature_.setGeometryName(this.geometryName_); - } - this.sketchFeature_.setGeometry(geometry); - this.updateSketchFeatures_(); - this.dispatchEvent(new DrawEvent(DrawEventType.DRAWSTART, this.sketchFeature_)); -}; - - -/** - * Modify the drawing. - * @param {module:ol/MapBrowserEvent} event Event. - * @private - */ -Draw.prototype.modifyDrawing_ = function(event) { - let coordinate = event.coordinate; - const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (this.sketchFeature_.getGeometry()); - let coordinates, last; - if (this.mode_ === Mode.POINT) { - last = this.sketchCoords_; - } else if (this.mode_ === Mode.POLYGON) { - coordinates = this.sketchCoords_[0]; - last = coordinates[coordinates.length - 1]; - if (this.atFinish_(event)) { - // snap to finish - coordinate = this.finishCoordinate_.slice(); - } - } else { - coordinates = this.sketchCoords_; - last = coordinates[coordinates.length - 1]; - } - last[0] = coordinate[0]; - last[1] = coordinate[1]; - this.geometryFunction_(/** @type {!Array.} */ (this.sketchCoords_), geometry); - if (this.sketchPoint_) { - const sketchPointGeom = /** @type {module:ol/geom/Point} */ (this.sketchPoint_.getGeometry()); - sketchPointGeom.setCoordinates(coordinate); - } - let sketchLineGeom; - if (geometry instanceof Polygon && - this.mode_ !== Mode.POLYGON) { - if (!this.sketchLine_) { - this.sketchLine_ = new Feature(); - } - const ring = geometry.getLinearRing(0); - sketchLineGeom = /** @type {module:ol/geom/LineString} */ (this.sketchLine_.getGeometry()); - if (!sketchLineGeom) { - sketchLineGeom = new LineString(ring.getFlatCoordinates(), ring.getLayout()); - this.sketchLine_.setGeometry(sketchLineGeom); - } else { - sketchLineGeom.setFlatCoordinates( - ring.getLayout(), ring.getFlatCoordinates()); - sketchLineGeom.changed(); - } - } else if (this.sketchLineCoords_) { - sketchLineGeom = /** @type {module:ol/geom/LineString} */ (this.sketchLine_.getGeometry()); - sketchLineGeom.setCoordinates(this.sketchLineCoords_); - } - this.updateSketchFeatures_(); -}; - - -/** - * Add a new coordinate to the drawing. - * @param {module:ol/MapBrowserEvent} event Event. - * @private - */ -Draw.prototype.addToDrawing_ = function(event) { - const coordinate = event.coordinate; - const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (this.sketchFeature_.getGeometry()); - let done; - let coordinates; - if (this.mode_ === Mode.LINE_STRING) { - this.finishCoordinate_ = coordinate.slice(); - coordinates = this.sketchCoords_; - if (coordinates.length >= this.maxPoints_) { - if (this.freehand_) { - coordinates.pop(); - } else { - done = true; - } - } - coordinates.push(coordinate.slice()); - this.geometryFunction_(coordinates, geometry); - } else if (this.mode_ === Mode.POLYGON) { - coordinates = this.sketchCoords_[0]; - if (coordinates.length >= this.maxPoints_) { - if (this.freehand_) { - coordinates.pop(); - } else { - done = true; - } - } - coordinates.push(coordinate.slice()); - if (done) { - this.finishCoordinate_ = coordinates[0]; - } - this.geometryFunction_(this.sketchCoords_, geometry); - } - this.updateSketchFeatures_(); - if (done) { - this.finishDrawing(); - } -}; - - -/** - * Remove last point of the feature currently being drawn. - * @api - */ -Draw.prototype.removeLastPoint = function() { - if (!this.sketchFeature_) { - return; - } - const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (this.sketchFeature_.getGeometry()); - let coordinates, sketchLineGeom; - if (this.mode_ === Mode.LINE_STRING) { - coordinates = this.sketchCoords_; - coordinates.splice(-2, 1); - this.geometryFunction_(coordinates, geometry); - if (coordinates.length >= 2) { - this.finishCoordinate_ = coordinates[coordinates.length - 2].slice(); - } - } else if (this.mode_ === Mode.POLYGON) { - coordinates = this.sketchCoords_[0]; - coordinates.splice(-2, 1); - sketchLineGeom = /** @type {module:ol/geom/LineString} */ (this.sketchLine_.getGeometry()); - sketchLineGeom.setCoordinates(coordinates); - this.geometryFunction_(this.sketchCoords_, geometry); - } - - if (coordinates.length === 0) { - this.finishCoordinate_ = null; - } - - this.updateSketchFeatures_(); -}; - - -/** - * Stop drawing and add the sketch feature to the target layer. - * The {@link module:ol/interaction/Draw~DrawEventType.DRAWEND} event is - * dispatched before inserting the feature. - * @api - */ -Draw.prototype.finishDrawing = function() { - const sketchFeature = this.abortDrawing_(); - if (!sketchFeature) { - return; - } - let coordinates = this.sketchCoords_; - const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (sketchFeature.getGeometry()); - if (this.mode_ === Mode.LINE_STRING) { - // remove the redundant last point - coordinates.pop(); - this.geometryFunction_(coordinates, geometry); - } else if (this.mode_ === Mode.POLYGON) { - // remove the redundant last point in ring - coordinates[0].pop(); - this.geometryFunction_(coordinates, geometry); - coordinates = geometry.getCoordinates(); - } - - // cast multi-part geometries - if (this.type_ === GeometryType.MULTI_POINT) { - sketchFeature.setGeometry(new MultiPoint([coordinates])); - } else if (this.type_ === GeometryType.MULTI_LINE_STRING) { - sketchFeature.setGeometry(new MultiLineString([coordinates])); - } else if (this.type_ === GeometryType.MULTI_POLYGON) { - sketchFeature.setGeometry(new MultiPolygon([coordinates])); - } - - // First dispatch event to allow full set up of feature - this.dispatchEvent(new DrawEvent(DrawEventType.DRAWEND, sketchFeature)); - - // Then insert feature - if (this.features_) { - this.features_.push(sketchFeature); - } - if (this.source_) { - this.source_.addFeature(sketchFeature); - } -}; - - -/** - * Stop drawing without adding the sketch feature to the target layer. - * @return {module:ol/Feature} The sketch feature (or null if none). - * @private - */ -Draw.prototype.abortDrawing_ = function() { - this.finishCoordinate_ = null; - const sketchFeature = this.sketchFeature_; - if (sketchFeature) { - this.sketchFeature_ = null; - this.sketchPoint_ = null; - this.sketchLine_ = null; - this.overlay_.getSource().clear(true); - } - return sketchFeature; -}; - - -/** - * Extend an existing geometry by adding additional points. This only works - * on features with `LineString` geometries, where the interaction will - * extend lines by adding points to the end of the coordinates array. - * @param {!module:ol/Feature} feature Feature to be extended. - * @api - */ -Draw.prototype.extend = function(feature) { - const geometry = feature.getGeometry(); - const lineString = /** @type {module:ol/geom/LineString} */ (geometry); - this.sketchFeature_ = feature; - this.sketchCoords_ = lineString.getCoordinates(); - const last = this.sketchCoords_[this.sketchCoords_.length - 1]; - this.finishCoordinate_ = last.slice(); - this.sketchCoords_.push(last.slice()); - this.updateSketchFeatures_(); - this.dispatchEvent(new DrawEvent(DrawEventType.DRAWSTART, this.sketchFeature_)); -}; - - /** * @inheritDoc */ Draw.prototype.shouldStopEvent = FALSE; -/** - * Redraw the sketch features. - * @private - */ -Draw.prototype.updateSketchFeatures_ = function() { - const sketchFeatures = []; - if (this.sketchFeature_) { - sketchFeatures.push(this.sketchFeature_); - } - if (this.sketchLine_) { - sketchFeatures.push(this.sketchLine_); - } - if (this.sketchPoint_) { - sketchFeatures.push(this.sketchPoint_); - } - const overlaySource = this.overlay_.getSource(); - overlaySource.clear(true); - overlaySource.addFeatures(sketchFeatures); -}; - - -/** - * @private - */ -Draw.prototype.updateState_ = function() { - const map = this.getMap(); - const active = this.getActive(); - if (!map || !active) { - this.abortDrawing_(); - } - this.overlay_.setMap(active ? map : null); -}; - - /** * Create a `geometryFunction` for `type: 'Circle'` that will create a regular * polygon with a user specified number of sides and start angle instead of an diff --git a/src/ol/interaction/Extent.js b/src/ol/interaction/Extent.js index 3520decb0e..a3b7fcaf38 100644 --- a/src/ol/interaction/Extent.js +++ b/src/ol/interaction/Extent.js @@ -82,98 +82,233 @@ inherits(ExtentInteractionEvent, Event); * @param {module:ol/interaction/Extent~Options=} opt_options Options. * @api */ -const ExtentInteraction = function(opt_options) { +class ExtentInteraction { + constructor(opt_options) { - const options = opt_options || {}; + const options = opt_options || {}; - /** - * Extent of the drawn box - * @type {module:ol/extent~Extent} - * @private - */ - this.extent_ = null; + /** + * Extent of the drawn box + * @type {module:ol/extent~Extent} + * @private + */ + this.extent_ = null; - /** - * Handler for pointer move events - * @type {function (module:ol/coordinate~Coordinate): module:ol/extent~Extent|null} - * @private - */ - this.pointerHandler_ = null; + /** + * Handler for pointer move events + * @type {function (module:ol/coordinate~Coordinate): module:ol/extent~Extent|null} + * @private + */ + this.pointerHandler_ = null; - /** - * Pixel threshold to snap to extent - * @type {number} - * @private - */ - this.pixelTolerance_ = options.pixelTolerance !== undefined ? - options.pixelTolerance : 10; + /** + * Pixel threshold to snap to extent + * @type {number} + * @private + */ + this.pixelTolerance_ = options.pixelTolerance !== undefined ? + options.pixelTolerance : 10; - /** - * Is the pointer snapped to an extent vertex - * @type {boolean} - * @private - */ - this.snappedToVertex_ = false; + /** + * Is the pointer snapped to an extent vertex + * @type {boolean} + * @private + */ + this.snappedToVertex_ = false; - /** - * Feature for displaying the visible extent - * @type {module:ol/Feature} - * @private - */ - this.extentFeature_ = null; + /** + * Feature for displaying the visible extent + * @type {module:ol/Feature} + * @private + */ + this.extentFeature_ = null; - /** - * Feature for displaying the visible pointer - * @type {module:ol/Feature} - * @private - */ - this.vertexFeature_ = null; + /** + * Feature for displaying the visible pointer + * @type {module:ol/Feature} + * @private + */ + this.vertexFeature_ = null; - if (!opt_options) { - opt_options = {}; + if (!opt_options) { + opt_options = {}; + } + + PointerInteraction.call(this, { + handleDownEvent: handleDownEvent, + handleDragEvent: handleDragEvent, + handleEvent: handleEvent, + handleUpEvent: handleUpEvent + }); + + /** + * Layer for the extentFeature + * @type {module:ol/layer/Vector} + * @private + */ + this.extentOverlay_ = new VectorLayer({ + source: new VectorSource({ + useSpatialIndex: false, + wrapX: !!opt_options.wrapX + }), + style: opt_options.boxStyle ? opt_options.boxStyle : getDefaultExtentStyleFunction(), + updateWhileAnimating: true, + updateWhileInteracting: true + }); + + /** + * Layer for the vertexFeature + * @type {module:ol/layer/Vector} + * @private + */ + this.vertexOverlay_ = new VectorLayer({ + source: new VectorSource({ + useSpatialIndex: false, + wrapX: !!opt_options.wrapX + }), + style: opt_options.pointerStyle ? opt_options.pointerStyle : getDefaultPointerStyleFunction(), + updateWhileAnimating: true, + updateWhileInteracting: true + }); + + if (opt_options.extent) { + this.setExtent(opt_options.extent); + } } - PointerInteraction.call(this, { - handleDownEvent: handleDownEvent, - handleDragEvent: handleDragEvent, - handleEvent: handleEvent, - handleUpEvent: handleUpEvent - }); - /** - * Layer for the extentFeature - * @type {module:ol/layer/Vector} + * @param {module:ol~Pixel} pixel cursor location + * @param {module:ol/PluggableMap} map map + * @returns {module:ol/coordinate~Coordinate|null} snapped vertex on extent * @private */ - this.extentOverlay_ = new VectorLayer({ - source: new VectorSource({ - useSpatialIndex: false, - wrapX: !!opt_options.wrapX - }), - style: opt_options.boxStyle ? opt_options.boxStyle : getDefaultExtentStyleFunction(), - updateWhileAnimating: true, - updateWhileInteracting: true - }); + snapToVertex_(pixel, map) { + const pixelCoordinate = map.getCoordinateFromPixel(pixel); + const sortByDistance = function(a, b) { + return squaredDistanceToSegment(pixelCoordinate, a) - + squaredDistanceToSegment(pixelCoordinate, b); + }; + const extent = this.getExtent(); + if (extent) { + //convert extents to line segments and find the segment closest to pixelCoordinate + const segments = getSegments(extent); + segments.sort(sortByDistance); + const closestSegment = segments[0]; - /** - * Layer for the vertexFeature - * @type {module:ol/layer/Vector} - * @private - */ - this.vertexOverlay_ = new VectorLayer({ - source: new VectorSource({ - useSpatialIndex: false, - wrapX: !!opt_options.wrapX - }), - style: opt_options.pointerStyle ? opt_options.pointerStyle : getDefaultPointerStyleFunction(), - updateWhileAnimating: true, - updateWhileInteracting: true - }); + let vertex = (closestOnSegment(pixelCoordinate, + closestSegment)); + const vertexPixel = map.getPixelFromCoordinate(vertex); - if (opt_options.extent) { - this.setExtent(opt_options.extent); + //if the distance is within tolerance, snap to the segment + if (coordinateDistance(pixel, vertexPixel) <= this.pixelTolerance_) { + //test if we should further snap to a vertex + const pixel1 = map.getPixelFromCoordinate(closestSegment[0]); + const pixel2 = map.getPixelFromCoordinate(closestSegment[1]); + const squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1); + const squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2); + const dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); + this.snappedToVertex_ = dist <= this.pixelTolerance_; + if (this.snappedToVertex_) { + vertex = squaredDist1 > squaredDist2 ? + closestSegment[1] : closestSegment[0]; + } + return vertex; + } + } + return null; } -}; + + /** + * @param {module:ol/MapBrowserEvent} mapBrowserEvent pointer move event + * @private + */ + handlePointerMove_(mapBrowserEvent) { + const pixel = mapBrowserEvent.pixel; + const map = mapBrowserEvent.map; + + let vertex = this.snapToVertex_(pixel, map); + if (!vertex) { + vertex = map.getCoordinateFromPixel(pixel); + } + this.createOrUpdatePointerFeature_(vertex); + } + + /** + * @param {module:ol/extent~Extent} extent extent + * @returns {module:ol/Feature} extent as featrue + * @private + */ + createOrUpdateExtentFeature_(extent) { + let extentFeature = this.extentFeature_; + + if (!extentFeature) { + if (!extent) { + extentFeature = new Feature({}); + } else { + extentFeature = new Feature(polygonFromExtent(extent)); + } + this.extentFeature_ = extentFeature; + this.extentOverlay_.getSource().addFeature(extentFeature); + } else { + if (!extent) { + extentFeature.setGeometry(undefined); + } else { + extentFeature.setGeometry(polygonFromExtent(extent)); + } + } + return extentFeature; + } + + /** + * @param {module:ol/coordinate~Coordinate} vertex location of feature + * @returns {module:ol/Feature} vertex as feature + * @private + */ + createOrUpdatePointerFeature_(vertex) { + let vertexFeature = this.vertexFeature_; + if (!vertexFeature) { + vertexFeature = new Feature(new Point(vertex)); + this.vertexFeature_ = vertexFeature; + this.vertexOverlay_.getSource().addFeature(vertexFeature); + } else { + const geometry = /** @type {module:ol/geom/Point} */ (vertexFeature.getGeometry()); + geometry.setCoordinates(vertex); + } + return vertexFeature; + } + + /** + * @inheritDoc + */ + setMap(map) { + this.extentOverlay_.setMap(map); + this.vertexOverlay_.setMap(map); + PointerInteraction.prototype.setMap.call(this, map); + } + + /** + * Returns the current drawn extent in the view projection + * + * @return {module:ol/extent~Extent} Drawn extent in the view projection. + * @api + */ + getExtent() { + return this.extent_; + } + + /** + * Manually sets the drawn extent, using the view projection. + * + * @param {module:ol/extent~Extent} extent Extent + * @api + */ + setExtent(extent) { + //Null extent means no bbox + this.extent_ = extent ? extent : null; + this.createOrUpdateExtentFeature_(extent); + this.dispatchEvent(new ExtentInteractionEvent(this.extent_)); + } +} inherits(ExtentInteraction, PointerInteraction); @@ -350,140 +485,5 @@ function getSegments(extent) { ]; } -/** - * @param {module:ol~Pixel} pixel cursor location - * @param {module:ol/PluggableMap} map map - * @returns {module:ol/coordinate~Coordinate|null} snapped vertex on extent - * @private - */ -ExtentInteraction.prototype.snapToVertex_ = function(pixel, map) { - const pixelCoordinate = map.getCoordinateFromPixel(pixel); - const sortByDistance = function(a, b) { - return squaredDistanceToSegment(pixelCoordinate, a) - - squaredDistanceToSegment(pixelCoordinate, b); - }; - const extent = this.getExtent(); - if (extent) { - //convert extents to line segments and find the segment closest to pixelCoordinate - const segments = getSegments(extent); - segments.sort(sortByDistance); - const closestSegment = segments[0]; - - let vertex = (closestOnSegment(pixelCoordinate, - closestSegment)); - const vertexPixel = map.getPixelFromCoordinate(vertex); - - //if the distance is within tolerance, snap to the segment - if (coordinateDistance(pixel, vertexPixel) <= this.pixelTolerance_) { - //test if we should further snap to a vertex - const pixel1 = map.getPixelFromCoordinate(closestSegment[0]); - const pixel2 = map.getPixelFromCoordinate(closestSegment[1]); - const squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1); - const squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2); - const dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); - this.snappedToVertex_ = dist <= this.pixelTolerance_; - if (this.snappedToVertex_) { - vertex = squaredDist1 > squaredDist2 ? - closestSegment[1] : closestSegment[0]; - } - return vertex; - } - } - return null; -}; - -/** - * @param {module:ol/MapBrowserEvent} mapBrowserEvent pointer move event - * @private - */ -ExtentInteraction.prototype.handlePointerMove_ = function(mapBrowserEvent) { - const pixel = mapBrowserEvent.pixel; - const map = mapBrowserEvent.map; - - let vertex = this.snapToVertex_(pixel, map); - if (!vertex) { - vertex = map.getCoordinateFromPixel(pixel); - } - this.createOrUpdatePointerFeature_(vertex); -}; - -/** - * @param {module:ol/extent~Extent} extent extent - * @returns {module:ol/Feature} extent as featrue - * @private - */ -ExtentInteraction.prototype.createOrUpdateExtentFeature_ = function(extent) { - let extentFeature = this.extentFeature_; - - if (!extentFeature) { - if (!extent) { - extentFeature = new Feature({}); - } else { - extentFeature = new Feature(polygonFromExtent(extent)); - } - this.extentFeature_ = extentFeature; - this.extentOverlay_.getSource().addFeature(extentFeature); - } else { - if (!extent) { - extentFeature.setGeometry(undefined); - } else { - extentFeature.setGeometry(polygonFromExtent(extent)); - } - } - return extentFeature; -}; - - -/** - * @param {module:ol/coordinate~Coordinate} vertex location of feature - * @returns {module:ol/Feature} vertex as feature - * @private - */ -ExtentInteraction.prototype.createOrUpdatePointerFeature_ = function(vertex) { - let vertexFeature = this.vertexFeature_; - if (!vertexFeature) { - vertexFeature = new Feature(new Point(vertex)); - this.vertexFeature_ = vertexFeature; - this.vertexOverlay_.getSource().addFeature(vertexFeature); - } else { - const geometry = /** @type {module:ol/geom/Point} */ (vertexFeature.getGeometry()); - geometry.setCoordinates(vertex); - } - return vertexFeature; -}; - - -/** - * @inheritDoc - */ -ExtentInteraction.prototype.setMap = function(map) { - this.extentOverlay_.setMap(map); - this.vertexOverlay_.setMap(map); - PointerInteraction.prototype.setMap.call(this, map); -}; - -/** - * Returns the current drawn extent in the view projection - * - * @return {module:ol/extent~Extent} Drawn extent in the view projection. - * @api - */ -ExtentInteraction.prototype.getExtent = function() { - return this.extent_; -}; - -/** - * Manually sets the drawn extent, using the view projection. - * - * @param {module:ol/extent~Extent} extent Extent - * @api - */ -ExtentInteraction.prototype.setExtent = function(extent) { - //Null extent means no bbox - this.extent_ = extent ? extent : null; - this.createOrUpdateExtentFeature_(extent); - this.dispatchEvent(new ExtentInteractionEvent(this.extent_)); -}; - export default ExtentInteraction; diff --git a/src/ol/interaction/Interaction.js b/src/ol/interaction/Interaction.js index df61855b82..7bd2e432f8 100644 --- a/src/ol/interaction/Interaction.js +++ b/src/ol/interaction/Interaction.js @@ -36,71 +36,69 @@ import {clamp} from '../math.js'; * @extends {module:ol/Object} * @api */ -const Interaction = function(options) { +class Interaction { + constructor(options) { - BaseObject.call(this); + BaseObject.call(this); + + /** + * @private + * @type {module:ol/PluggableMap} + */ + this.map_ = null; + + this.setActive(true); + + /** + * @type {function(module:ol/MapBrowserEvent):boolean} + */ + this.handleEvent = options.handleEvent; + + } /** - * @private - * @type {module:ol/PluggableMap} + * Return whether the interaction is currently active. + * @return {boolean} `true` if the interaction is active, `false` otherwise. + * @observable + * @api */ - this.map_ = null; - - this.setActive(true); + getActive() { + return /** @type {boolean} */ (this.get(InteractionProperty.ACTIVE)); + } /** - * @type {function(module:ol/MapBrowserEvent):boolean} + * Get the map associated with this interaction. + * @return {module:ol/PluggableMap} Map. + * @api */ - this.handleEvent = options.handleEvent; + getMap() { + return this.map_; + } -}; + /** + * Activate or deactivate the interaction. + * @param {boolean} active Active. + * @observable + * @api + */ + setActive(active) { + this.set(InteractionProperty.ACTIVE, active); + } + + /** + * Remove the interaction from its current map and attach it to the new map. + * Subclasses may set up event handlers to get notified about changes to + * the map here. + * @param {module:ol/PluggableMap} map Map. + */ + setMap(map) { + this.map_ = map; + } +} inherits(Interaction, BaseObject); -/** - * Return whether the interaction is currently active. - * @return {boolean} `true` if the interaction is active, `false` otherwise. - * @observable - * @api - */ -Interaction.prototype.getActive = function() { - return /** @type {boolean} */ (this.get(InteractionProperty.ACTIVE)); -}; - - -/** - * Get the map associated with this interaction. - * @return {module:ol/PluggableMap} Map. - * @api - */ -Interaction.prototype.getMap = function() { - return this.map_; -}; - - -/** - * Activate or deactivate the interaction. - * @param {boolean} active Active. - * @observable - * @api - */ -Interaction.prototype.setActive = function(active) { - this.set(InteractionProperty.ACTIVE, active); -}; - - -/** - * Remove the interaction from its current map and attach it to the new map. - * Subclasses may set up event handlers to get notified about changes to - * the map here. - * @param {module:ol/PluggableMap} map Map. - */ -Interaction.prototype.setMap = function(map) { - this.map_ = map; -}; - - /** * @param {module:ol/View} view View. * @param {module:ol/coordinate~Coordinate} delta Delta. diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index f1d2f8d005..bff27bc18d 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -140,187 +140,815 @@ inherits(ModifyEvent, Event); * @fires module:ol/interaction/Modify~ModifyEvent * @api */ -const Modify = function(options) { +class Modify { + constructor(options) { - PointerInteraction.call(this, { - handleDownEvent: handleDownEvent, - handleDragEvent: handleDragEvent, - handleEvent: handleEvent, - handleUpEvent: handleUpEvent - }); + PointerInteraction.call(this, { + handleDownEvent: handleDownEvent, + handleDragEvent: handleDragEvent, + handleEvent: handleEvent, + handleUpEvent: handleUpEvent + }); - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.condition_ = options.condition ? options.condition : primaryAction; + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.condition_ = options.condition ? options.condition : primaryAction; - /** - * @private - * @param {module:ol/MapBrowserEvent} mapBrowserEvent Browser event. - * @return {boolean} Combined condition result. - */ - this.defaultDeleteCondition_ = function(mapBrowserEvent) { - return altKeyOnly(mapBrowserEvent) && singleClick(mapBrowserEvent); - }; + /** + * @private + * @param {module:ol/MapBrowserEvent} mapBrowserEvent Browser event. + * @return {boolean} Combined condition result. + */ + this.defaultDeleteCondition_ = function(mapBrowserEvent) { + return altKeyOnly(mapBrowserEvent) && singleClick(mapBrowserEvent); + }; - /** - * @type {module:ol/events/condition~Condition} - * @private - */ - this.deleteCondition_ = options.deleteCondition ? - options.deleteCondition : this.defaultDeleteCondition_; + /** + * @type {module:ol/events/condition~Condition} + * @private + */ + this.deleteCondition_ = options.deleteCondition ? + options.deleteCondition : this.defaultDeleteCondition_; - /** - * @type {module:ol/events/condition~Condition} - * @private - */ - this.insertVertexCondition_ = options.insertVertexCondition ? - options.insertVertexCondition : always; + /** + * @type {module:ol/events/condition~Condition} + * @private + */ + this.insertVertexCondition_ = options.insertVertexCondition ? + options.insertVertexCondition : always; - /** - * Editing vertex. - * @type {module:ol/Feature} - * @private - */ - this.vertexFeature_ = null; + /** + * Editing vertex. + * @type {module:ol/Feature} + * @private + */ + this.vertexFeature_ = null; - /** - * Segments intersecting {@link this.vertexFeature_} by segment uid. - * @type {Object.} - * @private - */ - this.vertexSegments_ = null; + /** + * Segments intersecting {@link this.vertexFeature_} by segment uid. + * @type {Object.} + * @private + */ + this.vertexSegments_ = null; - /** - * @type {module:ol~Pixel} - * @private - */ - this.lastPixel_ = [0, 0]; + /** + * @type {module:ol~Pixel} + * @private + */ + this.lastPixel_ = [0, 0]; - /** - * Tracks if the next `singleclick` event should be ignored to prevent - * accidental deletion right after vertex creation. - * @type {boolean} - * @private - */ - this.ignoreNextSingleClick_ = false; + /** + * Tracks if the next `singleclick` event should be ignored to prevent + * accidental deletion right after vertex creation. + * @type {boolean} + * @private + */ + this.ignoreNextSingleClick_ = false; - /** - * @type {boolean} - * @private - */ - this.modified_ = false; + /** + * @type {boolean} + * @private + */ + this.modified_ = false; - /** - * Segment RTree for each layer - * @type {module:ol/structs/RBush.} - * @private - */ - this.rBush_ = new RBush(); + /** + * Segment RTree for each layer + * @type {module:ol/structs/RBush.} + * @private + */ + this.rBush_ = new RBush(); - /** - * @type {number} - * @private - */ - this.pixelTolerance_ = options.pixelTolerance !== undefined ? - options.pixelTolerance : 10; + /** + * @type {number} + * @private + */ + this.pixelTolerance_ = options.pixelTolerance !== undefined ? + options.pixelTolerance : 10; - /** - * @type {boolean} - * @private - */ - this.snappedToVertex_ = false; + /** + * @type {boolean} + * @private + */ + this.snappedToVertex_ = false; - /** - * Indicate whether the interaction is currently changing a feature's - * coordinates. - * @type {boolean} - * @private - */ - this.changingFeature_ = false; + /** + * Indicate whether the interaction is currently changing a feature's + * coordinates. + * @type {boolean} + * @private + */ + this.changingFeature_ = false; - /** - * @type {Array} - * @private - */ - this.dragSegments_ = []; + /** + * @type {Array} + * @private + */ + this.dragSegments_ = []; - /** - * Draw overlay where sketch features are drawn. - * @type {module:ol/layer/Vector} - * @private - */ - this.overlay_ = new VectorLayer({ - source: new VectorSource({ - useSpatialIndex: false, - wrapX: !!options.wrapX - }), - style: options.style ? options.style : - getDefaultStyleFunction(), - updateWhileAnimating: true, - updateWhileInteracting: true - }); + /** + * Draw overlay where sketch features are drawn. + * @type {module:ol/layer/Vector} + * @private + */ + this.overlay_ = new VectorLayer({ + source: new VectorSource({ + useSpatialIndex: false, + wrapX: !!options.wrapX + }), + style: options.style ? options.style : + getDefaultStyleFunction(), + updateWhileAnimating: true, + updateWhileInteracting: true + }); - /** - * @const - * @private - * @type {!Object.} - */ - this.SEGMENT_WRITERS_ = { - 'Point': this.writePointGeometry_, - 'LineString': this.writeLineStringGeometry_, - 'LinearRing': this.writeLineStringGeometry_, - 'Polygon': this.writePolygonGeometry_, - 'MultiPoint': this.writeMultiPointGeometry_, - 'MultiLineString': this.writeMultiLineStringGeometry_, - 'MultiPolygon': this.writeMultiPolygonGeometry_, - 'Circle': this.writeCircleGeometry_, - 'GeometryCollection': this.writeGeometryCollectionGeometry_ - }; + /** + * @const + * @private + * @type {!Object.} + */ + this.SEGMENT_WRITERS_ = { + 'Point': this.writePointGeometry_, + 'LineString': this.writeLineStringGeometry_, + 'LinearRing': this.writeLineStringGeometry_, + 'Polygon': this.writePolygonGeometry_, + 'MultiPoint': this.writeMultiPointGeometry_, + 'MultiLineString': this.writeMultiLineStringGeometry_, + 'MultiPolygon': this.writeMultiPolygonGeometry_, + 'Circle': this.writeCircleGeometry_, + 'GeometryCollection': this.writeGeometryCollectionGeometry_ + }; - /** - * @type {module:ol/source/Vector} - * @private - */ - this.source_ = null; + /** + * @type {module:ol/source/Vector} + * @private + */ + this.source_ = null; + + let features; + if (options.source) { + this.source_ = options.source; + features = new Collection(this.source_.getFeatures()); + listen(this.source_, VectorEventType.ADDFEATURE, + this.handleSourceAdd_, this); + listen(this.source_, VectorEventType.REMOVEFEATURE, + this.handleSourceRemove_, this); + } else { + features = options.features; + } + if (!features) { + throw new Error('The modify interaction requires features or a source'); + } + + /** + * @type {module:ol/Collection.} + * @private + */ + this.features_ = features; + + this.features_.forEach(this.addFeature_.bind(this)); + listen(this.features_, CollectionEventType.ADD, + this.handleFeatureAdd_, this); + listen(this.features_, CollectionEventType.REMOVE, + this.handleFeatureRemove_, this); + + /** + * @type {module:ol/MapBrowserPointerEvent} + * @private + */ + this.lastPointerEvent_ = null; - let features; - if (options.source) { - this.source_ = options.source; - features = new Collection(this.source_.getFeatures()); - listen(this.source_, VectorEventType.ADDFEATURE, - this.handleSourceAdd_, this); - listen(this.source_, VectorEventType.REMOVEFEATURE, - this.handleSourceRemove_, this); - } else { - features = options.features; - } - if (!features) { - throw new Error('The modify interaction requires features or a source'); } /** - * @type {module:ol/Collection.} + * @param {module:ol/Feature} feature Feature. * @private */ - this.features_ = features; - - this.features_.forEach(this.addFeature_.bind(this)); - listen(this.features_, CollectionEventType.ADD, - this.handleFeatureAdd_, this); - listen(this.features_, CollectionEventType.REMOVE, - this.handleFeatureRemove_, this); + addFeature_(feature) { + const geometry = feature.getGeometry(); + if (geometry && geometry.getType() in this.SEGMENT_WRITERS_) { + this.SEGMENT_WRITERS_[geometry.getType()].call(this, feature, geometry); + } + const map = this.getMap(); + if (map && map.isRendered() && this.getActive()) { + this.handlePointerAtPixel_(this.lastPixel_, map); + } + listen(feature, EventType.CHANGE, + this.handleFeatureChange_, this); + } /** - * @type {module:ol/MapBrowserPointerEvent} + * @param {module:ol/MapBrowserPointerEvent} evt Map browser event * @private */ - this.lastPointerEvent_ = null; + willModifyFeatures_(evt) { + if (!this.modified_) { + this.modified_ = true; + this.dispatchEvent(new ModifyEvent( + ModifyEventType.MODIFYSTART, this.features_, evt)); + } + } -}; + /** + * @param {module:ol/Feature} feature Feature. + * @private + */ + removeFeature_(feature) { + this.removeFeatureSegmentData_(feature); + // Remove the vertex feature if the collection of canditate features + // is empty. + if (this.vertexFeature_ && this.features_.getLength() === 0) { + this.overlay_.getSource().removeFeature(this.vertexFeature_); + this.vertexFeature_ = null; + } + unlisten(feature, EventType.CHANGE, + this.handleFeatureChange_, this); + } + + /** + * @param {module:ol/Feature} feature Feature. + * @private + */ + removeFeatureSegmentData_(feature) { + const rBush = this.rBush_; + const /** @type {Array.} */ nodesToRemove = []; + rBush.forEach( + /** + * @param {module:ol/interaction/Modify~SegmentData} node RTree node. + */ + function(node) { + if (feature === node.feature) { + nodesToRemove.push(node); + } + }); + for (let i = nodesToRemove.length - 1; i >= 0; --i) { + rBush.remove(nodesToRemove[i]); + } + } + + /** + * @inheritDoc + */ + setActive(active) { + if (this.vertexFeature_ && !active) { + this.overlay_.getSource().removeFeature(this.vertexFeature_); + this.vertexFeature_ = null; + } + PointerInteraction.prototype.setActive.call(this, active); + } + + /** + * @inheritDoc + */ + setMap(map) { + this.overlay_.setMap(map); + PointerInteraction.prototype.setMap.call(this, map); + } + + /** + * @param {module:ol/source/Vector~VectorSourceEvent} event Event. + * @private + */ + handleSourceAdd_(event) { + if (event.feature) { + this.features_.push(event.feature); + } + } + + /** + * @param {module:ol/source/Vector~VectorSourceEvent} event Event. + * @private + */ + handleSourceRemove_(event) { + if (event.feature) { + this.features_.remove(event.feature); + } + } + + /** + * @param {module:ol/Collection~CollectionEvent} evt Event. + * @private + */ + handleFeatureAdd_(evt) { + this.addFeature_(/** @type {module:ol/Feature} */ (evt.element)); + } + + /** + * @param {module:ol/events/Event} evt Event. + * @private + */ + handleFeatureChange_(evt) { + if (!this.changingFeature_) { + const feature = /** @type {module:ol/Feature} */ (evt.target); + this.removeFeature_(feature); + this.addFeature_(feature); + } + } + + /** + * @param {module:ol/Collection~CollectionEvent} evt Event. + * @private + */ + handleFeatureRemove_(evt) { + const feature = /** @type {module:ol/Feature} */ (evt.element); + this.removeFeature_(feature); + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/Point} geometry Geometry. + * @private + */ + writePointGeometry_(feature, geometry) { + const coordinates = geometry.getCoordinates(); + const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + feature: feature, + geometry: geometry, + segment: [coordinates, coordinates] + }); + this.rBush_.insert(geometry.getExtent(), segmentData); + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/MultiPoint} geometry Geometry. + * @private + */ + writeMultiPointGeometry_(feature, geometry) { + const points = geometry.getCoordinates(); + for (let i = 0, ii = points.length; i < ii; ++i) { + const coordinates = points[i]; + const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + feature: feature, + geometry: geometry, + depth: [i], + index: i, + segment: [coordinates, coordinates] + }); + this.rBush_.insert(geometry.getExtent(), segmentData); + } + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/LineString} geometry Geometry. + * @private + */ + writeLineStringGeometry_(feature, geometry) { + const coordinates = geometry.getCoordinates(); + for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { + const segment = coordinates.slice(i, i + 2); + const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + feature: feature, + geometry: geometry, + index: i, + segment: segment + }); + this.rBush_.insert(boundingExtent(segment), segmentData); + } + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/MultiLineString} geometry Geometry. + * @private + */ + writeMultiLineStringGeometry_(feature, geometry) { + const lines = geometry.getCoordinates(); + for (let j = 0, jj = lines.length; j < jj; ++j) { + const coordinates = lines[j]; + for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { + const segment = coordinates.slice(i, i + 2); + const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + feature: feature, + geometry: geometry, + depth: [j], + index: i, + segment: segment + }); + this.rBush_.insert(boundingExtent(segment), segmentData); + } + } + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/Polygon} geometry Geometry. + * @private + */ + writePolygonGeometry_(feature, geometry) { + const rings = geometry.getCoordinates(); + for (let j = 0, jj = rings.length; j < jj; ++j) { + const coordinates = rings[j]; + for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { + const segment = coordinates.slice(i, i + 2); + const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + feature: feature, + geometry: geometry, + depth: [j], + index: i, + segment: segment + }); + this.rBush_.insert(boundingExtent(segment), segmentData); + } + } + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/MultiPolygon} geometry Geometry. + * @private + */ + writeMultiPolygonGeometry_(feature, geometry) { + const polygons = geometry.getCoordinates(); + for (let k = 0, kk = polygons.length; k < kk; ++k) { + const rings = polygons[k]; + for (let j = 0, jj = rings.length; j < jj; ++j) { + const coordinates = rings[j]; + for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { + const segment = coordinates.slice(i, i + 2); + const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + feature: feature, + geometry: geometry, + depth: [j, k], + index: i, + segment: segment + }); + this.rBush_.insert(boundingExtent(segment), segmentData); + } + } + } + } + + /** + * We convert a circle into two segments. The segment at index + * {@link CIRCLE_CENTER_INDEX} is the + * circle's center (a point). The segment at index + * {@link CIRCLE_CIRCUMFERENCE_INDEX} is + * the circumference, and is not a line segment. + * + * @param {module:ol/Feature} feature Feature. + * @param {module:ol/geom/Circle} geometry Geometry. + * @private + */ + writeCircleGeometry_(feature, geometry) { + const coordinates = geometry.getCenter(); + const centerSegmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + feature: feature, + geometry: geometry, + index: CIRCLE_CENTER_INDEX, + segment: [coordinates, coordinates] + }); + const circumferenceSegmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + feature: feature, + geometry: geometry, + index: CIRCLE_CIRCUMFERENCE_INDEX, + segment: [coordinates, coordinates] + }); + const featureSegments = [centerSegmentData, circumferenceSegmentData]; + centerSegmentData.featureSegments = circumferenceSegmentData.featureSegments = featureSegments; + this.rBush_.insert(createOrUpdateFromCoordinate(coordinates), centerSegmentData); + this.rBush_.insert(geometry.getExtent(), circumferenceSegmentData); + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/GeometryCollection} geometry Geometry. + * @private + */ + writeGeometryCollectionGeometry_(feature, geometry) { + const geometries = geometry.getGeometriesArray(); + for (let i = 0; i < geometries.length; ++i) { + this.SEGMENT_WRITERS_[geometries[i].getType()].call(this, feature, geometries[i]); + } + } + + /** + * @param {module:ol/coordinate~Coordinate} coordinates Coordinates. + * @return {module:ol/Feature} Vertex feature. + * @private + */ + createOrUpdateVertexFeature_(coordinates) { + let vertexFeature = this.vertexFeature_; + if (!vertexFeature) { + vertexFeature = new Feature(new Point(coordinates)); + this.vertexFeature_ = vertexFeature; + this.overlay_.getSource().addFeature(vertexFeature); + } else { + const geometry = /** @type {module:ol/geom/Point} */ (vertexFeature.getGeometry()); + geometry.setCoordinates(coordinates); + } + return vertexFeature; + } + + /** + * @param {module:ol/MapBrowserEvent} evt Event. + * @private + */ + handlePointerMove_(evt) { + this.lastPixel_ = evt.pixel; + this.handlePointerAtPixel_(evt.pixel, evt.map); + } + + /** + * @param {module:ol~Pixel} pixel Pixel + * @param {module:ol/PluggableMap} map Map. + * @private + */ + handlePointerAtPixel_(pixel, map) { + const pixelCoordinate = map.getCoordinateFromPixel(pixel); + const sortByDistance = function(a, b) { + return pointDistanceToSegmentDataSquared(pixelCoordinate, a) - + pointDistanceToSegmentDataSquared(pixelCoordinate, b); + }; + + const box = buffer(createOrUpdateFromCoordinate(pixelCoordinate), + map.getView().getResolution() * this.pixelTolerance_); + + const rBush = this.rBush_; + const nodes = rBush.getInExtent(box); + if (nodes.length > 0) { + nodes.sort(sortByDistance); + const node = nodes[0]; + const closestSegment = node.segment; + let vertex = closestOnSegmentData(pixelCoordinate, node); + const vertexPixel = map.getPixelFromCoordinate(vertex); + let dist = coordinateDistance(pixel, vertexPixel); + if (dist <= this.pixelTolerance_) { + const vertexSegments = {}; + + if (node.geometry.getType() === GeometryType.CIRCLE && + node.index === CIRCLE_CIRCUMFERENCE_INDEX) { + + this.snappedToVertex_ = true; + this.createOrUpdateVertexFeature_(vertex); + } else { + const pixel1 = map.getPixelFromCoordinate(closestSegment[0]); + const pixel2 = map.getPixelFromCoordinate(closestSegment[1]); + const squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1); + const squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2); + dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); + this.snappedToVertex_ = dist <= this.pixelTolerance_; + if (this.snappedToVertex_) { + vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; + } + this.createOrUpdateVertexFeature_(vertex); + for (let i = 1, ii = nodes.length; i < ii; ++i) { + const segment = nodes[i].segment; + if ((coordinatesEqual(closestSegment[0], segment[0]) && + coordinatesEqual(closestSegment[1], segment[1]) || + (coordinatesEqual(closestSegment[0], segment[1]) && + coordinatesEqual(closestSegment[1], segment[0])))) { + vertexSegments[getUid(segment)] = true; + } else { + break; + } + } + } + + vertexSegments[getUid(closestSegment)] = true; + this.vertexSegments_ = vertexSegments; + return; + } + } + if (this.vertexFeature_) { + this.overlay_.getSource().removeFeature(this.vertexFeature_); + this.vertexFeature_ = null; + } + } + + /** + * @param {module:ol/interaction/Modify~SegmentData} segmentData Segment data. + * @param {module:ol/coordinate~Coordinate} vertex Vertex. + * @private + */ + insertVertex_(segmentData, vertex) { + const segment = segmentData.segment; + const feature = segmentData.feature; + const geometry = segmentData.geometry; + const depth = segmentData.depth; + const index = /** @type {number} */ (segmentData.index); + let coordinates; + + while (vertex.length < geometry.getStride()) { + vertex.push(0); + } + + switch (geometry.getType()) { + case GeometryType.MULTI_LINE_STRING: + coordinates = geometry.getCoordinates(); + coordinates[depth[0]].splice(index + 1, 0, vertex); + break; + case GeometryType.POLYGON: + coordinates = geometry.getCoordinates(); + coordinates[depth[0]].splice(index + 1, 0, vertex); + break; + case GeometryType.MULTI_POLYGON: + coordinates = geometry.getCoordinates(); + coordinates[depth[1]][depth[0]].splice(index + 1, 0, vertex); + break; + case GeometryType.LINE_STRING: + coordinates = geometry.getCoordinates(); + coordinates.splice(index + 1, 0, vertex); + break; + default: + return; + } + + this.setGeometryCoordinates_(geometry, coordinates); + const rTree = this.rBush_; + rTree.remove(segmentData); + this.updateSegmentIndices_(geometry, index, depth, 1); + const newSegmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + segment: [segment[0], vertex], + feature: feature, + geometry: geometry, + depth: depth, + index: index + }); + rTree.insert(boundingExtent(newSegmentData.segment), + newSegmentData); + this.dragSegments_.push([newSegmentData, 1]); + + const newSegmentData2 = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + segment: [vertex, segment[1]], + feature: feature, + geometry: geometry, + depth: depth, + index: index + 1 + }); + rTree.insert(boundingExtent(newSegmentData2.segment), newSegmentData2); + this.dragSegments_.push([newSegmentData2, 0]); + this.ignoreNextSingleClick_ = true; + } + + /** + * Removes the vertex currently being pointed. + * @return {boolean} True when a vertex was removed. + * @api + */ + removePoint() { + if (this.lastPointerEvent_ && this.lastPointerEvent_.type != MapBrowserEventType.POINTERDRAG) { + const evt = this.lastPointerEvent_; + this.willModifyFeatures_(evt); + this.removeVertex_(); + this.dispatchEvent(new ModifyEvent(ModifyEventType.MODIFYEND, this.features_, evt)); + this.modified_ = false; + return true; + } + return false; + } + + /** + * Removes a vertex from all matching features. + * @return {boolean} True when a vertex was removed. + * @private + */ + removeVertex_() { + const dragSegments = this.dragSegments_; + const segmentsByFeature = {}; + let deleted = false; + let component, coordinates, dragSegment, geometry, i, index, left; + let newIndex, right, segmentData, uid; + for (i = dragSegments.length - 1; i >= 0; --i) { + dragSegment = dragSegments[i]; + segmentData = dragSegment[0]; + uid = getUid(segmentData.feature); + if (segmentData.depth) { + // separate feature components + uid += '-' + segmentData.depth.join('-'); + } + if (!(uid in segmentsByFeature)) { + segmentsByFeature[uid] = {}; + } + if (dragSegment[1] === 0) { + segmentsByFeature[uid].right = segmentData; + segmentsByFeature[uid].index = segmentData.index; + } else if (dragSegment[1] == 1) { + segmentsByFeature[uid].left = segmentData; + segmentsByFeature[uid].index = segmentData.index + 1; + } + + } + for (uid in segmentsByFeature) { + right = segmentsByFeature[uid].right; + left = segmentsByFeature[uid].left; + index = segmentsByFeature[uid].index; + newIndex = index - 1; + if (left !== undefined) { + segmentData = left; + } else { + segmentData = right; + } + if (newIndex < 0) { + newIndex = 0; + } + geometry = segmentData.geometry; + coordinates = geometry.getCoordinates(); + component = coordinates; + deleted = false; + switch (geometry.getType()) { + case GeometryType.MULTI_LINE_STRING: + if (coordinates[segmentData.depth[0]].length > 2) { + coordinates[segmentData.depth[0]].splice(index, 1); + deleted = true; + } + break; + case GeometryType.LINE_STRING: + if (coordinates.length > 2) { + coordinates.splice(index, 1); + deleted = true; + } + break; + case GeometryType.MULTI_POLYGON: + component = component[segmentData.depth[1]]; + /* falls through */ + case GeometryType.POLYGON: + component = component[segmentData.depth[0]]; + if (component.length > 4) { + if (index == component.length - 1) { + index = 0; + } + component.splice(index, 1); + deleted = true; + if (index === 0) { + // close the ring again + component.pop(); + component.push(component[0]); + newIndex = component.length - 1; + } + } + break; + default: + // pass + } + + if (deleted) { + this.setGeometryCoordinates_(geometry, coordinates); + const segments = []; + if (left !== undefined) { + this.rBush_.remove(left); + segments.push(left.segment[0]); + } + if (right !== undefined) { + this.rBush_.remove(right); + segments.push(right.segment[1]); + } + if (left !== undefined && right !== undefined) { + const newSegmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ + depth: segmentData.depth, + feature: segmentData.feature, + geometry: segmentData.geometry, + index: newIndex, + segment: segments + }); + this.rBush_.insert(boundingExtent(newSegmentData.segment), + newSegmentData); + } + this.updateSegmentIndices_(geometry, index, segmentData.depth, -1); + if (this.vertexFeature_) { + this.overlay_.getSource().removeFeature(this.vertexFeature_); + this.vertexFeature_ = null; + } + dragSegments.length = 0; + } + + } + return deleted; + } + + /** + * @param {module:ol/geom/SimpleGeometry} geometry Geometry. + * @param {Array} coordinates Coordinates. + * @private + */ + setGeometryCoordinates_(geometry, coordinates) { + this.changingFeature_ = true; + geometry.setCoordinates(coordinates); + this.changingFeature_ = false; + } + + /** + * @param {module:ol/geom/SimpleGeometry} geometry Geometry. + * @param {number} index Index. + * @param {Array.|undefined} depth Depth. + * @param {number} delta Delta (1 or -1). + * @private + */ + updateSegmentIndices_(geometry, index, depth, delta) { + this.rBush_.forEachInExtent(geometry.getExtent(), function(segmentDataMatch) { + if (segmentDataMatch.geometry === geometry && + (depth === undefined || segmentDataMatch.depth === undefined || + equals(segmentDataMatch.depth, depth)) && + segmentDataMatch.index > index) { + segmentDataMatch.index += delta; + } + }); + } +} inherits(Modify, PointerInteraction); @@ -340,347 +968,6 @@ const CIRCLE_CENTER_INDEX = 0; const CIRCLE_CIRCUMFERENCE_INDEX = 1; -/** - * @param {module:ol/Feature} feature Feature. - * @private - */ -Modify.prototype.addFeature_ = function(feature) { - const geometry = feature.getGeometry(); - if (geometry && geometry.getType() in this.SEGMENT_WRITERS_) { - this.SEGMENT_WRITERS_[geometry.getType()].call(this, feature, geometry); - } - const map = this.getMap(); - if (map && map.isRendered() && this.getActive()) { - this.handlePointerAtPixel_(this.lastPixel_, map); - } - listen(feature, EventType.CHANGE, - this.handleFeatureChange_, this); -}; - - -/** - * @param {module:ol/MapBrowserPointerEvent} evt Map browser event - * @private - */ -Modify.prototype.willModifyFeatures_ = function(evt) { - if (!this.modified_) { - this.modified_ = true; - this.dispatchEvent(new ModifyEvent( - ModifyEventType.MODIFYSTART, this.features_, evt)); - } -}; - - -/** - * @param {module:ol/Feature} feature Feature. - * @private - */ -Modify.prototype.removeFeature_ = function(feature) { - this.removeFeatureSegmentData_(feature); - // Remove the vertex feature if the collection of canditate features - // is empty. - if (this.vertexFeature_ && this.features_.getLength() === 0) { - this.overlay_.getSource().removeFeature(this.vertexFeature_); - this.vertexFeature_ = null; - } - unlisten(feature, EventType.CHANGE, - this.handleFeatureChange_, this); -}; - - -/** - * @param {module:ol/Feature} feature Feature. - * @private - */ -Modify.prototype.removeFeatureSegmentData_ = function(feature) { - const rBush = this.rBush_; - const /** @type {Array.} */ nodesToRemove = []; - rBush.forEach( - /** - * @param {module:ol/interaction/Modify~SegmentData} node RTree node. - */ - function(node) { - if (feature === node.feature) { - nodesToRemove.push(node); - } - }); - for (let i = nodesToRemove.length - 1; i >= 0; --i) { - rBush.remove(nodesToRemove[i]); - } -}; - - -/** - * @inheritDoc - */ -Modify.prototype.setActive = function(active) { - if (this.vertexFeature_ && !active) { - this.overlay_.getSource().removeFeature(this.vertexFeature_); - this.vertexFeature_ = null; - } - PointerInteraction.prototype.setActive.call(this, active); -}; - - -/** - * @inheritDoc - */ -Modify.prototype.setMap = function(map) { - this.overlay_.setMap(map); - PointerInteraction.prototype.setMap.call(this, map); -}; - - -/** - * @param {module:ol/source/Vector~VectorSourceEvent} event Event. - * @private - */ -Modify.prototype.handleSourceAdd_ = function(event) { - if (event.feature) { - this.features_.push(event.feature); - } -}; - - -/** - * @param {module:ol/source/Vector~VectorSourceEvent} event Event. - * @private - */ -Modify.prototype.handleSourceRemove_ = function(event) { - if (event.feature) { - this.features_.remove(event.feature); - } -}; - - -/** - * @param {module:ol/Collection~CollectionEvent} evt Event. - * @private - */ -Modify.prototype.handleFeatureAdd_ = function(evt) { - this.addFeature_(/** @type {module:ol/Feature} */ (evt.element)); -}; - - -/** - * @param {module:ol/events/Event} evt Event. - * @private - */ -Modify.prototype.handleFeatureChange_ = function(evt) { - if (!this.changingFeature_) { - const feature = /** @type {module:ol/Feature} */ (evt.target); - this.removeFeature_(feature); - this.addFeature_(feature); - } -}; - - -/** - * @param {module:ol/Collection~CollectionEvent} evt Event. - * @private - */ -Modify.prototype.handleFeatureRemove_ = function(evt) { - const feature = /** @type {module:ol/Feature} */ (evt.element); - this.removeFeature_(feature); -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/Point} geometry Geometry. - * @private - */ -Modify.prototype.writePointGeometry_ = function(feature, geometry) { - const coordinates = geometry.getCoordinates(); - const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - feature: feature, - geometry: geometry, - segment: [coordinates, coordinates] - }); - this.rBush_.insert(geometry.getExtent(), segmentData); -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/MultiPoint} geometry Geometry. - * @private - */ -Modify.prototype.writeMultiPointGeometry_ = function(feature, geometry) { - const points = geometry.getCoordinates(); - for (let i = 0, ii = points.length; i < ii; ++i) { - const coordinates = points[i]; - const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - feature: feature, - geometry: geometry, - depth: [i], - index: i, - segment: [coordinates, coordinates] - }); - this.rBush_.insert(geometry.getExtent(), segmentData); - } -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/LineString} geometry Geometry. - * @private - */ -Modify.prototype.writeLineStringGeometry_ = function(feature, geometry) { - const coordinates = geometry.getCoordinates(); - for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { - const segment = coordinates.slice(i, i + 2); - const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - feature: feature, - geometry: geometry, - index: i, - segment: segment - }); - this.rBush_.insert(boundingExtent(segment), segmentData); - } -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/MultiLineString} geometry Geometry. - * @private - */ -Modify.prototype.writeMultiLineStringGeometry_ = function(feature, geometry) { - const lines = geometry.getCoordinates(); - for (let j = 0, jj = lines.length; j < jj; ++j) { - const coordinates = lines[j]; - for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { - const segment = coordinates.slice(i, i + 2); - const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - feature: feature, - geometry: geometry, - depth: [j], - index: i, - segment: segment - }); - this.rBush_.insert(boundingExtent(segment), segmentData); - } - } -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/Polygon} geometry Geometry. - * @private - */ -Modify.prototype.writePolygonGeometry_ = function(feature, geometry) { - const rings = geometry.getCoordinates(); - for (let j = 0, jj = rings.length; j < jj; ++j) { - const coordinates = rings[j]; - for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { - const segment = coordinates.slice(i, i + 2); - const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - feature: feature, - geometry: geometry, - depth: [j], - index: i, - segment: segment - }); - this.rBush_.insert(boundingExtent(segment), segmentData); - } - } -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/MultiPolygon} geometry Geometry. - * @private - */ -Modify.prototype.writeMultiPolygonGeometry_ = function(feature, geometry) { - const polygons = geometry.getCoordinates(); - for (let k = 0, kk = polygons.length; k < kk; ++k) { - const rings = polygons[k]; - for (let j = 0, jj = rings.length; j < jj; ++j) { - const coordinates = rings[j]; - for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { - const segment = coordinates.slice(i, i + 2); - const segmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - feature: feature, - geometry: geometry, - depth: [j, k], - index: i, - segment: segment - }); - this.rBush_.insert(boundingExtent(segment), segmentData); - } - } - } -}; - - -/** - * We convert a circle into two segments. The segment at index - * {@link CIRCLE_CENTER_INDEX} is the - * circle's center (a point). The segment at index - * {@link CIRCLE_CIRCUMFERENCE_INDEX} is - * the circumference, and is not a line segment. - * - * @param {module:ol/Feature} feature Feature. - * @param {module:ol/geom/Circle} geometry Geometry. - * @private - */ -Modify.prototype.writeCircleGeometry_ = function(feature, geometry) { - const coordinates = geometry.getCenter(); - const centerSegmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - feature: feature, - geometry: geometry, - index: CIRCLE_CENTER_INDEX, - segment: [coordinates, coordinates] - }); - const circumferenceSegmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - feature: feature, - geometry: geometry, - index: CIRCLE_CIRCUMFERENCE_INDEX, - segment: [coordinates, coordinates] - }); - const featureSegments = [centerSegmentData, circumferenceSegmentData]; - centerSegmentData.featureSegments = circumferenceSegmentData.featureSegments = featureSegments; - this.rBush_.insert(createOrUpdateFromCoordinate(coordinates), centerSegmentData); - this.rBush_.insert(geometry.getExtent(), circumferenceSegmentData); -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/GeometryCollection} geometry Geometry. - * @private - */ -Modify.prototype.writeGeometryCollectionGeometry_ = function(feature, geometry) { - const geometries = geometry.getGeometriesArray(); - for (let i = 0; i < geometries.length; ++i) { - this.SEGMENT_WRITERS_[geometries[i].getType()].call(this, feature, geometries[i]); - } -}; - - -/** - * @param {module:ol/coordinate~Coordinate} coordinates Coordinates. - * @return {module:ol/Feature} Vertex feature. - * @private - */ -Modify.prototype.createOrUpdateVertexFeature_ = function(coordinates) { - let vertexFeature = this.vertexFeature_; - if (!vertexFeature) { - vertexFeature = new Feature(new Point(coordinates)); - this.vertexFeature_ = vertexFeature; - this.overlay_.getSource().addFeature(vertexFeature); - } else { - const geometry = /** @type {module:ol/geom/Point} */ (vertexFeature.getGeometry()); - geometry.setCoordinates(coordinates); - } - return vertexFeature; -}; - - /** * @param {module:ol/interaction/Modify~SegmentData} a The first segment data. * @param {module:ol/interaction/Modify~SegmentData} b The second segment data. @@ -908,84 +1195,6 @@ function handleEvent(mapBrowserEvent) { } -/** - * @param {module:ol/MapBrowserEvent} evt Event. - * @private - */ -Modify.prototype.handlePointerMove_ = function(evt) { - this.lastPixel_ = evt.pixel; - this.handlePointerAtPixel_(evt.pixel, evt.map); -}; - - -/** - * @param {module:ol~Pixel} pixel Pixel - * @param {module:ol/PluggableMap} map Map. - * @private - */ -Modify.prototype.handlePointerAtPixel_ = function(pixel, map) { - const pixelCoordinate = map.getCoordinateFromPixel(pixel); - const sortByDistance = function(a, b) { - return pointDistanceToSegmentDataSquared(pixelCoordinate, a) - - pointDistanceToSegmentDataSquared(pixelCoordinate, b); - }; - - const box = buffer(createOrUpdateFromCoordinate(pixelCoordinate), - map.getView().getResolution() * this.pixelTolerance_); - - const rBush = this.rBush_; - const nodes = rBush.getInExtent(box); - if (nodes.length > 0) { - nodes.sort(sortByDistance); - const node = nodes[0]; - const closestSegment = node.segment; - let vertex = closestOnSegmentData(pixelCoordinate, node); - const vertexPixel = map.getPixelFromCoordinate(vertex); - let dist = coordinateDistance(pixel, vertexPixel); - if (dist <= this.pixelTolerance_) { - const vertexSegments = {}; - - if (node.geometry.getType() === GeometryType.CIRCLE && - node.index === CIRCLE_CIRCUMFERENCE_INDEX) { - - this.snappedToVertex_ = true; - this.createOrUpdateVertexFeature_(vertex); - } else { - const pixel1 = map.getPixelFromCoordinate(closestSegment[0]); - const pixel2 = map.getPixelFromCoordinate(closestSegment[1]); - const squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1); - const squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2); - dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); - this.snappedToVertex_ = dist <= this.pixelTolerance_; - if (this.snappedToVertex_) { - vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; - } - this.createOrUpdateVertexFeature_(vertex); - for (let i = 1, ii = nodes.length; i < ii; ++i) { - const segment = nodes[i].segment; - if ((coordinatesEqual(closestSegment[0], segment[0]) && - coordinatesEqual(closestSegment[1], segment[1]) || - (coordinatesEqual(closestSegment[0], segment[1]) && - coordinatesEqual(closestSegment[1], segment[0])))) { - vertexSegments[getUid(segment)] = true; - } else { - break; - } - } - } - - vertexSegments[getUid(closestSegment)] = true; - this.vertexSegments_ = vertexSegments; - return; - } - } - if (this.vertexFeature_) { - this.overlay_.getSource().removeFeature(this.vertexFeature_); - this.vertexFeature_ = null; - } -}; - - /** * Returns the distance from a point to a line segment. * @@ -1032,239 +1241,6 @@ function closestOnSegmentData(pointCoordinates, segmentData) { } -/** - * @param {module:ol/interaction/Modify~SegmentData} segmentData Segment data. - * @param {module:ol/coordinate~Coordinate} vertex Vertex. - * @private - */ -Modify.prototype.insertVertex_ = function(segmentData, vertex) { - const segment = segmentData.segment; - const feature = segmentData.feature; - const geometry = segmentData.geometry; - const depth = segmentData.depth; - const index = /** @type {number} */ (segmentData.index); - let coordinates; - - while (vertex.length < geometry.getStride()) { - vertex.push(0); - } - - switch (geometry.getType()) { - case GeometryType.MULTI_LINE_STRING: - coordinates = geometry.getCoordinates(); - coordinates[depth[0]].splice(index + 1, 0, vertex); - break; - case GeometryType.POLYGON: - coordinates = geometry.getCoordinates(); - coordinates[depth[0]].splice(index + 1, 0, vertex); - break; - case GeometryType.MULTI_POLYGON: - coordinates = geometry.getCoordinates(); - coordinates[depth[1]][depth[0]].splice(index + 1, 0, vertex); - break; - case GeometryType.LINE_STRING: - coordinates = geometry.getCoordinates(); - coordinates.splice(index + 1, 0, vertex); - break; - default: - return; - } - - this.setGeometryCoordinates_(geometry, coordinates); - const rTree = this.rBush_; - rTree.remove(segmentData); - this.updateSegmentIndices_(geometry, index, depth, 1); - const newSegmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - segment: [segment[0], vertex], - feature: feature, - geometry: geometry, - depth: depth, - index: index - }); - rTree.insert(boundingExtent(newSegmentData.segment), - newSegmentData); - this.dragSegments_.push([newSegmentData, 1]); - - const newSegmentData2 = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - segment: [vertex, segment[1]], - feature: feature, - geometry: geometry, - depth: depth, - index: index + 1 - }); - rTree.insert(boundingExtent(newSegmentData2.segment), newSegmentData2); - this.dragSegments_.push([newSegmentData2, 0]); - this.ignoreNextSingleClick_ = true; -}; - -/** - * Removes the vertex currently being pointed. - * @return {boolean} True when a vertex was removed. - * @api - */ -Modify.prototype.removePoint = function() { - if (this.lastPointerEvent_ && this.lastPointerEvent_.type != MapBrowserEventType.POINTERDRAG) { - const evt = this.lastPointerEvent_; - this.willModifyFeatures_(evt); - this.removeVertex_(); - this.dispatchEvent(new ModifyEvent(ModifyEventType.MODIFYEND, this.features_, evt)); - this.modified_ = false; - return true; - } - return false; -}; - -/** - * Removes a vertex from all matching features. - * @return {boolean} True when a vertex was removed. - * @private - */ -Modify.prototype.removeVertex_ = function() { - const dragSegments = this.dragSegments_; - const segmentsByFeature = {}; - let deleted = false; - let component, coordinates, dragSegment, geometry, i, index, left; - let newIndex, right, segmentData, uid; - for (i = dragSegments.length - 1; i >= 0; --i) { - dragSegment = dragSegments[i]; - segmentData = dragSegment[0]; - uid = getUid(segmentData.feature); - if (segmentData.depth) { - // separate feature components - uid += '-' + segmentData.depth.join('-'); - } - if (!(uid in segmentsByFeature)) { - segmentsByFeature[uid] = {}; - } - if (dragSegment[1] === 0) { - segmentsByFeature[uid].right = segmentData; - segmentsByFeature[uid].index = segmentData.index; - } else if (dragSegment[1] == 1) { - segmentsByFeature[uid].left = segmentData; - segmentsByFeature[uid].index = segmentData.index + 1; - } - - } - for (uid in segmentsByFeature) { - right = segmentsByFeature[uid].right; - left = segmentsByFeature[uid].left; - index = segmentsByFeature[uid].index; - newIndex = index - 1; - if (left !== undefined) { - segmentData = left; - } else { - segmentData = right; - } - if (newIndex < 0) { - newIndex = 0; - } - geometry = segmentData.geometry; - coordinates = geometry.getCoordinates(); - component = coordinates; - deleted = false; - switch (geometry.getType()) { - case GeometryType.MULTI_LINE_STRING: - if (coordinates[segmentData.depth[0]].length > 2) { - coordinates[segmentData.depth[0]].splice(index, 1); - deleted = true; - } - break; - case GeometryType.LINE_STRING: - if (coordinates.length > 2) { - coordinates.splice(index, 1); - deleted = true; - } - break; - case GeometryType.MULTI_POLYGON: - component = component[segmentData.depth[1]]; - /* falls through */ - case GeometryType.POLYGON: - component = component[segmentData.depth[0]]; - if (component.length > 4) { - if (index == component.length - 1) { - index = 0; - } - component.splice(index, 1); - deleted = true; - if (index === 0) { - // close the ring again - component.pop(); - component.push(component[0]); - newIndex = component.length - 1; - } - } - break; - default: - // pass - } - - if (deleted) { - this.setGeometryCoordinates_(geometry, coordinates); - const segments = []; - if (left !== undefined) { - this.rBush_.remove(left); - segments.push(left.segment[0]); - } - if (right !== undefined) { - this.rBush_.remove(right); - segments.push(right.segment[1]); - } - if (left !== undefined && right !== undefined) { - const newSegmentData = /** @type {module:ol/interaction/Modify~SegmentData} */ ({ - depth: segmentData.depth, - feature: segmentData.feature, - geometry: segmentData.geometry, - index: newIndex, - segment: segments - }); - this.rBush_.insert(boundingExtent(newSegmentData.segment), - newSegmentData); - } - this.updateSegmentIndices_(geometry, index, segmentData.depth, -1); - if (this.vertexFeature_) { - this.overlay_.getSource().removeFeature(this.vertexFeature_); - this.vertexFeature_ = null; - } - dragSegments.length = 0; - } - - } - return deleted; -}; - - -/** - * @param {module:ol/geom/SimpleGeometry} geometry Geometry. - * @param {Array} coordinates Coordinates. - * @private - */ -Modify.prototype.setGeometryCoordinates_ = function(geometry, coordinates) { - this.changingFeature_ = true; - geometry.setCoordinates(coordinates); - this.changingFeature_ = false; -}; - - -/** - * @param {module:ol/geom/SimpleGeometry} geometry Geometry. - * @param {number} index Index. - * @param {Array.|undefined} depth Depth. - * @param {number} delta Delta (1 or -1). - * @private - */ -Modify.prototype.updateSegmentIndices_ = function( - geometry, index, depth, delta) { - this.rBush_.forEachInExtent(geometry.getExtent(), function(segmentDataMatch) { - if (segmentDataMatch.geometry === geometry && - (depth === undefined || segmentDataMatch.depth === undefined || - equals(segmentDataMatch.depth, depth)) && - segmentDataMatch.index > index) { - segmentDataMatch.index += delta; - } - }); -}; - - /** * @return {module:ol/style/Style~StyleFunction} Styles. */ diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index d22e004721..e5ab1e3dce 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -53,101 +53,144 @@ export const Mode = { * @param {module:ol/interaction/MouseWheelZoom~Options=} opt_options Options. * @api */ -const MouseWheelZoom = function(opt_options) { +class MouseWheelZoom { + constructor(opt_options) { - Interaction.call(this, { - handleEvent: handleEvent - }); + Interaction.call(this, { + handleEvent: handleEvent + }); - const options = opt_options || {}; + const options = opt_options || {}; + + /** + * @private + * @type {number} + */ + this.delta_ = 0; + + /** + * @private + * @type {number} + */ + this.duration_ = options.duration !== undefined ? options.duration : 250; + + /** + * @private + * @type {number} + */ + this.timeout_ = options.timeout !== undefined ? options.timeout : 80; + + /** + * @private + * @type {boolean} + */ + this.useAnchor_ = options.useAnchor !== undefined ? options.useAnchor : true; + + /** + * @private + * @type {boolean} + */ + this.constrainResolution_ = options.constrainResolution || false; + + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.condition_ = options.condition ? options.condition : always; + + /** + * @private + * @type {?module:ol/coordinate~Coordinate} + */ + this.lastAnchor_ = null; + + /** + * @private + * @type {number|undefined} + */ + this.startTime_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.timeoutId_ = undefined; + + /** + * @private + * @type {module:ol/interaction/MouseWheelZoom~Mode|undefined} + */ + this.mode_ = undefined; + + /** + * Trackpad events separated by this delay will be considered separate + * interactions. + * @type {number} + */ + this.trackpadEventGap_ = 400; + + /** + * @type {number|undefined} + */ + this.trackpadTimeoutId_ = undefined; + + /** + * The number of delta values per zoom level + * @private + * @type {number} + */ + this.trackpadDeltaPerZoom_ = 300; + + /** + * The zoom factor by which scroll zooming is allowed to exceed the limits. + * @private + * @type {number} + */ + this.trackpadZoomBuffer_ = 1.5; + + } /** * @private - * @type {number} */ - this.delta_ = 0; + decrementInteractingHint_() { + this.trackpadTimeoutId_ = undefined; + const view = this.getMap().getView(); + view.setHint(ViewHint.INTERACTING, -1); + } /** * @private - * @type {number} + * @param {module:ol/PluggableMap} map Map. */ - this.duration_ = options.duration !== undefined ? options.duration : 250; + handleWheelZoom_(map) { + const view = map.getView(); + if (view.getAnimating()) { + view.cancelAnimations(); + } + const maxDelta = MAX_DELTA; + const delta = clamp(this.delta_, -maxDelta, maxDelta); + zoomByDelta(view, -delta, this.lastAnchor_, this.duration_); + this.mode_ = undefined; + this.delta_ = 0; + this.lastAnchor_ = null; + this.startTime_ = undefined; + this.timeoutId_ = undefined; + } /** - * @private - * @type {number} + * Enable or disable using the mouse's location as an anchor when zooming + * @param {boolean} useAnchor true to zoom to the mouse's location, false + * to zoom to the center of the map + * @api */ - this.timeout_ = options.timeout !== undefined ? options.timeout : 80; - - /** - * @private - * @type {boolean} - */ - this.useAnchor_ = options.useAnchor !== undefined ? options.useAnchor : true; - - /** - * @private - * @type {boolean} - */ - this.constrainResolution_ = options.constrainResolution || false; - - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.condition_ = options.condition ? options.condition : always; - - /** - * @private - * @type {?module:ol/coordinate~Coordinate} - */ - this.lastAnchor_ = null; - - /** - * @private - * @type {number|undefined} - */ - this.startTime_ = undefined; - - /** - * @private - * @type {number|undefined} - */ - this.timeoutId_ = undefined; - - /** - * @private - * @type {module:ol/interaction/MouseWheelZoom~Mode|undefined} - */ - this.mode_ = undefined; - - /** - * Trackpad events separated by this delay will be considered separate - * interactions. - * @type {number} - */ - this.trackpadEventGap_ = 400; - - /** - * @type {number|undefined} - */ - this.trackpadTimeoutId_ = undefined; - - /** - * The number of delta values per zoom level - * @private - * @type {number} - */ - this.trackpadDeltaPerZoom_ = 300; - - /** - * The zoom factor by which scroll zooming is allowed to exceed the limits. - * @private - * @type {number} - */ - this.trackpadZoomBuffer_ = 1.5; - -}; + setMouseAnchor(useAnchor) { + this.useAnchor_ = useAnchor; + if (!useAnchor) { + this.lastAnchor_ = null; + } + } +} inherits(MouseWheelZoom, Interaction); @@ -276,48 +319,4 @@ function handleEvent(mapBrowserEvent) { } -/** - * @private - */ -MouseWheelZoom.prototype.decrementInteractingHint_ = function() { - this.trackpadTimeoutId_ = undefined; - const view = this.getMap().getView(); - view.setHint(ViewHint.INTERACTING, -1); -}; - - -/** - * @private - * @param {module:ol/PluggableMap} map Map. - */ -MouseWheelZoom.prototype.handleWheelZoom_ = function(map) { - const view = map.getView(); - if (view.getAnimating()) { - view.cancelAnimations(); - } - const maxDelta = MAX_DELTA; - const delta = clamp(this.delta_, -maxDelta, maxDelta); - zoomByDelta(view, -delta, this.lastAnchor_, this.duration_); - this.mode_ = undefined; - this.delta_ = 0; - this.lastAnchor_ = null; - this.startTime_ = undefined; - this.timeoutId_ = undefined; -}; - - -/** - * Enable or disable using the mouse's location as an anchor when zooming - * @param {boolean} useAnchor true to zoom to the mouse's location, false - * to zoom to the center of the map - * @api - */ -MouseWheelZoom.prototype.setMouseAnchor = function(useAnchor) { - this.useAnchor_ = useAnchor; - if (!useAnchor) { - this.lastAnchor_ = null; - } -}; - - export default MouseWheelZoom; diff --git a/src/ol/interaction/Pointer.js b/src/ol/interaction/Pointer.js index c0a2f4df51..928a026508 100644 --- a/src/ol/interaction/Pointer.js +++ b/src/ol/interaction/Pointer.js @@ -77,61 +77,102 @@ const handleMoveEvent = UNDEFINED; * @extends {module:ol/interaction/Interaction} * @api */ -const PointerInteraction = function(opt_options) { +class PointerInteraction { + constructor(opt_options) { - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - Interaction.call(this, { - handleEvent: options.handleEvent || handleEvent - }); + Interaction.call(this, { + handleEvent: options.handleEvent || handleEvent + }); + + /** + * @type {function(module:ol/MapBrowserPointerEvent):boolean} + * @private + */ + this.handleDownEvent_ = options.handleDownEvent ? + options.handleDownEvent : handleDownEvent; + + /** + * @type {function(module:ol/MapBrowserPointerEvent)} + * @private + */ + this.handleDragEvent_ = options.handleDragEvent ? + options.handleDragEvent : handleDragEvent; + + /** + * @type {function(module:ol/MapBrowserPointerEvent)} + * @private + */ + this.handleMoveEvent_ = options.handleMoveEvent ? + options.handleMoveEvent : handleMoveEvent; + + /** + * @type {function(module:ol/MapBrowserPointerEvent):boolean} + * @private + */ + this.handleUpEvent_ = options.handleUpEvent ? + options.handleUpEvent : handleUpEvent; + + /** + * @type {boolean} + * @protected + */ + this.handlingDownUpSequence = false; + + /** + * @type {!Object.} + * @private + */ + this.trackedPointers_ = {}; + + /** + * @type {Array.} + * @protected + */ + this.targetPointers = []; + + } /** - * @type {function(module:ol/MapBrowserPointerEvent):boolean} + * @param {module:ol/MapBrowserPointerEvent} mapBrowserEvent Event. * @private */ - this.handleDownEvent_ = options.handleDownEvent ? - options.handleDownEvent : handleDownEvent; + updateTrackedPointers_(mapBrowserEvent) { + if (isPointerDraggingEvent(mapBrowserEvent)) { + const event = mapBrowserEvent.pointerEvent; + + const id = event.pointerId.toString(); + if (mapBrowserEvent.type == MapBrowserEventType.POINTERUP) { + delete this.trackedPointers_[id]; + } else if (mapBrowserEvent.type == + MapBrowserEventType.POINTERDOWN) { + this.trackedPointers_[id] = event; + } else if (id in this.trackedPointers_) { + // update only when there was a pointerdown event for this pointer + this.trackedPointers_[id] = event; + } + this.targetPointers = getValues(this.trackedPointers_); + } + } /** - * @type {function(module:ol/MapBrowserPointerEvent)} - * @private - */ - this.handleDragEvent_ = options.handleDragEvent ? - options.handleDragEvent : handleDragEvent; - - /** - * @type {function(module:ol/MapBrowserPointerEvent)} - * @private - */ - this.handleMoveEvent_ = options.handleMoveEvent ? - options.handleMoveEvent : handleMoveEvent; - - /** - * @type {function(module:ol/MapBrowserPointerEvent):boolean} - * @private - */ - this.handleUpEvent_ = options.handleUpEvent ? - options.handleUpEvent : handleUpEvent; - - /** - * @type {boolean} + * This method is used to determine if "down" events should be propagated to + * other interactions or should be stopped. + * + * The method receives the return code of the "handleDownEvent" function. + * + * By default this function is the "identity" function. It's overridden in + * child classes. + * + * @param {boolean} handled Was the event handled by the interaction? + * @return {boolean} Should the event be stopped? * @protected */ - this.handlingDownUpSequence = false; - - /** - * @type {!Object.} - * @private - */ - this.trackedPointers_ = {}; - - /** - * @type {Array.} - * @protected - */ - this.targetPointers = []; - -}; + shouldStopEvent(handled) { + return handled; + } +} inherits(PointerInteraction, Interaction); @@ -165,29 +206,6 @@ function isPointerDraggingEvent(mapBrowserEvent) { } -/** - * @param {module:ol/MapBrowserPointerEvent} mapBrowserEvent Event. - * @private - */ -PointerInteraction.prototype.updateTrackedPointers_ = function(mapBrowserEvent) { - if (isPointerDraggingEvent(mapBrowserEvent)) { - const event = mapBrowserEvent.pointerEvent; - - const id = event.pointerId.toString(); - if (mapBrowserEvent.type == MapBrowserEventType.POINTERUP) { - delete this.trackedPointers_[id]; - } else if (mapBrowserEvent.type == - MapBrowserEventType.POINTERDOWN) { - this.trackedPointers_[id] = event; - } else if (id in this.trackedPointers_) { - // update only when there was a pointerdown event for this pointer - this.trackedPointers_[id] = event; - } - this.targetPointers = getValues(this.trackedPointers_); - } -}; - - /** * Handles the {@link module:ol/MapBrowserEvent map browser event} and may call into * other functions, if event sequences like e.g. 'drag' or 'down-up' etc. are @@ -224,21 +242,4 @@ export function handleEvent(mapBrowserEvent) { } -/** - * This method is used to determine if "down" events should be propagated to - * other interactions or should be stopped. - * - * The method receives the return code of the "handleDownEvent" function. - * - * By default this function is the "identity" function. It's overridden in - * child classes. - * - * @param {boolean} handled Was the event handled by the interaction? - * @return {boolean} Should the event be stopped? - * @protected - */ -PointerInteraction.prototype.shouldStopEvent = function(handled) { - return handled; -}; - export default PointerInteraction; diff --git a/src/ol/interaction/Select.js b/src/ol/interaction/Select.js index d42403da7d..1fb87e9063 100644 --- a/src/ol/interaction/Select.js +++ b/src/ol/interaction/Select.js @@ -156,162 +156,223 @@ inherits(SelectEvent, Event); * @fires SelectEvent * @api */ -const Select = function(opt_options) { +class Select { + constructor(opt_options) { - Interaction.call(this, { - handleEvent: handleEvent - }); + Interaction.call(this, { + handleEvent: handleEvent + }); - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.condition_ = options.condition ? options.condition : singleClick; + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.condition_ = options.condition ? options.condition : singleClick; - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.addCondition_ = options.addCondition ? options.addCondition : never; + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.addCondition_ = options.addCondition ? options.addCondition : never; - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.removeCondition_ = options.removeCondition ? options.removeCondition : never; + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.removeCondition_ = options.removeCondition ? options.removeCondition : never; - /** - * @private - * @type {module:ol/events/condition~Condition} - */ - this.toggleCondition_ = options.toggleCondition ? options.toggleCondition : shiftKeyOnly; + /** + * @private + * @type {module:ol/events/condition~Condition} + */ + this.toggleCondition_ = options.toggleCondition ? options.toggleCondition : shiftKeyOnly; - /** - * @private - * @type {boolean} - */ - this.multi_ = options.multi ? options.multi : false; + /** + * @private + * @type {boolean} + */ + this.multi_ = options.multi ? options.multi : false; - /** - * @private - * @type {module:ol/interaction/Select~FilterFunction} - */ - this.filter_ = options.filter ? options.filter : TRUE; + /** + * @private + * @type {module:ol/interaction/Select~FilterFunction} + */ + this.filter_ = options.filter ? options.filter : TRUE; - /** - * @private - * @type {number} - */ - this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0; + /** + * @private + * @type {number} + */ + this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0; - const featureOverlay = new VectorLayer({ - source: new VectorSource({ - useSpatialIndex: false, - features: options.features, - wrapX: options.wrapX - }), - style: options.style ? options.style : - getDefaultStyleFunction(), - updateWhileAnimating: true, - updateWhileInteracting: true - }); + const featureOverlay = new VectorLayer({ + source: new VectorSource({ + useSpatialIndex: false, + features: options.features, + wrapX: options.wrapX + }), + style: options.style ? options.style : + getDefaultStyleFunction(), + updateWhileAnimating: true, + updateWhileInteracting: true + }); - /** - * @private - * @type {module:ol/layer/Vector} - */ - this.featureOverlay_ = featureOverlay; + /** + * @private + * @type {module:ol/layer/Vector} + */ + this.featureOverlay_ = featureOverlay; - /** @type {function(module:ol/layer/Layer): boolean} */ - let layerFilter; - if (options.layers) { - if (typeof options.layers === 'function') { - layerFilter = options.layers; + /** @type {function(module:ol/layer/Layer): boolean} */ + let layerFilter; + if (options.layers) { + if (typeof options.layers === 'function') { + layerFilter = options.layers; + } else { + const layers = options.layers; + layerFilter = function(layer) { + return includes(layers, layer); + }; + } } else { - const layers = options.layers; - layerFilter = function(layer) { - return includes(layers, layer); - }; + layerFilter = TRUE; } - } else { - layerFilter = TRUE; + + /** + * @private + * @type {function(module:ol/layer/Layer): boolean} + */ + this.layerFilter_ = layerFilter; + + /** + * An association between selected feature (key) + * and layer (value) + * @private + * @type {Object.} + */ + this.featureLayerAssociation_ = {}; + + const features = this.featureOverlay_.getSource().getFeaturesCollection(); + listen(features, CollectionEventType.ADD, + this.addFeature_, this); + listen(features, CollectionEventType.REMOVE, + this.removeFeature_, this); + } /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @param {module:ol/layer/Layer} layer Layer. * @private - * @type {function(module:ol/layer/Layer): boolean} */ - this.layerFilter_ = layerFilter; + addFeatureLayerAssociation_(feature, layer) { + const key = getUid(feature); + this.featureLayerAssociation_[key] = layer; + } /** - * An association between selected feature (key) - * and layer (value) - * @private - * @type {Object.} + * Get the selected features. + * @return {module:ol/Collection.} Features collection. + * @api */ - this.featureLayerAssociation_ = {}; + getFeatures() { + return this.featureOverlay_.getSource().getFeaturesCollection(); + } - const features = this.featureOverlay_.getSource().getFeaturesCollection(); - listen(features, CollectionEventType.ADD, - this.addFeature_, this); - listen(features, CollectionEventType.REMOVE, - this.removeFeature_, this); + /** + * Returns the Hit-detection tolerance. + * @returns {number} Hit tolerance in pixels. + * @api + */ + getHitTolerance() { + return this.hitTolerance_; + } -}; + /** + * Returns the associated {@link module:ol/layer/Vector~Vector vectorlayer} of + * the (last) selected feature. Note that this will not work with any + * programmatic method like pushing features to + * {@link module:ol/interaction/Select~Select#getFeatures collection}. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature + * @return {module:ol/layer/Vector} Layer. + * @api + */ + getLayer(feature) { + const key = getUid(feature); + return ( + /** @type {module:ol/layer/Vector} */ (this.featureLayerAssociation_[key]) + ); + } + + /** + * Hit-detection tolerance. Pixels inside the radius around the given position + * will be checked for features. This only works for the canvas renderer and + * not for WebGL. + * @param {number} hitTolerance Hit tolerance in pixels. + * @api + */ + setHitTolerance(hitTolerance) { + this.hitTolerance_ = hitTolerance; + } + + /** + * Remove the interaction from its current map, if any, and attach it to a new + * map, if any. Pass `null` to just remove the interaction from the current map. + * @param {module:ol/PluggableMap} map Map. + * @override + * @api + */ + setMap(map) { + const currentMap = this.getMap(); + const selectedFeatures = + this.featureOverlay_.getSource().getFeaturesCollection(); + if (currentMap) { + selectedFeatures.forEach(currentMap.unskipFeature.bind(currentMap)); + } + Interaction.prototype.setMap.call(this, map); + this.featureOverlay_.setMap(map); + if (map) { + selectedFeatures.forEach(map.skipFeature.bind(map)); + } + } + + /** + * @param {module:ol/Collection~CollectionEvent} evt Event. + * @private + */ + addFeature_(evt) { + const map = this.getMap(); + if (map) { + map.skipFeature(/** @type {module:ol/Feature} */ (evt.element)); + } + } + + /** + * @param {module:ol/Collection~CollectionEvent} evt Event. + * @private + */ + removeFeature_(evt) { + const map = this.getMap(); + if (map) { + map.unskipFeature(/** @type {module:ol/Feature} */ (evt.element)); + } + } + + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @private + */ + removeFeatureLayerAssociation_(feature) { + const key = getUid(feature); + delete this.featureLayerAssociation_[key]; + } +} inherits(Select, Interaction); -/** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @param {module:ol/layer/Layer} layer Layer. - * @private - */ -Select.prototype.addFeatureLayerAssociation_ = function(feature, layer) { - const key = getUid(feature); - this.featureLayerAssociation_[key] = layer; -}; - - -/** - * Get the selected features. - * @return {module:ol/Collection.} Features collection. - * @api - */ -Select.prototype.getFeatures = function() { - return this.featureOverlay_.getSource().getFeaturesCollection(); -}; - - -/** - * Returns the Hit-detection tolerance. - * @returns {number} Hit tolerance in pixels. - * @api - */ -Select.prototype.getHitTolerance = function() { - return this.hitTolerance_; -}; - - -/** - * Returns the associated {@link module:ol/layer/Vector~Vector vectorlayer} of - * the (last) selected feature. Note that this will not work with any - * programmatic method like pushing features to - * {@link module:ol/interaction/Select~Select#getFeatures collection}. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature - * @return {module:ol/layer/Vector} Layer. - * @api - */ -Select.prototype.getLayer = function(feature) { - const key = getUid(feature); - return ( - /** @type {module:ol/layer/Vector} */ (this.featureLayerAssociation_[key]) - ); -}; - - /** * Handles the {@link module:ol/MapBrowserEvent map browser event} and may change the * selected state of features. @@ -405,40 +466,6 @@ function handleEvent(mapBrowserEvent) { } -/** - * Hit-detection tolerance. Pixels inside the radius around the given position - * will be checked for features. This only works for the canvas renderer and - * not for WebGL. - * @param {number} hitTolerance Hit tolerance in pixels. - * @api - */ -Select.prototype.setHitTolerance = function(hitTolerance) { - this.hitTolerance_ = hitTolerance; -}; - - -/** - * Remove the interaction from its current map, if any, and attach it to a new - * map, if any. Pass `null` to just remove the interaction from the current map. - * @param {module:ol/PluggableMap} map Map. - * @override - * @api - */ -Select.prototype.setMap = function(map) { - const currentMap = this.getMap(); - const selectedFeatures = - this.featureOverlay_.getSource().getFeaturesCollection(); - if (currentMap) { - selectedFeatures.forEach(currentMap.unskipFeature.bind(currentMap)); - } - Interaction.prototype.setMap.call(this, map); - this.featureOverlay_.setMap(map); - if (map) { - selectedFeatures.forEach(map.skipFeature.bind(map)); - } -}; - - /** * @return {module:ol/style/Style~StyleFunction} Styles. */ @@ -456,38 +483,4 @@ function getDefaultStyleFunction() { } -/** - * @param {module:ol/Collection~CollectionEvent} evt Event. - * @private - */ -Select.prototype.addFeature_ = function(evt) { - const map = this.getMap(); - if (map) { - map.skipFeature(/** @type {module:ol/Feature} */ (evt.element)); - } -}; - - -/** - * @param {module:ol/Collection~CollectionEvent} evt Event. - * @private - */ -Select.prototype.removeFeature_ = function(evt) { - const map = this.getMap(); - if (map) { - map.unskipFeature(/** @type {module:ol/Feature} */ (evt.element)); - } -}; - - -/** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @private - */ -Select.prototype.removeFeatureLayerAssociation_ = function(feature) { - const key = getUid(feature); - delete this.featureLayerAssociation_[key]; -}; - - export default Select; diff --git a/src/ol/interaction/Snap.js b/src/ol/interaction/Snap.js index f8eff9024d..85c9bff6fc 100644 --- a/src/ol/interaction/Snap.js +++ b/src/ol/interaction/Snap.js @@ -68,459 +68,386 @@ import RBush from '../structs/RBush.js'; * @param {module:ol/interaction/Snap~Options=} opt_options Options. * @api */ -const Snap = function(opt_options) { +class Snap { + constructor(opt_options) { - PointerInteraction.call(this, { - handleEvent: handleEvent, - handleDownEvent: TRUE, - handleUpEvent: handleUpEvent - }); - - const options = opt_options ? opt_options : {}; - - /** - * @type {module:ol/source/Vector} - * @private - */ - this.source_ = options.source ? options.source : null; - - /** - * @private - * @type {boolean} - */ - this.vertex_ = options.vertex !== undefined ? options.vertex : true; - - /** - * @private - * @type {boolean} - */ - this.edge_ = options.edge !== undefined ? options.edge : true; - - /** - * @type {module:ol/Collection.} - * @private - */ - this.features_ = options.features ? options.features : null; - - /** - * @type {Array.} - * @private - */ - this.featuresListenerKeys_ = []; - - /** - * @type {Object.} - * @private - */ - this.featureChangeListenerKeys_ = {}; - - /** - * Extents are preserved so indexed segment can be quickly removed - * when its feature geometry changes - * @type {Object.} - * @private - */ - this.indexedFeaturesExtents_ = {}; - - /** - * If a feature geometry changes while a pointer drag|move event occurs, the - * feature doesn't get updated right away. It will be at the next 'pointerup' - * event fired. - * @type {!Object.} - * @private - */ - this.pendingFeatures_ = {}; - - /** - * Used for distance sorting in sortByDistance_ - * @type {module:ol/coordinate~Coordinate} - * @private - */ - this.pixelCoordinate_ = null; - - /** - * @type {number} - * @private - */ - this.pixelTolerance_ = options.pixelTolerance !== undefined ? - options.pixelTolerance : 10; - - /** - * @type {function(module:ol/interaction/Snap~SegmentData, module:ol/interaction/Snap~SegmentData): number} - * @private - */ - this.sortByDistance_ = sortByDistance.bind(this); - - - /** - * Segment RTree for each layer - * @type {module:ol/structs/RBush.} - * @private - */ - this.rBush_ = new RBush(); - - - /** - * @const - * @private - * @type {Object.} - */ - this.SEGMENT_WRITERS_ = { - 'Point': this.writePointGeometry_, - 'LineString': this.writeLineStringGeometry_, - 'LinearRing': this.writeLineStringGeometry_, - 'Polygon': this.writePolygonGeometry_, - 'MultiPoint': this.writeMultiPointGeometry_, - 'MultiLineString': this.writeMultiLineStringGeometry_, - 'MultiPolygon': this.writeMultiPolygonGeometry_, - 'GeometryCollection': this.writeGeometryCollectionGeometry_, - 'Circle': this.writeCircleGeometry_ - }; -}; - -inherits(Snap, PointerInteraction); - - -/** - * Add a feature to the collection of features that we may snap to. - * @param {module:ol/Feature} feature Feature. - * @param {boolean=} opt_listen Whether to listen to the feature change or not - * Defaults to `true`. - * @api - */ -Snap.prototype.addFeature = function(feature, opt_listen) { - const register = opt_listen !== undefined ? opt_listen : true; - const feature_uid = getUid(feature); - const geometry = feature.getGeometry(); - if (geometry) { - const segmentWriter = this.SEGMENT_WRITERS_[geometry.getType()]; - if (segmentWriter) { - this.indexedFeaturesExtents_[feature_uid] = geometry.getExtent(createEmpty()); - segmentWriter.call(this, feature, geometry); - } - } - - if (register) { - this.featureChangeListenerKeys_[feature_uid] = listen( - feature, - EventType.CHANGE, - this.handleFeatureChange_, this); - } -}; - - -/** - * @param {module:ol/Feature} feature Feature. - * @private - */ -Snap.prototype.forEachFeatureAdd_ = function(feature) { - this.addFeature(feature); -}; - - -/** - * @param {module:ol/Feature} feature Feature. - * @private - */ -Snap.prototype.forEachFeatureRemove_ = function(feature) { - this.removeFeature(feature); -}; - - -/** - * @return {module:ol/Collection.|Array.} Features. - * @private - */ -Snap.prototype.getFeatures_ = function() { - let features; - if (this.features_) { - features = this.features_; - } else if (this.source_) { - features = this.source_.getFeatures(); - } - return ( - /** @type {!Array.|!module:ol/Collection.} */ (features) - ); -}; - - -/** - * @param {module:ol/source/Vector|module:ol/Collection~CollectionEvent} evt Event. - * @private - */ -Snap.prototype.handleFeatureAdd_ = function(evt) { - let feature; - if (evt instanceof VectorSourceEvent) { - feature = evt.feature; - } else if (evt instanceof CollectionEvent) { - feature = evt.element; - } - this.addFeature(/** @type {module:ol/Feature} */ (feature)); -}; - - -/** - * @param {module:ol/source/Vector|module:ol/Collection~CollectionEvent} evt Event. - * @private - */ -Snap.prototype.handleFeatureRemove_ = function(evt) { - let feature; - if (evt instanceof VectorSourceEvent) { - feature = evt.feature; - } else if (evt instanceof CollectionEvent) { - feature = evt.element; - } - this.removeFeature(/** @type {module:ol/Feature} */ (feature)); -}; - - -/** - * @param {module:ol/events/Event} evt Event. - * @private - */ -Snap.prototype.handleFeatureChange_ = function(evt) { - const feature = /** @type {module:ol/Feature} */ (evt.target); - if (this.handlingDownUpSequence) { - const uid = getUid(feature); - if (!(uid in this.pendingFeatures_)) { - this.pendingFeatures_[uid] = feature; - } - } else { - this.updateFeature_(feature); - } -}; - - -/** - * Remove a feature from the collection of features that we may snap to. - * @param {module:ol/Feature} feature Feature - * @param {boolean=} opt_unlisten Whether to unlisten to the feature change - * or not. Defaults to `true`. - * @api - */ -Snap.prototype.removeFeature = function(feature, opt_unlisten) { - const unregister = opt_unlisten !== undefined ? opt_unlisten : true; - const feature_uid = getUid(feature); - const extent = this.indexedFeaturesExtents_[feature_uid]; - if (extent) { - const rBush = this.rBush_; - const nodesToRemove = []; - rBush.forEachInExtent(extent, function(node) { - if (feature === node.feature) { - nodesToRemove.push(node); - } + PointerInteraction.call(this, { + handleEvent: handleEvent, + handleDownEvent: TRUE, + handleUpEvent: handleUpEvent }); - for (let i = nodesToRemove.length - 1; i >= 0; --i) { - rBush.remove(nodesToRemove[i]); + + const options = opt_options ? opt_options : {}; + + /** + * @type {module:ol/source/Vector} + * @private + */ + this.source_ = options.source ? options.source : null; + + /** + * @private + * @type {boolean} + */ + this.vertex_ = options.vertex !== undefined ? options.vertex : true; + + /** + * @private + * @type {boolean} + */ + this.edge_ = options.edge !== undefined ? options.edge : true; + + /** + * @type {module:ol/Collection.} + * @private + */ + this.features_ = options.features ? options.features : null; + + /** + * @type {Array.} + * @private + */ + this.featuresListenerKeys_ = []; + + /** + * @type {Object.} + * @private + */ + this.featureChangeListenerKeys_ = {}; + + /** + * Extents are preserved so indexed segment can be quickly removed + * when its feature geometry changes + * @type {Object.} + * @private + */ + this.indexedFeaturesExtents_ = {}; + + /** + * If a feature geometry changes while a pointer drag|move event occurs, the + * feature doesn't get updated right away. It will be at the next 'pointerup' + * event fired. + * @type {!Object.} + * @private + */ + this.pendingFeatures_ = {}; + + /** + * Used for distance sorting in sortByDistance_ + * @type {module:ol/coordinate~Coordinate} + * @private + */ + this.pixelCoordinate_ = null; + + /** + * @type {number} + * @private + */ + this.pixelTolerance_ = options.pixelTolerance !== undefined ? + options.pixelTolerance : 10; + + /** + * @type {function(module:ol/interaction/Snap~SegmentData, module:ol/interaction/Snap~SegmentData): number} + * @private + */ + this.sortByDistance_ = sortByDistance.bind(this); + + + /** + * Segment RTree for each layer + * @type {module:ol/structs/RBush.} + * @private + */ + this.rBush_ = new RBush(); + + + /** + * @const + * @private + * @type {Object.} + */ + this.SEGMENT_WRITERS_ = { + 'Point': this.writePointGeometry_, + 'LineString': this.writeLineStringGeometry_, + 'LinearRing': this.writeLineStringGeometry_, + 'Polygon': this.writePolygonGeometry_, + 'MultiPoint': this.writeMultiPointGeometry_, + 'MultiLineString': this.writeMultiLineStringGeometry_, + 'MultiPolygon': this.writeMultiPolygonGeometry_, + 'GeometryCollection': this.writeGeometryCollectionGeometry_, + 'Circle': this.writeCircleGeometry_ + }; + } + + /** + * Add a feature to the collection of features that we may snap to. + * @param {module:ol/Feature} feature Feature. + * @param {boolean=} opt_listen Whether to listen to the feature change or not + * Defaults to `true`. + * @api + */ + addFeature(feature, opt_listen) { + const register = opt_listen !== undefined ? opt_listen : true; + const feature_uid = getUid(feature); + const geometry = feature.getGeometry(); + if (geometry) { + const segmentWriter = this.SEGMENT_WRITERS_[geometry.getType()]; + if (segmentWriter) { + this.indexedFeaturesExtents_[feature_uid] = geometry.getExtent(createEmpty()); + segmentWriter.call(this, feature, geometry); + } + } + + if (register) { + this.featureChangeListenerKeys_[feature_uid] = listen( + feature, + EventType.CHANGE, + this.handleFeatureChange_, this); } } - if (unregister) { - unlistenByKey(this.featureChangeListenerKeys_[feature_uid]); - delete this.featureChangeListenerKeys_[feature_uid]; + /** + * @param {module:ol/Feature} feature Feature. + * @private + */ + forEachFeatureAdd_(feature) { + this.addFeature(feature); } -}; - -/** - * @inheritDoc - */ -Snap.prototype.setMap = function(map) { - const currentMap = this.getMap(); - const keys = this.featuresListenerKeys_; - const features = this.getFeatures_(); - - if (currentMap) { - keys.forEach(unlistenByKey); - keys.length = 0; - features.forEach(this.forEachFeatureRemove_.bind(this)); + /** + * @param {module:ol/Feature} feature Feature. + * @private + */ + forEachFeatureRemove_(feature) { + this.removeFeature(feature); } - PointerInteraction.prototype.setMap.call(this, map); - if (map) { + /** + * @return {module:ol/Collection.|Array.} Features. + * @private + */ + getFeatures_() { + let features; if (this.features_) { - keys.push( - listen(this.features_, CollectionEventType.ADD, - this.handleFeatureAdd_, this), - listen(this.features_, CollectionEventType.REMOVE, - this.handleFeatureRemove_, this) - ); + features = this.features_; } else if (this.source_) { - keys.push( - listen(this.source_, VectorEventType.ADDFEATURE, - this.handleFeatureAdd_, this), - listen(this.source_, VectorEventType.REMOVEFEATURE, - this.handleFeatureRemove_, this) - ); + features = this.source_.getFeatures(); } - features.forEach(this.forEachFeatureAdd_.bind(this)); - } -}; - - -/** - * @inheritDoc - */ -Snap.prototype.shouldStopEvent = FALSE; - - -/** - * @param {module:ol~Pixel} pixel Pixel - * @param {module:ol/coordinate~Coordinate} pixelCoordinate Coordinate - * @param {module:ol/PluggableMap} map Map. - * @return {module:ol/interaction/Snap~Result} Snap result - */ -Snap.prototype.snapTo = function(pixel, pixelCoordinate, map) { - - const lowerLeft = map.getCoordinateFromPixel( - [pixel[0] - this.pixelTolerance_, pixel[1] + this.pixelTolerance_]); - const upperRight = map.getCoordinateFromPixel( - [pixel[0] + this.pixelTolerance_, pixel[1] - this.pixelTolerance_]); - const box = boundingExtent([lowerLeft, upperRight]); - - let segments = this.rBush_.getInExtent(box); - - // If snapping on vertices only, don't consider circles - if (this.vertex_ && !this.edge_) { - segments = segments.filter(function(segment) { - return segment.feature.getGeometry().getType() !== - GeometryType.CIRCLE; - }); + return ( + /** @type {!Array.|!module:ol/Collection.} */ (features) + ); } - let snappedToVertex = false; - let snapped = false; - let vertex = null; - let vertexPixel = null; - let dist, pixel1, pixel2, squaredDist1, squaredDist2; - if (segments.length > 0) { - this.pixelCoordinate_ = pixelCoordinate; - segments.sort(this.sortByDistance_); - const closestSegment = segments[0].segment; - const isCircle = segments[0].feature.getGeometry().getType() === - GeometryType.CIRCLE; + /** + * @param {module:ol/source/Vector|module:ol/Collection~CollectionEvent} evt Event. + * @private + */ + handleFeatureAdd_(evt) { + let feature; + if (evt instanceof VectorSourceEvent) { + feature = evt.feature; + } else if (evt instanceof CollectionEvent) { + feature = evt.element; + } + this.addFeature(/** @type {module:ol/Feature} */ (feature)); + } + + /** + * @param {module:ol/source/Vector|module:ol/Collection~CollectionEvent} evt Event. + * @private + */ + handleFeatureRemove_(evt) { + let feature; + if (evt instanceof VectorSourceEvent) { + feature = evt.feature; + } else if (evt instanceof CollectionEvent) { + feature = evt.element; + } + this.removeFeature(/** @type {module:ol/Feature} */ (feature)); + } + + /** + * @param {module:ol/events/Event} evt Event. + * @private + */ + handleFeatureChange_(evt) { + const feature = /** @type {module:ol/Feature} */ (evt.target); + if (this.handlingDownUpSequence) { + const uid = getUid(feature); + if (!(uid in this.pendingFeatures_)) { + this.pendingFeatures_[uid] = feature; + } + } else { + this.updateFeature_(feature); + } + } + + /** + * Remove a feature from the collection of features that we may snap to. + * @param {module:ol/Feature} feature Feature + * @param {boolean=} opt_unlisten Whether to unlisten to the feature change + * or not. Defaults to `true`. + * @api + */ + removeFeature(feature, opt_unlisten) { + const unregister = opt_unlisten !== undefined ? opt_unlisten : true; + const feature_uid = getUid(feature); + const extent = this.indexedFeaturesExtents_[feature_uid]; + if (extent) { + const rBush = this.rBush_; + const nodesToRemove = []; + rBush.forEachInExtent(extent, function(node) { + if (feature === node.feature) { + nodesToRemove.push(node); + } + }); + for (let i = nodesToRemove.length - 1; i >= 0; --i) { + rBush.remove(nodesToRemove[i]); + } + } + + if (unregister) { + unlistenByKey(this.featureChangeListenerKeys_[feature_uid]); + delete this.featureChangeListenerKeys_[feature_uid]; + } + } + + /** + * @inheritDoc + */ + setMap(map) { + const currentMap = this.getMap(); + const keys = this.featuresListenerKeys_; + const features = this.getFeatures_(); + + if (currentMap) { + keys.forEach(unlistenByKey); + keys.length = 0; + features.forEach(this.forEachFeatureRemove_.bind(this)); + } + PointerInteraction.prototype.setMap.call(this, map); + + if (map) { + if (this.features_) { + keys.push( + listen(this.features_, CollectionEventType.ADD, + this.handleFeatureAdd_, this), + listen(this.features_, CollectionEventType.REMOVE, + this.handleFeatureRemove_, this) + ); + } else if (this.source_) { + keys.push( + listen(this.source_, VectorEventType.ADDFEATURE, + this.handleFeatureAdd_, this), + listen(this.source_, VectorEventType.REMOVEFEATURE, + this.handleFeatureRemove_, this) + ); + } + features.forEach(this.forEachFeatureAdd_.bind(this)); + } + } + + /** + * @param {module:ol~Pixel} pixel Pixel + * @param {module:ol/coordinate~Coordinate} pixelCoordinate Coordinate + * @param {module:ol/PluggableMap} map Map. + * @return {module:ol/interaction/Snap~Result} Snap result + */ + snapTo(pixel, pixelCoordinate, map) { + + const lowerLeft = map.getCoordinateFromPixel( + [pixel[0] - this.pixelTolerance_, pixel[1] + this.pixelTolerance_]); + const upperRight = map.getCoordinateFromPixel( + [pixel[0] + this.pixelTolerance_, pixel[1] - this.pixelTolerance_]); + const box = boundingExtent([lowerLeft, upperRight]); + + let segments = this.rBush_.getInExtent(box); + + // If snapping on vertices only, don't consider circles if (this.vertex_ && !this.edge_) { - pixel1 = map.getPixelFromCoordinate(closestSegment[0]); - pixel2 = map.getPixelFromCoordinate(closestSegment[1]); - squaredDist1 = squaredCoordinateDistance(pixel, pixel1); - squaredDist2 = squaredCoordinateDistance(pixel, pixel2); - dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); - snappedToVertex = dist <= this.pixelTolerance_; - if (snappedToVertex) { - snapped = true; - vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; + segments = segments.filter(function(segment) { + return segment.feature.getGeometry().getType() !== + GeometryType.CIRCLE; + }); + } + + let snappedToVertex = false; + let snapped = false; + let vertex = null; + let vertexPixel = null; + let dist, pixel1, pixel2, squaredDist1, squaredDist2; + if (segments.length > 0) { + this.pixelCoordinate_ = pixelCoordinate; + segments.sort(this.sortByDistance_); + const closestSegment = segments[0].segment; + const isCircle = segments[0].feature.getGeometry().getType() === + GeometryType.CIRCLE; + if (this.vertex_ && !this.edge_) { + pixel1 = map.getPixelFromCoordinate(closestSegment[0]); + pixel2 = map.getPixelFromCoordinate(closestSegment[1]); + squaredDist1 = squaredCoordinateDistance(pixel, pixel1); + squaredDist2 = squaredCoordinateDistance(pixel, pixel2); + dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); + snappedToVertex = dist <= this.pixelTolerance_; + if (snappedToVertex) { + snapped = true; + vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; + vertexPixel = map.getPixelFromCoordinate(vertex); + } + } else if (this.edge_) { + if (isCircle) { + vertex = closestOnCircle(pixelCoordinate, + /** @type {module:ol/geom/Circle} */ (segments[0].feature.getGeometry())); + } else { + vertex = closestOnSegment(pixelCoordinate, closestSegment); + } vertexPixel = map.getPixelFromCoordinate(vertex); - } - } else if (this.edge_) { - if (isCircle) { - vertex = closestOnCircle(pixelCoordinate, - /** @type {module:ol/geom/Circle} */ (segments[0].feature.getGeometry())); - } else { - vertex = closestOnSegment(pixelCoordinate, closestSegment); - } - vertexPixel = map.getPixelFromCoordinate(vertex); - if (coordinateDistance(pixel, vertexPixel) <= this.pixelTolerance_) { - snapped = true; - if (this.vertex_ && !isCircle) { - pixel1 = map.getPixelFromCoordinate(closestSegment[0]); - pixel2 = map.getPixelFromCoordinate(closestSegment[1]); - squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1); - squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2); - dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); - snappedToVertex = dist <= this.pixelTolerance_; - if (snappedToVertex) { - vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; - vertexPixel = map.getPixelFromCoordinate(vertex); + if (coordinateDistance(pixel, vertexPixel) <= this.pixelTolerance_) { + snapped = true; + if (this.vertex_ && !isCircle) { + pixel1 = map.getPixelFromCoordinate(closestSegment[0]); + pixel2 = map.getPixelFromCoordinate(closestSegment[1]); + squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1); + squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2); + dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); + snappedToVertex = dist <= this.pixelTolerance_; + if (snappedToVertex) { + vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; + vertexPixel = map.getPixelFromCoordinate(vertex); + } } } } + if (snapped) { + vertexPixel = [Math.round(vertexPixel[0]), Math.round(vertexPixel[1])]; + } } - if (snapped) { - vertexPixel = [Math.round(vertexPixel[0]), Math.round(vertexPixel[1])]; - } + return ( + /** @type {module:ol/interaction/Snap~Result} */ ({ + snapped: snapped, + vertex: vertex, + vertexPixel: vertexPixel + }) + ); } - return ( - /** @type {module:ol/interaction/Snap~Result} */ ({ - snapped: snapped, - vertex: vertex, - vertexPixel: vertexPixel - }) - ); -}; - -/** - * @param {module:ol/Feature} feature Feature - * @private - */ -Snap.prototype.updateFeature_ = function(feature) { - this.removeFeature(feature, false); - this.addFeature(feature, false); -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/Circle} geometry Geometry. - * @private - */ -Snap.prototype.writeCircleGeometry_ = function(feature, geometry) { - const polygon = fromCircle(geometry); - const coordinates = polygon.getCoordinates()[0]; - for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { - const segment = coordinates.slice(i, i + 2); - const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ - feature: feature, - segment: segment - }); - this.rBush_.insert(boundingExtent(segment), segmentData); + /** + * @param {module:ol/Feature} feature Feature + * @private + */ + updateFeature_(feature) { + this.removeFeature(feature, false); + this.addFeature(feature, false); } -}; - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/GeometryCollection} geometry Geometry. - * @private - */ -Snap.prototype.writeGeometryCollectionGeometry_ = function(feature, geometry) { - const geometries = geometry.getGeometriesArray(); - for (let i = 0; i < geometries.length; ++i) { - const segmentWriter = this.SEGMENT_WRITERS_[geometries[i].getType()]; - if (segmentWriter) { - segmentWriter.call(this, feature, geometries[i]); - } - } -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/LineString} geometry Geometry. - * @private - */ -Snap.prototype.writeLineStringGeometry_ = function(feature, geometry) { - const coordinates = geometry.getCoordinates(); - for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { - const segment = coordinates.slice(i, i + 2); - const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ - feature: feature, - segment: segment - }); - this.rBush_.insert(boundingExtent(segment), segmentData); - } -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/MultiLineString} geometry Geometry. - * @private - */ -Snap.prototype.writeMultiLineStringGeometry_ = function(feature, geometry) { - const lines = geometry.getCoordinates(); - for (let j = 0, jj = lines.length; j < jj; ++j) { - const coordinates = lines[j]; + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/Circle} geometry Geometry. + * @private + */ + writeCircleGeometry_(feature, geometry) { + const polygon = fromCircle(geometry); + const coordinates = polygon.getCoordinates()[0]; for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { const segment = coordinates.slice(i, i + 2); const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ @@ -530,36 +457,120 @@ Snap.prototype.writeMultiLineStringGeometry_ = function(feature, geometry) { this.rBush_.insert(boundingExtent(segment), segmentData); } } -}; + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/GeometryCollection} geometry Geometry. + * @private + */ + writeGeometryCollectionGeometry_(feature, geometry) { + const geometries = geometry.getGeometriesArray(); + for (let i = 0; i < geometries.length; ++i) { + const segmentWriter = this.SEGMENT_WRITERS_[geometries[i].getType()]; + if (segmentWriter) { + segmentWriter.call(this, feature, geometries[i]); + } + } + } -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/MultiPoint} geometry Geometry. - * @private - */ -Snap.prototype.writeMultiPointGeometry_ = function(feature, geometry) { - const points = geometry.getCoordinates(); - for (let i = 0, ii = points.length; i < ii; ++i) { - const coordinates = points[i]; + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/LineString} geometry Geometry. + * @private + */ + writeLineStringGeometry_(feature, geometry) { + const coordinates = geometry.getCoordinates(); + for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { + const segment = coordinates.slice(i, i + 2); + const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ + feature: feature, + segment: segment + }); + this.rBush_.insert(boundingExtent(segment), segmentData); + } + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/MultiLineString} geometry Geometry. + * @private + */ + writeMultiLineStringGeometry_(feature, geometry) { + const lines = geometry.getCoordinates(); + for (let j = 0, jj = lines.length; j < jj; ++j) { + const coordinates = lines[j]; + for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { + const segment = coordinates.slice(i, i + 2); + const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ + feature: feature, + segment: segment + }); + this.rBush_.insert(boundingExtent(segment), segmentData); + } + } + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/MultiPoint} geometry Geometry. + * @private + */ + writeMultiPointGeometry_(feature, geometry) { + const points = geometry.getCoordinates(); + for (let i = 0, ii = points.length; i < ii; ++i) { + const coordinates = points[i]; + const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ + feature: feature, + segment: [coordinates, coordinates] + }); + this.rBush_.insert(geometry.getExtent(), segmentData); + } + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/MultiPolygon} geometry Geometry. + * @private + */ + writeMultiPolygonGeometry_(feature, geometry) { + const polygons = geometry.getCoordinates(); + for (let k = 0, kk = polygons.length; k < kk; ++k) { + const rings = polygons[k]; + for (let j = 0, jj = rings.length; j < jj; ++j) { + const coordinates = rings[j]; + for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { + const segment = coordinates.slice(i, i + 2); + const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ + feature: feature, + segment: segment + }); + this.rBush_.insert(boundingExtent(segment), segmentData); + } + } + } + } + + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/Point} geometry Geometry. + * @private + */ + writePointGeometry_(feature, geometry) { + const coordinates = geometry.getCoordinates(); const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ feature: feature, segment: [coordinates, coordinates] }); this.rBush_.insert(geometry.getExtent(), segmentData); } -}; - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/MultiPolygon} geometry Geometry. - * @private - */ -Snap.prototype.writeMultiPolygonGeometry_ = function(feature, geometry) { - const polygons = geometry.getCoordinates(); - for (let k = 0, kk = polygons.length; k < kk; ++k) { - const rings = polygons[k]; + /** + * @param {module:ol/Feature} feature Feature + * @param {module:ol/geom/Polygon} geometry Geometry. + * @private + */ + writePolygonGeometry_(feature, geometry) { + const rings = geometry.getCoordinates(); for (let j = 0, jj = rings.length; j < jj; ++j) { const coordinates = rings[j]; for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { @@ -572,43 +583,15 @@ Snap.prototype.writeMultiPolygonGeometry_ = function(feature, geometry) { } } } -}; +} + +inherits(Snap, PointerInteraction); /** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/Point} geometry Geometry. - * @private + * @inheritDoc */ -Snap.prototype.writePointGeometry_ = function(feature, geometry) { - const coordinates = geometry.getCoordinates(); - const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ - feature: feature, - segment: [coordinates, coordinates] - }); - this.rBush_.insert(geometry.getExtent(), segmentData); -}; - - -/** - * @param {module:ol/Feature} feature Feature - * @param {module:ol/geom/Polygon} geometry Geometry. - * @private - */ -Snap.prototype.writePolygonGeometry_ = function(feature, geometry) { - const rings = geometry.getCoordinates(); - for (let j = 0, jj = rings.length; j < jj; ++j) { - const coordinates = rings[j]; - for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { - const segment = coordinates.slice(i, i + 2); - const segmentData = /** @type {module:ol/interaction/Snap~SegmentData} */ ({ - feature: feature, - segment: segment - }); - this.rBush_.insert(boundingExtent(segment), segmentData); - } - } -}; +Snap.prototype.shouldStopEvent = FALSE; /** diff --git a/src/ol/interaction/Translate.js b/src/ol/interaction/Translate.js index 2e0c71a09f..31ab39650e 100644 --- a/src/ol/interaction/Translate.js +++ b/src/ol/interaction/Translate.js @@ -96,68 +96,143 @@ inherits(TranslateEvent, Event); * @param {module:ol/interaction/Translate~Options=} opt_options Options. * @api */ -const Translate = function(opt_options) { - PointerInteraction.call(this, { - handleDownEvent: handleDownEvent, - handleDragEvent: handleDragEvent, - handleMoveEvent: handleMoveEvent, - handleUpEvent: handleUpEvent - }); +class Translate { + constructor(opt_options) { + PointerInteraction.call(this, { + handleDownEvent: handleDownEvent, + handleDragEvent: handleDragEvent, + handleMoveEvent: handleMoveEvent, + handleUpEvent: handleUpEvent + }); - const options = opt_options ? opt_options : {}; + const options = opt_options ? opt_options : {}; - /** - * The last position we translated to. - * @type {module:ol/coordinate~Coordinate} - * @private - */ - this.lastCoordinate_ = null; + /** + * The last position we translated to. + * @type {module:ol/coordinate~Coordinate} + * @private + */ + this.lastCoordinate_ = null; - /** - * @type {module:ol/Collection.} - * @private - */ - this.features_ = options.features !== undefined ? options.features : null; + /** + * @type {module:ol/Collection.} + * @private + */ + this.features_ = options.features !== undefined ? options.features : null; - /** @type {function(module:ol/layer/Layer): boolean} */ - let layerFilter; - if (options.layers) { - if (typeof options.layers === 'function') { - layerFilter = options.layers; + /** @type {function(module:ol/layer/Layer): boolean} */ + let layerFilter; + if (options.layers) { + if (typeof options.layers === 'function') { + layerFilter = options.layers; + } else { + const layers = options.layers; + layerFilter = function(layer) { + return includes(layers, layer); + }; + } } else { - const layers = options.layers; - layerFilter = function(layer) { - return includes(layers, layer); - }; + layerFilter = TRUE; } - } else { - layerFilter = TRUE; + + /** + * @private + * @type {function(module:ol/layer/Layer): boolean} + */ + this.layerFilter_ = layerFilter; + + /** + * @private + * @type {number} + */ + this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0; + + /** + * @type {module:ol/Feature} + * @private + */ + this.lastFeature_ = null; + + listen(this, + getChangeEventType(InteractionProperty.ACTIVE), + this.handleActiveChanged_, this); + + } + + /** + * Tests to see if the given coordinates intersects any of our selected + * features. + * @param {module:ol~Pixel} pixel Pixel coordinate to test for intersection. + * @param {module:ol/PluggableMap} map Map to test the intersection on. + * @return {module:ol/Feature} Returns the feature found at the specified pixel + * coordinates. + * @private + */ + featuresAtPixel_(pixel, map) { + return map.forEachFeatureAtPixel(pixel, + function(feature) { + if (!this.features_ || includes(this.features_.getArray(), feature)) { + return feature; + } + }.bind(this), { + layerFilter: this.layerFilter_, + hitTolerance: this.hitTolerance_ + }); + } + + /** + * Returns the Hit-detection tolerance. + * @returns {number} Hit tolerance in pixels. + * @api + */ + getHitTolerance() { + return this.hitTolerance_; + } + + /** + * Hit-detection tolerance. Pixels inside the radius around the given position + * will be checked for features. This only works for the canvas renderer and + * not for WebGL. + * @param {number} hitTolerance Hit tolerance in pixels. + * @api + */ + setHitTolerance(hitTolerance) { + this.hitTolerance_ = hitTolerance; + } + + /** + * @inheritDoc + */ + setMap(map) { + const oldMap = this.getMap(); + PointerInteraction.prototype.setMap.call(this, map); + this.updateState_(oldMap); } /** * @private - * @type {function(module:ol/layer/Layer): boolean} */ - this.layerFilter_ = layerFilter; + handleActiveChanged_() { + this.updateState_(null); + } /** - * @private - * @type {number} - */ - this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0; - - /** - * @type {module:ol/Feature} + * @param {module:ol/PluggableMap} oldMap Old map. * @private */ - this.lastFeature_ = null; - - listen(this, - getChangeEventType(InteractionProperty.ACTIVE), - this.handleActiveChanged_, this); - -}; + updateState_(oldMap) { + let map = this.getMap(); + const active = this.getActive(); + if (!map || !active) { + map = map || oldMap; + if (map) { + const elem = map.getViewport(); + elem.classList.remove('ol-grab', 'ol-grabbing'); + } + } + } +} inherits(Translate, PointerInteraction); @@ -252,83 +327,4 @@ function handleMoveEvent(event) { } -/** - * Tests to see if the given coordinates intersects any of our selected - * features. - * @param {module:ol~Pixel} pixel Pixel coordinate to test for intersection. - * @param {module:ol/PluggableMap} map Map to test the intersection on. - * @return {module:ol/Feature} Returns the feature found at the specified pixel - * coordinates. - * @private - */ -Translate.prototype.featuresAtPixel_ = function(pixel, map) { - return map.forEachFeatureAtPixel(pixel, - function(feature) { - if (!this.features_ || includes(this.features_.getArray(), feature)) { - return feature; - } - }.bind(this), { - layerFilter: this.layerFilter_, - hitTolerance: this.hitTolerance_ - }); -}; - - -/** - * Returns the Hit-detection tolerance. - * @returns {number} Hit tolerance in pixels. - * @api - */ -Translate.prototype.getHitTolerance = function() { - return this.hitTolerance_; -}; - - -/** - * Hit-detection tolerance. Pixels inside the radius around the given position - * will be checked for features. This only works for the canvas renderer and - * not for WebGL. - * @param {number} hitTolerance Hit tolerance in pixels. - * @api - */ -Translate.prototype.setHitTolerance = function(hitTolerance) { - this.hitTolerance_ = hitTolerance; -}; - - -/** - * @inheritDoc - */ -Translate.prototype.setMap = function(map) { - const oldMap = this.getMap(); - PointerInteraction.prototype.setMap.call(this, map); - this.updateState_(oldMap); -}; - - -/** - * @private - */ -Translate.prototype.handleActiveChanged_ = function() { - this.updateState_(null); -}; - - -/** - * @param {module:ol/PluggableMap} oldMap Old map. - * @private - */ -Translate.prototype.updateState_ = function(oldMap) { - let map = this.getMap(); - const active = this.getActive(); - if (!map || !active) { - map = map || oldMap; - if (map) { - const elem = map.getViewport(); - elem.classList.remove('ol-grab', 'ol-grabbing'); - } - } -}; - - export default Translate; diff --git a/src/ol/layer/Base.js b/src/ol/layer/Base.js index 71c414b432..0218288a4f 100644 --- a/src/ol/layer/Base.js +++ b/src/ol/layer/Base.js @@ -37,232 +37,219 @@ import {assign} from '../obj.js'; * @param {module:ol/layer/Base~Options} options Layer options. * @api */ -const BaseLayer = function(options) { +class BaseLayer { + constructor(options) { - BaseObject.call(this); + BaseObject.call(this); - /** - * @type {Object.} - */ - const properties = assign({}, options); - properties[LayerProperty.OPACITY] = - options.opacity !== undefined ? options.opacity : 1; - properties[LayerProperty.VISIBLE] = - options.visible !== undefined ? options.visible : true; - properties[LayerProperty.Z_INDEX] = - options.zIndex !== undefined ? options.zIndex : 0; - properties[LayerProperty.MAX_RESOLUTION] = - options.maxResolution !== undefined ? options.maxResolution : Infinity; - properties[LayerProperty.MIN_RESOLUTION] = - options.minResolution !== undefined ? options.minResolution : 0; + /** + * @type {Object.} + */ + const properties = assign({}, options); + properties[LayerProperty.OPACITY] = + options.opacity !== undefined ? options.opacity : 1; + properties[LayerProperty.VISIBLE] = + options.visible !== undefined ? options.visible : true; + properties[LayerProperty.Z_INDEX] = + options.zIndex !== undefined ? options.zIndex : 0; + properties[LayerProperty.MAX_RESOLUTION] = + options.maxResolution !== undefined ? options.maxResolution : Infinity; + properties[LayerProperty.MIN_RESOLUTION] = + options.minResolution !== undefined ? options.minResolution : 0; - this.setProperties(properties); + this.setProperties(properties); - /** - * @type {module:ol/layer/Layer~State} - * @private - */ - this.state_ = /** @type {module:ol/layer/Layer~State} */ ({ - layer: /** @type {module:ol/layer/Layer} */ (this), - managed: true - }); + /** + * @type {module:ol/layer/Layer~State} + * @private + */ + this.state_ = /** @type {module:ol/layer/Layer~State} */ ({ + layer: /** @type {module:ol/layer/Layer} */ (this), + managed: true + }); - /** - * The layer type. - * @type {module:ol/LayerType} - * @protected; - */ - this.type; + /** + * The layer type. + * @type {module:ol/LayerType} + * @protected; + */ + this.type; -}; + } + + /** + * Get the layer type (used when creating a layer renderer). + * @return {module:ol/LayerType} The layer type. + */ + getType() { + return this.type; + } + + /** + * @return {module:ol/layer/Layer~State} Layer state. + */ + getLayerState() { + this.state_.opacity = clamp(this.getOpacity(), 0, 1); + this.state_.sourceState = this.getSourceState(); + this.state_.visible = this.getVisible(); + this.state_.extent = this.getExtent(); + this.state_.zIndex = this.getZIndex(); + this.state_.maxResolution = this.getMaxResolution(); + this.state_.minResolution = Math.max(this.getMinResolution(), 0); + + return this.state_; + } + + /** + * @abstract + * @param {Array.=} opt_array Array of layers (to be + * modified in place). + * @return {Array.} Array of layers. + */ + getLayersArray(opt_array) {} + + /** + * @abstract + * @param {Array.=} opt_states Optional list of layer + * states (to be modified in place). + * @return {Array.} List of layer states. + */ + getLayerStatesArray(opt_states) {} + + /** + * Return the {@link module:ol/extent~Extent extent} of the layer or `undefined` if it + * will be visible regardless of extent. + * @return {module:ol/extent~Extent|undefined} The layer extent. + * @observable + * @api + */ + getExtent() { + return ( + /** @type {module:ol/extent~Extent|undefined} */ (this.get(LayerProperty.EXTENT)) + ); + } + + /** + * Return the maximum resolution of the layer. + * @return {number} The maximum resolution of the layer. + * @observable + * @api + */ + getMaxResolution() { + return /** @type {number} */ (this.get(LayerProperty.MAX_RESOLUTION)); + } + + /** + * Return the minimum resolution of the layer. + * @return {number} The minimum resolution of the layer. + * @observable + * @api + */ + getMinResolution() { + return /** @type {number} */ (this.get(LayerProperty.MIN_RESOLUTION)); + } + + /** + * Return the opacity of the layer (between 0 and 1). + * @return {number} The opacity of the layer. + * @observable + * @api + */ + getOpacity() { + return /** @type {number} */ (this.get(LayerProperty.OPACITY)); + } + + /** + * @abstract + * @return {module:ol/source/State} Source state. + */ + getSourceState() {} + + /** + * Return the visibility of the layer (`true` or `false`). + * @return {boolean} The visibility of the layer. + * @observable + * @api + */ + getVisible() { + return /** @type {boolean} */ (this.get(LayerProperty.VISIBLE)); + } + + /** + * Return the Z-index of the layer, which is used to order layers before + * rendering. The default Z-index is 0. + * @return {number} The Z-index of the layer. + * @observable + * @api + */ + getZIndex() { + return /** @type {number} */ (this.get(LayerProperty.Z_INDEX)); + } + + /** + * Set the extent at which the layer is visible. If `undefined`, the layer + * will be visible at all extents. + * @param {module:ol/extent~Extent|undefined} extent The extent of the layer. + * @observable + * @api + */ + setExtent(extent) { + this.set(LayerProperty.EXTENT, extent); + } + + /** + * Set the maximum resolution at which the layer is visible. + * @param {number} maxResolution The maximum resolution of the layer. + * @observable + * @api + */ + setMaxResolution(maxResolution) { + this.set(LayerProperty.MAX_RESOLUTION, maxResolution); + } + + /** + * Set the minimum resolution at which the layer is visible. + * @param {number} minResolution The minimum resolution of the layer. + * @observable + * @api + */ + setMinResolution(minResolution) { + this.set(LayerProperty.MIN_RESOLUTION, minResolution); + } + + /** + * Set the opacity of the layer, allowed values range from 0 to 1. + * @param {number} opacity The opacity of the layer. + * @observable + * @api + */ + setOpacity(opacity) { + this.set(LayerProperty.OPACITY, opacity); + } + + /** + * Set the visibility of the layer (`true` or `false`). + * @param {boolean} visible The visibility of the layer. + * @observable + * @api + */ + setVisible(visible) { + this.set(LayerProperty.VISIBLE, visible); + } + + /** + * Set Z-index of the layer, which is used to order layers before rendering. + * The default Z-index is 0. + * @param {number} zindex The z-index of the layer. + * @observable + * @api + */ + setZIndex(zindex) { + this.set(LayerProperty.Z_INDEX, zindex); + } +} inherits(BaseLayer, BaseObject); -/** - * Get the layer type (used when creating a layer renderer). - * @return {module:ol/LayerType} The layer type. - */ -BaseLayer.prototype.getType = function() { - return this.type; -}; - - -/** - * @return {module:ol/layer/Layer~State} Layer state. - */ -BaseLayer.prototype.getLayerState = function() { - this.state_.opacity = clamp(this.getOpacity(), 0, 1); - this.state_.sourceState = this.getSourceState(); - this.state_.visible = this.getVisible(); - this.state_.extent = this.getExtent(); - this.state_.zIndex = this.getZIndex(); - this.state_.maxResolution = this.getMaxResolution(); - this.state_.minResolution = Math.max(this.getMinResolution(), 0); - - return this.state_; -}; - - -/** - * @abstract - * @param {Array.=} opt_array Array of layers (to be - * modified in place). - * @return {Array.} Array of layers. - */ -BaseLayer.prototype.getLayersArray = function(opt_array) {}; - - -/** - * @abstract - * @param {Array.=} opt_states Optional list of layer - * states (to be modified in place). - * @return {Array.} List of layer states. - */ -BaseLayer.prototype.getLayerStatesArray = function(opt_states) {}; - - -/** - * Return the {@link module:ol/extent~Extent extent} of the layer or `undefined` if it - * will be visible regardless of extent. - * @return {module:ol/extent~Extent|undefined} The layer extent. - * @observable - * @api - */ -BaseLayer.prototype.getExtent = function() { - return ( - /** @type {module:ol/extent~Extent|undefined} */ (this.get(LayerProperty.EXTENT)) - ); -}; - - -/** - * Return the maximum resolution of the layer. - * @return {number} The maximum resolution of the layer. - * @observable - * @api - */ -BaseLayer.prototype.getMaxResolution = function() { - return /** @type {number} */ (this.get(LayerProperty.MAX_RESOLUTION)); -}; - - -/** - * Return the minimum resolution of the layer. - * @return {number} The minimum resolution of the layer. - * @observable - * @api - */ -BaseLayer.prototype.getMinResolution = function() { - return /** @type {number} */ (this.get(LayerProperty.MIN_RESOLUTION)); -}; - - -/** - * Return the opacity of the layer (between 0 and 1). - * @return {number} The opacity of the layer. - * @observable - * @api - */ -BaseLayer.prototype.getOpacity = function() { - return /** @type {number} */ (this.get(LayerProperty.OPACITY)); -}; - - -/** - * @abstract - * @return {module:ol/source/State} Source state. - */ -BaseLayer.prototype.getSourceState = function() {}; - - -/** - * Return the visibility of the layer (`true` or `false`). - * @return {boolean} The visibility of the layer. - * @observable - * @api - */ -BaseLayer.prototype.getVisible = function() { - return /** @type {boolean} */ (this.get(LayerProperty.VISIBLE)); -}; - - -/** - * Return the Z-index of the layer, which is used to order layers before - * rendering. The default Z-index is 0. - * @return {number} The Z-index of the layer. - * @observable - * @api - */ -BaseLayer.prototype.getZIndex = function() { - return /** @type {number} */ (this.get(LayerProperty.Z_INDEX)); -}; - - -/** - * Set the extent at which the layer is visible. If `undefined`, the layer - * will be visible at all extents. - * @param {module:ol/extent~Extent|undefined} extent The extent of the layer. - * @observable - * @api - */ -BaseLayer.prototype.setExtent = function(extent) { - this.set(LayerProperty.EXTENT, extent); -}; - - -/** - * Set the maximum resolution at which the layer is visible. - * @param {number} maxResolution The maximum resolution of the layer. - * @observable - * @api - */ -BaseLayer.prototype.setMaxResolution = function(maxResolution) { - this.set(LayerProperty.MAX_RESOLUTION, maxResolution); -}; - - -/** - * Set the minimum resolution at which the layer is visible. - * @param {number} minResolution The minimum resolution of the layer. - * @observable - * @api - */ -BaseLayer.prototype.setMinResolution = function(minResolution) { - this.set(LayerProperty.MIN_RESOLUTION, minResolution); -}; - - -/** - * Set the opacity of the layer, allowed values range from 0 to 1. - * @param {number} opacity The opacity of the layer. - * @observable - * @api - */ -BaseLayer.prototype.setOpacity = function(opacity) { - this.set(LayerProperty.OPACITY, opacity); -}; - - -/** - * Set the visibility of the layer (`true` or `false`). - * @param {boolean} visible The visibility of the layer. - * @observable - * @api - */ -BaseLayer.prototype.setVisible = function(visible) { - this.set(LayerProperty.VISIBLE, visible); -}; - - -/** - * Set Z-index of the layer, which is used to order layers before rendering. - * The default Z-index is 0. - * @param {number} zindex The z-index of the layer. - * @observable - * @api - */ -BaseLayer.prototype.setZIndex = function(zindex) { - this.set(LayerProperty.Z_INDEX, zindex); -}; export default BaseLayer; diff --git a/src/ol/layer/Group.js b/src/ol/layer/Group.js index bb39810349..411c078d61 100644 --- a/src/ol/layer/Group.js +++ b/src/ol/layer/Group.js @@ -51,198 +51,192 @@ const Property = { * @param {module:ol/layer/Group~Options=} opt_options Layer options. * @api */ -const LayerGroup = function(opt_options) { +class LayerGroup { + constructor(opt_options) { - const options = opt_options || {}; - const baseOptions = /** @type {module:ol/layer/Group~Options} */ (assign({}, options)); - delete baseOptions.layers; + const options = opt_options || {}; + const baseOptions = /** @type {module:ol/layer/Group~Options} */ (assign({}, options)); + delete baseOptions.layers; - let layers = options.layers; + let layers = options.layers; - BaseLayer.call(this, baseOptions); + BaseLayer.call(this, baseOptions); - /** - * @private - * @type {Array.} - */ - this.layersListenerKeys_ = []; + /** + * @private + * @type {Array.} + */ + this.layersListenerKeys_ = []; - /** - * @private - * @type {Object.>} - */ - this.listenerKeys_ = {}; + /** + * @private + * @type {Object.>} + */ + this.listenerKeys_ = {}; - listen(this, - getChangeEventType(Property.LAYERS), - this.handleLayersChanged_, this); + listen(this, + getChangeEventType(Property.LAYERS), + this.handleLayersChanged_, this); - if (layers) { - if (Array.isArray(layers)) { - layers = new Collection(layers.slice(), {unique: true}); + if (layers) { + if (Array.isArray(layers)) { + layers = new Collection(layers.slice(), {unique: true}); + } else { + assert(layers instanceof Collection, + 43); // Expected `layers` to be an array or a `Collection` + layers = layers; + } } else { - assert(layers instanceof Collection, - 43); // Expected `layers` to be an array or a `Collection` - layers = layers; + layers = new Collection(undefined, {unique: true}); } - } else { - layers = new Collection(undefined, {unique: true}); + + this.setLayers(layers); + } - this.setLayers(layers); + /** + * @private + */ + handleLayerChange_() { + this.changed(); + } -}; + /** + * @param {module:ol/events/Event} event Event. + * @private + */ + handleLayersChanged_(event) { + this.layersListenerKeys_.forEach(unlistenByKey); + this.layersListenerKeys_.length = 0; + + const layers = this.getLayers(); + this.layersListenerKeys_.push( + listen(layers, CollectionEventType.ADD, this.handleLayersAdd_, this), + listen(layers, CollectionEventType.REMOVE, this.handleLayersRemove_, this) + ); + + for (const id in this.listenerKeys_) { + this.listenerKeys_[id].forEach(unlistenByKey); + } + clear(this.listenerKeys_); + + const layersArray = layers.getArray(); + for (let i = 0, ii = layersArray.length; i < ii; i++) { + const layer = layersArray[i]; + this.listenerKeys_[getUid(layer).toString()] = [ + listen(layer, ObjectEventType.PROPERTYCHANGE, this.handleLayerChange_, this), + listen(layer, EventType.CHANGE, this.handleLayerChange_, this) + ]; + } + + this.changed(); + } + + /** + * @param {module:ol/Collection~CollectionEvent} collectionEvent CollectionEvent. + * @private + */ + handleLayersAdd_(collectionEvent) { + const layer = /** @type {module:ol/layer/Base} */ (collectionEvent.element); + const key = getUid(layer).toString(); + this.listenerKeys_[key] = [ + listen(layer, ObjectEventType.PROPERTYCHANGE, this.handleLayerChange_, this), + listen(layer, EventType.CHANGE, this.handleLayerChange_, this) + ]; + this.changed(); + } + + /** + * @param {module:ol/Collection~CollectionEvent} collectionEvent CollectionEvent. + * @private + */ + handleLayersRemove_(collectionEvent) { + const layer = /** @type {module:ol/layer/Base} */ (collectionEvent.element); + const key = getUid(layer).toString(); + this.listenerKeys_[key].forEach(unlistenByKey); + delete this.listenerKeys_[key]; + this.changed(); + } + + /** + * Returns the {@link module:ol/Collection collection} of {@link module:ol/layer/Layer~Layer layers} + * in this group. + * @return {!module:ol/Collection.} Collection of + * {@link module:ol/layer/Base layers} that are part of this group. + * @observable + * @api + */ + getLayers() { + return ( + /** @type {!module:ol/Collection.} */ (this.get(Property.LAYERS)) + ); + } + + /** + * Set the {@link module:ol/Collection collection} of {@link module:ol/layer/Layer~Layer layers} + * in this group. + * @param {!module:ol/Collection.} layers Collection of + * {@link module:ol/layer/Base layers} that are part of this group. + * @observable + * @api + */ + setLayers(layers) { + this.set(Property.LAYERS, layers); + } + + /** + * @inheritDoc + */ + getLayersArray(opt_array) { + const array = opt_array !== undefined ? opt_array : []; + this.getLayers().forEach(function(layer) { + layer.getLayersArray(array); + }); + return array; + } + + /** + * @inheritDoc + */ + getLayerStatesArray(opt_states) { + const states = opt_states !== undefined ? opt_states : []; + + const pos = states.length; + + this.getLayers().forEach(function(layer) { + layer.getLayerStatesArray(states); + }); + + const ownLayerState = this.getLayerState(); + for (let i = pos, ii = states.length; i < ii; i++) { + const layerState = states[i]; + layerState.opacity *= ownLayerState.opacity; + layerState.visible = layerState.visible && ownLayerState.visible; + layerState.maxResolution = Math.min( + layerState.maxResolution, ownLayerState.maxResolution); + layerState.minResolution = Math.max( + layerState.minResolution, ownLayerState.minResolution); + if (ownLayerState.extent !== undefined) { + if (layerState.extent !== undefined) { + layerState.extent = getIntersection(layerState.extent, ownLayerState.extent); + } else { + layerState.extent = ownLayerState.extent; + } + } + } + + return states; + } + + /** + * @inheritDoc + */ + getSourceState() { + return SourceState.READY; + } +} inherits(LayerGroup, BaseLayer); -/** - * @private - */ -LayerGroup.prototype.handleLayerChange_ = function() { - this.changed(); -}; - - -/** - * @param {module:ol/events/Event} event Event. - * @private - */ -LayerGroup.prototype.handleLayersChanged_ = function(event) { - this.layersListenerKeys_.forEach(unlistenByKey); - this.layersListenerKeys_.length = 0; - - const layers = this.getLayers(); - this.layersListenerKeys_.push( - listen(layers, CollectionEventType.ADD, this.handleLayersAdd_, this), - listen(layers, CollectionEventType.REMOVE, this.handleLayersRemove_, this) - ); - - for (const id in this.listenerKeys_) { - this.listenerKeys_[id].forEach(unlistenByKey); - } - clear(this.listenerKeys_); - - const layersArray = layers.getArray(); - for (let i = 0, ii = layersArray.length; i < ii; i++) { - const layer = layersArray[i]; - this.listenerKeys_[getUid(layer).toString()] = [ - listen(layer, ObjectEventType.PROPERTYCHANGE, this.handleLayerChange_, this), - listen(layer, EventType.CHANGE, this.handleLayerChange_, this) - ]; - } - - this.changed(); -}; - - -/** - * @param {module:ol/Collection~CollectionEvent} collectionEvent CollectionEvent. - * @private - */ -LayerGroup.prototype.handleLayersAdd_ = function(collectionEvent) { - const layer = /** @type {module:ol/layer/Base} */ (collectionEvent.element); - const key = getUid(layer).toString(); - this.listenerKeys_[key] = [ - listen(layer, ObjectEventType.PROPERTYCHANGE, this.handleLayerChange_, this), - listen(layer, EventType.CHANGE, this.handleLayerChange_, this) - ]; - this.changed(); -}; - - -/** - * @param {module:ol/Collection~CollectionEvent} collectionEvent CollectionEvent. - * @private - */ -LayerGroup.prototype.handleLayersRemove_ = function(collectionEvent) { - const layer = /** @type {module:ol/layer/Base} */ (collectionEvent.element); - const key = getUid(layer).toString(); - this.listenerKeys_[key].forEach(unlistenByKey); - delete this.listenerKeys_[key]; - this.changed(); -}; - - -/** - * Returns the {@link module:ol/Collection collection} of {@link module:ol/layer/Layer~Layer layers} - * in this group. - * @return {!module:ol/Collection.} Collection of - * {@link module:ol/layer/Base layers} that are part of this group. - * @observable - * @api - */ -LayerGroup.prototype.getLayers = function() { - return ( - /** @type {!module:ol/Collection.} */ (this.get(Property.LAYERS)) - ); -}; - - -/** - * Set the {@link module:ol/Collection collection} of {@link module:ol/layer/Layer~Layer layers} - * in this group. - * @param {!module:ol/Collection.} layers Collection of - * {@link module:ol/layer/Base layers} that are part of this group. - * @observable - * @api - */ -LayerGroup.prototype.setLayers = function(layers) { - this.set(Property.LAYERS, layers); -}; - - -/** - * @inheritDoc - */ -LayerGroup.prototype.getLayersArray = function(opt_array) { - const array = opt_array !== undefined ? opt_array : []; - this.getLayers().forEach(function(layer) { - layer.getLayersArray(array); - }); - return array; -}; - - -/** - * @inheritDoc - */ -LayerGroup.prototype.getLayerStatesArray = function(opt_states) { - const states = opt_states !== undefined ? opt_states : []; - - const pos = states.length; - - this.getLayers().forEach(function(layer) { - layer.getLayerStatesArray(states); - }); - - const ownLayerState = this.getLayerState(); - for (let i = pos, ii = states.length; i < ii; i++) { - const layerState = states[i]; - layerState.opacity *= ownLayerState.opacity; - layerState.visible = layerState.visible && ownLayerState.visible; - layerState.maxResolution = Math.min( - layerState.maxResolution, ownLayerState.maxResolution); - layerState.minResolution = Math.max( - layerState.minResolution, ownLayerState.minResolution); - if (ownLayerState.extent !== undefined) { - if (layerState.extent !== undefined) { - layerState.extent = getIntersection(layerState.extent, ownLayerState.extent); - } else { - layerState.extent = ownLayerState.extent; - } - } - } - - return states; -}; - - -/** - * @inheritDoc - */ -LayerGroup.prototype.getSourceState = function() { - return SourceState.READY; -}; - export default LayerGroup; diff --git a/src/ol/layer/Heatmap.js b/src/ol/layer/Heatmap.js index 50b02c1633..5eafc51eee 100644 --- a/src/ol/layer/Heatmap.js +++ b/src/ol/layer/Heatmap.js @@ -73,97 +73,215 @@ const DEFAULT_GRADIENT = ['#00f', '#0ff', '#0f0', '#ff0', '#f00']; * @param {module:ol/layer/Heatmap~Options=} opt_options Options. * @api */ -const Heatmap = function(opt_options) { - const options = opt_options ? opt_options : {}; +class Heatmap { + constructor(opt_options) { + const options = opt_options ? opt_options : {}; - const baseOptions = assign({}, options); + const baseOptions = assign({}, options); - delete baseOptions.gradient; - delete baseOptions.radius; - delete baseOptions.blur; - delete baseOptions.shadow; - delete baseOptions.weight; - VectorLayer.call(this, /** @type {module:ol/layer/Vector~Options} */ (baseOptions)); + delete baseOptions.gradient; + delete baseOptions.radius; + delete baseOptions.blur; + delete baseOptions.shadow; + delete baseOptions.weight; + VectorLayer.call(this, /** @type {module:ol/layer/Vector~Options} */ (baseOptions)); - /** - * @private - * @type {Uint8ClampedArray} - */ - this.gradient_ = null; + /** + * @private + * @type {Uint8ClampedArray} + */ + this.gradient_ = null; - /** - * @private - * @type {number} - */ - this.shadow_ = options.shadow !== undefined ? options.shadow : 250; + /** + * @private + * @type {number} + */ + this.shadow_ = options.shadow !== undefined ? options.shadow : 250; - /** - * @private - * @type {string|undefined} - */ - this.circleImage_ = undefined; + /** + * @private + * @type {string|undefined} + */ + this.circleImage_ = undefined; - /** - * @private - * @type {Array.>} - */ - this.styleCache_ = null; + /** + * @private + * @type {Array.>} + */ + this.styleCache_ = null; - listen(this, - getChangeEventType(Property.GRADIENT), - this.handleGradientChanged_, this); + listen(this, + getChangeEventType(Property.GRADIENT), + this.handleGradientChanged_, this); - this.setGradient(options.gradient ? options.gradient : DEFAULT_GRADIENT); + this.setGradient(options.gradient ? options.gradient : DEFAULT_GRADIENT); - this.setBlur(options.blur !== undefined ? options.blur : 15); + this.setBlur(options.blur !== undefined ? options.blur : 15); - this.setRadius(options.radius !== undefined ? options.radius : 8); + this.setRadius(options.radius !== undefined ? options.radius : 8); - listen(this, - getChangeEventType(Property.BLUR), - this.handleStyleChanged_, this); - listen(this, - getChangeEventType(Property.RADIUS), - this.handleStyleChanged_, this); + listen(this, + getChangeEventType(Property.BLUR), + this.handleStyleChanged_, this); + listen(this, + getChangeEventType(Property.RADIUS), + this.handleStyleChanged_, this); - this.handleStyleChanged_(); + this.handleStyleChanged_(); - const weight = options.weight ? options.weight : 'weight'; - let weightFunction; - if (typeof weight === 'string') { - weightFunction = function(feature) { - return feature.get(weight); - }; - } else { - weightFunction = weight; + const weight = options.weight ? options.weight : 'weight'; + let weightFunction; + if (typeof weight === 'string') { + weightFunction = function(feature) { + return feature.get(weight); + }; + } else { + weightFunction = weight; + } + + this.setStyle(function(feature, resolution) { + const weight = weightFunction(feature); + const opacity = weight !== undefined ? clamp(weight, 0, 1) : 1; + // cast to 8 bits + const index = (255 * opacity) | 0; + let style = this.styleCache_[index]; + if (!style) { + style = [ + new Style({ + image: new Icon({ + opacity: opacity, + src: this.circleImage_ + }) + }) + ]; + this.styleCache_[index] = style; + } + return style; + }.bind(this)); + + // For performance reasons, don't sort the features before rendering. + // The render order is not relevant for a heatmap representation. + this.setRenderOrder(null); + + listen(this, RenderEventType.RENDER, this.handleRender_, this); } - this.setStyle(function(feature, resolution) { - const weight = weightFunction(feature); - const opacity = weight !== undefined ? clamp(weight, 0, 1) : 1; - // cast to 8 bits - const index = (255 * opacity) | 0; - let style = this.styleCache_[index]; - if (!style) { - style = [ - new Style({ - image: new Icon({ - opacity: opacity, - src: this.circleImage_ - }) - }) - ]; - this.styleCache_[index] = style; + /** + * @return {string} Data URL for a circle. + * @private + */ + createCircle_() { + const radius = this.getRadius(); + const blur = this.getBlur(); + const halfSize = radius + blur + 1; + const size = 2 * halfSize; + const context = createCanvasContext2D(size, size); + context.shadowOffsetX = context.shadowOffsetY = this.shadow_; + context.shadowBlur = blur; + context.shadowColor = '#000'; + context.beginPath(); + const center = halfSize - this.shadow_; + context.arc(center, center, radius, 0, Math.PI * 2, true); + context.fill(); + return context.canvas.toDataURL(); + } + + /** + * Return the blur size in pixels. + * @return {number} Blur size in pixels. + * @api + * @observable + */ + getBlur() { + return /** @type {number} */ (this.get(Property.BLUR)); + } + + /** + * Return the gradient colors as array of strings. + * @return {Array.} Colors. + * @api + * @observable + */ + getGradient() { + return /** @type {Array.} */ (this.get(Property.GRADIENT)); + } + + /** + * Return the size of the radius in pixels. + * @return {number} Radius size in pixel. + * @api + * @observable + */ + getRadius() { + return /** @type {number} */ (this.get(Property.RADIUS)); + } + + /** + * @private + */ + handleGradientChanged_() { + this.gradient_ = createGradient(this.getGradient()); + } + + /** + * @private + */ + handleStyleChanged_() { + this.circleImage_ = this.createCircle_(); + this.styleCache_ = new Array(256); + this.changed(); + } + + /** + * @param {module:ol/render/Event} event Post compose event + * @private + */ + handleRender_(event) { + const context = event.context; + const canvas = context.canvas; + const image = context.getImageData(0, 0, canvas.width, canvas.height); + const view8 = image.data; + for (let i = 0, ii = view8.length; i < ii; i += 4) { + const alpha = view8[i + 3] * 4; + if (alpha) { + view8[i] = this.gradient_[alpha]; + view8[i + 1] = this.gradient_[alpha + 1]; + view8[i + 2] = this.gradient_[alpha + 2]; + } } - return style; - }.bind(this)); + context.putImageData(image, 0, 0); + } - // For performance reasons, don't sort the features before rendering. - // The render order is not relevant for a heatmap representation. - this.setRenderOrder(null); + /** + * Set the blur size in pixels. + * @param {number} blur Blur size in pixels. + * @api + * @observable + */ + setBlur(blur) { + this.set(Property.BLUR, blur); + } - listen(this, RenderEventType.RENDER, this.handleRender_, this); -}; + /** + * Set the gradient colors as array of strings. + * @param {Array.} colors Gradient. + * @api + * @observable + */ + setGradient(colors) { + this.set(Property.GRADIENT, colors); + } + + /** + * Set the size of the radius in pixels. + * @param {number} radius Radius size in pixel. + * @api + * @observable + */ + setRadius(radius) { + this.set(Property.RADIUS, radius); + } +} inherits(Heatmap, VectorLayer); @@ -191,129 +309,4 @@ const createGradient = function(colors) { }; -/** - * @return {string} Data URL for a circle. - * @private - */ -Heatmap.prototype.createCircle_ = function() { - const radius = this.getRadius(); - const blur = this.getBlur(); - const halfSize = radius + blur + 1; - const size = 2 * halfSize; - const context = createCanvasContext2D(size, size); - context.shadowOffsetX = context.shadowOffsetY = this.shadow_; - context.shadowBlur = blur; - context.shadowColor = '#000'; - context.beginPath(); - const center = halfSize - this.shadow_; - context.arc(center, center, radius, 0, Math.PI * 2, true); - context.fill(); - return context.canvas.toDataURL(); -}; - - -/** - * Return the blur size in pixels. - * @return {number} Blur size in pixels. - * @api - * @observable - */ -Heatmap.prototype.getBlur = function() { - return /** @type {number} */ (this.get(Property.BLUR)); -}; - - -/** - * Return the gradient colors as array of strings. - * @return {Array.} Colors. - * @api - * @observable - */ -Heatmap.prototype.getGradient = function() { - return /** @type {Array.} */ (this.get(Property.GRADIENT)); -}; - - -/** - * Return the size of the radius in pixels. - * @return {number} Radius size in pixel. - * @api - * @observable - */ -Heatmap.prototype.getRadius = function() { - return /** @type {number} */ (this.get(Property.RADIUS)); -}; - - -/** - * @private - */ -Heatmap.prototype.handleGradientChanged_ = function() { - this.gradient_ = createGradient(this.getGradient()); -}; - - -/** - * @private - */ -Heatmap.prototype.handleStyleChanged_ = function() { - this.circleImage_ = this.createCircle_(); - this.styleCache_ = new Array(256); - this.changed(); -}; - - -/** - * @param {module:ol/render/Event} event Post compose event - * @private - */ -Heatmap.prototype.handleRender_ = function(event) { - const context = event.context; - const canvas = context.canvas; - const image = context.getImageData(0, 0, canvas.width, canvas.height); - const view8 = image.data; - for (let i = 0, ii = view8.length; i < ii; i += 4) { - const alpha = view8[i + 3] * 4; - if (alpha) { - view8[i] = this.gradient_[alpha]; - view8[i + 1] = this.gradient_[alpha + 1]; - view8[i + 2] = this.gradient_[alpha + 2]; - } - } - context.putImageData(image, 0, 0); -}; - - -/** - * Set the blur size in pixels. - * @param {number} blur Blur size in pixels. - * @api - * @observable - */ -Heatmap.prototype.setBlur = function(blur) { - this.set(Property.BLUR, blur); -}; - - -/** - * Set the gradient colors as array of strings. - * @param {Array.} colors Gradient. - * @api - * @observable - */ -Heatmap.prototype.setGradient = function(colors) { - this.set(Property.GRADIENT, colors); -}; - - -/** - * Set the size of the radius in pixels. - * @param {number} radius Radius size in pixel. - * @api - * @observable - */ -Heatmap.prototype.setRadius = function(radius) { - this.set(Property.RADIUS, radius); -}; - export default Heatmap; diff --git a/src/ol/layer/Layer.js b/src/ol/layer/Layer.js index 8654a63550..d3a2a80aef 100644 --- a/src/ol/layer/Layer.js +++ b/src/ol/layer/Layer.js @@ -66,42 +66,153 @@ import SourceState from '../source/State.js'; * @param {module:ol/layer/Layer~Options} options Layer options. * @api */ -const Layer = function(options) { +class Layer { + constructor(options) { - const baseOptions = assign({}, options); - delete baseOptions.source; + const baseOptions = assign({}, options); + delete baseOptions.source; - BaseLayer.call(this, /** @type {module:ol/layer/Base~Options} */ (baseOptions)); + BaseLayer.call(this, /** @type {module:ol/layer/Base~Options} */ (baseOptions)); - /** - * @private - * @type {?module:ol/events~EventsKey} - */ - this.mapPrecomposeKey_ = null; + /** + * @private + * @type {?module:ol/events~EventsKey} + */ + this.mapPrecomposeKey_ = null; - /** - * @private - * @type {?module:ol/events~EventsKey} - */ - this.mapRenderKey_ = null; + /** + * @private + * @type {?module:ol/events~EventsKey} + */ + this.mapRenderKey_ = null; - /** - * @private - * @type {?module:ol/events~EventsKey} - */ - this.sourceChangeKey_ = null; + /** + * @private + * @type {?module:ol/events~EventsKey} + */ + this.sourceChangeKey_ = null; - if (options.map) { - this.setMap(options.map); + if (options.map) { + this.setMap(options.map); + } + + listen(this, + getChangeEventType(LayerProperty.SOURCE), + this.handleSourcePropertyChange_, this); + + const source = options.source ? options.source : null; + this.setSource(source); } - listen(this, - getChangeEventType(LayerProperty.SOURCE), - this.handleSourcePropertyChange_, this); + /** + * @inheritDoc + */ + getLayersArray(opt_array) { + const array = opt_array ? opt_array : []; + array.push(this); + return array; + } - const source = options.source ? options.source : null; - this.setSource(source); -}; + /** + * @inheritDoc + */ + getLayerStatesArray(opt_states) { + const states = opt_states ? opt_states : []; + states.push(this.getLayerState()); + return states; + } + + /** + * Get the layer source. + * @return {module:ol/source/Source} The layer source (or `null` if not yet set). + * @observable + * @api + */ + getSource() { + const source = this.get(LayerProperty.SOURCE); + return ( + /** @type {module:ol/source/Source} */ (source) || null + ); + } + + /** + * @inheritDoc + */ + getSourceState() { + const source = this.getSource(); + return !source ? SourceState.UNDEFINED : source.getState(); + } + + /** + * @private + */ + handleSourceChange_() { + this.changed(); + } + + /** + * @private + */ + handleSourcePropertyChange_() { + if (this.sourceChangeKey_) { + unlistenByKey(this.sourceChangeKey_); + this.sourceChangeKey_ = null; + } + const source = this.getSource(); + if (source) { + this.sourceChangeKey_ = listen(source, + EventType.CHANGE, this.handleSourceChange_, this); + } + this.changed(); + } + + /** + * 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 + * {@link module:ol/Map#forEachLayerAtPixel} will receive `null` as layer. This + * is useful for temporary layers. To remove an unmanaged layer from the map, + * use `#setMap(null)`. + * + * To add the layer to a map and have it managed by the map, use + * {@link module:ol/Map#addLayer} instead. + * @param {module:ol/PluggableMap} map Map. + * @api + */ + setMap(map) { + if (this.mapPrecomposeKey_) { + unlistenByKey(this.mapPrecomposeKey_); + this.mapPrecomposeKey_ = null; + } + if (!map) { + this.changed(); + } + if (this.mapRenderKey_) { + unlistenByKey(this.mapRenderKey_); + this.mapRenderKey_ = null; + } + if (map) { + this.mapPrecomposeKey_ = listen(map, RenderEventType.PRECOMPOSE, function(evt) { + const layerState = this.getLayerState(); + layerState.managed = false; + layerState.zIndex = Infinity; + evt.frameState.layerStatesArray.push(layerState); + evt.frameState.layerStates[getUid(this)] = layerState; + }, this); + this.mapRenderKey_ = listen(this, EventType.CHANGE, map.render, map); + this.changed(); + } + } + + /** + * Set the layer source. + * @param {module:ol/source/Source} source The layer source. + * @observable + * @api + */ + setSource(source) { + this.set(LayerProperty.SOURCE, source); + } +} inherits(Layer, BaseLayer); @@ -120,119 +231,4 @@ export function visibleAtResolution(layerState, resolution) { } -/** - * @inheritDoc - */ -Layer.prototype.getLayersArray = function(opt_array) { - const array = opt_array ? opt_array : []; - array.push(this); - return array; -}; - - -/** - * @inheritDoc - */ -Layer.prototype.getLayerStatesArray = function(opt_states) { - const states = opt_states ? opt_states : []; - states.push(this.getLayerState()); - return states; -}; - - -/** - * Get the layer source. - * @return {module:ol/source/Source} The layer source (or `null` if not yet set). - * @observable - * @api - */ -Layer.prototype.getSource = function() { - const source = this.get(LayerProperty.SOURCE); - return ( - /** @type {module:ol/source/Source} */ (source) || null - ); -}; - - -/** - * @inheritDoc - */ -Layer.prototype.getSourceState = function() { - const source = this.getSource(); - return !source ? SourceState.UNDEFINED : source.getState(); -}; - - -/** - * @private - */ -Layer.prototype.handleSourceChange_ = function() { - this.changed(); -}; - - -/** - * @private - */ -Layer.prototype.handleSourcePropertyChange_ = function() { - if (this.sourceChangeKey_) { - unlistenByKey(this.sourceChangeKey_); - this.sourceChangeKey_ = null; - } - const source = this.getSource(); - if (source) { - this.sourceChangeKey_ = listen(source, - EventType.CHANGE, this.handleSourceChange_, this); - } - this.changed(); -}; - - -/** - * 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 - * {@link module:ol/Map#forEachLayerAtPixel} will receive `null` as layer. This - * is useful for temporary layers. To remove an unmanaged layer from the map, - * use `#setMap(null)`. - * - * To add the layer to a map and have it managed by the map, use - * {@link module:ol/Map#addLayer} instead. - * @param {module:ol/PluggableMap} map Map. - * @api - */ -Layer.prototype.setMap = function(map) { - if (this.mapPrecomposeKey_) { - unlistenByKey(this.mapPrecomposeKey_); - this.mapPrecomposeKey_ = null; - } - if (!map) { - this.changed(); - } - if (this.mapRenderKey_) { - unlistenByKey(this.mapRenderKey_); - this.mapRenderKey_ = null; - } - if (map) { - this.mapPrecomposeKey_ = listen(map, RenderEventType.PRECOMPOSE, function(evt) { - const layerState = this.getLayerState(); - layerState.managed = false; - layerState.zIndex = Infinity; - evt.frameState.layerStatesArray.push(layerState); - evt.frameState.layerStates[getUid(this)] = layerState; - }, this); - this.mapRenderKey_ = listen(this, EventType.CHANGE, map.render, map); - this.changed(); - } -}; - - -/** - * Set the layer source. - * @param {module:ol/source/Source} source The layer source. - * @observable - * @api - */ -Layer.prototype.setSource = function(source) { - this.set(LayerProperty.SOURCE, source); -}; export default Layer; diff --git a/src/ol/layer/Tile.js b/src/ol/layer/Tile.js index fa0529039f..5cfa7a9935 100644 --- a/src/ol/layer/Tile.js +++ b/src/ol/layer/Tile.js @@ -45,42 +45,73 @@ import {assign} from '../obj.js'; * @param {module:ol/layer/Tile~Options=} opt_options Tile layer options. * @api */ -const TileLayer = function(opt_options) { - const options = opt_options ? opt_options : {}; +class TileLayer { + constructor(opt_options) { + const options = opt_options ? opt_options : {}; - const baseOptions = assign({}, options); + const baseOptions = assign({}, options); - delete baseOptions.preload; - delete baseOptions.useInterimTilesOnError; - Layer.call(this, /** @type {module:ol/layer/Layer~Options} */ (baseOptions)); + delete baseOptions.preload; + delete baseOptions.useInterimTilesOnError; + Layer.call(this, /** @type {module:ol/layer/Layer~Options} */ (baseOptions)); - this.setPreload(options.preload !== undefined ? options.preload : 0); - this.setUseInterimTilesOnError(options.useInterimTilesOnError !== undefined ? - options.useInterimTilesOnError : true); + this.setPreload(options.preload !== undefined ? options.preload : 0); + this.setUseInterimTilesOnError(options.useInterimTilesOnError !== undefined ? + options.useInterimTilesOnError : true); - /** - * The layer type. - * @protected - * @type {module:ol/LayerType} - */ - this.type = LayerType.TILE; + /** + * The layer type. + * @protected + * @type {module:ol/LayerType} + */ + this.type = LayerType.TILE; -}; + } + + /** + * Return the level as number to which we will preload tiles up to. + * @return {number} The level to preload tiles up to. + * @observable + * @api + */ + getPreload() { + return /** @type {number} */ (this.get(TileProperty.PRELOAD)); + } + + /** + * Set the level as number to which we will preload tiles up to. + * @param {number} preload The level to preload tiles up to. + * @observable + * @api + */ + setPreload(preload) { + this.set(TileProperty.PRELOAD, preload); + } + + /** + * Whether we use interim tiles on error. + * @return {boolean} Use interim tiles on error. + * @observable + * @api + */ + getUseInterimTilesOnError() { + return /** @type {boolean} */ (this.get(TileProperty.USE_INTERIM_TILES_ON_ERROR)); + } + + /** + * Set whether we use interim tiles on error. + * @param {boolean} useInterimTilesOnError Use interim tiles on error. + * @observable + * @api + */ + setUseInterimTilesOnError(useInterimTilesOnError) { + this.set(TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError); + } +} inherits(TileLayer, Layer); -/** - * Return the level as number to which we will preload tiles up to. - * @return {number} The level to preload tiles up to. - * @observable - * @api - */ -TileLayer.prototype.getPreload = function() { - return /** @type {number} */ (this.get(TileProperty.PRELOAD)); -}; - - /** * Return the associated {@link module:ol/source/Tile tilesource} of the layer. * @function @@ -90,35 +121,4 @@ TileLayer.prototype.getPreload = function() { TileLayer.prototype.getSource; -/** - * Set the level as number to which we will preload tiles up to. - * @param {number} preload The level to preload tiles up to. - * @observable - * @api - */ -TileLayer.prototype.setPreload = function(preload) { - this.set(TileProperty.PRELOAD, preload); -}; - - -/** - * Whether we use interim tiles on error. - * @return {boolean} Use interim tiles on error. - * @observable - * @api - */ -TileLayer.prototype.getUseInterimTilesOnError = function() { - return /** @type {boolean} */ (this.get(TileProperty.USE_INTERIM_TILES_ON_ERROR)); -}; - - -/** - * Set whether we use interim tiles on error. - * @param {boolean} useInterimTilesOnError Use interim tiles on error. - * @observable - * @api - */ -TileLayer.prototype.setUseInterimTilesOnError = function(useInterimTilesOnError) { - this.set(TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError); -}; export default TileLayer; diff --git a/src/ol/layer/Vector.js b/src/ol/layer/Vector.js index 7d1d56c304..2cc7f33089 100644 --- a/src/ol/layer/Vector.js +++ b/src/ol/layer/Vector.js @@ -91,114 +91,181 @@ const Property = { * @param {module:ol/layer/Vector~Options=} opt_options Options. * @api */ -const VectorLayer = function(opt_options) { - const options = opt_options ? - opt_options : /** @type {module:ol/layer/Vector~Options} */ ({}); +class VectorLayer { + constructor(opt_options) { + const options = opt_options ? + opt_options : /** @type {module:ol/layer/Vector~Options} */ ({}); - const baseOptions = assign({}, options); + const baseOptions = assign({}, options); - delete baseOptions.style; - delete baseOptions.renderBuffer; - delete baseOptions.updateWhileAnimating; - delete baseOptions.updateWhileInteracting; - Layer.call(this, /** @type {module:ol/layer/Layer~Options} */ (baseOptions)); + delete baseOptions.style; + delete baseOptions.renderBuffer; + delete baseOptions.updateWhileAnimating; + delete baseOptions.updateWhileInteracting; + Layer.call(this, /** @type {module:ol/layer/Layer~Options} */ (baseOptions)); - /** - * @private - * @type {boolean} - */ - this.declutter_ = options.declutter !== undefined ? options.declutter : false; + /** + * @private + * @type {boolean} + */ + this.declutter_ = options.declutter !== undefined ? options.declutter : false; - /** - * @type {number} - * @private - */ - this.renderBuffer_ = options.renderBuffer !== undefined ? - options.renderBuffer : 100; + /** + * @type {number} + * @private + */ + this.renderBuffer_ = options.renderBuffer !== undefined ? + options.renderBuffer : 100; - /** - * User provided style. - * @type {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} - * @private - */ - this.style_ = null; + /** + * User provided style. + * @type {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} + * @private + */ + this.style_ = null; - /** - * Style function for use within the library. - * @type {module:ol/style/Style~StyleFunction|undefined} - * @private - */ - this.styleFunction_ = undefined; + /** + * Style function for use within the library. + * @type {module:ol/style/Style~StyleFunction|undefined} + * @private + */ + this.styleFunction_ = undefined; - this.setStyle(options.style); + this.setStyle(options.style); - /** - * @type {boolean} - * @private - */ - this.updateWhileAnimating_ = options.updateWhileAnimating !== undefined ? - options.updateWhileAnimating : false; + /** + * @type {boolean} + * @private + */ + this.updateWhileAnimating_ = options.updateWhileAnimating !== undefined ? + options.updateWhileAnimating : false; - /** - * @type {boolean} - * @private - */ - this.updateWhileInteracting_ = options.updateWhileInteracting !== undefined ? - options.updateWhileInteracting : false; + /** + * @type {boolean} + * @private + */ + this.updateWhileInteracting_ = options.updateWhileInteracting !== undefined ? + options.updateWhileInteracting : false; - /** - * @private - * @type {module:ol/layer/VectorTileRenderType|string} - */ - this.renderMode_ = options.renderMode || VectorRenderType.VECTOR; + /** + * @private + * @type {module:ol/layer/VectorTileRenderType|string} + */ + this.renderMode_ = options.renderMode || VectorRenderType.VECTOR; - /** - * The layer type. - * @protected - * @type {module:ol/LayerType} - */ - this.type = LayerType.VECTOR; + /** + * The layer type. + * @protected + * @type {module:ol/LayerType} + */ + this.type = LayerType.VECTOR; -}; + } + + /** + * @return {boolean} Declutter. + */ + getDeclutter() { + return this.declutter_; + } + + /** + * @param {boolean} declutter Declutter. + */ + setDeclutter(declutter) { + this.declutter_ = declutter; + } + + /** + * @return {number|undefined} Render buffer. + */ + getRenderBuffer() { + return this.renderBuffer_; + } + + /** + * @return {function(module:ol/Feature, module:ol/Feature): number|null|undefined} Render + * order. + */ + getRenderOrder() { + return ( + /** @type {module:ol/render~OrderFunction|null|undefined} */ (this.get(Property.RENDER_ORDER)) + ); + } + + /** + * Get the style for features. This returns whatever was passed to the `style` + * option at construction or to the `setStyle` method. + * @return {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} + * Layer style. + * @api + */ + getStyle() { + return this.style_; + } + + /** + * Get the style function. + * @return {module:ol/style/Style~StyleFunction|undefined} Layer style function. + * @api + */ + getStyleFunction() { + return this.styleFunction_; + } + + /** + * @return {boolean} Whether the rendered layer should be updated while + * animating. + */ + getUpdateWhileAnimating() { + return this.updateWhileAnimating_; + } + + /** + * @return {boolean} Whether the rendered layer should be updated while + * interacting. + */ + getUpdateWhileInteracting() { + return this.updateWhileInteracting_; + } + + /** + * @param {module:ol/render~OrderFunction|null|undefined} renderOrder + * Render order. + */ + setRenderOrder(renderOrder) { + this.set(Property.RENDER_ORDER, renderOrder); + } + + /** + * Set the style for features. This can be a single style object, an array + * of styles, or a function that takes a feature and resolution and returns + * an array of styles. If it is `undefined` the default style is used. If + * it is `null` the layer has no style (a `null` style), so only features + * that have their own styles will be rendered in the layer. See + * {@link module:ol/style} for information on the default style. + * @param {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction|null|undefined} + * style Layer style. + * @api + */ + setStyle(style) { + this.style_ = style !== undefined ? style : createDefaultStyle; + this.styleFunction_ = style === null ? + undefined : toStyleFunction(this.style_); + this.changed(); + } + + /** + * @return {module:ol/layer/VectorRenderType|string} The render mode. + */ + getRenderMode() { + return this.renderMode_; + } +} inherits(VectorLayer, Layer); -/** - * @return {boolean} Declutter. - */ -VectorLayer.prototype.getDeclutter = function() { - return this.declutter_; -}; - - -/** - * @param {boolean} declutter Declutter. - */ -VectorLayer.prototype.setDeclutter = function(declutter) { - this.declutter_ = declutter; -}; - - -/** - * @return {number|undefined} Render buffer. - */ -VectorLayer.prototype.getRenderBuffer = function() { - return this.renderBuffer_; -}; - - -/** - * @return {function(module:ol/Feature, module:ol/Feature): number|null|undefined} Render - * order. - */ -VectorLayer.prototype.getRenderOrder = function() { - return ( - /** @type {module:ol/render~OrderFunction|null|undefined} */ (this.get(Property.RENDER_ORDER)) - ); -}; - - /** * Return the associated {@link module:ol/source/Vector vectorsource} of the layer. * @function @@ -208,80 +275,4 @@ VectorLayer.prototype.getRenderOrder = function() { VectorLayer.prototype.getSource; -/** - * Get the style for features. This returns whatever was passed to the `style` - * option at construction or to the `setStyle` method. - * @return {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction} - * Layer style. - * @api - */ -VectorLayer.prototype.getStyle = function() { - return this.style_; -}; - - -/** - * Get the style function. - * @return {module:ol/style/Style~StyleFunction|undefined} Layer style function. - * @api - */ -VectorLayer.prototype.getStyleFunction = function() { - return this.styleFunction_; -}; - - -/** - * @return {boolean} Whether the rendered layer should be updated while - * animating. - */ -VectorLayer.prototype.getUpdateWhileAnimating = function() { - return this.updateWhileAnimating_; -}; - - -/** - * @return {boolean} Whether the rendered layer should be updated while - * interacting. - */ -VectorLayer.prototype.getUpdateWhileInteracting = function() { - return this.updateWhileInteracting_; -}; - - -/** - * @param {module:ol/render~OrderFunction|null|undefined} renderOrder - * Render order. - */ -VectorLayer.prototype.setRenderOrder = function(renderOrder) { - this.set(Property.RENDER_ORDER, renderOrder); -}; - - -/** - * Set the style for features. This can be a single style object, an array - * of styles, or a function that takes a feature and resolution and returns - * an array of styles. If it is `undefined` the default style is used. If - * it is `null` the layer has no style (a `null` style), so only features - * that have their own styles will be rendered in the layer. See - * {@link module:ol/style} for information on the default style. - * @param {module:ol/style/Style|Array.|module:ol/style/Style~StyleFunction|null|undefined} - * style Layer style. - * @api - */ -VectorLayer.prototype.setStyle = function(style) { - this.style_ = style !== undefined ? style : createDefaultStyle; - this.styleFunction_ = style === null ? - undefined : toStyleFunction(this.style_); - this.changed(); -}; - - -/** - * @return {module:ol/layer/VectorRenderType|string} The render mode. - */ -VectorLayer.prototype.getRenderMode = function() { - return this.renderMode_; -}; - - export default VectorLayer; diff --git a/src/ol/layer/VectorTile.js b/src/ol/layer/VectorTile.js index 265435afec..2f0ac36950 100644 --- a/src/ol/layer/VectorTile.js +++ b/src/ol/layer/VectorTile.js @@ -99,86 +99,84 @@ export const RenderType = { * @param {module:ol/layer/VectorTile~Options=} opt_options Options. * @api */ -const VectorTileLayer = function(opt_options) { - const options = opt_options ? opt_options : {}; +class VectorTileLayer { + constructor(opt_options) { + const options = opt_options ? opt_options : {}; - let renderMode = options.renderMode || VectorTileRenderType.HYBRID; - assert(renderMode == undefined || - renderMode == VectorTileRenderType.IMAGE || - renderMode == VectorTileRenderType.HYBRID || - renderMode == VectorTileRenderType.VECTOR, - 28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'` - if (options.declutter && renderMode == VectorTileRenderType.IMAGE) { - renderMode = VectorTileRenderType.HYBRID; - } - options.renderMode = renderMode; + let renderMode = options.renderMode || VectorTileRenderType.HYBRID; + assert(renderMode == undefined || + renderMode == VectorTileRenderType.IMAGE || + renderMode == VectorTileRenderType.HYBRID || + renderMode == VectorTileRenderType.VECTOR, + 28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'` + if (options.declutter && renderMode == VectorTileRenderType.IMAGE) { + renderMode = VectorTileRenderType.HYBRID; + } + options.renderMode = renderMode; - const baseOptions = assign({}, options); + const baseOptions = assign({}, options); - delete baseOptions.preload; - delete baseOptions.useInterimTilesOnError; - VectorLayer.call(this, /** @type {module:ol/layer/Vector~Options} */ (baseOptions)); + delete baseOptions.preload; + delete baseOptions.useInterimTilesOnError; + VectorLayer.call(this, /** @type {module:ol/layer/Vector~Options} */ (baseOptions)); - this.setPreload(options.preload ? options.preload : 0); - this.setUseInterimTilesOnError(options.useInterimTilesOnError !== undefined ? - options.useInterimTilesOnError : true); + this.setPreload(options.preload ? options.preload : 0); + this.setUseInterimTilesOnError(options.useInterimTilesOnError !== undefined ? + options.useInterimTilesOnError : true); - /** - * The layer type. - * @protected - * @type {module:ol/LayerType} - */ - this.type = LayerType.VECTOR_TILE; + /** + * The layer type. + * @protected + * @type {module:ol/LayerType} + */ + this.type = LayerType.VECTOR_TILE; -}; + } + + /** + * Return the level as number to which we will preload tiles up to. + * @return {number} The level to preload tiles up to. + * @observable + * @api + */ + getPreload() { + return /** @type {number} */ (this.get(TileProperty.PRELOAD)); + } + + /** + * Whether we use interim tiles on error. + * @return {boolean} Use interim tiles on error. + * @observable + * @api + */ + getUseInterimTilesOnError() { + return /** @type {boolean} */ (this.get(TileProperty.USE_INTERIM_TILES_ON_ERROR)); + } + + /** + * Set the level as number to which we will preload tiles up to. + * @param {number} preload The level to preload tiles up to. + * @observable + * @api + */ + setPreload(preload) { + this.set(TileProperty.PRELOAD, preload); + } + + /** + * Set whether we use interim tiles on error. + * @param {boolean} useInterimTilesOnError Use interim tiles on error. + * @observable + * @api + */ + setUseInterimTilesOnError(useInterimTilesOnError) { + this.set(TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError); + } +} inherits(VectorTileLayer, VectorLayer); -/** - * Return the level as number to which we will preload tiles up to. - * @return {number} The level to preload tiles up to. - * @observable - * @api - */ -VectorTileLayer.prototype.getPreload = function() { - return /** @type {number} */ (this.get(TileProperty.PRELOAD)); -}; - - -/** - * Whether we use interim tiles on error. - * @return {boolean} Use interim tiles on error. - * @observable - * @api - */ -VectorTileLayer.prototype.getUseInterimTilesOnError = function() { - return /** @type {boolean} */ (this.get(TileProperty.USE_INTERIM_TILES_ON_ERROR)); -}; - - -/** - * Set the level as number to which we will preload tiles up to. - * @param {number} preload The level to preload tiles up to. - * @observable - * @api - */ -VectorTileLayer.prototype.setPreload = function(preload) { - this.set(TileProperty.PRELOAD, preload); -}; - - -/** - * Set whether we use interim tiles on error. - * @param {boolean} useInterimTilesOnError Use interim tiles on error. - * @observable - * @api - */ -VectorTileLayer.prototype.setUseInterimTilesOnError = function(useInterimTilesOnError) { - this.set(TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError); -}; - - /** * Return the associated {@link module:ol/source/VectorTile vectortilesource} of the layer. * @function diff --git a/src/ol/pointer/EventSource.js b/src/ol/pointer/EventSource.js index 591171076b..e7ea944ce2 100644 --- a/src/ol/pointer/EventSource.js +++ b/src/ol/pointer/EventSource.js @@ -6,36 +6,37 @@ * @param {!Object.} mapping Event mapping. * @constructor */ -const EventSource = function(dispatcher, mapping) { - /** - * @type {module:ol/pointer/PointerEventHandler} - */ - this.dispatcher = dispatcher; +class EventSource { + constructor(dispatcher, mapping) { + /** + * @type {module:ol/pointer/PointerEventHandler} + */ + this.dispatcher = dispatcher; - /** - * @private - * @const - * @type {!Object.} - */ - this.mapping_ = mapping; -}; + /** + * @private + * @const + * @type {!Object.} + */ + this.mapping_ = mapping; + } + /** + * List of events supported by this source. + * @return {Array.} Event names + */ + getEvents() { + return Object.keys(this.mapping_); + } -/** - * List of events supported by this source. - * @return {Array.} Event names - */ -EventSource.prototype.getEvents = function() { - return Object.keys(this.mapping_); -}; + /** + * Returns the handler that should handle a given event type. + * @param {string} eventType The event type. + * @return {function(Event)} Handler + */ + getHandlerForEvent(eventType) { + return this.mapping_[eventType]; + } +} - -/** - * Returns the handler that should handle a given event type. - * @param {string} eventType The event type. - * @return {function(Event)} Handler - */ -EventSource.prototype.getHandlerForEvent = function(eventType) { - return this.mapping_[eventType]; -}; export default EventSource; diff --git a/src/ol/pointer/MouseSource.js b/src/ol/pointer/MouseSource.js index f9a00a40ce..c8f12bde3f 100644 --- a/src/ol/pointer/MouseSource.js +++ b/src/ol/pointer/MouseSource.js @@ -39,28 +39,158 @@ import EventSource from '../pointer/EventSource.js'; * @constructor * @extends {module:ol/pointer/EventSource} */ -const MouseSource = function(dispatcher) { - const mapping = { - 'mousedown': this.mousedown, - 'mousemove': this.mousemove, - 'mouseup': this.mouseup, - 'mouseover': this.mouseover, - 'mouseout': this.mouseout - }; - EventSource.call(this, dispatcher, mapping); +class MouseSource { + constructor(dispatcher) { + const mapping = { + 'mousedown': this.mousedown, + 'mousemove': this.mousemove, + 'mouseup': this.mouseup, + 'mouseover': this.mouseover, + 'mouseout': this.mouseout + }; + EventSource.call(this, dispatcher, mapping); + + /** + * @const + * @type {!Object.} + */ + this.pointerMap = dispatcher.pointerMap; + + /** + * @const + * @type {Array.} + */ + this.lastTouches = []; + } /** - * @const - * @type {!Object.} + * Detect if a mouse event was simulated from a touch by + * checking if previously there was a touch event at the + * same position. + * + * FIXME - Known problem with the native Android browser on + * Samsung GT-I9100 (Android 4.1.2): + * In case the page is scrolled, this function does not work + * correctly when a canvas is used (WebGL or canvas renderer). + * Mouse listeners on canvas elements (for this browser), create + * two mouse events: One 'good' and one 'bad' one (on other browsers or + * when a div is used, there is only one event). For the 'bad' one, + * clientX/clientY and also pageX/pageY are wrong when the page + * is scrolled. Because of that, this function can not detect if + * the events were simulated from a touch event. As result, a + * pointer event at a wrong position is dispatched, which confuses + * the map interactions. + * It is unclear, how one can get the correct position for the event + * or detect that the positions are invalid. + * + * @private + * @param {MouseEvent} inEvent The in event. + * @return {boolean} True, if the event was generated by a touch. */ - this.pointerMap = dispatcher.pointerMap; + isEventSimulatedFromTouch_(inEvent) { + const lts = this.lastTouches; + const x = inEvent.clientX; + const y = inEvent.clientY; + for (let i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) { + // simulated mouse events will be swallowed near a primary touchend + const dx = Math.abs(x - t[0]); + const dy = Math.abs(y - t[1]); + if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) { + return true; + } + } + return false; + } /** - * @const - * @type {Array.} + * Handler for `mousedown`. + * + * @param {MouseEvent} inEvent The in event. */ - this.lastTouches = []; -}; + mousedown(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + // TODO(dfreedman) workaround for some elements not sending mouseup + // http://crbug/149091 + if (POINTER_ID.toString() in this.pointerMap) { + this.cancel(inEvent); + } + const e = prepareEvent(inEvent, this.dispatcher); + this.pointerMap[POINTER_ID.toString()] = inEvent; + this.dispatcher.down(e, inEvent); + } + } + + /** + * Handler for `mousemove`. + * + * @param {MouseEvent} inEvent The in event. + */ + mousemove(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + const e = prepareEvent(inEvent, this.dispatcher); + this.dispatcher.move(e, inEvent); + } + } + + /** + * Handler for `mouseup`. + * + * @param {MouseEvent} inEvent The in event. + */ + mouseup(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + const p = this.pointerMap[POINTER_ID.toString()]; + + if (p && p.button === inEvent.button) { + const e = prepareEvent(inEvent, this.dispatcher); + this.dispatcher.up(e, inEvent); + this.cleanupMouse(); + } + } + } + + /** + * Handler for `mouseover`. + * + * @param {MouseEvent} inEvent The in event. + */ + mouseover(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + const e = prepareEvent(inEvent, this.dispatcher); + this.dispatcher.enterOver(e, inEvent); + } + } + + /** + * Handler for `mouseout`. + * + * @param {MouseEvent} inEvent The in event. + */ + mouseout(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + const e = prepareEvent(inEvent, this.dispatcher); + this.dispatcher.leaveOut(e, inEvent); + } + } + + /** + * Dispatches a `pointercancel` event. + * + * @param {Event} inEvent The in event. + */ + cancel(inEvent) { + const e = prepareEvent(inEvent, this.dispatcher); + this.dispatcher.cancel(e, inEvent); + this.cleanupMouse(); + } + + /** + * Remove the mouse from the list of active pointers. + */ + cleanupMouse() { + delete this.pointerMap[POINTER_ID.toString()]; + } +} inherits(MouseSource, EventSource); @@ -85,46 +215,6 @@ export const POINTER_TYPE = 'mouse'; const DEDUP_DIST = 25; -/** - * Detect if a mouse event was simulated from a touch by - * checking if previously there was a touch event at the - * same position. - * - * FIXME - Known problem with the native Android browser on - * Samsung GT-I9100 (Android 4.1.2): - * In case the page is scrolled, this function does not work - * correctly when a canvas is used (WebGL or canvas renderer). - * Mouse listeners on canvas elements (for this browser), create - * two mouse events: One 'good' and one 'bad' one (on other browsers or - * when a div is used, there is only one event). For the 'bad' one, - * clientX/clientY and also pageX/pageY are wrong when the page - * is scrolled. Because of that, this function can not detect if - * the events were simulated from a touch event. As result, a - * pointer event at a wrong position is dispatched, which confuses - * the map interactions. - * It is unclear, how one can get the correct position for the event - * or detect that the positions are invalid. - * - * @private - * @param {MouseEvent} inEvent The in event. - * @return {boolean} True, if the event was generated by a touch. - */ -MouseSource.prototype.isEventSimulatedFromTouch_ = function(inEvent) { - const lts = this.lastTouches; - const x = inEvent.clientX; - const y = inEvent.clientY; - for (let i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) { - // simulated mouse events will be swallowed near a primary touchend - const dx = Math.abs(x - t[0]); - const dy = Math.abs(y - t[1]); - if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) { - return true; - } - } - return false; -}; - - /** * Creates a copy of the original event that will be used * for the fake pointer event. @@ -151,98 +241,4 @@ function prepareEvent(inEvent, dispatcher) { } -/** - * Handler for `mousedown`. - * - * @param {MouseEvent} inEvent The in event. - */ -MouseSource.prototype.mousedown = function(inEvent) { - if (!this.isEventSimulatedFromTouch_(inEvent)) { - // TODO(dfreedman) workaround for some elements not sending mouseup - // http://crbug/149091 - if (POINTER_ID.toString() in this.pointerMap) { - this.cancel(inEvent); - } - const e = prepareEvent(inEvent, this.dispatcher); - this.pointerMap[POINTER_ID.toString()] = inEvent; - this.dispatcher.down(e, inEvent); - } -}; - - -/** - * Handler for `mousemove`. - * - * @param {MouseEvent} inEvent The in event. - */ -MouseSource.prototype.mousemove = function(inEvent) { - if (!this.isEventSimulatedFromTouch_(inEvent)) { - const e = prepareEvent(inEvent, this.dispatcher); - this.dispatcher.move(e, inEvent); - } -}; - - -/** - * Handler for `mouseup`. - * - * @param {MouseEvent} inEvent The in event. - */ -MouseSource.prototype.mouseup = function(inEvent) { - if (!this.isEventSimulatedFromTouch_(inEvent)) { - const p = this.pointerMap[POINTER_ID.toString()]; - - if (p && p.button === inEvent.button) { - const e = prepareEvent(inEvent, this.dispatcher); - this.dispatcher.up(e, inEvent); - this.cleanupMouse(); - } - } -}; - - -/** - * Handler for `mouseover`. - * - * @param {MouseEvent} inEvent The in event. - */ -MouseSource.prototype.mouseover = function(inEvent) { - if (!this.isEventSimulatedFromTouch_(inEvent)) { - const e = prepareEvent(inEvent, this.dispatcher); - this.dispatcher.enterOver(e, inEvent); - } -}; - - -/** - * Handler for `mouseout`. - * - * @param {MouseEvent} inEvent The in event. - */ -MouseSource.prototype.mouseout = function(inEvent) { - if (!this.isEventSimulatedFromTouch_(inEvent)) { - const e = prepareEvent(inEvent, this.dispatcher); - this.dispatcher.leaveOut(e, inEvent); - } -}; - - -/** - * Dispatches a `pointercancel` event. - * - * @param {Event} inEvent The in event. - */ -MouseSource.prototype.cancel = function(inEvent) { - const e = prepareEvent(inEvent, this.dispatcher); - this.dispatcher.cancel(e, inEvent); - this.cleanupMouse(); -}; - - -/** - * Remove the mouse from the list of active pointers. - */ -MouseSource.prototype.cleanupMouse = function() { - delete this.pointerMap[POINTER_ID.toString()]; -}; export default MouseSource; diff --git a/src/ol/pointer/MsSource.js b/src/ol/pointer/MsSource.js index a8ad9d64e2..402ff54b7f 100644 --- a/src/ol/pointer/MsSource.js +++ b/src/ol/pointer/MsSource.js @@ -39,25 +39,136 @@ import EventSource from '../pointer/EventSource.js'; * @constructor * @extends {module:ol/pointer/EventSource} */ -const MsSource = function(dispatcher) { - const mapping = { - 'MSPointerDown': this.msPointerDown, - 'MSPointerMove': this.msPointerMove, - 'MSPointerUp': this.msPointerUp, - 'MSPointerOut': this.msPointerOut, - 'MSPointerOver': this.msPointerOver, - 'MSPointerCancel': this.msPointerCancel, - 'MSGotPointerCapture': this.msGotPointerCapture, - 'MSLostPointerCapture': this.msLostPointerCapture - }; - EventSource.call(this, dispatcher, mapping); +class MsSource { + constructor(dispatcher) { + const mapping = { + 'MSPointerDown': this.msPointerDown, + 'MSPointerMove': this.msPointerMove, + 'MSPointerUp': this.msPointerUp, + 'MSPointerOut': this.msPointerOut, + 'MSPointerOver': this.msPointerOver, + 'MSPointerCancel': this.msPointerCancel, + 'MSGotPointerCapture': this.msGotPointerCapture, + 'MSLostPointerCapture': this.msLostPointerCapture + }; + EventSource.call(this, dispatcher, mapping); - /** - * @const - * @type {!Object.} - */ - this.pointerMap = dispatcher.pointerMap; -}; + /** + * @const + * @type {!Object.} + */ + this.pointerMap = dispatcher.pointerMap; + } + + /** + * Creates a copy of the original event that will be used + * for the fake pointer event. + * + * @private + * @param {MSPointerEvent} inEvent The in event. + * @return {Object} The copied event. + */ + prepareEvent_(inEvent) { + let e = inEvent; + if (typeof inEvent.pointerType === 'number') { + e = this.dispatcher.cloneEvent(inEvent, inEvent); + e.pointerType = POINTER_TYPES[inEvent.pointerType]; + } + + return e; + } + + /** + * Remove this pointer from the list of active pointers. + * @param {number} pointerId Pointer identifier. + */ + cleanup(pointerId) { + delete this.pointerMap[pointerId.toString()]; + } + + /** + * Handler for `msPointerDown`. + * + * @param {MSPointerEvent} inEvent The in event. + */ + msPointerDown(inEvent) { + this.pointerMap[inEvent.pointerId.toString()] = inEvent; + const e = this.prepareEvent_(inEvent); + this.dispatcher.down(e, inEvent); + } + + /** + * Handler for `msPointerMove`. + * + * @param {MSPointerEvent} inEvent The in event. + */ + msPointerMove(inEvent) { + const e = this.prepareEvent_(inEvent); + this.dispatcher.move(e, inEvent); + } + + /** + * Handler for `msPointerUp`. + * + * @param {MSPointerEvent} inEvent The in event. + */ + msPointerUp(inEvent) { + const e = this.prepareEvent_(inEvent); + this.dispatcher.up(e, inEvent); + this.cleanup(inEvent.pointerId); + } + + /** + * Handler for `msPointerOut`. + * + * @param {MSPointerEvent} inEvent The in event. + */ + msPointerOut(inEvent) { + const e = this.prepareEvent_(inEvent); + this.dispatcher.leaveOut(e, inEvent); + } + + /** + * Handler for `msPointerOver`. + * + * @param {MSPointerEvent} inEvent The in event. + */ + msPointerOver(inEvent) { + const e = this.prepareEvent_(inEvent); + this.dispatcher.enterOver(e, inEvent); + } + + /** + * Handler for `msPointerCancel`. + * + * @param {MSPointerEvent} inEvent The in event. + */ + msPointerCancel(inEvent) { + const e = this.prepareEvent_(inEvent); + this.dispatcher.cancel(e, inEvent); + this.cleanup(inEvent.pointerId); + } + + /** + * Handler for `msLostPointerCapture`. + * + * @param {MSPointerEvent} inEvent The in event. + */ + msLostPointerCapture(inEvent) { + const e = this.dispatcher.makeEvent('lostpointercapture', inEvent, inEvent); + this.dispatcher.dispatchEvent(e); + } + + /** + * Handler for `msGotPointerCapture`. + * + * @param {MSPointerEvent} inEvent The in event. + */ + msGotPointerCapture(inEvent) { + const e = this.dispatcher.makeEvent('gotpointercapture', inEvent, inEvent); + this.dispatcher.dispatchEvent(e); + } +} inherits(MsSource, EventSource); @@ -74,121 +185,4 @@ const POINTER_TYPES = [ ]; -/** - * Creates a copy of the original event that will be used - * for the fake pointer event. - * - * @private - * @param {MSPointerEvent} inEvent The in event. - * @return {Object} The copied event. - */ -MsSource.prototype.prepareEvent_ = function(inEvent) { - let e = inEvent; - if (typeof inEvent.pointerType === 'number') { - e = this.dispatcher.cloneEvent(inEvent, inEvent); - e.pointerType = POINTER_TYPES[inEvent.pointerType]; - } - - return e; -}; - - -/** - * Remove this pointer from the list of active pointers. - * @param {number} pointerId Pointer identifier. - */ -MsSource.prototype.cleanup = function(pointerId) { - delete this.pointerMap[pointerId.toString()]; -}; - - -/** - * Handler for `msPointerDown`. - * - * @param {MSPointerEvent} inEvent The in event. - */ -MsSource.prototype.msPointerDown = function(inEvent) { - this.pointerMap[inEvent.pointerId.toString()] = inEvent; - const e = this.prepareEvent_(inEvent); - this.dispatcher.down(e, inEvent); -}; - - -/** - * Handler for `msPointerMove`. - * - * @param {MSPointerEvent} inEvent The in event. - */ -MsSource.prototype.msPointerMove = function(inEvent) { - const e = this.prepareEvent_(inEvent); - this.dispatcher.move(e, inEvent); -}; - - -/** - * Handler for `msPointerUp`. - * - * @param {MSPointerEvent} inEvent The in event. - */ -MsSource.prototype.msPointerUp = function(inEvent) { - const e = this.prepareEvent_(inEvent); - this.dispatcher.up(e, inEvent); - this.cleanup(inEvent.pointerId); -}; - - -/** - * Handler for `msPointerOut`. - * - * @param {MSPointerEvent} inEvent The in event. - */ -MsSource.prototype.msPointerOut = function(inEvent) { - const e = this.prepareEvent_(inEvent); - this.dispatcher.leaveOut(e, inEvent); -}; - - -/** - * Handler for `msPointerOver`. - * - * @param {MSPointerEvent} inEvent The in event. - */ -MsSource.prototype.msPointerOver = function(inEvent) { - const e = this.prepareEvent_(inEvent); - this.dispatcher.enterOver(e, inEvent); -}; - - -/** - * Handler for `msPointerCancel`. - * - * @param {MSPointerEvent} inEvent The in event. - */ -MsSource.prototype.msPointerCancel = function(inEvent) { - const e = this.prepareEvent_(inEvent); - this.dispatcher.cancel(e, inEvent); - this.cleanup(inEvent.pointerId); -}; - - -/** - * Handler for `msLostPointerCapture`. - * - * @param {MSPointerEvent} inEvent The in event. - */ -MsSource.prototype.msLostPointerCapture = function(inEvent) { - const e = this.dispatcher.makeEvent('lostpointercapture', inEvent, inEvent); - this.dispatcher.dispatchEvent(e); -}; - - -/** - * Handler for `msGotPointerCapture`. - * - * @param {MSPointerEvent} inEvent The in event. - */ -MsSource.prototype.msGotPointerCapture = function(inEvent) { - const e = this.dispatcher.makeEvent('gotpointercapture', inEvent, inEvent); - this.dispatcher.dispatchEvent(e); -}; export default MsSource; diff --git a/src/ol/pointer/NativeSource.js b/src/ol/pointer/NativeSource.js index a745900ba0..a28740ee7b 100644 --- a/src/ol/pointer/NativeSource.js +++ b/src/ol/pointer/NativeSource.js @@ -39,99 +39,95 @@ import EventSource from '../pointer/EventSource.js'; * @constructor * @extends {module:ol/pointer/EventSource} */ -const NativeSource = function(dispatcher) { - const mapping = { - 'pointerdown': this.pointerDown, - 'pointermove': this.pointerMove, - 'pointerup': this.pointerUp, - 'pointerout': this.pointerOut, - 'pointerover': this.pointerOver, - 'pointercancel': this.pointerCancel, - 'gotpointercapture': this.gotPointerCapture, - 'lostpointercapture': this.lostPointerCapture - }; - EventSource.call(this, dispatcher, mapping); -}; +class NativeSource { + constructor(dispatcher) { + const mapping = { + 'pointerdown': this.pointerDown, + 'pointermove': this.pointerMove, + 'pointerup': this.pointerUp, + 'pointerout': this.pointerOut, + 'pointerover': this.pointerOver, + 'pointercancel': this.pointerCancel, + 'gotpointercapture': this.gotPointerCapture, + 'lostpointercapture': this.lostPointerCapture + }; + EventSource.call(this, dispatcher, mapping); + } + + /** + * Handler for `pointerdown`. + * + * @param {Event} inEvent The in event. + */ + pointerDown(inEvent) { + this.dispatcher.fireNativeEvent(inEvent); + } + + /** + * Handler for `pointermove`. + * + * @param {Event} inEvent The in event. + */ + pointerMove(inEvent) { + this.dispatcher.fireNativeEvent(inEvent); + } + + /** + * Handler for `pointerup`. + * + * @param {Event} inEvent The in event. + */ + pointerUp(inEvent) { + this.dispatcher.fireNativeEvent(inEvent); + } + + /** + * Handler for `pointerout`. + * + * @param {Event} inEvent The in event. + */ + pointerOut(inEvent) { + this.dispatcher.fireNativeEvent(inEvent); + } + + /** + * Handler for `pointerover`. + * + * @param {Event} inEvent The in event. + */ + pointerOver(inEvent) { + this.dispatcher.fireNativeEvent(inEvent); + } + + /** + * Handler for `pointercancel`. + * + * @param {Event} inEvent The in event. + */ + pointerCancel(inEvent) { + this.dispatcher.fireNativeEvent(inEvent); + } + + /** + * Handler for `lostpointercapture`. + * + * @param {Event} inEvent The in event. + */ + lostPointerCapture(inEvent) { + this.dispatcher.fireNativeEvent(inEvent); + } + + /** + * Handler for `gotpointercapture`. + * + * @param {Event} inEvent The in event. + */ + gotPointerCapture(inEvent) { + this.dispatcher.fireNativeEvent(inEvent); + } +} inherits(NativeSource, EventSource); -/** - * Handler for `pointerdown`. - * - * @param {Event} inEvent The in event. - */ -NativeSource.prototype.pointerDown = function(inEvent) { - this.dispatcher.fireNativeEvent(inEvent); -}; - - -/** - * Handler for `pointermove`. - * - * @param {Event} inEvent The in event. - */ -NativeSource.prototype.pointerMove = function(inEvent) { - this.dispatcher.fireNativeEvent(inEvent); -}; - - -/** - * Handler for `pointerup`. - * - * @param {Event} inEvent The in event. - */ -NativeSource.prototype.pointerUp = function(inEvent) { - this.dispatcher.fireNativeEvent(inEvent); -}; - - -/** - * Handler for `pointerout`. - * - * @param {Event} inEvent The in event. - */ -NativeSource.prototype.pointerOut = function(inEvent) { - this.dispatcher.fireNativeEvent(inEvent); -}; - - -/** - * Handler for `pointerover`. - * - * @param {Event} inEvent The in event. - */ -NativeSource.prototype.pointerOver = function(inEvent) { - this.dispatcher.fireNativeEvent(inEvent); -}; - - -/** - * Handler for `pointercancel`. - * - * @param {Event} inEvent The in event. - */ -NativeSource.prototype.pointerCancel = function(inEvent) { - this.dispatcher.fireNativeEvent(inEvent); -}; - - -/** - * Handler for `lostpointercapture`. - * - * @param {Event} inEvent The in event. - */ -NativeSource.prototype.lostPointerCapture = function(inEvent) { - this.dispatcher.fireNativeEvent(inEvent); -}; - - -/** - * Handler for `gotpointercapture`. - * - * @param {Event} inEvent The in event. - */ -NativeSource.prototype.gotPointerCapture = function(inEvent) { - this.dispatcher.fireNativeEvent(inEvent); -}; export default NativeSource; diff --git a/src/ol/pointer/PointerEvent.js b/src/ol/pointer/PointerEvent.js index 079cfc1e40..56957d9943 100644 --- a/src/ol/pointer/PointerEvent.js +++ b/src/ol/pointer/PointerEvent.js @@ -47,150 +47,211 @@ import Event from '../events/Event.js'; * @param {Object.=} opt_eventDict An optional dictionary of * initial event properties. */ -const PointerEvent = function(type, originalEvent, opt_eventDict) { - Event.call(this, type); +class PointerEvent { + constructor(type, originalEvent, opt_eventDict) { + Event.call(this, type); - /** - * @const - * @type {Event} - */ - this.originalEvent = originalEvent; + /** + * @const + * @type {Event} + */ + this.originalEvent = originalEvent; - const eventDict = opt_eventDict ? opt_eventDict : {}; + const eventDict = opt_eventDict ? opt_eventDict : {}; - /** - * @type {number} - */ - this.buttons = this.getButtons_(eventDict); + /** + * @type {number} + */ + this.buttons = this.getButtons_(eventDict); - /** - * @type {number} - */ - this.pressure = this.getPressure_(eventDict, this.buttons); + /** + * @type {number} + */ + this.pressure = this.getPressure_(eventDict, this.buttons); - // MouseEvent related properties + // MouseEvent related properties - /** - * @type {boolean} - */ - this.bubbles = 'bubbles' in eventDict ? eventDict['bubbles'] : false; + /** + * @type {boolean} + */ + this.bubbles = 'bubbles' in eventDict ? eventDict['bubbles'] : false; - /** - * @type {boolean} - */ - this.cancelable = 'cancelable' in eventDict ? eventDict['cancelable'] : false; + /** + * @type {boolean} + */ + this.cancelable = 'cancelable' in eventDict ? eventDict['cancelable'] : false; - /** - * @type {Object} - */ - this.view = 'view' in eventDict ? eventDict['view'] : null; + /** + * @type {Object} + */ + this.view = 'view' in eventDict ? eventDict['view'] : null; - /** - * @type {number} - */ - this.detail = 'detail' in eventDict ? eventDict['detail'] : null; + /** + * @type {number} + */ + this.detail = 'detail' in eventDict ? eventDict['detail'] : null; - /** - * @type {number} - */ - this.screenX = 'screenX' in eventDict ? eventDict['screenX'] : 0; + /** + * @type {number} + */ + this.screenX = 'screenX' in eventDict ? eventDict['screenX'] : 0; - /** - * @type {number} - */ - this.screenY = 'screenY' in eventDict ? eventDict['screenY'] : 0; + /** + * @type {number} + */ + this.screenY = 'screenY' in eventDict ? eventDict['screenY'] : 0; - /** - * @type {number} - */ - this.clientX = 'clientX' in eventDict ? eventDict['clientX'] : 0; + /** + * @type {number} + */ + this.clientX = 'clientX' in eventDict ? eventDict['clientX'] : 0; - /** - * @type {number} - */ - this.clientY = 'clientY' in eventDict ? eventDict['clientY'] : 0; + /** + * @type {number} + */ + this.clientY = 'clientY' in eventDict ? eventDict['clientY'] : 0; - /** - * @type {boolean} - */ - this.ctrlKey = 'ctrlKey' in eventDict ? eventDict['ctrlKey'] : false; + /** + * @type {boolean} + */ + this.ctrlKey = 'ctrlKey' in eventDict ? eventDict['ctrlKey'] : false; - /** - * @type {boolean} - */ - this.altKey = 'altKey' in eventDict ? eventDict['altKey'] : false; + /** + * @type {boolean} + */ + this.altKey = 'altKey' in eventDict ? eventDict['altKey'] : false; - /** - * @type {boolean} - */ - this.shiftKey = 'shiftKey' in eventDict ? eventDict['shiftKey'] : false; + /** + * @type {boolean} + */ + this.shiftKey = 'shiftKey' in eventDict ? eventDict['shiftKey'] : false; - /** - * @type {boolean} - */ - this.metaKey = 'metaKey' in eventDict ? eventDict['metaKey'] : false; + /** + * @type {boolean} + */ + this.metaKey = 'metaKey' in eventDict ? eventDict['metaKey'] : false; - /** - * @type {number} - */ - this.button = 'button' in eventDict ? eventDict['button'] : 0; + /** + * @type {number} + */ + this.button = 'button' in eventDict ? eventDict['button'] : 0; - /** - * @type {Node} - */ - this.relatedTarget = 'relatedTarget' in eventDict ? - eventDict['relatedTarget'] : null; + /** + * @type {Node} + */ + this.relatedTarget = 'relatedTarget' in eventDict ? + eventDict['relatedTarget'] : null; - // PointerEvent related properties + // PointerEvent related properties - /** - * @const - * @type {number} - */ - this.pointerId = 'pointerId' in eventDict ? eventDict['pointerId'] : 0; + /** + * @const + * @type {number} + */ + this.pointerId = 'pointerId' in eventDict ? eventDict['pointerId'] : 0; - /** - * @type {number} - */ - this.width = 'width' in eventDict ? eventDict['width'] : 0; + /** + * @type {number} + */ + this.width = 'width' in eventDict ? eventDict['width'] : 0; - /** - * @type {number} - */ - this.height = 'height' in eventDict ? eventDict['height'] : 0; + /** + * @type {number} + */ + this.height = 'height' in eventDict ? eventDict['height'] : 0; - /** - * @type {number} - */ - this.tiltX = 'tiltX' in eventDict ? eventDict['tiltX'] : 0; + /** + * @type {number} + */ + this.tiltX = 'tiltX' in eventDict ? eventDict['tiltX'] : 0; - /** - * @type {number} - */ - this.tiltY = 'tiltY' in eventDict ? eventDict['tiltY'] : 0; + /** + * @type {number} + */ + this.tiltY = 'tiltY' in eventDict ? eventDict['tiltY'] : 0; - /** - * @type {string} - */ - this.pointerType = 'pointerType' in eventDict ? eventDict['pointerType'] : ''; + /** + * @type {string} + */ + this.pointerType = 'pointerType' in eventDict ? eventDict['pointerType'] : ''; - /** - * @type {number} - */ - this.hwTimestamp = 'hwTimestamp' in eventDict ? eventDict['hwTimestamp'] : 0; + /** + * @type {number} + */ + this.hwTimestamp = 'hwTimestamp' in eventDict ? eventDict['hwTimestamp'] : 0; - /** - * @type {boolean} - */ - this.isPrimary = 'isPrimary' in eventDict ? eventDict['isPrimary'] : false; + /** + * @type {boolean} + */ + this.isPrimary = 'isPrimary' in eventDict ? eventDict['isPrimary'] : false; - // keep the semantics of preventDefault - if (originalEvent.preventDefault) { - this.preventDefault = function() { - originalEvent.preventDefault(); - }; - } -}; + // keep the semantics of preventDefault + if (originalEvent.preventDefault) { + this.preventDefault = function() { + originalEvent.preventDefault(); + }; + } + } + + /** + * @private + * @param {Object.} eventDict The event dictionary. + * @return {number} Button indicator. + */ + getButtons_(eventDict) { + // According to the w3c spec, + // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-button + // MouseEvent.button == 0 can mean either no mouse button depressed, or the + // left mouse button depressed. + // + // As of now, the only way to distinguish between the two states of + // MouseEvent.button is by using the deprecated MouseEvent.which property, as + // this maps mouse buttons to positive integers > 0, and uses 0 to mean that + // no mouse button is held. + // + // MouseEvent.which is derived from MouseEvent.button at MouseEvent creation, + // but initMouseEvent does not expose an argument with which to set + // MouseEvent.which. Calling initMouseEvent with a buttonArg of 0 will set + // MouseEvent.button == 0 and MouseEvent.which == 1, breaking the expectations + // of app developers. + // + // The only way to propagate the correct state of MouseEvent.which and + // MouseEvent.button to a new MouseEvent.button == 0 and MouseEvent.which == 0 + // is to call initMouseEvent with a buttonArg value of -1. + // + // This is fixed with DOM Level 4's use of buttons + let buttons; + if (eventDict.buttons || HAS_BUTTONS) { + buttons = eventDict.buttons; + } else { + switch (eventDict.which) { + case 1: buttons = 1; break; + case 2: buttons = 4; break; + case 3: buttons = 2; break; + default: buttons = 0; + } + } + return buttons; + } + + /** + * @private + * @param {Object.} eventDict The event dictionary. + * @param {number} buttons Button indicator. + * @return {number} The pressure. + */ + getPressure_(eventDict, buttons) { + // Spec requires that pointers without pressure specified use 0.5 for down + // state and 0 for up state. + let pressure = 0; + if (eventDict.pressure) { + pressure = eventDict.pressure; + } else { + pressure = buttons ? 0.5 : 0; + } + return pressure; + } +} inherits(PointerEvent, Event); @@ -202,67 +263,6 @@ inherits(PointerEvent, Event); let HAS_BUTTONS = false; -/** - * @private - * @param {Object.} eventDict The event dictionary. - * @return {number} Button indicator. - */ -PointerEvent.prototype.getButtons_ = function(eventDict) { - // According to the w3c spec, - // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-button - // MouseEvent.button == 0 can mean either no mouse button depressed, or the - // left mouse button depressed. - // - // As of now, the only way to distinguish between the two states of - // MouseEvent.button is by using the deprecated MouseEvent.which property, as - // this maps mouse buttons to positive integers > 0, and uses 0 to mean that - // no mouse button is held. - // - // MouseEvent.which is derived from MouseEvent.button at MouseEvent creation, - // but initMouseEvent does not expose an argument with which to set - // MouseEvent.which. Calling initMouseEvent with a buttonArg of 0 will set - // MouseEvent.button == 0 and MouseEvent.which == 1, breaking the expectations - // of app developers. - // - // The only way to propagate the correct state of MouseEvent.which and - // MouseEvent.button to a new MouseEvent.button == 0 and MouseEvent.which == 0 - // is to call initMouseEvent with a buttonArg value of -1. - // - // This is fixed with DOM Level 4's use of buttons - let buttons; - if (eventDict.buttons || HAS_BUTTONS) { - buttons = eventDict.buttons; - } else { - switch (eventDict.which) { - case 1: buttons = 1; break; - case 2: buttons = 4; break; - case 3: buttons = 2; break; - default: buttons = 0; - } - } - return buttons; -}; - - -/** - * @private - * @param {Object.} eventDict The event dictionary. - * @param {number} buttons Button indicator. - * @return {number} The pressure. - */ -PointerEvent.prototype.getPressure_ = function(eventDict, buttons) { - // Spec requires that pointers without pressure specified use 0.5 for down - // state and 0 for up state. - let pressure = 0; - if (eventDict.pressure) { - pressure = eventDict.pressure; - } else { - pressure = buttons ? 0.5 : 0; - } - return pressure; -}; - - /** * Checks if the `buttons` property is supported. */ diff --git a/src/ol/pointer/PointerEventHandler.js b/src/ol/pointer/PointerEventHandler.js index ffa139d290..2be6d9ca34 100644 --- a/src/ol/pointer/PointerEventHandler.js +++ b/src/ol/pointer/PointerEventHandler.js @@ -47,36 +47,333 @@ import TouchSource from '../pointer/TouchSource.js'; * @extends {module:ol/events/EventTarget} * @param {Element|HTMLDocument} element Viewport element. */ -const PointerEventHandler = function(element) { - EventTarget.call(this); +class PointerEventHandler { + constructor(element) { + EventTarget.call(this); + + /** + * @const + * @private + * @type {Element|HTMLDocument} + */ + this.element_ = element; + + /** + * @const + * @type {!Object.} + */ + this.pointerMap = {}; + + /** + * @type {Object.} + * @private + */ + this.eventMap_ = {}; + + /** + * @type {Array.} + * @private + */ + this.eventSourceList_ = []; + + this.registerSources(); + } /** - * @const - * @private - * @type {Element|HTMLDocument} + * Set up the event sources (mouse, touch and native pointers) + * that generate pointer events. */ - this.element_ = element; + registerSources() { + if (POINTER) { + this.registerSource('native', new NativeSource(this)); + } else if (MSPOINTER) { + this.registerSource('ms', new MsSource(this)); + } else { + const mouseSource = new MouseSource(this); + this.registerSource('mouse', mouseSource); + + if (TOUCH) { + this.registerSource('touch', new TouchSource(this, mouseSource)); + } + } + + // register events on the viewport element + this.register_(); + } /** - * @const - * @type {!Object.} + * Add a new event source that will generate pointer events. + * + * @param {string} name A name for the event source + * @param {module:ol/pointer/EventSource} source The source event. */ - this.pointerMap = {}; + registerSource(name, source) { + const s = source; + const newEvents = s.getEvents(); + + if (newEvents) { + newEvents.forEach(function(e) { + const handler = s.getHandlerForEvent(e); + + if (handler) { + this.eventMap_[e] = handler.bind(s); + } + }.bind(this)); + this.eventSourceList_.push(s); + } + } /** - * @type {Object.} + * Set up the events for all registered event sources. * @private */ - this.eventMap_ = {}; + register_() { + const l = this.eventSourceList_.length; + for (let i = 0; i < l; i++) { + const eventSource = this.eventSourceList_[i]; + this.addEvents_(eventSource.getEvents()); + } + } /** - * @type {Array.} + * Remove all registered events. * @private */ - this.eventSourceList_ = []; + unregister_() { + const l = this.eventSourceList_.length; + for (let i = 0; i < l; i++) { + const eventSource = this.eventSourceList_[i]; + this.removeEvents_(eventSource.getEvents()); + } + } - this.registerSources(); -}; + /** + * Calls the right handler for a new event. + * @private + * @param {Event} inEvent Browser event. + */ + eventHandler_(inEvent) { + const type = inEvent.type; + const handler = this.eventMap_[type]; + if (handler) { + handler(inEvent); + } + } + + /** + * Setup listeners for the given events. + * @private + * @param {Array.} events List of events. + */ + addEvents_(events) { + events.forEach(function(eventName) { + listen(this.element_, eventName, this.eventHandler_, this); + }.bind(this)); + } + + /** + * Unregister listeners for the given events. + * @private + * @param {Array.} events List of events. + */ + removeEvents_(events) { + events.forEach(function(e) { + unlisten(this.element_, e, this.eventHandler_, this); + }.bind(this)); + } + + /** + * Returns a snapshot of inEvent, with writable properties. + * + * @param {Event} event Browser event. + * @param {Event|Touch} inEvent An event that contains + * properties to copy. + * @return {Object} An object containing shallow copies of + * `inEvent`'s properties. + */ + cloneEvent(event, inEvent) { + const eventCopy = {}; + for (let i = 0, ii = CLONE_PROPS.length; i < ii; i++) { + const p = CLONE_PROPS[i][0]; + eventCopy[p] = event[p] || inEvent[p] || CLONE_PROPS[i][1]; + } + + return eventCopy; + } + + // EVENTS + + + /** + * Triggers a 'pointerdown' event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + down(data, event) { + this.fireEvent(PointerEventType.POINTERDOWN, data, event); + } + + /** + * Triggers a 'pointermove' event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + move(data, event) { + this.fireEvent(PointerEventType.POINTERMOVE, data, event); + } + + /** + * Triggers a 'pointerup' event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + up(data, event) { + this.fireEvent(PointerEventType.POINTERUP, data, event); + } + + /** + * Triggers a 'pointerenter' event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + enter(data, event) { + data.bubbles = false; + this.fireEvent(PointerEventType.POINTERENTER, data, event); + } + + /** + * Triggers a 'pointerleave' event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + leave(data, event) { + data.bubbles = false; + this.fireEvent(PointerEventType.POINTERLEAVE, data, event); + } + + /** + * Triggers a 'pointerover' event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + over(data, event) { + data.bubbles = true; + this.fireEvent(PointerEventType.POINTEROVER, data, event); + } + + /** + * Triggers a 'pointerout' event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + out(data, event) { + data.bubbles = true; + this.fireEvent(PointerEventType.POINTEROUT, data, event); + } + + /** + * Triggers a 'pointercancel' event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + cancel(data, event) { + this.fireEvent(PointerEventType.POINTERCANCEL, data, event); + } + + /** + * Triggers a combination of 'pointerout' and 'pointerleave' events. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + leaveOut(data, event) { + this.out(data, event); + if (!this.contains_(data.target, data.relatedTarget)) { + this.leave(data, event); + } + } + + /** + * Triggers a combination of 'pointerover' and 'pointerevents' events. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + enterOver(data, event) { + this.over(data, event); + if (!this.contains_(data.target, data.relatedTarget)) { + this.enter(data, event); + } + } + + /** + * @private + * @param {Element} container The container element. + * @param {Element} contained The contained element. + * @return {boolean} Returns true if the container element + * contains the other element. + */ + contains_(container, contained) { + if (!container || !contained) { + return false; + } + return container.contains(contained); + } + + // EVENT CREATION AND TRACKING + /** + * Creates a new Event of type `inType`, based on the information in + * `data`. + * + * @param {string} inType A string representing the type of event to create. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + * @return {module:ol/pointer/PointerEvent} A PointerEvent of type `inType`. + */ + makeEvent(inType, data, event) { + return new PointerEvent(inType, event, data); + } + + /** + * Make and dispatch an event in one call. + * @param {string} inType A string representing the type of event. + * @param {Object} data Pointer event data. + * @param {Event} event The event. + */ + fireEvent(inType, data, event) { + const e = this.makeEvent(inType, data, event); + this.dispatchEvent(e); + } + + /** + * Creates a pointer event from a native pointer event + * and dispatches this event. + * @param {Event} event A platform event with a target. + */ + fireNativeEvent(event) { + const e = this.makeEvent(event.type, event, event); + this.dispatchEvent(e); + } + + /** + * Wrap a native mouse event into a pointer event. + * This proxy method is required for the legacy IE support. + * @param {string} eventType The pointer event type. + * @param {Event} event The event. + * @return {module:ol/pointer/PointerEvent} The wrapped event. + */ + wrapMouseEvent(eventType, event) { + const pointerEvent = this.makeEvent( + eventType, MouseSource.prepareEvent(event, this), event); + return pointerEvent; + } + + /** + * @inheritDoc + */ + disposeInternal() { + this.unregister_(); + EventTarget.prototype.disposeInternal.call(this); + } +} inherits(PointerEventHandler, EventTarget); @@ -120,323 +417,4 @@ const CLONE_PROPS = [ ]; -/** - * Set up the event sources (mouse, touch and native pointers) - * that generate pointer events. - */ -PointerEventHandler.prototype.registerSources = function() { - if (POINTER) { - this.registerSource('native', new NativeSource(this)); - } else if (MSPOINTER) { - this.registerSource('ms', new MsSource(this)); - } else { - const mouseSource = new MouseSource(this); - this.registerSource('mouse', mouseSource); - - if (TOUCH) { - this.registerSource('touch', new TouchSource(this, mouseSource)); - } - } - - // register events on the viewport element - this.register_(); -}; - - -/** - * Add a new event source that will generate pointer events. - * - * @param {string} name A name for the event source - * @param {module:ol/pointer/EventSource} source The source event. - */ -PointerEventHandler.prototype.registerSource = function(name, source) { - const s = source; - const newEvents = s.getEvents(); - - if (newEvents) { - newEvents.forEach(function(e) { - const handler = s.getHandlerForEvent(e); - - if (handler) { - this.eventMap_[e] = handler.bind(s); - } - }.bind(this)); - this.eventSourceList_.push(s); - } -}; - - -/** - * Set up the events for all registered event sources. - * @private - */ -PointerEventHandler.prototype.register_ = function() { - const l = this.eventSourceList_.length; - for (let i = 0; i < l; i++) { - const eventSource = this.eventSourceList_[i]; - this.addEvents_(eventSource.getEvents()); - } -}; - - -/** - * Remove all registered events. - * @private - */ -PointerEventHandler.prototype.unregister_ = function() { - const l = this.eventSourceList_.length; - for (let i = 0; i < l; i++) { - const eventSource = this.eventSourceList_[i]; - this.removeEvents_(eventSource.getEvents()); - } -}; - - -/** - * Calls the right handler for a new event. - * @private - * @param {Event} inEvent Browser event. - */ -PointerEventHandler.prototype.eventHandler_ = function(inEvent) { - const type = inEvent.type; - const handler = this.eventMap_[type]; - if (handler) { - handler(inEvent); - } -}; - - -/** - * Setup listeners for the given events. - * @private - * @param {Array.} events List of events. - */ -PointerEventHandler.prototype.addEvents_ = function(events) { - events.forEach(function(eventName) { - listen(this.element_, eventName, this.eventHandler_, this); - }.bind(this)); -}; - - -/** - * Unregister listeners for the given events. - * @private - * @param {Array.} events List of events. - */ -PointerEventHandler.prototype.removeEvents_ = function(events) { - events.forEach(function(e) { - unlisten(this.element_, e, this.eventHandler_, this); - }.bind(this)); -}; - - -/** - * Returns a snapshot of inEvent, with writable properties. - * - * @param {Event} event Browser event. - * @param {Event|Touch} inEvent An event that contains - * properties to copy. - * @return {Object} An object containing shallow copies of - * `inEvent`'s properties. - */ -PointerEventHandler.prototype.cloneEvent = function(event, inEvent) { - const eventCopy = {}; - for (let i = 0, ii = CLONE_PROPS.length; i < ii; i++) { - const p = CLONE_PROPS[i][0]; - eventCopy[p] = event[p] || inEvent[p] || CLONE_PROPS[i][1]; - } - - return eventCopy; -}; - - -// EVENTS - - -/** - * Triggers a 'pointerdown' event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.down = function(data, event) { - this.fireEvent(PointerEventType.POINTERDOWN, data, event); -}; - - -/** - * Triggers a 'pointermove' event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.move = function(data, event) { - this.fireEvent(PointerEventType.POINTERMOVE, data, event); -}; - - -/** - * Triggers a 'pointerup' event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.up = function(data, event) { - this.fireEvent(PointerEventType.POINTERUP, data, event); -}; - - -/** - * Triggers a 'pointerenter' event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.enter = function(data, event) { - data.bubbles = false; - this.fireEvent(PointerEventType.POINTERENTER, data, event); -}; - - -/** - * Triggers a 'pointerleave' event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.leave = function(data, event) { - data.bubbles = false; - this.fireEvent(PointerEventType.POINTERLEAVE, data, event); -}; - - -/** - * Triggers a 'pointerover' event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.over = function(data, event) { - data.bubbles = true; - this.fireEvent(PointerEventType.POINTEROVER, data, event); -}; - - -/** - * Triggers a 'pointerout' event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.out = function(data, event) { - data.bubbles = true; - this.fireEvent(PointerEventType.POINTEROUT, data, event); -}; - - -/** - * Triggers a 'pointercancel' event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.cancel = function(data, event) { - this.fireEvent(PointerEventType.POINTERCANCEL, data, event); -}; - - -/** - * Triggers a combination of 'pointerout' and 'pointerleave' events. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.leaveOut = function(data, event) { - this.out(data, event); - if (!this.contains_(data.target, data.relatedTarget)) { - this.leave(data, event); - } -}; - - -/** - * Triggers a combination of 'pointerover' and 'pointerevents' events. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.enterOver = function(data, event) { - this.over(data, event); - if (!this.contains_(data.target, data.relatedTarget)) { - this.enter(data, event); - } -}; - - -/** - * @private - * @param {Element} container The container element. - * @param {Element} contained The contained element. - * @return {boolean} Returns true if the container element - * contains the other element. - */ -PointerEventHandler.prototype.contains_ = function(container, contained) { - if (!container || !contained) { - return false; - } - return container.contains(contained); -}; - - -// EVENT CREATION AND TRACKING -/** - * Creates a new Event of type `inType`, based on the information in - * `data`. - * - * @param {string} inType A string representing the type of event to create. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - * @return {module:ol/pointer/PointerEvent} A PointerEvent of type `inType`. - */ -PointerEventHandler.prototype.makeEvent = function(inType, data, event) { - return new PointerEvent(inType, event, data); -}; - - -/** - * Make and dispatch an event in one call. - * @param {string} inType A string representing the type of event. - * @param {Object} data Pointer event data. - * @param {Event} event The event. - */ -PointerEventHandler.prototype.fireEvent = function(inType, data, event) { - const e = this.makeEvent(inType, data, event); - this.dispatchEvent(e); -}; - - -/** - * Creates a pointer event from a native pointer event - * and dispatches this event. - * @param {Event} event A platform event with a target. - */ -PointerEventHandler.prototype.fireNativeEvent = function(event) { - const e = this.makeEvent(event.type, event, event); - this.dispatchEvent(e); -}; - - -/** - * Wrap a native mouse event into a pointer event. - * This proxy method is required for the legacy IE support. - * @param {string} eventType The pointer event type. - * @param {Event} event The event. - * @return {module:ol/pointer/PointerEvent} The wrapped event. - */ -PointerEventHandler.prototype.wrapMouseEvent = function(eventType, event) { - const pointerEvent = this.makeEvent( - eventType, MouseSource.prepareEvent(event, this), event); - return pointerEvent; -}; - - -/** - * @inheritDoc - */ -PointerEventHandler.prototype.disposeInternal = function() { - this.unregister_(); - EventTarget.prototype.disposeInternal.call(this); -}; - - export default PointerEventHandler; diff --git a/src/ol/pointer/TouchSource.js b/src/ol/pointer/TouchSource.js index fd54196370..a9c2b05e62 100644 --- a/src/ol/pointer/TouchSource.js +++ b/src/ol/pointer/TouchSource.js @@ -43,53 +43,370 @@ import {POINTER_ID} from '../pointer/MouseSource.js'; * @param {module:ol/pointer/MouseSource} mouseSource Mouse source. * @extends {module:ol/pointer/EventSource} */ -const TouchSource = function(dispatcher, mouseSource) { - const mapping = { - 'touchstart': this.touchstart, - 'touchmove': this.touchmove, - 'touchend': this.touchend, - 'touchcancel': this.touchcancel - }; - EventSource.call(this, dispatcher, mapping); +class TouchSource { + constructor(dispatcher, mouseSource) { + const mapping = { + 'touchstart': this.touchstart, + 'touchmove': this.touchmove, + 'touchend': this.touchend, + 'touchcancel': this.touchcancel + }; + EventSource.call(this, dispatcher, mapping); - /** - * @const - * @type {!Object.} - */ - this.pointerMap = dispatcher.pointerMap; + /** + * @const + * @type {!Object.} + */ + this.pointerMap = dispatcher.pointerMap; - /** - * @const - * @type {module:ol/pointer/MouseSource} - */ - this.mouseSource = mouseSource; + /** + * @const + * @type {module:ol/pointer/MouseSource} + */ + this.mouseSource = mouseSource; + + /** + * @private + * @type {number|undefined} + */ + this.firstTouchId_ = undefined; + + /** + * @private + * @type {number} + */ + this.clickCount_ = 0; + + /** + * @private + * @type {number|undefined} + */ + this.resetId_ = undefined; + + /** + * Mouse event timeout: This should be long enough to + * ignore compat mouse events made by touch. + * @private + * @type {number} + */ + this.dedupTimeout_ = 2500; + } /** * @private - * @type {number|undefined} + * @param {Touch} inTouch The in touch. + * @return {boolean} True, if this is the primary touch. */ - this.firstTouchId_ = undefined; + isPrimaryTouch_(inTouch) { + return this.firstTouchId_ === inTouch.identifier; + } + + /** + * Set primary touch if there are no pointers, or the only pointer is the mouse. + * @param {Touch} inTouch The in touch. + * @private + */ + setPrimaryTouch_(inTouch) { + const count = Object.keys(this.pointerMap).length; + if (count === 0 || (count === 1 && POINTER_ID.toString() in this.pointerMap)) { + this.firstTouchId_ = inTouch.identifier; + this.cancelResetClickCount_(); + } + } /** * @private - * @type {number} + * @param {PointerEvent} inPointer The in pointer object. */ - this.clickCount_ = 0; + removePrimaryPointer_(inPointer) { + if (inPointer.isPrimary) { + this.firstTouchId_ = undefined; + this.resetClickCount_(); + } + } /** * @private - * @type {number|undefined} */ - this.resetId_ = undefined; + resetClickCount_() { + this.resetId_ = setTimeout( + this.resetClickCountHandler_.bind(this), + CLICK_COUNT_TIMEOUT); + } /** - * Mouse event timeout: This should be long enough to - * ignore compat mouse events made by touch. * @private - * @type {number} */ - this.dedupTimeout_ = 2500; -}; + resetClickCountHandler_() { + this.clickCount_ = 0; + this.resetId_ = undefined; + } + + /** + * @private + */ + cancelResetClickCount_() { + if (this.resetId_ !== undefined) { + clearTimeout(this.resetId_); + } + } + + /** + * @private + * @param {TouchEvent} browserEvent Browser event + * @param {Touch} inTouch Touch event + * @return {PointerEvent} A pointer object. + */ + touchToPointer_(browserEvent, inTouch) { + const e = this.dispatcher.cloneEvent(browserEvent, inTouch); + // Spec specifies that pointerId 1 is reserved for Mouse. + // Touch identifiers can start at 0. + // Add 2 to the touch identifier for compatibility. + e.pointerId = inTouch.identifier + 2; + // TODO: check if this is necessary? + //e.target = findTarget(e); + e.bubbles = true; + e.cancelable = true; + e.detail = this.clickCount_; + e.button = 0; + e.buttons = 1; + e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0; + e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0; + e.pressure = inTouch.webkitForce || inTouch.force || 0.5; + e.isPrimary = this.isPrimaryTouch_(inTouch); + e.pointerType = POINTER_TYPE; + + // make sure that the properties that are different for + // each `Touch` object are not copied from the BrowserEvent object + e.clientX = inTouch.clientX; + e.clientY = inTouch.clientY; + e.screenX = inTouch.screenX; + e.screenY = inTouch.screenY; + + return e; + } + + /** + * @private + * @param {TouchEvent} inEvent Touch event + * @param {function(TouchEvent, PointerEvent)} inFunction In function. + */ + processTouches_(inEvent, inFunction) { + const touches = Array.prototype.slice.call(inEvent.changedTouches); + const count = touches.length; + function preventDefault() { + inEvent.preventDefault(); + } + for (let i = 0; i < count; ++i) { + const pointer = this.touchToPointer_(inEvent, touches[i]); + // forward touch preventDefaults + pointer.preventDefault = preventDefault; + inFunction.call(this, inEvent, pointer); + } + } + + /** + * @private + * @param {TouchList} touchList The touch list. + * @param {number} searchId Search identifier. + * @return {boolean} True, if the `Touch` with the given id is in the list. + */ + findTouch_(touchList, searchId) { + const l = touchList.length; + for (let i = 0; i < l; i++) { + const touch = touchList[i]; + if (touch.identifier === searchId) { + return true; + } + } + return false; + } + + /** + * In some instances, a touchstart can happen without a touchend. This + * leaves the pointermap in a broken state. + * Therefore, on every touchstart, we remove the touches that did not fire a + * touchend event. + * To keep state globally consistent, we fire a pointercancel for + * this "abandoned" touch + * + * @private + * @param {TouchEvent} inEvent The in event. + */ + vacuumTouches_(inEvent) { + const touchList = inEvent.touches; + // pointerMap.getCount() should be < touchList.length here, + // as the touchstart has not been processed yet. + const keys = Object.keys(this.pointerMap); + const count = keys.length; + if (count >= touchList.length) { + const d = []; + for (let i = 0; i < count; ++i) { + const key = keys[i]; + const value = this.pointerMap[key]; + // Never remove pointerId == 1, which is mouse. + // Touch identifiers are 2 smaller than their pointerId, which is the + // index in pointermap. + if (key != POINTER_ID && !this.findTouch_(touchList, key - 2)) { + d.push(value.out); + } + } + for (let i = 0; i < d.length; ++i) { + this.cancelOut_(inEvent, d[i]); + } + } + } + + /** + * Handler for `touchstart`, triggers `pointerover`, + * `pointerenter` and `pointerdown` events. + * + * @param {TouchEvent} inEvent The in event. + */ + touchstart(inEvent) { + this.vacuumTouches_(inEvent); + this.setPrimaryTouch_(inEvent.changedTouches[0]); + this.dedupSynthMouse_(inEvent); + this.clickCount_++; + this.processTouches_(inEvent, this.overDown_); + } + + /** + * @private + * @param {TouchEvent} browserEvent The event. + * @param {PointerEvent} inPointer The in pointer object. + */ + overDown_(browserEvent, inPointer) { + this.pointerMap[inPointer.pointerId] = { + target: inPointer.target, + out: inPointer, + outTarget: inPointer.target + }; + this.dispatcher.over(inPointer, browserEvent); + this.dispatcher.enter(inPointer, browserEvent); + this.dispatcher.down(inPointer, browserEvent); + } + + /** + * Handler for `touchmove`. + * + * @param {TouchEvent} inEvent The in event. + */ + touchmove(inEvent) { + inEvent.preventDefault(); + this.processTouches_(inEvent, this.moveOverOut_); + } + + /** + * @private + * @param {TouchEvent} browserEvent The event. + * @param {PointerEvent} inPointer The in pointer. + */ + moveOverOut_(browserEvent, inPointer) { + const event = inPointer; + const pointer = this.pointerMap[event.pointerId]; + // a finger drifted off the screen, ignore it + if (!pointer) { + return; + } + const outEvent = pointer.out; + const outTarget = pointer.outTarget; + this.dispatcher.move(event, browserEvent); + if (outEvent && outTarget !== event.target) { + outEvent.relatedTarget = event.target; + event.relatedTarget = outTarget; + // recover from retargeting by shadow + outEvent.target = outTarget; + if (event.target) { + this.dispatcher.leaveOut(outEvent, browserEvent); + this.dispatcher.enterOver(event, browserEvent); + } else { + // clean up case when finger leaves the screen + event.target = outTarget; + event.relatedTarget = null; + this.cancelOut_(browserEvent, event); + } + } + pointer.out = event; + pointer.outTarget = event.target; + } + + /** + * Handler for `touchend`, triggers `pointerup`, + * `pointerout` and `pointerleave` events. + * + * @param {TouchEvent} inEvent The event. + */ + touchend(inEvent) { + this.dedupSynthMouse_(inEvent); + this.processTouches_(inEvent, this.upOut_); + } + + /** + * @private + * @param {TouchEvent} browserEvent An event. + * @param {PointerEvent} inPointer The inPointer object. + */ + upOut_(browserEvent, inPointer) { + this.dispatcher.up(inPointer, browserEvent); + this.dispatcher.out(inPointer, browserEvent); + this.dispatcher.leave(inPointer, browserEvent); + this.cleanUpPointer_(inPointer); + } + + /** + * Handler for `touchcancel`, triggers `pointercancel`, + * `pointerout` and `pointerleave` events. + * + * @param {TouchEvent} inEvent The in event. + */ + touchcancel(inEvent) { + this.processTouches_(inEvent, this.cancelOut_); + } + + /** + * @private + * @param {TouchEvent} browserEvent The event. + * @param {PointerEvent} inPointer The in pointer. + */ + cancelOut_(browserEvent, inPointer) { + this.dispatcher.cancel(inPointer, browserEvent); + this.dispatcher.out(inPointer, browserEvent); + this.dispatcher.leave(inPointer, browserEvent); + this.cleanUpPointer_(inPointer); + } + + /** + * @private + * @param {PointerEvent} inPointer The inPointer object. + */ + cleanUpPointer_(inPointer) { + delete this.pointerMap[inPointer.pointerId]; + this.removePrimaryPointer_(inPointer); + } + + /** + * Prevent synth mouse events from creating pointer events. + * + * @private + * @param {TouchEvent} inEvent The in event. + */ + dedupSynthMouse_(inEvent) { + const lts = this.mouseSource.lastTouches; + const t = inEvent.changedTouches[0]; + // only the primary finger will synth mouse events + if (this.isPrimaryTouch_(t)) { + // remember x/y of last touch + const lt = [t.clientX, t.clientY]; + lts.push(lt); + + setTimeout(function() { + // remove touch after timeout + remove(lts, lt); + }, this.dedupTimeout_); + } + } +} inherits(TouchSource, EventSource); @@ -105,337 +422,4 @@ const CLICK_COUNT_TIMEOUT = 200; */ const POINTER_TYPE = 'touch'; -/** - * @private - * @param {Touch} inTouch The in touch. - * @return {boolean} True, if this is the primary touch. - */ -TouchSource.prototype.isPrimaryTouch_ = function(inTouch) { - return this.firstTouchId_ === inTouch.identifier; -}; - - -/** - * Set primary touch if there are no pointers, or the only pointer is the mouse. - * @param {Touch} inTouch The in touch. - * @private - */ -TouchSource.prototype.setPrimaryTouch_ = function(inTouch) { - const count = Object.keys(this.pointerMap).length; - if (count === 0 || (count === 1 && POINTER_ID.toString() in this.pointerMap)) { - this.firstTouchId_ = inTouch.identifier; - this.cancelResetClickCount_(); - } -}; - - -/** - * @private - * @param {PointerEvent} inPointer The in pointer object. - */ -TouchSource.prototype.removePrimaryPointer_ = function(inPointer) { - if (inPointer.isPrimary) { - this.firstTouchId_ = undefined; - this.resetClickCount_(); - } -}; - - -/** - * @private - */ -TouchSource.prototype.resetClickCount_ = function() { - this.resetId_ = setTimeout( - this.resetClickCountHandler_.bind(this), - CLICK_COUNT_TIMEOUT); -}; - - -/** - * @private - */ -TouchSource.prototype.resetClickCountHandler_ = function() { - this.clickCount_ = 0; - this.resetId_ = undefined; -}; - - -/** - * @private - */ -TouchSource.prototype.cancelResetClickCount_ = function() { - if (this.resetId_ !== undefined) { - clearTimeout(this.resetId_); - } -}; - - -/** - * @private - * @param {TouchEvent} browserEvent Browser event - * @param {Touch} inTouch Touch event - * @return {PointerEvent} A pointer object. - */ -TouchSource.prototype.touchToPointer_ = function(browserEvent, inTouch) { - const e = this.dispatcher.cloneEvent(browserEvent, inTouch); - // Spec specifies that pointerId 1 is reserved for Mouse. - // Touch identifiers can start at 0. - // Add 2 to the touch identifier for compatibility. - e.pointerId = inTouch.identifier + 2; - // TODO: check if this is necessary? - //e.target = findTarget(e); - e.bubbles = true; - e.cancelable = true; - e.detail = this.clickCount_; - e.button = 0; - e.buttons = 1; - e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0; - e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0; - e.pressure = inTouch.webkitForce || inTouch.force || 0.5; - e.isPrimary = this.isPrimaryTouch_(inTouch); - e.pointerType = POINTER_TYPE; - - // make sure that the properties that are different for - // each `Touch` object are not copied from the BrowserEvent object - e.clientX = inTouch.clientX; - e.clientY = inTouch.clientY; - e.screenX = inTouch.screenX; - e.screenY = inTouch.screenY; - - return e; -}; - - -/** - * @private - * @param {TouchEvent} inEvent Touch event - * @param {function(TouchEvent, PointerEvent)} inFunction In function. - */ -TouchSource.prototype.processTouches_ = function(inEvent, inFunction) { - const touches = Array.prototype.slice.call(inEvent.changedTouches); - const count = touches.length; - function preventDefault() { - inEvent.preventDefault(); - } - for (let i = 0; i < count; ++i) { - const pointer = this.touchToPointer_(inEvent, touches[i]); - // forward touch preventDefaults - pointer.preventDefault = preventDefault; - inFunction.call(this, inEvent, pointer); - } -}; - - -/** - * @private - * @param {TouchList} touchList The touch list. - * @param {number} searchId Search identifier. - * @return {boolean} True, if the `Touch` with the given id is in the list. - */ -TouchSource.prototype.findTouch_ = function(touchList, searchId) { - const l = touchList.length; - for (let i = 0; i < l; i++) { - const touch = touchList[i]; - if (touch.identifier === searchId) { - return true; - } - } - return false; -}; - - -/** - * In some instances, a touchstart can happen without a touchend. This - * leaves the pointermap in a broken state. - * Therefore, on every touchstart, we remove the touches that did not fire a - * touchend event. - * To keep state globally consistent, we fire a pointercancel for - * this "abandoned" touch - * - * @private - * @param {TouchEvent} inEvent The in event. - */ -TouchSource.prototype.vacuumTouches_ = function(inEvent) { - const touchList = inEvent.touches; - // pointerMap.getCount() should be < touchList.length here, - // as the touchstart has not been processed yet. - const keys = Object.keys(this.pointerMap); - const count = keys.length; - if (count >= touchList.length) { - const d = []; - for (let i = 0; i < count; ++i) { - const key = keys[i]; - const value = this.pointerMap[key]; - // Never remove pointerId == 1, which is mouse. - // Touch identifiers are 2 smaller than their pointerId, which is the - // index in pointermap. - if (key != POINTER_ID && !this.findTouch_(touchList, key - 2)) { - d.push(value.out); - } - } - for (let i = 0; i < d.length; ++i) { - this.cancelOut_(inEvent, d[i]); - } - } -}; - - -/** - * Handler for `touchstart`, triggers `pointerover`, - * `pointerenter` and `pointerdown` events. - * - * @param {TouchEvent} inEvent The in event. - */ -TouchSource.prototype.touchstart = function(inEvent) { - this.vacuumTouches_(inEvent); - this.setPrimaryTouch_(inEvent.changedTouches[0]); - this.dedupSynthMouse_(inEvent); - this.clickCount_++; - this.processTouches_(inEvent, this.overDown_); -}; - - -/** - * @private - * @param {TouchEvent} browserEvent The event. - * @param {PointerEvent} inPointer The in pointer object. - */ -TouchSource.prototype.overDown_ = function(browserEvent, inPointer) { - this.pointerMap[inPointer.pointerId] = { - target: inPointer.target, - out: inPointer, - outTarget: inPointer.target - }; - this.dispatcher.over(inPointer, browserEvent); - this.dispatcher.enter(inPointer, browserEvent); - this.dispatcher.down(inPointer, browserEvent); -}; - - -/** - * Handler for `touchmove`. - * - * @param {TouchEvent} inEvent The in event. - */ -TouchSource.prototype.touchmove = function(inEvent) { - inEvent.preventDefault(); - this.processTouches_(inEvent, this.moveOverOut_); -}; - - -/** - * @private - * @param {TouchEvent} browserEvent The event. - * @param {PointerEvent} inPointer The in pointer. - */ -TouchSource.prototype.moveOverOut_ = function(browserEvent, inPointer) { - const event = inPointer; - const pointer = this.pointerMap[event.pointerId]; - // a finger drifted off the screen, ignore it - if (!pointer) { - return; - } - const outEvent = pointer.out; - const outTarget = pointer.outTarget; - this.dispatcher.move(event, browserEvent); - if (outEvent && outTarget !== event.target) { - outEvent.relatedTarget = event.target; - event.relatedTarget = outTarget; - // recover from retargeting by shadow - outEvent.target = outTarget; - if (event.target) { - this.dispatcher.leaveOut(outEvent, browserEvent); - this.dispatcher.enterOver(event, browserEvent); - } else { - // clean up case when finger leaves the screen - event.target = outTarget; - event.relatedTarget = null; - this.cancelOut_(browserEvent, event); - } - } - pointer.out = event; - pointer.outTarget = event.target; -}; - - -/** - * Handler for `touchend`, triggers `pointerup`, - * `pointerout` and `pointerleave` events. - * - * @param {TouchEvent} inEvent The event. - */ -TouchSource.prototype.touchend = function(inEvent) { - this.dedupSynthMouse_(inEvent); - this.processTouches_(inEvent, this.upOut_); -}; - - -/** - * @private - * @param {TouchEvent} browserEvent An event. - * @param {PointerEvent} inPointer The inPointer object. - */ -TouchSource.prototype.upOut_ = function(browserEvent, inPointer) { - this.dispatcher.up(inPointer, browserEvent); - this.dispatcher.out(inPointer, browserEvent); - this.dispatcher.leave(inPointer, browserEvent); - this.cleanUpPointer_(inPointer); -}; - - -/** - * Handler for `touchcancel`, triggers `pointercancel`, - * `pointerout` and `pointerleave` events. - * - * @param {TouchEvent} inEvent The in event. - */ -TouchSource.prototype.touchcancel = function(inEvent) { - this.processTouches_(inEvent, this.cancelOut_); -}; - - -/** - * @private - * @param {TouchEvent} browserEvent The event. - * @param {PointerEvent} inPointer The in pointer. - */ -TouchSource.prototype.cancelOut_ = function(browserEvent, inPointer) { - this.dispatcher.cancel(inPointer, browserEvent); - this.dispatcher.out(inPointer, browserEvent); - this.dispatcher.leave(inPointer, browserEvent); - this.cleanUpPointer_(inPointer); -}; - - -/** - * @private - * @param {PointerEvent} inPointer The inPointer object. - */ -TouchSource.prototype.cleanUpPointer_ = function(inPointer) { - delete this.pointerMap[inPointer.pointerId]; - this.removePrimaryPointer_(inPointer); -}; - - -/** - * Prevent synth mouse events from creating pointer events. - * - * @private - * @param {TouchEvent} inEvent The in event. - */ -TouchSource.prototype.dedupSynthMouse_ = function(inEvent) { - const lts = this.mouseSource.lastTouches; - const t = inEvent.changedTouches[0]; - // only the primary finger will synth mouse events - if (this.isPrimaryTouch_(t)) { - // remember x/y of last touch - const lt = [t.clientX, t.clientY]; - lts.push(lt); - - setTimeout(function() { - // remove touch after timeout - remove(lts, lt); - }, this.dedupTimeout_); - } -}; export default TouchSource; diff --git a/src/ol/proj/Projection.js b/src/ol/proj/Projection.js index 72621fb66d..91ccf3a2e9 100644 --- a/src/ol/proj/Projection.js +++ b/src/ol/proj/Projection.js @@ -54,232 +54,220 @@ import {METERS_PER_UNIT} from '../proj/Units.js'; * @struct * @api */ -const Projection = function(options) { - /** - * @private - * @type {string} - */ - this.code_ = options.code; +class Projection { + constructor(options) { + /** + * @private + * @type {string} + */ + this.code_ = options.code; - /** - * Units of projected coordinates. When set to `TILE_PIXELS`, a - * `this.extent_` and `this.worldExtent_` must be configured properly for each - * tile. - * @private - * @type {module:ol/proj/Units} - */ - this.units_ = /** @type {module:ol/proj/Units} */ (options.units); + /** + * Units of projected coordinates. When set to `TILE_PIXELS`, a + * `this.extent_` and `this.worldExtent_` must be configured properly for each + * tile. + * @private + * @type {module:ol/proj/Units} + */ + this.units_ = /** @type {module:ol/proj/Units} */ (options.units); - /** - * Validity extent of the projection in projected coordinates. For projections - * with `TILE_PIXELS` units, this is the extent of the tile in - * tile pixel space. - * @private - * @type {module:ol/extent~Extent} - */ - this.extent_ = options.extent !== undefined ? options.extent : null; + /** + * Validity extent of the projection in projected coordinates. For projections + * with `TILE_PIXELS` units, this is the extent of the tile in + * tile pixel space. + * @private + * @type {module:ol/extent~Extent} + */ + this.extent_ = options.extent !== undefined ? options.extent : null; - /** - * Extent of the world in EPSG:4326. For projections with - * `TILE_PIXELS` units, this is the extent of the tile in - * projected coordinate space. - * @private - * @type {module:ol/extent~Extent} - */ - this.worldExtent_ = options.worldExtent !== undefined ? - options.worldExtent : null; + /** + * Extent of the world in EPSG:4326. For projections with + * `TILE_PIXELS` units, this is the extent of the tile in + * projected coordinate space. + * @private + * @type {module:ol/extent~Extent} + */ + this.worldExtent_ = options.worldExtent !== undefined ? + options.worldExtent : null; - /** - * @private - * @type {string} - */ - this.axisOrientation_ = options.axisOrientation !== undefined ? - options.axisOrientation : 'enu'; + /** + * @private + * @type {string} + */ + this.axisOrientation_ = options.axisOrientation !== undefined ? + options.axisOrientation : 'enu'; - /** - * @private - * @type {boolean} - */ - this.global_ = options.global !== undefined ? options.global : false; + /** + * @private + * @type {boolean} + */ + this.global_ = options.global !== undefined ? options.global : false; - /** - * @private - * @type {boolean} - */ - this.canWrapX_ = !!(this.global_ && this.extent_); + /** + * @private + * @type {boolean} + */ + this.canWrapX_ = !!(this.global_ && this.extent_); - /** - * @private - * @type {function(number, module:ol/coordinate~Coordinate):number|undefined} - */ - this.getPointResolutionFunc_ = options.getPointResolution; + /** + * @private + * @type {function(number, module:ol/coordinate~Coordinate):number|undefined} + */ + this.getPointResolutionFunc_ = options.getPointResolution; - /** - * @private - * @type {module:ol/tilegrid/TileGrid} - */ - this.defaultTileGrid_ = null; + /** + * @private + * @type {module:ol/tilegrid/TileGrid} + */ + this.defaultTileGrid_ = null; - /** - * @private - * @type {number|undefined} - */ - this.metersPerUnit_ = options.metersPerUnit; -}; + /** + * @private + * @type {number|undefined} + */ + this.metersPerUnit_ = options.metersPerUnit; + } + /** + * @return {boolean} The projection is suitable for wrapping the x-axis + */ + canWrapX() { + return this.canWrapX_; + } -/** - * @return {boolean} The projection is suitable for wrapping the x-axis - */ -Projection.prototype.canWrapX = function() { - return this.canWrapX_; -}; + /** + * Get the code for this projection, e.g. 'EPSG:4326'. + * @return {string} Code. + * @api + */ + getCode() { + return this.code_; + } + /** + * Get the validity extent for this projection. + * @return {module:ol/extent~Extent} Extent. + * @api + */ + getExtent() { + return this.extent_; + } -/** - * Get the code for this projection, e.g. 'EPSG:4326'. - * @return {string} Code. + /** + * Get the units of this projection. + * @return {module:ol/proj/Units} Units. + * @api + */ + getUnits() { + return this.units_; + } + + /** + * Get the amount of meters per unit of this projection. If the projection is + * not configured with `metersPerUnit` or a units identifier, the return is + * `undefined`. + * @return {number|undefined} Meters. + * @api + */ + getMetersPerUnit() { + return this.metersPerUnit_ || METERS_PER_UNIT[this.units_]; + } + + /** + * Get the world extent for this projection. + * @return {module:ol/extent~Extent} Extent. + * @api + */ + getWorldExtent() { + return this.worldExtent_; + } + + /** + * Get the axis orientation of this projection. + * Example values are: + * enu - the default easting, northing, elevation. + * neu - northing, easting, up - useful for "lat/long" geographic coordinates, + * or south orientated transverse mercator. + * wnu - westing, northing, up - some planetary coordinate systems have + * "west positive" coordinate systems + * @return {string} Axis orientation. + * @api + */ + getAxisOrientation() { + return this.axisOrientation_; + } + + /** + * Is this projection a global projection which spans the whole world? + * @return {boolean} Whether the projection is global. + * @api + */ + isGlobal() { + return this.global_; + } + + /** + * Set if the projection is a global projection which spans the whole world + * @param {boolean} global Whether the projection is global. * @api */ -Projection.prototype.getCode = function() { - return this.code_; -}; + setGlobal(global) { + this.global_ = global; + this.canWrapX_ = !!(global && this.extent_); + } + /** + * @return {module:ol/tilegrid/TileGrid} The default tile grid. + */ + getDefaultTileGrid() { + return this.defaultTileGrid_; + } -/** - * Get the validity extent for this projection. - * @return {module:ol/extent~Extent} Extent. - * @api - */ -Projection.prototype.getExtent = function() { - return this.extent_; -}; + /** + * @param {module:ol/tilegrid/TileGrid} tileGrid The default tile grid. + */ + setDefaultTileGrid(tileGrid) { + this.defaultTileGrid_ = tileGrid; + } + /** + * Set the validity extent for this projection. + * @param {module:ol/extent~Extent} extent Extent. + * @api + */ + setExtent(extent) { + this.extent_ = extent; + this.canWrapX_ = !!(this.global_ && extent); + } -/** - * Get the units of this projection. - * @return {module:ol/proj/Units} Units. - * @api - */ -Projection.prototype.getUnits = function() { - return this.units_; -}; + /** + * Set the world extent for this projection. + * @param {module:ol/extent~Extent} worldExtent World extent + * [minlon, minlat, maxlon, maxlat]. + * @api + */ + setWorldExtent(worldExtent) { + this.worldExtent_ = worldExtent; + } + /** + * Set the getPointResolution function (see {@link module:ol/proj~getPointResolution} + * for this projection. + * @param {function(number, module:ol/coordinate~Coordinate):number} func Function + * @api + */ + setGetPointResolution(func) { + this.getPointResolutionFunc_ = func; + } -/** - * Get the amount of meters per unit of this projection. If the projection is - * not configured with `metersPerUnit` or a units identifier, the return is - * `undefined`. - * @return {number|undefined} Meters. - * @api - */ -Projection.prototype.getMetersPerUnit = function() { - return this.metersPerUnit_ || METERS_PER_UNIT[this.units_]; -}; + /** + * Get the custom point resolution function for this projection (if set). + * @return {function(number, module:ol/coordinate~Coordinate):number|undefined} The custom point + * resolution function (if set). + */ + getPointResolutionFunc() { + return this.getPointResolutionFunc_; + } +} - -/** - * Get the world extent for this projection. - * @return {module:ol/extent~Extent} Extent. - * @api - */ -Projection.prototype.getWorldExtent = function() { - return this.worldExtent_; -}; - - -/** - * Get the axis orientation of this projection. - * Example values are: - * enu - the default easting, northing, elevation. - * neu - northing, easting, up - useful for "lat/long" geographic coordinates, - * or south orientated transverse mercator. - * wnu - westing, northing, up - some planetary coordinate systems have - * "west positive" coordinate systems - * @return {string} Axis orientation. - * @api - */ -Projection.prototype.getAxisOrientation = function() { - return this.axisOrientation_; -}; - - -/** - * Is this projection a global projection which spans the whole world? - * @return {boolean} Whether the projection is global. - * @api - */ -Projection.prototype.isGlobal = function() { - return this.global_; -}; - - -/** -* Set if the projection is a global projection which spans the whole world -* @param {boolean} global Whether the projection is global. -* @api -*/ -Projection.prototype.setGlobal = function(global) { - this.global_ = global; - this.canWrapX_ = !!(global && this.extent_); -}; - - -/** - * @return {module:ol/tilegrid/TileGrid} The default tile grid. - */ -Projection.prototype.getDefaultTileGrid = function() { - return this.defaultTileGrid_; -}; - - -/** - * @param {module:ol/tilegrid/TileGrid} tileGrid The default tile grid. - */ -Projection.prototype.setDefaultTileGrid = function(tileGrid) { - this.defaultTileGrid_ = tileGrid; -}; - - -/** - * Set the validity extent for this projection. - * @param {module:ol/extent~Extent} extent Extent. - * @api - */ -Projection.prototype.setExtent = function(extent) { - this.extent_ = extent; - this.canWrapX_ = !!(this.global_ && extent); -}; - - -/** - * Set the world extent for this projection. - * @param {module:ol/extent~Extent} worldExtent World extent - * [minlon, minlat, maxlon, maxlat]. - * @api - */ -Projection.prototype.setWorldExtent = function(worldExtent) { - this.worldExtent_ = worldExtent; -}; - - -/** - * Set the getPointResolution function (see {@link module:ol/proj~getPointResolution} - * for this projection. - * @param {function(number, module:ol/coordinate~Coordinate):number} func Function - * @api - */ -Projection.prototype.setGetPointResolution = function(func) { - this.getPointResolutionFunc_ = func; -}; - - -/** - * Get the custom point resolution function for this projection (if set). - * @return {function(number, module:ol/coordinate~Coordinate):number|undefined} The custom point - * resolution function (if set). - */ -Projection.prototype.getPointResolutionFunc = function() { - return this.getPointResolutionFunc_; -}; export default Projection; diff --git a/src/ol/render/Box.js b/src/ol/render/Box.js index 31f0811328..59892c8cd5 100644 --- a/src/ol/render/Box.js +++ b/src/ol/render/Box.js @@ -12,123 +12,121 @@ import Polygon from '../geom/Polygon.js'; * @extends {module:ol/Disposable} * @param {string} className CSS class name. */ -const RenderBox = function(className) { +class RenderBox { + constructor(className) { - /** - * @type {module:ol/geom/Polygon} - * @private - */ - this.geometry_ = null; + /** + * @type {module:ol/geom/Polygon} + * @private + */ + this.geometry_ = null; - /** - * @type {HTMLDivElement} - * @private - */ - this.element_ = /** @type {HTMLDivElement} */ (document.createElement('div')); - this.element_.style.position = 'absolute'; - this.element_.className = 'ol-box ' + className; + /** + * @type {HTMLDivElement} + * @private + */ + this.element_ = /** @type {HTMLDivElement} */ (document.createElement('div')); + this.element_.style.position = 'absolute'; + this.element_.className = 'ol-box ' + className; - /** - * @private - * @type {module:ol/PluggableMap} - */ - this.map_ = null; + /** + * @private + * @type {module:ol/PluggableMap} + */ + this.map_ = null; - /** - * @private - * @type {module:ol~Pixel} - */ - this.startPixel_ = null; + /** + * @private + * @type {module:ol~Pixel} + */ + this.startPixel_ = null; - /** - * @private - * @type {module:ol~Pixel} - */ - this.endPixel_ = null; + /** + * @private + * @type {module:ol~Pixel} + */ + this.endPixel_ = null; -}; + } + + /** + * @inheritDoc + */ + disposeInternal() { + this.setMap(null); + } + + /** + * @private + */ + render_() { + const startPixel = this.startPixel_; + const endPixel = this.endPixel_; + const px = 'px'; + const style = this.element_.style; + style.left = Math.min(startPixel[0], endPixel[0]) + px; + style.top = Math.min(startPixel[1], endPixel[1]) + px; + style.width = Math.abs(endPixel[0] - startPixel[0]) + px; + style.height = Math.abs(endPixel[1] - startPixel[1]) + px; + } + + /** + * @param {module:ol/PluggableMap} map Map. + */ + setMap(map) { + if (this.map_) { + this.map_.getOverlayContainer().removeChild(this.element_); + const style = this.element_.style; + style.left = style.top = style.width = style.height = 'inherit'; + } + this.map_ = map; + if (this.map_) { + this.map_.getOverlayContainer().appendChild(this.element_); + } + } + + /** + * @param {module:ol~Pixel} startPixel Start pixel. + * @param {module:ol~Pixel} endPixel End pixel. + */ + setPixels(startPixel, endPixel) { + this.startPixel_ = startPixel; + this.endPixel_ = endPixel; + this.createOrUpdateGeometry(); + this.render_(); + } + + /** + * Creates or updates the cached geometry. + */ + createOrUpdateGeometry() { + const startPixel = this.startPixel_; + const endPixel = this.endPixel_; + const pixels = [ + startPixel, + [startPixel[0], endPixel[1]], + endPixel, + [endPixel[0], startPixel[1]] + ]; + const coordinates = pixels.map(this.map_.getCoordinateFromPixel, this.map_); + // close the polygon + coordinates[4] = coordinates[0].slice(); + if (!this.geometry_) { + this.geometry_ = new Polygon([coordinates]); + } else { + this.geometry_.setCoordinates([coordinates]); + } + } + + /** + * @return {module:ol/geom/Polygon} Geometry. + */ + getGeometry() { + return this.geometry_; + } +} inherits(RenderBox, Disposable); -/** - * @inheritDoc - */ -RenderBox.prototype.disposeInternal = function() { - this.setMap(null); -}; - - -/** - * @private - */ -RenderBox.prototype.render_ = function() { - const startPixel = this.startPixel_; - const endPixel = this.endPixel_; - const px = 'px'; - const style = this.element_.style; - style.left = Math.min(startPixel[0], endPixel[0]) + px; - style.top = Math.min(startPixel[1], endPixel[1]) + px; - style.width = Math.abs(endPixel[0] - startPixel[0]) + px; - style.height = Math.abs(endPixel[1] - startPixel[1]) + px; -}; - - -/** - * @param {module:ol/PluggableMap} map Map. - */ -RenderBox.prototype.setMap = function(map) { - if (this.map_) { - this.map_.getOverlayContainer().removeChild(this.element_); - const style = this.element_.style; - style.left = style.top = style.width = style.height = 'inherit'; - } - this.map_ = map; - if (this.map_) { - this.map_.getOverlayContainer().appendChild(this.element_); - } -}; - - -/** - * @param {module:ol~Pixel} startPixel Start pixel. - * @param {module:ol~Pixel} endPixel End pixel. - */ -RenderBox.prototype.setPixels = function(startPixel, endPixel) { - this.startPixel_ = startPixel; - this.endPixel_ = endPixel; - this.createOrUpdateGeometry(); - this.render_(); -}; - - -/** - * Creates or updates the cached geometry. - */ -RenderBox.prototype.createOrUpdateGeometry = function() { - const startPixel = this.startPixel_; - const endPixel = this.endPixel_; - const pixels = [ - startPixel, - [startPixel[0], endPixel[1]], - endPixel, - [endPixel[0], startPixel[1]] - ]; - const coordinates = pixels.map(this.map_.getCoordinateFromPixel, this.map_); - // close the polygon - coordinates[4] = coordinates[0].slice(); - if (!this.geometry_) { - this.geometry_ = new Polygon([coordinates]); - } else { - this.geometry_.setCoordinates([coordinates]); - } -}; - - -/** - * @return {module:ol/geom/Polygon} Geometry. - */ -RenderBox.prototype.getGeometry = function() { - return this.geometry_; -}; export default RenderBox; diff --git a/src/ol/render/Feature.js b/src/ol/render/Feature.js index 8fde1792ea..c305f03eee 100644 --- a/src/ol/render/Feature.js +++ b/src/ol/render/Feature.js @@ -25,56 +25,212 @@ import {create as createTransform, compose as composeTransform} from '../transfo * @param {Object.} properties Properties. * @param {number|string|undefined} id Feature id. */ -const RenderFeature = function(type, flatCoordinates, ends, properties, id) { - /** - * @private - * @type {module:ol/extent~Extent|undefined} - */ - this.extent_; +class RenderFeature { + constructor(type, flatCoordinates, ends, properties, id) { + /** + * @private + * @type {module:ol/extent~Extent|undefined} + */ + this.extent_; - /** - * @private - * @type {number|string|undefined} - */ - this.id_ = id; + /** + * @private + * @type {number|string|undefined} + */ + this.id_ = id; - /** - * @private - * @type {module:ol/geom/GeometryType} - */ - this.type_ = type; + /** + * @private + * @type {module:ol/geom/GeometryType} + */ + this.type_ = type; - /** - * @private - * @type {Array.} - */ - this.flatCoordinates_ = flatCoordinates; + /** + * @private + * @type {Array.} + */ + this.flatCoordinates_ = flatCoordinates; - /** - * @private - * @type {Array.} - */ - this.flatInteriorPoints_ = null; + /** + * @private + * @type {Array.} + */ + this.flatInteriorPoints_ = null; - /** - * @private - * @type {Array.} - */ - this.flatMidpoints_ = null; + /** + * @private + * @type {Array.} + */ + this.flatMidpoints_ = null; - /** - * @private - * @type {Array.|Array.>} - */ - this.ends_ = ends; + /** + * @private + * @type {Array.|Array.>} + */ + this.ends_ = ends; - /** - * @private - * @type {Object.} - */ - this.properties_ = properties; + /** + * @private + * @type {Object.} + */ + this.properties_ = properties; -}; + } + + /** + * Get a feature property by its key. + * @param {string} key Key + * @return {*} Value for the requested key. + * @api + */ + get(key) { + return this.properties_[key]; + } + + /** + * Get the extent of this feature's geometry. + * @return {module:ol/extent~Extent} Extent. + * @api + */ + getExtent() { + if (!this.extent_) { + this.extent_ = this.type_ === GeometryType.POINT ? + createOrUpdateFromCoordinate(this.flatCoordinates_) : + createOrUpdateFromFlatCoordinates( + this.flatCoordinates_, 0, this.flatCoordinates_.length, 2); + + } + return this.extent_; + } + + /** + * @return {Array.} Flat interior points. + */ + getFlatInteriorPoint() { + if (!this.flatInteriorPoints_) { + const flatCenter = getCenter(this.getExtent()); + this.flatInteriorPoints_ = getInteriorPointOfArray( + this.flatCoordinates_, 0, this.ends_, 2, flatCenter, 0); + } + return this.flatInteriorPoints_; + } + + /** + * @return {Array.} Flat interior points. + */ + getFlatInteriorPoints() { + if (!this.flatInteriorPoints_) { + const flatCenters = linearRingssCenter( + this.flatCoordinates_, 0, this.ends_, 2); + this.flatInteriorPoints_ = getInteriorPointsOfMultiArray( + this.flatCoordinates_, 0, this.ends_, 2, flatCenters); + } + return this.flatInteriorPoints_; + } + + /** + * @return {Array.} Flat midpoint. + */ + getFlatMidpoint() { + if (!this.flatMidpoints_) { + this.flatMidpoints_ = interpolatePoint( + this.flatCoordinates_, 0, this.flatCoordinates_.length, 2, 0.5); + } + return this.flatMidpoints_; + } + + /** + * @return {Array.} Flat midpoints. + */ + getFlatMidpoints() { + if (!this.flatMidpoints_) { + this.flatMidpoints_ = []; + const flatCoordinates = this.flatCoordinates_; + let offset = 0; + const ends = this.ends_; + for (let i = 0, ii = ends.length; i < ii; ++i) { + const end = ends[i]; + const midpoint = interpolatePoint( + flatCoordinates, offset, end, 2, 0.5); + extend(this.flatMidpoints_, midpoint); + offset = end; + } + } + return this.flatMidpoints_; + } + + /** + * Get the feature identifier. This is a stable identifier for the feature and + * is set when reading data from a remote source. + * @return {number|string|undefined} Id. + * @api + */ + getId() { + return this.id_; + } + + /** + * @return {Array.} Flat coordinates. + */ + getOrientedFlatCoordinates() { + return this.flatCoordinates_; + } + + /** + * For API compatibility with {@link module:ol/Feature~Feature}, this method is useful when + * determining the geometry type in style function (see {@link #getType}). + * @return {module:ol/render/Feature} Feature. + * @api + */ + getGeometry() { + return this; + } + + /** + * Get the feature properties. + * @return {Object.} Feature properties. + * @api + */ + getProperties() { + return this.properties_; + } + + /** + * @return {number} Stride. + */ + getStride() { + return 2; + } + + /** + * Get the type of this feature's geometry. + * @return {module:ol/geom/GeometryType} Geometry type. + * @api + */ + getType() { + return this.type_; + } + + /** + * Transform geometry coordinates from tile pixel space to projected. + * The SRS of the source and destination are expected to be the same. + * + * @param {module:ol/proj~ProjectionLike} source The current projection + * @param {module:ol/proj~ProjectionLike} destination The desired projection. + */ + transform(source, destination) { + source = getProjection(source); + const pixelExtent = source.getExtent(); + const projectedExtent = source.getWorldExtent(); + const scale = getHeight(projectedExtent) / getHeight(pixelExtent); + composeTransform(tmpTransform, + projectedExtent[0], projectedExtent[3], + scale, -scale, 0, + 0, 0); + transform2D(this.flatCoordinates_, 0, this.flatCoordinates_.length, 2, + tmpTransform, this.flatCoordinates_); + } +} /** @@ -83,17 +239,6 @@ const RenderFeature = function(type, flatCoordinates, ends, properties, id) { const tmpTransform = createTransform(); -/** - * Get a feature property by its key. - * @param {string} key Key - * @return {*} Value for the requested key. - * @api - */ -RenderFeature.prototype.get = function(key) { - return this.properties_[key]; -}; - - /** * @return {Array.|Array.>} Ends or endss. */ @@ -103,101 +248,6 @@ RenderFeature.prototype.getEndss = function() { }; -/** - * Get the extent of this feature's geometry. - * @return {module:ol/extent~Extent} Extent. - * @api - */ -RenderFeature.prototype.getExtent = function() { - if (!this.extent_) { - this.extent_ = this.type_ === GeometryType.POINT ? - createOrUpdateFromCoordinate(this.flatCoordinates_) : - createOrUpdateFromFlatCoordinates( - this.flatCoordinates_, 0, this.flatCoordinates_.length, 2); - - } - return this.extent_; -}; - - -/** - * @return {Array.} Flat interior points. - */ -RenderFeature.prototype.getFlatInteriorPoint = function() { - if (!this.flatInteriorPoints_) { - const flatCenter = getCenter(this.getExtent()); - this.flatInteriorPoints_ = getInteriorPointOfArray( - this.flatCoordinates_, 0, this.ends_, 2, flatCenter, 0); - } - return this.flatInteriorPoints_; -}; - - -/** - * @return {Array.} Flat interior points. - */ -RenderFeature.prototype.getFlatInteriorPoints = function() { - if (!this.flatInteriorPoints_) { - const flatCenters = linearRingssCenter( - this.flatCoordinates_, 0, this.ends_, 2); - this.flatInteriorPoints_ = getInteriorPointsOfMultiArray( - this.flatCoordinates_, 0, this.ends_, 2, flatCenters); - } - return this.flatInteriorPoints_; -}; - - -/** - * @return {Array.} Flat midpoint. - */ -RenderFeature.prototype.getFlatMidpoint = function() { - if (!this.flatMidpoints_) { - this.flatMidpoints_ = interpolatePoint( - this.flatCoordinates_, 0, this.flatCoordinates_.length, 2, 0.5); - } - return this.flatMidpoints_; -}; - - -/** - * @return {Array.} Flat midpoints. - */ -RenderFeature.prototype.getFlatMidpoints = function() { - if (!this.flatMidpoints_) { - this.flatMidpoints_ = []; - const flatCoordinates = this.flatCoordinates_; - let offset = 0; - const ends = this.ends_; - for (let i = 0, ii = ends.length; i < ii; ++i) { - const end = ends[i]; - const midpoint = interpolatePoint( - flatCoordinates, offset, end, 2, 0.5); - extend(this.flatMidpoints_, midpoint); - offset = end; - } - } - return this.flatMidpoints_; -}; - -/** - * Get the feature identifier. This is a stable identifier for the feature and - * is set when reading data from a remote source. - * @return {number|string|undefined} Id. - * @api - */ -RenderFeature.prototype.getId = function() { - return this.id_; -}; - - -/** - * @return {Array.} Flat coordinates. - */ -RenderFeature.prototype.getOrientedFlatCoordinates = function() { - return this.flatCoordinates_; -}; - - /** * @return {Array.} Flat coordinates. */ @@ -205,27 +255,6 @@ RenderFeature.prototype.getFlatCoordinates = RenderFeature.prototype.getOrientedFlatCoordinates; -/** - * For API compatibility with {@link module:ol/Feature~Feature}, this method is useful when - * determining the geometry type in style function (see {@link #getType}). - * @return {module:ol/render/Feature} Feature. - * @api - */ -RenderFeature.prototype.getGeometry = function() { - return this; -}; - - -/** - * Get the feature properties. - * @return {Object.} Feature properties. - * @api - */ -RenderFeature.prototype.getProperties = function() { - return this.properties_; -}; - - /** * Get the feature for working with its geometry. * @return {module:ol/render/Feature} Feature. @@ -234,46 +263,10 @@ RenderFeature.prototype.getSimplifiedGeometry = RenderFeature.prototype.getGeometry; -/** - * @return {number} Stride. - */ -RenderFeature.prototype.getStride = function() { - return 2; -}; - - /** * @return {undefined} */ RenderFeature.prototype.getStyleFunction = UNDEFINED; -/** - * Get the type of this feature's geometry. - * @return {module:ol/geom/GeometryType} Geometry type. - * @api - */ -RenderFeature.prototype.getType = function() { - return this.type_; -}; - -/** - * Transform geometry coordinates from tile pixel space to projected. - * The SRS of the source and destination are expected to be the same. - * - * @param {module:ol/proj~ProjectionLike} source The current projection - * @param {module:ol/proj~ProjectionLike} destination The desired projection. - */ -RenderFeature.prototype.transform = function(source, destination) { - source = getProjection(source); - const pixelExtent = source.getExtent(); - const projectedExtent = source.getWorldExtent(); - const scale = getHeight(projectedExtent) / getHeight(pixelExtent); - composeTransform(tmpTransform, - projectedExtent[0], projectedExtent[3], - scale, -scale, 0, - 0, 0); - transform2D(this.flatCoordinates_, 0, this.flatCoordinates_.length, 2, - tmpTransform, this.flatCoordinates_); -}; export default RenderFeature; diff --git a/src/ol/render/ReplayGroup.js b/src/ol/render/ReplayGroup.js index 264179e08e..c0b3c37fcb 100644 --- a/src/ol/render/ReplayGroup.js +++ b/src/ol/render/ReplayGroup.js @@ -6,21 +6,20 @@ * @constructor * @abstract */ -const ReplayGroup = function() {}; +class ReplayGroup { + /** + * @abstract + * @param {number|undefined} zIndex Z index. + * @param {module:ol/render/ReplayType} replayType Replay type. + * @return {module:ol/render/VectorContext} Replay. + */ + getReplay(zIndex, replayType) {} + /** + * @abstract + * @return {boolean} Is empty. + */ + isEmpty() {} +} -/** - * @abstract - * @param {number|undefined} zIndex Z index. - * @param {module:ol/render/ReplayType} replayType Replay type. - * @return {module:ol/render/VectorContext} Replay. - */ -ReplayGroup.prototype.getReplay = function(zIndex, replayType) {}; - - -/** - * @abstract - * @return {boolean} Is empty. - */ -ReplayGroup.prototype.isEmpty = function() {}; export default ReplayGroup; diff --git a/src/ol/render/VectorContext.js b/src/ol/render/VectorContext.js index d9317cc65a..0d160c6547 100644 --- a/src/ol/render/VectorContext.js +++ b/src/ol/render/VectorContext.js @@ -9,124 +9,108 @@ * @struct * @api */ -const VectorContext = function() { -}; +class VectorContext { + /** + * Render a geometry with a custom renderer. + * + * @param {module:ol/geom/SimpleGeometry} geometry Geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @param {Function} renderer Renderer. + */ + drawCustom(geometry, feature, renderer) {} + /** + * Render a geometry. + * + * @param {module:ol/geom/Geometry} geometry The geometry to render. + */ + drawGeometry(geometry) {} -/** - * Render a geometry with a custom renderer. - * - * @param {module:ol/geom/SimpleGeometry} geometry Geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @param {Function} renderer Renderer. - */ -VectorContext.prototype.drawCustom = function(geometry, feature, renderer) {}; + /** + * Set the rendering style. + * + * @param {module:ol/style/Style} style The rendering style. + */ + setStyle(style) {} + /** + * @param {module:ol/geom/Circle} circleGeometry Circle geometry. + * @param {module:ol/Feature} feature Feature. + */ + drawCircle(circleGeometry, feature) {} -/** - * Render a geometry. - * - * @param {module:ol/geom/Geometry} geometry The geometry to render. - */ -VectorContext.prototype.drawGeometry = function(geometry) {}; + /** + * @param {module:ol/Feature} feature Feature. + * @param {module:ol/style/Style} style Style. + */ + drawFeature(feature, style) {} + /** + * @param {module:ol/geom/GeometryCollection} geometryCollectionGeometry Geometry + * collection. + * @param {module:ol/Feature} feature Feature. + */ + drawGeometryCollection(geometryCollectionGeometry, feature) {} -/** - * Set the rendering style. - * - * @param {module:ol/style/Style} style The rendering style. - */ -VectorContext.prototype.setStyle = function(style) {}; + /** + * @param {module:ol/geom/LineString|module:ol/render/Feature} lineStringGeometry Line string geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + drawLineString(lineStringGeometry, feature) {} + /** + * @param {module:ol/geom/MultiLineString|module:ol/render/Feature} multiLineStringGeometry MultiLineString geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + drawMultiLineString(multiLineStringGeometry, feature) {} -/** - * @param {module:ol/geom/Circle} circleGeometry Circle geometry. - * @param {module:ol/Feature} feature Feature. - */ -VectorContext.prototype.drawCircle = function(circleGeometry, feature) {}; + /** + * @param {module:ol/geom/MultiPoint|module:ol/render/Feature} multiPointGeometry MultiPoint geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + drawMultiPoint(multiPointGeometry, feature) {} + /** + * @param {module:ol/geom/MultiPolygon} multiPolygonGeometry MultiPolygon geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + drawMultiPolygon(multiPolygonGeometry, feature) {} -/** - * @param {module:ol/Feature} feature Feature. - * @param {module:ol/style/Style} style Style. - */ -VectorContext.prototype.drawFeature = function(feature, style) {}; + /** + * @param {module:ol/geom/Point|module:ol/render/Feature} pointGeometry Point geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + drawPoint(pointGeometry, feature) {} + /** + * @param {module:ol/geom/Polygon|module:ol/render/Feature} polygonGeometry Polygon geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + drawPolygon(polygonGeometry, feature) {} -/** - * @param {module:ol/geom/GeometryCollection} geometryCollectionGeometry Geometry - * collection. - * @param {module:ol/Feature} feature Feature. - */ -VectorContext.prototype.drawGeometryCollection = function(geometryCollectionGeometry, feature) {}; + /** + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + drawText(geometry, feature) {} + /** + * @param {module:ol/style/Fill} fillStyle Fill style. + * @param {module:ol/style/Stroke} strokeStyle Stroke style. + */ + setFillStrokeStyle(fillStyle, strokeStyle) {} -/** - * @param {module:ol/geom/LineString|module:ol/render/Feature} lineStringGeometry Line string geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -VectorContext.prototype.drawLineString = function(lineStringGeometry, feature) {}; + /** + * @param {module:ol/style/Image} imageStyle Image style. + * @param {module:ol/render/canvas~DeclutterGroup=} opt_declutterGroup Declutter. + */ + setImageStyle(imageStyle, opt_declutterGroup) {} + /** + * @param {module:ol/style/Text} textStyle Text style. + * @param {module:ol/render/canvas~DeclutterGroup=} opt_declutterGroup Declutter. + */ + setTextStyle(textStyle, opt_declutterGroup) {} +} -/** - * @param {module:ol/geom/MultiLineString|module:ol/render/Feature} multiLineStringGeometry MultiLineString geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -VectorContext.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) {}; - - -/** - * @param {module:ol/geom/MultiPoint|module:ol/render/Feature} multiPointGeometry MultiPoint geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -VectorContext.prototype.drawMultiPoint = function(multiPointGeometry, feature) {}; - - -/** - * @param {module:ol/geom/MultiPolygon} multiPolygonGeometry MultiPolygon geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -VectorContext.prototype.drawMultiPolygon = function(multiPolygonGeometry, feature) {}; - - -/** - * @param {module:ol/geom/Point|module:ol/render/Feature} pointGeometry Point geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -VectorContext.prototype.drawPoint = function(pointGeometry, feature) {}; - - -/** - * @param {module:ol/geom/Polygon|module:ol/render/Feature} polygonGeometry Polygon geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -VectorContext.prototype.drawPolygon = function(polygonGeometry, feature) {}; - - -/** - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -VectorContext.prototype.drawText = function(geometry, feature) {}; - - -/** - * @param {module:ol/style/Fill} fillStyle Fill style. - * @param {module:ol/style/Stroke} strokeStyle Stroke style. - */ -VectorContext.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {}; - - -/** - * @param {module:ol/style/Image} imageStyle Image style. - * @param {module:ol/render/canvas~DeclutterGroup=} opt_declutterGroup Declutter. - */ -VectorContext.prototype.setImageStyle = function(imageStyle, opt_declutterGroup) {}; - - -/** - * @param {module:ol/style/Text} textStyle Text style. - * @param {module:ol/render/canvas~DeclutterGroup=} opt_declutterGroup Declutter. - */ -VectorContext.prototype.setTextStyle = function(textStyle, opt_declutterGroup) {}; export default VectorContext; diff --git a/src/ol/render/canvas/ImageReplay.js b/src/ol/render/canvas/ImageReplay.js index c17d3f2e44..e39e7009c4 100644 --- a/src/ol/render/canvas/ImageReplay.js +++ b/src/ol/render/canvas/ImageReplay.js @@ -16,218 +16,216 @@ import CanvasReplay from '../canvas/Replay.js'; * @param {?} declutterTree Declutter tree. * @struct */ -const CanvasImageReplay = function( - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { - CanvasReplay.call(this, - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); +class CanvasImageReplay { + constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + CanvasReplay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); - /** - * @private - * @type {module:ol/render/canvas~DeclutterGroup} - */ - this.declutterGroup_ = null; + /** + * @private + * @type {module:ol/render/canvas~DeclutterGroup} + */ + this.declutterGroup_ = null; - /** - * @private - * @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} - */ - this.hitDetectionImage_ = null; + /** + * @private + * @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} + */ + this.hitDetectionImage_ = null; - /** - * @private - * @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} - */ - this.image_ = null; + /** + * @private + * @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} + */ + this.image_ = null; - /** - * @private - * @type {number|undefined} - */ - this.anchorX_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.anchorX_ = undefined; - /** - * @private - * @type {number|undefined} - */ - this.anchorY_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.anchorY_ = undefined; - /** - * @private - * @type {number|undefined} - */ - this.height_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.height_ = undefined; - /** - * @private - * @type {number|undefined} - */ - this.opacity_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.opacity_ = undefined; - /** - * @private - * @type {number|undefined} - */ - this.originX_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.originX_ = undefined; - /** - * @private - * @type {number|undefined} - */ - this.originY_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.originY_ = undefined; - /** - * @private - * @type {boolean|undefined} - */ - this.rotateWithView_ = undefined; + /** + * @private + * @type {boolean|undefined} + */ + this.rotateWithView_ = undefined; - /** - * @private - * @type {number|undefined} - */ - this.rotation_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.rotation_ = undefined; - /** - * @private - * @type {number|undefined} - */ - this.scale_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.scale_ = undefined; - /** - * @private - * @type {boolean|undefined} - */ - this.snapToPixel_ = undefined; + /** + * @private + * @type {boolean|undefined} + */ + this.snapToPixel_ = undefined; - /** - * @private - * @type {number|undefined} - */ - this.width_ = undefined; + /** + * @private + * @type {number|undefined} + */ + this.width_ = undefined; -}; + } + + /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @private + * @return {number} My end. + */ + drawCoordinates_(flatCoordinates, offset, end, stride) { + return this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false); + } + + /** + * @inheritDoc + */ + drawPoint(pointGeometry, feature) { + if (!this.image_) { + return; + } + this.beginGeometry(pointGeometry, feature); + const flatCoordinates = pointGeometry.getFlatCoordinates(); + const stride = pointGeometry.getStride(); + const myBegin = this.coordinates.length; + const myEnd = this.drawCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride); + this.instructions.push([ + CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_, + // Remaining arguments to DRAW_IMAGE are in alphabetical order + this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, + this.originX_, this.originY_, this.rotateWithView_, this.rotation_, + this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_ + ]); + this.hitDetectionInstructions.push([ + CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, + // Remaining arguments to DRAW_IMAGE are in alphabetical order + this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, + this.originX_, this.originY_, this.rotateWithView_, this.rotation_, + this.scale_, this.snapToPixel_, this.width_ + ]); + this.endGeometry(pointGeometry, feature); + } + + /** + * @inheritDoc + */ + drawMultiPoint(multiPointGeometry, feature) { + if (!this.image_) { + return; + } + this.beginGeometry(multiPointGeometry, feature); + const flatCoordinates = multiPointGeometry.getFlatCoordinates(); + const stride = multiPointGeometry.getStride(); + const myBegin = this.coordinates.length; + const myEnd = this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); + this.instructions.push([ + CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_, + // Remaining arguments to DRAW_IMAGE are in alphabetical order + this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, + this.originX_, this.originY_, this.rotateWithView_, this.rotation_, + this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_ + ]); + this.hitDetectionInstructions.push([ + CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, + // Remaining arguments to DRAW_IMAGE are in alphabetical order + this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, + this.originX_, this.originY_, this.rotateWithView_, this.rotation_, + this.scale_, this.snapToPixel_, this.width_ + ]); + this.endGeometry(multiPointGeometry, feature); + } + + /** + * @inheritDoc + */ + finish() { + this.reverseHitDetectionInstructions(); + // FIXME this doesn't really protect us against further calls to draw*Geometry + this.anchorX_ = undefined; + this.anchorY_ = undefined; + this.hitDetectionImage_ = null; + this.image_ = null; + this.height_ = undefined; + this.scale_ = undefined; + this.opacity_ = undefined; + this.originX_ = undefined; + this.originY_ = undefined; + this.rotateWithView_ = undefined; + this.rotation_ = undefined; + this.snapToPixel_ = undefined; + this.width_ = undefined; + } + + /** + * @inheritDoc + */ + setImageStyle(imageStyle, declutterGroup) { + const anchor = imageStyle.getAnchor(); + const size = imageStyle.getSize(); + const hitDetectionImage = imageStyle.getHitDetectionImage(1); + const image = imageStyle.getImage(1); + const origin = imageStyle.getOrigin(); + this.anchorX_ = anchor[0]; + this.anchorY_ = anchor[1]; + this.declutterGroup_ = /** @type {module:ol/render/canvas~DeclutterGroup} */ (declutterGroup); + this.hitDetectionImage_ = hitDetectionImage; + this.image_ = image; + this.height_ = size[1]; + this.opacity_ = imageStyle.getOpacity(); + this.originX_ = origin[0]; + this.originY_ = origin[1]; + this.rotateWithView_ = imageStyle.getRotateWithView(); + this.rotation_ = imageStyle.getRotation(); + this.scale_ = imageStyle.getScale(); + this.snapToPixel_ = imageStyle.getSnapToPixel(); + this.width_ = size[0]; + } +} inherits(CanvasImageReplay, CanvasReplay); -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @private - * @return {number} My end. - */ -CanvasImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) { - return this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false); -}; - - -/** - * @inheritDoc - */ -CanvasImageReplay.prototype.drawPoint = function(pointGeometry, feature) { - if (!this.image_) { - return; - } - this.beginGeometry(pointGeometry, feature); - const flatCoordinates = pointGeometry.getFlatCoordinates(); - const stride = pointGeometry.getStride(); - const myBegin = this.coordinates.length; - const myEnd = this.drawCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride); - this.instructions.push([ - CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_, - // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, - this.originX_, this.originY_, this.rotateWithView_, this.rotation_, - this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_ - ]); - this.hitDetectionInstructions.push([ - CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, - // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, - this.originX_, this.originY_, this.rotateWithView_, this.rotation_, - this.scale_, this.snapToPixel_, this.width_ - ]); - this.endGeometry(pointGeometry, feature); -}; - - -/** - * @inheritDoc - */ -CanvasImageReplay.prototype.drawMultiPoint = function(multiPointGeometry, feature) { - if (!this.image_) { - return; - } - this.beginGeometry(multiPointGeometry, feature); - const flatCoordinates = multiPointGeometry.getFlatCoordinates(); - const stride = multiPointGeometry.getStride(); - const myBegin = this.coordinates.length; - const myEnd = this.drawCoordinates_( - flatCoordinates, 0, flatCoordinates.length, stride); - this.instructions.push([ - CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_, - // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, - this.originX_, this.originY_, this.rotateWithView_, this.rotation_, - this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_ - ]); - this.hitDetectionInstructions.push([ - CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, - // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, - this.originX_, this.originY_, this.rotateWithView_, this.rotation_, - this.scale_, this.snapToPixel_, this.width_ - ]); - this.endGeometry(multiPointGeometry, feature); -}; - - -/** - * @inheritDoc - */ -CanvasImageReplay.prototype.finish = function() { - this.reverseHitDetectionInstructions(); - // FIXME this doesn't really protect us against further calls to draw*Geometry - this.anchorX_ = undefined; - this.anchorY_ = undefined; - this.hitDetectionImage_ = null; - this.image_ = null; - this.height_ = undefined; - this.scale_ = undefined; - this.opacity_ = undefined; - this.originX_ = undefined; - this.originY_ = undefined; - this.rotateWithView_ = undefined; - this.rotation_ = undefined; - this.snapToPixel_ = undefined; - this.width_ = undefined; -}; - - -/** - * @inheritDoc - */ -CanvasImageReplay.prototype.setImageStyle = function(imageStyle, declutterGroup) { - const anchor = imageStyle.getAnchor(); - const size = imageStyle.getSize(); - const hitDetectionImage = imageStyle.getHitDetectionImage(1); - const image = imageStyle.getImage(1); - const origin = imageStyle.getOrigin(); - this.anchorX_ = anchor[0]; - this.anchorY_ = anchor[1]; - this.declutterGroup_ = /** @type {module:ol/render/canvas~DeclutterGroup} */ (declutterGroup); - this.hitDetectionImage_ = hitDetectionImage; - this.image_ = image; - this.height_ = size[1]; - this.opacity_ = imageStyle.getOpacity(); - this.originX_ = origin[0]; - this.originY_ = origin[1]; - this.rotateWithView_ = imageStyle.getRotateWithView(); - this.rotation_ = imageStyle.getRotation(); - this.scale_ = imageStyle.getScale(); - this.snapToPixel_ = imageStyle.getSnapToPixel(); - this.width_ = size[0]; -}; export default CanvasImageReplay; diff --git a/src/ol/render/canvas/Immediate.js b/src/ol/render/canvas/Immediate.js index e27881e1f5..8843869e91 100644 --- a/src/ol/render/canvas/Immediate.js +++ b/src/ol/render/canvas/Immediate.js @@ -35,932 +35,915 @@ import {create as createTransform, compose as composeTransform} from '../../tran * @param {number} viewRotation View rotation. * @struct */ -const CanvasImmediateRenderer = function(context, pixelRatio, extent, transform, viewRotation) { - VectorContext.call(this); +class CanvasImmediateRenderer { + constructor(context, pixelRatio, extent, transform, viewRotation) { + VectorContext.call(this); + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = context; + + /** + * @private + * @type {number} + */ + this.pixelRatio_ = pixelRatio; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.extent_ = extent; + + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.transform_ = transform; + + /** + * @private + * @type {number} + */ + this.viewRotation_ = viewRotation; + + /** + * @private + * @type {?module:ol/render/canvas~FillState} + */ + this.contextFillState_ = null; + + /** + * @private + * @type {?module:ol/render/canvas~StrokeState} + */ + this.contextStrokeState_ = null; + + /** + * @private + * @type {?module:ol/render/canvas~TextState} + */ + this.contextTextState_ = null; + + /** + * @private + * @type {?module:ol/render/canvas~FillState} + */ + this.fillState_ = null; + + /** + * @private + * @type {?module:ol/render/canvas~StrokeState} + */ + this.strokeState_ = null; + + /** + * @private + * @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} + */ + this.image_ = null; + + /** + * @private + * @type {number} + */ + this.imageAnchorX_ = 0; + + /** + * @private + * @type {number} + */ + this.imageAnchorY_ = 0; + + /** + * @private + * @type {number} + */ + this.imageHeight_ = 0; + + /** + * @private + * @type {number} + */ + this.imageOpacity_ = 0; + + /** + * @private + * @type {number} + */ + this.imageOriginX_ = 0; + + /** + * @private + * @type {number} + */ + this.imageOriginY_ = 0; + + /** + * @private + * @type {boolean} + */ + this.imageRotateWithView_ = false; + + /** + * @private + * @type {number} + */ + this.imageRotation_ = 0; + + /** + * @private + * @type {number} + */ + this.imageScale_ = 0; + + /** + * @private + * @type {boolean} + */ + this.imageSnapToPixel_ = false; + + /** + * @private + * @type {number} + */ + this.imageWidth_ = 0; + + /** + * @private + * @type {string} + */ + this.text_ = ''; + + /** + * @private + * @type {number} + */ + this.textOffsetX_ = 0; + + /** + * @private + * @type {number} + */ + this.textOffsetY_ = 0; + + /** + * @private + * @type {boolean} + */ + this.textRotateWithView_ = false; + + /** + * @private + * @type {number} + */ + this.textRotation_ = 0; + + /** + * @private + * @type {number} + */ + this.textScale_ = 0; + + /** + * @private + * @type {?module:ol/render/canvas~FillState} + */ + this.textFillState_ = null; + + /** + * @private + * @type {?module:ol/render/canvas~StrokeState} + */ + this.textStrokeState_ = null; + + /** + * @private + * @type {?module:ol/render/canvas~TextState} + */ + this.textState_ = null; + + /** + * @private + * @type {Array.} + */ + this.pixelCoordinates_ = []; + + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.tmpLocalTransform_ = createTransform(); + + } /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. * @private - * @type {CanvasRenderingContext2D} */ - this.context_ = context; + drawImages_(flatCoordinates, offset, end, stride) { + if (!this.image_) { + return; + } + const pixelCoordinates = transform2D( + flatCoordinates, offset, end, 2, this.transform_, + this.pixelCoordinates_); + const context = this.context_; + const localTransform = this.tmpLocalTransform_; + const alpha = context.globalAlpha; + if (this.imageOpacity_ != 1) { + context.globalAlpha = alpha * this.imageOpacity_; + } + let rotation = this.imageRotation_; + if (this.imageRotateWithView_) { + rotation += this.viewRotation_; + } + for (let i = 0, ii = pixelCoordinates.length; i < ii; i += 2) { + let x = pixelCoordinates[i] - this.imageAnchorX_; + let y = pixelCoordinates[i + 1] - this.imageAnchorY_; + if (this.imageSnapToPixel_) { + x = Math.round(x); + y = Math.round(y); + } + if (rotation !== 0 || this.imageScale_ != 1) { + const centerX = x + this.imageAnchorX_; + const centerY = y + this.imageAnchorY_; + composeTransform(localTransform, + centerX, centerY, + this.imageScale_, this.imageScale_, + rotation, + -centerX, -centerY); + context.setTransform.apply(context, localTransform); + } + context.drawImage(this.image_, this.imageOriginX_, this.imageOriginY_, + this.imageWidth_, this.imageHeight_, x, y, + this.imageWidth_, this.imageHeight_); + } + if (rotation !== 0 || this.imageScale_ != 1) { + context.setTransform(1, 0, 0, 1, 0, 0); + } + if (this.imageOpacity_ != 1) { + context.globalAlpha = alpha; + } + } /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. * @private - * @type {number} */ - this.pixelRatio_ = pixelRatio; + drawText_(flatCoordinates, offset, end, stride) { + if (!this.textState_ || this.text_ === '') { + return; + } + if (this.textFillState_) { + this.setContextFillState_(this.textFillState_); + } + if (this.textStrokeState_) { + this.setContextStrokeState_(this.textStrokeState_); + } + this.setContextTextState_(this.textState_); + const pixelCoordinates = transform2D( + flatCoordinates, offset, end, stride, this.transform_, + this.pixelCoordinates_); + const context = this.context_; + let rotation = this.textRotation_; + if (this.textRotateWithView_) { + rotation += this.viewRotation_; + } + for (; offset < end; offset += stride) { + const x = pixelCoordinates[offset] + this.textOffsetX_; + const y = pixelCoordinates[offset + 1] + this.textOffsetY_; + if (rotation !== 0 || this.textScale_ != 1) { + const localTransform = composeTransform(this.tmpLocalTransform_, + x, y, + this.textScale_, this.textScale_, + rotation, + -x, -y); + context.setTransform.apply(context, localTransform); + } + if (this.textStrokeState_) { + context.strokeText(this.text_, x, y); + } + if (this.textFillState_) { + context.fillText(this.text_, x, y); + } + } + if (rotation !== 0 || this.textScale_ != 1) { + context.setTransform(1, 0, 0, 1, 0, 0); + } + } /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @param {boolean} close Close. * @private - * @type {module:ol/extent~Extent} + * @return {number} end End. */ - this.extent_ = extent; + moveToLineTo_(flatCoordinates, offset, end, stride, close) { + const context = this.context_; + const pixelCoordinates = transform2D( + flatCoordinates, offset, end, stride, this.transform_, + this.pixelCoordinates_); + context.moveTo(pixelCoordinates[0], pixelCoordinates[1]); + let length = pixelCoordinates.length; + if (close) { + length -= 2; + } + for (let i = 2; i < length; i += 2) { + context.lineTo(pixelCoordinates[i], pixelCoordinates[i + 1]); + } + if (close) { + context.closePath(); + } + return end; + } /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {Array.} ends Ends. + * @param {number} stride Stride. * @private - * @type {module:ol/transform~Transform} + * @return {number} End. */ - this.transform_ = transform; + drawRings_(flatCoordinates, offset, ends, stride) { + for (let i = 0, ii = ends.length; i < ii; ++i) { + offset = this.moveToLineTo_(flatCoordinates, offset, ends[i], stride, true); + } + return offset; + } /** - * @private - * @type {number} + * Render a circle geometry into the canvas. Rendering is immediate and uses + * the current fill and stroke styles. + * + * @param {module:ol/geom/Circle} geometry Circle geometry. + * @override + * @api */ - this.viewRotation_ = viewRotation; + drawCircle(geometry) { + if (!intersects(this.extent_, geometry.getExtent())) { + return; + } + if (this.fillState_ || this.strokeState_) { + if (this.fillState_) { + this.setContextFillState_(this.fillState_); + } + if (this.strokeState_) { + this.setContextStrokeState_(this.strokeState_); + } + const pixelCoordinates = transformGeom2D( + geometry, this.transform_, this.pixelCoordinates_); + const dx = pixelCoordinates[2] - pixelCoordinates[0]; + const dy = pixelCoordinates[3] - pixelCoordinates[1]; + const radius = Math.sqrt(dx * dx + dy * dy); + const context = this.context_; + context.beginPath(); + context.arc( + pixelCoordinates[0], pixelCoordinates[1], radius, 0, 2 * Math.PI); + if (this.fillState_) { + context.fill(); + } + if (this.strokeState_) { + context.stroke(); + } + } + if (this.text_ !== '') { + this.drawText_(geometry.getCenter(), 0, 2, 2); + } + } /** - * @private - * @type {?module:ol/render/canvas~FillState} + * Set the rendering style. Note that since this is an immediate rendering API, + * any `zIndex` on the provided style will be ignored. + * + * @param {module:ol/style/Style} style The rendering style. + * @override + * @api */ - this.contextFillState_ = null; + setStyle(style) { + this.setFillStrokeStyle(style.getFill(), style.getStroke()); + this.setImageStyle(style.getImage()); + this.setTextStyle(style.getText()); + } /** - * @private - * @type {?module:ol/render/canvas~StrokeState} + * Render a geometry into the canvas. Call + * {@link module:ol/render/canvas/Immediate#setStyle} first to set the rendering style. + * + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry The geometry to render. + * @override + * @api */ - this.contextStrokeState_ = null; + drawGeometry(geometry) { + const type = geometry.getType(); + switch (type) { + case GeometryType.POINT: + this.drawPoint(/** @type {module:ol/geom/Point} */ (geometry)); + break; + case GeometryType.LINE_STRING: + this.drawLineString(/** @type {module:ol/geom/LineString} */ (geometry)); + break; + case GeometryType.POLYGON: + this.drawPolygon(/** @type {module:ol/geom/Polygon} */ (geometry)); + break; + case GeometryType.MULTI_POINT: + this.drawMultiPoint(/** @type {module:ol/geom/MultiPoint} */ (geometry)); + break; + case GeometryType.MULTI_LINE_STRING: + this.drawMultiLineString(/** @type {module:ol/geom/MultiLineString} */ (geometry)); + break; + case GeometryType.MULTI_POLYGON: + this.drawMultiPolygon(/** @type {module:ol/geom/MultiPolygon} */ (geometry)); + break; + case GeometryType.GEOMETRY_COLLECTION: + this.drawGeometryCollection(/** @type {module:ol/geom/GeometryCollection} */ (geometry)); + break; + case GeometryType.CIRCLE: + this.drawCircle(/** @type {module:ol/geom/Circle} */ (geometry)); + break; + default: + } + } /** - * @private - * @type {?module:ol/render/canvas~TextState} + * Render a feature into the canvas. Note that any `zIndex` on the provided + * style will be ignored - features are rendered immediately in the order that + * this method is called. If you need `zIndex` support, you should be using an + * {@link module:ol/layer/Vector~VectorLayer} instead. + * + * @param {module:ol/Feature} feature Feature. + * @param {module:ol/style/Style} style Style. + * @override + * @api */ - this.contextTextState_ = null; + drawFeature(feature, style) { + const geometry = style.getGeometryFunction()(feature); + if (!geometry || !intersects(this.extent_, geometry.getExtent())) { + return; + } + this.setStyle(style); + this.drawGeometry(geometry); + } /** - * @private - * @type {?module:ol/render/canvas~FillState} + * Render a GeometryCollection to the canvas. Rendering is immediate and + * uses the current styles appropriate for each geometry in the collection. + * + * @param {module:ol/geom/GeometryCollection} geometry Geometry collection. + * @override */ - this.fillState_ = null; + drawGeometryCollection(geometry) { + const geometries = geometry.getGeometriesArray(); + for (let i = 0, ii = geometries.length; i < ii; ++i) { + this.drawGeometry(geometries[i]); + } + } /** - * @private - * @type {?module:ol/render/canvas~StrokeState} + * Render a Point geometry into the canvas. Rendering is immediate and uses + * the current style. + * + * @param {module:ol/geom/Point|module:ol/render/Feature} geometry Point geometry. + * @override */ - this.strokeState_ = null; + drawPoint(geometry) { + const flatCoordinates = geometry.getFlatCoordinates(); + const stride = geometry.getStride(); + if (this.image_) { + this.drawImages_(flatCoordinates, 0, flatCoordinates.length, stride); + } + if (this.text_ !== '') { + this.drawText_(flatCoordinates, 0, flatCoordinates.length, stride); + } + } /** - * @private - * @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} + * Render a MultiPoint geometry into the canvas. Rendering is immediate and + * uses the current style. + * + * @param {module:ol/geom/MultiPoint|module:ol/render/Feature} geometry MultiPoint geometry. + * @override */ - this.image_ = null; + drawMultiPoint(geometry) { + const flatCoordinates = geometry.getFlatCoordinates(); + const stride = geometry.getStride(); + if (this.image_) { + this.drawImages_(flatCoordinates, 0, flatCoordinates.length, stride); + } + if (this.text_ !== '') { + this.drawText_(flatCoordinates, 0, flatCoordinates.length, stride); + } + } /** - * @private - * @type {number} + * Render a LineString into the canvas. Rendering is immediate and uses + * the current style. + * + * @param {module:ol/geom/LineString|module:ol/render/Feature} geometry LineString geometry. + * @override */ - this.imageAnchorX_ = 0; + drawLineString(geometry) { + if (!intersects(this.extent_, geometry.getExtent())) { + return; + } + if (this.strokeState_) { + this.setContextStrokeState_(this.strokeState_); + const context = this.context_; + const flatCoordinates = geometry.getFlatCoordinates(); + context.beginPath(); + this.moveToLineTo_(flatCoordinates, 0, flatCoordinates.length, + geometry.getStride(), false); + context.stroke(); + } + if (this.text_ !== '') { + const flatMidpoint = geometry.getFlatMidpoint(); + this.drawText_(flatMidpoint, 0, 2, 2); + } + } /** - * @private - * @type {number} + * Render a MultiLineString geometry into the canvas. Rendering is immediate + * and uses the current style. + * + * @param {module:ol/geom/MultiLineString|module:ol/render/Feature} geometry MultiLineString geometry. + * @override */ - this.imageAnchorY_ = 0; + drawMultiLineString(geometry) { + const geometryExtent = geometry.getExtent(); + if (!intersects(this.extent_, geometryExtent)) { + return; + } + if (this.strokeState_) { + this.setContextStrokeState_(this.strokeState_); + const context = this.context_; + const flatCoordinates = geometry.getFlatCoordinates(); + let offset = 0; + const ends = geometry.getEnds(); + const stride = geometry.getStride(); + context.beginPath(); + for (let i = 0, ii = ends.length; i < ii; ++i) { + offset = this.moveToLineTo_(flatCoordinates, offset, ends[i], stride, false); + } + context.stroke(); + } + if (this.text_ !== '') { + const flatMidpoints = geometry.getFlatMidpoints(); + this.drawText_(flatMidpoints, 0, flatMidpoints.length, 2); + } + } /** - * @private - * @type {number} + * Render a Polygon geometry into the canvas. Rendering is immediate and uses + * the current style. + * + * @param {module:ol/geom/Polygon|module:ol/render/Feature} geometry Polygon geometry. + * @override */ - this.imageHeight_ = 0; + drawPolygon(geometry) { + if (!intersects(this.extent_, geometry.getExtent())) { + return; + } + if (this.strokeState_ || this.fillState_) { + if (this.fillState_) { + this.setContextFillState_(this.fillState_); + } + if (this.strokeState_) { + this.setContextStrokeState_(this.strokeState_); + } + const context = this.context_; + context.beginPath(); + this.drawRings_(geometry.getOrientedFlatCoordinates(), + 0, geometry.getEnds(), geometry.getStride()); + if (this.fillState_) { + context.fill(); + } + if (this.strokeState_) { + context.stroke(); + } + } + if (this.text_ !== '') { + const flatInteriorPoint = geometry.getFlatInteriorPoint(); + this.drawText_(flatInteriorPoint, 0, 2, 2); + } + } /** - * @private - * @type {number} + * Render MultiPolygon geometry into the canvas. Rendering is immediate and + * uses the current style. + * @param {module:ol/geom/MultiPolygon} geometry MultiPolygon geometry. + * @override */ - this.imageOpacity_ = 0; + drawMultiPolygon(geometry) { + if (!intersects(this.extent_, geometry.getExtent())) { + return; + } + if (this.strokeState_ || this.fillState_) { + if (this.fillState_) { + this.setContextFillState_(this.fillState_); + } + if (this.strokeState_) { + this.setContextStrokeState_(this.strokeState_); + } + const context = this.context_; + const flatCoordinates = geometry.getOrientedFlatCoordinates(); + let offset = 0; + const endss = geometry.getEndss(); + const stride = geometry.getStride(); + context.beginPath(); + for (let i = 0, ii = endss.length; i < ii; ++i) { + const ends = endss[i]; + offset = this.drawRings_(flatCoordinates, offset, ends, stride); + } + if (this.fillState_) { + context.fill(); + } + if (this.strokeState_) { + context.stroke(); + } + } + if (this.text_ !== '') { + const flatInteriorPoints = geometry.getFlatInteriorPoints(); + this.drawText_(flatInteriorPoints, 0, flatInteriorPoints.length, 2); + } + } /** + * @param {module:ol/render/canvas~FillState} fillState Fill state. * @private - * @type {number} */ - this.imageOriginX_ = 0; + setContextFillState_(fillState) { + const context = this.context_; + const contextFillState = this.contextFillState_; + if (!contextFillState) { + context.fillStyle = fillState.fillStyle; + this.contextFillState_ = { + fillStyle: fillState.fillStyle + }; + } else { + if (contextFillState.fillStyle != fillState.fillStyle) { + contextFillState.fillStyle = context.fillStyle = fillState.fillStyle; + } + } + } /** + * @param {module:ol/render/canvas~StrokeState} strokeState Stroke state. * @private - * @type {number} */ - this.imageOriginY_ = 0; + setContextStrokeState_(strokeState) { + const context = this.context_; + const contextStrokeState = this.contextStrokeState_; + if (!contextStrokeState) { + context.lineCap = strokeState.lineCap; + if (CANVAS_LINE_DASH) { + context.setLineDash(strokeState.lineDash); + context.lineDashOffset = strokeState.lineDashOffset; + } + context.lineJoin = strokeState.lineJoin; + context.lineWidth = strokeState.lineWidth; + context.miterLimit = strokeState.miterLimit; + context.strokeStyle = strokeState.strokeStyle; + this.contextStrokeState_ = { + lineCap: strokeState.lineCap, + lineDash: strokeState.lineDash, + lineDashOffset: strokeState.lineDashOffset, + lineJoin: strokeState.lineJoin, + lineWidth: strokeState.lineWidth, + miterLimit: strokeState.miterLimit, + strokeStyle: strokeState.strokeStyle + }; + } else { + if (contextStrokeState.lineCap != strokeState.lineCap) { + contextStrokeState.lineCap = context.lineCap = strokeState.lineCap; + } + if (CANVAS_LINE_DASH) { + if (!equals(contextStrokeState.lineDash, strokeState.lineDash)) { + context.setLineDash(contextStrokeState.lineDash = strokeState.lineDash); + } + if (contextStrokeState.lineDashOffset != strokeState.lineDashOffset) { + contextStrokeState.lineDashOffset = context.lineDashOffset = + strokeState.lineDashOffset; + } + } + if (contextStrokeState.lineJoin != strokeState.lineJoin) { + contextStrokeState.lineJoin = context.lineJoin = strokeState.lineJoin; + } + if (contextStrokeState.lineWidth != strokeState.lineWidth) { + contextStrokeState.lineWidth = context.lineWidth = strokeState.lineWidth; + } + if (contextStrokeState.miterLimit != strokeState.miterLimit) { + contextStrokeState.miterLimit = context.miterLimit = + strokeState.miterLimit; + } + if (contextStrokeState.strokeStyle != strokeState.strokeStyle) { + contextStrokeState.strokeStyle = context.strokeStyle = + strokeState.strokeStyle; + } + } + } /** + * @param {module:ol/render/canvas~TextState} textState Text state. * @private - * @type {boolean} */ - this.imageRotateWithView_ = false; + setContextTextState_(textState) { + const context = this.context_; + const contextTextState = this.contextTextState_; + const textAlign = textState.textAlign ? + textState.textAlign : defaultTextAlign; + if (!contextTextState) { + context.font = textState.font; + context.textAlign = textAlign; + context.textBaseline = textState.textBaseline; + this.contextTextState_ = { + font: textState.font, + textAlign: textAlign, + textBaseline: textState.textBaseline + }; + } else { + if (contextTextState.font != textState.font) { + contextTextState.font = context.font = textState.font; + } + if (contextTextState.textAlign != textAlign) { + contextTextState.textAlign = context.textAlign = textAlign; + } + if (contextTextState.textBaseline != textState.textBaseline) { + contextTextState.textBaseline = context.textBaseline = + textState.textBaseline; + } + } + } /** - * @private - * @type {number} + * Set the fill and stroke style for subsequent draw operations. To clear + * either fill or stroke styles, pass null for the appropriate parameter. + * + * @param {module:ol/style/Fill} fillStyle Fill style. + * @param {module:ol/style/Stroke} strokeStyle Stroke style. + * @override */ - this.imageRotation_ = 0; + setFillStrokeStyle(fillStyle, strokeStyle) { + if (!fillStyle) { + this.fillState_ = null; + } else { + const fillStyleColor = fillStyle.getColor(); + this.fillState_ = { + fillStyle: asColorLike(fillStyleColor ? + fillStyleColor : defaultFillStyle) + }; + } + if (!strokeStyle) { + this.strokeState_ = null; + } else { + const strokeStyleColor = strokeStyle.getColor(); + const strokeStyleLineCap = strokeStyle.getLineCap(); + const strokeStyleLineDash = strokeStyle.getLineDash(); + const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); + const strokeStyleLineJoin = strokeStyle.getLineJoin(); + const strokeStyleWidth = strokeStyle.getWidth(); + const strokeStyleMiterLimit = strokeStyle.getMiterLimit(); + this.strokeState_ = { + lineCap: strokeStyleLineCap !== undefined ? + strokeStyleLineCap : defaultLineCap, + lineDash: strokeStyleLineDash ? + strokeStyleLineDash : defaultLineDash, + lineDashOffset: strokeStyleLineDashOffset ? + strokeStyleLineDashOffset : defaultLineDashOffset, + lineJoin: strokeStyleLineJoin !== undefined ? + strokeStyleLineJoin : defaultLineJoin, + lineWidth: this.pixelRatio_ * (strokeStyleWidth !== undefined ? + strokeStyleWidth : defaultLineWidth), + miterLimit: strokeStyleMiterLimit !== undefined ? + strokeStyleMiterLimit : defaultMiterLimit, + strokeStyle: asColorLike(strokeStyleColor ? + strokeStyleColor : defaultStrokeStyle) + }; + } + } /** - * @private - * @type {number} + * Set the image style for subsequent draw operations. Pass null to remove + * the image style. + * + * @param {module:ol/style/Image} imageStyle Image style. + * @override */ - this.imageScale_ = 0; + setImageStyle(imageStyle) { + if (!imageStyle) { + this.image_ = null; + } else { + const imageAnchor = imageStyle.getAnchor(); + // FIXME pixel ratio + const imageImage = imageStyle.getImage(1); + const imageOrigin = imageStyle.getOrigin(); + const imageSize = imageStyle.getSize(); + this.imageAnchorX_ = imageAnchor[0]; + this.imageAnchorY_ = imageAnchor[1]; + this.imageHeight_ = imageSize[1]; + this.image_ = imageImage; + this.imageOpacity_ = imageStyle.getOpacity(); + this.imageOriginX_ = imageOrigin[0]; + this.imageOriginY_ = imageOrigin[1]; + this.imageRotateWithView_ = imageStyle.getRotateWithView(); + this.imageRotation_ = imageStyle.getRotation(); + this.imageScale_ = imageStyle.getScale() * this.pixelRatio_; + this.imageSnapToPixel_ = imageStyle.getSnapToPixel(); + this.imageWidth_ = imageSize[0]; + } + } /** - * @private - * @type {boolean} + * Set the text style for subsequent draw operations. Pass null to + * remove the text style. + * + * @param {module:ol/style/Text} textStyle Text style. + * @override */ - this.imageSnapToPixel_ = false; - - /** - * @private - * @type {number} - */ - this.imageWidth_ = 0; - - /** - * @private - * @type {string} - */ - this.text_ = ''; - - /** - * @private - * @type {number} - */ - this.textOffsetX_ = 0; - - /** - * @private - * @type {number} - */ - this.textOffsetY_ = 0; - - /** - * @private - * @type {boolean} - */ - this.textRotateWithView_ = false; - - /** - * @private - * @type {number} - */ - this.textRotation_ = 0; - - /** - * @private - * @type {number} - */ - this.textScale_ = 0; - - /** - * @private - * @type {?module:ol/render/canvas~FillState} - */ - this.textFillState_ = null; - - /** - * @private - * @type {?module:ol/render/canvas~StrokeState} - */ - this.textStrokeState_ = null; - - /** - * @private - * @type {?module:ol/render/canvas~TextState} - */ - this.textState_ = null; - - /** - * @private - * @type {Array.} - */ - this.pixelCoordinates_ = []; - - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.tmpLocalTransform_ = createTransform(); - -}; + setTextStyle(textStyle) { + if (!textStyle) { + this.text_ = ''; + } else { + const textFillStyle = textStyle.getFill(); + if (!textFillStyle) { + this.textFillState_ = null; + } else { + const textFillStyleColor = textFillStyle.getColor(); + this.textFillState_ = { + fillStyle: asColorLike(textFillStyleColor ? + textFillStyleColor : defaultFillStyle) + }; + } + const textStrokeStyle = textStyle.getStroke(); + if (!textStrokeStyle) { + this.textStrokeState_ = null; + } else { + const textStrokeStyleColor = textStrokeStyle.getColor(); + const textStrokeStyleLineCap = textStrokeStyle.getLineCap(); + const textStrokeStyleLineDash = textStrokeStyle.getLineDash(); + const textStrokeStyleLineDashOffset = textStrokeStyle.getLineDashOffset(); + const textStrokeStyleLineJoin = textStrokeStyle.getLineJoin(); + const textStrokeStyleWidth = textStrokeStyle.getWidth(); + const textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit(); + this.textStrokeState_ = { + lineCap: textStrokeStyleLineCap !== undefined ? + textStrokeStyleLineCap : defaultLineCap, + lineDash: textStrokeStyleLineDash ? + textStrokeStyleLineDash : defaultLineDash, + lineDashOffset: textStrokeStyleLineDashOffset ? + textStrokeStyleLineDashOffset : defaultLineDashOffset, + lineJoin: textStrokeStyleLineJoin !== undefined ? + textStrokeStyleLineJoin : defaultLineJoin, + lineWidth: textStrokeStyleWidth !== undefined ? + textStrokeStyleWidth : defaultLineWidth, + miterLimit: textStrokeStyleMiterLimit !== undefined ? + textStrokeStyleMiterLimit : defaultMiterLimit, + strokeStyle: asColorLike(textStrokeStyleColor ? + textStrokeStyleColor : defaultStrokeStyle) + }; + } + const textFont = textStyle.getFont(); + const textOffsetX = textStyle.getOffsetX(); + const textOffsetY = textStyle.getOffsetY(); + const textRotateWithView = textStyle.getRotateWithView(); + const textRotation = textStyle.getRotation(); + const textScale = textStyle.getScale(); + const textText = textStyle.getText(); + const textTextAlign = textStyle.getTextAlign(); + const textTextBaseline = textStyle.getTextBaseline(); + this.textState_ = { + font: textFont !== undefined ? + textFont : defaultFont, + textAlign: textTextAlign !== undefined ? + textTextAlign : defaultTextAlign, + textBaseline: textTextBaseline !== undefined ? + textTextBaseline : defaultTextBaseline + }; + this.text_ = textText !== undefined ? textText : ''; + this.textOffsetX_ = + textOffsetX !== undefined ? (this.pixelRatio_ * textOffsetX) : 0; + this.textOffsetY_ = + textOffsetY !== undefined ? (this.pixelRatio_ * textOffsetY) : 0; + this.textRotateWithView_ = textRotateWithView !== undefined ? textRotateWithView : false; + this.textRotation_ = textRotation !== undefined ? textRotation : 0; + this.textScale_ = this.pixelRatio_ * (textScale !== undefined ? + textScale : 1); + } + } +} inherits(CanvasImmediateRenderer, VectorContext); -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @private - */ -CanvasImmediateRenderer.prototype.drawImages_ = function(flatCoordinates, offset, end, stride) { - if (!this.image_) { - return; - } - const pixelCoordinates = transform2D( - flatCoordinates, offset, end, 2, this.transform_, - this.pixelCoordinates_); - const context = this.context_; - const localTransform = this.tmpLocalTransform_; - const alpha = context.globalAlpha; - if (this.imageOpacity_ != 1) { - context.globalAlpha = alpha * this.imageOpacity_; - } - let rotation = this.imageRotation_; - if (this.imageRotateWithView_) { - rotation += this.viewRotation_; - } - for (let i = 0, ii = pixelCoordinates.length; i < ii; i += 2) { - let x = pixelCoordinates[i] - this.imageAnchorX_; - let y = pixelCoordinates[i + 1] - this.imageAnchorY_; - if (this.imageSnapToPixel_) { - x = Math.round(x); - y = Math.round(y); - } - if (rotation !== 0 || this.imageScale_ != 1) { - const centerX = x + this.imageAnchorX_; - const centerY = y + this.imageAnchorY_; - composeTransform(localTransform, - centerX, centerY, - this.imageScale_, this.imageScale_, - rotation, - -centerX, -centerY); - context.setTransform.apply(context, localTransform); - } - context.drawImage(this.image_, this.imageOriginX_, this.imageOriginY_, - this.imageWidth_, this.imageHeight_, x, y, - this.imageWidth_, this.imageHeight_); - } - if (rotation !== 0 || this.imageScale_ != 1) { - context.setTransform(1, 0, 0, 1, 0, 0); - } - if (this.imageOpacity_ != 1) { - context.globalAlpha = alpha; - } -}; - - -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @private - */ -CanvasImmediateRenderer.prototype.drawText_ = function(flatCoordinates, offset, end, stride) { - if (!this.textState_ || this.text_ === '') { - return; - } - if (this.textFillState_) { - this.setContextFillState_(this.textFillState_); - } - if (this.textStrokeState_) { - this.setContextStrokeState_(this.textStrokeState_); - } - this.setContextTextState_(this.textState_); - const pixelCoordinates = transform2D( - flatCoordinates, offset, end, stride, this.transform_, - this.pixelCoordinates_); - const context = this.context_; - let rotation = this.textRotation_; - if (this.textRotateWithView_) { - rotation += this.viewRotation_; - } - for (; offset < end; offset += stride) { - const x = pixelCoordinates[offset] + this.textOffsetX_; - const y = pixelCoordinates[offset + 1] + this.textOffsetY_; - if (rotation !== 0 || this.textScale_ != 1) { - const localTransform = composeTransform(this.tmpLocalTransform_, - x, y, - this.textScale_, this.textScale_, - rotation, - -x, -y); - context.setTransform.apply(context, localTransform); - } - if (this.textStrokeState_) { - context.strokeText(this.text_, x, y); - } - if (this.textFillState_) { - context.fillText(this.text_, x, y); - } - } - if (rotation !== 0 || this.textScale_ != 1) { - context.setTransform(1, 0, 0, 1, 0, 0); - } -}; - - -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @param {boolean} close Close. - * @private - * @return {number} end End. - */ -CanvasImmediateRenderer.prototype.moveToLineTo_ = function(flatCoordinates, offset, end, stride, close) { - const context = this.context_; - const pixelCoordinates = transform2D( - flatCoordinates, offset, end, stride, this.transform_, - this.pixelCoordinates_); - context.moveTo(pixelCoordinates[0], pixelCoordinates[1]); - let length = pixelCoordinates.length; - if (close) { - length -= 2; - } - for (let i = 2; i < length; i += 2) { - context.lineTo(pixelCoordinates[i], pixelCoordinates[i + 1]); - } - if (close) { - context.closePath(); - } - return end; -}; - - -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {Array.} ends Ends. - * @param {number} stride Stride. - * @private - * @return {number} End. - */ -CanvasImmediateRenderer.prototype.drawRings_ = function(flatCoordinates, offset, ends, stride) { - for (let i = 0, ii = ends.length; i < ii; ++i) { - offset = this.moveToLineTo_(flatCoordinates, offset, ends[i], stride, true); - } - return offset; -}; - - -/** - * Render a circle geometry into the canvas. Rendering is immediate and uses - * the current fill and stroke styles. - * - * @param {module:ol/geom/Circle} geometry Circle geometry. - * @override - * @api - */ -CanvasImmediateRenderer.prototype.drawCircle = function(geometry) { - if (!intersects(this.extent_, geometry.getExtent())) { - return; - } - if (this.fillState_ || this.strokeState_) { - if (this.fillState_) { - this.setContextFillState_(this.fillState_); - } - if (this.strokeState_) { - this.setContextStrokeState_(this.strokeState_); - } - const pixelCoordinates = transformGeom2D( - geometry, this.transform_, this.pixelCoordinates_); - const dx = pixelCoordinates[2] - pixelCoordinates[0]; - const dy = pixelCoordinates[3] - pixelCoordinates[1]; - const radius = Math.sqrt(dx * dx + dy * dy); - const context = this.context_; - context.beginPath(); - context.arc( - pixelCoordinates[0], pixelCoordinates[1], radius, 0, 2 * Math.PI); - if (this.fillState_) { - context.fill(); - } - if (this.strokeState_) { - context.stroke(); - } - } - if (this.text_ !== '') { - this.drawText_(geometry.getCenter(), 0, 2, 2); - } -}; - - -/** - * Set the rendering style. Note that since this is an immediate rendering API, - * any `zIndex` on the provided style will be ignored. - * - * @param {module:ol/style/Style} style The rendering style. - * @override - * @api - */ -CanvasImmediateRenderer.prototype.setStyle = function(style) { - this.setFillStrokeStyle(style.getFill(), style.getStroke()); - this.setImageStyle(style.getImage()); - this.setTextStyle(style.getText()); -}; - - -/** - * Render a geometry into the canvas. Call - * {@link module:ol/render/canvas/Immediate#setStyle} first to set the rendering style. - * - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry The geometry to render. - * @override - * @api - */ -CanvasImmediateRenderer.prototype.drawGeometry = function(geometry) { - const type = geometry.getType(); - switch (type) { - case GeometryType.POINT: - this.drawPoint(/** @type {module:ol/geom/Point} */ (geometry)); - break; - case GeometryType.LINE_STRING: - this.drawLineString(/** @type {module:ol/geom/LineString} */ (geometry)); - break; - case GeometryType.POLYGON: - this.drawPolygon(/** @type {module:ol/geom/Polygon} */ (geometry)); - break; - case GeometryType.MULTI_POINT: - this.drawMultiPoint(/** @type {module:ol/geom/MultiPoint} */ (geometry)); - break; - case GeometryType.MULTI_LINE_STRING: - this.drawMultiLineString(/** @type {module:ol/geom/MultiLineString} */ (geometry)); - break; - case GeometryType.MULTI_POLYGON: - this.drawMultiPolygon(/** @type {module:ol/geom/MultiPolygon} */ (geometry)); - break; - case GeometryType.GEOMETRY_COLLECTION: - this.drawGeometryCollection(/** @type {module:ol/geom/GeometryCollection} */ (geometry)); - break; - case GeometryType.CIRCLE: - this.drawCircle(/** @type {module:ol/geom/Circle} */ (geometry)); - break; - default: - } -}; - - -/** - * Render a feature into the canvas. Note that any `zIndex` on the provided - * style will be ignored - features are rendered immediately in the order that - * this method is called. If you need `zIndex` support, you should be using an - * {@link module:ol/layer/Vector~VectorLayer} instead. - * - * @param {module:ol/Feature} feature Feature. - * @param {module:ol/style/Style} style Style. - * @override - * @api - */ -CanvasImmediateRenderer.prototype.drawFeature = function(feature, style) { - const geometry = style.getGeometryFunction()(feature); - if (!geometry || !intersects(this.extent_, geometry.getExtent())) { - return; - } - this.setStyle(style); - this.drawGeometry(geometry); -}; - - -/** - * Render a GeometryCollection to the canvas. Rendering is immediate and - * uses the current styles appropriate for each geometry in the collection. - * - * @param {module:ol/geom/GeometryCollection} geometry Geometry collection. - * @override - */ -CanvasImmediateRenderer.prototype.drawGeometryCollection = function(geometry) { - const geometries = geometry.getGeometriesArray(); - for (let i = 0, ii = geometries.length; i < ii; ++i) { - this.drawGeometry(geometries[i]); - } -}; - - -/** - * Render a Point geometry into the canvas. Rendering is immediate and uses - * the current style. - * - * @param {module:ol/geom/Point|module:ol/render/Feature} geometry Point geometry. - * @override - */ -CanvasImmediateRenderer.prototype.drawPoint = function(geometry) { - const flatCoordinates = geometry.getFlatCoordinates(); - const stride = geometry.getStride(); - if (this.image_) { - this.drawImages_(flatCoordinates, 0, flatCoordinates.length, stride); - } - if (this.text_ !== '') { - this.drawText_(flatCoordinates, 0, flatCoordinates.length, stride); - } -}; - - -/** - * Render a MultiPoint geometry into the canvas. Rendering is immediate and - * uses the current style. - * - * @param {module:ol/geom/MultiPoint|module:ol/render/Feature} geometry MultiPoint geometry. - * @override - */ -CanvasImmediateRenderer.prototype.drawMultiPoint = function(geometry) { - const flatCoordinates = geometry.getFlatCoordinates(); - const stride = geometry.getStride(); - if (this.image_) { - this.drawImages_(flatCoordinates, 0, flatCoordinates.length, stride); - } - if (this.text_ !== '') { - this.drawText_(flatCoordinates, 0, flatCoordinates.length, stride); - } -}; - - -/** - * Render a LineString into the canvas. Rendering is immediate and uses - * the current style. - * - * @param {module:ol/geom/LineString|module:ol/render/Feature} geometry LineString geometry. - * @override - */ -CanvasImmediateRenderer.prototype.drawLineString = function(geometry) { - if (!intersects(this.extent_, geometry.getExtent())) { - return; - } - if (this.strokeState_) { - this.setContextStrokeState_(this.strokeState_); - const context = this.context_; - const flatCoordinates = geometry.getFlatCoordinates(); - context.beginPath(); - this.moveToLineTo_(flatCoordinates, 0, flatCoordinates.length, - geometry.getStride(), false); - context.stroke(); - } - if (this.text_ !== '') { - const flatMidpoint = geometry.getFlatMidpoint(); - this.drawText_(flatMidpoint, 0, 2, 2); - } -}; - - -/** - * Render a MultiLineString geometry into the canvas. Rendering is immediate - * and uses the current style. - * - * @param {module:ol/geom/MultiLineString|module:ol/render/Feature} geometry MultiLineString geometry. - * @override - */ -CanvasImmediateRenderer.prototype.drawMultiLineString = function(geometry) { - const geometryExtent = geometry.getExtent(); - if (!intersects(this.extent_, geometryExtent)) { - return; - } - if (this.strokeState_) { - this.setContextStrokeState_(this.strokeState_); - const context = this.context_; - const flatCoordinates = geometry.getFlatCoordinates(); - let offset = 0; - const ends = geometry.getEnds(); - const stride = geometry.getStride(); - context.beginPath(); - for (let i = 0, ii = ends.length; i < ii; ++i) { - offset = this.moveToLineTo_(flatCoordinates, offset, ends[i], stride, false); - } - context.stroke(); - } - if (this.text_ !== '') { - const flatMidpoints = geometry.getFlatMidpoints(); - this.drawText_(flatMidpoints, 0, flatMidpoints.length, 2); - } -}; - - -/** - * Render a Polygon geometry into the canvas. Rendering is immediate and uses - * the current style. - * - * @param {module:ol/geom/Polygon|module:ol/render/Feature} geometry Polygon geometry. - * @override - */ -CanvasImmediateRenderer.prototype.drawPolygon = function(geometry) { - if (!intersects(this.extent_, geometry.getExtent())) { - return; - } - if (this.strokeState_ || this.fillState_) { - if (this.fillState_) { - this.setContextFillState_(this.fillState_); - } - if (this.strokeState_) { - this.setContextStrokeState_(this.strokeState_); - } - const context = this.context_; - context.beginPath(); - this.drawRings_(geometry.getOrientedFlatCoordinates(), - 0, geometry.getEnds(), geometry.getStride()); - if (this.fillState_) { - context.fill(); - } - if (this.strokeState_) { - context.stroke(); - } - } - if (this.text_ !== '') { - const flatInteriorPoint = geometry.getFlatInteriorPoint(); - this.drawText_(flatInteriorPoint, 0, 2, 2); - } -}; - - -/** - * Render MultiPolygon geometry into the canvas. Rendering is immediate and - * uses the current style. - * @param {module:ol/geom/MultiPolygon} geometry MultiPolygon geometry. - * @override - */ -CanvasImmediateRenderer.prototype.drawMultiPolygon = function(geometry) { - if (!intersects(this.extent_, geometry.getExtent())) { - return; - } - if (this.strokeState_ || this.fillState_) { - if (this.fillState_) { - this.setContextFillState_(this.fillState_); - } - if (this.strokeState_) { - this.setContextStrokeState_(this.strokeState_); - } - const context = this.context_; - const flatCoordinates = geometry.getOrientedFlatCoordinates(); - let offset = 0; - const endss = geometry.getEndss(); - const stride = geometry.getStride(); - context.beginPath(); - for (let i = 0, ii = endss.length; i < ii; ++i) { - const ends = endss[i]; - offset = this.drawRings_(flatCoordinates, offset, ends, stride); - } - if (this.fillState_) { - context.fill(); - } - if (this.strokeState_) { - context.stroke(); - } - } - if (this.text_ !== '') { - const flatInteriorPoints = geometry.getFlatInteriorPoints(); - this.drawText_(flatInteriorPoints, 0, flatInteriorPoints.length, 2); - } -}; - - -/** - * @param {module:ol/render/canvas~FillState} fillState Fill state. - * @private - */ -CanvasImmediateRenderer.prototype.setContextFillState_ = function(fillState) { - const context = this.context_; - const contextFillState = this.contextFillState_; - if (!contextFillState) { - context.fillStyle = fillState.fillStyle; - this.contextFillState_ = { - fillStyle: fillState.fillStyle - }; - } else { - if (contextFillState.fillStyle != fillState.fillStyle) { - contextFillState.fillStyle = context.fillStyle = fillState.fillStyle; - } - } -}; - - -/** - * @param {module:ol/render/canvas~StrokeState} strokeState Stroke state. - * @private - */ -CanvasImmediateRenderer.prototype.setContextStrokeState_ = function(strokeState) { - const context = this.context_; - const contextStrokeState = this.contextStrokeState_; - if (!contextStrokeState) { - context.lineCap = strokeState.lineCap; - if (CANVAS_LINE_DASH) { - context.setLineDash(strokeState.lineDash); - context.lineDashOffset = strokeState.lineDashOffset; - } - context.lineJoin = strokeState.lineJoin; - context.lineWidth = strokeState.lineWidth; - context.miterLimit = strokeState.miterLimit; - context.strokeStyle = strokeState.strokeStyle; - this.contextStrokeState_ = { - lineCap: strokeState.lineCap, - lineDash: strokeState.lineDash, - lineDashOffset: strokeState.lineDashOffset, - lineJoin: strokeState.lineJoin, - lineWidth: strokeState.lineWidth, - miterLimit: strokeState.miterLimit, - strokeStyle: strokeState.strokeStyle - }; - } else { - if (contextStrokeState.lineCap != strokeState.lineCap) { - contextStrokeState.lineCap = context.lineCap = strokeState.lineCap; - } - if (CANVAS_LINE_DASH) { - if (!equals(contextStrokeState.lineDash, strokeState.lineDash)) { - context.setLineDash(contextStrokeState.lineDash = strokeState.lineDash); - } - if (contextStrokeState.lineDashOffset != strokeState.lineDashOffset) { - contextStrokeState.lineDashOffset = context.lineDashOffset = - strokeState.lineDashOffset; - } - } - if (contextStrokeState.lineJoin != strokeState.lineJoin) { - contextStrokeState.lineJoin = context.lineJoin = strokeState.lineJoin; - } - if (contextStrokeState.lineWidth != strokeState.lineWidth) { - contextStrokeState.lineWidth = context.lineWidth = strokeState.lineWidth; - } - if (contextStrokeState.miterLimit != strokeState.miterLimit) { - contextStrokeState.miterLimit = context.miterLimit = - strokeState.miterLimit; - } - if (contextStrokeState.strokeStyle != strokeState.strokeStyle) { - contextStrokeState.strokeStyle = context.strokeStyle = - strokeState.strokeStyle; - } - } -}; - - -/** - * @param {module:ol/render/canvas~TextState} textState Text state. - * @private - */ -CanvasImmediateRenderer.prototype.setContextTextState_ = function(textState) { - const context = this.context_; - const contextTextState = this.contextTextState_; - const textAlign = textState.textAlign ? - textState.textAlign : defaultTextAlign; - if (!contextTextState) { - context.font = textState.font; - context.textAlign = textAlign; - context.textBaseline = textState.textBaseline; - this.contextTextState_ = { - font: textState.font, - textAlign: textAlign, - textBaseline: textState.textBaseline - }; - } else { - if (contextTextState.font != textState.font) { - contextTextState.font = context.font = textState.font; - } - if (contextTextState.textAlign != textAlign) { - contextTextState.textAlign = context.textAlign = textAlign; - } - if (contextTextState.textBaseline != textState.textBaseline) { - contextTextState.textBaseline = context.textBaseline = - textState.textBaseline; - } - } -}; - - -/** - * Set the fill and stroke style for subsequent draw operations. To clear - * either fill or stroke styles, pass null for the appropriate parameter. - * - * @param {module:ol/style/Fill} fillStyle Fill style. - * @param {module:ol/style/Stroke} strokeStyle Stroke style. - * @override - */ -CanvasImmediateRenderer.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { - if (!fillStyle) { - this.fillState_ = null; - } else { - const fillStyleColor = fillStyle.getColor(); - this.fillState_ = { - fillStyle: asColorLike(fillStyleColor ? - fillStyleColor : defaultFillStyle) - }; - } - if (!strokeStyle) { - this.strokeState_ = null; - } else { - const strokeStyleColor = strokeStyle.getColor(); - const strokeStyleLineCap = strokeStyle.getLineCap(); - const strokeStyleLineDash = strokeStyle.getLineDash(); - const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); - const strokeStyleLineJoin = strokeStyle.getLineJoin(); - const strokeStyleWidth = strokeStyle.getWidth(); - const strokeStyleMiterLimit = strokeStyle.getMiterLimit(); - this.strokeState_ = { - lineCap: strokeStyleLineCap !== undefined ? - strokeStyleLineCap : defaultLineCap, - lineDash: strokeStyleLineDash ? - strokeStyleLineDash : defaultLineDash, - lineDashOffset: strokeStyleLineDashOffset ? - strokeStyleLineDashOffset : defaultLineDashOffset, - lineJoin: strokeStyleLineJoin !== undefined ? - strokeStyleLineJoin : defaultLineJoin, - lineWidth: this.pixelRatio_ * (strokeStyleWidth !== undefined ? - strokeStyleWidth : defaultLineWidth), - miterLimit: strokeStyleMiterLimit !== undefined ? - strokeStyleMiterLimit : defaultMiterLimit, - strokeStyle: asColorLike(strokeStyleColor ? - strokeStyleColor : defaultStrokeStyle) - }; - } -}; - - -/** - * Set the image style for subsequent draw operations. Pass null to remove - * the image style. - * - * @param {module:ol/style/Image} imageStyle Image style. - * @override - */ -CanvasImmediateRenderer.prototype.setImageStyle = function(imageStyle) { - if (!imageStyle) { - this.image_ = null; - } else { - const imageAnchor = imageStyle.getAnchor(); - // FIXME pixel ratio - const imageImage = imageStyle.getImage(1); - const imageOrigin = imageStyle.getOrigin(); - const imageSize = imageStyle.getSize(); - this.imageAnchorX_ = imageAnchor[0]; - this.imageAnchorY_ = imageAnchor[1]; - this.imageHeight_ = imageSize[1]; - this.image_ = imageImage; - this.imageOpacity_ = imageStyle.getOpacity(); - this.imageOriginX_ = imageOrigin[0]; - this.imageOriginY_ = imageOrigin[1]; - this.imageRotateWithView_ = imageStyle.getRotateWithView(); - this.imageRotation_ = imageStyle.getRotation(); - this.imageScale_ = imageStyle.getScale() * this.pixelRatio_; - this.imageSnapToPixel_ = imageStyle.getSnapToPixel(); - this.imageWidth_ = imageSize[0]; - } -}; - - -/** - * Set the text style for subsequent draw operations. Pass null to - * remove the text style. - * - * @param {module:ol/style/Text} textStyle Text style. - * @override - */ -CanvasImmediateRenderer.prototype.setTextStyle = function(textStyle) { - if (!textStyle) { - this.text_ = ''; - } else { - const textFillStyle = textStyle.getFill(); - if (!textFillStyle) { - this.textFillState_ = null; - } else { - const textFillStyleColor = textFillStyle.getColor(); - this.textFillState_ = { - fillStyle: asColorLike(textFillStyleColor ? - textFillStyleColor : defaultFillStyle) - }; - } - const textStrokeStyle = textStyle.getStroke(); - if (!textStrokeStyle) { - this.textStrokeState_ = null; - } else { - const textStrokeStyleColor = textStrokeStyle.getColor(); - const textStrokeStyleLineCap = textStrokeStyle.getLineCap(); - const textStrokeStyleLineDash = textStrokeStyle.getLineDash(); - const textStrokeStyleLineDashOffset = textStrokeStyle.getLineDashOffset(); - const textStrokeStyleLineJoin = textStrokeStyle.getLineJoin(); - const textStrokeStyleWidth = textStrokeStyle.getWidth(); - const textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit(); - this.textStrokeState_ = { - lineCap: textStrokeStyleLineCap !== undefined ? - textStrokeStyleLineCap : defaultLineCap, - lineDash: textStrokeStyleLineDash ? - textStrokeStyleLineDash : defaultLineDash, - lineDashOffset: textStrokeStyleLineDashOffset ? - textStrokeStyleLineDashOffset : defaultLineDashOffset, - lineJoin: textStrokeStyleLineJoin !== undefined ? - textStrokeStyleLineJoin : defaultLineJoin, - lineWidth: textStrokeStyleWidth !== undefined ? - textStrokeStyleWidth : defaultLineWidth, - miterLimit: textStrokeStyleMiterLimit !== undefined ? - textStrokeStyleMiterLimit : defaultMiterLimit, - strokeStyle: asColorLike(textStrokeStyleColor ? - textStrokeStyleColor : defaultStrokeStyle) - }; - } - const textFont = textStyle.getFont(); - const textOffsetX = textStyle.getOffsetX(); - const textOffsetY = textStyle.getOffsetY(); - const textRotateWithView = textStyle.getRotateWithView(); - const textRotation = textStyle.getRotation(); - const textScale = textStyle.getScale(); - const textText = textStyle.getText(); - const textTextAlign = textStyle.getTextAlign(); - const textTextBaseline = textStyle.getTextBaseline(); - this.textState_ = { - font: textFont !== undefined ? - textFont : defaultFont, - textAlign: textTextAlign !== undefined ? - textTextAlign : defaultTextAlign, - textBaseline: textTextBaseline !== undefined ? - textTextBaseline : defaultTextBaseline - }; - this.text_ = textText !== undefined ? textText : ''; - this.textOffsetX_ = - textOffsetX !== undefined ? (this.pixelRatio_ * textOffsetX) : 0; - this.textOffsetY_ = - textOffsetY !== undefined ? (this.pixelRatio_ * textOffsetY) : 0; - this.textRotateWithView_ = textRotateWithView !== undefined ? textRotateWithView : false; - this.textRotation_ = textRotation !== undefined ? textRotation : 0; - this.textScale_ = this.pixelRatio_ * (textScale !== undefined ? - textScale : 1); - } -}; export default CanvasImmediateRenderer; diff --git a/src/ol/render/canvas/LineStringReplay.js b/src/ol/render/canvas/LineStringReplay.js index c68da1b111..1e99544fcc 100644 --- a/src/ol/render/canvas/LineStringReplay.js +++ b/src/ol/render/canvas/LineStringReplay.js @@ -16,111 +16,109 @@ import CanvasReplay from '../canvas/Replay.js'; * @param {?} declutterTree Declutter tree. * @struct */ -const CanvasLineStringReplay = function( - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { - CanvasReplay.call(this, - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); -}; +class CanvasLineStringReplay { + constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + CanvasReplay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); + } + + /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @private + * @return {number} end. + */ + drawFlatCoordinates_(flatCoordinates, offset, end, stride) { + const myBegin = this.coordinates.length; + const myEnd = this.appendFlatCoordinates( + flatCoordinates, offset, end, stride, false, false); + const moveToLineToInstruction = [CanvasInstruction.MOVE_TO_LINE_TO, myBegin, myEnd]; + this.instructions.push(moveToLineToInstruction); + this.hitDetectionInstructions.push(moveToLineToInstruction); + return end; + } + + /** + * @inheritDoc + */ + drawLineString(lineStringGeometry, feature) { + const state = this.state; + const strokeStyle = state.strokeStyle; + const lineWidth = state.lineWidth; + if (strokeStyle === undefined || lineWidth === undefined) { + return; + } + this.updateStrokeStyle(state, this.applyStroke); + this.beginGeometry(lineStringGeometry, feature); + this.hitDetectionInstructions.push([ + CanvasInstruction.SET_STROKE_STYLE, + state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, + state.miterLimit, state.lineDash, state.lineDashOffset + ], beginPathInstruction); + const flatCoordinates = lineStringGeometry.getFlatCoordinates(); + const stride = lineStringGeometry.getStride(); + this.drawFlatCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride); + this.hitDetectionInstructions.push(strokeInstruction); + this.endGeometry(lineStringGeometry, feature); + } + + /** + * @inheritDoc + */ + drawMultiLineString(multiLineStringGeometry, feature) { + const state = this.state; + const strokeStyle = state.strokeStyle; + const lineWidth = state.lineWidth; + if (strokeStyle === undefined || lineWidth === undefined) { + return; + } + this.updateStrokeStyle(state, this.applyStroke); + this.beginGeometry(multiLineStringGeometry, feature); + this.hitDetectionInstructions.push([ + CanvasInstruction.SET_STROKE_STYLE, + state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, + state.miterLimit, state.lineDash, state.lineDashOffset + ], beginPathInstruction); + const ends = multiLineStringGeometry.getEnds(); + const flatCoordinates = multiLineStringGeometry.getFlatCoordinates(); + const stride = multiLineStringGeometry.getStride(); + let offset = 0; + for (let i = 0, ii = ends.length; i < ii; ++i) { + offset = this.drawFlatCoordinates_(flatCoordinates, offset, ends[i], stride); + } + this.hitDetectionInstructions.push(strokeInstruction); + this.endGeometry(multiLineStringGeometry, feature); + } + + /** + * @inheritDoc + */ + finish() { + const state = this.state; + if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) { + this.instructions.push(strokeInstruction); + } + this.reverseHitDetectionInstructions(); + this.state = null; + } + + /** + * @inheritDoc. + */ + applyStroke(state) { + if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) { + this.instructions.push(strokeInstruction); + state.lastStroke = this.coordinates.length; + } + state.lastStroke = 0; + CanvasReplay.prototype.applyStroke.call(this, state); + this.instructions.push(beginPathInstruction); + } +} inherits(CanvasLineStringReplay, CanvasReplay); -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @private - * @return {number} end. - */ -CanvasLineStringReplay.prototype.drawFlatCoordinates_ = function(flatCoordinates, offset, end, stride) { - const myBegin = this.coordinates.length; - const myEnd = this.appendFlatCoordinates( - flatCoordinates, offset, end, stride, false, false); - const moveToLineToInstruction = [CanvasInstruction.MOVE_TO_LINE_TO, myBegin, myEnd]; - this.instructions.push(moveToLineToInstruction); - this.hitDetectionInstructions.push(moveToLineToInstruction); - return end; -}; - - -/** - * @inheritDoc - */ -CanvasLineStringReplay.prototype.drawLineString = function(lineStringGeometry, feature) { - const state = this.state; - const strokeStyle = state.strokeStyle; - const lineWidth = state.lineWidth; - if (strokeStyle === undefined || lineWidth === undefined) { - return; - } - this.updateStrokeStyle(state, this.applyStroke); - this.beginGeometry(lineStringGeometry, feature); - this.hitDetectionInstructions.push([ - CanvasInstruction.SET_STROKE_STYLE, - state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, - state.miterLimit, state.lineDash, state.lineDashOffset - ], beginPathInstruction); - const flatCoordinates = lineStringGeometry.getFlatCoordinates(); - const stride = lineStringGeometry.getStride(); - this.drawFlatCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride); - this.hitDetectionInstructions.push(strokeInstruction); - this.endGeometry(lineStringGeometry, feature); -}; - - -/** - * @inheritDoc - */ -CanvasLineStringReplay.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) { - const state = this.state; - const strokeStyle = state.strokeStyle; - const lineWidth = state.lineWidth; - if (strokeStyle === undefined || lineWidth === undefined) { - return; - } - this.updateStrokeStyle(state, this.applyStroke); - this.beginGeometry(multiLineStringGeometry, feature); - this.hitDetectionInstructions.push([ - CanvasInstruction.SET_STROKE_STYLE, - state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, - state.miterLimit, state.lineDash, state.lineDashOffset - ], beginPathInstruction); - const ends = multiLineStringGeometry.getEnds(); - const flatCoordinates = multiLineStringGeometry.getFlatCoordinates(); - const stride = multiLineStringGeometry.getStride(); - let offset = 0; - for (let i = 0, ii = ends.length; i < ii; ++i) { - offset = this.drawFlatCoordinates_(flatCoordinates, offset, ends[i], stride); - } - this.hitDetectionInstructions.push(strokeInstruction); - this.endGeometry(multiLineStringGeometry, feature); -}; - - -/** - * @inheritDoc - */ -CanvasLineStringReplay.prototype.finish = function() { - const state = this.state; - if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) { - this.instructions.push(strokeInstruction); - } - this.reverseHitDetectionInstructions(); - this.state = null; -}; - - -/** - * @inheritDoc. - */ -CanvasLineStringReplay.prototype.applyStroke = function(state) { - if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) { - this.instructions.push(strokeInstruction); - state.lastStroke = this.coordinates.length; - } - state.lastStroke = 0; - CanvasReplay.prototype.applyStroke.call(this, state); - this.instructions.push(beginPathInstruction); -}; export default CanvasLineStringReplay; diff --git a/src/ol/render/canvas/PolygonReplay.js b/src/ol/render/canvas/PolygonReplay.js index d73453a8d0..efb597f3dd 100644 --- a/src/ol/render/canvas/PolygonReplay.js +++ b/src/ol/render/canvas/PolygonReplay.js @@ -22,203 +22,200 @@ import CanvasReplay from '../canvas/Replay.js'; * @param {?} declutterTree Declutter tree. * @struct */ -const CanvasPolygonReplay = function( - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { - CanvasReplay.call(this, - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); -}; +class CanvasPolygonReplay { + constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + CanvasReplay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); + } + + /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {Array.} ends Ends. + * @param {number} stride Stride. + * @private + * @return {number} End. + */ + drawFlatCoordinatess_(flatCoordinates, offset, ends, stride) { + const state = this.state; + const fill = state.fillStyle !== undefined; + const stroke = state.strokeStyle != undefined; + const numEnds = ends.length; + this.instructions.push(beginPathInstruction); + this.hitDetectionInstructions.push(beginPathInstruction); + for (let i = 0; i < numEnds; ++i) { + const end = ends[i]; + const myBegin = this.coordinates.length; + const myEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, true, !stroke); + const moveToLineToInstruction = [CanvasInstruction.MOVE_TO_LINE_TO, myBegin, myEnd]; + this.instructions.push(moveToLineToInstruction); + this.hitDetectionInstructions.push(moveToLineToInstruction); + if (stroke) { + // Performance optimization: only call closePath() when we have a stroke. + // Otherwise the ring is closed already (see appendFlatCoordinates above). + this.instructions.push(closePathInstruction); + this.hitDetectionInstructions.push(closePathInstruction); + } + offset = end; + } + if (fill) { + this.instructions.push(fillInstruction); + this.hitDetectionInstructions.push(fillInstruction); + } + if (stroke) { + this.instructions.push(strokeInstruction); + this.hitDetectionInstructions.push(strokeInstruction); + } + return offset; + } + + /** + * @inheritDoc + */ + drawCircle(circleGeometry, feature) { + const state = this.state; + const fillStyle = state.fillStyle; + const strokeStyle = state.strokeStyle; + if (fillStyle === undefined && strokeStyle === undefined) { + return; + } + this.setFillStrokeStyles_(circleGeometry); + this.beginGeometry(circleGeometry, feature); + if (state.fillStyle !== undefined) { + this.hitDetectionInstructions.push([ + CanvasInstruction.SET_FILL_STYLE, + asString(defaultFillStyle) + ]); + } + if (state.strokeStyle !== undefined) { + this.hitDetectionInstructions.push([ + CanvasInstruction.SET_STROKE_STYLE, + state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, + state.miterLimit, state.lineDash, state.lineDashOffset + ]); + } + const flatCoordinates = circleGeometry.getFlatCoordinates(); + const stride = circleGeometry.getStride(); + const myBegin = this.coordinates.length; + this.appendFlatCoordinates( + flatCoordinates, 0, flatCoordinates.length, stride, false, false); + const circleInstruction = [CanvasInstruction.CIRCLE, myBegin]; + this.instructions.push(beginPathInstruction, circleInstruction); + this.hitDetectionInstructions.push(beginPathInstruction, circleInstruction); + this.hitDetectionInstructions.push(fillInstruction); + if (state.fillStyle !== undefined) { + this.instructions.push(fillInstruction); + } + if (state.strokeStyle !== undefined) { + this.instructions.push(strokeInstruction); + this.hitDetectionInstructions.push(strokeInstruction); + } + this.endGeometry(circleGeometry, feature); + } + + /** + * @inheritDoc + */ + drawPolygon(polygonGeometry, feature) { + const state = this.state; + const fillStyle = state.fillStyle; + const strokeStyle = state.strokeStyle; + if (fillStyle === undefined && strokeStyle === undefined) { + return; + } + this.setFillStrokeStyles_(polygonGeometry); + this.beginGeometry(polygonGeometry, feature); + if (state.fillStyle !== undefined) { + this.hitDetectionInstructions.push([ + CanvasInstruction.SET_FILL_STYLE, + asString(defaultFillStyle) + ]); + } + if (state.strokeStyle !== undefined) { + this.hitDetectionInstructions.push([ + CanvasInstruction.SET_STROKE_STYLE, + state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, + state.miterLimit, state.lineDash, state.lineDashOffset + ]); + } + const ends = polygonGeometry.getEnds(); + const flatCoordinates = polygonGeometry.getOrientedFlatCoordinates(); + const stride = polygonGeometry.getStride(); + this.drawFlatCoordinatess_(flatCoordinates, 0, ends, stride); + this.endGeometry(polygonGeometry, feature); + } + + /** + * @inheritDoc + */ + drawMultiPolygon(multiPolygonGeometry, feature) { + const state = this.state; + const fillStyle = state.fillStyle; + const strokeStyle = state.strokeStyle; + if (fillStyle === undefined && strokeStyle === undefined) { + return; + } + this.setFillStrokeStyles_(multiPolygonGeometry); + this.beginGeometry(multiPolygonGeometry, feature); + if (state.fillStyle !== undefined) { + this.hitDetectionInstructions.push([ + CanvasInstruction.SET_FILL_STYLE, + asString(defaultFillStyle) + ]); + } + if (state.strokeStyle !== undefined) { + this.hitDetectionInstructions.push([ + CanvasInstruction.SET_STROKE_STYLE, + state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, + state.miterLimit, state.lineDash, state.lineDashOffset + ]); + } + const endss = multiPolygonGeometry.getEndss(); + const flatCoordinates = multiPolygonGeometry.getOrientedFlatCoordinates(); + const stride = multiPolygonGeometry.getStride(); + let offset = 0; + for (let i = 0, ii = endss.length; i < ii; ++i) { + offset = this.drawFlatCoordinatess_(flatCoordinates, offset, endss[i], stride); + } + this.endGeometry(multiPolygonGeometry, feature); + } + + /** + * @inheritDoc + */ + finish() { + this.reverseHitDetectionInstructions(); + this.state = null; + // We want to preserve topology when drawing polygons. Polygons are + // simplified using quantization and point elimination. However, we might + // have received a mix of quantized and non-quantized geometries, so ensure + // that all are quantized by quantizing all coordinates in the batch. + const tolerance = this.tolerance; + if (tolerance !== 0) { + const coordinates = this.coordinates; + for (let i = 0, ii = coordinates.length; i < ii; ++i) { + coordinates[i] = snap(coordinates[i], tolerance); + } + } + } + + /** + * @private + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. + */ + setFillStrokeStyles_(geometry) { + const state = this.state; + const fillStyle = state.fillStyle; + if (fillStyle !== undefined) { + this.updateFillStyle(state, this.createFill, geometry); + } + if (state.strokeStyle !== undefined) { + this.updateStrokeStyle(state, this.applyStroke); + } + } +} inherits(CanvasPolygonReplay, CanvasReplay); -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {Array.} ends Ends. - * @param {number} stride Stride. - * @private - * @return {number} End. - */ -CanvasPolygonReplay.prototype.drawFlatCoordinatess_ = function(flatCoordinates, offset, ends, stride) { - const state = this.state; - const fill = state.fillStyle !== undefined; - const stroke = state.strokeStyle != undefined; - const numEnds = ends.length; - this.instructions.push(beginPathInstruction); - this.hitDetectionInstructions.push(beginPathInstruction); - for (let i = 0; i < numEnds; ++i) { - const end = ends[i]; - const myBegin = this.coordinates.length; - const myEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, true, !stroke); - const moveToLineToInstruction = [CanvasInstruction.MOVE_TO_LINE_TO, myBegin, myEnd]; - this.instructions.push(moveToLineToInstruction); - this.hitDetectionInstructions.push(moveToLineToInstruction); - if (stroke) { - // Performance optimization: only call closePath() when we have a stroke. - // Otherwise the ring is closed already (see appendFlatCoordinates above). - this.instructions.push(closePathInstruction); - this.hitDetectionInstructions.push(closePathInstruction); - } - offset = end; - } - if (fill) { - this.instructions.push(fillInstruction); - this.hitDetectionInstructions.push(fillInstruction); - } - if (stroke) { - this.instructions.push(strokeInstruction); - this.hitDetectionInstructions.push(strokeInstruction); - } - return offset; -}; - - -/** - * @inheritDoc - */ -CanvasPolygonReplay.prototype.drawCircle = function(circleGeometry, feature) { - const state = this.state; - const fillStyle = state.fillStyle; - const strokeStyle = state.strokeStyle; - if (fillStyle === undefined && strokeStyle === undefined) { - return; - } - this.setFillStrokeStyles_(circleGeometry); - this.beginGeometry(circleGeometry, feature); - if (state.fillStyle !== undefined) { - this.hitDetectionInstructions.push([ - CanvasInstruction.SET_FILL_STYLE, - asString(defaultFillStyle) - ]); - } - if (state.strokeStyle !== undefined) { - this.hitDetectionInstructions.push([ - CanvasInstruction.SET_STROKE_STYLE, - state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, - state.miterLimit, state.lineDash, state.lineDashOffset - ]); - } - const flatCoordinates = circleGeometry.getFlatCoordinates(); - const stride = circleGeometry.getStride(); - const myBegin = this.coordinates.length; - this.appendFlatCoordinates( - flatCoordinates, 0, flatCoordinates.length, stride, false, false); - const circleInstruction = [CanvasInstruction.CIRCLE, myBegin]; - this.instructions.push(beginPathInstruction, circleInstruction); - this.hitDetectionInstructions.push(beginPathInstruction, circleInstruction); - this.hitDetectionInstructions.push(fillInstruction); - if (state.fillStyle !== undefined) { - this.instructions.push(fillInstruction); - } - if (state.strokeStyle !== undefined) { - this.instructions.push(strokeInstruction); - this.hitDetectionInstructions.push(strokeInstruction); - } - this.endGeometry(circleGeometry, feature); -}; - - -/** - * @inheritDoc - */ -CanvasPolygonReplay.prototype.drawPolygon = function(polygonGeometry, feature) { - const state = this.state; - const fillStyle = state.fillStyle; - const strokeStyle = state.strokeStyle; - if (fillStyle === undefined && strokeStyle === undefined) { - return; - } - this.setFillStrokeStyles_(polygonGeometry); - this.beginGeometry(polygonGeometry, feature); - if (state.fillStyle !== undefined) { - this.hitDetectionInstructions.push([ - CanvasInstruction.SET_FILL_STYLE, - asString(defaultFillStyle) - ]); - } - if (state.strokeStyle !== undefined) { - this.hitDetectionInstructions.push([ - CanvasInstruction.SET_STROKE_STYLE, - state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, - state.miterLimit, state.lineDash, state.lineDashOffset - ]); - } - const ends = polygonGeometry.getEnds(); - const flatCoordinates = polygonGeometry.getOrientedFlatCoordinates(); - const stride = polygonGeometry.getStride(); - this.drawFlatCoordinatess_(flatCoordinates, 0, ends, stride); - this.endGeometry(polygonGeometry, feature); -}; - - -/** - * @inheritDoc - */ -CanvasPolygonReplay.prototype.drawMultiPolygon = function(multiPolygonGeometry, feature) { - const state = this.state; - const fillStyle = state.fillStyle; - const strokeStyle = state.strokeStyle; - if (fillStyle === undefined && strokeStyle === undefined) { - return; - } - this.setFillStrokeStyles_(multiPolygonGeometry); - this.beginGeometry(multiPolygonGeometry, feature); - if (state.fillStyle !== undefined) { - this.hitDetectionInstructions.push([ - CanvasInstruction.SET_FILL_STYLE, - asString(defaultFillStyle) - ]); - } - if (state.strokeStyle !== undefined) { - this.hitDetectionInstructions.push([ - CanvasInstruction.SET_STROKE_STYLE, - state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin, - state.miterLimit, state.lineDash, state.lineDashOffset - ]); - } - const endss = multiPolygonGeometry.getEndss(); - const flatCoordinates = multiPolygonGeometry.getOrientedFlatCoordinates(); - const stride = multiPolygonGeometry.getStride(); - let offset = 0; - for (let i = 0, ii = endss.length; i < ii; ++i) { - offset = this.drawFlatCoordinatess_(flatCoordinates, offset, endss[i], stride); - } - this.endGeometry(multiPolygonGeometry, feature); -}; - - -/** - * @inheritDoc - */ -CanvasPolygonReplay.prototype.finish = function() { - this.reverseHitDetectionInstructions(); - this.state = null; - // We want to preserve topology when drawing polygons. Polygons are - // simplified using quantization and point elimination. However, we might - // have received a mix of quantized and non-quantized geometries, so ensure - // that all are quantized by quantizing all coordinates in the batch. - const tolerance = this.tolerance; - if (tolerance !== 0) { - const coordinates = this.coordinates; - for (let i = 0, ii = coordinates.length; i < ii; ++i) { - coordinates[i] = snap(coordinates[i], tolerance); - } - } -}; - - -/** - * @private - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. - */ -CanvasPolygonReplay.prototype.setFillStrokeStyles_ = function(geometry) { - const state = this.state; - const fillStyle = state.fillStyle; - if (fillStyle !== undefined) { - this.updateFillStyle(state, this.createFill, geometry); - } - if (state.strokeStyle !== undefined) { - this.updateStrokeStyle(state, this.applyStroke); - } -}; export default CanvasPolygonReplay; diff --git a/src/ol/render/canvas/Replay.js b/src/ol/render/canvas/Replay.js index c54c63edec..398b34bbda 100644 --- a/src/ol/render/canvas/Replay.js +++ b/src/ol/render/canvas/Replay.js @@ -39,125 +39,1035 @@ import { * @param {?} declutterTree Declutter tree. * @struct */ -const CanvasReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { - VectorContext.call(this); +class CanvasReplay { + constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + VectorContext.call(this); + + /** + * @type {?} + */ + this.declutterTree = declutterTree; + + /** + * @protected + * @type {number} + */ + this.tolerance = tolerance; + + /** + * @protected + * @const + * @type {module:ol/extent~Extent} + */ + this.maxExtent = maxExtent; + + /** + * @protected + * @type {boolean} + */ + this.overlaps = overlaps; + + /** + * @protected + * @type {number} + */ + this.pixelRatio = pixelRatio; + + /** + * @protected + * @type {number} + */ + this.maxLineWidth = 0; + + /** + * @protected + * @const + * @type {number} + */ + this.resolution = resolution; + + /** + * @private + * @type {boolean} + */ + this.alignFill_; + + /** + * @private + * @type {Array.<*>} + */ + this.beginGeometryInstruction1_ = null; + + /** + * @private + * @type {Array.<*>} + */ + this.beginGeometryInstruction2_ = null; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.bufferedMaxExtent_ = null; + + /** + * @protected + * @type {Array.<*>} + */ + this.instructions = []; + + /** + * @protected + * @type {Array.} + */ + this.coordinates = []; + + /** + * @private + * @type {!Object.|Array.>>} + */ + this.coordinateCache_ = {}; + + /** + * @private + * @type {!module:ol/transform~Transform} + */ + this.renderedTransform_ = createTransform(); + + /** + * @protected + * @type {Array.<*>} + */ + this.hitDetectionInstructions = []; + + /** + * @private + * @type {Array.} + */ + this.pixelCoordinates_ = null; + + /** + * @protected + * @type {module:ol/render/canvas~FillStrokeState} + */ + this.state = /** @type {module:ol/render/canvas~FillStrokeState} */ ({}); + + /** + * @private + * @type {number} + */ + this.viewRotation_ = 0; + + } /** - * @type {?} + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/coordinate~Coordinate} p1 1st point of the background box. + * @param {module:ol/coordinate~Coordinate} p2 2nd point of the background box. + * @param {module:ol/coordinate~Coordinate} p3 3rd point of the background box. + * @param {module:ol/coordinate~Coordinate} p4 4th point of the background box. + * @param {Array.<*>} fillInstruction Fill instruction. + * @param {Array.<*>} strokeInstruction Stroke instruction. */ - this.declutterTree = declutterTree; + replayTextBackground_(context, p1, p2, p3, p4, fillInstruction, strokeInstruction) { + context.beginPath(); + context.moveTo.apply(context, p1); + context.lineTo.apply(context, p2); + context.lineTo.apply(context, p3); + context.lineTo.apply(context, p4); + context.lineTo.apply(context, p1); + if (fillInstruction) { + this.alignFill_ = /** @type {boolean} */ (fillInstruction[2]); + this.fill_(context); + } + if (strokeInstruction) { + this.setStrokeStyle_(context, /** @type {Array.<*>} */ (strokeInstruction)); + context.stroke(); + } + } + + /** + * @param {CanvasRenderingContext2D} context Context. + * @param {number} x X. + * @param {number} y Y. + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image. + * @param {number} anchorX Anchor X. + * @param {number} anchorY Anchor Y. + * @param {module:ol/render/canvas~DeclutterGroup} declutterGroup Declutter group. + * @param {number} height Height. + * @param {number} opacity Opacity. + * @param {number} originX Origin X. + * @param {number} originY Origin Y. + * @param {number} rotation Rotation. + * @param {number} scale Scale. + * @param {boolean} snapToPixel Snap to pixel. + * @param {number} width Width. + * @param {Array.} padding Padding. + * @param {Array.<*>} fillInstruction Fill instruction. + * @param {Array.<*>} strokeInstruction Stroke instruction. + */ + replayImage_( + context, + x, + y, + image, + anchorX, + anchorY, + declutterGroup, + height, + opacity, + originX, + originY, + rotation, + scale, + snapToPixel, + width, + padding, + fillInstruction, + strokeInstruction + ) { + const fillStroke = fillInstruction || strokeInstruction; + anchorX *= scale; + anchorY *= scale; + x -= anchorX; + y -= anchorY; + + const w = (width + originX > image.width) ? image.width - originX : width; + const h = (height + originY > image.height) ? image.height - originY : height; + const boxW = padding[3] + w * scale + padding[1]; + const boxH = padding[0] + h * scale + padding[2]; + const boxX = x - padding[3]; + const boxY = y - padding[0]; + + /** @type {module:ol/coordinate~Coordinate} */ + let p1; + /** @type {module:ol/coordinate~Coordinate} */ + let p2; + /** @type {module:ol/coordinate~Coordinate} */ + let p3; + /** @type {module:ol/coordinate~Coordinate} */ + let p4; + if (fillStroke || rotation !== 0) { + p1 = [boxX, boxY]; + p2 = [boxX + boxW, boxY]; + p3 = [boxX + boxW, boxY + boxH]; + p4 = [boxX, boxY + boxH]; + } + + let transform = null; + if (rotation !== 0) { + const centerX = x + anchorX; + const centerY = y + anchorY; + transform = composeTransform(tmpTransform, centerX, centerY, 1, 1, rotation, -centerX, -centerY); + + createOrUpdateEmpty(tmpExtent); + extendCoordinate(tmpExtent, applyTransform(tmpTransform, p1)); + extendCoordinate(tmpExtent, applyTransform(tmpTransform, p2)); + extendCoordinate(tmpExtent, applyTransform(tmpTransform, p3)); + extendCoordinate(tmpExtent, applyTransform(tmpTransform, p4)); + } else { + createOrUpdate(boxX, boxY, boxX + boxW, boxY + boxH, tmpExtent); + } + const canvas = context.canvas; + const strokePadding = strokeInstruction ? (strokeInstruction[2] * scale / 2) : 0; + const intersects = + tmpExtent[0] - strokePadding <= canvas.width && tmpExtent[2] + strokePadding >= 0 && + tmpExtent[1] - strokePadding <= canvas.height && tmpExtent[3] + strokePadding >= 0; + + if (snapToPixel) { + x = Math.round(x); + y = Math.round(y); + } + + if (declutterGroup) { + if (!intersects && declutterGroup[4] == 1) { + return; + } + extend(declutterGroup, tmpExtent); + const declutterArgs = intersects ? + [context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] : + null; + if (declutterArgs && fillStroke) { + declutterArgs.push(fillInstruction, strokeInstruction, p1, p2, p3, p4); + } + declutterGroup.push(declutterArgs); + } else if (intersects) { + if (fillStroke) { + this.replayTextBackground_(context, p1, p2, p3, p4, + /** @type {Array.<*>} */ (fillInstruction), + /** @type {Array.<*>} */ (strokeInstruction)); + } + drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale); + } + } /** * @protected - * @type {number} + * @param {Array.} dashArray Dash array. + * @return {Array.} Dash array with pixel ratio applied */ - this.tolerance = tolerance; + applyPixelRatio(dashArray) { + const pixelRatio = this.pixelRatio; + return pixelRatio == 1 ? dashArray : dashArray.map(function(dash) { + return dash * pixelRatio; + }); + } + + /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @param {boolean} closed Last input coordinate equals first. + * @param {boolean} skipFirst Skip first coordinate. + * @protected + * @return {number} My end. + */ + appendFlatCoordinates(flatCoordinates, offset, end, stride, closed, skipFirst) { + + let myEnd = this.coordinates.length; + const extent = this.getBufferedMaxExtent(); + if (skipFirst) { + offset += stride; + } + const lastCoord = [flatCoordinates[offset], flatCoordinates[offset + 1]]; + const nextCoord = [NaN, NaN]; + let skipped = true; + + let i, lastRel, nextRel; + for (i = offset + stride; i < end; i += stride) { + nextCoord[0] = flatCoordinates[i]; + nextCoord[1] = flatCoordinates[i + 1]; + nextRel = coordinateRelationship(extent, nextCoord); + if (nextRel !== lastRel) { + if (skipped) { + this.coordinates[myEnd++] = lastCoord[0]; + this.coordinates[myEnd++] = lastCoord[1]; + } + this.coordinates[myEnd++] = nextCoord[0]; + this.coordinates[myEnd++] = nextCoord[1]; + skipped = false; + } else if (nextRel === Relationship.INTERSECTING) { + this.coordinates[myEnd++] = nextCoord[0]; + this.coordinates[myEnd++] = nextCoord[1]; + skipped = false; + } else { + skipped = true; + } + lastCoord[0] = nextCoord[0]; + lastCoord[1] = nextCoord[1]; + lastRel = nextRel; + } + + // Last coordinate equals first or only one point to append: + if ((closed && skipped) || i === offset + stride) { + this.coordinates[myEnd++] = lastCoord[0]; + this.coordinates[myEnd++] = lastCoord[1]; + } + return myEnd; + } + + /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {Array.} ends Ends. + * @param {number} stride Stride. + * @param {Array.} replayEnds Replay ends. + * @return {number} Offset. + */ + drawCustomCoordinates_(flatCoordinates, offset, ends, stride, replayEnds) { + for (let i = 0, ii = ends.length; i < ii; ++i) { + const end = ends[i]; + const replayEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false); + replayEnds.push(replayEnd); + offset = end; + } + return offset; + } + + /** + * @inheritDoc. + */ + drawCustom(geometry, feature, renderer) { + this.beginGeometry(geometry, feature); + const type = geometry.getType(); + const stride = geometry.getStride(); + const replayBegin = this.coordinates.length; + let flatCoordinates, replayEnd, replayEnds, replayEndss; + let offset; + if (type == GeometryType.MULTI_POLYGON) { + geometry = /** @type {module:ol/geom/MultiPolygon} */ (geometry); + flatCoordinates = geometry.getOrientedFlatCoordinates(); + replayEndss = []; + const endss = geometry.getEndss(); + offset = 0; + for (let i = 0, ii = endss.length; i < ii; ++i) { + const myEnds = []; + offset = this.drawCustomCoordinates_(flatCoordinates, offset, endss[i], stride, myEnds); + replayEndss.push(myEnds); + } + this.instructions.push([CanvasInstruction.CUSTOM, + replayBegin, replayEndss, geometry, renderer, inflateMultiCoordinatesArray]); + } else if (type == GeometryType.POLYGON || type == GeometryType.MULTI_LINE_STRING) { + replayEnds = []; + flatCoordinates = (type == GeometryType.POLYGON) ? + /** @type {module:ol/geom/Polygon} */ (geometry).getOrientedFlatCoordinates() : + geometry.getFlatCoordinates(); + offset = this.drawCustomCoordinates_(flatCoordinates, 0, + /** @type {module:ol/geom/Polygon|module:ol/geom/MultiLineString} */ (geometry).getEnds(), + stride, replayEnds); + this.instructions.push([CanvasInstruction.CUSTOM, + replayBegin, replayEnds, geometry, renderer, inflateCoordinatesArray]); + } else if (type == GeometryType.LINE_STRING || type == GeometryType.MULTI_POINT) { + flatCoordinates = geometry.getFlatCoordinates(); + replayEnd = this.appendFlatCoordinates( + flatCoordinates, 0, flatCoordinates.length, stride, false, false); + this.instructions.push([CanvasInstruction.CUSTOM, + replayBegin, replayEnd, geometry, renderer, inflateCoordinates]); + } else if (type == GeometryType.POINT) { + flatCoordinates = geometry.getFlatCoordinates(); + this.coordinates.push(flatCoordinates[0], flatCoordinates[1]); + replayEnd = this.coordinates.length; + this.instructions.push([CanvasInstruction.CUSTOM, + replayBegin, replayEnd, geometry, renderer]); + } + this.endGeometry(geometry, feature); + } /** * @protected - * @const - * @type {module:ol/extent~Extent} + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. */ - this.maxExtent = maxExtent; - - /** - * @protected - * @type {boolean} - */ - this.overlaps = overlaps; - - /** - * @protected - * @type {number} - */ - this.pixelRatio = pixelRatio; - - /** - * @protected - * @type {number} - */ - this.maxLineWidth = 0; - - /** - * @protected - * @const - * @type {number} - */ - this.resolution = resolution; + beginGeometry(geometry, feature) { + this.beginGeometryInstruction1_ = [CanvasInstruction.BEGIN_GEOMETRY, feature, 0]; + this.instructions.push(this.beginGeometryInstruction1_); + this.beginGeometryInstruction2_ = [CanvasInstruction.BEGIN_GEOMETRY, feature, 0]; + this.hitDetectionInstructions.push(this.beginGeometryInstruction2_); + } /** * @private - * @type {boolean} + * @param {CanvasRenderingContext2D} context Context. */ - this.alignFill_; + fill_(context) { + if (this.alignFill_) { + const origin = applyTransform(this.renderedTransform_, [0, 0]); + const repeatSize = 512 * this.pixelRatio; + context.translate(origin[0] % repeatSize, origin[1] % repeatSize); + context.rotate(this.viewRotation_); + } + context.fill(); + if (this.alignFill_) { + context.setTransform.apply(context, resetTransform); + } + } /** * @private - * @type {Array.<*>} + * @param {CanvasRenderingContext2D} context Context. + * @param {Array.<*>} instruction Instruction. */ - this.beginGeometryInstruction1_ = null; + setStrokeStyle_(context, instruction) { + context.strokeStyle = /** @type {module:ol/colorlike~ColorLike} */ (instruction[1]); + context.lineWidth = /** @type {number} */ (instruction[2]); + context.lineCap = /** @type {string} */ (instruction[3]); + context.lineJoin = /** @type {string} */ (instruction[4]); + context.miterLimit = /** @type {number} */ (instruction[5]); + if (CANVAS_LINE_DASH) { + context.lineDashOffset = /** @type {number} */ (instruction[7]); + context.setLineDash(/** @type {Array.} */ (instruction[6])); + } + } + + /** + * @param {module:ol/render/canvas~DeclutterGroup} declutterGroup Declutter group. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + renderDeclutter_(declutterGroup, feature) { + if (declutterGroup && declutterGroup.length > 5) { + const groupCount = declutterGroup[4]; + if (groupCount == 1 || groupCount == declutterGroup.length - 5) { + /** @type {module:ol/structs/RBush~Entry} */ + const box = { + minX: /** @type {number} */ (declutterGroup[0]), + minY: /** @type {number} */ (declutterGroup[1]), + maxX: /** @type {number} */ (declutterGroup[2]), + maxY: /** @type {number} */ (declutterGroup[3]), + value: feature + }; + if (!this.declutterTree.collides(box)) { + this.declutterTree.insert(box); + for (let j = 5, jj = declutterGroup.length; j < jj; ++j) { + const declutterData = /** @type {Array} */ (declutterGroup[j]); + if (declutterData) { + if (declutterData.length > 11) { + this.replayTextBackground_(declutterData[0], + declutterData[13], declutterData[14], declutterData[15], declutterData[16], + declutterData[11], declutterData[12]); + } + drawImage.apply(undefined, declutterData); + } + } + } + declutterGroup.length = 5; + createOrUpdateEmpty(declutterGroup); + } + } + } /** * @private - * @type {Array.<*>} + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/transform~Transform} transform Transform. + * @param {Object.} skippedFeaturesHash Ids of features + * to skip. + * @param {Array.<*>} instructions Instructions array. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} + * featureCallback Feature callback. + * @param {module:ol/extent~Extent=} opt_hitExtent Only check features that intersect this + * extent. + * @return {T|undefined} Callback result. + * @template T */ - this.beginGeometryInstruction2_ = null; + replay_( + context, + transform, + skippedFeaturesHash, + instructions, + featureCallback, + opt_hitExtent + ) { + /** @type {Array.} */ + let pixelCoordinates; + if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) { + pixelCoordinates = this.pixelCoordinates_; + } else { + if (!this.pixelCoordinates_) { + this.pixelCoordinates_ = []; + } + pixelCoordinates = transform2D( + this.coordinates, 0, this.coordinates.length, 2, + transform, this.pixelCoordinates_); + transformSetFromArray(this.renderedTransform_, transform); + } + const skipFeatures = !isEmpty(skippedFeaturesHash); + let i = 0; // instruction index + const ii = instructions.length; // end of instructions + let d = 0; // data index + let dd; // end of per-instruction data + let anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image; + let pendingFill = 0; + let pendingStroke = 0; + let lastFillInstruction = null; + let lastStrokeInstruction = null; + const coordinateCache = this.coordinateCache_; + const viewRotation = this.viewRotation_; + + const state = /** @type {module:ol/render~State} */ ({ + context: context, + pixelRatio: this.pixelRatio, + resolution: this.resolution, + rotation: viewRotation + }); + + // When the batch size gets too big, performance decreases. 200 is a good + // balance between batch size and number of fill/stroke instructions. + const batchSize = this.instructions != instructions || this.overlaps ? 0 : 200; + let /** @type {module:ol/Feature|module:ol/render/Feature} */ feature; + let x, y; + while (i < ii) { + const instruction = instructions[i]; + const type = /** @type {module:ol/render/canvas/Instruction} */ (instruction[0]); + switch (type) { + case CanvasInstruction.BEGIN_GEOMETRY: + feature = /** @type {module:ol/Feature|module:ol/render/Feature} */ (instruction[1]); + if ((skipFeatures && + skippedFeaturesHash[getUid(feature).toString()]) || + !feature.getGeometry()) { + i = /** @type {number} */ (instruction[2]); + } else if (opt_hitExtent !== undefined && !intersects( + opt_hitExtent, feature.getGeometry().getExtent())) { + i = /** @type {number} */ (instruction[2]) + 1; + } else { + ++i; + } + break; + case CanvasInstruction.BEGIN_PATH: + if (pendingFill > batchSize) { + this.fill_(context); + pendingFill = 0; + } + if (pendingStroke > batchSize) { + context.stroke(); + pendingStroke = 0; + } + if (!pendingFill && !pendingStroke) { + context.beginPath(); + prevX = prevY = NaN; + } + ++i; + break; + case CanvasInstruction.CIRCLE: + d = /** @type {number} */ (instruction[1]); + const x1 = pixelCoordinates[d]; + const y1 = pixelCoordinates[d + 1]; + const x2 = pixelCoordinates[d + 2]; + const y2 = pixelCoordinates[d + 3]; + const dx = x2 - x1; + const dy = y2 - y1; + const r = Math.sqrt(dx * dx + dy * dy); + context.moveTo(x1 + r, y1); + context.arc(x1, y1, r, 0, 2 * Math.PI, true); + ++i; + break; + case CanvasInstruction.CLOSE_PATH: + context.closePath(); + ++i; + break; + case CanvasInstruction.CUSTOM: + d = /** @type {number} */ (instruction[1]); + dd = instruction[2]; + const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (instruction[3]); + const renderer = instruction[4]; + const fn = instruction.length == 6 ? instruction[5] : undefined; + state.geometry = geometry; + state.feature = feature; + if (!(i in coordinateCache)) { + coordinateCache[i] = []; + } + const coords = coordinateCache[i]; + if (fn) { + fn(pixelCoordinates, d, dd, 2, coords); + } else { + coords[0] = pixelCoordinates[d]; + coords[1] = pixelCoordinates[d + 1]; + coords.length = 2; + } + renderer(coords, state); + ++i; + break; + case CanvasInstruction.DRAW_IMAGE: + d = /** @type {number} */ (instruction[1]); + dd = /** @type {number} */ (instruction[2]); + image = /** @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} */ + (instruction[3]); + // Remaining arguments in DRAW_IMAGE are in alphabetical order + anchorX = /** @type {number} */ (instruction[4]); + anchorY = /** @type {number} */ (instruction[5]); + declutterGroup = featureCallback ? null : /** @type {module:ol/render/canvas~DeclutterGroup} */ (instruction[6]); + const height = /** @type {number} */ (instruction[7]); + const opacity = /** @type {number} */ (instruction[8]); + const originX = /** @type {number} */ (instruction[9]); + const originY = /** @type {number} */ (instruction[10]); + const rotateWithView = /** @type {boolean} */ (instruction[11]); + let rotation = /** @type {number} */ (instruction[12]); + const scale = /** @type {number} */ (instruction[13]); + const snapToPixel = /** @type {boolean} */ (instruction[14]); + const width = /** @type {number} */ (instruction[15]); + + let padding, backgroundFill, backgroundStroke; + if (instruction.length > 16) { + padding = /** @type {Array.} */ (instruction[16]); + backgroundFill = /** @type {boolean} */ (instruction[17]); + backgroundStroke = /** @type {boolean} */ (instruction[18]); + } else { + padding = defaultPadding; + backgroundFill = backgroundStroke = false; + } + + if (rotateWithView) { + rotation += viewRotation; + } + for (; d < dd; d += 2) { + this.replayImage_(context, + pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, + declutterGroup, height, opacity, originX, originY, rotation, scale, + snapToPixel, width, padding, + backgroundFill ? /** @type {Array.<*>} */ (lastFillInstruction) : null, + backgroundStroke ? /** @type {Array.<*>} */ (lastStrokeInstruction) : null); + } + this.renderDeclutter_(declutterGroup, feature); + ++i; + break; + case CanvasInstruction.DRAW_CHARS: + const begin = /** @type {number} */ (instruction[1]); + const end = /** @type {number} */ (instruction[2]); + const baseline = /** @type {number} */ (instruction[3]); + declutterGroup = featureCallback ? null : /** @type {module:ol/render/canvas~DeclutterGroup} */ (instruction[4]); + const overflow = /** @type {number} */ (instruction[5]); + const fillKey = /** @type {string} */ (instruction[6]); + const maxAngle = /** @type {number} */ (instruction[7]); + const measure = /** @type {function(string):number} */ (instruction[8]); + const offsetY = /** @type {number} */ (instruction[9]); + const strokeKey = /** @type {string} */ (instruction[10]); + const strokeWidth = /** @type {number} */ (instruction[11]); + const text = /** @type {string} */ (instruction[12]); + const textKey = /** @type {string} */ (instruction[13]); + const textScale = /** @type {number} */ (instruction[14]); + + const pathLength = lineStringLength(pixelCoordinates, begin, end, 2); + const textLength = measure(text); + if (overflow || textLength <= pathLength) { + const textAlign = /** @type {module:ol~render} */ (this).textStates[textKey].textAlign; + const startM = (pathLength - textLength) * TEXT_ALIGN[textAlign]; + const parts = drawTextOnPath( + pixelCoordinates, begin, end, 2, text, measure, startM, maxAngle); + if (parts) { + let c, cc, chars, label, part; + if (strokeKey) { + for (c = 0, cc = parts.length; c < cc; ++c) { + part = parts[c]; // x, y, anchorX, rotation, chunk + chars = /** @type {string} */ (part[4]); + label = /** @type {module:ol~render} */ (this).getImage(chars, textKey, '', strokeKey); + anchorX = /** @type {number} */ (part[2]) + strokeWidth; + anchorY = baseline * label.height + (0.5 - baseline) * 2 * strokeWidth - offsetY; + this.replayImage_(context, + /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, + anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, + /** @type {number} */ (part[3]), textScale, false, label.width, + defaultPadding, null, null); + } + } + if (fillKey) { + for (c = 0, cc = parts.length; c < cc; ++c) { + part = parts[c]; // x, y, anchorX, rotation, chunk + chars = /** @type {string} */ (part[4]); + label = /** @type {module:ol~render} */ (this).getImage(chars, textKey, fillKey, ''); + anchorX = /** @type {number} */ (part[2]); + anchorY = baseline * label.height - offsetY; + this.replayImage_(context, + /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, + anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, + /** @type {number} */ (part[3]), textScale, false, label.width, + defaultPadding, null, null); + } + } + } + } + this.renderDeclutter_(declutterGroup, feature); + ++i; + break; + case CanvasInstruction.END_GEOMETRY: + if (featureCallback !== undefined) { + feature = /** @type {module:ol/Feature|module:ol/render/Feature} */ (instruction[1]); + const result = featureCallback(feature); + if (result) { + return result; + } + } + ++i; + break; + case CanvasInstruction.FILL: + if (batchSize) { + pendingFill++; + } else { + this.fill_(context); + } + ++i; + break; + case CanvasInstruction.MOVE_TO_LINE_TO: + d = /** @type {number} */ (instruction[1]); + dd = /** @type {number} */ (instruction[2]); + x = pixelCoordinates[d]; + y = pixelCoordinates[d + 1]; + roundX = (x + 0.5) | 0; + roundY = (y + 0.5) | 0; + if (roundX !== prevX || roundY !== prevY) { + context.moveTo(x, y); + prevX = roundX; + prevY = roundY; + } + for (d += 2; d < dd; d += 2) { + x = pixelCoordinates[d]; + y = pixelCoordinates[d + 1]; + roundX = (x + 0.5) | 0; + roundY = (y + 0.5) | 0; + if (d == dd - 2 || roundX !== prevX || roundY !== prevY) { + context.lineTo(x, y); + prevX = roundX; + prevY = roundY; + } + } + ++i; + break; + case CanvasInstruction.SET_FILL_STYLE: + lastFillInstruction = instruction; + this.alignFill_ = instruction[2]; + + if (pendingFill) { + this.fill_(context); + pendingFill = 0; + if (pendingStroke) { + context.stroke(); + pendingStroke = 0; + } + } + + context.fillStyle = /** @type {module:ol/colorlike~ColorLike} */ (instruction[1]); + ++i; + break; + case CanvasInstruction.SET_STROKE_STYLE: + lastStrokeInstruction = instruction; + if (pendingStroke) { + context.stroke(); + pendingStroke = 0; + } + this.setStrokeStyle_(context, /** @type {Array.<*>} */ (instruction)); + ++i; + break; + case CanvasInstruction.STROKE: + if (batchSize) { + pendingStroke++; + } else { + context.stroke(); + } + ++i; + break; + default: + ++i; // consume the instruction anyway, to avoid an infinite loop + break; + } + } + if (pendingFill) { + this.fill_(context); + } + if (pendingStroke) { + context.stroke(); + } + return undefined; + } /** - * @private - * @type {module:ol/extent~Extent} + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/transform~Transform} transform Transform. + * @param {number} viewRotation View rotation. + * @param {Object.} skippedFeaturesHash Ids of features + * to skip. */ - this.bufferedMaxExtent_ = null; + replay(context, transform, viewRotation, skippedFeaturesHash) { + this.viewRotation_ = viewRotation; + this.replay_(context, transform, + skippedFeaturesHash, this.instructions, undefined, undefined); + } /** + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/transform~Transform} transform Transform. + * @param {number} viewRotation View rotation. + * @param {Object.} skippedFeaturesHash Ids of features + * to skip. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T=} opt_featureCallback + * Feature callback. + * @param {module:ol/extent~Extent=} opt_hitExtent Only check features that intersect this + * extent. + * @return {T|undefined} Callback result. + * @template T + */ + replayHitDetection( + context, + transform, + viewRotation, + skippedFeaturesHash, + opt_featureCallback, + opt_hitExtent + ) { + this.viewRotation_ = viewRotation; + return this.replay_(context, transform, skippedFeaturesHash, + this.hitDetectionInstructions, opt_featureCallback, opt_hitExtent); + } + + /** + * Reverse the hit detection instructions. + */ + reverseHitDetectionInstructions() { + const hitDetectionInstructions = this.hitDetectionInstructions; + // step 1 - reverse array + hitDetectionInstructions.reverse(); + // step 2 - reverse instructions within geometry blocks + let i; + const n = hitDetectionInstructions.length; + let instruction; + let type; + let begin = -1; + for (i = 0; i < n; ++i) { + instruction = hitDetectionInstructions[i]; + type = /** @type {module:ol/render/canvas/Instruction} */ (instruction[0]); + if (type == CanvasInstruction.END_GEOMETRY) { + begin = i; + } else if (type == CanvasInstruction.BEGIN_GEOMETRY) { + instruction[2] = i; + reverseSubArray(this.hitDetectionInstructions, begin, i); + begin = -1; + } + } + } + + /** + * @inheritDoc + */ + setFillStrokeStyle(fillStyle, strokeStyle) { + const state = this.state; + if (fillStyle) { + const fillStyleColor = fillStyle.getColor(); + state.fillStyle = asColorLike(fillStyleColor ? + fillStyleColor : defaultFillStyle); + } else { + state.fillStyle = undefined; + } + if (strokeStyle) { + const strokeStyleColor = strokeStyle.getColor(); + state.strokeStyle = asColorLike(strokeStyleColor ? + strokeStyleColor : defaultStrokeStyle); + const strokeStyleLineCap = strokeStyle.getLineCap(); + state.lineCap = strokeStyleLineCap !== undefined ? + strokeStyleLineCap : defaultLineCap; + const strokeStyleLineDash = strokeStyle.getLineDash(); + state.lineDash = strokeStyleLineDash ? + strokeStyleLineDash.slice() : defaultLineDash; + const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); + state.lineDashOffset = strokeStyleLineDashOffset ? + strokeStyleLineDashOffset : defaultLineDashOffset; + const strokeStyleLineJoin = strokeStyle.getLineJoin(); + state.lineJoin = strokeStyleLineJoin !== undefined ? + strokeStyleLineJoin : defaultLineJoin; + const strokeStyleWidth = strokeStyle.getWidth(); + state.lineWidth = strokeStyleWidth !== undefined ? + strokeStyleWidth : defaultLineWidth; + const strokeStyleMiterLimit = strokeStyle.getMiterLimit(); + state.miterLimit = strokeStyleMiterLimit !== undefined ? + strokeStyleMiterLimit : defaultMiterLimit; + + if (state.lineWidth > this.maxLineWidth) { + this.maxLineWidth = state.lineWidth; + // invalidate the buffered max extent cache + this.bufferedMaxExtent_ = null; + } + } else { + state.strokeStyle = undefined; + state.lineCap = undefined; + state.lineDash = null; + state.lineDashOffset = undefined; + state.lineJoin = undefined; + state.lineWidth = undefined; + state.miterLimit = undefined; + } + } + + /** + * @param {module:ol/render/canvas~FillStrokeState} state State. + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. + * @return {Array.<*>} Fill instruction. + */ + createFill(state, geometry) { + const fillStyle = state.fillStyle; + const fillInstruction = [CanvasInstruction.SET_FILL_STYLE, fillStyle]; + if (typeof fillStyle !== 'string') { + // Fill is a pattern or gradient - align it! + fillInstruction.push(true); + } + return fillInstruction; + } + + /** + * @param {module:ol/render/canvas~FillStrokeState} state State. + */ + applyStroke(state) { + this.instructions.push(this.createStroke(state)); + } + + /** + * @param {module:ol/render/canvas~FillStrokeState} state State. + * @return {Array.<*>} Stroke instruction. + */ + createStroke(state) { + return [ + CanvasInstruction.SET_STROKE_STYLE, + state.strokeStyle, state.lineWidth * this.pixelRatio, state.lineCap, + state.lineJoin, state.miterLimit, + this.applyPixelRatio(state.lineDash), state.lineDashOffset * this.pixelRatio + ]; + } + + /** + * @param {module:ol/render/canvas~FillStrokeState} state State. + * @param {function(this:module:ol/render/canvas/Replay, module:ol/render/canvas~FillStrokeState, (module:ol/geom/Geometry|module:ol/render/Feature)):Array.<*>} createFill Create fill. + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. + */ + updateFillStyle(state, createFill, geometry) { + const fillStyle = state.fillStyle; + if (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle) { + if (fillStyle !== undefined) { + this.instructions.push(createFill.call(this, state, geometry)); + } + state.currentFillStyle = fillStyle; + } + } + + /** + * @param {module:ol/render/canvas~FillStrokeState} state State. + * @param {function(this:module:ol/render/canvas/Replay, module:ol/render/canvas~FillStrokeState)} applyStroke Apply stroke. + */ + updateStrokeStyle(state, applyStroke) { + const strokeStyle = state.strokeStyle; + const lineCap = state.lineCap; + const lineDash = state.lineDash; + const lineDashOffset = state.lineDashOffset; + const lineJoin = state.lineJoin; + const lineWidth = state.lineWidth; + const miterLimit = state.miterLimit; + if (state.currentStrokeStyle != strokeStyle || + state.currentLineCap != lineCap || + (lineDash != state.currentLineDash && !equals(state.currentLineDash, lineDash)) || + state.currentLineDashOffset != lineDashOffset || + state.currentLineJoin != lineJoin || + state.currentLineWidth != lineWidth || + state.currentMiterLimit != miterLimit) { + if (strokeStyle !== undefined) { + applyStroke.call(this, state); + } + state.currentStrokeStyle = strokeStyle; + state.currentLineCap = lineCap; + state.currentLineDash = lineDash; + state.currentLineDashOffset = lineDashOffset; + state.currentLineJoin = lineJoin; + state.currentLineWidth = lineWidth; + state.currentMiterLimit = miterLimit; + } + } + + /** + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + */ + endGeometry(geometry, feature) { + this.beginGeometryInstruction1_[2] = this.instructions.length; + this.beginGeometryInstruction1_ = null; + this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length; + this.beginGeometryInstruction2_ = null; + const endGeometryInstruction = [CanvasInstruction.END_GEOMETRY, feature]; + this.instructions.push(endGeometryInstruction); + this.hitDetectionInstructions.push(endGeometryInstruction); + } + + /** + * Get the buffered rendering extent. Rendering will be clipped to the extent + * provided to the constructor. To account for symbolizers that may intersect + * this extent, we calculate a buffered extent (e.g. based on stroke width). + * @return {module:ol/extent~Extent} The buffered rendering extent. * @protected - * @type {Array.<*>} */ - this.instructions = []; - - /** - * @protected - * @type {Array.} - */ - this.coordinates = []; - - /** - * @private - * @type {!Object.|Array.>>} - */ - this.coordinateCache_ = {}; - - /** - * @private - * @type {!module:ol/transform~Transform} - */ - this.renderedTransform_ = createTransform(); - - /** - * @protected - * @type {Array.<*>} - */ - this.hitDetectionInstructions = []; - - /** - * @private - * @type {Array.} - */ - this.pixelCoordinates_ = null; - - /** - * @protected - * @type {module:ol/render/canvas~FillStrokeState} - */ - this.state = /** @type {module:ol/render/canvas~FillStrokeState} */ ({}); - - /** - * @private - * @type {number} - */ - this.viewRotation_ = 0; - -}; + getBufferedMaxExtent() { + if (!this.bufferedMaxExtent_) { + this.bufferedMaxExtent_ = clone(this.maxExtent); + if (this.maxLineWidth > 0) { + const width = this.resolution * (this.maxLineWidth + 1) / 2; + buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_); + } + } + return this.bufferedMaxExtent_; + } +} inherits(CanvasReplay, VectorContext); @@ -173,891 +1083,6 @@ const tmpExtent = createEmpty(); */ const tmpTransform = createTransform(); -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/coordinate~Coordinate} p1 1st point of the background box. - * @param {module:ol/coordinate~Coordinate} p2 2nd point of the background box. - * @param {module:ol/coordinate~Coordinate} p3 3rd point of the background box. - * @param {module:ol/coordinate~Coordinate} p4 4th point of the background box. - * @param {Array.<*>} fillInstruction Fill instruction. - * @param {Array.<*>} strokeInstruction Stroke instruction. - */ -CanvasReplay.prototype.replayTextBackground_ = function(context, p1, p2, p3, p4, - fillInstruction, strokeInstruction) { - context.beginPath(); - context.moveTo.apply(context, p1); - context.lineTo.apply(context, p2); - context.lineTo.apply(context, p3); - context.lineTo.apply(context, p4); - context.lineTo.apply(context, p1); - if (fillInstruction) { - this.alignFill_ = /** @type {boolean} */ (fillInstruction[2]); - this.fill_(context); - } - if (strokeInstruction) { - this.setStrokeStyle_(context, /** @type {Array.<*>} */ (strokeInstruction)); - context.stroke(); - } -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {number} x X. - * @param {number} y Y. - * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image. - * @param {number} anchorX Anchor X. - * @param {number} anchorY Anchor Y. - * @param {module:ol/render/canvas~DeclutterGroup} declutterGroup Declutter group. - * @param {number} height Height. - * @param {number} opacity Opacity. - * @param {number} originX Origin X. - * @param {number} originY Origin Y. - * @param {number} rotation Rotation. - * @param {number} scale Scale. - * @param {boolean} snapToPixel Snap to pixel. - * @param {number} width Width. - * @param {Array.} padding Padding. - * @param {Array.<*>} fillInstruction Fill instruction. - * @param {Array.<*>} strokeInstruction Stroke instruction. - */ -CanvasReplay.prototype.replayImage_ = function(context, x, y, image, - anchorX, anchorY, declutterGroup, height, opacity, originX, originY, - rotation, scale, snapToPixel, width, padding, fillInstruction, strokeInstruction) { - const fillStroke = fillInstruction || strokeInstruction; - anchorX *= scale; - anchorY *= scale; - x -= anchorX; - y -= anchorY; - - const w = (width + originX > image.width) ? image.width - originX : width; - const h = (height + originY > image.height) ? image.height - originY : height; - const boxW = padding[3] + w * scale + padding[1]; - const boxH = padding[0] + h * scale + padding[2]; - const boxX = x - padding[3]; - const boxY = y - padding[0]; - - /** @type {module:ol/coordinate~Coordinate} */ - let p1; - /** @type {module:ol/coordinate~Coordinate} */ - let p2; - /** @type {module:ol/coordinate~Coordinate} */ - let p3; - /** @type {module:ol/coordinate~Coordinate} */ - let p4; - if (fillStroke || rotation !== 0) { - p1 = [boxX, boxY]; - p2 = [boxX + boxW, boxY]; - p3 = [boxX + boxW, boxY + boxH]; - p4 = [boxX, boxY + boxH]; - } - - let transform = null; - if (rotation !== 0) { - const centerX = x + anchorX; - const centerY = y + anchorY; - transform = composeTransform(tmpTransform, centerX, centerY, 1, 1, rotation, -centerX, -centerY); - - createOrUpdateEmpty(tmpExtent); - extendCoordinate(tmpExtent, applyTransform(tmpTransform, p1)); - extendCoordinate(tmpExtent, applyTransform(tmpTransform, p2)); - extendCoordinate(tmpExtent, applyTransform(tmpTransform, p3)); - extendCoordinate(tmpExtent, applyTransform(tmpTransform, p4)); - } else { - createOrUpdate(boxX, boxY, boxX + boxW, boxY + boxH, tmpExtent); - } - const canvas = context.canvas; - const strokePadding = strokeInstruction ? (strokeInstruction[2] * scale / 2) : 0; - const intersects = - tmpExtent[0] - strokePadding <= canvas.width && tmpExtent[2] + strokePadding >= 0 && - tmpExtent[1] - strokePadding <= canvas.height && tmpExtent[3] + strokePadding >= 0; - - if (snapToPixel) { - x = Math.round(x); - y = Math.round(y); - } - - if (declutterGroup) { - if (!intersects && declutterGroup[4] == 1) { - return; - } - extend(declutterGroup, tmpExtent); - const declutterArgs = intersects ? - [context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] : - null; - if (declutterArgs && fillStroke) { - declutterArgs.push(fillInstruction, strokeInstruction, p1, p2, p3, p4); - } - declutterGroup.push(declutterArgs); - } else if (intersects) { - if (fillStroke) { - this.replayTextBackground_(context, p1, p2, p3, p4, - /** @type {Array.<*>} */ (fillInstruction), - /** @type {Array.<*>} */ (strokeInstruction)); - } - drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale); - } -}; - - -/** - * @protected - * @param {Array.} dashArray Dash array. - * @return {Array.} Dash array with pixel ratio applied - */ -CanvasReplay.prototype.applyPixelRatio = function(dashArray) { - const pixelRatio = this.pixelRatio; - return pixelRatio == 1 ? dashArray : dashArray.map(function(dash) { - return dash * pixelRatio; - }); -}; - - -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @param {boolean} closed Last input coordinate equals first. - * @param {boolean} skipFirst Skip first coordinate. - * @protected - * @return {number} My end. - */ -CanvasReplay.prototype.appendFlatCoordinates = function(flatCoordinates, offset, end, stride, closed, skipFirst) { - - let myEnd = this.coordinates.length; - const extent = this.getBufferedMaxExtent(); - if (skipFirst) { - offset += stride; - } - const lastCoord = [flatCoordinates[offset], flatCoordinates[offset + 1]]; - const nextCoord = [NaN, NaN]; - let skipped = true; - - let i, lastRel, nextRel; - for (i = offset + stride; i < end; i += stride) { - nextCoord[0] = flatCoordinates[i]; - nextCoord[1] = flatCoordinates[i + 1]; - nextRel = coordinateRelationship(extent, nextCoord); - if (nextRel !== lastRel) { - if (skipped) { - this.coordinates[myEnd++] = lastCoord[0]; - this.coordinates[myEnd++] = lastCoord[1]; - } - this.coordinates[myEnd++] = nextCoord[0]; - this.coordinates[myEnd++] = nextCoord[1]; - skipped = false; - } else if (nextRel === Relationship.INTERSECTING) { - this.coordinates[myEnd++] = nextCoord[0]; - this.coordinates[myEnd++] = nextCoord[1]; - skipped = false; - } else { - skipped = true; - } - lastCoord[0] = nextCoord[0]; - lastCoord[1] = nextCoord[1]; - lastRel = nextRel; - } - - // Last coordinate equals first or only one point to append: - if ((closed && skipped) || i === offset + stride) { - this.coordinates[myEnd++] = lastCoord[0]; - this.coordinates[myEnd++] = lastCoord[1]; - } - return myEnd; -}; - - -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {Array.} ends Ends. - * @param {number} stride Stride. - * @param {Array.} replayEnds Replay ends. - * @return {number} Offset. - */ -CanvasReplay.prototype.drawCustomCoordinates_ = function(flatCoordinates, offset, ends, stride, replayEnds) { - for (let i = 0, ii = ends.length; i < ii; ++i) { - const end = ends[i]; - const replayEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false); - replayEnds.push(replayEnd); - offset = end; - } - return offset; -}; - - -/** - * @inheritDoc. - */ -CanvasReplay.prototype.drawCustom = function(geometry, feature, renderer) { - this.beginGeometry(geometry, feature); - const type = geometry.getType(); - const stride = geometry.getStride(); - const replayBegin = this.coordinates.length; - let flatCoordinates, replayEnd, replayEnds, replayEndss; - let offset; - if (type == GeometryType.MULTI_POLYGON) { - geometry = /** @type {module:ol/geom/MultiPolygon} */ (geometry); - flatCoordinates = geometry.getOrientedFlatCoordinates(); - replayEndss = []; - const endss = geometry.getEndss(); - offset = 0; - for (let i = 0, ii = endss.length; i < ii; ++i) { - const myEnds = []; - offset = this.drawCustomCoordinates_(flatCoordinates, offset, endss[i], stride, myEnds); - replayEndss.push(myEnds); - } - this.instructions.push([CanvasInstruction.CUSTOM, - replayBegin, replayEndss, geometry, renderer, inflateMultiCoordinatesArray]); - } else if (type == GeometryType.POLYGON || type == GeometryType.MULTI_LINE_STRING) { - replayEnds = []; - flatCoordinates = (type == GeometryType.POLYGON) ? - /** @type {module:ol/geom/Polygon} */ (geometry).getOrientedFlatCoordinates() : - geometry.getFlatCoordinates(); - offset = this.drawCustomCoordinates_(flatCoordinates, 0, - /** @type {module:ol/geom/Polygon|module:ol/geom/MultiLineString} */ (geometry).getEnds(), - stride, replayEnds); - this.instructions.push([CanvasInstruction.CUSTOM, - replayBegin, replayEnds, geometry, renderer, inflateCoordinatesArray]); - } else if (type == GeometryType.LINE_STRING || type == GeometryType.MULTI_POINT) { - flatCoordinates = geometry.getFlatCoordinates(); - replayEnd = this.appendFlatCoordinates( - flatCoordinates, 0, flatCoordinates.length, stride, false, false); - this.instructions.push([CanvasInstruction.CUSTOM, - replayBegin, replayEnd, geometry, renderer, inflateCoordinates]); - } else if (type == GeometryType.POINT) { - flatCoordinates = geometry.getFlatCoordinates(); - this.coordinates.push(flatCoordinates[0], flatCoordinates[1]); - replayEnd = this.coordinates.length; - this.instructions.push([CanvasInstruction.CUSTOM, - replayBegin, replayEnd, geometry, renderer]); - } - this.endGeometry(geometry, feature); -}; - - -/** - * @protected - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -CanvasReplay.prototype.beginGeometry = function(geometry, feature) { - this.beginGeometryInstruction1_ = [CanvasInstruction.BEGIN_GEOMETRY, feature, 0]; - this.instructions.push(this.beginGeometryInstruction1_); - this.beginGeometryInstruction2_ = [CanvasInstruction.BEGIN_GEOMETRY, feature, 0]; - this.hitDetectionInstructions.push(this.beginGeometryInstruction2_); -}; - - -/** - * @private - * @param {CanvasRenderingContext2D} context Context. - */ -CanvasReplay.prototype.fill_ = function(context) { - if (this.alignFill_) { - const origin = applyTransform(this.renderedTransform_, [0, 0]); - const repeatSize = 512 * this.pixelRatio; - context.translate(origin[0] % repeatSize, origin[1] % repeatSize); - context.rotate(this.viewRotation_); - } - context.fill(); - if (this.alignFill_) { - context.setTransform.apply(context, resetTransform); - } -}; - - -/** - * @private - * @param {CanvasRenderingContext2D} context Context. - * @param {Array.<*>} instruction Instruction. - */ -CanvasReplay.prototype.setStrokeStyle_ = function(context, instruction) { - context.strokeStyle = /** @type {module:ol/colorlike~ColorLike} */ (instruction[1]); - context.lineWidth = /** @type {number} */ (instruction[2]); - context.lineCap = /** @type {string} */ (instruction[3]); - context.lineJoin = /** @type {string} */ (instruction[4]); - context.miterLimit = /** @type {number} */ (instruction[5]); - if (CANVAS_LINE_DASH) { - context.lineDashOffset = /** @type {number} */ (instruction[7]); - context.setLineDash(/** @type {Array.} */ (instruction[6])); - } -}; - - -/** - * @param {module:ol/render/canvas~DeclutterGroup} declutterGroup Declutter group. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -CanvasReplay.prototype.renderDeclutter_ = function(declutterGroup, feature) { - if (declutterGroup && declutterGroup.length > 5) { - const groupCount = declutterGroup[4]; - if (groupCount == 1 || groupCount == declutterGroup.length - 5) { - /** @type {module:ol/structs/RBush~Entry} */ - const box = { - minX: /** @type {number} */ (declutterGroup[0]), - minY: /** @type {number} */ (declutterGroup[1]), - maxX: /** @type {number} */ (declutterGroup[2]), - maxY: /** @type {number} */ (declutterGroup[3]), - value: feature - }; - if (!this.declutterTree.collides(box)) { - this.declutterTree.insert(box); - for (let j = 5, jj = declutterGroup.length; j < jj; ++j) { - const declutterData = /** @type {Array} */ (declutterGroup[j]); - if (declutterData) { - if (declutterData.length > 11) { - this.replayTextBackground_(declutterData[0], - declutterData[13], declutterData[14], declutterData[15], declutterData[16], - declutterData[11], declutterData[12]); - } - drawImage.apply(undefined, declutterData); - } - } - } - declutterGroup.length = 5; - createOrUpdateEmpty(declutterGroup); - } - } -}; - - -/** - * @private - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/transform~Transform} transform Transform. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - * @param {Array.<*>} instructions Instructions array. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} - * featureCallback Feature callback. - * @param {module:ol/extent~Extent=} opt_hitExtent Only check features that intersect this - * extent. - * @return {T|undefined} Callback result. - * @template T - */ -CanvasReplay.prototype.replay_ = function( - context, transform, skippedFeaturesHash, - instructions, featureCallback, opt_hitExtent) { - /** @type {Array.} */ - let pixelCoordinates; - if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) { - pixelCoordinates = this.pixelCoordinates_; - } else { - if (!this.pixelCoordinates_) { - this.pixelCoordinates_ = []; - } - pixelCoordinates = transform2D( - this.coordinates, 0, this.coordinates.length, 2, - transform, this.pixelCoordinates_); - transformSetFromArray(this.renderedTransform_, transform); - } - const skipFeatures = !isEmpty(skippedFeaturesHash); - let i = 0; // instruction index - const ii = instructions.length; // end of instructions - let d = 0; // data index - let dd; // end of per-instruction data - let anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image; - let pendingFill = 0; - let pendingStroke = 0; - let lastFillInstruction = null; - let lastStrokeInstruction = null; - const coordinateCache = this.coordinateCache_; - const viewRotation = this.viewRotation_; - - const state = /** @type {module:ol/render~State} */ ({ - context: context, - pixelRatio: this.pixelRatio, - resolution: this.resolution, - rotation: viewRotation - }); - - // When the batch size gets too big, performance decreases. 200 is a good - // balance between batch size and number of fill/stroke instructions. - const batchSize = this.instructions != instructions || this.overlaps ? 0 : 200; - let /** @type {module:ol/Feature|module:ol/render/Feature} */ feature; - let x, y; - while (i < ii) { - const instruction = instructions[i]; - const type = /** @type {module:ol/render/canvas/Instruction} */ (instruction[0]); - switch (type) { - case CanvasInstruction.BEGIN_GEOMETRY: - feature = /** @type {module:ol/Feature|module:ol/render/Feature} */ (instruction[1]); - if ((skipFeatures && - skippedFeaturesHash[getUid(feature).toString()]) || - !feature.getGeometry()) { - i = /** @type {number} */ (instruction[2]); - } else if (opt_hitExtent !== undefined && !intersects( - opt_hitExtent, feature.getGeometry().getExtent())) { - i = /** @type {number} */ (instruction[2]) + 1; - } else { - ++i; - } - break; - case CanvasInstruction.BEGIN_PATH: - if (pendingFill > batchSize) { - this.fill_(context); - pendingFill = 0; - } - if (pendingStroke > batchSize) { - context.stroke(); - pendingStroke = 0; - } - if (!pendingFill && !pendingStroke) { - context.beginPath(); - prevX = prevY = NaN; - } - ++i; - break; - case CanvasInstruction.CIRCLE: - d = /** @type {number} */ (instruction[1]); - const x1 = pixelCoordinates[d]; - const y1 = pixelCoordinates[d + 1]; - const x2 = pixelCoordinates[d + 2]; - const y2 = pixelCoordinates[d + 3]; - const dx = x2 - x1; - const dy = y2 - y1; - const r = Math.sqrt(dx * dx + dy * dy); - context.moveTo(x1 + r, y1); - context.arc(x1, y1, r, 0, 2 * Math.PI, true); - ++i; - break; - case CanvasInstruction.CLOSE_PATH: - context.closePath(); - ++i; - break; - case CanvasInstruction.CUSTOM: - d = /** @type {number} */ (instruction[1]); - dd = instruction[2]; - const geometry = /** @type {module:ol/geom/SimpleGeometry} */ (instruction[3]); - const renderer = instruction[4]; - const fn = instruction.length == 6 ? instruction[5] : undefined; - state.geometry = geometry; - state.feature = feature; - if (!(i in coordinateCache)) { - coordinateCache[i] = []; - } - const coords = coordinateCache[i]; - if (fn) { - fn(pixelCoordinates, d, dd, 2, coords); - } else { - coords[0] = pixelCoordinates[d]; - coords[1] = pixelCoordinates[d + 1]; - coords.length = 2; - } - renderer(coords, state); - ++i; - break; - case CanvasInstruction.DRAW_IMAGE: - d = /** @type {number} */ (instruction[1]); - dd = /** @type {number} */ (instruction[2]); - image = /** @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} */ - (instruction[3]); - // Remaining arguments in DRAW_IMAGE are in alphabetical order - anchorX = /** @type {number} */ (instruction[4]); - anchorY = /** @type {number} */ (instruction[5]); - declutterGroup = featureCallback ? null : /** @type {module:ol/render/canvas~DeclutterGroup} */ (instruction[6]); - const height = /** @type {number} */ (instruction[7]); - const opacity = /** @type {number} */ (instruction[8]); - const originX = /** @type {number} */ (instruction[9]); - const originY = /** @type {number} */ (instruction[10]); - const rotateWithView = /** @type {boolean} */ (instruction[11]); - let rotation = /** @type {number} */ (instruction[12]); - const scale = /** @type {number} */ (instruction[13]); - const snapToPixel = /** @type {boolean} */ (instruction[14]); - const width = /** @type {number} */ (instruction[15]); - - let padding, backgroundFill, backgroundStroke; - if (instruction.length > 16) { - padding = /** @type {Array.} */ (instruction[16]); - backgroundFill = /** @type {boolean} */ (instruction[17]); - backgroundStroke = /** @type {boolean} */ (instruction[18]); - } else { - padding = defaultPadding; - backgroundFill = backgroundStroke = false; - } - - if (rotateWithView) { - rotation += viewRotation; - } - for (; d < dd; d += 2) { - this.replayImage_(context, - pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, - declutterGroup, height, opacity, originX, originY, rotation, scale, - snapToPixel, width, padding, - backgroundFill ? /** @type {Array.<*>} */ (lastFillInstruction) : null, - backgroundStroke ? /** @type {Array.<*>} */ (lastStrokeInstruction) : null); - } - this.renderDeclutter_(declutterGroup, feature); - ++i; - break; - case CanvasInstruction.DRAW_CHARS: - const begin = /** @type {number} */ (instruction[1]); - const end = /** @type {number} */ (instruction[2]); - const baseline = /** @type {number} */ (instruction[3]); - declutterGroup = featureCallback ? null : /** @type {module:ol/render/canvas~DeclutterGroup} */ (instruction[4]); - const overflow = /** @type {number} */ (instruction[5]); - const fillKey = /** @type {string} */ (instruction[6]); - const maxAngle = /** @type {number} */ (instruction[7]); - const measure = /** @type {function(string):number} */ (instruction[8]); - const offsetY = /** @type {number} */ (instruction[9]); - const strokeKey = /** @type {string} */ (instruction[10]); - const strokeWidth = /** @type {number} */ (instruction[11]); - const text = /** @type {string} */ (instruction[12]); - const textKey = /** @type {string} */ (instruction[13]); - const textScale = /** @type {number} */ (instruction[14]); - - const pathLength = lineStringLength(pixelCoordinates, begin, end, 2); - const textLength = measure(text); - if (overflow || textLength <= pathLength) { - const textAlign = /** @type {module:ol~render} */ (this).textStates[textKey].textAlign; - const startM = (pathLength - textLength) * TEXT_ALIGN[textAlign]; - const parts = drawTextOnPath( - pixelCoordinates, begin, end, 2, text, measure, startM, maxAngle); - if (parts) { - let c, cc, chars, label, part; - if (strokeKey) { - for (c = 0, cc = parts.length; c < cc; ++c) { - part = parts[c]; // x, y, anchorX, rotation, chunk - chars = /** @type {string} */ (part[4]); - label = /** @type {module:ol~render} */ (this).getImage(chars, textKey, '', strokeKey); - anchorX = /** @type {number} */ (part[2]) + strokeWidth; - anchorY = baseline * label.height + (0.5 - baseline) * 2 * strokeWidth - offsetY; - this.replayImage_(context, - /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, - anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, - /** @type {number} */ (part[3]), textScale, false, label.width, - defaultPadding, null, null); - } - } - if (fillKey) { - for (c = 0, cc = parts.length; c < cc; ++c) { - part = parts[c]; // x, y, anchorX, rotation, chunk - chars = /** @type {string} */ (part[4]); - label = /** @type {module:ol~render} */ (this).getImage(chars, textKey, fillKey, ''); - anchorX = /** @type {number} */ (part[2]); - anchorY = baseline * label.height - offsetY; - this.replayImage_(context, - /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, - anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, - /** @type {number} */ (part[3]), textScale, false, label.width, - defaultPadding, null, null); - } - } - } - } - this.renderDeclutter_(declutterGroup, feature); - ++i; - break; - case CanvasInstruction.END_GEOMETRY: - if (featureCallback !== undefined) { - feature = /** @type {module:ol/Feature|module:ol/render/Feature} */ (instruction[1]); - const result = featureCallback(feature); - if (result) { - return result; - } - } - ++i; - break; - case CanvasInstruction.FILL: - if (batchSize) { - pendingFill++; - } else { - this.fill_(context); - } - ++i; - break; - case CanvasInstruction.MOVE_TO_LINE_TO: - d = /** @type {number} */ (instruction[1]); - dd = /** @type {number} */ (instruction[2]); - x = pixelCoordinates[d]; - y = pixelCoordinates[d + 1]; - roundX = (x + 0.5) | 0; - roundY = (y + 0.5) | 0; - if (roundX !== prevX || roundY !== prevY) { - context.moveTo(x, y); - prevX = roundX; - prevY = roundY; - } - for (d += 2; d < dd; d += 2) { - x = pixelCoordinates[d]; - y = pixelCoordinates[d + 1]; - roundX = (x + 0.5) | 0; - roundY = (y + 0.5) | 0; - if (d == dd - 2 || roundX !== prevX || roundY !== prevY) { - context.lineTo(x, y); - prevX = roundX; - prevY = roundY; - } - } - ++i; - break; - case CanvasInstruction.SET_FILL_STYLE: - lastFillInstruction = instruction; - this.alignFill_ = instruction[2]; - - if (pendingFill) { - this.fill_(context); - pendingFill = 0; - if (pendingStroke) { - context.stroke(); - pendingStroke = 0; - } - } - - context.fillStyle = /** @type {module:ol/colorlike~ColorLike} */ (instruction[1]); - ++i; - break; - case CanvasInstruction.SET_STROKE_STYLE: - lastStrokeInstruction = instruction; - if (pendingStroke) { - context.stroke(); - pendingStroke = 0; - } - this.setStrokeStyle_(context, /** @type {Array.<*>} */ (instruction)); - ++i; - break; - case CanvasInstruction.STROKE: - if (batchSize) { - pendingStroke++; - } else { - context.stroke(); - } - ++i; - break; - default: - ++i; // consume the instruction anyway, to avoid an infinite loop - break; - } - } - if (pendingFill) { - this.fill_(context); - } - if (pendingStroke) { - context.stroke(); - } - return undefined; -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/transform~Transform} transform Transform. - * @param {number} viewRotation View rotation. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - */ -CanvasReplay.prototype.replay = function( - context, transform, viewRotation, skippedFeaturesHash) { - this.viewRotation_ = viewRotation; - this.replay_(context, transform, - skippedFeaturesHash, this.instructions, undefined, undefined); -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/transform~Transform} transform Transform. - * @param {number} viewRotation View rotation. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T=} opt_featureCallback - * Feature callback. - * @param {module:ol/extent~Extent=} opt_hitExtent Only check features that intersect this - * extent. - * @return {T|undefined} Callback result. - * @template T - */ -CanvasReplay.prototype.replayHitDetection = function( - context, transform, viewRotation, skippedFeaturesHash, - opt_featureCallback, opt_hitExtent) { - this.viewRotation_ = viewRotation; - return this.replay_(context, transform, skippedFeaturesHash, - this.hitDetectionInstructions, opt_featureCallback, opt_hitExtent); -}; - - -/** - * Reverse the hit detection instructions. - */ -CanvasReplay.prototype.reverseHitDetectionInstructions = function() { - const hitDetectionInstructions = this.hitDetectionInstructions; - // step 1 - reverse array - hitDetectionInstructions.reverse(); - // step 2 - reverse instructions within geometry blocks - let i; - const n = hitDetectionInstructions.length; - let instruction; - let type; - let begin = -1; - for (i = 0; i < n; ++i) { - instruction = hitDetectionInstructions[i]; - type = /** @type {module:ol/render/canvas/Instruction} */ (instruction[0]); - if (type == CanvasInstruction.END_GEOMETRY) { - begin = i; - } else if (type == CanvasInstruction.BEGIN_GEOMETRY) { - instruction[2] = i; - reverseSubArray(this.hitDetectionInstructions, begin, i); - begin = -1; - } - } -}; - - -/** - * @inheritDoc - */ -CanvasReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { - const state = this.state; - if (fillStyle) { - const fillStyleColor = fillStyle.getColor(); - state.fillStyle = asColorLike(fillStyleColor ? - fillStyleColor : defaultFillStyle); - } else { - state.fillStyle = undefined; - } - if (strokeStyle) { - const strokeStyleColor = strokeStyle.getColor(); - state.strokeStyle = asColorLike(strokeStyleColor ? - strokeStyleColor : defaultStrokeStyle); - const strokeStyleLineCap = strokeStyle.getLineCap(); - state.lineCap = strokeStyleLineCap !== undefined ? - strokeStyleLineCap : defaultLineCap; - const strokeStyleLineDash = strokeStyle.getLineDash(); - state.lineDash = strokeStyleLineDash ? - strokeStyleLineDash.slice() : defaultLineDash; - const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); - state.lineDashOffset = strokeStyleLineDashOffset ? - strokeStyleLineDashOffset : defaultLineDashOffset; - const strokeStyleLineJoin = strokeStyle.getLineJoin(); - state.lineJoin = strokeStyleLineJoin !== undefined ? - strokeStyleLineJoin : defaultLineJoin; - const strokeStyleWidth = strokeStyle.getWidth(); - state.lineWidth = strokeStyleWidth !== undefined ? - strokeStyleWidth : defaultLineWidth; - const strokeStyleMiterLimit = strokeStyle.getMiterLimit(); - state.miterLimit = strokeStyleMiterLimit !== undefined ? - strokeStyleMiterLimit : defaultMiterLimit; - - if (state.lineWidth > this.maxLineWidth) { - this.maxLineWidth = state.lineWidth; - // invalidate the buffered max extent cache - this.bufferedMaxExtent_ = null; - } - } else { - state.strokeStyle = undefined; - state.lineCap = undefined; - state.lineDash = null; - state.lineDashOffset = undefined; - state.lineJoin = undefined; - state.lineWidth = undefined; - state.miterLimit = undefined; - } -}; - - -/** - * @param {module:ol/render/canvas~FillStrokeState} state State. - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. - * @return {Array.<*>} Fill instruction. - */ -CanvasReplay.prototype.createFill = function(state, geometry) { - const fillStyle = state.fillStyle; - const fillInstruction = [CanvasInstruction.SET_FILL_STYLE, fillStyle]; - if (typeof fillStyle !== 'string') { - // Fill is a pattern or gradient - align it! - fillInstruction.push(true); - } - return fillInstruction; -}; - - -/** - * @param {module:ol/render/canvas~FillStrokeState} state State. - */ -CanvasReplay.prototype.applyStroke = function(state) { - this.instructions.push(this.createStroke(state)); -}; - - -/** - * @param {module:ol/render/canvas~FillStrokeState} state State. - * @return {Array.<*>} Stroke instruction. - */ -CanvasReplay.prototype.createStroke = function(state) { - return [ - CanvasInstruction.SET_STROKE_STYLE, - state.strokeStyle, state.lineWidth * this.pixelRatio, state.lineCap, - state.lineJoin, state.miterLimit, - this.applyPixelRatio(state.lineDash), state.lineDashOffset * this.pixelRatio - ]; -}; - - -/** - * @param {module:ol/render/canvas~FillStrokeState} state State. - * @param {function(this:module:ol/render/canvas/Replay, module:ol/render/canvas~FillStrokeState, (module:ol/geom/Geometry|module:ol/render/Feature)):Array.<*>} createFill Create fill. - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. - */ -CanvasReplay.prototype.updateFillStyle = function(state, createFill, geometry) { - const fillStyle = state.fillStyle; - if (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle) { - if (fillStyle !== undefined) { - this.instructions.push(createFill.call(this, state, geometry)); - } - state.currentFillStyle = fillStyle; - } -}; - - -/** - * @param {module:ol/render/canvas~FillStrokeState} state State. - * @param {function(this:module:ol/render/canvas/Replay, module:ol/render/canvas~FillStrokeState)} applyStroke Apply stroke. - */ -CanvasReplay.prototype.updateStrokeStyle = function(state, applyStroke) { - const strokeStyle = state.strokeStyle; - const lineCap = state.lineCap; - const lineDash = state.lineDash; - const lineDashOffset = state.lineDashOffset; - const lineJoin = state.lineJoin; - const lineWidth = state.lineWidth; - const miterLimit = state.miterLimit; - if (state.currentStrokeStyle != strokeStyle || - state.currentLineCap != lineCap || - (lineDash != state.currentLineDash && !equals(state.currentLineDash, lineDash)) || - state.currentLineDashOffset != lineDashOffset || - state.currentLineJoin != lineJoin || - state.currentLineWidth != lineWidth || - state.currentMiterLimit != miterLimit) { - if (strokeStyle !== undefined) { - applyStroke.call(this, state); - } - state.currentStrokeStyle = strokeStyle; - state.currentLineCap = lineCap; - state.currentLineDash = lineDash; - state.currentLineDashOffset = lineDashOffset; - state.currentLineJoin = lineJoin; - state.currentLineWidth = lineWidth; - state.currentMiterLimit = miterLimit; - } -}; - - -/** - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - */ -CanvasReplay.prototype.endGeometry = function(geometry, feature) { - this.beginGeometryInstruction1_[2] = this.instructions.length; - this.beginGeometryInstruction1_ = null; - this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length; - this.beginGeometryInstruction2_ = null; - const endGeometryInstruction = [CanvasInstruction.END_GEOMETRY, feature]; - this.instructions.push(endGeometryInstruction); - this.hitDetectionInstructions.push(endGeometryInstruction); -}; - /** * FIXME empty description for jsdoc @@ -1065,21 +1090,4 @@ CanvasReplay.prototype.endGeometry = function(geometry, feature) { CanvasReplay.prototype.finish = UNDEFINED; -/** - * Get the buffered rendering extent. Rendering will be clipped to the extent - * provided to the constructor. To account for symbolizers that may intersect - * this extent, we calculate a buffered extent (e.g. based on stroke width). - * @return {module:ol/extent~Extent} The buffered rendering extent. - * @protected - */ -CanvasReplay.prototype.getBufferedMaxExtent = function() { - if (!this.bufferedMaxExtent_) { - this.bufferedMaxExtent_ = clone(this.maxExtent); - if (this.maxLineWidth > 0) { - const width = this.resolution * (this.maxLineWidth + 1) / 2; - buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_); - } - } - return this.bufferedMaxExtent_; -}; export default CanvasReplay; diff --git a/src/ol/render/canvas/ReplayGroup.js b/src/ol/render/canvas/ReplayGroup.js index 2096b6d6c5..2aa3f5cfeb 100644 --- a/src/ol/render/canvas/ReplayGroup.js +++ b/src/ol/render/canvas/ReplayGroup.js @@ -46,76 +46,366 @@ const BATCH_CONSTRUCTORS = { * @param {number=} opt_renderBuffer Optional rendering buffer. * @struct */ -const CanvasReplayGroup = function( - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree, opt_renderBuffer) { - ReplayGroup.call(this); +class CanvasReplayGroup { + constructor( + tolerance, + maxExtent, + resolution, + pixelRatio, + overlaps, + declutterTree, + opt_renderBuffer + ) { + ReplayGroup.call(this); + + /** + * Declutter tree. + * @private + */ + this.declutterTree_ = declutterTree; + + /** + * @type {module:ol/render/canvas~DeclutterGroup} + * @private + */ + this.declutterGroup_ = null; + + /** + * @private + * @type {number} + */ + this.tolerance_ = tolerance; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.maxExtent_ = maxExtent; + + /** + * @private + * @type {boolean} + */ + this.overlaps_ = overlaps; + + /** + * @private + * @type {number} + */ + this.pixelRatio_ = pixelRatio; + + /** + * @private + * @type {number} + */ + this.resolution_ = resolution; + + /** + * @private + * @type {number|undefined} + */ + this.renderBuffer_ = opt_renderBuffer; + + /** + * @private + * @type {!Object.>} + */ + this.replaysByZIndex_ = {}; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.hitDetectionContext_ = createCanvasContext2D(1, 1); + + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.hitDetectionTransform_ = createTransform(); + } /** - * Declutter tree. - * @private + * @param {boolean} group Group with previous replay. + * @return {module:ol/render/canvas~DeclutterGroup} Declutter instruction group. */ - this.declutterTree_ = declutterTree; + addDeclutter(group) { + let declutter = null; + if (this.declutterTree_) { + if (group) { + declutter = this.declutterGroup_; + /** @type {number} */ (declutter[4])++; + } else { + declutter = this.declutterGroup_ = createEmpty(); + declutter.push(1); + } + } + return declutter; + } /** - * @type {module:ol/render/canvas~DeclutterGroup} - * @private + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/transform~Transform} transform Transform. */ - this.declutterGroup_ = null; + clip(context, transform) { + const flatClipCoords = this.getClipCoords(transform); + context.beginPath(); + context.moveTo(flatClipCoords[0], flatClipCoords[1]); + context.lineTo(flatClipCoords[2], flatClipCoords[3]); + context.lineTo(flatClipCoords[4], flatClipCoords[5]); + context.lineTo(flatClipCoords[6], flatClipCoords[7]); + context.clip(); + } /** - * @private - * @type {number} + * @param {Array.} replays Replays. + * @return {boolean} Has replays of the provided types. */ - this.tolerance_ = tolerance; + hasReplays(replays) { + for (const zIndex in this.replaysByZIndex_) { + const candidates = this.replaysByZIndex_[zIndex]; + for (let i = 0, ii = replays.length; i < ii; ++i) { + if (replays[i] in candidates) { + return true; + } + } + } + return false; + } /** - * @private - * @type {module:ol/extent~Extent} + * FIXME empty description for jsdoc */ - this.maxExtent_ = maxExtent; + finish() { + for (const zKey in this.replaysByZIndex_) { + const replays = this.replaysByZIndex_[zKey]; + for (const replayKey in replays) { + replays[replayKey].finish(); + } + } + } /** - * @private - * @type {boolean} + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {number} hitTolerance Hit tolerance in pixels. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T} callback Feature callback. + * @param {Object.} declutterReplays Declutter replays. + * @return {T|undefined} Callback result. + * @template T */ - this.overlaps_ = overlaps; + forEachFeatureAtCoordinate( + coordinate, + resolution, + rotation, + hitTolerance, + skippedFeaturesHash, + callback, + declutterReplays + ) { + + hitTolerance = Math.round(hitTolerance); + const contextSize = hitTolerance * 2 + 1; + const transform = composeTransform(this.hitDetectionTransform_, + hitTolerance + 0.5, hitTolerance + 0.5, + 1 / resolution, -1 / resolution, + -rotation, + -coordinate[0], -coordinate[1]); + const context = this.hitDetectionContext_; + + if (context.canvas.width !== contextSize || context.canvas.height !== contextSize) { + context.canvas.width = contextSize; + context.canvas.height = contextSize; + } else { + context.clearRect(0, 0, contextSize, contextSize); + } + + /** + * @type {module:ol/extent~Extent} + */ + let hitExtent; + if (this.renderBuffer_ !== undefined) { + hitExtent = createEmpty(); + extendCoordinate(hitExtent, coordinate); + buffer(hitExtent, resolution * (this.renderBuffer_ + hitTolerance), hitExtent); + } + + const mask = getCircleArray(hitTolerance); + let declutteredFeatures; + if (this.declutterTree_) { + declutteredFeatures = this.declutterTree_.all().map(function(entry) { + return entry.value; + }); + } + + let replayType; + + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @return {?} Callback result. + */ + function featureCallback(feature) { + const imageData = context.getImageData(0, 0, contextSize, contextSize).data; + for (let i = 0; i < contextSize; i++) { + for (let j = 0; j < contextSize; j++) { + if (mask[i][j]) { + if (imageData[(j * contextSize + i) * 4 + 3] > 0) { + let result; + if (!(declutteredFeatures && (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) || + declutteredFeatures.indexOf(feature) !== -1) { + result = callback(feature); + } + if (result) { + return result; + } else { + context.clearRect(0, 0, contextSize, contextSize); + return undefined; + } + } + } + } + } + } + + /** @type {Array.} */ + const zs = Object.keys(this.replaysByZIndex_).map(Number); + zs.sort(numberSafeCompareFunction); + + let i, j, replays, replay, result; + for (i = zs.length - 1; i >= 0; --i) { + const zIndexKey = zs[i].toString(); + replays = this.replaysByZIndex_[zIndexKey]; + for (j = ORDER.length - 1; j >= 0; --j) { + replayType = ORDER[j]; + replay = replays[replayType]; + if (replay !== undefined) { + if (declutterReplays && + (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) { + const declutter = declutterReplays[zIndexKey]; + if (!declutter) { + declutterReplays[zIndexKey] = [replay, transform.slice(0)]; + } else { + declutter.push(replay, transform.slice(0)); + } + } else { + result = replay.replayHitDetection(context, transform, rotation, + skippedFeaturesHash, featureCallback, hitExtent); + if (result) { + return result; + } + } + } + } + } + return undefined; + } /** - * @private - * @type {number} + * @param {module:ol/transform~Transform} transform Transform. + * @return {Array.} Clip coordinates. */ - this.pixelRatio_ = pixelRatio; + getClipCoords(transform) { + const maxExtent = this.maxExtent_; + const minX = maxExtent[0]; + const minY = maxExtent[1]; + const maxX = maxExtent[2]; + const maxY = maxExtent[3]; + const flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY]; + transform2D( + flatClipCoords, 0, 8, 2, transform, flatClipCoords); + return flatClipCoords; + } /** - * @private - * @type {number} + * @inheritDoc */ - this.resolution_ = resolution; + getReplay(zIndex, replayType) { + const zIndexKey = zIndex !== undefined ? zIndex.toString() : '0'; + let replays = this.replaysByZIndex_[zIndexKey]; + if (replays === undefined) { + replays = {}; + this.replaysByZIndex_[zIndexKey] = replays; + } + let replay = replays[replayType]; + if (replay === undefined) { + const Constructor = BATCH_CONSTRUCTORS[replayType]; + replay = new Constructor(this.tolerance_, this.maxExtent_, + this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_); + replays[replayType] = replay; + } + return replay; + } /** - * @private - * @type {number|undefined} + * @return {Object.>} Replays. */ - this.renderBuffer_ = opt_renderBuffer; + getReplays() { + return this.replaysByZIndex_; + } /** - * @private - * @type {!Object.>} + * @inheritDoc */ - this.replaysByZIndex_ = {}; + isEmpty() { + return isEmpty(this.replaysByZIndex_); + } /** - * @private - * @type {CanvasRenderingContext2D} + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/transform~Transform} transform Transform. + * @param {number} viewRotation View rotation. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {Array.=} opt_replayTypes Ordered replay types to replay. + * Default is {@link module:ol/render/replay~ORDER} + * @param {Object.=} opt_declutterReplays Declutter replays. */ - this.hitDetectionContext_ = createCanvasContext2D(1, 1); + replay( + context, + transform, + viewRotation, + skippedFeaturesHash, + opt_replayTypes, + opt_declutterReplays + ) { - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.hitDetectionTransform_ = createTransform(); -}; + /** @type {Array.} */ + const zs = Object.keys(this.replaysByZIndex_).map(Number); + zs.sort(numberSafeCompareFunction); + + // setup clipping so that the parts of over-simplified geometries are not + // visible outside the current extent when panning + context.save(); + this.clip(context, transform); + + const replayTypes = opt_replayTypes ? opt_replayTypes : ORDER; + let i, ii, j, jj, replays, replay; + for (i = 0, ii = zs.length; i < ii; ++i) { + const zIndexKey = zs[i].toString(); + replays = this.replaysByZIndex_[zIndexKey]; + for (j = 0, jj = replayTypes.length; j < jj; ++j) { + const replayType = replayTypes[j]; + replay = replays[replayType]; + if (replay !== undefined) { + if (opt_declutterReplays && + (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) { + const declutter = opt_declutterReplays[zIndexKey]; + if (!declutter) { + opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)]; + } else { + declutter.push(replay, transform.slice(0)); + } + } else { + replay.replay(context, transform, viewRotation, skippedFeaturesHash); + } + } + } + } + + context.restore(); + } +} inherits(CanvasReplayGroup, ReplayGroup); @@ -217,281 +507,4 @@ export function replayDeclutter(declutterReplays, context, rotation) { } -/** - * @param {boolean} group Group with previous replay. - * @return {module:ol/render/canvas~DeclutterGroup} Declutter instruction group. - */ -CanvasReplayGroup.prototype.addDeclutter = function(group) { - let declutter = null; - if (this.declutterTree_) { - if (group) { - declutter = this.declutterGroup_; - /** @type {number} */ (declutter[4])++; - } else { - declutter = this.declutterGroup_ = createEmpty(); - declutter.push(1); - } - } - return declutter; -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/transform~Transform} transform Transform. - */ -CanvasReplayGroup.prototype.clip = function(context, transform) { - const flatClipCoords = this.getClipCoords(transform); - context.beginPath(); - context.moveTo(flatClipCoords[0], flatClipCoords[1]); - context.lineTo(flatClipCoords[2], flatClipCoords[3]); - context.lineTo(flatClipCoords[4], flatClipCoords[5]); - context.lineTo(flatClipCoords[6], flatClipCoords[7]); - context.clip(); -}; - - -/** - * @param {Array.} replays Replays. - * @return {boolean} Has replays of the provided types. - */ -CanvasReplayGroup.prototype.hasReplays = function(replays) { - for (const zIndex in this.replaysByZIndex_) { - const candidates = this.replaysByZIndex_[zIndex]; - for (let i = 0, ii = replays.length; i < ii; ++i) { - if (replays[i] in candidates) { - return true; - } - } - } - return false; -}; - - -/** - * FIXME empty description for jsdoc - */ -CanvasReplayGroup.prototype.finish = function() { - for (const zKey in this.replaysByZIndex_) { - const replays = this.replaysByZIndex_[zKey]; - for (const replayKey in replays) { - replays[replayKey].finish(); - } - } -}; - - -/** - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {number} resolution Resolution. - * @param {number} rotation Rotation. - * @param {number} hitTolerance Hit tolerance in pixels. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T} callback Feature callback. - * @param {Object.} declutterReplays Declutter replays. - * @return {T|undefined} Callback result. - * @template T - */ -CanvasReplayGroup.prototype.forEachFeatureAtCoordinate = function( - coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback, declutterReplays) { - - hitTolerance = Math.round(hitTolerance); - const contextSize = hitTolerance * 2 + 1; - const transform = composeTransform(this.hitDetectionTransform_, - hitTolerance + 0.5, hitTolerance + 0.5, - 1 / resolution, -1 / resolution, - -rotation, - -coordinate[0], -coordinate[1]); - const context = this.hitDetectionContext_; - - if (context.canvas.width !== contextSize || context.canvas.height !== contextSize) { - context.canvas.width = contextSize; - context.canvas.height = contextSize; - } else { - context.clearRect(0, 0, contextSize, contextSize); - } - - /** - * @type {module:ol/extent~Extent} - */ - let hitExtent; - if (this.renderBuffer_ !== undefined) { - hitExtent = createEmpty(); - extendCoordinate(hitExtent, coordinate); - buffer(hitExtent, resolution * (this.renderBuffer_ + hitTolerance), hitExtent); - } - - const mask = getCircleArray(hitTolerance); - let declutteredFeatures; - if (this.declutterTree_) { - declutteredFeatures = this.declutterTree_.all().map(function(entry) { - return entry.value; - }); - } - - let replayType; - - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @return {?} Callback result. - */ - function featureCallback(feature) { - const imageData = context.getImageData(0, 0, contextSize, contextSize).data; - for (let i = 0; i < contextSize; i++) { - for (let j = 0; j < contextSize; j++) { - if (mask[i][j]) { - if (imageData[(j * contextSize + i) * 4 + 3] > 0) { - let result; - if (!(declutteredFeatures && (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) || - declutteredFeatures.indexOf(feature) !== -1) { - result = callback(feature); - } - if (result) { - return result; - } else { - context.clearRect(0, 0, contextSize, contextSize); - return undefined; - } - } - } - } - } - } - - /** @type {Array.} */ - const zs = Object.keys(this.replaysByZIndex_).map(Number); - zs.sort(numberSafeCompareFunction); - - let i, j, replays, replay, result; - for (i = zs.length - 1; i >= 0; --i) { - const zIndexKey = zs[i].toString(); - replays = this.replaysByZIndex_[zIndexKey]; - for (j = ORDER.length - 1; j >= 0; --j) { - replayType = ORDER[j]; - replay = replays[replayType]; - if (replay !== undefined) { - if (declutterReplays && - (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) { - const declutter = declutterReplays[zIndexKey]; - if (!declutter) { - declutterReplays[zIndexKey] = [replay, transform.slice(0)]; - } else { - declutter.push(replay, transform.slice(0)); - } - } else { - result = replay.replayHitDetection(context, transform, rotation, - skippedFeaturesHash, featureCallback, hitExtent); - if (result) { - return result; - } - } - } - } - } - return undefined; -}; - - -/** - * @param {module:ol/transform~Transform} transform Transform. - * @return {Array.} Clip coordinates. - */ -CanvasReplayGroup.prototype.getClipCoords = function(transform) { - const maxExtent = this.maxExtent_; - const minX = maxExtent[0]; - const minY = maxExtent[1]; - const maxX = maxExtent[2]; - const maxY = maxExtent[3]; - const flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY]; - transform2D( - flatClipCoords, 0, 8, 2, transform, flatClipCoords); - return flatClipCoords; -}; - - -/** - * @inheritDoc - */ -CanvasReplayGroup.prototype.getReplay = function(zIndex, replayType) { - const zIndexKey = zIndex !== undefined ? zIndex.toString() : '0'; - let replays = this.replaysByZIndex_[zIndexKey]; - if (replays === undefined) { - replays = {}; - this.replaysByZIndex_[zIndexKey] = replays; - } - let replay = replays[replayType]; - if (replay === undefined) { - const Constructor = BATCH_CONSTRUCTORS[replayType]; - replay = new Constructor(this.tolerance_, this.maxExtent_, - this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_); - replays[replayType] = replay; - } - return replay; -}; - - -/** - * @return {Object.>} Replays. - */ -CanvasReplayGroup.prototype.getReplays = function() { - return this.replaysByZIndex_; -}; - - -/** - * @inheritDoc - */ -CanvasReplayGroup.prototype.isEmpty = function() { - return isEmpty(this.replaysByZIndex_); -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/transform~Transform} transform Transform. - * @param {number} viewRotation View rotation. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {Array.=} opt_replayTypes Ordered replay types to replay. - * Default is {@link module:ol/render/replay~ORDER} - * @param {Object.=} opt_declutterReplays Declutter replays. - */ -CanvasReplayGroup.prototype.replay = function(context, - transform, viewRotation, skippedFeaturesHash, opt_replayTypes, opt_declutterReplays) { - - /** @type {Array.} */ - const zs = Object.keys(this.replaysByZIndex_).map(Number); - zs.sort(numberSafeCompareFunction); - - // setup clipping so that the parts of over-simplified geometries are not - // visible outside the current extent when panning - context.save(); - this.clip(context, transform); - - const replayTypes = opt_replayTypes ? opt_replayTypes : ORDER; - let i, ii, j, jj, replays, replay; - for (i = 0, ii = zs.length; i < ii; ++i) { - const zIndexKey = zs[i].toString(); - replays = this.replaysByZIndex_[zIndexKey]; - for (j = 0, jj = replayTypes.length; j < jj; ++j) { - const replayType = replayTypes[j]; - replay = replays[replayType]; - if (replay !== undefined) { - if (opt_declutterReplays && - (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) { - const declutter = opt_declutterReplays[zIndexKey]; - if (!declutter) { - opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)]; - } else { - declutter.push(replay, transform.slice(0)); - } - } else { - replay.replay(context, transform, viewRotation, skippedFeaturesHash); - } - } - } - } - - context.restore(); -}; - export default CanvasReplayGroup; diff --git a/src/ol/render/canvas/TextReplay.js b/src/ol/render/canvas/TextReplay.js index f398b7d196..945243bad1 100644 --- a/src/ol/render/canvas/TextReplay.js +++ b/src/ol/render/canvas/TextReplay.js @@ -25,113 +25,501 @@ import TextPlacement from '../../style/TextPlacement.js'; * @param {?} declutterTree Declutter tree. * @struct */ -const CanvasTextReplay = function( - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { - CanvasReplay.call(this, - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); +class CanvasTextReplay { + constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + CanvasReplay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); + + /** + * @private + * @type {module:ol/render/canvas~DeclutterGroup} + */ + this.declutterGroup_; + + /** + * @private + * @type {Array.} + */ + this.labels_ = null; + + /** + * @private + * @type {string} + */ + this.text_ = ''; + + /** + * @private + * @type {number} + */ + this.textOffsetX_ = 0; + + /** + * @private + * @type {number} + */ + this.textOffsetY_ = 0; + + /** + * @private + * @type {boolean|undefined} + */ + this.textRotateWithView_ = undefined; + + /** + * @private + * @type {number} + */ + this.textRotation_ = 0; + + /** + * @private + * @type {?module:ol/render/canvas~FillState} + */ + this.textFillState_ = null; + + /** + * @type {!Object.} + */ + this.fillStates = {}; + + /** + * @private + * @type {?module:ol/render/canvas~StrokeState} + */ + this.textStrokeState_ = null; + + /** + * @type {!Object.} + */ + this.strokeStates = {}; + + /** + * @private + * @type {module:ol/render/canvas~TextState} + */ + this.textState_ = /** @type {module:ol/render/canvas~TextState} */ ({}); + + /** + * @type {!Object.} + */ + this.textStates = {}; + + /** + * @private + * @type {string} + */ + this.textKey_ = ''; + + /** + * @private + * @type {string} + */ + this.fillKey_ = ''; + + /** + * @private + * @type {string} + */ + this.strokeKey_ = ''; + + /** + * @private + * @type {Object.>} + */ + this.widths_ = {}; + + labelCache.prune(); + + } + + /** + * @inheritDoc + */ + drawText(geometry, feature) { + const fillState = this.textFillState_; + const strokeState = this.textStrokeState_; + const textState = this.textState_; + if (this.text_ === '' || !textState || (!fillState && !strokeState)) { + return; + } + + let begin = this.coordinates.length; + + const geometryType = geometry.getType(); + let flatCoordinates = null; + let end = 2; + let stride = 2; + let i, ii; + + if (textState.placement === TextPlacement.LINE) { + if (!intersects(this.getBufferedMaxExtent(), geometry.getExtent())) { + return; + } + let ends; + flatCoordinates = geometry.getFlatCoordinates(); + stride = geometry.getStride(); + if (geometryType == GeometryType.LINE_STRING) { + ends = [flatCoordinates.length]; + } else if (geometryType == GeometryType.MULTI_LINE_STRING) { + ends = geometry.getEnds(); + } else if (geometryType == GeometryType.POLYGON) { + ends = geometry.getEnds().slice(0, 1); + } else if (geometryType == GeometryType.MULTI_POLYGON) { + const endss = geometry.getEndss(); + ends = []; + for (i = 0, ii = endss.length; i < ii; ++i) { + ends.push(endss[i][0]); + } + } + this.beginGeometry(geometry, feature); + const textAlign = textState.textAlign; + let flatOffset = 0; + let flatEnd; + for (let o = 0, oo = ends.length; o < oo; ++o) { + if (textAlign == undefined) { + const range = matchingChunk(textState.maxAngle, flatCoordinates, flatOffset, ends[o], stride); + flatOffset = range[0]; + flatEnd = range[1]; + } else { + flatEnd = ends[o]; + } + for (i = flatOffset; i < flatEnd; i += stride) { + this.coordinates.push(flatCoordinates[i], flatCoordinates[i + 1]); + } + end = this.coordinates.length; + flatOffset = ends[o]; + this.drawChars_(begin, end, this.declutterGroup_); + begin = end; + } + this.endGeometry(geometry, feature); + + } else { + const label = this.getImage(this.text_, this.textKey_, this.fillKey_, this.strokeKey_); + const width = label.width / this.pixelRatio; + switch (geometryType) { + case GeometryType.POINT: + case GeometryType.MULTI_POINT: + flatCoordinates = geometry.getFlatCoordinates(); + end = flatCoordinates.length; + break; + case GeometryType.LINE_STRING: + flatCoordinates = /** @type {module:ol/geom/LineString} */ (geometry).getFlatMidpoint(); + break; + case GeometryType.CIRCLE: + flatCoordinates = /** @type {module:ol/geom/Circle} */ (geometry).getCenter(); + break; + case GeometryType.MULTI_LINE_STRING: + flatCoordinates = /** @type {module:ol/geom/MultiLineString} */ (geometry).getFlatMidpoints(); + end = flatCoordinates.length; + break; + case GeometryType.POLYGON: + flatCoordinates = /** @type {module:ol/geom/Polygon} */ (geometry).getFlatInteriorPoint(); + if (!textState.overflow && flatCoordinates[2] / this.resolution < width) { + return; + } + stride = 3; + break; + case GeometryType.MULTI_POLYGON: + const interiorPoints = /** @type {module:ol/geom/MultiPolygon} */ (geometry).getFlatInteriorPoints(); + flatCoordinates = []; + for (i = 0, ii = interiorPoints.length; i < ii; i += 3) { + if (textState.overflow || interiorPoints[i + 2] / this.resolution >= width) { + flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]); + } + } + end = flatCoordinates.length; + if (end == 0) { + return; + } + break; + default: + } + end = this.appendFlatCoordinates(flatCoordinates, 0, end, stride, false, false); + if (textState.backgroundFill || textState.backgroundStroke) { + this.setFillStrokeStyle(textState.backgroundFill, textState.backgroundStroke); + if (textState.backgroundFill) { + this.updateFillStyle(this.state, this.createFill, geometry); + this.hitDetectionInstructions.push(this.createFill(this.state, geometry)); + } + if (textState.backgroundStroke) { + this.updateStrokeStyle(this.state, this.applyStroke); + this.hitDetectionInstructions.push(this.createStroke(this.state)); + } + } + this.beginGeometry(geometry, feature); + this.drawTextImage_(label, begin, end); + this.endGeometry(geometry, feature); + } + } + + /** + * @param {string} text Text. + * @param {string} textKey Text style key. + * @param {string} fillKey Fill style key. + * @param {string} strokeKey Stroke style key. + * @return {HTMLCanvasElement} Image. + */ + getImage(text, textKey, fillKey, strokeKey) { + let label; + const key = strokeKey + textKey + text + fillKey + this.pixelRatio; + + if (!labelCache.containsKey(key)) { + const strokeState = strokeKey ? this.strokeStates[strokeKey] || this.textStrokeState_ : null; + const fillState = fillKey ? this.fillStates[fillKey] || this.textFillState_ : null; + const textState = this.textStates[textKey] || this.textState_; + const pixelRatio = this.pixelRatio; + const scale = textState.scale * pixelRatio; + const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; + const strokeWidth = strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0; + + const lines = text.split('\n'); + const numLines = lines.length; + const widths = []; + const width = measureTextWidths(textState.font, lines, widths); + const lineHeight = measureTextHeight(textState.font); + const height = lineHeight * numLines; + const renderWidth = (width + strokeWidth); + const context = createCanvasContext2D( + Math.ceil(renderWidth * scale), + Math.ceil((height + strokeWidth) * scale)); + label = context.canvas; + labelCache.set(key, label); + if (scale != 1) { + context.scale(scale, scale); + } + context.font = textState.font; + if (strokeKey) { + context.strokeStyle = strokeState.strokeStyle; + context.lineWidth = strokeWidth; + context.lineCap = strokeState.lineCap; + context.lineJoin = strokeState.lineJoin; + context.miterLimit = strokeState.miterLimit; + if (CANVAS_LINE_DASH && strokeState.lineDash.length) { + context.setLineDash(strokeState.lineDash); + context.lineDashOffset = strokeState.lineDashOffset; + } + } + if (fillKey) { + context.fillStyle = fillState.fillStyle; + } + context.textBaseline = 'middle'; + context.textAlign = 'center'; + const leftRight = (0.5 - align); + const x = align * label.width / scale + leftRight * strokeWidth; + let i; + if (strokeKey) { + for (i = 0; i < numLines; ++i) { + context.strokeText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight); + } + } + if (fillKey) { + for (i = 0; i < numLines; ++i) { + context.fillText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight); + } + } + } + return labelCache.get(key); + } /** * @private - * @type {module:ol/render/canvas~DeclutterGroup} + * @param {HTMLCanvasElement} label Label. + * @param {number} begin Begin. + * @param {number} end End. */ - this.declutterGroup_; + drawTextImage_(label, begin, end) { + const textState = this.textState_; + const strokeState = this.textStrokeState_; + const pixelRatio = this.pixelRatio; + const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; + const baseline = TEXT_ALIGN[textState.textBaseline]; + const strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0; + + const anchorX = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth; + const anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth; + this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, + label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, + this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, + 1, true, label.width, + textState.padding == defaultPadding ? + defaultPadding : textState.padding.map(function(p) { + return p * pixelRatio; + }), + !!textState.backgroundFill, !!textState.backgroundStroke + ]); + this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, + label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, + this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, + 1 / pixelRatio, true, label.width, textState.padding, + !!textState.backgroundFill, !!textState.backgroundStroke + ]); + } /** * @private - * @type {Array.} + * @param {number} begin Begin. + * @param {number} end End. + * @param {module:ol/render/canvas~DeclutterGroup} declutterGroup Declutter group. */ - this.labels_ = null; + drawChars_(begin, end, declutterGroup) { + const strokeState = this.textStrokeState_; + const textState = this.textState_; + const fillState = this.textFillState_; + + const strokeKey = this.strokeKey_; + if (strokeState) { + if (!(strokeKey in this.strokeStates)) { + this.strokeStates[strokeKey] = /** @type {module:ol/render/canvas~StrokeState} */ ({ + strokeStyle: strokeState.strokeStyle, + lineCap: strokeState.lineCap, + lineDashOffset: strokeState.lineDashOffset, + lineWidth: strokeState.lineWidth, + lineJoin: strokeState.lineJoin, + miterLimit: strokeState.miterLimit, + lineDash: strokeState.lineDash + }); + } + } + const textKey = this.textKey_; + if (!(this.textKey_ in this.textStates)) { + this.textStates[this.textKey_] = /** @type {module:ol/render/canvas~TextState} */ ({ + font: textState.font, + textAlign: textState.textAlign || defaultTextAlign, + scale: textState.scale + }); + } + const fillKey = this.fillKey_; + if (fillState) { + if (!(fillKey in this.fillStates)) { + this.fillStates[fillKey] = /** @type {module:ol/render/canvas~FillState} */ ({ + fillStyle: fillState.fillStyle + }); + } + } + + const pixelRatio = this.pixelRatio; + const baseline = TEXT_ALIGN[textState.textBaseline]; + + const offsetY = this.textOffsetY_ * pixelRatio; + const text = this.text_; + const font = textState.font; + const textScale = textState.scale; + const strokeWidth = strokeState ? strokeState.lineWidth * textScale / 2 : 0; + let widths = this.widths_[font]; + if (!widths) { + this.widths_[font] = widths = {}; + } + this.instructions.push([CanvasInstruction.DRAW_CHARS, + begin, end, baseline, declutterGroup, + textState.overflow, fillKey, textState.maxAngle, + function(text) { + let width = widths[text]; + if (!width) { + width = widths[text] = measureTextWidth(font, text); + } + return width * textScale * pixelRatio; + }, + offsetY, strokeKey, strokeWidth * pixelRatio, text, textKey, 1 + ]); + this.hitDetectionInstructions.push([CanvasInstruction.DRAW_CHARS, + begin, end, baseline, declutterGroup, + textState.overflow, fillKey, textState.maxAngle, + function(text) { + let width = widths[text]; + if (!width) { + width = widths[text] = measureTextWidth(font, text); + } + return width * textScale; + }, + offsetY, strokeKey, strokeWidth, text, textKey, 1 / pixelRatio + ]); + } /** - * @private - * @type {string} + * @inheritDoc */ - this.text_ = ''; + setTextStyle(textStyle, declutterGroup) { + let textState, fillState, strokeState; + if (!textStyle) { + this.text_ = ''; + } else { + this.declutterGroup_ = /** @type {module:ol/render/canvas~DeclutterGroup} */ (declutterGroup); - /** - * @private - * @type {number} - */ - this.textOffsetX_ = 0; + const textFillStyle = textStyle.getFill(); + if (!textFillStyle) { + fillState = this.textFillState_ = null; + } else { + fillState = this.textFillState_; + if (!fillState) { + fillState = this.textFillState_ = /** @type {module:ol/render/canvas~FillState} */ ({}); + } + fillState.fillStyle = asColorLike( + textFillStyle.getColor() || defaultFillStyle); + } - /** - * @private - * @type {number} - */ - this.textOffsetY_ = 0; + const textStrokeStyle = textStyle.getStroke(); + if (!textStrokeStyle) { + strokeState = this.textStrokeState_ = null; + } else { + strokeState = this.textStrokeState_; + if (!strokeState) { + strokeState = this.textStrokeState_ = /** @type {module:ol/render/canvas~StrokeState} */ ({}); + } + const lineDash = textStrokeStyle.getLineDash(); + const lineDashOffset = textStrokeStyle.getLineDashOffset(); + const lineWidth = textStrokeStyle.getWidth(); + const miterLimit = textStrokeStyle.getMiterLimit(); + strokeState.lineCap = textStrokeStyle.getLineCap() || defaultLineCap; + strokeState.lineDash = lineDash ? lineDash.slice() : defaultLineDash; + strokeState.lineDashOffset = + lineDashOffset === undefined ? defaultLineDashOffset : lineDashOffset; + strokeState.lineJoin = textStrokeStyle.getLineJoin() || defaultLineJoin; + strokeState.lineWidth = + lineWidth === undefined ? defaultLineWidth : lineWidth; + strokeState.miterLimit = + miterLimit === undefined ? defaultMiterLimit : miterLimit; + strokeState.strokeStyle = asColorLike( + textStrokeStyle.getColor() || defaultStrokeStyle); + } - /** - * @private - * @type {boolean|undefined} - */ - this.textRotateWithView_ = undefined; + textState = this.textState_; + const font = textStyle.getFont() || defaultFont; + checkFont(font); + const textScale = textStyle.getScale(); + textState.overflow = textStyle.getOverflow(); + textState.font = font; + textState.maxAngle = textStyle.getMaxAngle(); + textState.placement = textStyle.getPlacement(); + textState.textAlign = textStyle.getTextAlign(); + textState.textBaseline = textStyle.getTextBaseline() || defaultTextBaseline; + textState.backgroundFill = textStyle.getBackgroundFill(); + textState.backgroundStroke = textStyle.getBackgroundStroke(); + textState.padding = textStyle.getPadding() || defaultPadding; + textState.scale = textScale === undefined ? 1 : textScale; - /** - * @private - * @type {number} - */ - this.textRotation_ = 0; + const textOffsetX = textStyle.getOffsetX(); + const textOffsetY = textStyle.getOffsetY(); + const textRotateWithView = textStyle.getRotateWithView(); + const textRotation = textStyle.getRotation(); + this.text_ = textStyle.getText() || ''; + this.textOffsetX_ = textOffsetX === undefined ? 0 : textOffsetX; + this.textOffsetY_ = textOffsetY === undefined ? 0 : textOffsetY; + this.textRotateWithView_ = textRotateWithView === undefined ? false : textRotateWithView; + this.textRotation_ = textRotation === undefined ? 0 : textRotation; - /** - * @private - * @type {?module:ol/render/canvas~FillState} - */ - this.textFillState_ = null; - - /** - * @type {!Object.} - */ - this.fillStates = {}; - - /** - * @private - * @type {?module:ol/render/canvas~StrokeState} - */ - this.textStrokeState_ = null; - - /** - * @type {!Object.} - */ - this.strokeStates = {}; - - /** - * @private - * @type {module:ol/render/canvas~TextState} - */ - this.textState_ = /** @type {module:ol/render/canvas~TextState} */ ({}); - - /** - * @type {!Object.} - */ - this.textStates = {}; - - /** - * @private - * @type {string} - */ - this.textKey_ = ''; - - /** - * @private - * @type {string} - */ - this.fillKey_ = ''; - - /** - * @private - * @type {string} - */ - this.strokeKey_ = ''; - - /** - * @private - * @type {Object.>} - */ - this.widths_ = {}; - - labelCache.prune(); - -}; + this.strokeKey_ = strokeState ? + (typeof strokeState.strokeStyle == 'string' ? strokeState.strokeStyle : getUid(strokeState.strokeStyle)) + + strokeState.lineCap + strokeState.lineDashOffset + '|' + strokeState.lineWidth + + strokeState.lineJoin + strokeState.miterLimit + '[' + strokeState.lineDash.join() + ']' : + ''; + this.textKey_ = textState.font + textState.scale + (textState.textAlign || '?'); + this.fillKey_ = fillState ? + (typeof fillState.fillStyle == 'string' ? fillState.fillStyle : ('|' + getUid(fillState.fillStyle))) : + ''; + } + } +} inherits(CanvasTextReplay, CanvasReplay); @@ -155,394 +543,4 @@ export function measureTextWidths(font, lines, widths) { } -/** - * @inheritDoc - */ -CanvasTextReplay.prototype.drawText = function(geometry, feature) { - const fillState = this.textFillState_; - const strokeState = this.textStrokeState_; - const textState = this.textState_; - if (this.text_ === '' || !textState || (!fillState && !strokeState)) { - return; - } - - let begin = this.coordinates.length; - - const geometryType = geometry.getType(); - let flatCoordinates = null; - let end = 2; - let stride = 2; - let i, ii; - - if (textState.placement === TextPlacement.LINE) { - if (!intersects(this.getBufferedMaxExtent(), geometry.getExtent())) { - return; - } - let ends; - flatCoordinates = geometry.getFlatCoordinates(); - stride = geometry.getStride(); - if (geometryType == GeometryType.LINE_STRING) { - ends = [flatCoordinates.length]; - } else if (geometryType == GeometryType.MULTI_LINE_STRING) { - ends = geometry.getEnds(); - } else if (geometryType == GeometryType.POLYGON) { - ends = geometry.getEnds().slice(0, 1); - } else if (geometryType == GeometryType.MULTI_POLYGON) { - const endss = geometry.getEndss(); - ends = []; - for (i = 0, ii = endss.length; i < ii; ++i) { - ends.push(endss[i][0]); - } - } - this.beginGeometry(geometry, feature); - const textAlign = textState.textAlign; - let flatOffset = 0; - let flatEnd; - for (let o = 0, oo = ends.length; o < oo; ++o) { - if (textAlign == undefined) { - const range = matchingChunk(textState.maxAngle, flatCoordinates, flatOffset, ends[o], stride); - flatOffset = range[0]; - flatEnd = range[1]; - } else { - flatEnd = ends[o]; - } - for (i = flatOffset; i < flatEnd; i += stride) { - this.coordinates.push(flatCoordinates[i], flatCoordinates[i + 1]); - } - end = this.coordinates.length; - flatOffset = ends[o]; - this.drawChars_(begin, end, this.declutterGroup_); - begin = end; - } - this.endGeometry(geometry, feature); - - } else { - const label = this.getImage(this.text_, this.textKey_, this.fillKey_, this.strokeKey_); - const width = label.width / this.pixelRatio; - switch (geometryType) { - case GeometryType.POINT: - case GeometryType.MULTI_POINT: - flatCoordinates = geometry.getFlatCoordinates(); - end = flatCoordinates.length; - break; - case GeometryType.LINE_STRING: - flatCoordinates = /** @type {module:ol/geom/LineString} */ (geometry).getFlatMidpoint(); - break; - case GeometryType.CIRCLE: - flatCoordinates = /** @type {module:ol/geom/Circle} */ (geometry).getCenter(); - break; - case GeometryType.MULTI_LINE_STRING: - flatCoordinates = /** @type {module:ol/geom/MultiLineString} */ (geometry).getFlatMidpoints(); - end = flatCoordinates.length; - break; - case GeometryType.POLYGON: - flatCoordinates = /** @type {module:ol/geom/Polygon} */ (geometry).getFlatInteriorPoint(); - if (!textState.overflow && flatCoordinates[2] / this.resolution < width) { - return; - } - stride = 3; - break; - case GeometryType.MULTI_POLYGON: - const interiorPoints = /** @type {module:ol/geom/MultiPolygon} */ (geometry).getFlatInteriorPoints(); - flatCoordinates = []; - for (i = 0, ii = interiorPoints.length; i < ii; i += 3) { - if (textState.overflow || interiorPoints[i + 2] / this.resolution >= width) { - flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]); - } - } - end = flatCoordinates.length; - if (end == 0) { - return; - } - break; - default: - } - end = this.appendFlatCoordinates(flatCoordinates, 0, end, stride, false, false); - if (textState.backgroundFill || textState.backgroundStroke) { - this.setFillStrokeStyle(textState.backgroundFill, textState.backgroundStroke); - if (textState.backgroundFill) { - this.updateFillStyle(this.state, this.createFill, geometry); - this.hitDetectionInstructions.push(this.createFill(this.state, geometry)); - } - if (textState.backgroundStroke) { - this.updateStrokeStyle(this.state, this.applyStroke); - this.hitDetectionInstructions.push(this.createStroke(this.state)); - } - } - this.beginGeometry(geometry, feature); - this.drawTextImage_(label, begin, end); - this.endGeometry(geometry, feature); - } -}; - - -/** - * @param {string} text Text. - * @param {string} textKey Text style key. - * @param {string} fillKey Fill style key. - * @param {string} strokeKey Stroke style key. - * @return {HTMLCanvasElement} Image. - */ -CanvasTextReplay.prototype.getImage = function(text, textKey, fillKey, strokeKey) { - let label; - const key = strokeKey + textKey + text + fillKey + this.pixelRatio; - - if (!labelCache.containsKey(key)) { - const strokeState = strokeKey ? this.strokeStates[strokeKey] || this.textStrokeState_ : null; - const fillState = fillKey ? this.fillStates[fillKey] || this.textFillState_ : null; - const textState = this.textStates[textKey] || this.textState_; - const pixelRatio = this.pixelRatio; - const scale = textState.scale * pixelRatio; - const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; - const strokeWidth = strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0; - - const lines = text.split('\n'); - const numLines = lines.length; - const widths = []; - const width = measureTextWidths(textState.font, lines, widths); - const lineHeight = measureTextHeight(textState.font); - const height = lineHeight * numLines; - const renderWidth = (width + strokeWidth); - const context = createCanvasContext2D( - Math.ceil(renderWidth * scale), - Math.ceil((height + strokeWidth) * scale)); - label = context.canvas; - labelCache.set(key, label); - if (scale != 1) { - context.scale(scale, scale); - } - context.font = textState.font; - if (strokeKey) { - context.strokeStyle = strokeState.strokeStyle; - context.lineWidth = strokeWidth; - context.lineCap = strokeState.lineCap; - context.lineJoin = strokeState.lineJoin; - context.miterLimit = strokeState.miterLimit; - if (CANVAS_LINE_DASH && strokeState.lineDash.length) { - context.setLineDash(strokeState.lineDash); - context.lineDashOffset = strokeState.lineDashOffset; - } - } - if (fillKey) { - context.fillStyle = fillState.fillStyle; - } - context.textBaseline = 'middle'; - context.textAlign = 'center'; - const leftRight = (0.5 - align); - const x = align * label.width / scale + leftRight * strokeWidth; - let i; - if (strokeKey) { - for (i = 0; i < numLines; ++i) { - context.strokeText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight); - } - } - if (fillKey) { - for (i = 0; i < numLines; ++i) { - context.fillText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight); - } - } - } - return labelCache.get(key); -}; - - -/** - * @private - * @param {HTMLCanvasElement} label Label. - * @param {number} begin Begin. - * @param {number} end End. - */ -CanvasTextReplay.prototype.drawTextImage_ = function(label, begin, end) { - const textState = this.textState_; - const strokeState = this.textStrokeState_; - const pixelRatio = this.pixelRatio; - const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; - const baseline = TEXT_ALIGN[textState.textBaseline]; - const strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0; - - const anchorX = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth; - const anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth; - this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, - label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, - this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, - 1, true, label.width, - textState.padding == defaultPadding ? - defaultPadding : textState.padding.map(function(p) { - return p * pixelRatio; - }), - !!textState.backgroundFill, !!textState.backgroundStroke - ]); - this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, - label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, - this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, - 1 / pixelRatio, true, label.width, textState.padding, - !!textState.backgroundFill, !!textState.backgroundStroke - ]); -}; - - -/** - * @private - * @param {number} begin Begin. - * @param {number} end End. - * @param {module:ol/render/canvas~DeclutterGroup} declutterGroup Declutter group. - */ -CanvasTextReplay.prototype.drawChars_ = function(begin, end, declutterGroup) { - const strokeState = this.textStrokeState_; - const textState = this.textState_; - const fillState = this.textFillState_; - - const strokeKey = this.strokeKey_; - if (strokeState) { - if (!(strokeKey in this.strokeStates)) { - this.strokeStates[strokeKey] = /** @type {module:ol/render/canvas~StrokeState} */ ({ - strokeStyle: strokeState.strokeStyle, - lineCap: strokeState.lineCap, - lineDashOffset: strokeState.lineDashOffset, - lineWidth: strokeState.lineWidth, - lineJoin: strokeState.lineJoin, - miterLimit: strokeState.miterLimit, - lineDash: strokeState.lineDash - }); - } - } - const textKey = this.textKey_; - if (!(this.textKey_ in this.textStates)) { - this.textStates[this.textKey_] = /** @type {module:ol/render/canvas~TextState} */ ({ - font: textState.font, - textAlign: textState.textAlign || defaultTextAlign, - scale: textState.scale - }); - } - const fillKey = this.fillKey_; - if (fillState) { - if (!(fillKey in this.fillStates)) { - this.fillStates[fillKey] = /** @type {module:ol/render/canvas~FillState} */ ({ - fillStyle: fillState.fillStyle - }); - } - } - - const pixelRatio = this.pixelRatio; - const baseline = TEXT_ALIGN[textState.textBaseline]; - - const offsetY = this.textOffsetY_ * pixelRatio; - const text = this.text_; - const font = textState.font; - const textScale = textState.scale; - const strokeWidth = strokeState ? strokeState.lineWidth * textScale / 2 : 0; - let widths = this.widths_[font]; - if (!widths) { - this.widths_[font] = widths = {}; - } - this.instructions.push([CanvasInstruction.DRAW_CHARS, - begin, end, baseline, declutterGroup, - textState.overflow, fillKey, textState.maxAngle, - function(text) { - let width = widths[text]; - if (!width) { - width = widths[text] = measureTextWidth(font, text); - } - return width * textScale * pixelRatio; - }, - offsetY, strokeKey, strokeWidth * pixelRatio, text, textKey, 1 - ]); - this.hitDetectionInstructions.push([CanvasInstruction.DRAW_CHARS, - begin, end, baseline, declutterGroup, - textState.overflow, fillKey, textState.maxAngle, - function(text) { - let width = widths[text]; - if (!width) { - width = widths[text] = measureTextWidth(font, text); - } - return width * textScale; - }, - offsetY, strokeKey, strokeWidth, text, textKey, 1 / pixelRatio - ]); -}; - - -/** - * @inheritDoc - */ -CanvasTextReplay.prototype.setTextStyle = function(textStyle, declutterGroup) { - let textState, fillState, strokeState; - if (!textStyle) { - this.text_ = ''; - } else { - this.declutterGroup_ = /** @type {module:ol/render/canvas~DeclutterGroup} */ (declutterGroup); - - const textFillStyle = textStyle.getFill(); - if (!textFillStyle) { - fillState = this.textFillState_ = null; - } else { - fillState = this.textFillState_; - if (!fillState) { - fillState = this.textFillState_ = /** @type {module:ol/render/canvas~FillState} */ ({}); - } - fillState.fillStyle = asColorLike( - textFillStyle.getColor() || defaultFillStyle); - } - - const textStrokeStyle = textStyle.getStroke(); - if (!textStrokeStyle) { - strokeState = this.textStrokeState_ = null; - } else { - strokeState = this.textStrokeState_; - if (!strokeState) { - strokeState = this.textStrokeState_ = /** @type {module:ol/render/canvas~StrokeState} */ ({}); - } - const lineDash = textStrokeStyle.getLineDash(); - const lineDashOffset = textStrokeStyle.getLineDashOffset(); - const lineWidth = textStrokeStyle.getWidth(); - const miterLimit = textStrokeStyle.getMiterLimit(); - strokeState.lineCap = textStrokeStyle.getLineCap() || defaultLineCap; - strokeState.lineDash = lineDash ? lineDash.slice() : defaultLineDash; - strokeState.lineDashOffset = - lineDashOffset === undefined ? defaultLineDashOffset : lineDashOffset; - strokeState.lineJoin = textStrokeStyle.getLineJoin() || defaultLineJoin; - strokeState.lineWidth = - lineWidth === undefined ? defaultLineWidth : lineWidth; - strokeState.miterLimit = - miterLimit === undefined ? defaultMiterLimit : miterLimit; - strokeState.strokeStyle = asColorLike( - textStrokeStyle.getColor() || defaultStrokeStyle); - } - - textState = this.textState_; - const font = textStyle.getFont() || defaultFont; - checkFont(font); - const textScale = textStyle.getScale(); - textState.overflow = textStyle.getOverflow(); - textState.font = font; - textState.maxAngle = textStyle.getMaxAngle(); - textState.placement = textStyle.getPlacement(); - textState.textAlign = textStyle.getTextAlign(); - textState.textBaseline = textStyle.getTextBaseline() || defaultTextBaseline; - textState.backgroundFill = textStyle.getBackgroundFill(); - textState.backgroundStroke = textStyle.getBackgroundStroke(); - textState.padding = textStyle.getPadding() || defaultPadding; - textState.scale = textScale === undefined ? 1 : textScale; - - const textOffsetX = textStyle.getOffsetX(); - const textOffsetY = textStyle.getOffsetY(); - const textRotateWithView = textStyle.getRotateWithView(); - const textRotation = textStyle.getRotation(); - this.text_ = textStyle.getText() || ''; - this.textOffsetX_ = textOffsetX === undefined ? 0 : textOffsetX; - this.textOffsetY_ = textOffsetY === undefined ? 0 : textOffsetY; - this.textRotateWithView_ = textRotateWithView === undefined ? false : textRotateWithView; - this.textRotation_ = textRotation === undefined ? 0 : textRotation; - - this.strokeKey_ = strokeState ? - (typeof strokeState.strokeStyle == 'string' ? strokeState.strokeStyle : getUid(strokeState.strokeStyle)) + - strokeState.lineCap + strokeState.lineDashOffset + '|' + strokeState.lineWidth + - strokeState.lineJoin + strokeState.miterLimit + '[' + strokeState.lineDash.join() + ']' : - ''; - this.textKey_ = textState.font + textState.scale + (textState.textAlign || '?'); - this.fillKey_ = fillState ? - (typeof fillState.fillStyle == 'string' ? fillState.fillStyle : ('|' + getUid(fillState.fillStyle))) : - ''; - } -}; export default CanvasTextReplay; diff --git a/src/ol/render/webgl/CircleReplay.js b/src/ol/render/webgl/CircleReplay.js index 2be78ff8e4..6d98db4d2c 100644 --- a/src/ol/render/webgl/CircleReplay.js +++ b/src/ol/render/webgl/CircleReplay.js @@ -22,399 +22,389 @@ import WebGLBuffer from '../../webgl/Buffer.js'; * @param {module:ol/extent~Extent} maxExtent Max extent. * @struct */ -const WebGLCircleReplay = function(tolerance, maxExtent) { - WebGLReplay.call(this, tolerance, maxExtent); +class WebGLCircleReplay { + constructor(tolerance, maxExtent) { + WebGLReplay.call(this, tolerance, maxExtent); - /** - * @private - * @type {module:ol/render/webgl/circlereplay/defaultshader/Locations} - */ - this.defaultLocations_ = null; + /** + * @private + * @type {module:ol/render/webgl/circlereplay/defaultshader/Locations} + */ + this.defaultLocations_ = null; - /** - * @private - * @type {Array.|number>>} - */ - this.styles_ = []; + /** + * @private + * @type {Array.|number>>} + */ + this.styles_ = []; - /** - * @private - * @type {Array.} - */ - this.styleIndices_ = []; + /** + * @private + * @type {Array.} + */ + this.styleIndices_ = []; - /** - * @private - * @type {number} - */ - this.radius_ = 0; + /** + * @private + * @type {number} + */ + this.radius_ = 0; - /** - * @private - * @type {{fillColor: (Array.|null), - * strokeColor: (Array.|null), - * lineDash: Array., - * lineDashOffset: (number|undefined), - * lineWidth: (number|undefined), - * changed: boolean}|null} - */ - this.state_ = { - fillColor: null, - strokeColor: null, - lineDash: null, - lineDashOffset: undefined, - lineWidth: undefined, - changed: false - }; + /** + * @private + * @type {{fillColor: (Array.|null), + * strokeColor: (Array.|null), + * lineDash: Array., + * lineDashOffset: (number|undefined), + * lineWidth: (number|undefined), + * changed: boolean}|null} + */ + this.state_ = { + fillColor: null, + strokeColor: null, + lineDash: null, + lineDashOffset: undefined, + lineWidth: undefined, + changed: false + }; -}; - -inherits(WebGLCircleReplay, WebGLReplay); - - -/** - * @private - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - */ -WebGLCircleReplay.prototype.drawCoordinates_ = function( - flatCoordinates, offset, end, stride) { - let numVertices = this.vertices.length; - let numIndices = this.indices.length; - let n = numVertices / 4; - let i, ii; - for (i = offset, ii = end; i < ii; i += stride) { - this.vertices[numVertices++] = flatCoordinates[i]; - this.vertices[numVertices++] = flatCoordinates[i + 1]; - this.vertices[numVertices++] = 0; - this.vertices[numVertices++] = this.radius_; - - this.vertices[numVertices++] = flatCoordinates[i]; - this.vertices[numVertices++] = flatCoordinates[i + 1]; - this.vertices[numVertices++] = 1; - this.vertices[numVertices++] = this.radius_; - - this.vertices[numVertices++] = flatCoordinates[i]; - this.vertices[numVertices++] = flatCoordinates[i + 1]; - this.vertices[numVertices++] = 2; - this.vertices[numVertices++] = this.radius_; - - this.vertices[numVertices++] = flatCoordinates[i]; - this.vertices[numVertices++] = flatCoordinates[i + 1]; - this.vertices[numVertices++] = 3; - this.vertices[numVertices++] = this.radius_; - - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 1; - this.indices[numIndices++] = n + 2; - - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n + 3; - this.indices[numIndices++] = n; - - n += 4; } -}; + /** + * @private + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + */ + drawCoordinates_(flatCoordinates, offset, end, stride) { + let numVertices = this.vertices.length; + let numIndices = this.indices.length; + let n = numVertices / 4; + let i, ii; + for (i = offset, ii = end; i < ii; i += stride) { + this.vertices[numVertices++] = flatCoordinates[i]; + this.vertices[numVertices++] = flatCoordinates[i + 1]; + this.vertices[numVertices++] = 0; + this.vertices[numVertices++] = this.radius_; -/** - * @inheritDoc - */ -WebGLCircleReplay.prototype.drawCircle = function(circleGeometry, feature) { - const radius = circleGeometry.getRadius(); - const stride = circleGeometry.getStride(); - if (radius) { - this.startIndices.push(this.indices.length); - this.startIndicesFeature.push(feature); - if (this.state_.changed) { - this.styleIndices_.push(this.indices.length); - this.state_.changed = false; + this.vertices[numVertices++] = flatCoordinates[i]; + this.vertices[numVertices++] = flatCoordinates[i + 1]; + this.vertices[numVertices++] = 1; + this.vertices[numVertices++] = this.radius_; + + this.vertices[numVertices++] = flatCoordinates[i]; + this.vertices[numVertices++] = flatCoordinates[i + 1]; + this.vertices[numVertices++] = 2; + this.vertices[numVertices++] = this.radius_; + + this.vertices[numVertices++] = flatCoordinates[i]; + this.vertices[numVertices++] = flatCoordinates[i + 1]; + this.vertices[numVertices++] = 3; + this.vertices[numVertices++] = this.radius_; + + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 2; + + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n + 3; + this.indices[numIndices++] = n; + + n += 4; } + } - this.radius_ = radius; - let flatCoordinates = circleGeometry.getFlatCoordinates(); - flatCoordinates = translate(flatCoordinates, 0, 2, - stride, -this.origin[0], -this.origin[1]); - this.drawCoordinates_(flatCoordinates, 0, 2, stride); - } else { - if (this.state_.changed) { - this.styles_.pop(); - if (this.styles_.length) { - const lastState = this.styles_[this.styles_.length - 1]; - this.state_.fillColor = /** @type {Array.} */ (lastState[0]); - this.state_.strokeColor = /** @type {Array.} */ (lastState[1]); - this.state_.lineWidth = /** @type {number} */ (lastState[2]); + /** + * @inheritDoc + */ + drawCircle(circleGeometry, feature) { + const radius = circleGeometry.getRadius(); + const stride = circleGeometry.getStride(); + if (radius) { + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(this.indices.length); this.state_.changed = false; } + + this.radius_ = radius; + let flatCoordinates = circleGeometry.getFlatCoordinates(); + flatCoordinates = translate(flatCoordinates, 0, 2, + stride, -this.origin[0], -this.origin[1]); + this.drawCoordinates_(flatCoordinates, 0, 2, stride); + } else { + if (this.state_.changed) { + this.styles_.pop(); + if (this.styles_.length) { + const lastState = this.styles_[this.styles_.length - 1]; + this.state_.fillColor = /** @type {Array.} */ (lastState[0]); + this.state_.strokeColor = /** @type {Array.} */ (lastState[1]); + this.state_.lineWidth = /** @type {number} */ (lastState[2]); + this.state_.changed = false; + } + } } } -}; + /** + * @inheritDoc + **/ + finish(context) { + // create, bind, and populate the vertices buffer + this.verticesBuffer = new WebGLBuffer(this.vertices); -/** - * @inheritDoc - **/ -WebGLCircleReplay.prototype.finish = function(context) { - // create, bind, and populate the vertices buffer - this.verticesBuffer = new WebGLBuffer(this.vertices); + // create, bind, and populate the indices buffer + this.indicesBuffer = new WebGLBuffer(this.indices); - // create, bind, and populate the indices buffer - this.indicesBuffer = new WebGLBuffer(this.indices); + this.startIndices.push(this.indices.length); - this.startIndices.push(this.indices.length); + //Clean up, if there is nothing to draw + if (this.styleIndices_.length === 0 && this.styles_.length > 0) { + this.styles_ = []; + } - //Clean up, if there is nothing to draw - if (this.styleIndices_.length === 0 && this.styles_.length > 0) { - this.styles_ = []; + this.vertices = null; + this.indices = null; } - this.vertices = null; - this.indices = null; -}; - - -/** - * @inheritDoc - */ -WebGLCircleReplay.prototype.getDeleteResourcesFunction = function(context) { - // We only delete our stuff here. The shaders and the program may - // be used by other CircleReplay instances (for other layers). And - // they will be deleted when disposing of the module:ol/webgl/Context~WebGLContext - // object. - const verticesBuffer = this.verticesBuffer; - const indicesBuffer = this.indicesBuffer; - return function() { - context.deleteBuffer(verticesBuffer); - context.deleteBuffer(indicesBuffer); - }; -}; - - -/** - * @inheritDoc - */ -WebGLCircleReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { - // get the program - const program = context.getProgram(fragment, vertex); - - // get the locations - let locations; - if (!this.defaultLocations_) { - locations = new Locations(gl, program); - this.defaultLocations_ = locations; - } else { - locations = this.defaultLocations_; + /** + * @inheritDoc + */ + getDeleteResourcesFunction(context) { + // We only delete our stuff here. The shaders and the program may + // be used by other CircleReplay instances (for other layers). And + // they will be deleted when disposing of the module:ol/webgl/Context~WebGLContext + // object. + const verticesBuffer = this.verticesBuffer; + const indicesBuffer = this.indicesBuffer; + return function() { + context.deleteBuffer(verticesBuffer); + context.deleteBuffer(indicesBuffer); + }; } - context.useProgram(program); + /** + * @inheritDoc + */ + setUpProgram(gl, context, size, pixelRatio) { + // get the program + const program = context.getProgram(fragment, vertex); - // enable the vertex attrib arrays - gl.enableVertexAttribArray(locations.a_position); - gl.vertexAttribPointer(locations.a_position, 2, FLOAT, - false, 16, 0); + // get the locations + let locations; + if (!this.defaultLocations_) { + locations = new Locations(gl, program); + this.defaultLocations_ = locations; + } else { + locations = this.defaultLocations_; + } - gl.enableVertexAttribArray(locations.a_instruction); - gl.vertexAttribPointer(locations.a_instruction, 1, FLOAT, - false, 16, 8); + context.useProgram(program); - gl.enableVertexAttribArray(locations.a_radius); - gl.vertexAttribPointer(locations.a_radius, 1, FLOAT, - false, 16, 12); + // enable the vertex attrib arrays + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, FLOAT, + false, 16, 0); - // Enable renderer specific uniforms. - gl.uniform2fv(locations.u_size, size); - gl.uniform1f(locations.u_pixelRatio, pixelRatio); + gl.enableVertexAttribArray(locations.a_instruction); + gl.vertexAttribPointer(locations.a_instruction, 1, FLOAT, + false, 16, 8); - return locations; -}; + gl.enableVertexAttribArray(locations.a_radius); + gl.vertexAttribPointer(locations.a_radius, 1, FLOAT, + false, 16, 12); + // Enable renderer specific uniforms. + gl.uniform2fv(locations.u_size, size); + gl.uniform1f(locations.u_pixelRatio, pixelRatio); -/** - * @inheritDoc - */ -WebGLCircleReplay.prototype.shutDownProgram = function(gl, locations) { - gl.disableVertexAttribArray(locations.a_position); - gl.disableVertexAttribArray(locations.a_instruction); - gl.disableVertexAttribArray(locations.a_radius); -}; + return locations; + } + /** + * @inheritDoc + */ + shutDownProgram(gl, locations) { + gl.disableVertexAttribArray(locations.a_position); + gl.disableVertexAttribArray(locations.a_instruction); + gl.disableVertexAttribArray(locations.a_radius); + } -/** - * @inheritDoc - */ -WebGLCircleReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { - if (!isEmpty(skippedFeaturesHash)) { - this.drawReplaySkipping_(gl, context, skippedFeaturesHash); - } else { - //Draw by style groups to minimize drawElements() calls. - let i, start, end, nextStyle; - end = this.startIndices[this.startIndices.length - 1]; + /** + * @inheritDoc + */ + drawReplay(gl, context, skippedFeaturesHash, hitDetection) { + if (!isEmpty(skippedFeaturesHash)) { + this.drawReplaySkipping_(gl, context, skippedFeaturesHash); + } else { + //Draw by style groups to minimize drawElements() calls. + let i, start, end, nextStyle; + end = this.startIndices[this.startIndices.length - 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + start = this.styleIndices_[i]; + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, /** @type {Array.} */ (nextStyle[0])); + this.setStrokeStyle_(gl, /** @type {Array.} */ (nextStyle[1]), + /** @type {number} */ (nextStyle[2])); + this.drawElements(gl, context, start, end); + end = start; + } + } + } + + /** + * @inheritDoc + */ + drawHitDetectionReplayOneByOne(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) { + let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; + featureIndex = this.startIndices.length - 2; + end = this.startIndices[featureIndex + 1]; for (i = this.styleIndices_.length - 1; i >= 0; --i) { - start = this.styleIndices_[i]; nextStyle = this.styles_[i]; this.setFillStyle_(gl, /** @type {Array.} */ (nextStyle[0])); this.setStrokeStyle_(gl, /** @type {Array.} */ (nextStyle[1]), /** @type {number} */ (nextStyle[2])); - this.drawElements(gl, context, start, end); - end = start; - } - } -}; + groupStart = this.styleIndices_[i]; + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + start = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = getUid(feature).toString(); -/** - * @inheritDoc - */ -WebGLCircleReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, - featureCallback, opt_hitExtent) { - let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; - featureIndex = this.startIndices.length - 2; - end = this.startIndices[featureIndex + 1]; - for (i = this.styleIndices_.length - 1; i >= 0; --i) { - nextStyle = this.styles_[i]; - this.setFillStyle_(gl, /** @type {Array.} */ (nextStyle[0])); - this.setStrokeStyle_(gl, /** @type {Array.} */ (nextStyle[1]), - /** @type {number} */ (nextStyle[2])); - groupStart = this.styleIndices_[i]; - - while (featureIndex >= 0 && - this.startIndices[featureIndex] >= groupStart) { - start = this.startIndices[featureIndex]; - feature = this.startIndicesFeature[featureIndex]; - featureUid = getUid(feature).toString(); - - if (skippedFeaturesHash[featureUid] === undefined && - feature.getGeometry() && - (opt_hitExtent === undefined || intersects( - /** @type {Array} */ (opt_hitExtent), - feature.getGeometry().getExtent()))) { - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.drawElements(gl, context, start, end); - - const result = featureCallback(feature); - - if (result) { - return result; - } - - } - featureIndex--; - end = start; - } - } - return undefined; -}; - - -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {Object} skippedFeaturesHash Ids of features to skip. - */ -WebGLCircleReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) { - let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; - featureIndex = this.startIndices.length - 2; - end = start = this.startIndices[featureIndex + 1]; - for (i = this.styleIndices_.length - 1; i >= 0; --i) { - nextStyle = this.styles_[i]; - this.setFillStyle_(gl, /** @type {Array.} */ (nextStyle[0])); - this.setStrokeStyle_(gl, /** @type {Array.} */ (nextStyle[1]), - /** @type {number} */ (nextStyle[2])); - groupStart = this.styleIndices_[i]; - - while (featureIndex >= 0 && - this.startIndices[featureIndex] >= groupStart) { - featureStart = this.startIndices[featureIndex]; - feature = this.startIndicesFeature[featureIndex]; - featureUid = getUid(feature).toString(); - - if (skippedFeaturesHash[featureUid]) { - if (start !== end) { + if (skippedFeaturesHash[featureUid] === undefined && + feature.getGeometry() && + (opt_hitExtent === undefined || intersects( + /** @type {Array} */ (opt_hitExtent), + feature.getGeometry().getExtent()))) { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); this.drawElements(gl, context, start, end); + + const result = featureCallback(feature); + + if (result) { + return result; + } + } - end = featureStart; + featureIndex--; + end = start; } - featureIndex--; - start = featureStart; } - if (start !== end) { - this.drawElements(gl, context, start, end); - } - start = end = groupStart; + return undefined; } -}; + /** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {Object} skippedFeaturesHash Ids of features to skip. + */ + drawReplaySkipping_(gl, context, skippedFeaturesHash) { + let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; + featureIndex = this.startIndices.length - 2; + end = start = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, /** @type {Array.} */ (nextStyle[0])); + this.setStrokeStyle_(gl, /** @type {Array.} */ (nextStyle[1]), + /** @type {number} */ (nextStyle[2])); + groupStart = this.styleIndices_[i]; -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {Array.} color Color. - */ -WebGLCircleReplay.prototype.setFillStyle_ = function(gl, color) { - gl.uniform4fv(this.defaultLocations_.u_fillColor, color); -}; + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + featureStart = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = getUid(feature).toString(); + if (skippedFeaturesHash[featureUid]) { + if (start !== end) { + this.drawElements(gl, context, start, end); + } + end = featureStart; + } + featureIndex--; + start = featureStart; + } + if (start !== end) { + this.drawElements(gl, context, start, end); + } + start = end = groupStart; + } + } -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {Array.} color Color. - * @param {number} lineWidth Line width. - */ -WebGLCircleReplay.prototype.setStrokeStyle_ = function(gl, color, lineWidth) { - gl.uniform4fv(this.defaultLocations_.u_strokeColor, color); - gl.uniform1f(this.defaultLocations_.u_lineWidth, lineWidth); -}; + /** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {Array.} color Color. + */ + setFillStyle_(gl, color) { + gl.uniform4fv(this.defaultLocations_.u_fillColor, color); + } + /** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {Array.} color Color. + * @param {number} lineWidth Line width. + */ + setStrokeStyle_(gl, color, lineWidth) { + gl.uniform4fv(this.defaultLocations_.u_strokeColor, color); + gl.uniform1f(this.defaultLocations_.u_lineWidth, lineWidth); + } -/** - * @inheritDoc - */ -WebGLCircleReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { - let strokeStyleColor, strokeStyleWidth; - if (strokeStyle) { - const strokeStyleLineDash = strokeStyle.getLineDash(); - this.state_.lineDash = strokeStyleLineDash ? - strokeStyleLineDash : DEFAULT_LINEDASH; - const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); - this.state_.lineDashOffset = strokeStyleLineDashOffset ? - strokeStyleLineDashOffset : DEFAULT_LINEDASHOFFSET; - strokeStyleColor = strokeStyle.getColor(); - if (!(strokeStyleColor instanceof CanvasGradient) && - !(strokeStyleColor instanceof CanvasPattern)) { - strokeStyleColor = asArray(strokeStyleColor).map(function(c, i) { - return i != 3 ? c / 255 : c; - }) || DEFAULT_STROKESTYLE; + /** + * @inheritDoc + */ + setFillStrokeStyle(fillStyle, strokeStyle) { + let strokeStyleColor, strokeStyleWidth; + if (strokeStyle) { + const strokeStyleLineDash = strokeStyle.getLineDash(); + this.state_.lineDash = strokeStyleLineDash ? + strokeStyleLineDash : DEFAULT_LINEDASH; + const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); + this.state_.lineDashOffset = strokeStyleLineDashOffset ? + strokeStyleLineDashOffset : DEFAULT_LINEDASHOFFSET; + strokeStyleColor = strokeStyle.getColor(); + if (!(strokeStyleColor instanceof CanvasGradient) && + !(strokeStyleColor instanceof CanvasPattern)) { + strokeStyleColor = asArray(strokeStyleColor).map(function(c, i) { + return i != 3 ? c / 255 : c; + }) || DEFAULT_STROKESTYLE; + } else { + strokeStyleColor = DEFAULT_STROKESTYLE; + } + strokeStyleWidth = strokeStyle.getWidth(); + strokeStyleWidth = strokeStyleWidth !== undefined ? + strokeStyleWidth : DEFAULT_LINEWIDTH; } else { - strokeStyleColor = DEFAULT_STROKESTYLE; + strokeStyleColor = [0, 0, 0, 0]; + strokeStyleWidth = 0; + } + let fillStyleColor = fillStyle ? fillStyle.getColor() : [0, 0, 0, 0]; + if (!(fillStyleColor instanceof CanvasGradient) && + !(fillStyleColor instanceof CanvasPattern)) { + fillStyleColor = asArray(fillStyleColor).map(function(c, i) { + return i != 3 ? c / 255 : c; + }) || DEFAULT_FILLSTYLE; + } else { + fillStyleColor = DEFAULT_FILLSTYLE; + } + if (!this.state_.strokeColor || !equals(this.state_.strokeColor, strokeStyleColor) || + !this.state_.fillColor || !equals(this.state_.fillColor, fillStyleColor) || + this.state_.lineWidth !== strokeStyleWidth) { + this.state_.changed = true; + this.state_.fillColor = fillStyleColor; + this.state_.strokeColor = strokeStyleColor; + this.state_.lineWidth = strokeStyleWidth; + this.styles_.push([fillStyleColor, strokeStyleColor, strokeStyleWidth]); } - strokeStyleWidth = strokeStyle.getWidth(); - strokeStyleWidth = strokeStyleWidth !== undefined ? - strokeStyleWidth : DEFAULT_LINEWIDTH; - } else { - strokeStyleColor = [0, 0, 0, 0]; - strokeStyleWidth = 0; } - let fillStyleColor = fillStyle ? fillStyle.getColor() : [0, 0, 0, 0]; - if (!(fillStyleColor instanceof CanvasGradient) && - !(fillStyleColor instanceof CanvasPattern)) { - fillStyleColor = asArray(fillStyleColor).map(function(c, i) { - return i != 3 ? c / 255 : c; - }) || DEFAULT_FILLSTYLE; - } else { - fillStyleColor = DEFAULT_FILLSTYLE; - } - if (!this.state_.strokeColor || !equals(this.state_.strokeColor, strokeStyleColor) || - !this.state_.fillColor || !equals(this.state_.fillColor, fillStyleColor) || - this.state_.lineWidth !== strokeStyleWidth) { - this.state_.changed = true; - this.state_.fillColor = fillStyleColor; - this.state_.strokeColor = strokeStyleColor; - this.state_.lineWidth = strokeStyleWidth; - this.styles_.push([fillStyleColor, strokeStyleColor, strokeStyleWidth]); - } -}; +} + +inherits(WebGLCircleReplay, WebGLReplay); + + export default WebGLCircleReplay; diff --git a/src/ol/render/webgl/ImageReplay.js b/src/ol/render/webgl/ImageReplay.js index 111454549c..03f6350fdf 100644 --- a/src/ol/render/webgl/ImageReplay.js +++ b/src/ol/render/webgl/ImageReplay.js @@ -12,160 +12,158 @@ import WebGLBuffer from '../../webgl/Buffer.js'; * @param {module:ol/extent~Extent} maxExtent Max extent. * @struct */ -const WebGLImageReplay = function(tolerance, maxExtent) { - WebGLTextureReplay.call(this, tolerance, maxExtent); +class WebGLImageReplay { + constructor(tolerance, maxExtent) { + WebGLTextureReplay.call(this, tolerance, maxExtent); + + /** + * @type {Array.} + * @protected + */ + this.images_ = []; + + /** + * @type {Array.} + * @protected + */ + this.hitDetectionImages_ = []; + + /** + * @type {Array.} + * @private + */ + this.textures_ = []; + + /** + * @type {Array.} + * @private + */ + this.hitDetectionTextures_ = []; + + } /** - * @type {Array.} - * @protected + * @inheritDoc */ - this.images_ = []; + drawMultiPoint(multiPointGeometry, feature) { + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + const flatCoordinates = multiPointGeometry.getFlatCoordinates(); + const stride = multiPointGeometry.getStride(); + this.drawCoordinates( + flatCoordinates, 0, flatCoordinates.length, stride); + } /** - * @type {Array.} - * @protected + * @inheritDoc */ - this.hitDetectionImages_ = []; + drawPoint(pointGeometry, feature) { + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + const flatCoordinates = pointGeometry.getFlatCoordinates(); + const stride = pointGeometry.getStride(); + this.drawCoordinates( + flatCoordinates, 0, flatCoordinates.length, stride); + } /** - * @type {Array.} - * @private + * @inheritDoc */ - this.textures_ = []; + finish(context) { + const gl = context.getGL(); + + this.groupIndices.push(this.indices.length); + this.hitDetectionGroupIndices.push(this.indices.length); + + // create, bind, and populate the vertices buffer + this.verticesBuffer = new WebGLBuffer(this.vertices); + + const indices = this.indices; + + // create, bind, and populate the indices buffer + this.indicesBuffer = new WebGLBuffer(indices); + + // create textures + /** @type {Object.} */ + const texturePerImage = {}; + + this.createTextures(this.textures_, this.images_, texturePerImage, gl); + + this.createTextures(this.hitDetectionTextures_, this.hitDetectionImages_, + texturePerImage, gl); + + this.images_ = null; + this.hitDetectionImages_ = null; + WebGLTextureReplay.prototype.finish.call(this, context); + } /** - * @type {Array.} - * @private + * @inheritDoc */ - this.hitDetectionTextures_ = []; + setImageStyle(imageStyle) { + const anchor = imageStyle.getAnchor(); + const image = imageStyle.getImage(1); + const imageSize = imageStyle.getImageSize(); + const hitDetectionImage = imageStyle.getHitDetectionImage(1); + const opacity = imageStyle.getOpacity(); + const origin = imageStyle.getOrigin(); + const rotateWithView = imageStyle.getRotateWithView(); + const rotation = imageStyle.getRotation(); + const size = imageStyle.getSize(); + const scale = imageStyle.getScale(); -}; + let currentImage; + if (this.images_.length === 0) { + this.images_.push(image); + } else { + currentImage = this.images_[this.images_.length - 1]; + if (getUid(currentImage) != getUid(image)) { + this.groupIndices.push(this.indices.length); + this.images_.push(image); + } + } + + if (this.hitDetectionImages_.length === 0) { + this.hitDetectionImages_.push(hitDetectionImage); + } else { + currentImage = + this.hitDetectionImages_[this.hitDetectionImages_.length - 1]; + if (getUid(currentImage) != getUid(hitDetectionImage)) { + this.hitDetectionGroupIndices.push(this.indices.length); + this.hitDetectionImages_.push(hitDetectionImage); + } + } + + this.anchorX = anchor[0]; + this.anchorY = anchor[1]; + this.height = size[1]; + this.imageHeight = imageSize[1]; + this.imageWidth = imageSize[0]; + this.opacity = opacity; + this.originX = origin[0]; + this.originY = origin[1]; + this.rotation = rotation; + this.rotateWithView = rotateWithView; + this.scale = scale; + this.width = size[0]; + } + + /** + * @inheritDoc + */ + getTextures(opt_all) { + return opt_all ? this.textures_.concat(this.hitDetectionTextures_) : this.textures_; + } + + /** + * @inheritDoc + */ + getHitDetectionTextures() { + return this.hitDetectionTextures_; + } +} inherits(WebGLImageReplay, WebGLTextureReplay); -/** - * @inheritDoc - */ -WebGLImageReplay.prototype.drawMultiPoint = function(multiPointGeometry, feature) { - this.startIndices.push(this.indices.length); - this.startIndicesFeature.push(feature); - const flatCoordinates = multiPointGeometry.getFlatCoordinates(); - const stride = multiPointGeometry.getStride(); - this.drawCoordinates( - flatCoordinates, 0, flatCoordinates.length, stride); -}; - - -/** - * @inheritDoc - */ -WebGLImageReplay.prototype.drawPoint = function(pointGeometry, feature) { - this.startIndices.push(this.indices.length); - this.startIndicesFeature.push(feature); - const flatCoordinates = pointGeometry.getFlatCoordinates(); - const stride = pointGeometry.getStride(); - this.drawCoordinates( - flatCoordinates, 0, flatCoordinates.length, stride); -}; - - -/** - * @inheritDoc - */ -WebGLImageReplay.prototype.finish = function(context) { - const gl = context.getGL(); - - this.groupIndices.push(this.indices.length); - this.hitDetectionGroupIndices.push(this.indices.length); - - // create, bind, and populate the vertices buffer - this.verticesBuffer = new WebGLBuffer(this.vertices); - - const indices = this.indices; - - // create, bind, and populate the indices buffer - this.indicesBuffer = new WebGLBuffer(indices); - - // create textures - /** @type {Object.} */ - const texturePerImage = {}; - - this.createTextures(this.textures_, this.images_, texturePerImage, gl); - - this.createTextures(this.hitDetectionTextures_, this.hitDetectionImages_, - texturePerImage, gl); - - this.images_ = null; - this.hitDetectionImages_ = null; - WebGLTextureReplay.prototype.finish.call(this, context); -}; - - -/** - * @inheritDoc - */ -WebGLImageReplay.prototype.setImageStyle = function(imageStyle) { - const anchor = imageStyle.getAnchor(); - const image = imageStyle.getImage(1); - const imageSize = imageStyle.getImageSize(); - const hitDetectionImage = imageStyle.getHitDetectionImage(1); - const opacity = imageStyle.getOpacity(); - const origin = imageStyle.getOrigin(); - const rotateWithView = imageStyle.getRotateWithView(); - const rotation = imageStyle.getRotation(); - const size = imageStyle.getSize(); - const scale = imageStyle.getScale(); - - let currentImage; - if (this.images_.length === 0) { - this.images_.push(image); - } else { - currentImage = this.images_[this.images_.length - 1]; - if (getUid(currentImage) != getUid(image)) { - this.groupIndices.push(this.indices.length); - this.images_.push(image); - } - } - - if (this.hitDetectionImages_.length === 0) { - this.hitDetectionImages_.push(hitDetectionImage); - } else { - currentImage = - this.hitDetectionImages_[this.hitDetectionImages_.length - 1]; - if (getUid(currentImage) != getUid(hitDetectionImage)) { - this.hitDetectionGroupIndices.push(this.indices.length); - this.hitDetectionImages_.push(hitDetectionImage); - } - } - - this.anchorX = anchor[0]; - this.anchorY = anchor[1]; - this.height = size[1]; - this.imageHeight = imageSize[1]; - this.imageWidth = imageSize[0]; - this.opacity = opacity; - this.originX = origin[0]; - this.originY = origin[1]; - this.rotation = rotation; - this.rotateWithView = rotateWithView; - this.scale = scale; - this.width = size[0]; -}; - - -/** - * @inheritDoc - */ -WebGLImageReplay.prototype.getTextures = function(opt_all) { - return opt_all ? this.textures_.concat(this.hitDetectionTextures_) : this.textures_; -}; - - -/** - * @inheritDoc - */ -WebGLImageReplay.prototype.getHitDetectionTextures = function() { - return this.hitDetectionTextures_; -}; export default WebGLImageReplay; diff --git a/src/ol/render/webgl/Immediate.js b/src/ol/render/webgl/Immediate.js index c50614b697..ef281f2510 100644 --- a/src/ol/render/webgl/Immediate.js +++ b/src/ol/render/webgl/Immediate.js @@ -20,383 +20,372 @@ import WebGLReplayGroup from '../webgl/ReplayGroup.js'; * @param {number} pixelRatio Pixel ratio. * @struct */ -const WebGLImmediateRenderer = function(context, center, resolution, rotation, size, extent, pixelRatio) { - VectorContext.call(this); +class WebGLImmediateRenderer { + constructor(context, center, resolution, rotation, size, extent, pixelRatio) { + VectorContext.call(this); + + /** + * @private + */ + this.context_ = context; + + /** + * @private + */ + this.center_ = center; + + /** + * @private + */ + this.extent_ = extent; + + /** + * @private + */ + this.pixelRatio_ = pixelRatio; + + /** + * @private + */ + this.size_ = size; + + /** + * @private + */ + this.rotation_ = rotation; + + /** + * @private + */ + this.resolution_ = resolution; + + /** + * @private + * @type {module:ol/style/Image} + */ + this.imageStyle_ = null; + + /** + * @private + * @type {module:ol/style/Fill} + */ + this.fillStyle_ = null; + + /** + * @private + * @type {module:ol/style/Stroke} + */ + this.strokeStyle_ = null; + + /** + * @private + * @type {module:ol/style/Text} + */ + this.textStyle_ = null; + + } /** + * @param {module:ol/render/webgl/ReplayGroup} replayGroup Replay group. + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. * @private */ - this.context_ = context; + drawText_(replayGroup, geometry) { + const context = this.context_; + const replay = /** @type {module:ol/render/webgl/TextReplay} */ ( + replayGroup.getReplay(0, ReplayType.TEXT)); + replay.setTextStyle(this.textStyle_); + replay.drawText(geometry, null); + replay.finish(context); + // default colors + const opacity = 1; + const skippedFeatures = {}; + let featureCallback; + const oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); + } /** - * @private + * Set the rendering style. Note that since this is an immediate rendering API, + * any `zIndex` on the provided style will be ignored. + * + * @param {module:ol/style/Style} style The rendering style. + * @override + * @api */ - this.center_ = center; + setStyle(style) { + this.setFillStrokeStyle(style.getFill(), style.getStroke()); + this.setImageStyle(style.getImage()); + this.setTextStyle(style.getText()); + } /** - * @private + * Render a geometry into the canvas. Call + * {@link ol/render/webgl/Immediate#setStyle} first to set the rendering style. + * + * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry The geometry to render. + * @override + * @api */ - this.extent_ = extent; + drawGeometry(geometry) { + const type = geometry.getType(); + switch (type) { + case GeometryType.POINT: + this.drawPoint(/** @type {module:ol/geom/Point} */ (geometry), null); + break; + case GeometryType.LINE_STRING: + this.drawLineString(/** @type {module:ol/geom/LineString} */ (geometry), null); + break; + case GeometryType.POLYGON: + this.drawPolygon(/** @type {module:ol/geom/Polygon} */ (geometry), null); + break; + case GeometryType.MULTI_POINT: + this.drawMultiPoint(/** @type {module:ol/geom/MultiPoint} */ (geometry), null); + break; + case GeometryType.MULTI_LINE_STRING: + this.drawMultiLineString(/** @type {module:ol/geom/MultiLineString} */ (geometry), null); + break; + case GeometryType.MULTI_POLYGON: + this.drawMultiPolygon(/** @type {module:ol/geom/MultiPolygon} */ (geometry), null); + break; + case GeometryType.GEOMETRY_COLLECTION: + this.drawGeometryCollection(/** @type {module:ol/geom/GeometryCollection} */ (geometry), null); + break; + case GeometryType.CIRCLE: + this.drawCircle(/** @type {module:ol/geom/Circle} */ (geometry), null); + break; + default: + // pass + } + } /** - * @private + * @inheritDoc + * @api */ - this.pixelRatio_ = pixelRatio; + drawFeature(feature, style) { + const geometry = style.getGeometryFunction()(feature); + if (!geometry || !intersects(this.extent_, geometry.getExtent())) { + return; + } + this.setStyle(style); + this.drawGeometry(geometry); + } /** - * @private + * @inheritDoc */ - this.size_ = size; + drawGeometryCollection(geometry, data) { + const geometries = geometry.getGeometriesArray(); + let i, ii; + for (i = 0, ii = geometries.length; i < ii; ++i) { + this.drawGeometry(geometries[i]); + } + } /** - * @private + * @inheritDoc */ - this.rotation_ = rotation; + drawPoint(geometry, data) { + const context = this.context_; + const replayGroup = new WebGLReplayGroup(1, this.extent_); + const replay = /** @type {module:ol/render/webgl/ImageReplay} */ ( + replayGroup.getReplay(0, ReplayType.IMAGE)); + replay.setImageStyle(this.imageStyle_); + replay.drawPoint(geometry, data); + replay.finish(context); + // default colors + const opacity = 1; + const skippedFeatures = {}; + let featureCallback; + const oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); + + if (this.textStyle_) { + this.drawText_(replayGroup, geometry); + } + } /** - * @private + * @inheritDoc */ - this.resolution_ = resolution; + drawMultiPoint(geometry, data) { + const context = this.context_; + const replayGroup = new WebGLReplayGroup(1, this.extent_); + const replay = /** @type {module:ol/render/webgl/ImageReplay} */ ( + replayGroup.getReplay(0, ReplayType.IMAGE)); + replay.setImageStyle(this.imageStyle_); + replay.drawMultiPoint(geometry, data); + replay.finish(context); + const opacity = 1; + const skippedFeatures = {}; + let featureCallback; + const oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); + + if (this.textStyle_) { + this.drawText_(replayGroup, geometry); + } + } /** - * @private - * @type {module:ol/style/Image} + * @inheritDoc */ - this.imageStyle_ = null; + drawLineString(geometry, data) { + const context = this.context_; + const replayGroup = new WebGLReplayGroup(1, this.extent_); + const replay = /** @type {module:ol/render/webgl/LineStringReplay} */ ( + replayGroup.getReplay(0, ReplayType.LINE_STRING)); + replay.setFillStrokeStyle(null, this.strokeStyle_); + replay.drawLineString(geometry, data); + replay.finish(context); + const opacity = 1; + const skippedFeatures = {}; + let featureCallback; + const oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); + + if (this.textStyle_) { + this.drawText_(replayGroup, geometry); + } + } /** - * @private - * @type {module:ol/style/Fill} + * @inheritDoc */ - this.fillStyle_ = null; + drawMultiLineString(geometry, data) { + const context = this.context_; + const replayGroup = new WebGLReplayGroup(1, this.extent_); + const replay = /** @type {module:ol/render/webgl/LineStringReplay} */ ( + replayGroup.getReplay(0, ReplayType.LINE_STRING)); + replay.setFillStrokeStyle(null, this.strokeStyle_); + replay.drawMultiLineString(geometry, data); + replay.finish(context); + const opacity = 1; + const skippedFeatures = {}; + let featureCallback; + const oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); + + if (this.textStyle_) { + this.drawText_(replayGroup, geometry); + } + } /** - * @private - * @type {module:ol/style/Stroke} + * @inheritDoc */ - this.strokeStyle_ = null; + drawPolygon(geometry, data) { + const context = this.context_; + const replayGroup = new WebGLReplayGroup(1, this.extent_); + const replay = /** @type {module:ol/render/webgl/PolygonReplay} */ ( + replayGroup.getReplay(0, ReplayType.POLYGON)); + replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); + replay.drawPolygon(geometry, data); + replay.finish(context); + const opacity = 1; + const skippedFeatures = {}; + let featureCallback; + const oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); + + if (this.textStyle_) { + this.drawText_(replayGroup, geometry); + } + } /** - * @private - * @type {module:ol/style/Text} + * @inheritDoc */ - this.textStyle_ = null; + drawMultiPolygon(geometry, data) { + const context = this.context_; + const replayGroup = new WebGLReplayGroup(1, this.extent_); + const replay = /** @type {module:ol/render/webgl/PolygonReplay} */ ( + replayGroup.getReplay(0, ReplayType.POLYGON)); + replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); + replay.drawMultiPolygon(geometry, data); + replay.finish(context); + const opacity = 1; + const skippedFeatures = {}; + let featureCallback; + const oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); -}; + if (this.textStyle_) { + this.drawText_(replayGroup, geometry); + } + } + + /** + * @inheritDoc + */ + drawCircle(geometry, data) { + const context = this.context_; + const replayGroup = new WebGLReplayGroup(1, this.extent_); + const replay = /** @type {module:ol/render/webgl/CircleReplay} */ ( + replayGroup.getReplay(0, ReplayType.CIRCLE)); + replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); + replay.drawCircle(geometry, data); + replay.finish(context); + const opacity = 1; + const skippedFeatures = {}; + let featureCallback; + const oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); + + if (this.textStyle_) { + this.drawText_(replayGroup, geometry); + } + } + + /** + * @inheritDoc + */ + setImageStyle(imageStyle) { + this.imageStyle_ = imageStyle; + } + + /** + * @inheritDoc + */ + setFillStrokeStyle(fillStyle, strokeStyle) { + this.fillStyle_ = fillStyle; + this.strokeStyle_ = strokeStyle; + } + + /** + * @inheritDoc + */ + setTextStyle(textStyle) { + this.textStyle_ = textStyle; + } +} inherits(WebGLImmediateRenderer, VectorContext); -/** - * @param {module:ol/render/webgl/ReplayGroup} replayGroup Replay group. - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry Geometry. - * @private - */ -WebGLImmediateRenderer.prototype.drawText_ = function(replayGroup, geometry) { - const context = this.context_; - const replay = /** @type {module:ol/render/webgl/TextReplay} */ ( - replayGroup.getReplay(0, ReplayType.TEXT)); - replay.setTextStyle(this.textStyle_); - replay.drawText(geometry, null); - replay.finish(context); - // default colors - const opacity = 1; - const skippedFeatures = {}; - let featureCallback; - const oneByOne = false; - replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, - this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, - oneByOne); - replay.getDeleteResourcesFunction(context)(); -}; - - -/** - * Set the rendering style. Note that since this is an immediate rendering API, - * any `zIndex` on the provided style will be ignored. - * - * @param {module:ol/style/Style} style The rendering style. - * @override - * @api - */ -WebGLImmediateRenderer.prototype.setStyle = function(style) { - this.setFillStrokeStyle(style.getFill(), style.getStroke()); - this.setImageStyle(style.getImage()); - this.setTextStyle(style.getText()); -}; - - -/** - * Render a geometry into the canvas. Call - * {@link ol/render/webgl/Immediate#setStyle} first to set the rendering style. - * - * @param {module:ol/geom/Geometry|module:ol/render/Feature} geometry The geometry to render. - * @override - * @api - */ -WebGLImmediateRenderer.prototype.drawGeometry = function(geometry) { - const type = geometry.getType(); - switch (type) { - case GeometryType.POINT: - this.drawPoint(/** @type {module:ol/geom/Point} */ (geometry), null); - break; - case GeometryType.LINE_STRING: - this.drawLineString(/** @type {module:ol/geom/LineString} */ (geometry), null); - break; - case GeometryType.POLYGON: - this.drawPolygon(/** @type {module:ol/geom/Polygon} */ (geometry), null); - break; - case GeometryType.MULTI_POINT: - this.drawMultiPoint(/** @type {module:ol/geom/MultiPoint} */ (geometry), null); - break; - case GeometryType.MULTI_LINE_STRING: - this.drawMultiLineString(/** @type {module:ol/geom/MultiLineString} */ (geometry), null); - break; - case GeometryType.MULTI_POLYGON: - this.drawMultiPolygon(/** @type {module:ol/geom/MultiPolygon} */ (geometry), null); - break; - case GeometryType.GEOMETRY_COLLECTION: - this.drawGeometryCollection(/** @type {module:ol/geom/GeometryCollection} */ (geometry), null); - break; - case GeometryType.CIRCLE: - this.drawCircle(/** @type {module:ol/geom/Circle} */ (geometry), null); - break; - default: - // pass - } -}; - - -/** - * @inheritDoc - * @api - */ -WebGLImmediateRenderer.prototype.drawFeature = function(feature, style) { - const geometry = style.getGeometryFunction()(feature); - if (!geometry || !intersects(this.extent_, geometry.getExtent())) { - return; - } - this.setStyle(style); - this.drawGeometry(geometry); -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.drawGeometryCollection = function(geometry, data) { - const geometries = geometry.getGeometriesArray(); - let i, ii; - for (i = 0, ii = geometries.length; i < ii; ++i) { - this.drawGeometry(geometries[i]); - } -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.drawPoint = function(geometry, data) { - const context = this.context_; - const replayGroup = new WebGLReplayGroup(1, this.extent_); - const replay = /** @type {module:ol/render/webgl/ImageReplay} */ ( - replayGroup.getReplay(0, ReplayType.IMAGE)); - replay.setImageStyle(this.imageStyle_); - replay.drawPoint(geometry, data); - replay.finish(context); - // default colors - const opacity = 1; - const skippedFeatures = {}; - let featureCallback; - const oneByOne = false; - replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, - this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, - oneByOne); - replay.getDeleteResourcesFunction(context)(); - - if (this.textStyle_) { - this.drawText_(replayGroup, geometry); - } -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.drawMultiPoint = function(geometry, data) { - const context = this.context_; - const replayGroup = new WebGLReplayGroup(1, this.extent_); - const replay = /** @type {module:ol/render/webgl/ImageReplay} */ ( - replayGroup.getReplay(0, ReplayType.IMAGE)); - replay.setImageStyle(this.imageStyle_); - replay.drawMultiPoint(geometry, data); - replay.finish(context); - const opacity = 1; - const skippedFeatures = {}; - let featureCallback; - const oneByOne = false; - replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, - this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, - oneByOne); - replay.getDeleteResourcesFunction(context)(); - - if (this.textStyle_) { - this.drawText_(replayGroup, geometry); - } -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.drawLineString = function(geometry, data) { - const context = this.context_; - const replayGroup = new WebGLReplayGroup(1, this.extent_); - const replay = /** @type {module:ol/render/webgl/LineStringReplay} */ ( - replayGroup.getReplay(0, ReplayType.LINE_STRING)); - replay.setFillStrokeStyle(null, this.strokeStyle_); - replay.drawLineString(geometry, data); - replay.finish(context); - const opacity = 1; - const skippedFeatures = {}; - let featureCallback; - const oneByOne = false; - replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, - this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, - oneByOne); - replay.getDeleteResourcesFunction(context)(); - - if (this.textStyle_) { - this.drawText_(replayGroup, geometry); - } -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.drawMultiLineString = function(geometry, data) { - const context = this.context_; - const replayGroup = new WebGLReplayGroup(1, this.extent_); - const replay = /** @type {module:ol/render/webgl/LineStringReplay} */ ( - replayGroup.getReplay(0, ReplayType.LINE_STRING)); - replay.setFillStrokeStyle(null, this.strokeStyle_); - replay.drawMultiLineString(geometry, data); - replay.finish(context); - const opacity = 1; - const skippedFeatures = {}; - let featureCallback; - const oneByOne = false; - replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, - this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, - oneByOne); - replay.getDeleteResourcesFunction(context)(); - - if (this.textStyle_) { - this.drawText_(replayGroup, geometry); - } -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.drawPolygon = function(geometry, data) { - const context = this.context_; - const replayGroup = new WebGLReplayGroup(1, this.extent_); - const replay = /** @type {module:ol/render/webgl/PolygonReplay} */ ( - replayGroup.getReplay(0, ReplayType.POLYGON)); - replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); - replay.drawPolygon(geometry, data); - replay.finish(context); - const opacity = 1; - const skippedFeatures = {}; - let featureCallback; - const oneByOne = false; - replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, - this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, - oneByOne); - replay.getDeleteResourcesFunction(context)(); - - if (this.textStyle_) { - this.drawText_(replayGroup, geometry); - } -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.drawMultiPolygon = function(geometry, data) { - const context = this.context_; - const replayGroup = new WebGLReplayGroup(1, this.extent_); - const replay = /** @type {module:ol/render/webgl/PolygonReplay} */ ( - replayGroup.getReplay(0, ReplayType.POLYGON)); - replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); - replay.drawMultiPolygon(geometry, data); - replay.finish(context); - const opacity = 1; - const skippedFeatures = {}; - let featureCallback; - const oneByOne = false; - replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, - this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, - oneByOne); - replay.getDeleteResourcesFunction(context)(); - - if (this.textStyle_) { - this.drawText_(replayGroup, geometry); - } -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.drawCircle = function(geometry, data) { - const context = this.context_; - const replayGroup = new WebGLReplayGroup(1, this.extent_); - const replay = /** @type {module:ol/render/webgl/CircleReplay} */ ( - replayGroup.getReplay(0, ReplayType.CIRCLE)); - replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); - replay.drawCircle(geometry, data); - replay.finish(context); - const opacity = 1; - const skippedFeatures = {}; - let featureCallback; - const oneByOne = false; - replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, - this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, - oneByOne); - replay.getDeleteResourcesFunction(context)(); - - if (this.textStyle_) { - this.drawText_(replayGroup, geometry); - } -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.setImageStyle = function(imageStyle) { - this.imageStyle_ = imageStyle; -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { - this.fillStyle_ = fillStyle; - this.strokeStyle_ = strokeStyle; -}; - - -/** - * @inheritDoc - */ -WebGLImmediateRenderer.prototype.setTextStyle = function(textStyle) { - this.textStyle_ = textStyle; -}; export default WebGLImmediateRenderer; diff --git a/src/ol/render/webgl/LineStringReplay.js b/src/ol/render/webgl/LineStringReplay.js index c5932a5baf..db18c94472 100644 --- a/src/ol/render/webgl/LineStringReplay.js +++ b/src/ol/render/webgl/LineStringReplay.js @@ -42,645 +42,631 @@ const Instruction = { * @param {module:ol/extent~Extent} maxExtent Max extent. * @struct */ -const WebGLLineStringReplay = function(tolerance, maxExtent) { - WebGLReplay.call(this, tolerance, maxExtent); +class WebGLLineStringReplay { + constructor(tolerance, maxExtent) { + WebGLReplay.call(this, tolerance, maxExtent); + + /** + * @private + * @type {module:ol/render/webgl/linestringreplay/defaultshader/Locations} + */ + this.defaultLocations_ = null; + + /** + * @private + * @type {Array.>} + */ + this.styles_ = []; + + /** + * @private + * @type {Array.} + */ + this.styleIndices_ = []; + + /** + * @private + * @type {{strokeColor: (Array.|null), + * lineCap: (string|undefined), + * lineDash: Array., + * lineDashOffset: (number|undefined), + * lineJoin: (string|undefined), + * lineWidth: (number|undefined), + * miterLimit: (number|undefined), + * changed: boolean}|null} + */ + this.state_ = { + strokeColor: null, + lineCap: undefined, + lineDash: null, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: undefined, + miterLimit: undefined, + changed: false + }; + + } /** + * Draw one segment. * @private - * @type {module:ol/render/webgl/linestringreplay/defaultshader/Locations} + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. */ - this.defaultLocations_ = null; + drawCoordinates_(flatCoordinates, offset, end, stride) { - /** - * @private - * @type {Array.>} - */ - this.styles_ = []; + let i, ii; + let numVertices = this.vertices.length; + let numIndices = this.indices.length; + //To save a vertex, the direction of a point is a product of the sign (1 or -1), a prime from + //Instruction, and a rounding factor (1 or 2). If the product is even, + //we round it. If it is odd, we don't. + const lineJoin = this.state_.lineJoin === 'bevel' ? 0 : + this.state_.lineJoin === 'miter' ? 1 : 2; + const lineCap = this.state_.lineCap === 'butt' ? 0 : + this.state_.lineCap === 'square' ? 1 : 2; + const closed = lineStringIsClosed(flatCoordinates, offset, end, stride); + let startCoords, sign, n; + let lastIndex = numIndices; + let lastSign = 1; + //We need the adjacent vertices to define normals in joins. p0 = last, p1 = current, p2 = next. + let p0, p1, p2; - /** - * @private - * @type {Array.} - */ - this.styleIndices_ = []; + for (i = offset, ii = end; i < ii; i += stride) { - /** - * @private - * @type {{strokeColor: (Array.|null), - * lineCap: (string|undefined), - * lineDash: Array., - * lineDashOffset: (number|undefined), - * lineJoin: (string|undefined), - * lineWidth: (number|undefined), - * miterLimit: (number|undefined), - * changed: boolean}|null} - */ - this.state_ = { - strokeColor: null, - lineCap: undefined, - lineDash: null, - lineDashOffset: undefined, - lineJoin: undefined, - lineWidth: undefined, - miterLimit: undefined, - changed: false - }; - -}; - -inherits(WebGLLineStringReplay, WebGLReplay); - - -/** - * Draw one segment. - * @private - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - */ -WebGLLineStringReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) { - - let i, ii; - let numVertices = this.vertices.length; - let numIndices = this.indices.length; - //To save a vertex, the direction of a point is a product of the sign (1 or -1), a prime from - //Instruction, and a rounding factor (1 or 2). If the product is even, - //we round it. If it is odd, we don't. - const lineJoin = this.state_.lineJoin === 'bevel' ? 0 : - this.state_.lineJoin === 'miter' ? 1 : 2; - const lineCap = this.state_.lineCap === 'butt' ? 0 : - this.state_.lineCap === 'square' ? 1 : 2; - const closed = lineStringIsClosed(flatCoordinates, offset, end, stride); - let startCoords, sign, n; - let lastIndex = numIndices; - let lastSign = 1; - //We need the adjacent vertices to define normals in joins. p0 = last, p1 = current, p2 = next. - let p0, p1, p2; - - for (i = offset, ii = end; i < ii; i += stride) { - - n = numVertices / 7; - - p0 = p1; - p1 = p2 || [flatCoordinates[i], flatCoordinates[i + 1]]; - //First vertex. - if (i === offset) { - p2 = [flatCoordinates[i + stride], flatCoordinates[i + stride + 1]]; - if (end - offset === stride * 2 && equals(p1, p2)) { - break; - } - if (closed) { - //A closed line! Complete the circle. - p0 = [flatCoordinates[end - stride * 2], - flatCoordinates[end - stride * 2 + 1]]; - - startCoords = p2; - } else { - //Add the first two/four vertices. - - if (lineCap) { - numVertices = this.addVertices_([0, 0], p1, p2, - lastSign * Instruction.BEGIN_LINE_CAP * lineCap, numVertices); - - numVertices = this.addVertices_([0, 0], p1, p2, - -lastSign * Instruction.BEGIN_LINE_CAP * lineCap, numVertices); - - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 1; - - this.indices[numIndices++] = n + 1; - this.indices[numIndices++] = n + 3; - this.indices[numIndices++] = n + 2; + n = numVertices / 7; + p0 = p1; + p1 = p2 || [flatCoordinates[i], flatCoordinates[i + 1]]; + //First vertex. + if (i === offset) { + p2 = [flatCoordinates[i + stride], flatCoordinates[i + stride + 1]]; + if (end - offset === stride * 2 && equals(p1, p2)) { + break; } + if (closed) { + //A closed line! Complete the circle. + p0 = [flatCoordinates[end - stride * 2], + flatCoordinates[end - stride * 2 + 1]]; - numVertices = this.addVertices_([0, 0], p1, p2, - lastSign * Instruction.BEGIN_LINE * (lineCap || 1), numVertices); + startCoords = p2; + } else { + //Add the first two/four vertices. - numVertices = this.addVertices_([0, 0], p1, p2, - -lastSign * Instruction.BEGIN_LINE * (lineCap || 1), numVertices); + if (lineCap) { + numVertices = this.addVertices_([0, 0], p1, p2, + lastSign * Instruction.BEGIN_LINE_CAP * lineCap, numVertices); - lastIndex = numVertices / 7 - 1; + numVertices = this.addVertices_([0, 0], p1, p2, + -lastSign * Instruction.BEGIN_LINE_CAP * lineCap, numVertices); - continue; - } - } else if (i === end - stride) { - //Last vertex. - if (closed) { - //Same as the first vertex. - p2 = startCoords; - break; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 3; + this.indices[numIndices++] = n + 2; + + } + + numVertices = this.addVertices_([0, 0], p1, p2, + lastSign * Instruction.BEGIN_LINE * (lineCap || 1), numVertices); + + numVertices = this.addVertices_([0, 0], p1, p2, + -lastSign * Instruction.BEGIN_LINE * (lineCap || 1), numVertices); + + lastIndex = numVertices / 7 - 1; + + continue; + } + } else if (i === end - stride) { + //Last vertex. + if (closed) { + //Same as the first vertex. + p2 = startCoords; + break; + } else { + p0 = p0 || [0, 0]; + + numVertices = this.addVertices_(p0, p1, [0, 0], + lastSign * Instruction.END_LINE * (lineCap || 1), numVertices); + + numVertices = this.addVertices_(p0, p1, [0, 0], + -lastSign * Instruction.END_LINE * (lineCap || 1), numVertices); + + this.indices[numIndices++] = n; + this.indices[numIndices++] = lastIndex - 1; + this.indices[numIndices++] = lastIndex; + + this.indices[numIndices++] = lastIndex; + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n; + + if (lineCap) { + numVertices = this.addVertices_(p0, p1, [0, 0], + lastSign * Instruction.END_LINE_CAP * lineCap, numVertices); + + numVertices = this.addVertices_(p0, p1, [0, 0], + -lastSign * Instruction.END_LINE_CAP * lineCap, numVertices); + + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 3; + this.indices[numIndices++] = n + 2; + + } + + break; + } } else { - p0 = p0 || [0, 0]; + p2 = [flatCoordinates[i + stride], flatCoordinates[i + stride + 1]]; + } - numVertices = this.addVertices_(p0, p1, [0, 0], - lastSign * Instruction.END_LINE * (lineCap || 1), numVertices); + // We group CW and straight lines, thus the not so inituitive CCW checking function. + sign = triangleIsCounterClockwise(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]) + ? -1 : 1; - numVertices = this.addVertices_(p0, p1, [0, 0], - -lastSign * Instruction.END_LINE * (lineCap || 1), numVertices); + numVertices = this.addVertices_(p0, p1, p2, + sign * Instruction.BEVEL_FIRST * (lineJoin || 1), numVertices); + numVertices = this.addVertices_(p0, p1, p2, + sign * Instruction.BEVEL_SECOND * (lineJoin || 1), numVertices); + + numVertices = this.addVertices_(p0, p1, p2, + -sign * Instruction.MITER_BOTTOM * (lineJoin || 1), numVertices); + + if (i > offset) { this.indices[numIndices++] = n; this.indices[numIndices++] = lastIndex - 1; this.indices[numIndices++] = lastIndex; - this.indices[numIndices++] = lastIndex; - this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = lastSign * sign > 0 ? lastIndex : lastIndex - 1; + } + + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n + 1; + + lastIndex = n + 2; + lastSign = sign; + + //Add miter + if (lineJoin) { + numVertices = this.addVertices_(p0, p1, p2, + sign * Instruction.MITER_TOP * lineJoin, numVertices); + + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 3; this.indices[numIndices++] = n; - - if (lineCap) { - numVertices = this.addVertices_(p0, p1, [0, 0], - lastSign * Instruction.END_LINE_CAP * lineCap, numVertices); - - numVertices = this.addVertices_(p0, p1, [0, 0], - -lastSign * Instruction.END_LINE_CAP * lineCap, numVertices); - - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 1; - - this.indices[numIndices++] = n + 1; - this.indices[numIndices++] = n + 3; - this.indices[numIndices++] = n + 2; - - } - - break; } - } else { - p2 = [flatCoordinates[i + stride], flatCoordinates[i + stride + 1]]; } - // We group CW and straight lines, thus the not so inituitive CCW checking function. - sign = triangleIsCounterClockwise(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]) - ? -1 : 1; + if (closed) { + n = n || numVertices / 7; + sign = linearRingIsClockwise([p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]], 0, 6, 2) + ? 1 : -1; - numVertices = this.addVertices_(p0, p1, p2, - sign * Instruction.BEVEL_FIRST * (lineJoin || 1), numVertices); + numVertices = this.addVertices_(p0, p1, p2, + sign * Instruction.BEVEL_FIRST * (lineJoin || 1), numVertices); - numVertices = this.addVertices_(p0, p1, p2, - sign * Instruction.BEVEL_SECOND * (lineJoin || 1), numVertices); + numVertices = this.addVertices_(p0, p1, p2, + -sign * Instruction.MITER_BOTTOM * (lineJoin || 1), numVertices); - numVertices = this.addVertices_(p0, p1, p2, - -sign * Instruction.MITER_BOTTOM * (lineJoin || 1), numVertices); - - if (i > offset) { this.indices[numIndices++] = n; this.indices[numIndices++] = lastIndex - 1; this.indices[numIndices++] = lastIndex; - this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n + 1; this.indices[numIndices++] = n; this.indices[numIndices++] = lastSign * sign > 0 ? lastIndex : lastIndex - 1; } + } - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n + 1; + /** + * @param {Array.} p0 Last coordinates. + * @param {Array.} p1 Current coordinates. + * @param {Array.} p2 Next coordinates. + * @param {number} product Sign, instruction, and rounding product. + * @param {number} numVertices Vertex counter. + * @return {number} Vertex counter. + * @private + */ + addVertices_(p0, p1, p2, product, numVertices) { + this.vertices[numVertices++] = p0[0]; + this.vertices[numVertices++] = p0[1]; + this.vertices[numVertices++] = p1[0]; + this.vertices[numVertices++] = p1[1]; + this.vertices[numVertices++] = p2[0]; + this.vertices[numVertices++] = p2[1]; + this.vertices[numVertices++] = product; - lastIndex = n + 2; - lastSign = sign; + return numVertices; + } - //Add miter - if (lineJoin) { - numVertices = this.addVertices_(p0, p1, p2, - sign * Instruction.MITER_TOP * lineJoin, numVertices); - - this.indices[numIndices++] = n + 1; - this.indices[numIndices++] = n + 3; - this.indices[numIndices++] = n; + /** + * Check if the linestring can be drawn (i. e. valid). + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @return {boolean} The linestring can be drawn. + * @private + */ + isValid_(flatCoordinates, offset, end, stride) { + const range = end - offset; + if (range < stride * 2) { + return false; + } else if (range === stride * 2) { + const firstP = [flatCoordinates[offset], flatCoordinates[offset + 1]]; + const lastP = [flatCoordinates[offset + stride], flatCoordinates[offset + stride + 1]]; + return !equals(firstP, lastP); } + + return true; } - if (closed) { - n = n || numVertices / 7; - sign = linearRingIsClockwise([p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]], 0, 6, 2) - ? 1 : -1; - - numVertices = this.addVertices_(p0, p1, p2, - sign * Instruction.BEVEL_FIRST * (lineJoin || 1), numVertices); - - numVertices = this.addVertices_(p0, p1, p2, - -sign * Instruction.MITER_BOTTOM * (lineJoin || 1), numVertices); - - this.indices[numIndices++] = n; - this.indices[numIndices++] = lastIndex - 1; - this.indices[numIndices++] = lastIndex; - - this.indices[numIndices++] = n + 1; - this.indices[numIndices++] = n; - this.indices[numIndices++] = lastSign * sign > 0 ? lastIndex : lastIndex - 1; - } -}; - -/** - * @param {Array.} p0 Last coordinates. - * @param {Array.} p1 Current coordinates. - * @param {Array.} p2 Next coordinates. - * @param {number} product Sign, instruction, and rounding product. - * @param {number} numVertices Vertex counter. - * @return {number} Vertex counter. - * @private - */ -WebGLLineStringReplay.prototype.addVertices_ = function(p0, p1, p2, product, numVertices) { - this.vertices[numVertices++] = p0[0]; - this.vertices[numVertices++] = p0[1]; - this.vertices[numVertices++] = p1[0]; - this.vertices[numVertices++] = p1[1]; - this.vertices[numVertices++] = p2[0]; - this.vertices[numVertices++] = p2[1]; - this.vertices[numVertices++] = product; - - return numVertices; -}; - -/** - * Check if the linestring can be drawn (i. e. valid). - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @return {boolean} The linestring can be drawn. - * @private - */ -WebGLLineStringReplay.prototype.isValid_ = function(flatCoordinates, offset, end, stride) { - const range = end - offset; - if (range < stride * 2) { - return false; - } else if (range === stride * 2) { - const firstP = [flatCoordinates[offset], flatCoordinates[offset + 1]]; - const lastP = [flatCoordinates[offset + stride], flatCoordinates[offset + stride + 1]]; - return !equals(firstP, lastP); - } - - return true; -}; - - -/** - * @inheritDoc - */ -WebGLLineStringReplay.prototype.drawLineString = function(lineStringGeometry, feature) { - let flatCoordinates = lineStringGeometry.getFlatCoordinates(); - const stride = lineStringGeometry.getStride(); - if (this.isValid_(flatCoordinates, 0, flatCoordinates.length, stride)) { - flatCoordinates = translate(flatCoordinates, 0, flatCoordinates.length, - stride, -this.origin[0], -this.origin[1]); - if (this.state_.changed) { - this.styleIndices_.push(this.indices.length); - this.state_.changed = false; - } - this.startIndices.push(this.indices.length); - this.startIndicesFeature.push(feature); - this.drawCoordinates_( - flatCoordinates, 0, flatCoordinates.length, stride); - } -}; - - -/** - * @inheritDoc - */ -WebGLLineStringReplay.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) { - const indexCount = this.indices.length; - const ends = multiLineStringGeometry.getEnds(); - ends.unshift(0); - const flatCoordinates = multiLineStringGeometry.getFlatCoordinates(); - const stride = multiLineStringGeometry.getStride(); - let i, ii; - if (ends.length > 1) { - for (i = 1, ii = ends.length; i < ii; ++i) { - if (this.isValid_(flatCoordinates, ends[i - 1], ends[i], stride)) { - const lineString = translate(flatCoordinates, ends[i - 1], ends[i], - stride, -this.origin[0], -this.origin[1]); - this.drawCoordinates_( - lineString, 0, lineString.length, stride); + /** + * @inheritDoc + */ + drawLineString(lineStringGeometry, feature) { + let flatCoordinates = lineStringGeometry.getFlatCoordinates(); + const stride = lineStringGeometry.getStride(); + if (this.isValid_(flatCoordinates, 0, flatCoordinates.length, stride)) { + flatCoordinates = translate(flatCoordinates, 0, flatCoordinates.length, + stride, -this.origin[0], -this.origin[1]); + if (this.state_.changed) { + this.styleIndices_.push(this.indices.length); + this.state_.changed = false; } + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); } } - if (this.indices.length > indexCount) { - this.startIndices.push(indexCount); - this.startIndicesFeature.push(feature); - if (this.state_.changed) { - this.styleIndices_.push(indexCount); - this.state_.changed = false; - } - } -}; - -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {Array.>} holeFlatCoordinates Hole flat coordinates. - * @param {number} stride Stride. - */ -WebGLLineStringReplay.prototype.drawPolygonCoordinates = function( - flatCoordinates, holeFlatCoordinates, stride) { - if (!lineStringIsClosed(flatCoordinates, 0, flatCoordinates.length, stride)) { - flatCoordinates.push(flatCoordinates[0]); - flatCoordinates.push(flatCoordinates[1]); - } - this.drawCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride); - if (holeFlatCoordinates.length) { + /** + * @inheritDoc + */ + drawMultiLineString(multiLineStringGeometry, feature) { + const indexCount = this.indices.length; + const ends = multiLineStringGeometry.getEnds(); + ends.unshift(0); + const flatCoordinates = multiLineStringGeometry.getFlatCoordinates(); + const stride = multiLineStringGeometry.getStride(); let i, ii; - for (i = 0, ii = holeFlatCoordinates.length; i < ii; ++i) { - if (!lineStringIsClosed(holeFlatCoordinates[i], 0, holeFlatCoordinates[i].length, stride)) { - holeFlatCoordinates[i].push(holeFlatCoordinates[i][0]); - holeFlatCoordinates[i].push(holeFlatCoordinates[i][1]); + if (ends.length > 1) { + for (i = 1, ii = ends.length; i < ii; ++i) { + if (this.isValid_(flatCoordinates, ends[i - 1], ends[i], stride)) { + const lineString = translate(flatCoordinates, ends[i - 1], ends[i], + stride, -this.origin[0], -this.origin[1]); + this.drawCoordinates_( + lineString, 0, lineString.length, stride); + } + } + } + if (this.indices.length > indexCount) { + this.startIndices.push(indexCount); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(indexCount); + this.state_.changed = false; } - this.drawCoordinates_(holeFlatCoordinates[i], 0, - holeFlatCoordinates[i].length, stride); } } -}; - -/** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @param {number=} opt_index Index count. - */ -WebGLLineStringReplay.prototype.setPolygonStyle = function(feature, opt_index) { - const index = opt_index === undefined ? this.indices.length : opt_index; - this.startIndices.push(index); - this.startIndicesFeature.push(feature); - if (this.state_.changed) { - this.styleIndices_.push(index); - this.state_.changed = false; - } -}; - - -/** - * @return {number} Current index. - */ -WebGLLineStringReplay.prototype.getCurrentIndex = function() { - return this.indices.length; -}; - - -/** - * @inheritDoc - **/ -WebGLLineStringReplay.prototype.finish = function(context) { - // create, bind, and populate the vertices buffer - this.verticesBuffer = new WebGLBuffer(this.vertices); - - // create, bind, and populate the indices buffer - this.indicesBuffer = new WebGLBuffer(this.indices); - - this.startIndices.push(this.indices.length); - - //Clean up, if there is nothing to draw - if (this.styleIndices_.length === 0 && this.styles_.length > 0) { - this.styles_ = []; + /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {Array.>} holeFlatCoordinates Hole flat coordinates. + * @param {number} stride Stride. + */ + drawPolygonCoordinates(flatCoordinates, holeFlatCoordinates, stride) { + if (!lineStringIsClosed(flatCoordinates, 0, flatCoordinates.length, stride)) { + flatCoordinates.push(flatCoordinates[0]); + flatCoordinates.push(flatCoordinates[1]); + } + this.drawCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride); + if (holeFlatCoordinates.length) { + let i, ii; + for (i = 0, ii = holeFlatCoordinates.length; i < ii; ++i) { + if (!lineStringIsClosed(holeFlatCoordinates[i], 0, holeFlatCoordinates[i].length, stride)) { + holeFlatCoordinates[i].push(holeFlatCoordinates[i][0]); + holeFlatCoordinates[i].push(holeFlatCoordinates[i][1]); + } + this.drawCoordinates_(holeFlatCoordinates[i], 0, + holeFlatCoordinates[i].length, stride); + } + } } - this.vertices = null; - this.indices = null; -}; - - -/** - * @inheritDoc - */ -WebGLLineStringReplay.prototype.getDeleteResourcesFunction = function(context) { - const verticesBuffer = this.verticesBuffer; - const indicesBuffer = this.indicesBuffer; - return function() { - context.deleteBuffer(verticesBuffer); - context.deleteBuffer(indicesBuffer); - }; -}; - - -/** - * @inheritDoc - */ -WebGLLineStringReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { - // get the program - const program = context.getProgram(fragment, vertex); - - // get the locations - let locations; - if (!this.defaultLocations_) { - locations = new Locations(gl, program); - this.defaultLocations_ = locations; - } else { - locations = this.defaultLocations_; + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @param {number=} opt_index Index count. + */ + setPolygonStyle(feature, opt_index) { + const index = opt_index === undefined ? this.indices.length : opt_index; + this.startIndices.push(index); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(index); + this.state_.changed = false; + } } - context.useProgram(program); - - // enable the vertex attrib arrays - gl.enableVertexAttribArray(locations.a_lastPos); - gl.vertexAttribPointer(locations.a_lastPos, 2, FLOAT, - false, 28, 0); - - gl.enableVertexAttribArray(locations.a_position); - gl.vertexAttribPointer(locations.a_position, 2, FLOAT, - false, 28, 8); - - gl.enableVertexAttribArray(locations.a_nextPos); - gl.vertexAttribPointer(locations.a_nextPos, 2, FLOAT, - false, 28, 16); - - gl.enableVertexAttribArray(locations.a_direction); - gl.vertexAttribPointer(locations.a_direction, 1, FLOAT, - false, 28, 24); - - // Enable renderer specific uniforms. - gl.uniform2fv(locations.u_size, size); - gl.uniform1f(locations.u_pixelRatio, pixelRatio); - - return locations; -}; - - -/** - * @inheritDoc - */ -WebGLLineStringReplay.prototype.shutDownProgram = function(gl, locations) { - gl.disableVertexAttribArray(locations.a_lastPos); - gl.disableVertexAttribArray(locations.a_position); - gl.disableVertexAttribArray(locations.a_nextPos); - gl.disableVertexAttribArray(locations.a_direction); -}; - - -/** - * @inheritDoc - */ -WebGLLineStringReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { - //Save GL parameters. - const tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC)); - const tmpDepthMask = /** @type {boolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK)); - - if (!hitDetection) { - gl.enable(gl.DEPTH_TEST); - gl.depthMask(true); - gl.depthFunc(gl.NOTEQUAL); + /** + * @return {number} Current index. + */ + getCurrentIndex() { + return this.indices.length; } - if (!isEmpty(skippedFeaturesHash)) { - this.drawReplaySkipping_(gl, context, skippedFeaturesHash); - } else { - //Draw by style groups to minimize drawElements() calls. - let i, start, end, nextStyle; - end = this.startIndices[this.startIndices.length - 1]; + /** + * @inheritDoc + **/ + finish(context) { + // create, bind, and populate the vertices buffer + this.verticesBuffer = new WebGLBuffer(this.vertices); + + // create, bind, and populate the indices buffer + this.indicesBuffer = new WebGLBuffer(this.indices); + + this.startIndices.push(this.indices.length); + + //Clean up, if there is nothing to draw + if (this.styleIndices_.length === 0 && this.styles_.length > 0) { + this.styles_ = []; + } + + this.vertices = null; + this.indices = null; + } + + /** + * @inheritDoc + */ + getDeleteResourcesFunction(context) { + const verticesBuffer = this.verticesBuffer; + const indicesBuffer = this.indicesBuffer; + return function() { + context.deleteBuffer(verticesBuffer); + context.deleteBuffer(indicesBuffer); + }; + } + + /** + * @inheritDoc + */ + setUpProgram(gl, context, size, pixelRatio) { + // get the program + const program = context.getProgram(fragment, vertex); + + // get the locations + let locations; + if (!this.defaultLocations_) { + locations = new Locations(gl, program); + this.defaultLocations_ = locations; + } else { + locations = this.defaultLocations_; + } + + context.useProgram(program); + + // enable the vertex attrib arrays + gl.enableVertexAttribArray(locations.a_lastPos); + gl.vertexAttribPointer(locations.a_lastPos, 2, FLOAT, + false, 28, 0); + + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, FLOAT, + false, 28, 8); + + gl.enableVertexAttribArray(locations.a_nextPos); + gl.vertexAttribPointer(locations.a_nextPos, 2, FLOAT, + false, 28, 16); + + gl.enableVertexAttribArray(locations.a_direction); + gl.vertexAttribPointer(locations.a_direction, 1, FLOAT, + false, 28, 24); + + // Enable renderer specific uniforms. + gl.uniform2fv(locations.u_size, size); + gl.uniform1f(locations.u_pixelRatio, pixelRatio); + + return locations; + } + + /** + * @inheritDoc + */ + shutDownProgram(gl, locations) { + gl.disableVertexAttribArray(locations.a_lastPos); + gl.disableVertexAttribArray(locations.a_position); + gl.disableVertexAttribArray(locations.a_nextPos); + gl.disableVertexAttribArray(locations.a_direction); + } + + /** + * @inheritDoc + */ + drawReplay(gl, context, skippedFeaturesHash, hitDetection) { + //Save GL parameters. + const tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC)); + const tmpDepthMask = /** @type {boolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK)); + + if (!hitDetection) { + gl.enable(gl.DEPTH_TEST); + gl.depthMask(true); + gl.depthFunc(gl.NOTEQUAL); + } + + if (!isEmpty(skippedFeaturesHash)) { + this.drawReplaySkipping_(gl, context, skippedFeaturesHash); + } else { + //Draw by style groups to minimize drawElements() calls. + let i, start, end, nextStyle; + end = this.startIndices[this.startIndices.length - 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + start = this.styleIndices_[i]; + nextStyle = this.styles_[i]; + this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]); + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + end = start; + } + } + if (!hitDetection) { + gl.disable(gl.DEPTH_TEST); + gl.clear(gl.DEPTH_BUFFER_BIT); + //Restore GL parameters. + gl.depthMask(tmpDepthMask); + gl.depthFunc(tmpDepthFunc); + } + } + + /** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {Object} skippedFeaturesHash Ids of features to skip. + */ + drawReplaySkipping_(gl, context, skippedFeaturesHash) { + let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; + featureIndex = this.startIndices.length - 2; + end = start = this.startIndices[featureIndex + 1]; for (i = this.styleIndices_.length - 1; i >= 0; --i) { - start = this.styleIndices_[i]; nextStyle = this.styles_[i]; this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]); - this.drawElements(gl, context, start, end); - gl.clear(gl.DEPTH_BUFFER_BIT); - end = start; - } - } - if (!hitDetection) { - gl.disable(gl.DEPTH_TEST); - gl.clear(gl.DEPTH_BUFFER_BIT); - //Restore GL parameters. - gl.depthMask(tmpDepthMask); - gl.depthFunc(tmpDepthFunc); - } -}; + groupStart = this.styleIndices_[i]; + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + featureStart = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = getUid(feature).toString(); -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {Object} skippedFeaturesHash Ids of features to skip. - */ -WebGLLineStringReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) { - let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; - featureIndex = this.startIndices.length - 2; - end = start = this.startIndices[featureIndex + 1]; - for (i = this.styleIndices_.length - 1; i >= 0; --i) { - nextStyle = this.styles_[i]; - this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]); - groupStart = this.styleIndices_[i]; - - while (featureIndex >= 0 && - this.startIndices[featureIndex] >= groupStart) { - featureStart = this.startIndices[featureIndex]; - feature = this.startIndicesFeature[featureIndex]; - featureUid = getUid(feature).toString(); - - if (skippedFeaturesHash[featureUid]) { - if (start !== end) { - this.drawElements(gl, context, start, end); - gl.clear(gl.DEPTH_BUFFER_BIT); + if (skippedFeaturesHash[featureUid]) { + if (start !== end) { + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + end = featureStart; } - end = featureStart; + featureIndex--; + start = featureStart; } - featureIndex--; - start = featureStart; - } - if (start !== end) { - this.drawElements(gl, context, start, end); - gl.clear(gl.DEPTH_BUFFER_BIT); - } - start = end = groupStart; - } -}; - - -/** - * @inheritDoc - */ -WebGLLineStringReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, - featureCallback, opt_hitExtent) { - let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; - featureIndex = this.startIndices.length - 2; - end = this.startIndices[featureIndex + 1]; - for (i = this.styleIndices_.length - 1; i >= 0; --i) { - nextStyle = this.styles_[i]; - this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]); - groupStart = this.styleIndices_[i]; - - while (featureIndex >= 0 && - this.startIndices[featureIndex] >= groupStart) { - start = this.startIndices[featureIndex]; - feature = this.startIndicesFeature[featureIndex]; - featureUid = getUid(feature).toString(); - - if (skippedFeaturesHash[featureUid] === undefined && - feature.getGeometry() && - (opt_hitExtent === undefined || intersects( - /** @type {Array} */ (opt_hitExtent), - feature.getGeometry().getExtent()))) { - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + if (start !== end) { this.drawElements(gl, context, start, end); - - const result = featureCallback(feature); - - if (result) { - return result; - } - + gl.clear(gl.DEPTH_BUFFER_BIT); } - featureIndex--; - end = start; + start = end = groupStart; } } - return undefined; -}; + /** + * @inheritDoc + */ + drawHitDetectionReplayOneByOne(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) { + let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; + featureIndex = this.startIndices.length - 2; + end = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]); + groupStart = this.styleIndices_[i]; -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {Array.} color Color. - * @param {number} lineWidth Line width. - * @param {number} miterLimit Miter limit. - */ -WebGLLineStringReplay.prototype.setStrokeStyle_ = function(gl, color, lineWidth, miterLimit) { - gl.uniform4fv(this.defaultLocations_.u_color, color); - gl.uniform1f(this.defaultLocations_.u_lineWidth, lineWidth); - gl.uniform1f(this.defaultLocations_.u_miterLimit, miterLimit); -}; + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + start = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = getUid(feature).toString(); + if (skippedFeaturesHash[featureUid] === undefined && + feature.getGeometry() && + (opt_hitExtent === undefined || intersects( + /** @type {Array} */ (opt_hitExtent), + feature.getGeometry().getExtent()))) { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + this.drawElements(gl, context, start, end); -/** - * @inheritDoc - */ -WebGLLineStringReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { - const strokeStyleLineCap = strokeStyle.getLineCap(); - this.state_.lineCap = strokeStyleLineCap !== undefined ? - strokeStyleLineCap : DEFAULT_LINECAP; - const strokeStyleLineDash = strokeStyle.getLineDash(); - this.state_.lineDash = strokeStyleLineDash ? - strokeStyleLineDash : DEFAULT_LINEDASH; - const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); - this.state_.lineDashOffset = strokeStyleLineDashOffset ? - strokeStyleLineDashOffset : DEFAULT_LINEDASHOFFSET; - const strokeStyleLineJoin = strokeStyle.getLineJoin(); - this.state_.lineJoin = strokeStyleLineJoin !== undefined ? - strokeStyleLineJoin : DEFAULT_LINEJOIN; - let strokeStyleColor = strokeStyle.getColor(); - if (!(strokeStyleColor instanceof CanvasGradient) && - !(strokeStyleColor instanceof CanvasPattern)) { - strokeStyleColor = asArray(strokeStyleColor).map(function(c, i) { - return i != 3 ? c / 255 : c; - }) || DEFAULT_STROKESTYLE; - } else { - strokeStyleColor = DEFAULT_STROKESTYLE; + const result = featureCallback(feature); + + if (result) { + return result; + } + + } + featureIndex--; + end = start; + } + } + return undefined; } - let strokeStyleWidth = strokeStyle.getWidth(); - strokeStyleWidth = strokeStyleWidth !== undefined ? - strokeStyleWidth : DEFAULT_LINEWIDTH; - let strokeStyleMiterLimit = strokeStyle.getMiterLimit(); - strokeStyleMiterLimit = strokeStyleMiterLimit !== undefined ? - strokeStyleMiterLimit : DEFAULT_MITERLIMIT; - if (!this.state_.strokeColor || !equals(this.state_.strokeColor, strokeStyleColor) || - this.state_.lineWidth !== strokeStyleWidth || this.state_.miterLimit !== strokeStyleMiterLimit) { - this.state_.changed = true; - this.state_.strokeColor = strokeStyleColor; - this.state_.lineWidth = strokeStyleWidth; - this.state_.miterLimit = strokeStyleMiterLimit; - this.styles_.push([strokeStyleColor, strokeStyleWidth, strokeStyleMiterLimit]); + + /** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {Array.} color Color. + * @param {number} lineWidth Line width. + * @param {number} miterLimit Miter limit. + */ + setStrokeStyle_(gl, color, lineWidth, miterLimit) { + gl.uniform4fv(this.defaultLocations_.u_color, color); + gl.uniform1f(this.defaultLocations_.u_lineWidth, lineWidth); + gl.uniform1f(this.defaultLocations_.u_miterLimit, miterLimit); } -}; + + /** + * @inheritDoc + */ + setFillStrokeStyle(fillStyle, strokeStyle) { + const strokeStyleLineCap = strokeStyle.getLineCap(); + this.state_.lineCap = strokeStyleLineCap !== undefined ? + strokeStyleLineCap : DEFAULT_LINECAP; + const strokeStyleLineDash = strokeStyle.getLineDash(); + this.state_.lineDash = strokeStyleLineDash ? + strokeStyleLineDash : DEFAULT_LINEDASH; + const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); + this.state_.lineDashOffset = strokeStyleLineDashOffset ? + strokeStyleLineDashOffset : DEFAULT_LINEDASHOFFSET; + const strokeStyleLineJoin = strokeStyle.getLineJoin(); + this.state_.lineJoin = strokeStyleLineJoin !== undefined ? + strokeStyleLineJoin : DEFAULT_LINEJOIN; + let strokeStyleColor = strokeStyle.getColor(); + if (!(strokeStyleColor instanceof CanvasGradient) && + !(strokeStyleColor instanceof CanvasPattern)) { + strokeStyleColor = asArray(strokeStyleColor).map(function(c, i) { + return i != 3 ? c / 255 : c; + }) || DEFAULT_STROKESTYLE; + } else { + strokeStyleColor = DEFAULT_STROKESTYLE; + } + let strokeStyleWidth = strokeStyle.getWidth(); + strokeStyleWidth = strokeStyleWidth !== undefined ? + strokeStyleWidth : DEFAULT_LINEWIDTH; + let strokeStyleMiterLimit = strokeStyle.getMiterLimit(); + strokeStyleMiterLimit = strokeStyleMiterLimit !== undefined ? + strokeStyleMiterLimit : DEFAULT_MITERLIMIT; + if (!this.state_.strokeColor || !equals(this.state_.strokeColor, strokeStyleColor) || + this.state_.lineWidth !== strokeStyleWidth || this.state_.miterLimit !== strokeStyleMiterLimit) { + this.state_.changed = true; + this.state_.strokeColor = strokeStyleColor; + this.state_.lineWidth = strokeStyleWidth; + this.state_.miterLimit = strokeStyleMiterLimit; + this.styles_.push([strokeStyleColor, strokeStyleWidth, strokeStyleMiterLimit]); + } + } +} + +inherits(WebGLLineStringReplay, WebGLReplay); + export default WebGLLineStringReplay; diff --git a/src/ol/render/webgl/PolygonReplay.js b/src/ol/render/webgl/PolygonReplay.js index 580363bd88..14bb136170 100644 --- a/src/ol/render/webgl/PolygonReplay.js +++ b/src/ol/render/webgl/PolygonReplay.js @@ -43,1039 +43,1009 @@ import WebGLBuffer from '../../webgl/Buffer.js'; * @param {module:ol/extent~Extent} maxExtent Max extent. * @struct */ -const WebGLPolygonReplay = function(tolerance, maxExtent) { - WebGLReplay.call(this, tolerance, maxExtent); +class WebGLPolygonReplay { + constructor(tolerance, maxExtent) { + WebGLReplay.call(this, tolerance, maxExtent); - this.lineStringReplay = new WebGLLineStringReplay( - tolerance, maxExtent); + this.lineStringReplay = new WebGLLineStringReplay( + tolerance, maxExtent); + + /** + * @private + * @type {module:ol/render/webgl/polygonreplay/defaultshader/Locations} + */ + this.defaultLocations_ = null; + + /** + * @private + * @type {Array.>} + */ + this.styles_ = []; + + /** + * @private + * @type {Array.} + */ + this.styleIndices_ = []; + + /** + * @private + * @type {{fillColor: (Array.|null), + * changed: boolean}|null} + */ + this.state_ = { + fillColor: null, + changed: false + }; + + } /** + * Draw one polygon. + * @param {Array.} flatCoordinates Flat coordinates. + * @param {Array.>} holeFlatCoordinates Hole flat coordinates. + * @param {number} stride Stride. * @private - * @type {module:ol/render/webgl/polygonreplay/defaultshader/Locations} */ - this.defaultLocations_ = null; + drawCoordinates_(flatCoordinates, holeFlatCoordinates, stride) { + // Triangulate the polygon + const outerRing = new LinkedList(); + const rtree = new RBush(); + // Initialize the outer ring + this.processFlatCoordinates_(flatCoordinates, stride, outerRing, rtree, true); + const maxCoords = this.getMaxCoords_(outerRing); - /** - * @private - * @type {Array.>} - */ - this.styles_ = []; - - /** - * @private - * @type {Array.} - */ - this.styleIndices_ = []; - - /** - * @private - * @type {{fillColor: (Array.|null), - * changed: boolean}|null} - */ - this.state_ = { - fillColor: null, - changed: false - }; - -}; - -inherits(WebGLPolygonReplay, WebGLReplay); - - -/** - * Draw one polygon. - * @param {Array.} flatCoordinates Flat coordinates. - * @param {Array.>} holeFlatCoordinates Hole flat coordinates. - * @param {number} stride Stride. - * @private - */ -WebGLPolygonReplay.prototype.drawCoordinates_ = function( - flatCoordinates, holeFlatCoordinates, stride) { - // Triangulate the polygon - const outerRing = new LinkedList(); - const rtree = new RBush(); - // Initialize the outer ring - this.processFlatCoordinates_(flatCoordinates, stride, outerRing, rtree, true); - const maxCoords = this.getMaxCoords_(outerRing); - - // Eliminate holes, if there are any - if (holeFlatCoordinates.length) { - let i, ii; - const holeLists = []; - for (i = 0, ii = holeFlatCoordinates.length; i < ii; ++i) { - const holeList = { - list: new LinkedList(), - maxCoords: undefined, - rtree: new RBush() - }; - holeLists.push(holeList); - this.processFlatCoordinates_(holeFlatCoordinates[i], - stride, holeList.list, holeList.rtree, false); - this.classifyPoints_(holeList.list, holeList.rtree, true); - holeList.maxCoords = this.getMaxCoords_(holeList.list); - } - holeLists.sort(function(a, b) { - return b.maxCoords[0] === a.maxCoords[0] ? - a.maxCoords[1] - b.maxCoords[1] : b.maxCoords[0] - a.maxCoords[0]; - }); - for (i = 0; i < holeLists.length; ++i) { - const currList = holeLists[i].list; - const start = currList.firstItem(); - let currItem = start; - let intersection; - do { - //TODO: Triangulate holes when they intersect the outer ring. - if (this.getIntersections_(currItem, rtree).length) { - intersection = true; - break; - } - currItem = currList.nextItem(); - } while (start !== currItem); - if (!intersection) { - if (this.bridgeHole_(currList, holeLists[i].maxCoords[0], outerRing, maxCoords[0], rtree)) { - rtree.concat(holeLists[i].rtree); - this.classifyPoints_(outerRing, rtree, false); - } + // Eliminate holes, if there are any + if (holeFlatCoordinates.length) { + let i, ii; + const holeLists = []; + for (i = 0, ii = holeFlatCoordinates.length; i < ii; ++i) { + const holeList = { + list: new LinkedList(), + maxCoords: undefined, + rtree: new RBush() + }; + holeLists.push(holeList); + this.processFlatCoordinates_(holeFlatCoordinates[i], + stride, holeList.list, holeList.rtree, false); + this.classifyPoints_(holeList.list, holeList.rtree, true); + holeList.maxCoords = this.getMaxCoords_(holeList.list); } - } - } else { - this.classifyPoints_(outerRing, rtree, false); - } - this.triangulate_(outerRing, rtree); -}; - - -/** - * Inserts flat coordinates in a linked list and adds them to the vertex buffer. - * @private - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} stride Stride. - * @param {module:ol/structs/LinkedList} list Linked list. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - * @param {boolean} clockwise Coordinate order should be clockwise. - */ -WebGLPolygonReplay.prototype.processFlatCoordinates_ = function( - flatCoordinates, stride, list, rtree, clockwise) { - const isClockwise = linearRingIsClockwise(flatCoordinates, - 0, flatCoordinates.length, stride); - let i, ii; - let n = this.vertices.length / 2; - /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ - let start; - /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ - let p0; - /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ - let p1; - const extents = []; - const segments = []; - if (clockwise === isClockwise) { - start = this.createPoint_(flatCoordinates[0], flatCoordinates[1], n++); - p0 = start; - for (i = stride, ii = flatCoordinates.length; i < ii; i += stride) { - p1 = this.createPoint_(flatCoordinates[i], flatCoordinates[i + 1], n++); - segments.push(this.insertItem_(p0, p1, list)); - extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), - Math.max(p0.y, p1.y)]); - p0 = p1; - } - segments.push(this.insertItem_(p1, start, list)); - extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), - Math.max(p0.y, p1.y)]); - } else { - const end = flatCoordinates.length - stride; - start = this.createPoint_(flatCoordinates[end], flatCoordinates[end + 1], n++); - p0 = start; - for (i = end - stride, ii = 0; i >= ii; i -= stride) { - p1 = this.createPoint_(flatCoordinates[i], flatCoordinates[i + 1], n++); - segments.push(this.insertItem_(p0, p1, list)); - extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), - Math.max(p0.y, p1.y)]); - p0 = p1; - } - segments.push(this.insertItem_(p1, start, list)); - extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), - Math.max(p0.y, p1.y)]); - } - rtree.load(extents, segments); -}; - - -/** - * Returns the rightmost coordinates of a polygon on the X axis. - * @private - * @param {module:ol/structs/LinkedList} list Polygons ring. - * @return {Array.} Max X coordinates. - */ -WebGLPolygonReplay.prototype.getMaxCoords_ = function(list) { - const start = list.firstItem(); - let seg = start; - let maxCoords = [seg.p0.x, seg.p0.y]; - - do { - seg = list.nextItem(); - if (seg.p0.x > maxCoords[0]) { - maxCoords = [seg.p0.x, seg.p0.y]; - } - } while (seg !== start); - - return maxCoords; -}; - - -/** - * Classifies the points of a polygon list as convex, reflex. Removes collinear vertices. - * @private - * @param {module:ol/structs/LinkedList} list Polygon ring. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - * @param {boolean} ccw The orientation of the polygon is counter-clockwise. - * @return {boolean} There were reclassified points. - */ -WebGLPolygonReplay.prototype.classifyPoints_ = function(list, rtree, ccw) { - let start = list.firstItem(); - let s0 = start; - let s1 = list.nextItem(); - let pointsReclassified = false; - do { - const reflex = ccw ? triangleIsCounterClockwise(s1.p1.x, - s1.p1.y, s0.p1.x, s0.p1.y, s0.p0.x, s0.p0.y) : - triangleIsCounterClockwise(s0.p0.x, s0.p0.y, s0.p1.x, - s0.p1.y, s1.p1.x, s1.p1.y); - if (reflex === undefined) { - this.removeItem_(s0, s1, list, rtree); - pointsReclassified = true; - if (s1 === start) { - start = list.getNextItem(); - } - s1 = s0; - list.prevItem(); - } else if (s0.p1.reflex !== reflex) { - s0.p1.reflex = reflex; - pointsReclassified = true; - } - s0 = s1; - s1 = list.nextItem(); - } while (s0 !== start); - return pointsReclassified; -}; - - -/** - * @private - * @param {module:ol/structs/LinkedList} hole Linked list of the hole. - * @param {number} holeMaxX Maximum X value of the hole. - * @param {module:ol/structs/LinkedList} list Linked list of the polygon. - * @param {number} listMaxX Maximum X value of the polygon. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - * @return {boolean} Bridging was successful. - */ -WebGLPolygonReplay.prototype.bridgeHole_ = function(hole, holeMaxX, - list, listMaxX, rtree) { - let seg = hole.firstItem(); - while (seg.p1.x !== holeMaxX) { - seg = hole.nextItem(); - } - - const p1 = seg.p1; - /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ - const p2 = {x: listMaxX, y: p1.y, i: -1}; - let minDist = Infinity; - let i, ii, bestPoint; - /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ - let p5; - - const intersectingSegments = this.getIntersections_({p0: p1, p1: p2}, rtree, true); - for (i = 0, ii = intersectingSegments.length; i < ii; ++i) { - const currSeg = intersectingSegments[i]; - const intersection = this.calculateIntersection_(p1, p2, currSeg.p0, - currSeg.p1, true); - const dist = Math.abs(p1.x - intersection[0]); - if (dist < minDist && triangleIsCounterClockwise(p1.x, p1.y, - currSeg.p0.x, currSeg.p0.y, currSeg.p1.x, currSeg.p1.y) !== undefined) { - minDist = dist; - p5 = {x: intersection[0], y: intersection[1], i: -1}; - seg = currSeg; - } - } - if (minDist === Infinity) { - return false; - } - bestPoint = seg.p1; - - if (minDist > 0) { - const pointsInTriangle = this.getPointsInTriangle_(p1, p5, seg.p1, rtree); - if (pointsInTriangle.length) { - let theta = Infinity; - for (i = 0, ii = pointsInTriangle.length; i < ii; ++i) { - const currPoint = pointsInTriangle[i]; - const currTheta = Math.atan2(p1.y - currPoint.y, p2.x - currPoint.x); - if (currTheta < theta || (currTheta === theta && currPoint.x < bestPoint.x)) { - theta = currTheta; - bestPoint = currPoint; - } - } - } - } - - seg = list.firstItem(); - while (seg.p1.x !== bestPoint.x || seg.p1.y !== bestPoint.y) { - seg = list.nextItem(); - } - - //We clone the bridge points as they can have different convexity. - const p0Bridge = {x: p1.x, y: p1.y, i: p1.i, reflex: undefined}; - const p1Bridge = {x: seg.p1.x, y: seg.p1.y, i: seg.p1.i, reflex: undefined}; - - hole.getNextItem().p0 = p0Bridge; - this.insertItem_(p1, seg.p1, hole, rtree); - this.insertItem_(p1Bridge, p0Bridge, hole, rtree); - seg.p1 = p1Bridge; - hole.setFirstItem(); - list.concat(hole); - - return true; -}; - - -/** - * @private - * @param {module:ol/structs/LinkedList} list Linked list of the polygon. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - */ -WebGLPolygonReplay.prototype.triangulate_ = function(list, rtree) { - let ccw = false; - let simple = this.isSimple_(list, rtree); - - // Start clipping ears - while (list.getLength() > 3) { - if (simple) { - if (!this.clipEars_(list, rtree, simple, ccw)) { - if (!this.classifyPoints_(list, rtree, ccw)) { - // Due to the behavior of OL's PIP algorithm, the ear clipping cannot - // introduce touching segments. However, the original data may have some. - if (!this.resolveSelfIntersections_(list, rtree, true)) { + holeLists.sort(function(a, b) { + return b.maxCoords[0] === a.maxCoords[0] ? + a.maxCoords[1] - b.maxCoords[1] : b.maxCoords[0] - a.maxCoords[0]; + }); + for (i = 0; i < holeLists.length; ++i) { + const currList = holeLists[i].list; + const start = currList.firstItem(); + let currItem = start; + let intersection; + do { + //TODO: Triangulate holes when they intersect the outer ring. + if (this.getIntersections_(currItem, rtree).length) { + intersection = true; break; } + currItem = currList.nextItem(); + } while (start !== currItem); + if (!intersection) { + if (this.bridgeHole_(currList, holeLists[i].maxCoords[0], outerRing, maxCoords[0], rtree)) { + rtree.concat(holeLists[i].rtree); + this.classifyPoints_(outerRing, rtree, false); + } } } } else { - if (!this.clipEars_(list, rtree, simple, ccw)) { - // We ran out of ears, try to reclassify. - if (!this.classifyPoints_(list, rtree, ccw)) { - // We have a bad polygon, try to resolve local self-intersections. - if (!this.resolveSelfIntersections_(list, rtree)) { - simple = this.isSimple_(list, rtree); - if (!simple) { - // We have a really bad polygon, try more time consuming methods. - this.splitPolygon_(list, rtree); + this.classifyPoints_(outerRing, rtree, false); + } + this.triangulate_(outerRing, rtree); + } + + /** + * Inserts flat coordinates in a linked list and adds them to the vertex buffer. + * @private + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} stride Stride. + * @param {module:ol/structs/LinkedList} list Linked list. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + * @param {boolean} clockwise Coordinate order should be clockwise. + */ + processFlatCoordinates_(flatCoordinates, stride, list, rtree, clockwise) { + const isClockwise = linearRingIsClockwise(flatCoordinates, + 0, flatCoordinates.length, stride); + let i, ii; + let n = this.vertices.length / 2; + /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ + let start; + /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ + let p0; + /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ + let p1; + const extents = []; + const segments = []; + if (clockwise === isClockwise) { + start = this.createPoint_(flatCoordinates[0], flatCoordinates[1], n++); + p0 = start; + for (i = stride, ii = flatCoordinates.length; i < ii; i += stride) { + p1 = this.createPoint_(flatCoordinates[i], flatCoordinates[i + 1], n++); + segments.push(this.insertItem_(p0, p1, list)); + extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), + Math.max(p0.y, p1.y)]); + p0 = p1; + } + segments.push(this.insertItem_(p1, start, list)); + extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), + Math.max(p0.y, p1.y)]); + } else { + const end = flatCoordinates.length - stride; + start = this.createPoint_(flatCoordinates[end], flatCoordinates[end + 1], n++); + p0 = start; + for (i = end - stride, ii = 0; i >= ii; i -= stride) { + p1 = this.createPoint_(flatCoordinates[i], flatCoordinates[i + 1], n++); + segments.push(this.insertItem_(p0, p1, list)); + extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), + Math.max(p0.y, p1.y)]); + p0 = p1; + } + segments.push(this.insertItem_(p1, start, list)); + extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), + Math.max(p0.y, p1.y)]); + } + rtree.load(extents, segments); + } + + /** + * Returns the rightmost coordinates of a polygon on the X axis. + * @private + * @param {module:ol/structs/LinkedList} list Polygons ring. + * @return {Array.} Max X coordinates. + */ + getMaxCoords_(list) { + const start = list.firstItem(); + let seg = start; + let maxCoords = [seg.p0.x, seg.p0.y]; + + do { + seg = list.nextItem(); + if (seg.p0.x > maxCoords[0]) { + maxCoords = [seg.p0.x, seg.p0.y]; + } + } while (seg !== start); + + return maxCoords; + } + + /** + * Classifies the points of a polygon list as convex, reflex. Removes collinear vertices. + * @private + * @param {module:ol/structs/LinkedList} list Polygon ring. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + * @param {boolean} ccw The orientation of the polygon is counter-clockwise. + * @return {boolean} There were reclassified points. + */ + classifyPoints_(list, rtree, ccw) { + let start = list.firstItem(); + let s0 = start; + let s1 = list.nextItem(); + let pointsReclassified = false; + do { + const reflex = ccw ? triangleIsCounterClockwise(s1.p1.x, + s1.p1.y, s0.p1.x, s0.p1.y, s0.p0.x, s0.p0.y) : + triangleIsCounterClockwise(s0.p0.x, s0.p0.y, s0.p1.x, + s0.p1.y, s1.p1.x, s1.p1.y); + if (reflex === undefined) { + this.removeItem_(s0, s1, list, rtree); + pointsReclassified = true; + if (s1 === start) { + start = list.getNextItem(); + } + s1 = s0; + list.prevItem(); + } else if (s0.p1.reflex !== reflex) { + s0.p1.reflex = reflex; + pointsReclassified = true; + } + s0 = s1; + s1 = list.nextItem(); + } while (s0 !== start); + return pointsReclassified; + } + + /** + * @private + * @param {module:ol/structs/LinkedList} hole Linked list of the hole. + * @param {number} holeMaxX Maximum X value of the hole. + * @param {module:ol/structs/LinkedList} list Linked list of the polygon. + * @param {number} listMaxX Maximum X value of the polygon. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + * @return {boolean} Bridging was successful. + */ + bridgeHole_(hole, holeMaxX, list, listMaxX, rtree) { + let seg = hole.firstItem(); + while (seg.p1.x !== holeMaxX) { + seg = hole.nextItem(); + } + + const p1 = seg.p1; + /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ + const p2 = {x: listMaxX, y: p1.y, i: -1}; + let minDist = Infinity; + let i, ii, bestPoint; + /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ + let p5; + + const intersectingSegments = this.getIntersections_({p0: p1, p1: p2}, rtree, true); + for (i = 0, ii = intersectingSegments.length; i < ii; ++i) { + const currSeg = intersectingSegments[i]; + const intersection = this.calculateIntersection_(p1, p2, currSeg.p0, + currSeg.p1, true); + const dist = Math.abs(p1.x - intersection[0]); + if (dist < minDist && triangleIsCounterClockwise(p1.x, p1.y, + currSeg.p0.x, currSeg.p0.y, currSeg.p1.x, currSeg.p1.y) !== undefined) { + minDist = dist; + p5 = {x: intersection[0], y: intersection[1], i: -1}; + seg = currSeg; + } + } + if (minDist === Infinity) { + return false; + } + bestPoint = seg.p1; + + if (minDist > 0) { + const pointsInTriangle = this.getPointsInTriangle_(p1, p5, seg.p1, rtree); + if (pointsInTriangle.length) { + let theta = Infinity; + for (i = 0, ii = pointsInTriangle.length; i < ii; ++i) { + const currPoint = pointsInTriangle[i]; + const currTheta = Math.atan2(p1.y - currPoint.y, p2.x - currPoint.x); + if (currTheta < theta || (currTheta === theta && currPoint.x < bestPoint.x)) { + theta = currTheta; + bestPoint = currPoint; + } + } + } + } + + seg = list.firstItem(); + while (seg.p1.x !== bestPoint.x || seg.p1.y !== bestPoint.y) { + seg = list.nextItem(); + } + + //We clone the bridge points as they can have different convexity. + const p0Bridge = {x: p1.x, y: p1.y, i: p1.i, reflex: undefined}; + const p1Bridge = {x: seg.p1.x, y: seg.p1.y, i: seg.p1.i, reflex: undefined}; + + hole.getNextItem().p0 = p0Bridge; + this.insertItem_(p1, seg.p1, hole, rtree); + this.insertItem_(p1Bridge, p0Bridge, hole, rtree); + seg.p1 = p1Bridge; + hole.setFirstItem(); + list.concat(hole); + + return true; + } + + /** + * @private + * @param {module:ol/structs/LinkedList} list Linked list of the polygon. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + */ + triangulate_(list, rtree) { + let ccw = false; + let simple = this.isSimple_(list, rtree); + + // Start clipping ears + while (list.getLength() > 3) { + if (simple) { + if (!this.clipEars_(list, rtree, simple, ccw)) { + if (!this.classifyPoints_(list, rtree, ccw)) { + // Due to the behavior of OL's PIP algorithm, the ear clipping cannot + // introduce touching segments. However, the original data may have some. + if (!this.resolveSelfIntersections_(list, rtree, true)) { break; - } else { - ccw = !this.isClockwise_(list); - this.classifyPoints_(list, rtree, ccw); + } + } + } + } else { + if (!this.clipEars_(list, rtree, simple, ccw)) { + // We ran out of ears, try to reclassify. + if (!this.classifyPoints_(list, rtree, ccw)) { + // We have a bad polygon, try to resolve local self-intersections. + if (!this.resolveSelfIntersections_(list, rtree)) { + simple = this.isSimple_(list, rtree); + if (!simple) { + // We have a really bad polygon, try more time consuming methods. + this.splitPolygon_(list, rtree); + break; + } else { + ccw = !this.isClockwise_(list); + this.classifyPoints_(list, rtree, ccw); + } } } } } } - } - if (list.getLength() === 3) { - let numIndices = this.indices.length; - this.indices[numIndices++] = list.getPrevItem().p0.i; - this.indices[numIndices++] = list.getCurrItem().p0.i; - this.indices[numIndices++] = list.getNextItem().p0.i; - } -}; - - -/** - * @private - * @param {module:ol/structs/LinkedList} list Linked list of the polygon. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - * @param {boolean} simple The polygon is simple. - * @param {boolean} ccw Orientation of the polygon is counter-clockwise. - * @return {boolean} There were processed ears. - */ -WebGLPolygonReplay.prototype.clipEars_ = function(list, rtree, simple, ccw) { - let numIndices = this.indices.length; - let start = list.firstItem(); - let s0 = list.getPrevItem(); - let s1 = start; - let s2 = list.nextItem(); - let s3 = list.getNextItem(); - let p0, p1, p2; - let processedEars = false; - do { - p0 = s1.p0; - p1 = s1.p1; - p2 = s2.p1; - if (p1.reflex === false) { - // We might have a valid ear - let variableCriterion; - if (simple) { - variableCriterion = this.getPointsInTriangle_(p0, p1, p2, rtree, true).length === 0; - } else { - variableCriterion = ccw ? this.diagonalIsInside_(s3.p1, p2, p1, p0, - s0.p0) : this.diagonalIsInside_(s0.p0, p0, p1, p2, s3.p1); - } - if ((simple || this.getIntersections_({p0: p0, p1: p2}, rtree).length === 0) && - variableCriterion) { - //The diagonal is completely inside the polygon - if (simple || p0.reflex === false || p2.reflex === false || - linearRingIsClockwise([s0.p0.x, s0.p0.y, p0.x, - p0.y, p1.x, p1.y, p2.x, p2.y, s3.p1.x, s3.p1.y], 0, 10, 2) === !ccw) { - //The diagonal is persumably valid, we have an ear - this.indices[numIndices++] = p0.i; - this.indices[numIndices++] = p1.i; - this.indices[numIndices++] = p2.i; - this.removeItem_(s1, s2, list, rtree); - if (s2 === start) { - start = s3; - } - processedEars = true; - } - } - } - // Else we have a reflex point. - s0 = list.getPrevItem(); - s1 = list.getCurrItem(); - s2 = list.nextItem(); - s3 = list.getNextItem(); - } while (s1 !== start && list.getLength() > 3); - - return processedEars; -}; - - -/** - * @private - * @param {module:ol/structs/LinkedList} list Linked list of the polygon. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - * @param {boolean=} opt_touch Resolve touching segments. - * @return {boolean} There were resolved intersections. -*/ -WebGLPolygonReplay.prototype.resolveSelfIntersections_ = function( - list, rtree, opt_touch) { - const start = list.firstItem(); - list.nextItem(); - let s0 = start; - let s1 = list.nextItem(); - let resolvedIntersections = false; - - do { - const intersection = this.calculateIntersection_(s0.p0, s0.p1, s1.p0, s1.p1, - opt_touch); - if (intersection) { - let breakCond = false; - const numVertices = this.vertices.length; + if (list.getLength() === 3) { let numIndices = this.indices.length; - const n = numVertices / 2; - const seg = list.prevItem(); - list.removeItem(); - rtree.remove(seg); - breakCond = (seg === start); - let p; - if (opt_touch) { - if (intersection[0] === s0.p0.x && intersection[1] === s0.p0.y) { - list.prevItem(); - p = s0.p0; - s1.p0 = p; - rtree.remove(s0); - breakCond = breakCond || (s0 === start); + this.indices[numIndices++] = list.getPrevItem().p0.i; + this.indices[numIndices++] = list.getCurrItem().p0.i; + this.indices[numIndices++] = list.getNextItem().p0.i; + } + } + + /** + * @private + * @param {module:ol/structs/LinkedList} list Linked list of the polygon. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + * @param {boolean} simple The polygon is simple. + * @param {boolean} ccw Orientation of the polygon is counter-clockwise. + * @return {boolean} There were processed ears. + */ + clipEars_(list, rtree, simple, ccw) { + let numIndices = this.indices.length; + let start = list.firstItem(); + let s0 = list.getPrevItem(); + let s1 = start; + let s2 = list.nextItem(); + let s3 = list.getNextItem(); + let p0, p1, p2; + let processedEars = false; + do { + p0 = s1.p0; + p1 = s1.p1; + p2 = s2.p1; + if (p1.reflex === false) { + // We might have a valid ear + let variableCriterion; + if (simple) { + variableCriterion = this.getPointsInTriangle_(p0, p1, p2, rtree, true).length === 0; } else { - p = s1.p1; - s0.p1 = p; - rtree.remove(s1); - breakCond = breakCond || (s1 === start); + variableCriterion = ccw ? this.diagonalIsInside_(s3.p1, p2, p1, p0, + s0.p0) : this.diagonalIsInside_(s0.p0, p0, p1, p2, s3.p1); } + if ((simple || this.getIntersections_({p0: p0, p1: p2}, rtree).length === 0) && + variableCriterion) { + //The diagonal is completely inside the polygon + if (simple || p0.reflex === false || p2.reflex === false || + linearRingIsClockwise([s0.p0.x, s0.p0.y, p0.x, + p0.y, p1.x, p1.y, p2.x, p2.y, s3.p1.x, s3.p1.y], 0, 10, 2) === !ccw) { + //The diagonal is persumably valid, we have an ear + this.indices[numIndices++] = p0.i; + this.indices[numIndices++] = p1.i; + this.indices[numIndices++] = p2.i; + this.removeItem_(s1, s2, list, rtree); + if (s2 === start) { + start = s3; + } + processedEars = true; + } + } + } + // Else we have a reflex point. + s0 = list.getPrevItem(); + s1 = list.getCurrItem(); + s2 = list.nextItem(); + s3 = list.getNextItem(); + } while (s1 !== start && list.getLength() > 3); + + return processedEars; + } + + /** + * @private + * @param {module:ol/structs/LinkedList} list Linked list of the polygon. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + * @param {boolean=} opt_touch Resolve touching segments. + * @return {boolean} There were resolved intersections. + */ + resolveSelfIntersections_(list, rtree, opt_touch) { + const start = list.firstItem(); + list.nextItem(); + let s0 = start; + let s1 = list.nextItem(); + let resolvedIntersections = false; + + do { + const intersection = this.calculateIntersection_(s0.p0, s0.p1, s1.p0, s1.p1, + opt_touch); + if (intersection) { + let breakCond = false; + const numVertices = this.vertices.length; + let numIndices = this.indices.length; + const n = numVertices / 2; + const seg = list.prevItem(); list.removeItem(); - } else { - p = this.createPoint_(intersection[0], intersection[1], n); - s0.p1 = p; - s1.p0 = p; - rtree.update([Math.min(s0.p0.x, s0.p1.x), Math.min(s0.p0.y, s0.p1.y), - Math.max(s0.p0.x, s0.p1.x), Math.max(s0.p0.y, s0.p1.y)], s0); - rtree.update([Math.min(s1.p0.x, s1.p1.x), Math.min(s1.p0.y, s1.p1.y), - Math.max(s1.p0.x, s1.p1.x), Math.max(s1.p0.y, s1.p1.y)], s1); + rtree.remove(seg); + breakCond = (seg === start); + let p; + if (opt_touch) { + if (intersection[0] === s0.p0.x && intersection[1] === s0.p0.y) { + list.prevItem(); + p = s0.p0; + s1.p0 = p; + rtree.remove(s0); + breakCond = breakCond || (s0 === start); + } else { + p = s1.p1; + s0.p1 = p; + rtree.remove(s1); + breakCond = breakCond || (s1 === start); + } + list.removeItem(); + } else { + p = this.createPoint_(intersection[0], intersection[1], n); + s0.p1 = p; + s1.p0 = p; + rtree.update([Math.min(s0.p0.x, s0.p1.x), Math.min(s0.p0.y, s0.p1.y), + Math.max(s0.p0.x, s0.p1.x), Math.max(s0.p0.y, s0.p1.y)], s0); + rtree.update([Math.min(s1.p0.x, s1.p1.x), Math.min(s1.p0.y, s1.p1.y), + Math.max(s1.p0.x, s1.p1.x), Math.max(s1.p0.y, s1.p1.y)], s1); + } + + this.indices[numIndices++] = seg.p0.i; + this.indices[numIndices++] = seg.p1.i; + this.indices[numIndices++] = p.i; + + resolvedIntersections = true; + if (breakCond) { + break; + } } - this.indices[numIndices++] = seg.p0.i; - this.indices[numIndices++] = seg.p1.i; - this.indices[numIndices++] = p.i; + s0 = list.getPrevItem(); + s1 = list.nextItem(); + } while (s0 !== start); + return resolvedIntersections; + } - resolvedIntersections = true; - if (breakCond) { + /** + * @private + * @param {module:ol/structs/LinkedList} list Linked list of the polygon. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + * @return {boolean} The polygon is simple. + */ + isSimple_(list, rtree) { + const start = list.firstItem(); + let seg = start; + do { + if (this.getIntersections_(seg, rtree).length) { + return false; + } + seg = list.nextItem(); + } while (seg !== start); + return true; + } + + /** + * @private + * @param {module:ol/structs/LinkedList} list Linked list of the polygon. + * @return {boolean} Orientation is clockwise. + */ + isClockwise_(list) { + const length = list.getLength() * 2; + const flatCoordinates = new Array(length); + const start = list.firstItem(); + let seg = start; + let i = 0; + do { + flatCoordinates[i++] = seg.p0.x; + flatCoordinates[i++] = seg.p0.y; + seg = list.nextItem(); + } while (seg !== start); + return linearRingIsClockwise(flatCoordinates, 0, length, 2); + } + + /** + * @private + * @param {module:ol/structs/LinkedList} list Linked list of the polygon. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + */ + splitPolygon_(list, rtree) { + const start = list.firstItem(); + let s0 = start; + do { + const intersections = this.getIntersections_(s0, rtree); + if (intersections.length) { + const s1 = intersections[0]; + const n = this.vertices.length / 2; + const intersection = this.calculateIntersection_(s0.p0, + s0.p1, s1.p0, s1.p1); + const p = this.createPoint_(intersection[0], intersection[1], n); + const newPolygon = new LinkedList(); + const newRtree = new RBush(); + this.insertItem_(p, s0.p1, newPolygon, newRtree); + s0.p1 = p; + rtree.update([Math.min(s0.p0.x, p.x), Math.min(s0.p0.y, p.y), + Math.max(s0.p0.x, p.x), Math.max(s0.p0.y, p.y)], s0); + let currItem = list.nextItem(); + while (currItem !== s1) { + this.insertItem_(currItem.p0, currItem.p1, newPolygon, newRtree); + rtree.remove(currItem); + list.removeItem(); + currItem = list.getCurrItem(); + } + this.insertItem_(s1.p0, p, newPolygon, newRtree); + s1.p0 = p; + rtree.update([Math.min(s1.p1.x, p.x), Math.min(s1.p1.y, p.y), + Math.max(s1.p1.x, p.x), Math.max(s1.p1.y, p.y)], s1); + this.classifyPoints_(list, rtree, false); + this.triangulate_(list, rtree); + this.classifyPoints_(newPolygon, newRtree, false); + this.triangulate_(newPolygon, newRtree); break; } - } - - s0 = list.getPrevItem(); - s1 = list.nextItem(); - } while (s0 !== start); - return resolvedIntersections; -}; - - -/** - * @private - * @param {module:ol/structs/LinkedList} list Linked list of the polygon. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - * @return {boolean} The polygon is simple. - */ -WebGLPolygonReplay.prototype.isSimple_ = function(list, rtree) { - const start = list.firstItem(); - let seg = start; - do { - if (this.getIntersections_(seg, rtree).length) { - return false; - } - seg = list.nextItem(); - } while (seg !== start); - return true; -}; - - -/** - * @private - * @param {module:ol/structs/LinkedList} list Linked list of the polygon. - * @return {boolean} Orientation is clockwise. - */ -WebGLPolygonReplay.prototype.isClockwise_ = function(list) { - const length = list.getLength() * 2; - const flatCoordinates = new Array(length); - const start = list.firstItem(); - let seg = start; - let i = 0; - do { - flatCoordinates[i++] = seg.p0.x; - flatCoordinates[i++] = seg.p0.y; - seg = list.nextItem(); - } while (seg !== start); - return linearRingIsClockwise(flatCoordinates, 0, length, 2); -}; - - -/** - * @private - * @param {module:ol/structs/LinkedList} list Linked list of the polygon. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - */ -WebGLPolygonReplay.prototype.splitPolygon_ = function(list, rtree) { - const start = list.firstItem(); - let s0 = start; - do { - const intersections = this.getIntersections_(s0, rtree); - if (intersections.length) { - const s1 = intersections[0]; - const n = this.vertices.length / 2; - const intersection = this.calculateIntersection_(s0.p0, - s0.p1, s1.p0, s1.p1); - const p = this.createPoint_(intersection[0], intersection[1], n); - const newPolygon = new LinkedList(); - const newRtree = new RBush(); - this.insertItem_(p, s0.p1, newPolygon, newRtree); - s0.p1 = p; - rtree.update([Math.min(s0.p0.x, p.x), Math.min(s0.p0.y, p.y), - Math.max(s0.p0.x, p.x), Math.max(s0.p0.y, p.y)], s0); - let currItem = list.nextItem(); - while (currItem !== s1) { - this.insertItem_(currItem.p0, currItem.p1, newPolygon, newRtree); - rtree.remove(currItem); - list.removeItem(); - currItem = list.getCurrItem(); - } - this.insertItem_(s1.p0, p, newPolygon, newRtree); - s1.p0 = p; - rtree.update([Math.min(s1.p1.x, p.x), Math.min(s1.p1.y, p.y), - Math.max(s1.p1.x, p.x), Math.max(s1.p1.y, p.y)], s1); - this.classifyPoints_(list, rtree, false); - this.triangulate_(list, rtree); - this.classifyPoints_(newPolygon, newRtree, false); - this.triangulate_(newPolygon, newRtree); - break; - } - s0 = list.nextItem(); - } while (s0 !== start); -}; - - -/** - * @private - * @param {number} x X coordinate. - * @param {number} y Y coordinate. - * @param {number} i Index. - * @return {module:ol/render/webgl/PolygonReplay~PolygonVertex} List item. - */ -WebGLPolygonReplay.prototype.createPoint_ = function(x, y, i) { - let numVertices = this.vertices.length; - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ - const p = { - x: x, - y: y, - i: i, - reflex: undefined - }; - return p; -}; - - -/** - * @private - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p0 First point of segment. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p1 Second point of segment. - * @param {module:ol/structs/LinkedList} list Polygon ring. - * @param {module:ol/structs/RBush=} opt_rtree Insert the segment into the R-Tree. - * @return {module:ol/render/webgl/PolygonReplay~PolygonSegment} segment. - */ -WebGLPolygonReplay.prototype.insertItem_ = function(p0, p1, list, opt_rtree) { - const seg = { - p0: p0, - p1: p1 - }; - list.insertItem(seg); - if (opt_rtree) { - opt_rtree.insert([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), - Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)], seg); + s0 = list.nextItem(); + } while (s0 !== start); } - return seg; -}; - -/** - * @private - * @param {module:ol/render/webgl/PolygonReplay~PolygonSegment} s0 Segment before the remove candidate. - * @param {module:ol/render/webgl/PolygonReplay~PolygonSegment} s1 Remove candidate segment. - * @param {module:ol/structs/LinkedList} list Polygon ring. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - */ -WebGLPolygonReplay.prototype.removeItem_ = function(s0, s1, list, rtree) { - if (list.getCurrItem() === s1) { - list.removeItem(); - s0.p1 = s1.p1; - rtree.remove(s1); - rtree.update([Math.min(s0.p0.x, s0.p1.x), Math.min(s0.p0.y, s0.p1.y), - Math.max(s0.p0.x, s0.p1.x), Math.max(s0.p0.y, s0.p1.y)], s0); + /** + * @private + * @param {number} x X coordinate. + * @param {number} y Y coordinate. + * @param {number} i Index. + * @return {module:ol/render/webgl/PolygonReplay~PolygonVertex} List item. + */ + createPoint_(x, y, i) { + let numVertices = this.vertices.length; + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + /** @type {module:ol/render/webgl/PolygonReplay~PolygonVertex} */ + const p = { + x: x, + y: y, + i: i, + reflex: undefined + }; + return p; } -}; + /** + * @private + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p0 First point of segment. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p1 Second point of segment. + * @param {module:ol/structs/LinkedList} list Polygon ring. + * @param {module:ol/structs/RBush=} opt_rtree Insert the segment into the R-Tree. + * @return {module:ol/render/webgl/PolygonReplay~PolygonSegment} segment. + */ + insertItem_(p0, p1, list, opt_rtree) { + const seg = { + p0: p0, + p1: p1 + }; + list.insertItem(seg); + if (opt_rtree) { + opt_rtree.insert([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), + Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)], seg); + } + return seg; + } -/** - * @private - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p0 First point. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p1 Second point. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p2 Third point. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - * @param {boolean=} opt_reflex Only include reflex points. - * @return {Array.} Points in the triangle. - */ -WebGLPolygonReplay.prototype.getPointsInTriangle_ = function(p0, p1, p2, rtree, opt_reflex) { - const result = []; - const segmentsInExtent = rtree.getInExtent([Math.min(p0.x, p1.x, p2.x), - Math.min(p0.y, p1.y, p2.y), Math.max(p0.x, p1.x, p2.x), Math.max(p0.y, - p1.y, p2.y)]); - for (let i = 0, ii = segmentsInExtent.length; i < ii; ++i) { - for (const j in segmentsInExtent[i]) { - const p = segmentsInExtent[i][j]; - if (typeof p === 'object' && (!opt_reflex || p.reflex)) { - if ((p.x !== p0.x || p.y !== p0.y) && (p.x !== p1.x || p.y !== p1.y) && - (p.x !== p2.x || p.y !== p2.y) && result.indexOf(p) === -1 && - linearRingContainsXY([p0.x, p0.y, p1.x, p1.y, p2.x, p2.y], 0, 6, 2, p.x, p.y)) { - result.push(p); + /** + * @private + * @param {module:ol/render/webgl/PolygonReplay~PolygonSegment} s0 Segment before the remove candidate. + * @param {module:ol/render/webgl/PolygonReplay~PolygonSegment} s1 Remove candidate segment. + * @param {module:ol/structs/LinkedList} list Polygon ring. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + */ + removeItem_(s0, s1, list, rtree) { + if (list.getCurrItem() === s1) { + list.removeItem(); + s0.p1 = s1.p1; + rtree.remove(s1); + rtree.update([Math.min(s0.p0.x, s0.p1.x), Math.min(s0.p0.y, s0.p1.y), + Math.max(s0.p0.x, s0.p1.x), Math.max(s0.p0.y, s0.p1.y)], s0); + } + } + + /** + * @private + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p0 First point. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p1 Second point. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p2 Third point. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + * @param {boolean=} opt_reflex Only include reflex points. + * @return {Array.} Points in the triangle. + */ + getPointsInTriangle_(p0, p1, p2, rtree, opt_reflex) { + const result = []; + const segmentsInExtent = rtree.getInExtent([Math.min(p0.x, p1.x, p2.x), + Math.min(p0.y, p1.y, p2.y), Math.max(p0.x, p1.x, p2.x), Math.max(p0.y, + p1.y, p2.y)]); + for (let i = 0, ii = segmentsInExtent.length; i < ii; ++i) { + for (const j in segmentsInExtent[i]) { + const p = segmentsInExtent[i][j]; + if (typeof p === 'object' && (!opt_reflex || p.reflex)) { + if ((p.x !== p0.x || p.y !== p0.y) && (p.x !== p1.x || p.y !== p1.y) && + (p.x !== p2.x || p.y !== p2.y) && result.indexOf(p) === -1 && + linearRingContainsXY([p0.x, p0.y, p1.x, p1.y, p2.x, p2.y], 0, 6, 2, p.x, p.y)) { + result.push(p); + } } } } + return result; } - return result; -}; + /** + * @private + * @param {module:ol/render/webgl/PolygonReplay~PolygonSegment} segment Segment. + * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. + * @param {boolean=} opt_touch Touching segments should be considered an intersection. + * @return {Array.} Intersecting segments. + */ + getIntersections_(segment, rtree, opt_touch) { + const p0 = segment.p0; + const p1 = segment.p1; + const segmentsInExtent = rtree.getInExtent([Math.min(p0.x, p1.x), + Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)]); + const result = []; + for (let i = 0, ii = segmentsInExtent.length; i < ii; ++i) { + const currSeg = segmentsInExtent[i]; + if (segment !== currSeg && (opt_touch || currSeg.p0 !== p1 || currSeg.p1 !== p0) && + this.calculateIntersection_(p0, p1, currSeg.p0, currSeg.p1, opt_touch)) { + result.push(currSeg); + } + } + return result; + } -/** - * @private - * @param {module:ol/render/webgl/PolygonReplay~PolygonSegment} segment Segment. - * @param {module:ol/structs/RBush} rtree R-Tree of the polygon. - * @param {boolean=} opt_touch Touching segments should be considered an intersection. - * @return {Array.} Intersecting segments. - */ -WebGLPolygonReplay.prototype.getIntersections_ = function(segment, rtree, opt_touch) { - const p0 = segment.p0; - const p1 = segment.p1; - const segmentsInExtent = rtree.getInExtent([Math.min(p0.x, p1.x), - Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)]); - const result = []; - for (let i = 0, ii = segmentsInExtent.length; i < ii; ++i) { - const currSeg = segmentsInExtent[i]; - if (segment !== currSeg && (opt_touch || currSeg.p0 !== p1 || currSeg.p1 !== p0) && - this.calculateIntersection_(p0, p1, currSeg.p0, currSeg.p1, opt_touch)) { - result.push(currSeg); + /** + * Line intersection algorithm by Paul Bourke. + * @see http://paulbourke.net/geometry/pointlineplane/ + * + * @private + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p0 First point. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p1 Second point. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p2 Third point. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p3 Fourth point. + * @param {boolean=} opt_touch Touching segments should be considered an intersection. + * @return {Array.|undefined} Intersection coordinates. + */ + calculateIntersection_(p0, p1, p2, p3, opt_touch) { + const denom = (p3.y - p2.y) * (p1.x - p0.x) - (p3.x - p2.x) * (p1.y - p0.y); + if (denom !== 0) { + const ua = ((p3.x - p2.x) * (p0.y - p2.y) - (p3.y - p2.y) * (p0.x - p2.x)) / denom; + const ub = ((p1.x - p0.x) * (p0.y - p2.y) - (p1.y - p0.y) * (p0.x - p2.x)) / denom; + if ((!opt_touch && ua > EPSILON && ua < 1 - EPSILON && + ub > EPSILON && ub < 1 - EPSILON) || (opt_touch && + ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1)) { + return [p0.x + ua * (p1.x - p0.x), p0.y + ua * (p1.y - p0.y)]; + } + } + return undefined; + } + + /** + * @private + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p0 Point before the start of the diagonal. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p1 Start point of the diagonal. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p2 Ear candidate. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p3 End point of the diagonal. + * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p4 Point after the end of the diagonal. + * @return {boolean} Diagonal is inside the polygon. + */ + diagonalIsInside_(p0, p1, p2, p3, p4) { + if (p1.reflex === undefined || p3.reflex === undefined) { + return false; + } + const p1IsLeftOf = (p2.x - p3.x) * (p1.y - p3.y) > (p2.y - p3.y) * (p1.x - p3.x); + const p1IsRightOf = (p4.x - p3.x) * (p1.y - p3.y) < (p4.y - p3.y) * (p1.x - p3.x); + const p3IsLeftOf = (p0.x - p1.x) * (p3.y - p1.y) > (p0.y - p1.y) * (p3.x - p1.x); + const p3IsRightOf = (p2.x - p1.x) * (p3.y - p1.y) < (p2.y - p1.y) * (p3.x - p1.x); + const p1InCone = p3.reflex ? p1IsRightOf || p1IsLeftOf : p1IsRightOf && p1IsLeftOf; + const p3InCone = p1.reflex ? p3IsRightOf || p3IsLeftOf : p3IsRightOf && p3IsLeftOf; + return p1InCone && p3InCone; + } + + /** + * @inheritDoc + */ + drawMultiPolygon(multiPolygonGeometry, feature) { + const endss = multiPolygonGeometry.getEndss(); + const stride = multiPolygonGeometry.getStride(); + const currIndex = this.indices.length; + const currLineIndex = this.lineStringReplay.getCurrentIndex(); + const flatCoordinates = multiPolygonGeometry.getFlatCoordinates(); + let i, ii, j, jj; + let start = 0; + for (i = 0, ii = endss.length; i < ii; ++i) { + const ends = endss[i]; + if (ends.length > 0) { + const outerRing = translate(flatCoordinates, start, ends[0], + stride, -this.origin[0], -this.origin[1]); + if (outerRing.length) { + const holes = []; + let holeFlatCoords; + for (j = 1, jj = ends.length; j < jj; ++j) { + if (ends[j] !== ends[j - 1]) { + holeFlatCoords = translate(flatCoordinates, ends[j - 1], + ends[j], stride, -this.origin[0], -this.origin[1]); + holes.push(holeFlatCoords); + } + } + this.lineStringReplay.drawPolygonCoordinates(outerRing, holes, stride); + this.drawCoordinates_(outerRing, holes, stride); + } + } + start = ends[ends.length - 1]; + } + if (this.indices.length > currIndex) { + this.startIndices.push(currIndex); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(currIndex); + this.state_.changed = false; + } + } + if (this.lineStringReplay.getCurrentIndex() > currLineIndex) { + this.lineStringReplay.setPolygonStyle(feature, currLineIndex); } } - return result; -}; - -/** - * Line intersection algorithm by Paul Bourke. - * @see http://paulbourke.net/geometry/pointlineplane/ - * - * @private - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p0 First point. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p1 Second point. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p2 Third point. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p3 Fourth point. - * @param {boolean=} opt_touch Touching segments should be considered an intersection. - * @return {Array.|undefined} Intersection coordinates. - */ -WebGLPolygonReplay.prototype.calculateIntersection_ = function(p0, p1, p2, p3, opt_touch) { - const denom = (p3.y - p2.y) * (p1.x - p0.x) - (p3.x - p2.x) * (p1.y - p0.y); - if (denom !== 0) { - const ua = ((p3.x - p2.x) * (p0.y - p2.y) - (p3.y - p2.y) * (p0.x - p2.x)) / denom; - const ub = ((p1.x - p0.x) * (p0.y - p2.y) - (p1.y - p0.y) * (p0.x - p2.x)) / denom; - if ((!opt_touch && ua > EPSILON && ua < 1 - EPSILON && - ub > EPSILON && ub < 1 - EPSILON) || (opt_touch && - ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1)) { - return [p0.x + ua * (p1.x - p0.x), p0.y + ua * (p1.y - p0.y)]; - } - } - return undefined; -}; - - -/** - * @private - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p0 Point before the start of the diagonal. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p1 Start point of the diagonal. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p2 Ear candidate. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p3 End point of the diagonal. - * @param {module:ol/render/webgl/PolygonReplay~PolygonVertex} p4 Point after the end of the diagonal. - * @return {boolean} Diagonal is inside the polygon. - */ -WebGLPolygonReplay.prototype.diagonalIsInside_ = function(p0, p1, p2, p3, p4) { - if (p1.reflex === undefined || p3.reflex === undefined) { - return false; - } - const p1IsLeftOf = (p2.x - p3.x) * (p1.y - p3.y) > (p2.y - p3.y) * (p1.x - p3.x); - const p1IsRightOf = (p4.x - p3.x) * (p1.y - p3.y) < (p4.y - p3.y) * (p1.x - p3.x); - const p3IsLeftOf = (p0.x - p1.x) * (p3.y - p1.y) > (p0.y - p1.y) * (p3.x - p1.x); - const p3IsRightOf = (p2.x - p1.x) * (p3.y - p1.y) < (p2.y - p1.y) * (p3.x - p1.x); - const p1InCone = p3.reflex ? p1IsRightOf || p1IsLeftOf : p1IsRightOf && p1IsLeftOf; - const p3InCone = p1.reflex ? p3IsRightOf || p3IsLeftOf : p3IsRightOf && p3IsLeftOf; - return p1InCone && p3InCone; -}; - - -/** - * @inheritDoc - */ -WebGLPolygonReplay.prototype.drawMultiPolygon = function(multiPolygonGeometry, feature) { - const endss = multiPolygonGeometry.getEndss(); - const stride = multiPolygonGeometry.getStride(); - const currIndex = this.indices.length; - const currLineIndex = this.lineStringReplay.getCurrentIndex(); - const flatCoordinates = multiPolygonGeometry.getFlatCoordinates(); - let i, ii, j, jj; - let start = 0; - for (i = 0, ii = endss.length; i < ii; ++i) { - const ends = endss[i]; + /** + * @inheritDoc + */ + drawPolygon(polygonGeometry, feature) { + const ends = polygonGeometry.getEnds(); + const stride = polygonGeometry.getStride(); if (ends.length > 0) { - const outerRing = translate(flatCoordinates, start, ends[0], + const flatCoordinates = polygonGeometry.getFlatCoordinates().map(Number); + const outerRing = translate(flatCoordinates, 0, ends[0], stride, -this.origin[0], -this.origin[1]); if (outerRing.length) { const holes = []; - let holeFlatCoords; - for (j = 1, jj = ends.length; j < jj; ++j) { - if (ends[j] !== ends[j - 1]) { - holeFlatCoords = translate(flatCoordinates, ends[j - 1], - ends[j], stride, -this.origin[0], -this.origin[1]); + let i, ii, holeFlatCoords; + for (i = 1, ii = ends.length; i < ii; ++i) { + if (ends[i] !== ends[i - 1]) { + holeFlatCoords = translate(flatCoordinates, ends[i - 1], + ends[i], stride, -this.origin[0], -this.origin[1]); holes.push(holeFlatCoords); } } + + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(this.indices.length); + this.state_.changed = false; + } + this.lineStringReplay.setPolygonStyle(feature); + this.lineStringReplay.drawPolygonCoordinates(outerRing, holes, stride); this.drawCoordinates_(outerRing, holes, stride); } } - start = ends[ends.length - 1]; } - if (this.indices.length > currIndex) { - this.startIndices.push(currIndex); - this.startIndicesFeature.push(feature); - if (this.state_.changed) { - this.styleIndices_.push(currIndex); - this.state_.changed = false; + + /** + * @inheritDoc + **/ + finish(context) { + // create, bind, and populate the vertices buffer + this.verticesBuffer = new WebGLBuffer(this.vertices); + + // create, bind, and populate the indices buffer + this.indicesBuffer = new WebGLBuffer(this.indices); + + this.startIndices.push(this.indices.length); + + this.lineStringReplay.finish(context); + + //Clean up, if there is nothing to draw + if (this.styleIndices_.length === 0 && this.styles_.length > 0) { + this.styles_ = []; + } + + this.vertices = null; + this.indices = null; + } + + /** + * @inheritDoc + */ + getDeleteResourcesFunction(context) { + const verticesBuffer = this.verticesBuffer; + const indicesBuffer = this.indicesBuffer; + const lineDeleter = this.lineStringReplay.getDeleteResourcesFunction(context); + return function() { + context.deleteBuffer(verticesBuffer); + context.deleteBuffer(indicesBuffer); + lineDeleter(); + }; + } + + /** + * @inheritDoc + */ + setUpProgram(gl, context, size, pixelRatio) { + // get the program + const program = context.getProgram(fragment, vertex); + + // get the locations + let locations; + if (!this.defaultLocations_) { + locations = new Locations(gl, program); + this.defaultLocations_ = locations; + } else { + locations = this.defaultLocations_; + } + + context.useProgram(program); + + // enable the vertex attrib arrays + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, FLOAT, + false, 8, 0); + + return locations; + } + + /** + * @inheritDoc + */ + shutDownProgram(gl, locations) { + gl.disableVertexAttribArray(locations.a_position); + } + + /** + * @inheritDoc + */ + drawReplay(gl, context, skippedFeaturesHash, hitDetection) { + //Save GL parameters. + const tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC)); + const tmpDepthMask = /** @type {boolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK)); + + if (!hitDetection) { + gl.enable(gl.DEPTH_TEST); + gl.depthMask(true); + gl.depthFunc(gl.NOTEQUAL); + } + + if (!isEmpty(skippedFeaturesHash)) { + this.drawReplaySkipping_(gl, context, skippedFeaturesHash); + } else { + //Draw by style groups to minimize drawElements() calls. + let i, start, end, nextStyle; + end = this.startIndices[this.startIndices.length - 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + start = this.styleIndices_[i]; + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, nextStyle); + this.drawElements(gl, context, start, end); + end = start; + } + } + if (!hitDetection) { + gl.disable(gl.DEPTH_TEST); + gl.clear(gl.DEPTH_BUFFER_BIT); + //Restore GL parameters. + gl.depthMask(tmpDepthMask); + gl.depthFunc(tmpDepthFunc); } } - if (this.lineStringReplay.getCurrentIndex() > currLineIndex) { - this.lineStringReplay.setPolygonStyle(feature, currLineIndex); - } -}; - -/** - * @inheritDoc - */ -WebGLPolygonReplay.prototype.drawPolygon = function(polygonGeometry, feature) { - const ends = polygonGeometry.getEnds(); - const stride = polygonGeometry.getStride(); - if (ends.length > 0) { - const flatCoordinates = polygonGeometry.getFlatCoordinates().map(Number); - const outerRing = translate(flatCoordinates, 0, ends[0], - stride, -this.origin[0], -this.origin[1]); - if (outerRing.length) { - const holes = []; - let i, ii, holeFlatCoords; - for (i = 1, ii = ends.length; i < ii; ++i) { - if (ends[i] !== ends[i - 1]) { - holeFlatCoords = translate(flatCoordinates, ends[i - 1], - ends[i], stride, -this.origin[0], -this.origin[1]); - holes.push(holeFlatCoords); - } - } - - this.startIndices.push(this.indices.length); - this.startIndicesFeature.push(feature); - if (this.state_.changed) { - this.styleIndices_.push(this.indices.length); - this.state_.changed = false; - } - this.lineStringReplay.setPolygonStyle(feature); - - this.lineStringReplay.drawPolygonCoordinates(outerRing, holes, stride); - this.drawCoordinates_(outerRing, holes, stride); - } - } -}; - - -/** - * @inheritDoc - **/ -WebGLPolygonReplay.prototype.finish = function(context) { - // create, bind, and populate the vertices buffer - this.verticesBuffer = new WebGLBuffer(this.vertices); - - // create, bind, and populate the indices buffer - this.indicesBuffer = new WebGLBuffer(this.indices); - - this.startIndices.push(this.indices.length); - - this.lineStringReplay.finish(context); - - //Clean up, if there is nothing to draw - if (this.styleIndices_.length === 0 && this.styles_.length > 0) { - this.styles_ = []; - } - - this.vertices = null; - this.indices = null; -}; - - -/** - * @inheritDoc - */ -WebGLPolygonReplay.prototype.getDeleteResourcesFunction = function(context) { - const verticesBuffer = this.verticesBuffer; - const indicesBuffer = this.indicesBuffer; - const lineDeleter = this.lineStringReplay.getDeleteResourcesFunction(context); - return function() { - context.deleteBuffer(verticesBuffer); - context.deleteBuffer(indicesBuffer); - lineDeleter(); - }; -}; - - -/** - * @inheritDoc - */ -WebGLPolygonReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { - // get the program - const program = context.getProgram(fragment, vertex); - - // get the locations - let locations; - if (!this.defaultLocations_) { - locations = new Locations(gl, program); - this.defaultLocations_ = locations; - } else { - locations = this.defaultLocations_; - } - - context.useProgram(program); - - // enable the vertex attrib arrays - gl.enableVertexAttribArray(locations.a_position); - gl.vertexAttribPointer(locations.a_position, 2, FLOAT, - false, 8, 0); - - return locations; -}; - - -/** - * @inheritDoc - */ -WebGLPolygonReplay.prototype.shutDownProgram = function(gl, locations) { - gl.disableVertexAttribArray(locations.a_position); -}; - - -/** - * @inheritDoc - */ -WebGLPolygonReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { - //Save GL parameters. - const tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC)); - const tmpDepthMask = /** @type {boolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK)); - - if (!hitDetection) { - gl.enable(gl.DEPTH_TEST); - gl.depthMask(true); - gl.depthFunc(gl.NOTEQUAL); - } - - if (!isEmpty(skippedFeaturesHash)) { - this.drawReplaySkipping_(gl, context, skippedFeaturesHash); - } else { - //Draw by style groups to minimize drawElements() calls. - let i, start, end, nextStyle; - end = this.startIndices[this.startIndices.length - 1]; + /** + * @inheritDoc + */ + drawHitDetectionReplayOneByOne(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) { + let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; + featureIndex = this.startIndices.length - 2; + end = this.startIndices[featureIndex + 1]; for (i = this.styleIndices_.length - 1; i >= 0; --i) { - start = this.styleIndices_[i]; nextStyle = this.styles_[i]; this.setFillStyle_(gl, nextStyle); - this.drawElements(gl, context, start, end); - end = start; - } - } - if (!hitDetection) { - gl.disable(gl.DEPTH_TEST); - gl.clear(gl.DEPTH_BUFFER_BIT); - //Restore GL parameters. - gl.depthMask(tmpDepthMask); - gl.depthFunc(tmpDepthFunc); - } -}; + groupStart = this.styleIndices_[i]; + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + start = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = getUid(feature).toString(); -/** - * @inheritDoc - */ -WebGLPolygonReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, - featureCallback, opt_hitExtent) { - let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; - featureIndex = this.startIndices.length - 2; - end = this.startIndices[featureIndex + 1]; - for (i = this.styleIndices_.length - 1; i >= 0; --i) { - nextStyle = this.styles_[i]; - this.setFillStyle_(gl, nextStyle); - groupStart = this.styleIndices_[i]; - - while (featureIndex >= 0 && - this.startIndices[featureIndex] >= groupStart) { - start = this.startIndices[featureIndex]; - feature = this.startIndicesFeature[featureIndex]; - featureUid = getUid(feature).toString(); - - if (skippedFeaturesHash[featureUid] === undefined && - feature.getGeometry() && - (opt_hitExtent === undefined || intersects( - /** @type {Array} */ (opt_hitExtent), - feature.getGeometry().getExtent()))) { - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.drawElements(gl, context, start, end); - - const result = featureCallback(feature); - - if (result) { - return result; - } - - } - featureIndex--; - end = start; - } - } - return undefined; -}; - - -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {Object} skippedFeaturesHash Ids of features to skip. - */ -WebGLPolygonReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) { - let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; - featureIndex = this.startIndices.length - 2; - end = start = this.startIndices[featureIndex + 1]; - for (i = this.styleIndices_.length - 1; i >= 0; --i) { - nextStyle = this.styles_[i]; - this.setFillStyle_(gl, nextStyle); - groupStart = this.styleIndices_[i]; - - while (featureIndex >= 0 && - this.startIndices[featureIndex] >= groupStart) { - featureStart = this.startIndices[featureIndex]; - feature = this.startIndicesFeature[featureIndex]; - featureUid = getUid(feature).toString(); - - if (skippedFeaturesHash[featureUid]) { - if (start !== end) { + if (skippedFeaturesHash[featureUid] === undefined && + feature.getGeometry() && + (opt_hitExtent === undefined || intersects( + /** @type {Array} */ (opt_hitExtent), + feature.getGeometry().getExtent()))) { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); this.drawElements(gl, context, start, end); - gl.clear(gl.DEPTH_BUFFER_BIT); + + const result = featureCallback(feature); + + if (result) { + return result; + } + } - end = featureStart; + featureIndex--; + end = start; } - featureIndex--; - start = featureStart; } - if (start !== end) { - this.drawElements(gl, context, start, end); - gl.clear(gl.DEPTH_BUFFER_BIT); + return undefined; + } + + /** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {Object} skippedFeaturesHash Ids of features to skip. + */ + drawReplaySkipping_(gl, context, skippedFeaturesHash) { + let i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; + featureIndex = this.startIndices.length - 2; + end = start = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, nextStyle); + groupStart = this.styleIndices_[i]; + + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + featureStart = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = getUid(feature).toString(); + + if (skippedFeaturesHash[featureUid]) { + if (start !== end) { + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + end = featureStart; + } + featureIndex--; + start = featureStart; + } + if (start !== end) { + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + start = end = groupStart; } - start = end = groupStart; } -}; + + /** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {Array.} color Color. + */ + setFillStyle_(gl, color) { + gl.uniform4fv(this.defaultLocations_.u_color, color); + } + + /** + * @inheritDoc + */ + setFillStrokeStyle(fillStyle, strokeStyle) { + let fillStyleColor = fillStyle ? fillStyle.getColor() : [0, 0, 0, 0]; + if (!(fillStyleColor instanceof CanvasGradient) && + !(fillStyleColor instanceof CanvasPattern)) { + fillStyleColor = asArray(fillStyleColor).map(function(c, i) { + return i != 3 ? c / 255 : c; + }) || DEFAULT_FILLSTYLE; + } else { + fillStyleColor = DEFAULT_FILLSTYLE; + } + if (!this.state_.fillColor || !equals(fillStyleColor, this.state_.fillColor)) { + this.state_.fillColor = fillStyleColor; + this.state_.changed = true; + this.styles_.push(fillStyleColor); + } + //Provide a null stroke style, if no strokeStyle is provided. Required for the draw interaction to work. + if (strokeStyle) { + this.lineStringReplay.setFillStrokeStyle(null, strokeStyle); + } else { + const nullStrokeStyle = new Stroke({ + color: [0, 0, 0, 0], + lineWidth: 0 + }); + this.lineStringReplay.setFillStrokeStyle(null, nullStrokeStyle); + } + } +} + +inherits(WebGLPolygonReplay, WebGLReplay); -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {Array.} color Color. - */ -WebGLPolygonReplay.prototype.setFillStyle_ = function(gl, color) { - gl.uniform4fv(this.defaultLocations_.u_color, color); -}; - - -/** - * @inheritDoc - */ -WebGLPolygonReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { - let fillStyleColor = fillStyle ? fillStyle.getColor() : [0, 0, 0, 0]; - if (!(fillStyleColor instanceof CanvasGradient) && - !(fillStyleColor instanceof CanvasPattern)) { - fillStyleColor = asArray(fillStyleColor).map(function(c, i) { - return i != 3 ? c / 255 : c; - }) || DEFAULT_FILLSTYLE; - } else { - fillStyleColor = DEFAULT_FILLSTYLE; - } - if (!this.state_.fillColor || !equals(fillStyleColor, this.state_.fillColor)) { - this.state_.fillColor = fillStyleColor; - this.state_.changed = true; - this.styles_.push(fillStyleColor); - } - //Provide a null stroke style, if no strokeStyle is provided. Required for the draw interaction to work. - if (strokeStyle) { - this.lineStringReplay.setFillStrokeStyle(null, strokeStyle); - } else { - const nullStrokeStyle = new Stroke({ - color: [0, 0, 0, 0], - lineWidth: 0 - }); - this.lineStringReplay.setFillStrokeStyle(null, nullStrokeStyle); - } -}; export default WebGLPolygonReplay; diff --git a/src/ol/render/webgl/Replay.js b/src/ol/render/webgl/Replay.js index 339b8eeefe..9f82854f69 100644 --- a/src/ol/render/webgl/Replay.js +++ b/src/ol/render/webgl/Replay.js @@ -23,342 +23,343 @@ import {ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, TRIANGLES, * @param {module:ol/extent~Extent} maxExtent Max extent. * @struct */ -const WebGLReplay = function(tolerance, maxExtent) { - VectorContext.call(this); +class WebGLReplay { + constructor(tolerance, maxExtent) { + VectorContext.call(this); - /** - * @protected - * @type {number} - */ - this.tolerance = tolerance; + /** + * @protected + * @type {number} + */ + this.tolerance = tolerance; - /** - * @protected - * @const - * @type {module:ol/extent~Extent} - */ - this.maxExtent = maxExtent; + /** + * @protected + * @const + * @type {module:ol/extent~Extent} + */ + this.maxExtent = maxExtent; - /** - * The origin of the coordinate system for the point coordinates sent to - * the GPU. To eliminate jitter caused by precision problems in the GPU - * we use the "Rendering Relative to Eye" technique described in the "3D - * Engine Design for Virtual Globes" book. - * @protected - * @type {module:ol/coordinate~Coordinate} - */ - this.origin = getCenter(maxExtent); + /** + * The origin of the coordinate system for the point coordinates sent to + * the GPU. To eliminate jitter caused by precision problems in the GPU + * we use the "Rendering Relative to Eye" technique described in the "3D + * Engine Design for Virtual Globes" book. + * @protected + * @type {module:ol/coordinate~Coordinate} + */ + this.origin = getCenter(maxExtent); - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.projectionMatrix_ = createTransform(); + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.projectionMatrix_ = createTransform(); - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.offsetRotateMatrix_ = createTransform(); + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.offsetRotateMatrix_ = createTransform(); - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.offsetScaleMatrix_ = createTransform(); + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.offsetScaleMatrix_ = createTransform(); - /** - * @private - * @type {Array.} - */ - this.tmpMat4_ = create(); + /** + * @private + * @type {Array.} + */ + this.tmpMat4_ = create(); - /** - * @protected - * @type {Array.} - */ - this.indices = []; + /** + * @protected + * @type {Array.} + */ + this.indices = []; - /** - * @protected - * @type {?module:ol/webgl/Buffer} - */ - this.indicesBuffer = null; + /** + * @protected + * @type {?module:ol/webgl/Buffer} + */ + this.indicesBuffer = null; - /** - * Start index per feature (the index). - * @protected - * @type {Array.} - */ - this.startIndices = []; + /** + * Start index per feature (the index). + * @protected + * @type {Array.} + */ + this.startIndices = []; - /** - * Start index per feature (the feature). - * @protected - * @type {Array.} - */ - this.startIndicesFeature = []; + /** + * Start index per feature (the feature). + * @protected + * @type {Array.} + */ + this.startIndicesFeature = []; - /** - * @protected - * @type {Array.} - */ - this.vertices = []; + /** + * @protected + * @type {Array.} + */ + this.vertices = []; - /** - * @protected - * @type {?module:ol/webgl/Buffer} - */ - this.verticesBuffer = null; + /** + * @protected + * @type {?module:ol/webgl/Buffer} + */ + this.verticesBuffer = null; - /** - * Optional parameter for PolygonReplay instances. - * @protected - * @type {module:ol/render/webgl/LineStringReplay|undefined} - */ - this.lineStringReplay = undefined; + /** + * Optional parameter for PolygonReplay instances. + * @protected + * @type {module:ol/render/webgl/LineStringReplay|undefined} + */ + this.lineStringReplay = undefined; -}; + } + + /** + * @abstract + * @param {module:ol/webgl/Context} context WebGL context. + * @return {function()} Delete resources function. + */ + getDeleteResourcesFunction(context) {} + + /** + * @abstract + * @param {module:ol/webgl/Context} context Context. + */ + finish(context) {} + + /** + * @abstract + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {module:ol/size~Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @return {module:ol/render/webgl/circlereplay/defaultshader/Locations| + module:ol/render/webgl/linestringreplay/defaultshader/Locations| + module:ol/render/webgl/polygonreplay/defaultshader/Locations| + module:ol/render/webgl/texturereplay/defaultshader/Locations} Locations. + */ + setUpProgram(gl, context, size, pixelRatio) {} + + /** + * @abstract + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/render/webgl/circlereplay/defaultshader/Locations| + module:ol/render/webgl/linestringreplay/defaultshader/Locations| + module:ol/render/webgl/polygonreplay/defaultshader/Locations| + module:ol/render/webgl/texturereplay/defaultshader/Locations} locations Locations. + */ + shutDownProgram(gl, locations) {} + + /** + * @abstract + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {boolean} hitDetection Hit detection mode. + */ + drawReplay(gl, context, skippedFeaturesHash, hitDetection) {} + + /** + * @abstract + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. + * @param {module:ol/extent~Extent=} opt_hitExtent Hit extent: Only features intersecting this extent are checked. + * @return {T|undefined} Callback result. + * @template T + */ + drawHitDetectionReplayOneByOne(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) {} + + /** + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. + * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. + * @param {module:ol/extent~Extent=} opt_hitExtent Hit extent: Only features intersecting + * this extent are checked. + * @return {T|undefined} Callback result. + * @template T + */ + drawHitDetectionReplay(gl, context, skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent) { + if (!oneByOne) { + // draw all hit-detection features in "once" (by texture group) + return this.drawHitDetectionReplayAll(gl, context, + skippedFeaturesHash, featureCallback); + } else { + // draw hit-detection features one by one + return this.drawHitDetectionReplayOneByOne(gl, context, + skippedFeaturesHash, featureCallback, opt_hitExtent); + } + } + + /** + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. + * @return {T|undefined} Callback result. + * @template T + */ + drawHitDetectionReplayAll(gl, context, skippedFeaturesHash, featureCallback) { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + this.drawReplay(gl, context, skippedFeaturesHash, true); + + const result = featureCallback(null); + if (result) { + return result; + } else { + return undefined; + } + } + + /** + * @param {module:ol/webgl/Context} context Context. + * @param {module:ol/coordinate~Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {module:ol/size~Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. + * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. + * @param {module:ol/extent~Extent=} opt_hitExtent Hit extent: Only features intersecting + * this extent are checked. + * @return {T|undefined} Callback result. + * @template T + */ + replay( + context, + center, + resolution, + rotation, + size, + pixelRatio, + opacity, + skippedFeaturesHash, + featureCallback, + oneByOne, + opt_hitExtent + ) { + const gl = context.getGL(); + let tmpStencil, tmpStencilFunc, tmpStencilMaskVal, tmpStencilRef, tmpStencilMask, + tmpStencilOpFail, tmpStencilOpPass, tmpStencilOpZFail; + + if (this.lineStringReplay) { + tmpStencil = gl.isEnabled(gl.STENCIL_TEST); + tmpStencilFunc = gl.getParameter(gl.STENCIL_FUNC); + tmpStencilMaskVal = gl.getParameter(gl.STENCIL_VALUE_MASK); + tmpStencilRef = gl.getParameter(gl.STENCIL_REF); + tmpStencilMask = gl.getParameter(gl.STENCIL_WRITEMASK); + tmpStencilOpFail = gl.getParameter(gl.STENCIL_FAIL); + tmpStencilOpPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS); + tmpStencilOpZFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL); + + gl.enable(gl.STENCIL_TEST); + gl.clear(gl.STENCIL_BUFFER_BIT); + gl.stencilMask(255); + gl.stencilFunc(gl.ALWAYS, 1, 255); + gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); + + this.lineStringReplay.replay(context, + center, resolution, rotation, size, pixelRatio, + opacity, skippedFeaturesHash, + featureCallback, oneByOne, opt_hitExtent); + + gl.stencilMask(0); + gl.stencilFunc(gl.NOTEQUAL, 1, 255); + } + + context.bindBuffer(ARRAY_BUFFER, this.verticesBuffer); + + context.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer); + + const locations = this.setUpProgram(gl, context, size, pixelRatio); + + // set the "uniform" values + const projectionMatrix = resetTransform(this.projectionMatrix_); + scaleTransform(projectionMatrix, 2 / (resolution * size[0]), 2 / (resolution * size[1])); + rotateTransform(projectionMatrix, -rotation); + translateTransform(projectionMatrix, -(center[0] - this.origin[0]), -(center[1] - this.origin[1])); + + const offsetScaleMatrix = resetTransform(this.offsetScaleMatrix_); + scaleTransform(offsetScaleMatrix, 2 / size[0], 2 / size[1]); + + const offsetRotateMatrix = resetTransform(this.offsetRotateMatrix_); + if (rotation !== 0) { + rotateTransform(offsetRotateMatrix, -rotation); + } + + gl.uniformMatrix4fv(locations.u_projectionMatrix, false, + fromTransform(this.tmpMat4_, projectionMatrix)); + gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, + fromTransform(this.tmpMat4_, offsetScaleMatrix)); + gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false, + fromTransform(this.tmpMat4_, offsetRotateMatrix)); + gl.uniform1f(locations.u_opacity, opacity); + + // draw! + let result; + if (featureCallback === undefined) { + this.drawReplay(gl, context, skippedFeaturesHash, false); + } else { + // draw feature by feature for the hit-detection + result = this.drawHitDetectionReplay(gl, context, skippedFeaturesHash, + featureCallback, oneByOne, opt_hitExtent); + } + + // disable the vertex attrib arrays + this.shutDownProgram(gl, locations); + + if (this.lineStringReplay) { + if (!tmpStencil) { + gl.disable(gl.STENCIL_TEST); + } + gl.clear(gl.STENCIL_BUFFER_BIT); + gl.stencilFunc(/** @type {number} */ (tmpStencilFunc), + /** @type {number} */ (tmpStencilRef), /** @type {number} */ (tmpStencilMaskVal)); + gl.stencilMask(/** @type {number} */ (tmpStencilMask)); + gl.stencilOp(/** @type {number} */ (tmpStencilOpFail), + /** @type {number} */ (tmpStencilOpZFail), /** @type {number} */ (tmpStencilOpPass)); + } + + return result; + } + + /** + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {number} start Start index. + * @param {number} end End index. + */ + drawElements(gl, context, start, end) { + const elementType = context.hasOESElementIndexUint ? + UNSIGNED_INT : UNSIGNED_SHORT; + const elementSize = context.hasOESElementIndexUint ? 4 : 2; + + const numItems = end - start; + const offsetInBytes = start * elementSize; + gl.drawElements(TRIANGLES, numItems, elementType, offsetInBytes); + } +} inherits(WebGLReplay, VectorContext); -/** - * @abstract - * @param {module:ol/webgl/Context} context WebGL context. - * @return {function()} Delete resources function. - */ -WebGLReplay.prototype.getDeleteResourcesFunction = function(context) {}; - - -/** - * @abstract - * @param {module:ol/webgl/Context} context Context. - */ -WebGLReplay.prototype.finish = function(context) {}; - - -/** - * @abstract - * @protected - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {module:ol/size~Size} size Size. - * @param {number} pixelRatio Pixel ratio. - * @return {module:ol/render/webgl/circlereplay/defaultshader/Locations| - module:ol/render/webgl/linestringreplay/defaultshader/Locations| - module:ol/render/webgl/polygonreplay/defaultshader/Locations| - module:ol/render/webgl/texturereplay/defaultshader/Locations} Locations. - */ -WebGLReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) {}; - - -/** - * @abstract - * @protected - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/render/webgl/circlereplay/defaultshader/Locations| - module:ol/render/webgl/linestringreplay/defaultshader/Locations| - module:ol/render/webgl/polygonreplay/defaultshader/Locations| - module:ol/render/webgl/texturereplay/defaultshader/Locations} locations Locations. - */ -WebGLReplay.prototype.shutDownProgram = function(gl, locations) {}; - - -/** - * @abstract - * @protected - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {boolean} hitDetection Hit detection mode. - */ -WebGLReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) {}; - - -/** - * @abstract - * @protected - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. - * @param {module:ol/extent~Extent=} opt_hitExtent Hit extent: Only features intersecting this extent are checked. - * @return {T|undefined} Callback result. - * @template T - */ -WebGLReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) {}; - - -/** - * @protected - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. - * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. - * @param {module:ol/extent~Extent=} opt_hitExtent Hit extent: Only features intersecting - * this extent are checked. - * @return {T|undefined} Callback result. - * @template T - */ -WebGLReplay.prototype.drawHitDetectionReplay = function(gl, context, skippedFeaturesHash, - featureCallback, oneByOne, opt_hitExtent) { - if (!oneByOne) { - // draw all hit-detection features in "once" (by texture group) - return this.drawHitDetectionReplayAll(gl, context, - skippedFeaturesHash, featureCallback); - } else { - // draw hit-detection features one by one - return this.drawHitDetectionReplayOneByOne(gl, context, - skippedFeaturesHash, featureCallback, opt_hitExtent); - } -}; - - -/** - * @protected - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. - * @return {T|undefined} Callback result. - * @template T - */ -WebGLReplay.prototype.drawHitDetectionReplayAll = function(gl, context, skippedFeaturesHash, - featureCallback) { - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.drawReplay(gl, context, skippedFeaturesHash, true); - - const result = featureCallback(null); - if (result) { - return result; - } else { - return undefined; - } -}; - - -/** - * @param {module:ol/webgl/Context} context Context. - * @param {module:ol/coordinate~Coordinate} center Center. - * @param {number} resolution Resolution. - * @param {number} rotation Rotation. - * @param {module:ol/size~Size} size Size. - * @param {number} pixelRatio Pixel ratio. - * @param {number} opacity Global opacity. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. - * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. - * @param {module:ol/extent~Extent=} opt_hitExtent Hit extent: Only features intersecting - * this extent are checked. - * @return {T|undefined} Callback result. - * @template T - */ -WebGLReplay.prototype.replay = function(context, - center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash, - featureCallback, oneByOne, opt_hitExtent) { - const gl = context.getGL(); - let tmpStencil, tmpStencilFunc, tmpStencilMaskVal, tmpStencilRef, tmpStencilMask, - tmpStencilOpFail, tmpStencilOpPass, tmpStencilOpZFail; - - if (this.lineStringReplay) { - tmpStencil = gl.isEnabled(gl.STENCIL_TEST); - tmpStencilFunc = gl.getParameter(gl.STENCIL_FUNC); - tmpStencilMaskVal = gl.getParameter(gl.STENCIL_VALUE_MASK); - tmpStencilRef = gl.getParameter(gl.STENCIL_REF); - tmpStencilMask = gl.getParameter(gl.STENCIL_WRITEMASK); - tmpStencilOpFail = gl.getParameter(gl.STENCIL_FAIL); - tmpStencilOpPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS); - tmpStencilOpZFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL); - - gl.enable(gl.STENCIL_TEST); - gl.clear(gl.STENCIL_BUFFER_BIT); - gl.stencilMask(255); - gl.stencilFunc(gl.ALWAYS, 1, 255); - gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); - - this.lineStringReplay.replay(context, - center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash, - featureCallback, oneByOne, opt_hitExtent); - - gl.stencilMask(0); - gl.stencilFunc(gl.NOTEQUAL, 1, 255); - } - - context.bindBuffer(ARRAY_BUFFER, this.verticesBuffer); - - context.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer); - - const locations = this.setUpProgram(gl, context, size, pixelRatio); - - // set the "uniform" values - const projectionMatrix = resetTransform(this.projectionMatrix_); - scaleTransform(projectionMatrix, 2 / (resolution * size[0]), 2 / (resolution * size[1])); - rotateTransform(projectionMatrix, -rotation); - translateTransform(projectionMatrix, -(center[0] - this.origin[0]), -(center[1] - this.origin[1])); - - const offsetScaleMatrix = resetTransform(this.offsetScaleMatrix_); - scaleTransform(offsetScaleMatrix, 2 / size[0], 2 / size[1]); - - const offsetRotateMatrix = resetTransform(this.offsetRotateMatrix_); - if (rotation !== 0) { - rotateTransform(offsetRotateMatrix, -rotation); - } - - gl.uniformMatrix4fv(locations.u_projectionMatrix, false, - fromTransform(this.tmpMat4_, projectionMatrix)); - gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, - fromTransform(this.tmpMat4_, offsetScaleMatrix)); - gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false, - fromTransform(this.tmpMat4_, offsetRotateMatrix)); - gl.uniform1f(locations.u_opacity, opacity); - - // draw! - let result; - if (featureCallback === undefined) { - this.drawReplay(gl, context, skippedFeaturesHash, false); - } else { - // draw feature by feature for the hit-detection - result = this.drawHitDetectionReplay(gl, context, skippedFeaturesHash, - featureCallback, oneByOne, opt_hitExtent); - } - - // disable the vertex attrib arrays - this.shutDownProgram(gl, locations); - - if (this.lineStringReplay) { - if (!tmpStencil) { - gl.disable(gl.STENCIL_TEST); - } - gl.clear(gl.STENCIL_BUFFER_BIT); - gl.stencilFunc(/** @type {number} */ (tmpStencilFunc), - /** @type {number} */ (tmpStencilRef), /** @type {number} */ (tmpStencilMaskVal)); - gl.stencilMask(/** @type {number} */ (tmpStencilMask)); - gl.stencilOp(/** @type {number} */ (tmpStencilOpFail), - /** @type {number} */ (tmpStencilOpZFail), /** @type {number} */ (tmpStencilOpPass)); - } - - return result; -}; - -/** - * @protected - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {number} start Start index. - * @param {number} end End index. - */ -WebGLReplay.prototype.drawElements = function( - gl, context, start, end) { - const elementType = context.hasOESElementIndexUint ? - UNSIGNED_INT : UNSIGNED_SHORT; - const elementSize = context.hasOESElementIndexUint ? 4 : 2; - - const numItems = end - start; - const offsetInBytes = start * elementSize; - gl.drawElements(TRIANGLES, numItems, elementType, offsetInBytes); -}; export default WebGLReplay; diff --git a/src/ol/render/webgl/ReplayGroup.js b/src/ol/render/webgl/ReplayGroup.js index 1589f03193..3bf6cc030a 100644 --- a/src/ol/render/webgl/ReplayGroup.js +++ b/src/ol/render/webgl/ReplayGroup.js @@ -40,281 +40,308 @@ const BATCH_CONSTRUCTORS = { * @param {number=} opt_renderBuffer Render buffer. * @struct */ -const WebGLReplayGroup = function(tolerance, maxExtent, opt_renderBuffer) { - ReplayGroup.call(this); +class WebGLReplayGroup { + constructor(tolerance, maxExtent, opt_renderBuffer) { + ReplayGroup.call(this); + + /** + * @type {module:ol/extent~Extent} + * @private + */ + this.maxExtent_ = maxExtent; + + /** + * @type {number} + * @private + */ + this.tolerance_ = tolerance; + + /** + * @type {number|undefined} + * @private + */ + this.renderBuffer_ = opt_renderBuffer; + + /** + * @private + * @type {!Object.>} + */ + this.replaysByZIndex_ = {}; + + } /** - * @type {module:ol/extent~Extent} - * @private + * @param {module:ol/style/Style} style Style. + * @param {boolean} group Group with previous replay. */ - this.maxExtent_ = maxExtent; + addDeclutter(style, group) {} /** - * @type {number} - * @private + * @param {module:ol/webgl/Context} context WebGL context. + * @return {function()} Delete resources function. */ - this.tolerance_ = tolerance; + getDeleteResourcesFunction(context) { + const functions = []; + let zKey; + for (zKey in this.replaysByZIndex_) { + const replays = this.replaysByZIndex_[zKey]; + for (const replayKey in replays) { + functions.push( + replays[replayKey].getDeleteResourcesFunction(context)); + } + } + return function() { + const length = functions.length; + let result; + for (let i = 0; i < length; i++) { + result = functions[i].apply(this, arguments); + } + return result; + }; + } /** - * @type {number|undefined} - * @private + * @param {module:ol/webgl/Context} context Context. */ - this.renderBuffer_ = opt_renderBuffer; + finish(context) { + let zKey; + for (zKey in this.replaysByZIndex_) { + const replays = this.replaysByZIndex_[zKey]; + for (const replayKey in replays) { + replays[replayKey].finish(context); + } + } + } + + /** + * @inheritDoc + */ + getReplay(zIndex, replayType) { + const zIndexKey = zIndex !== undefined ? zIndex.toString() : '0'; + let replays = this.replaysByZIndex_[zIndexKey]; + if (replays === undefined) { + replays = {}; + this.replaysByZIndex_[zIndexKey] = replays; + } + let replay = replays[replayType]; + if (replay === undefined) { + /** + * @type {Function} + */ + const Constructor = BATCH_CONSTRUCTORS[replayType]; + replay = new Constructor(this.tolerance_, this.maxExtent_); + replays[replayType] = replay; + } + return replay; + } + + /** + * @inheritDoc + */ + isEmpty() { + return isEmpty(this.replaysByZIndex_); + } + + /** + * @param {module:ol/webgl/Context} context Context. + * @param {module:ol/coordinate~Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {module:ol/size~Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + */ + replay( + context, + center, + resolution, + rotation, + size, + pixelRatio, + opacity, + skippedFeaturesHash + ) { + /** @type {Array.} */ + const zs = Object.keys(this.replaysByZIndex_).map(Number); + zs.sort(numberSafeCompareFunction); + + let i, ii, j, jj, replays, replay; + for (i = 0, ii = zs.length; i < ii; ++i) { + replays = this.replaysByZIndex_[zs[i].toString()]; + for (j = 0, jj = ORDER.length; j < jj; ++j) { + replay = replays[ORDER[j]]; + if (replay !== undefined) { + replay.replay(context, + center, resolution, rotation, size, pixelRatio, + opacity, skippedFeaturesHash, + undefined, false); + } + } + } + } /** * @private - * @type {!Object.>} + * @param {module:ol/webgl/Context} context Context. + * @param {module:ol/coordinate~Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {module:ol/size~Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. + * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. + * @param {module:ol/extent~Extent=} opt_hitExtent Hit extent: Only features intersecting + * this extent are checked. + * @return {T|undefined} Callback result. + * @template T */ - this.replaysByZIndex_ = {}; + replayHitDetection_( + context, + center, + resolution, + rotation, + size, + pixelRatio, + opacity, + skippedFeaturesHash, + featureCallback, + oneByOne, + opt_hitExtent + ) { + /** @type {Array.} */ + const zs = Object.keys(this.replaysByZIndex_).map(Number); + zs.sort(function(a, b) { + return b - a; + }); -}; + let i, ii, j, replays, replay, result; + for (i = 0, ii = zs.length; i < ii; ++i) { + replays = this.replaysByZIndex_[zs[i].toString()]; + for (j = ORDER.length - 1; j >= 0; --j) { + replay = replays[ORDER[j]]; + if (replay !== undefined) { + result = replay.replay(context, + center, resolution, rotation, size, pixelRatio, opacity, + skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent); + if (result) { + return result; + } + } + } + } + return undefined; + } + + /** + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {module:ol/webgl/Context} context Context. + * @param {module:ol/coordinate~Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {module:ol/size~Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} callback Feature callback. + * @return {T|undefined} Callback result. + * @template T + */ + forEachFeatureAtCoordinate( + coordinate, + context, + center, + resolution, + rotation, + size, + pixelRatio, + opacity, + skippedFeaturesHash, + callback + ) { + const gl = context.getGL(); + gl.bindFramebuffer( + gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); + + + /** + * @type {module:ol/extent~Extent} + */ + let hitExtent; + if (this.renderBuffer_ !== undefined) { + // build an extent around the coordinate, so that only features that + // intersect this extent are checked + hitExtent = buffer(createOrUpdateFromCoordinate(coordinate), resolution * this.renderBuffer_); + } + + return this.replayHitDetection_(context, + coordinate, resolution, rotation, HIT_DETECTION_SIZE, + pixelRatio, opacity, skippedFeaturesHash, + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + const imageData = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); + + if (imageData[3] > 0) { + const result = callback(feature); + if (result) { + return result; + } + } + }, true, hitExtent); + } + + /** + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {module:ol/webgl/Context} context Context. + * @param {module:ol/coordinate~Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {module:ol/size~Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {Object.} skippedFeaturesHash Ids of features to skip. + * @return {boolean} Is there a feature at the given coordinate? + */ + hasFeatureAtCoordinate( + coordinate, + context, + center, + resolution, + rotation, + size, + pixelRatio, + opacity, + skippedFeaturesHash + ) { + const gl = context.getGL(); + gl.bindFramebuffer( + gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); + + const hasFeature = this.replayHitDetection_(context, + coordinate, resolution, rotation, HIT_DETECTION_SIZE, + pixelRatio, opacity, skippedFeaturesHash, + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @return {boolean} Is there a feature? + */ + function(feature) { + const imageData = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); + return imageData[3] > 0; + }, false); + + return hasFeature !== undefined; + } +} inherits(WebGLReplayGroup, ReplayGroup); -/** - * @param {module:ol/style/Style} style Style. - * @param {boolean} group Group with previous replay. - */ -WebGLReplayGroup.prototype.addDeclutter = function(style, group) {}; - - -/** - * @param {module:ol/webgl/Context} context WebGL context. - * @return {function()} Delete resources function. - */ -WebGLReplayGroup.prototype.getDeleteResourcesFunction = function(context) { - const functions = []; - let zKey; - for (zKey in this.replaysByZIndex_) { - const replays = this.replaysByZIndex_[zKey]; - for (const replayKey in replays) { - functions.push( - replays[replayKey].getDeleteResourcesFunction(context)); - } - } - return function() { - const length = functions.length; - let result; - for (let i = 0; i < length; i++) { - result = functions[i].apply(this, arguments); - } - return result; - }; -}; - - -/** - * @param {module:ol/webgl/Context} context Context. - */ -WebGLReplayGroup.prototype.finish = function(context) { - let zKey; - for (zKey in this.replaysByZIndex_) { - const replays = this.replaysByZIndex_[zKey]; - for (const replayKey in replays) { - replays[replayKey].finish(context); - } - } -}; - - -/** - * @inheritDoc - */ -WebGLReplayGroup.prototype.getReplay = function(zIndex, replayType) { - const zIndexKey = zIndex !== undefined ? zIndex.toString() : '0'; - let replays = this.replaysByZIndex_[zIndexKey]; - if (replays === undefined) { - replays = {}; - this.replaysByZIndex_[zIndexKey] = replays; - } - let replay = replays[replayType]; - if (replay === undefined) { - /** - * @type {Function} - */ - const Constructor = BATCH_CONSTRUCTORS[replayType]; - replay = new Constructor(this.tolerance_, this.maxExtent_); - replays[replayType] = replay; - } - return replay; -}; - - -/** - * @inheritDoc - */ -WebGLReplayGroup.prototype.isEmpty = function() { - return isEmpty(this.replaysByZIndex_); -}; - - -/** - * @param {module:ol/webgl/Context} context Context. - * @param {module:ol/coordinate~Coordinate} center Center. - * @param {number} resolution Resolution. - * @param {number} rotation Rotation. - * @param {module:ol/size~Size} size Size. - * @param {number} pixelRatio Pixel ratio. - * @param {number} opacity Global opacity. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - */ -WebGLReplayGroup.prototype.replay = function(context, - center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash) { - /** @type {Array.} */ - const zs = Object.keys(this.replaysByZIndex_).map(Number); - zs.sort(numberSafeCompareFunction); - - let i, ii, j, jj, replays, replay; - for (i = 0, ii = zs.length; i < ii; ++i) { - replays = this.replaysByZIndex_[zs[i].toString()]; - for (j = 0, jj = ORDER.length; j < jj; ++j) { - replay = replays[ORDER[j]]; - if (replay !== undefined) { - replay.replay(context, - center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash, - undefined, false); - } - } - } -}; - - -/** - * @private - * @param {module:ol/webgl/Context} context Context. - * @param {module:ol/coordinate~Coordinate} center Center. - * @param {number} resolution Resolution. - * @param {number} rotation Rotation. - * @param {module:ol/size~Size} size Size. - * @param {number} pixelRatio Pixel ratio. - * @param {number} opacity Global opacity. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} featureCallback Feature callback. - * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. - * @param {module:ol/extent~Extent=} opt_hitExtent Hit extent: Only features intersecting - * this extent are checked. - * @return {T|undefined} Callback result. - * @template T - */ -WebGLReplayGroup.prototype.replayHitDetection_ = function(context, - center, resolution, rotation, size, pixelRatio, opacity, - skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent) { - /** @type {Array.} */ - const zs = Object.keys(this.replaysByZIndex_).map(Number); - zs.sort(function(a, b) { - return b - a; - }); - - let i, ii, j, replays, replay, result; - for (i = 0, ii = zs.length; i < ii; ++i) { - replays = this.replaysByZIndex_[zs[i].toString()]; - for (j = ORDER.length - 1; j >= 0; --j) { - replay = replays[ORDER[j]]; - if (replay !== undefined) { - result = replay.replay(context, - center, resolution, rotation, size, pixelRatio, opacity, - skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent); - if (result) { - return result; - } - } - } - } - return undefined; -}; - - -/** - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {module:ol/webgl/Context} context Context. - * @param {module:ol/coordinate~Coordinate} center Center. - * @param {number} resolution Resolution. - * @param {number} rotation Rotation. - * @param {module:ol/size~Size} size Size. - * @param {number} pixelRatio Pixel ratio. - * @param {number} opacity Global opacity. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @param {function((module:ol/Feature|module:ol/render/Feature)): T|undefined} callback Feature callback. - * @return {T|undefined} Callback result. - * @template T - */ -WebGLReplayGroup.prototype.forEachFeatureAtCoordinate = function( - coordinate, context, center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash, - callback) { - const gl = context.getGL(); - gl.bindFramebuffer( - gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); - - - /** - * @type {module:ol/extent~Extent} - */ - let hitExtent; - if (this.renderBuffer_ !== undefined) { - // build an extent around the coordinate, so that only features that - // intersect this extent are checked - hitExtent = buffer(createOrUpdateFromCoordinate(coordinate), resolution * this.renderBuffer_); - } - - return this.replayHitDetection_(context, - coordinate, resolution, rotation, HIT_DETECTION_SIZE, - pixelRatio, opacity, skippedFeaturesHash, - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - const imageData = new Uint8Array(4); - gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); - - if (imageData[3] > 0) { - const result = callback(feature); - if (result) { - return result; - } - } - }, true, hitExtent); -}; - - -/** - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {module:ol/webgl/Context} context Context. - * @param {module:ol/coordinate~Coordinate} center Center. - * @param {number} resolution Resolution. - * @param {number} rotation Rotation. - * @param {module:ol/size~Size} size Size. - * @param {number} pixelRatio Pixel ratio. - * @param {number} opacity Global opacity. - * @param {Object.} skippedFeaturesHash Ids of features to skip. - * @return {boolean} Is there a feature at the given coordinate? - */ -WebGLReplayGroup.prototype.hasFeatureAtCoordinate = function( - coordinate, context, center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash) { - const gl = context.getGL(); - gl.bindFramebuffer( - gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); - - const hasFeature = this.replayHitDetection_(context, - coordinate, resolution, rotation, HIT_DETECTION_SIZE, - pixelRatio, opacity, skippedFeaturesHash, - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @return {boolean} Is there a feature? - */ - function(feature) { - const imageData = new Uint8Array(4); - gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); - return imageData[3] > 0; - }, false); - - return hasFeature !== undefined; -}; - export default WebGLReplayGroup; diff --git a/src/ol/render/webgl/TextReplay.js b/src/ol/render/webgl/TextReplay.js index 13094b9946..7f4e9a37db 100644 --- a/src/ol/render/webgl/TextReplay.js +++ b/src/ol/render/webgl/TextReplay.js @@ -29,441 +29,435 @@ import WebGLBuffer from '../../webgl/Buffer.js'; * @param {module:ol/extent~Extent} maxExtent Max extent. * @struct */ -const WebGLTextReplay = function(tolerance, maxExtent) { - WebGLTextureReplay.call(this, tolerance, maxExtent); +class WebGLTextReplay { + constructor(tolerance, maxExtent) { + WebGLTextureReplay.call(this, tolerance, maxExtent); + + /** + * @private + * @type {Array.} + */ + this.images_ = []; + + /** + * @private + * @type {Array.} + */ + this.textures_ = []; + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.measureCanvas_ = createCanvasContext2D(0, 0).canvas; + + /** + * @private + * @type {{strokeColor: (module:ol/colorlike~ColorLike|null), + * lineCap: (string|undefined), + * lineDash: Array., + * lineDashOffset: (number|undefined), + * lineJoin: (string|undefined), + * lineWidth: number, + * miterLimit: (number|undefined), + * fillColor: (module:ol/colorlike~ColorLike|null), + * font: (string|undefined), + * scale: (number|undefined)}} + */ + this.state_ = { + strokeColor: null, + lineCap: undefined, + lineDash: null, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 0, + miterLimit: undefined, + fillColor: null, + font: undefined, + scale: undefined + }; + + /** + * @private + * @type {string} + */ + this.text_ = ''; + + /** + * @private + * @type {number|undefined} + */ + this.textAlign_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.textBaseline_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.offsetX_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.offsetY_ = undefined; + + /** + * @private + * @type {Object.} + */ + this.atlases_ = {}; + + /** + * @private + * @type {module:ol/render/webgl/TextReplay~GlyphAtlas|undefined} + */ + this.currAtlas_ = undefined; + + this.scale = 1; + + this.opacity = 1; + + } + + /** + * @inheritDoc + */ + drawText(geometry, feature) { + if (this.text_) { + let flatCoordinates = null; + const offset = 0; + let end = 2; + let stride = 2; + switch (geometry.getType()) { + case GeometryType.POINT: + case GeometryType.MULTI_POINT: + flatCoordinates = geometry.getFlatCoordinates(); + end = flatCoordinates.length; + stride = geometry.getStride(); + break; + case GeometryType.CIRCLE: + flatCoordinates = /** @type {module:ol/geom/Circle} */ (geometry).getCenter(); + break; + case GeometryType.LINE_STRING: + flatCoordinates = /** @type {module:ol/geom/LineString} */ (geometry).getFlatMidpoint(); + break; + case GeometryType.MULTI_LINE_STRING: + flatCoordinates = /** @type {module:ol/geom/MultiLineString} */ (geometry).getFlatMidpoints(); + end = flatCoordinates.length; + break; + case GeometryType.POLYGON: + flatCoordinates = /** @type {module:ol/geom/Polygon} */ (geometry).getFlatInteriorPoint(); + break; + case GeometryType.MULTI_POLYGON: + flatCoordinates = /** @type {module:ol/geom/MultiPolygon} */ (geometry).getFlatInteriorPoints(); + end = flatCoordinates.length; + break; + default: + } + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + + const glyphAtlas = this.currAtlas_; + const lines = this.text_.split('\n'); + const textSize = this.getTextSize_(lines); + let i, ii, j, jj, currX, currY, charArr, charInfo; + const anchorX = Math.round(textSize[0] * this.textAlign_ - this.offsetX_); + const anchorY = Math.round(textSize[1] * this.textBaseline_ - this.offsetY_); + const lineWidth = (this.state_.lineWidth / 2) * this.state_.scale; + + for (i = 0, ii = lines.length; i < ii; ++i) { + currX = 0; + currY = glyphAtlas.height * i; + charArr = lines[i].split(''); + + for (j = 0, jj = charArr.length; j < jj; ++j) { + charInfo = glyphAtlas.atlas.getInfo(charArr[j]); + + if (charInfo) { + const image = charInfo.image; + + this.anchorX = anchorX - currX; + this.anchorY = anchorY - currY; + this.originX = j === 0 ? charInfo.offsetX - lineWidth : charInfo.offsetX; + this.originY = charInfo.offsetY; + this.height = glyphAtlas.height; + this.width = j === 0 || j === charArr.length - 1 ? + glyphAtlas.width[charArr[j]] + lineWidth : glyphAtlas.width[charArr[j]]; + this.imageHeight = image.height; + this.imageWidth = image.width; + + if (this.images_.length === 0) { + this.images_.push(image); + } else { + const currentImage = this.images_[this.images_.length - 1]; + if (getUid(currentImage) != getUid(image)) { + this.groupIndices.push(this.indices.length); + this.images_.push(image); + } + } + + this.drawText_(flatCoordinates, offset, end, stride); + } + currX += this.width; + } + } + } + } /** * @private - * @type {Array.} + * @param {Array.} lines Label to draw split to lines. + * @return {Array.} Size of the label in pixels. */ - this.images_ = []; + getTextSize_(lines) { + const self = this; + const glyphAtlas = this.currAtlas_; + const textHeight = lines.length * glyphAtlas.height; + //Split every line to an array of chars, sum up their width, and select the longest. + const textWidth = lines.map(function(str) { + let sum = 0; + for (let i = 0, ii = str.length; i < ii; ++i) { + const curr = str[i]; + if (!glyphAtlas.width[curr]) { + self.addCharToAtlas_(curr); + } + sum += glyphAtlas.width[curr] ? glyphAtlas.width[curr] : 0; + } + return sum; + }).reduce(function(max, curr) { + return Math.max(max, curr); + }); + + return [textWidth, textHeight]; + } /** * @private - * @type {Array.} + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. */ - this.textures_ = []; + drawText_(flatCoordinates, offset, end, stride) { + for (let i = offset, ii = end; i < ii; i += stride) { + this.drawCoordinates(flatCoordinates, offset, end, stride); + } + } /** * @private - * @type {HTMLCanvasElement} + * @param {string} char Character. */ - this.measureCanvas_ = createCanvasContext2D(0, 0).canvas; + addCharToAtlas_(char) { + if (char.length === 1) { + const glyphAtlas = this.currAtlas_; + const state = this.state_; + const mCtx = this.measureCanvas_.getContext('2d'); + mCtx.font = state.font; + const width = Math.ceil(mCtx.measureText(char).width * state.scale); + + const info = glyphAtlas.atlas.add(char, width, glyphAtlas.height, + function(ctx, x, y) { + //Parameterize the canvas + ctx.font = /** @type {string} */ (state.font); + ctx.fillStyle = state.fillColor; + ctx.strokeStyle = state.strokeColor; + ctx.lineWidth = state.lineWidth; + ctx.lineCap = /*** @type {string} */ (state.lineCap); + ctx.lineJoin = /** @type {string} */ (state.lineJoin); + ctx.miterLimit = /** @type {number} */ (state.miterLimit); + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + if (CANVAS_LINE_DASH && state.lineDash) { + //FIXME: use pixelRatio + ctx.setLineDash(state.lineDash); + ctx.lineDashOffset = /** @type {number} */ (state.lineDashOffset); + } + if (state.scale !== 1) { + //FIXME: use pixelRatio + ctx.setTransform(/** @type {number} */ (state.scale), 0, 0, + /** @type {number} */ (state.scale), 0, 0); + } + + //Draw the character on the canvas + if (state.strokeColor) { + ctx.strokeText(char, x, y); + } + if (state.fillColor) { + ctx.fillText(char, x, y); + } + }); + + if (info) { + glyphAtlas.width[char] = width; + } + } + } + + /** + * @inheritDoc + */ + finish(context) { + const gl = context.getGL(); + + this.groupIndices.push(this.indices.length); + this.hitDetectionGroupIndices = this.groupIndices; + + // create, bind, and populate the vertices buffer + this.verticesBuffer = new WebGLBuffer(this.vertices); + + // create, bind, and populate the indices buffer + this.indicesBuffer = new WebGLBuffer(this.indices); + + // create textures + /** @type {Object.} */ + const texturePerImage = {}; + + this.createTextures(this.textures_, this.images_, texturePerImage, gl); + + this.state_ = { + strokeColor: null, + lineCap: undefined, + lineDash: null, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 0, + miterLimit: undefined, + fillColor: null, + font: undefined, + scale: undefined + }; + this.text_ = ''; + this.textAlign_ = undefined; + this.textBaseline_ = undefined; + this.offsetX_ = undefined; + this.offsetY_ = undefined; + this.images_ = null; + this.atlases_ = {}; + this.currAtlas_ = undefined; + WebGLTextureReplay.prototype.finish.call(this, context); + } + + /** + * @inheritDoc + */ + setTextStyle(textStyle) { + const state = this.state_; + const textFillStyle = textStyle.getFill(); + const textStrokeStyle = textStyle.getStroke(); + if (!textStyle || !textStyle.getText() || (!textFillStyle && !textStrokeStyle)) { + this.text_ = ''; + } else { + if (!textFillStyle) { + state.fillColor = null; + } else { + const textFillStyleColor = textFillStyle.getColor(); + state.fillColor = asColorLike(textFillStyleColor ? + textFillStyleColor : DEFAULT_FILLSTYLE); + } + if (!textStrokeStyle) { + state.strokeColor = null; + state.lineWidth = 0; + } else { + const textStrokeStyleColor = textStrokeStyle.getColor(); + state.strokeColor = asColorLike(textStrokeStyleColor ? + textStrokeStyleColor : DEFAULT_STROKESTYLE); + state.lineWidth = textStrokeStyle.getWidth() || DEFAULT_LINEWIDTH; + state.lineCap = textStrokeStyle.getLineCap() || DEFAULT_LINECAP; + state.lineDashOffset = textStrokeStyle.getLineDashOffset() || DEFAULT_LINEDASHOFFSET; + state.lineJoin = textStrokeStyle.getLineJoin() || DEFAULT_LINEJOIN; + state.miterLimit = textStrokeStyle.getMiterLimit() || DEFAULT_MITERLIMIT; + const lineDash = textStrokeStyle.getLineDash(); + state.lineDash = lineDash ? lineDash.slice() : DEFAULT_LINEDASH; + } + state.font = textStyle.getFont() || DEFAULT_FONT; + state.scale = textStyle.getScale() || 1; + this.text_ = /** @type {string} */ (textStyle.getText()); + const textAlign = TEXT_ALIGN[textStyle.getTextAlign()]; + const textBaseline = TEXT_ALIGN[textStyle.getTextBaseline()]; + this.textAlign_ = textAlign === undefined ? + DEFAULT_TEXTALIGN : textAlign; + this.textBaseline_ = textBaseline === undefined ? + DEFAULT_TEXTBASELINE : textBaseline; + this.offsetX_ = textStyle.getOffsetX() || 0; + this.offsetY_ = textStyle.getOffsetY() || 0; + this.rotateWithView = !!textStyle.getRotateWithView(); + this.rotation = textStyle.getRotation() || 0; + + this.currAtlas_ = this.getAtlas_(state); + } + } /** * @private - * @type {{strokeColor: (module:ol/colorlike~ColorLike|null), - * lineCap: (string|undefined), - * lineDash: Array., - * lineDashOffset: (number|undefined), - * lineJoin: (string|undefined), - * lineWidth: number, - * miterLimit: (number|undefined), - * fillColor: (module:ol/colorlike~ColorLike|null), - * font: (string|undefined), - * scale: (number|undefined)}} + * @param {Object} state Font attributes. + * @return {module:ol/render/webgl/TextReplay~GlyphAtlas} Glyph atlas. */ - this.state_ = { - strokeColor: null, - lineCap: undefined, - lineDash: null, - lineDashOffset: undefined, - lineJoin: undefined, - lineWidth: 0, - miterLimit: undefined, - fillColor: null, - font: undefined, - scale: undefined - }; + getAtlas_(state) { + let params = []; + for (const i in state) { + if (state[i] || state[i] === 0) { + if (Array.isArray(state[i])) { + params = params.concat(state[i]); + } else { + params.push(state[i]); + } + } + } + const hash = this.calculateHash_(params); + if (!this.atlases_[hash]) { + const mCtx = this.measureCanvas_.getContext('2d'); + mCtx.font = state.font; + const height = Math.ceil((mCtx.measureText('M').width * 1.5 + + state.lineWidth / 2) * state.scale); + + this.atlases_[hash] = { + atlas: new AtlasManager({ + space: state.lineWidth + 1 + }), + width: {}, + height: height + }; + } + return this.atlases_[hash]; + } /** * @private - * @type {string} + * @param {Array.} params Array of parameters. + * @return {string} Hash string. */ - this.text_ = ''; + calculateHash_(params) { + //TODO: Create a more performant, reliable, general hash function. + let hash = ''; + for (let i = 0, ii = params.length; i < ii; ++i) { + hash += params[i]; + } + return hash; + } /** - * @private - * @type {number|undefined} + * @inheritDoc */ - this.textAlign_ = undefined; + getTextures(opt_all) { + return this.textures_; + } /** - * @private - * @type {number|undefined} + * @inheritDoc */ - this.textBaseline_ = undefined; - - /** - * @private - * @type {number|undefined} - */ - this.offsetX_ = undefined; - - /** - * @private - * @type {number|undefined} - */ - this.offsetY_ = undefined; - - /** - * @private - * @type {Object.} - */ - this.atlases_ = {}; - - /** - * @private - * @type {module:ol/render/webgl/TextReplay~GlyphAtlas|undefined} - */ - this.currAtlas_ = undefined; - - this.scale = 1; - - this.opacity = 1; - -}; + getHitDetectionTextures() { + return this.textures_; + } +} inherits(WebGLTextReplay, WebGLTextureReplay); -/** - * @inheritDoc - */ -WebGLTextReplay.prototype.drawText = function(geometry, feature) { - if (this.text_) { - let flatCoordinates = null; - const offset = 0; - let end = 2; - let stride = 2; - switch (geometry.getType()) { - case GeometryType.POINT: - case GeometryType.MULTI_POINT: - flatCoordinates = geometry.getFlatCoordinates(); - end = flatCoordinates.length; - stride = geometry.getStride(); - break; - case GeometryType.CIRCLE: - flatCoordinates = /** @type {module:ol/geom/Circle} */ (geometry).getCenter(); - break; - case GeometryType.LINE_STRING: - flatCoordinates = /** @type {module:ol/geom/LineString} */ (geometry).getFlatMidpoint(); - break; - case GeometryType.MULTI_LINE_STRING: - flatCoordinates = /** @type {module:ol/geom/MultiLineString} */ (geometry).getFlatMidpoints(); - end = flatCoordinates.length; - break; - case GeometryType.POLYGON: - flatCoordinates = /** @type {module:ol/geom/Polygon} */ (geometry).getFlatInteriorPoint(); - break; - case GeometryType.MULTI_POLYGON: - flatCoordinates = /** @type {module:ol/geom/MultiPolygon} */ (geometry).getFlatInteriorPoints(); - end = flatCoordinates.length; - break; - default: - } - this.startIndices.push(this.indices.length); - this.startIndicesFeature.push(feature); - - const glyphAtlas = this.currAtlas_; - const lines = this.text_.split('\n'); - const textSize = this.getTextSize_(lines); - let i, ii, j, jj, currX, currY, charArr, charInfo; - const anchorX = Math.round(textSize[0] * this.textAlign_ - this.offsetX_); - const anchorY = Math.round(textSize[1] * this.textBaseline_ - this.offsetY_); - const lineWidth = (this.state_.lineWidth / 2) * this.state_.scale; - - for (i = 0, ii = lines.length; i < ii; ++i) { - currX = 0; - currY = glyphAtlas.height * i; - charArr = lines[i].split(''); - - for (j = 0, jj = charArr.length; j < jj; ++j) { - charInfo = glyphAtlas.atlas.getInfo(charArr[j]); - - if (charInfo) { - const image = charInfo.image; - - this.anchorX = anchorX - currX; - this.anchorY = anchorY - currY; - this.originX = j === 0 ? charInfo.offsetX - lineWidth : charInfo.offsetX; - this.originY = charInfo.offsetY; - this.height = glyphAtlas.height; - this.width = j === 0 || j === charArr.length - 1 ? - glyphAtlas.width[charArr[j]] + lineWidth : glyphAtlas.width[charArr[j]]; - this.imageHeight = image.height; - this.imageWidth = image.width; - - if (this.images_.length === 0) { - this.images_.push(image); - } else { - const currentImage = this.images_[this.images_.length - 1]; - if (getUid(currentImage) != getUid(image)) { - this.groupIndices.push(this.indices.length); - this.images_.push(image); - } - } - - this.drawText_(flatCoordinates, offset, end, stride); - } - currX += this.width; - } - } - } -}; - - -/** - * @private - * @param {Array.} lines Label to draw split to lines. - * @return {Array.} Size of the label in pixels. - */ -WebGLTextReplay.prototype.getTextSize_ = function(lines) { - const self = this; - const glyphAtlas = this.currAtlas_; - const textHeight = lines.length * glyphAtlas.height; - //Split every line to an array of chars, sum up their width, and select the longest. - const textWidth = lines.map(function(str) { - let sum = 0; - for (let i = 0, ii = str.length; i < ii; ++i) { - const curr = str[i]; - if (!glyphAtlas.width[curr]) { - self.addCharToAtlas_(curr); - } - sum += glyphAtlas.width[curr] ? glyphAtlas.width[curr] : 0; - } - return sum; - }).reduce(function(max, curr) { - return Math.max(max, curr); - }); - - return [textWidth, textHeight]; -}; - - -/** - * @private - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - */ -WebGLTextReplay.prototype.drawText_ = function(flatCoordinates, offset, end, stride) { - for (let i = offset, ii = end; i < ii; i += stride) { - this.drawCoordinates(flatCoordinates, offset, end, stride); - } -}; - - -/** - * @private - * @param {string} char Character. - */ -WebGLTextReplay.prototype.addCharToAtlas_ = function(char) { - if (char.length === 1) { - const glyphAtlas = this.currAtlas_; - const state = this.state_; - const mCtx = this.measureCanvas_.getContext('2d'); - mCtx.font = state.font; - const width = Math.ceil(mCtx.measureText(char).width * state.scale); - - const info = glyphAtlas.atlas.add(char, width, glyphAtlas.height, - function(ctx, x, y) { - //Parameterize the canvas - ctx.font = /** @type {string} */ (state.font); - ctx.fillStyle = state.fillColor; - ctx.strokeStyle = state.strokeColor; - ctx.lineWidth = state.lineWidth; - ctx.lineCap = /*** @type {string} */ (state.lineCap); - ctx.lineJoin = /** @type {string} */ (state.lineJoin); - ctx.miterLimit = /** @type {number} */ (state.miterLimit); - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - if (CANVAS_LINE_DASH && state.lineDash) { - //FIXME: use pixelRatio - ctx.setLineDash(state.lineDash); - ctx.lineDashOffset = /** @type {number} */ (state.lineDashOffset); - } - if (state.scale !== 1) { - //FIXME: use pixelRatio - ctx.setTransform(/** @type {number} */ (state.scale), 0, 0, - /** @type {number} */ (state.scale), 0, 0); - } - - //Draw the character on the canvas - if (state.strokeColor) { - ctx.strokeText(char, x, y); - } - if (state.fillColor) { - ctx.fillText(char, x, y); - } - }); - - if (info) { - glyphAtlas.width[char] = width; - } - } -}; - - -/** - * @inheritDoc - */ -WebGLTextReplay.prototype.finish = function(context) { - const gl = context.getGL(); - - this.groupIndices.push(this.indices.length); - this.hitDetectionGroupIndices = this.groupIndices; - - // create, bind, and populate the vertices buffer - this.verticesBuffer = new WebGLBuffer(this.vertices); - - // create, bind, and populate the indices buffer - this.indicesBuffer = new WebGLBuffer(this.indices); - - // create textures - /** @type {Object.} */ - const texturePerImage = {}; - - this.createTextures(this.textures_, this.images_, texturePerImage, gl); - - this.state_ = { - strokeColor: null, - lineCap: undefined, - lineDash: null, - lineDashOffset: undefined, - lineJoin: undefined, - lineWidth: 0, - miterLimit: undefined, - fillColor: null, - font: undefined, - scale: undefined - }; - this.text_ = ''; - this.textAlign_ = undefined; - this.textBaseline_ = undefined; - this.offsetX_ = undefined; - this.offsetY_ = undefined; - this.images_ = null; - this.atlases_ = {}; - this.currAtlas_ = undefined; - WebGLTextureReplay.prototype.finish.call(this, context); -}; - - -/** - * @inheritDoc - */ -WebGLTextReplay.prototype.setTextStyle = function(textStyle) { - const state = this.state_; - const textFillStyle = textStyle.getFill(); - const textStrokeStyle = textStyle.getStroke(); - if (!textStyle || !textStyle.getText() || (!textFillStyle && !textStrokeStyle)) { - this.text_ = ''; - } else { - if (!textFillStyle) { - state.fillColor = null; - } else { - const textFillStyleColor = textFillStyle.getColor(); - state.fillColor = asColorLike(textFillStyleColor ? - textFillStyleColor : DEFAULT_FILLSTYLE); - } - if (!textStrokeStyle) { - state.strokeColor = null; - state.lineWidth = 0; - } else { - const textStrokeStyleColor = textStrokeStyle.getColor(); - state.strokeColor = asColorLike(textStrokeStyleColor ? - textStrokeStyleColor : DEFAULT_STROKESTYLE); - state.lineWidth = textStrokeStyle.getWidth() || DEFAULT_LINEWIDTH; - state.lineCap = textStrokeStyle.getLineCap() || DEFAULT_LINECAP; - state.lineDashOffset = textStrokeStyle.getLineDashOffset() || DEFAULT_LINEDASHOFFSET; - state.lineJoin = textStrokeStyle.getLineJoin() || DEFAULT_LINEJOIN; - state.miterLimit = textStrokeStyle.getMiterLimit() || DEFAULT_MITERLIMIT; - const lineDash = textStrokeStyle.getLineDash(); - state.lineDash = lineDash ? lineDash.slice() : DEFAULT_LINEDASH; - } - state.font = textStyle.getFont() || DEFAULT_FONT; - state.scale = textStyle.getScale() || 1; - this.text_ = /** @type {string} */ (textStyle.getText()); - const textAlign = TEXT_ALIGN[textStyle.getTextAlign()]; - const textBaseline = TEXT_ALIGN[textStyle.getTextBaseline()]; - this.textAlign_ = textAlign === undefined ? - DEFAULT_TEXTALIGN : textAlign; - this.textBaseline_ = textBaseline === undefined ? - DEFAULT_TEXTBASELINE : textBaseline; - this.offsetX_ = textStyle.getOffsetX() || 0; - this.offsetY_ = textStyle.getOffsetY() || 0; - this.rotateWithView = !!textStyle.getRotateWithView(); - this.rotation = textStyle.getRotation() || 0; - - this.currAtlas_ = this.getAtlas_(state); - } -}; - - -/** - * @private - * @param {Object} state Font attributes. - * @return {module:ol/render/webgl/TextReplay~GlyphAtlas} Glyph atlas. - */ -WebGLTextReplay.prototype.getAtlas_ = function(state) { - let params = []; - for (const i in state) { - if (state[i] || state[i] === 0) { - if (Array.isArray(state[i])) { - params = params.concat(state[i]); - } else { - params.push(state[i]); - } - } - } - const hash = this.calculateHash_(params); - if (!this.atlases_[hash]) { - const mCtx = this.measureCanvas_.getContext('2d'); - mCtx.font = state.font; - const height = Math.ceil((mCtx.measureText('M').width * 1.5 + - state.lineWidth / 2) * state.scale); - - this.atlases_[hash] = { - atlas: new AtlasManager({ - space: state.lineWidth + 1 - }), - width: {}, - height: height - }; - } - return this.atlases_[hash]; -}; - - -/** - * @private - * @param {Array.} params Array of parameters. - * @return {string} Hash string. - */ -WebGLTextReplay.prototype.calculateHash_ = function(params) { - //TODO: Create a more performant, reliable, general hash function. - let hash = ''; - for (let i = 0, ii = params.length; i < ii; ++i) { - hash += params[i]; - } - return hash; -}; - - -/** - * @inheritDoc - */ -WebGLTextReplay.prototype.getTextures = function(opt_all) { - return this.textures_; -}; - - -/** - * @inheritDoc - */ -WebGLTextReplay.prototype.getHitDetectionTextures = function() { - return this.textures_; -}; export default WebGLTextReplay; diff --git a/src/ol/render/webgl/TextureReplay.js b/src/ol/render/webgl/TextureReplay.js index c98812d1fc..3ce621d0b3 100644 --- a/src/ol/render/webgl/TextureReplay.js +++ b/src/ol/render/webgl/TextureReplay.js @@ -18,476 +18,467 @@ import {createTexture} from '../../webgl/Context.js'; * @param {module:ol/extent~Extent} maxExtent Max extent. * @struct */ -const WebGLTextureReplay = function(tolerance, maxExtent) { - WebGLReplay.call(this, tolerance, maxExtent); +class WebGLTextureReplay { + constructor(tolerance, maxExtent) { + WebGLReplay.call(this, tolerance, maxExtent); + + /** + * @type {number|undefined} + * @protected + */ + this.anchorX = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.anchorY = undefined; + + /** + * @type {Array.} + * @protected + */ + this.groupIndices = []; + + /** + * @type {Array.} + * @protected + */ + this.hitDetectionGroupIndices = []; + + /** + * @type {number|undefined} + * @protected + */ + this.height = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.imageHeight = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.imageWidth = undefined; + + /** + * @protected + * @type {module:ol/render/webgl/texturereplay/defaultshader/Locations} + */ + this.defaultLocations = null; + + /** + * @protected + * @type {number|undefined} + */ + this.opacity = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.originX = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.originY = undefined; + + /** + * @protected + * @type {boolean|undefined} + */ + this.rotateWithView = undefined; + + /** + * @protected + * @type {number|undefined} + */ + this.rotation = undefined; + + /** + * @protected + * @type {number|undefined} + */ + this.scale = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.width = undefined; + } /** - * @type {number|undefined} - * @protected + * @inheritDoc */ - this.anchorX = undefined; + getDeleteResourcesFunction(context) { + const verticesBuffer = this.verticesBuffer; + const indicesBuffer = this.indicesBuffer; + const textures = this.getTextures(true); + const gl = context.getGL(); + return function() { + if (!gl.isContextLost()) { + let i, ii; + for (i = 0, ii = textures.length; i < ii; ++i) { + gl.deleteTexture(textures[i]); + } + } + context.deleteBuffer(verticesBuffer); + context.deleteBuffer(indicesBuffer); + }; + } /** - * @type {number|undefined} + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @return {number} My end. * @protected */ - this.anchorY = undefined; + drawCoordinates(flatCoordinates, offset, end, stride) { + const anchorX = /** @type {number} */ (this.anchorX); + const anchorY = /** @type {number} */ (this.anchorY); + const height = /** @type {number} */ (this.height); + const imageHeight = /** @type {number} */ (this.imageHeight); + const imageWidth = /** @type {number} */ (this.imageWidth); + const opacity = /** @type {number} */ (this.opacity); + const originX = /** @type {number} */ (this.originX); + const originY = /** @type {number} */ (this.originY); + const rotateWithView = this.rotateWithView ? 1.0 : 0.0; + // this.rotation_ is anti-clockwise, but rotation is clockwise + const rotation = /** @type {number} */ (-this.rotation); + const scale = /** @type {number} */ (this.scale); + const width = /** @type {number} */ (this.width); + const cos = Math.cos(rotation); + const sin = Math.sin(rotation); + let numIndices = this.indices.length; + let numVertices = this.vertices.length; + let i, n, offsetX, offsetY, x, y; + for (i = offset; i < end; i += stride) { + x = flatCoordinates[i] - this.origin[0]; + y = flatCoordinates[i + 1] - this.origin[1]; - /** - * @type {Array.} - * @protected - */ - this.groupIndices = []; + // There are 4 vertices per [x, y] point, one for each corner of the + // rectangle we're going to draw. We'd use 1 vertex per [x, y] point if + // WebGL supported Geometry Shaders (which can emit new vertices), but that + // is not currently the case. + // + // And each vertex includes 8 values: the x and y coordinates, the x and + // y offsets used to calculate the position of the corner, the u and + // v texture coordinates for the corner, the opacity, and whether the + // the image should be rotated with the view (rotateWithView). - /** - * @type {Array.} - * @protected - */ - this.hitDetectionGroupIndices = []; + n = numVertices / 8; - /** - * @type {number|undefined} - * @protected - */ - this.height = undefined; + // bottom-left corner + offsetX = -scale * anchorX; + offsetY = -scale * (height - anchorY); + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices[numVertices++] = offsetX * sin + offsetY * cos; + this.vertices[numVertices++] = originX / imageWidth; + this.vertices[numVertices++] = (originY + height) / imageHeight; + this.vertices[numVertices++] = opacity; + this.vertices[numVertices++] = rotateWithView; - /** - * @type {number|undefined} - * @protected - */ - this.imageHeight = undefined; + // bottom-right corner + offsetX = scale * (width - anchorX); + offsetY = -scale * (height - anchorY); + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices[numVertices++] = offsetX * sin + offsetY * cos; + this.vertices[numVertices++] = (originX + width) / imageWidth; + this.vertices[numVertices++] = (originY + height) / imageHeight; + this.vertices[numVertices++] = opacity; + this.vertices[numVertices++] = rotateWithView; - /** - * @type {number|undefined} - * @protected - */ - this.imageWidth = undefined; + // top-right corner + offsetX = scale * (width - anchorX); + offsetY = scale * anchorY; + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices[numVertices++] = offsetX * sin + offsetY * cos; + this.vertices[numVertices++] = (originX + width) / imageWidth; + this.vertices[numVertices++] = originY / imageHeight; + this.vertices[numVertices++] = opacity; + this.vertices[numVertices++] = rotateWithView; + + // top-left corner + offsetX = -scale * anchorX; + offsetY = scale * anchorY; + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices[numVertices++] = offsetX * sin + offsetY * cos; + this.vertices[numVertices++] = originX / imageWidth; + this.vertices[numVertices++] = originY / imageHeight; + this.vertices[numVertices++] = opacity; + this.vertices[numVertices++] = rotateWithView; + + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n + 3; + } + + return numVertices; + } /** * @protected - * @type {module:ol/render/webgl/texturereplay/defaultshader/Locations} + * @param {Array.} textures Textures. + * @param {Array.} images Images. + * @param {!Object.} texturePerImage Texture cache. + * @param {WebGLRenderingContext} gl Gl. */ - this.defaultLocations = null; + createTextures(textures, images, texturePerImage, gl) { + let texture, image, uid, i; + const ii = images.length; + for (i = 0; i < ii; ++i) { + image = images[i]; + + uid = getUid(image).toString(); + if (uid in texturePerImage) { + texture = texturePerImage[uid]; + } else { + texture = createTexture( + gl, image, CLAMP_TO_EDGE, CLAMP_TO_EDGE); + texturePerImage[uid] = texture; + } + textures[i] = texture; + } + } /** - * @protected - * @type {number|undefined} + * @inheritDoc */ - this.opacity = undefined; + setUpProgram(gl, context, size, pixelRatio) { + // get the program + const program = context.getProgram(fragment, vertex); + + // get the locations + let locations; + if (!this.defaultLocations) { + locations = new Locations(gl, program); + this.defaultLocations = locations; + } else { + locations = this.defaultLocations; + } + + // use the program (FIXME: use the return value) + context.useProgram(program); + + // enable the vertex attrib arrays + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, FLOAT, + false, 32, 0); + + gl.enableVertexAttribArray(locations.a_offsets); + gl.vertexAttribPointer(locations.a_offsets, 2, FLOAT, + false, 32, 8); + + gl.enableVertexAttribArray(locations.a_texCoord); + gl.vertexAttribPointer(locations.a_texCoord, 2, FLOAT, + false, 32, 16); + + gl.enableVertexAttribArray(locations.a_opacity); + gl.vertexAttribPointer(locations.a_opacity, 1, FLOAT, + false, 32, 24); + + gl.enableVertexAttribArray(locations.a_rotateWithView); + gl.vertexAttribPointer(locations.a_rotateWithView, 1, FLOAT, + false, 32, 28); + + return locations; + } /** - * @type {number|undefined} - * @protected + * @inheritDoc */ - this.originX = undefined; + shutDownProgram(gl, locations) { + gl.disableVertexAttribArray(locations.a_position); + gl.disableVertexAttribArray(locations.a_offsets); + gl.disableVertexAttribArray(locations.a_texCoord); + gl.disableVertexAttribArray(locations.a_opacity); + gl.disableVertexAttribArray(locations.a_rotateWithView); + } /** - * @type {number|undefined} - * @protected + * @inheritDoc */ - this.originY = undefined; + drawReplay(gl, context, skippedFeaturesHash, hitDetection) { + const textures = hitDetection ? this.getHitDetectionTextures() : this.getTextures(); + const groupIndices = hitDetection ? this.hitDetectionGroupIndices : this.groupIndices; + + if (!isEmpty(skippedFeaturesHash)) { + this.drawReplaySkipping(gl, context, skippedFeaturesHash, textures, groupIndices); + } else { + let i, ii, start; + for (i = 0, ii = textures.length, start = 0; i < ii; ++i) { + gl.bindTexture(TEXTURE_2D, textures[i]); + const end = groupIndices[i]; + this.drawElements(gl, context, start, end); + start = end; + } + } + } /** + * Draw the replay while paying attention to skipped features. + * + * This functions creates groups of features that can be drawn to together, + * so that the number of `drawElements` calls is minimized. + * + * For example given the following texture groups: + * + * Group 1: A B C + * Group 2: D [E] F G + * + * If feature E should be skipped, the following `drawElements` calls will be + * made: + * + * drawElements with feature A, B and C + * drawElements with feature D + * drawElements with feature F and G + * * @protected - * @type {boolean|undefined} + * @param {WebGLRenderingContext} gl gl. + * @param {module:ol/webgl/Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features + * to skip. + * @param {Array.} textures Textures. + * @param {Array.} groupIndices Texture group indices. */ - this.rotateWithView = undefined; + drawReplaySkipping(gl, context, skippedFeaturesHash, textures, groupIndices) { + let featureIndex = 0; + + let i, ii; + for (i = 0, ii = textures.length; i < ii; ++i) { + gl.bindTexture(TEXTURE_2D, textures[i]); + const groupStart = (i > 0) ? groupIndices[i - 1] : 0; + const groupEnd = groupIndices[i]; + + let start = groupStart; + let end = groupStart; + while (featureIndex < this.startIndices.length && + this.startIndices[featureIndex] <= groupEnd) { + const feature = this.startIndicesFeature[featureIndex]; + + const featureUid = getUid(feature).toString(); + if (skippedFeaturesHash[featureUid] !== undefined) { + // feature should be skipped + if (start !== end) { + // draw the features so far + this.drawElements(gl, context, start, end); + } + // continue with the next feature + start = (featureIndex === this.startIndices.length - 1) ? + groupEnd : this.startIndices[featureIndex + 1]; + end = start; + } else { + // the feature is not skipped, augment the end index + end = (featureIndex === this.startIndices.length - 1) ? + groupEnd : this.startIndices[featureIndex + 1]; + } + featureIndex++; + } + + if (start !== end) { + // draw the remaining features (in case there was no skipped feature + // in this texture group, all features of a group are drawn together) + this.drawElements(gl, context, start, end); + } + } + } /** - * @protected - * @type {number|undefined} + * @inheritDoc */ - this.rotation = undefined; + drawHitDetectionReplayOneByOne(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) { + let i, groupStart, start, end, feature, featureUid; + let featureIndex = this.startIndices.length - 1; + const hitDetectionTextures = this.getHitDetectionTextures(); + for (i = hitDetectionTextures.length - 1; i >= 0; --i) { + gl.bindTexture(TEXTURE_2D, hitDetectionTextures[i]); + groupStart = (i > 0) ? this.hitDetectionGroupIndices[i - 1] : 0; + end = this.hitDetectionGroupIndices[i]; + + // draw all features for this texture group + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + start = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = getUid(feature).toString(); + + if (skippedFeaturesHash[featureUid] === undefined && + feature.getGeometry() && + (opt_hitExtent === undefined || intersects( + /** @type {Array} */ (opt_hitExtent), + feature.getGeometry().getExtent()))) { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + this.drawElements(gl, context, start, end); + + const result = featureCallback(feature); + if (result) { + return result; + } + } + + end = start; + featureIndex--; + } + } + return undefined; + } /** - * @protected - * @type {number|undefined} + * @inheritDoc */ - this.scale = undefined; + finish(context) { + this.anchorX = undefined; + this.anchorY = undefined; + this.height = undefined; + this.imageHeight = undefined; + this.imageWidth = undefined; + this.indices = null; + this.opacity = undefined; + this.originX = undefined; + this.originY = undefined; + this.rotateWithView = undefined; + this.rotation = undefined; + this.scale = undefined; + this.vertices = null; + this.width = undefined; + } /** - * @type {number|undefined} + * @abstract * @protected + * @param {boolean=} opt_all Return hit detection textures with regular ones. + * @returns {Array.} Textures. */ - this.width = undefined; -}; + getTextures(opt_all) {} + + /** + * @abstract + * @protected + * @returns {Array.} Textures. + */ + getHitDetectionTextures() {} +} inherits(WebGLTextureReplay, WebGLReplay); -/** - * @inheritDoc - */ -WebGLTextureReplay.prototype.getDeleteResourcesFunction = function(context) { - const verticesBuffer = this.verticesBuffer; - const indicesBuffer = this.indicesBuffer; - const textures = this.getTextures(true); - const gl = context.getGL(); - return function() { - if (!gl.isContextLost()) { - let i, ii; - for (i = 0, ii = textures.length; i < ii; ++i) { - gl.deleteTexture(textures[i]); - } - } - context.deleteBuffer(verticesBuffer); - context.deleteBuffer(indicesBuffer); - }; -}; - - -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @return {number} My end. - * @protected - */ -WebGLTextureReplay.prototype.drawCoordinates = function(flatCoordinates, offset, end, stride) { - const anchorX = /** @type {number} */ (this.anchorX); - const anchorY = /** @type {number} */ (this.anchorY); - const height = /** @type {number} */ (this.height); - const imageHeight = /** @type {number} */ (this.imageHeight); - const imageWidth = /** @type {number} */ (this.imageWidth); - const opacity = /** @type {number} */ (this.opacity); - const originX = /** @type {number} */ (this.originX); - const originY = /** @type {number} */ (this.originY); - const rotateWithView = this.rotateWithView ? 1.0 : 0.0; - // this.rotation_ is anti-clockwise, but rotation is clockwise - const rotation = /** @type {number} */ (-this.rotation); - const scale = /** @type {number} */ (this.scale); - const width = /** @type {number} */ (this.width); - const cos = Math.cos(rotation); - const sin = Math.sin(rotation); - let numIndices = this.indices.length; - let numVertices = this.vertices.length; - let i, n, offsetX, offsetY, x, y; - for (i = offset; i < end; i += stride) { - x = flatCoordinates[i] - this.origin[0]; - y = flatCoordinates[i + 1] - this.origin[1]; - - // There are 4 vertices per [x, y] point, one for each corner of the - // rectangle we're going to draw. We'd use 1 vertex per [x, y] point if - // WebGL supported Geometry Shaders (which can emit new vertices), but that - // is not currently the case. - // - // And each vertex includes 8 values: the x and y coordinates, the x and - // y offsets used to calculate the position of the corner, the u and - // v texture coordinates for the corner, the opacity, and whether the - // the image should be rotated with the view (rotateWithView). - - n = numVertices / 8; - - // bottom-left corner - offsetX = -scale * anchorX; - offsetY = -scale * (height - anchorY); - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = offsetX * cos - offsetY * sin; - this.vertices[numVertices++] = offsetX * sin + offsetY * cos; - this.vertices[numVertices++] = originX / imageWidth; - this.vertices[numVertices++] = (originY + height) / imageHeight; - this.vertices[numVertices++] = opacity; - this.vertices[numVertices++] = rotateWithView; - - // bottom-right corner - offsetX = scale * (width - anchorX); - offsetY = -scale * (height - anchorY); - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = offsetX * cos - offsetY * sin; - this.vertices[numVertices++] = offsetX * sin + offsetY * cos; - this.vertices[numVertices++] = (originX + width) / imageWidth; - this.vertices[numVertices++] = (originY + height) / imageHeight; - this.vertices[numVertices++] = opacity; - this.vertices[numVertices++] = rotateWithView; - - // top-right corner - offsetX = scale * (width - anchorX); - offsetY = scale * anchorY; - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = offsetX * cos - offsetY * sin; - this.vertices[numVertices++] = offsetX * sin + offsetY * cos; - this.vertices[numVertices++] = (originX + width) / imageWidth; - this.vertices[numVertices++] = originY / imageHeight; - this.vertices[numVertices++] = opacity; - this.vertices[numVertices++] = rotateWithView; - - // top-left corner - offsetX = -scale * anchorX; - offsetY = scale * anchorY; - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = offsetX * cos - offsetY * sin; - this.vertices[numVertices++] = offsetX * sin + offsetY * cos; - this.vertices[numVertices++] = originX / imageWidth; - this.vertices[numVertices++] = originY / imageHeight; - this.vertices[numVertices++] = opacity; - this.vertices[numVertices++] = rotateWithView; - - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 1; - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n + 3; - } - - return numVertices; -}; - - -/** - * @protected - * @param {Array.} textures Textures. - * @param {Array.} images Images. - * @param {!Object.} texturePerImage Texture cache. - * @param {WebGLRenderingContext} gl Gl. - */ -WebGLTextureReplay.prototype.createTextures = function(textures, images, texturePerImage, gl) { - let texture, image, uid, i; - const ii = images.length; - for (i = 0; i < ii; ++i) { - image = images[i]; - - uid = getUid(image).toString(); - if (uid in texturePerImage) { - texture = texturePerImage[uid]; - } else { - texture = createTexture( - gl, image, CLAMP_TO_EDGE, CLAMP_TO_EDGE); - texturePerImage[uid] = texture; - } - textures[i] = texture; - } -}; - - -/** - * @inheritDoc - */ -WebGLTextureReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { - // get the program - const program = context.getProgram(fragment, vertex); - - // get the locations - let locations; - if (!this.defaultLocations) { - locations = new Locations(gl, program); - this.defaultLocations = locations; - } else { - locations = this.defaultLocations; - } - - // use the program (FIXME: use the return value) - context.useProgram(program); - - // enable the vertex attrib arrays - gl.enableVertexAttribArray(locations.a_position); - gl.vertexAttribPointer(locations.a_position, 2, FLOAT, - false, 32, 0); - - gl.enableVertexAttribArray(locations.a_offsets); - gl.vertexAttribPointer(locations.a_offsets, 2, FLOAT, - false, 32, 8); - - gl.enableVertexAttribArray(locations.a_texCoord); - gl.vertexAttribPointer(locations.a_texCoord, 2, FLOAT, - false, 32, 16); - - gl.enableVertexAttribArray(locations.a_opacity); - gl.vertexAttribPointer(locations.a_opacity, 1, FLOAT, - false, 32, 24); - - gl.enableVertexAttribArray(locations.a_rotateWithView); - gl.vertexAttribPointer(locations.a_rotateWithView, 1, FLOAT, - false, 32, 28); - - return locations; -}; - - -/** - * @inheritDoc - */ -WebGLTextureReplay.prototype.shutDownProgram = function(gl, locations) { - gl.disableVertexAttribArray(locations.a_position); - gl.disableVertexAttribArray(locations.a_offsets); - gl.disableVertexAttribArray(locations.a_texCoord); - gl.disableVertexAttribArray(locations.a_opacity); - gl.disableVertexAttribArray(locations.a_rotateWithView); -}; - - -/** - * @inheritDoc - */ -WebGLTextureReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { - const textures = hitDetection ? this.getHitDetectionTextures() : this.getTextures(); - const groupIndices = hitDetection ? this.hitDetectionGroupIndices : this.groupIndices; - - if (!isEmpty(skippedFeaturesHash)) { - this.drawReplaySkipping(gl, context, skippedFeaturesHash, textures, groupIndices); - } else { - let i, ii, start; - for (i = 0, ii = textures.length, start = 0; i < ii; ++i) { - gl.bindTexture(TEXTURE_2D, textures[i]); - const end = groupIndices[i]; - this.drawElements(gl, context, start, end); - start = end; - } - } -}; - - -/** - * Draw the replay while paying attention to skipped features. - * - * This functions creates groups of features that can be drawn to together, - * so that the number of `drawElements` calls is minimized. - * - * For example given the following texture groups: - * - * Group 1: A B C - * Group 2: D [E] F G - * - * If feature E should be skipped, the following `drawElements` calls will be - * made: - * - * drawElements with feature A, B and C - * drawElements with feature D - * drawElements with feature F and G - * - * @protected - * @param {WebGLRenderingContext} gl gl. - * @param {module:ol/webgl/Context} context Context. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - * @param {Array.} textures Textures. - * @param {Array.} groupIndices Texture group indices. - */ -WebGLTextureReplay.prototype.drawReplaySkipping = function(gl, context, skippedFeaturesHash, textures, - groupIndices) { - let featureIndex = 0; - - let i, ii; - for (i = 0, ii = textures.length; i < ii; ++i) { - gl.bindTexture(TEXTURE_2D, textures[i]); - const groupStart = (i > 0) ? groupIndices[i - 1] : 0; - const groupEnd = groupIndices[i]; - - let start = groupStart; - let end = groupStart; - while (featureIndex < this.startIndices.length && - this.startIndices[featureIndex] <= groupEnd) { - const feature = this.startIndicesFeature[featureIndex]; - - const featureUid = getUid(feature).toString(); - if (skippedFeaturesHash[featureUid] !== undefined) { - // feature should be skipped - if (start !== end) { - // draw the features so far - this.drawElements(gl, context, start, end); - } - // continue with the next feature - start = (featureIndex === this.startIndices.length - 1) ? - groupEnd : this.startIndices[featureIndex + 1]; - end = start; - } else { - // the feature is not skipped, augment the end index - end = (featureIndex === this.startIndices.length - 1) ? - groupEnd : this.startIndices[featureIndex + 1]; - } - featureIndex++; - } - - if (start !== end) { - // draw the remaining features (in case there was no skipped feature - // in this texture group, all features of a group are drawn together) - this.drawElements(gl, context, start, end); - } - } -}; - - -/** - * @inheritDoc - */ -WebGLTextureReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, - featureCallback, opt_hitExtent) { - let i, groupStart, start, end, feature, featureUid; - let featureIndex = this.startIndices.length - 1; - const hitDetectionTextures = this.getHitDetectionTextures(); - for (i = hitDetectionTextures.length - 1; i >= 0; --i) { - gl.bindTexture(TEXTURE_2D, hitDetectionTextures[i]); - groupStart = (i > 0) ? this.hitDetectionGroupIndices[i - 1] : 0; - end = this.hitDetectionGroupIndices[i]; - - // draw all features for this texture group - while (featureIndex >= 0 && - this.startIndices[featureIndex] >= groupStart) { - start = this.startIndices[featureIndex]; - feature = this.startIndicesFeature[featureIndex]; - featureUid = getUid(feature).toString(); - - if (skippedFeaturesHash[featureUid] === undefined && - feature.getGeometry() && - (opt_hitExtent === undefined || intersects( - /** @type {Array} */ (opt_hitExtent), - feature.getGeometry().getExtent()))) { - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.drawElements(gl, context, start, end); - - const result = featureCallback(feature); - if (result) { - return result; - } - } - - end = start; - featureIndex--; - } - } - return undefined; -}; - - -/** - * @inheritDoc - */ -WebGLTextureReplay.prototype.finish = function(context) { - this.anchorX = undefined; - this.anchorY = undefined; - this.height = undefined; - this.imageHeight = undefined; - this.imageWidth = undefined; - this.indices = null; - this.opacity = undefined; - this.originX = undefined; - this.originY = undefined; - this.rotateWithView = undefined; - this.rotation = undefined; - this.scale = undefined; - this.vertices = null; - this.width = undefined; -}; - - -/** - * @abstract - * @protected - * @param {boolean=} opt_all Return hit detection textures with regular ones. - * @returns {Array.} Textures. - */ -WebGLTextureReplay.prototype.getTextures = function(opt_all) {}; - - -/** - * @abstract - * @protected - * @returns {Array.} Textures. - */ -WebGLTextureReplay.prototype.getHitDetectionTextures = function() {}; export default WebGLTextureReplay; diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index 5e23741585..f6014665f3 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -16,18 +16,210 @@ import SourceState from '../source/State.js'; * @param {module:ol/layer/Layer} layer Layer. * @struct */ -const LayerRenderer = function(layer) { +class LayerRenderer { + constructor(layer) { - Observable.call(this); + Observable.call(this); + + /** + * @private + * @type {module:ol/layer/Layer} + */ + this.layer_ = layer; + + + } /** - * @private - * @type {module:ol/layer/Layer} + * Create a function that adds loaded tiles to the tile lookup. + * @param {module:ol/source/Tile} source Tile source. + * @param {module:ol/proj/Projection} projection Projection of the tiles. + * @param {Object.>} tiles Lookup of loaded tiles by zoom level. + * @return {function(number, module:ol/TileRange):boolean} A function that can be + * called with a zoom level and a tile range to add loaded tiles to the lookup. + * @protected */ - this.layer_ = layer; + createLoadedTileFinder(source, projection, tiles) { + return ( + /** + * @param {number} zoom Zoom level. + * @param {module:ol/TileRange} tileRange Tile range. + * @return {boolean} The tile range is fully loaded. + */ + function(zoom, tileRange) { + function callback(tile) { + if (!tiles[zoom]) { + tiles[zoom] = {}; + } + tiles[zoom][tile.tileCoord.toString()] = tile; + } + return source.forEachLoadedTile(projection, zoom, tileRange, callback); + } + ); + } + /** + * @return {module:ol/layer/Layer} Layer. + */ + getLayer() { + return this.layer_; + } -}; + /** + * Handle changes in image state. + * @param {module:ol/events/Event} event Image change event. + * @private + */ + handleImageChange_(event) { + const image = /** @type {module:ol/Image} */ (event.target); + if (image.getState() === ImageState.LOADED) { + this.renderIfReadyAndVisible(); + } + } + + /** + * Load the image if not already loaded, and register the image change + * listener if needed. + * @param {module:ol/ImageBase} image Image. + * @return {boolean} `true` if the image is already loaded, `false` otherwise. + * @protected + */ + loadImage(image) { + let imageState = image.getState(); + if (imageState != ImageState.LOADED && imageState != ImageState.ERROR) { + listen(image, EventType.CHANGE, this.handleImageChange_, this); + } + if (imageState == ImageState.IDLE) { + image.load(); + imageState = image.getState(); + } + return imageState == ImageState.LOADED; + } + + /** + * @protected + */ + renderIfReadyAndVisible() { + const layer = this.getLayer(); + if (layer.getVisible() && layer.getSourceState() == SourceState.READY) { + this.changed(); + } + } + + /** + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/source/Tile} tileSource Tile source. + * @protected + */ + scheduleExpireCache(frameState, tileSource) { + if (tileSource.canExpireCache()) { + /** + * @param {module:ol/source/Tile} tileSource Tile source. + * @param {module:ol/PluggableMap} map Map. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + */ + const postRenderFunction = function(tileSource, map, frameState) { + const tileSourceKey = getUid(tileSource).toString(); + if (tileSourceKey in frameState.usedTiles) { + tileSource.expireCache(frameState.viewState.projection, + frameState.usedTiles[tileSourceKey]); + } + }.bind(null, tileSource); + + frameState.postRenderFunctions.push( + /** @type {module:ol/PluggableMap~PostRenderFunction} */ (postRenderFunction) + ); + } + } + + /** + * @param {!Object.>} usedTiles Used tiles. + * @param {module:ol/source/Tile} tileSource Tile source. + * @param {number} z Z. + * @param {module:ol/TileRange} tileRange Tile range. + * @protected + */ + updateUsedTiles(usedTiles, tileSource, z, tileRange) { + // FIXME should we use tilesToDrawByZ instead? + const tileSourceKey = getUid(tileSource).toString(); + const zKey = z.toString(); + if (tileSourceKey in usedTiles) { + if (zKey in usedTiles[tileSourceKey]) { + usedTiles[tileSourceKey][zKey].extend(tileRange); + } else { + usedTiles[tileSourceKey][zKey] = tileRange; + } + } else { + usedTiles[tileSourceKey] = {}; + usedTiles[tileSourceKey][zKey] = tileRange; + } + } + + /** + * Manage tile pyramid. + * This function performs a number of functions related to the tiles at the + * current zoom and lower zoom levels: + * - registers idle tiles in frameState.wantedTiles so that they are not + * discarded by the tile queue + * - enqueues missing tiles + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/source/Tile} tileSource Tile source. + * @param {module:ol/tilegrid/TileGrid} tileGrid Tile grid. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @param {module:ol/extent~Extent} extent Extent. + * @param {number} currentZ Current Z. + * @param {number} preload Load low resolution tiles up to 'preload' levels. + * @param {function(this: T, module:ol/Tile)=} opt_tileCallback Tile callback. + * @param {T=} opt_this Object to use as `this` in `opt_tileCallback`. + * @protected + * @template T + */ + manageTilePyramid( + frameState, + tileSource, + tileGrid, + pixelRatio, + projection, + extent, + currentZ, + preload, + opt_tileCallback, + opt_this + ) { + const tileSourceKey = getUid(tileSource).toString(); + if (!(tileSourceKey in frameState.wantedTiles)) { + frameState.wantedTiles[tileSourceKey] = {}; + } + const wantedTiles = frameState.wantedTiles[tileSourceKey]; + const tileQueue = frameState.tileQueue; + const minZoom = tileGrid.getMinZoom(); + let tile, tileRange, tileResolution, x, y, z; + for (z = minZoom; z <= currentZ; ++z) { + tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z, tileRange); + tileResolution = tileGrid.getResolution(z); + for (x = tileRange.minX; x <= tileRange.maxX; ++x) { + for (y = tileRange.minY; y <= tileRange.maxY; ++y) { + if (currentZ - z <= preload) { + tile = tileSource.getTile(z, x, y, pixelRatio, projection); + if (tile.getState() == TileState.IDLE) { + wantedTiles[tile.getKey()] = true; + if (!tileQueue.isKeyQueued(tile.getKey())) { + tileQueue.enqueue([tile, tileSourceKey, + tileGrid.getTileCoordCenter(tile.tileCoord), tileResolution]); + } + } + if (opt_tileCallback !== undefined) { + opt_tileCallback.call(opt_this, tile); + } + } else { + tileSource.useTile(z, x, y, projection); + } + } + } + } + } +} inherits(LayerRenderer, Observable); @@ -53,191 +245,4 @@ LayerRenderer.prototype.forEachFeatureAtCoordinate = UNDEFINED; LayerRenderer.prototype.hasFeatureAtCoordinate = FALSE; -/** - * Create a function that adds loaded tiles to the tile lookup. - * @param {module:ol/source/Tile} source Tile source. - * @param {module:ol/proj/Projection} projection Projection of the tiles. - * @param {Object.>} tiles Lookup of loaded tiles by zoom level. - * @return {function(number, module:ol/TileRange):boolean} A function that can be - * called with a zoom level and a tile range to add loaded tiles to the lookup. - * @protected - */ -LayerRenderer.prototype.createLoadedTileFinder = function(source, projection, tiles) { - return ( - /** - * @param {number} zoom Zoom level. - * @param {module:ol/TileRange} tileRange Tile range. - * @return {boolean} The tile range is fully loaded. - */ - function(zoom, tileRange) { - function callback(tile) { - if (!tiles[zoom]) { - tiles[zoom] = {}; - } - tiles[zoom][tile.tileCoord.toString()] = tile; - } - return source.forEachLoadedTile(projection, zoom, tileRange, callback); - } - ); -}; - - -/** - * @return {module:ol/layer/Layer} Layer. - */ -LayerRenderer.prototype.getLayer = function() { - return this.layer_; -}; - - -/** - * Handle changes in image state. - * @param {module:ol/events/Event} event Image change event. - * @private - */ -LayerRenderer.prototype.handleImageChange_ = function(event) { - const image = /** @type {module:ol/Image} */ (event.target); - if (image.getState() === ImageState.LOADED) { - this.renderIfReadyAndVisible(); - } -}; - - -/** - * Load the image if not already loaded, and register the image change - * listener if needed. - * @param {module:ol/ImageBase} image Image. - * @return {boolean} `true` if the image is already loaded, `false` otherwise. - * @protected - */ -LayerRenderer.prototype.loadImage = function(image) { - let imageState = image.getState(); - if (imageState != ImageState.LOADED && imageState != ImageState.ERROR) { - listen(image, EventType.CHANGE, this.handleImageChange_, this); - } - if (imageState == ImageState.IDLE) { - image.load(); - imageState = image.getState(); - } - return imageState == ImageState.LOADED; -}; - - -/** - * @protected - */ -LayerRenderer.prototype.renderIfReadyAndVisible = function() { - const layer = this.getLayer(); - if (layer.getVisible() && layer.getSourceState() == SourceState.READY) { - this.changed(); - } -}; - - -/** - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/source/Tile} tileSource Tile source. - * @protected - */ -LayerRenderer.prototype.scheduleExpireCache = function(frameState, tileSource) { - if (tileSource.canExpireCache()) { - /** - * @param {module:ol/source/Tile} tileSource Tile source. - * @param {module:ol/PluggableMap} map Map. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - */ - const postRenderFunction = function(tileSource, map, frameState) { - const tileSourceKey = getUid(tileSource).toString(); - if (tileSourceKey in frameState.usedTiles) { - tileSource.expireCache(frameState.viewState.projection, - frameState.usedTiles[tileSourceKey]); - } - }.bind(null, tileSource); - - frameState.postRenderFunctions.push( - /** @type {module:ol/PluggableMap~PostRenderFunction} */ (postRenderFunction) - ); - } -}; - - -/** - * @param {!Object.>} usedTiles Used tiles. - * @param {module:ol/source/Tile} tileSource Tile source. - * @param {number} z Z. - * @param {module:ol/TileRange} tileRange Tile range. - * @protected - */ -LayerRenderer.prototype.updateUsedTiles = function(usedTiles, tileSource, z, tileRange) { - // FIXME should we use tilesToDrawByZ instead? - const tileSourceKey = getUid(tileSource).toString(); - const zKey = z.toString(); - if (tileSourceKey in usedTiles) { - if (zKey in usedTiles[tileSourceKey]) { - usedTiles[tileSourceKey][zKey].extend(tileRange); - } else { - usedTiles[tileSourceKey][zKey] = tileRange; - } - } else { - usedTiles[tileSourceKey] = {}; - usedTiles[tileSourceKey][zKey] = tileRange; - } -}; - - -/** - * Manage tile pyramid. - * This function performs a number of functions related to the tiles at the - * current zoom and lower zoom levels: - * - registers idle tiles in frameState.wantedTiles so that they are not - * discarded by the tile queue - * - enqueues missing tiles - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/source/Tile} tileSource Tile source. - * @param {module:ol/tilegrid/TileGrid} tileGrid Tile grid. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @param {module:ol/extent~Extent} extent Extent. - * @param {number} currentZ Current Z. - * @param {number} preload Load low resolution tiles up to 'preload' levels. - * @param {function(this: T, module:ol/Tile)=} opt_tileCallback Tile callback. - * @param {T=} opt_this Object to use as `this` in `opt_tileCallback`. - * @protected - * @template T - */ -LayerRenderer.prototype.manageTilePyramid = function( - frameState, tileSource, tileGrid, pixelRatio, projection, extent, - currentZ, preload, opt_tileCallback, opt_this) { - const tileSourceKey = getUid(tileSource).toString(); - if (!(tileSourceKey in frameState.wantedTiles)) { - frameState.wantedTiles[tileSourceKey] = {}; - } - const wantedTiles = frameState.wantedTiles[tileSourceKey]; - const tileQueue = frameState.tileQueue; - const minZoom = tileGrid.getMinZoom(); - let tile, tileRange, tileResolution, x, y, z; - for (z = minZoom; z <= currentZ; ++z) { - tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z, tileRange); - tileResolution = tileGrid.getResolution(z); - for (x = tileRange.minX; x <= tileRange.maxX; ++x) { - for (y = tileRange.minY; y <= tileRange.maxY; ++y) { - if (currentZ - z <= preload) { - tile = tileSource.getTile(z, x, y, pixelRatio, projection); - if (tile.getState() == TileState.IDLE) { - wantedTiles[tile.getKey()] = true; - if (!tileQueue.isKeyQueued(tile.getKey())) { - tileQueue.enqueue([tile, tileSourceKey, - tileGrid.getTileCoordCenter(tile.tileCoord), tileResolution]); - } - } - if (opt_tileCallback !== undefined) { - opt_tileCallback.call(opt_this, tile); - } - } else { - tileSource.useTile(z, x, y, projection); - } - } - } - } -}; export default LayerRenderer; diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index e1e64a40c1..f914b0b9ba 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -19,86 +19,308 @@ import {compose as composeTransform, invert as invertTransform, setFromArray as * @param {module:ol/PluggableMap} map Map. * @struct */ -const MapRenderer = function(map) { - Disposable.call(this); +class MapRenderer { + constructor(map) { + Disposable.call(this); + + /** + * @private + * @type {module:ol/PluggableMap} + */ + this.map_ = map; + + /** + * @private + * @type {!Object.} + */ + this.layerRenderers_ = {}; + + /** + * @private + * @type {Object.} + */ + this.layerRendererListeners_ = {}; + + /** + * @private + * @type {Array.} + */ + this.layerRendererConstructors_ = []; + + } /** - * @private - * @type {module:ol/PluggableMap} + * Register layer renderer constructors. + * @param {Array.} constructors Layer renderers. */ - this.map_ = map; + registerLayerRenderers(constructors) { + this.layerRendererConstructors_.push.apply(this.layerRendererConstructors_, constructors); + } /** - * @private - * @type {!Object.} + * Get the registered layer renderer constructors. + * @return {Array.} Registered layer renderers. */ - this.layerRenderers_ = {}; + getLayerRendererConstructors() { + return this.layerRendererConstructors_; + } /** - * @private - * @type {Object.} + * @param {module:ol/PluggableMap~FrameState} frameState FrameState. + * @protected */ - this.layerRendererListeners_ = {}; + calculateMatrices2D(frameState) { + const viewState = frameState.viewState; + const coordinateToPixelTransform = frameState.coordinateToPixelTransform; + const pixelToCoordinateTransform = frameState.pixelToCoordinateTransform; + + composeTransform(coordinateToPixelTransform, + frameState.size[0] / 2, frameState.size[1] / 2, + 1 / viewState.resolution, -1 / viewState.resolution, + -viewState.rotation, + -viewState.center[0], -viewState.center[1]); + + invertTransform( + transformSetFromArray(pixelToCoordinateTransform, coordinateToPixelTransform)); + } /** - * @private - * @type {Array.} + * Removes all layer renderers. */ - this.layerRendererConstructors_ = []; + removeLayerRenderers() { + for (const key in this.layerRenderers_) { + this.removeLayerRendererByKey_(key).dispose(); + } + } -}; + /** + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {module:ol/PluggableMap~FrameState} frameState FrameState. + * @param {number} hitTolerance Hit tolerance in pixels. + * @param {function(this: S, (module:ol/Feature|module:ol/render/Feature), + * module:ol/layer/Layer): T} callback Feature callback. + * @param {S} thisArg Value to use as `this` when executing `callback`. + * @param {function(this: U, module:ol/layer/Layer): boolean} layerFilter Layer filter + * function, 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. + * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`. + * @return {T|undefined} Callback result. + * @template S,T,U + */ + forEachFeatureAtCoordinate( + coordinate, + frameState, + hitTolerance, + callback, + thisArg, + layerFilter, + thisArg2 + ) { + let result; + const viewState = frameState.viewState; + const viewResolution = viewState.resolution; + + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @param {module:ol/layer/Layer} layer Layer. + * @return {?} Callback result. + */ + function forEachFeatureAtCoordinate(feature, layer) { + const key = getUid(feature).toString(); + const managed = frameState.layerStates[getUid(layer)].managed; + if (!(key in frameState.skippedFeatureUids && !managed)) { + return callback.call(thisArg, feature, managed ? layer : null); + } + } + + const projection = viewState.projection; + + let translatedCoordinate = coordinate; + if (projection.canWrapX()) { + const projectionExtent = projection.getExtent(); + const worldWidth = getWidth(projectionExtent); + const x = coordinate[0]; + if (x < projectionExtent[0] || x > projectionExtent[2]) { + const worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth); + translatedCoordinate = [x + worldWidth * worldsAway, coordinate[1]]; + } + } + + const layerStates = frameState.layerStatesArray; + const numLayers = layerStates.length; + let i; + for (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); + if (layer.getSource()) { + result = layerRenderer.forEachFeatureAtCoordinate( + layer.getSource().getWrapX() ? translatedCoordinate : coordinate, + frameState, hitTolerance, forEachFeatureAtCoordinate, thisArg); + } + if (result) { + return result; + } + } + } + return undefined; + } + + /** + * @abstract + * @param {module:ol~Pixel} pixel Pixel. + * @param {module:ol/PluggableMap~FrameState} frameState FrameState. + * @param {number} hitTolerance Hit tolerance in pixels. + * @param {function(this: S, module:ol/layer/Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer + * callback. + * @param {S} thisArg Value to use as `this` when executing `callback`. + * @param {function(this: U, module:ol/layer/Layer): boolean} layerFilter Layer filter + * function, 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. + * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`. + * @return {T|undefined} Callback result. + * @template S,T,U + */ + forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) {} + + /** + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {module:ol/PluggableMap~FrameState} frameState FrameState. + * @param {number} hitTolerance Hit tolerance in pixels. + * @param {function(this: U, module:ol/layer/Layer): boolean} layerFilter Layer filter + * function, 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. + * @param {U} thisArg Value to use as `this` when executing `layerFilter`. + * @return {boolean} Is there a feature at the given coordinate? + * @template U + */ + hasFeatureAtCoordinate(coordinate, frameState, hitTolerance, layerFilter, thisArg) { + const hasFeature = this.forEachFeatureAtCoordinate( + coordinate, frameState, hitTolerance, TRUE, this, layerFilter, thisArg); + + return hasFeature !== undefined; + } + + /** + * @param {module:ol/layer/Layer} layer Layer. + * @protected + * @return {module:ol/renderer/Layer} Layer renderer. + */ + getLayerRenderer(layer) { + const layerKey = getUid(layer).toString(); + if (layerKey in this.layerRenderers_) { + return this.layerRenderers_[layerKey]; + } else { + let renderer; + for (let i = 0, ii = this.layerRendererConstructors_.length; i < ii; ++i) { + const candidate = this.layerRendererConstructors_[i]; + if (candidate['handles'](layer)) { + renderer = candidate['create'](this, layer); + break; + } + } + if (renderer) { + this.layerRenderers_[layerKey] = renderer; + this.layerRendererListeners_[layerKey] = listen(renderer, + EventType.CHANGE, this.handleLayerRendererChange_, this); + } else { + throw new Error('Unable to create renderer for layer: ' + layer.getType()); + } + return renderer; + } + } + + /** + * @param {string} layerKey Layer key. + * @protected + * @return {module:ol/renderer/Layer} Layer renderer. + */ + getLayerRendererByKey(layerKey) { + return this.layerRenderers_[layerKey]; + } + + /** + * @protected + * @return {Object.} Layer renderers. + */ + getLayerRenderers() { + return this.layerRenderers_; + } + + /** + * @return {module:ol/PluggableMap} Map. + */ + getMap() { + return this.map_; + } + + /** + * Handle changes in a layer renderer. + * @private + */ + handleLayerRendererChange_() { + this.map_.render(); + } + + /** + * @param {string} layerKey Layer key. + * @return {module:ol/renderer/Layer} Layer renderer. + * @private + */ + removeLayerRendererByKey_(layerKey) { + const layerRenderer = this.layerRenderers_[layerKey]; + delete this.layerRenderers_[layerKey]; + + unlistenByKey(this.layerRendererListeners_[layerKey]); + delete this.layerRendererListeners_[layerKey]; + + return layerRenderer; + } + + /** + * @param {module:ol/PluggableMap} map Map. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @private + */ + removeUnusedLayerRenderers_(map, frameState) { + for (const layerKey in this.layerRenderers_) { + if (!frameState || !(layerKey in frameState.layerStates)) { + this.removeLayerRendererByKey_(layerKey).dispose(); + } + } + } + + /** + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @protected + */ + scheduleExpireIconCache(frameState) { + frameState.postRenderFunctions.push(/** @type {module:ol/PluggableMap~PostRenderFunction} */ (expireIconCache)); + } + + /** + * @param {!module:ol/PluggableMap~FrameState} frameState Frame state. + * @protected + */ + scheduleRemoveUnusedLayerRenderers(frameState) { + for (const layerKey in this.layerRenderers_) { + if (!(layerKey in frameState.layerStates)) { + frameState.postRenderFunctions.push( + /** @type {module:ol/PluggableMap~PostRenderFunction} */ (this.removeUnusedLayerRenderers_.bind(this)) + ); + return; + } + } + } +} inherits(MapRenderer, Disposable); -/** - * Register layer renderer constructors. - * @param {Array.} constructors Layer renderers. - */ -MapRenderer.prototype.registerLayerRenderers = function(constructors) { - this.layerRendererConstructors_.push.apply(this.layerRendererConstructors_, constructors); -}; - - -/** - * Get the registered layer renderer constructors. - * @return {Array.} Registered layer renderers. - */ -MapRenderer.prototype.getLayerRendererConstructors = function() { - return this.layerRendererConstructors_; -}; - - -/** - * @param {module:ol/PluggableMap~FrameState} frameState FrameState. - * @protected - */ -MapRenderer.prototype.calculateMatrices2D = function(frameState) { - const viewState = frameState.viewState; - const coordinateToPixelTransform = frameState.coordinateToPixelTransform; - const pixelToCoordinateTransform = frameState.pixelToCoordinateTransform; - - composeTransform(coordinateToPixelTransform, - frameState.size[0] / 2, frameState.size[1] / 2, - 1 / viewState.resolution, -1 / viewState.resolution, - -viewState.rotation, - -viewState.center[0], -viewState.center[1]); - - invertTransform( - transformSetFromArray(pixelToCoordinateTransform, coordinateToPixelTransform)); -}; - - -/** - * Removes all layer renderers. - */ -MapRenderer.prototype.removeLayerRenderers = function() { - for (const key in this.layerRenderers_) { - this.removeLayerRendererByKey_(key).dispose(); - } -}; - - /** * @param {module:ol/PluggableMap} map Map. * @param {module:ol/PluggableMap~FrameState} frameState Frame state. @@ -108,197 +330,6 @@ function expireIconCache(map, frameState) { } -/** - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {module:ol/PluggableMap~FrameState} frameState FrameState. - * @param {number} hitTolerance Hit tolerance in pixels. - * @param {function(this: S, (module:ol/Feature|module:ol/render/Feature), - * module:ol/layer/Layer): T} callback Feature callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. - * @param {function(this: U, module:ol/layer/Layer): boolean} layerFilter Layer filter - * function, 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. - * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`. - * @return {T|undefined} Callback result. - * @template S,T,U - */ -MapRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg, - layerFilter, thisArg2) { - let result; - const viewState = frameState.viewState; - const viewResolution = viewState.resolution; - - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @param {module:ol/layer/Layer} layer Layer. - * @return {?} Callback result. - */ - function forEachFeatureAtCoordinate(feature, layer) { - const key = getUid(feature).toString(); - const managed = frameState.layerStates[getUid(layer)].managed; - if (!(key in frameState.skippedFeatureUids && !managed)) { - return callback.call(thisArg, feature, managed ? layer : null); - } - } - - const projection = viewState.projection; - - let translatedCoordinate = coordinate; - if (projection.canWrapX()) { - const projectionExtent = projection.getExtent(); - const worldWidth = getWidth(projectionExtent); - const x = coordinate[0]; - if (x < projectionExtent[0] || x > projectionExtent[2]) { - const worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth); - translatedCoordinate = [x + worldWidth * worldsAway, coordinate[1]]; - } - } - - const layerStates = frameState.layerStatesArray; - const numLayers = layerStates.length; - let i; - for (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); - if (layer.getSource()) { - result = layerRenderer.forEachFeatureAtCoordinate( - layer.getSource().getWrapX() ? translatedCoordinate : coordinate, - frameState, hitTolerance, forEachFeatureAtCoordinate, thisArg); - } - if (result) { - return result; - } - } - } - return undefined; -}; - - -/** - * @abstract - * @param {module:ol~Pixel} pixel Pixel. - * @param {module:ol/PluggableMap~FrameState} frameState FrameState. - * @param {number} hitTolerance Hit tolerance in pixels. - * @param {function(this: S, module:ol/layer/Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer - * callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. - * @param {function(this: U, module:ol/layer/Layer): boolean} layerFilter Layer filter - * function, 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. - * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`. - * @return {T|undefined} Callback result. - * @template S,T,U - */ -MapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, hitTolerance, callback, thisArg, - layerFilter, thisArg2) {}; - - -/** - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {module:ol/PluggableMap~FrameState} frameState FrameState. - * @param {number} hitTolerance Hit tolerance in pixels. - * @param {function(this: U, module:ol/layer/Layer): boolean} layerFilter Layer filter - * function, 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. - * @param {U} thisArg Value to use as `this` when executing `layerFilter`. - * @return {boolean} Is there a feature at the given coordinate? - * @template U - */ -MapRenderer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, layerFilter, thisArg) { - const hasFeature = this.forEachFeatureAtCoordinate( - coordinate, frameState, hitTolerance, TRUE, this, layerFilter, thisArg); - - return hasFeature !== undefined; -}; - - -/** - * @param {module:ol/layer/Layer} layer Layer. - * @protected - * @return {module:ol/renderer/Layer} Layer renderer. - */ -MapRenderer.prototype.getLayerRenderer = function(layer) { - const layerKey = getUid(layer).toString(); - if (layerKey in this.layerRenderers_) { - return this.layerRenderers_[layerKey]; - } else { - let renderer; - for (let i = 0, ii = this.layerRendererConstructors_.length; i < ii; ++i) { - const candidate = this.layerRendererConstructors_[i]; - if (candidate['handles'](layer)) { - renderer = candidate['create'](this, layer); - break; - } - } - if (renderer) { - this.layerRenderers_[layerKey] = renderer; - this.layerRendererListeners_[layerKey] = listen(renderer, - EventType.CHANGE, this.handleLayerRendererChange_, this); - } else { - throw new Error('Unable to create renderer for layer: ' + layer.getType()); - } - return renderer; - } -}; - - -/** - * @param {string} layerKey Layer key. - * @protected - * @return {module:ol/renderer/Layer} Layer renderer. - */ -MapRenderer.prototype.getLayerRendererByKey = function(layerKey) { - return this.layerRenderers_[layerKey]; -}; - - -/** - * @protected - * @return {Object.} Layer renderers. - */ -MapRenderer.prototype.getLayerRenderers = function() { - return this.layerRenderers_; -}; - - -/** - * @return {module:ol/PluggableMap} Map. - */ -MapRenderer.prototype.getMap = function() { - return this.map_; -}; - - -/** - * Handle changes in a layer renderer. - * @private - */ -MapRenderer.prototype.handleLayerRendererChange_ = function() { - this.map_.render(); -}; - - -/** - * @param {string} layerKey Layer key. - * @return {module:ol/renderer/Layer} Layer renderer. - * @private - */ -MapRenderer.prototype.removeLayerRendererByKey_ = function(layerKey) { - const layerRenderer = this.layerRenderers_[layerKey]; - delete this.layerRenderers_[layerKey]; - - unlistenByKey(this.layerRendererListeners_[layerKey]); - delete this.layerRendererListeners_[layerKey]; - - return layerRenderer; -}; - - /** * Render. * @param {?module:ol/PluggableMap~FrameState} frameState Frame state. @@ -306,45 +337,6 @@ MapRenderer.prototype.removeLayerRendererByKey_ = function(layerKey) { MapRenderer.prototype.renderFrame = UNDEFINED; -/** - * @param {module:ol/PluggableMap} map Map. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @private - */ -MapRenderer.prototype.removeUnusedLayerRenderers_ = function(map, frameState) { - for (const layerKey in this.layerRenderers_) { - if (!frameState || !(layerKey in frameState.layerStates)) { - this.removeLayerRendererByKey_(layerKey).dispose(); - } - } -}; - - -/** - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @protected - */ -MapRenderer.prototype.scheduleExpireIconCache = function(frameState) { - frameState.postRenderFunctions.push(/** @type {module:ol/PluggableMap~PostRenderFunction} */ (expireIconCache)); -}; - - -/** - * @param {!module:ol/PluggableMap~FrameState} frameState Frame state. - * @protected - */ -MapRenderer.prototype.scheduleRemoveUnusedLayerRenderers = function(frameState) { - for (const layerKey in this.layerRenderers_) { - if (!(layerKey in frameState.layerStates)) { - frameState.postRenderFunctions.push( - /** @type {module:ol/PluggableMap~PostRenderFunction} */ (this.removeUnusedLayerRenderers_.bind(this)) - ); - return; - } - } -}; - - /** * @param {module:ol/layer/Layer~State} state1 First layer state. * @param {module:ol/layer/Layer~State} state2 Second layer state. diff --git a/src/ol/renderer/canvas/ImageLayer.js b/src/ol/renderer/canvas/ImageLayer.js index a9f4b414b6..f0006e4621 100644 --- a/src/ol/renderer/canvas/ImageLayer.js +++ b/src/ol/renderer/canvas/ImageLayer.js @@ -20,44 +20,172 @@ import {create as createTransform, compose as composeTransform} from '../../tran * @param {module:ol/layer/Image|module:ol/layer/Vector} imageLayer Image or vector layer. * @api */ -const CanvasImageLayerRenderer = function(imageLayer) { +class CanvasImageLayerRenderer { + constructor(imageLayer) { - IntermediateCanvasRenderer.call(this, imageLayer); + IntermediateCanvasRenderer.call(this, imageLayer); - /** - * @private - * @type {?module:ol/ImageBase} - */ - this.image_ = null; + /** + * @private + * @type {?module:ol/ImageBase} + */ + this.image_ = null; - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.imageTransform_ = createTransform(); + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.imageTransform_ = createTransform(); - /** - * @type {!Array.} - */ - this.skippedFeatures_ = []; + /** + * @type {!Array.} + */ + this.skippedFeatures_ = []; - /** - * @private - * @type {module:ol/renderer/canvas/VectorLayer} - */ - this.vectorRenderer_ = null; + /** + * @private + * @type {module:ol/renderer/canvas/VectorLayer} + */ + this.vectorRenderer_ = null; - if (imageLayer.getType() === LayerType.VECTOR) { - for (let i = 0, ii = layerRendererConstructors.length; i < ii; ++i) { - const ctor = layerRendererConstructors[i]; - if (ctor !== CanvasImageLayerRenderer && ctor['handles'](imageLayer)) { - this.vectorRenderer_ = new ctor(imageLayer); - break; + if (imageLayer.getType() === LayerType.VECTOR) { + for (let i = 0, ii = layerRendererConstructors.length; i < ii; ++i) { + const ctor = layerRendererConstructors[i]; + if (ctor !== CanvasImageLayerRenderer && ctor['handles'](imageLayer)) { + this.vectorRenderer_ = new ctor(imageLayer); + break; + } } } + } -}; + /** + * @inheritDoc + */ + disposeInternal() { + if (this.vectorRenderer_) { + this.vectorRenderer_.dispose(); + } + IntermediateCanvasRenderer.prototype.disposeInternal.call(this); + } + + /** + * @inheritDoc + */ + getImage() { + return !this.image_ ? null : this.image_.getImage(); + } + + /** + * @inheritDoc + */ + getImageTransform() { + return this.imageTransform_; + } + + /** + * @inheritDoc + */ + prepareFrame(frameState, layerState) { + + const pixelRatio = frameState.pixelRatio; + const size = frameState.size; + const viewState = frameState.viewState; + const viewCenter = viewState.center; + const viewResolution = viewState.resolution; + + let image; + const imageLayer = /** @type {module:ol/layer/Image} */ (this.getLayer()); + const imageSource = imageLayer.getSource(); + + const hints = frameState.viewHints; + + const vectorRenderer = this.vectorRenderer_; + let renderedExtent = frameState.extent; + if (!vectorRenderer && layerState.extent !== undefined) { + renderedExtent = getIntersection(renderedExtent, layerState.extent); + } + + if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] && + !isEmpty(renderedExtent)) { + let projection = viewState.projection; + if (!ENABLE_RASTER_REPROJECTION) { + const sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + projection = sourceProjection; + } + } + let skippedFeatures = this.skippedFeatures_; + if (vectorRenderer) { + const context = vectorRenderer.context; + const imageFrameState = /** @type {module:ol/PluggableMap~FrameState} */ (assign({}, frameState, { + size: [ + getWidth(renderedExtent) / viewResolution, + getHeight(renderedExtent) / viewResolution + ], + viewState: /** @type {module:ol/View~State} */ (assign({}, frameState.viewState, { + rotation: 0 + })) + })); + const newSkippedFeatures = Object.keys(imageFrameState.skippedFeatureUids).sort(); + image = new ImageCanvas(renderedExtent, viewResolution, pixelRatio, context.canvas, function(callback) { + if (vectorRenderer.prepareFrame(imageFrameState, layerState) && + (vectorRenderer.replayGroupChanged || + !equals(skippedFeatures, newSkippedFeatures))) { + context.canvas.width = imageFrameState.size[0] * pixelRatio; + context.canvas.height = imageFrameState.size[1] * pixelRatio; + vectorRenderer.compose(context, imageFrameState, layerState); + skippedFeatures = newSkippedFeatures; + callback(); + } + }); + } else { + image = imageSource.getImage( + renderedExtent, viewResolution, pixelRatio, projection); + } + if (image && this.loadImage(image)) { + this.image_ = image; + this.skippedFeatures_ = skippedFeatures; + } + } + + if (this.image_) { + image = this.image_; + const imageExtent = image.getExtent(); + const imageResolution = image.getResolution(); + const imagePixelRatio = image.getPixelRatio(); + const scale = pixelRatio * imageResolution / + (viewResolution * imagePixelRatio); + const transform = composeTransform(this.imageTransform_, + pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, + scale, scale, + 0, + imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution, + imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution); + composeTransform(this.coordinateToCanvasPixelTransform, + pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], + pixelRatio / viewResolution, -pixelRatio / viewResolution, + 0, + -viewCenter[0], -viewCenter[1]); + + this.renderedResolution = imageResolution * pixelRatio / imagePixelRatio; + } + + return !!this.image_; + } + + /** + * @inheritDoc + */ + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + if (this.vectorRenderer_) { + return this.vectorRenderer_.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg); + } else { + return IntermediateCanvasRenderer.prototype.forEachFeatureAtCoordinate.call(this, coordinate, frameState, hitTolerance, callback, thisArg); + } + } +} inherits(CanvasImageLayerRenderer, IntermediateCanvasRenderer); @@ -85,135 +213,4 @@ CanvasImageLayerRenderer['create'] = function(mapRenderer, layer) { }; -/** - * @inheritDoc - */ -CanvasImageLayerRenderer.prototype.disposeInternal = function() { - if (this.vectorRenderer_) { - this.vectorRenderer_.dispose(); - } - IntermediateCanvasRenderer.prototype.disposeInternal.call(this); -}; - - -/** - * @inheritDoc - */ -CanvasImageLayerRenderer.prototype.getImage = function() { - return !this.image_ ? null : this.image_.getImage(); -}; - - -/** - * @inheritDoc - */ -CanvasImageLayerRenderer.prototype.getImageTransform = function() { - return this.imageTransform_; -}; - - -/** - * @inheritDoc - */ -CanvasImageLayerRenderer.prototype.prepareFrame = function(frameState, layerState) { - - const pixelRatio = frameState.pixelRatio; - const size = frameState.size; - const viewState = frameState.viewState; - const viewCenter = viewState.center; - const viewResolution = viewState.resolution; - - let image; - const imageLayer = /** @type {module:ol/layer/Image} */ (this.getLayer()); - const imageSource = imageLayer.getSource(); - - const hints = frameState.viewHints; - - const vectorRenderer = this.vectorRenderer_; - let renderedExtent = frameState.extent; - if (!vectorRenderer && layerState.extent !== undefined) { - renderedExtent = getIntersection(renderedExtent, layerState.extent); - } - - if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] && - !isEmpty(renderedExtent)) { - let projection = viewState.projection; - if (!ENABLE_RASTER_REPROJECTION) { - const sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - projection = sourceProjection; - } - } - let skippedFeatures = this.skippedFeatures_; - if (vectorRenderer) { - const context = vectorRenderer.context; - const imageFrameState = /** @type {module:ol/PluggableMap~FrameState} */ (assign({}, frameState, { - size: [ - getWidth(renderedExtent) / viewResolution, - getHeight(renderedExtent) / viewResolution - ], - viewState: /** @type {module:ol/View~State} */ (assign({}, frameState.viewState, { - rotation: 0 - })) - })); - const newSkippedFeatures = Object.keys(imageFrameState.skippedFeatureUids).sort(); - image = new ImageCanvas(renderedExtent, viewResolution, pixelRatio, context.canvas, function(callback) { - if (vectorRenderer.prepareFrame(imageFrameState, layerState) && - (vectorRenderer.replayGroupChanged || - !equals(skippedFeatures, newSkippedFeatures))) { - context.canvas.width = imageFrameState.size[0] * pixelRatio; - context.canvas.height = imageFrameState.size[1] * pixelRatio; - vectorRenderer.compose(context, imageFrameState, layerState); - skippedFeatures = newSkippedFeatures; - callback(); - } - }); - } else { - image = imageSource.getImage( - renderedExtent, viewResolution, pixelRatio, projection); - } - if (image && this.loadImage(image)) { - this.image_ = image; - this.skippedFeatures_ = skippedFeatures; - } - } - - if (this.image_) { - image = this.image_; - const imageExtent = image.getExtent(); - const imageResolution = image.getResolution(); - const imagePixelRatio = image.getPixelRatio(); - const scale = pixelRatio * imageResolution / - (viewResolution * imagePixelRatio); - const transform = composeTransform(this.imageTransform_, - pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, - scale, scale, - 0, - imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution, - imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution); - composeTransform(this.coordinateToCanvasPixelTransform, - pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], - pixelRatio / viewResolution, -pixelRatio / viewResolution, - 0, - -viewCenter[0], -viewCenter[1]); - - this.renderedResolution = imageResolution * pixelRatio / imagePixelRatio; - } - - return !!this.image_; -}; - - -/** - * @inheritDoc - */ -CanvasImageLayerRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { - if (this.vectorRenderer_) { - return this.vectorRenderer_.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg); - } else { - return IntermediateCanvasRenderer.prototype.forEachFeatureAtCoordinate.call(this, coordinate, frameState, hitTolerance, callback, thisArg); - } -}; - - export default CanvasImageLayerRenderer; diff --git a/src/ol/renderer/canvas/IntermediateCanvas.js b/src/ol/renderer/canvas/IntermediateCanvas.js index 2ccfdb8feb..f2147f829d 100644 --- a/src/ol/renderer/canvas/IntermediateCanvas.js +++ b/src/ol/renderer/canvas/IntermediateCanvas.js @@ -15,137 +15,135 @@ import {create as createTransform, apply as applyTransform} from '../../transfor * @extends {module:ol/renderer/canvas/Layer} * @param {module:ol/layer/Layer} layer Layer. */ -const IntermediateCanvasRenderer = function(layer) { +class IntermediateCanvasRenderer { + constructor(layer) { - CanvasLayerRenderer.call(this, layer); + CanvasLayerRenderer.call(this, layer); + + /** + * @protected + * @type {module:ol/transform~Transform} + */ + this.coordinateToCanvasPixelTransform = createTransform(); + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.hitCanvasContext_ = null; + + } /** - * @protected - * @type {module:ol/transform~Transform} + * @inheritDoc */ - this.coordinateToCanvasPixelTransform = createTransform(); + composeFrame(frameState, layerState, context) { + + this.preCompose(context, frameState); + + const image = this.getImage(); + if (image) { + + // clipped rendering if layer extent is set + const extent = layerState.extent; + const clipped = extent !== undefined && + !containsExtent(extent, frameState.extent) && + intersects(extent, frameState.extent); + if (clipped) { + this.clip(context, frameState, /** @type {module:ol/extent~Extent} */ (extent)); + } + + const imageTransform = this.getImageTransform(); + // for performance reasons, context.save / context.restore is not used + // to save and restore the transformation matrix and the opacity. + // see http://jsperf.com/context-save-restore-versus-variable + const alpha = context.globalAlpha; + context.globalAlpha = layerState.opacity; + + // for performance reasons, context.setTransform is only used + // when the view is rotated. see http://jsperf.com/canvas-transform + const dx = imageTransform[4]; + const dy = imageTransform[5]; + const dw = image.width * imageTransform[0]; + const dh = image.height * imageTransform[3]; + context.drawImage(image, 0, 0, +image.width, +image.height, + Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh)); + context.globalAlpha = alpha; + + if (clipped) { + context.restore(); + } + } + + this.postCompose(context, frameState, layerState); + } /** - * @private - * @type {CanvasRenderingContext2D} + * @abstract + * @return {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} Canvas. */ - this.hitCanvasContext_ = null; + getImage() {} -}; + /** + * @abstract + * @return {!module:ol/transform~Transform} Image transform. + */ + getImageTransform() {} + + /** + * @inheritDoc + */ + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + const layer = this.getLayer(); + const source = layer.getSource(); + const resolution = frameState.viewState.resolution; + const rotation = frameState.viewState.rotation; + const skippedFeatureUids = frameState.skippedFeatureUids; + return source.forEachFeatureAtCoordinate( + coordinate, resolution, rotation, hitTolerance, skippedFeatureUids, + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + return callback.call(thisArg, feature, layer); + }); + } + + /** + * @inheritDoc + */ + forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + if (!this.getImage()) { + return undefined; + } + + if (this.getLayer().getSource().forEachFeatureAtCoordinate !== UNDEFINED) { + // for ImageCanvas sources use the original hit-detection logic, + // so that for example also transparent polygons are detected + return CanvasLayerRenderer.prototype.forEachLayerAtCoordinate.apply(this, arguments); + } else { + const pixel = applyTransform(this.coordinateToCanvasPixelTransform, coordinate.slice()); + scaleCoordinate(pixel, frameState.viewState.resolution / this.renderedResolution); + + if (!this.hitCanvasContext_) { + this.hitCanvasContext_ = createCanvasContext2D(1, 1); + } + + this.hitCanvasContext_.clearRect(0, 0, 1, 1); + this.hitCanvasContext_.drawImage(this.getImage(), pixel[0], pixel[1], 1, 1, 0, 0, 1, 1); + + const imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data; + if (imageData[3] > 0) { + return callback.call(thisArg, this.getLayer(), imageData); + } else { + return undefined; + } + } + } +} inherits(IntermediateCanvasRenderer, CanvasLayerRenderer); -/** - * @inheritDoc - */ -IntermediateCanvasRenderer.prototype.composeFrame = function(frameState, layerState, context) { - - this.preCompose(context, frameState); - - const image = this.getImage(); - if (image) { - - // clipped rendering if layer extent is set - const extent = layerState.extent; - const clipped = extent !== undefined && - !containsExtent(extent, frameState.extent) && - intersects(extent, frameState.extent); - if (clipped) { - this.clip(context, frameState, /** @type {module:ol/extent~Extent} */ (extent)); - } - - const imageTransform = this.getImageTransform(); - // for performance reasons, context.save / context.restore is not used - // to save and restore the transformation matrix and the opacity. - // see http://jsperf.com/context-save-restore-versus-variable - const alpha = context.globalAlpha; - context.globalAlpha = layerState.opacity; - - // for performance reasons, context.setTransform is only used - // when the view is rotated. see http://jsperf.com/canvas-transform - const dx = imageTransform[4]; - const dy = imageTransform[5]; - const dw = image.width * imageTransform[0]; - const dh = image.height * imageTransform[3]; - context.drawImage(image, 0, 0, +image.width, +image.height, - Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh)); - context.globalAlpha = alpha; - - if (clipped) { - context.restore(); - } - } - - this.postCompose(context, frameState, layerState); -}; - - -/** - * @abstract - * @return {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} Canvas. - */ -IntermediateCanvasRenderer.prototype.getImage = function() {}; - - -/** - * @abstract - * @return {!module:ol/transform~Transform} Image transform. - */ -IntermediateCanvasRenderer.prototype.getImageTransform = function() {}; - - -/** - * @inheritDoc - */ -IntermediateCanvasRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { - const layer = this.getLayer(); - const source = layer.getSource(); - const resolution = frameState.viewState.resolution; - const rotation = frameState.viewState.rotation; - const skippedFeatureUids = frameState.skippedFeatureUids; - return source.forEachFeatureAtCoordinate( - coordinate, resolution, rotation, hitTolerance, skippedFeatureUids, - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - return callback.call(thisArg, feature, layer); - }); -}; - - -/** - * @inheritDoc - */ -IntermediateCanvasRenderer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { - if (!this.getImage()) { - return undefined; - } - - if (this.getLayer().getSource().forEachFeatureAtCoordinate !== UNDEFINED) { - // for ImageCanvas sources use the original hit-detection logic, - // so that for example also transparent polygons are detected - return CanvasLayerRenderer.prototype.forEachLayerAtCoordinate.apply(this, arguments); - } else { - const pixel = applyTransform(this.coordinateToCanvasPixelTransform, coordinate.slice()); - scaleCoordinate(pixel, frameState.viewState.resolution / this.renderedResolution); - - if (!this.hitCanvasContext_) { - this.hitCanvasContext_ = createCanvasContext2D(1, 1); - } - - this.hitCanvasContext_.clearRect(0, 0, 1, 1); - this.hitCanvasContext_.drawImage(this.getImage(), pixel[0], pixel[1], 1, 1, 0, 0, 1, 1); - - const imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data; - if (imageData[3] > 0) { - return callback.call(thisArg, this.getLayer(), imageData); - } else { - return undefined; - } - } -}; - export default IntermediateCanvasRenderer; diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index f07024db51..30d1d166b8 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -17,175 +17,171 @@ import {create as createTransform, apply as applyTransform, compose as composeTr * @extends {module:ol/renderer/Layer} * @param {module:ol/layer/Layer} layer Layer. */ -const CanvasLayerRenderer = function(layer) { +class CanvasLayerRenderer { + constructor(layer) { - LayerRenderer.call(this, layer); + LayerRenderer.call(this, layer); + + /** + * @protected + * @type {number} + */ + this.renderedResolution; + + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.transform_ = createTransform(); + + } /** + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/extent~Extent} extent Clip extent. * @protected - * @type {number} */ - this.renderedResolution; + clip(context, frameState, extent) { + const pixelRatio = frameState.pixelRatio; + const width = frameState.size[0] * pixelRatio; + const height = frameState.size[1] * pixelRatio; + const rotation = frameState.viewState.rotation; + const topLeft = getTopLeft(/** @type {module:ol/extent~Extent} */ (extent)); + const topRight = getTopRight(/** @type {module:ol/extent~Extent} */ (extent)); + const bottomRight = getBottomRight(/** @type {module:ol/extent~Extent} */ (extent)); + const bottomLeft = getBottomLeft(/** @type {module:ol/extent~Extent} */ (extent)); + + applyTransform(frameState.coordinateToPixelTransform, topLeft); + applyTransform(frameState.coordinateToPixelTransform, topRight); + applyTransform(frameState.coordinateToPixelTransform, bottomRight); + applyTransform(frameState.coordinateToPixelTransform, bottomLeft); + + context.save(); + rotateAtOffset(context, -rotation, width / 2, height / 2); + context.beginPath(); + context.moveTo(topLeft[0] * pixelRatio, topLeft[1] * pixelRatio); + context.lineTo(topRight[0] * pixelRatio, topRight[1] * pixelRatio); + context.lineTo(bottomRight[0] * pixelRatio, bottomRight[1] * pixelRatio); + context.lineTo(bottomLeft[0] * pixelRatio, bottomLeft[1] * pixelRatio); + context.clip(); + rotateAtOffset(context, rotation, width / 2, height / 2); + } /** + * @param {module:ol/render/EventType} type Event type. + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/transform~Transform=} opt_transform Transform. * @private - * @type {module:ol/transform~Transform} */ - this.transform_ = createTransform(); + dispatchComposeEvent_(type, context, frameState, opt_transform) { + const layer = this.getLayer(); + if (layer.hasListener(type)) { + const width = frameState.size[0] * frameState.pixelRatio; + const height = frameState.size[1] * frameState.pixelRatio; + const rotation = frameState.viewState.rotation; + rotateAtOffset(context, -rotation, width / 2, height / 2); + const transform = opt_transform !== undefined ? + opt_transform : this.getTransform(frameState, 0); + const render = new CanvasImmediateRenderer( + context, frameState.pixelRatio, frameState.extent, transform, + frameState.viewState.rotation); + const composeEvent = new RenderEvent(type, render, frameState, + context, null); + layer.dispatchEvent(composeEvent); + rotateAtOffset(context, rotation, width / 2, height / 2); + } + } -}; + /** + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {module:ol/PluggableMap~FrameState} frameState FrameState. + * @param {number} hitTolerance Hit tolerance in pixels. + * @param {function(this: S, module:ol/layer/Layer, (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,U + */ + forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, TRUE, this); + + if (hasFeature) { + return callback.call(thisArg, this.getLayer(), null); + } else { + return undefined; + } + } + + /** + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/layer/Layer~State} layerState Layer state. + * @param {module:ol/transform~Transform=} opt_transform Transform. + * @protected + */ + postCompose(context, frameState, layerState, opt_transform) { + this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, context, frameState, opt_transform); + } + + /** + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/transform~Transform=} opt_transform Transform. + * @protected + */ + preCompose(context, frameState, opt_transform) { + this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, context, frameState, opt_transform); + } + + /** + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/transform~Transform=} opt_transform Transform. + * @protected + */ + dispatchRenderEvent(context, frameState, opt_transform) { + this.dispatchComposeEvent_(RenderEventType.RENDER, context, frameState, opt_transform); + } + + /** + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {number} offsetX Offset on the x-axis in view coordinates. + * @protected + * @return {!module:ol/transform~Transform} Transform. + */ + getTransform(frameState, offsetX) { + const viewState = frameState.viewState; + const pixelRatio = frameState.pixelRatio; + const dx1 = pixelRatio * frameState.size[0] / 2; + const dy1 = pixelRatio * frameState.size[1] / 2; + const sx = pixelRatio / viewState.resolution; + const sy = -sx; + const angle = -viewState.rotation; + const dx2 = -viewState.center[0] + offsetX; + const dy2 = -viewState.center[1]; + return composeTransform(this.transform_, dx1, dy1, sx, sy, angle, dx2, dy2); + } + + /** + * @abstract + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/layer/Layer~State} layerState Layer state. + * @param {CanvasRenderingContext2D} context Context. + */ + composeFrame(frameState, layerState, context) {} + + /** + * @abstract + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/layer/Layer~State} layerState Layer state. + * @return {boolean} whether composeFrame should be called. + */ + prepareFrame(frameState, layerState) {} +} inherits(CanvasLayerRenderer, LayerRenderer); -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/extent~Extent} extent Clip extent. - * @protected - */ -CanvasLayerRenderer.prototype.clip = function(context, frameState, extent) { - const pixelRatio = frameState.pixelRatio; - const width = frameState.size[0] * pixelRatio; - const height = frameState.size[1] * pixelRatio; - const rotation = frameState.viewState.rotation; - const topLeft = getTopLeft(/** @type {module:ol/extent~Extent} */ (extent)); - const topRight = getTopRight(/** @type {module:ol/extent~Extent} */ (extent)); - const bottomRight = getBottomRight(/** @type {module:ol/extent~Extent} */ (extent)); - const bottomLeft = getBottomLeft(/** @type {module:ol/extent~Extent} */ (extent)); - - applyTransform(frameState.coordinateToPixelTransform, topLeft); - applyTransform(frameState.coordinateToPixelTransform, topRight); - applyTransform(frameState.coordinateToPixelTransform, bottomRight); - applyTransform(frameState.coordinateToPixelTransform, bottomLeft); - - context.save(); - rotateAtOffset(context, -rotation, width / 2, height / 2); - context.beginPath(); - context.moveTo(topLeft[0] * pixelRatio, topLeft[1] * pixelRatio); - context.lineTo(topRight[0] * pixelRatio, topRight[1] * pixelRatio); - context.lineTo(bottomRight[0] * pixelRatio, bottomRight[1] * pixelRatio); - context.lineTo(bottomLeft[0] * pixelRatio, bottomLeft[1] * pixelRatio); - context.clip(); - rotateAtOffset(context, rotation, width / 2, height / 2); -}; - - -/** - * @param {module:ol/render/EventType} type Event type. - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/transform~Transform=} opt_transform Transform. - * @private - */ -CanvasLayerRenderer.prototype.dispatchComposeEvent_ = function(type, context, frameState, opt_transform) { - const layer = this.getLayer(); - if (layer.hasListener(type)) { - const width = frameState.size[0] * frameState.pixelRatio; - const height = frameState.size[1] * frameState.pixelRatio; - const rotation = frameState.viewState.rotation; - rotateAtOffset(context, -rotation, width / 2, height / 2); - const transform = opt_transform !== undefined ? - opt_transform : this.getTransform(frameState, 0); - const render = new CanvasImmediateRenderer( - context, frameState.pixelRatio, frameState.extent, transform, - frameState.viewState.rotation); - const composeEvent = new RenderEvent(type, render, frameState, - context, null); - layer.dispatchEvent(composeEvent); - rotateAtOffset(context, rotation, width / 2, height / 2); - } -}; - - -/** - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {module:ol/PluggableMap~FrameState} frameState FrameState. - * @param {number} hitTolerance Hit tolerance in pixels. - * @param {function(this: S, module:ol/layer/Layer, (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,U - */ -CanvasLayerRenderer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { - const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, TRUE, this); - - if (hasFeature) { - return callback.call(thisArg, this.getLayer(), null); - } else { - return undefined; - } -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/layer/Layer~State} layerState Layer state. - * @param {module:ol/transform~Transform=} opt_transform Transform. - * @protected - */ -CanvasLayerRenderer.prototype.postCompose = function(context, frameState, layerState, opt_transform) { - this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, context, frameState, opt_transform); -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/transform~Transform=} opt_transform Transform. - * @protected - */ -CanvasLayerRenderer.prototype.preCompose = function(context, frameState, opt_transform) { - this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, context, frameState, opt_transform); -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/transform~Transform=} opt_transform Transform. - * @protected - */ -CanvasLayerRenderer.prototype.dispatchRenderEvent = function(context, frameState, opt_transform) { - this.dispatchComposeEvent_(RenderEventType.RENDER, context, frameState, opt_transform); -}; - - -/** - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {number} offsetX Offset on the x-axis in view coordinates. - * @protected - * @return {!module:ol/transform~Transform} Transform. - */ -CanvasLayerRenderer.prototype.getTransform = function(frameState, offsetX) { - const viewState = frameState.viewState; - const pixelRatio = frameState.pixelRatio; - const dx1 = pixelRatio * frameState.size[0] / 2; - const dy1 = pixelRatio * frameState.size[1] / 2; - const sx = pixelRatio / viewState.resolution; - const sy = -sx; - const angle = -viewState.rotation; - const dx2 = -viewState.center[0] + offsetX; - const dy2 = -viewState.center[1]; - return composeTransform(this.transform_, dx1, dy1, sx, sy, angle, dx2, dy2); -}; - - -/** - * @abstract - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/layer/Layer~State} layerState Layer state. - * @param {CanvasRenderingContext2D} context Context. - */ -CanvasLayerRenderer.prototype.composeFrame = function(frameState, layerState, context) {}; - -/** - * @abstract - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/layer/Layer~State} layerState Layer state. - * @return {boolean} whether composeFrame should be called. - */ -CanvasLayerRenderer.prototype.prepareFrame = function(frameState, layerState) {}; export default CanvasLayerRenderer; diff --git a/src/ol/renderer/canvas/Map.js b/src/ol/renderer/canvas/Map.js index e544b2e513..7e5ea1ad2a 100644 --- a/src/ol/renderer/canvas/Map.js +++ b/src/ol/renderer/canvas/Map.js @@ -27,201 +27,198 @@ export const layerRendererConstructors = []; * @param {module:ol/PluggableMap} map Map. * @api */ -const CanvasMapRenderer = function(map) { - MapRenderer.call(this, map); +class CanvasMapRenderer { + constructor(map) { + MapRenderer.call(this, map); - const container = map.getViewport(); + const container = map.getViewport(); + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = createCanvasContext2D(); + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = this.context_.canvas; + + this.canvas_.style.width = '100%'; + this.canvas_.style.height = '100%'; + this.canvas_.style.display = 'block'; + this.canvas_.className = CLASS_UNSELECTABLE; + container.insertBefore(this.canvas_, container.childNodes[0] || null); + + /** + * @private + * @type {boolean} + */ + this.renderedVisible_ = true; + + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.transform_ = createTransform(); + + } /** + * @param {module:ol/render/EventType} type Event type. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. * @private - * @type {CanvasRenderingContext2D} */ - this.context_ = createCanvasContext2D(); + dispatchComposeEvent_(type, frameState) { + const map = this.getMap(); + const context = this.context_; + if (map.hasListener(type)) { + const extent = frameState.extent; + const pixelRatio = frameState.pixelRatio; + const viewState = frameState.viewState; + const rotation = viewState.rotation; + + const transform = this.getTransform(frameState); + + const vectorContext = new CanvasImmediateRenderer(context, pixelRatio, + extent, transform, rotation); + const composeEvent = new RenderEvent(type, vectorContext, + frameState, context, null); + map.dispatchEvent(composeEvent); + } + } /** - * @private - * @type {HTMLCanvasElement} + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @protected + * @return {!module:ol/transform~Transform} Transform. */ - this.canvas_ = this.context_.canvas; - - this.canvas_.style.width = '100%'; - this.canvas_.style.height = '100%'; - this.canvas_.style.display = 'block'; - this.canvas_.className = CLASS_UNSELECTABLE; - container.insertBefore(this.canvas_, container.childNodes[0] || null); + getTransform(frameState) { + const viewState = frameState.viewState; + const dx1 = this.canvas_.width / 2; + const dy1 = this.canvas_.height / 2; + const sx = frameState.pixelRatio / viewState.resolution; + const sy = -sx; + const angle = -viewState.rotation; + const dx2 = -viewState.center[0]; + const dy2 = -viewState.center[1]; + return composeTransform(this.transform_, dx1, dy1, sx, sy, angle, dx2, dy2); + } /** - * @private - * @type {boolean} + * @inheritDoc */ - this.renderedVisible_ = true; + renderFrame(frameState) { + + if (!frameState) { + if (this.renderedVisible_) { + this.canvas_.style.display = 'none'; + this.renderedVisible_ = false; + } + return; + } + + const context = this.context_; + const pixelRatio = frameState.pixelRatio; + const width = Math.round(frameState.size[0] * pixelRatio); + const height = Math.round(frameState.size[1] * pixelRatio); + if (this.canvas_.width != width || this.canvas_.height != height) { + this.canvas_.width = width; + this.canvas_.height = height; + } else { + context.clearRect(0, 0, width, height); + } + + const rotation = frameState.viewState.rotation; + + this.calculateMatrices2D(frameState); + + this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, frameState); + + const layerStatesArray = frameState.layerStatesArray; + stableSort(layerStatesArray, sortByZIndex); + + if (rotation) { + context.save(); + rotateAtOffset(context, rotation, width / 2, height / 2); + } + + const viewResolution = frameState.viewState.resolution; + let i, ii, layer, layerRenderer, layerState; + for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { + layerState = layerStatesArray[i]; + layer = layerState.layer; + layerRenderer = /** @type {module:ol/renderer/canvas/Layer} */ (this.getLayerRenderer(layer)); + if (!visibleAtResolution(layerState, viewResolution) || + layerState.sourceState != SourceState.READY) { + continue; + } + if (layerRenderer.prepareFrame(frameState, layerState)) { + layerRenderer.composeFrame(frameState, layerState, context); + } + } + + if (rotation) { + context.restore(); + } + + this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, frameState); + + if (!this.renderedVisible_) { + this.canvas_.style.display = ''; + this.renderedVisible_ = true; + } + + this.scheduleRemoveUnusedLayerRenderers(frameState); + this.scheduleExpireIconCache(frameState); + } /** - * @private - * @type {module:ol/transform~Transform} + * @inheritDoc */ - this.transform_ = createTransform(); + 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()); + + let i; + for (i = numLayers - 1; i >= 0; --i) { + const layerState = layerStates[i]; + const layer = layerState.layer; + if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) { + const layerRenderer = /** @type {module:ol/renderer/canvas/Layer} */ (this.getLayerRenderer(layer)); + result = layerRenderer.forEachLayerAtCoordinate( + coordinate, frameState, hitTolerance, callback, thisArg); + if (result) { + return result; + } + } + } + return undefined; + } + + /** + * @inheritDoc + */ + registerLayerRenderers(constructors) { + MapRenderer.prototype.registerLayerRenderers.call(this, constructors); + for (let i = 0, ii = constructors.length; i < ii; ++i) { + const ctor = constructors[i]; + if (!includes(layerRendererConstructors, ctor)) { + layerRendererConstructors.push(ctor); + } + } + } +} inherits(CanvasMapRenderer, MapRenderer); -/** - * @param {module:ol/render/EventType} type Event type. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @private - */ -CanvasMapRenderer.prototype.dispatchComposeEvent_ = function(type, frameState) { - const map = this.getMap(); - const context = this.context_; - if (map.hasListener(type)) { - const extent = frameState.extent; - const pixelRatio = frameState.pixelRatio; - const viewState = frameState.viewState; - const rotation = viewState.rotation; - - const transform = this.getTransform(frameState); - - const vectorContext = new CanvasImmediateRenderer(context, pixelRatio, - extent, transform, rotation); - const composeEvent = new RenderEvent(type, vectorContext, - frameState, context, null); - map.dispatchEvent(composeEvent); - } -}; - - -/** - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @protected - * @return {!module:ol/transform~Transform} Transform. - */ -CanvasMapRenderer.prototype.getTransform = function(frameState) { - const viewState = frameState.viewState; - const dx1 = this.canvas_.width / 2; - const dy1 = this.canvas_.height / 2; - const sx = frameState.pixelRatio / viewState.resolution; - const sy = -sx; - const angle = -viewState.rotation; - const dx2 = -viewState.center[0]; - const dy2 = -viewState.center[1]; - return composeTransform(this.transform_, dx1, dy1, sx, sy, angle, dx2, dy2); -}; - - -/** - * @inheritDoc - */ -CanvasMapRenderer.prototype.renderFrame = function(frameState) { - - if (!frameState) { - if (this.renderedVisible_) { - this.canvas_.style.display = 'none'; - this.renderedVisible_ = false; - } - return; - } - - const context = this.context_; - const pixelRatio = frameState.pixelRatio; - const width = Math.round(frameState.size[0] * pixelRatio); - const height = Math.round(frameState.size[1] * pixelRatio); - if (this.canvas_.width != width || this.canvas_.height != height) { - this.canvas_.width = width; - this.canvas_.height = height; - } else { - context.clearRect(0, 0, width, height); - } - - const rotation = frameState.viewState.rotation; - - this.calculateMatrices2D(frameState); - - this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, frameState); - - const layerStatesArray = frameState.layerStatesArray; - stableSort(layerStatesArray, sortByZIndex); - - if (rotation) { - context.save(); - rotateAtOffset(context, rotation, width / 2, height / 2); - } - - const viewResolution = frameState.viewState.resolution; - let i, ii, layer, layerRenderer, layerState; - for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { - layerState = layerStatesArray[i]; - layer = layerState.layer; - layerRenderer = /** @type {module:ol/renderer/canvas/Layer} */ (this.getLayerRenderer(layer)); - if (!visibleAtResolution(layerState, viewResolution) || - layerState.sourceState != SourceState.READY) { - continue; - } - if (layerRenderer.prepareFrame(frameState, layerState)) { - layerRenderer.composeFrame(frameState, layerState, context); - } - } - - if (rotation) { - context.restore(); - } - - this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, frameState); - - if (!this.renderedVisible_) { - this.canvas_.style.display = ''; - this.renderedVisible_ = true; - } - - this.scheduleRemoveUnusedLayerRenderers(frameState); - this.scheduleExpireIconCache(frameState); -}; - - -/** - * @inheritDoc - */ -CanvasMapRenderer.prototype.forEachLayerAtPixel = function(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()); - - let i; - for (i = numLayers - 1; i >= 0; --i) { - const layerState = layerStates[i]; - const layer = layerState.layer; - if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) { - const layerRenderer = /** @type {module:ol/renderer/canvas/Layer} */ (this.getLayerRenderer(layer)); - result = layerRenderer.forEachLayerAtCoordinate( - coordinate, frameState, hitTolerance, callback, thisArg); - if (result) { - return result; - } - } - } - return undefined; -}; - - -/** - * @inheritDoc - */ -CanvasMapRenderer.prototype.registerLayerRenderers = function(constructors) { - MapRenderer.prototype.registerLayerRenderers.call(this, constructors); - for (let i = 0, ii = constructors.length; i < ii; ++i) { - const ctor = constructors[i]; - if (!includes(layerRendererConstructors, ctor)) { - layerRendererConstructors.push(ctor); - } - } -}; - export default CanvasMapRenderer; diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index 68d6423880..b0069fa973 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -17,71 +17,339 @@ import {create as createTransform, compose as composeTransform} from '../../tran * @param {module:ol/layer/Tile|module:ol/layer/VectorTile} tileLayer Tile layer. * @api */ -const CanvasTileLayerRenderer = function(tileLayer) { +class CanvasTileLayerRenderer { + constructor(tileLayer) { - IntermediateCanvasRenderer.call(this, tileLayer); + IntermediateCanvasRenderer.call(this, tileLayer); - /** - * @protected - * @type {CanvasRenderingContext2D} - */ - this.context = this.context === null ? null : createCanvasContext2D(); + /** + * @protected + * @type {CanvasRenderingContext2D} + */ + this.context = this.context === null ? null : createCanvasContext2D(); + + /** + * @private + * @type {number} + */ + this.oversampling_; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.renderedExtent_ = null; + + /** + * @protected + * @type {number} + */ + this.renderedRevision; + + /** + * @protected + * @type {!Array.} + */ + this.renderedTiles = []; + + /** + * @private + * @type {boolean} + */ + this.newTiles_ = false; + + /** + * @protected + * @type {module:ol/extent~Extent} + */ + this.tmpExtent = createEmpty(); + + /** + * @private + * @type {module:ol/TileRange} + */ + this.tmpTileRange_ = new TileRange(0, 0, 0, 0); + + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.imageTransform_ = createTransform(); + + /** + * @protected + * @type {number} + */ + this.zDirection = 0; + + } /** * @private - * @type {number} + * @param {module:ol/Tile} tile Tile. + * @return {boolean} Tile is drawable. */ - this.oversampling_; + isDrawableTile_(tile) { + const tileState = tile.getState(); + const useInterimTilesOnError = this.getLayer().getUseInterimTilesOnError(); + return tileState == TileState.LOADED || + tileState == TileState.EMPTY || + tileState == TileState.ERROR && !useInterimTilesOnError; + } /** - * @private - * @type {module:ol/extent~Extent} + * @param {number} z Tile coordinate z. + * @param {number} x Tile coordinate x. + * @param {number} y Tile coordinate y. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @return {!module:ol/Tile} Tile. */ - this.renderedExtent_ = null; + getTile(z, x, y, pixelRatio, projection) { + const layer = this.getLayer(); + const source = /** @type {module:ol/source/Tile} */ (layer.getSource()); + let tile = source.getTile(z, x, y, pixelRatio, projection); + if (tile.getState() == TileState.ERROR) { + if (!layer.getUseInterimTilesOnError()) { + // When useInterimTilesOnError is false, we consider the error tile as loaded. + tile.setState(TileState.LOADED); + } else if (layer.getPreload() > 0) { + // Preloaded tiles for lower resolutions might have finished loading. + this.newTiles_ = true; + } + } + if (!this.isDrawableTile_(tile)) { + tile = tile.getInterimTile(); + } + return tile; + } /** - * @protected - * @type {number} + * @inheritDoc */ - this.renderedRevision; + prepareFrame(frameState, layerState) { + + const pixelRatio = frameState.pixelRatio; + const size = frameState.size; + const viewState = frameState.viewState; + const projection = viewState.projection; + const viewResolution = viewState.resolution; + const viewCenter = viewState.center; + + const tileLayer = this.getLayer(); + const tileSource = /** @type {module:ol/source/Tile} */ (tileLayer.getSource()); + const sourceRevision = tileSource.getRevision(); + const tileGrid = tileSource.getTileGridForProjection(projection); + const z = tileGrid.getZForResolution(viewResolution, this.zDirection); + const tileResolution = tileGrid.getResolution(z); + let oversampling = Math.round(viewResolution / tileResolution) || 1; + let extent = frameState.extent; + + if (layerState.extent !== undefined) { + extent = getIntersection(extent, layerState.extent); + } + if (isEmpty(extent)) { + // Return false to prevent the rendering of the layer. + return false; + } + + const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z); + const imageExtent = tileGrid.getTileRangeExtent(z, tileRange); + + const tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio); + + /** + * @type {Object.>} + */ + const tilesToDrawByZ = {}; + tilesToDrawByZ[z] = {}; + + const findLoadedTiles = this.createLoadedTileFinder( + tileSource, projection, tilesToDrawByZ); + + const hints = frameState.viewHints; + const animatingOrInteracting = hints[ViewHint.ANIMATING] || hints[ViewHint.INTERACTING]; + + const tmpExtent = this.tmpExtent; + const tmpTileRange = this.tmpTileRange_; + this.newTiles_ = false; + let tile, x, y; + for (x = tileRange.minX; x <= tileRange.maxX; ++x) { + for (y = tileRange.minY; y <= tileRange.maxY; ++y) { + if (Date.now() - frameState.time > 16 && animatingOrInteracting) { + continue; + } + tile = this.getTile(z, x, y, pixelRatio, projection); + if (this.isDrawableTile_(tile)) { + const uid = getUid(this); + if (tile.getState() == TileState.LOADED) { + tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; + const inTransition = tile.inTransition(uid); + if (!this.newTiles_ && (inTransition || this.renderedTiles.indexOf(tile) === -1)) { + this.newTiles_ = true; + } + } + if (tile.getAlpha(uid, frameState.time) === 1) { + // don't look for alt tiles if alpha is 1 + continue; + } + } + + const childTileRange = tileGrid.getTileCoordChildTileRange( + tile.tileCoord, tmpTileRange, tmpExtent); + let covered = false; + if (childTileRange) { + covered = findLoadedTiles(z + 1, childTileRange); + } + if (!covered) { + tileGrid.forEachTileCoordParentTileRange( + tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); + } + + } + } + + const renderedResolution = tileResolution * pixelRatio / tilePixelRatio * oversampling; + if (!(this.renderedResolution && Date.now() - frameState.time > 16 && animatingOrInteracting) && ( + this.newTiles_ || + !(this.renderedExtent_ && containsExtent(this.renderedExtent_, extent)) || + this.renderedRevision != sourceRevision || + oversampling != this.oversampling_ || + !animatingOrInteracting && renderedResolution != this.renderedResolution + )) { + + const context = this.context; + if (context) { + const tilePixelSize = tileSource.getTilePixelSize(z, pixelRatio, projection); + const width = Math.round(tileRange.getWidth() * tilePixelSize[0] / oversampling); + const height = Math.round(tileRange.getHeight() * tilePixelSize[1] / oversampling); + const canvas = context.canvas; + if (canvas.width != width || canvas.height != height) { + this.oversampling_ = oversampling; + canvas.width = width; + canvas.height = height; + } else { + if (this.renderedExtent_ && !equals(imageExtent, this.renderedExtent_)) { + context.clearRect(0, 0, width, height); + } + oversampling = this.oversampling_; + } + } + + this.renderedTiles.length = 0; + /** @type {Array.} */ + const zs = Object.keys(tilesToDrawByZ).map(Number); + zs.sort(function(a, b) { + if (a === z) { + return 1; + } else if (b === z) { + return -1; + } else { + return a > b ? 1 : a < b ? -1 : 0; + } + }); + let currentResolution, currentScale, currentTilePixelSize, currentZ, i, ii; + let tileExtent, tileGutter, tilesToDraw, w, h; + for (i = 0, ii = zs.length; i < ii; ++i) { + currentZ = zs[i]; + currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection); + currentResolution = tileGrid.getResolution(currentZ); + currentScale = currentResolution / tileResolution; + tileGutter = tilePixelRatio * tileSource.getGutter(projection); + tilesToDraw = tilesToDrawByZ[currentZ]; + for (const tileCoordKey in tilesToDraw) { + tile = tilesToDraw[tileCoordKey]; + tileExtent = tileGrid.getTileCoordExtent(tile.getTileCoord(), tmpExtent); + x = (tileExtent[0] - imageExtent[0]) / tileResolution * tilePixelRatio / oversampling; + y = (imageExtent[3] - tileExtent[3]) / tileResolution * tilePixelRatio / oversampling; + w = currentTilePixelSize[0] * currentScale / oversampling; + h = currentTilePixelSize[1] * currentScale / oversampling; + this.drawTileImage(tile, frameState, layerState, x, y, w, h, tileGutter, z === currentZ); + this.renderedTiles.push(tile); + } + } + + this.renderedRevision = sourceRevision; + this.renderedResolution = tileResolution * pixelRatio / tilePixelRatio * oversampling; + this.renderedExtent_ = imageExtent; + } + + const scale = this.renderedResolution / viewResolution; + const transform = composeTransform(this.imageTransform_, + pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, + scale, scale, + 0, + (this.renderedExtent_[0] - viewCenter[0]) / this.renderedResolution * pixelRatio, + (viewCenter[1] - this.renderedExtent_[3]) / this.renderedResolution * pixelRatio); + composeTransform(this.coordinateToCanvasPixelTransform, + pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], + pixelRatio / viewResolution, -pixelRatio / viewResolution, + 0, + -viewCenter[0], -viewCenter[1]); + + + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); + this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio, + projection, extent, z, tileLayer.getPreload()); + this.scheduleExpireCache(frameState, tileSource); + + return this.renderedTiles.length > 0; + } /** - * @protected - * @type {!Array.} + * @param {module:ol/Tile} tile Tile. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/layer/Layer~State} layerState Layer state. + * @param {number} x Left of the tile. + * @param {number} y Top of the tile. + * @param {number} w Width of the tile. + * @param {number} h Height of the tile. + * @param {number} gutter Tile gutter. + * @param {boolean} transition Apply an alpha transition. */ - this.renderedTiles = []; + drawTileImage(tile, frameState, layerState, x, y, w, h, gutter, transition) { + const image = tile.getImage(this.getLayer()); + if (!image) { + return; + } + const uid = getUid(this); + const alpha = transition ? tile.getAlpha(uid, frameState.time) : 1; + if (alpha === 1 && !this.getLayer().getSource().getOpaque(frameState.viewState.projection)) { + this.context.clearRect(x, y, w, h); + } + const alphaChanged = alpha !== this.context.globalAlpha; + if (alphaChanged) { + this.context.save(); + this.context.globalAlpha = alpha; + } + this.context.drawImage(image, gutter, gutter, + image.width - 2 * gutter, image.height - 2 * gutter, x, y, w, h); + + if (alphaChanged) { + this.context.restore(); + } + if (alpha !== 1) { + frameState.animate = true; + } else if (transition) { + tile.endTransition(uid); + } + } /** - * @private - * @type {boolean} + * @inheritDoc */ - this.newTiles_ = false; + getImage() { + const context = this.context; + return context ? context.canvas : null; + } /** - * @protected - * @type {module:ol/extent~Extent} + * @inheritDoc */ - this.tmpExtent = createEmpty(); - - /** - * @private - * @type {module:ol/TileRange} - */ - this.tmpTileRange_ = new TileRange(0, 0, 0, 0); - - /** - * @private - * @type {module:ol/transform~Transform} - */ - this.imageTransform_ = createTransform(); - - /** - * @protected - * @type {number} - */ - this.zDirection = 0; - -}; + getImageTransform() { + return this.imageTransform_; + } +} inherits(CanvasTileLayerRenderer, IntermediateCanvasRenderer); @@ -107,269 +375,6 @@ CanvasTileLayerRenderer['create'] = function(mapRenderer, layer) { }; -/** - * @private - * @param {module:ol/Tile} tile Tile. - * @return {boolean} Tile is drawable. - */ -CanvasTileLayerRenderer.prototype.isDrawableTile_ = function(tile) { - const tileState = tile.getState(); - const useInterimTilesOnError = this.getLayer().getUseInterimTilesOnError(); - return tileState == TileState.LOADED || - tileState == TileState.EMPTY || - tileState == TileState.ERROR && !useInterimTilesOnError; -}; - - -/** - * @param {number} z Tile coordinate z. - * @param {number} x Tile coordinate x. - * @param {number} y Tile coordinate y. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @return {!module:ol/Tile} Tile. - */ -CanvasTileLayerRenderer.prototype.getTile = function(z, x, y, pixelRatio, projection) { - const layer = this.getLayer(); - const source = /** @type {module:ol/source/Tile} */ (layer.getSource()); - let tile = source.getTile(z, x, y, pixelRatio, projection); - if (tile.getState() == TileState.ERROR) { - if (!layer.getUseInterimTilesOnError()) { - // When useInterimTilesOnError is false, we consider the error tile as loaded. - tile.setState(TileState.LOADED); - } else if (layer.getPreload() > 0) { - // Preloaded tiles for lower resolutions might have finished loading. - this.newTiles_ = true; - } - } - if (!this.isDrawableTile_(tile)) { - tile = tile.getInterimTile(); - } - return tile; -}; - -/** - * @inheritDoc - */ -CanvasTileLayerRenderer.prototype.prepareFrame = function(frameState, layerState) { - - const pixelRatio = frameState.pixelRatio; - const size = frameState.size; - const viewState = frameState.viewState; - const projection = viewState.projection; - const viewResolution = viewState.resolution; - const viewCenter = viewState.center; - - const tileLayer = this.getLayer(); - const tileSource = /** @type {module:ol/source/Tile} */ (tileLayer.getSource()); - const sourceRevision = tileSource.getRevision(); - const tileGrid = tileSource.getTileGridForProjection(projection); - const z = tileGrid.getZForResolution(viewResolution, this.zDirection); - const tileResolution = tileGrid.getResolution(z); - let oversampling = Math.round(viewResolution / tileResolution) || 1; - let extent = frameState.extent; - - if (layerState.extent !== undefined) { - extent = getIntersection(extent, layerState.extent); - } - if (isEmpty(extent)) { - // Return false to prevent the rendering of the layer. - return false; - } - - const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z); - const imageExtent = tileGrid.getTileRangeExtent(z, tileRange); - - const tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio); - - /** - * @type {Object.>} - */ - const tilesToDrawByZ = {}; - tilesToDrawByZ[z] = {}; - - const findLoadedTiles = this.createLoadedTileFinder( - tileSource, projection, tilesToDrawByZ); - - const hints = frameState.viewHints; - const animatingOrInteracting = hints[ViewHint.ANIMATING] || hints[ViewHint.INTERACTING]; - - const tmpExtent = this.tmpExtent; - const tmpTileRange = this.tmpTileRange_; - this.newTiles_ = false; - let tile, x, y; - for (x = tileRange.minX; x <= tileRange.maxX; ++x) { - for (y = tileRange.minY; y <= tileRange.maxY; ++y) { - if (Date.now() - frameState.time > 16 && animatingOrInteracting) { - continue; - } - tile = this.getTile(z, x, y, pixelRatio, projection); - if (this.isDrawableTile_(tile)) { - const uid = getUid(this); - if (tile.getState() == TileState.LOADED) { - tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; - const inTransition = tile.inTransition(uid); - if (!this.newTiles_ && (inTransition || this.renderedTiles.indexOf(tile) === -1)) { - this.newTiles_ = true; - } - } - if (tile.getAlpha(uid, frameState.time) === 1) { - // don't look for alt tiles if alpha is 1 - continue; - } - } - - const childTileRange = tileGrid.getTileCoordChildTileRange( - tile.tileCoord, tmpTileRange, tmpExtent); - let covered = false; - if (childTileRange) { - covered = findLoadedTiles(z + 1, childTileRange); - } - if (!covered) { - tileGrid.forEachTileCoordParentTileRange( - tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); - } - - } - } - - const renderedResolution = tileResolution * pixelRatio / tilePixelRatio * oversampling; - if (!(this.renderedResolution && Date.now() - frameState.time > 16 && animatingOrInteracting) && ( - this.newTiles_ || - !(this.renderedExtent_ && containsExtent(this.renderedExtent_, extent)) || - this.renderedRevision != sourceRevision || - oversampling != this.oversampling_ || - !animatingOrInteracting && renderedResolution != this.renderedResolution - )) { - - const context = this.context; - if (context) { - const tilePixelSize = tileSource.getTilePixelSize(z, pixelRatio, projection); - const width = Math.round(tileRange.getWidth() * tilePixelSize[0] / oversampling); - const height = Math.round(tileRange.getHeight() * tilePixelSize[1] / oversampling); - const canvas = context.canvas; - if (canvas.width != width || canvas.height != height) { - this.oversampling_ = oversampling; - canvas.width = width; - canvas.height = height; - } else { - if (this.renderedExtent_ && !equals(imageExtent, this.renderedExtent_)) { - context.clearRect(0, 0, width, height); - } - oversampling = this.oversampling_; - } - } - - this.renderedTiles.length = 0; - /** @type {Array.} */ - const zs = Object.keys(tilesToDrawByZ).map(Number); - zs.sort(function(a, b) { - if (a === z) { - return 1; - } else if (b === z) { - return -1; - } else { - return a > b ? 1 : a < b ? -1 : 0; - } - }); - let currentResolution, currentScale, currentTilePixelSize, currentZ, i, ii; - let tileExtent, tileGutter, tilesToDraw, w, h; - for (i = 0, ii = zs.length; i < ii; ++i) { - currentZ = zs[i]; - currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection); - currentResolution = tileGrid.getResolution(currentZ); - currentScale = currentResolution / tileResolution; - tileGutter = tilePixelRatio * tileSource.getGutter(projection); - tilesToDraw = tilesToDrawByZ[currentZ]; - for (const tileCoordKey in tilesToDraw) { - tile = tilesToDraw[tileCoordKey]; - tileExtent = tileGrid.getTileCoordExtent(tile.getTileCoord(), tmpExtent); - x = (tileExtent[0] - imageExtent[0]) / tileResolution * tilePixelRatio / oversampling; - y = (imageExtent[3] - tileExtent[3]) / tileResolution * tilePixelRatio / oversampling; - w = currentTilePixelSize[0] * currentScale / oversampling; - h = currentTilePixelSize[1] * currentScale / oversampling; - this.drawTileImage(tile, frameState, layerState, x, y, w, h, tileGutter, z === currentZ); - this.renderedTiles.push(tile); - } - } - - this.renderedRevision = sourceRevision; - this.renderedResolution = tileResolution * pixelRatio / tilePixelRatio * oversampling; - this.renderedExtent_ = imageExtent; - } - - const scale = this.renderedResolution / viewResolution; - const transform = composeTransform(this.imageTransform_, - pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, - scale, scale, - 0, - (this.renderedExtent_[0] - viewCenter[0]) / this.renderedResolution * pixelRatio, - (viewCenter[1] - this.renderedExtent_[3]) / this.renderedResolution * pixelRatio); - composeTransform(this.coordinateToCanvasPixelTransform, - pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], - pixelRatio / viewResolution, -pixelRatio / viewResolution, - 0, - -viewCenter[0], -viewCenter[1]); - - - this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); - this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio, - projection, extent, z, tileLayer.getPreload()); - this.scheduleExpireCache(frameState, tileSource); - - return this.renderedTiles.length > 0; -}; - - -/** - * @param {module:ol/Tile} tile Tile. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/layer/Layer~State} layerState Layer state. - * @param {number} x Left of the tile. - * @param {number} y Top of the tile. - * @param {number} w Width of the tile. - * @param {number} h Height of the tile. - * @param {number} gutter Tile gutter. - * @param {boolean} transition Apply an alpha transition. - */ -CanvasTileLayerRenderer.prototype.drawTileImage = function(tile, frameState, layerState, x, y, w, h, gutter, transition) { - const image = tile.getImage(this.getLayer()); - if (!image) { - return; - } - const uid = getUid(this); - const alpha = transition ? tile.getAlpha(uid, frameState.time) : 1; - if (alpha === 1 && !this.getLayer().getSource().getOpaque(frameState.viewState.projection)) { - this.context.clearRect(x, y, w, h); - } - const alphaChanged = alpha !== this.context.globalAlpha; - if (alphaChanged) { - this.context.save(); - this.context.globalAlpha = alpha; - } - this.context.drawImage(image, gutter, gutter, - image.width - 2 * gutter, image.height - 2 * gutter, x, y, w, h); - - if (alphaChanged) { - this.context.restore(); - } - if (alpha !== 1) { - frameState.animate = true; - } else if (transition) { - tile.endTransition(uid); - } -}; - - -/** - * @inheritDoc - */ -CanvasTileLayerRenderer.prototype.getImage = function() { - const context = this.context; - return context ? context.canvas : null; -}; - - /** * @function * @return {module:ol/layer/Tile|module:ol/layer/VectorTile} @@ -377,10 +382,4 @@ CanvasTileLayerRenderer.prototype.getImage = function() { CanvasTileLayerRenderer.prototype.getLayer; -/** - * @inheritDoc - */ -CanvasTileLayerRenderer.prototype.getImageTransform = function() { - return this.imageTransform_; -}; export default CanvasTileLayerRenderer; diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index f2e2fe9235..206c5f5c49 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -21,66 +21,391 @@ import {defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, * @param {module:ol/layer/Vector} vectorLayer Vector layer. * @api */ -const CanvasVectorLayerRenderer = function(vectorLayer) { +class CanvasVectorLayerRenderer { + constructor(vectorLayer) { - CanvasLayerRenderer.call(this, vectorLayer); + CanvasLayerRenderer.call(this, vectorLayer); + + /** + * Declutter tree. + * @private + */ + this.declutterTree_ = vectorLayer.getDeclutter() ? rbush(9, undefined) : null; + + /** + * @private + * @type {boolean} + */ + this.dirty_ = false; + + /** + * @private + * @type {number} + */ + this.renderedRevision_ = -1; + + /** + * @private + * @type {number} + */ + this.renderedResolution_ = NaN; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.renderedExtent_ = createEmpty(); + + /** + * @private + * @type {function(module:ol/Feature, module:ol/Feature): number|null} + */ + this.renderedRenderOrder_ = null; + + /** + * @private + * @type {module:ol/render/canvas/ReplayGroup} + */ + this.replayGroup_ = null; + + /** + * A new replay group had to be created by `prepareFrame()` + * @type {boolean} + */ + this.replayGroupChanged = true; + + /** + * @type {CanvasRenderingContext2D} + */ + this.context = createCanvasContext2D(); + + listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); + + } /** - * Declutter tree. + * @inheritDoc + */ + disposeInternal() { + unlisten(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); + CanvasLayerRenderer.prototype.disposeInternal.call(this); + } + + /** + * @param {CanvasRenderingContext2D} context Context. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/layer/Layer~State} layerState Layer state. + */ + compose(context, frameState, layerState) { + const extent = frameState.extent; + const pixelRatio = frameState.pixelRatio; + const skippedFeatureUids = layerState.managed ? + frameState.skippedFeatureUids : {}; + const viewState = frameState.viewState; + const projection = viewState.projection; + const rotation = viewState.rotation; + const projectionExtent = projection.getExtent(); + const vectorSource = /** @type {module:ol/source/Vector} */ (this.getLayer().getSource()); + + let transform = this.getTransform(frameState, 0); + + // clipped rendering if layer extent is set + const clipExtent = layerState.extent; + const clipped = clipExtent !== undefined; + if (clipped) { + this.clip(context, frameState, /** @type {module:ol/extent~Extent} */ (clipExtent)); + } + const replayGroup = this.replayGroup_; + if (replayGroup && !replayGroup.isEmpty()) { + if (this.declutterTree_) { + this.declutterTree_.clear(); + } + const layer = /** @type {module:ol/layer/Vector} */ (this.getLayer()); + let drawOffsetX = 0; + let drawOffsetY = 0; + let replayContext; + const transparentLayer = layerState.opacity !== 1; + const hasRenderListeners = layer.hasListener(RenderEventType.RENDER); + if (transparentLayer || hasRenderListeners) { + let drawWidth = context.canvas.width; + let drawHeight = context.canvas.height; + if (rotation) { + const drawSize = Math.round(Math.sqrt(drawWidth * drawWidth + drawHeight * drawHeight)); + drawOffsetX = (drawSize - drawWidth) / 2; + drawOffsetY = (drawSize - drawHeight) / 2; + drawWidth = drawHeight = drawSize; + } + // resize and clear + this.context.canvas.width = drawWidth; + this.context.canvas.height = drawHeight; + replayContext = this.context; + } else { + replayContext = context; + } + + const alpha = replayContext.globalAlpha; + if (!transparentLayer) { + // for performance reasons, context.save / context.restore is not used + // to save and restore the transformation matrix and the opacity. + // see http://jsperf.com/context-save-restore-versus-variable + replayContext.globalAlpha = layerState.opacity; + } + + if (replayContext != context) { + replayContext.translate(drawOffsetX, drawOffsetY); + } + + const width = frameState.size[0] * pixelRatio; + const height = frameState.size[1] * pixelRatio; + rotateAtOffset(replayContext, -rotation, + width / 2, height / 2); + replayGroup.replay(replayContext, transform, rotation, skippedFeatureUids); + 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(replayContext, transform, rotation, skippedFeatureUids); + startX += worldWidth; + } + world = 0; + startX = extent[2]; + while (startX > projectionExtent[2]) { + ++world; + offsetX = worldWidth * world; + transform = this.getTransform(frameState, offsetX); + replayGroup.replay(replayContext, transform, rotation, skippedFeatureUids); + startX -= worldWidth; + } + } + rotateAtOffset(replayContext, rotation, + width / 2, height / 2); + + if (hasRenderListeners) { + this.dispatchRenderEvent(replayContext, frameState, transform); + } + if (replayContext != context) { + if (transparentLayer) { + const mainContextAlpha = context.globalAlpha; + context.globalAlpha = layerState.opacity; + context.drawImage(replayContext.canvas, -drawOffsetX, -drawOffsetY); + context.globalAlpha = mainContextAlpha; + } else { + context.drawImage(replayContext.canvas, -drawOffsetX, -drawOffsetY); + } + replayContext.translate(-drawOffsetX, -drawOffsetY); + } + + if (!transparentLayer) { + replayContext.globalAlpha = alpha; + } + } + + if (clipped) { + context.restore(); + } + } + + /** + * @inheritDoc + */ + composeFrame(frameState, layerState, context) { + const transform = this.getTransform(frameState, 0); + this.preCompose(context, frameState, transform); + this.compose(context, frameState, layerState); + this.postCompose(context, frameState, layerState, transform); + } + + /** + * @inheritDoc + */ + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + if (!this.replayGroup_) { + return undefined; + } else { + const resolution = frameState.viewState.resolution; + const rotation = frameState.viewState.rotation; + const layer = /** @type {module:ol/layer/Vector} */ (this.getLayer()); + /** @type {!Object.} */ + const features = {}; + const result = this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + const key = getUid(feature).toString(); + if (!(key in features)) { + features[key] = true; + return callback.call(thisArg, feature, layer); + } + }, null); + return result; + } + } + + /** + * @param {module:ol/events/Event} event Event. + */ + handleFontsChanged_(event) { + const layer = this.getLayer(); + if (layer.getVisible() && this.replayGroup_) { + layer.changed(); + } + } + + /** + * Handle changes in image style state. + * @param {module:ol/events/Event} event Image style change event. * @private */ - this.declutterTree_ = vectorLayer.getDeclutter() ? rbush(9, undefined) : null; + handleStyleImageChange_(event) { + this.renderIfReadyAndVisible(); + } /** - * @private - * @type {boolean} + * @inheritDoc */ - this.dirty_ = false; + prepareFrame(frameState, layerState) { + const vectorLayer = /** @type {module:ol/layer/Vector} */ (this.getLayer()); + const vectorSource = vectorLayer.getSource(); + + const animating = frameState.viewHints[ViewHint.ANIMATING]; + const interacting = frameState.viewHints[ViewHint.INTERACTING]; + const updateWhileAnimating = vectorLayer.getUpdateWhileAnimating(); + const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting(); + + if (!this.dirty_ && (!updateWhileAnimating && animating) || + (!updateWhileInteracting && interacting)) { + return true; + } + + const frameStateExtent = frameState.extent; + const viewState = frameState.viewState; + const projection = viewState.projection; + const resolution = viewState.resolution; + const pixelRatio = frameState.pixelRatio; + const vectorLayerRevision = vectorLayer.getRevision(); + const vectorLayerRenderBuffer = vectorLayer.getRenderBuffer(); + let vectorLayerRenderOrder = vectorLayer.getRenderOrder(); + + if (vectorLayerRenderOrder === undefined) { + vectorLayerRenderOrder = defaultRenderOrder; + } + + const extent = buffer(frameStateExtent, + vectorLayerRenderBuffer * resolution); + const projectionExtent = viewState.projection.getExtent(); + + if (vectorSource.getWrapX() && viewState.projection.canWrapX() && + !containsExtent(projectionExtent, frameState.extent)) { + // For the replay group, we need an extent that intersects the real world + // (-180° to +180°). To support geometries in a coordinate range from -540° + // to +540°, we add at least 1 world width on each side of the projection + // extent. If the viewport is wider than the world, we need to add half of + // the viewport width to make sure we cover the whole viewport. + const worldWidth = getWidth(projectionExtent); + const gutter = Math.max(getWidth(extent) / 2, worldWidth); + extent[0] = projectionExtent[0] - gutter; + extent[2] = projectionExtent[2] + gutter; + } + + if (!this.dirty_ && + this.renderedResolution_ == resolution && + this.renderedRevision_ == vectorLayerRevision && + this.renderedRenderOrder_ == vectorLayerRenderOrder && + containsExtent(this.renderedExtent_, extent)) { + this.replayGroupChanged = false; + return true; + } + + this.replayGroup_ = null; + + this.dirty_ = false; + + const replayGroup = new CanvasReplayGroup( + getRenderTolerance(resolution, pixelRatio), extent, resolution, + pixelRatio, vectorSource.getOverlaps(), this.declutterTree_, vectorLayer.getRenderBuffer()); + vectorSource.loadFeatures(extent, resolution, projection); + /** + * @param {module:ol/Feature} feature Feature. + * @this {module:ol/renderer/canvas/VectorLayer} + */ + const render = function(feature) { + let styles; + const styleFunction = feature.getStyleFunction() || vectorLayer.getStyleFunction(); + if (styleFunction) { + styles = styleFunction(feature, resolution); + } + if (styles) { + const dirty = this.renderFeature( + feature, resolution, pixelRatio, styles, replayGroup); + this.dirty_ = this.dirty_ || dirty; + } + }.bind(this); + if (vectorLayerRenderOrder) { + /** @type {Array.} */ + const features = []; + vectorSource.forEachFeatureInExtent(extent, + /** + * @param {module:ol/Feature} feature Feature. + */ + function(feature) { + features.push(feature); + }, this); + features.sort(vectorLayerRenderOrder); + for (let i = 0, ii = features.length; i < ii; ++i) { + render(features[i]); + } + } else { + vectorSource.forEachFeatureInExtent(extent, render, this); + } + replayGroup.finish(); + + this.renderedResolution_ = resolution; + this.renderedRevision_ = vectorLayerRevision; + this.renderedRenderOrder_ = vectorLayerRenderOrder; + this.renderedExtent_ = extent; + this.replayGroup_ = replayGroup; + + this.replayGroupChanged = true; + return true; + } /** - * @private - * @type {number} + * @param {module:ol/Feature} feature Feature. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {(module:ol/style/Style|Array.)} styles The style or array of styles. + * @param {module:ol/render/canvas/ReplayGroup} replayGroup Replay group. + * @return {boolean} `true` if an image is loading. */ - this.renderedRevision_ = -1; - - /** - * @private - * @type {number} - */ - this.renderedResolution_ = NaN; - - /** - * @private - * @type {module:ol/extent~Extent} - */ - this.renderedExtent_ = createEmpty(); - - /** - * @private - * @type {function(module:ol/Feature, module:ol/Feature): number|null} - */ - this.renderedRenderOrder_ = null; - - /** - * @private - * @type {module:ol/render/canvas/ReplayGroup} - */ - this.replayGroup_ = null; - - /** - * A new replay group had to be created by `prepareFrame()` - * @type {boolean} - */ - this.replayGroupChanged = true; - - /** - * @type {CanvasRenderingContext2D} - */ - this.context = createCanvasContext2D(); - - listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); - -}; + renderFeature(feature, resolution, pixelRatio, styles, replayGroup) { + if (!styles) { + return false; + } + let loading = false; + if (Array.isArray(styles)) { + for (let i = 0, ii = styles.length; i < ii; ++i) { + loading = renderFeature( + replayGroup, feature, styles[i], + getSquaredRenderTolerance(resolution, pixelRatio), + this.handleStyleImageChange_, this) || loading; + } + } else { + loading = renderFeature( + replayGroup, feature, styles, + getSquaredRenderTolerance(resolution, pixelRatio), + this.handleStyleImageChange_, this); + } + return loading; + } +} inherits(CanvasVectorLayerRenderer, CanvasLayerRenderer); @@ -106,333 +431,4 @@ CanvasVectorLayerRenderer['create'] = function(mapRenderer, layer) { }; -/** - * @inheritDoc - */ -CanvasVectorLayerRenderer.prototype.disposeInternal = function() { - unlisten(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); - CanvasLayerRenderer.prototype.disposeInternal.call(this); -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/layer/Layer~State} layerState Layer state. - */ -CanvasVectorLayerRenderer.prototype.compose = function(context, frameState, layerState) { - const extent = frameState.extent; - const pixelRatio = frameState.pixelRatio; - const skippedFeatureUids = layerState.managed ? - frameState.skippedFeatureUids : {}; - const viewState = frameState.viewState; - const projection = viewState.projection; - const rotation = viewState.rotation; - const projectionExtent = projection.getExtent(); - const vectorSource = /** @type {module:ol/source/Vector} */ (this.getLayer().getSource()); - - let transform = this.getTransform(frameState, 0); - - // clipped rendering if layer extent is set - const clipExtent = layerState.extent; - const clipped = clipExtent !== undefined; - if (clipped) { - this.clip(context, frameState, /** @type {module:ol/extent~Extent} */ (clipExtent)); - } - const replayGroup = this.replayGroup_; - if (replayGroup && !replayGroup.isEmpty()) { - if (this.declutterTree_) { - this.declutterTree_.clear(); - } - const layer = /** @type {module:ol/layer/Vector} */ (this.getLayer()); - let drawOffsetX = 0; - let drawOffsetY = 0; - let replayContext; - const transparentLayer = layerState.opacity !== 1; - const hasRenderListeners = layer.hasListener(RenderEventType.RENDER); - if (transparentLayer || hasRenderListeners) { - let drawWidth = context.canvas.width; - let drawHeight = context.canvas.height; - if (rotation) { - const drawSize = Math.round(Math.sqrt(drawWidth * drawWidth + drawHeight * drawHeight)); - drawOffsetX = (drawSize - drawWidth) / 2; - drawOffsetY = (drawSize - drawHeight) / 2; - drawWidth = drawHeight = drawSize; - } - // resize and clear - this.context.canvas.width = drawWidth; - this.context.canvas.height = drawHeight; - replayContext = this.context; - } else { - replayContext = context; - } - - const alpha = replayContext.globalAlpha; - if (!transparentLayer) { - // for performance reasons, context.save / context.restore is not used - // to save and restore the transformation matrix and the opacity. - // see http://jsperf.com/context-save-restore-versus-variable - replayContext.globalAlpha = layerState.opacity; - } - - if (replayContext != context) { - replayContext.translate(drawOffsetX, drawOffsetY); - } - - const width = frameState.size[0] * pixelRatio; - const height = frameState.size[1] * pixelRatio; - rotateAtOffset(replayContext, -rotation, - width / 2, height / 2); - replayGroup.replay(replayContext, transform, rotation, skippedFeatureUids); - 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(replayContext, transform, rotation, skippedFeatureUids); - startX += worldWidth; - } - world = 0; - startX = extent[2]; - while (startX > projectionExtent[2]) { - ++world; - offsetX = worldWidth * world; - transform = this.getTransform(frameState, offsetX); - replayGroup.replay(replayContext, transform, rotation, skippedFeatureUids); - startX -= worldWidth; - } - } - rotateAtOffset(replayContext, rotation, - width / 2, height / 2); - - if (hasRenderListeners) { - this.dispatchRenderEvent(replayContext, frameState, transform); - } - if (replayContext != context) { - if (transparentLayer) { - const mainContextAlpha = context.globalAlpha; - context.globalAlpha = layerState.opacity; - context.drawImage(replayContext.canvas, -drawOffsetX, -drawOffsetY); - context.globalAlpha = mainContextAlpha; - } else { - context.drawImage(replayContext.canvas, -drawOffsetX, -drawOffsetY); - } - replayContext.translate(-drawOffsetX, -drawOffsetY); - } - - if (!transparentLayer) { - replayContext.globalAlpha = alpha; - } - } - - if (clipped) { - context.restore(); - } -}; - - -/** - * @inheritDoc - */ -CanvasVectorLayerRenderer.prototype.composeFrame = function(frameState, layerState, context) { - const transform = this.getTransform(frameState, 0); - this.preCompose(context, frameState, transform); - this.compose(context, frameState, layerState); - this.postCompose(context, frameState, layerState, transform); -}; - - -/** - * @inheritDoc - */ -CanvasVectorLayerRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { - if (!this.replayGroup_) { - return undefined; - } else { - const resolution = frameState.viewState.resolution; - const rotation = frameState.viewState.rotation; - const layer = /** @type {module:ol/layer/Vector} */ (this.getLayer()); - /** @type {!Object.} */ - const features = {}; - const result = this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - const key = getUid(feature).toString(); - if (!(key in features)) { - features[key] = true; - return callback.call(thisArg, feature, layer); - } - }, null); - return result; - } -}; - - -/** - * @param {module:ol/events/Event} event Event. - */ -CanvasVectorLayerRenderer.prototype.handleFontsChanged_ = function(event) { - const layer = this.getLayer(); - if (layer.getVisible() && this.replayGroup_) { - layer.changed(); - } -}; - - -/** - * Handle changes in image style state. - * @param {module:ol/events/Event} event Image style change event. - * @private - */ -CanvasVectorLayerRenderer.prototype.handleStyleImageChange_ = function(event) { - this.renderIfReadyAndVisible(); -}; - - -/** - * @inheritDoc - */ -CanvasVectorLayerRenderer.prototype.prepareFrame = function(frameState, layerState) { - const vectorLayer = /** @type {module:ol/layer/Vector} */ (this.getLayer()); - const vectorSource = vectorLayer.getSource(); - - const animating = frameState.viewHints[ViewHint.ANIMATING]; - const interacting = frameState.viewHints[ViewHint.INTERACTING]; - const updateWhileAnimating = vectorLayer.getUpdateWhileAnimating(); - const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting(); - - if (!this.dirty_ && (!updateWhileAnimating && animating) || - (!updateWhileInteracting && interacting)) { - return true; - } - - const frameStateExtent = frameState.extent; - const viewState = frameState.viewState; - const projection = viewState.projection; - const resolution = viewState.resolution; - const pixelRatio = frameState.pixelRatio; - const vectorLayerRevision = vectorLayer.getRevision(); - const vectorLayerRenderBuffer = vectorLayer.getRenderBuffer(); - let vectorLayerRenderOrder = vectorLayer.getRenderOrder(); - - if (vectorLayerRenderOrder === undefined) { - vectorLayerRenderOrder = defaultRenderOrder; - } - - const extent = buffer(frameStateExtent, - vectorLayerRenderBuffer * resolution); - const projectionExtent = viewState.projection.getExtent(); - - if (vectorSource.getWrapX() && viewState.projection.canWrapX() && - !containsExtent(projectionExtent, frameState.extent)) { - // For the replay group, we need an extent that intersects the real world - // (-180° to +180°). To support geometries in a coordinate range from -540° - // to +540°, we add at least 1 world width on each side of the projection - // extent. If the viewport is wider than the world, we need to add half of - // the viewport width to make sure we cover the whole viewport. - const worldWidth = getWidth(projectionExtent); - const gutter = Math.max(getWidth(extent) / 2, worldWidth); - extent[0] = projectionExtent[0] - gutter; - extent[2] = projectionExtent[2] + gutter; - } - - if (!this.dirty_ && - this.renderedResolution_ == resolution && - this.renderedRevision_ == vectorLayerRevision && - this.renderedRenderOrder_ == vectorLayerRenderOrder && - containsExtent(this.renderedExtent_, extent)) { - this.replayGroupChanged = false; - return true; - } - - this.replayGroup_ = null; - - this.dirty_ = false; - - const replayGroup = new CanvasReplayGroup( - getRenderTolerance(resolution, pixelRatio), extent, resolution, - pixelRatio, vectorSource.getOverlaps(), this.declutterTree_, vectorLayer.getRenderBuffer()); - vectorSource.loadFeatures(extent, resolution, projection); - /** - * @param {module:ol/Feature} feature Feature. - * @this {module:ol/renderer/canvas/VectorLayer} - */ - const render = function(feature) { - let styles; - const styleFunction = feature.getStyleFunction() || vectorLayer.getStyleFunction(); - if (styleFunction) { - styles = styleFunction(feature, resolution); - } - if (styles) { - const dirty = this.renderFeature( - feature, resolution, pixelRatio, styles, replayGroup); - this.dirty_ = this.dirty_ || dirty; - } - }.bind(this); - if (vectorLayerRenderOrder) { - /** @type {Array.} */ - const features = []; - vectorSource.forEachFeatureInExtent(extent, - /** - * @param {module:ol/Feature} feature Feature. - */ - function(feature) { - features.push(feature); - }, this); - features.sort(vectorLayerRenderOrder); - for (let i = 0, ii = features.length; i < ii; ++i) { - render(features[i]); - } - } else { - vectorSource.forEachFeatureInExtent(extent, render, this); - } - replayGroup.finish(); - - this.renderedResolution_ = resolution; - this.renderedRevision_ = vectorLayerRevision; - this.renderedRenderOrder_ = vectorLayerRenderOrder; - this.renderedExtent_ = extent; - this.replayGroup_ = replayGroup; - - this.replayGroupChanged = true; - return true; -}; - - -/** - * @param {module:ol/Feature} feature Feature. - * @param {number} resolution Resolution. - * @param {number} pixelRatio Pixel ratio. - * @param {(module:ol/style/Style|Array.)} styles The style or array of styles. - * @param {module:ol/render/canvas/ReplayGroup} replayGroup Replay group. - * @return {boolean} `true` if an image is loading. - */ -CanvasVectorLayerRenderer.prototype.renderFeature = function(feature, resolution, pixelRatio, styles, replayGroup) { - if (!styles) { - return false; - } - let loading = false; - if (Array.isArray(styles)) { - for (let i = 0, ii = styles.length; i < ii; ++i) { - loading = renderFeature( - replayGroup, feature, styles[i], - getSquaredRenderTolerance(resolution, pixelRatio), - this.handleStyleImageChange_, this) || loading; - } - } else { - loading = renderFeature( - replayGroup, feature, styles, - getSquaredRenderTolerance(resolution, pixelRatio), - this.handleStyleImageChange_, this); - } - return loading; -}; export default CanvasVectorLayerRenderer; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index afe459dc2a..deb400bafd 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -53,45 +53,426 @@ const VECTOR_REPLAYS = { * @param {module:ol/layer/VectorTile} layer VectorTile layer. * @api */ -const CanvasVectorTileLayerRenderer = function(layer) { +class CanvasVectorTileLayerRenderer { + constructor(layer) { + + /** + * @type {CanvasRenderingContext2D} + */ + this.context = null; + + CanvasTileLayerRenderer.call(this, layer); + + /** + * Declutter tree. + * @private + */ + this.declutterTree_ = layer.getDeclutter() ? rbush(9, undefined) : null; + + /** + * @private + * @type {boolean} + */ + this.dirty_ = false; + + /** + * @private + * @type {number} + */ + this.renderedLayerRevision_; + + /** + * @private + * @type {module:ol/transform~Transform} + */ + this.tmpTransform_ = createTransform(); + + // Use lower resolution for pure vector rendering. Closest resolution otherwise. + this.zDirection = layer.getRenderMode() == VectorTileRenderType.VECTOR ? 1 : 0; + + listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); + + } /** - * @type {CanvasRenderingContext2D} + * @inheritDoc */ - this.context = null; - - CanvasTileLayerRenderer.call(this, layer); + disposeInternal() { + unlisten(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); + CanvasTileLayerRenderer.prototype.disposeInternal.call(this); + } /** - * Declutter tree. + * @inheritDoc + */ + getTile(z, x, y, pixelRatio, projection) { + const tile = CanvasTileLayerRenderer.prototype.getTile.call(this, z, x, y, pixelRatio, projection); + if (tile.getState() === TileState.LOADED) { + this.createReplayGroup_(tile, pixelRatio, projection); + if (this.context) { + this.renderTileImage_(tile, pixelRatio, projection); + } + } + return tile; + } + + /** + * @inheritDoc + */ + prepareFrame(frameState, layerState) { + const layer = this.getLayer(); + const layerRevision = layer.getRevision(); + if (this.renderedLayerRevision_ != layerRevision) { + this.renderedTiles.length = 0; + const renderMode = layer.getRenderMode(); + if (!this.context && renderMode != VectorTileRenderType.VECTOR) { + this.context = createCanvasContext2D(); + } + if (this.context && renderMode == VectorTileRenderType.VECTOR) { + this.context = null; + } + } + this.renderedLayerRevision_ = layerRevision; + return CanvasTileLayerRenderer.prototype.prepareFrame.apply(this, arguments); + } + + /** + * @param {module:ol/VectorImageTile} tile Tile. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. * @private */ - this.declutterTree_ = layer.getDeclutter() ? rbush(9, undefined) : null; + createReplayGroup_(tile, pixelRatio, projection) { + const layer = this.getLayer(); + const revision = layer.getRevision(); + const renderOrder = /** @type {module:ol/render~OrderFunction} */ (layer.getRenderOrder()) || null; + + const replayState = tile.getReplayState(layer); + if (!replayState.dirty && replayState.renderedRevision == revision && + replayState.renderedRenderOrder == renderOrder) { + return; + } + + const source = /** @type {module:ol/source/VectorTile} */ (layer.getSource()); + const sourceTileGrid = source.getTileGrid(); + const tileGrid = source.getTileGridForProjection(projection); + const resolution = tileGrid.getResolution(tile.tileCoord[0]); + const tileExtent = tile.extent; + + const zIndexKeys = {}; + for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) { + const sourceTile = tile.getTile(tile.tileKeys[t]); + if (sourceTile.getState() != TileState.LOADED) { + continue; + } + + const sourceTileCoord = sourceTile.tileCoord; + const sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord); + const sharedExtent = getIntersection(tileExtent, sourceTileExtent); + const bufferedExtent = equals(sourceTileExtent, sharedExtent) ? null : + buffer(sharedExtent, layer.getRenderBuffer() * resolution, this.tmpExtent); + const tileProjection = sourceTile.getProjection(); + let reproject = false; + if (!equivalentProjection(projection, tileProjection)) { + reproject = true; + sourceTile.setProjection(projection); + } + replayState.dirty = false; + const replayGroup = new CanvasReplayGroup(0, sharedExtent, resolution, + pixelRatio, source.getOverlaps(), this.declutterTree_, layer.getRenderBuffer()); + const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio); + + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @this {module:ol/renderer/canvas/VectorTileLayer} + */ + const render = function(feature) { + let styles; + const styleFunction = feature.getStyleFunction() || layer.getStyleFunction(); + if (styleFunction) { + styles = styleFunction(feature, resolution); + } + if (styles) { + const dirty = this.renderFeature(feature, squaredTolerance, styles, replayGroup); + this.dirty_ = this.dirty_ || dirty; + replayState.dirty = replayState.dirty || dirty; + } + }; + + const features = sourceTile.getFeatures(); + if (renderOrder && renderOrder !== replayState.renderedRenderOrder) { + features.sort(renderOrder); + } + for (let i = 0, ii = features.length; i < ii; ++i) { + const feature = features[i]; + if (reproject) { + if (tileProjection.getUnits() == Units.TILE_PIXELS) { + // projected tile extent + tileProjection.setWorldExtent(sourceTileExtent); + // tile extent in tile pixel space + tileProjection.setExtent(sourceTile.getExtent()); + } + feature.getGeometry().transform(tileProjection, projection); + } + if (!bufferedExtent || intersects(bufferedExtent, feature.getGeometry().getExtent())) { + render.call(this, feature); + } + } + replayGroup.finish(); + for (const r in replayGroup.getReplays()) { + zIndexKeys[r] = true; + } + sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup); + } + replayState.renderedRevision = revision; + replayState.renderedRenderOrder = renderOrder; + } /** - * @private - * @type {boolean} + * @inheritDoc */ - this.dirty_ = false; + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + const resolution = frameState.viewState.resolution; + const rotation = frameState.viewState.rotation; + hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; + const layer = this.getLayer(); + /** @type {!Object.} */ + const features = {}; + + /** @type {Array.} */ + const renderedTiles = this.renderedTiles; + + let bufferedExtent, found; + let i, ii, replayGroup; + for (i = 0, ii = renderedTiles.length; i < ii; ++i) { + const tile = renderedTiles[i]; + bufferedExtent = buffer(tile.extent, hitTolerance * resolution, bufferedExtent); + if (!containsCoordinate(bufferedExtent, coordinate)) { + continue; + } + for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) { + const sourceTile = tile.getTile(tile.tileKeys[t]); + if (sourceTile.getState() != TileState.LOADED) { + continue; + } + replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString()); + found = found || replayGroup.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + const key = getUid(feature).toString(); + if (!(key in features)) { + features[key] = true; + return callback.call(thisArg, feature, layer); + } + }, null); + } + } + return found; + } /** + * @param {module:ol/VectorTile} tile Tile. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @return {module:ol/transform~Transform} transform Transform. * @private - * @type {number} */ - this.renderedLayerRevision_; + getReplayTransform_(tile, frameState) { + const layer = this.getLayer(); + const source = /** @type {module:ol/source/VectorTile} */ (layer.getSource()); + const tileGrid = source.getTileGrid(); + const tileCoord = tile.tileCoord; + const tileResolution = tileGrid.getResolution(tileCoord[0]); + const viewState = frameState.viewState; + const pixelRatio = frameState.pixelRatio; + const renderResolution = viewState.resolution / pixelRatio; + const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); + const center = viewState.center; + const origin = getTopLeft(tileExtent); + const size = frameState.size; + const offsetX = Math.round(pixelRatio * size[0] / 2); + const offsetY = Math.round(pixelRatio * size[1] / 2); + return composeTransform(this.tmpTransform_, + offsetX, offsetY, + tileResolution / renderResolution, tileResolution / renderResolution, + viewState.rotation, + (origin[0] - center[0]) / tileResolution, + (center[1] - origin[1]) / tileResolution); + } /** - * @private - * @type {module:ol/transform~Transform} + * @param {module:ol/events/Event} event Event. */ - this.tmpTransform_ = createTransform(); + handleFontsChanged_(event) { + const layer = this.getLayer(); + if (layer.getVisible() && this.renderedLayerRevision_ !== undefined) { + layer.changed(); + } + } - // Use lower resolution for pure vector rendering. Closest resolution otherwise. - this.zDirection = layer.getRenderMode() == VectorTileRenderType.VECTOR ? 1 : 0; + /** + * Handle changes in image style state. + * @param {module:ol/events/Event} event Image style change event. + * @private + */ + handleStyleImageChange_(event) { + this.renderIfReadyAndVisible(); + } - listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); + /** + * @inheritDoc + */ + postCompose(context, frameState, layerState) { + const layer = this.getLayer(); + const renderMode = layer.getRenderMode(); + if (renderMode != VectorTileRenderType.IMAGE) { + const declutterReplays = layer.getDeclutter() ? {} : null; + const source = /** @type {module:ol/source/VectorTile} */ (layer.getSource()); + const replayTypes = VECTOR_REPLAYS[renderMode]; + const pixelRatio = frameState.pixelRatio; + const rotation = frameState.viewState.rotation; + const size = frameState.size; + let offsetX, offsetY; + if (rotation) { + offsetX = Math.round(pixelRatio * size[0] / 2); + offsetY = Math.round(pixelRatio * size[1] / 2); + rotateAtOffset(context, -rotation, offsetX, offsetY); + } + if (declutterReplays) { + this.declutterTree_.clear(); + } + const tiles = this.renderedTiles; + const tileGrid = source.getTileGridForProjection(frameState.viewState.projection); + const clips = []; + const zs = []; + for (let i = tiles.length - 1; i >= 0; --i) { + const tile = /** @type {module:ol/VectorImageTile} */ (tiles[i]); + if (tile.getState() == TileState.ABORT) { + continue; + } + const tileCoord = tile.tileCoord; + const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tile.extent[0]; + let transform = undefined; + for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) { + const sourceTile = tile.getTile(tile.tileKeys[t]); + if (sourceTile.getState() != TileState.LOADED) { + continue; + } + const replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString()); + if (!replayGroup || !replayGroup.hasReplays(replayTypes)) { + // sourceTile was not yet loaded when this.createReplayGroup_() was + // called, or it has no replays of the types we want to render + continue; + } + if (!transform) { + transform = this.getTransform(frameState, worldOffset); + } + const currentZ = sourceTile.tileCoord[0]; + const currentClip = replayGroup.getClipCoords(transform); + context.save(); + context.globalAlpha = layerState.opacity; + // Create a clip mask for regions in this low resolution tile that are + // already filled by a higher resolution tile + for (let j = 0, jj = clips.length; j < jj; ++j) { + const clip = clips[j]; + if (currentZ < zs[j]) { + context.beginPath(); + // counter-clockwise (outer ring) for current tile + context.moveTo(currentClip[0], currentClip[1]); + context.lineTo(currentClip[2], currentClip[3]); + context.lineTo(currentClip[4], currentClip[5]); + context.lineTo(currentClip[6], currentClip[7]); + // clockwise (inner ring) for higher resolution tile + context.moveTo(clip[6], clip[7]); + context.lineTo(clip[4], clip[5]); + context.lineTo(clip[2], clip[3]); + context.lineTo(clip[0], clip[1]); + context.clip(); + } + } + replayGroup.replay(context, transform, rotation, {}, replayTypes, declutterReplays); + context.restore(); + clips.push(currentClip); + zs.push(currentZ); + } + } + if (declutterReplays) { + replayDeclutter(declutterReplays, context, rotation); + } + if (rotation) { + rotateAtOffset(context, rotation, + /** @type {number} */ (offsetX), /** @type {number} */ (offsetY)); + } + } + CanvasTileLayerRenderer.prototype.postCompose.apply(this, arguments); + } -}; + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @param {number} squaredTolerance Squared tolerance. + * @param {(module:ol/style/Style|Array.)} styles The style or array of styles. + * @param {module:ol/render/canvas/ReplayGroup} replayGroup Replay group. + * @return {boolean} `true` if an image is loading. + */ + renderFeature(feature, squaredTolerance, styles, replayGroup) { + if (!styles) { + return false; + } + let loading = false; + if (Array.isArray(styles)) { + for (let i = 0, ii = styles.length; i < ii; ++i) { + loading = renderFeature( + replayGroup, feature, styles[i], squaredTolerance, + this.handleStyleImageChange_, this) || loading; + } + } else { + loading = renderFeature( + replayGroup, feature, styles, squaredTolerance, + this.handleStyleImageChange_, this); + } + return loading; + } + + /** + * @param {module:ol/VectorImageTile} tile Tile. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @private + */ + renderTileImage_(tile, pixelRatio, projection) { + const layer = this.getLayer(); + const replayState = tile.getReplayState(layer); + const revision = layer.getRevision(); + const replays = IMAGE_REPLAYS[layer.getRenderMode()]; + if (replays && replayState.renderedTileRevision !== revision) { + replayState.renderedTileRevision = revision; + const tileCoord = tile.wrappedTileCoord; + const z = tileCoord[0]; + const source = /** @type {module:ol/source/VectorTile} */ (layer.getSource()); + const tileGrid = source.getTileGridForProjection(projection); + const resolution = tileGrid.getResolution(z); + const context = tile.getContext(layer); + const size = source.getTilePixelSize(z, pixelRatio, projection); + context.canvas.width = size[0]; + context.canvas.height = size[1]; + const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); + for (let i = 0, ii = tile.tileKeys.length; i < ii; ++i) { + const sourceTile = tile.getTile(tile.tileKeys[i]); + if (sourceTile.getState() != TileState.LOADED) { + continue; + } + const pixelScale = pixelRatio / resolution; + const transform = resetTransform(this.tmpTransform_); + scaleTransform(transform, pixelScale, -pixelScale); + translateTransform(transform, -tileExtent[0], -tileExtent[3]); + const replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString()); + replayGroup.replay(context, transform, 0, {}, replays); + } + } + } +} inherits(CanvasVectorTileLayerRenderer, CanvasTileLayerRenderer); @@ -117,394 +498,4 @@ CanvasVectorTileLayerRenderer['create'] = function(mapRenderer, layer) { }; -/** - * @inheritDoc - */ -CanvasVectorTileLayerRenderer.prototype.disposeInternal = function() { - unlisten(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); - CanvasTileLayerRenderer.prototype.disposeInternal.call(this); -}; - - -/** - * @inheritDoc - */ -CanvasVectorTileLayerRenderer.prototype.getTile = function(z, x, y, pixelRatio, projection) { - const tile = CanvasTileLayerRenderer.prototype.getTile.call(this, z, x, y, pixelRatio, projection); - if (tile.getState() === TileState.LOADED) { - this.createReplayGroup_(tile, pixelRatio, projection); - if (this.context) { - this.renderTileImage_(tile, pixelRatio, projection); - } - } - return tile; -}; - - -/** - * @inheritDoc - */ -CanvasVectorTileLayerRenderer.prototype.prepareFrame = function(frameState, layerState) { - const layer = this.getLayer(); - const layerRevision = layer.getRevision(); - if (this.renderedLayerRevision_ != layerRevision) { - this.renderedTiles.length = 0; - const renderMode = layer.getRenderMode(); - if (!this.context && renderMode != VectorTileRenderType.VECTOR) { - this.context = createCanvasContext2D(); - } - if (this.context && renderMode == VectorTileRenderType.VECTOR) { - this.context = null; - } - } - this.renderedLayerRevision_ = layerRevision; - return CanvasTileLayerRenderer.prototype.prepareFrame.apply(this, arguments); -}; - - -/** - * @param {module:ol/VectorImageTile} tile Tile. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @private - */ -CanvasVectorTileLayerRenderer.prototype.createReplayGroup_ = function(tile, pixelRatio, projection) { - const layer = this.getLayer(); - const revision = layer.getRevision(); - const renderOrder = /** @type {module:ol/render~OrderFunction} */ (layer.getRenderOrder()) || null; - - const replayState = tile.getReplayState(layer); - if (!replayState.dirty && replayState.renderedRevision == revision && - replayState.renderedRenderOrder == renderOrder) { - return; - } - - const source = /** @type {module:ol/source/VectorTile} */ (layer.getSource()); - const sourceTileGrid = source.getTileGrid(); - const tileGrid = source.getTileGridForProjection(projection); - const resolution = tileGrid.getResolution(tile.tileCoord[0]); - const tileExtent = tile.extent; - - const zIndexKeys = {}; - for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) { - const sourceTile = tile.getTile(tile.tileKeys[t]); - if (sourceTile.getState() != TileState.LOADED) { - continue; - } - - const sourceTileCoord = sourceTile.tileCoord; - const sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord); - const sharedExtent = getIntersection(tileExtent, sourceTileExtent); - const bufferedExtent = equals(sourceTileExtent, sharedExtent) ? null : - buffer(sharedExtent, layer.getRenderBuffer() * resolution, this.tmpExtent); - const tileProjection = sourceTile.getProjection(); - let reproject = false; - if (!equivalentProjection(projection, tileProjection)) { - reproject = true; - sourceTile.setProjection(projection); - } - replayState.dirty = false; - const replayGroup = new CanvasReplayGroup(0, sharedExtent, resolution, - pixelRatio, source.getOverlaps(), this.declutterTree_, layer.getRenderBuffer()); - const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio); - - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @this {module:ol/renderer/canvas/VectorTileLayer} - */ - const render = function(feature) { - let styles; - const styleFunction = feature.getStyleFunction() || layer.getStyleFunction(); - if (styleFunction) { - styles = styleFunction(feature, resolution); - } - if (styles) { - const dirty = this.renderFeature(feature, squaredTolerance, styles, replayGroup); - this.dirty_ = this.dirty_ || dirty; - replayState.dirty = replayState.dirty || dirty; - } - }; - - const features = sourceTile.getFeatures(); - if (renderOrder && renderOrder !== replayState.renderedRenderOrder) { - features.sort(renderOrder); - } - for (let i = 0, ii = features.length; i < ii; ++i) { - const feature = features[i]; - if (reproject) { - if (tileProjection.getUnits() == Units.TILE_PIXELS) { - // projected tile extent - tileProjection.setWorldExtent(sourceTileExtent); - // tile extent in tile pixel space - tileProjection.setExtent(sourceTile.getExtent()); - } - feature.getGeometry().transform(tileProjection, projection); - } - if (!bufferedExtent || intersects(bufferedExtent, feature.getGeometry().getExtent())) { - render.call(this, feature); - } - } - replayGroup.finish(); - for (const r in replayGroup.getReplays()) { - zIndexKeys[r] = true; - } - sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup); - } - replayState.renderedRevision = revision; - replayState.renderedRenderOrder = renderOrder; -}; - - -/** - * @inheritDoc - */ -CanvasVectorTileLayerRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { - const resolution = frameState.viewState.resolution; - const rotation = frameState.viewState.rotation; - hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; - const layer = this.getLayer(); - /** @type {!Object.} */ - const features = {}; - - /** @type {Array.} */ - const renderedTiles = this.renderedTiles; - - let bufferedExtent, found; - let i, ii, replayGroup; - for (i = 0, ii = renderedTiles.length; i < ii; ++i) { - const tile = renderedTiles[i]; - bufferedExtent = buffer(tile.extent, hitTolerance * resolution, bufferedExtent); - if (!containsCoordinate(bufferedExtent, coordinate)) { - continue; - } - for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) { - const sourceTile = tile.getTile(tile.tileKeys[t]); - if (sourceTile.getState() != TileState.LOADED) { - continue; - } - replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString()); - found = found || replayGroup.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - const key = getUid(feature).toString(); - if (!(key in features)) { - features[key] = true; - return callback.call(thisArg, feature, layer); - } - }, null); - } - } - return found; -}; - - -/** - * @param {module:ol/VectorTile} tile Tile. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @return {module:ol/transform~Transform} transform Transform. - * @private - */ -CanvasVectorTileLayerRenderer.prototype.getReplayTransform_ = function(tile, frameState) { - const layer = this.getLayer(); - const source = /** @type {module:ol/source/VectorTile} */ (layer.getSource()); - const tileGrid = source.getTileGrid(); - const tileCoord = tile.tileCoord; - const tileResolution = tileGrid.getResolution(tileCoord[0]); - const viewState = frameState.viewState; - const pixelRatio = frameState.pixelRatio; - const renderResolution = viewState.resolution / pixelRatio; - const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); - const center = viewState.center; - const origin = getTopLeft(tileExtent); - const size = frameState.size; - const offsetX = Math.round(pixelRatio * size[0] / 2); - const offsetY = Math.round(pixelRatio * size[1] / 2); - return composeTransform(this.tmpTransform_, - offsetX, offsetY, - tileResolution / renderResolution, tileResolution / renderResolution, - viewState.rotation, - (origin[0] - center[0]) / tileResolution, - (center[1] - origin[1]) / tileResolution); -}; - - -/** - * @param {module:ol/events/Event} event Event. - */ -CanvasVectorTileLayerRenderer.prototype.handleFontsChanged_ = function(event) { - const layer = this.getLayer(); - if (layer.getVisible() && this.renderedLayerRevision_ !== undefined) { - layer.changed(); - } -}; - - -/** - * Handle changes in image style state. - * @param {module:ol/events/Event} event Image style change event. - * @private - */ -CanvasVectorTileLayerRenderer.prototype.handleStyleImageChange_ = function(event) { - this.renderIfReadyAndVisible(); -}; - - -/** - * @inheritDoc - */ -CanvasVectorTileLayerRenderer.prototype.postCompose = function(context, frameState, layerState) { - const layer = this.getLayer(); - const renderMode = layer.getRenderMode(); - if (renderMode != VectorTileRenderType.IMAGE) { - const declutterReplays = layer.getDeclutter() ? {} : null; - const source = /** @type {module:ol/source/VectorTile} */ (layer.getSource()); - const replayTypes = VECTOR_REPLAYS[renderMode]; - const pixelRatio = frameState.pixelRatio; - const rotation = frameState.viewState.rotation; - const size = frameState.size; - let offsetX, offsetY; - if (rotation) { - offsetX = Math.round(pixelRatio * size[0] / 2); - offsetY = Math.round(pixelRatio * size[1] / 2); - rotateAtOffset(context, -rotation, offsetX, offsetY); - } - if (declutterReplays) { - this.declutterTree_.clear(); - } - const tiles = this.renderedTiles; - const tileGrid = source.getTileGridForProjection(frameState.viewState.projection); - const clips = []; - const zs = []; - for (let i = tiles.length - 1; i >= 0; --i) { - const tile = /** @type {module:ol/VectorImageTile} */ (tiles[i]); - if (tile.getState() == TileState.ABORT) { - continue; - } - const tileCoord = tile.tileCoord; - const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tile.extent[0]; - let transform = undefined; - for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) { - const sourceTile = tile.getTile(tile.tileKeys[t]); - if (sourceTile.getState() != TileState.LOADED) { - continue; - } - const replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString()); - if (!replayGroup || !replayGroup.hasReplays(replayTypes)) { - // sourceTile was not yet loaded when this.createReplayGroup_() was - // called, or it has no replays of the types we want to render - continue; - } - if (!transform) { - transform = this.getTransform(frameState, worldOffset); - } - const currentZ = sourceTile.tileCoord[0]; - const currentClip = replayGroup.getClipCoords(transform); - context.save(); - context.globalAlpha = layerState.opacity; - // Create a clip mask for regions in this low resolution tile that are - // already filled by a higher resolution tile - for (let j = 0, jj = clips.length; j < jj; ++j) { - const clip = clips[j]; - if (currentZ < zs[j]) { - context.beginPath(); - // counter-clockwise (outer ring) for current tile - context.moveTo(currentClip[0], currentClip[1]); - context.lineTo(currentClip[2], currentClip[3]); - context.lineTo(currentClip[4], currentClip[5]); - context.lineTo(currentClip[6], currentClip[7]); - // clockwise (inner ring) for higher resolution tile - context.moveTo(clip[6], clip[7]); - context.lineTo(clip[4], clip[5]); - context.lineTo(clip[2], clip[3]); - context.lineTo(clip[0], clip[1]); - context.clip(); - } - } - replayGroup.replay(context, transform, rotation, {}, replayTypes, declutterReplays); - context.restore(); - clips.push(currentClip); - zs.push(currentZ); - } - } - if (declutterReplays) { - replayDeclutter(declutterReplays, context, rotation); - } - if (rotation) { - rotateAtOffset(context, rotation, - /** @type {number} */ (offsetX), /** @type {number} */ (offsetY)); - } - } - CanvasTileLayerRenderer.prototype.postCompose.apply(this, arguments); -}; - - -/** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @param {number} squaredTolerance Squared tolerance. - * @param {(module:ol/style/Style|Array.)} styles The style or array of styles. - * @param {module:ol/render/canvas/ReplayGroup} replayGroup Replay group. - * @return {boolean} `true` if an image is loading. - */ -CanvasVectorTileLayerRenderer.prototype.renderFeature = function(feature, squaredTolerance, styles, replayGroup) { - if (!styles) { - return false; - } - let loading = false; - if (Array.isArray(styles)) { - for (let i = 0, ii = styles.length; i < ii; ++i) { - loading = renderFeature( - replayGroup, feature, styles[i], squaredTolerance, - this.handleStyleImageChange_, this) || loading; - } - } else { - loading = renderFeature( - replayGroup, feature, styles, squaredTolerance, - this.handleStyleImageChange_, this); - } - return loading; -}; - - -/** - * @param {module:ol/VectorImageTile} tile Tile. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @private - */ -CanvasVectorTileLayerRenderer.prototype.renderTileImage_ = function( - tile, pixelRatio, projection) { - const layer = this.getLayer(); - const replayState = tile.getReplayState(layer); - const revision = layer.getRevision(); - const replays = IMAGE_REPLAYS[layer.getRenderMode()]; - if (replays && replayState.renderedTileRevision !== revision) { - replayState.renderedTileRevision = revision; - const tileCoord = tile.wrappedTileCoord; - const z = tileCoord[0]; - const source = /** @type {module:ol/source/VectorTile} */ (layer.getSource()); - const tileGrid = source.getTileGridForProjection(projection); - const resolution = tileGrid.getResolution(z); - const context = tile.getContext(layer); - const size = source.getTilePixelSize(z, pixelRatio, projection); - context.canvas.width = size[0]; - context.canvas.height = size[1]; - const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); - for (let i = 0, ii = tile.tileKeys.length; i < ii; ++i) { - const sourceTile = tile.getTile(tile.tileKeys[i]); - if (sourceTile.getState() != TileState.LOADED) { - continue; - } - const pixelScale = pixelRatio / resolution; - const transform = resetTransform(this.tmpTransform_); - scaleTransform(transform, pixelScale, -pixelScale); - translateTransform(transform, -tileExtent[0], -tileExtent[3]); - const replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString()); - replayGroup.replay(context, transform, 0, {}, replays); - } - } -}; - export default CanvasVectorTileLayerRenderer; diff --git a/src/ol/renderer/webgl/ImageLayer.js b/src/ol/renderer/webgl/ImageLayer.js index 68bd42b483..644d9c0c76 100644 --- a/src/ol/renderer/webgl/ImageLayer.js +++ b/src/ol/renderer/webgl/ImageLayer.js @@ -29,30 +29,286 @@ import {createTexture} from '../../webgl/Context.js'; * @param {module:ol/layer/Image} imageLayer Tile layer. * @api */ -const WebGLImageLayerRenderer = function(mapRenderer, imageLayer) { +class WebGLImageLayerRenderer { + constructor(mapRenderer, imageLayer) { - WebGLLayerRenderer.call(this, mapRenderer, imageLayer); + WebGLLayerRenderer.call(this, mapRenderer, imageLayer); + + /** + * The last rendered image. + * @private + * @type {?module:ol/ImageBase} + */ + this.image_ = null; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.hitCanvasContext_ = null; + + /** + * @private + * @type {?module:ol/transform~Transform} + */ + this.hitTransformationMatrix_ = null; + + } /** - * The last rendered image. + * @param {module:ol/ImageBase} image Image. * @private - * @type {?module:ol/ImageBase} + * @return {WebGLTexture} Texture. */ - this.image_ = null; + createTexture_(image) { + + // We meet the conditions to work with non-power of two textures. + // http://www.khronos.org/webgl/wiki/WebGL_and_OpenGL_Differences#Non-Power_of_Two_Texture_Support + // http://learningwebgl.com/blog/?p=2101 + + const imageElement = image.getImage(); + const gl = this.mapRenderer.getGL(); + + return createTexture( + gl, imageElement, CLAMP_TO_EDGE, CLAMP_TO_EDGE); + } /** - * @private - * @type {CanvasRenderingContext2D} + * @inheritDoc */ - this.hitCanvasContext_ = null; + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + const layer = this.getLayer(); + const source = layer.getSource(); + const resolution = frameState.viewState.resolution; + const rotation = frameState.viewState.rotation; + const skippedFeatureUids = frameState.skippedFeatureUids; + return source.forEachFeatureAtCoordinate( + coordinate, resolution, rotation, hitTolerance, skippedFeatureUids, + + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + return callback.call(thisArg, feature, layer); + }); + } /** - * @private - * @type {?module:ol/transform~Transform} + * @inheritDoc */ - this.hitTransformationMatrix_ = null; + prepareFrame(frameState, layerState, context) { -}; + const gl = this.mapRenderer.getGL(); + + const pixelRatio = frameState.pixelRatio; + const viewState = frameState.viewState; + const viewCenter = viewState.center; + const viewResolution = viewState.resolution; + const viewRotation = viewState.rotation; + + let image = this.image_; + let texture = this.texture; + const imageLayer = /** @type {module:ol/layer/Image} */ (this.getLayer()); + const imageSource = imageLayer.getSource(); + + const hints = frameState.viewHints; + + let renderedExtent = frameState.extent; + if (layerState.extent !== undefined) { + renderedExtent = getIntersection(renderedExtent, layerState.extent); + } + if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] && + !isEmpty(renderedExtent)) { + let projection = viewState.projection; + if (!ENABLE_RASTER_REPROJECTION) { + const sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + projection = sourceProjection; + } + } + const image_ = imageSource.getImage(renderedExtent, viewResolution, + pixelRatio, projection); + if (image_) { + const loaded = this.loadImage(image_); + if (loaded) { + image = image_; + texture = this.createTexture_(image_); + if (this.texture) { + /** + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLTexture} texture Texture. + */ + const postRenderFunction = function(gl, texture) { + if (!gl.isContextLost()) { + gl.deleteTexture(texture); + } + }.bind(null, gl, this.texture); + frameState.postRenderFunctions.push( + /** @type {module:ol/PluggableMap~PostRenderFunction} */ (postRenderFunction) + ); + } + } + } + } + + if (image) { + const canvas = this.mapRenderer.getContext().getCanvas(); + + this.updateProjectionMatrix_(canvas.width, canvas.height, + pixelRatio, viewCenter, viewResolution, viewRotation, + image.getExtent()); + this.hitTransformationMatrix_ = null; + + // Translate and scale to flip the Y coord. + const texCoordMatrix = this.texCoordMatrix; + resetTransform(texCoordMatrix); + scaleTransform(texCoordMatrix, 1, -1); + translateTransform(texCoordMatrix, 0, -1); + + this.image_ = image; + this.texture = texture; + } + + return !!image; + } + + /** + * @param {number} canvasWidth Canvas width. + * @param {number} canvasHeight Canvas height. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/coordinate~Coordinate} viewCenter View center. + * @param {number} viewResolution View resolution. + * @param {number} viewRotation View rotation. + * @param {module:ol/extent~Extent} imageExtent Image extent. + * @private + */ + updateProjectionMatrix_( + canvasWidth, + canvasHeight, + pixelRatio, + viewCenter, + viewResolution, + viewRotation, + imageExtent + ) { + + const canvasExtentWidth = canvasWidth * viewResolution; + const canvasExtentHeight = canvasHeight * viewResolution; + + const projectionMatrix = this.projectionMatrix; + resetTransform(projectionMatrix); + scaleTransform(projectionMatrix, + pixelRatio * 2 / canvasExtentWidth, + pixelRatio * 2 / canvasExtentHeight); + rotateTransform(projectionMatrix, -viewRotation); + translateTransform(projectionMatrix, + imageExtent[0] - viewCenter[0], + imageExtent[1] - viewCenter[1]); + scaleTransform(projectionMatrix, + (imageExtent[2] - imageExtent[0]) / 2, + (imageExtent[3] - imageExtent[1]) / 2); + translateTransform(projectionMatrix, 1, 1); + + } + + /** + * @inheritDoc + */ + hasFeatureAtCoordinate(coordinate, frameState) { + const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, 0, TRUE, this); + return hasFeature !== undefined; + } + + /** + * @inheritDoc + */ + forEachLayerAtPixel(pixel, frameState, callback, thisArg) { + if (!this.image_ || !this.image_.getImage()) { + return undefined; + } + + if (this.getLayer().getSource().forEachFeatureAtCoordinate !== UNDEFINED) { + // for ImageCanvas sources use the original hit-detection logic, + // so that for example also transparent polygons are detected + const coordinate = applyTransform( + frameState.pixelToCoordinateTransform, pixel.slice()); + const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, 0, TRUE, this); + + if (hasFeature) { + return callback.call(thisArg, this.getLayer(), null); + } else { + return undefined; + } + } else { + const imageSize = + [this.image_.getImage().width, this.image_.getImage().height]; + + if (!this.hitTransformationMatrix_) { + this.hitTransformationMatrix_ = this.getHitTransformationMatrix_( + frameState.size, imageSize); + } + + const pixelOnFrameBuffer = applyTransform( + this.hitTransformationMatrix_, pixel.slice()); + + if (pixelOnFrameBuffer[0] < 0 || pixelOnFrameBuffer[0] > imageSize[0] || + pixelOnFrameBuffer[1] < 0 || pixelOnFrameBuffer[1] > imageSize[1]) { + // outside the image, no need to check + return undefined; + } + + if (!this.hitCanvasContext_) { + this.hitCanvasContext_ = createCanvasContext2D(1, 1); + } + + this.hitCanvasContext_.clearRect(0, 0, 1, 1); + this.hitCanvasContext_.drawImage(this.image_.getImage(), + pixelOnFrameBuffer[0], pixelOnFrameBuffer[1], 1, 1, 0, 0, 1, 1); + + const imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data; + if (imageData[3] > 0) { + return callback.call(thisArg, this.getLayer(), imageData); + } else { + return undefined; + } + } + } + + /** + * The transformation matrix to get the pixel on the image for a + * pixel on the map. + * @param {module:ol/size~Size} mapSize The map size. + * @param {module:ol/size~Size} imageSize The image size. + * @return {module:ol/transform~Transform} The transformation matrix. + * @private + */ + getHitTransformationMatrix_(mapSize, imageSize) { + // the first matrix takes a map pixel, flips the y-axis and scales to + // a range between -1 ... 1 + const mapCoordTransform = createTransform(); + translateTransform(mapCoordTransform, -1, -1); + scaleTransform(mapCoordTransform, 2 / mapSize[0], 2 / mapSize[1]); + translateTransform(mapCoordTransform, 0, mapSize[1]); + scaleTransform(mapCoordTransform, 1, -1); + + // the second matrix is the inverse of the projection matrix used in the + // shader for drawing + const projectionMatrixInv = invertTransform(this.projectionMatrix.slice()); + + // the third matrix scales to the image dimensions and flips the y-axis again + const transform = createTransform(); + translateTransform(transform, 0, imageSize[1]); + scaleTransform(transform, 1, -1); + scaleTransform(transform, imageSize[0] / 2, imageSize[1] / 2); + translateTransform(transform, 1, 1); + + multiplyTransform(transform, projectionMatrixInv); + multiplyTransform(transform, mapCoordTransform); + + return transform; + } +} inherits(WebGLImageLayerRenderer, WebGLLayerRenderer); @@ -81,256 +337,4 @@ WebGLImageLayerRenderer['create'] = function(mapRenderer, layer) { }; -/** - * @param {module:ol/ImageBase} image Image. - * @private - * @return {WebGLTexture} Texture. - */ -WebGLImageLayerRenderer.prototype.createTexture_ = function(image) { - - // We meet the conditions to work with non-power of two textures. - // http://www.khronos.org/webgl/wiki/WebGL_and_OpenGL_Differences#Non-Power_of_Two_Texture_Support - // http://learningwebgl.com/blog/?p=2101 - - const imageElement = image.getImage(); - const gl = this.mapRenderer.getGL(); - - return createTexture( - gl, imageElement, CLAMP_TO_EDGE, CLAMP_TO_EDGE); -}; - - -/** - * @inheritDoc - */ -WebGLImageLayerRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { - const layer = this.getLayer(); - const source = layer.getSource(); - const resolution = frameState.viewState.resolution; - const rotation = frameState.viewState.rotation; - const skippedFeatureUids = frameState.skippedFeatureUids; - return source.forEachFeatureAtCoordinate( - coordinate, resolution, rotation, hitTolerance, skippedFeatureUids, - - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - return callback.call(thisArg, feature, layer); - }); -}; - - -/** - * @inheritDoc - */ -WebGLImageLayerRenderer.prototype.prepareFrame = function(frameState, layerState, context) { - - const gl = this.mapRenderer.getGL(); - - const pixelRatio = frameState.pixelRatio; - const viewState = frameState.viewState; - const viewCenter = viewState.center; - const viewResolution = viewState.resolution; - const viewRotation = viewState.rotation; - - let image = this.image_; - let texture = this.texture; - const imageLayer = /** @type {module:ol/layer/Image} */ (this.getLayer()); - const imageSource = imageLayer.getSource(); - - const hints = frameState.viewHints; - - let renderedExtent = frameState.extent; - if (layerState.extent !== undefined) { - renderedExtent = getIntersection(renderedExtent, layerState.extent); - } - if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] && - !isEmpty(renderedExtent)) { - let projection = viewState.projection; - if (!ENABLE_RASTER_REPROJECTION) { - const sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - projection = sourceProjection; - } - } - const image_ = imageSource.getImage(renderedExtent, viewResolution, - pixelRatio, projection); - if (image_) { - const loaded = this.loadImage(image_); - if (loaded) { - image = image_; - texture = this.createTexture_(image_); - if (this.texture) { - /** - * @param {WebGLRenderingContext} gl GL. - * @param {WebGLTexture} texture Texture. - */ - const postRenderFunction = function(gl, texture) { - if (!gl.isContextLost()) { - gl.deleteTexture(texture); - } - }.bind(null, gl, this.texture); - frameState.postRenderFunctions.push( - /** @type {module:ol/PluggableMap~PostRenderFunction} */ (postRenderFunction) - ); - } - } - } - } - - if (image) { - const canvas = this.mapRenderer.getContext().getCanvas(); - - this.updateProjectionMatrix_(canvas.width, canvas.height, - pixelRatio, viewCenter, viewResolution, viewRotation, - image.getExtent()); - this.hitTransformationMatrix_ = null; - - // Translate and scale to flip the Y coord. - const texCoordMatrix = this.texCoordMatrix; - resetTransform(texCoordMatrix); - scaleTransform(texCoordMatrix, 1, -1); - translateTransform(texCoordMatrix, 0, -1); - - this.image_ = image; - this.texture = texture; - } - - return !!image; -}; - - -/** - * @param {number} canvasWidth Canvas width. - * @param {number} canvasHeight Canvas height. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/coordinate~Coordinate} viewCenter View center. - * @param {number} viewResolution View resolution. - * @param {number} viewRotation View rotation. - * @param {module:ol/extent~Extent} imageExtent Image extent. - * @private - */ -WebGLImageLayerRenderer.prototype.updateProjectionMatrix_ = function(canvasWidth, canvasHeight, pixelRatio, - viewCenter, viewResolution, viewRotation, imageExtent) { - - const canvasExtentWidth = canvasWidth * viewResolution; - const canvasExtentHeight = canvasHeight * viewResolution; - - const projectionMatrix = this.projectionMatrix; - resetTransform(projectionMatrix); - scaleTransform(projectionMatrix, - pixelRatio * 2 / canvasExtentWidth, - pixelRatio * 2 / canvasExtentHeight); - rotateTransform(projectionMatrix, -viewRotation); - translateTransform(projectionMatrix, - imageExtent[0] - viewCenter[0], - imageExtent[1] - viewCenter[1]); - scaleTransform(projectionMatrix, - (imageExtent[2] - imageExtent[0]) / 2, - (imageExtent[3] - imageExtent[1]) / 2); - translateTransform(projectionMatrix, 1, 1); - -}; - - -/** - * @inheritDoc - */ -WebGLImageLayerRenderer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState) { - const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, 0, TRUE, this); - return hasFeature !== undefined; -}; - - -/** - * @inheritDoc - */ -WebGLImageLayerRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { - if (!this.image_ || !this.image_.getImage()) { - return undefined; - } - - if (this.getLayer().getSource().forEachFeatureAtCoordinate !== UNDEFINED) { - // for ImageCanvas sources use the original hit-detection logic, - // so that for example also transparent polygons are detected - const coordinate = applyTransform( - frameState.pixelToCoordinateTransform, pixel.slice()); - const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, 0, TRUE, this); - - if (hasFeature) { - return callback.call(thisArg, this.getLayer(), null); - } else { - return undefined; - } - } else { - const imageSize = - [this.image_.getImage().width, this.image_.getImage().height]; - - if (!this.hitTransformationMatrix_) { - this.hitTransformationMatrix_ = this.getHitTransformationMatrix_( - frameState.size, imageSize); - } - - const pixelOnFrameBuffer = applyTransform( - this.hitTransformationMatrix_, pixel.slice()); - - if (pixelOnFrameBuffer[0] < 0 || pixelOnFrameBuffer[0] > imageSize[0] || - pixelOnFrameBuffer[1] < 0 || pixelOnFrameBuffer[1] > imageSize[1]) { - // outside the image, no need to check - return undefined; - } - - if (!this.hitCanvasContext_) { - this.hitCanvasContext_ = createCanvasContext2D(1, 1); - } - - this.hitCanvasContext_.clearRect(0, 0, 1, 1); - this.hitCanvasContext_.drawImage(this.image_.getImage(), - pixelOnFrameBuffer[0], pixelOnFrameBuffer[1], 1, 1, 0, 0, 1, 1); - - const imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data; - if (imageData[3] > 0) { - return callback.call(thisArg, this.getLayer(), imageData); - } else { - return undefined; - } - } -}; - - -/** - * The transformation matrix to get the pixel on the image for a - * pixel on the map. - * @param {module:ol/size~Size} mapSize The map size. - * @param {module:ol/size~Size} imageSize The image size. - * @return {module:ol/transform~Transform} The transformation matrix. - * @private - */ -WebGLImageLayerRenderer.prototype.getHitTransformationMatrix_ = function(mapSize, imageSize) { - // the first matrix takes a map pixel, flips the y-axis and scales to - // a range between -1 ... 1 - const mapCoordTransform = createTransform(); - translateTransform(mapCoordTransform, -1, -1); - scaleTransform(mapCoordTransform, 2 / mapSize[0], 2 / mapSize[1]); - translateTransform(mapCoordTransform, 0, mapSize[1]); - scaleTransform(mapCoordTransform, 1, -1); - - // the second matrix is the inverse of the projection matrix used in the - // shader for drawing - const projectionMatrixInv = invertTransform(this.projectionMatrix.slice()); - - // the third matrix scales to the image dimensions and flips the y-axis again - const transform = createTransform(); - translateTransform(transform, 0, imageSize[1]); - scaleTransform(transform, 1, -1); - scaleTransform(transform, imageSize[0] / 2, imageSize[1] / 2); - translateTransform(transform, 1, 1); - - multiplyTransform(transform, projectionMatrixInv); - multiplyTransform(transform, mapCoordTransform); - - return transform; -}; export default WebGLImageLayerRenderer; diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js index 6f15a1779e..0fffb2fe21 100644 --- a/src/ol/renderer/webgl/Layer.js +++ b/src/ol/renderer/webgl/Layer.js @@ -22,244 +22,239 @@ import {createEmptyTexture} from '../../webgl/Context.js'; * @param {module:ol/renderer/webgl/Map} mapRenderer Map renderer. * @param {module:ol/layer/Layer} layer Layer. */ -const WebGLLayerRenderer = function(mapRenderer, layer) { +class WebGLLayerRenderer { + constructor(mapRenderer, layer) { - LayerRenderer.call(this, layer); + LayerRenderer.call(this, layer); + + /** + * @protected + * @type {module:ol/renderer/webgl/Map} + */ + this.mapRenderer = mapRenderer; + + /** + * @private + * @type {module:ol/webgl/Buffer} + */ + this.arrayBuffer_ = new WebGLBuffer([ + -1, -1, 0, 0, + 1, -1, 1, 0, + -1, 1, 0, 1, + 1, 1, 1, 1 + ]); + + /** + * @protected + * @type {WebGLTexture} + */ + this.texture = null; + + /** + * @protected + * @type {WebGLFramebuffer} + */ + this.framebuffer = null; + + /** + * @protected + * @type {number|undefined} + */ + this.framebufferDimension = undefined; + + /** + * @protected + * @type {module:ol/transform~Transform} + */ + this.texCoordMatrix = createTransform(); + + /** + * @protected + * @type {module:ol/transform~Transform} + */ + this.projectionMatrix = createTransform(); + + /** + * @type {Array.} + * @private + */ + this.tmpMat4_ = create(); + + /** + * @private + * @type {module:ol/renderer/webgl/defaultmapshader/Locations} + */ + this.defaultLocations_ = null; + + } /** + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {number} framebufferDimension Framebuffer dimension. * @protected - * @type {module:ol/renderer/webgl/Map} */ - this.mapRenderer = mapRenderer; + bindFramebuffer(frameState, framebufferDimension) { + + const gl = this.mapRenderer.getGL(); + + if (this.framebufferDimension === undefined || + this.framebufferDimension != framebufferDimension) { + /** + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLFramebuffer} framebuffer Framebuffer. + * @param {WebGLTexture} texture Texture. + */ + const postRenderFunction = function(gl, framebuffer, texture) { + if (!gl.isContextLost()) { + gl.deleteFramebuffer(framebuffer); + gl.deleteTexture(texture); + } + }.bind(null, gl, this.framebuffer, this.texture); + + frameState.postRenderFunctions.push( + /** @type {module:ol/PluggableMap~PostRenderFunction} */ (postRenderFunction) + ); + + const texture = createEmptyTexture( + gl, framebufferDimension, framebufferDimension); + + const framebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(FRAMEBUFFER, framebuffer); + gl.framebufferTexture2D(FRAMEBUFFER, + COLOR_ATTACHMENT0, TEXTURE_2D, texture, 0); + + this.texture = texture; + this.framebuffer = framebuffer; + this.framebufferDimension = framebufferDimension; + + } else { + gl.bindFramebuffer(FRAMEBUFFER, this.framebuffer); + } + + } /** - * @private - * @type {module:ol/webgl/Buffer} + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/layer/Layer~State} layerState Layer state. + * @param {module:ol/webgl/Context} context Context. */ - this.arrayBuffer_ = new WebGLBuffer([ - -1, -1, 0, 0, - 1, -1, 1, 0, - -1, 1, 0, 1, - 1, 1, 1, 1 - ]); + composeFrame(frameState, layerState, context) { + + this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, context, frameState); + + context.bindBuffer(ARRAY_BUFFER, this.arrayBuffer_); + + const gl = context.getGL(); + + const program = context.getProgram(fragment, vertex); + + let locations; + if (!this.defaultLocations_) { + locations = new Locations(gl, program); + this.defaultLocations_ = locations; + } else { + locations = this.defaultLocations_; + } + + if (context.useProgram(program)) { + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer( + locations.a_position, 2, FLOAT, false, 16, 0); + gl.enableVertexAttribArray(locations.a_texCoord); + gl.vertexAttribPointer( + locations.a_texCoord, 2, FLOAT, false, 16, 8); + gl.uniform1i(locations.u_texture, 0); + } + + gl.uniformMatrix4fv(locations.u_texCoordMatrix, false, + fromTransform(this.tmpMat4_, this.getTexCoordMatrix())); + gl.uniformMatrix4fv(locations.u_projectionMatrix, false, + fromTransform(this.tmpMat4_, this.getProjectionMatrix())); + gl.uniform1f(locations.u_opacity, layerState.opacity); + gl.bindTexture(TEXTURE_2D, this.getTexture()); + gl.drawArrays(TRIANGLE_STRIP, 0, 4); + + this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, context, frameState); + } /** - * @protected - * @type {WebGLTexture} - */ - this.texture = null; - - /** - * @protected - * @type {WebGLFramebuffer} - */ - this.framebuffer = null; - - /** - * @protected - * @type {number|undefined} - */ - this.framebufferDimension = undefined; - - /** - * @protected - * @type {module:ol/transform~Transform} - */ - this.texCoordMatrix = createTransform(); - - /** - * @protected - * @type {module:ol/transform~Transform} - */ - this.projectionMatrix = createTransform(); - - /** - * @type {Array.} + * @param {module:ol/render/EventType} type Event type. + * @param {module:ol/webgl/Context} context WebGL context. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. * @private */ - this.tmpMat4_ = create(); + dispatchComposeEvent_(type, context, frameState) { + const layer = this.getLayer(); + if (layer.hasListener(type)) { + const viewState = frameState.viewState; + const resolution = viewState.resolution; + const pixelRatio = frameState.pixelRatio; + const extent = frameState.extent; + const center = viewState.center; + const rotation = viewState.rotation; + const size = frameState.size; + + const render = new WebGLImmediateRenderer( + context, center, resolution, rotation, size, extent, pixelRatio); + const composeEvent = new RenderEvent( + type, render, frameState, null, context); + layer.dispatchEvent(composeEvent); + } + } /** - * @private - * @type {module:ol/renderer/webgl/defaultmapshader/Locations} + * @return {!module:ol/transform~Transform} Matrix. */ - this.defaultLocations_ = null; + getTexCoordMatrix() { + return this.texCoordMatrix; + } -}; + /** + * @return {WebGLTexture} Texture. + */ + getTexture() { + return this.texture; + } + + /** + * @return {!module:ol/transform~Transform} Matrix. + */ + getProjectionMatrix() { + return this.projectionMatrix; + } + + /** + * Handle webglcontextlost. + */ + handleWebGLContextLost() { + this.texture = null; + this.framebuffer = null; + this.framebufferDimension = undefined; + } + + /** + * @abstract + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @param {module:ol/layer/Layer~State} layerState Layer state. + * @param {module:ol/webgl/Context} context Context. + * @return {boolean} whether composeFrame should be called. + */ + prepareFrame(frameState, layerState, context) {} + + /** + * @abstract + * @param {module:ol~Pixel} pixel Pixel. + * @param {module:ol/PluggableMap~FrameState} frameState FrameState. + * @param {function(this: S, module:ol/layer/Layer, (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,U + */ + forEachLayerAtPixel(pixel, frameState, callback, thisArg) {} +} inherits(WebGLLayerRenderer, LayerRenderer); -/** - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {number} framebufferDimension Framebuffer dimension. - * @protected - */ -WebGLLayerRenderer.prototype.bindFramebuffer = function(frameState, framebufferDimension) { - - const gl = this.mapRenderer.getGL(); - - if (this.framebufferDimension === undefined || - this.framebufferDimension != framebufferDimension) { - /** - * @param {WebGLRenderingContext} gl GL. - * @param {WebGLFramebuffer} framebuffer Framebuffer. - * @param {WebGLTexture} texture Texture. - */ - const postRenderFunction = function(gl, framebuffer, texture) { - if (!gl.isContextLost()) { - gl.deleteFramebuffer(framebuffer); - gl.deleteTexture(texture); - } - }.bind(null, gl, this.framebuffer, this.texture); - - frameState.postRenderFunctions.push( - /** @type {module:ol/PluggableMap~PostRenderFunction} */ (postRenderFunction) - ); - - const texture = createEmptyTexture( - gl, framebufferDimension, framebufferDimension); - - const framebuffer = gl.createFramebuffer(); - gl.bindFramebuffer(FRAMEBUFFER, framebuffer); - gl.framebufferTexture2D(FRAMEBUFFER, - COLOR_ATTACHMENT0, TEXTURE_2D, texture, 0); - - this.texture = texture; - this.framebuffer = framebuffer; - this.framebufferDimension = framebufferDimension; - - } else { - gl.bindFramebuffer(FRAMEBUFFER, this.framebuffer); - } - -}; - - -/** - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/layer/Layer~State} layerState Layer state. - * @param {module:ol/webgl/Context} context Context. - */ -WebGLLayerRenderer.prototype.composeFrame = function(frameState, layerState, context) { - - this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, context, frameState); - - context.bindBuffer(ARRAY_BUFFER, this.arrayBuffer_); - - const gl = context.getGL(); - - const program = context.getProgram(fragment, vertex); - - let locations; - if (!this.defaultLocations_) { - locations = new Locations(gl, program); - this.defaultLocations_ = locations; - } else { - locations = this.defaultLocations_; - } - - if (context.useProgram(program)) { - gl.enableVertexAttribArray(locations.a_position); - gl.vertexAttribPointer( - locations.a_position, 2, FLOAT, false, 16, 0); - gl.enableVertexAttribArray(locations.a_texCoord); - gl.vertexAttribPointer( - locations.a_texCoord, 2, FLOAT, false, 16, 8); - gl.uniform1i(locations.u_texture, 0); - } - - gl.uniformMatrix4fv(locations.u_texCoordMatrix, false, - fromTransform(this.tmpMat4_, this.getTexCoordMatrix())); - gl.uniformMatrix4fv(locations.u_projectionMatrix, false, - fromTransform(this.tmpMat4_, this.getProjectionMatrix())); - gl.uniform1f(locations.u_opacity, layerState.opacity); - gl.bindTexture(TEXTURE_2D, this.getTexture()); - gl.drawArrays(TRIANGLE_STRIP, 0, 4); - - this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, context, frameState); -}; - - -/** - * @param {module:ol/render/EventType} type Event type. - * @param {module:ol/webgl/Context} context WebGL context. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @private - */ -WebGLLayerRenderer.prototype.dispatchComposeEvent_ = function(type, context, frameState) { - const layer = this.getLayer(); - if (layer.hasListener(type)) { - const viewState = frameState.viewState; - const resolution = viewState.resolution; - const pixelRatio = frameState.pixelRatio; - const extent = frameState.extent; - const center = viewState.center; - const rotation = viewState.rotation; - const size = frameState.size; - - const render = new WebGLImmediateRenderer( - context, center, resolution, rotation, size, extent, pixelRatio); - const composeEvent = new RenderEvent( - type, render, frameState, null, context); - layer.dispatchEvent(composeEvent); - } -}; - - -/** - * @return {!module:ol/transform~Transform} Matrix. - */ -WebGLLayerRenderer.prototype.getTexCoordMatrix = function() { - return this.texCoordMatrix; -}; - - -/** - * @return {WebGLTexture} Texture. - */ -WebGLLayerRenderer.prototype.getTexture = function() { - return this.texture; -}; - - -/** - * @return {!module:ol/transform~Transform} Matrix. - */ -WebGLLayerRenderer.prototype.getProjectionMatrix = function() { - return this.projectionMatrix; -}; - - -/** - * Handle webglcontextlost. - */ -WebGLLayerRenderer.prototype.handleWebGLContextLost = function() { - this.texture = null; - this.framebuffer = null; - this.framebufferDimension = undefined; -}; - - -/** - * @abstract - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @param {module:ol/layer/Layer~State} layerState Layer state. - * @param {module:ol/webgl/Context} context Context. - * @return {boolean} whether composeFrame should be called. - */ -WebGLLayerRenderer.prototype.prepareFrame = function(frameState, layerState, context) {}; - - -/** - * @abstract - * @param {module:ol~Pixel} pixel Pixel. - * @param {module:ol/PluggableMap~FrameState} frameState FrameState. - * @param {function(this: S, module:ol/layer/Layer, (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,U - */ -WebGLLayerRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {}; export default WebGLLayerRenderer; diff --git a/src/ol/renderer/webgl/Map.js b/src/ol/renderer/webgl/Map.js index 960e1a093d..8d4c254aa8 100644 --- a/src/ol/renderer/webgl/Map.js +++ b/src/ol/renderer/webgl/Map.js @@ -44,545 +44,539 @@ const WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024; * @param {module:ol/PluggableMap} map Map. * @api */ -const WebGLMapRenderer = function(map) { - MapRenderer.call(this, map); +class WebGLMapRenderer { + constructor(map) { + MapRenderer.call(this, map); - const container = map.getViewport(); + const container = map.getViewport(); - /** - * @private - * @type {HTMLCanvasElement} - */ - this.canvas_ = /** @type {HTMLCanvasElement} */ - (document.createElement('CANVAS')); - this.canvas_.style.width = '100%'; - this.canvas_.style.height = '100%'; - this.canvas_.style.display = 'block'; - this.canvas_.className = CLASS_UNSELECTABLE; - container.insertBefore(this.canvas_, container.childNodes[0] || null); - - /** - * @private - * @type {number} - */ - this.clipTileCanvasWidth_ = 0; - - /** - * @private - * @type {number} - */ - this.clipTileCanvasHeight_ = 0; - - /** - * @private - * @type {CanvasRenderingContext2D} - */ - this.clipTileContext_ = createCanvasContext2D(); - - /** - * @private - * @type {boolean} - */ - this.renderedVisible_ = true; - - /** - * @private - * @type {WebGLRenderingContext} - */ - this.gl_ = getContext(this.canvas_, { - antialias: true, - depth: true, - failIfMajorPerformanceCaveat: true, - preserveDrawingBuffer: false, - stencil: true - }); - - /** - * @private - * @type {module:ol/webgl/Context} - */ - this.context_ = new WebGLContext(this.canvas_, this.gl_); - - listen(this.canvas_, ContextEventType.LOST, - this.handleWebGLContextLost, this); - listen(this.canvas_, ContextEventType.RESTORED, - this.handleWebGLContextRestored, this); - - /** - * @private - * @type {module:ol/structs/LRUCache.} - */ - this.textureCache_ = new LRUCache(); - - /** - * @private - * @type {module:ol/coordinate~Coordinate} - */ - this.focus_ = null; - - /** - * @private - * @type {module:ol/structs/PriorityQueue.} - */ - this.tileTextureQueue_ = new PriorityQueue( /** - * @param {Array.<*>} element Element. - * @return {number} Priority. - * @this {module:ol/renderer/webgl/Map} + * @private + * @type {HTMLCanvasElement} */ - (function(element) { - const tileCenter = /** @type {module:ol/coordinate~Coordinate} */ (element[1]); - const tileResolution = /** @type {number} */ (element[2]); - const deltaX = tileCenter[0] - this.focus_[0]; - const deltaY = tileCenter[1] - this.focus_[1]; - return 65536 * Math.log(tileResolution) + - Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution; - }).bind(this), + this.canvas_ = /** @type {HTMLCanvasElement} */ + (document.createElement('CANVAS')); + this.canvas_.style.width = '100%'; + this.canvas_.style.height = '100%'; + this.canvas_.style.display = 'block'; + this.canvas_.className = CLASS_UNSELECTABLE; + container.insertBefore(this.canvas_, container.childNodes[0] || null); + /** - * @param {Array.<*>} element Element. - * @return {string} Key. + * @private + * @type {number} */ - function(element) { - return ( - /** @type {module:ol/Tile} */ (element[0]).getKey() - ); + this.clipTileCanvasWidth_ = 0; + + /** + * @private + * @type {number} + */ + this.clipTileCanvasHeight_ = 0; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.clipTileContext_ = createCanvasContext2D(); + + /** + * @private + * @type {boolean} + */ + this.renderedVisible_ = true; + + /** + * @private + * @type {WebGLRenderingContext} + */ + this.gl_ = getContext(this.canvas_, { + antialias: true, + depth: true, + failIfMajorPerformanceCaveat: true, + preserveDrawingBuffer: false, + stencil: true }); + /** + * @private + * @type {module:ol/webgl/Context} + */ + this.context_ = new WebGLContext(this.canvas_, this.gl_); + + listen(this.canvas_, ContextEventType.LOST, + this.handleWebGLContextLost, this); + listen(this.canvas_, ContextEventType.RESTORED, + this.handleWebGLContextRestored, this); + + /** + * @private + * @type {module:ol/structs/LRUCache.} + */ + this.textureCache_ = new LRUCache(); + + /** + * @private + * @type {module:ol/coordinate~Coordinate} + */ + this.focus_ = null; + + /** + * @private + * @type {module:ol/structs/PriorityQueue.} + */ + this.tileTextureQueue_ = new PriorityQueue( + /** + * @param {Array.<*>} element Element. + * @return {number} Priority. + * @this {module:ol/renderer/webgl/Map} + */ + (function(element) { + const tileCenter = /** @type {module:ol/coordinate~Coordinate} */ (element[1]); + const tileResolution = /** @type {number} */ (element[2]); + const deltaX = tileCenter[0] - this.focus_[0]; + const deltaY = tileCenter[1] - this.focus_[1]; + return 65536 * Math.log(tileResolution) + + Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution; + }).bind(this), + /** + * @param {Array.<*>} element Element. + * @return {string} Key. + */ + function(element) { + return ( + /** @type {module:ol/Tile} */ (element[0]).getKey() + ); + }); + + + /** + * @param {module:ol/PluggableMap} map Map. + * @param {?module:ol/PluggableMap~FrameState} frameState Frame state. + * @return {boolean} false. + * @this {module:ol/renderer/webgl/Map} + */ + this.loadNextTileTexture_ = + function(map, frameState) { + if (!this.tileTextureQueue_.isEmpty()) { + this.tileTextureQueue_.reprioritize(); + const element = this.tileTextureQueue_.dequeue(); + const tile = /** @type {module:ol/Tile} */ (element[0]); + const tileSize = /** @type {module:ol/size~Size} */ (element[3]); + const tileGutter = /** @type {number} */ (element[4]); + this.bindTileTexture( + tile, tileSize, tileGutter, LINEAR, LINEAR); + } + return false; + }.bind(this); + + + /** + * @private + * @type {number} + */ + this.textureCacheFrameMarkerCount_ = 0; + + this.initializeGL_(); + } + + /** + * @param {module:ol/Tile} tile Tile. + * @param {module:ol/size~Size} tileSize Tile size. + * @param {number} tileGutter Tile gutter. + * @param {number} magFilter Mag filter. + * @param {number} minFilter Min filter. + */ + bindTileTexture(tile, tileSize, tileGutter, magFilter, minFilter) { + const gl = this.getGL(); + const tileKey = tile.getKey(); + if (this.textureCache_.containsKey(tileKey)) { + const textureCacheEntry = this.textureCache_.get(tileKey); + gl.bindTexture(TEXTURE_2D, textureCacheEntry.texture); + if (textureCacheEntry.magFilter != magFilter) { + gl.texParameteri( + TEXTURE_2D, TEXTURE_MAG_FILTER, magFilter); + textureCacheEntry.magFilter = magFilter; + } + if (textureCacheEntry.minFilter != minFilter) { + gl.texParameteri( + TEXTURE_2D, TEXTURE_MIN_FILTER, minFilter); + textureCacheEntry.minFilter = minFilter; + } + } else { + const texture = gl.createTexture(); + gl.bindTexture(TEXTURE_2D, texture); + if (tileGutter > 0) { + const clipTileCanvas = this.clipTileContext_.canvas; + const clipTileContext = this.clipTileContext_; + if (this.clipTileCanvasWidth_ !== tileSize[0] || + this.clipTileCanvasHeight_ !== tileSize[1]) { + clipTileCanvas.width = tileSize[0]; + clipTileCanvas.height = tileSize[1]; + this.clipTileCanvasWidth_ = tileSize[0]; + this.clipTileCanvasHeight_ = tileSize[1]; + } else { + clipTileContext.clearRect(0, 0, tileSize[0], tileSize[1]); + } + clipTileContext.drawImage(tile.getImage(), tileGutter, tileGutter, + tileSize[0], tileSize[1], 0, 0, tileSize[0], tileSize[1]); + gl.texImage2D(TEXTURE_2D, 0, + RGBA, RGBA, + UNSIGNED_BYTE, clipTileCanvas); + } else { + gl.texImage2D(TEXTURE_2D, 0, + RGBA, RGBA, + UNSIGNED_BYTE, tile.getImage()); + } + gl.texParameteri( + TEXTURE_2D, TEXTURE_MAG_FILTER, magFilter); + gl.texParameteri( + TEXTURE_2D, TEXTURE_MIN_FILTER, minFilter); + gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, + CLAMP_TO_EDGE); + gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, + CLAMP_TO_EDGE); + this.textureCache_.set(tileKey, { + texture: texture, + magFilter: magFilter, + minFilter: minFilter + }); + } + } + + /** + * @param {module:ol/render/EventType} type Event type. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @private + */ + dispatchComposeEvent_(type, frameState) { + const map = this.getMap(); + if (map.hasListener(type)) { + const context = this.context_; + + const extent = frameState.extent; + const size = frameState.size; + const viewState = frameState.viewState; + const pixelRatio = frameState.pixelRatio; + + const resolution = viewState.resolution; + const center = viewState.center; + const rotation = viewState.rotation; + + const vectorContext = new WebGLImmediateRenderer(context, + center, resolution, rotation, size, extent, pixelRatio); + const composeEvent = new RenderEvent(type, vectorContext, + frameState, null, context); + map.dispatchEvent(composeEvent); + } + } + + /** + * @inheritDoc + */ + disposeInternal() { + const gl = this.getGL(); + if (!gl.isContextLost()) { + this.textureCache_.forEach( + /** + * @param {?module:ol/renderer/webgl/Map~TextureCacheEntry} textureCacheEntry + * Texture cache entry. + */ + function(textureCacheEntry) { + if (textureCacheEntry) { + gl.deleteTexture(textureCacheEntry.texture); + } + }); + } + this.context_.dispose(); + MapRenderer.prototype.disposeInternal.call(this); + } /** * @param {module:ol/PluggableMap} map Map. - * @param {?module:ol/PluggableMap~FrameState} frameState Frame state. - * @return {boolean} false. - * @this {module:ol/renderer/webgl/Map} + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + * @private */ - this.loadNextTileTexture_ = - function(map, frameState) { - if (!this.tileTextureQueue_.isEmpty()) { - this.tileTextureQueue_.reprioritize(); - const element = this.tileTextureQueue_.dequeue(); - const tile = /** @type {module:ol/Tile} */ (element[0]); - const tileSize = /** @type {module:ol/size~Size} */ (element[3]); - const tileGutter = /** @type {number} */ (element[4]); - this.bindTileTexture( - tile, tileSize, tileGutter, LINEAR, LINEAR); + expireCache_(map, frameState) { + const gl = this.getGL(); + let textureCacheEntry; + while (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ > + WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) { + textureCacheEntry = this.textureCache_.peekLast(); + if (!textureCacheEntry) { + if (+this.textureCache_.peekLastKey() == frameState.index) { + break; + } else { + --this.textureCacheFrameMarkerCount_; } - return false; - }.bind(this); + } else { + gl.deleteTexture(textureCacheEntry.texture); + } + this.textureCache_.pop(); + } + } + /** + * @return {module:ol/webgl/Context} The context. + */ + getContext() { + return this.context_; + } + + /** + * @return {WebGLRenderingContext} GL. + */ + getGL() { + return this.gl_; + } + + /** + * @return {module:ol/structs/PriorityQueue.} Tile texture queue. + */ + getTileTextureQueue() { + return this.tileTextureQueue_; + } + + /** + * @param {module:ol/events/Event} event Event. + * @protected + */ + handleWebGLContextLost(event) { + event.preventDefault(); + this.textureCache_.clear(); + this.textureCacheFrameMarkerCount_ = 0; + + const renderers = this.getLayerRenderers(); + for (const id in renderers) { + const renderer = /** @type {module:ol/renderer/webgl/Layer} */ (renderers[id]); + renderer.handleWebGLContextLost(); + } + } + + /** + * @protected + */ + handleWebGLContextRestored() { + this.initializeGL_(); + this.getMap().render(); + } /** * @private - * @type {number} */ - this.textureCacheFrameMarkerCount_ = 0; + initializeGL_() { + const gl = this.gl_; + gl.activeTexture(TEXTURE0); + gl.blendFuncSeparate( + SRC_ALPHA, ONE_MINUS_SRC_ALPHA, + ONE, ONE_MINUS_SRC_ALPHA); + gl.disable(CULL_FACE); + gl.disable(DEPTH_TEST); + gl.disable(SCISSOR_TEST); + gl.disable(STENCIL_TEST); + } - this.initializeGL_(); -}; + /** + * @param {module:ol/Tile} tile Tile. + * @return {boolean} Is tile texture loaded. + */ + isTileTextureLoaded(tile) { + return this.textureCache_.containsKey(tile.getKey()); + } + + /** + * @inheritDoc + */ + renderFrame(frameState) { + + const context = this.getContext(); + const gl = this.getGL(); + + if (gl.isContextLost()) { + return false; + } + + if (!frameState) { + if (this.renderedVisible_) { + this.canvas_.style.display = 'none'; + this.renderedVisible_ = false; + } + return false; + } + + this.focus_ = frameState.focus; + + this.textureCache_.set((-frameState.index).toString(), null); + ++this.textureCacheFrameMarkerCount_; + + this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, frameState); + + /** @type {Array.} */ + const layerStatesToDraw = []; + const layerStatesArray = frameState.layerStatesArray; + stableSort(layerStatesArray, sortByZIndex); + + const viewResolution = frameState.viewState.resolution; + let i, ii, layerRenderer, layerState; + for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { + layerState = layerStatesArray[i]; + if (visibleAtResolution(layerState, viewResolution) && + layerState.sourceState == SourceState.READY) { + layerRenderer = /** @type {module:ol/renderer/webgl/Layer} */ (this.getLayerRenderer(layerState.layer)); + if (layerRenderer.prepareFrame(frameState, layerState, context)) { + layerStatesToDraw.push(layerState); + } + } + } + + const width = frameState.size[0] * frameState.pixelRatio; + const height = frameState.size[1] * frameState.pixelRatio; + if (this.canvas_.width != width || this.canvas_.height != height) { + this.canvas_.width = width; + this.canvas_.height = height; + } + + gl.bindFramebuffer(FRAMEBUFFER, null); + + gl.clearColor(0, 0, 0, 0); + gl.clear(COLOR_BUFFER_BIT); + gl.enable(BLEND); + gl.viewport(0, 0, this.canvas_.width, this.canvas_.height); + + for (i = 0, ii = layerStatesToDraw.length; i < ii; ++i) { + layerState = layerStatesToDraw[i]; + layerRenderer = /** @type {module:ol/renderer/webgl/Layer} */ (this.getLayerRenderer(layerState.layer)); + layerRenderer.composeFrame(frameState, layerState, context); + } + + if (!this.renderedVisible_) { + this.canvas_.style.display = ''; + this.renderedVisible_ = true; + } + + this.calculateMatrices2D(frameState); + + if (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ > + WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) { + frameState.postRenderFunctions.push( + /** @type {module:ol/PluggableMap~PostRenderFunction} */ (this.expireCache_.bind(this)) + ); + } + + if (!this.tileTextureQueue_.isEmpty()) { + frameState.postRenderFunctions.push(this.loadNextTileTexture_); + frameState.animate = true; + } + + this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, frameState); + + this.scheduleRemoveUnusedLayerRenderers(frameState); + this.scheduleExpireIconCache(frameState); + + } + + /** + * @inheritDoc + */ + forEachFeatureAtCoordinate( + coordinate, + frameState, + hitTolerance, + callback, + thisArg, + layerFilter, + thisArg2 + ) { + let result; + + if (this.getGL().isContextLost()) { + return false; + } + + const viewState = frameState.viewState; + + const layerStates = frameState.layerStatesArray; + const numLayers = layerStates.length; + let i; + for (i = numLayers - 1; i >= 0; --i) { + const layerState = layerStates[i]; + const layer = layerState.layer; + if (visibleAtResolution(layerState, viewState.resolution) && + layerFilter.call(thisArg2, layer)) { + const layerRenderer = this.getLayerRenderer(layer); + result = layerRenderer.forEachFeatureAtCoordinate( + coordinate, frameState, hitTolerance, callback, thisArg); + if (result) { + return result; + } + } + } + return undefined; + } + + /** + * @inheritDoc + */ + hasFeatureAtCoordinate(coordinate, frameState, hitTolerance, layerFilter, thisArg) { + let hasFeature = false; + + if (this.getGL().isContextLost()) { + return false; + } + + const viewState = frameState.viewState; + + const layerStates = frameState.layerStatesArray; + const numLayers = layerStates.length; + let i; + for (i = numLayers - 1; i >= 0; --i) { + const layerState = layerStates[i]; + const layer = layerState.layer; + if (visibleAtResolution(layerState, viewState.resolution) && + layerFilter.call(thisArg, layer)) { + const layerRenderer = this.getLayerRenderer(layer); + hasFeature = + layerRenderer.hasFeatureAtCoordinate(coordinate, frameState); + if (hasFeature) { + return true; + } + } + } + return hasFeature; + } + + /** + * @inheritDoc + */ + forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { + if (this.getGL().isContextLost()) { + return false; + } + + const viewState = frameState.viewState; + let result; + + const layerStates = frameState.layerStatesArray; + const numLayers = layerStates.length; + let i; + for (i = numLayers - 1; i >= 0; --i) { + const layerState = layerStates[i]; + const layer = layerState.layer; + if (visibleAtResolution(layerState, viewState.resolution) && + layerFilter.call(thisArg, layer)) { + const layerRenderer = /** @type {module:ol/renderer/webgl/Layer} */ (this.getLayerRenderer(layer)); + result = layerRenderer.forEachLayerAtPixel( + pixel, frameState, callback, thisArg); + if (result) { + return result; + } + } + } + return undefined; + } +} inherits(WebGLMapRenderer, MapRenderer); -/** - * @param {module:ol/Tile} tile Tile. - * @param {module:ol/size~Size} tileSize Tile size. - * @param {number} tileGutter Tile gutter. - * @param {number} magFilter Mag filter. - * @param {number} minFilter Min filter. - */ -WebGLMapRenderer.prototype.bindTileTexture = function(tile, tileSize, tileGutter, magFilter, minFilter) { - const gl = this.getGL(); - const tileKey = tile.getKey(); - if (this.textureCache_.containsKey(tileKey)) { - const textureCacheEntry = this.textureCache_.get(tileKey); - gl.bindTexture(TEXTURE_2D, textureCacheEntry.texture); - if (textureCacheEntry.magFilter != magFilter) { - gl.texParameteri( - TEXTURE_2D, TEXTURE_MAG_FILTER, magFilter); - textureCacheEntry.magFilter = magFilter; - } - if (textureCacheEntry.minFilter != minFilter) { - gl.texParameteri( - TEXTURE_2D, TEXTURE_MIN_FILTER, minFilter); - textureCacheEntry.minFilter = minFilter; - } - } else { - const texture = gl.createTexture(); - gl.bindTexture(TEXTURE_2D, texture); - if (tileGutter > 0) { - const clipTileCanvas = this.clipTileContext_.canvas; - const clipTileContext = this.clipTileContext_; - if (this.clipTileCanvasWidth_ !== tileSize[0] || - this.clipTileCanvasHeight_ !== tileSize[1]) { - clipTileCanvas.width = tileSize[0]; - clipTileCanvas.height = tileSize[1]; - this.clipTileCanvasWidth_ = tileSize[0]; - this.clipTileCanvasHeight_ = tileSize[1]; - } else { - clipTileContext.clearRect(0, 0, tileSize[0], tileSize[1]); - } - clipTileContext.drawImage(tile.getImage(), tileGutter, tileGutter, - tileSize[0], tileSize[1], 0, 0, tileSize[0], tileSize[1]); - gl.texImage2D(TEXTURE_2D, 0, - RGBA, RGBA, - UNSIGNED_BYTE, clipTileCanvas); - } else { - gl.texImage2D(TEXTURE_2D, 0, - RGBA, RGBA, - UNSIGNED_BYTE, tile.getImage()); - } - gl.texParameteri( - TEXTURE_2D, TEXTURE_MAG_FILTER, magFilter); - gl.texParameteri( - TEXTURE_2D, TEXTURE_MIN_FILTER, minFilter); - gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, - CLAMP_TO_EDGE); - gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, - CLAMP_TO_EDGE); - this.textureCache_.set(tileKey, { - texture: texture, - magFilter: magFilter, - minFilter: minFilter - }); - } -}; - - -/** - * @param {module:ol/render/EventType} type Event type. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @private - */ -WebGLMapRenderer.prototype.dispatchComposeEvent_ = function(type, frameState) { - const map = this.getMap(); - if (map.hasListener(type)) { - const context = this.context_; - - const extent = frameState.extent; - const size = frameState.size; - const viewState = frameState.viewState; - const pixelRatio = frameState.pixelRatio; - - const resolution = viewState.resolution; - const center = viewState.center; - const rotation = viewState.rotation; - - const vectorContext = new WebGLImmediateRenderer(context, - center, resolution, rotation, size, extent, pixelRatio); - const composeEvent = new RenderEvent(type, vectorContext, - frameState, null, context); - map.dispatchEvent(composeEvent); - } -}; - - -/** - * @inheritDoc - */ -WebGLMapRenderer.prototype.disposeInternal = function() { - const gl = this.getGL(); - if (!gl.isContextLost()) { - this.textureCache_.forEach( - /** - * @param {?module:ol/renderer/webgl/Map~TextureCacheEntry} textureCacheEntry - * Texture cache entry. - */ - function(textureCacheEntry) { - if (textureCacheEntry) { - gl.deleteTexture(textureCacheEntry.texture); - } - }); - } - this.context_.dispose(); - MapRenderer.prototype.disposeInternal.call(this); -}; - - -/** - * @param {module:ol/PluggableMap} map Map. - * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @private - */ -WebGLMapRenderer.prototype.expireCache_ = function(map, frameState) { - const gl = this.getGL(); - let textureCacheEntry; - while (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ > - WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) { - textureCacheEntry = this.textureCache_.peekLast(); - if (!textureCacheEntry) { - if (+this.textureCache_.peekLastKey() == frameState.index) { - break; - } else { - --this.textureCacheFrameMarkerCount_; - } - } else { - gl.deleteTexture(textureCacheEntry.texture); - } - this.textureCache_.pop(); - } -}; - - -/** - * @return {module:ol/webgl/Context} The context. - */ -WebGLMapRenderer.prototype.getContext = function() { - return this.context_; -}; - - -/** - * @return {WebGLRenderingContext} GL. - */ -WebGLMapRenderer.prototype.getGL = function() { - return this.gl_; -}; - - -/** - * @return {module:ol/structs/PriorityQueue.} Tile texture queue. - */ -WebGLMapRenderer.prototype.getTileTextureQueue = function() { - return this.tileTextureQueue_; -}; - - -/** - * @param {module:ol/events/Event} event Event. - * @protected - */ -WebGLMapRenderer.prototype.handleWebGLContextLost = function(event) { - event.preventDefault(); - this.textureCache_.clear(); - this.textureCacheFrameMarkerCount_ = 0; - - const renderers = this.getLayerRenderers(); - for (const id in renderers) { - const renderer = /** @type {module:ol/renderer/webgl/Layer} */ (renderers[id]); - renderer.handleWebGLContextLost(); - } -}; - - -/** - * @protected - */ -WebGLMapRenderer.prototype.handleWebGLContextRestored = function() { - this.initializeGL_(); - this.getMap().render(); -}; - - -/** - * @private - */ -WebGLMapRenderer.prototype.initializeGL_ = function() { - const gl = this.gl_; - gl.activeTexture(TEXTURE0); - gl.blendFuncSeparate( - SRC_ALPHA, ONE_MINUS_SRC_ALPHA, - ONE, ONE_MINUS_SRC_ALPHA); - gl.disable(CULL_FACE); - gl.disable(DEPTH_TEST); - gl.disable(SCISSOR_TEST); - gl.disable(STENCIL_TEST); -}; - - -/** - * @param {module:ol/Tile} tile Tile. - * @return {boolean} Is tile texture loaded. - */ -WebGLMapRenderer.prototype.isTileTextureLoaded = function(tile) { - return this.textureCache_.containsKey(tile.getKey()); -}; - - -/** - * @inheritDoc - */ -WebGLMapRenderer.prototype.renderFrame = function(frameState) { - - const context = this.getContext(); - const gl = this.getGL(); - - if (gl.isContextLost()) { - return false; - } - - if (!frameState) { - if (this.renderedVisible_) { - this.canvas_.style.display = 'none'; - this.renderedVisible_ = false; - } - return false; - } - - this.focus_ = frameState.focus; - - this.textureCache_.set((-frameState.index).toString(), null); - ++this.textureCacheFrameMarkerCount_; - - this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, frameState); - - /** @type {Array.} */ - const layerStatesToDraw = []; - const layerStatesArray = frameState.layerStatesArray; - stableSort(layerStatesArray, sortByZIndex); - - const viewResolution = frameState.viewState.resolution; - let i, ii, layerRenderer, layerState; - for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { - layerState = layerStatesArray[i]; - if (visibleAtResolution(layerState, viewResolution) && - layerState.sourceState == SourceState.READY) { - layerRenderer = /** @type {module:ol/renderer/webgl/Layer} */ (this.getLayerRenderer(layerState.layer)); - if (layerRenderer.prepareFrame(frameState, layerState, context)) { - layerStatesToDraw.push(layerState); - } - } - } - - const width = frameState.size[0] * frameState.pixelRatio; - const height = frameState.size[1] * frameState.pixelRatio; - if (this.canvas_.width != width || this.canvas_.height != height) { - this.canvas_.width = width; - this.canvas_.height = height; - } - - gl.bindFramebuffer(FRAMEBUFFER, null); - - gl.clearColor(0, 0, 0, 0); - gl.clear(COLOR_BUFFER_BIT); - gl.enable(BLEND); - gl.viewport(0, 0, this.canvas_.width, this.canvas_.height); - - for (i = 0, ii = layerStatesToDraw.length; i < ii; ++i) { - layerState = layerStatesToDraw[i]; - layerRenderer = /** @type {module:ol/renderer/webgl/Layer} */ (this.getLayerRenderer(layerState.layer)); - layerRenderer.composeFrame(frameState, layerState, context); - } - - if (!this.renderedVisible_) { - this.canvas_.style.display = ''; - this.renderedVisible_ = true; - } - - this.calculateMatrices2D(frameState); - - if (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ > - WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) { - frameState.postRenderFunctions.push( - /** @type {module:ol/PluggableMap~PostRenderFunction} */ (this.expireCache_.bind(this)) - ); - } - - if (!this.tileTextureQueue_.isEmpty()) { - frameState.postRenderFunctions.push(this.loadNextTileTexture_); - frameState.animate = true; - } - - this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, frameState); - - this.scheduleRemoveUnusedLayerRenderers(frameState); - this.scheduleExpireIconCache(frameState); - -}; - - -/** - * @inheritDoc - */ -WebGLMapRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg, - layerFilter, thisArg2) { - let result; - - if (this.getGL().isContextLost()) { - return false; - } - - const viewState = frameState.viewState; - - const layerStates = frameState.layerStatesArray; - const numLayers = layerStates.length; - let i; - for (i = numLayers - 1; i >= 0; --i) { - const layerState = layerStates[i]; - const layer = layerState.layer; - if (visibleAtResolution(layerState, viewState.resolution) && - layerFilter.call(thisArg2, layer)) { - const layerRenderer = this.getLayerRenderer(layer); - result = layerRenderer.forEachFeatureAtCoordinate( - coordinate, frameState, hitTolerance, callback, thisArg); - if (result) { - return result; - } - } - } - return undefined; -}; - - -/** - * @inheritDoc - */ -WebGLMapRenderer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, layerFilter, thisArg) { - let hasFeature = false; - - if (this.getGL().isContextLost()) { - return false; - } - - const viewState = frameState.viewState; - - const layerStates = frameState.layerStatesArray; - const numLayers = layerStates.length; - let i; - for (i = numLayers - 1; i >= 0; --i) { - const layerState = layerStates[i]; - const layer = layerState.layer; - if (visibleAtResolution(layerState, viewState.resolution) && - layerFilter.call(thisArg, layer)) { - const layerRenderer = this.getLayerRenderer(layer); - hasFeature = - layerRenderer.hasFeatureAtCoordinate(coordinate, frameState); - if (hasFeature) { - return true; - } - } - } - return hasFeature; -}; - - -/** - * @inheritDoc - */ -WebGLMapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, hitTolerance, callback, thisArg, - layerFilter, thisArg2) { - if (this.getGL().isContextLost()) { - return false; - } - - const viewState = frameState.viewState; - let result; - - const layerStates = frameState.layerStatesArray; - const numLayers = layerStates.length; - let i; - for (i = numLayers - 1; i >= 0; --i) { - const layerState = layerStates[i]; - const layer = layerState.layer; - if (visibleAtResolution(layerState, viewState.resolution) && - layerFilter.call(thisArg, layer)) { - const layerRenderer = /** @type {module:ol/renderer/webgl/Layer} */ (this.getLayerRenderer(layer)); - result = layerRenderer.forEachLayerAtPixel( - pixel, frameState, callback, thisArg); - if (result) { - return result; - } - } - } - return undefined; -}; - export default WebGLMapRenderer; diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js index 265408e816..dfc9161f8d 100644 --- a/src/ol/renderer/webgl/TileLayer.js +++ b/src/ol/renderer/webgl/TileLayer.js @@ -32,64 +32,355 @@ import WebGLBuffer from '../../webgl/Buffer.js'; * @param {module:ol/layer/Tile} tileLayer Tile layer. * @api */ -const WebGLTileLayerRenderer = function(mapRenderer, tileLayer) { +class WebGLTileLayerRenderer { + constructor(mapRenderer, tileLayer) { - WebGLLayerRenderer.call(this, mapRenderer, tileLayer); + WebGLLayerRenderer.call(this, mapRenderer, tileLayer); + + /** + * @private + * @type {module:ol/webgl/Fragment} + */ + this.fragmentShader_ = fragment; + + /** + * @private + * @type {module:ol/webgl/Vertex} + */ + this.vertexShader_ = vertex; + + /** + * @private + * @type {module:ol/renderer/webgl/tilelayershader/Locations} + */ + this.locations_ = null; + + /** + * @private + * @type {module:ol/webgl/Buffer} + */ + this.renderArrayBuffer_ = new WebGLBuffer([ + 0, 0, 0, 1, + 1, 0, 1, 1, + 0, 1, 0, 0, + 1, 1, 1, 0 + ]); + + /** + * @private + * @type {module:ol/TileRange} + */ + this.renderedTileRange_ = null; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.renderedFramebufferExtent_ = null; + + /** + * @private + * @type {number} + */ + this.renderedRevision_ = -1; + + /** + * @private + * @type {module:ol/size~Size} + */ + this.tmpSize_ = [0, 0]; + + } /** - * @private - * @type {module:ol/webgl/Fragment} + * @inheritDoc */ - this.fragmentShader_ = fragment; + disposeInternal() { + const context = this.mapRenderer.getContext(); + context.deleteBuffer(this.renderArrayBuffer_); + WebGLLayerRenderer.prototype.disposeInternal.call(this); + } /** - * @private - * @type {module:ol/webgl/Vertex} + * @inheritDoc */ - this.vertexShader_ = vertex; + createLoadedTileFinder(source, projection, tiles) { + const mapRenderer = this.mapRenderer; + + return ( + /** + * @param {number} zoom Zoom level. + * @param {module:ol/TileRange} tileRange Tile range. + * @return {boolean} The tile range is fully loaded. + */ + function(zoom, tileRange) { + function callback(tile) { + const loaded = mapRenderer.isTileTextureLoaded(tile); + if (loaded) { + if (!tiles[zoom]) { + tiles[zoom] = {}; + } + tiles[zoom][tile.tileCoord.toString()] = tile; + } + return loaded; + } + return source.forEachLoadedTile(projection, zoom, tileRange, callback); + } + ); + } /** - * @private - * @type {module:ol/renderer/webgl/tilelayershader/Locations} + * @inheritDoc */ - this.locations_ = null; + handleWebGLContextLost() { + WebGLLayerRenderer.prototype.handleWebGLContextLost.call(this); + this.locations_ = null; + } /** - * @private - * @type {module:ol/webgl/Buffer} + * @inheritDoc */ - this.renderArrayBuffer_ = new WebGLBuffer([ - 0, 0, 0, 1, - 1, 0, 1, 1, - 0, 1, 0, 0, - 1, 1, 1, 0 - ]); + prepareFrame(frameState, layerState, context) { + + const mapRenderer = this.mapRenderer; + const gl = context.getGL(); + + const viewState = frameState.viewState; + const projection = viewState.projection; + + const tileLayer = /** @type {module:ol/layer/Tile} */ (this.getLayer()); + const tileSource = tileLayer.getSource(); + const tileGrid = tileSource.getTileGridForProjection(projection); + const z = tileGrid.getZForResolution(viewState.resolution); + const tileResolution = tileGrid.getResolution(z); + + const tilePixelSize = + tileSource.getTilePixelSize(z, frameState.pixelRatio, projection); + const pixelRatio = tilePixelSize[0] / + toSize(tileGrid.getTileSize(z), this.tmpSize_)[0]; + const tilePixelResolution = tileResolution / pixelRatio; + const tileGutter = tileSource.getTilePixelRatio(pixelRatio) * tileSource.getGutter(projection); + + const center = viewState.center; + const extent = frameState.extent; + const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z); + + let framebufferExtent; + if (this.renderedTileRange_ && + this.renderedTileRange_.equals(tileRange) && + this.renderedRevision_ == tileSource.getRevision()) { + framebufferExtent = this.renderedFramebufferExtent_; + } else { + + const tileRangeSize = tileRange.getSize(); + + const maxDimension = Math.max( + tileRangeSize[0] * tilePixelSize[0], + tileRangeSize[1] * tilePixelSize[1]); + const framebufferDimension = roundUpToPowerOfTwo(maxDimension); + const framebufferExtentDimension = tilePixelResolution * framebufferDimension; + const origin = tileGrid.getOrigin(z); + const minX = origin[0] + + tileRange.minX * tilePixelSize[0] * tilePixelResolution; + const minY = origin[1] + + tileRange.minY * tilePixelSize[1] * tilePixelResolution; + framebufferExtent = [ + minX, minY, + minX + framebufferExtentDimension, minY + framebufferExtentDimension + ]; + + this.bindFramebuffer(frameState, framebufferDimension); + gl.viewport(0, 0, framebufferDimension, framebufferDimension); + + gl.clearColor(0, 0, 0, 0); + gl.clear(COLOR_BUFFER_BIT); + gl.disable(BLEND); + + const program = context.getProgram(this.fragmentShader_, this.vertexShader_); + context.useProgram(program); + if (!this.locations_) { + this.locations_ = new Locations(gl, program); + } + + context.bindBuffer(ARRAY_BUFFER, this.renderArrayBuffer_); + gl.enableVertexAttribArray(this.locations_.a_position); + gl.vertexAttribPointer( + this.locations_.a_position, 2, FLOAT, false, 16, 0); + gl.enableVertexAttribArray(this.locations_.a_texCoord); + gl.vertexAttribPointer( + this.locations_.a_texCoord, 2, FLOAT, false, 16, 8); + gl.uniform1i(this.locations_.u_texture, 0); + + /** + * @type {Object.>} + */ + const tilesToDrawByZ = {}; + tilesToDrawByZ[z] = {}; + + const findLoadedTiles = this.createLoadedTileFinder( + tileSource, projection, tilesToDrawByZ); + + const useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); + let allTilesLoaded = true; + const tmpExtent = createEmpty(); + const tmpTileRange = new TileRange(0, 0, 0, 0); + let childTileRange, drawable, fullyLoaded, tile, tileState; + let x, y, tileExtent; + for (x = tileRange.minX; x <= tileRange.maxX; ++x) { + for (y = tileRange.minY; y <= tileRange.maxY; ++y) { + + tile = tileSource.getTile(z, x, y, pixelRatio, projection); + if (layerState.extent !== undefined) { + // ignore tiles outside layer extent + tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent); + if (!intersects(tileExtent, layerState.extent)) { + continue; + } + } + tileState = tile.getState(); + drawable = tileState == TileState.LOADED || + tileState == TileState.EMPTY || + tileState == TileState.ERROR && !useInterimTilesOnError; + if (!drawable) { + tile = tile.getInterimTile(); + } + tileState = tile.getState(); + if (tileState == TileState.LOADED) { + if (mapRenderer.isTileTextureLoaded(tile)) { + tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; + continue; + } + } else if (tileState == TileState.EMPTY || + (tileState == TileState.ERROR && + !useInterimTilesOnError)) { + continue; + } + + allTilesLoaded = false; + fullyLoaded = tileGrid.forEachTileCoordParentTileRange( + tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); + if (!fullyLoaded) { + childTileRange = tileGrid.getTileCoordChildTileRange( + tile.tileCoord, tmpTileRange, tmpExtent); + if (childTileRange) { + findLoadedTiles(z + 1, childTileRange); + } + } + + } + + } + + /** @type {Array.} */ + const zs = Object.keys(tilesToDrawByZ).map(Number); + zs.sort(numberSafeCompareFunction); + const u_tileOffset = new Float32Array(4); + for (let i = 0, ii = zs.length; i < ii; ++i) { + const tilesToDraw = tilesToDrawByZ[zs[i]]; + for (const tileKey in tilesToDraw) { + tile = tilesToDraw[tileKey]; + tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent); + u_tileOffset[0] = 2 * (tileExtent[2] - tileExtent[0]) / + framebufferExtentDimension; + u_tileOffset[1] = 2 * (tileExtent[3] - tileExtent[1]) / + framebufferExtentDimension; + u_tileOffset[2] = 2 * (tileExtent[0] - framebufferExtent[0]) / + framebufferExtentDimension - 1; + u_tileOffset[3] = 2 * (tileExtent[1] - framebufferExtent[1]) / + framebufferExtentDimension - 1; + gl.uniform4fv(this.locations_.u_tileOffset, u_tileOffset); + mapRenderer.bindTileTexture(tile, tilePixelSize, + tileGutter * pixelRatio, LINEAR, LINEAR); + gl.drawArrays(TRIANGLE_STRIP, 0, 4); + } + } + + if (allTilesLoaded) { + this.renderedTileRange_ = tileRange; + this.renderedFramebufferExtent_ = framebufferExtent; + this.renderedRevision_ = tileSource.getRevision(); + } else { + this.renderedTileRange_ = null; + this.renderedFramebufferExtent_ = null; + this.renderedRevision_ = -1; + frameState.animate = true; + } + + } + + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); + const tileTextureQueue = mapRenderer.getTileTextureQueue(); + this.manageTilePyramid( + frameState, tileSource, tileGrid, pixelRatio, projection, extent, z, + tileLayer.getPreload(), + /** + * @param {module:ol/Tile} tile Tile. + */ + function(tile) { + if (tile.getState() == TileState.LOADED && + !mapRenderer.isTileTextureLoaded(tile) && + !tileTextureQueue.isKeyQueued(tile.getKey())) { + tileTextureQueue.enqueue([ + tile, + tileGrid.getTileCoordCenter(tile.tileCoord), + tileGrid.getResolution(tile.tileCoord[0]), + tilePixelSize, tileGutter * pixelRatio + ]); + } + }, this); + this.scheduleExpireCache(frameState, tileSource); + + const texCoordMatrix = this.texCoordMatrix; + resetTransform(texCoordMatrix); + translateTransform(texCoordMatrix, + (Math.round(center[0] / tileResolution) * tileResolution - framebufferExtent[0]) / + (framebufferExtent[2] - framebufferExtent[0]), + (Math.round(center[1] / tileResolution) * tileResolution - framebufferExtent[1]) / + (framebufferExtent[3] - framebufferExtent[1])); + if (viewState.rotation !== 0) { + rotateTransform(texCoordMatrix, viewState.rotation); + } + scaleTransform(texCoordMatrix, + frameState.size[0] * viewState.resolution / + (framebufferExtent[2] - framebufferExtent[0]), + frameState.size[1] * viewState.resolution / + (framebufferExtent[3] - framebufferExtent[1])); + translateTransform(texCoordMatrix, -0.5, -0.5); + + return true; + } /** - * @private - * @type {module:ol/TileRange} + * @inheritDoc */ - this.renderedTileRange_ = null; + forEachLayerAtPixel(pixel, frameState, callback, thisArg) { + if (!this.framebuffer) { + return undefined; + } - /** - * @private - * @type {module:ol/extent~Extent} - */ - this.renderedFramebufferExtent_ = null; + const pixelOnMapScaled = [ + pixel[0] / frameState.size[0], + (frameState.size[1] - pixel[1]) / frameState.size[1]]; - /** - * @private - * @type {number} - */ - this.renderedRevision_ = -1; + const pixelOnFrameBufferScaled = applyTransform( + this.texCoordMatrix, pixelOnMapScaled.slice()); + const pixelOnFrameBuffer = [ + pixelOnFrameBufferScaled[0] * this.framebufferDimension, + pixelOnFrameBufferScaled[1] * this.framebufferDimension]; - /** - * @private - * @type {module:ol/size~Size} - */ - this.tmpSize_ = [0, 0]; + const gl = this.mapRenderer.getContext().getGL(); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + const imageData = new Uint8Array(4); + gl.readPixels(pixelOnFrameBuffer[0], pixelOnFrameBuffer[1], 1, 1, + gl.RGBA, gl.UNSIGNED_BYTE, imageData); -}; + if (imageData[3] > 0) { + return callback.call(thisArg, this.getLayer(), imageData); + } else { + return undefined; + } + } +} inherits(WebGLTileLayerRenderer, WebGLLayerRenderer); @@ -118,296 +409,4 @@ WebGLTileLayerRenderer['create'] = function(mapRenderer, layer) { }; -/** - * @inheritDoc - */ -WebGLTileLayerRenderer.prototype.disposeInternal = function() { - const context = this.mapRenderer.getContext(); - context.deleteBuffer(this.renderArrayBuffer_); - WebGLLayerRenderer.prototype.disposeInternal.call(this); -}; - - -/** - * @inheritDoc - */ -WebGLTileLayerRenderer.prototype.createLoadedTileFinder = function(source, projection, tiles) { - const mapRenderer = this.mapRenderer; - - return ( - /** - * @param {number} zoom Zoom level. - * @param {module:ol/TileRange} tileRange Tile range. - * @return {boolean} The tile range is fully loaded. - */ - function(zoom, tileRange) { - function callback(tile) { - const loaded = mapRenderer.isTileTextureLoaded(tile); - if (loaded) { - if (!tiles[zoom]) { - tiles[zoom] = {}; - } - tiles[zoom][tile.tileCoord.toString()] = tile; - } - return loaded; - } - return source.forEachLoadedTile(projection, zoom, tileRange, callback); - } - ); -}; - - -/** - * @inheritDoc - */ -WebGLTileLayerRenderer.prototype.handleWebGLContextLost = function() { - WebGLLayerRenderer.prototype.handleWebGLContextLost.call(this); - this.locations_ = null; -}; - - -/** - * @inheritDoc - */ -WebGLTileLayerRenderer.prototype.prepareFrame = function(frameState, layerState, context) { - - const mapRenderer = this.mapRenderer; - const gl = context.getGL(); - - const viewState = frameState.viewState; - const projection = viewState.projection; - - const tileLayer = /** @type {module:ol/layer/Tile} */ (this.getLayer()); - const tileSource = tileLayer.getSource(); - const tileGrid = tileSource.getTileGridForProjection(projection); - const z = tileGrid.getZForResolution(viewState.resolution); - const tileResolution = tileGrid.getResolution(z); - - const tilePixelSize = - tileSource.getTilePixelSize(z, frameState.pixelRatio, projection); - const pixelRatio = tilePixelSize[0] / - toSize(tileGrid.getTileSize(z), this.tmpSize_)[0]; - const tilePixelResolution = tileResolution / pixelRatio; - const tileGutter = tileSource.getTilePixelRatio(pixelRatio) * tileSource.getGutter(projection); - - const center = viewState.center; - const extent = frameState.extent; - const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z); - - let framebufferExtent; - if (this.renderedTileRange_ && - this.renderedTileRange_.equals(tileRange) && - this.renderedRevision_ == tileSource.getRevision()) { - framebufferExtent = this.renderedFramebufferExtent_; - } else { - - const tileRangeSize = tileRange.getSize(); - - const maxDimension = Math.max( - tileRangeSize[0] * tilePixelSize[0], - tileRangeSize[1] * tilePixelSize[1]); - const framebufferDimension = roundUpToPowerOfTwo(maxDimension); - const framebufferExtentDimension = tilePixelResolution * framebufferDimension; - const origin = tileGrid.getOrigin(z); - const minX = origin[0] + - tileRange.minX * tilePixelSize[0] * tilePixelResolution; - const minY = origin[1] + - tileRange.minY * tilePixelSize[1] * tilePixelResolution; - framebufferExtent = [ - minX, minY, - minX + framebufferExtentDimension, minY + framebufferExtentDimension - ]; - - this.bindFramebuffer(frameState, framebufferDimension); - gl.viewport(0, 0, framebufferDimension, framebufferDimension); - - gl.clearColor(0, 0, 0, 0); - gl.clear(COLOR_BUFFER_BIT); - gl.disable(BLEND); - - const program = context.getProgram(this.fragmentShader_, this.vertexShader_); - context.useProgram(program); - if (!this.locations_) { - this.locations_ = new Locations(gl, program); - } - - context.bindBuffer(ARRAY_BUFFER, this.renderArrayBuffer_); - gl.enableVertexAttribArray(this.locations_.a_position); - gl.vertexAttribPointer( - this.locations_.a_position, 2, FLOAT, false, 16, 0); - gl.enableVertexAttribArray(this.locations_.a_texCoord); - gl.vertexAttribPointer( - this.locations_.a_texCoord, 2, FLOAT, false, 16, 8); - gl.uniform1i(this.locations_.u_texture, 0); - - /** - * @type {Object.>} - */ - const tilesToDrawByZ = {}; - tilesToDrawByZ[z] = {}; - - const findLoadedTiles = this.createLoadedTileFinder( - tileSource, projection, tilesToDrawByZ); - - const useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); - let allTilesLoaded = true; - const tmpExtent = createEmpty(); - const tmpTileRange = new TileRange(0, 0, 0, 0); - let childTileRange, drawable, fullyLoaded, tile, tileState; - let x, y, tileExtent; - for (x = tileRange.minX; x <= tileRange.maxX; ++x) { - for (y = tileRange.minY; y <= tileRange.maxY; ++y) { - - tile = tileSource.getTile(z, x, y, pixelRatio, projection); - if (layerState.extent !== undefined) { - // ignore tiles outside layer extent - tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent); - if (!intersects(tileExtent, layerState.extent)) { - continue; - } - } - tileState = tile.getState(); - drawable = tileState == TileState.LOADED || - tileState == TileState.EMPTY || - tileState == TileState.ERROR && !useInterimTilesOnError; - if (!drawable) { - tile = tile.getInterimTile(); - } - tileState = tile.getState(); - if (tileState == TileState.LOADED) { - if (mapRenderer.isTileTextureLoaded(tile)) { - tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; - continue; - } - } else if (tileState == TileState.EMPTY || - (tileState == TileState.ERROR && - !useInterimTilesOnError)) { - continue; - } - - allTilesLoaded = false; - fullyLoaded = tileGrid.forEachTileCoordParentTileRange( - tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); - if (!fullyLoaded) { - childTileRange = tileGrid.getTileCoordChildTileRange( - tile.tileCoord, tmpTileRange, tmpExtent); - if (childTileRange) { - findLoadedTiles(z + 1, childTileRange); - } - } - - } - - } - - /** @type {Array.} */ - const zs = Object.keys(tilesToDrawByZ).map(Number); - zs.sort(numberSafeCompareFunction); - const u_tileOffset = new Float32Array(4); - for (let i = 0, ii = zs.length; i < ii; ++i) { - const tilesToDraw = tilesToDrawByZ[zs[i]]; - for (const tileKey in tilesToDraw) { - tile = tilesToDraw[tileKey]; - tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent); - u_tileOffset[0] = 2 * (tileExtent[2] - tileExtent[0]) / - framebufferExtentDimension; - u_tileOffset[1] = 2 * (tileExtent[3] - tileExtent[1]) / - framebufferExtentDimension; - u_tileOffset[2] = 2 * (tileExtent[0] - framebufferExtent[0]) / - framebufferExtentDimension - 1; - u_tileOffset[3] = 2 * (tileExtent[1] - framebufferExtent[1]) / - framebufferExtentDimension - 1; - gl.uniform4fv(this.locations_.u_tileOffset, u_tileOffset); - mapRenderer.bindTileTexture(tile, tilePixelSize, - tileGutter * pixelRatio, LINEAR, LINEAR); - gl.drawArrays(TRIANGLE_STRIP, 0, 4); - } - } - - if (allTilesLoaded) { - this.renderedTileRange_ = tileRange; - this.renderedFramebufferExtent_ = framebufferExtent; - this.renderedRevision_ = tileSource.getRevision(); - } else { - this.renderedTileRange_ = null; - this.renderedFramebufferExtent_ = null; - this.renderedRevision_ = -1; - frameState.animate = true; - } - - } - - this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); - const tileTextureQueue = mapRenderer.getTileTextureQueue(); - this.manageTilePyramid( - frameState, tileSource, tileGrid, pixelRatio, projection, extent, z, - tileLayer.getPreload(), - /** - * @param {module:ol/Tile} tile Tile. - */ - function(tile) { - if (tile.getState() == TileState.LOADED && - !mapRenderer.isTileTextureLoaded(tile) && - !tileTextureQueue.isKeyQueued(tile.getKey())) { - tileTextureQueue.enqueue([ - tile, - tileGrid.getTileCoordCenter(tile.tileCoord), - tileGrid.getResolution(tile.tileCoord[0]), - tilePixelSize, tileGutter * pixelRatio - ]); - } - }, this); - this.scheduleExpireCache(frameState, tileSource); - - const texCoordMatrix = this.texCoordMatrix; - resetTransform(texCoordMatrix); - translateTransform(texCoordMatrix, - (Math.round(center[0] / tileResolution) * tileResolution - framebufferExtent[0]) / - (framebufferExtent[2] - framebufferExtent[0]), - (Math.round(center[1] / tileResolution) * tileResolution - framebufferExtent[1]) / - (framebufferExtent[3] - framebufferExtent[1])); - if (viewState.rotation !== 0) { - rotateTransform(texCoordMatrix, viewState.rotation); - } - scaleTransform(texCoordMatrix, - frameState.size[0] * viewState.resolution / - (framebufferExtent[2] - framebufferExtent[0]), - frameState.size[1] * viewState.resolution / - (framebufferExtent[3] - framebufferExtent[1])); - translateTransform(texCoordMatrix, -0.5, -0.5); - - return true; -}; - - -/** - * @inheritDoc - */ -WebGLTileLayerRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { - if (!this.framebuffer) { - return undefined; - } - - const pixelOnMapScaled = [ - pixel[0] / frameState.size[0], - (frameState.size[1] - pixel[1]) / frameState.size[1]]; - - const pixelOnFrameBufferScaled = applyTransform( - this.texCoordMatrix, pixelOnMapScaled.slice()); - const pixelOnFrameBuffer = [ - pixelOnFrameBufferScaled[0] * this.framebufferDimension, - pixelOnFrameBufferScaled[1] * this.framebufferDimension]; - - const gl = this.mapRenderer.getContext().getGL(); - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); - const imageData = new Uint8Array(4); - gl.readPixels(pixelOnFrameBuffer[0], pixelOnFrameBuffer[1], 1, 1, - gl.RGBA, gl.UNSIGNED_BYTE, imageData); - - if (imageData[3] > 0) { - return callback.call(thisArg, this.getLayer(), imageData); - } else { - return undefined; - } -}; export default WebGLTileLayerRenderer; diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index bf4a5d2476..02016204b3 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -17,54 +17,287 @@ import {apply as applyTransform} from '../../transform.js'; * @param {module:ol/layer/Vector} vectorLayer Vector layer. * @api */ -const WebGLVectorLayerRenderer = function(mapRenderer, vectorLayer) { +class WebGLVectorLayerRenderer { + constructor(mapRenderer, vectorLayer) { - WebGLLayerRenderer.call(this, mapRenderer, vectorLayer); + WebGLLayerRenderer.call(this, mapRenderer, vectorLayer); + + /** + * @private + * @type {boolean} + */ + this.dirty_ = false; + + /** + * @private + * @type {number} + */ + this.renderedRevision_ = -1; + + /** + * @private + * @type {number} + */ + this.renderedResolution_ = NaN; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.renderedExtent_ = createEmpty(); + + /** + * @private + * @type {function(module:ol/Feature, module:ol/Feature): number|null} + */ + this.renderedRenderOrder_ = null; + + /** + * @private + * @type {module:ol/render/webgl/ReplayGroup} + */ + this.replayGroup_ = null; + + /** + * The last layer state. + * @private + * @type {?module:ol/layer/Layer~State} + */ + this.layerState_ = null; + + } /** - * @private - * @type {boolean} + * @inheritDoc */ - this.dirty_ = false; + composeFrame(frameState, layerState, context) { + this.layerState_ = layerState; + const viewState = frameState.viewState; + const replayGroup = this.replayGroup_; + const size = frameState.size; + const pixelRatio = frameState.pixelRatio; + const gl = this.mapRenderer.getGL(); + if (replayGroup && !replayGroup.isEmpty()) { + gl.enable(gl.SCISSOR_TEST); + gl.scissor(0, 0, size[0] * pixelRatio, size[1] * pixelRatio); + replayGroup.replay(context, + viewState.center, viewState.resolution, viewState.rotation, + size, pixelRatio, layerState.opacity, + layerState.managed ? frameState.skippedFeatureUids : {}); + gl.disable(gl.SCISSOR_TEST); + } + + } /** - * @private - * @type {number} + * @inheritDoc */ - this.renderedRevision_ = -1; + disposeInternal() { + const replayGroup = this.replayGroup_; + if (replayGroup) { + const context = this.mapRenderer.getContext(); + replayGroup.getDeleteResourcesFunction(context)(); + this.replayGroup_ = null; + } + WebGLLayerRenderer.prototype.disposeInternal.call(this); + } /** - * @private - * @type {number} + * @inheritDoc */ - this.renderedResolution_ = NaN; + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + if (!this.replayGroup_ || !this.layerState_) { + return undefined; + } else { + const context = this.mapRenderer.getContext(); + const viewState = frameState.viewState; + const layer = this.getLayer(); + const layerState = this.layerState_; + /** @type {!Object.} */ + const features = {}; + return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, + context, viewState.center, viewState.resolution, viewState.rotation, + frameState.size, frameState.pixelRatio, layerState.opacity, + {}, + /** + * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + const key = getUid(feature).toString(); + if (!(key in features)) { + features[key] = true; + return callback.call(thisArg, feature, layer); + } + }); + } + } /** - * @private - * @type {module:ol/extent~Extent} + * @inheritDoc */ - this.renderedExtent_ = createEmpty(); + hasFeatureAtCoordinate(coordinate, frameState) { + if (!this.replayGroup_ || !this.layerState_) { + return false; + } else { + const context = this.mapRenderer.getContext(); + const viewState = frameState.viewState; + const layerState = this.layerState_; + return this.replayGroup_.hasFeatureAtCoordinate(coordinate, + context, viewState.center, viewState.resolution, viewState.rotation, + frameState.size, frameState.pixelRatio, layerState.opacity, + frameState.skippedFeatureUids); + } + } /** - * @private - * @type {function(module:ol/Feature, module:ol/Feature): number|null} + * @inheritDoc */ - this.renderedRenderOrder_ = null; + forEachLayerAtPixel(pixel, frameState, callback, thisArg) { + const coordinate = applyTransform( + frameState.pixelToCoordinateTransform, pixel.slice()); + const hasFeature = this.hasFeatureAtCoordinate(coordinate, frameState); + + if (hasFeature) { + return callback.call(thisArg, this.getLayer(), null); + } else { + return undefined; + } + } /** + * Handle changes in image style state. + * @param {module:ol/events/Event} event Image style change event. * @private - * @type {module:ol/render/webgl/ReplayGroup} */ - this.replayGroup_ = null; + handleStyleImageChange_(event) { + this.renderIfReadyAndVisible(); + } /** - * The last layer state. - * @private - * @type {?module:ol/layer/Layer~State} + * @inheritDoc */ - this.layerState_ = null; + prepareFrame(frameState, layerState, context) { + const vectorLayer = /** @type {module:ol/layer/Vector} */ (this.getLayer()); + const vectorSource = vectorLayer.getSource(); -}; + const animating = frameState.viewHints[ViewHint.ANIMATING]; + const interacting = frameState.viewHints[ViewHint.INTERACTING]; + const updateWhileAnimating = vectorLayer.getUpdateWhileAnimating(); + const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting(); + + if (!this.dirty_ && (!updateWhileAnimating && animating) || + (!updateWhileInteracting && interacting)) { + return true; + } + + const frameStateExtent = frameState.extent; + const viewState = frameState.viewState; + const projection = viewState.projection; + const resolution = viewState.resolution; + const pixelRatio = frameState.pixelRatio; + const vectorLayerRevision = vectorLayer.getRevision(); + const vectorLayerRenderBuffer = vectorLayer.getRenderBuffer(); + let vectorLayerRenderOrder = vectorLayer.getRenderOrder(); + + if (vectorLayerRenderOrder === undefined) { + vectorLayerRenderOrder = defaultRenderOrder; + } + + const extent = buffer(frameStateExtent, + vectorLayerRenderBuffer * resolution); + + if (!this.dirty_ && + this.renderedResolution_ == resolution && + this.renderedRevision_ == vectorLayerRevision && + this.renderedRenderOrder_ == vectorLayerRenderOrder && + containsExtent(this.renderedExtent_, extent)) { + return true; + } + + if (this.replayGroup_) { + frameState.postRenderFunctions.push( + this.replayGroup_.getDeleteResourcesFunction(context)); + } + + this.dirty_ = false; + + const replayGroup = new WebGLReplayGroup( + getRenderTolerance(resolution, pixelRatio), + extent, vectorLayer.getRenderBuffer()); + vectorSource.loadFeatures(extent, resolution, projection); + /** + * @param {module:ol/Feature} feature Feature. + * @this {module:ol/renderer/webgl/VectorLayer} + */ + const render = function(feature) { + let styles; + const styleFunction = feature.getStyleFunction() || vectorLayer.getStyleFunction(); + if (styleFunction) { + styles = styleFunction(feature, resolution); + } + if (styles) { + const dirty = this.renderFeature( + feature, resolution, pixelRatio, styles, replayGroup); + this.dirty_ = this.dirty_ || dirty; + } + }; + if (vectorLayerRenderOrder) { + /** @type {Array.} */ + const features = []; + vectorSource.forEachFeatureInExtent(extent, + /** + * @param {module:ol/Feature} feature Feature. + */ + function(feature) { + features.push(feature); + }, this); + features.sort(vectorLayerRenderOrder); + features.forEach(render.bind(this)); + } else { + vectorSource.forEachFeatureInExtent(extent, render, this); + } + replayGroup.finish(context); + + this.renderedResolution_ = resolution; + this.renderedRevision_ = vectorLayerRevision; + this.renderedRenderOrder_ = vectorLayerRenderOrder; + this.renderedExtent_ = extent; + this.replayGroup_ = replayGroup; + + return true; + } + + /** + * @param {module:ol/Feature} feature Feature. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {(module:ol/style/Style|Array.)} styles The style or array of + * styles. + * @param {module:ol/render/webgl/ReplayGroup} replayGroup Replay group. + * @return {boolean} `true` if an image is loading. + */ + renderFeature(feature, resolution, pixelRatio, styles, replayGroup) { + if (!styles) { + return false; + } + let loading = false; + if (Array.isArray(styles)) { + for (let i = styles.length - 1, ii = 0; i >= ii; --i) { + loading = renderFeature( + replayGroup, feature, styles[i], + getSquaredRenderTolerance(resolution, pixelRatio), + this.handleStyleImageChange_, this) || loading; + } + } else { + loading = renderFeature( + replayGroup, feature, styles, + getSquaredRenderTolerance(resolution, pixelRatio), + this.handleStyleImageChange_, this) || loading; + } + return loading; + } +} inherits(WebGLVectorLayerRenderer, WebGLLayerRenderer); @@ -93,241 +326,4 @@ WebGLVectorLayerRenderer['create'] = function(mapRenderer, layer) { }; -/** - * @inheritDoc - */ -WebGLVectorLayerRenderer.prototype.composeFrame = function(frameState, layerState, context) { - this.layerState_ = layerState; - const viewState = frameState.viewState; - const replayGroup = this.replayGroup_; - const size = frameState.size; - const pixelRatio = frameState.pixelRatio; - const gl = this.mapRenderer.getGL(); - if (replayGroup && !replayGroup.isEmpty()) { - gl.enable(gl.SCISSOR_TEST); - gl.scissor(0, 0, size[0] * pixelRatio, size[1] * pixelRatio); - replayGroup.replay(context, - viewState.center, viewState.resolution, viewState.rotation, - size, pixelRatio, layerState.opacity, - layerState.managed ? frameState.skippedFeatureUids : {}); - gl.disable(gl.SCISSOR_TEST); - } - -}; - - -/** - * @inheritDoc - */ -WebGLVectorLayerRenderer.prototype.disposeInternal = function() { - const replayGroup = this.replayGroup_; - if (replayGroup) { - const context = this.mapRenderer.getContext(); - replayGroup.getDeleteResourcesFunction(context)(); - this.replayGroup_ = null; - } - WebGLLayerRenderer.prototype.disposeInternal.call(this); -}; - - -/** - * @inheritDoc - */ -WebGLVectorLayerRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { - if (!this.replayGroup_ || !this.layerState_) { - return undefined; - } else { - const context = this.mapRenderer.getContext(); - const viewState = frameState.viewState; - const layer = this.getLayer(); - const layerState = this.layerState_; - /** @type {!Object.} */ - const features = {}; - return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, - context, viewState.center, viewState.resolution, viewState.rotation, - frameState.size, frameState.pixelRatio, layerState.opacity, - {}, - /** - * @param {module:ol/Feature|module:ol/render/Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - const key = getUid(feature).toString(); - if (!(key in features)) { - features[key] = true; - return callback.call(thisArg, feature, layer); - } - }); - } -}; - - -/** - * @inheritDoc - */ -WebGLVectorLayerRenderer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState) { - if (!this.replayGroup_ || !this.layerState_) { - return false; - } else { - const context = this.mapRenderer.getContext(); - const viewState = frameState.viewState; - const layerState = this.layerState_; - return this.replayGroup_.hasFeatureAtCoordinate(coordinate, - context, viewState.center, viewState.resolution, viewState.rotation, - frameState.size, frameState.pixelRatio, layerState.opacity, - frameState.skippedFeatureUids); - } -}; - - -/** - * @inheritDoc - */ -WebGLVectorLayerRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { - const coordinate = applyTransform( - frameState.pixelToCoordinateTransform, pixel.slice()); - const hasFeature = this.hasFeatureAtCoordinate(coordinate, frameState); - - if (hasFeature) { - return callback.call(thisArg, this.getLayer(), null); - } else { - return undefined; - } -}; - - -/** - * Handle changes in image style state. - * @param {module:ol/events/Event} event Image style change event. - * @private - */ -WebGLVectorLayerRenderer.prototype.handleStyleImageChange_ = function(event) { - this.renderIfReadyAndVisible(); -}; - - -/** - * @inheritDoc - */ -WebGLVectorLayerRenderer.prototype.prepareFrame = function(frameState, layerState, context) { - const vectorLayer = /** @type {module:ol/layer/Vector} */ (this.getLayer()); - const vectorSource = vectorLayer.getSource(); - - const animating = frameState.viewHints[ViewHint.ANIMATING]; - const interacting = frameState.viewHints[ViewHint.INTERACTING]; - const updateWhileAnimating = vectorLayer.getUpdateWhileAnimating(); - const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting(); - - if (!this.dirty_ && (!updateWhileAnimating && animating) || - (!updateWhileInteracting && interacting)) { - return true; - } - - const frameStateExtent = frameState.extent; - const viewState = frameState.viewState; - const projection = viewState.projection; - const resolution = viewState.resolution; - const pixelRatio = frameState.pixelRatio; - const vectorLayerRevision = vectorLayer.getRevision(); - const vectorLayerRenderBuffer = vectorLayer.getRenderBuffer(); - let vectorLayerRenderOrder = vectorLayer.getRenderOrder(); - - if (vectorLayerRenderOrder === undefined) { - vectorLayerRenderOrder = defaultRenderOrder; - } - - const extent = buffer(frameStateExtent, - vectorLayerRenderBuffer * resolution); - - if (!this.dirty_ && - this.renderedResolution_ == resolution && - this.renderedRevision_ == vectorLayerRevision && - this.renderedRenderOrder_ == vectorLayerRenderOrder && - containsExtent(this.renderedExtent_, extent)) { - return true; - } - - if (this.replayGroup_) { - frameState.postRenderFunctions.push( - this.replayGroup_.getDeleteResourcesFunction(context)); - } - - this.dirty_ = false; - - const replayGroup = new WebGLReplayGroup( - getRenderTolerance(resolution, pixelRatio), - extent, vectorLayer.getRenderBuffer()); - vectorSource.loadFeatures(extent, resolution, projection); - /** - * @param {module:ol/Feature} feature Feature. - * @this {module:ol/renderer/webgl/VectorLayer} - */ - const render = function(feature) { - let styles; - const styleFunction = feature.getStyleFunction() || vectorLayer.getStyleFunction(); - if (styleFunction) { - styles = styleFunction(feature, resolution); - } - if (styles) { - const dirty = this.renderFeature( - feature, resolution, pixelRatio, styles, replayGroup); - this.dirty_ = this.dirty_ || dirty; - } - }; - if (vectorLayerRenderOrder) { - /** @type {Array.} */ - const features = []; - vectorSource.forEachFeatureInExtent(extent, - /** - * @param {module:ol/Feature} feature Feature. - */ - function(feature) { - features.push(feature); - }, this); - features.sort(vectorLayerRenderOrder); - features.forEach(render.bind(this)); - } else { - vectorSource.forEachFeatureInExtent(extent, render, this); - } - replayGroup.finish(context); - - this.renderedResolution_ = resolution; - this.renderedRevision_ = vectorLayerRevision; - this.renderedRenderOrder_ = vectorLayerRenderOrder; - this.renderedExtent_ = extent; - this.replayGroup_ = replayGroup; - - return true; -}; - - -/** - * @param {module:ol/Feature} feature Feature. - * @param {number} resolution Resolution. - * @param {number} pixelRatio Pixel ratio. - * @param {(module:ol/style/Style|Array.)} styles The style or array of - * styles. - * @param {module:ol/render/webgl/ReplayGroup} replayGroup Replay group. - * @return {boolean} `true` if an image is loading. - */ -WebGLVectorLayerRenderer.prototype.renderFeature = function(feature, resolution, pixelRatio, styles, replayGroup) { - if (!styles) { - return false; - } - let loading = false; - if (Array.isArray(styles)) { - for (let i = styles.length - 1, ii = 0; i >= ii; --i) { - loading = renderFeature( - replayGroup, feature, styles[i], - getSquaredRenderTolerance(resolution, pixelRatio), - this.handleStyleImageChange_, this) || loading; - } - } else { - loading = renderFeature( - replayGroup, feature, styles, - getSquaredRenderTolerance(resolution, pixelRatio), - this.handleStyleImageChange_, this) || loading; - } - return loading; -}; export default WebGLVectorLayerRenderer; diff --git a/src/ol/reproj/Image.js b/src/ol/reproj/Image.js index f73ad9915f..4fc11ccec8 100644 --- a/src/ol/reproj/Image.js +++ b/src/ol/reproj/Image.js @@ -32,171 +32,175 @@ import Triangulation from '../reproj/Triangulation.js'; * @param {module:ol/reproj/Image~FunctionType} getImageFunction * Function returning source images (extent, resolution, pixelRatio). */ -const ReprojImage = function(sourceProj, targetProj, - targetExtent, targetResolution, pixelRatio, getImageFunction) { +class ReprojImage { + constructor( + sourceProj, + targetProj, + targetExtent, + targetResolution, + pixelRatio, + getImageFunction + ) { - /** - * @private - * @type {module:ol/proj/Projection} - */ - this.targetProj_ = targetProj; + /** + * @private + * @type {module:ol/proj/Projection} + */ + this.targetProj_ = targetProj; - /** - * @private - * @type {module:ol/extent~Extent} - */ - this.maxSourceExtent_ = sourceProj.getExtent(); - const maxTargetExtent = targetProj.getExtent(); + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.maxSourceExtent_ = sourceProj.getExtent(); + const maxTargetExtent = targetProj.getExtent(); - const limitedTargetExtent = maxTargetExtent ? - getIntersection(targetExtent, maxTargetExtent) : targetExtent; + const limitedTargetExtent = maxTargetExtent ? + getIntersection(targetExtent, maxTargetExtent) : targetExtent; - const targetCenter = getCenter(limitedTargetExtent); - const sourceResolution = calculateSourceResolution( - sourceProj, targetProj, targetCenter, targetResolution); + const targetCenter = getCenter(limitedTargetExtent); + const sourceResolution = calculateSourceResolution( + sourceProj, targetProj, targetCenter, targetResolution); - const errorThresholdInPixels = ERROR_THRESHOLD; + const errorThresholdInPixels = ERROR_THRESHOLD; - /** - * @private - * @type {!module:ol/reproj/Triangulation} - */ - this.triangulation_ = new Triangulation( - sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_, - sourceResolution * errorThresholdInPixels); + /** + * @private + * @type {!module:ol/reproj/Triangulation} + */ + this.triangulation_ = new Triangulation( + sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_, + sourceResolution * errorThresholdInPixels); - /** - * @private - * @type {number} - */ - this.targetResolution_ = targetResolution; + /** + * @private + * @type {number} + */ + this.targetResolution_ = targetResolution; - /** - * @private - * @type {module:ol/extent~Extent} - */ - this.targetExtent_ = targetExtent; + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.targetExtent_ = targetExtent; - const sourceExtent = this.triangulation_.calculateSourceExtent(); + const sourceExtent = this.triangulation_.calculateSourceExtent(); - /** - * @private - * @type {module:ol/ImageBase} - */ - this.sourceImage_ = - getImageFunction(sourceExtent, sourceResolution, pixelRatio); + /** + * @private + * @type {module:ol/ImageBase} + */ + this.sourceImage_ = + getImageFunction(sourceExtent, sourceResolution, pixelRatio); - /** - * @private - * @type {number} - */ - this.sourcePixelRatio_ = - this.sourceImage_ ? this.sourceImage_.getPixelRatio() : 1; + /** + * @private + * @type {number} + */ + this.sourcePixelRatio_ = + this.sourceImage_ ? this.sourceImage_.getPixelRatio() : 1; - /** - * @private - * @type {HTMLCanvasElement} - */ - this.canvas_ = null; + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = null; - /** - * @private - * @type {?module:ol/events~EventsKey} - */ - this.sourceListenerKey_ = null; + /** + * @private + * @type {?module:ol/events~EventsKey} + */ + this.sourceListenerKey_ = null; - let state = ImageState.LOADED; + let state = ImageState.LOADED; - if (this.sourceImage_) { - state = ImageState.IDLE; + if (this.sourceImage_) { + state = ImageState.IDLE; + } + + ImageBase.call(this, targetExtent, targetResolution, this.sourcePixelRatio_, state); } - ImageBase.call(this, targetExtent, targetResolution, this.sourcePixelRatio_, state); -}; + /** + * @inheritDoc + */ + disposeInternal() { + if (this.state == ImageState.LOADING) { + this.unlistenSource_(); + } + ImageBase.prototype.disposeInternal.call(this); + } + + /** + * @inheritDoc + */ + getImage() { + return this.canvas_; + } + + /** + * @return {module:ol/proj/Projection} Projection. + */ + getProjection() { + return this.targetProj_; + } + + /** + * @private + */ + reproject_() { + const sourceState = this.sourceImage_.getState(); + if (sourceState == ImageState.LOADED) { + const width = getWidth(this.targetExtent_) / this.targetResolution_; + const height = getHeight(this.targetExtent_) / this.targetResolution_; + + this.canvas_ = renderReprojected(width, height, this.sourcePixelRatio_, + this.sourceImage_.getResolution(), this.maxSourceExtent_, + this.targetResolution_, this.targetExtent_, this.triangulation_, [{ + extent: this.sourceImage_.getExtent(), + image: this.sourceImage_.getImage() + }], 0); + } + this.state = sourceState; + this.changed(); + } + + /** + * @inheritDoc + */ + load() { + if (this.state == ImageState.IDLE) { + this.state = ImageState.LOADING; + this.changed(); + + const sourceState = this.sourceImage_.getState(); + if (sourceState == ImageState.LOADED || sourceState == ImageState.ERROR) { + this.reproject_(); + } else { + this.sourceListenerKey_ = listen(this.sourceImage_, + EventType.CHANGE, function(e) { + const sourceState = this.sourceImage_.getState(); + if (sourceState == ImageState.LOADED || sourceState == ImageState.ERROR) { + this.unlistenSource_(); + this.reproject_(); + } + }, this); + this.sourceImage_.load(); + } + } + } + + /** + * @private + */ + unlistenSource_() { + unlistenByKey(/** @type {!module:ol/events~EventsKey} */ (this.sourceListenerKey_)); + this.sourceListenerKey_ = null; + } +} inherits(ReprojImage, ImageBase); -/** - * @inheritDoc - */ -ReprojImage.prototype.disposeInternal = function() { - if (this.state == ImageState.LOADING) { - this.unlistenSource_(); - } - ImageBase.prototype.disposeInternal.call(this); -}; - - -/** - * @inheritDoc - */ -ReprojImage.prototype.getImage = function() { - return this.canvas_; -}; - - -/** - * @return {module:ol/proj/Projection} Projection. - */ -ReprojImage.prototype.getProjection = function() { - return this.targetProj_; -}; - - -/** - * @private - */ -ReprojImage.prototype.reproject_ = function() { - const sourceState = this.sourceImage_.getState(); - if (sourceState == ImageState.LOADED) { - const width = getWidth(this.targetExtent_) / this.targetResolution_; - const height = getHeight(this.targetExtent_) / this.targetResolution_; - - this.canvas_ = renderReprojected(width, height, this.sourcePixelRatio_, - this.sourceImage_.getResolution(), this.maxSourceExtent_, - this.targetResolution_, this.targetExtent_, this.triangulation_, [{ - extent: this.sourceImage_.getExtent(), - image: this.sourceImage_.getImage() - }], 0); - } - this.state = sourceState; - this.changed(); -}; - - -/** - * @inheritDoc - */ -ReprojImage.prototype.load = function() { - if (this.state == ImageState.IDLE) { - this.state = ImageState.LOADING; - this.changed(); - - const sourceState = this.sourceImage_.getState(); - if (sourceState == ImageState.LOADED || sourceState == ImageState.ERROR) { - this.reproject_(); - } else { - this.sourceListenerKey_ = listen(this.sourceImage_, - EventType.CHANGE, function(e) { - const sourceState = this.sourceImage_.getState(); - if (sourceState == ImageState.LOADED || sourceState == ImageState.ERROR) { - this.unlistenSource_(); - this.reproject_(); - } - }, this); - this.sourceImage_.load(); - } - } -}; - - -/** - * @private - */ -ReprojImage.prototype.unlistenSource_ = function() { - unlistenByKey(/** @type {!module:ol/events~EventsKey} */ (this.sourceListenerKey_)); - this.sourceListenerKey_ = null; -}; export default ReprojImage; diff --git a/src/ol/reproj/Tile.js b/src/ol/reproj/Tile.js index 81c3bd2907..c399cabacf 100644 --- a/src/ol/reproj/Tile.js +++ b/src/ol/reproj/Tile.js @@ -38,275 +38,283 @@ import Triangulation from '../reproj/Triangulation.js'; * @param {number=} opt_errorThreshold Acceptable reprojection error (in px). * @param {boolean=} opt_renderEdges Render reprojection edges. */ -const ReprojTile = function(sourceProj, sourceTileGrid, - targetProj, targetTileGrid, tileCoord, wrappedTileCoord, - pixelRatio, gutter, getTileFunction, - opt_errorThreshold, opt_renderEdges) { - Tile.call(this, tileCoord, TileState.IDLE); +class ReprojTile { + constructor( + sourceProj, + sourceTileGrid, + targetProj, + targetTileGrid, + tileCoord, + wrappedTileCoord, + pixelRatio, + gutter, + getTileFunction, + opt_errorThreshold, + opt_renderEdges + ) { + Tile.call(this, tileCoord, TileState.IDLE); - /** - * @private - * @type {boolean} - */ - this.renderEdges_ = opt_renderEdges !== undefined ? opt_renderEdges : false; + /** + * @private + * @type {boolean} + */ + this.renderEdges_ = opt_renderEdges !== undefined ? opt_renderEdges : false; - /** - * @private - * @type {number} - */ - this.pixelRatio_ = pixelRatio; + /** + * @private + * @type {number} + */ + this.pixelRatio_ = pixelRatio; - /** - * @private - * @type {number} - */ - this.gutter_ = gutter; + /** + * @private + * @type {number} + */ + this.gutter_ = gutter; - /** - * @private - * @type {HTMLCanvasElement} - */ - this.canvas_ = null; + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = null; - /** - * @private - * @type {module:ol/tilegrid/TileGrid} - */ - this.sourceTileGrid_ = sourceTileGrid; + /** + * @private + * @type {module:ol/tilegrid/TileGrid} + */ + this.sourceTileGrid_ = sourceTileGrid; - /** - * @private - * @type {module:ol/tilegrid/TileGrid} - */ - this.targetTileGrid_ = targetTileGrid; + /** + * @private + * @type {module:ol/tilegrid/TileGrid} + */ + this.targetTileGrid_ = targetTileGrid; - /** - * @private - * @type {module:ol/tilecoord~TileCoord} - */ - this.wrappedTileCoord_ = wrappedTileCoord ? wrappedTileCoord : tileCoord; + /** + * @private + * @type {module:ol/tilecoord~TileCoord} + */ + this.wrappedTileCoord_ = wrappedTileCoord ? wrappedTileCoord : tileCoord; - /** - * @private - * @type {!Array.} - */ - this.sourceTiles_ = []; + /** + * @private + * @type {!Array.} + */ + this.sourceTiles_ = []; - /** - * @private - * @type {Array.} - */ - this.sourcesListenerKeys_ = null; + /** + * @private + * @type {Array.} + */ + this.sourcesListenerKeys_ = null; - /** - * @private - * @type {number} - */ - this.sourceZ_ = 0; + /** + * @private + * @type {number} + */ + this.sourceZ_ = 0; - const targetExtent = targetTileGrid.getTileCoordExtent(this.wrappedTileCoord_); - const maxTargetExtent = this.targetTileGrid_.getExtent(); - let maxSourceExtent = this.sourceTileGrid_.getExtent(); + const targetExtent = targetTileGrid.getTileCoordExtent(this.wrappedTileCoord_); + const maxTargetExtent = this.targetTileGrid_.getExtent(); + let maxSourceExtent = this.sourceTileGrid_.getExtent(); - const limitedTargetExtent = maxTargetExtent ? - getIntersection(targetExtent, maxTargetExtent) : targetExtent; + const limitedTargetExtent = maxTargetExtent ? + getIntersection(targetExtent, maxTargetExtent) : targetExtent; - if (getArea(limitedTargetExtent) === 0) { - // Tile is completely outside range -> EMPTY - // TODO: is it actually correct that the source even creates the tile ? - this.state = TileState.EMPTY; - return; - } - - const sourceProjExtent = sourceProj.getExtent(); - if (sourceProjExtent) { - if (!maxSourceExtent) { - maxSourceExtent = sourceProjExtent; - } else { - maxSourceExtent = getIntersection(maxSourceExtent, sourceProjExtent); + if (getArea(limitedTargetExtent) === 0) { + // Tile is completely outside range -> EMPTY + // TODO: is it actually correct that the source even creates the tile ? + this.state = TileState.EMPTY; + return; } - } - const targetResolution = targetTileGrid.getResolution( - this.wrappedTileCoord_[0]); - - const targetCenter = getCenter(limitedTargetExtent); - const sourceResolution = calculateSourceResolution( - sourceProj, targetProj, targetCenter, targetResolution); - - if (!isFinite(sourceResolution) || sourceResolution <= 0) { - // invalid sourceResolution -> EMPTY - // probably edges of the projections when no extent is defined - this.state = TileState.EMPTY; - return; - } - - const errorThresholdInPixels = opt_errorThreshold !== undefined ? - opt_errorThreshold : ERROR_THRESHOLD; - - /** - * @private - * @type {!module:ol/reproj/Triangulation} - */ - this.triangulation_ = new Triangulation( - sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, - sourceResolution * errorThresholdInPixels); - - if (this.triangulation_.getTriangles().length === 0) { - // no valid triangles -> EMPTY - this.state = TileState.EMPTY; - return; - } - - this.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution); - let sourceExtent = this.triangulation_.calculateSourceExtent(); - - if (maxSourceExtent) { - if (sourceProj.canWrapX()) { - sourceExtent[1] = clamp( - sourceExtent[1], maxSourceExtent[1], maxSourceExtent[3]); - sourceExtent[3] = clamp( - sourceExtent[3], maxSourceExtent[1], maxSourceExtent[3]); - } else { - sourceExtent = getIntersection(sourceExtent, maxSourceExtent); - } - } - - if (!getArea(sourceExtent)) { - this.state = TileState.EMPTY; - } else { - const sourceRange = sourceTileGrid.getTileRangeForExtentAndZ( - sourceExtent, this.sourceZ_); - - for (let srcX = sourceRange.minX; srcX <= sourceRange.maxX; srcX++) { - for (let srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) { - const tile = getTileFunction(this.sourceZ_, srcX, srcY, pixelRatio); - if (tile) { - this.sourceTiles_.push(tile); - } + const sourceProjExtent = sourceProj.getExtent(); + if (sourceProjExtent) { + if (!maxSourceExtent) { + maxSourceExtent = sourceProjExtent; + } else { + maxSourceExtent = getIntersection(maxSourceExtent, sourceProjExtent); } } - if (this.sourceTiles_.length === 0) { + const targetResolution = targetTileGrid.getResolution( + this.wrappedTileCoord_[0]); + + const targetCenter = getCenter(limitedTargetExtent); + const sourceResolution = calculateSourceResolution( + sourceProj, targetProj, targetCenter, targetResolution); + + if (!isFinite(sourceResolution) || sourceResolution <= 0) { + // invalid sourceResolution -> EMPTY + // probably edges of the projections when no extent is defined this.state = TileState.EMPTY; + return; + } + + const errorThresholdInPixels = opt_errorThreshold !== undefined ? + opt_errorThreshold : ERROR_THRESHOLD; + + /** + * @private + * @type {!module:ol/reproj/Triangulation} + */ + this.triangulation_ = new Triangulation( + sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, + sourceResolution * errorThresholdInPixels); + + if (this.triangulation_.getTriangles().length === 0) { + // no valid triangles -> EMPTY + this.state = TileState.EMPTY; + return; + } + + this.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution); + let sourceExtent = this.triangulation_.calculateSourceExtent(); + + if (maxSourceExtent) { + if (sourceProj.canWrapX()) { + sourceExtent[1] = clamp( + sourceExtent[1], maxSourceExtent[1], maxSourceExtent[3]); + sourceExtent[3] = clamp( + sourceExtent[3], maxSourceExtent[1], maxSourceExtent[3]); + } else { + sourceExtent = getIntersection(sourceExtent, maxSourceExtent); + } + } + + if (!getArea(sourceExtent)) { + this.state = TileState.EMPTY; + } else { + const sourceRange = sourceTileGrid.getTileRangeForExtentAndZ( + sourceExtent, this.sourceZ_); + + for (let srcX = sourceRange.minX; srcX <= sourceRange.maxX; srcX++) { + for (let srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) { + const tile = getTileFunction(this.sourceZ_, srcX, srcY, pixelRatio); + if (tile) { + this.sourceTiles_.push(tile); + } + } + } + + if (this.sourceTiles_.length === 0) { + this.state = TileState.EMPTY; + } } } -}; + + /** + * @inheritDoc + */ + disposeInternal() { + if (this.state == TileState.LOADING) { + this.unlistenSources_(); + } + Tile.prototype.disposeInternal.call(this); + } + + /** + * Get the HTML Canvas element for this tile. + * @return {HTMLCanvasElement} Canvas. + */ + getImage() { + return this.canvas_; + } + + /** + * @private + */ + reproject_() { + const sources = []; + this.sourceTiles_.forEach(function(tile, i, arr) { + if (tile && tile.getState() == TileState.LOADED) { + sources.push({ + extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord), + image: tile.getImage() + }); + } + }.bind(this)); + this.sourceTiles_.length = 0; + + if (sources.length === 0) { + this.state = TileState.ERROR; + } else { + const z = this.wrappedTileCoord_[0]; + const size = this.targetTileGrid_.getTileSize(z); + const width = typeof size === 'number' ? size : size[0]; + const height = typeof size === 'number' ? size : size[1]; + const targetResolution = this.targetTileGrid_.getResolution(z); + const sourceResolution = this.sourceTileGrid_.getResolution(this.sourceZ_); + + const targetExtent = this.targetTileGrid_.getTileCoordExtent( + this.wrappedTileCoord_); + this.canvas_ = renderReprojected(width, height, this.pixelRatio_, + sourceResolution, this.sourceTileGrid_.getExtent(), + targetResolution, targetExtent, this.triangulation_, sources, + this.gutter_, this.renderEdges_); + + this.state = TileState.LOADED; + } + this.changed(); + } + + /** + * @inheritDoc + */ + load() { + if (this.state == TileState.IDLE) { + this.state = TileState.LOADING; + this.changed(); + + let leftToLoad = 0; + + this.sourcesListenerKeys_ = []; + this.sourceTiles_.forEach(function(tile, i, arr) { + const state = tile.getState(); + if (state == TileState.IDLE || state == TileState.LOADING) { + leftToLoad++; + + const sourceListenKey = listen(tile, EventType.CHANGE, + function(e) { + const state = tile.getState(); + if (state == TileState.LOADED || + state == TileState.ERROR || + state == TileState.EMPTY) { + unlistenByKey(sourceListenKey); + leftToLoad--; + if (leftToLoad === 0) { + this.unlistenSources_(); + this.reproject_(); + } + } + }, this); + this.sourcesListenerKeys_.push(sourceListenKey); + } + }.bind(this)); + + this.sourceTiles_.forEach(function(tile, i, arr) { + const state = tile.getState(); + if (state == TileState.IDLE) { + tile.load(); + } + }); + + if (leftToLoad === 0) { + setTimeout(this.reproject_.bind(this), 0); + } + } + } + + /** + * @private + */ + unlistenSources_() { + this.sourcesListenerKeys_.forEach(unlistenByKey); + this.sourcesListenerKeys_ = null; + } +} inherits(ReprojTile, Tile); -/** - * @inheritDoc - */ -ReprojTile.prototype.disposeInternal = function() { - if (this.state == TileState.LOADING) { - this.unlistenSources_(); - } - Tile.prototype.disposeInternal.call(this); -}; - - -/** - * Get the HTML Canvas element for this tile. - * @return {HTMLCanvasElement} Canvas. - */ -ReprojTile.prototype.getImage = function() { - return this.canvas_; -}; - - -/** - * @private - */ -ReprojTile.prototype.reproject_ = function() { - const sources = []; - this.sourceTiles_.forEach(function(tile, i, arr) { - if (tile && tile.getState() == TileState.LOADED) { - sources.push({ - extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord), - image: tile.getImage() - }); - } - }.bind(this)); - this.sourceTiles_.length = 0; - - if (sources.length === 0) { - this.state = TileState.ERROR; - } else { - const z = this.wrappedTileCoord_[0]; - const size = this.targetTileGrid_.getTileSize(z); - const width = typeof size === 'number' ? size : size[0]; - const height = typeof size === 'number' ? size : size[1]; - const targetResolution = this.targetTileGrid_.getResolution(z); - const sourceResolution = this.sourceTileGrid_.getResolution(this.sourceZ_); - - const targetExtent = this.targetTileGrid_.getTileCoordExtent( - this.wrappedTileCoord_); - this.canvas_ = renderReprojected(width, height, this.pixelRatio_, - sourceResolution, this.sourceTileGrid_.getExtent(), - targetResolution, targetExtent, this.triangulation_, sources, - this.gutter_, this.renderEdges_); - - this.state = TileState.LOADED; - } - this.changed(); -}; - - -/** - * @inheritDoc - */ -ReprojTile.prototype.load = function() { - if (this.state == TileState.IDLE) { - this.state = TileState.LOADING; - this.changed(); - - let leftToLoad = 0; - - this.sourcesListenerKeys_ = []; - this.sourceTiles_.forEach(function(tile, i, arr) { - const state = tile.getState(); - if (state == TileState.IDLE || state == TileState.LOADING) { - leftToLoad++; - - const sourceListenKey = listen(tile, EventType.CHANGE, - function(e) { - const state = tile.getState(); - if (state == TileState.LOADED || - state == TileState.ERROR || - state == TileState.EMPTY) { - unlistenByKey(sourceListenKey); - leftToLoad--; - if (leftToLoad === 0) { - this.unlistenSources_(); - this.reproject_(); - } - } - }, this); - this.sourcesListenerKeys_.push(sourceListenKey); - } - }.bind(this)); - - this.sourceTiles_.forEach(function(tile, i, arr) { - const state = tile.getState(); - if (state == TileState.IDLE) { - tile.load(); - } - }); - - if (leftToLoad === 0) { - setTimeout(this.reproject_.bind(this), 0); - } - } -}; - - -/** - * @private - */ -ReprojTile.prototype.unlistenSources_ = function() { - this.sourcesListenerKeys_.forEach(unlistenByKey); - this.sourcesListenerKeys_ = null; -}; export default ReprojTile; diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index a511d0ccdb..838c653ca7 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -48,309 +48,305 @@ const MAX_TRIANGLE_WIDTH = 0.25; * @param {number} errorThreshold Acceptable error (in source units). * @constructor */ -const Triangulation = function(sourceProj, targetProj, targetExtent, - maxSourceExtent, errorThreshold) { +class Triangulation { + constructor(sourceProj, targetProj, targetExtent, maxSourceExtent, errorThreshold) { - /** - * @type {module:ol/proj/Projection} - * @private - */ - this.sourceProj_ = sourceProj; + /** + * @type {module:ol/proj/Projection} + * @private + */ + this.sourceProj_ = sourceProj; - /** - * @type {module:ol/proj/Projection} - * @private - */ - this.targetProj_ = targetProj; + /** + * @type {module:ol/proj/Projection} + * @private + */ + this.targetProj_ = targetProj; - /** @type {!Object.} */ - let transformInvCache = {}; - const transformInv = getTransform(this.targetProj_, this.sourceProj_); + /** @type {!Object.} */ + let transformInvCache = {}; + const transformInv = getTransform(this.targetProj_, this.sourceProj_); - /** - * @param {module:ol/coordinate~Coordinate} c A coordinate. - * @return {module:ol/coordinate~Coordinate} Transformed coordinate. - * @private - */ - this.transformInv_ = function(c) { - const key = c[0] + '/' + c[1]; - if (!transformInvCache[key]) { - transformInvCache[key] = transformInv(c); - } - return transformInvCache[key]; - }; - - /** - * @type {module:ol/extent~Extent} - * @private - */ - this.maxSourceExtent_ = maxSourceExtent; - - /** - * @type {number} - * @private - */ - this.errorThresholdSquared_ = errorThreshold * errorThreshold; - - /** - * @type {Array.} - * @private - */ - this.triangles_ = []; - - /** - * Indicates that the triangulation crosses edge of the source projection. - * @type {boolean} - * @private - */ - this.wrapsXInSource_ = false; - - /** - * @type {boolean} - * @private - */ - this.canWrapXInSource_ = this.sourceProj_.canWrapX() && - !!maxSourceExtent && - !!this.sourceProj_.getExtent() && - (getWidth(maxSourceExtent) == getWidth(this.sourceProj_.getExtent())); - - /** - * @type {?number} - * @private - */ - this.sourceWorldWidth_ = this.sourceProj_.getExtent() ? - getWidth(this.sourceProj_.getExtent()) : null; - - /** - * @type {?number} - * @private - */ - this.targetWorldWidth_ = this.targetProj_.getExtent() ? - getWidth(this.targetProj_.getExtent()) : null; - - const destinationTopLeft = getTopLeft(targetExtent); - const destinationTopRight = getTopRight(targetExtent); - const destinationBottomRight = getBottomRight(targetExtent); - const destinationBottomLeft = getBottomLeft(targetExtent); - const sourceTopLeft = this.transformInv_(destinationTopLeft); - const sourceTopRight = this.transformInv_(destinationTopRight); - const sourceBottomRight = this.transformInv_(destinationBottomRight); - const sourceBottomLeft = this.transformInv_(destinationBottomLeft); - - this.addQuad_( - destinationTopLeft, destinationTopRight, - destinationBottomRight, destinationBottomLeft, - sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft, - MAX_SUBDIVISION); - - if (this.wrapsXInSource_) { - let leftBound = Infinity; - this.triangles_.forEach(function(triangle, i, arr) { - leftBound = Math.min(leftBound, - triangle.source[0][0], triangle.source[1][0], triangle.source[2][0]); - }); - - // Shift triangles to be as close to `leftBound` as possible - // (if the distance is more than `worldWidth / 2` it can be closer. - this.triangles_.forEach(function(triangle) { - if (Math.max(triangle.source[0][0], triangle.source[1][0], - triangle.source[2][0]) - leftBound > this.sourceWorldWidth_ / 2) { - const newTriangle = [[triangle.source[0][0], triangle.source[0][1]], - [triangle.source[1][0], triangle.source[1][1]], - [triangle.source[2][0], triangle.source[2][1]]]; - if ((newTriangle[0][0] - leftBound) > this.sourceWorldWidth_ / 2) { - newTriangle[0][0] -= this.sourceWorldWidth_; - } - if ((newTriangle[1][0] - leftBound) > this.sourceWorldWidth_ / 2) { - newTriangle[1][0] -= this.sourceWorldWidth_; - } - if ((newTriangle[2][0] - leftBound) > this.sourceWorldWidth_ / 2) { - newTriangle[2][0] -= this.sourceWorldWidth_; - } - - // Rarely (if the extent contains both the dateline and prime meridian) - // the shift can in turn break some triangles. - // Detect this here and don't shift in such cases. - const minX = Math.min( - newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]); - const maxX = Math.max( - newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]); - if ((maxX - minX) < this.sourceWorldWidth_ / 2) { - triangle.source = newTriangle; - } + /** + * @param {module:ol/coordinate~Coordinate} c A coordinate. + * @return {module:ol/coordinate~Coordinate} Transformed coordinate. + * @private + */ + this.transformInv_ = function(c) { + const key = c[0] + '/' + c[1]; + if (!transformInvCache[key]) { + transformInvCache[key] = transformInv(c); } - }.bind(this)); + return transformInvCache[key]; + }; + + /** + * @type {module:ol/extent~Extent} + * @private + */ + this.maxSourceExtent_ = maxSourceExtent; + + /** + * @type {number} + * @private + */ + this.errorThresholdSquared_ = errorThreshold * errorThreshold; + + /** + * @type {Array.} + * @private + */ + this.triangles_ = []; + + /** + * Indicates that the triangulation crosses edge of the source projection. + * @type {boolean} + * @private + */ + this.wrapsXInSource_ = false; + + /** + * @type {boolean} + * @private + */ + this.canWrapXInSource_ = this.sourceProj_.canWrapX() && + !!maxSourceExtent && + !!this.sourceProj_.getExtent() && + (getWidth(maxSourceExtent) == getWidth(this.sourceProj_.getExtent())); + + /** + * @type {?number} + * @private + */ + this.sourceWorldWidth_ = this.sourceProj_.getExtent() ? + getWidth(this.sourceProj_.getExtent()) : null; + + /** + * @type {?number} + * @private + */ + this.targetWorldWidth_ = this.targetProj_.getExtent() ? + getWidth(this.targetProj_.getExtent()) : null; + + const destinationTopLeft = getTopLeft(targetExtent); + const destinationTopRight = getTopRight(targetExtent); + const destinationBottomRight = getBottomRight(targetExtent); + const destinationBottomLeft = getBottomLeft(targetExtent); + const sourceTopLeft = this.transformInv_(destinationTopLeft); + const sourceTopRight = this.transformInv_(destinationTopRight); + const sourceBottomRight = this.transformInv_(destinationBottomRight); + const sourceBottomLeft = this.transformInv_(destinationBottomLeft); + + this.addQuad_( + destinationTopLeft, destinationTopRight, + destinationBottomRight, destinationBottomLeft, + sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft, + MAX_SUBDIVISION); + + if (this.wrapsXInSource_) { + let leftBound = Infinity; + this.triangles_.forEach(function(triangle, i, arr) { + leftBound = Math.min(leftBound, + triangle.source[0][0], triangle.source[1][0], triangle.source[2][0]); + }); + + // Shift triangles to be as close to `leftBound` as possible + // (if the distance is more than `worldWidth / 2` it can be closer. + this.triangles_.forEach(function(triangle) { + if (Math.max(triangle.source[0][0], triangle.source[1][0], + triangle.source[2][0]) - leftBound > this.sourceWorldWidth_ / 2) { + const newTriangle = [[triangle.source[0][0], triangle.source[0][1]], + [triangle.source[1][0], triangle.source[1][1]], + [triangle.source[2][0], triangle.source[2][1]]]; + if ((newTriangle[0][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[0][0] -= this.sourceWorldWidth_; + } + if ((newTriangle[1][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[1][0] -= this.sourceWorldWidth_; + } + if ((newTriangle[2][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[2][0] -= this.sourceWorldWidth_; + } + + // Rarely (if the extent contains both the dateline and prime meridian) + // the shift can in turn break some triangles. + // Detect this here and don't shift in such cases. + const minX = Math.min( + newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]); + const maxX = Math.max( + newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]); + if ((maxX - minX) < this.sourceWorldWidth_ / 2) { + triangle.source = newTriangle; + } + } + }.bind(this)); + } + + transformInvCache = {}; } - transformInvCache = {}; -}; - - -/** - * Adds triangle to the triangulation. - * @param {module:ol/coordinate~Coordinate} a The target a coordinate. - * @param {module:ol/coordinate~Coordinate} b The target b coordinate. - * @param {module:ol/coordinate~Coordinate} c The target c coordinate. - * @param {module:ol/coordinate~Coordinate} aSrc The source a coordinate. - * @param {module:ol/coordinate~Coordinate} bSrc The source b coordinate. - * @param {module:ol/coordinate~Coordinate} cSrc The source c coordinate. - * @private - */ -Triangulation.prototype.addTriangle_ = function(a, b, c, - aSrc, bSrc, cSrc) { - this.triangles_.push({ - source: [aSrc, bSrc, cSrc], - target: [a, b, c] - }); -}; - - -/** - * Adds quad (points in clock-wise order) to the triangulation - * (and reprojects the vertices) if valid. - * Performs quad subdivision if needed to increase precision. - * - * @param {module:ol/coordinate~Coordinate} a The target a coordinate. - * @param {module:ol/coordinate~Coordinate} b The target b coordinate. - * @param {module:ol/coordinate~Coordinate} c The target c coordinate. - * @param {module:ol/coordinate~Coordinate} d The target d coordinate. - * @param {module:ol/coordinate~Coordinate} aSrc The source a coordinate. - * @param {module:ol/coordinate~Coordinate} bSrc The source b coordinate. - * @param {module:ol/coordinate~Coordinate} cSrc The source c coordinate. - * @param {module:ol/coordinate~Coordinate} dSrc The source d coordinate. - * @param {number} maxSubdivision Maximal allowed subdivision of the quad. - * @private - */ -Triangulation.prototype.addQuad_ = function(a, b, c, d, - aSrc, bSrc, cSrc, dSrc, maxSubdivision) { - - const sourceQuadExtent = boundingExtent([aSrc, bSrc, cSrc, dSrc]); - const sourceCoverageX = this.sourceWorldWidth_ ? - getWidth(sourceQuadExtent) / this.sourceWorldWidth_ : null; - const sourceWorldWidth = /** @type {number} */ (this.sourceWorldWidth_); - - // when the quad is wrapped in the source projection - // it covers most of the projection extent, but not fully - const wrapsX = this.sourceProj_.canWrapX() && - sourceCoverageX > 0.5 && sourceCoverageX < 1; - - let needsSubdivision = false; - - if (maxSubdivision > 0) { - if (this.targetProj_.isGlobal() && this.targetWorldWidth_) { - const targetQuadExtent = boundingExtent([a, b, c, d]); - const targetCoverageX = getWidth(targetQuadExtent) / this.targetWorldWidth_; - needsSubdivision |= - targetCoverageX > MAX_TRIANGLE_WIDTH; - } - if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) { - needsSubdivision |= - sourceCoverageX > MAX_TRIANGLE_WIDTH; - } + /** + * Adds triangle to the triangulation. + * @param {module:ol/coordinate~Coordinate} a The target a coordinate. + * @param {module:ol/coordinate~Coordinate} b The target b coordinate. + * @param {module:ol/coordinate~Coordinate} c The target c coordinate. + * @param {module:ol/coordinate~Coordinate} aSrc The source a coordinate. + * @param {module:ol/coordinate~Coordinate} bSrc The source b coordinate. + * @param {module:ol/coordinate~Coordinate} cSrc The source c coordinate. + * @private + */ + addTriangle_(a, b, c, aSrc, bSrc, cSrc) { + this.triangles_.push({ + source: [aSrc, bSrc, cSrc], + target: [a, b, c] + }); } - if (!needsSubdivision && this.maxSourceExtent_) { - if (!intersects(sourceQuadExtent, this.maxSourceExtent_)) { - // whole quad outside source projection extent -> ignore - return; - } - } + /** + * Adds quad (points in clock-wise order) to the triangulation + * (and reprojects the vertices) if valid. + * Performs quad subdivision if needed to increase precision. + * + * @param {module:ol/coordinate~Coordinate} a The target a coordinate. + * @param {module:ol/coordinate~Coordinate} b The target b coordinate. + * @param {module:ol/coordinate~Coordinate} c The target c coordinate. + * @param {module:ol/coordinate~Coordinate} d The target d coordinate. + * @param {module:ol/coordinate~Coordinate} aSrc The source a coordinate. + * @param {module:ol/coordinate~Coordinate} bSrc The source b coordinate. + * @param {module:ol/coordinate~Coordinate} cSrc The source c coordinate. + * @param {module:ol/coordinate~Coordinate} dSrc The source d coordinate. + * @param {number} maxSubdivision Maximal allowed subdivision of the quad. + * @private + */ + addQuad_(a, b, c, d, aSrc, bSrc, cSrc, dSrc, maxSubdivision) { - if (!needsSubdivision) { - if (!isFinite(aSrc[0]) || !isFinite(aSrc[1]) || - !isFinite(bSrc[0]) || !isFinite(bSrc[1]) || - !isFinite(cSrc[0]) || !isFinite(cSrc[1]) || - !isFinite(dSrc[0]) || !isFinite(dSrc[1])) { - if (maxSubdivision > 0) { - needsSubdivision = true; - } else { + const sourceQuadExtent = boundingExtent([aSrc, bSrc, cSrc, dSrc]); + const sourceCoverageX = this.sourceWorldWidth_ ? + getWidth(sourceQuadExtent) / this.sourceWorldWidth_ : null; + const sourceWorldWidth = /** @type {number} */ (this.sourceWorldWidth_); + + // when the quad is wrapped in the source projection + // it covers most of the projection extent, but not fully + const wrapsX = this.sourceProj_.canWrapX() && + sourceCoverageX > 0.5 && sourceCoverageX < 1; + + let needsSubdivision = false; + + if (maxSubdivision > 0) { + if (this.targetProj_.isGlobal() && this.targetWorldWidth_) { + const targetQuadExtent = boundingExtent([a, b, c, d]); + const targetCoverageX = getWidth(targetQuadExtent) / this.targetWorldWidth_; + needsSubdivision |= + targetCoverageX > MAX_TRIANGLE_WIDTH; + } + if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) { + needsSubdivision |= + sourceCoverageX > MAX_TRIANGLE_WIDTH; + } + } + + if (!needsSubdivision && this.maxSourceExtent_) { + if (!intersects(sourceQuadExtent, this.maxSourceExtent_)) { + // whole quad outside source projection extent -> ignore return; } } - } - if (maxSubdivision > 0) { if (!needsSubdivision) { - const center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; - const centerSrc = this.transformInv_(center); - - let dx; - if (wrapsX) { - const centerSrcEstimX = - (modulo(aSrc[0], sourceWorldWidth) + - modulo(cSrc[0], sourceWorldWidth)) / 2; - dx = centerSrcEstimX - - modulo(centerSrc[0], sourceWorldWidth); - } else { - dx = (aSrc[0] + cSrc[0]) / 2 - centerSrc[0]; + if (!isFinite(aSrc[0]) || !isFinite(aSrc[1]) || + !isFinite(bSrc[0]) || !isFinite(bSrc[1]) || + !isFinite(cSrc[0]) || !isFinite(cSrc[1]) || + !isFinite(dSrc[0]) || !isFinite(dSrc[1])) { + if (maxSubdivision > 0) { + needsSubdivision = true; + } else { + return; + } } - const dy = (aSrc[1] + cSrc[1]) / 2 - centerSrc[1]; - const centerSrcErrorSquared = dx * dx + dy * dy; - needsSubdivision = centerSrcErrorSquared > this.errorThresholdSquared_; } - if (needsSubdivision) { - if (Math.abs(a[0] - c[0]) <= Math.abs(a[1] - c[1])) { - // split horizontally (top & bottom) - const bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2]; - const bcSrc = this.transformInv_(bc); - const da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2]; - const daSrc = this.transformInv_(da); - this.addQuad_( - a, b, bc, da, aSrc, bSrc, bcSrc, daSrc, maxSubdivision - 1); - this.addQuad_( - da, bc, c, d, daSrc, bcSrc, cSrc, dSrc, maxSubdivision - 1); - } else { - // split vertically (left & right) - const ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; - const abSrc = this.transformInv_(ab); - const cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2]; - const cdSrc = this.transformInv_(cd); + if (maxSubdivision > 0) { + if (!needsSubdivision) { + const center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; + const centerSrc = this.transformInv_(center); - this.addQuad_( - a, ab, cd, d, aSrc, abSrc, cdSrc, dSrc, maxSubdivision - 1); - this.addQuad_( - ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdivision - 1); + let dx; + if (wrapsX) { + const centerSrcEstimX = + (modulo(aSrc[0], sourceWorldWidth) + + modulo(cSrc[0], sourceWorldWidth)) / 2; + dx = centerSrcEstimX - + modulo(centerSrc[0], sourceWorldWidth); + } else { + dx = (aSrc[0] + cSrc[0]) / 2 - centerSrc[0]; + } + const dy = (aSrc[1] + cSrc[1]) / 2 - centerSrc[1]; + const centerSrcErrorSquared = dx * dx + dy * dy; + needsSubdivision = centerSrcErrorSquared > this.errorThresholdSquared_; + } + if (needsSubdivision) { + if (Math.abs(a[0] - c[0]) <= Math.abs(a[1] - c[1])) { + // split horizontally (top & bottom) + const bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2]; + const bcSrc = this.transformInv_(bc); + const da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2]; + const daSrc = this.transformInv_(da); + + this.addQuad_( + a, b, bc, da, aSrc, bSrc, bcSrc, daSrc, maxSubdivision - 1); + this.addQuad_( + da, bc, c, d, daSrc, bcSrc, cSrc, dSrc, maxSubdivision - 1); + } else { + // split vertically (left & right) + const ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; + const abSrc = this.transformInv_(ab); + const cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2]; + const cdSrc = this.transformInv_(cd); + + this.addQuad_( + a, ab, cd, d, aSrc, abSrc, cdSrc, dSrc, maxSubdivision - 1); + this.addQuad_( + ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdivision - 1); + } + return; } - return; } + + if (wrapsX) { + if (!this.canWrapXInSource_) { + return; + } + this.wrapsXInSource_ = true; + } + + this.addTriangle_(a, c, d, aSrc, cSrc, dSrc); + this.addTriangle_(a, b, c, aSrc, bSrc, cSrc); } - if (wrapsX) { - if (!this.canWrapXInSource_) { - return; - } - this.wrapsXInSource_ = true; + /** + * Calculates extent of the 'source' coordinates from all the triangles. + * + * @return {module:ol/extent~Extent} Calculated extent. + */ + calculateSourceExtent() { + const extent = createEmpty(); + + this.triangles_.forEach(function(triangle, i, arr) { + const src = triangle.source; + extendCoordinate(extent, src[0]); + extendCoordinate(extent, src[1]); + extendCoordinate(extent, src[2]); + }); + + return extent; } - this.addTriangle_(a, c, d, aSrc, cSrc, dSrc); - this.addTriangle_(a, b, c, aSrc, bSrc, cSrc); -}; + /** + * @return {Array.} Array of the calculated triangles. + */ + getTriangles() { + return this.triangles_; + } +} - -/** - * Calculates extent of the 'source' coordinates from all the triangles. - * - * @return {module:ol/extent~Extent} Calculated extent. - */ -Triangulation.prototype.calculateSourceExtent = function() { - const extent = createEmpty(); - - this.triangles_.forEach(function(triangle, i, arr) { - const src = triangle.source; - extendCoordinate(extent, src[0]); - extendCoordinate(extent, src[1]); - extendCoordinate(extent, src[2]); - }); - - return extent; -}; - - -/** - * @return {Array.} Array of the calculated triangles. - */ -Triangulation.prototype.getTriangles = function() { - return this.triangles_; -}; export default Triangulation; diff --git a/src/ol/source/BingMaps.js b/src/ol/source/BingMaps.js index 414d971ebc..928befb475 100644 --- a/src/ol/source/BingMaps.js +++ b/src/ol/source/BingMaps.js @@ -41,60 +41,175 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js'; * @param {module:ol/source/BingMaps~Options=} options Bing Maps options. * @api */ -const BingMaps = function(options) { +class BingMaps { + constructor(options) { + + /** + * @private + * @type {boolean} + */ + this.hidpi_ = options.hidpi !== undefined ? options.hidpi : false; + + TileImage.call(this, { + cacheSize: options.cacheSize, + crossOrigin: 'anonymous', + opaque: true, + projection: getProjection('EPSG:3857'), + reprojectionErrorThreshold: options.reprojectionErrorThreshold, + state: SourceState.LOADING, + tileLoadFunction: options.tileLoadFunction, + tilePixelRatio: this.hidpi_ ? 2 : 1, + wrapX: options.wrapX !== undefined ? options.wrapX : true, + transition: options.transition + }); + + /** + * @private + * @type {string} + */ + this.culture_ = options.culture !== undefined ? options.culture : 'en-us'; + + /** + * @private + * @type {number} + */ + this.maxZoom_ = options.maxZoom !== undefined ? options.maxZoom : -1; + + /** + * @private + * @type {string} + */ + this.apiKey_ = options.key; + + /** + * @private + * @type {string} + */ + this.imagerySet_ = options.imagerySet; + + const url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/' + + this.imagerySet_ + + '?uriScheme=https&include=ImageryProviders&key=' + this.apiKey_ + + '&c=' + this.culture_; + + requestJSONP(url, this.handleImageryMetadataResponse.bind(this), undefined, + 'jsonp'); + + } /** - * @private - * @type {boolean} + * Get the api key used for this source. + * + * @return {string} The api key. + * @api */ - this.hidpi_ = options.hidpi !== undefined ? options.hidpi : false; - - TileImage.call(this, { - cacheSize: options.cacheSize, - crossOrigin: 'anonymous', - opaque: true, - projection: getProjection('EPSG:3857'), - reprojectionErrorThreshold: options.reprojectionErrorThreshold, - state: SourceState.LOADING, - tileLoadFunction: options.tileLoadFunction, - tilePixelRatio: this.hidpi_ ? 2 : 1, - wrapX: options.wrapX !== undefined ? options.wrapX : true, - transition: options.transition - }); + getApiKey() { + return this.apiKey_; + } /** - * @private - * @type {string} + * Get the imagery set associated with this source. + * + * @return {string} The imagery set. + * @api */ - this.culture_ = options.culture !== undefined ? options.culture : 'en-us'; + getImagerySet() { + return this.imagerySet_; + } /** - * @private - * @type {number} + * @param {BingMapsImageryMetadataResponse} response Response. */ - this.maxZoom_ = options.maxZoom !== undefined ? options.maxZoom : -1; + handleImageryMetadataResponse(response) { + if (response.statusCode != 200 || + response.statusDescription != 'OK' || + response.authenticationResultCode != 'ValidCredentials' || + response.resourceSets.length != 1 || + response.resourceSets[0].resources.length != 1) { + this.setState(SourceState.ERROR); + return; + } - /** - * @private - * @type {string} - */ - this.apiKey_ = options.key; + const resource = response.resourceSets[0].resources[0]; + const maxZoom = this.maxZoom_ == -1 ? resource.zoomMax : this.maxZoom_; - /** - * @private - * @type {string} - */ - this.imagerySet_ = options.imagerySet; + const sourceProjection = this.getProjection(); + const extent = extentFromProjection(sourceProjection); + const tileSize = resource.imageWidth == resource.imageHeight ? + resource.imageWidth : [resource.imageWidth, resource.imageHeight]; + const tileGrid = createXYZ({ + extent: extent, + minZoom: resource.zoomMin, + maxZoom: maxZoom, + tileSize: tileSize / (this.hidpi_ ? 2 : 1) + }); + this.tileGrid = tileGrid; - const url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/' + - this.imagerySet_ + - '?uriScheme=https&include=ImageryProviders&key=' + this.apiKey_ + - '&c=' + this.culture_; + const culture = this.culture_; + const hidpi = this.hidpi_; + this.tileUrlFunction = createFromTileUrlFunctions( + resource.imageUrlSubdomains.map(function(subdomain) { + const quadKeyTileCoord = [0, 0, 0]; + const imageUrl = resource.imageUrl + .replace('{subdomain}', subdomain) + .replace('{culture}', culture); + return ( + /** + * @param {module:ol/tilecoord~TileCoord} tileCoord Tile coordinate. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @return {string|undefined} Tile URL. + */ + function(tileCoord, pixelRatio, projection) { + if (!tileCoord) { + return undefined; + } else { + createOrUpdate(tileCoord[0], tileCoord[1], -tileCoord[2] - 1, quadKeyTileCoord); + let url = imageUrl; + if (hidpi) { + url += '&dpi=d1&device=mobile'; + } + return url.replace('{quadkey}', quadKey(quadKeyTileCoord)); + } + } + ); + })); - requestJSONP(url, this.handleImageryMetadataResponse.bind(this), undefined, - 'jsonp'); + if (resource.imageryProviders) { + const transform = getTransformFromProjections( + getProjection('EPSG:4326'), this.getProjection()); -}; + this.setAttributions(function(frameState) { + const attributions = []; + const zoom = frameState.viewState.zoom; + resource.imageryProviders.map(function(imageryProvider) { + let intersecting = false; + const coverageAreas = imageryProvider.coverageAreas; + for (let i = 0, ii = coverageAreas.length; i < ii; ++i) { + const coverageArea = coverageAreas[i]; + if (zoom >= coverageArea.zoomMin && zoom <= coverageArea.zoomMax) { + const bbox = coverageArea.bbox; + const epsg4326Extent = [bbox[1], bbox[0], bbox[3], bbox[2]]; + const extent = applyTransform(epsg4326Extent, transform); + if (intersects(extent, frameState.extent)) { + intersecting = true; + break; + } + } + } + if (intersecting) { + attributions.push(imageryProvider.attribution); + } + }); + + attributions.push(TOS_ATTRIBUTION); + return attributions; + }); + } + + this.setState(SourceState.READY); + } +} inherits(BingMaps, TileImage); @@ -110,118 +225,4 @@ const TOS_ATTRIBUTION = ''; -/** - * Get the api key used for this source. - * - * @return {string} The api key. - * @api - */ -BingMaps.prototype.getApiKey = function() { - return this.apiKey_; -}; - - -/** - * Get the imagery set associated with this source. - * - * @return {string} The imagery set. - * @api - */ -BingMaps.prototype.getImagerySet = function() { - return this.imagerySet_; -}; - - -/** - * @param {BingMapsImageryMetadataResponse} response Response. - */ -BingMaps.prototype.handleImageryMetadataResponse = function(response) { - if (response.statusCode != 200 || - response.statusDescription != 'OK' || - response.authenticationResultCode != 'ValidCredentials' || - response.resourceSets.length != 1 || - response.resourceSets[0].resources.length != 1) { - this.setState(SourceState.ERROR); - return; - } - - const resource = response.resourceSets[0].resources[0]; - const maxZoom = this.maxZoom_ == -1 ? resource.zoomMax : this.maxZoom_; - - const sourceProjection = this.getProjection(); - const extent = extentFromProjection(sourceProjection); - const tileSize = resource.imageWidth == resource.imageHeight ? - resource.imageWidth : [resource.imageWidth, resource.imageHeight]; - const tileGrid = createXYZ({ - extent: extent, - minZoom: resource.zoomMin, - maxZoom: maxZoom, - tileSize: tileSize / (this.hidpi_ ? 2 : 1) - }); - this.tileGrid = tileGrid; - - const culture = this.culture_; - const hidpi = this.hidpi_; - this.tileUrlFunction = createFromTileUrlFunctions( - resource.imageUrlSubdomains.map(function(subdomain) { - const quadKeyTileCoord = [0, 0, 0]; - const imageUrl = resource.imageUrl - .replace('{subdomain}', subdomain) - .replace('{culture}', culture); - return ( - /** - * @param {module:ol/tilecoord~TileCoord} tileCoord Tile coordinate. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @return {string|undefined} Tile URL. - */ - function(tileCoord, pixelRatio, projection) { - if (!tileCoord) { - return undefined; - } else { - createOrUpdate(tileCoord[0], tileCoord[1], -tileCoord[2] - 1, quadKeyTileCoord); - let url = imageUrl; - if (hidpi) { - url += '&dpi=d1&device=mobile'; - } - return url.replace('{quadkey}', quadKey(quadKeyTileCoord)); - } - } - ); - })); - - if (resource.imageryProviders) { - const transform = getTransformFromProjections( - getProjection('EPSG:4326'), this.getProjection()); - - this.setAttributions(function(frameState) { - const attributions = []; - const zoom = frameState.viewState.zoom; - resource.imageryProviders.map(function(imageryProvider) { - let intersecting = false; - const coverageAreas = imageryProvider.coverageAreas; - for (let i = 0, ii = coverageAreas.length; i < ii; ++i) { - const coverageArea = coverageAreas[i]; - if (zoom >= coverageArea.zoomMin && zoom <= coverageArea.zoomMax) { - const bbox = coverageArea.bbox; - const epsg4326Extent = [bbox[1], bbox[0], bbox[3], bbox[2]]; - const extent = applyTransform(epsg4326Extent, transform); - if (intersects(extent, frameState.extent)) { - intersecting = true; - break; - } - } - } - if (intersecting) { - attributions.push(imageryProvider.attribution); - } - }); - - attributions.push(TOS_ATTRIBUTION); - return attributions; - }); - } - - this.setState(SourceState.READY); -}; export default BingMaps; diff --git a/src/ol/source/CartoDB.js b/src/ol/source/CartoDB.js index a23cb38a55..5b8af63d6a 100644 --- a/src/ol/source/CartoDB.js +++ b/src/ol/source/CartoDB.js @@ -40,152 +40,149 @@ import XYZ from '../source/XYZ.js'; * @param {module:ol/source/CartoDB~Options=} options CartoDB options. * @api */ -const CartoDB = function(options) { +class CartoDB { + constructor(options) { + + /** + * @type {string} + * @private + */ + this.account_ = options.account; + + /** + * @type {string} + * @private + */ + this.mapId_ = options.map || ''; + + /** + * @type {!Object} + * @private + */ + this.config_ = options.config || {}; + + /** + * @type {!Object.} + * @private + */ + this.templateCache_ = {}; + + XYZ.call(this, { + attributions: options.attributions, + cacheSize: options.cacheSize, + crossOrigin: options.crossOrigin, + maxZoom: options.maxZoom !== undefined ? options.maxZoom : 18, + minZoom: options.minZoom, + projection: options.projection, + state: SourceState.LOADING, + wrapX: options.wrapX + }); + this.initializeMap_(); + } /** - * @type {string} - * @private + * Returns the current config. + * @return {!Object} The current configuration. + * @api */ - this.account_ = options.account; + getConfig() { + return this.config_; + } /** - * @type {string} - * @private + * Updates the carto db config. + * @param {Object} config a key-value lookup. Values will replace current values + * in the config. + * @api */ - this.mapId_ = options.map || ''; + updateConfig(config) { + assign(this.config_, config); + this.initializeMap_(); + } /** - * @type {!Object} - * @private + * Sets the CartoDB config + * @param {Object} config In the case of anonymous maps, a CartoDB configuration + * object. + * If using named maps, a key-value lookup with the template parameters. + * @api */ - this.config_ = options.config || {}; + setConfig(config) { + this.config_ = config || {}; + this.initializeMap_(); + } /** - * @type {!Object.} + * Issue a request to initialize the CartoDB map. * @private */ - this.templateCache_ = {}; + initializeMap_() { + const paramHash = JSON.stringify(this.config_); + if (this.templateCache_[paramHash]) { + this.applyTemplate_(this.templateCache_[paramHash]); + return; + } + let mapUrl = 'https://' + this.account_ + '.carto.com/api/v1/map'; - XYZ.call(this, { - attributions: options.attributions, - cacheSize: options.cacheSize, - crossOrigin: options.crossOrigin, - maxZoom: options.maxZoom !== undefined ? options.maxZoom : 18, - minZoom: options.minZoom, - projection: options.projection, - state: SourceState.LOADING, - wrapX: options.wrapX - }); - this.initializeMap_(); -}; + if (this.mapId_) { + mapUrl += '/named/' + this.mapId_; + } + + const client = new XMLHttpRequest(); + client.addEventListener('load', this.handleInitResponse_.bind(this, paramHash)); + client.addEventListener('error', this.handleInitError_.bind(this)); + client.open('POST', mapUrl); + client.setRequestHeader('Content-type', 'application/json'); + client.send(JSON.stringify(this.config_)); + } + + /** + * Handle map initialization response. + * @param {string} paramHash a hash representing the parameter set that was used + * for the request + * @param {Event} event Event. + * @private + */ + handleInitResponse_(paramHash, event) { + const client = /** @type {XMLHttpRequest} */ (event.target); + // status will be 0 for file:// urls + if (!client.status || client.status >= 200 && client.status < 300) { + let response; + try { + response = /** @type {CartoDBLayerInfo} */(JSON.parse(client.responseText)); + } catch (err) { + this.setState(SourceState.ERROR); + return; + } + this.applyTemplate_(response); + this.templateCache_[paramHash] = response; + this.setState(SourceState.READY); + } else { + this.setState(SourceState.ERROR); + } + } + + /** + * @private + * @param {Event} event Event. + */ + handleInitError_(event) { + this.setState(SourceState.ERROR); + } + + /** + * Apply the new tile urls returned by carto db + * @param {CartoDBLayerInfo} data Result of carto db call. + * @private + */ + applyTemplate_(data) { + const tilesUrl = 'https://' + data.cdn_url.https + '/' + this.account_ + + '/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'; + this.setUrl(tilesUrl); + } +} inherits(CartoDB, XYZ); -/** - * Returns the current config. - * @return {!Object} The current configuration. - * @api - */ -CartoDB.prototype.getConfig = function() { - return this.config_; -}; - - -/** - * Updates the carto db config. - * @param {Object} config a key-value lookup. Values will replace current values - * in the config. - * @api - */ -CartoDB.prototype.updateConfig = function(config) { - assign(this.config_, config); - this.initializeMap_(); -}; - - -/** - * Sets the CartoDB config - * @param {Object} config In the case of anonymous maps, a CartoDB configuration - * object. - * If using named maps, a key-value lookup with the template parameters. - * @api - */ -CartoDB.prototype.setConfig = function(config) { - this.config_ = config || {}; - this.initializeMap_(); -}; - - -/** - * Issue a request to initialize the CartoDB map. - * @private - */ -CartoDB.prototype.initializeMap_ = function() { - const paramHash = JSON.stringify(this.config_); - if (this.templateCache_[paramHash]) { - this.applyTemplate_(this.templateCache_[paramHash]); - return; - } - let mapUrl = 'https://' + this.account_ + '.carto.com/api/v1/map'; - - if (this.mapId_) { - mapUrl += '/named/' + this.mapId_; - } - - const client = new XMLHttpRequest(); - client.addEventListener('load', this.handleInitResponse_.bind(this, paramHash)); - client.addEventListener('error', this.handleInitError_.bind(this)); - client.open('POST', mapUrl); - client.setRequestHeader('Content-type', 'application/json'); - client.send(JSON.stringify(this.config_)); -}; - - -/** - * Handle map initialization response. - * @param {string} paramHash a hash representing the parameter set that was used - * for the request - * @param {Event} event Event. - * @private - */ -CartoDB.prototype.handleInitResponse_ = function(paramHash, event) { - const client = /** @type {XMLHttpRequest} */ (event.target); - // status will be 0 for file:// urls - if (!client.status || client.status >= 200 && client.status < 300) { - let response; - try { - response = /** @type {CartoDBLayerInfo} */(JSON.parse(client.responseText)); - } catch (err) { - this.setState(SourceState.ERROR); - return; - } - this.applyTemplate_(response); - this.templateCache_[paramHash] = response; - this.setState(SourceState.READY); - } else { - this.setState(SourceState.ERROR); - } -}; - - -/** - * @private - * @param {Event} event Event. - */ -CartoDB.prototype.handleInitError_ = function(event) { - this.setState(SourceState.ERROR); -}; - - -/** - * Apply the new tile urls returned by carto db - * @param {CartoDBLayerInfo} data Result of carto db call. - * @private - */ -CartoDB.prototype.applyTemplate_ = function(data) { - const tilesUrl = 'https://' + data.cdn_url.https + '/' + this.account_ + - '/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'; - this.setUrl(tilesUrl); -}; export default CartoDB; diff --git a/src/ol/source/Cluster.js b/src/ol/source/Cluster.js index 0dd242e719..5df37479c2 100644 --- a/src/ol/source/Cluster.js +++ b/src/ol/source/Cluster.js @@ -47,175 +47,172 @@ import VectorSource from '../source/Vector.js'; * @extends {module:ol/source/Vector} * @api */ -const Cluster = function(options) { - VectorSource.call(this, { - attributions: options.attributions, - extent: options.extent, - projection: options.projection, - wrapX: options.wrapX - }); +class Cluster { + constructor(options) { + VectorSource.call(this, { + attributions: options.attributions, + extent: options.extent, + projection: options.projection, + wrapX: options.wrapX + }); + + /** + * @type {number|undefined} + * @protected + */ + this.resolution = undefined; + + /** + * @type {number} + * @protected + */ + this.distance = options.distance !== undefined ? options.distance : 20; + + /** + * @type {Array.} + * @protected + */ + this.features = []; + + /** + * @param {module:ol/Feature} feature Feature. + * @return {module:ol/geom/Point} Cluster calculation point. + * @protected + */ + this.geometryFunction = options.geometryFunction || function(feature) { + const geometry = /** @type {module:ol/geom/Point} */ (feature.getGeometry()); + assert(geometry instanceof Point, + 10); // The default `geometryFunction` can only handle `module:ol/geom/Point~Point` geometries + return geometry; + }; + + /** + * @type {module:ol/source/Vector} + * @protected + */ + this.source = options.source; + + listen(this.source, EventType.CHANGE, this.refresh, this); + } /** - * @type {number|undefined} - * @protected + * Get the distance in pixels between clusters. + * @return {number} Distance. + * @api */ - this.resolution = undefined; + getDistance() { + return this.distance; + } /** - * @type {number} - * @protected + * Get a reference to the wrapped source. + * @return {module:ol/source/Vector} Source. + * @api */ - this.distance = options.distance !== undefined ? options.distance : 20; + getSource() { + return this.source; + } /** - * @type {Array.} - * @protected + * @inheritDoc */ - this.features = []; + loadFeatures(extent, resolution, projection) { + this.source.loadFeatures(extent, resolution, projection); + if (resolution !== this.resolution) { + this.clear(); + this.resolution = resolution; + this.cluster(); + this.addFeatures(this.features); + } + } /** - * @param {module:ol/Feature} feature Feature. - * @return {module:ol/geom/Point} Cluster calculation point. - * @protected + * Set the distance in pixels between clusters. + * @param {number} distance The distance in pixels. + * @api */ - this.geometryFunction = options.geometryFunction || function(feature) { - const geometry = /** @type {module:ol/geom/Point} */ (feature.getGeometry()); - assert(geometry instanceof Point, - 10); // The default `geometryFunction` can only handle `module:ol/geom/Point~Point` geometries - return geometry; - }; + setDistance(distance) { + this.distance = distance; + this.refresh(); + } + + /** + * handle the source changing + * @override + */ + refresh() { + this.clear(); + this.cluster(); + this.addFeatures(this.features); + VectorSource.prototype.refresh.call(this); + } /** - * @type {module:ol/source/Vector} * @protected */ - this.source = options.source; + cluster() { + if (this.resolution === undefined) { + return; + } + this.features.length = 0; + const extent = createEmpty(); + const mapDistance = this.distance * this.resolution; + const features = this.source.getFeatures(); - listen(this.source, EventType.CHANGE, this.refresh, this); -}; + /** + * @type {!Object.} + */ + const clustered = {}; + + for (let i = 0, ii = features.length; i < ii; i++) { + const feature = features[i]; + if (!(getUid(feature).toString() in clustered)) { + const geometry = this.geometryFunction(feature); + if (geometry) { + const coordinates = geometry.getCoordinates(); + createOrUpdateFromCoordinate(coordinates, extent); + buffer(extent, mapDistance, extent); + + let neighbors = this.source.getFeaturesInExtent(extent); + neighbors = neighbors.filter(function(neighbor) { + const uid = getUid(neighbor).toString(); + if (!(uid in clustered)) { + clustered[uid] = true; + return true; + } else { + return false; + } + }); + this.features.push(this.createCluster(neighbors)); + } + } + } + } + + /** + * @param {Array.} features Features + * @return {module:ol/Feature} The cluster feature. + * @protected + */ + createCluster(features) { + const centroid = [0, 0]; + for (let i = features.length - 1; i >= 0; --i) { + const geometry = this.geometryFunction(features[i]); + if (geometry) { + addCoordinate(centroid, geometry.getCoordinates()); + } else { + features.splice(i, 1); + } + } + scaleCoordinate(centroid, 1 / features.length); + + const cluster = new Feature(new Point(centroid)); + cluster.set('features', features); + return cluster; + } +} inherits(Cluster, VectorSource); -/** - * Get the distance in pixels between clusters. - * @return {number} Distance. - * @api - */ -Cluster.prototype.getDistance = function() { - return this.distance; -}; - - -/** - * Get a reference to the wrapped source. - * @return {module:ol/source/Vector} Source. - * @api - */ -Cluster.prototype.getSource = function() { - return this.source; -}; - - -/** - * @inheritDoc - */ -Cluster.prototype.loadFeatures = function(extent, resolution, projection) { - this.source.loadFeatures(extent, resolution, projection); - if (resolution !== this.resolution) { - this.clear(); - this.resolution = resolution; - this.cluster(); - this.addFeatures(this.features); - } -}; - - -/** - * Set the distance in pixels between clusters. - * @param {number} distance The distance in pixels. - * @api - */ -Cluster.prototype.setDistance = function(distance) { - this.distance = distance; - this.refresh(); -}; - - -/** - * handle the source changing - * @override - */ -Cluster.prototype.refresh = function() { - this.clear(); - this.cluster(); - this.addFeatures(this.features); - VectorSource.prototype.refresh.call(this); -}; - - -/** - * @protected - */ -Cluster.prototype.cluster = function() { - if (this.resolution === undefined) { - return; - } - this.features.length = 0; - const extent = createEmpty(); - const mapDistance = this.distance * this.resolution; - const features = this.source.getFeatures(); - - /** - * @type {!Object.} - */ - const clustered = {}; - - for (let i = 0, ii = features.length; i < ii; i++) { - const feature = features[i]; - if (!(getUid(feature).toString() in clustered)) { - const geometry = this.geometryFunction(feature); - if (geometry) { - const coordinates = geometry.getCoordinates(); - createOrUpdateFromCoordinate(coordinates, extent); - buffer(extent, mapDistance, extent); - - let neighbors = this.source.getFeaturesInExtent(extent); - neighbors = neighbors.filter(function(neighbor) { - const uid = getUid(neighbor).toString(); - if (!(uid in clustered)) { - clustered[uid] = true; - return true; - } else { - return false; - } - }); - this.features.push(this.createCluster(neighbors)); - } - } - } -}; - - -/** - * @param {Array.} features Features - * @return {module:ol/Feature} The cluster feature. - * @protected - */ -Cluster.prototype.createCluster = function(features) { - const centroid = [0, 0]; - for (let i = features.length - 1; i >= 0; --i) { - const geometry = this.geometryFunction(features[i]); - if (geometry) { - addCoordinate(centroid, geometry.getCoordinates()); - } else { - features.splice(i, 1); - } - } - scaleCoordinate(centroid, 1 / features.length); - - const cluster = new Feature(new Point(centroid)); - cluster.set('features', features); - return cluster; -}; export default Cluster; diff --git a/src/ol/source/Image.js b/src/ol/source/Image.js index 75a63b7136..d5bcea2eb7 100644 --- a/src/ol/source/Image.js +++ b/src/ol/source/Image.js @@ -88,146 +88,143 @@ inherits(ImageSourceEvent, Event); * @param {module:ol/source/Image~Options} options Single image source options. * @api */ -const ImageSource = function(options) { - Source.call(this, { - attributions: options.attributions, - extent: options.extent, - projection: options.projection, - state: options.state - }); +class ImageSource { + constructor(options) { + Source.call(this, { + attributions: options.attributions, + extent: options.extent, + projection: options.projection, + state: options.state + }); + + /** + * @private + * @type {Array.} + */ + this.resolutions_ = options.resolutions !== undefined ? + options.resolutions : null; + + + /** + * @private + * @type {module:ol/reproj/Image} + */ + this.reprojectedImage_ = null; + + + /** + * @private + * @type {number} + */ + this.reprojectedRevision_ = 0; + } /** - * @private - * @type {Array.} + * @return {Array.} Resolutions. + * @override */ - this.resolutions_ = options.resolutions !== undefined ? - options.resolutions : null; - + getResolutions() { + return this.resolutions_; + } /** - * @private - * @type {module:ol/reproj/Image} + * @protected + * @param {number} resolution Resolution. + * @return {number} Resolution. */ - this.reprojectedImage_ = null; - + findNearestResolution(resolution) { + if (this.resolutions_) { + const idx = linearFindNearest(this.resolutions_, resolution, 0); + resolution = this.resolutions_[idx]; + } + return resolution; + } /** - * @private - * @type {number} + * @param {module:ol/extent~Extent} extent Extent. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @return {module:ol/ImageBase} Single image. */ - this.reprojectedRevision_ = 0; -}; + getImage(extent, resolution, pixelRatio, projection) { + const sourceProjection = this.getProjection(); + if (!ENABLE_RASTER_REPROJECTION || + !sourceProjection || + !projection || + equivalent(sourceProjection, projection)) { + if (sourceProjection) { + projection = sourceProjection; + } + return this.getImageInternal(extent, resolution, pixelRatio, projection); + } else { + if (this.reprojectedImage_) { + if (this.reprojectedRevision_ == this.getRevision() && + equivalent( + this.reprojectedImage_.getProjection(), projection) && + this.reprojectedImage_.getResolution() == resolution && + equals(this.reprojectedImage_.getExtent(), extent)) { + return this.reprojectedImage_; + } + this.reprojectedImage_.dispose(); + this.reprojectedImage_ = null; + } + + this.reprojectedImage_ = new ReprojImage( + sourceProjection, projection, extent, resolution, pixelRatio, + function(extent, resolution, pixelRatio) { + return this.getImageInternal(extent, resolution, + pixelRatio, sourceProjection); + }.bind(this)); + this.reprojectedRevision_ = this.getRevision(); + + return this.reprojectedImage_; + } + } + + /** + * @abstract + * @param {module:ol/extent~Extent} extent Extent. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @return {module:ol/ImageBase} Single image. + * @protected + */ + getImageInternal(extent, resolution, pixelRatio, projection) {} + + /** + * Handle image change events. + * @param {module:ol/events/Event} event Event. + * @protected + */ + handleImageChange(event) { + const image = /** @type {module:ol/Image} */ (event.target); + switch (image.getState()) { + case ImageState.LOADING: + this.dispatchEvent( + new ImageSourceEvent(ImageSourceEventType.IMAGELOADSTART, + image)); + break; + case ImageState.LOADED: + this.dispatchEvent( + new ImageSourceEvent(ImageSourceEventType.IMAGELOADEND, + image)); + break; + case ImageState.ERROR: + this.dispatchEvent( + new ImageSourceEvent(ImageSourceEventType.IMAGELOADERROR, + image)); + break; + default: + // pass + } + } +} inherits(ImageSource, Source); -/** - * @return {Array.} Resolutions. - * @override - */ -ImageSource.prototype.getResolutions = function() { - return this.resolutions_; -}; - - -/** - * @protected - * @param {number} resolution Resolution. - * @return {number} Resolution. - */ -ImageSource.prototype.findNearestResolution = function(resolution) { - if (this.resolutions_) { - const idx = linearFindNearest(this.resolutions_, resolution, 0); - resolution = this.resolutions_[idx]; - } - return resolution; -}; - - -/** - * @param {module:ol/extent~Extent} extent Extent. - * @param {number} resolution Resolution. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @return {module:ol/ImageBase} Single image. - */ -ImageSource.prototype.getImage = function(extent, resolution, pixelRatio, projection) { - const sourceProjection = this.getProjection(); - if (!ENABLE_RASTER_REPROJECTION || - !sourceProjection || - !projection || - equivalent(sourceProjection, projection)) { - if (sourceProjection) { - projection = sourceProjection; - } - return this.getImageInternal(extent, resolution, pixelRatio, projection); - } else { - if (this.reprojectedImage_) { - if (this.reprojectedRevision_ == this.getRevision() && - equivalent( - this.reprojectedImage_.getProjection(), projection) && - this.reprojectedImage_.getResolution() == resolution && - equals(this.reprojectedImage_.getExtent(), extent)) { - return this.reprojectedImage_; - } - this.reprojectedImage_.dispose(); - this.reprojectedImage_ = null; - } - - this.reprojectedImage_ = new ReprojImage( - sourceProjection, projection, extent, resolution, pixelRatio, - function(extent, resolution, pixelRatio) { - return this.getImageInternal(extent, resolution, - pixelRatio, sourceProjection); - }.bind(this)); - this.reprojectedRevision_ = this.getRevision(); - - return this.reprojectedImage_; - } -}; - - -/** - * @abstract - * @param {module:ol/extent~Extent} extent Extent. - * @param {number} resolution Resolution. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @return {module:ol/ImageBase} Single image. - * @protected - */ -ImageSource.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {}; - - -/** - * Handle image change events. - * @param {module:ol/events/Event} event Event. - * @protected - */ -ImageSource.prototype.handleImageChange = function(event) { - const image = /** @type {module:ol/Image} */ (event.target); - switch (image.getState()) { - case ImageState.LOADING: - this.dispatchEvent( - new ImageSourceEvent(ImageSourceEventType.IMAGELOADSTART, - image)); - break; - case ImageState.LOADED: - this.dispatchEvent( - new ImageSourceEvent(ImageSourceEventType.IMAGELOADEND, - image)); - break; - case ImageState.ERROR: - this.dispatchEvent( - new ImageSourceEvent(ImageSourceEventType.IMAGELOADERROR, - image)); - break; - default: - // pass - } -}; - - /** * Default image load function for image sources that use module:ol/Image~Image image * instances. diff --git a/src/ol/source/ImageArcGISRest.js b/src/ol/source/ImageArcGISRest.js index 6c5af41d6c..78d5a42ac5 100644 --- a/src/ol/source/ImageArcGISRest.js +++ b/src/ol/source/ImageArcGISRest.js @@ -54,246 +54,242 @@ import {appendParams} from '../uri.js'; * @param {module:ol/source/ImageArcGISRest~Options=} opt_options Image ArcGIS Rest Options. * @api */ -const ImageArcGISRest = function(opt_options) { +class ImageArcGISRest { + constructor(opt_options) { - const options = opt_options || {}; + const options = opt_options || {}; - ImageSource.call(this, { - attributions: options.attributions, - projection: options.projection, - resolutions: options.resolutions - }); + ImageSource.call(this, { + attributions: options.attributions, + projection: options.projection, + resolutions: options.resolutions + }); + + /** + * @private + * @type {?string} + */ + this.crossOrigin_ = + options.crossOrigin !== undefined ? options.crossOrigin : null; + + /** + * @private + * @type {boolean} + */ + this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true; + + /** + * @private + * @type {string|undefined} + */ + this.url_ = options.url; + + /** + * @private + * @type {module:ol/Image~LoadFunction} + */ + this.imageLoadFunction_ = options.imageLoadFunction !== undefined ? + options.imageLoadFunction : defaultImageLoadFunction; + + + /** + * @private + * @type {!Object} + */ + this.params_ = options.params || {}; + + /** + * @private + * @type {module:ol/Image} + */ + this.image_ = null; + + /** + * @private + * @type {module:ol/size~Size} + */ + this.imageSize_ = [0, 0]; + + + /** + * @private + * @type {number} + */ + this.renderedRevision_ = 0; + + /** + * @private + * @type {number} + */ + this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5; + + } /** - * @private - * @type {?string} + * Get the user-provided params, i.e. those passed to the constructor through + * the "params" option, and possibly updated using the updateParams method. + * @return {Object} Params. + * @api */ - this.crossOrigin_ = - options.crossOrigin !== undefined ? options.crossOrigin : null; + getParams() { + return this.params_; + } /** - * @private - * @type {boolean} + * @inheritDoc */ - this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true; + getImageInternal(extent, resolution, pixelRatio, projection) { + + if (this.url_ === undefined) { + return null; + } + + resolution = this.findNearestResolution(resolution); + pixelRatio = this.hidpi_ ? pixelRatio : 1; + + const image = this.image_; + if (image && + this.renderedRevision_ == this.getRevision() && + image.getResolution() == resolution && + image.getPixelRatio() == pixelRatio && + containsExtent(image.getExtent(), extent)) { + return image; + } + + const params = { + 'F': 'image', + 'FORMAT': 'PNG32', + 'TRANSPARENT': true + }; + assign(params, this.params_); + + extent = extent.slice(); + const centerX = (extent[0] + extent[2]) / 2; + const centerY = (extent[1] + extent[3]) / 2; + if (this.ratio_ != 1) { + const halfWidth = this.ratio_ * getWidth(extent) / 2; + const halfHeight = this.ratio_ * getHeight(extent) / 2; + extent[0] = centerX - halfWidth; + extent[1] = centerY - halfHeight; + extent[2] = centerX + halfWidth; + extent[3] = centerY + halfHeight; + } + + const imageResolution = resolution / pixelRatio; + + // Compute an integer width and height. + const width = Math.ceil(getWidth(extent) / imageResolution); + const height = Math.ceil(getHeight(extent) / imageResolution); + + // Modify the extent to match the integer width and height. + extent[0] = centerX - imageResolution * width / 2; + extent[2] = centerX + imageResolution * width / 2; + extent[1] = centerY - imageResolution * height / 2; + extent[3] = centerY + imageResolution * height / 2; + + this.imageSize_[0] = width; + this.imageSize_[1] = height; + + const url = this.getRequestUrl_(extent, this.imageSize_, pixelRatio, + projection, params); + + this.image_ = new ImageWrapper(extent, resolution, pixelRatio, + url, this.crossOrigin_, this.imageLoadFunction_); + + this.renderedRevision_ = this.getRevision(); + + listen(this.image_, EventType.CHANGE, + this.handleImageChange, this); + + return this.image_; + + } /** - * @private - * @type {string|undefined} + * Return the image load function of the source. + * @return {module:ol/Image~LoadFunction} The image load function. + * @api */ - this.url_ = options.url; + getImageLoadFunction() { + return this.imageLoadFunction_; + } /** + * @param {module:ol/extent~Extent} extent Extent. + * @param {module:ol/size~Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @param {Object} params Params. + * @return {string} Request URL. * @private - * @type {module:ol/Image~LoadFunction} */ - this.imageLoadFunction_ = options.imageLoadFunction !== undefined ? - options.imageLoadFunction : defaultImageLoadFunction; + getRequestUrl_(extent, size, pixelRatio, projection, params) { + // ArcGIS Server only wants the numeric portion of the projection ID. + const srid = projection.getCode().split(':').pop(); + params['SIZE'] = size[0] + ',' + size[1]; + params['BBOX'] = extent.join(','); + params['BBOXSR'] = srid; + params['IMAGESR'] = srid; + params['DPI'] = Math.round(90 * pixelRatio); + + const url = this.url_; + + const modifiedUrl = url + .replace(/MapServer\/?$/, 'MapServer/export') + .replace(/ImageServer\/?$/, 'ImageServer/exportImage'); + if (modifiedUrl == url) { + assert(false, 50); // `options.featureTypes` should be an Array + } + return appendParams(modifiedUrl, params); + } /** - * @private - * @type {!Object} + * Return the URL used for this ArcGIS source. + * @return {string|undefined} URL. + * @api */ - this.params_ = options.params || {}; + getUrl() { + return this.url_; + } /** - * @private - * @type {module:ol/Image} + * Set the image load function of the source. + * @param {module:ol/Image~LoadFunction} imageLoadFunction Image load function. + * @api */ - this.image_ = null; + setImageLoadFunction(imageLoadFunction) { + this.image_ = null; + this.imageLoadFunction_ = imageLoadFunction; + this.changed(); + } /** - * @private - * @type {module:ol/size~Size} + * Set the URL to use for requests. + * @param {string|undefined} url URL. + * @api */ - this.imageSize_ = [0, 0]; - + setUrl(url) { + if (url != this.url_) { + this.url_ = url; + this.image_ = null; + this.changed(); + } + } /** - * @private - * @type {number} + * Update the user-provided params. + * @param {Object} params Params. + * @api */ - this.renderedRevision_ = 0; - - /** - * @private - * @type {number} - */ - this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5; - -}; + updateParams(params) { + assign(this.params_, params); + this.image_ = null; + this.changed(); + } +} inherits(ImageArcGISRest, ImageSource); -/** - * Get the user-provided params, i.e. those passed to the constructor through - * the "params" option, and possibly updated using the updateParams method. - * @return {Object} Params. - * @api - */ -ImageArcGISRest.prototype.getParams = function() { - return this.params_; -}; - - -/** - * @inheritDoc - */ -ImageArcGISRest.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { - - if (this.url_ === undefined) { - return null; - } - - resolution = this.findNearestResolution(resolution); - pixelRatio = this.hidpi_ ? pixelRatio : 1; - - const image = this.image_; - if (image && - this.renderedRevision_ == this.getRevision() && - image.getResolution() == resolution && - image.getPixelRatio() == pixelRatio && - containsExtent(image.getExtent(), extent)) { - return image; - } - - const params = { - 'F': 'image', - 'FORMAT': 'PNG32', - 'TRANSPARENT': true - }; - assign(params, this.params_); - - extent = extent.slice(); - const centerX = (extent[0] + extent[2]) / 2; - const centerY = (extent[1] + extent[3]) / 2; - if (this.ratio_ != 1) { - const halfWidth = this.ratio_ * getWidth(extent) / 2; - const halfHeight = this.ratio_ * getHeight(extent) / 2; - extent[0] = centerX - halfWidth; - extent[1] = centerY - halfHeight; - extent[2] = centerX + halfWidth; - extent[3] = centerY + halfHeight; - } - - const imageResolution = resolution / pixelRatio; - - // Compute an integer width and height. - const width = Math.ceil(getWidth(extent) / imageResolution); - const height = Math.ceil(getHeight(extent) / imageResolution); - - // Modify the extent to match the integer width and height. - extent[0] = centerX - imageResolution * width / 2; - extent[2] = centerX + imageResolution * width / 2; - extent[1] = centerY - imageResolution * height / 2; - extent[3] = centerY + imageResolution * height / 2; - - this.imageSize_[0] = width; - this.imageSize_[1] = height; - - const url = this.getRequestUrl_(extent, this.imageSize_, pixelRatio, - projection, params); - - this.image_ = new ImageWrapper(extent, resolution, pixelRatio, - url, this.crossOrigin_, this.imageLoadFunction_); - - this.renderedRevision_ = this.getRevision(); - - listen(this.image_, EventType.CHANGE, - this.handleImageChange, this); - - return this.image_; - -}; - - -/** - * Return the image load function of the source. - * @return {module:ol/Image~LoadFunction} The image load function. - * @api - */ -ImageArcGISRest.prototype.getImageLoadFunction = function() { - return this.imageLoadFunction_; -}; - - -/** - * @param {module:ol/extent~Extent} extent Extent. - * @param {module:ol/size~Size} size Size. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @param {Object} params Params. - * @return {string} Request URL. - * @private - */ -ImageArcGISRest.prototype.getRequestUrl_ = function(extent, size, pixelRatio, projection, params) { - // ArcGIS Server only wants the numeric portion of the projection ID. - const srid = projection.getCode().split(':').pop(); - - params['SIZE'] = size[0] + ',' + size[1]; - params['BBOX'] = extent.join(','); - params['BBOXSR'] = srid; - params['IMAGESR'] = srid; - params['DPI'] = Math.round(90 * pixelRatio); - - const url = this.url_; - - const modifiedUrl = url - .replace(/MapServer\/?$/, 'MapServer/export') - .replace(/ImageServer\/?$/, 'ImageServer/exportImage'); - if (modifiedUrl == url) { - assert(false, 50); // `options.featureTypes` should be an Array - } - return appendParams(modifiedUrl, params); -}; - - -/** - * Return the URL used for this ArcGIS source. - * @return {string|undefined} URL. - * @api - */ -ImageArcGISRest.prototype.getUrl = function() { - return this.url_; -}; - - -/** - * Set the image load function of the source. - * @param {module:ol/Image~LoadFunction} imageLoadFunction Image load function. - * @api - */ -ImageArcGISRest.prototype.setImageLoadFunction = function(imageLoadFunction) { - this.image_ = null; - this.imageLoadFunction_ = imageLoadFunction; - this.changed(); -}; - - -/** - * Set the URL to use for requests. - * @param {string|undefined} url URL. - * @api - */ -ImageArcGISRest.prototype.setUrl = function(url) { - if (url != this.url_) { - this.url_ = url; - this.image_ = null; - this.changed(); - } -}; - - -/** - * Update the user-provided params. - * @param {Object} params Params. - * @api - */ -ImageArcGISRest.prototype.updateParams = function(params) { - assign(this.params_, params); - this.image_ = null; - this.changed(); -}; export default ImageArcGISRest; diff --git a/src/ol/source/ImageCanvas.js b/src/ol/source/ImageCanvas.js index dcf48da40e..4080b6fe06 100644 --- a/src/ol/source/ImageCanvas.js +++ b/src/ol/source/ImageCanvas.js @@ -51,74 +51,77 @@ import ImageSource from '../source/Image.js'; * @param {module:ol/source/ImageCanvas~Options=} options ImageCanvas options. * @api */ -const ImageCanvasSource = function(options) { +class ImageCanvasSource { + constructor(options) { - ImageSource.call(this, { - attributions: options.attributions, - projection: options.projection, - resolutions: options.resolutions, - state: options.state - }); + ImageSource.call(this, { + attributions: options.attributions, + projection: options.projection, + resolutions: options.resolutions, + state: options.state + }); - /** - * @private - * @type {module:ol/source/ImageCanvas~FunctionType} - */ - this.canvasFunction_ = options.canvasFunction; + /** + * @private + * @type {module:ol/source/ImageCanvas~FunctionType} + */ + this.canvasFunction_ = options.canvasFunction; - /** - * @private - * @type {module:ol/ImageCanvas} - */ - this.canvas_ = null; + /** + * @private + * @type {module:ol/ImageCanvas} + */ + this.canvas_ = null; - /** - * @private - * @type {number} - */ - this.renderedRevision_ = 0; + /** + * @private + * @type {number} + */ + this.renderedRevision_ = 0; - /** - * @private - * @type {number} - */ - this.ratio_ = options.ratio !== undefined ? - options.ratio : 1.5; + /** + * @private + * @type {number} + */ + this.ratio_ = options.ratio !== undefined ? + options.ratio : 1.5; -}; + } + + /** + * @inheritDoc + */ + getImageInternal(extent, resolution, pixelRatio, projection) { + resolution = this.findNearestResolution(resolution); + + let canvas = this.canvas_; + if (canvas && + this.renderedRevision_ == this.getRevision() && + canvas.getResolution() == resolution && + canvas.getPixelRatio() == pixelRatio && + containsExtent(canvas.getExtent(), extent)) { + return canvas; + } + + extent = extent.slice(); + scaleFromCenter(extent, this.ratio_); + const width = getWidth(extent) / resolution; + const height = getHeight(extent) / resolution; + const size = [width * pixelRatio, height * pixelRatio]; + + const canvasElement = this.canvasFunction_( + extent, resolution, pixelRatio, size, projection); + if (canvasElement) { + canvas = new ImageCanvas(extent, resolution, pixelRatio, canvasElement); + } + this.canvas_ = canvas; + this.renderedRevision_ = this.getRevision(); + + return canvas; + } +} inherits(ImageCanvasSource, ImageSource); -/** - * @inheritDoc - */ -ImageCanvasSource.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { - resolution = this.findNearestResolution(resolution); - - let canvas = this.canvas_; - if (canvas && - this.renderedRevision_ == this.getRevision() && - canvas.getResolution() == resolution && - canvas.getPixelRatio() == pixelRatio && - containsExtent(canvas.getExtent(), extent)) { - return canvas; - } - - extent = extent.slice(); - scaleFromCenter(extent, this.ratio_); - const width = getWidth(extent) / resolution; - const height = getHeight(extent) / resolution; - const size = [width * pixelRatio, height * pixelRatio]; - - const canvasElement = this.canvasFunction_( - extent, resolution, pixelRatio, size, projection); - if (canvasElement) { - canvas = new ImageCanvas(extent, resolution, pixelRatio, canvasElement); - } - this.canvas_ = canvas; - this.renderedRevision_ = this.getRevision(); - - return canvas; -}; export default ImageCanvasSource; diff --git a/src/ol/source/ImageMapGuide.js b/src/ol/source/ImageMapGuide.js index 84470ce419..4c04944e3c 100644 --- a/src/ol/source/ImageMapGuide.js +++ b/src/ol/source/ImageMapGuide.js @@ -42,150 +42,199 @@ import {appendParams} from '../uri.js'; * @param {module:ol/source/ImageMapGuide~Options=} options ImageMapGuide options. * @api */ -const ImageMapGuide = function(options) { +class ImageMapGuide { + constructor(options) { - ImageSource.call(this, { - projection: options.projection, - resolutions: options.resolutions - }); + ImageSource.call(this, { + projection: options.projection, + resolutions: options.resolutions + }); + + /** + * @private + * @type {?string} + */ + this.crossOrigin_ = + options.crossOrigin !== undefined ? options.crossOrigin : null; + + /** + * @private + * @type {number} + */ + this.displayDpi_ = options.displayDpi !== undefined ? + options.displayDpi : 96; + + /** + * @private + * @type {!Object} + */ + this.params_ = options.params || {}; + + /** + * @private + * @type {string|undefined} + */ + this.url_ = options.url; + + /** + * @private + * @type {module:ol/Image~LoadFunction} + */ + this.imageLoadFunction_ = options.imageLoadFunction !== undefined ? + options.imageLoadFunction : defaultImageLoadFunction; + + /** + * @private + * @type {boolean} + */ + this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true; + + /** + * @private + * @type {number} + */ + this.metersPerUnit_ = options.metersPerUnit !== undefined ? + options.metersPerUnit : 1; + + /** + * @private + * @type {number} + */ + this.ratio_ = options.ratio !== undefined ? options.ratio : 1; + + /** + * @private + * @type {boolean} + */ + this.useOverlay_ = options.useOverlay !== undefined ? + options.useOverlay : false; + + /** + * @private + * @type {module:ol/Image} + */ + this.image_ = null; + + /** + * @private + * @type {number} + */ + this.renderedRevision_ = 0; + + } /** - * @private - * @type {?string} + * Get the user-provided params, i.e. those passed to the constructor through + * the "params" option, and possibly updated using the updateParams method. + * @return {Object} Params. + * @api */ - this.crossOrigin_ = - options.crossOrigin !== undefined ? options.crossOrigin : null; + getParams() { + return this.params_; + } /** - * @private - * @type {number} + * @inheritDoc */ - this.displayDpi_ = options.displayDpi !== undefined ? - options.displayDpi : 96; + getImageInternal(extent, resolution, pixelRatio, projection) { + resolution = this.findNearestResolution(resolution); + pixelRatio = this.hidpi_ ? pixelRatio : 1; - /** - * @private - * @type {!Object} - */ - this.params_ = options.params || {}; + let image = this.image_; + if (image && + this.renderedRevision_ == this.getRevision() && + image.getResolution() == resolution && + image.getPixelRatio() == pixelRatio && + containsExtent(image.getExtent(), extent)) { + return image; + } - /** - * @private - * @type {string|undefined} - */ - this.url_ = options.url; + if (this.ratio_ != 1) { + extent = extent.slice(); + scaleFromCenter(extent, this.ratio_); + } + const width = getWidth(extent) / resolution; + const height = getHeight(extent) / resolution; + const size = [width * pixelRatio, height * pixelRatio]; - /** - * @private - * @type {module:ol/Image~LoadFunction} - */ - this.imageLoadFunction_ = options.imageLoadFunction !== undefined ? - options.imageLoadFunction : defaultImageLoadFunction; + if (this.url_ !== undefined) { + const imageUrl = this.getUrl(this.url_, this.params_, extent, size, + projection); + image = new ImageWrapper(extent, resolution, pixelRatio, + imageUrl, this.crossOrigin_, + this.imageLoadFunction_); + listen(image, EventType.CHANGE, + this.handleImageChange, this); + } else { + image = null; + } + this.image_ = image; + this.renderedRevision_ = this.getRevision(); - /** - * @private - * @type {boolean} - */ - this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true; - - /** - * @private - * @type {number} - */ - this.metersPerUnit_ = options.metersPerUnit !== undefined ? - options.metersPerUnit : 1; - - /** - * @private - * @type {number} - */ - this.ratio_ = options.ratio !== undefined ? options.ratio : 1; - - /** - * @private - * @type {boolean} - */ - this.useOverlay_ = options.useOverlay !== undefined ? - options.useOverlay : false; - - /** - * @private - * @type {module:ol/Image} - */ - this.image_ = null; - - /** - * @private - * @type {number} - */ - this.renderedRevision_ = 0; - -}; - -inherits(ImageMapGuide, ImageSource); - - -/** - * Get the user-provided params, i.e. those passed to the constructor through - * the "params" option, and possibly updated using the updateParams method. - * @return {Object} Params. - * @api - */ -ImageMapGuide.prototype.getParams = function() { - return this.params_; -}; - - -/** - * @inheritDoc - */ -ImageMapGuide.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { - resolution = this.findNearestResolution(resolution); - pixelRatio = this.hidpi_ ? pixelRatio : 1; - - let image = this.image_; - if (image && - this.renderedRevision_ == this.getRevision() && - image.getResolution() == resolution && - image.getPixelRatio() == pixelRatio && - containsExtent(image.getExtent(), extent)) { return image; } - if (this.ratio_ != 1) { - extent = extent.slice(); - scaleFromCenter(extent, this.ratio_); + /** + * Return the image load function of the source. + * @return {module:ol/Image~LoadFunction} The image load function. + * @api + */ + getImageLoadFunction() { + return this.imageLoadFunction_; } - const width = getWidth(extent) / resolution; - const height = getHeight(extent) / resolution; - const size = [width * pixelRatio, height * pixelRatio]; - if (this.url_ !== undefined) { - const imageUrl = this.getUrl(this.url_, this.params_, extent, size, - projection); - image = new ImageWrapper(extent, resolution, pixelRatio, - imageUrl, this.crossOrigin_, - this.imageLoadFunction_); - listen(image, EventType.CHANGE, - this.handleImageChange, this); - } else { - image = null; + /** + * Update the user-provided params. + * @param {Object} params Params. + * @api + */ + updateParams(params) { + assign(this.params_, params); + this.changed(); } - this.image_ = image; - this.renderedRevision_ = this.getRevision(); - return image; -}; + /** + * @param {string} baseUrl The mapagent url. + * @param {Object.} params Request parameters. + * @param {module:ol/extent~Extent} extent Extent. + * @param {module:ol/size~Size} size Size. + * @param {module:ol/proj/Projection} projection Projection. + * @return {string} The mapagent map image request URL. + */ + getUrl(baseUrl, params, extent, size, projection) { + const scale = getScale(extent, size, + this.metersPerUnit_, this.displayDpi_); + const center = getCenter(extent); + const baseParams = { + 'OPERATION': this.useOverlay_ ? 'GETDYNAMICMAPOVERLAYIMAGE' : 'GETMAPIMAGE', + 'VERSION': '2.0.0', + 'LOCALE': 'en', + 'CLIENTAGENT': 'ol/source/ImageMapGuide source', + 'CLIP': '1', + 'SETDISPLAYDPI': this.displayDpi_, + 'SETDISPLAYWIDTH': Math.round(size[0]), + 'SETDISPLAYHEIGHT': Math.round(size[1]), + 'SETVIEWSCALE': scale, + 'SETVIEWCENTERX': center[0], + 'SETVIEWCENTERY': center[1] + }; + assign(baseParams, params); + return appendParams(baseUrl, baseParams); + } + /** + * Set the image load function of the MapGuide source. + * @param {module:ol/Image~LoadFunction} imageLoadFunction Image load function. + * @api + */ + setImageLoadFunction(imageLoadFunction) { + this.image_ = null; + this.imageLoadFunction_ = imageLoadFunction; + this.changed(); + } +} -/** - * Return the image load function of the source. - * @return {module:ol/Image~LoadFunction} The image load function. - * @api - */ -ImageMapGuide.prototype.getImageLoadFunction = function() { - return this.imageLoadFunction_; -}; +inherits(ImageMapGuide, ImageSource); /** @@ -209,55 +258,4 @@ function getScale(extent, size, metersPerUnit, dpi) { } -/** - * Update the user-provided params. - * @param {Object} params Params. - * @api - */ -ImageMapGuide.prototype.updateParams = function(params) { - assign(this.params_, params); - this.changed(); -}; - - -/** - * @param {string} baseUrl The mapagent url. - * @param {Object.} params Request parameters. - * @param {module:ol/extent~Extent} extent Extent. - * @param {module:ol/size~Size} size Size. - * @param {module:ol/proj/Projection} projection Projection. - * @return {string} The mapagent map image request URL. - */ -ImageMapGuide.prototype.getUrl = function(baseUrl, params, extent, size, projection) { - const scale = getScale(extent, size, - this.metersPerUnit_, this.displayDpi_); - const center = getCenter(extent); - const baseParams = { - 'OPERATION': this.useOverlay_ ? 'GETDYNAMICMAPOVERLAYIMAGE' : 'GETMAPIMAGE', - 'VERSION': '2.0.0', - 'LOCALE': 'en', - 'CLIENTAGENT': 'ol/source/ImageMapGuide source', - 'CLIP': '1', - 'SETDISPLAYDPI': this.displayDpi_, - 'SETDISPLAYWIDTH': Math.round(size[0]), - 'SETDISPLAYHEIGHT': Math.round(size[1]), - 'SETVIEWSCALE': scale, - 'SETVIEWCENTERX': center[0], - 'SETVIEWCENTERY': center[1] - }; - assign(baseParams, params); - return appendParams(baseUrl, baseParams); -}; - - -/** - * Set the image load function of the MapGuide source. - * @param {module:ol/Image~LoadFunction} imageLoadFunction Image load function. - * @api - */ -ImageMapGuide.prototype.setImageLoadFunction = function(imageLoadFunction) { - this.image_ = null; - this.imageLoadFunction_ = imageLoadFunction; - this.changed(); -}; export default ImageMapGuide; diff --git a/src/ol/source/ImageStatic.js b/src/ol/source/ImageStatic.js index 6075a0271e..d1ad988bdd 100644 --- a/src/ol/source/ImageStatic.js +++ b/src/ol/source/ImageStatic.js @@ -37,77 +37,79 @@ import ImageSource, {defaultImageLoadFunction} from '../source/Image.js'; * @param {module:ol/source/ImageStatic~Options=} options ImageStatic options. * @api */ -const Static = function(options) { - const imageExtent = options.imageExtent; +class Static { + constructor(options) { + const imageExtent = options.imageExtent; - const crossOrigin = options.crossOrigin !== undefined ? - options.crossOrigin : null; + const crossOrigin = options.crossOrigin !== undefined ? + options.crossOrigin : null; - const /** @type {module:ol/Image~LoadFunction} */ imageLoadFunction = - options.imageLoadFunction !== undefined ? - options.imageLoadFunction : defaultImageLoadFunction; + const /** @type {module:ol/Image~LoadFunction} */ imageLoadFunction = + options.imageLoadFunction !== undefined ? + options.imageLoadFunction : defaultImageLoadFunction; - ImageSource.call(this, { - attributions: options.attributions, - projection: getProjection(options.projection) - }); + ImageSource.call(this, { + attributions: options.attributions, + projection: getProjection(options.projection) + }); + + /** + * @private + * @type {module:ol/Image} + */ + this.image_ = new ImageWrapper(imageExtent, undefined, 1, options.url, crossOrigin, imageLoadFunction); + + /** + * @private + * @type {module:ol/size~Size} + */ + this.imageSize_ = options.imageSize ? options.imageSize : null; + + listen(this.image_, EventType.CHANGE, + this.handleImageChange, this); + + } /** - * @private - * @type {module:ol/Image} + * @inheritDoc */ - this.image_ = new ImageWrapper(imageExtent, undefined, 1, options.url, crossOrigin, imageLoadFunction); + getImageInternal(extent, resolution, pixelRatio, projection) { + if (intersects(extent, this.image_.getExtent())) { + return this.image_; + } + return null; + } /** - * @private - * @type {module:ol/size~Size} + * @inheritDoc */ - this.imageSize_ = options.imageSize ? options.imageSize : null; - - listen(this.image_, EventType.CHANGE, - this.handleImageChange, this); - -}; + handleImageChange(evt) { + if (this.image_.getState() == ImageState.LOADED) { + const imageExtent = this.image_.getExtent(); + const image = this.image_.getImage(); + let imageWidth, imageHeight; + if (this.imageSize_) { + imageWidth = this.imageSize_[0]; + imageHeight = this.imageSize_[1]; + } else { + imageWidth = image.width; + imageHeight = image.height; + } + const resolution = getHeight(imageExtent) / imageHeight; + const targetWidth = Math.ceil(getWidth(imageExtent) / resolution); + if (targetWidth != imageWidth) { + const context = createCanvasContext2D(targetWidth, imageHeight); + const canvas = context.canvas; + context.drawImage(image, 0, 0, imageWidth, imageHeight, + 0, 0, canvas.width, canvas.height); + this.image_.setImage(canvas); + } + } + ImageSource.prototype.handleImageChange.call(this, evt); + } +} inherits(Static, ImageSource); -/** - * @inheritDoc - */ -Static.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { - if (intersects(extent, this.image_.getExtent())) { - return this.image_; - } - return null; -}; - - -/** - * @inheritDoc - */ -Static.prototype.handleImageChange = function(evt) { - if (this.image_.getState() == ImageState.LOADED) { - const imageExtent = this.image_.getExtent(); - const image = this.image_.getImage(); - let imageWidth, imageHeight; - if (this.imageSize_) { - imageWidth = this.imageSize_[0]; - imageHeight = this.imageSize_[1]; - } else { - imageWidth = image.width; - imageHeight = image.height; - } - const resolution = getHeight(imageExtent) / imageHeight; - const targetWidth = Math.ceil(getWidth(imageExtent) / resolution); - if (targetWidth != imageWidth) { - const context = createCanvasContext2D(targetWidth, imageHeight); - const canvas = context.canvas; - context.drawImage(image, 0, 0, imageWidth, imageHeight, - 0, 0, canvas.width, canvas.height); - this.image_.setImage(canvas); - } - } - ImageSource.prototype.handleImageChange.call(this, evt); -}; export default Static; diff --git a/src/ol/source/ImageWMS.js b/src/ol/source/ImageWMS.js index 842578a950..963f66fda8 100644 --- a/src/ol/source/ImageWMS.js +++ b/src/ol/source/ImageWMS.js @@ -53,86 +53,329 @@ import {appendParams} from '../uri.js'; * @param {module:ol/source/ImageWMS~Options=} [opt_options] ImageWMS options. * @api */ -const ImageWMS = function(opt_options) { +class ImageWMS { + constructor(opt_options) { - const options = opt_options || {}; + const options = opt_options || {}; - ImageSource.call(this, { - attributions: options.attributions, - projection: options.projection, - resolutions: options.resolutions - }); + ImageSource.call(this, { + attributions: options.attributions, + projection: options.projection, + resolutions: options.resolutions + }); + + /** + * @private + * @type {?string} + */ + this.crossOrigin_ = + options.crossOrigin !== undefined ? options.crossOrigin : null; + + /** + * @private + * @type {string|undefined} + */ + this.url_ = options.url; + + /** + * @private + * @type {module:ol/Image~LoadFunction} + */ + this.imageLoadFunction_ = options.imageLoadFunction !== undefined ? + options.imageLoadFunction : defaultImageLoadFunction; + + /** + * @private + * @type {!Object} + */ + this.params_ = options.params || {}; + + /** + * @private + * @type {boolean} + */ + this.v13_ = true; + this.updateV13_(); + + /** + * @private + * @type {module:ol/source/WMSServerType|undefined} + */ + this.serverType_ = /** @type {module:ol/source/WMSServerType|undefined} */ (options.serverType); + + /** + * @private + * @type {boolean} + */ + this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true; + + /** + * @private + * @type {module:ol/Image} + */ + this.image_ = null; + + /** + * @private + * @type {module:ol/size~Size} + */ + this.imageSize_ = [0, 0]; + + /** + * @private + * @type {number} + */ + this.renderedRevision_ = 0; + + /** + * @private + * @type {number} + */ + this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5; + + } + + /** + * Return the GetFeatureInfo URL for the passed coordinate, resolution, and + * projection. Return `undefined` if the GetFeatureInfo URL cannot be + * constructed. + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {number} resolution Resolution. + * @param {module:ol/proj~ProjectionLike} projection Projection. + * @param {!Object} params GetFeatureInfo params. `INFO_FORMAT` at least should + * be provided. If `QUERY_LAYERS` is not provided then the layers specified + * in the `LAYERS` parameter will be used. `VERSION` should not be + * specified here. + * @return {string|undefined} GetFeatureInfo URL. + * @api + */ + getGetFeatureInfoUrl(coordinate, resolution, projection, params) { + if (this.url_ === undefined) { + return undefined; + } + const projectionObj = getProjection(projection); + const sourceProjectionObj = this.getProjection(); + + if (sourceProjectionObj && sourceProjectionObj !== projectionObj) { + resolution = calculateSourceResolution(sourceProjectionObj, projectionObj, coordinate, resolution); + coordinate = transform(coordinate, projectionObj, sourceProjectionObj); + } + + const extent = getForViewAndSize(coordinate, resolution, 0, + GETFEATUREINFO_IMAGE_SIZE); + + const baseParams = { + 'SERVICE': 'WMS', + 'VERSION': DEFAULT_WMS_VERSION, + 'REQUEST': 'GetFeatureInfo', + 'FORMAT': 'image/png', + 'TRANSPARENT': true, + 'QUERY_LAYERS': this.params_['LAYERS'] + }; + assign(baseParams, this.params_, params); + + const x = Math.floor((coordinate[0] - extent[0]) / resolution); + const y = Math.floor((extent[3] - coordinate[1]) / resolution); + baseParams[this.v13_ ? 'I' : 'X'] = x; + baseParams[this.v13_ ? 'J' : 'Y'] = y; + + return this.getRequestUrl_( + extent, GETFEATUREINFO_IMAGE_SIZE, + 1, sourceProjectionObj || projectionObj, baseParams); + } + + /** + * Get the user-provided params, i.e. those passed to the constructor through + * the "params" option, and possibly updated using the updateParams method. + * @return {Object} Params. + * @api + */ + getParams() { + return this.params_; + } + + /** + * @inheritDoc + */ + getImageInternal(extent, resolution, pixelRatio, projection) { + + if (this.url_ === undefined) { + return null; + } + + resolution = this.findNearestResolution(resolution); + + if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) { + pixelRatio = 1; + } + + const imageResolution = resolution / pixelRatio; + + const center = getCenter(extent); + const viewWidth = Math.ceil(getWidth(extent) / imageResolution); + const viewHeight = Math.ceil(getHeight(extent) / imageResolution); + const viewExtent = getForViewAndSize(center, imageResolution, 0, + [viewWidth, viewHeight]); + const requestWidth = Math.ceil(this.ratio_ * getWidth(extent) / imageResolution); + const requestHeight = Math.ceil(this.ratio_ * getHeight(extent) / imageResolution); + const requestExtent = getForViewAndSize(center, imageResolution, 0, + [requestWidth, requestHeight]); + + const image = this.image_; + if (image && + this.renderedRevision_ == this.getRevision() && + image.getResolution() == resolution && + image.getPixelRatio() == pixelRatio && + containsExtent(image.getExtent(), viewExtent)) { + return image; + } + + const params = { + 'SERVICE': 'WMS', + 'VERSION': DEFAULT_WMS_VERSION, + 'REQUEST': 'GetMap', + 'FORMAT': 'image/png', + 'TRANSPARENT': true + }; + assign(params, this.params_); + + this.imageSize_[0] = Math.round(getWidth(requestExtent) / imageResolution); + this.imageSize_[1] = Math.round(getHeight(requestExtent) / imageResolution); + + const url = this.getRequestUrl_(requestExtent, this.imageSize_, pixelRatio, + projection, params); + + this.image_ = new ImageWrapper(requestExtent, resolution, pixelRatio, + url, this.crossOrigin_, this.imageLoadFunction_); + + this.renderedRevision_ = this.getRevision(); + + listen(this.image_, EventType.CHANGE, + this.handleImageChange, this); + + return this.image_; + + } + + /** + * Return the image load function of the source. + * @return {module:ol/Image~LoadFunction} The image load function. + * @api + */ + getImageLoadFunction() { + return this.imageLoadFunction_; + } + + /** + * @param {module:ol/extent~Extent} extent Extent. + * @param {module:ol/size~Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @param {Object} params Params. + * @return {string} Request URL. + * @private + */ + getRequestUrl_(extent, size, pixelRatio, projection, params) { + + assert(this.url_ !== undefined, 9); // `url` must be configured or set using `#setUrl()` + + params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode(); + + if (!('STYLES' in this.params_)) { + params['STYLES'] = ''; + } + + if (pixelRatio != 1) { + switch (this.serverType_) { + case WMSServerType.GEOSERVER: + const dpi = (90 * pixelRatio + 0.5) | 0; + if ('FORMAT_OPTIONS' in params) { + params['FORMAT_OPTIONS'] += ';dpi:' + dpi; + } else { + params['FORMAT_OPTIONS'] = 'dpi:' + dpi; + } + break; + case WMSServerType.MAPSERVER: + params['MAP_RESOLUTION'] = 90 * pixelRatio; + break; + case WMSServerType.CARMENTA_SERVER: + case WMSServerType.QGIS: + params['DPI'] = 90 * pixelRatio; + break; + default: + assert(false, 8); // Unknown `serverType` configured + break; + } + } + + params['WIDTH'] = size[0]; + params['HEIGHT'] = size[1]; + + const axisOrientation = projection.getAxisOrientation(); + let bbox; + if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') { + bbox = [extent[1], extent[0], extent[3], extent[2]]; + } else { + bbox = extent; + } + params['BBOX'] = bbox.join(','); + + return appendParams(/** @type {string} */ (this.url_), params); + } + + /** + * Return the URL used for this WMS source. + * @return {string|undefined} URL. + * @api + */ + getUrl() { + return this.url_; + } + + /** + * Set the image load function of the source. + * @param {module:ol/Image~LoadFunction} imageLoadFunction Image load function. + * @api + */ + setImageLoadFunction(imageLoadFunction) { + this.image_ = null; + this.imageLoadFunction_ = imageLoadFunction; + this.changed(); + } + + /** + * Set the URL to use for requests. + * @param {string|undefined} url URL. + * @api + */ + setUrl(url) { + if (url != this.url_) { + this.url_ = url; + this.image_ = null; + this.changed(); + } + } + + /** + * Update the user-provided params. + * @param {Object} params Params. + * @api + */ + updateParams(params) { + assign(this.params_, params); + this.updateV13_(); + this.image_ = null; + this.changed(); + } /** * @private - * @type {?string} */ - this.crossOrigin_ = - options.crossOrigin !== undefined ? options.crossOrigin : null; - - /** - * @private - * @type {string|undefined} - */ - this.url_ = options.url; - - /** - * @private - * @type {module:ol/Image~LoadFunction} - */ - this.imageLoadFunction_ = options.imageLoadFunction !== undefined ? - options.imageLoadFunction : defaultImageLoadFunction; - - /** - * @private - * @type {!Object} - */ - this.params_ = options.params || {}; - - /** - * @private - * @type {boolean} - */ - this.v13_ = true; - this.updateV13_(); - - /** - * @private - * @type {module:ol/source/WMSServerType|undefined} - */ - this.serverType_ = /** @type {module:ol/source/WMSServerType|undefined} */ (options.serverType); - - /** - * @private - * @type {boolean} - */ - this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true; - - /** - * @private - * @type {module:ol/Image} - */ - this.image_ = null; - - /** - * @private - * @type {module:ol/size~Size} - */ - this.imageSize_ = [0, 0]; - - /** - * @private - * @type {number} - */ - this.renderedRevision_ = 0; - - /** - * @private - * @type {number} - */ - this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5; - -}; + updateV13_() { + const version = this.params_['VERSION'] || DEFAULT_WMS_VERSION; + this.v13_ = compareVersions(version, '1.3') >= 0; + } +} inherits(ImageWMS, ImageSource); @@ -144,253 +387,4 @@ inherits(ImageWMS, ImageSource); const GETFEATUREINFO_IMAGE_SIZE = [101, 101]; -/** - * Return the GetFeatureInfo URL for the passed coordinate, resolution, and - * projection. Return `undefined` if the GetFeatureInfo URL cannot be - * constructed. - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {number} resolution Resolution. - * @param {module:ol/proj~ProjectionLike} projection Projection. - * @param {!Object} params GetFeatureInfo params. `INFO_FORMAT` at least should - * be provided. If `QUERY_LAYERS` is not provided then the layers specified - * in the `LAYERS` parameter will be used. `VERSION` should not be - * specified here. - * @return {string|undefined} GetFeatureInfo URL. - * @api - */ -ImageWMS.prototype.getGetFeatureInfoUrl = function(coordinate, resolution, projection, params) { - if (this.url_ === undefined) { - return undefined; - } - const projectionObj = getProjection(projection); - const sourceProjectionObj = this.getProjection(); - - if (sourceProjectionObj && sourceProjectionObj !== projectionObj) { - resolution = calculateSourceResolution(sourceProjectionObj, projectionObj, coordinate, resolution); - coordinate = transform(coordinate, projectionObj, sourceProjectionObj); - } - - const extent = getForViewAndSize(coordinate, resolution, 0, - GETFEATUREINFO_IMAGE_SIZE); - - const baseParams = { - 'SERVICE': 'WMS', - 'VERSION': DEFAULT_WMS_VERSION, - 'REQUEST': 'GetFeatureInfo', - 'FORMAT': 'image/png', - 'TRANSPARENT': true, - 'QUERY_LAYERS': this.params_['LAYERS'] - }; - assign(baseParams, this.params_, params); - - const x = Math.floor((coordinate[0] - extent[0]) / resolution); - const y = Math.floor((extent[3] - coordinate[1]) / resolution); - baseParams[this.v13_ ? 'I' : 'X'] = x; - baseParams[this.v13_ ? 'J' : 'Y'] = y; - - return this.getRequestUrl_( - extent, GETFEATUREINFO_IMAGE_SIZE, - 1, sourceProjectionObj || projectionObj, baseParams); -}; - - -/** - * Get the user-provided params, i.e. those passed to the constructor through - * the "params" option, and possibly updated using the updateParams method. - * @return {Object} Params. - * @api - */ -ImageWMS.prototype.getParams = function() { - return this.params_; -}; - - -/** - * @inheritDoc - */ -ImageWMS.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { - - if (this.url_ === undefined) { - return null; - } - - resolution = this.findNearestResolution(resolution); - - if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) { - pixelRatio = 1; - } - - const imageResolution = resolution / pixelRatio; - - const center = getCenter(extent); - const viewWidth = Math.ceil(getWidth(extent) / imageResolution); - const viewHeight = Math.ceil(getHeight(extent) / imageResolution); - const viewExtent = getForViewAndSize(center, imageResolution, 0, - [viewWidth, viewHeight]); - const requestWidth = Math.ceil(this.ratio_ * getWidth(extent) / imageResolution); - const requestHeight = Math.ceil(this.ratio_ * getHeight(extent) / imageResolution); - const requestExtent = getForViewAndSize(center, imageResolution, 0, - [requestWidth, requestHeight]); - - const image = this.image_; - if (image && - this.renderedRevision_ == this.getRevision() && - image.getResolution() == resolution && - image.getPixelRatio() == pixelRatio && - containsExtent(image.getExtent(), viewExtent)) { - return image; - } - - const params = { - 'SERVICE': 'WMS', - 'VERSION': DEFAULT_WMS_VERSION, - 'REQUEST': 'GetMap', - 'FORMAT': 'image/png', - 'TRANSPARENT': true - }; - assign(params, this.params_); - - this.imageSize_[0] = Math.round(getWidth(requestExtent) / imageResolution); - this.imageSize_[1] = Math.round(getHeight(requestExtent) / imageResolution); - - const url = this.getRequestUrl_(requestExtent, this.imageSize_, pixelRatio, - projection, params); - - this.image_ = new ImageWrapper(requestExtent, resolution, pixelRatio, - url, this.crossOrigin_, this.imageLoadFunction_); - - this.renderedRevision_ = this.getRevision(); - - listen(this.image_, EventType.CHANGE, - this.handleImageChange, this); - - return this.image_; - -}; - - -/** - * Return the image load function of the source. - * @return {module:ol/Image~LoadFunction} The image load function. - * @api - */ -ImageWMS.prototype.getImageLoadFunction = function() { - return this.imageLoadFunction_; -}; - - -/** - * @param {module:ol/extent~Extent} extent Extent. - * @param {module:ol/size~Size} size Size. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @param {Object} params Params. - * @return {string} Request URL. - * @private - */ -ImageWMS.prototype.getRequestUrl_ = function(extent, size, pixelRatio, projection, params) { - - assert(this.url_ !== undefined, 9); // `url` must be configured or set using `#setUrl()` - - params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode(); - - if (!('STYLES' in this.params_)) { - params['STYLES'] = ''; - } - - if (pixelRatio != 1) { - switch (this.serverType_) { - case WMSServerType.GEOSERVER: - const dpi = (90 * pixelRatio + 0.5) | 0; - if ('FORMAT_OPTIONS' in params) { - params['FORMAT_OPTIONS'] += ';dpi:' + dpi; - } else { - params['FORMAT_OPTIONS'] = 'dpi:' + dpi; - } - break; - case WMSServerType.MAPSERVER: - params['MAP_RESOLUTION'] = 90 * pixelRatio; - break; - case WMSServerType.CARMENTA_SERVER: - case WMSServerType.QGIS: - params['DPI'] = 90 * pixelRatio; - break; - default: - assert(false, 8); // Unknown `serverType` configured - break; - } - } - - params['WIDTH'] = size[0]; - params['HEIGHT'] = size[1]; - - const axisOrientation = projection.getAxisOrientation(); - let bbox; - if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') { - bbox = [extent[1], extent[0], extent[3], extent[2]]; - } else { - bbox = extent; - } - params['BBOX'] = bbox.join(','); - - return appendParams(/** @type {string} */ (this.url_), params); -}; - - -/** - * Return the URL used for this WMS source. - * @return {string|undefined} URL. - * @api - */ -ImageWMS.prototype.getUrl = function() { - return this.url_; -}; - - -/** - * Set the image load function of the source. - * @param {module:ol/Image~LoadFunction} imageLoadFunction Image load function. - * @api - */ -ImageWMS.prototype.setImageLoadFunction = function(imageLoadFunction) { - this.image_ = null; - this.imageLoadFunction_ = imageLoadFunction; - this.changed(); -}; - - -/** - * Set the URL to use for requests. - * @param {string|undefined} url URL. - * @api - */ -ImageWMS.prototype.setUrl = function(url) { - if (url != this.url_) { - this.url_ = url; - this.image_ = null; - this.changed(); - } -}; - - -/** - * Update the user-provided params. - * @param {Object} params Params. - * @api - */ -ImageWMS.prototype.updateParams = function(params) { - assign(this.params_, params); - this.updateV13_(); - this.image_ = null; - this.changed(); -}; - - -/** - * @private - */ -ImageWMS.prototype.updateV13_ = function() { - const version = this.params_['VERSION'] || DEFAULT_WMS_VERSION; - this.v13_ = compareVersions(version, '1.3') >= 0; -}; export default ImageWMS; diff --git a/src/ol/source/Raster.js b/src/ol/source/Raster.js index c848144181..74a76700af 100644 --- a/src/ol/source/Raster.js +++ b/src/ol/source/Raster.js @@ -144,276 +144,279 @@ inherits(RasterSourceEvent, Event); * @param {module:ol/source/Raster~Options=} options Options. * @api */ -const RasterSource = function(options) { +class RasterSource { + constructor(options) { - /** - * @private - * @type {*} - */ - this.worker_ = null; + /** + * @private + * @type {*} + */ + this.worker_ = null; - /** - * @private - * @type {module:ol/source/Raster~RasterOperationType} - */ - this.operationType_ = options.operationType !== undefined ? - options.operationType : RasterOperationType.PIXEL; + /** + * @private + * @type {module:ol/source/Raster~RasterOperationType} + */ + this.operationType_ = options.operationType !== undefined ? + options.operationType : RasterOperationType.PIXEL; - /** - * @private - * @type {number} - */ - this.threads_ = options.threads !== undefined ? options.threads : 1; + /** + * @private + * @type {number} + */ + this.threads_ = options.threads !== undefined ? options.threads : 1; - /** - * @private - * @type {Array.} - */ - this.renderers_ = createRenderers(options.sources); + /** + * @private + * @type {Array.} + */ + this.renderers_ = createRenderers(options.sources); - for (let r = 0, rr = this.renderers_.length; r < rr; ++r) { - listen(this.renderers_[r], EventType.CHANGE, - this.changed, this); - } - - /** - * @private - * @type {module:ol/TileQueue} - */ - this.tileQueue_ = new TileQueue( - function() { - return 1; - }, - this.changed.bind(this)); - - const layerStatesArray = getLayerStatesArray(this.renderers_); - const layerStates = {}; - for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { - layerStates[getUid(layerStatesArray[i].layer)] = layerStatesArray[i]; - } - - /** - * The most recently requested frame state. - * @type {module:ol/PluggableMap~FrameState} - * @private - */ - this.requestedFrameState_; - - /** - * The most recently rendered image canvas. - * @type {module:ol/ImageCanvas} - * @private - */ - this.renderedImageCanvas_ = null; - - /** - * The most recently rendered revision. - * @type {number} - */ - this.renderedRevision_; - - /** - * @private - * @type {module:ol/PluggableMap~FrameState} - */ - this.frameState_ = { - animate: false, - coordinateToPixelTransform: createTransform(), - extent: null, - focus: null, - index: 0, - layerStates: layerStates, - layerStatesArray: layerStatesArray, - pixelRatio: 1, - pixelToCoordinateTransform: createTransform(), - postRenderFunctions: [], - size: [0, 0], - skippedFeatureUids: {}, - tileQueue: this.tileQueue_, - time: Date.now(), - usedTiles: {}, - viewState: /** @type {module:ol/View~State} */ ({ - rotation: 0 - }), - viewHints: [], - wantedTiles: {} - }; - - ImageSource.call(this, {}); - - if (options.operation !== undefined) { - this.setOperation(options.operation, options.lib); - } - -}; - -inherits(RasterSource, ImageSource); - - -/** - * Set the operation. - * @param {module:ol/source/Raster~Operation} operation New operation. - * @param {Object=} opt_lib Functions that will be available to operations run - * in a worker. - * @api - */ -RasterSource.prototype.setOperation = function(operation, opt_lib) { - this.worker_ = new Processor({ - operation: operation, - imageOps: this.operationType_ === RasterOperationType.IMAGE, - queue: 1, - lib: opt_lib, - threads: this.threads_ - }); - this.changed(); -}; - - -/** - * Update the stored frame state. - * @param {module:ol/extent~Extent} extent The view extent (in map units). - * @param {number} resolution The view resolution. - * @param {module:ol/proj/Projection} projection The view projection. - * @return {module:ol/PluggableMap~FrameState} The updated frame state. - * @private - */ -RasterSource.prototype.updateFrameState_ = function(extent, resolution, projection) { - - const frameState = /** @type {module:ol/PluggableMap~FrameState} */ (assign({}, this.frameState_)); - - frameState.viewState = /** @type {module:ol/View~State} */ (assign({}, frameState.viewState)); - - const center = getCenter(extent); - - frameState.extent = extent.slice(); - frameState.focus = center; - frameState.size[0] = Math.round(getWidth(extent) / resolution); - frameState.size[1] = Math.round(getHeight(extent) / resolution); - frameState.time = Date.now(); - frameState.animate = false; - - const viewState = frameState.viewState; - viewState.center = center; - viewState.projection = projection; - viewState.resolution = resolution; - return frameState; -}; - - -/** - * Determine if all sources are ready. - * @return {boolean} All sources are ready. - * @private - */ -RasterSource.prototype.allSourcesReady_ = function() { - let ready = true; - let source; - for (let i = 0, ii = this.renderers_.length; i < ii; ++i) { - source = this.renderers_[i].getLayer().getSource(); - if (source.getState() !== SourceState.READY) { - ready = false; - break; + for (let r = 0, rr = this.renderers_.length; r < rr; ++r) { + listen(this.renderers_[r], EventType.CHANGE, + this.changed, this); } - } - return ready; -}; + /** + * @private + * @type {module:ol/TileQueue} + */ + this.tileQueue_ = new TileQueue( + function() { + return 1; + }, + this.changed.bind(this)); -/** - * @inheritDoc - */ -RasterSource.prototype.getImage = function(extent, resolution, pixelRatio, projection) { - if (!this.allSourcesReady_()) { - return null; - } - - const frameState = this.updateFrameState_(extent, resolution, projection); - this.requestedFrameState_ = frameState; - - // check if we can't reuse the existing ol/ImageCanvas - if (this.renderedImageCanvas_) { - const renderedResolution = this.renderedImageCanvas_.getResolution(); - const renderedExtent = this.renderedImageCanvas_.getExtent(); - if (resolution !== renderedResolution || !equals(extent, renderedExtent)) { - this.renderedImageCanvas_ = null; + const layerStatesArray = getLayerStatesArray(this.renderers_); + const layerStates = {}; + for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { + layerStates[getUid(layerStatesArray[i].layer)] = layerStatesArray[i]; } + + /** + * The most recently requested frame state. + * @type {module:ol/PluggableMap~FrameState} + * @private + */ + this.requestedFrameState_; + + /** + * The most recently rendered image canvas. + * @type {module:ol/ImageCanvas} + * @private + */ + this.renderedImageCanvas_ = null; + + /** + * The most recently rendered revision. + * @type {number} + */ + this.renderedRevision_; + + /** + * @private + * @type {module:ol/PluggableMap~FrameState} + */ + this.frameState_ = { + animate: false, + coordinateToPixelTransform: createTransform(), + extent: null, + focus: null, + index: 0, + layerStates: layerStates, + layerStatesArray: layerStatesArray, + pixelRatio: 1, + pixelToCoordinateTransform: createTransform(), + postRenderFunctions: [], + size: [0, 0], + skippedFeatureUids: {}, + tileQueue: this.tileQueue_, + time: Date.now(), + usedTiles: {}, + viewState: /** @type {module:ol/View~State} */ ({ + rotation: 0 + }), + viewHints: [], + wantedTiles: {} + }; + + ImageSource.call(this, {}); + + if (options.operation !== undefined) { + this.setOperation(options.operation, options.lib); + } + } - if (!this.renderedImageCanvas_ || this.getRevision() !== this.renderedRevision_) { - this.processSources_(); + /** + * Set the operation. + * @param {module:ol/source/Raster~Operation} operation New operation. + * @param {Object=} opt_lib Functions that will be available to operations run + * in a worker. + * @api + */ + setOperation(operation, opt_lib) { + this.worker_ = new Processor({ + operation: operation, + imageOps: this.operationType_ === RasterOperationType.IMAGE, + queue: 1, + lib: opt_lib, + threads: this.threads_ + }); + this.changed(); } - frameState.tileQueue.loadMoreTiles(16, 16); + /** + * Update the stored frame state. + * @param {module:ol/extent~Extent} extent The view extent (in map units). + * @param {number} resolution The view resolution. + * @param {module:ol/proj/Projection} projection The view projection. + * @return {module:ol/PluggableMap~FrameState} The updated frame state. + * @private + */ + updateFrameState_(extent, resolution, projection) { - if (frameState.animate) { - requestAnimationFrame(this.changed.bind(this)); + const frameState = /** @type {module:ol/PluggableMap~FrameState} */ (assign({}, this.frameState_)); + + frameState.viewState = /** @type {module:ol/View~State} */ (assign({}, frameState.viewState)); + + const center = getCenter(extent); + + frameState.extent = extent.slice(); + frameState.focus = center; + frameState.size[0] = Math.round(getWidth(extent) / resolution); + frameState.size[1] = Math.round(getHeight(extent) / resolution); + frameState.time = Date.now(); + frameState.animate = false; + + const viewState = frameState.viewState; + viewState.center = center; + viewState.projection = projection; + viewState.resolution = resolution; + return frameState; } - return this.renderedImageCanvas_; -}; + /** + * Determine if all sources are ready. + * @return {boolean} All sources are ready. + * @private + */ + allSourcesReady_() { + let ready = true; + let source; + for (let i = 0, ii = this.renderers_.length; i < ii; ++i) { + source = this.renderers_[i].getLayer().getSource(); + if (source.getState() !== SourceState.READY) { + ready = false; + break; + } + } + return ready; + } + /** + * @inheritDoc + */ + getImage(extent, resolution, pixelRatio, projection) { + if (!this.allSourcesReady_()) { + return null; + } -/** - * Start processing source data. - * @private - */ -RasterSource.prototype.processSources_ = function() { - const frameState = this.requestedFrameState_; - const len = this.renderers_.length; - const imageDatas = new Array(len); - for (let i = 0; i < len; ++i) { - const imageData = getImageData( - this.renderers_[i], frameState, frameState.layerStatesArray[i]); - if (imageData) { - imageDatas[i] = imageData; - } else { + const frameState = this.updateFrameState_(extent, resolution, projection); + this.requestedFrameState_ = frameState; + + // check if we can't reuse the existing ol/ImageCanvas + if (this.renderedImageCanvas_) { + const renderedResolution = this.renderedImageCanvas_.getResolution(); + const renderedExtent = this.renderedImageCanvas_.getExtent(); + if (resolution !== renderedResolution || !equals(extent, renderedExtent)) { + this.renderedImageCanvas_ = null; + } + } + + if (!this.renderedImageCanvas_ || this.getRevision() !== this.renderedRevision_) { + this.processSources_(); + } + + frameState.tileQueue.loadMoreTiles(16, 16); + + if (frameState.animate) { + requestAnimationFrame(this.changed.bind(this)); + } + + return this.renderedImageCanvas_; + } + + /** + * Start processing source data. + * @private + */ + processSources_() { + const frameState = this.requestedFrameState_; + const len = this.renderers_.length; + const imageDatas = new Array(len); + for (let i = 0; i < len; ++i) { + const imageData = getImageData( + this.renderers_[i], frameState, frameState.layerStatesArray[i]); + if (imageData) { + imageDatas[i] = imageData; + } else { + return; + } + } + + const data = {}; + this.dispatchEvent(new RasterSourceEvent(RasterEventType.BEFOREOPERATIONS, frameState, data)); + this.worker_.process(imageDatas, data, this.onWorkerComplete_.bind(this, frameState)); + } + + /** + * Called when pixel processing is complete. + * @param {module:ol/PluggableMap~FrameState} frameState The frame state. + * @param {Error} err Any error during processing. + * @param {ImageData} output The output image data. + * @param {Object} data The user data. + * @private + */ + onWorkerComplete_(frameState, err, output, data) { + if (err || !output) { return; } + + // do nothing if extent or resolution changed + const extent = frameState.extent; + const resolution = frameState.viewState.resolution; + if (resolution !== this.requestedFrameState_.viewState.resolution || + !equals(extent, this.requestedFrameState_.extent)) { + return; + } + + let context; + if (this.renderedImageCanvas_) { + context = this.renderedImageCanvas_.getImage().getContext('2d'); + } else { + const width = Math.round(getWidth(extent) / resolution); + const height = Math.round(getHeight(extent) / resolution); + context = createCanvasContext2D(width, height); + this.renderedImageCanvas_ = new ImageCanvas(extent, resolution, 1, context.canvas); + } + context.putImageData(output, 0, 0); + + this.changed(); + this.renderedRevision_ = this.getRevision(); + + this.dispatchEvent(new RasterSourceEvent(RasterEventType.AFTEROPERATIONS, frameState, data)); } - const data = {}; - this.dispatchEvent(new RasterSourceEvent(RasterEventType.BEFOREOPERATIONS, frameState, data)); - this.worker_.process(imageDatas, data, this.onWorkerComplete_.bind(this, frameState)); -}; - - -/** - * Called when pixel processing is complete. - * @param {module:ol/PluggableMap~FrameState} frameState The frame state. - * @param {Error} err Any error during processing. - * @param {ImageData} output The output image data. - * @param {Object} data The user data. - * @private - */ -RasterSource.prototype.onWorkerComplete_ = function(frameState, err, output, data) { - if (err || !output) { - return; + /** + * @override + */ + getImageInternal() { + return null; // not implemented } +} - // do nothing if extent or resolution changed - const extent = frameState.extent; - const resolution = frameState.viewState.resolution; - if (resolution !== this.requestedFrameState_.viewState.resolution || - !equals(extent, this.requestedFrameState_.extent)) { - return; - } - - let context; - if (this.renderedImageCanvas_) { - context = this.renderedImageCanvas_.getImage().getContext('2d'); - } else { - const width = Math.round(getWidth(extent) / resolution); - const height = Math.round(getHeight(extent) / resolution); - context = createCanvasContext2D(width, height); - this.renderedImageCanvas_ = new ImageCanvas(extent, resolution, 1, context.canvas); - } - context.putImageData(output, 0, 0); - - this.changed(); - this.renderedRevision_ = this.getRevision(); - - this.dispatchEvent(new RasterSourceEvent(RasterEventType.AFTEROPERATIONS, frameState, data)); -}; +inherits(RasterSource, ImageSource); /** @@ -522,12 +525,4 @@ function createTileRenderer(source) { } -/** - * @override - */ -RasterSource.prototype.getImageInternal = function() { - return null; // not implemented -}; - - export default RasterSource; diff --git a/src/ol/source/Source.js b/src/ol/source/Source.js index a07c81c15f..c16e41a232 100644 --- a/src/ol/source/Source.js +++ b/src/ol/source/Source.js @@ -51,63 +51,134 @@ import SourceState from '../source/State.js'; * @param {module:ol/source/Source~Options} options Source options. * @api */ -const Source = function(options) { +class Source { + constructor(options) { - BaseObject.call(this); + BaseObject.call(this); - /** - * @private - * @type {module:ol/proj/Projection} - */ - this.projection_ = getProjection(options.projection); + /** + * @private + * @type {module:ol/proj/Projection} + */ + this.projection_ = getProjection(options.projection); - /** - * @private - * @type {?module:ol/source/Source~Attribution} - */ - this.attributions_ = this.adaptAttributions_(options.attributions); + /** + * @private + * @type {?module:ol/source/Source~Attribution} + */ + this.attributions_ = this.adaptAttributions_(options.attributions); - /** - * @private - * @type {module:ol/source/State} - */ - this.state_ = options.state !== undefined ? - options.state : SourceState.READY; + /** + * @private + * @type {module:ol/source/State} + */ + this.state_ = options.state !== undefined ? + options.state : SourceState.READY; - /** - * @private - * @type {boolean} - */ - this.wrapX_ = options.wrapX !== undefined ? options.wrapX : false; + /** + * @private + * @type {boolean} + */ + this.wrapX_ = options.wrapX !== undefined ? options.wrapX : false; -}; + } + + /** + * Turns the attributions option into an attributions function. + * @param {module:ol/source/Source~AttributionLike|undefined} attributionLike The attribution option. + * @return {?module:ol/source/Source~Attribution} An attribution function (or null). + */ + adaptAttributions_(attributionLike) { + if (!attributionLike) { + return null; + } + if (Array.isArray(attributionLike)) { + return function(frameState) { + return attributionLike; + }; + } + + if (typeof attributionLike === 'function') { + return attributionLike; + } + + return function(frameState) { + return [attributionLike]; + }; + } + + /** + * Get the attribution function for the source. + * @return {?module:ol/source/Source~Attribution} Attribution function. + */ + getAttributions() { + return this.attributions_; + } + + /** + * Get the projection of the source. + * @return {module:ol/proj/Projection} Projection. + * @api + */ + getProjection() { + return this.projection_; + } + + /** + * @abstract + * @return {Array.|undefined} Resolutions. + */ + getResolutions() {} + + /** + * Get the state of the source, see {@link module:ol/source/State~State} for possible states. + * @return {module:ol/source/State} State. + * @api + */ + getState() { + return this.state_; + } + + /** + * @return {boolean|undefined} Wrap X. + */ + getWrapX() { + return this.wrapX_; + } + + /** + * Refreshes the source and finally dispatches a 'change' event. + * @api + */ + refresh() { + this.changed(); + } + + /** + * Set the attributions of the source. + * @param {module:ol/source/Source~AttributionLike|undefined} attributions Attributions. + * Can be passed as `string`, `Array`, `{@link module:ol/source/Source~Attribution}`, + * or `undefined`. + * @api + */ + setAttributions(attributions) { + this.attributions_ = this.adaptAttributions_(attributions); + this.changed(); + } + + /** + * Set the state of the source. + * @param {module:ol/source/State} state State. + * @protected + */ + setState(state) { + this.state_ = state; + this.changed(); + } +} inherits(Source, BaseObject); -/** - * Turns the attributions option into an attributions function. - * @param {module:ol/source/Source~AttributionLike|undefined} attributionLike The attribution option. - * @return {?module:ol/source/Source~Attribution} An attribution function (or null). - */ -Source.prototype.adaptAttributions_ = function(attributionLike) { - if (!attributionLike) { - return null; - } - if (Array.isArray(attributionLike)) { - return function(frameState) { - return attributionLike; - }; - } - - if (typeof attributionLike === 'function') { - return attributionLike; - } - - return function(frameState) { - return [attributionLike]; - }; -}; - /** * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. * @param {number} resolution Resolution. @@ -121,79 +192,4 @@ Source.prototype.adaptAttributions_ = function(attributionLike) { Source.prototype.forEachFeatureAtCoordinate = UNDEFINED; -/** - * Get the attribution function for the source. - * @return {?module:ol/source/Source~Attribution} Attribution function. - */ -Source.prototype.getAttributions = function() { - return this.attributions_; -}; - - -/** - * Get the projection of the source. - * @return {module:ol/proj/Projection} Projection. - * @api - */ -Source.prototype.getProjection = function() { - return this.projection_; -}; - - -/** - * @abstract - * @return {Array.|undefined} Resolutions. - */ -Source.prototype.getResolutions = function() {}; - - -/** - * Get the state of the source, see {@link module:ol/source/State~State} for possible states. - * @return {module:ol/source/State} State. - * @api - */ -Source.prototype.getState = function() { - return this.state_; -}; - - -/** - * @return {boolean|undefined} Wrap X. - */ -Source.prototype.getWrapX = function() { - return this.wrapX_; -}; - - -/** - * Refreshes the source and finally dispatches a 'change' event. - * @api - */ -Source.prototype.refresh = function() { - this.changed(); -}; - - -/** - * Set the attributions of the source. - * @param {module:ol/source/Source~AttributionLike|undefined} attributions Attributions. - * Can be passed as `string`, `Array`, `{@link module:ol/source/Source~Attribution}`, - * or `undefined`. - * @api - */ -Source.prototype.setAttributions = function(attributions) { - this.attributions_ = this.adaptAttributions_(attributions); - this.changed(); -}; - - -/** - * Set the state of the source. - * @param {module:ol/source/State} state State. - * @protected - */ -Source.prototype.setState = function(state) { - this.state_ = state; - this.changed(); -}; export default Source; diff --git a/src/ol/source/Tile.js b/src/ol/source/Tile.js index 1f3d7f44ae..7435278073 100644 --- a/src/ol/source/Tile.js +++ b/src/ol/source/Tile.js @@ -39,277 +39,263 @@ import {wrapX, getForProjection as getTileGridForProjection} from '../tilegrid.j * @param {module:ol/source/Tile~Options=} options SourceTile source options. * @api */ -const TileSource = function(options) { +class TileSource { + constructor(options) { - Source.call(this, { - attributions: options.attributions, - extent: options.extent, - projection: options.projection, - state: options.state, - wrapX: options.wrapX - }); + Source.call(this, { + attributions: options.attributions, + extent: options.extent, + projection: options.projection, + state: options.state, + wrapX: options.wrapX + }); - /** - * @private - * @type {boolean} - */ - this.opaque_ = options.opaque !== undefined ? options.opaque : false; + /** + * @private + * @type {boolean} + */ + this.opaque_ = options.opaque !== undefined ? options.opaque : false; - /** - * @private - * @type {number} - */ - this.tilePixelRatio_ = options.tilePixelRatio !== undefined ? - options.tilePixelRatio : 1; + /** + * @private + * @type {number} + */ + this.tilePixelRatio_ = options.tilePixelRatio !== undefined ? + options.tilePixelRatio : 1; - /** - * @protected - * @type {module:ol/tilegrid/TileGrid} - */ - this.tileGrid = options.tileGrid !== undefined ? options.tileGrid : null; + /** + * @protected + * @type {module:ol/tilegrid/TileGrid} + */ + this.tileGrid = options.tileGrid !== undefined ? options.tileGrid : null; - /** - * @protected - * @type {module:ol/TileCache} - */ - this.tileCache = new TileCache(options.cacheSize); + /** + * @protected + * @type {module:ol/TileCache} + */ + this.tileCache = new TileCache(options.cacheSize); - /** - * @protected - * @type {module:ol/size~Size} - */ - this.tmpSize = [0, 0]; + /** + * @protected + * @type {module:ol/size~Size} + */ + this.tmpSize = [0, 0]; - /** - * @private - * @type {string} - */ - this.key_ = ''; + /** + * @private + * @type {string} + */ + this.key_ = ''; - /** - * @protected - * @type {module:ol/Tile~Options} - */ - this.tileOptions = {transition: options.transition}; + /** + * @protected + * @type {module:ol/Tile~Options} + */ + this.tileOptions = {transition: options.transition}; -}; - -inherits(TileSource, Source); - - -/** - * @return {boolean} Can expire cache. - */ -TileSource.prototype.canExpireCache = function() { - return this.tileCache.canExpireCache(); -}; - - -/** - * @param {module:ol/proj/Projection} projection Projection. - * @param {!Object.} usedTiles Used tiles. - */ -TileSource.prototype.expireCache = function(projection, usedTiles) { - const tileCache = this.getTileCacheForProjection(projection); - if (tileCache) { - tileCache.expireCache(usedTiles); - } -}; - - -/** - * @param {module:ol/proj/Projection} projection Projection. - * @param {number} z Zoom level. - * @param {module:ol/TileRange} tileRange Tile range. - * @param {function(module:ol/Tile):(boolean|undefined)} callback Called with each - * loaded tile. If the callback returns `false`, the tile will not be - * considered loaded. - * @return {boolean} The tile range is fully covered with loaded tiles. - */ -TileSource.prototype.forEachLoadedTile = function(projection, z, tileRange, callback) { - const tileCache = this.getTileCacheForProjection(projection); - if (!tileCache) { - return false; } - let covered = true; - let tile, tileCoordKey, loaded; - for (let x = tileRange.minX; x <= tileRange.maxX; ++x) { - for (let y = tileRange.minY; y <= tileRange.maxY; ++y) { - tileCoordKey = getKeyZXY(z, x, y); - loaded = false; - if (tileCache.containsKey(tileCoordKey)) { - tile = /** @type {!module:ol/Tile} */ (tileCache.get(tileCoordKey)); - loaded = tile.getState() === TileState.LOADED; - if (loaded) { - loaded = (callback(tile) !== false); - } - } - if (!loaded) { - covered = false; - } + /** + * @return {boolean} Can expire cache. + */ + canExpireCache() { + return this.tileCache.canExpireCache(); + } + + /** + * @param {module:ol/proj/Projection} projection Projection. + * @param {!Object.} usedTiles Used tiles. + */ + expireCache(projection, usedTiles) { + const tileCache = this.getTileCacheForProjection(projection); + if (tileCache) { + tileCache.expireCache(usedTiles); } } - return covered; -}; + /** + * @param {module:ol/proj/Projection} projection Projection. + * @param {number} z Zoom level. + * @param {module:ol/TileRange} tileRange Tile range. + * @param {function(module:ol/Tile):(boolean|undefined)} callback Called with each + * loaded tile. If the callback returns `false`, the tile will not be + * considered loaded. + * @return {boolean} The tile range is fully covered with loaded tiles. + */ + forEachLoadedTile(projection, z, tileRange, callback) { + const tileCache = this.getTileCacheForProjection(projection); + if (!tileCache) { + return false; + } -/** - * @param {module:ol/proj/Projection} projection Projection. - * @return {number} Gutter. - */ -TileSource.prototype.getGutter = function(projection) { - return 0; -}; - - -/** - * Return the key to be used for all tiles in the source. - * @return {string} The key for all tiles. - * @protected - */ -TileSource.prototype.getKey = function() { - return this.key_; -}; - - -/** - * Set the value to be used as the key for all tiles in the source. - * @param {string} key The key for tiles. - * @protected - */ -TileSource.prototype.setKey = function(key) { - if (this.key_ !== key) { - this.key_ = key; - this.changed(); + let covered = true; + let tile, tileCoordKey, loaded; + for (let x = tileRange.minX; x <= tileRange.maxX; ++x) { + for (let y = tileRange.minY; y <= tileRange.maxY; ++y) { + tileCoordKey = getKeyZXY(z, x, y); + loaded = false; + if (tileCache.containsKey(tileCoordKey)) { + tile = /** @type {!module:ol/Tile} */ (tileCache.get(tileCoordKey)); + loaded = tile.getState() === TileState.LOADED; + if (loaded) { + loaded = (callback(tile) !== false); + } + } + if (!loaded) { + covered = false; + } + } + } + return covered; } -}; + /** + * @param {module:ol/proj/Projection} projection Projection. + * @return {number} Gutter. + */ + getGutter(projection) { + return 0; + } -/** - * @param {module:ol/proj/Projection} projection Projection. - * @return {boolean} Opaque. - */ -TileSource.prototype.getOpaque = function(projection) { - return this.opaque_; -}; + /** + * Return the key to be used for all tiles in the source. + * @return {string} The key for all tiles. + * @protected + */ + getKey() { + return this.key_; + } + /** + * Set the value to be used as the key for all tiles in the source. + * @param {string} key The key for tiles. + * @protected + */ + setKey(key) { + if (this.key_ !== key) { + this.key_ = key; + this.changed(); + } + } -/** - * @inheritDoc - */ -TileSource.prototype.getResolutions = function() { - return this.tileGrid.getResolutions(); -}; + /** + * @param {module:ol/proj/Projection} projection Projection. + * @return {boolean} Opaque. + */ + getOpaque(projection) { + return this.opaque_; + } + /** + * @inheritDoc + */ + getResolutions() { + return this.tileGrid.getResolutions(); + } -/** - * @abstract - * @param {number} z Tile coordinate z. - * @param {number} x Tile coordinate x. - * @param {number} y Tile coordinate y. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @return {!module:ol/Tile} Tile. - */ -TileSource.prototype.getTile = function(z, x, y, pixelRatio, projection) {}; + /** + * @abstract + * @param {number} z Tile coordinate z. + * @param {number} x Tile coordinate x. + * @param {number} y Tile coordinate y. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @return {!module:ol/Tile} Tile. + */ + getTile(z, x, y, pixelRatio, projection) {} - -/** - * Return the tile grid of the tile source. - * @return {module:ol/tilegrid/TileGrid} Tile grid. - * @api - */ -TileSource.prototype.getTileGrid = function() { - return this.tileGrid; -}; - - -/** - * @param {module:ol/proj/Projection} projection Projection. - * @return {!module:ol/tilegrid/TileGrid} Tile grid. - */ -TileSource.prototype.getTileGridForProjection = function(projection) { - if (!this.tileGrid) { - return getTileGridForProjection(projection); - } else { + /** + * Return the tile grid of the tile source. + * @return {module:ol/tilegrid/TileGrid} Tile grid. + * @api + */ + getTileGrid() { return this.tileGrid; } -}; - -/** - * @param {module:ol/proj/Projection} projection Projection. - * @return {module:ol/TileCache} Tile cache. - * @protected - */ -TileSource.prototype.getTileCacheForProjection = function(projection) { - const thisProj = this.getProjection(); - if (thisProj && !equivalent(thisProj, projection)) { - return null; - } else { - return this.tileCache; + /** + * @param {module:ol/proj/Projection} projection Projection. + * @return {!module:ol/tilegrid/TileGrid} Tile grid. + */ + getTileGridForProjection(projection) { + if (!this.tileGrid) { + return getTileGridForProjection(projection); + } else { + return this.tileGrid; + } } -}; - -/** - * Get the tile pixel ratio for this source. Subclasses may override this - * method, which is meant to return a supported pixel ratio that matches the - * provided `pixelRatio` as close as possible. - * @param {number} pixelRatio Pixel ratio. - * @return {number} Tile pixel ratio. - */ -TileSource.prototype.getTilePixelRatio = function(pixelRatio) { - return this.tilePixelRatio_; -}; - - -/** - * @param {number} z Z. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @return {module:ol/size~Size} Tile size. - */ -TileSource.prototype.getTilePixelSize = function(z, pixelRatio, projection) { - const tileGrid = this.getTileGridForProjection(projection); - const tilePixelRatio = this.getTilePixelRatio(pixelRatio); - const tileSize = toSize(tileGrid.getTileSize(z), this.tmpSize); - if (tilePixelRatio == 1) { - return tileSize; - } else { - return scaleSize(tileSize, tilePixelRatio, this.tmpSize); + /** + * @param {module:ol/proj/Projection} projection Projection. + * @return {module:ol/TileCache} Tile cache. + * @protected + */ + getTileCacheForProjection(projection) { + const thisProj = this.getProjection(); + if (thisProj && !equivalent(thisProj, projection)) { + return null; + } else { + return this.tileCache; + } } -}; - -/** - * Returns a tile coordinate wrapped around the x-axis. When the tile coordinate - * is outside the resolution and extent range of the tile grid, `null` will be - * returned. - * @param {module:ol/tilecoord~TileCoord} tileCoord Tile coordinate. - * @param {module:ol/proj/Projection=} opt_projection Projection. - * @return {module:ol/tilecoord~TileCoord} Tile coordinate to be passed to the tileUrlFunction or - * null if no tile URL should be created for the passed `tileCoord`. - */ -TileSource.prototype.getTileCoordForTileUrlFunction = function(tileCoord, opt_projection) { - const projection = opt_projection !== undefined ? - opt_projection : this.getProjection(); - const tileGrid = this.getTileGridForProjection(projection); - if (this.getWrapX() && projection.isGlobal()) { - tileCoord = wrapX(tileGrid, tileCoord, projection); + /** + * Get the tile pixel ratio for this source. Subclasses may override this + * method, which is meant to return a supported pixel ratio that matches the + * provided `pixelRatio` as close as possible. + * @param {number} pixelRatio Pixel ratio. + * @return {number} Tile pixel ratio. + */ + getTilePixelRatio(pixelRatio) { + return this.tilePixelRatio_; } - return withinExtentAndZ(tileCoord, tileGrid) ? tileCoord : null; -}; + /** + * @param {number} z Z. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @return {module:ol/size~Size} Tile size. + */ + getTilePixelSize(z, pixelRatio, projection) { + const tileGrid = this.getTileGridForProjection(projection); + const tilePixelRatio = this.getTilePixelRatio(pixelRatio); + const tileSize = toSize(tileGrid.getTileSize(z), this.tmpSize); + if (tilePixelRatio == 1) { + return tileSize; + } else { + return scaleSize(tileSize, tilePixelRatio, this.tmpSize); + } + } -/** - * @inheritDoc - */ -TileSource.prototype.refresh = function() { - this.tileCache.clear(); - this.changed(); -}; + /** + * Returns a tile coordinate wrapped around the x-axis. When the tile coordinate + * is outside the resolution and extent range of the tile grid, `null` will be + * returned. + * @param {module:ol/tilecoord~TileCoord} tileCoord Tile coordinate. + * @param {module:ol/proj/Projection=} opt_projection Projection. + * @return {module:ol/tilecoord~TileCoord} Tile coordinate to be passed to the tileUrlFunction or + * null if no tile URL should be created for the passed `tileCoord`. + */ + getTileCoordForTileUrlFunction(tileCoord, opt_projection) { + const projection = opt_projection !== undefined ? + opt_projection : this.getProjection(); + const tileGrid = this.getTileGridForProjection(projection); + if (this.getWrapX() && projection.isGlobal()) { + tileCoord = wrapX(tileGrid, tileCoord, projection); + } + return withinExtentAndZ(tileCoord, tileGrid) ? tileCoord : null; + } + + /** + * @inheritDoc + */ + refresh() { + this.tileCache.clear(); + this.changed(); + } +} + +inherits(TileSource, Source); /** diff --git a/src/ol/source/TileArcGISRest.js b/src/ol/source/TileArcGISRest.js index 5fa0056cbf..b06e72493c 100644 --- a/src/ol/source/TileArcGISRest.js +++ b/src/ol/source/TileArcGISRest.js @@ -64,162 +64,159 @@ import {appendParams} from '../uri.js'; * @param {module:ol/source/TileArcGISRest~Options=} opt_options Tile ArcGIS Rest options. * @api */ -const TileArcGISRest = function(opt_options) { +class TileArcGISRest { + constructor(opt_options) { - const options = opt_options || {}; + const options = opt_options || {}; - TileImage.call(this, { - attributions: options.attributions, - cacheSize: options.cacheSize, - crossOrigin: options.crossOrigin, - projection: options.projection, - reprojectionErrorThreshold: options.reprojectionErrorThreshold, - tileGrid: options.tileGrid, - tileLoadFunction: options.tileLoadFunction, - url: options.url, - urls: options.urls, - wrapX: options.wrapX !== undefined ? options.wrapX : true, - transition: options.transition - }); + TileImage.call(this, { + attributions: options.attributions, + cacheSize: options.cacheSize, + crossOrigin: options.crossOrigin, + projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, + tileGrid: options.tileGrid, + tileLoadFunction: options.tileLoadFunction, + url: options.url, + urls: options.urls, + wrapX: options.wrapX !== undefined ? options.wrapX : true, + transition: options.transition + }); + + /** + * @private + * @type {!Object} + */ + this.params_ = options.params || {}; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.tmpExtent_ = createEmpty(); + + this.setKey(this.getKeyForParams_()); + } /** * @private - * @type {!Object} + * @return {string} The key for the current params. */ - this.params_ = options.params || {}; + getKeyForParams_() { + let i = 0; + const res = []; + for (const key in this.params_) { + res[i++] = key + '-' + this.params_[key]; + } + return res.join('/'); + } /** - * @private - * @type {module:ol/extent~Extent} + * Get the user-provided params, i.e. those passed to the constructor through + * the "params" option, and possibly updated using the updateParams method. + * @return {Object} Params. + * @api */ - this.tmpExtent_ = createEmpty(); + getParams() { + return this.params_; + } - this.setKey(this.getKeyForParams_()); -}; + /** + * @param {module:ol/tilecoord~TileCoord} tileCoord Tile coordinate. + * @param {module:ol/size~Size} tileSize Tile size. + * @param {module:ol/extent~Extent} tileExtent Tile extent. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @param {Object} params Params. + * @return {string|undefined} Request URL. + * @private + */ + getRequestUrl_(tileCoord, tileSize, tileExtent, pixelRatio, projection, params) { + + const urls = this.urls; + if (!urls) { + return undefined; + } + + // ArcGIS Server only wants the numeric portion of the projection ID. + const srid = projection.getCode().split(':').pop(); + + params['SIZE'] = tileSize[0] + ',' + tileSize[1]; + params['BBOX'] = tileExtent.join(','); + params['BBOXSR'] = srid; + params['IMAGESR'] = srid; + params['DPI'] = Math.round( + params['DPI'] ? params['DPI'] * pixelRatio : 90 * pixelRatio + ); + + let url; + if (urls.length == 1) { + url = urls[0]; + } else { + const index = modulo(tileCoordHash(tileCoord), urls.length); + url = urls[index]; + } + + const modifiedUrl = url + .replace(/MapServer\/?$/, 'MapServer/export') + .replace(/ImageServer\/?$/, 'ImageServer/exportImage'); + return appendParams(modifiedUrl, params); + } + + /** + * @inheritDoc + */ + getTilePixelRatio(pixelRatio) { + return /** @type {number} */ (pixelRatio); + } + + /** + * @inheritDoc + */ + fixedTileUrlFunction(tileCoord, pixelRatio, projection) { + + let tileGrid = this.getTileGrid(); + if (!tileGrid) { + tileGrid = this.getTileGridForProjection(projection); + } + + if (tileGrid.getResolutions().length <= tileCoord[0]) { + return undefined; + } + + const tileExtent = tileGrid.getTileCoordExtent( + tileCoord, this.tmpExtent_); + let tileSize = toSize( + tileGrid.getTileSize(tileCoord[0]), this.tmpSize); + + if (pixelRatio != 1) { + tileSize = scaleSize(tileSize, pixelRatio, this.tmpSize); + } + + // Apply default params and override with user specified values. + const baseParams = { + 'F': 'image', + 'FORMAT': 'PNG32', + 'TRANSPARENT': true + }; + assign(baseParams, this.params_); + + return this.getRequestUrl_(tileCoord, tileSize, tileExtent, + pixelRatio, projection, baseParams); + } + + /** + * Update the user-provided params. + * @param {Object} params Params. + * @api + */ + updateParams(params) { + assign(this.params_, params); + this.setKey(this.getKeyForParams_()); + } +} inherits(TileArcGISRest, TileImage); -/** - * @private - * @return {string} The key for the current params. - */ -TileArcGISRest.prototype.getKeyForParams_ = function() { - let i = 0; - const res = []; - for (const key in this.params_) { - res[i++] = key + '-' + this.params_[key]; - } - return res.join('/'); -}; - - -/** - * Get the user-provided params, i.e. those passed to the constructor through - * the "params" option, and possibly updated using the updateParams method. - * @return {Object} Params. - * @api - */ -TileArcGISRest.prototype.getParams = function() { - return this.params_; -}; - - -/** - * @param {module:ol/tilecoord~TileCoord} tileCoord Tile coordinate. - * @param {module:ol/size~Size} tileSize Tile size. - * @param {module:ol/extent~Extent} tileExtent Tile extent. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @param {Object} params Params. - * @return {string|undefined} Request URL. - * @private - */ -TileArcGISRest.prototype.getRequestUrl_ = function(tileCoord, tileSize, tileExtent, - pixelRatio, projection, params) { - - const urls = this.urls; - if (!urls) { - return undefined; - } - - // ArcGIS Server only wants the numeric portion of the projection ID. - const srid = projection.getCode().split(':').pop(); - - params['SIZE'] = tileSize[0] + ',' + tileSize[1]; - params['BBOX'] = tileExtent.join(','); - params['BBOXSR'] = srid; - params['IMAGESR'] = srid; - params['DPI'] = Math.round( - params['DPI'] ? params['DPI'] * pixelRatio : 90 * pixelRatio - ); - - let url; - if (urls.length == 1) { - url = urls[0]; - } else { - const index = modulo(tileCoordHash(tileCoord), urls.length); - url = urls[index]; - } - - const modifiedUrl = url - .replace(/MapServer\/?$/, 'MapServer/export') - .replace(/ImageServer\/?$/, 'ImageServer/exportImage'); - return appendParams(modifiedUrl, params); -}; - - -/** - * @inheritDoc - */ -TileArcGISRest.prototype.getTilePixelRatio = function(pixelRatio) { - return /** @type {number} */ (pixelRatio); -}; - - -/** - * @inheritDoc - */ -TileArcGISRest.prototype.fixedTileUrlFunction = function(tileCoord, pixelRatio, projection) { - - let tileGrid = this.getTileGrid(); - if (!tileGrid) { - tileGrid = this.getTileGridForProjection(projection); - } - - if (tileGrid.getResolutions().length <= tileCoord[0]) { - return undefined; - } - - const tileExtent = tileGrid.getTileCoordExtent( - tileCoord, this.tmpExtent_); - let tileSize = toSize( - tileGrid.getTileSize(tileCoord[0]), this.tmpSize); - - if (pixelRatio != 1) { - tileSize = scaleSize(tileSize, pixelRatio, this.tmpSize); - } - - // Apply default params and override with user specified values. - const baseParams = { - 'F': 'image', - 'FORMAT': 'PNG32', - 'TRANSPARENT': true - }; - assign(baseParams, this.params_); - - return this.getRequestUrl_(tileCoord, tileSize, tileExtent, - pixelRatio, projection, baseParams); -}; - - -/** - * Update the user-provided params. - * @param {Object} params Params. - * @api - */ -TileArcGISRest.prototype.updateParams = function(params) { - assign(this.params_, params); - this.setKey(this.getKeyForParams_()); -}; export default TileArcGISRest; diff --git a/src/ol/source/TileDebug.js b/src/ol/source/TileDebug.js index 4b78d9ecde..20eefc9d78 100644 --- a/src/ol/source/TileDebug.js +++ b/src/ol/source/TileDebug.js @@ -17,64 +17,65 @@ import {getKeyZXY} from '../tilecoord.js'; * @param {module:ol/size~Size} tileSize Tile size. * @param {string} text Text. */ -const LabeledTile = function(tileCoord, tileSize, text) { +class LabeledTile { + constructor(tileCoord, tileSize, text) { - Tile.call(this, tileCoord, TileState.LOADED); + Tile.call(this, tileCoord, TileState.LOADED); - /** - * @private - * @type {module:ol/size~Size} - */ - this.tileSize_ = tileSize; + /** + * @private + * @type {module:ol/size~Size} + */ + this.tileSize_ = tileSize; - /** - * @private - * @type {string} - */ - this.text_ = text; + /** + * @private + * @type {string} + */ + this.text_ = text; - /** - * @private - * @type {HTMLCanvasElement} - */ - this.canvas_ = null; + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = null; + + } + + /** + * Get the image element for this tile. + * @return {HTMLCanvasElement} Image. + */ + getImage() { + if (this.canvas_) { + return this.canvas_; + } else { + const tileSize = this.tileSize_; + const context = createCanvasContext2D(tileSize[0], tileSize[1]); + + context.strokeStyle = 'black'; + context.strokeRect(0.5, 0.5, tileSize[0] + 0.5, tileSize[1] + 0.5); + + context.fillStyle = 'black'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.font = '24px sans-serif'; + context.fillText(this.text_, tileSize[0] / 2, tileSize[1] / 2); + + this.canvas_ = context.canvas; + return context.canvas; + } + } + + /** + * @override + */ + load() {} +} -}; inherits(LabeledTile, Tile); -/** - * Get the image element for this tile. - * @return {HTMLCanvasElement} Image. - */ -LabeledTile.prototype.getImage = function() { - if (this.canvas_) { - return this.canvas_; - } else { - const tileSize = this.tileSize_; - const context = createCanvasContext2D(tileSize[0], tileSize[1]); - - context.strokeStyle = 'black'; - context.strokeRect(0.5, 0.5, tileSize[0] + 0.5, tileSize[1] + 0.5); - - context.fillStyle = 'black'; - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.font = '24px sans-serif'; - context.fillText(this.text_, tileSize[0] / 2, tileSize[1] / 2); - - this.canvas_ = context.canvas; - return context.canvas; - } -}; - - -/** - * @override - */ -LabeledTile.prototype.load = function() {}; - - /** * @typedef {Object} Options * @property {module:ol/proj~ProjectionLike} projection Projection. @@ -96,38 +97,39 @@ LabeledTile.prototype.load = function() {}; * @param {module:ol/source/TileDebug~Options=} options Debug tile options. * @api */ -const TileDebug = function(options) { +class TileDebug { + constructor(options) { - TileSource.call(this, { - opaque: false, - projection: options.projection, - tileGrid: options.tileGrid, - wrapX: options.wrapX !== undefined ? options.wrapX : true - }); + TileSource.call(this, { + opaque: false, + projection: options.projection, + tileGrid: options.tileGrid, + wrapX: options.wrapX !== undefined ? options.wrapX : true + }); -}; + } + + /** + * @inheritDoc + */ + getTile(z, x, y) { + const tileCoordKey = getKeyZXY(z, x, y); + if (this.tileCache.containsKey(tileCoordKey)) { + return /** @type {!module:ol/source/TileDebug~LabeledTile} */ (this.tileCache.get(tileCoordKey)); + } else { + const tileSize = toSize(this.tileGrid.getTileSize(z)); + const tileCoord = [z, x, y]; + const textTileCoord = this.getTileCoordForTileUrlFunction(tileCoord); + const text = !textTileCoord ? '' : + this.getTileCoordForTileUrlFunction(textTileCoord).toString(); + const tile = new LabeledTile(tileCoord, tileSize, text); + this.tileCache.set(tileCoordKey, tile); + return tile; + } + } +} inherits(TileDebug, TileSource); -/** - * @inheritDoc - */ -TileDebug.prototype.getTile = function(z, x, y) { - const tileCoordKey = getKeyZXY(z, x, y); - if (this.tileCache.containsKey(tileCoordKey)) { - return /** @type {!module:ol/source/TileDebug~LabeledTile} */ (this.tileCache.get(tileCoordKey)); - } else { - const tileSize = toSize(this.tileGrid.getTileSize(z)); - const tileCoord = [z, x, y]; - const textTileCoord = this.getTileCoordForTileUrlFunction(tileCoord); - const text = !textTileCoord ? '' : - this.getTileCoordForTileUrlFunction(textTileCoord).toString(); - const tile = new LabeledTile(tileCoord, tileSize, text); - this.tileCache.set(tileCoordKey, tile); - return tile; - } -}; - - export default TileDebug; diff --git a/src/ol/source/TileImage.js b/src/ol/source/TileImage.js index 887412c2f8..f59a06ec41 100644 --- a/src/ol/source/TileImage.js +++ b/src/ol/source/TileImage.js @@ -64,344 +64,334 @@ import {getForProjection as getTileGridForProjection} from '../tilegrid.js'; * @param {module:ol/source/TileImage~Options=} options Image tile options. * @api */ -const TileImage = function(options) { +class TileImage { + constructor(options) { - UrlTile.call(this, { - attributions: options.attributions, - cacheSize: options.cacheSize, - extent: options.extent, - opaque: options.opaque, - projection: options.projection, - state: options.state, - tileGrid: options.tileGrid, - tileLoadFunction: options.tileLoadFunction ? - options.tileLoadFunction : defaultTileLoadFunction, - tilePixelRatio: options.tilePixelRatio, - tileUrlFunction: options.tileUrlFunction, - url: options.url, - urls: options.urls, - wrapX: options.wrapX, - transition: options.transition - }); + UrlTile.call(this, { + attributions: options.attributions, + cacheSize: options.cacheSize, + extent: options.extent, + opaque: options.opaque, + projection: options.projection, + state: options.state, + tileGrid: options.tileGrid, + tileLoadFunction: options.tileLoadFunction ? + options.tileLoadFunction : defaultTileLoadFunction, + tilePixelRatio: options.tilePixelRatio, + tileUrlFunction: options.tileUrlFunction, + url: options.url, + urls: options.urls, + wrapX: options.wrapX, + transition: options.transition + }); + + /** + * @protected + * @type {?string} + */ + this.crossOrigin = + options.crossOrigin !== undefined ? options.crossOrigin : null; + + /** + * @protected + * @type {function(new: module:ol/ImageTile, module:ol/tilecoord~TileCoord, module:ol/TileState, string, + * ?string, module:ol/Tile~LoadFunction, module:ol/Tile~Options=)} + */ + this.tileClass = options.tileClass !== undefined ? + options.tileClass : ImageTile; + + /** + * @protected + * @type {!Object.} + */ + this.tileCacheForProjection = {}; + + /** + * @protected + * @type {!Object.} + */ + this.tileGridForProjection = {}; + + /** + * @private + * @type {number|undefined} + */ + this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold; + + /** + * @private + * @type {boolean} + */ + this.renderReprojectionEdges_ = false; + } + + /** + * @inheritDoc + */ + canExpireCache() { + if (!ENABLE_RASTER_REPROJECTION) { + return UrlTile.prototype.canExpireCache.call(this); + } + if (this.tileCache.canExpireCache()) { + return true; + } else { + for (const key in this.tileCacheForProjection) { + if (this.tileCacheForProjection[key].canExpireCache()) { + return true; + } + } + } + return false; + } + + /** + * @inheritDoc + */ + expireCache(projection, usedTiles) { + if (!ENABLE_RASTER_REPROJECTION) { + UrlTile.prototype.expireCache.call(this, projection, usedTiles); + return; + } + const usedTileCache = this.getTileCacheForProjection(projection); + + this.tileCache.expireCache(this.tileCache == usedTileCache ? usedTiles : {}); + for (const id in this.tileCacheForProjection) { + const tileCache = this.tileCacheForProjection[id]; + tileCache.expireCache(tileCache == usedTileCache ? usedTiles : {}); + } + } + + /** + * @inheritDoc + */ + getGutter(projection) { + if (ENABLE_RASTER_REPROJECTION && + this.getProjection() && projection && !equivalent(this.getProjection(), projection)) { + return 0; + } else { + return this.getGutterInternal(); + } + } /** * @protected - * @type {?string} + * @return {number} Gutter. */ - this.crossOrigin = - options.crossOrigin !== undefined ? options.crossOrigin : null; + getGutterInternal() { + return 0; + } /** - * @protected - * @type {function(new: module:ol/ImageTile, module:ol/tilecoord~TileCoord, module:ol/TileState, string, - * ?string, module:ol/Tile~LoadFunction, module:ol/Tile~Options=)} + * @inheritDoc */ - this.tileClass = options.tileClass !== undefined ? - options.tileClass : ImageTile; + getOpaque(projection) { + if (ENABLE_RASTER_REPROJECTION && + this.getProjection() && projection && !equivalent(this.getProjection(), projection)) { + return false; + } else { + return UrlTile.prototype.getOpaque.call(this, projection); + } + } /** - * @protected - * @type {!Object.} + * @inheritDoc */ - this.tileCacheForProjection = {}; + getTileGridForProjection(projection) { + if (!ENABLE_RASTER_REPROJECTION) { + return UrlTile.prototype.getTileGridForProjection.call(this, projection); + } + const thisProj = this.getProjection(); + if (this.tileGrid && (!thisProj || equivalent(thisProj, projection))) { + return this.tileGrid; + } else { + const projKey = getUid(projection).toString(); + if (!(projKey in this.tileGridForProjection)) { + this.tileGridForProjection[projKey] = getTileGridForProjection(projection); + } + return ( + /** @type {!module:ol/tilegrid/TileGrid} */ (this.tileGridForProjection[projKey]) + ); + } + } /** - * @protected - * @type {!Object.} + * @inheritDoc */ - this.tileGridForProjection = {}; + getTileCacheForProjection(projection) { + if (!ENABLE_RASTER_REPROJECTION) { + return UrlTile.prototype.getTileCacheForProjection.call(this, projection); + } + const thisProj = this.getProjection(); if (!thisProj || equivalent(thisProj, projection)) { + return this.tileCache; + } else { + const projKey = getUid(projection).toString(); + if (!(projKey in this.tileCacheForProjection)) { + this.tileCacheForProjection[projKey] = new TileCache(this.tileCache.highWaterMark); + } + return this.tileCacheForProjection[projKey]; + } + } /** + * @param {number} z Tile coordinate z. + * @param {number} x Tile coordinate x. + * @param {number} y Tile coordinate y. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @param {string} key The key set on the tile. + * @return {!module:ol/Tile} Tile. * @private - * @type {number|undefined} */ - this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold; + createTile_(z, x, y, pixelRatio, projection, key) { + const tileCoord = [z, x, y]; + const urlTileCoord = this.getTileCoordForTileUrlFunction( + tileCoord, projection); + const tileUrl = urlTileCoord ? + this.tileUrlFunction(urlTileCoord, pixelRatio, projection) : undefined; + const tile = new this.tileClass( + tileCoord, + tileUrl !== undefined ? TileState.IDLE : TileState.EMPTY, + tileUrl !== undefined ? tileUrl : '', + this.crossOrigin, + this.tileLoadFunction, + this.tileOptions); + tile.key = key; + listen(tile, EventType.CHANGE, + this.handleTileChange, this); + return tile; + } /** - * @private - * @type {boolean} + * @inheritDoc */ - this.renderReprojectionEdges_ = false; -}; + getTile(z, x, y, pixelRatio, projection) { + const sourceProjection = /** @type {!module:ol/proj/Projection} */ (this.getProjection()); + if (!ENABLE_RASTER_REPROJECTION || + !sourceProjection || !projection || equivalent(sourceProjection, projection)) { + return this.getTileInternal(z, x, y, pixelRatio, sourceProjection || projection); + } else { + const cache = this.getTileCacheForProjection(projection); + const tileCoord = [z, x, y]; + let tile; + const tileCoordKey = getKey(tileCoord); + if (cache.containsKey(tileCoordKey)) { + tile = /** @type {!module:ol/Tile} */ (cache.get(tileCoordKey)); + } + const key = this.getKey(); + if (tile && tile.key == key) { + return tile; + } else { + const sourceTileGrid = this.getTileGridForProjection(sourceProjection); + const targetTileGrid = this.getTileGridForProjection(projection); + const wrappedTileCoord = + this.getTileCoordForTileUrlFunction(tileCoord, projection); + const newTile = new ReprojTile( + sourceProjection, sourceTileGrid, + projection, targetTileGrid, + tileCoord, wrappedTileCoord, this.getTilePixelRatio(pixelRatio), + this.getGutterInternal(), + function(z, x, y, pixelRatio) { + return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); + }.bind(this), this.reprojectionErrorThreshold_, + this.renderReprojectionEdges_); + newTile.key = key; + + if (tile) { + newTile.interimTile = tile; + newTile.refreshInterimChain(); + cache.replace(tileCoordKey, newTile); + } else { + cache.set(tileCoordKey, newTile); + } + return newTile; + } + } + } + + /** + * @param {number} z Tile coordinate z. + * @param {number} x Tile coordinate x. + * @param {number} y Tile coordinate y. + * @param {number} pixelRatio Pixel ratio. + * @param {!module:ol/proj/Projection} projection Projection. + * @return {!module:ol/Tile} Tile. + * @protected + */ + getTileInternal(z, x, y, pixelRatio, projection) { + let tile = null; + const tileCoordKey = getKeyZXY(z, x, y); + const key = this.getKey(); + if (!this.tileCache.containsKey(tileCoordKey)) { + tile = this.createTile_(z, x, y, pixelRatio, projection, key); + this.tileCache.set(tileCoordKey, tile); + } else { + tile = this.tileCache.get(tileCoordKey); + if (tile.key != key) { + // The source's params changed. If the tile has an interim tile and if we + // can use it then we use it. Otherwise we create a new tile. In both + // cases we attempt to assign an interim tile to the new tile. + const interimTile = tile; + tile = this.createTile_(z, x, y, pixelRatio, projection, key); + + //make the new tile the head of the list, + if (interimTile.getState() == TileState.IDLE) { + //the old tile hasn't begun loading yet, and is now outdated, so we can simply discard it + tile.interimTile = interimTile.interimTile; + } else { + tile.interimTile = interimTile; + } + tile.refreshInterimChain(); + this.tileCache.replace(tileCoordKey, tile); + } + } + return tile; + } + + /** + * Sets whether to render reprojection edges or not (usually for debugging). + * @param {boolean} render Render the edges. + * @api + */ + setRenderReprojectionEdges(render) { + if (!ENABLE_RASTER_REPROJECTION || + this.renderReprojectionEdges_ == render) { + return; + } + this.renderReprojectionEdges_ = render; + for (const id in this.tileCacheForProjection) { + this.tileCacheForProjection[id].clear(); + } + this.changed(); + } + + /** + * Sets the tile grid to use when reprojecting the tiles to the given + * projection instead of the default tile grid for the projection. + * + * This can be useful when the default tile grid cannot be created + * (e.g. projection has no extent defined) or + * for optimization reasons (custom tile size, resolutions, ...). + * + * @param {module:ol/proj~ProjectionLike} projection Projection. + * @param {module:ol/tilegrid/TileGrid} tilegrid Tile grid to use for the projection. + * @api + */ + setTileGridForProjection(projection, tilegrid) { + if (ENABLE_RASTER_REPROJECTION) { + const proj = getProjection(projection); + if (proj) { + const projKey = getUid(proj).toString(); + if (!(projKey in this.tileGridForProjection)) { + this.tileGridForProjection[projKey] = tilegrid; + } + } + } + } +} inherits(TileImage, UrlTile); -/** - * @inheritDoc - */ -TileImage.prototype.canExpireCache = function() { - if (!ENABLE_RASTER_REPROJECTION) { - return UrlTile.prototype.canExpireCache.call(this); - } - if (this.tileCache.canExpireCache()) { - return true; - } else { - for (const key in this.tileCacheForProjection) { - if (this.tileCacheForProjection[key].canExpireCache()) { - return true; - } - } - } - return false; -}; - - -/** - * @inheritDoc - */ -TileImage.prototype.expireCache = function(projection, usedTiles) { - if (!ENABLE_RASTER_REPROJECTION) { - UrlTile.prototype.expireCache.call(this, projection, usedTiles); - return; - } - const usedTileCache = this.getTileCacheForProjection(projection); - - this.tileCache.expireCache(this.tileCache == usedTileCache ? usedTiles : {}); - for (const id in this.tileCacheForProjection) { - const tileCache = this.tileCacheForProjection[id]; - tileCache.expireCache(tileCache == usedTileCache ? usedTiles : {}); - } -}; - - -/** - * @inheritDoc - */ -TileImage.prototype.getGutter = function(projection) { - if (ENABLE_RASTER_REPROJECTION && - this.getProjection() && projection && !equivalent(this.getProjection(), projection)) { - return 0; - } else { - return this.getGutterInternal(); - } -}; - - -/** - * @protected - * @return {number} Gutter. - */ -TileImage.prototype.getGutterInternal = function() { - return 0; -}; - - -/** - * @inheritDoc - */ -TileImage.prototype.getOpaque = function(projection) { - if (ENABLE_RASTER_REPROJECTION && - this.getProjection() && projection && !equivalent(this.getProjection(), projection)) { - return false; - } else { - return UrlTile.prototype.getOpaque.call(this, projection); - } -}; - - -/** - * @inheritDoc - */ -TileImage.prototype.getTileGridForProjection = function(projection) { - if (!ENABLE_RASTER_REPROJECTION) { - return UrlTile.prototype.getTileGridForProjection.call(this, projection); - } - const thisProj = this.getProjection(); - if (this.tileGrid && (!thisProj || equivalent(thisProj, projection))) { - return this.tileGrid; - } else { - const projKey = getUid(projection).toString(); - if (!(projKey in this.tileGridForProjection)) { - this.tileGridForProjection[projKey] = getTileGridForProjection(projection); - } - return ( - /** @type {!module:ol/tilegrid/TileGrid} */ (this.tileGridForProjection[projKey]) - ); - } -}; - - -/** - * @inheritDoc - */ -TileImage.prototype.getTileCacheForProjection = function(projection) { - if (!ENABLE_RASTER_REPROJECTION) { - return UrlTile.prototype.getTileCacheForProjection.call(this, projection); - } - const thisProj = this.getProjection(); if (!thisProj || equivalent(thisProj, projection)) { - return this.tileCache; - } else { - const projKey = getUid(projection).toString(); - if (!(projKey in this.tileCacheForProjection)) { - this.tileCacheForProjection[projKey] = new TileCache(this.tileCache.highWaterMark); - } - return this.tileCacheForProjection[projKey]; - } -}; - - -/** - * @param {number} z Tile coordinate z. - * @param {number} x Tile coordinate x. - * @param {number} y Tile coordinate y. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @param {string} key The key set on the tile. - * @return {!module:ol/Tile} Tile. - * @private - */ -TileImage.prototype.createTile_ = function(z, x, y, pixelRatio, projection, key) { - const tileCoord = [z, x, y]; - const urlTileCoord = this.getTileCoordForTileUrlFunction( - tileCoord, projection); - const tileUrl = urlTileCoord ? - this.tileUrlFunction(urlTileCoord, pixelRatio, projection) : undefined; - const tile = new this.tileClass( - tileCoord, - tileUrl !== undefined ? TileState.IDLE : TileState.EMPTY, - tileUrl !== undefined ? tileUrl : '', - this.crossOrigin, - this.tileLoadFunction, - this.tileOptions); - tile.key = key; - listen(tile, EventType.CHANGE, - this.handleTileChange, this); - return tile; -}; - - -/** - * @inheritDoc - */ -TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) { - const sourceProjection = /** @type {!module:ol/proj/Projection} */ (this.getProjection()); - if (!ENABLE_RASTER_REPROJECTION || - !sourceProjection || !projection || equivalent(sourceProjection, projection)) { - return this.getTileInternal(z, x, y, pixelRatio, sourceProjection || projection); - } else { - const cache = this.getTileCacheForProjection(projection); - const tileCoord = [z, x, y]; - let tile; - const tileCoordKey = getKey(tileCoord); - if (cache.containsKey(tileCoordKey)) { - tile = /** @type {!module:ol/Tile} */ (cache.get(tileCoordKey)); - } - const key = this.getKey(); - if (tile && tile.key == key) { - return tile; - } else { - const sourceTileGrid = this.getTileGridForProjection(sourceProjection); - const targetTileGrid = this.getTileGridForProjection(projection); - const wrappedTileCoord = - this.getTileCoordForTileUrlFunction(tileCoord, projection); - const newTile = new ReprojTile( - sourceProjection, sourceTileGrid, - projection, targetTileGrid, - tileCoord, wrappedTileCoord, this.getTilePixelRatio(pixelRatio), - this.getGutterInternal(), - function(z, x, y, pixelRatio) { - return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); - }.bind(this), this.reprojectionErrorThreshold_, - this.renderReprojectionEdges_); - newTile.key = key; - - if (tile) { - newTile.interimTile = tile; - newTile.refreshInterimChain(); - cache.replace(tileCoordKey, newTile); - } else { - cache.set(tileCoordKey, newTile); - } - return newTile; - } - } -}; - - -/** - * @param {number} z Tile coordinate z. - * @param {number} x Tile coordinate x. - * @param {number} y Tile coordinate y. - * @param {number} pixelRatio Pixel ratio. - * @param {!module:ol/proj/Projection} projection Projection. - * @return {!module:ol/Tile} Tile. - * @protected - */ -TileImage.prototype.getTileInternal = function(z, x, y, pixelRatio, projection) { - let tile = null; - const tileCoordKey = getKeyZXY(z, x, y); - const key = this.getKey(); - if (!this.tileCache.containsKey(tileCoordKey)) { - tile = this.createTile_(z, x, y, pixelRatio, projection, key); - this.tileCache.set(tileCoordKey, tile); - } else { - tile = this.tileCache.get(tileCoordKey); - if (tile.key != key) { - // The source's params changed. If the tile has an interim tile and if we - // can use it then we use it. Otherwise we create a new tile. In both - // cases we attempt to assign an interim tile to the new tile. - const interimTile = tile; - tile = this.createTile_(z, x, y, pixelRatio, projection, key); - - //make the new tile the head of the list, - if (interimTile.getState() == TileState.IDLE) { - //the old tile hasn't begun loading yet, and is now outdated, so we can simply discard it - tile.interimTile = interimTile.interimTile; - } else { - tile.interimTile = interimTile; - } - tile.refreshInterimChain(); - this.tileCache.replace(tileCoordKey, tile); - } - } - return tile; -}; - - -/** - * Sets whether to render reprojection edges or not (usually for debugging). - * @param {boolean} render Render the edges. - * @api - */ -TileImage.prototype.setRenderReprojectionEdges = function(render) { - if (!ENABLE_RASTER_REPROJECTION || - this.renderReprojectionEdges_ == render) { - return; - } - this.renderReprojectionEdges_ = render; - for (const id in this.tileCacheForProjection) { - this.tileCacheForProjection[id].clear(); - } - this.changed(); -}; - - -/** - * Sets the tile grid to use when reprojecting the tiles to the given - * projection instead of the default tile grid for the projection. - * - * This can be useful when the default tile grid cannot be created - * (e.g. projection has no extent defined) or - * for optimization reasons (custom tile size, resolutions, ...). - * - * @param {module:ol/proj~ProjectionLike} projection Projection. - * @param {module:ol/tilegrid/TileGrid} tilegrid Tile grid to use for the projection. - * @api - */ -TileImage.prototype.setTileGridForProjection = function(projection, tilegrid) { - if (ENABLE_RASTER_REPROJECTION) { - const proj = getProjection(projection); - if (proj) { - const projKey = getUid(proj).toString(); - if (!(projKey in this.tileGridForProjection)) { - this.tileGridForProjection[projKey] = tilegrid; - } - } - } -}; - - /** * @param {module:ol/ImageTile} imageTile Image tile. * @param {string} src Source. diff --git a/src/ol/source/TileJSON.js b/src/ol/source/TileJSON.js index 0602f0f61a..7a6ee11678 100644 --- a/src/ol/source/TileJSON.js +++ b/src/ol/source/TileJSON.js @@ -53,137 +53,136 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js'; * @param {module:ol/source/TileJSON~Options=} options TileJSON options. * @api */ -const TileJSON = function(options) { +class TileJSON { + constructor(options) { - /** - * @type {TileJSON} - * @private - */ - this.tileJSON_ = null; + /** + * @type {TileJSON} + * @private + */ + this.tileJSON_ = null; - TileImage.call(this, { - attributions: options.attributions, - cacheSize: options.cacheSize, - crossOrigin: options.crossOrigin, - projection: getProjection('EPSG:3857'), - reprojectionErrorThreshold: options.reprojectionErrorThreshold, - state: SourceState.LOADING, - tileLoadFunction: options.tileLoadFunction, - wrapX: options.wrapX !== undefined ? options.wrapX : true, - transition: options.transition - }); + TileImage.call(this, { + attributions: options.attributions, + cacheSize: options.cacheSize, + crossOrigin: options.crossOrigin, + projection: getProjection('EPSG:3857'), + reprojectionErrorThreshold: options.reprojectionErrorThreshold, + state: SourceState.LOADING, + tileLoadFunction: options.tileLoadFunction, + wrapX: options.wrapX !== undefined ? options.wrapX : true, + transition: options.transition + }); - if (options.url) { - if (options.jsonp) { - requestJSONP(options.url, this.handleTileJSONResponse.bind(this), - this.handleTileJSONError.bind(this)); + if (options.url) { + if (options.jsonp) { + requestJSONP(options.url, this.handleTileJSONResponse.bind(this), + this.handleTileJSONError.bind(this)); + } else { + const client = new XMLHttpRequest(); + client.addEventListener('load', this.onXHRLoad_.bind(this)); + client.addEventListener('error', this.onXHRError_.bind(this)); + client.open('GET', options.url); + client.send(); + } + } else if (options.tileJSON) { + this.handleTileJSONResponse(options.tileJSON); } else { - const client = new XMLHttpRequest(); - client.addEventListener('load', this.onXHRLoad_.bind(this)); - client.addEventListener('error', this.onXHRError_.bind(this)); - client.open('GET', options.url); - client.send(); + assert(false, 51); // Either `url` or `tileJSON` options must be provided } - } else if (options.tileJSON) { - this.handleTileJSONResponse(options.tileJSON); - } else { - assert(false, 51); // Either `url` or `tileJSON` options must be provided + } -}; + /** + * @private + * @param {Event} event The load event. + */ + onXHRLoad_(event) { + const client = /** @type {XMLHttpRequest} */ (event.target); + // status will be 0 for file:// urls + if (!client.status || client.status >= 200 && client.status < 300) { + let response; + try { + response = /** @type {TileJSON} */(JSON.parse(client.responseText)); + } catch (err) { + this.handleTileJSONError(); + return; + } + this.handleTileJSONResponse(response); + } else { + this.handleTileJSONError(); + } + } + + /** + * @private + * @param {Event} event The error event. + */ + onXHRError_(event) { + this.handleTileJSONError(); + } + + /** + * @return {TileJSON} The tilejson object. + * @api + */ + getTileJSON() { + return this.tileJSON_; + } + + /** + * @protected + * @param {TileJSON} tileJSON Tile JSON. + */ + handleTileJSONResponse(tileJSON) { + + const epsg4326Projection = getProjection('EPSG:4326'); + + const sourceProjection = this.getProjection(); + let extent; + if (tileJSON.bounds !== undefined) { + const transform = getTransformFromProjections( + epsg4326Projection, sourceProjection); + extent = applyTransform(tileJSON.bounds, transform); + } + + const minZoom = tileJSON.minzoom || 0; + const maxZoom = tileJSON.maxzoom || 22; + const tileGrid = createXYZ({ + extent: extentFromProjection(sourceProjection), + maxZoom: maxZoom, + minZoom: minZoom + }); + this.tileGrid = tileGrid; + + this.tileUrlFunction = createFromTemplates(tileJSON.tiles, tileGrid); + + if (tileJSON.attribution !== undefined && !this.getAttributions()) { + const attributionExtent = extent !== undefined ? + extent : epsg4326Projection.getExtent(); + + this.setAttributions(function(frameState) { + if (intersects(attributionExtent, frameState.extent)) { + return [tileJSON.attribution]; + } + return null; + }); + + } + this.tileJSON_ = tileJSON; + this.setState(SourceState.READY); + + } + + /** + * @protected + */ + handleTileJSONError() { + this.setState(SourceState.ERROR); + } +} inherits(TileJSON, TileImage); -/** - * @private - * @param {Event} event The load event. - */ -TileJSON.prototype.onXHRLoad_ = function(event) { - const client = /** @type {XMLHttpRequest} */ (event.target); - // status will be 0 for file:// urls - if (!client.status || client.status >= 200 && client.status < 300) { - let response; - try { - response = /** @type {TileJSON} */(JSON.parse(client.responseText)); - } catch (err) { - this.handleTileJSONError(); - return; - } - this.handleTileJSONResponse(response); - } else { - this.handleTileJSONError(); - } -}; - - -/** - * @private - * @param {Event} event The error event. - */ -TileJSON.prototype.onXHRError_ = function(event) { - this.handleTileJSONError(); -}; - - -/** - * @return {TileJSON} The tilejson object. - * @api - */ -TileJSON.prototype.getTileJSON = function() { - return this.tileJSON_; -}; - - -/** - * @protected - * @param {TileJSON} tileJSON Tile JSON. - */ -TileJSON.prototype.handleTileJSONResponse = function(tileJSON) { - - const epsg4326Projection = getProjection('EPSG:4326'); - - const sourceProjection = this.getProjection(); - let extent; - if (tileJSON.bounds !== undefined) { - const transform = getTransformFromProjections( - epsg4326Projection, sourceProjection); - extent = applyTransform(tileJSON.bounds, transform); - } - - const minZoom = tileJSON.minzoom || 0; - const maxZoom = tileJSON.maxzoom || 22; - const tileGrid = createXYZ({ - extent: extentFromProjection(sourceProjection), - maxZoom: maxZoom, - minZoom: minZoom - }); - this.tileGrid = tileGrid; - - this.tileUrlFunction = createFromTemplates(tileJSON.tiles, tileGrid); - - if (tileJSON.attribution !== undefined && !this.getAttributions()) { - const attributionExtent = extent !== undefined ? - extent : epsg4326Projection.getExtent(); - - this.setAttributions(function(frameState) { - if (intersects(attributionExtent, frameState.extent)) { - return [tileJSON.attribution]; - } - return null; - }); - - } - this.tileJSON_ = tileJSON; - this.setState(SourceState.READY); - -}; - - -/** - * @protected - */ -TileJSON.prototype.handleTileJSONError = function() { - this.setState(SourceState.ERROR); -}; export default TileJSON; diff --git a/src/ol/source/TileWMS.js b/src/ol/source/TileWMS.js index c1f40149a9..1e053d19c0 100644 --- a/src/ol/source/TileWMS.js +++ b/src/ol/source/TileWMS.js @@ -80,320 +80,315 @@ import {appendParams} from '../uri.js'; * @param {module:ol/source/TileWMS~Options=} [opt_options] Tile WMS options. * @api */ -const TileWMS = function(opt_options) { +class TileWMS { + constructor(opt_options) { - const options = opt_options || {}; + const options = opt_options || {}; - const params = options.params || {}; + const params = options.params || {}; - const transparent = 'TRANSPARENT' in params ? params['TRANSPARENT'] : true; + const transparent = 'TRANSPARENT' in params ? params['TRANSPARENT'] : true; - TileImage.call(this, { - attributions: options.attributions, - cacheSize: options.cacheSize, - crossOrigin: options.crossOrigin, - opaque: !transparent, - projection: options.projection, - reprojectionErrorThreshold: options.reprojectionErrorThreshold, - tileClass: options.tileClass, - tileGrid: options.tileGrid, - tileLoadFunction: options.tileLoadFunction, - url: options.url, - urls: options.urls, - wrapX: options.wrapX !== undefined ? options.wrapX : true, - transition: options.transition - }); + TileImage.call(this, { + attributions: options.attributions, + cacheSize: options.cacheSize, + crossOrigin: options.crossOrigin, + opaque: !transparent, + projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, + tileClass: options.tileClass, + tileGrid: options.tileGrid, + tileLoadFunction: options.tileLoadFunction, + url: options.url, + urls: options.urls, + wrapX: options.wrapX !== undefined ? options.wrapX : true, + transition: options.transition + }); + + /** + * @private + * @type {number} + */ + this.gutter_ = options.gutter !== undefined ? options.gutter : 0; + + /** + * @private + * @type {!Object} + */ + this.params_ = params; + + /** + * @private + * @type {boolean} + */ + this.v13_ = true; + + /** + * @private + * @type {module:ol/source/WMSServerType|undefined} + */ + this.serverType_ = /** @type {module:ol/source/WMSServerType|undefined} */ (options.serverType); + + /** + * @private + * @type {boolean} + */ + this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true; + + /** + * @private + * @type {module:ol/extent~Extent} + */ + this.tmpExtent_ = createEmpty(); + + this.updateV13_(); + this.setKey(this.getKeyForParams_()); + + } + + /** + * Return the GetFeatureInfo URL for the passed coordinate, resolution, and + * projection. Return `undefined` if the GetFeatureInfo URL cannot be + * constructed. + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {number} resolution Resolution. + * @param {module:ol/proj~ProjectionLike} projection Projection. + * @param {!Object} params GetFeatureInfo params. `INFO_FORMAT` at least should + * be provided. If `QUERY_LAYERS` is not provided then the layers specified + * in the `LAYERS` parameter will be used. `VERSION` should not be + * specified here. + * @return {string|undefined} GetFeatureInfo URL. + * @api + */ + getGetFeatureInfoUrl(coordinate, resolution, projection, params) { + const projectionObj = getProjection(projection); + const sourceProjectionObj = this.getProjection(); + + let tileGrid = this.getTileGrid(); + if (!tileGrid) { + tileGrid = this.getTileGridForProjection(projectionObj); + } + + const tileCoord = tileGrid.getTileCoordForCoordAndResolution(coordinate, resolution); + + if (tileGrid.getResolutions().length <= tileCoord[0]) { + return undefined; + } + + let tileResolution = tileGrid.getResolution(tileCoord[0]); + let tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_); + let tileSize = toSize(tileGrid.getTileSize(tileCoord[0]), this.tmpSize); + + + const gutter = this.gutter_; + if (gutter !== 0) { + tileSize = bufferSize(tileSize, gutter, this.tmpSize); + tileExtent = buffer(tileExtent, tileResolution * gutter, tileExtent); + } + + if (sourceProjectionObj && sourceProjectionObj !== projectionObj) { + tileResolution = calculateSourceResolution(sourceProjectionObj, projectionObj, coordinate, tileResolution); + tileExtent = transformExtent(tileExtent, projectionObj, sourceProjectionObj); + coordinate = transform(coordinate, projectionObj, sourceProjectionObj); + } + + const baseParams = { + 'SERVICE': 'WMS', + 'VERSION': DEFAULT_WMS_VERSION, + 'REQUEST': 'GetFeatureInfo', + 'FORMAT': 'image/png', + 'TRANSPARENT': true, + 'QUERY_LAYERS': this.params_['LAYERS'] + }; + assign(baseParams, this.params_, params); + + const x = Math.floor((coordinate[0] - tileExtent[0]) / tileResolution); + const y = Math.floor((tileExtent[3] - coordinate[1]) / tileResolution); + + baseParams[this.v13_ ? 'I' : 'X'] = x; + baseParams[this.v13_ ? 'J' : 'Y'] = y; + + return this.getRequestUrl_(tileCoord, tileSize, tileExtent, + 1, sourceProjectionObj || projectionObj, baseParams); + } + + /** + * @inheritDoc + */ + getGutterInternal() { + return this.gutter_; + } + + /** + * Get the user-provided params, i.e. those passed to the constructor through + * the "params" option, and possibly updated using the updateParams method. + * @return {Object} Params. + * @api + */ + getParams() { + return this.params_; + } + + /** + * @param {module:ol/tilecoord~TileCoord} tileCoord Tile coordinate. + * @param {module:ol/size~Size} tileSize Tile size. + * @param {module:ol/extent~Extent} tileExtent Tile extent. + * @param {number} pixelRatio Pixel ratio. + * @param {module:ol/proj/Projection} projection Projection. + * @param {Object} params Params. + * @return {string|undefined} Request URL. + * @private + */ + getRequestUrl_(tileCoord, tileSize, tileExtent, pixelRatio, projection, params) { + + const urls = this.urls; + if (!urls) { + return undefined; + } + + params['WIDTH'] = tileSize[0]; + params['HEIGHT'] = tileSize[1]; + + params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode(); + + if (!('STYLES' in this.params_)) { + params['STYLES'] = ''; + } + + if (pixelRatio != 1) { + switch (this.serverType_) { + case WMSServerType.GEOSERVER: + const dpi = (90 * pixelRatio + 0.5) | 0; + if ('FORMAT_OPTIONS' in params) { + params['FORMAT_OPTIONS'] += ';dpi:' + dpi; + } else { + params['FORMAT_OPTIONS'] = 'dpi:' + dpi; + } + break; + case WMSServerType.MAPSERVER: + params['MAP_RESOLUTION'] = 90 * pixelRatio; + break; + case WMSServerType.CARMENTA_SERVER: + case WMSServerType.QGIS: + params['DPI'] = 90 * pixelRatio; + break; + default: + assert(false, 52); // Unknown `serverType` configured + break; + } + } + + const axisOrientation = projection.getAxisOrientation(); + const bbox = tileExtent; + if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') { + let tmp; + tmp = tileExtent[0]; + bbox[0] = tileExtent[1]; + bbox[1] = tmp; + tmp = tileExtent[2]; + bbox[2] = tileExtent[3]; + bbox[3] = tmp; + } + params['BBOX'] = bbox.join(','); + + let url; + if (urls.length == 1) { + url = urls[0]; + } else { + const index = modulo(tileCoordHash(tileCoord), urls.length); + url = urls[index]; + } + return appendParams(url, params); + } + + /** + * @inheritDoc + */ + getTilePixelRatio(pixelRatio) { + return (!this.hidpi_ || this.serverType_ === undefined) ? 1 : + /** @type {number} */ (pixelRatio); + } /** * @private - * @type {number} + * @return {string} The key for the current params. */ - this.gutter_ = options.gutter !== undefined ? options.gutter : 0; + getKeyForParams_() { + let i = 0; + const res = []; + for (const key in this.params_) { + res[i++] = key + '-' + this.params_[key]; + } + return res.join('/'); + } + + /** + * @inheritDoc + */ + fixedTileUrlFunction(tileCoord, pixelRatio, projection) { + + let tileGrid = this.getTileGrid(); + if (!tileGrid) { + tileGrid = this.getTileGridForProjection(projection); + } + + if (tileGrid.getResolutions().length <= tileCoord[0]) { + return undefined; + } + + if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) { + pixelRatio = 1; + } + + const tileResolution = tileGrid.getResolution(tileCoord[0]); + let tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_); + let tileSize = toSize( + tileGrid.getTileSize(tileCoord[0]), this.tmpSize); + + const gutter = this.gutter_; + if (gutter !== 0) { + tileSize = bufferSize(tileSize, gutter, this.tmpSize); + tileExtent = buffer(tileExtent, tileResolution * gutter, tileExtent); + } + + if (pixelRatio != 1) { + tileSize = scaleSize(tileSize, pixelRatio, this.tmpSize); + } + + const baseParams = { + 'SERVICE': 'WMS', + 'VERSION': DEFAULT_WMS_VERSION, + 'REQUEST': 'GetMap', + 'FORMAT': 'image/png', + 'TRANSPARENT': true + }; + assign(baseParams, this.params_); + + return this.getRequestUrl_(tileCoord, tileSize, tileExtent, + pixelRatio, projection, baseParams); + } + + /** + * Update the user-provided params. + * @param {Object} params Params. + * @api + */ + updateParams(params) { + assign(this.params_, params); + this.updateV13_(); + this.setKey(this.getKeyForParams_()); + } /** * @private - * @type {!Object} */ - this.params_ = params; - - /** - * @private - * @type {boolean} - */ - this.v13_ = true; - - /** - * @private - * @type {module:ol/source/WMSServerType|undefined} - */ - this.serverType_ = /** @type {module:ol/source/WMSServerType|undefined} */ (options.serverType); - - /** - * @private - * @type {boolean} - */ - this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true; - - /** - * @private - * @type {module:ol/extent~Extent} - */ - this.tmpExtent_ = createEmpty(); - - this.updateV13_(); - this.setKey(this.getKeyForParams_()); - -}; + updateV13_() { + const version = this.params_['VERSION'] || DEFAULT_WMS_VERSION; + this.v13_ = compareVersions(version, '1.3') >= 0; + } +} inherits(TileWMS, TileImage); -/** - * Return the GetFeatureInfo URL for the passed coordinate, resolution, and - * projection. Return `undefined` if the GetFeatureInfo URL cannot be - * constructed. - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {number} resolution Resolution. - * @param {module:ol/proj~ProjectionLike} projection Projection. - * @param {!Object} params GetFeatureInfo params. `INFO_FORMAT` at least should - * be provided. If `QUERY_LAYERS` is not provided then the layers specified - * in the `LAYERS` parameter will be used. `VERSION` should not be - * specified here. - * @return {string|undefined} GetFeatureInfo URL. - * @api - */ -TileWMS.prototype.getGetFeatureInfoUrl = function(coordinate, resolution, projection, params) { - const projectionObj = getProjection(projection); - const sourceProjectionObj = this.getProjection(); - - let tileGrid = this.getTileGrid(); - if (!tileGrid) { - tileGrid = this.getTileGridForProjection(projectionObj); - } - - const tileCoord = tileGrid.getTileCoordForCoordAndResolution(coordinate, resolution); - - if (tileGrid.getResolutions().length <= tileCoord[0]) { - return undefined; - } - - let tileResolution = tileGrid.getResolution(tileCoord[0]); - let tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_); - let tileSize = toSize(tileGrid.getTileSize(tileCoord[0]), this.tmpSize); - - - const gutter = this.gutter_; - if (gutter !== 0) { - tileSize = bufferSize(tileSize, gutter, this.tmpSize); - tileExtent = buffer(tileExtent, tileResolution * gutter, tileExtent); - } - - if (sourceProjectionObj && sourceProjectionObj !== projectionObj) { - tileResolution = calculateSourceResolution(sourceProjectionObj, projectionObj, coordinate, tileResolution); - tileExtent = transformExtent(tileExtent, projectionObj, sourceProjectionObj); - coordinate = transform(coordinate, projectionObj, sourceProjectionObj); - } - - const baseParams = { - 'SERVICE': 'WMS', - 'VERSION': DEFAULT_WMS_VERSION, - 'REQUEST': 'GetFeatureInfo', - 'FORMAT': 'image/png', - 'TRANSPARENT': true, - 'QUERY_LAYERS': this.params_['LAYERS'] - }; - assign(baseParams, this.params_, params); - - const x = Math.floor((coordinate[0] - tileExtent[0]) / tileResolution); - const y = Math.floor((tileExtent[3] - coordinate[1]) / tileResolution); - - baseParams[this.v13_ ? 'I' : 'X'] = x; - baseParams[this.v13_ ? 'J' : 'Y'] = y; - - return this.getRequestUrl_(tileCoord, tileSize, tileExtent, - 1, sourceProjectionObj || projectionObj, baseParams); -}; - - -/** - * @inheritDoc - */ -TileWMS.prototype.getGutterInternal = function() { - return this.gutter_; -}; - - -/** - * Get the user-provided params, i.e. those passed to the constructor through - * the "params" option, and possibly updated using the updateParams method. - * @return {Object} Params. - * @api - */ -TileWMS.prototype.getParams = function() { - return this.params_; -}; - - -/** - * @param {module:ol/tilecoord~TileCoord} tileCoord Tile coordinate. - * @param {module:ol/size~Size} tileSize Tile size. - * @param {module:ol/extent~Extent} tileExtent Tile extent. - * @param {number} pixelRatio Pixel ratio. - * @param {module:ol/proj/Projection} projection Projection. - * @param {Object} params Params. - * @return {string|undefined} Request URL. - * @private - */ -TileWMS.prototype.getRequestUrl_ = function(tileCoord, tileSize, tileExtent, - pixelRatio, projection, params) { - - const urls = this.urls; - if (!urls) { - return undefined; - } - - params['WIDTH'] = tileSize[0]; - params['HEIGHT'] = tileSize[1]; - - params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode(); - - if (!('STYLES' in this.params_)) { - params['STYLES'] = ''; - } - - if (pixelRatio != 1) { - switch (this.serverType_) { - case WMSServerType.GEOSERVER: - const dpi = (90 * pixelRatio + 0.5) | 0; - if ('FORMAT_OPTIONS' in params) { - params['FORMAT_OPTIONS'] += ';dpi:' + dpi; - } else { - params['FORMAT_OPTIONS'] = 'dpi:' + dpi; - } - break; - case WMSServerType.MAPSERVER: - params['MAP_RESOLUTION'] = 90 * pixelRatio; - break; - case WMSServerType.CARMENTA_SERVER: - case WMSServerType.QGIS: - params['DPI'] = 90 * pixelRatio; - break; - default: - assert(false, 52); // Unknown `serverType` configured - break; - } - } - - const axisOrientation = projection.getAxisOrientation(); - const bbox = tileExtent; - if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') { - let tmp; - tmp = tileExtent[0]; - bbox[0] = tileExtent[1]; - bbox[1] = tmp; - tmp = tileExtent[2]; - bbox[2] = tileExtent[3]; - bbox[3] = tmp; - } - params['BBOX'] = bbox.join(','); - - let url; - if (urls.length == 1) { - url = urls[0]; - } else { - const index = modulo(tileCoordHash(tileCoord), urls.length); - url = urls[index]; - } - return appendParams(url, params); -}; - - -/** - * @inheritDoc - */ -TileWMS.prototype.getTilePixelRatio = function(pixelRatio) { - return (!this.hidpi_ || this.serverType_ === undefined) ? 1 : - /** @type {number} */ (pixelRatio); -}; - - -/** - * @private - * @return {string} The key for the current params. - */ -TileWMS.prototype.getKeyForParams_ = function() { - let i = 0; - const res = []; - for (const key in this.params_) { - res[i++] = key + '-' + this.params_[key]; - } - return res.join('/'); -}; - - -/** - * @inheritDoc - */ -TileWMS.prototype.fixedTileUrlFunction = function(tileCoord, pixelRatio, projection) { - - let tileGrid = this.getTileGrid(); - if (!tileGrid) { - tileGrid = this.getTileGridForProjection(projection); - } - - if (tileGrid.getResolutions().length <= tileCoord[0]) { - return undefined; - } - - if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) { - pixelRatio = 1; - } - - const tileResolution = tileGrid.getResolution(tileCoord[0]); - let tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_); - let tileSize = toSize( - tileGrid.getTileSize(tileCoord[0]), this.tmpSize); - - const gutter = this.gutter_; - if (gutter !== 0) { - tileSize = bufferSize(tileSize, gutter, this.tmpSize); - tileExtent = buffer(tileExtent, tileResolution * gutter, tileExtent); - } - - if (pixelRatio != 1) { - tileSize = scaleSize(tileSize, pixelRatio, this.tmpSize); - } - - const baseParams = { - 'SERVICE': 'WMS', - 'VERSION': DEFAULT_WMS_VERSION, - 'REQUEST': 'GetMap', - 'FORMAT': 'image/png', - 'TRANSPARENT': true - }; - assign(baseParams, this.params_); - - return this.getRequestUrl_(tileCoord, tileSize, tileExtent, - pixelRatio, projection, baseParams); -}; - -/** - * Update the user-provided params. - * @param {Object} params Params. - * @api - */ -TileWMS.prototype.updateParams = function(params) { - assign(this.params_, params); - this.updateV13_(); - this.setKey(this.getKeyForParams_()); -}; - - -/** - * @private - */ -TileWMS.prototype.updateV13_ = function() { - const version = this.params_['VERSION'] || DEFAULT_WMS_VERSION; - this.v13_ = compareVersions(version, '1.3') >= 0; -}; export default TileWMS; diff --git a/src/ol/source/UrlTile.js b/src/ol/source/UrlTile.js index d8412a29f4..542c376d61 100644 --- a/src/ol/source/UrlTile.js +++ b/src/ol/source/UrlTile.js @@ -37,56 +37,173 @@ import {getKeyZXY} from '../tilecoord.js'; * @extends {module:ol/source/Tile} * @param {module:ol/source/UrlTile~Options=} options Image tile options. */ -const UrlTile = function(options) { +class UrlTile { + constructor(options) { - TileSource.call(this, { - attributions: options.attributions, - cacheSize: options.cacheSize, - extent: options.extent, - opaque: options.opaque, - projection: options.projection, - state: options.state, - tileGrid: options.tileGrid, - tilePixelRatio: options.tilePixelRatio, - wrapX: options.wrapX, - transition: options.transition - }); + TileSource.call(this, { + attributions: options.attributions, + cacheSize: options.cacheSize, + extent: options.extent, + opaque: options.opaque, + projection: options.projection, + state: options.state, + tileGrid: options.tileGrid, + tilePixelRatio: options.tilePixelRatio, + wrapX: options.wrapX, + transition: options.transition + }); - /** - * @protected - * @type {module:ol/Tile~LoadFunction} - */ - this.tileLoadFunction = options.tileLoadFunction; + /** + * @protected + * @type {module:ol/Tile~LoadFunction} + */ + this.tileLoadFunction = options.tileLoadFunction; - /** - * @protected - * @type {module:ol/Tile~UrlFunction} - */ - this.tileUrlFunction = this.fixedTileUrlFunction ? - this.fixedTileUrlFunction.bind(this) : nullTileUrlFunction; + /** + * @protected + * @type {module:ol/Tile~UrlFunction} + */ + this.tileUrlFunction = this.fixedTileUrlFunction ? + this.fixedTileUrlFunction.bind(this) : nullTileUrlFunction; - /** - * @protected - * @type {!Array.|null} - */ - this.urls = null; + /** + * @protected + * @type {!Array.|null} + */ + this.urls = null; + + if (options.urls) { + this.setUrls(options.urls); + } else if (options.url) { + this.setUrl(options.url); + } + if (options.tileUrlFunction) { + this.setTileUrlFunction(options.tileUrlFunction); + } + + /** + * @private + * @type {!Object.} + */ + this.tileLoadingKeys_ = {}; - if (options.urls) { - this.setUrls(options.urls); - } else if (options.url) { - this.setUrl(options.url); - } - if (options.tileUrlFunction) { - this.setTileUrlFunction(options.tileUrlFunction); } /** - * @private - * @type {!Object.} + * Return the tile load function of the source. + * @return {module:ol/Tile~LoadFunction} TileLoadFunction + * @api */ - this.tileLoadingKeys_ = {}; + getTileLoadFunction() { + return this.tileLoadFunction; + } -}; + /** + * Return the tile URL function of the source. + * @return {module:ol/Tile~UrlFunction} TileUrlFunction + * @api + */ + getTileUrlFunction() { + return this.tileUrlFunction; + } + + /** + * Return the URLs used for this source. + * When a tileUrlFunction is used instead of url or urls, + * null will be returned. + * @return {!Array.|null} URLs. + * @api + */ + getUrls() { + return this.urls; + } + + /** + * Handle tile change events. + * @param {module:ol/events/Event} event Event. + * @protected + */ + handleTileChange(event) { + const tile = /** @type {module:ol/Tile} */ (event.target); + const uid = getUid(tile); + const tileState = tile.getState(); + let type; + if (tileState == TileState.LOADING) { + this.tileLoadingKeys_[uid] = true; + type = TileEventType.TILELOADSTART; + } else if (uid in this.tileLoadingKeys_) { + delete this.tileLoadingKeys_[uid]; + type = tileState == TileState.ERROR ? TileEventType.TILELOADERROR : + (tileState == TileState.LOADED || tileState == TileState.ABORT) ? + TileEventType.TILELOADEND : undefined; + } + if (type != undefined) { + this.dispatchEvent(new TileSourceEvent(type, tile)); + } + } + + /** + * Set the tile load function of the source. + * @param {module:ol/Tile~LoadFunction} tileLoadFunction Tile load function. + * @api + */ + setTileLoadFunction(tileLoadFunction) { + this.tileCache.clear(); + this.tileLoadFunction = tileLoadFunction; + this.changed(); + } + + /** + * Set the tile URL function of the source. + * @param {module:ol/Tile~UrlFunction} tileUrlFunction Tile URL function. + * @param {string=} opt_key Optional new tile key for the source. + * @api + */ + setTileUrlFunction(tileUrlFunction, opt_key) { + this.tileUrlFunction = tileUrlFunction; + this.tileCache.pruneExceptNewestZ(); + if (typeof opt_key !== 'undefined') { + this.setKey(opt_key); + } else { + this.changed(); + } + } + + /** + * Set the URL to use for requests. + * @param {string} url URL. + * @api + */ + setUrl(url) { + const urls = this.urls = expandUrl(url); + this.setTileUrlFunction(this.fixedTileUrlFunction ? + this.fixedTileUrlFunction.bind(this) : + createFromTemplates(urls, this.tileGrid), url); + } + + /** + * Set the URLs to use for requests. + * @param {Array.} urls URLs. + * @api + */ + setUrls(urls) { + this.urls = urls; + const key = urls.join('\n'); + this.setTileUrlFunction(this.fixedTileUrlFunction ? + this.fixedTileUrlFunction.bind(this) : + createFromTemplates(urls, this.tileGrid), key); + } + + /** + * @inheritDoc + */ + useTile(z, x, y) { + const tileCoordKey = getKeyZXY(z, x, y); + if (this.tileCache.containsKey(tileCoordKey)) { + this.tileCache.get(tileCoordKey); + } + } +} inherits(UrlTile, TileSource); @@ -97,126 +214,4 @@ inherits(UrlTile, TileSource); */ UrlTile.prototype.fixedTileUrlFunction; -/** - * Return the tile load function of the source. - * @return {module:ol/Tile~LoadFunction} TileLoadFunction - * @api - */ -UrlTile.prototype.getTileLoadFunction = function() { - return this.tileLoadFunction; -}; - - -/** - * Return the tile URL function of the source. - * @return {module:ol/Tile~UrlFunction} TileUrlFunction - * @api - */ -UrlTile.prototype.getTileUrlFunction = function() { - return this.tileUrlFunction; -}; - - -/** - * Return the URLs used for this source. - * When a tileUrlFunction is used instead of url or urls, - * null will be returned. - * @return {!Array.|null} URLs. - * @api - */ -UrlTile.prototype.getUrls = function() { - return this.urls; -}; - - -/** - * Handle tile change events. - * @param {module:ol/events/Event} event Event. - * @protected - */ -UrlTile.prototype.handleTileChange = function(event) { - const tile = /** @type {module:ol/Tile} */ (event.target); - const uid = getUid(tile); - const tileState = tile.getState(); - let type; - if (tileState == TileState.LOADING) { - this.tileLoadingKeys_[uid] = true; - type = TileEventType.TILELOADSTART; - } else if (uid in this.tileLoadingKeys_) { - delete this.tileLoadingKeys_[uid]; - type = tileState == TileState.ERROR ? TileEventType.TILELOADERROR : - (tileState == TileState.LOADED || tileState == TileState.ABORT) ? - TileEventType.TILELOADEND : undefined; - } - if (type != undefined) { - this.dispatchEvent(new TileSourceEvent(type, tile)); - } -}; - - -/** - * Set the tile load function of the source. - * @param {module:ol/Tile~LoadFunction} tileLoadFunction Tile load function. - * @api - */ -UrlTile.prototype.setTileLoadFunction = function(tileLoadFunction) { - this.tileCache.clear(); - this.tileLoadFunction = tileLoadFunction; - this.changed(); -}; - - -/** - * Set the tile URL function of the source. - * @param {module:ol/Tile~UrlFunction} tileUrlFunction Tile URL function. - * @param {string=} opt_key Optional new tile key for the source. - * @api - */ -UrlTile.prototype.setTileUrlFunction = function(tileUrlFunction, opt_key) { - this.tileUrlFunction = tileUrlFunction; - this.tileCache.pruneExceptNewestZ(); - if (typeof opt_key !== 'undefined') { - this.setKey(opt_key); - } else { - this.changed(); - } -}; - - -/** - * Set the URL to use for requests. - * @param {string} url URL. - * @api - */ -UrlTile.prototype.setUrl = function(url) { - const urls = this.urls = expandUrl(url); - this.setTileUrlFunction(this.fixedTileUrlFunction ? - this.fixedTileUrlFunction.bind(this) : - createFromTemplates(urls, this.tileGrid), url); -}; - - -/** - * Set the URLs to use for requests. - * @param {Array.} urls URLs. - * @api - */ -UrlTile.prototype.setUrls = function(urls) { - this.urls = urls; - const key = urls.join('\n'); - this.setTileUrlFunction(this.fixedTileUrlFunction ? - this.fixedTileUrlFunction.bind(this) : - createFromTemplates(urls, this.tileGrid), key); -}; - - -/** - * @inheritDoc - */ -UrlTile.prototype.useTile = function(z, x, y) { - const tileCoordKey = getKeyZXY(z, x, y); - if (this.tileCache.containsKey(tileCoordKey)) { - this.tileCache.get(tileCoordKey); - } -}; export default UrlTile;