diff --git a/examples/cartodb.html b/examples/cartodb.html
new file mode 100644
index 0000000000..6d3fb99ebc
--- /dev/null
+++ b/examples/cartodb.html
@@ -0,0 +1,26 @@
+---
+template: example.html
+title: CartoDB source example
+shortdesc: Example of a cartodb map.
+docs: >
+ A simple map with customized Attribution control.
+tags: "simple, openstreetmap, attribution"
+---
+
+
diff --git a/examples/cartodb.js b/examples/cartodb.js
new file mode 100644
index 0000000000..8822a7ebf9
--- /dev/null
+++ b/examples/cartodb.js
@@ -0,0 +1,48 @@
+goog.require('ol.Map');
+goog.require('ol.View');
+goog.require('ol.layer.Tile');
+goog.require('ol.source.CartoDB');
+goog.require('ol.source.OSM');
+
+var mapConfig = {
+ 'layers': [{
+ 'type': 'cartodb',
+ 'options': {
+ 'cartocss_version': '2.1.1',
+ 'cartocss': '#layer { polygon-fill: #F00; }',
+ 'sql': 'select * from european_countries_e where area > 0'
+ }
+ }]
+};
+
+var cartoDBSource = new ol.source.CartoDB({
+ account: 'documentation',
+ config: mapConfig
+});
+
+var map = new ol.Map({
+ layers: [
+ new ol.layer.Tile({
+ source: new ol.source.OSM()
+ }),
+ new ol.layer.Tile({
+ source: cartoDBSource
+ })
+ ],
+ target: 'map',
+ view: new ol.View({
+ center: [0, 0],
+ zoom: 2
+ })
+});
+
+function setArea(n) {
+ mapConfig.layers[0].options.sql =
+ 'select * from european_countries_e where area > ' + n;
+ cartoDBSource.setConfig(mapConfig);
+}
+
+
+document.getElementById('country-area').addEventListener('change', function() {
+ setArea(this.value);
+});
diff --git a/externs/olx.js b/externs/olx.js
index 3178bea698..16dfcbab43 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -6209,6 +6209,104 @@ olx.source.XYZOptions.prototype.urls;
*/
olx.source.XYZOptions.prototype.wrapX;
+/**
+ * @typedef {{attributions: (Array.
|undefined),
+ * crossOrigin: (null|string|undefined),
+ * logo: (string|olx.LogoOptions|undefined),
+ * projection: ol.proj.ProjectionLike,
+ * maxZoom: (number|undefined),
+ * minZoom: (number|undefined),
+ * wrapX: (boolean|undefined),
+ * config: (Object|undefined),
+ * map: string,
+ * account: (string|undefined)}}
+ * @api
+ */
+olx.source.CartoDBOptions;
+
+
+/**
+ * Attributions.
+ * @type {Array.|undefined}
+ * @api stable
+ */
+olx.source.CartoDBOptions.prototype.attributions;
+
+
+/**
+ * The `crossOrigin` attribute for loaded images. Note that you must provide a
+ * `crossOrigin` value if you are using the WebGL renderer or if you want to
+ * access pixel data with the Canvas renderer. See
+ * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image}
+ * for more detail.
+ * @type {null|string|undefined}
+ * @api stable
+ */
+olx.source.CartoDBOptions.prototype.crossOrigin;
+
+
+/**
+ * Logo.
+ * @type {string|olx.LogoOptions|undefined}
+ * @api stable
+ */
+olx.source.CartoDBOptions.prototype.logo;
+
+
+/**
+ * Projection. Default is `EPSG:3857`.
+ * @type {ol.proj.ProjectionLike}
+ * @api
+ */
+olx.source.CartoDBOptions.prototype.projection;
+
+
+/**
+ * Optional max zoom level. Default is `18`.
+ * @type {number|undefined}
+ * @api
+ */
+olx.source.CartoDBOptions.prototype.maxZoom;
+
+
+/**
+ * Whether to wrap the world horizontally. Default is `true`.
+ * @type {boolean|undefined}
+ * @api
+ */
+olx.source.CartoDBOptions.prototype.wrapX;
+
+
+/**
+ * If using anonymous maps, the CartoDB config to use. See
+ * {@link http://docs.cartodb.com/cartodb-platform/maps-api.html#anonymous-maps}
+ * for more detail.
+ * If using named maps, a key-value lookup with the template parameters.
+ * See {@link http://docs.cartodb.com/cartodb-platform/maps-api.html#named-maps}
+ * for more detail.
+ * @type {Object|undefined}
+ * @api
+ */
+olx.source.CartoDBOptions.prototype.config;
+
+
+/**
+ * If using named maps, this will be the name of the template to load.
+ * See {@link http://docs.cartodb.com/cartodb-platform/maps-api.html#named-maps}
+ * for more detail.
+ * @type {boolean|undefined}
+ * @api
+ */
+olx.source.CartoDBOptions.prototype.map;
+
+
+/**
+ * CartoDB account name
+ * @type {string}
+ * @api
+ */
+olx.source.CartoDBOptions.prototype.account;
+
/**
* @typedef {{attributions: (olx.source.AttributionOption|undefined),
diff --git a/src/ol/source/cartodb.js b/src/ol/source/cartodb.js
new file mode 100644
index 0000000000..d41af2a54f
--- /dev/null
+++ b/src/ol/source/cartodb.js
@@ -0,0 +1,127 @@
+goog.provide('ol.source.CartoDB');
+
+goog.require('goog.asserts');
+goog.require('goog.events');
+goog.require('goog.net.EventType');
+goog.require('goog.net.XhrIo');
+goog.require('goog.net.XhrIo.ResponseType');
+goog.require('ol.source.XYZ');
+
+
+/**
+ * @classdesc
+ * Layer source for the CartoDB tiles.
+ *
+ * @constructor
+ * @extends {ol.source.XYZ}
+ * @param {olx.source.CartoDBOptions} options CartoDB options.
+ * @api
+ */
+ol.source.CartoDB = function(options) {
+ this.account_ = options.account;
+ this.mapId_ = options.map || '';
+ this.config_ = options.config || {};
+ this.templateCache_ = {};
+ delete options.map;
+ goog.base(this, options);
+ this.initializeMap_();
+};
+goog.inherits(ol.source.CartoDB, ol.source.XYZ);
+
+
+/**
+ * Returns the current config.
+ * @return {Object} The current configuration.
+ * @api
+ */
+ol.source.CartoDB.prototype.getConfig = function() {
+ return this.config_;
+};
+
+
+/**
+ * Updates the carto db config.
+ * @param {Object} config a key-value lookup. Values will replace current values
+ * in the config.
+ * @api
+ */
+ol.source.CartoDB.prototype.updateConfig = function(config) {
+ for (var key in config) {
+ this.config_[key] = config[key];
+ }
+ this.initializeMap_();
+};
+
+
+/**
+ * Sets the CartoDB config
+ * @param {Object} config In the case of anonymous maps, a CartoDB configuration
+ * object.
+ * If using named maps, a key-value lookup with the template parameters.
+ */
+ol.source.CartoDB.prototype.setConfig = function(config) {
+ this.config_ = config || {};
+ this.initializeMap_();
+};
+
+
+/**
+ * Issue a request to initialize the CartoDB map.
+ * @private
+ */
+ol.source.CartoDB.prototype.initializeMap_ = function() {
+ var paramHash = JSON.stringify(this.config_);
+ if (this.templateCache_[paramHash]) {
+ this.applyTemplate_(this.templateCache_[paramHash]);
+ return;
+ }
+ var protocol = window.location.protocol;
+ var mapUrl = protocol + '//' + this.account_ +
+ '.cartodb.com/api/v1/map';
+
+ if (this.mapId_) {
+ mapUrl += '/named/' + this.mapId_;
+ }
+
+ var xhrIo = new goog.net.XhrIo();
+ xhrIo.setResponseType(goog.net.XhrIo.ResponseType.TEXT);
+ xhrIo.setWithCredentials(false);
+ goog.events.listen(xhrIo, goog.net.EventType.COMPLETE,
+ this.handleInitResponse_.bind(this, paramHash));
+ xhrIo.send(mapUrl,
+ 'POST',
+ JSON.stringify(this.config_),
+ {'Content-Type': 'application/json'});
+};
+
+
+/**
+ * Handle map initialization response.
+ * @param {string} paramHash a hash representing the parameter set that was used
+ * for the request
+ * @param {Event} event Event.
+ * @private
+ */
+ol.source.CartoDB.prototype.handleInitResponse_ = function(paramHash, event) {
+ var xhrIo = event.target;
+ goog.asserts.assertInstanceof(xhrIo, goog.net.XhrIo,
+ 'event.target/xhrIo is an instance of goog.net.XhrIo');
+ var data = xhrIo.getResponseJson();
+ if (xhrIo.isSuccess()) {
+ this.applyTemplate_(data);
+ }
+ this.templateCache_[paramHash] = data;
+};
+
+
+/**
+ * Apply the new tile urls returned by carto db
+ * @param {Object} data Result of carto db call.
+ * @private
+ */
+ol.source.CartoDB.prototype.applyTemplate_ = function(data) {
+ var layerId = data['layergroupid'];
+ var tilesUrl = 'https://' + data['cdn_url']['https'] + '/' + this.account_ +
+ '/api/v1/map/' + layerId + '/{z}/{x}/{y}.png';
+ this.setUrl(tilesUrl);
+};
diff --git a/test/spec/ol/source/cartodbsource.test.js b/test/spec/ol/source/cartodbsource.test.js
new file mode 100644
index 0000000000..9f62f6981c
--- /dev/null
+++ b/test/spec/ol/source/cartodbsource.test.js
@@ -0,0 +1,18 @@
+goog.provide('ol.test.source.CartoDBSource');
+
+goog.require('ol.source.CartoDB');
+goog.require('ol.source.XYZ');
+
+describe('ol.source.CartoDB', function() {
+
+ describe('constructor', function() {
+ it('returns a CartoDB source', function() {
+ var source = new ol.source.CartoDB({
+ map: 'example',
+ config: {}
+ });
+ expect(source).to.be.a(ol.source.XYZ);
+ expect(source).to.be.a(ol.source.CartoDB);
+ });
+ });
+});