diff --git a/examples/arcgis-tiled.html b/examples/arcgis-tiled.html
new file mode 100644
index 0000000000..d922b78ca3
--- /dev/null
+++ b/examples/arcgis-tiled.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+ Tiled ArcGIS MapServer example
+
+
+
+
+
+
+
+
+
+
+
+
+
Tiled ArcGIS MapServer example
+
Example of a tiled ArcGIS layer.
+
+
arcgis, tile, tilelayer
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/arcgis-tiled.js b/examples/arcgis-tiled.js
new file mode 100644
index 0000000000..614c721740
--- /dev/null
+++ b/examples/arcgis-tiled.js
@@ -0,0 +1,28 @@
+goog.require('ol.Map');
+goog.require('ol.View');
+goog.require('ol.layer.Tile');
+goog.require('ol.source.MapQuest');
+goog.require('ol.source.TileArcGISRest');
+
+var url = 'http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/' +
+ 'Specialty/ESRI_StateCityHighway_USA/MapServer';
+
+var layers = [
+ new ol.layer.Tile({
+ source: new ol.source.MapQuest({layer: 'sat'})
+ }),
+ new ol.layer.Tile({
+ extent: [-13884991, 2870341, -7455066, 6338219],
+ source: new ol.source.TileArcGISRest({
+ url: url
+ })
+ })
+];
+var map = new ol.Map({
+ layers: layers,
+ target: 'map',
+ view: new ol.View({
+ center: [-10997148, 4569099],
+ zoom: 4
+ })
+});
diff --git a/externs/olx.js b/externs/olx.js
index 81bb68b691..57db7a97c9 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -5010,6 +5010,93 @@ olx.source.ServerVectorOptions.prototype.logo;
*/
olx.source.ServerVectorOptions.prototype.projection;
+/**
+ * @typedef {{attributions: (Array.|undefined),
+ * params: (Object.|undefined),
+ * logo: (string|olx.LogoOptions|undefined),
+ * tileGrid: (ol.tilegrid.TileGrid|undefined),
+ * projection: ol.proj.ProjectionLike,
+ * tileLoadFunction: (ol.TileLoadFunctionType|undefined),
+ * url: (string|undefined),
+ * urls: (Array.|undefined)}}
+ * @api
+ */
+olx.source.TileArcGISRestOptions;
+
+/**
+ * Attributions.
+ * @type {Array.|undefined}
+ * @api
+ */
+olx.source.TileArcGISRestOptions.prototype.attributions;
+
+
+/**
+ * ArcGIS Rest parameters. This field is optional. Service defaults will be
+ * used for any fields not specified. `FORMAT` is `PNG32` by default. `F` is `IMAGE` by
+ * default. `TRANSPARENT` is `true` by default. `BBOX, `SIZE`, `BBOXSR`,
+ * and `IMAGESR` will be set dynamically. Set `LAYERS` to
+ * override the default service layer visibility. See
+ * {@link http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#/Export_Map/02r3000000v7000000/}
+ * for further reference.
+ * @type {Object.|undefined}
+ * @api
+ */
+olx.source.TileArcGISRestOptions.prototype.params;
+
+
+/**
+ * Logo.
+ * @type {string|olx.LogoOptions|undefined}
+ * @api
+ */
+olx.source.TileArcGISRestOptions.prototype.logo;
+
+
+/**
+ * Tile grid. Base this on the resolutions, tilesize and extent supported by the
+ * server.
+ * If this is not defined, a default grid will be used: if there is a projection
+ * extent, the grid will be based on that; if not, a grid based on a global
+ * extent with origin at 0,0 will be used.
+ * @type {ol.tilegrid.TileGrid|undefined}
+ * @api
+ */
+olx.source.TileArcGISRestOptions.prototype.tileGrid;
+
+/**
+ * Projection.
+ * @type {ol.proj.ProjectionLike}
+ * @api
+ */
+olx.source.TileArcGISRestOptions.prototype.projection;
+
+
+/**
+ * Optional function to load a tile given a URL.
+ * @type {ol.TileLoadFunctionType|undefined}
+ * @api
+ */
+olx.source.TileArcGISRestOptions.prototype.tileLoadFunction;
+
+
+/**
+ * ArcGIS Rest service URL for a Map Service or Image Service. The
+ * url should include /MapServer or /ImageServer.
+ * @type {string|undefined}
+ * @api
+ */
+olx.source.TileArcGISRestOptions.prototype.url;
+
+
+/**
+ * ArcGIS Rest service urls. Use this instead of `url` when the ArcGIS Service supports multiple
+ * urls for export requests.
+ * @type {Array.|undefined}
+ * @api
+ */
+olx.source.TileArcGISRestOptions.prototype.urls;
+
/**
* @typedef {{attributions: (Array.|undefined),
diff --git a/src/ol/source/tilearcgisrestsource.js b/src/ol/source/tilearcgisrestsource.js
new file mode 100644
index 0000000000..56f0b2cfd5
--- /dev/null
+++ b/src/ol/source/tilearcgisrestsource.js
@@ -0,0 +1,218 @@
+goog.provide('ol.source.TileArcGISRest');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.math');
+goog.require('goog.object');
+goog.require('goog.string');
+goog.require('goog.uri.utils');
+goog.require('ol');
+goog.require('ol.TileCoord');
+goog.require('ol.TileUrlFunction');
+goog.require('ol.extent');
+goog.require('ol.proj');
+goog.require('ol.source.TileImage');
+goog.require('ol.tilecoord');
+
+
+
+/**
+ * @classdesc
+ * Layer source for tile data from ArcGIS Rest services. Map and Image
+ * Services are supported.
+ *
+ * For cached ArcGIS services, better performance is available using the
+ * {@link ol.source.XYZ} data source.
+ *
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {olx.source.TileArcGISRestOptions=} opt_options Tile ArcGIS Rest
+ * options.
+ * @api
+ */
+ol.source.TileArcGISRest = function(opt_options) {
+
+ var options = goog.isDef(opt_options) ? opt_options : {};
+
+ var params = goog.isDef(options.params) ? options.params : {};
+
+ goog.base(this, {
+ attributions: options.attributions,
+ logo: options.logo,
+ projection: options.projection,
+ tileGrid: options.tileGrid,
+ tileLoadFunction: options.tileLoadFunction,
+ tileUrlFunction: goog.bind(this.tileUrlFunction_, this)
+ });
+
+ var urls = options.urls;
+ if (!goog.isDef(urls) && goog.isDef(options.url)) {
+ urls = ol.TileUrlFunction.expandUrl(options.url);
+ }
+
+ /**
+ * @private
+ * @type {!Array.}
+ */
+ this.urls_ = goog.isDefAndNotNull(urls) ? urls : [];
+
+ /**
+ * @private
+ * @type {Object}
+ */
+ this.params_ = params;
+
+ /**
+ * @private
+ * @type {ol.Extent}
+ */
+ this.tmpExtent_ = ol.extent.createEmpty();
+
+};
+goog.inherits(ol.source.TileArcGISRest, ol.source.TileImage);
+
+
+/**
+ * Get the user-provided params, i.e. those passed to the constructor through
+ * the "params" option, and possibly updated using the updateParams method.
+ * @return {Object} Params.
+ * @api
+ */
+ol.source.TileArcGISRest.prototype.getParams = function() {
+ return this.params_;
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {number} tileSize Tile size.
+ * @param {ol.Extent} tileExtent Tile extent.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {Object} params Params.
+ * @return {string|undefined} Request URL.
+ * @private
+ */
+ol.source.TileArcGISRest.prototype.getRequestUrl_ =
+ function(tileCoord, tileSize, tileExtent,
+ pixelRatio, projection, params) {
+
+ var urls = this.urls_;
+ if (goog.array.isEmpty(urls)) {
+ return undefined;
+ }
+
+ // ArcGIS Server only wants the numeric portion of the projection ID.
+ var srid = projection.getCode().split(':').pop();
+
+ params['SIZE'] = tileSize + ',' + tileSize;
+ params['BBOX'] = tileExtent.join(',');
+ params['BBOXSR'] = srid;
+ params['IMAGESR'] = srid;
+
+ var url;
+ if (urls.length == 1) {
+ url = urls[0];
+ } else {
+ var index = goog.math.modulo(ol.tilecoord.hash(tileCoord), urls.length);
+ url = urls[index];
+ }
+
+ if (!goog.string.endsWith(url, '/')) {
+ url = url + '/';
+ }
+
+ // If a MapServer, use export. If an ImageServer, use exportImage.
+ if (goog.string.endsWith(url, 'MapServer/')) {
+ url = url + 'export';
+ }
+ else if (goog.string.endsWith(url, 'ImageServer/')) {
+ url = url + 'exportImage';
+ }
+ else {
+ goog.asserts.fail('Unknown Rest Service', url);
+ }
+
+ return goog.uri.utils.appendParamsFromMap(url, params);
+};
+
+
+/**
+ * Return the URLs used for this ArcGISÂ source.
+ * @return {!Array.} URLs.
+ * @api stable
+ */
+ol.source.TileArcGISRest.prototype.getUrls = function() {
+ return this.urls_;
+};
+
+
+/**
+ * @param {string|undefined} url URL.
+ * @api stable
+ */
+ol.source.TileArcGISRest.prototype.setUrl = function(url) {
+ var urls = goog.isDef(url) ? ol.TileUrlFunction.expandUrl(url) : null;
+ this.setUrls(urls);
+};
+
+
+/**
+ * @param {Array.|undefined} urls URLs.
+ * @api stable
+ */
+ol.source.TileArcGISRest.prototype.setUrls = function(urls) {
+ this.urls_ = goog.isDefAndNotNull(urls) ? urls : [];
+ this.changed();
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {string|undefined} Tile URL.
+ * @private
+ */
+ol.source.TileArcGISRest.prototype.tileUrlFunction_ =
+ function(tileCoord, pixelRatio, projection) {
+
+ var tileGrid = this.getTileGrid();
+ if (goog.isNull(tileGrid)) {
+ tileGrid = this.getTileGridForProjection(projection);
+ }
+
+ if (tileGrid.getResolutions().length <= tileCoord[0]) {
+ return undefined;
+ }
+
+ var tileExtent = tileGrid.getTileCoordExtent(
+ tileCoord, this.tmpExtent_);
+ var tileSize = tileGrid.getTileSize(tileCoord[0]);
+
+ if (pixelRatio != 1) {
+ tileSize = (tileSize * pixelRatio + 0.5) | 0;
+ }
+
+ // Apply default params and override with user specified values.
+ var baseParams = {
+ 'F': 'image',
+ 'FORMAT': 'PNG32',
+ 'TRANSPARENT': true
+ };
+ goog.object.extend(baseParams, this.params_);
+
+ return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
+ pixelRatio, projection, baseParams);
+};
+
+
+/**
+ * Update the user-provided params.
+ * @param {Object} params Params.
+ * @api stable
+ */
+ol.source.TileArcGISRest.prototype.updateParams = function(params) {
+ goog.object.extend(this.params_, params);
+ this.changed();
+};
diff --git a/test/spec/ol/source/tilearcgisrestsource.test.js b/test/spec/ol/source/tilearcgisrestsource.test.js
new file mode 100644
index 0000000000..c5f3eb631b
--- /dev/null
+++ b/test/spec/ol/source/tilearcgisrestsource.test.js
@@ -0,0 +1,242 @@
+goog.provide('ol.test.source.TileArcGISRest');
+
+
+describe('ol.source.TileArcGISRest', function() {
+
+ var options;
+ beforeEach(function() {
+ options = {
+ params: {},
+ url: 'http://example.com/MapServer'
+ };
+ });
+
+ describe('#getTile', function() {
+
+ it('returns a tile with the expected URL', function() {
+ var source = new ol.source.TileArcGISRest(options);
+ var tile = source.getTile(3, 2, 1, 1, ol.proj.get('EPSG:3857'));
+ expect(tile).to.be.an(ol.ImageTile);
+ var uri = new goog.Uri(tile.src_);
+ expect(uri.getScheme()).to.be('http');
+ expect(uri.getDomain()).to.be('example.com');
+ expect(uri.getPath()).to.be('/MapServer/export');
+ var queryData = uri.getQueryData();
+ expect(queryData.get('BBOX')).to.be(
+ '-10018754.171394622,-15028131.257091932,' +
+ '-5009377.085697311,-10018754.17139462');
+ expect(queryData.get('FORMAT')).to.be('PNG32');
+ expect(queryData.get('SIZE')).to.be('256,256');
+ expect(queryData.get('IMAGESR')).to.be('3857');
+ expect(queryData.get('BBOXSR')).to.be('3857');
+ expect(queryData.get('TRANSPARENT')).to.be('true');
+
+ });
+
+ it('returns a tile with the expected URL with url list', function() {
+
+ options.urls = ['http://test1.com/MapServer',
+ 'http://test2.com/MapServer'];
+ var source = new ol.source.TileArcGISRest(options);
+
+ var tile = source.getTile(3, 2, 1, 1, ol.proj.get('EPSG:3857'));
+ expect(tile).to.be.an(ol.ImageTile);
+ var uri = new goog.Uri(tile.src_);
+ expect(uri.getScheme()).to.be('http');
+ expect(uri.getDomain()).to.match(/test[12]\.com/);
+ expect(uri.getPath()).to.be('/MapServer/export');
+ var queryData = uri.getQueryData();
+ expect(queryData.get('BBOX')).to.be(
+ '-10018754.171394622,-15028131.257091932,' +
+ '-5009377.085697311,-10018754.17139462');
+ expect(queryData.get('FORMAT')).to.be('PNG32');
+ expect(queryData.get('SIZE')).to.be('256,256');
+ expect(queryData.get('IMAGESR')).to.be('3857');
+ expect(queryData.get('BBOXSR')).to.be('3857');
+ expect(queryData.get('TRANSPARENT')).to.be('true');
+
+ });
+
+ it('returns a tile with the expected URL for ImageServer', function() {
+ options.url = 'http://example.com/ImageServer';
+ var source = new ol.source.TileArcGISRest(options);
+ var tile = source.getTile(3, 2, 1, 1, ol.proj.get('EPSG:3857'));
+ expect(tile).to.be.an(ol.ImageTile);
+ var uri = new goog.Uri(tile.src_);
+ expect(uri.getScheme()).to.be('http');
+ expect(uri.getDomain()).to.be('example.com');
+ expect(uri.getPath()).to.be('/ImageServer/exportImage');
+ var queryData = uri.getQueryData();
+ expect(queryData.get('BBOX')).to.be(
+ '-10018754.171394622,-15028131.257091932,' +
+ '-5009377.085697311,-10018754.17139462');
+ expect(queryData.get('FORMAT')).to.be('PNG32');
+ expect(queryData.get('SIZE')).to.be('256,256');
+ expect(queryData.get('IMAGESR')).to.be('3857');
+ expect(queryData.get('BBOXSR')).to.be('3857');
+ expect(queryData.get('TRANSPARENT')).to.be('true');
+ });
+
+ it('allows various parameters to be overridden', function() {
+ options.params.FORMAT = 'png';
+ options.params.TRANSPARENT = false;
+ var source = new ol.source.TileArcGISRest(options);
+ var tile = source.getTile(3, 2, 1, 1, ol.proj.get('EPSG:4326'));
+ var uri = new goog.Uri(tile.src_);
+ var queryData = uri.getQueryData();
+ expect(queryData.get('FORMAT')).to.be('png');
+ expect(queryData.get('TRANSPARENT')).to.be('false');
+ });
+
+ it('allows adding rest option', function() {
+ options.params.LAYERS = 'show:1,3,4';
+ var source = new ol.source.TileArcGISRest(options);
+ var tile = source.getTile(3, 2, 1, 1, ol.proj.get('EPSG:4326'));
+ var uri = new goog.Uri(tile.src_);
+ var queryData = uri.getQueryData();
+ expect(queryData.get('LAYERS')).to.be('show:1,3,4');
+ });
+ });
+
+ describe('#updateParams', function() {
+
+ it('add a new param', function() {
+ var source = new ol.source.TileArcGISRest(options);
+ source.updateParams({ 'TEST': 'value' });
+
+ var tile = source.getTile(3, 2, 1, 1, ol.proj.get('EPSG:3857'));
+ var uri = new goog.Uri(tile.src_);
+ var queryData = uri.getQueryData();
+
+ expect(queryData.get('TEST')).to.be('value');
+ });
+
+ it('updates an existing param', function() {
+ options.params.TEST = 'value';
+
+ var source = new ol.source.TileArcGISRest(options);
+ source.updateParams({ 'TEST': 'newValue' });
+
+ var tile = source.getTile(3, 2, 1, 1, ol.proj.get('EPSG:3857'));
+ var uri = new goog.Uri(tile.src_);
+ var queryData = uri.getQueryData();
+
+ expect(queryData.get('TEST')).to.be('newValue');
+ });
+
+ });
+
+ describe('#getParams', function() {
+
+ it('verify getting a param', function() {
+ options.params.TEST = 'value';
+ var source = new ol.source.TileArcGISRest(options);
+
+ var setParams = source.getParams();
+
+ expect(setParams).to.eql({ TEST: 'value' });
+ });
+
+ it('verify on adding a param', function() {
+ options.params.TEST = 'value';
+
+ var source = new ol.source.TileArcGISRest(options);
+ source.updateParams({ 'TEST2': 'newValue' });
+
+ var setParams = source.getParams();
+
+ expect(setParams).to.eql({ TEST: 'value', TEST2: 'newValue' });
+ });
+
+ it('verify on update a param', function() {
+ options.params.TEST = 'value';
+
+ var source = new ol.source.TileArcGISRest(options);
+ source.updateParams({ 'TEST': 'newValue' });
+
+ var setParams = source.getParams();
+
+ expect(setParams).to.eql({ TEST: 'newValue' });
+ });
+
+ });
+
+ describe('#getUrls', function() {
+
+ it('verify getting array of urls', function() {
+ options.urls = ['http://test.com/MapServer',
+ 'http://test2.com/MapServer'];
+
+ var source = new ol.source.TileArcGISRest(options);
+
+ var urls = source.getUrls();
+
+ expect(urls).to.eql(['http://test.com/MapServer',
+ 'http://test2.com/MapServer']);
+ });
+
+
+ });
+
+ describe('#setUrls', function() {
+
+ it('verify setting urls when not set yet', function() {
+
+ var source = new ol.source.TileArcGISRest(options);
+ source.setUrls(['http://test.com/MapServer',
+ 'http://test2.com/MapServer']);
+
+ var urls = source.getUrls();
+
+ expect(urls).to.eql(['http://test.com/MapServer',
+ 'http://test2.com/MapServer']);
+ });
+
+ it('verify setting urls with existing list', function() {
+ options.urls = ['http://test.com/MapServer',
+ 'http://test2.com/MapServer'];
+
+ var source = new ol.source.TileArcGISRest(options);
+ source.setUrls(['http://test3.com/MapServer',
+ 'http://test4.com/MapServer']);
+
+ var urls = source.getUrls();
+
+ expect(urls).to.eql(['http://test3.com/MapServer',
+ 'http://test4.com/MapServer']);
+ });
+ });
+
+ describe('#setUrl', function() {
+
+ it('verify setting url with no urls', function() {
+
+ var source = new ol.source.TileArcGISRest(options);
+ source.setUrl('http://test.com/MapServer');
+
+ var urls = source.getUrls();
+
+ expect(urls).to.eql(['http://test.com/MapServer']);
+ });
+
+ it('verify setting url with list of urls', function() {
+ options.urls = ['http://test.com/MapServer',
+ 'http://test2.com/MapServer'];
+
+ var source = new ol.source.TileArcGISRest(options);
+ source.setUrl('http://test3.com/MapServer');
+
+ var urls = source.getUrls();
+
+ expect(urls).to.eql(['http://test3.com/MapServer']);
+ });
+
+
+ });
+
+});
+
+goog.require('goog.Uri');
+goog.require('ol.ImageTile');
+goog.require('ol.source.TileArcGISRest');
+goog.require('ol.proj');