Merge pull request #2254 from elemoine/replaygroup

Use replay groups for feature overlays
This commit is contained in:
Andreas Hocevar
2014-06-27 12:59:06 +02:00
15 changed files with 165 additions and 131 deletions

View File

@@ -10,7 +10,7 @@ goog.require('ol.CollectionEventType');
goog.require('ol.Feature');
goog.require('ol.feature');
goog.require('ol.render.EventType');
goog.require('ol.style.ImageState');
goog.require('ol.renderer.vector');
@@ -71,8 +71,11 @@ ol.FeatureOverlay = function(opt_options) {
* @private
* @type {ol.feature.StyleFunction|undefined}
*/
this.styleFunction_ = goog.isDef(options.style) ?
ol.feature.createStyleFunction(options.style) : undefined;
this.styleFunction_ = undefined;
if (goog.isDef(options.style)) {
this.setStyle(options.style);
}
if (goog.isDef(options.features)) {
if (goog.isArray(options.features)) {
@@ -101,37 +104,6 @@ ol.FeatureOverlay.prototype.addFeature = function(feature) {
};
/**
* @param {ol.render.IVectorContext|undefined} vectorContext Vector context.
* @param {ol.Feature} feature Feature.
* @param {ol.style.Style} style Style.
* @private
*/
ol.FeatureOverlay.prototype.drawFeature_ = function(vectorContext, feature,
style) {
var imageStyle = style.getImage();
if (!goog.isNull(imageStyle)) {
var imageState = imageStyle.getImageState();
if (imageState == ol.style.ImageState.LOADED ||
imageState == ol.style.ImageState.ERROR) {
imageStyle.unlistenImageChange(this.handleImageChange_, this);
if (imageState == ol.style.ImageState.LOADED) {
vectorContext.drawFeature(feature, style);
}
} else {
if (imageState == ol.style.ImageState.IDLE) {
imageStyle.load();
}
imageState = imageStyle.getImageState();
goog.asserts.assert(imageState == ol.style.ImageState.LOADING);
imageStyle.listenImageChange(this.handleImageChange_, this);
}
} else {
vectorContext.drawFeature(feature, style);
}
};
/**
* @return {ol.Collection} Features collection.
* @todo api
@@ -199,8 +171,12 @@ ol.FeatureOverlay.prototype.handleMapPostCompose_ = function(event) {
if (!goog.isDef(styleFunction)) {
styleFunction = ol.feature.defaultStyleFunction;
}
var resolution = event.frameState.view2DState.resolution;
var vectorContext = event.vectorContext;
var replayGroup = /** @type {ol.render.IReplayGroup} */
(event.replayGroup);
goog.asserts.assert(goog.isDef(replayGroup));
var frameState = event.frameState;
var pixelRatio = frameState.pixelRatio;
var resolution = frameState.view2DState.resolution;
var i, ii, styles;
this.features_.forEach(function(feature) {
styles = styleFunction(feature, resolution);
@@ -209,7 +185,9 @@ ol.FeatureOverlay.prototype.handleMapPostCompose_ = function(event) {
}
ii = styles.length;
for (i = 0; i < ii; ++i) {
this.drawFeature_(vectorContext, feature, styles[i]);
ol.renderer.vector.renderFeature(replayGroup, feature, styles[i],
ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
feature, this.handleImageChange_, this);
}
}, this);
};

View File

@@ -238,7 +238,7 @@ ol.geom.flat.simplify.radialDistance = function(flatCoordinates, offset, end,
/**
* @param {number} value Value.
* @param {number} tolerance Squared tolerance.
* @param {number} tolerance Tolerance.
* @return {number} Rounded value.
*/
ol.geom.flat.simplify.snap = function(value, tolerance) {
@@ -259,7 +259,7 @@ ol.geom.flat.simplify.snap = function(value, tolerance) {
* @param {number} offset Offset.
* @param {number} end End.
* @param {number} stride Stride.
* @param {number} tolerance Squared tolerance.
* @param {number} tolerance Tolerance.
* @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
* coordinates.
* @param {number} simplifiedOffset Simplified offset.
@@ -344,7 +344,7 @@ ol.geom.flat.simplify.quantize = function(flatCoordinates, offset, end, stride,
* @param {number} offset Offset.
* @param {Array.<number>} ends Ends.
* @param {number} stride Stride.
* @param {number} tolerance Squared tolerance.
* @param {number} tolerance Tolerance.
* @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
* coordinates.
* @param {number} simplifiedOffset Simplified offset.
@@ -374,7 +374,7 @@ ol.geom.flat.simplify.quantizes = function(
* @param {number} offset Offset.
* @param {Array.<Array.<number>>} endss Endss.
* @param {number} stride Stride.
* @param {number} tolerance Squared tolerance.
* @param {number} tolerance Tolerance.
* @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
* coordinates.
* @param {number} simplifiedOffset Simplified offset.

View File

@@ -150,6 +150,8 @@ ol.interaction.Select.prototype.handleMapBrowserEvent =
}
} else {
// Modify the currently selected feature(s).
var /** @type {Array.<number>} */ deselected = [];
var /** @type {Array.<ol.Feature>} */ selected = [];
map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
/**
* @param {ol.Feature} feature Feature.
@@ -159,14 +161,19 @@ ol.interaction.Select.prototype.handleMapBrowserEvent =
var index = goog.array.indexOf(features.getArray(), feature);
if (index == -1) {
if (add || toggle) {
features.push(feature);
selected.push(feature);
}
} else {
if (remove || toggle) {
features.removeAt(index);
deselected.push(index);
}
}
}, undefined, this.layerFilter_);
var i;
for (i = deselected.length - 1; i >= 0; --i) {
features.removeAt(deselected[i]);
}
features.extend(selected);
}
return false;
};

View File

@@ -535,16 +535,24 @@ ol.Map.prototype.disposeInternal = function() {
/**
* 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 `opt_layerFilter`. Feature overlays will always be
* included in the detection.
* @param {ol.Pixel} pixel Pixel.
* @param {function(this: S, ol.Feature, ol.layer.Layer): T} callback Feature
* callback.
* callback. If the detected feature is not on a layer, but on a
* {@link ol.FeatureOverlay}, then the 2nd argument to this function will
* be `null`. To stop detection, callback functions can return a truthy
* value.
* @param {S=} opt_this Value to use as `this` when executing `callback`.
* @param {function(this: U, ol.layer.Layer): boolean=} opt_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.
* function returns `true` will be tested for features. By default, all
* visible layers will be tested. Feature overlays will always be tested.
* @param {U=} opt_this2 Value to use as `this` when executing `layerFilter`.
* @return {T|undefined} Callback result.
* @return {T|undefined} Callback result, i.e. the return value of last
* callback execution, or the first truthy callback return value.
* @template S,T,U
* @todo api stable
*/

View File

@@ -173,6 +173,12 @@ ol.MOUSEWHEELZOOM_TIMEOUT_DURATION = 80;
ol.ROTATE_ANIMATION_DURATION = 250;
/**
* @define {number} Tolerance for geometry simplification in device pixels.
*/
ol.SIMPLIFY_TOLERANCE = 0.5;
/**
* @define {number} Texture cache high water mark.
*/

View File

@@ -35,13 +35,14 @@ ol.render.EventType = {
* @param {ol.render.EventType} type Type.
* @param {Object=} opt_target Target.
* @param {ol.render.IVectorContext=} opt_vectorContext Vector context.
* @param {ol.render.IReplayGroup=} opt_replayGroup Replay group.
* @param {olx.FrameState=} opt_frameState Frame state.
* @param {?CanvasRenderingContext2D=} opt_context Context.
* @param {?ol.webgl.Context=} opt_glContext WebGL Context.
*/
ol.render.Event = function(
type, opt_target, opt_vectorContext, opt_frameState, opt_context,
opt_glContext) {
type, opt_target, opt_vectorContext, opt_replayGroup, opt_frameState,
opt_context, opt_glContext) {
goog.base(this, type, opt_target);
@@ -52,6 +53,11 @@ ol.render.Event = function(
*/
this.vectorContext = opt_vectorContext;
/**
* @type {ol.render.IReplayGroup|undefined}
*/
this.replayGroup = opt_replayGroup;
/**
* @type {olx.FrameState|undefined}
* @todo api

View File

@@ -24,6 +24,27 @@ ol.renderer.vector.defaultOrder = function(feature1, feature2) {
};
/**
* @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio.
* @return {number} Squared pixel tolerance.
*/
ol.renderer.vector.getSquaredTolerance = function(resolution, pixelRatio) {
var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio);
return tolerance * tolerance;
};
/**
* @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio.
* @return {number} Pixel tolerance.
*/
ol.renderer.vector.getTolerance = function(resolution, pixelRatio) {
return ol.SIMPLIFY_TOLERANCE * resolution / pixelRatio;
};
/**
* @param {ol.render.IReplayGroup} replayGroup Replay group.
* @param {ol.geom.Geometry} geometry Geometry.

View File

@@ -90,8 +90,8 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ =
var render = new ol.render.canvas.Immediate(
context, frameState.pixelRatio, frameState.extent, transform,
frameState.view2DState.rotation);
var composeEvent = new ol.render.Event(type, layer, render, frameState,
context, null);
var composeEvent = new ol.render.Event(type, layer, render, null,
frameState, context, null);
layer.dispatchEvent(composeEvent);
render.flush();
}

View File

@@ -17,11 +17,13 @@ goog.require('ol.layer.Vector');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.canvas.Immediate');
goog.require('ol.render.canvas.ReplayGroup');
goog.require('ol.renderer.Map');
goog.require('ol.renderer.canvas.ImageLayer');
goog.require('ol.renderer.canvas.Layer');
goog.require('ol.renderer.canvas.TileLayer');
goog.require('ol.renderer.canvas.VectorLayer');
goog.require('ol.renderer.vector');
goog.require('ol.source.State');
goog.require('ol.vec.Mat4');
@@ -97,8 +99,11 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ =
var map = this.getMap();
var context = this.context_;
if (map.hasListener(type)) {
var view2DState = frameState.view2DState;
var extent = frameState.extent;
var pixelRatio = frameState.pixelRatio;
var view2DState = frameState.view2DState;
var resolution = view2DState.resolution;
var rotation = view2DState.rotation;
ol.vec.Mat4.makeTransform2D(this.transform_,
this.canvas_.width / 2,
this.canvas_.height / 2,
@@ -106,12 +111,21 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ =
-pixelRatio / view2DState.resolution,
-view2DState.rotation,
-view2DState.center[0], -view2DState.center[1]);
var render = new ol.render.canvas.Immediate(context, pixelRatio,
frameState.extent, this.transform_, view2DState.rotation);
var composeEvent = new ol.render.Event(type, map, render, frameState,
context, null);
var vectorContext = new ol.render.canvas.Immediate(context, pixelRatio,
extent, this.transform_, rotation);
var replayGroup = new ol.render.canvas.ReplayGroup(
ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
resolution);
var composeEvent = new ol.render.Event(type, map, vectorContext,
replayGroup, frameState, context, null);
map.dispatchEvent(composeEvent);
render.flush();
replayGroup.finish();
if (!replayGroup.isEmpty()) {
replayGroup.replay(context, extent, pixelRatio, this.transform_,
rotation, {});
}
vectorContext.flush();
this.replayGroup = replayGroup;
}
};

View File

@@ -206,9 +206,10 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame =
if (!goog.isDef(styleFunction)) {
styleFunction = ol.feature.defaultStyleFunction;
}
var tolerance = resolution / (2 * pixelRatio);
var replayGroup =
new ol.render.canvas.ReplayGroup(tolerance, extent, resolution);
new ol.render.canvas.ReplayGroup(
ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
resolution);
vectorSource.loadFeatures(extent, resolution, projection);
var renderFeature =
/**
@@ -261,14 +262,12 @@ ol.renderer.canvas.VectorLayer.prototype.renderFeature =
if (!goog.isDefAndNotNull(styles)) {
return false;
}
// simplify to a tolerance of half a device pixel
var squaredTolerance =
resolution * resolution / (4 * pixelRatio * pixelRatio);
var i, ii, loading = false;
for (i = 0, ii = styles.length; i < ii; ++i) {
loading = ol.renderer.vector.renderFeature(
replayGroup, feature, styles[i], squaredTolerance, feature,
this.handleImageChange_, this) || loading;
replayGroup, feature, styles[i],
ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
feature, this.handleImageChange_, this) || loading;
}
return loading;
};

View File

@@ -43,6 +43,12 @@ ol.renderer.Map = function(container, map) {
*/
this.map_ = map;
/**
* @protected
* @type {ol.render.IReplayGroup}
*/
this.replayGroup = null;
/**
* @private
* @type {Object.<string, ol.renderer.Layer>}
@@ -110,9 +116,36 @@ ol.renderer.Map.prototype.disposeInternal = function() {
ol.renderer.Map.prototype.forEachFeatureAtPixel =
function(coordinate, frameState, callback, thisArg,
layerFilter, thisArg2) {
var result;
var extent = frameState.extent;
var view2DState = frameState.view2DState;
var viewResolution = view2DState.resolution;
var viewRotation = view2DState.rotation;
if (!goog.isNull(this.replayGroup)) {
/** @type {Object.<string, boolean>} */
var features = {};
result = this.replayGroup.forEachGeometryAtPixel(extent, viewResolution,
viewRotation, coordinate, {},
/**
* @param {ol.geom.Geometry} geometry Geometry.
* @param {Object} data Data.
* @return {?} Callback result.
*/
function(geometry, data) {
var feature = /** @type {ol.Feature} */ (data);
goog.asserts.assert(goog.isDef(feature));
var key = goog.getUid(feature).toString();
if (!(key in features)) {
features[key] = true;
return callback.call(thisArg, feature, null);
}
});
if (result) {
return result;
}
}
var layerStates = this.map_.getLayerGroup().getLayerStatesArray();
var numLayers = layerStates.length;
var viewResolution = frameState.view2DState.resolution;
var i;
for (i = numLayers - 1; i >= 0; --i) {
var layerState = layerStates[i];
@@ -120,7 +153,7 @@ ol.renderer.Map.prototype.forEachFeatureAtPixel =
if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
layerFilter.call(thisArg2, layer)) {
var layerRenderer = this.getLayerRenderer(layer);
var result = layerRenderer.forEachFeatureAtPixel(
result = layerRenderer.forEachFeatureAtPixel(
coordinate, frameState, callback, thisArg);
if (result) {
return result;

View File

@@ -239,7 +239,7 @@ ol.renderer.webgl.Layer.prototype.dispatchComposeEvent_ =
if (layer.hasListener(type)) {
var render = new ol.render.webgl.Immediate(context, frameState.pixelRatio);
var composeEvent = new ol.render.Event(
type, layer, render, frameState, null, context);
type, layer, render, null, frameState, null, context);
layer.dispatchEvent(composeEvent);
}
};

View File

@@ -269,7 +269,7 @@ ol.renderer.webgl.Map.prototype.dispatchComposeEvent_ =
var context = this.getContext();
var render = new ol.render.webgl.Immediate(context, frameState.pixelRatio);
var composeEvent = new ol.render.Event(
type, map, render, frameState, null, context);
type, map, render, null, frameState, null, context);
map.dispatchEvent(composeEvent);
}
};

View File

@@ -102,8 +102,8 @@ goog.inherits(ol.source.ImageVector, ol.source.ImageCanvas);
ol.source.ImageVector.prototype.canvasFunctionInternal_ =
function(extent, resolution, pixelRatio, size, projection) {
var tolerance = resolution / (2 * pixelRatio);
var replayGroup = new ol.render.canvas.ReplayGroup(tolerance, extent,
var replayGroup = new ol.render.canvas.ReplayGroup(
ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), extent,
resolution);
var loading = false;
@@ -226,14 +226,12 @@ ol.source.ImageVector.prototype.renderFeature_ =
if (!goog.isDefAndNotNull(styles)) {
return false;
}
// simplify to a tolerance of half a device pixel
var squaredTolerance =
resolution * resolution / (4 * pixelRatio * pixelRatio);
var i, ii, loading = false;
for (i = 0, ii = styles.length; i < ii; ++i) {
loading = ol.renderer.vector.renderFeature(
replayGroup, feature, styles[i], squaredTolerance, feature,
this.handleImageChange_, this) || loading;
replayGroup, feature, styles[i],
ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
feature, this.handleImageChange_, this) || loading;
}
return loading;
};

View File

@@ -1,70 +1,34 @@
goog.provide('ol.test.FeatureOverlay');
describe('ol.Feature', function() {
var featureOverlay;
describe('ol.FeatureOverlay', function() {
beforeEach(function() {
featureOverlay = new ol.FeatureOverlay();
});
describe('constructor', function() {
afterEach(function() {
ol.style.IconImageCache.getInstance().clear();
});
describe('#drawFeature_ style with no image', function() {
it('calls vectorContext.drawFeature', function() {
var vectorContext = new ol.render.canvas.Immediate(
null, // context
1, // pixelRatio
[], // extent
goog.vec.Mat4.createNumberIdentity(), // transform
0 // viewRotation
);
var feature = new ol.Feature();
var style = new ol.style.Style({
fill: new ol.style.Fill({
color: '#ffffff'
})
});
var spy = sinon.spy(vectorContext, 'drawFeature');
featureOverlay.drawFeature_(vectorContext, feature, style);
expect(spy.calledOnce).to.be.ok();
it('creates an new feature overlay', function() {
var featureOverlay = new ol.FeatureOverlay();
expect(featureOverlay).to.be.a(ol.FeatureOverlay);
});
});
describe('#drawFeature_ style with unloaded image', function() {
it('calls image.load', function() {
var vectorContext = new ol.render.canvas.Immediate(
null, // context
1, // pixelRatio
[], // extent
goog.vec.Mat4.createNumberIdentity(), // transform
0 // viewRotation
);
var feature = new ol.Feature();
var style = new ol.style.Style({
image: new ol.style.Icon({
src: 'http://example.com/icon.png'
})
it('takes features', function() {
var featureOverlay = new ol.FeatureOverlay({
features: [new ol.Feature(new ol.geom.Point([0, 0]))]
});
var stub = sinon.stub(style.getImage(), 'load', function() {
style.getImage().iconImage_.imageState_ =
ol.style.ImageState.LOADING;
});
featureOverlay.drawFeature_(vectorContext, feature, style);
expect(stub.calledOnce).to.be.ok();
expect(featureOverlay.getFeatures().getLength()).to.be(1);
});
it('takes a style', function() {
var style = [new ol.style.Style()];
var featureOverlay = new ol.FeatureOverlay({
style: [new ol.style.Style()]
});
expect(featureOverlay.getStyle()).to.eql(style);
expect(featureOverlay.getStyleFunction()()).to.eql(style);
});
});
});
goog.require('goog.vec.Mat4');
goog.require('ol.Feature');
goog.require('ol.FeatureOverlay');
goog.require('ol.style.ImageState');
goog.require('ol.render.canvas.Immediate');
goog.require('ol.style.Fill');
goog.require('ol.style.Icon');
goog.require('ol.style.IconImageCache');
goog.require('ol.geom.Point');
goog.require('ol.style.Style');