Add decluttering for images and text

This commit is contained in:
Andreas Hocevar
2017-10-09 18:27:31 +02:00
parent 5ebc969599
commit 08af207724
18 changed files with 450 additions and 130 deletions

View File

@@ -15,6 +15,7 @@ var key = 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiRk1kMWZaSSJ9.E5BkluenyWQMsBLsuByrmg';
var map = new ol.Map({ var map = new ol.Map({
layers: [ layers: [
new ol.layer.VectorTile({ new ol.layer.VectorTile({
declutter: true,
source: new ol.source.VectorTile({ source: new ol.source.VectorTile({
attributions: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> ' + attributions: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> ' +
'© <a href="https://www.openstreetmap.org/copyright">' + '© <a href="https://www.openstreetmap.org/copyright">' +

View File

@@ -28,6 +28,7 @@ var map = new ol.Map({
imagerySet: 'Aerial' imagerySet: 'Aerial'
}) })
}), new ol.layer.Vector({ }), new ol.layer.Vector({
declutter: true,
source: new ol.source.Vector({ source: new ol.source.Vector({
format: new ol.format.GeoJSON(), format: new ol.format.GeoJSON(),
url: 'data/geojson/vienna-streets.geojson' url: 'data/geojson/vienna-streets.geojson'

View File

@@ -4250,6 +4250,7 @@ olx.layer.TileOptions.prototype.zIndex;
* renderBuffer: (number|undefined), * renderBuffer: (number|undefined),
* source: (ol.source.Vector|undefined), * source: (ol.source.Vector|undefined),
* map: (ol.PluggableMap|undefined), * map: (ol.PluggableMap|undefined),
* declutter: (boolean|undefined),
* style: (ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction|undefined), * style: (ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction|undefined),
* updateWhileAnimating: (boolean|undefined), * updateWhileAnimating: (boolean|undefined),
* updateWhileInteracting: (boolean|undefined), * updateWhileInteracting: (boolean|undefined),
@@ -4332,6 +4333,14 @@ olx.layer.VectorOptions.prototype.renderBuffer;
olx.layer.VectorOptions.prototype.source; olx.layer.VectorOptions.prototype.source;
/**
* Declutter images and text. Default is `false`.
* @type {boolean|undefined}
* @api
*/
olx.layer.VectorOptions.prototype.declutter;
/** /**
* Layer style. See {@link ol.style} for default style which will be used if * Layer style. See {@link ol.style} for default style which will be used if
* this is not defined. * this is not defined.
@@ -4389,6 +4398,7 @@ olx.layer.VectorOptions.prototype.zIndex;
* renderMode: (ol.layer.VectorTileRenderType|string|undefined), * renderMode: (ol.layer.VectorTileRenderType|string|undefined),
* renderOrder: (ol.RenderOrderFunction|undefined), * renderOrder: (ol.RenderOrderFunction|undefined),
* source: (ol.source.VectorTile|undefined), * source: (ol.source.VectorTile|undefined),
* declutter: (boolean|undefined),
* style: (ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction|undefined), * style: (ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction|undefined),
* updateWhileAnimating: (boolean|undefined), * updateWhileAnimating: (boolean|undefined),
* updateWhileInteracting: (boolean|undefined), * updateWhileInteracting: (boolean|undefined),
@@ -4422,7 +4432,8 @@ olx.layer.VectorTileOptions.prototype.renderBuffer;
* * `'vector'`: Vector tiles are rendered as vectors. Most accurate rendering * * `'vector'`: Vector tiles are rendered as vectors. Most accurate rendering
* even during animations, but slower performance than the other options. * even during animations, but slower performance than the other options.
* *
* The default is `'hybrid'`. * When `declutter` is set to `true`, `'hybrid'` will be used instead of
* `'image'`. The default is `'hybrid'`.
* @type {ol.layer.VectorTileRenderType|string|undefined} * @type {ol.layer.VectorTileRenderType|string|undefined}
* @api * @api
*/ */
@@ -4498,6 +4509,14 @@ olx.layer.VectorTileOptions.prototype.preload;
olx.layer.VectorTileOptions.prototype.source; olx.layer.VectorTileOptions.prototype.source;
/**
* Declutter images and text. Default is `false`.
* @type {boolean|undefined}
* @api
*/
olx.layer.VectorTileOptions.prototype.declutter;
/** /**
* Layer style. See {@link ol.style} for default style which will be used if * Layer style. See {@link ol.style} for default style which will be used if
* this is not defined. * this is not defined.

View File

@@ -32,6 +32,12 @@ ol.layer.Vector = function(opt_options) {
delete baseOptions.updateWhileInteracting; delete baseOptions.updateWhileInteracting;
ol.layer.Layer.call(this, /** @type {olx.layer.LayerOptions} */ (baseOptions)); ol.layer.Layer.call(this, /** @type {olx.layer.LayerOptions} */ (baseOptions));
/**
* @private
* @type {boolean}
*/
this.declutter_ = options.declutter !== undefined ? options.declutter : false;
/** /**
* @type {number} * @type {number}
* @private * @private
@@ -80,6 +86,22 @@ ol.layer.Vector = function(opt_options) {
ol.inherits(ol.layer.Vector, ol.layer.Layer); ol.inherits(ol.layer.Vector, ol.layer.Layer);
/**
* @return {boolean} Declutter.
*/
ol.layer.Vector.prototype.getDeclutter = function() {
return this.declutter_;
};
/**
* @param {boolean} declutter Declutter.
*/
ol.layer.Vector.prototype.setDeclutter = function(declutter) {
this.declutter_ = declutter;
};
/** /**
* @return {number|undefined} Render buffer. * @return {number|undefined} Render buffer.
*/ */

View File

@@ -34,17 +34,23 @@ ol.layer.VectorTile = function(opt_options) {
this.setUseInterimTilesOnError(options.useInterimTilesOnError ? this.setUseInterimTilesOnError(options.useInterimTilesOnError ?
options.useInterimTilesOnError : true); options.useInterimTilesOnError : true);
ol.asserts.assert(options.renderMode == undefined || var renderMode = options.renderMode;
options.renderMode == ol.layer.VectorTileRenderType.IMAGE ||
options.renderMode == ol.layer.VectorTileRenderType.HYBRID || ol.asserts.assert(renderMode == undefined ||
options.renderMode == ol.layer.VectorTileRenderType.VECTOR, renderMode == ol.layer.VectorTileRenderType.IMAGE ||
renderMode == ol.layer.VectorTileRenderType.HYBRID ||
renderMode == ol.layer.VectorTileRenderType.VECTOR,
28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'` 28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'`
if (options.declutter && renderMode == ol.layer.VectorTileRenderType.IMAGE) {
renderMode = ol.layer.VectorTileRenderType.HYBRID;
}
/** /**
* @private * @private
* @type {ol.layer.VectorTileRenderType|string} * @type {ol.layer.VectorTileRenderType|string}
*/ */
this.renderMode_ = options.renderMode || ol.layer.VectorTileRenderType.HYBRID; this.renderMode_ = renderMode || ol.layer.VectorTileRenderType.HYBRID;
/** /**
* The layer type. * The layer type.

View File

@@ -1,6 +1,9 @@
goog.provide('ol.render.canvas'); goog.provide('ol.render.canvas');
goog.require('ol.transform');
/** /**
* @const * @const
* @type {string} * @type {string}
@@ -91,3 +94,41 @@ ol.render.canvas.rotateAtOffset = function(context, rotation, offsetX, offsetY)
context.translate(-offsetX, -offsetY); context.translate(-offsetX, -offsetY);
} }
}; };
ol.render.canvas.resetTransform_ = ol.transform.create();
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {ol.Transform|null} transform Transform.
* @param {number} opacity Opacity.
* @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image.
* @param {number} originX Origin X.
* @param {number} originY Origin Y.
* @param {number} w Width.
* @param {number} h Height.
* @param {number} x X.
* @param {number} y Y.
* @param {number} scale Scale.
*/
ol.render.canvas.drawImage = function(context,
transform, opacity, image, originX, originY, w, h, x, y, scale) {
var alpha;
if (opacity != 1) {
alpha = context.globalAlpha;
context.globalAlpha = alpha * opacity;
}
if (transform) {
context.setTransform.apply(context, transform);
}
context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale);
if (alpha) {
context.globalAlpha = alpha;
}
if (transform) {
context.setTransform.apply(context, ol.render.canvas.resetTransform_);
}
};

View File

@@ -13,10 +13,19 @@ goog.require('ol.render.canvas.Replay');
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} overlaps The replay can have overlapping geometries. * @param {boolean} overlaps The replay can have overlapping geometries.
* @param {?} declutterTree Declutter tree.
* @struct * @struct
*/ */
ol.render.canvas.ImageReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { ol.render.canvas.ImageReplay = function(
ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps); tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) {
ol.render.canvas.Replay.call(this,
tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree);
/**
* @private
* @type {Array.<*>}
*/
this.declutterGroup_ = null;
/** /**
* @private * @private
@@ -130,7 +139,7 @@ ol.render.canvas.ImageReplay.prototype.drawPoint = function(pointGeometry, featu
this.instructions.push([ this.instructions.push([
ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_, ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order // Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_, this.anchorY_, this.height_, this.opacity_, this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_,
this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_ this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_
]); ]);
@@ -138,7 +147,7 @@ ol.render.canvas.ImageReplay.prototype.drawPoint = function(pointGeometry, featu
ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
this.hitDetectionImage_, this.hitDetectionImage_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order // Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_, this.anchorY_, this.height_, this.opacity_, this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_,
this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_, this.snapToPixel_, this.width_ this.scale_, this.snapToPixel_, this.width_
]); ]);
@@ -162,7 +171,7 @@ ol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeome
this.instructions.push([ this.instructions.push([
ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_, ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order // Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_, this.anchorY_, this.height_, this.opacity_, this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_,
this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_ this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_
]); ]);
@@ -170,7 +179,7 @@ ol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeome
ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
this.hitDetectionImage_, this.hitDetectionImage_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order // Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_, this.anchorY_, this.height_, this.opacity_, this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_,
this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_, this.snapToPixel_, this.width_ this.scale_, this.snapToPixel_, this.width_
]); ]);
@@ -203,7 +212,7 @@ ol.render.canvas.ImageReplay.prototype.finish = function() {
/** /**
* @inheritDoc * @inheritDoc
*/ */
ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle) { ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle, declutterGroup) {
var anchor = imageStyle.getAnchor(); var anchor = imageStyle.getAnchor();
var size = imageStyle.getSize(); var size = imageStyle.getSize();
var hitDetectionImage = imageStyle.getHitDetectionImage(1); var hitDetectionImage = imageStyle.getHitDetectionImage(1);
@@ -211,6 +220,7 @@ ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle) {
var origin = imageStyle.getOrigin(); var origin = imageStyle.getOrigin();
this.anchorX_ = anchor[0]; this.anchorX_ = anchor[0];
this.anchorY_ = anchor[1]; this.anchorY_ = anchor[1];
this.declutterGroup_ = /** @type {Array.<*>} */ (declutterGroup);
this.hitDetectionImage_ = hitDetectionImage; this.hitDetectionImage_ = hitDetectionImage;
this.image_ = image; this.image_ = image;
this.height_ = size[1]; this.height_ = size[1];

View File

@@ -17,11 +17,13 @@ goog.require('ol.render.canvas.Replay');
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} overlaps The replay can have overlapping geometries. * @param {boolean} overlaps The replay can have overlapping geometries.
* @param {?} declutter Declutter tree.
* @struct * @struct
*/ */
ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { ol.render.canvas.LineStringReplay = function(
tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter) {
ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps); ol.render.canvas.Replay.call(this,
tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter);
/** /**
* @private * @private

View File

@@ -19,11 +19,13 @@ goog.require('ol.render.canvas.Replay');
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} overlaps The replay can have overlapping geometries. * @param {boolean} overlaps The replay can have overlapping geometries.
* @param {?} declutter Declutter tree.
* @struct * @struct
*/ */
ol.render.canvas.PolygonReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { ol.render.canvas.PolygonReplay = function(
tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter) {
ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps); ol.render.canvas.Replay.call(this,
tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter);
/** /**
* @private * @private

View File

@@ -12,6 +12,7 @@ goog.require('ol.geom.flat.transform');
goog.require('ol.has'); goog.require('ol.has');
goog.require('ol.obj'); goog.require('ol.obj');
goog.require('ol.render.VectorContext'); goog.require('ol.render.VectorContext');
goog.require('ol.render.canvas');
goog.require('ol.render.canvas.Instruction'); goog.require('ol.render.canvas.Instruction');
goog.require('ol.transform'); goog.require('ol.transform');
@@ -24,11 +25,23 @@ goog.require('ol.transform');
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} overlaps The replay can have overlapping geometries. * @param {boolean} overlaps The replay can have overlapping geometries.
* @param {?} declutterTree Declutter tree.
* @struct * @struct
*/ */
ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) {
ol.render.VectorContext.call(this); ol.render.VectorContext.call(this);
/**
* @type {?}
*/
this.declutterTree = declutterTree;
/**
* @private
* @type {ol.Extent}
*/
this.tmpExtent_ = ol.extent.createEmpty();
/** /**
* @protected * @protected
* @type {number} * @type {number}
@@ -127,12 +140,6 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio,
*/ */
this.tmpLocalTransform_ = ol.transform.create(); this.tmpLocalTransform_ = ol.transform.create();
/**
* @private
* @type {!ol.Transform}
*/
this.resetTransform_ = ol.transform.create();
/** /**
* @private * @private
* @type {Array.<Array.<number>>} * @type {Array.<Array.<number>>}
@@ -149,6 +156,7 @@ ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext);
* @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image. * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image.
* @param {number} anchorX Anchor X. * @param {number} anchorX Anchor X.
* @param {number} anchorY Anchor Y. * @param {number} anchorY Anchor Y.
* @param {Array.<*>} declutterGroup Declutter group.
* @param {number} height Height. * @param {number} height Height.
* @param {number} opacity Opacity. * @param {number} opacity Opacity.
* @param {number} originX Origin X. * @param {number} originX Origin X.
@@ -159,7 +167,7 @@ ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext);
* @param {number} width Width. * @param {number} width Width.
*/ */
ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, anchorX, anchorY, ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, anchorX, anchorY,
height, opacity, originX, originY, rotation, scale, snapToPixel, width) { declutterGroup, height, opacity, originX, originY, rotation, scale, snapToPixel, width) {
var localTransform = this.tmpLocalTransform_; var localTransform = this.tmpLocalTransform_;
anchorX *= scale; anchorX *= scale;
anchorY *= scale; anchorY *= scale;
@@ -172,38 +180,34 @@ ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image,
var w = (width + originX > image.width) ? image.width - originX : width; var w = (width + originX > image.width) ? image.width - originX : width;
var h = (height + originY > image.height) ? image.height - originY : height; var h = (height + originY > image.height) ? image.height - originY : height;
var box = this.tmpExtent_;
var box; var transform = null;
if (rotation !== 0) { if (rotation !== 0) {
var centerX = x + anchorX; var centerX = x + anchorX;
var centerY = y + anchorY; var centerY = y + anchorY;
ol.transform.compose(localTransform, transform = ol.transform.compose(localTransform,
centerX, centerY, 1, 1, rotation, -centerX, -centerY); centerX, centerY, 1, 1, rotation, -centerX, -centerY);
context.setTransform.apply(context, localTransform); ol.extent.createOrUpdateEmpty(box);
box = ol.extent.createEmpty();
ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x, y])); ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x, y]));
ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x + w, y])); ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x + w, y]));
ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x + w, y + h])); ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x + w, y + h]));
ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x, y + w])); ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x, y + w]));
} else { } else {
box = [x, y, x + w * scale, y + h * scale]; ol.extent.createOrUpdate(x, y, x + w * scale, y + h * scale, box);
} }
var canvas = context.canvas; var canvas = context.canvas;
if (box[0] > canvas.width || box[2] < 0 || box[1] > canvas.height || box[3] < 0) { var intersects = box[0] <= canvas.width && box[2] >= 0 && box[1] <= canvas.height && box[3] >= 0;
return; if (declutterGroup) {
} if (!intersects && declutterGroup[4] == 1) {
var alpha = context.globalAlpha; return;
if (opacity != 1) { }
context.globalAlpha = alpha * opacity; ol.extent.extend(declutterGroup, box);
} declutterGroup.push(intersects ?
[context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] :
context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale); null);
} else if (intersects) {
if (opacity != 1) { ol.render.canvas.drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale);
context.globalAlpha = alpha;
}
if (rotation !== 0) {
context.setTransform.apply(context, this.resetTransform_);
} }
}; };
@@ -373,7 +377,30 @@ ol.render.canvas.Replay.prototype.fill_ = function(context, rotation) {
} }
context.fill(); context.fill();
if (this.fillOrigin_) { if (this.fillOrigin_) {
context.setTransform.apply(context, this.resetTransform_); context.setTransform.apply(context, ol.render.canvas.resetTransform_);
}
};
/**
* @param {Array.<*>} declutterGroup Declutter group.
*/
ol.render.canvas.Replay.prototype.renderDeclutter_ = function(declutterGroup) {
if (declutterGroup && declutterGroup.length > 5) {
var groupCount = declutterGroup[4];
if (groupCount == 1 || groupCount == declutterGroup.length - 5) {
if (!this.declutterTree.collides(this.declutterTree.toBBox(declutterGroup))) {
this.declutterTree.insert(declutterGroup.slice(0, 4));
var drawImage = ol.render.canvas.drawImage;
for (var j = 5, jj = declutterGroup.length; j < jj; ++j) {
if (declutterGroup[j]) {
drawImage.apply(undefined, /** @type {Array} */ (declutterGroup[j]));
}
}
}
declutterGroup.length = 5;
ol.extent.createOrUpdateEmpty(declutterGroup);
}
} }
}; };
@@ -414,7 +441,7 @@ ol.render.canvas.Replay.prototype.replay_ = function(
var ii = instructions.length; // end of instructions var ii = instructions.length; // end of instructions
var d = 0; // data index var d = 0; // data index
var dd; // end of per-instruction data var dd; // end of per-instruction data
var anchorX, anchorY, prevX, prevY, roundX, roundY; var anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup;
var pendingFill = 0; var pendingFill = 0;
var pendingStroke = 0; var pendingStroke = 0;
var coordinateCache = this.coordinateCache_; var coordinateCache = this.coordinateCache_;
@@ -510,24 +537,26 @@ ol.render.canvas.Replay.prototype.replay_ = function(
// Remaining arguments in DRAW_IMAGE are in alphabetical order // Remaining arguments in DRAW_IMAGE are in alphabetical order
anchorX = /** @type {number} */ (instruction[4]); anchorX = /** @type {number} */ (instruction[4]);
anchorY = /** @type {number} */ (instruction[5]); anchorY = /** @type {number} */ (instruction[5]);
var height = /** @type {number} */ (instruction[6]); declutterGroup = /** @type {Array.<*>} */ (instruction[6]);
var opacity = /** @type {number} */ (instruction[7]); var height = /** @type {number} */ (instruction[7]);
var originX = /** @type {number} */ (instruction[8]); var opacity = /** @type {number} */ (instruction[8]);
var originY = /** @type {number} */ (instruction[9]); var originX = /** @type {number} */ (instruction[9]);
var rotateWithView = /** @type {boolean} */ (instruction[10]); var originY = /** @type {number} */ (instruction[10]);
var rotation = /** @type {number} */ (instruction[11]); var rotateWithView = /** @type {boolean} */ (instruction[11]);
var scale = /** @type {number} */ (instruction[12]); var rotation = /** @type {number} */ (instruction[12]);
var snapToPixel = /** @type {boolean} */ (instruction[13]); var scale = /** @type {number} */ (instruction[13]);
var width = /** @type {number} */ (instruction[14]); var snapToPixel = /** @type {boolean} */ (instruction[14]);
var width = /** @type {number} */ (instruction[15]);
if (rotateWithView) { if (rotateWithView) {
rotation += viewRotation; rotation += viewRotation;
} }
for (; d < dd; d += 2) { for (; d < dd; d += 2) {
this.replayImage_(context, pixelCoordinates[d], pixelCoordinates[d + 1], this.replayImage_(context,
image, anchorX, anchorY, height, opacity, originX, originY, pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY,
rotation, scale, snapToPixel, width); declutterGroup, height, opacity, originX, originY, rotation, scale, snapToPixel, width);
} }
this.renderDeclutter_(declutterGroup);
++i; ++i;
break; break;
case ol.render.canvas.Instruction.DRAW_CHARS: case ol.render.canvas.Instruction.DRAW_CHARS:
@@ -536,13 +565,14 @@ ol.render.canvas.Replay.prototype.replay_ = function(
var images = /** @type {Array.<HTMLCanvasElement>} */ (instruction[3]); var images = /** @type {Array.<HTMLCanvasElement>} */ (instruction[3]);
// Remaining arguments in DRAW_CHARS are in alphabetical order // Remaining arguments in DRAW_CHARS are in alphabetical order
var baseline = /** @type {number} */ (instruction[4]); var baseline = /** @type {number} */ (instruction[4]);
var exceedLength = /** @type {number} */ (instruction[5]); declutterGroup = /** @type {Array.<*>} */ (instruction[5]);
var maxAngle = /** @type {number} */ (instruction[6]); var exceedLength = /** @type {number} */ (instruction[6]);
var measure = /** @type {function(string):number} */ (instruction[7]); var maxAngle = /** @type {number} */ (instruction[7]);
var offsetY = /** @type {number} */ (instruction[8]); var measure = /** @type {function(string):number} */ (instruction[8]);
var text = /** @type {string} */ (instruction[9]); var offsetY = /** @type {number} */ (instruction[9]);
var align = /** @type {number} */ (instruction[10]); var text = /** @type {string} */ (instruction[10]);
var textScale = /** @type {number} */ (instruction[11]); var align = /** @type {number} */ (instruction[11]);
var textScale = /** @type {number} */ (instruction[12]);
var pathLength = ol.geom.flat.length.lineString(pixelCoordinates, begin, end, 2); var pathLength = ol.geom.flat.length.lineString(pixelCoordinates, begin, end, 2);
var textLength = measure(text); var textLength = measure(text);
@@ -558,12 +588,12 @@ ol.render.canvas.Replay.prototype.replay_ = function(
var label = images[c]; var label = images[c];
anchorX = label.width / 2; anchorX = label.width / 2;
anchorY = baseline * label.height + (0.5 - baseline) * (label.height - fillHeight) - offsetY; anchorY = baseline * label.height + (0.5 - baseline) * (label.height - fillHeight) - offsetY;
this.replayImage_(context, char[0], char[1], label, anchorX, anchorY, this.replayImage_(context, char[0], char[1], label,
label.height, 1, 0, 0, char[2], textScale, false, label.width); anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, char[2], textScale, false, label.width);
} }
} }
} }
this.renderDeclutter_(declutterGroup);
++i; ++i;
break; break;
case ol.render.canvas.Instruction.END_GEOMETRY: case ol.render.canvas.Instruction.END_GEOMETRY:

View File

@@ -7,6 +7,7 @@ goog.require('ol.extent');
goog.require('ol.geom.flat.transform'); goog.require('ol.geom.flat.transform');
goog.require('ol.obj'); goog.require('ol.obj');
goog.require('ol.render.ReplayGroup'); goog.require('ol.render.ReplayGroup');
goog.require('ol.render.ReplayType');
goog.require('ol.render.canvas.Replay'); goog.require('ol.render.canvas.Replay');
goog.require('ol.render.canvas.ImageReplay'); goog.require('ol.render.canvas.ImageReplay');
goog.require('ol.render.canvas.LineStringReplay'); goog.require('ol.render.canvas.LineStringReplay');
@@ -24,13 +25,33 @@ goog.require('ol.transform');
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} overlaps The replay group can have overlapping geometries. * @param {boolean} overlaps The replay group can have overlapping geometries.
* @param {?} declutterTree Declutter tree
* for declutter processing in postrender.
* @param {number=} opt_renderBuffer Optional rendering buffer. * @param {number=} opt_renderBuffer Optional rendering buffer.
* @struct * @struct
*/ */
ol.render.canvas.ReplayGroup = function( ol.render.canvas.ReplayGroup = function(
tolerance, maxExtent, resolution, pixelRatio, overlaps, opt_renderBuffer) { tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree, opt_renderBuffer) {
ol.render.ReplayGroup.call(this); ol.render.ReplayGroup.call(this);
/**
* Declutter tree.
* @private
*/
this.declutterTree_ = declutterTree;
/**
* Container for decluttered replay instructions that need to be rendered or
* omitted together, i.e. when styles render both an image and text. The basic
* elements of this array are `[minX, minY, maxX, maxY, count]`, where the
* first four entries are the rendered extent in pixel space. `count` is the
* number of replay instructions in the group. In addition to these basic
* elements, declutter instructions are appended to the array.
* @type {Array.<*>}
* @private
*/
this.declutterGroup_ = null;
/** /**
* @private * @private
* @type {number} * @type {number}
@@ -170,6 +191,71 @@ ol.render.canvas.ReplayGroup.getCircleArray_ = function(radius) {
}; };
ol.render.canvas.ReplayGroup.replayDeclutter = function(declutterReplays, context, rotation) {
var zs = Object.keys(declutterReplays).map(Number).sort(ol.array.numberSafeCompareFunction);
for (var z = zs.length - 1; z >= 0; --z) {
var replayData = declutterReplays[zs[z].toString()];
for (var i = 0, ii = replayData.length; i < ii;) {
var replay = replayData[i++];
var transform = replayData[i++];
replay.replay(context, transform, rotation, {});
}
}
};
ol.render.canvas.ReplayGroup.replayDeclutterHitDetection = function(
declutterReplays, context, rotation, featureCallback, hitExtent) {
var zs = Object.keys(declutterReplays).map(Number).sort(ol.array.numberSafeCompareFunction);
for (var z = zs.length - 1; z >= 0; --z) {
var replayData = declutterReplays[zs[z].toString()];
for (var i = 0, ii = replayData.length; i < ii;) {
var replay = replayData[i++];
var transform = replayData[i++];
var result = replay.replayHitDetection(context, transform, rotation, {},
featureCallback, hitExtent);
if (result) {
return result;
}
}
}
};
/**
* @param {boolean} group Group with previous replay.
* @return {Array.<*>} Declutter instruction group.
*/
ol.render.canvas.ReplayGroup.prototype.addDeclutter = function(group) {
var declutter = null;
if (this.declutterTree_) {
if (group) {
declutter = this.declutterGroup_;
/** @type {number} */ (declutter[4])++;
} else {
declutter = this.declutterGroup_ = ol.extent.createEmpty();
declutter.push(1);
}
}
return declutter;
};
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {ol.Transform} transform Transform.
*/
ol.render.canvas.ReplayGroup.prototype.clip = function(context, transform) {
var 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.<ol.render.ReplayType>} replays Replays. * @param {Array.<ol.render.ReplayType>} replays Replays.
* @return {boolean} Has replays of the provided types. * @return {boolean} Has replays of the provided types.
@@ -211,11 +297,12 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() {
* to skip. * to skip.
* @param {function((ol.Feature|ol.render.Feature)): T} callback Feature * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
* callback. * callback.
* @param {Object.<string, Array.<*>>} declutterReplays Declutter replays.
* @return {T|undefined} Callback result. * @return {T|undefined} Callback result.
* @template T * @template T
*/ */
ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback) { coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback, declutterReplays) {
hitTolerance = Math.round(hitTolerance); hitTolerance = Math.round(hitTolerance);
var contextSize = hitTolerance * 2 + 1; var contextSize = hitTolerance * 2 + 1;
@@ -245,7 +332,7 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance); var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance);
return this.replayHitDetection_(context, transform, rotation, var result = this.replayHitDetection_(context, transform, rotation,
skippedFeaturesHash, skippedFeaturesHash,
/** /**
* @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.Feature|ol.render.Feature} feature Feature.
@@ -269,6 +356,11 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
} }
} }
}, hitExtent); }, hitExtent);
if (!result && declutterReplays) {
result = ol.render.canvas.ReplayGroup.replayDeclutterHitDetection(
declutterReplays, context, rotation, callback, hitExtent);
}
return result;
}; };
@@ -303,13 +395,21 @@ ol.render.canvas.ReplayGroup.prototype.getReplay = function(zIndex, replayType)
if (replay === undefined) { if (replay === undefined) {
var Constructor = ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_[replayType]; var Constructor = ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_[replayType];
replay = new Constructor(this.tolerance_, this.maxExtent_, replay = new Constructor(this.tolerance_, this.maxExtent_,
this.resolution_, this.pixelRatio_, this.overlaps_); this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_);
replays[replayType] = replay; replays[replayType] = replay;
} }
return replay; return replay;
}; };
/**
* @return {Object.<string, Object.<ol.render.ReplayType, ol.render.canvas.Replay>>} Replays.
*/
ol.render.canvas.ReplayGroup.prototype.getReplays = function() {
return this.replaysByZIndex_;
};
/** /**
* @inheritDoc * @inheritDoc
*/ */
@@ -326,9 +426,10 @@ ol.render.canvas.ReplayGroup.prototype.isEmpty = function() {
* to skip. * to skip.
* @param {Array.<ol.render.ReplayType>=} opt_replayTypes Ordered replay types * @param {Array.<ol.render.ReplayType>=} opt_replayTypes Ordered replay types
* to replay. Default is {@link ol.render.replay.ORDER} * to replay. Default is {@link ol.render.replay.ORDER}
* @param {Object.<string, Array.<*>>=} opt_declutterReplays Declutter replays.
*/ */
ol.render.canvas.ReplayGroup.prototype.replay = function(context, ol.render.canvas.ReplayGroup.prototype.replay = function(context,
transform, viewRotation, skippedFeaturesHash, opt_replayTypes) { transform, viewRotation, skippedFeaturesHash, opt_replayTypes, opt_declutterReplays) {
/** @type {Array.<number>} */ /** @type {Array.<number>} */
var zs = Object.keys(this.replaysByZIndex_).map(Number); var zs = Object.keys(this.replaysByZIndex_).map(Number);
@@ -336,23 +437,29 @@ ol.render.canvas.ReplayGroup.prototype.replay = function(context,
// setup clipping so that the parts of over-simplified geometries are not // setup clipping so that the parts of over-simplified geometries are not
// visible outside the current extent when panning // visible outside the current extent when panning
var flatClipCoords = this.getClipCoords(transform);
context.save(); context.save();
context.beginPath(); this.clip(context, transform);
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();
var replayTypes = opt_replayTypes ? opt_replayTypes : ol.render.replay.ORDER; var replayTypes = opt_replayTypes ? opt_replayTypes : ol.render.replay.ORDER;
var i, ii, j, jj, replays, replay; var i, ii, j, jj, replays, replay;
for (i = 0, ii = zs.length; i < ii; ++i) { for (i = 0, ii = zs.length; i < ii; ++i) {
replays = this.replaysByZIndex_[zs[i].toString()]; var zIndexKey = zs[i].toString();
replays = this.replaysByZIndex_[zIndexKey];
for (j = 0, jj = replayTypes.length; j < jj; ++j) { for (j = 0, jj = replayTypes.length; j < jj; ++j) {
replay = replays[replayTypes[j]]; var replayType = replayTypes[j];
replay = replays[replayType];
if (replay !== undefined) { if (replay !== undefined) {
replay.replay(context, transform, viewRotation, skippedFeaturesHash); if (opt_declutterReplays &&
(replayType == ol.render.ReplayType.IMAGE || replayType == ol.render.ReplayType.TEXT)) {
var 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);
}
} }
} }
} }
@@ -372,12 +479,13 @@ ol.render.canvas.ReplayGroup.prototype.replay = function(context,
* Feature callback. * Feature callback.
* @param {ol.Extent=} opt_hitExtent Only check features that intersect this * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
* extent. * extent.
* @param {Object.<string, Array.<*>>=} opt_declutterReplays Declutter replays.
* @return {T|undefined} Callback result. * @return {T|undefined} Callback result.
* @template T * @template T
*/ */
ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function( ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
context, transform, viewRotation, skippedFeaturesHash, context, transform, viewRotation, skippedFeaturesHash,
featureCallback, opt_hitExtent) { featureCallback, opt_hitExtent, opt_declutterReplays) {
/** @type {Array.<number>} */ /** @type {Array.<number>} */
var zs = Object.keys(this.replaysByZIndex_).map(Number); var zs = Object.keys(this.replaysByZIndex_).map(Number);
zs.sort(function(a, b) { zs.sort(function(a, b) {
@@ -386,14 +494,26 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
var i, ii, j, replays, replay, result; var i, ii, j, replays, replay, result;
for (i = 0, ii = zs.length; i < ii; ++i) { for (i = 0, ii = zs.length; i < ii; ++i) {
replays = this.replaysByZIndex_[zs[i].toString()]; var zIndexKey = zs[i].toString();
replays = this.replaysByZIndex_[zIndexKey];
for (j = ol.render.replay.ORDER.length - 1; j >= 0; --j) { for (j = ol.render.replay.ORDER.length - 1; j >= 0; --j) {
replay = replays[ol.render.replay.ORDER[j]]; var replayType = ol.render.replay.ORDER[j];
replay = replays[replayType];
if (replay !== undefined) { if (replay !== undefined) {
result = replay.replayHitDetection(context, transform, viewRotation, if (opt_declutterReplays &&
skippedFeaturesHash, featureCallback, opt_hitExtent); (replayType == ol.render.ReplayType.IMAGE || replayType == ol.render.ReplayType.TEXT)) {
if (result) { var declutter = opt_declutterReplays[zIndexKey];
return result; if (!declutter) {
opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)];
} else {
declutter.push(replay, transform.slice(0));
}
} else {
result = replay.replayHitDetection(context, transform, viewRotation,
skippedFeaturesHash, featureCallback, opt_hitExtent);
if (result) {
return result;
}
} }
} }
} }
@@ -407,7 +527,7 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
* @private * @private
* @type {Object.<ol.render.ReplayType, * @type {Object.<ol.render.ReplayType,
* function(new: ol.render.canvas.Replay, number, ol.Extent, * function(new: ol.render.canvas.Replay, number, ol.Extent,
* number, number, boolean)>} * number, number, boolean, Array.<Array.<*>>)>}
*/ */
ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = { ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = {
'Circle': ol.render.canvas.PolygonReplay, 'Circle': ol.render.canvas.PolygonReplay,

View File

@@ -22,11 +22,19 @@ goog.require('ol.style.TextPlacement');
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} overlaps The replay can have overlapping geometries. * @param {boolean} overlaps The replay can have overlapping geometries.
* @param {?} declutterTree Declutter tree.
* @struct * @struct
*/ */
ol.render.canvas.TextReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { ol.render.canvas.TextReplay = function(
tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) {
ol.render.canvas.Replay.call(this,
tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree);
ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps); /**
* @private
* @type {Array.<*>}
*/
this.declutterGroup_;
/** /**
* @private * @private
@@ -238,7 +246,7 @@ ol.render.canvas.TextReplay.prototype.drawText = function(geometry, feature) {
} }
end = this.appendFlatCoordinates(flatCoordinates, flatOffset, flatEnd, stride, false, false); end = this.appendFlatCoordinates(flatCoordinates, flatOffset, flatEnd, stride, false, false);
flatOffset = ends[o]; flatOffset = ends[o];
this.drawChars_(begin, end); this.drawChars_(begin, end, this.declutterGroup_);
begin = end; begin = end;
} }
this.endGeometry(geometry, feature); this.endGeometry(geometry, feature);
@@ -378,12 +386,12 @@ ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(label, begin, en
var anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth; var anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth;
this.instructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end, this.instructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end,
label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_,
1, true, label.width 1, true, label.width
]); ]);
this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end, this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end,
label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_,
1 / pixelRatio, true, label.width 1 / pixelRatio, true, label.width
]); ]);
}; };
@@ -393,8 +401,9 @@ ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(label, begin, en
* @private * @private
* @param {number} begin Begin. * @param {number} begin Begin.
* @param {number} end End. * @param {number} end End.
* @param {Array.<*>} declutterGroup Declutter group.
*/ */
ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end) { ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end, declutterGroup) {
var pixelRatio = this.pixelRatio; var pixelRatio = this.pixelRatio;
var strokeState = this.textStrokeState_; var strokeState = this.textStrokeState_;
var fill = !!this.textFillState_; var fill = !!this.textFillState_;
@@ -423,13 +432,13 @@ ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end) {
var align = ol.render.replay.TEXT_ALIGN[textState.textAlign || ol.render.canvas.defaultTextAlign]; var align = ol.render.replay.TEXT_ALIGN[textState.textAlign || ol.render.canvas.defaultTextAlign];
var widths = {}; var widths = {};
this.instructions.push([ol.render.canvas.Instruction.DRAW_CHARS, this.instructions.push([ol.render.canvas.Instruction.DRAW_CHARS,
begin, end, labels, baseline, begin, end, labels, baseline, declutterGroup,
textState.exceedLength, textState.maxAngle, textState.exceedLength, textState.maxAngle,
ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, pixelRatio * this.textScale_), ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, pixelRatio * this.textScale_),
offsetY, this.text_, align, 1 offsetY, this.text_, align, 1
]); ]);
this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_CHARS, this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_CHARS,
begin, end, labels, baseline, begin, end, labels, baseline, declutterGroup,
textState.exceedLength, textState.maxAngle, textState.exceedLength, textState.maxAngle,
ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, this.textScale_), ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, this.textScale_),
offsetY, this.text_, align, 1 / pixelRatio offsetY, this.text_, align, 1 / pixelRatio
@@ -440,11 +449,12 @@ ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end) {
/** /**
* @inheritDoc * @inheritDoc
*/ */
ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) { ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle, declutterGroup) {
var textState, fillState, strokeState; var textState, fillState, strokeState;
if (!textStyle) { if (!textStyle) {
this.text_ = ''; this.text_ = '';
} else { } else {
this.declutterGroup_ = /** @type {Array.<*>} */ (declutterGroup);
var textFillStyle = textStyle.getFill(); var textFillStyle = textStyle.getFill();
if (!textFillStyle) { if (!textFillStyle) {
fillState = this.textFillState_ = null; fillState = this.textFillState_ = null;

View File

@@ -123,11 +123,13 @@ ol.render.VectorContext.prototype.setFillStrokeStyle = function(fillStyle, strok
/** /**
* @param {ol.style.Image} imageStyle Image style. * @param {ol.style.Image} imageStyle Image style.
* @param {Array.<*>=} opt_declutterGroup Declutter.
*/ */
ol.render.VectorContext.prototype.setImageStyle = function(imageStyle) {}; ol.render.VectorContext.prototype.setImageStyle = function(imageStyle, opt_declutterGroup) {};
/** /**
* @param {ol.style.Text} textStyle Text style. * @param {ol.style.Text} textStyle Text style.
* @param {Array.<*>=} opt_declutterGroup Declutter.
*/ */
ol.render.VectorContext.prototype.setTextStyle = function(textStyle) {}; ol.render.VectorContext.prototype.setTextStyle = function(textStyle, opt_declutterGroup) {};

View File

@@ -53,6 +53,13 @@ ol.render.webgl.ReplayGroup = function(tolerance, maxExtent, opt_renderBuffer) {
ol.inherits(ol.render.webgl.ReplayGroup, ol.render.ReplayGroup); ol.inherits(ol.render.webgl.ReplayGroup, ol.render.ReplayGroup);
/**
* @param {ol.style.Style} style Style.
* @param {boolean} group Group with previous replay.
*/
ol.render.webgl.ReplayGroup.prototype.addDeclutter = function(style, group) {};
/** /**
* @param {ol.webgl.Context} context WebGL context. * @param {ol.webgl.Context} context WebGL context.
* @return {function()} Delete resources function. * @return {function()} Delete resources function.

View File

@@ -4,6 +4,7 @@ goog.require('ol');
goog.require('ol.LayerType'); goog.require('ol.LayerType');
goog.require('ol.ViewHint'); goog.require('ol.ViewHint');
goog.require('ol.dom'); goog.require('ol.dom');
goog.require('ol.ext.rbush');
goog.require('ol.extent'); goog.require('ol.extent');
goog.require('ol.render.EventType'); goog.require('ol.render.EventType');
goog.require('ol.render.canvas'); goog.require('ol.render.canvas');
@@ -23,6 +24,13 @@ ol.renderer.canvas.VectorLayer = function(vectorLayer) {
ol.renderer.canvas.Layer.call(this, vectorLayer); ol.renderer.canvas.Layer.call(this, vectorLayer);
/**
* Declutter tree.
* @private
*/
this.declutterTree_ = vectorLayer.getDeclutter() ?
ol.ext.rbush(9, ['[0]', '[1]', '[2]', '[3]']) : null;
/** /**
* @private * @private
* @type {boolean} * @type {boolean}
@@ -209,6 +217,9 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay
if (clipped) { if (clipped) {
context.restore(); context.restore();
} }
if (this.declutterTree_) {
this.declutterTree_.clear();
}
this.postCompose(context, frameState, layerState, transform); this.postCompose(context, frameState, layerState, transform);
}; };
@@ -226,7 +237,7 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c
var layer = this.getLayer(); var layer = this.getLayer();
/** @type {Object.<string, boolean>} */ /** @type {Object.<string, boolean>} */
var features = {}; var features = {};
return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, var result = this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution,
rotation, hitTolerance, {}, rotation, hitTolerance, {},
/** /**
* @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.Feature|ol.render.Feature} feature Feature.
@@ -238,7 +249,11 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c
features[key] = true; features[key] = true;
return callback.call(thisArg, feature, layer); return callback.call(thisArg, feature, layer);
} }
}); }, null);
if (this.declutterTree_) {
this.declutterTree_.clear();
}
return result;
} }
}; };
@@ -317,7 +332,7 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = function(frameState, lay
var replayGroup = new ol.render.canvas.ReplayGroup( var replayGroup = new ol.render.canvas.ReplayGroup(
ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, resolution, ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, resolution,
pixelRatio, vectorSource.getOverlaps(), vectorLayer.getRenderBuffer()); pixelRatio, vectorSource.getOverlaps(), this.declutterTree_, vectorLayer.getRenderBuffer());
vectorSource.loadFeatures(extent, resolution, projection); vectorSource.loadFeatures(extent, resolution, projection);
/** /**
* @param {ol.Feature} feature Feature. * @param {ol.Feature} feature Feature.

View File

@@ -4,6 +4,7 @@ goog.require('ol');
goog.require('ol.LayerType'); goog.require('ol.LayerType');
goog.require('ol.TileState'); goog.require('ol.TileState');
goog.require('ol.dom'); goog.require('ol.dom');
goog.require('ol.ext.rbush');
goog.require('ol.extent'); goog.require('ol.extent');
goog.require('ol.layer.VectorTileRenderType'); goog.require('ol.layer.VectorTileRenderType');
goog.require('ol.proj'); goog.require('ol.proj');
@@ -33,6 +34,12 @@ ol.renderer.canvas.VectorTileLayer = function(layer) {
ol.renderer.canvas.TileLayer.call(this, layer); ol.renderer.canvas.TileLayer.call(this, layer);
/**
* Declutter tree.
* @private
*/
this.declutterTree_ = layer.getDeclutter() ? ol.ext.rbush(9, ['[0]', '[1]', '[2]', '[3]']) : null;
/** /**
* @private * @private
* @type {boolean} * @type {boolean}
@@ -54,7 +61,6 @@ ol.renderer.canvas.VectorTileLayer = function(layer) {
// Use lower resolution for pure vector rendering. Closest resolution otherwise. // Use lower resolution for pure vector rendering. Closest resolution otherwise.
this.zDirection = this.zDirection =
layer.getRenderMode() == ol.layer.VectorTileRenderType.VECTOR ? 1 : 0; layer.getRenderMode() == ol.layer.VectorTileRenderType.VECTOR ? 1 : 0;
}; };
ol.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.TileLayer); ol.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.TileLayer);
@@ -150,6 +156,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function(
var resolution = tileGrid.getResolution(tile.tileCoord[0]); var resolution = tileGrid.getResolution(tile.tileCoord[0]);
var tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); var tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
var zIndexKeys = {};
for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) { for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
var sourceTile = tile.getTile(tile.tileKeys[t]); var sourceTile = tile.getTile(tile.tileKeys[t]);
if (sourceTile.getState() == ol.TileState.ERROR) { if (sourceTile.getState() == ol.TileState.ERROR) {
@@ -168,8 +175,8 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function(
sourceTile.setProjection(projection); sourceTile.setProjection(projection);
} }
replayState.dirty = false; replayState.dirty = false;
var replayGroup = new ol.render.canvas.ReplayGroup(0, sharedExtent, var replayGroup = new ol.render.canvas.ReplayGroup(0, sharedExtent, resolution,
resolution, pixelRatio, source.getOverlaps(), layer.getRenderBuffer()); pixelRatio, source.getOverlaps(), this.declutterTree_, layer.getRenderBuffer());
var squaredTolerance = ol.renderer.vector.getSquaredTolerance( var squaredTolerance = ol.renderer.vector.getSquaredTolerance(
resolution, pixelRatio); resolution, pixelRatio);
@@ -217,6 +224,9 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function(
} }
} }
replayGroup.finish(); replayGroup.finish();
for (var r in replayGroup.getReplays()) {
zIndexKeys[r] = true;
}
sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup); sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup);
} }
replayState.renderedRevision = revision; replayState.renderedRevision = revision;
@@ -246,6 +256,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi
var rotation = frameState.viewState.rotation; var rotation = frameState.viewState.rotation;
hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; hitTolerance = hitTolerance == undefined ? 0 : hitTolerance;
var layer = this.getLayer(); var layer = this.getLayer();
var declutterReplays = layer.getDeclutter() ? {} : null;
/** @type {Object.<string, boolean>} */ /** @type {Object.<string, boolean>} */
var features = {}; var features = {};
@@ -283,9 +294,12 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi
features[key] = true; features[key] = true;
return callback.call(thisArg, feature, layer); return callback.call(thisArg, feature, layer);
} }
}); }, declutterReplays);
} }
} }
if (this.declutterTree_) {
this.declutterTree_.clear();
}
return found; return found;
}; };
@@ -335,14 +349,16 @@ ol.renderer.canvas.VectorTileLayer.prototype.handleStyleImageChange_ = function(
*/ */
ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, frameState, layerState) { ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, frameState, layerState) {
var layer = this.getLayer(); var layer = this.getLayer();
var declutterReplays = layer.getDeclutter() ? {} : null;
var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); var source = /** @type {ol.source.VectorTile} */ (layer.getSource());
var renderMode = layer.getRenderMode(); var renderMode = layer.getRenderMode();
var replays = ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[renderMode]; var replayTypes = ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[renderMode];
var pixelRatio = frameState.pixelRatio; var pixelRatio = frameState.pixelRatio;
var rotation = frameState.viewState.rotation; var rotation = frameState.viewState.rotation;
var size = frameState.size; var size = frameState.size;
var offsetX = Math.round(pixelRatio * size[0] / 2); var offsetX = Math.round(pixelRatio * size[0] / 2);
var offsetY = Math.round(pixelRatio * size[1] / 2); var offsetY = Math.round(pixelRatio * size[1] / 2);
ol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY);
var tiles = this.renderedTiles; var tiles = this.renderedTiles;
var tileGrid = source.getTileGridForProjection(frameState.viewState.projection); var tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
var clips = []; var clips = [];
@@ -355,21 +371,23 @@ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, fra
var tileCoord = tile.tileCoord; var tileCoord = tile.tileCoord;
var worldOffset = tileGrid.getTileCoordExtent(tileCoord)[0] - var worldOffset = tileGrid.getTileCoordExtent(tileCoord)[0] -
tileGrid.getTileCoordExtent(tile.wrappedTileCoord)[0]; tileGrid.getTileCoordExtent(tile.wrappedTileCoord)[0];
var transform = undefined;
for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) { for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
var sourceTile = tile.getTile(tile.tileKeys[t]); var sourceTile = tile.getTile(tile.tileKeys[t]);
if (sourceTile.getState() == ol.TileState.ERROR) { if (sourceTile.getState() == ol.TileState.ERROR) {
continue; continue;
} }
var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString()); var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString());
if (renderMode != ol.layer.VectorTileRenderType.VECTOR && !replayGroup.hasReplays(replays)) { if (renderMode != ol.layer.VectorTileRenderType.VECTOR && !replayGroup.hasReplays(replayTypes)) {
continue; continue;
} }
if (!transform) {
transform = this.getTransform(frameState, worldOffset);
}
var currentZ = sourceTile.tileCoord[0]; var currentZ = sourceTile.tileCoord[0];
var transform = this.getTransform(frameState, worldOffset);
var currentClip = replayGroup.getClipCoords(transform); var currentClip = replayGroup.getClipCoords(transform);
context.save(); context.save();
context.globalAlpha = layerState.opacity; context.globalAlpha = layerState.opacity;
ol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY);
// Create a clip mask for regions in this low resolution tile that are // Create a clip mask for regions in this low resolution tile that are
// already filled by a higher resolution tile // already filled by a higher resolution tile
for (var j = 0, jj = clips.length; j < jj; ++j) { for (var j = 0, jj = clips.length; j < jj; ++j) {
@@ -389,12 +407,16 @@ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, fra
context.clip(); context.clip();
} }
} }
replayGroup.replay(context, transform, rotation, {}, replays); replayGroup.replay(context, transform, rotation, {}, replayTypes, declutterReplays);
context.restore(); context.restore();
clips.push(currentClip); clips.push(currentClip);
zs.push(currentZ); zs.push(currentZ);
} }
} }
if (declutterReplays) {
ol.render.canvas.ReplayGroup.replayDeclutter(declutterReplays, context, rotation);
this.declutterTree_.clear();
}
ol.renderer.canvas.TileLayer.prototype.postCompose.apply(this, arguments); ol.renderer.canvas.TileLayer.prototype.postCompose.apply(this, arguments);
}; };
@@ -462,7 +484,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function(
ol.transform.scale(transform, pixelScale, -pixelScale); ol.transform.scale(transform, pixelScale, -pixelScale);
ol.transform.translate(transform, -tileExtent[0], -tileExtent[3]); ol.transform.translate(transform, -tileExtent[0], -tileExtent[3]);
var replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString()); var replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString());
replayGroup.replay(context, transform, 0, {}, replays, true); replayGroup.replay(context, transform, 0, {}, replays);
} }
} }
}; };

View File

@@ -57,7 +57,7 @@ ol.renderer.vector.renderCircleGeometry_ = function(replayGroup, geometry, style
if (textStyle) { if (textStyle) {
var textReplay = replayGroup.getReplay( var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT); style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle); textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
}; };
@@ -180,7 +180,7 @@ ol.renderer.vector.renderLineStringGeometry_ = function(replayGroup, geometry, s
if (textStyle) { if (textStyle) {
var textReplay = replayGroup.getReplay( var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT); style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle); textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
}; };
@@ -205,7 +205,7 @@ ol.renderer.vector.renderMultiLineStringGeometry_ = function(replayGroup, geomet
if (textStyle) { if (textStyle) {
var textReplay = replayGroup.getReplay( var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT); style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle); textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
}; };
@@ -231,7 +231,7 @@ ol.renderer.vector.renderMultiPolygonGeometry_ = function(replayGroup, geometry,
if (textStyle) { if (textStyle) {
var textReplay = replayGroup.getReplay( var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT); style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle); textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
}; };
@@ -252,14 +252,14 @@ ol.renderer.vector.renderPointGeometry_ = function(replayGroup, geometry, style,
} }
var imageReplay = replayGroup.getReplay( var imageReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.IMAGE); style.getZIndex(), ol.render.ReplayType.IMAGE);
imageReplay.setImageStyle(imageStyle); imageReplay.setImageStyle(imageStyle, replayGroup.addDeclutter(false));
imageReplay.drawPoint(geometry, feature); imageReplay.drawPoint(geometry, feature);
} }
var textStyle = style.getText(); var textStyle = style.getText();
if (textStyle) { if (textStyle) {
var textReplay = replayGroup.getReplay( var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT); style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle); textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(!!imageStyle));
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
}; };
@@ -280,14 +280,14 @@ ol.renderer.vector.renderMultiPointGeometry_ = function(replayGroup, geometry, s
} }
var imageReplay = replayGroup.getReplay( var imageReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.IMAGE); style.getZIndex(), ol.render.ReplayType.IMAGE);
imageReplay.setImageStyle(imageStyle); imageReplay.setImageStyle(imageStyle, replayGroup.addDeclutter(false));
imageReplay.drawMultiPoint(geometry, feature); imageReplay.drawMultiPoint(geometry, feature);
} }
var textStyle = style.getText(); var textStyle = style.getText();
if (textStyle) { if (textStyle) {
var textReplay = replayGroup.getReplay( var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT); style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle); textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(!!imageStyle));
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
}; };
@@ -313,7 +313,7 @@ ol.renderer.vector.renderPolygonGeometry_ = function(replayGroup, geometry, styl
if (textStyle) { if (textStyle) {
var textReplay = replayGroup.getReplay( var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT); style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle); textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
}; };

View File

@@ -4,6 +4,7 @@ goog.require('ol');
goog.require('ol.dom'); goog.require('ol.dom');
goog.require('ol.events'); goog.require('ol.events');
goog.require('ol.events.EventType'); goog.require('ol.events.EventType');
goog.require('ol.ext.rbush');
goog.require('ol.extent'); goog.require('ol.extent');
goog.require('ol.render.canvas.ReplayGroup'); goog.require('ol.render.canvas.ReplayGroup');
goog.require('ol.renderer.vector'); goog.require('ol.renderer.vector');
@@ -55,6 +56,12 @@ ol.source.ImageVector = function(options) {
*/ */
this.canvasSize_ = [0, 0]; this.canvasSize_ = [0, 0];
/**
* Declutter tree.
* @private
*/
this.declutterTree_ = ol.ext.rbush(9, ['[0]', '[1]', '[2]', '[3]']);
/** /**
* @private * @private
* @type {number} * @type {number}
@@ -113,7 +120,7 @@ ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resol
var replayGroup = new ol.render.canvas.ReplayGroup( var replayGroup = new ol.render.canvas.ReplayGroup(
ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
resolution, pixelRatio, this.source_.getOverlaps(), this.renderBuffer_); resolution, pixelRatio, this.source_.getOverlaps(), this.declutterTree_, this.renderBuffer_);
this.source_.loadFeatures(extent, resolution, projection); this.source_.loadFeatures(extent, resolution, projection);
@@ -146,6 +153,7 @@ ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resol
replayGroup.replay(this.canvasContext_, transform, 0, {}); replayGroup.replay(this.canvasContext_, transform, 0, {});
this.replayGroup_ = replayGroup; this.replayGroup_ = replayGroup;
this.declutterTree_.clear();
return this.canvasContext_.canvas; return this.canvasContext_.canvas;
}; };
@@ -161,7 +169,7 @@ ol.source.ImageVector.prototype.forEachFeatureAtCoordinate = function(
} else { } else {
/** @type {Object.<string, boolean>} */ /** @type {Object.<string, boolean>} */
var features = {}; var features = {};
return this.replayGroup_.forEachFeatureAtCoordinate( var result = this.replayGroup_.forEachFeatureAtCoordinate(
coordinate, resolution, 0, hitTolerance, skippedFeatureUids, coordinate, resolution, 0, hitTolerance, skippedFeatureUids,
/** /**
* @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.Feature|ol.render.Feature} feature Feature.
@@ -173,7 +181,9 @@ ol.source.ImageVector.prototype.forEachFeatureAtCoordinate = function(
features[key] = true; features[key] = true;
return callback(feature); return callback(feature);
} }
}); }, null);
this.declutterTree_.clear();
return result;
} }
}; };