Merge pull request #6222 from ahocevar/fix-foreachlayeratpixel

Fix forEachLayerAtPixel and improve class hierarchy
This commit is contained in:
Andreas Hocevar
2016-12-07 22:29:05 +01:00
committed by GitHub
5 changed files with 208 additions and 222 deletions

View File

@@ -2,23 +2,20 @@ goog.provide('ol.renderer.canvas.ImageLayer');
goog.require('ol'); goog.require('ol');
goog.require('ol.View'); goog.require('ol.View');
goog.require('ol.dom');
goog.require('ol.extent'); goog.require('ol.extent');
goog.require('ol.functions');
goog.require('ol.proj'); goog.require('ol.proj');
goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.IntermediateCanvas');
goog.require('ol.source.ImageVector');
goog.require('ol.transform'); goog.require('ol.transform');
/** /**
* @constructor * @constructor
* @extends {ol.renderer.canvas.Layer} * @extends {ol.renderer.canvas.IntermediateCanvas}
* @param {ol.layer.Image} imageLayer Single image layer. * @param {ol.layer.Image} imageLayer Single image layer.
*/ */
ol.renderer.canvas.ImageLayer = function(imageLayer) { ol.renderer.canvas.ImageLayer = function(imageLayer) {
ol.renderer.canvas.Layer.call(this, imageLayer); ol.renderer.canvas.IntermediateCanvas.call(this, imageLayer);
/** /**
* @private * @private
@@ -32,88 +29,8 @@ ol.renderer.canvas.ImageLayer = function(imageLayer) {
*/ */
this.imageTransform_ = ol.transform.create(); this.imageTransform_ = ol.transform.create();
/**
* @private
* @type {ol.Transform}
*/
this.coordinateToCanvasPixelTransform_ = ol.transform.create();
/**
* @private
* @type {CanvasRenderingContext2D}
*/
this.hitCanvasContext_ = null;
};
ol.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.Layer);
/**
* @inheritDoc
*/
ol.renderer.canvas.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
var layer = this.getLayer();
var source = layer.getSource();
var resolution = frameState.viewState.resolution;
var rotation = frameState.viewState.rotation;
var skippedFeatureUids = frameState.skippedFeatureUids;
return source.forEachFeatureAtCoordinate(
coordinate, resolution, rotation, skippedFeatureUids,
/**
* @param {ol.Feature|ol.render.Feature} feature Feature.
* @return {?} Callback result.
*/
function(feature) {
return callback.call(thisArg, feature, layer);
});
};
/**
* @param {ol.Coordinate} coordinate Coordinate.
* @param {olx.FrameState} frameState FrameState.
* @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer
* callback.
* @param {S} thisArg Value to use as `this` when executing `callback`.
* @return {T|undefined} Callback result.
* @template S,T,U
*/
ol.renderer.canvas.ImageLayer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) {
if (!this.getImage()) {
return undefined;
}
if (this.getLayer().getSource() instanceof ol.source.ImageVector) {
// for ImageVector sources use the original hit-detection logic,
// so that for example also transparent polygons are detected
var hasFeature = this.forEachFeatureAtCoordinate(
coordinate, frameState, ol.functions.TRUE, this);
if (hasFeature) {
return callback.call(thisArg, this.getLayer(), null);
} else {
return undefined;
}
} else {
var pixelOnCanvas = ol.transform.apply(
this.coordinateToCanvasPixelTransform_, coordinate.slice());
if (!this.hitCanvasContext_) {
this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1);
}
this.hitCanvasContext_.clearRect(0, 0, 1, 1);
this.hitCanvasContext_.drawImage(
this.getImage(), pixelOnCanvas[0], pixelOnCanvas[1], 1, 1, 0, 0, 1, 1);
var imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data;
if (imageData[3] > 0) {
return callback.call(thisArg, this.getLayer(), imageData);
} else {
return undefined;
}
}
}; };
ol.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.IntermediateCanvas);
/** /**
@@ -172,6 +89,7 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye
var loaded = this.loadImage(image); var loaded = this.loadImage(image);
if (loaded) { if (loaded) {
this.image_ = image; this.image_ = image;
this.renderedResolution = viewResolution;
} }
} }
} }
@@ -183,19 +101,18 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye
var imagePixelRatio = image.getPixelRatio(); var imagePixelRatio = image.getPixelRatio();
var scale = pixelRatio * imageResolution / var scale = pixelRatio * imageResolution /
(viewResolution * imagePixelRatio); (viewResolution * imagePixelRatio);
var transform = ol.transform.reset(this.imageTransform_); var transform = ol.transform.compose(this.imageTransform_,
ol.transform.translate(transform, pixelRatio * size[0] / 2, pixelRatio * size[1] / 2,
pixelRatio * frameState.size[0] / 2, scale, scale,
pixelRatio * frameState.size[1] / 2); 0,
ol.transform.scale(transform, scale, scale);
ol.transform.translate(transform,
imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution, imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution,
imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution); imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution);
ol.transform.compose(ol.transform.reset(this.coordinateToCanvasPixelTransform_), ol.transform.compose(this.coordinateToCanvasPixelTransform,
pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5],
pixelRatio / viewResolution, -pixelRatio / viewResolution, pixelRatio / viewResolution, -pixelRatio / viewResolution,
0, 0,
-viewCenter[0], -viewCenter[1]); -viewCenter[0], -viewCenter[1]);
this.updateAttributions(frameState.attributions, image.getAttributions()); this.updateAttributions(frameState.attributions, image.getAttributions());
this.updateLogos(frameState, imageSource); this.updateLogos(frameState, imageSource);
} }

View File

@@ -0,0 +1,149 @@
goog.provide('ol.renderer.canvas.IntermediateCanvas');
goog.require('ol');
goog.require('ol.coordinate');
goog.require('ol.dom');
goog.require('ol.renderer.canvas.Layer');
goog.require('ol.transform');
/**
* @constructor
* @extends {ol.renderer.canvas.Layer}
* @param {ol.layer.Layer} layer Layer.
*/
ol.renderer.canvas.IntermediateCanvas = function(layer) {
ol.renderer.canvas.Layer.call(this, layer);
/**
* @protected
* @type {ol.Transform}
*/
this.coordinateToCanvasPixelTransform = ol.transform.create();
/**
* @private
* @type {CanvasRenderingContext2D}
*/
this.hitCanvasContext_ = null;
/**
* @protected
* @type {number}
*/
this.renderedResolution;
};
ol.inherits(ol.renderer.canvas.IntermediateCanvas, ol.renderer.canvas.Layer);
/**
* @inheritDoc
*/
ol.renderer.canvas.IntermediateCanvas.prototype.composeFrame = function(frameState, layerState, context) {
this.preCompose(context, frameState);
var image = this.getImage();
if (image) {
// clipped rendering if layer extent is set
var extent = layerState.extent;
var clipped = extent !== undefined;
if (clipped) {
this.clip(context, frameState, /** @type {ol.Extent} */ (extent));
}
var imageTransform = this.getImageTransform();
// for performance reasons, context.save / context.restore is not used
// to save and restore the transformation matrix and the opacity.
// see http://jsperf.com/context-save-restore-versus-variable
var alpha = context.globalAlpha;
context.globalAlpha = layerState.opacity;
// for performance reasons, context.setTransform is only used
// when the view is rotated. see http://jsperf.com/canvas-transform
var dx = imageTransform[4];
var dy = imageTransform[5];
var dw = image.width * imageTransform[0];
var dh = image.height * imageTransform[3];
context.drawImage(image, 0, 0, +image.width, +image.height,
Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh));
context.globalAlpha = alpha;
if (clipped) {
context.restore();
}
}
this.postCompose(context, frameState, layerState);
};
/**
* @abstract
* @return {HTMLCanvasElement|HTMLVideoElement|Image} Canvas.
*/
ol.renderer.canvas.IntermediateCanvas.prototype.getImage = function() {};
/**
* @abstract
* @return {!ol.Transform} Image transform.
*/
ol.renderer.canvas.IntermediateCanvas.prototype.getImageTransform = function() {};
/**
* @inheritDoc
*/
ol.renderer.canvas.IntermediateCanvas.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
var layer = this.getLayer();
var source = layer.getSource();
var resolution = frameState.viewState.resolution;
var rotation = frameState.viewState.rotation;
var skippedFeatureUids = frameState.skippedFeatureUids;
return source.forEachFeatureAtCoordinate(
coordinate, resolution, rotation, skippedFeatureUids,
/**
* @param {ol.Feature|ol.render.Feature} feature Feature.
* @return {?} Callback result.
*/
function(feature) {
return callback.call(thisArg, feature, layer);
});
};
/**
* @inheritDoc
*/
ol.renderer.canvas.IntermediateCanvas.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) {
if (!this.getImage()) {
return undefined;
}
if (this.getLayer().getSource().forEachFeatureAtCoordinate !== ol.nullFunction) {
// for ImageVector sources use the original hit-detection logic,
// so that for example also transparent polygons are detected
return ol.renderer.canvas.Layer.prototype.forEachLayerAtCoordinate.apply(this, arguments);
} else {
var pixel = ol.transform.apply(this.coordinateToCanvasPixelTransform, coordinate);
ol.coordinate.scale(pixel, frameState.viewState.resolution / this.renderedResolution);
if (!this.hitCanvasContext_) {
this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1);
}
this.hitCanvasContext_.clearRect(0, 0, 1, 1);
this.hitCanvasContext_.drawImage(this.getImage(), pixel[0], pixel[1], 1, 1, 0, 0, 1, 1);
var imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data;
if (imageData[3] > 0) {
return callback.call(thisArg, this.getLayer(), imageData);
} else {
return undefined;
}
}
};

View File

@@ -19,6 +19,12 @@ ol.renderer.canvas.Layer = function(layer) {
ol.renderer.Layer.call(this, layer); ol.renderer.Layer.call(this, layer);
/**
* @protected
* @type {number}
*/
this.renderedResolution;
/** /**
* @private * @private
* @type {ol.Transform} * @type {ol.Transform}
@@ -62,51 +68,6 @@ ol.renderer.canvas.Layer.prototype.clip = function(context, frameState, extent)
}; };
/**
* @param {olx.FrameState} frameState Frame state.
* @param {ol.LayerState} layerState Layer state.
* @param {CanvasRenderingContext2D} context Context.
*/
ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerState, context) {
this.preCompose(context, frameState);
var image = this.getImage();
if (image) {
// clipped rendering if layer extent is set
var extent = layerState.extent;
var clipped = extent !== undefined;
if (clipped) {
this.clip(context, frameState, /** @type {ol.Extent} */ (extent));
}
var imageTransform = this.getImageTransform();
// for performance reasons, context.save / context.restore is not used
// to save and restore the transformation matrix and the opacity.
// see http://jsperf.com/context-save-restore-versus-variable
var alpha = context.globalAlpha;
context.globalAlpha = layerState.opacity;
// for performance reasons, context.setTransform is only used
// when the view is rotated. see http://jsperf.com/canvas-transform
var dx = imageTransform[4];
var dy = imageTransform[5];
var dw = image.width * imageTransform[0];
var dh = image.height * imageTransform[3];
context.drawImage(image, 0, 0, +image.width, +image.height,
Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh));
context.globalAlpha = alpha;
if (clipped) {
context.restore();
}
}
this.postCompose(context, frameState, layerState);
};
/** /**
* @param {ol.render.Event.Type} type Event type. * @param {ol.render.Event.Type} type Event type.
* @param {CanvasRenderingContext2D} context Context. * @param {CanvasRenderingContext2D} context Context.
@@ -134,6 +95,27 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, contex
}; };
/**
* @param {ol.Coordinate} coordinate Coordinate.
* @param {olx.FrameState} frameState FrameState.
* @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer
* callback.
* @param {S} thisArg Value to use as `this` when executing `callback`.
* @return {T|undefined} Callback result.
* @template S,T,U
*/
ol.renderer.canvas.Layer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) {
var hasFeature = this.forEachFeatureAtCoordinate(
coordinate, frameState, ol.functions.TRUE, this);
if (hasFeature) {
return callback.call(thisArg, this.getLayer(), null);
} else {
return undefined;
}
};
/** /**
* @param {CanvasRenderingContext2D} context Context. * @param {CanvasRenderingContext2D} context Context.
* @param {olx.FrameState} frameState Frame state. * @param {olx.FrameState} frameState Frame state.
@@ -171,20 +153,6 @@ ol.renderer.canvas.Layer.prototype.dispatchRenderEvent = function(context, frame
}; };
/**
* @abstract
* @return {HTMLCanvasElement|HTMLVideoElement|Image} Canvas.
*/
ol.renderer.canvas.Layer.prototype.getImage = function() {};
/**
* @abstract
* @return {!ol.Transform} Image transform.
*/
ol.renderer.canvas.Layer.prototype.getImageTransform = function() {};
/** /**
* @param {olx.FrameState} frameState Frame state. * @param {olx.FrameState} frameState Frame state.
* @param {number} offsetX Offset on the x-axis in view coordinates. * @param {number} offsetX Offset on the x-axis in view coordinates.
@@ -205,6 +173,14 @@ ol.renderer.canvas.Layer.prototype.getTransform = function(frameState, offsetX)
}; };
/**
* @abstract
* @param {olx.FrameState} frameState Frame state.
* @param {ol.LayerState} layerState Layer state.
* @param {CanvasRenderingContext2D} context Context.
*/
ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerState, context) {};
/** /**
* @abstract * @abstract
* @param {olx.FrameState} frameState Frame state. * @param {olx.FrameState} frameState Frame state.
@@ -212,24 +188,3 @@ ol.renderer.canvas.Layer.prototype.getTransform = function(frameState, offsetX)
* @return {boolean} whether composeFrame should be called. * @return {boolean} whether composeFrame should be called.
*/ */
ol.renderer.canvas.Layer.prototype.prepareFrame = function(frameState, layerState) {}; ol.renderer.canvas.Layer.prototype.prepareFrame = function(frameState, layerState) {};
/**
* @param {ol.Coordinate} coordinate Coordinate.
* @param {olx.FrameState} frameState Frame state.
* @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer callback.
* @param {S} thisArg Value to use as `this` when executing `callback`.
* @return {T|undefined} Callback result.
* @template S,T
*/
ol.renderer.canvas.Layer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) {
var hasFeature = this.forEachFeatureAtCoordinate(
coordinate, frameState, ol.functions.TRUE, this);
if (hasFeature) {
return callback.call(thisArg, this.getLayer(), null);
} else {
return undefined;
}
};

View File

@@ -9,17 +9,17 @@ goog.require('ol.Tile');
goog.require('ol.array'); goog.require('ol.array');
goog.require('ol.dom'); goog.require('ol.dom');
goog.require('ol.extent'); goog.require('ol.extent');
goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.IntermediateCanvas');
/** /**
* @constructor * @constructor
* @extends {ol.renderer.canvas.Layer} * @extends {ol.renderer.canvas.IntermediateCanvas}
* @param {ol.layer.Tile|ol.layer.VectorTile} tileLayer Tile layer. * @param {ol.layer.Tile|ol.layer.VectorTile} tileLayer Tile layer.
*/ */
ol.renderer.canvas.TileLayer = function(tileLayer) { ol.renderer.canvas.TileLayer = function(tileLayer) {
ol.renderer.canvas.Layer.call(this, tileLayer); ol.renderer.canvas.IntermediateCanvas.call(this, tileLayer);
/** /**
* @protected * @protected
@@ -33,12 +33,6 @@ ol.renderer.canvas.TileLayer = function(tileLayer) {
*/ */
this.renderedExtent_ = null; this.renderedExtent_ = null;
/**
* @private
* @type {number}
*/
this.renderedResolution_;
/** /**
* @private * @private
* @type {number} * @type {number}
@@ -75,12 +69,6 @@ ol.renderer.canvas.TileLayer = function(tileLayer) {
*/ */
this.imageTransform_ = ol.transform.create(); this.imageTransform_ = ol.transform.create();
/**
* @private
* @type {ol.Transform}
*/
this.coordinateToCanvasPixelTransform_ = ol.transform.create();
/** /**
* @protected * @protected
* @type {number} * @type {number}
@@ -88,7 +76,7 @@ ol.renderer.canvas.TileLayer = function(tileLayer) {
this.zDirection = 0; this.zDirection = 0;
}; };
ol.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer); ol.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.IntermediateCanvas);
/** /**
@@ -172,7 +160,7 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer
} }
var hints = frameState.viewHints; var hints = frameState.viewHints;
if (!(this.renderedResolution_ && Date.now() - frameState.time > 16 && if (!(this.renderedResolution && Date.now() - frameState.time > 16 &&
(hints[ol.View.Hint.ANIMATING] || hints[ol.View.Hint.INTERACTING])) && (hints[ol.View.Hint.ANIMATING] || hints[ol.View.Hint.INTERACTING])) &&
(newTiles || !(this.renderedExtent_ && (newTiles || !(this.renderedExtent_ &&
ol.extent.equals(this.renderedExtent_, imageExtent)) || ol.extent.equals(this.renderedExtent_, imageExtent)) ||
@@ -220,18 +208,18 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer
} }
this.renderedRevision_ = sourceRevision; this.renderedRevision_ = sourceRevision;
this.renderedResolution_ = tileResolution; this.renderedResolution = tileResolution;
this.renderedExtent_ = imageExtent; this.renderedExtent_ = imageExtent;
} }
var scale = pixelRatio / tilePixelRatio * this.renderedResolution_ / viewResolution; var scale = pixelRatio / tilePixelRatio * this.renderedResolution / viewResolution;
var transform = ol.transform.compose(this.imageTransform_, var transform = ol.transform.compose(this.imageTransform_,
pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, pixelRatio * size[0] / 2, pixelRatio * size[1] / 2,
scale, scale, scale, scale,
0, 0,
tilePixelRatio * (this.renderedExtent_[0] - viewCenter[0]) / this.renderedResolution_, tilePixelRatio * (this.renderedExtent_[0] - viewCenter[0]) / this.renderedResolution,
tilePixelRatio * (viewCenter[1] - this.renderedExtent_[3]) / this.renderedResolution_); tilePixelRatio * (viewCenter[1] - this.renderedExtent_[3]) / this.renderedResolution);
ol.transform.compose(this.coordinateToCanvasPixelTransform_, ol.transform.compose(this.coordinateToCanvasPixelTransform,
pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5],
pixelRatio / viewResolution, -pixelRatio / viewResolution, pixelRatio / viewResolution, -pixelRatio / viewResolution,
0, 0,
@@ -267,29 +255,6 @@ ol.renderer.canvas.TileLayer.prototype.drawTileImage = function(tile, frameState
}; };
/**
* @param {ol.Coordinate} coordinate Coordinate.
* @param {olx.FrameState} frameState FrameState.
* @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer
* callback.
* @param {S} thisArg Value to use as `this` when executing `callback`.
* @return {T|undefined} Callback result.
* @template S,T,U
*/
ol.renderer.canvas.TileLayer.prototype.forEachLayerAtCoordinate = function(
coordinate, frameState, callback, thisArg) {
var canvasPixel = ol.transform.apply(this.coordinateToCanvasPixelTransform_, coordinate);
var imageData = this.context.getImageData(canvasPixel[0], canvasPixel[1], 1, 1).data;
if (imageData[3] > 0) {
return callback.call(thisArg, this.getLayer(), imageData);
} else {
return undefined;
}
};
/** /**
* @inheritDoc * @inheritDoc
*/ */

View File

@@ -3,17 +3,17 @@ goog.provide('ol.test.renderer.canvas.Layer');
goog.require('ol.transform'); goog.require('ol.transform');
goog.require('ol.layer.Image'); goog.require('ol.layer.Image');
goog.require('ol.renderer.Map'); goog.require('ol.renderer.Map');
goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.IntermediateCanvas');
describe('ol.renderer.canvas.Layer', function() { describe('ol.renderer.canvas.IntermediateCanvas', function() {
describe('#composeFrame()', function() { describe('#composeFrame()', function() {
it('clips to layer extent and draws image', function() { it('clips to layer extent and draws image', function() {
var layer = new ol.layer.Image({ var layer = new ol.layer.Image({
extent: [1, 2, 3, 4] extent: [1, 2, 3, 4]
}); });
var renderer = new ol.renderer.canvas.Layer(layer); var renderer = new ol.renderer.canvas.IntermediateCanvas(layer);
var image = new Image(); var image = new Image();
image.width = 3; image.width = 3;
image.height = 3; image.height = 3;