diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc
index b8c7700e6c..ed43c96a2f 100644
--- a/src/objectliterals.jsdoc
+++ b/src/objectliterals.jsdoc
@@ -638,6 +638,20 @@
* @property {ol.source.State|undefined} state Source state.
*/
+/**
+ * @typedef {Object} olx.source.ImageVectorOptions
+ * @property {Array.
|undefined} attributions Attributions.
+ * @property {ol.Extent|undefined} extent Extent.
+ * @property {string|undefined} logo Logo.
+ * @property {ol.proj.ProjectionLike} projection Projection.
+ * @property {number|undefined} ratio Ratio. 1 means canvases are the size
+ * of the map viewport, 2 means twice the size of the map viewport, and so
+ * on.
+ * @property {Array.|undefined} resolutions Resolutions. If specified,
+ * new canvases will be created for these resolutions only.
+ * @property {ol.source.Vector} source Vector source.
+ */
+
/**
* @typedef {Object} olx.source.ImageWMSOptions
* @property {Array.|undefined} attributions Attributions.
diff --git a/src/ol/source/imagevectorsource.exports b/src/ol/source/imagevectorsource.exports
new file mode 100644
index 0000000000..17ff190f77
--- /dev/null
+++ b/src/ol/source/imagevectorsource.exports
@@ -0,0 +1 @@
+@exportSymbol ol.source.ImageVector
diff --git a/src/ol/source/imagevectorsource.js b/src/ol/source/imagevectorsource.js
new file mode 100644
index 0000000000..34b70ec949
--- /dev/null
+++ b/src/ol/source/imagevectorsource.js
@@ -0,0 +1,213 @@
+goog.provide('ol.source.ImageVector');
+
+goog.require('goog.asserts');
+goog.require('goog.dom');
+goog.require('goog.dom.TagName');
+goog.require('goog.events');
+goog.require('goog.events.EventType');
+goog.require('goog.vec.Mat4');
+goog.require('ol.extent');
+goog.require('ol.render.canvas.ReplayGroup');
+goog.require('ol.renderer.vector');
+goog.require('ol.source.ImageCanvas');
+goog.require('ol.source.Vector');
+goog.require('ol.style.ImageState');
+goog.require('ol.vec.Mat4');
+
+
+
+/**
+ * @constructor
+ * @extends {ol.source.ImageCanvas}
+ * @param {olx.source.ImageVectorOptions} options Options.
+ */
+ol.source.ImageVector = function(options) {
+
+ /**
+ * @private
+ * @type {ol.source.Vector}
+ */
+ this.source_ = options.source;
+
+ /**
+ * @private
+ * @type {ol.feature.StyleFunction}
+ */
+ this.styleFunction_ = options.styleFunction;
+
+ /**
+ * @private
+ * @type {!goog.vec.Mat4.Number}
+ */
+ this.transform_ = goog.vec.Mat4.createNumber();
+
+ /**
+ * @private
+ * @type {HTMLCanvasElement}
+ */
+ this.canvasElement_ = /** @type {HTMLCanvasElement} */
+ (goog.dom.createElement(goog.dom.TagName.CANVAS));
+
+ /**
+ * @private
+ * @type {CanvasRenderingContext2D}
+ */
+ this.canvasContext_ = /** @type {CanvasRenderingContext2D} */
+ (this.canvasElement_.getContext('2d'));
+
+ /**
+ * @private
+ * @type {ol.Size}
+ */
+ this.canvasSize_ = [0, 0];
+
+ goog.base(this, {
+ attributions: options.attributions,
+ canvasFunction: goog.bind(this.canvasFunctionInternal_, this),
+ extent: options.extent,
+ logo: options.logo,
+ projection: options.projection,
+ ratio: options.ratio,
+ resolutions: options.resolutions,
+ state: this.source_.getState()
+ });
+
+ goog.events.listen(this.source_, goog.events.EventType.CHANGE,
+ this.handleSourceChange_, undefined, this);
+
+};
+goog.inherits(ol.source.ImageVector, ol.source.ImageCanvas);
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.Size} size Size.
+ * @param {ol.proj.Projection} projection Projection;
+ * @return {HTMLCanvasElement} Canvas element.
+ * @private
+ */
+ol.source.ImageVector.prototype.canvasFunctionInternal_ =
+ function(extent, resolution, pixelRatio, size, projection) {
+
+ var tolerance = resolution / (2 * pixelRatio);
+ var replayGroup = new ol.render.canvas.ReplayGroup(
+ pixelRatio, tolerance);
+
+ var loading = false;
+ this.source_.forEachFeatureInExtent(extent,
+ /**
+ * @param {ol.Feature} feature Feature.
+ */
+ function(feature) {
+ loading = loading ||
+ this.renderFeature_(feature, resolution, pixelRatio, replayGroup);
+ }, this);
+ replayGroup.finish();
+
+ if (loading) {
+ return null;
+ }
+
+ if (this.canvasSize_[0] != size[0] || this.canvasSize_[1] != size[1]) {
+ this.canvasElement_.width = size[0];
+ this.canvasElement_.height = size[1];
+ this.canvasSize_[0] = size[0];
+ this.canvasSize_[1] = size[1];
+ } else {
+ this.canvasContext_.clearRect(0, 0, size[0], size[1]);
+ }
+
+ var transform = this.getTransform_(ol.extent.getCenter(extent),
+ resolution, pixelRatio, size);
+ replayGroup.replay(this.canvasContext_, extent, transform,
+ goog.functions.TRUE);
+
+ return this.canvasElement_;
+};
+
+
+/**
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.Size} size Size.
+ * @return {!goog.vec.Mat4.Number} Transform.
+ * @private
+ */
+ol.source.ImageVector.prototype.getTransform_ =
+ function(center, resolution, pixelRatio, size) {
+ return ol.vec.Mat4.makeTransform2D(this.transform_,
+ size[0] / 2, size[1] / 2,
+ pixelRatio / resolution, -pixelRatio / resolution,
+ 0,
+ -center[0], -center[1]);
+};
+
+
+/**
+ * Handle changes in image style state.
+ * @param {goog.events.Event} event Image style change event.
+ * @private
+ */
+ol.source.ImageVector.prototype.handleImageStyleChange_ =
+ function(event) {
+ var imageStyle = /** @type {ol.style.Image} */ (event.target);
+ if (imageStyle.getImageState() == ol.style.ImageState.LOADED) {
+ this.dispatchChangeEvent();
+ }
+};
+
+
+/**
+ * @private
+ */
+ol.source.ImageVector.prototype.handleSourceChange_ = function() {
+ // setState will trigger a CHANGE event, so we always rely
+ // change events by calling setState.
+ this.setState(this.source_.getState());
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
+ * @return {boolean} `true` if an image is loading.
+ * @private
+ */
+ol.source.ImageVector.prototype.renderFeature_ =
+ function(feature, resolution, pixelRatio, replayGroup) {
+ var loading = false;
+ var styles = this.styleFunction_(feature, resolution);
+ 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, style, imageStyle, imageState;
+ for (i = 0, ii = styles.length; i < ii; ++i) {
+ style = styles[i];
+ imageStyle = style.getImage();
+ if (!goog.isNull(imageStyle)) {
+ if (imageStyle.getImageState() == ol.style.ImageState.IDLE) {
+ goog.events.listenOnce(imageStyle, goog.events.EventType.CHANGE,
+ this.handleImageStyleChange_, false, this);
+ imageStyle.load();
+ } else if (imageStyle.getImageState() == ol.style.ImageState.LOADED) {
+ ol.renderer.vector.renderFeature(
+ replayGroup, feature, style, squaredTolerance, feature);
+ }
+ goog.asserts.assert(
+ imageStyle.getImageState() != ol.style.ImageState.IDLE);
+ loading = imageStyle.getImageState() == ol.style.ImageState.LOADING;
+ } else {
+ ol.renderer.vector.renderFeature(
+ replayGroup, feature, style, squaredTolerance, feature);
+ }
+ }
+ return loading;
+};