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

@@ -7,6 +7,7 @@ goog.require('ol.extent');
goog.require('ol.geom.flat.transform');
goog.require('ol.obj');
goog.require('ol.render.ReplayGroup');
goog.require('ol.render.ReplayType');
goog.require('ol.render.canvas.Replay');
goog.require('ol.render.canvas.ImageReplay');
goog.require('ol.render.canvas.LineStringReplay');
@@ -24,13 +25,33 @@ goog.require('ol.transform');
* @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio.
* @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.
* @struct
*/
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);
/**
* 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
* @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.
* @return {boolean} Has replays of the provided types.
@@ -211,11 +297,12 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() {
* to skip.
* @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
* callback.
* @param {Object.<string, Array.<*>>} declutterReplays Declutter replays.
* @return {T|undefined} Callback result.
* @template T
*/
ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback) {
coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback, declutterReplays) {
hitTolerance = Math.round(hitTolerance);
var contextSize = hitTolerance * 2 + 1;
@@ -245,7 +332,7 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance);
return this.replayHitDetection_(context, transform, rotation,
var result = this.replayHitDetection_(context, transform, rotation,
skippedFeaturesHash,
/**
* @param {ol.Feature|ol.render.Feature} feature Feature.
@@ -269,6 +356,11 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
}
}
}, 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) {
var Constructor = ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_[replayType];
replay = new Constructor(this.tolerance_, this.maxExtent_,
this.resolution_, this.pixelRatio_, this.overlaps_);
this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_);
replays[replayType] = 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
*/
@@ -326,9 +426,10 @@ ol.render.canvas.ReplayGroup.prototype.isEmpty = function() {
* to skip.
* @param {Array.<ol.render.ReplayType>=} opt_replayTypes Ordered replay types
* 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,
transform, viewRotation, skippedFeaturesHash, opt_replayTypes) {
transform, viewRotation, skippedFeaturesHash, opt_replayTypes, opt_declutterReplays) {
/** @type {Array.<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
// visible outside the current extent when panning
var flatClipCoords = this.getClipCoords(transform);
context.save();
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();
this.clip(context, transform);
var replayTypes = opt_replayTypes ? opt_replayTypes : ol.render.replay.ORDER;
var i, ii, j, jj, replays, replay;
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) {
replay = replays[replayTypes[j]];
var replayType = replayTypes[j];
replay = replays[replayType];
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.
* @param {ol.Extent=} opt_hitExtent Only check features that intersect this
* extent.
* @param {Object.<string, Array.<*>>=} opt_declutterReplays Declutter replays.
* @return {T|undefined} Callback result.
* @template T
*/
ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
context, transform, viewRotation, skippedFeaturesHash,
featureCallback, opt_hitExtent) {
featureCallback, opt_hitExtent, opt_declutterReplays) {
/** @type {Array.<number>} */
var zs = Object.keys(this.replaysByZIndex_).map(Number);
zs.sort(function(a, b) {
@@ -386,14 +494,26 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
var i, ii, j, replays, replay, result;
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) {
replay = replays[ol.render.replay.ORDER[j]];
var replayType = ol.render.replay.ORDER[j];
replay = replays[replayType];
if (replay !== undefined) {
result = replay.replayHitDetection(context, transform, viewRotation,
skippedFeaturesHash, featureCallback, opt_hitExtent);
if (result) {
return result;
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 {
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
* @type {Object.<ol.render.ReplayType,
* function(new: ol.render.canvas.Replay, number, ol.Extent,
* number, number, boolean)>}
* number, number, boolean, Array.<Array.<*>>)>}
*/
ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = {
'Circle': ol.render.canvas.PolygonReplay,