From f63a8567417e1e7d3045bd43ca495881a11717c2 Mon Sep 17 00:00:00 2001 From: cwgrant Date: Mon, 16 Feb 2015 18:37:13 -0500 Subject: [PATCH] Add support for ArcGIS Rest Services Adding a data source to support ArcGIS Map Server and Image Server. Functionality is similar to the ArcGIS93Rest Layer in OpenLayers 2. --- examples/arcgis-tiled.html | 51 ++++ examples/arcgis-tiled.js | 28 ++ externs/olx.js | 87 +++++++ src/ol/source/tilearcgisrestsource.js | 218 ++++++++++++++++ .../ol/source/tilearcgisrestsource.test.js | 242 ++++++++++++++++++ 5 files changed, 626 insertions(+) create mode 100644 examples/arcgis-tiled.html create mode 100644 examples/arcgis-tiled.js create mode 100644 src/ol/source/tilearcgisrestsource.js create mode 100644 test/spec/ol/source/tilearcgisrestsource.test.js 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.

+
+

See the arcgis-tiled.js source to see how this is done.

+
+
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 0b9924c79b..08aaf2a7d9 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4978,6 +4978,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');