diff --git a/examples/wmts-from-capabilities.html b/examples/wmts-from-capabilities.html new file mode 100644 index 0000000000..5d090fff56 --- /dev/null +++ b/examples/wmts-from-capabilities.html @@ -0,0 +1,54 @@ + + + + + + + + + + WMTS from capabilities example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

WMTS from capabilities example

+

Example of a WMTS source built from a WMTS getCapabilities response.

+
+

See the wmts-from-capabilities.js source to see how this is done.

+
+
wmts
+
+ +
+ +
+ + + + + + + diff --git a/examples/wmts-from-capabilities.js b/examples/wmts-from-capabilities.js new file mode 100644 index 0000000000..9858e676e5 --- /dev/null +++ b/examples/wmts-from-capabilities.js @@ -0,0 +1,47 @@ +goog.require('ol.Coordinate'); +goog.require('ol.Extent'); +goog.require('ol.Map'); +goog.require('ol.RendererHints'); +goog.require('ol.View2D'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.parser.ogc.WMTSCapabilities'); +goog.require('ol.projection'); +goog.require('ol.source.WMTS'); + + +var map, capabilities; +var parser = new ol.parser.ogc.WMTSCapabilities(); +var projection = ol.projection.configureProj4jsProjection({ + code: 'EPSG:21781', + extent: new ol.Extent(485869.5728, 76443.1884, 837076.5648, 299941.7864) +}); + +var xhr = new XMLHttpRequest(); +xhr.open('GET', 'http://wmts.geo.admin.ch/1.0.0/WMTSCapabilities.xml', true); + + +/** + * onload handler for the XHR request. + */ +xhr.onload = function() { + if (xhr.status == 200) { + capabilities = parser.read(xhr.responseXML); + var wmtsOptions = ol.source.WMTS.optionsFromCapabilities( + capabilities, 'ch.swisstopo.pixelkarte-farbe'); + map = new ol.Map({ + layers: [ + new ol.layer.TileLayer({ + source: new ol.source.WMTS(wmtsOptions) + }) + ], + renderers: ol.RendererHints.createFromQueryData(), + target: 'map', + view: new ol.View2D({ + center: projection.getExtent().getCenter(), + projection: projection, + zoom: 1 + }) + }); + } +}; +xhr.send(); diff --git a/examples/wmts.html b/examples/wmts.html new file mode 100644 index 0000000000..07d0f69eda --- /dev/null +++ b/examples/wmts.html @@ -0,0 +1,52 @@ + + + + + + + + + + WMTS example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

WMTS example

+

Example of a WMTS source.

+
+

See the wmts.js source to see how this is done.

+
+
wmts
+
+ +
+ +
+ + + + + diff --git a/examples/wmts.js b/examples/wmts.js new file mode 100644 index 0000000000..9ac2d31a1b --- /dev/null +++ b/examples/wmts.js @@ -0,0 +1,54 @@ +goog.require('ol.Coordinate'); +goog.require('ol.Extent'); +goog.require('ol.Map'); +goog.require('ol.RendererHint'); +goog.require('ol.View2D'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.projection'); +goog.require('ol.source.OpenStreetMap'); +goog.require('ol.source.WMTS'); +goog.require('ol.tilegrid.WMTS'); + + +var projection = ol.projection.get('EPSG:900913'); +var projectionExtent = projection.getExtent(); +var size = projectionExtent.getWidth() / 256; +var resolutions = new Array(26); +var matrixIds = new Array(26); +for (var z = 0; z < 26; ++z) { + // generate resolutions and matrixIds arrays for this WMTS + resolutions[z] = size / Math.pow(2, z); + matrixIds[z] = 'EPSG:900913:' + z; +} + +var map = new ol.Map({ + layers: [ + new ol.layer.TileLayer({ + source: new ol.source.OpenStreetMap(), + opacity: 0.7 + }), + new ol.layer.TileLayer({ + source: new ol.source.WMTS({ + url: 'http://v2.suite.opengeo.org/geoserver/gwc/service/wmts/', + layer: 'medford:buildings', + matrixSet: 'EPSG:900913', + format: 'image/png', + projection: projection, + tileGrid: new ol.tilegrid.WMTS({ + origin: projectionExtent.getTopLeft(), + resolutions: resolutions, + matrixIds: matrixIds + }), + style: '_null', + crossOrigin: null, // FIXME: this should be the default + extent: new ol.Extent(-13682835, 5204068, -13667473, 5221690) + }) + }) + ], + renderer: ol.RendererHint.CANVAS, + target: 'map', + view: new ol.View2D({ + center: new ol.Coordinate(-13677832, 5213272), + zoom: 13 + }) +}); diff --git a/src/objectliterals.exports b/src/objectliterals.exports index 2e348ad82d..b2a4019bce 100644 --- a/src/objectliterals.exports +++ b/src/objectliterals.exports @@ -150,6 +150,22 @@ @exportObjectLiteralProperty ol.source.TiledWMSOptions.url string|undefined @exportObjectLiteralProperty ol.source.TiledWMSOptions.urls Array.|undefined +@exportObjectLiteral ol.source.WMTSOptions +@exportObjectLiteralProperty ol.source.WMTSOptions.attributions Array.|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.crossOrigin string|null|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.extent ol.Extent|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.tileGrid ol.tilegrid.WMTS +@exportObjectLiteralProperty ol.source.WMTSOptions.projection ol.Projection|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.requestEncoding ol.source.WMTSRequestEncoding|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.layer string +@exportObjectLiteralProperty ol.source.WMTSOptions.style string +@exportObjectLiteralProperty ol.source.WMTSOptions.format string|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.matrixSet string +@exportObjectLiteralProperty ol.source.WMTSOptions.dimensions Object|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.url string|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.maxZoom number|undefined +@exportObjectLiteralProperty ol.source.WMTSOptions.urls Array.|undefined + @exportObjectLiteral ol.tilegrid.TileGridOptions @exportObjectLiteralProperty ol.tilegrid.TileGridOptions.origin ol.Coordinate|undefined @exportObjectLiteralProperty ol.tilegrid.TileGridOptions.origins Array.|undefined @@ -157,6 +173,14 @@ @exportObjectLiteralProperty ol.tilegrid.TileGridOptions.tileSize ol.Size|undefined @exportObjectLiteralProperty ol.tilegrid.TileGridOptions.tileSizes Array.|undefined +@exportObjectLiteral ol.tilegrid.WMTSOptions +@exportObjectLiteralProperty ol.tilegrid.WMTSOptions.origin ol.Coordinate|undefined +@exportObjectLiteralProperty ol.tilegrid.WMTSOptions.origins Array.|undefined +@exportObjectLiteralProperty ol.tilegrid.WMTSOptions.resolutions !Array. +@exportObjectLiteralProperty ol.tilegrid.WMTSOptions.matrixIds !Array. +@exportObjectLiteralProperty ol.tilegrid.WMTSOptions.tileSize ol.Size|undefined +@exportObjectLiteralProperty ol.tilegrid.WMTSOptions.tileSizes Array.|undefined + @exportObjectLiteral ol.tilegrid.XYZOptions @exportObjectLiteralProperty ol.tilegrid.XYZOptions.maxZoom number diff --git a/src/ol/coordinate.exports b/src/ol/coordinate.exports index 1dec787cd7..c538ed312c 100644 --- a/src/ol/coordinate.exports +++ b/src/ol/coordinate.exports @@ -1,2 +1,5 @@ @exportSymbol ol.Coordinate -@exportProperty ol.Coordinate.toStringHDMS +@exportSymbol ol.Coordinate.createStringXY +@exportSymbol ol.Coordinate.toStringHDMS +@exportSymbol ol.Coordinate.toStringXY +@exportSymbol ol.Coordinate.fromProjectedArray diff --git a/src/ol/extent.exports b/src/ol/extent.exports index b52ee8007a..9ba8337eaa 100644 --- a/src/ol/extent.exports +++ b/src/ol/extent.exports @@ -1,3 +1,11 @@ @exportSymbol ol.Extent +@exportProperty ol.Extent.prototype.getCenter @exportProperty ol.Extent.prototype.getHeight @exportProperty ol.Extent.prototype.getWidth +@exportProperty ol.Extent.prototype.containsCoordinate +@exportProperty ol.Extent.prototype.containsExtent +@exportProperty ol.Extent.prototype.getBottomLeft +@exportProperty ol.Extent.prototype.getBottomRight +@exportProperty ol.Extent.prototype.getTopLeft +@exportProperty ol.Extent.prototype.getTopRight +@exportProperty ol.Extent.prototype.transform diff --git a/src/ol/parser/ogc/wmtscapabilities_v1_0_0.js b/src/ol/parser/ogc/wmtscapabilities_v1_0_0.js index 4c88cdd59f..11cf87949d 100644 --- a/src/ol/parser/ogc/wmtscapabilities_v1_0_0.js +++ b/src/ol/parser/ogc/wmtscapabilities_v1_0_0.js @@ -67,7 +67,7 @@ ol.parser.ogc.WMTSCapabilities_v1_0_0 = function() { }, 'TileMatrix': function(node, obj) { var tileMatrix = { - 'supportedCRS': obj.supportedCRS + 'supportedCRS': obj['supportedCRS'] }; this.readChildNodes(node, tileMatrix); obj['matrixIds'].push(tileMatrix); diff --git a/src/ol/source/wmtssource.exports b/src/ol/source/wmtssource.exports new file mode 100644 index 0000000000..ef840a8124 --- /dev/null +++ b/src/ol/source/wmtssource.exports @@ -0,0 +1,2 @@ +@exportClass ol.source.WMTS ol.source.WMTSOptions +@exportSymbol ol.source.WMTS.optionsFromCapabilities diff --git a/src/ol/source/wmtssource.js b/src/ol/source/wmtssource.js new file mode 100644 index 0000000000..cd5b1b8a85 --- /dev/null +++ b/src/ol/source/wmtssource.js @@ -0,0 +1,260 @@ +goog.provide('ol.source.WMTS'); +goog.provide('ol.source.WMTSRequestEncoding'); + +goog.require('ol.Attribution'); +goog.require('ol.Extent'); +goog.require('ol.Projection'); +goog.require('ol.TileCoord'); +goog.require('ol.TileUrlFunction'); +goog.require('ol.TileUrlFunctionType'); +goog.require('ol.projection'); +goog.require('ol.source.ImageTileSource'); +goog.require('ol.tilegrid.WMTS'); + + +/** + * @enum {string} + */ +ol.source.WMTSRequestEncoding = { + KVP: 'KVP', // see spec §8 + REST: 'REST' // see spec §10 +}; + + + +/** + * @constructor + * @extends {ol.source.ImageTileSource} + * @param {ol.source.WMTSOptions} wmtsOptions WMTS options. + */ +ol.source.WMTS = function(wmtsOptions) { + + // TODO: add support for TileMatrixLimits + + var version = goog.isDef(wmtsOptions.version) ? + wmtsOptions.version : '1.0.0'; + var format = goog.isDef(wmtsOptions.format) ? + wmtsOptions.format : 'image/jpeg'; + var dimensions = wmtsOptions.dimensions || {}; + + // FIXME: should we guess this requestEncoding from wmtsOptions.url(s) + // structure? that would mean KVP only if a template is not provided. + var requestEncoding = goog.isDef(wmtsOptions.requestEncoding) ? + wmtsOptions.requestEncoding : ol.source.WMTSRequestEncoding.KVP; + + // FIXME: should we create a default tileGrid? + // we could issue a getCapabilities xhr to retrieve missing configuration + var tileGrid = wmtsOptions.tileGrid; + + var context = { + 'Layer': wmtsOptions.layer, + 'style': wmtsOptions.style, + 'TileMatrixSet': wmtsOptions.matrixSet + }; + goog.object.extend(context, dimensions); + var kvpParams; + if (requestEncoding == ol.source.WMTSRequestEncoding.KVP) { + kvpParams = { + 'Service': 'WMTS', + 'Request': 'GetTile', + 'Version': version, + 'Format': format, + 'TileMatrix': '{TileMatrix}', + 'TileRow': '{TileRow}', + 'TileCol': '{TileCol}' + }; + goog.object.extend(kvpParams, context); + } + + // TODO: factorize the code below so that it is usable by all sources + var urls = wmtsOptions.urls; + if (!goog.isDef(urls)) { + urls = []; + var url = wmtsOptions.url; + goog.asserts.assert(goog.isDef(url)); + var match = /\{(\d)-(\d)\}/.exec(url) || /\{([a-z])-([a-z])\}/.exec(url); + if (match) { + var startCharCode = match[1].charCodeAt(0); + var stopCharCode = match[2].charCodeAt(0); + var charCode; + for (charCode = startCharCode; charCode <= stopCharCode; ++charCode) { + urls.push(url.replace(match[0], String.fromCharCode(charCode))); + } + } else { + urls.push(url); + } + } + + /** + * @param {string} template Template. + * @return {ol.TileUrlFunctionType} Tile URL function. + */ + function createFromWMTSTemplate(template) { + return function(tileCoord) { + if (goog.isNull(tileCoord)) { + return undefined; + } else { + var localContext = { + 'TileMatrix': tileGrid.getMatrixId(tileCoord.z), + 'TileCol': tileCoord.x, + 'TileRow': tileCoord.y + }; + if (requestEncoding != ol.source.WMTSRequestEncoding.KVP) { + goog.object.extend(localContext, context); + } + var url = template; + for (var key in localContext) { + // FIXME: should we filter properties with hasOwnProperty? + url = url.replace('{' + key + '}', localContext[key]) + .replace('%7B' + key + '%7D', localContext[key]); + } + return url; + } + }; + } + + var tileUrlFunction = ol.TileUrlFunction.createFromTileUrlFunctions( + goog.array.map(urls, function(url) { + if (goog.isDef(kvpParams)) { + // TODO: we may want to create our own appendParams function + // so that params order conforms to wmts spec guidance, + // and so that we can avoid to escape special template params + url = goog.uri.utils.appendParamsFromMap(url, kvpParams); + } + return createFromWMTSTemplate(url); + })); + + tileUrlFunction = ol.TileUrlFunction.withTileCoordTransform( + function(tileCoord, tileGrid, projection) { + if (tileGrid.getResolutions().length <= tileCoord.z) { + return null; + } + var x = tileCoord.x; + var y = -tileCoord.y - 1; + var tileExtent = tileGrid.getTileCoordExtent(tileCoord); + var projectionExtent = projection.getExtent(); + var extent = goog.isDef(wmtsOptions.extent) ? + wmtsOptions.extent : projectionExtent; + + if (!goog.isNull(extent) && projection.isGlobal() && + extent.minX === projectionExtent.minX && + extent.maxX === projectionExtent.maxX) { + var numCols = Math.ceil( + (extent.maxX - extent.minX) / + (tileExtent.maxX - tileExtent.minX)); + x = goog.math.modulo(x, numCols); + tileExtent = tileGrid.getTileCoordExtent( + new ol.TileCoord(tileCoord.z, x, tileCoord.y)); + } + if (!tileExtent.intersects(extent)) { + return null; + } + return new ol.TileCoord(tileCoord.z, x, y); + }, + tileUrlFunction); + + goog.base(this, { + attributions: wmtsOptions.attributions, + crossOrigin: wmtsOptions.crossOrigin, + extent: wmtsOptions.extent, + projection: wmtsOptions.projection, + tileGrid: tileGrid, + tileUrlFunction: tileUrlFunction + }); + +}; +goog.inherits(ol.source.WMTS, ol.source.ImageTileSource); + + +/** + * @param {Object} wmtsCap An object representing the capabilities document. + * @param {string} layer The layer identifier. + * @return {ol.source.WMTSOptions} WMTS source options object. + */ +ol.source.WMTS.optionsFromCapabilities = function(wmtsCap, layer) { + + // TODO: add support for TileMatrixLimits + + var layers = wmtsCap['contents']['layers']; + var l = goog.array.find(layers, function(elt, index, array) { + return elt['identifier'] == layer; + }); + goog.asserts.assert(!goog.isNull(l)); + goog.asserts.assert(l['tileMatrixSetLinks'].length > 0); + var matrixSet = /** @type {string} */ + (l['tileMatrixSetLinks'][0]['tileMatrixSet']); + var format = /** @type {string} */ (l['formats'][0]); + var idx = goog.array.findIndex(l['styles'], function(elt, index, array) { + return elt['isDefault']; + }); + if (idx < 0) { + idx = 0; + } + var style = /** @type {string} */ (l['styles'][idx]['identifier']); + + var dimensions = {}; + goog.array.forEach(l['dimensions'], function(elt, index, array) { + var key = elt['identifier']; + var value = elt['default']; + if (goog.isDef(value)) { + goog.asserts.assert(goog.array.indexOf(elt['values'], value) >= 0); + } else { + value = elt['values'][0]; + } + goog.asserts.assert(goog.isDef(value)); + dimensions[key] = value; + }); + + var matrixSets = wmtsCap['contents']['tileMatrixSets']; + goog.asserts.assert(matrixSet in matrixSets); + var matrixSetObj = matrixSets[matrixSet]; + + var tileGrid = ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet( + matrixSetObj); + + var projection = ol.projection.get(matrixSetObj['supportedCRS']); + + var gets = wmtsCap['operationsMetadata']['GetTile']['dcp']['http']['get']; + var encodings = goog.object.getKeys( + gets[0]['constraints']['GetEncoding']['allowedValues']); + goog.asserts.assert(encodings.length > 0); + var requestEncoding = /** @type {ol.source.WMTSRequestEncoding} */ + (encodings[0]); + // TODO: arcgis support, quote from ol2: + // The OGC documentation is not clear if we should use REST or RESTful, + // ArcGis use RESTful, and OpenLayers use REST. + + var urls; + switch (requestEncoding) { + case ol.source.WMTSRequestEncoding.REST: + goog.asserts.assert(l['resourceUrls'].hasOwnProperty('tile')); + goog.asserts.assert(l['resourceUrls']['tile'].hasOwnProperty(format)); + urls = /** @type {Array.} */ + (l['resourceUrls']['tile'][format]); + break; + case ol.source.WMTSRequestEncoding.KVP: + urls = []; + goog.array.forEach(gets, function(elt, index, array) { + if (elt['constraints']['GetEncoding']['allowedValues'].hasOwnProperty( + ol.source.WMTSRequestEncoding.KVP)) { + urls.push(/** @type {string} */ (elt['url'])); + } + }); + goog.asserts.assert(urls.length > 0); + break; + default: + goog.asserts.assert(false); + } + + return { + urls: urls, + layer: layer, + matrixSet: matrixSet, + format: format, + projection: projection, + requestEncoding: requestEncoding, + tileGrid: tileGrid, + style: style, + dimensions: dimensions + }; +}; diff --git a/src/ol/tilegrid/wmtstilegrid.exports b/src/ol/tilegrid/wmtstilegrid.exports new file mode 100644 index 0000000000..bf723f3d17 --- /dev/null +++ b/src/ol/tilegrid/wmtstilegrid.exports @@ -0,0 +1 @@ +@exportClass ol.tilegrid.WMTS ol.tilegrid.WMTSOptions diff --git a/src/ol/tilegrid/wmtstilegrid.js b/src/ol/tilegrid/wmtstilegrid.js new file mode 100644 index 0000000000..b939795662 --- /dev/null +++ b/src/ol/tilegrid/wmtstilegrid.js @@ -0,0 +1,83 @@ +goog.provide('ol.tilegrid.WMTS'); + +goog.require('ol.Size'); +goog.require('ol.projection'); +goog.require('ol.tilegrid.TileGrid'); + + + +/** + * @constructor + * @extends {ol.tilegrid.TileGrid} + * @param {ol.tilegrid.WMTSOptions} wmtsOptions WMTS options. + */ +ol.tilegrid.WMTS = function(wmtsOptions) { + + goog.asserts.assert( + wmtsOptions.resolutions.length == wmtsOptions.matrixIds.length); + + /** + * @private + * @type {!Array.} + */ + this.matrixIds_ = wmtsOptions.matrixIds; + // FIXME: should the matrixIds become optionnal? + + goog.base(this, { + origin: wmtsOptions.origin, + origins: wmtsOptions.origins, + resolutions: wmtsOptions.resolutions, + tileSize: wmtsOptions.tileSize, + tileSizes: wmtsOptions.tileSizes + }); + +}; +goog.inherits(ol.tilegrid.WMTS, ol.tilegrid.TileGrid); + + +/** + * @param {number} z Z. + * @return {string} MatrixId.. + */ +ol.tilegrid.WMTS.prototype.getMatrixId = function(z) { + goog.asserts.assert(0 <= z && z < this.matrixIds_.length); + return this.matrixIds_[z]; +}; + + +/** + * @return {Array.} MatrixIds. + */ +ol.tilegrid.WMTS.prototype.getMatrixIds = function() { + return this.matrixIds_; +}; + + +/** + * @param {Object} matrixSet An object representing a matrixSet in the + * capabilities document. + * @return {ol.tilegrid.WMTS} WMTS tileGrid instance. + */ +ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet = + function(matrixSet) { + + var resolutions = []; + var matrixIds = []; + var origins = []; + var tileSizes = []; + var projection = ol.projection.get(matrixSet['supportedCRS']); + var metersPerUnit = projection.getMetersPerUnit(); + goog.array.forEach(matrixSet['matrixIds'], function(elt, index, array) { + matrixIds.push(elt['identifier']); + origins.push(elt['topLeftCorner']); + resolutions.push(elt['scaleDenominator'] * 0.28E-3 / metersPerUnit); + tileSizes.push(new ol.Size(elt['tileWidth'], elt['tileHeight'])); + }); + + return new ol.tilegrid.WMTS({ + origins: origins, + resolutions: resolutions, + matrixIds: matrixIds, + tileSizes: tileSizes + }); +}; diff --git a/src/ol/tileurlfunction.js b/src/ol/tileurlfunction.js index 3c270b80c6..209febe19c 100644 --- a/src/ol/tileurlfunction.js +++ b/src/ol/tileurlfunction.js @@ -60,6 +60,9 @@ ol.TileUrlFunction.createFromTemplates = function(templates) { * @return {ol.TileUrlFunctionType} Tile URL function. */ ol.TileUrlFunction.createFromTileUrlFunctions = function(tileUrlFunctions) { + if (tileUrlFunctions.length === 1) { + return tileUrlFunctions[0]; + } return function(tileCoord, tileGrid, projection) { if (goog.isNull(tileCoord)) { return undefined;