From 0e2da7e2272881e95c48000ec5b6e51ca9f1329b Mon Sep 17 00:00:00 2001 From: Mauro Bartolomeoli Date: Tue, 24 Jan 2017 15:09:13 +0100 Subject: [PATCH] Fixes #1217, fixes #1303, fixes #1304 adding WMTS support to Leaflet, Openlayers and Catalog --- package.json | 9 +- web/client/actions/catalog.js | 3 +- web/client/api/WMTS.js | 98 + web/client/api/__tests__/WMTS-test.js | 24 + web/client/components/catalog/RecordItem.jsx | 60 +- .../catalog/__tests__/RecordItem-test.jsx | 50 + .../map/leaflet/HighlightFeatureSupport.jsx | 2 +- web/client/components/map/leaflet/Map.jsx | 8 +- .../map/leaflet/__tests__/Layer-test.jsx | 51 + .../map/leaflet/plugins/WMTSLayer.js | 58 + .../components/map/leaflet/plugins/index.js | 1 + .../map/openlayers/__tests__/Layer-test.jsx | 45 + .../map/openlayers/plugins/WMTSLayer.js | 77 + .../map/openlayers/plugins/index.js | 3 +- web/client/localConfig.json | 3 +- web/client/plugins/MetadataExplorer.jsx | 2 +- web/client/plugins/Save.jsx | 25 +- web/client/plugins/SaveAs.jsx | 24 +- web/client/reducers/layers.js | 2 +- .../wmts/GetCapabilities-1.0.0.xml | 1913 +++++++++++++++++ web/client/utils/CatalogUtils.js | 70 + web/client/utils/CoordinatesUtils.js | 12 + web/client/utils/LayersUtils.js | 27 + web/client/utils/WMTSUtils.js | 39 + web/client/utils/leaflet/WMSUtils.js | 7 + web/client/utils/leaflet/WMTS.js | 105 + web/client/utils/ogc/WMTS.js | 38 + 27 files changed, 2700 insertions(+), 56 deletions(-) create mode 100644 web/client/api/WMTS.js create mode 100644 web/client/api/__tests__/WMTS-test.js create mode 100644 web/client/components/map/leaflet/plugins/WMTSLayer.js create mode 100644 web/client/components/map/openlayers/plugins/WMTSLayer.js create mode 100644 web/client/test-resources/wmts/GetCapabilities-1.0.0.xml create mode 100644 web/client/utils/WMTSUtils.js create mode 100644 web/client/utils/leaflet/WMTS.js create mode 100644 web/client/utils/ogc/WMTS.js diff --git a/package.json b/package.json index 9fe5d20b26..c7d0e88dff 100644 --- a/package.json +++ b/package.json @@ -74,12 +74,12 @@ "babel-polyfill": "6.8.0", "babel-standalone": "6.7.7", "bootstrap": "3.3.6", - "file-saver": "1.3.3", "canvas-to-blob": "0.0.0", "classnames": "2.2.5", "colorbrewer": "1.0.0", "es6-promise": "2.3.0", "eventlistener": "0.0.1", + "file-saver": "1.3.3", "html2canvas": "0.5.0-beta4", "intl": "1.2.2", "ismobilejs": "0.4.0", @@ -93,6 +93,7 @@ "leaflet-simple-graticule": "1.0.2", "leaflet.locatecontrol": "0.45.1", "lodash": "3.10.1", + "lodash.castarray": "4.4.0", "moment": "2.13.0", "node-uuid": "1.4.3", "object-assign": "3.0.0", @@ -108,11 +109,11 @@ "react-color": "2.4.0", "react-confirm-button": "0.0.2", "react-copy-to-clipboard": "4.1.0", + "react-dnd": "2.1.3", + "react-dnd-html5-backend": "2.1.2", "react-dock": "0.2.3", "react-dom": "0.14.8", "react-draggable": "1.3.4", - "react-dnd": "2.1.3", - "react-dnd-html5-backend": "2.1.2", "react-dropzone": "3.4.0", "react-intl": "https://github.com/geosolutions-it/react-intl/tarball/react_014_1x", "react-nouislider": "1.11.0", @@ -134,10 +135,10 @@ "redux-undo": "0.5.0", "reselect": "2.5.1", "shpjs": "3.3.2", + "turf-bbox": "3.0.10", "turf-buffer": "3.0.10", "turf-intersect": "3.0.10", "turf-union": "3.0.10", - "turf-bbox": "3.0.10", "url": "0.10.3", "w3c-schemas": "1.3.1", "xml2js": "0.4.17" diff --git a/web/client/actions/catalog.js b/web/client/actions/catalog.js index 52f45e7ff0..f02307387e 100644 --- a/web/client/actions/catalog.js +++ b/web/client/actions/catalog.js @@ -8,7 +8,8 @@ var API = { csw: require('../api/CSW'), - wms: require('../api/WMS') + wms: require('../api/WMS'), + wmts: require('../api/WMTS') }; const {addLayer, changeLayerProperties} = require('./layers'); diff --git a/web/client/api/WMTS.js b/web/client/api/WMTS.js new file mode 100644 index 0000000000..3187c4f887 --- /dev/null +++ b/web/client/api/WMTS.js @@ -0,0 +1,98 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const axios = require('../libs/ajax'); +const ConfigUtils = require('../utils/ConfigUtils'); + +const urlUtil = require('url'); +const assign = require('object-assign'); + +const xml2js = require('xml2js'); + +const capabilitiesCache = {}; + +const {isArray, head} = require('lodash'); + +const castArray = require('lodash.castarray'); + +const CoordinatesUtils = require('../utils/CoordinatesUtils'); + +const parseUrl = (url) => { + const parsed = urlUtil.parse(url, true); + return urlUtil.format(assign({}, parsed, {search: null}, { + query: assign({ + SERVICE: "WMTS", + VERSION: "1.0.0", + REQUEST: "getcapabilities" + }, parsed.query) + })); +}; + +const flatLayers = (root) => { + return root.Layer ? (isArray(root.Layer) && root.Layer || [root.Layer]).reduce((previous, current) => { + return previous.concat(flatLayers(current)).concat((current.Layer && current["ows:Identifier"]) ? [current] : []); + }, []) : (root.ows.Title && [root] || []); +}; + +const getOperation = (operations, name, type) => { + return head(head(operations + .filter((operation) => operation.$.name === name) + .map((operation) => castArray(operation["ows:DCP"]["ows:HTTP"]["ows:Get"]))) + .filter((request) => (request["ows:Constraint"] && request["ows:Constraint"]["ows:AllowedValues"]["ows:Value"]) === type) + .map((request) => request.$["xlink:href"]) + ); +}; + +const searchAndPaginate = (json, startPosition, maxRecords, text) => { + const root = json.Capabilities.Contents; + const operations = castArray(json.Capabilities["ows:OperationsMetadata"]["ows:Operation"]); + const TileMatrixSet = (root.TileMatrixSet) || []; + let SRSList = []; + let len = TileMatrixSet.length; + for (let i = 0; i < len; i++) { + SRSList.push(CoordinatesUtils.getEPSGCode(TileMatrixSet[i]["ows:SupportedCRS"])); + } + const layersObj = root.Layer; + const layers = castArray(layersObj); + const filteredLayers = layers + .filter((layer) => !text || layer["ows:Identifier"].toLowerCase().indexOf(text.toLowerCase()) !== -1 || (layer["ows:Title"] && layer["ows:Title"].toLowerCase().indexOf(text.toLowerCase()) !== -1)); + return { + numberOfRecordsMatched: filteredLayers.length, + numberOfRecordsReturned: Math.min(maxRecords, filteredLayers.length), + nextRecord: startPosition + Math.min(maxRecords, filteredLayers.length) + 1, + records: filteredLayers + .filter((layer, index) => index >= (startPosition - 1) && index < (startPosition - 1) + maxRecords) + .map((layer) => assign({}, layer, {SRS: SRSList, TileMatrixSet, GetTileUrl: getOperation(operations, "GetTile", "KVP")})) + }; +}; + +const Api = { + getRecords: function(url, startPosition, maxRecords, text) { + const cached = capabilitiesCache[url]; + if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) { + return new Promise((resolve) => { + resolve(searchAndPaginate(cached.data, startPosition, maxRecords, text)); + }); + } + return axios.get(parseUrl(url)).then((response) => { + let json; + xml2js.parseString(response.data, {explicitArray: false}, (ignore, result) => { + json = result; + }); + capabilitiesCache[url] = { + timestamp: new Date().getTime(), + data: json + }; + return searchAndPaginate(json, startPosition, maxRecords, text); + }); + }, + textSearch: function(url, startPosition, maxRecords, text) { + return Api.getRecords(url, startPosition, maxRecords, text); + } +}; + +module.exports = Api; diff --git a/web/client/api/__tests__/WMTS-test.js b/web/client/api/__tests__/WMTS-test.js new file mode 100644 index 0000000000..ccc9c2b5f7 --- /dev/null +++ b/web/client/api/__tests__/WMTS-test.js @@ -0,0 +1,24 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const expect = require('expect'); +const API = require('../WMTS'); + +describe('Test correctness of the WMTS APIs', () => { + it('GetRecords', (done) => { + API.getRecords('base/web/client/test-resources/wmts/GetCapabilities-1.0.0.xml', 0, 1, '').then((result) => { + try { + expect(result).toExist(); + expect(result.numberOfRecordsMatched).toBe(3); + done(); + } catch(ex) { + done(ex); + } + }); + }); +}); diff --git a/web/client/components/catalog/RecordItem.jsx b/web/client/components/catalog/RecordItem.jsx index 0431f6c149..4a18bb6160 100644 --- a/web/client/components/catalog/RecordItem.jsx +++ b/web/client/components/catalog/RecordItem.jsx @@ -26,7 +26,7 @@ const buildSRSMap = memoize((srs) => { const removeParameters = (url, skip) => { const urlparts = url.split('?'); const params = {}; - if (urlparts.length >= 2) { + if (urlparts.length >= 2 && urlparts[1]) { const pars = urlparts[1].split(/[&;]/g); pars.forEach((par) => { const param = par.split('='); @@ -80,6 +80,8 @@ const RecordItem = React.createClass({ reference.type.indexOf("OGC:WMS") > -1 && reference.type.indexOf("http-get-capabilities") > -1)); let wfsGetCap = head(record.references.filter(reference => reference.type && reference.type.indexOf("OGC:WFS") > -1 && reference.type.indexOf("http-get-capabilities") > -1)); + let wmtsGetCap = head(record.references.filter(reference => reference.type && + reference.type.indexOf("OGC:WMTS") > -1 && reference.type.indexOf("http-get-capabilities") > -1)); let links = []; if (wmsGetCap) { links.push({ @@ -88,6 +90,13 @@ const RecordItem = React.createClass({ labelId: 'catalog.wmsGetCapLink' }); } + if (wmtsGetCap) { + links.push({ + type: "WMTS_GET_CAPABILITIES", + url: wmtsGetCap.url, + labelId: 'catalog.wmtsGetCapLink' + }); + } if (wfsGetCap) { links.push({ type: "WFS_GET_CAPABILITIES", @@ -116,6 +125,8 @@ const RecordItem = React.createClass({ // let's extract the references we need let wms = head(record.references.filter(reference => reference.type && (reference.type === "OGC:WMS" || ((reference.type.indexOf("OGC:WMS") > -1 && reference.type.indexOf("http-get-map") > -1))))); + let wmts = head(record.references.filter(reference => reference.type && (reference.type === "OGC:WMTS" + || ((reference.type.indexOf("OGC:WMTS") > -1 && reference.type.indexOf("http-get-map") > -1))))); // let's create the buttons let buttons = []; if (wms) { @@ -131,6 +142,19 @@ const RecordItem = React.createClass({ ); } + if (wmts) { + buttons.push( + + ); + } // creating get capbilities links that will be used to share layers info if (this.props.showGetCapLinks) { let links = this.getLinks(record); @@ -207,6 +231,40 @@ const RecordItem = React.createClass({ this.props.onZoomToExtent(extent, crs); } } + }, + addwmtsLayer(wmts) { + const {url, params} = removeParameters(wmts.url, ["request", "layer"]); + const allowedSRS = buildSRSMap(wmts.SRS); + if (wmts.SRS.length > 0 && !CoordinatesUtils.isAllowedSRS(this.props.crs, allowedSRS)) { + this.props.onError('catalog.srs_not_allowed'); + } else { + this.props.onLayerAdd({ + type: "wmts", + url: url, + visibility: true, + name: wmts.params && wmts.params.name, + title: this.props.record.title || (wmts.params && wmts.params.name), + matrixIds: this.props.record.matrixIds || [], + tileMatrixSet: this.props.record.tileMatrixSet || [], + bbox: { + crs: this.props.record.boundingBox.crs, + bounds: { + minx: this.props.record.boundingBox.extent[0], + miny: this.props.record.boundingBox.extent[1], + maxx: this.props.record.boundingBox.extent[2], + maxy: this.props.record.boundingBox.extent[3] + } + }, + links: this.getLinks(this.props.record), + params: params, + allowedSRS: allowedSRS + }); + if (this.props.record.boundingBox) { + let extent = this.props.record.boundingBox.extent; + let crs = this.props.record.boundingBox.crs; + this.props.onZoomToExtent(extent, crs); + } + } } }); diff --git a/web/client/components/catalog/__tests__/RecordItem-test.jsx b/web/client/components/catalog/__tests__/RecordItem-test.jsx index 678c4aa41f..09350c5771 100644 --- a/web/client/components/catalog/__tests__/RecordItem-test.jsx +++ b/web/client/components/catalog/__tests__/RecordItem-test.jsx @@ -57,6 +57,28 @@ const sampleRecord2 = { }] }; +const sampleRecord3 = { + identifier: "test-identifier", + title: "sample title", + tags: ["subject1", "subject2"], + description: "sample abstract", + thumbnail: "img.jpg", + boundingBox: { + extent: [10.686, + 44.931, + 46.693, + 12.54], + crs: "EPSG:4326" + + }, + references: [{ + type: "OGC:WMTS", + url: "http://wms.sample.service:80/geoserver/gwc/service/wmts", + SRS: ['EPSG:4326', 'EPSG:3857'], + params: {name: "workspace:layername"} + }] +}; + const getCapRecord = assign({}, sampleRecord, {references: [{ type: "OGC:WMS", url: "http://wms.sample.service:80/geoserver/wms?SERVICE=WMS&", @@ -101,6 +123,34 @@ describe('This test for RecordItem', () => { expect(itemDom).toExist(); expect(itemDom.className).toBe('record-item panel panel-default'); }); + it('check WMTS resource', () => { + let actions = { + onLayerAdd: () => { + + }, + onZoomToExtent: () => { + + } + }; + let actionsSpy = expect.spyOn(actions, "onLayerAdd"); + let actionsSpy2 = expect.spyOn(actions, "onZoomToExtent"); + const item = ReactDOM.render((), document.getElementById("container")); + expect(item).toExist(); + + const itemDom = ReactDOM.findDOMNode(item); + expect(itemDom).toExist(); + expect(itemDom.className).toBe('record-item panel panel-default'); + let button = TestUtils.findRenderedDOMComponentWithTag( + item, 'button' + ); + expect(button).toExist(); + button.click(); + expect(actionsSpy.calls.length).toBe(1); + expect(actionsSpy2.calls.length).toBe(1); + }); // test handlers it('check event handlers', () => { let actions = { diff --git a/web/client/components/map/leaflet/HighlightFeatureSupport.jsx b/web/client/components/map/leaflet/HighlightFeatureSupport.jsx index 3c1b4445ff..791ce6d95b 100644 --- a/web/client/components/map/leaflet/HighlightFeatureSupport.jsx +++ b/web/client/components/map/leaflet/HighlightFeatureSupport.jsx @@ -111,7 +111,7 @@ const HighlightFeatureSupport = React.createClass({ this.props.updateHighlighted(this._selectedFeatures.map((f) => {return f.msId; }), ""); }, cleanSupport() { - if (this._layer !== null) { + if (this._layer) { this._selectedFeatures.map((f) => {this._layer.resetStyle(f); }); this._layer.off("click", this.featureClicked, this); } diff --git a/web/client/components/map/leaflet/Map.jsx b/web/client/components/map/leaflet/Map.jsx index 973e29e63c..ce35ab0e51 100644 --- a/web/client/components/map/leaflet/Map.jsx +++ b/web/client/components/map/leaflet/Map.jsx @@ -150,7 +150,13 @@ let LeafletMap = React.createClass({ this.props.onLayerLoading(loadingEvent.target.layerId); }); event.layer.on('load', (loadEvent) => { this.props.onLayerLoad(loadEvent.target.layerId, hadError); }); - event.layer.on('tileerror', (errorEvent) => { hadError = true; this.props.onLayerError(errorEvent.target.layerId); }); + event.layer.on('tileerror', (errorEvent) => { + const isError = errorEvent.target.onError ? (errorEvent.target.onError(errorEvent)) : true; + if (isError) { + hadError = true; + this.props.onLayerError(errorEvent.target.layerId); + } + }); } }); diff --git a/web/client/components/map/leaflet/__tests__/Layer-test.jsx b/web/client/components/map/leaflet/__tests__/Layer-test.jsx index 25e75909f2..b0f437981a 100644 --- a/web/client/components/map/leaflet/__tests__/Layer-test.jsx +++ b/web/client/components/map/leaflet/__tests__/Layer-test.jsx @@ -16,6 +16,7 @@ require('../../../../utils/leaflet/Layers'); require('../plugins/OSMLayer'); require('../plugins/GraticuleLayer'); require('../plugins/WMSLayer'); +require('../plugins/WMTSLayer'); require('../plugins/GoogleLayer'); require('../plugins/BingLayer'); require('../plugins/MapQuest'); @@ -179,6 +180,56 @@ describe('Leaflet layer', () => { expect(urls.length).toBe(1); }); + it('creates a wmts layer for leaflet map', () => { + var options = { + "type": "wmts", + "visibility": true, + "name": "nurc:Arc_Sample", + "group": "Meteo", + "format": "image/png", + "tileMatrixSet": "EPSG:900913", + "url": "http://demo.geo-solutions.it/geoserver/gwc/service/wmts" + }; + // create layers + var layer = ReactDOM.render( + , document.getElementById("container")); + var lcount = 0; + + expect(layer).toExist(); + // count layers + map.eachLayer(function() {lcount++; }); + expect(lcount).toBe(1); + let urls; + map.eachLayer((l) => urls = l._urls); + expect(urls.length).toBe(1); + }); + + it('creates a wmts layer with multiple urls for leaflet map', () => { + var options = { + "type": "wmts", + "visibility": true, + "name": "nurc:Arc_Sample", + "group": "Meteo", + "format": "image/png", + "tileMatrixSet": "EPSG:900913", + "url": ["http://demo.geo-solutions.it/geoserver/gwc/service/wmts", "http://demo.geo-solutions.it/geoserver/gwc/service/wmts"] + }; + // create layers + var layer = ReactDOM.render( + , document.getElementById("container")); + var lcount = 0; + + expect(layer).toExist(); + // count layers + map.eachLayer(function() {lcount++; }); + expect(lcount).toBe(1); + let urls; + map.eachLayer((l) => urls = l._urls); + expect(urls.length).toBe(2); + }); + it('creates a vector layer for leaflet map', () => { var options = { "type": "wms", diff --git a/web/client/components/map/leaflet/plugins/WMTSLayer.js b/web/client/components/map/leaflet/plugins/WMTSLayer.js new file mode 100644 index 0000000000..c634efdcde --- /dev/null +++ b/web/client/components/map/leaflet/plugins/WMTSLayer.js @@ -0,0 +1,58 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const Layers = require('../../../../utils/leaflet/Layers'); +const CoordinatesUtils = require('../../../../utils/CoordinatesUtils'); +const L = require('leaflet'); +const assign = require('object-assign'); +const SecurityUtils = require('../../../../utils/SecurityUtils'); +const WMTSUtils = require('../../../../utils/WMTSUtils'); +const WMTS = require('../../../../utils/leaflet/WMTS'); +const {isObject, isArray} = require('lodash'); + +L.tileLayer.wmts = function(urls, options, matrixOptions) { + return new WMTS(urls, options, matrixOptions); +}; + +function wmtsToLeafletOptions(options) { + const srs = CoordinatesUtils.normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS); + const tileMatrixSet = WMTSUtils.getTileMatrixSet(options.tileMatrixSet, srs, options.allowedSRS); + return assign({ + layer: options.name, + style: options.style || "", + format: options.format || 'image/png', + tileMatrixSet: tileMatrixSet, + version: options.version || "1.0.0", + tileSize: options.tileSize || 256, + CRS: CoordinatesUtils.normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS) + }, options.params || {}); +} + +function getWMSURLs(urls) { + return urls.map((url) => url.split("\?")[0]); +} + +function getMatrixIds(matrix, srs) { + return isObject(matrix) && matrix[srs] || matrix; +} + +Layers.registerType('wmts', { + create: (options) => { + const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]); + const queryParameters = wmtsToLeafletOptions(options) || {}; + urls.forEach(url => SecurityUtils.addAuthenticationParameter(url, queryParameters)); + const srs = CoordinatesUtils.normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS); + return L.tileLayer.wmts(urls, queryParameters, { + tileMatrixPrefix: options.tileMatrixPrefix || (queryParameters.tileMatrixSet + ':') || (srs + ':'), + originY: options.originY || 20037508.3428, + originX: options.originX || -20037508.3428, + ignoreErrors: options.ignoreErrors || false, + matrixIds: options.matrixIds && getMatrixIds(options.matrixIds, queryParameters.tileMatrixSet || srs) || null + }); + } +}); diff --git a/web/client/components/map/leaflet/plugins/index.js b/web/client/components/map/leaflet/plugins/index.js index df32e48943..2e576370a6 100644 --- a/web/client/components/map/leaflet/plugins/index.js +++ b/web/client/components/map/leaflet/plugins/index.js @@ -15,5 +15,6 @@ module.exports = { OSMLayer: require('./OSMLayer'), TileProviderLayer: require('./TileProviderLayer'), WMSLayer: require('./WMSLayer'), + WMTSLayer: require('./WMTSLayer'), VectorLayer: require('./VectorLayer') }; diff --git a/web/client/components/map/openlayers/__tests__/Layer-test.jsx b/web/client/components/map/openlayers/__tests__/Layer-test.jsx index a22c888075..78f4b981e8 100644 --- a/web/client/components/map/openlayers/__tests__/Layer-test.jsx +++ b/web/client/components/map/openlayers/__tests__/Layer-test.jsx @@ -14,6 +14,7 @@ var assign = require('object-assign'); require('../../../../utils/openlayers/Layers'); require('../plugins/OSMLayer'); require('../plugins/WMSLayer'); +require('../plugins/WMTSLayer'); require('../plugins/GoogleLayer'); require('../plugins/BingLayer'); require('../plugins/MapQuest'); @@ -148,6 +149,50 @@ describe('Openlayers layer', () => { expect(map.getLayers().item(0).getSource().urls.length).toBe(1); }); + it('creates a wmts layer for openlayers map', () => { + var options = { + "type": "wmts", + "visibility": true, + "name": "nurc:Arc_Sample", + "group": "Meteo", + "format": "image/png", + "tileMatrixSet": "EPSG:900913", + "url": "http://demo.geo-solutions.it/geoserver/gwc/service/wmts" + }; + // create layers + var layer = ReactDOM.render( + , document.getElementById("container")); + + + expect(layer).toExist(); + // count layers + expect(map.getLayers().getLength()).toBe(1); + expect(map.getLayers().item(0).getSource().urls.length).toBe(1); + }); + + it('creates a wmts layer with multiple urls for openlayers map', () => { + var options = { + "type": "wmts", + "visibility": true, + "name": "nurc:Arc_Sample", + "group": "Meteo", + "format": "image/png", + "tileMatrixSet": "EPSG:900913", + "url": ["http://demo.geo-solutions.it/geoserver/gwc/service/wmts", "http://demo.geo-solutions.it/geoserver/gwc/service/wmts"] + }; + // create layers + var layer = ReactDOM.render( + , document.getElementById("container")); + + + expect(layer).toExist(); + // count layers + expect(map.getLayers().getLength()).toBe(1); + expect(map.getLayers().item(0).getSource().urls.length).toBe(2); + }); + it('creates a wms layer for openlayers map with custom tileSize', () => { var options = { "type": "wms", diff --git a/web/client/components/map/openlayers/plugins/WMTSLayer.js b/web/client/components/map/openlayers/plugins/WMTSLayer.js new file mode 100644 index 0000000000..b730f64132 --- /dev/null +++ b/web/client/components/map/openlayers/plugins/WMTSLayer.js @@ -0,0 +1,77 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +var Layers = require('../../../../utils/openlayers/Layers'); +var ol = require('openlayers'); +const {isArray, isObject, slice} = require('lodash'); +// const SecurityUtils = require('../../../../utils/SecurityUtils'); +const WMTSUtils = require('../../../../utils/WMTSUtils'); +const CoordinatesUtils = require('../../../../utils/CoordinatesUtils'); +const mapUtils = require('../../../../utils/MapUtils'); +const assign = require('object-assign'); + +function getWMSURLs( urls ) { + return urls.map((url) => url.split("\?")[0]); +} + +function getDefaultMatrixId(options) { + let matrixIds = new Array(30); + for (let z = 0; z < 30; ++z) { + // generate matrixIds arrays for this WMTS + matrixIds[z] = options.tileMatrixPrefix + z; + } + return matrixIds; +} + +function getMatrixIds(matrix, srs) { + return (isObject(matrix) && matrix[srs] || matrix).map((el) => el.identifier); +} + +const limitMatrix = (matrix, len) => { + if (matrix.length > len) { + return slice(matrix, 0, len); + } + if (matrix.length < len) { + return matrix.concat(new Array(len - matrix.length)); + } + return matrix; +}; + +Layers.registerType('wmts', { + create: (options) => { + const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]); + const srs = CoordinatesUtils.normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS); + const tileMatrixSet = WMTSUtils.getTileMatrixSet(options.tileMatrixSet, srs, options.allowedSRS); + const resolutions = options.resolutions || mapUtils.getResolutions(); + const matrixIds = limitMatrix(options.matrixIds && getMatrixIds(options.matrixIds, tileMatrixSet || srs) || getDefaultMatrixId(options), resolutions.length); + const extent = options.bbox ? ol.extent.applyTransform([parseFloat(options.bbox.bounds.minx), parseFloat(options.bbox.bounds.miny), parseFloat(options.bbox.bounds.maxx), parseFloat(options.bbox.bounds.maxy)], ol.proj.getTransform(options.bbox.crs, options.srs)) : null; + // urls.forEach(url => SecurityUtils.addAuthenticationParameter(url, queryParameters)); + return new ol.layer.Tile({ + opacity: options.opacity !== undefined ? options.opacity : 1, + zIndex: options.zIndex, + source: new ol.source.WMTS(assign({ + urls: urls, + layer: options.name, + version: options.version || "1.0.0", + matrixSet: tileMatrixSet, + format: options.format || 'image/png', + tileGrid: new ol.tilegrid.WMTS({ + origin: [ + options.originX || -20037508.3428, + options.originY || 20037508.3428 + ], + extent: extent, + resolutions: resolutions, + matrixIds: matrixIds + }), + style: options.style || '', + wrapX: true + })) + }); + } +}); diff --git a/web/client/components/map/openlayers/plugins/index.js b/web/client/components/map/openlayers/plugins/index.js index 0327309959..614c5d53dd 100644 --- a/web/client/components/map/openlayers/plugins/index.js +++ b/web/client/components/map/openlayers/plugins/index.js @@ -15,5 +15,6 @@ module.exports = { OverlayLayer: require('./OverlayLayer'), TileProviderLayer: require('./TileProviderLayer'), VectorLayer: require('./VectorLayer'), - WMSLayer: require('./WMSLayer') + WMSLayer: require('./WMSLayer'), + WMTSLayer: require('./WMTSLayer') }; diff --git a/web/client/localConfig.json b/web/client/localConfig.json index 4fa3a2544d..b4f425eaa7 100644 --- a/web/client/localConfig.json +++ b/web/client/localConfig.json @@ -429,7 +429,8 @@ "closeGlyph": "1-close", "initialCatalogURL": { "csw": "http://demo.geo-solutions.it/geoserver/csw", - "wms": "http://demo.geo-solutions.it/geoserver/wms" + "wms": "http://demo.geo-solutions.it/geoserver/wms", + "wmts": "http://demo.geo-solutions.it/geoserver/gwc/service/wmts" } } }, { diff --git a/web/client/plugins/MetadataExplorer.jsx b/web/client/plugins/MetadataExplorer.jsx index 92f3e11457..41767ba504 100644 --- a/web/client/plugins/MetadataExplorer.jsx +++ b/web/client/plugins/MetadataExplorer.jsx @@ -95,7 +95,7 @@ const MetadataExplorerComponent = React.createClass({ }); const MetadataExplorerPlugin = connect((state) => ({ searchOptions: state.catalog && state.catalog.searchOptions, - formats: state.catalog && state.catalog.supportedFormats || [{name: 'csw', label: 'CSW'}, {name: 'wms', label: 'WMS'}], + formats: state.catalog && state.catalog.supportedFormats || [{name: 'csw', label: 'CSW'}, {name: 'wms', label: 'WMS'}, {name: "wmts", label: "WMTS"}], result: state.catalog && state.catalog.result, loadingError: state.catalog && state.catalog.loadingError, layerError: state.catalog && state.catalog.layerError, diff --git a/web/client/plugins/Save.jsx b/web/client/plugins/Save.jsx index 467fc156a2..4f041710fe 100644 --- a/web/client/plugins/Save.jsx +++ b/web/client/plugins/Save.jsx @@ -22,6 +22,8 @@ const {mapSelector} = require('../selectors/map'); const {layersSelector} = require('../selectors/layers'); const stateSelector = state => state; +const LayersUtils = require('../utils/LayersUtils'); + const selector = createSelector(mapSelector, stateSelector, layersSelector, (map, state, layers) => ({ currentZoomLvl: map && map.zoom, show: state.controls && state.controls.save && state.controls.save.enabled, @@ -83,28 +85,7 @@ const Save = React.createClass({ zoom: this.props.map.zoom }; let layers = this.props.layers.map((layer) => { - return { - features: layer.features, - format: layer.format, - group: layer.group, - search: layer.search, - source: layer.source, - name: layer.name, - opacity: layer.opacity, - provider: layer.provider, - styles: layer.styles, - style: layer.style, - availableStyles: layer.availableStyles, - capabilitiesURL: layer.capabilitiesURL, - title: layer.title, - transparent: layer.transparent, - type: layer.type, - url: layer.url, - bbox: layer.bbox, - visibility: layer.visibility, - singleTile: layer.singleTile || false, - ...assign({}, layer.params ? {params: layer.params} : {}) - }; + return LayersUtils.saveLayer(layer); }); // Groups are ignored, as they already are defined in the layers let resultingmap = { diff --git a/web/client/plugins/SaveAs.jsx b/web/client/plugins/SaveAs.jsx index e806900aaf..f348835768 100644 --- a/web/client/plugins/SaveAs.jsx +++ b/web/client/plugins/SaveAs.jsx @@ -22,6 +22,7 @@ const stateSelector = state => state; const {layersSelector} = require('../selectors/layers'); const {indexOf} = require('lodash'); +const LayersUtils = require('../utils/LayersUtils'); const selector = createSelector(mapSelector, stateSelector, layersSelector, (map, state, layers) => ({ currentZoomLvl: map && map.zoom, @@ -115,28 +116,7 @@ const SaveAs = React.createClass({ zoom: this.props.map.zoom }; let layers = this.props.layers.map((layer) => { - return { - features: layer.features, - format: layer.format, - group: layer.group, - search: layer.search, - source: layer.source, - name: layer.name, - opacity: layer.opacity, - provider: layer.provider, - styles: layer.styles, - style: layer.style, - availableStyles: layer.availableStyles, - capabilitiesURL: layer.capabilitiesURL, - title: layer.title, - transparent: layer.transparent, - type: layer.type, - url: layer.url, - bbox: layer.bbox, - visibility: layer.visibility, - singleTile: layer.singleTile || false, - ...assign({}, layer.params ? {params: layer.params} : {}) - }; + return LayersUtils.saveLayer(layer); }); // Groups are ignored, as they already are defined in the layers let resultingmap = { diff --git a/web/client/reducers/layers.js b/web/client/reducers/layers.js index a3a2c9a0f5..31c0d4d0e3 100644 --- a/web/client/reducers/layers.js +++ b/web/client/reducers/layers.js @@ -126,7 +126,7 @@ function layers(state = [], action) { } case LAYER_LOAD: { const newLayers = (state.flat || []).map((layer) => { - return layer.id === action.layerId ? assign({}, layer, {loading: false, loadingError: action.error}) : layer; + return layer.id === action.layerId ? assign({}, layer, {loading: false, loadingError: action.error ? "Error" : false}) : layer; }); return assign({}, state, {flat: newLayers}); } diff --git a/web/client/test-resources/wmts/GetCapabilities-1.0.0.xml b/web/client/test-resources/wmts/GetCapabilities-1.0.0.xml new file mode 100644 index 0000000000..431f653890 --- /dev/null +++ b/web/client/test-resources/wmts/GetCapabilities-1.0.0.xml @@ -0,0 +1,1913 @@ + + + + + Web Map Tile Service - GeoWebCache + OGC WMTS + 1.0.0 + + + http://demo.geo-solutions.it/geoserver/gwc/service/wmts + + + GeoWebCache User + + + + + + + + + + KVP + + + + + + + + + + + + + KVP + + + + + + + + + + + + + KVP + + + + + + + + + + tasmania_cities + + 147.2909004483 -42.85110181689001 + 147.2911004483 -42.85090181689 + + topp:tasmania_cities_hidden + + image/png + image/jpeg + image/png8 + text/plain + application/vnd.ogc.gml + text/xml + application/vnd.ogc.gml/3.1.1 + text/xml + text/html + application/json + + EPSG:4326 + + + EPSG:4326:0 + 0 + 0 + 1 + 1 + + + EPSG:4326:1 + 1 + 1 + 3 + 3 + + + EPSG:4326:2 + 2 + 2 + 7 + 7 + + + EPSG:4326:3 + 5 + 5 + 14 + 14 + + + EPSG:4326:4 + 11 + 11 + 29 + 29 + + + EPSG:4326:5 + 23 + 23 + 58 + 58 + + + EPSG:4326:6 + 47 + 47 + 116 + 116 + + + EPSG:4326:7 + 94 + 94 + 232 + 232 + + + EPSG:4326:8 + 188 + 188 + 465 + 465 + + + EPSG:4326:9 + 377 + 377 + 930 + 930 + + + EPSG:4326:10 + 755 + 755 + 1861 + 1861 + + + EPSG:4326:11 + 1511 + 1511 + 3723 + 3723 + + + EPSG:4326:12 + 3023 + 3023 + 7447 + 7447 + + + EPSG:4326:13 + 6046 + 6046 + 14895 + 14895 + + + EPSG:4326:14 + 12092 + 12092 + 29790 + 29790 + + + EPSG:4326:15 + 24184 + 24184 + 59581 + 59581 + + + EPSG:4326:16 + 48369 + 48369 + 119162 + 119163 + + + EPSG:4326:17 + 96739 + 96739 + 238325 + 238326 + + + EPSG:4326:18 + 193478 + 193478 + 476651 + 476652 + + + EPSG:4326:19 + 386956 + 386956 + 953303 + 953304 + + + EPSG:4326:20 + 773912 + 773913 + 1906607 + 1906608 + + + EPSG:4326:21 + 1547825 + 1547827 + 3813215 + 3813217 + + + + + EPSG:900913 + + + EPSG:900913:0 + 0 + 0 + 0 + 0 + + + EPSG:900913:1 + 1 + 1 + 1 + 1 + + + EPSG:900913:2 + 2 + 2 + 3 + 3 + + + EPSG:900913:3 + 5 + 5 + 7 + 7 + + + EPSG:900913:4 + 10 + 10 + 14 + 14 + + + EPSG:900913:5 + 20 + 20 + 29 + 29 + + + EPSG:900913:6 + 40 + 40 + 58 + 58 + + + EPSG:900913:7 + 80 + 80 + 116 + 116 + + + EPSG:900913:8 + 161 + 161 + 232 + 232 + + + EPSG:900913:9 + 323 + 323 + 465 + 465 + + + EPSG:900913:10 + 647 + 647 + 930 + 930 + + + EPSG:900913:11 + 1294 + 1294 + 1861 + 1861 + + + EPSG:900913:12 + 2588 + 2588 + 3723 + 3723 + + + EPSG:900913:13 + 5177 + 5177 + 7447 + 7447 + + + EPSG:900913:14 + 10354 + 10354 + 14895 + 14895 + + + EPSG:900913:15 + 20708 + 20708 + 29790 + 29790 + + + EPSG:900913:16 + 41417 + 41417 + 59581 + 59581 + + + EPSG:900913:17 + 82835 + 82835 + 119162 + 119163 + + + EPSG:900913:18 + 165671 + 165671 + 238325 + 238326 + + + EPSG:900913:19 + 331342 + 331342 + 476651 + 476652 + + + EPSG:900913:20 + 662684 + 662685 + 953303 + 953304 + + + EPSG:900913:21 + 1325368 + 1325370 + 1906607 + 1906608 + + + EPSG:900913:22 + 2650737 + 2650740 + 3813215 + 3813217 + + + EPSG:900913:23 + 5301475 + 5301481 + 7626430 + 7626435 + + + EPSG:900913:24 + 10602950 + 10602963 + 15252861 + 15252870 + + + EPSG:900913:25 + 21205901 + 21205926 + 30505722 + 30505741 + + + EPSG:900913:26 + 42411802 + 42411852 + 61011445 + 61011483 + + + EPSG:900913:27 + 84823604 + 84823705 + 122022891 + 122022966 + + + EPSG:900913:28 + 169647208 + 169647411 + 244045783 + 244045932 + + + EPSG:900913:29 + 339294416 + 339294823 + 488091567 + 488091865 + + + EPSG:900913:30 + 678588833 + 678589646 + 976183134 + 976183731 + + + + + + archsites + + -103.87237752497381 44.37731979707393 + -103.63775627308381 44.48795905249415 + + sf:archsites_hidden + + image/png + image/jpeg + image/png8 + text/plain + application/vnd.ogc.gml + text/xml + application/vnd.ogc.gml/3.1.1 + text/xml + text/html + application/json + + EPSG:4326 + + + EPSG:4326:0 + 0 + 0 + 0 + 0 + + + EPSG:4326:1 + 0 + 0 + 0 + 0 + + + EPSG:4326:2 + 1 + 1 + 1 + 1 + + + EPSG:4326:3 + 2 + 2 + 3 + 3 + + + EPSG:4326:4 + 4 + 4 + 6 + 6 + + + EPSG:4326:5 + 8 + 8 + 13 + 13 + + + EPSG:4326:6 + 16 + 16 + 27 + 27 + + + EPSG:4326:7 + 32 + 32 + 54 + 54 + + + EPSG:4326:8 + 64 + 64 + 108 + 108 + + + EPSG:4326:9 + 129 + 129 + 216 + 217 + + + EPSG:4326:10 + 258 + 259 + 433 + 434 + + + EPSG:4326:11 + 517 + 519 + 866 + 868 + + + EPSG:4326:12 + 1035 + 1038 + 1732 + 1737 + + + EPSG:4326:13 + 2071 + 2076 + 3464 + 3475 + + + EPSG:4326:14 + 4142 + 4152 + 6929 + 6950 + + + EPSG:4326:15 + 8285 + 8305 + 13858 + 13901 + + + EPSG:4326:16 + 16570 + 16610 + 27717 + 27802 + + + EPSG:4326:17 + 33140 + 33221 + 55434 + 55605 + + + EPSG:4326:18 + 66281 + 66442 + 110868 + 111210 + + + EPSG:4326:19 + 132563 + 132885 + 221737 + 222421 + + + EPSG:4326:20 + 265126 + 265771 + 443475 + 444842 + + + EPSG:4326:21 + 530253 + 531542 + 886951 + 889684 + + + + + EPSG:900913 + + + EPSG:900913:0 + 0 + 0 + 0 + 0 + + + EPSG:900913:1 + 0 + 0 + 0 + 0 + + + EPSG:900913:2 + 1 + 1 + 0 + 0 + + + EPSG:900913:3 + 2 + 2 + 1 + 1 + + + EPSG:900913:4 + 5 + 5 + 3 + 3 + + + EPSG:900913:5 + 11 + 11 + 6 + 6 + + + EPSG:900913:6 + 23 + 23 + 13 + 13 + + + EPSG:900913:7 + 46 + 46 + 27 + 27 + + + EPSG:900913:8 + 92 + 92 + 54 + 54 + + + EPSG:900913:9 + 185 + 185 + 108 + 108 + + + EPSG:900913:10 + 370 + 370 + 216 + 217 + + + EPSG:900913:11 + 740 + 741 + 433 + 434 + + + EPSG:900913:12 + 1481 + 1483 + 866 + 868 + + + EPSG:900913:13 + 2963 + 2966 + 1732 + 1737 + + + EPSG:900913:14 + 5926 + 5933 + 3464 + 3475 + + + EPSG:900913:15 + 11853 + 11867 + 6929 + 6950 + + + EPSG:900913:16 + 23706 + 23734 + 13858 + 13901 + + + EPSG:900913:17 + 47412 + 47468 + 27717 + 27802 + + + EPSG:900913:18 + 94824 + 94937 + 55434 + 55605 + + + EPSG:900913:19 + 189649 + 189875 + 110868 + 111210 + + + EPSG:900913:20 + 379298 + 379750 + 221737 + 222421 + + + EPSG:900913:21 + 758597 + 759500 + 443475 + 444842 + + + EPSG:900913:22 + 1517195 + 1519000 + 886951 + 889684 + + + EPSG:900913:23 + 3034391 + 3038001 + 1773902 + 1779369 + + + EPSG:900913:24 + 6068782 + 6076003 + 3547804 + 3558738 + + + EPSG:900913:25 + 12137564 + 12152006 + 7095608 + 7117476 + + + EPSG:900913:26 + 24275129 + 24304012 + 14191217 + 14234953 + + + EPSG:900913:27 + 48550258 + 48608024 + 28382434 + 28469907 + + + EPSG:900913:28 + 97100517 + 97216049 + 56764869 + 56939815 + + + EPSG:900913:29 + 194201034 + 194432099 + 113529739 + 113879631 + + + EPSG:900913:30 + 388402069 + 388864198 + 227059478 + 227759263 + + + + + + ny-parent-group + + + -180.0 -90.0 + 180.0 90.0 + + ny-parent-group + + image/png + image/jpeg + image/png8 + text/plain + application/vnd.ogc.gml + text/xml + application/vnd.ogc.gml/3.1.1 + text/xml + text/html + application/json + + EPSG:4326 + + + EPSG:900913 + + + + GlobalCRS84Pixel + urn:ogc:def:crs:EPSG::4326 + + GlobalCRS84Pixel:0 + 7.951392199519542E8 + 90.0 -180.0 + 256 + 256 + 1 + 1 + + + GlobalCRS84Pixel:1 + 3.975696099759771E8 + 90.0 -180.0 + 256 + 256 + 2 + 1 + + + GlobalCRS84Pixel:2 + 1.9878480498798856E8 + 90.0 -180.0 + 256 + 256 + 3 + 2 + + + GlobalCRS84Pixel:3 + 1.325232033253257E8 + 90.0 -180.0 + 256 + 256 + 5 + 3 + + + GlobalCRS84Pixel:4 + 6.626160166266285E7 + 90.0 -180.0 + 256 + 256 + 9 + 5 + + + GlobalCRS84Pixel:5 + 3.3130800831331424E7 + 90.0 -180.0 + 256 + 256 + 17 + 9 + + + GlobalCRS84Pixel:6 + 1.325232033253257E7 + 90.0 -180.0 + 256 + 256 + 43 + 22 + + + GlobalCRS84Pixel:7 + 6626160.166266285 + 90.0 -180.0 + 256 + 256 + 85 + 43 + + + GlobalCRS84Pixel:8 + 3313080.0831331424 + 90.0 -180.0 + 256 + 256 + 169 + 85 + + + GlobalCRS84Pixel:9 + 1656540.0415665712 + 90.0 -180.0 + 256 + 256 + 338 + 169 + + + GlobalCRS84Pixel:10 + 552180.0138555238 + 90.0 -180.0 + 256 + 256 + 1013 + 507 + + + GlobalCRS84Pixel:11 + 331308.00831331423 + 90.0 -180.0 + 256 + 256 + 1688 + 844 + + + GlobalCRS84Pixel:12 + 110436.00277110476 + 90.0 -180.0 + 256 + 256 + 5063 + 2532 + + + GlobalCRS84Pixel:13 + 55218.00138555238 + 90.0 -180.0 + 256 + 256 + 10125 + 5063 + + + GlobalCRS84Pixel:14 + 33130.80083133143 + 90.0 -180.0 + 256 + 256 + 16875 + 8438 + + + GlobalCRS84Pixel:15 + 11043.600277110474 + 90.0 -180.0 + 256 + 256 + 50625 + 25313 + + + GlobalCRS84Pixel:16 + 3313.080083133142 + 90.0 -180.0 + 256 + 256 + 168750 + 84375 + + + GlobalCRS84Pixel:17 + 1104.3600277110472 + 90.0 -180.0 + 256 + 256 + 506250 + 253125 + + + + EPSG:4326 + urn:ogc:def:crs:EPSG::4326 + + EPSG:4326:0 + 2.795411320143589E8 + 90.0 -180.0 + 256 + 256 + 2 + 1 + + + EPSG:4326:1 + 1.3977056600717944E8 + 90.0 -180.0 + 256 + 256 + 4 + 2 + + + EPSG:4326:2 + 6.988528300358972E7 + 90.0 -180.0 + 256 + 256 + 8 + 4 + + + EPSG:4326:3 + 3.494264150179486E7 + 90.0 -180.0 + 256 + 256 + 16 + 8 + + + EPSG:4326:4 + 1.747132075089743E7 + 90.0 -180.0 + 256 + 256 + 32 + 16 + + + EPSG:4326:5 + 8735660.375448715 + 90.0 -180.0 + 256 + 256 + 64 + 32 + + + EPSG:4326:6 + 4367830.1877243575 + 90.0 -180.0 + 256 + 256 + 128 + 64 + + + EPSG:4326:7 + 2183915.0938621787 + 90.0 -180.0 + 256 + 256 + 256 + 128 + + + EPSG:4326:8 + 1091957.5469310894 + 90.0 -180.0 + 256 + 256 + 512 + 256 + + + EPSG:4326:9 + 545978.7734655447 + 90.0 -180.0 + 256 + 256 + 1024 + 512 + + + EPSG:4326:10 + 272989.38673277234 + 90.0 -180.0 + 256 + 256 + 2048 + 1024 + + + EPSG:4326:11 + 136494.69336638617 + 90.0 -180.0 + 256 + 256 + 4096 + 2048 + + + EPSG:4326:12 + 68247.34668319309 + 90.0 -180.0 + 256 + 256 + 8192 + 4096 + + + EPSG:4326:13 + 34123.67334159654 + 90.0 -180.0 + 256 + 256 + 16384 + 8192 + + + EPSG:4326:14 + 17061.83667079827 + 90.0 -180.0 + 256 + 256 + 32768 + 16384 + + + EPSG:4326:15 + 8530.918335399136 + 90.0 -180.0 + 256 + 256 + 65536 + 32768 + + + EPSG:4326:16 + 4265.459167699568 + 90.0 -180.0 + 256 + 256 + 131072 + 65536 + + + EPSG:4326:17 + 2132.729583849784 + 90.0 -180.0 + 256 + 256 + 262144 + 131072 + + + EPSG:4326:18 + 1066.364791924892 + 90.0 -180.0 + 256 + 256 + 524288 + 262144 + + + EPSG:4326:19 + 533.182395962446 + 90.0 -180.0 + 256 + 256 + 1048576 + 524288 + + + EPSG:4326:20 + 266.591197981223 + 90.0 -180.0 + 256 + 256 + 2097152 + 1048576 + + + EPSG:4326:21 + 133.2955989906115 + 90.0 -180.0 + 256 + 256 + 4194304 + 2097152 + + + + GoogleCRS84Quad + urn:ogc:def:crs:EPSG::4326 + + GoogleCRS84Quad:0 + 5.590822640287178E8 + 90.0 -180.0 + 256 + 256 + 1 + 1 + + + GoogleCRS84Quad:1 + 2.795411320143589E8 + 90.0 -180.0 + 256 + 256 + 2 + 1 + + + GoogleCRS84Quad:2 + 1.397705660071794E8 + 90.0 -180.0 + 256 + 256 + 4 + 2 + + + GoogleCRS84Quad:3 + 6.988528300358972E7 + 90.0 -180.0 + 256 + 256 + 8 + 4 + + + GoogleCRS84Quad:4 + 3.494264150179486E7 + 90.0 -180.0 + 256 + 256 + 16 + 8 + + + GoogleCRS84Quad:5 + 1.747132075089743E7 + 90.0 -180.0 + 256 + 256 + 32 + 16 + + + GoogleCRS84Quad:6 + 8735660.375448715 + 90.0 -180.0 + 256 + 256 + 64 + 32 + + + GoogleCRS84Quad:7 + 4367830.187724357 + 90.0 -180.0 + 256 + 256 + 128 + 64 + + + GoogleCRS84Quad:8 + 2183915.093862179 + 90.0 -180.0 + 256 + 256 + 256 + 128 + + + GoogleCRS84Quad:9 + 1091957.546931089 + 90.0 -180.0 + 256 + 256 + 512 + 256 + + + GoogleCRS84Quad:10 + 545978.7734655447 + 90.0 -180.0 + 256 + 256 + 1024 + 512 + + + GoogleCRS84Quad:11 + 272989.3867327723 + 90.0 -180.0 + 256 + 256 + 2048 + 1024 + + + GoogleCRS84Quad:12 + 136494.6933663862 + 90.0 -180.0 + 256 + 256 + 4096 + 2048 + + + GoogleCRS84Quad:13 + 68247.34668319309 + 90.0 -180.0 + 256 + 256 + 8192 + 4096 + + + GoogleCRS84Quad:14 + 34123.67334159654 + 90.0 -180.0 + 256 + 256 + 16384 + 8192 + + + GoogleCRS84Quad:15 + 17061.83667079827 + 90.0 -180.0 + 256 + 256 + 32768 + 16384 + + + GoogleCRS84Quad:16 + 8530.918335399136 + 90.0 -180.0 + 256 + 256 + 65536 + 32768 + + + GoogleCRS84Quad:17 + 4265.459167699568 + 90.0 -180.0 + 256 + 256 + 131072 + 65536 + + + GoogleCRS84Quad:18 + 2132.729583849784 + 90.0 -180.0 + 256 + 256 + 262144 + 131072 + + + + EPSG:900913 + urn:ogc:def:crs:EPSG::900913 + + EPSG:900913:0 + 5.590822639508929E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1 + 1 + + + EPSG:900913:1 + 2.7954113197544646E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2 + 2 + + + EPSG:900913:2 + 1.3977056598772323E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4 + 4 + + + EPSG:900913:3 + 6.988528299386162E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8 + 8 + + + EPSG:900913:4 + 3.494264149693081E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16 + 16 + + + EPSG:900913:5 + 1.7471320748465404E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 32 + 32 + + + EPSG:900913:6 + 8735660.374232702 + -2.003750834E7 2.0037508E7 + 256 + 256 + 64 + 64 + + + EPSG:900913:7 + 4367830.187116351 + -2.003750834E7 2.0037508E7 + 256 + 256 + 128 + 128 + + + EPSG:900913:8 + 2183915.0935581755 + -2.003750834E7 2.0037508E7 + 256 + 256 + 256 + 256 + + + EPSG:900913:9 + 1091957.5467790877 + -2.003750834E7 2.0037508E7 + 256 + 256 + 512 + 512 + + + EPSG:900913:10 + 545978.7733895439 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1024 + 1024 + + + EPSG:900913:11 + 272989.38669477194 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2048 + 2048 + + + EPSG:900913:12 + 136494.69334738597 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4096 + 4096 + + + EPSG:900913:13 + 68247.34667369298 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8192 + 8192 + + + EPSG:900913:14 + 34123.67333684649 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16384 + 16384 + + + EPSG:900913:15 + 17061.836668423246 + -2.003750834E7 2.0037508E7 + 256 + 256 + 32768 + 32768 + + + EPSG:900913:16 + 8530.918334211623 + -2.003750834E7 2.0037508E7 + 256 + 256 + 65536 + 65536 + + + EPSG:900913:17 + 4265.4591671058115 + -2.003750834E7 2.0037508E7 + 256 + 256 + 131072 + 131072 + + + EPSG:900913:18 + 2132.7295835529058 + -2.003750834E7 2.0037508E7 + 256 + 256 + 262144 + 262144 + + + EPSG:900913:19 + 1066.3647917764529 + -2.003750834E7 2.0037508E7 + 256 + 256 + 524288 + 524288 + + + EPSG:900913:20 + 533.1823958882264 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1048576 + 1048576 + + + EPSG:900913:21 + 266.5911979441132 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2097152 + 2097152 + + + EPSG:900913:22 + 133.2955989720566 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4194304 + 4194304 + + + EPSG:900913:23 + 66.6477994860283 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8388608 + 8388608 + + + EPSG:900913:24 + 33.32389974301415 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16777216 + 16777216 + + + EPSG:900913:25 + 16.661949871507076 + -2.003750834E7 2.0037508E7 + 256 + 256 + 33554432 + 33554432 + + + EPSG:900913:26 + 8.330974935753538 + -2.003750834E7 2.0037508E7 + 256 + 256 + 67108864 + 67108864 + + + EPSG:900913:27 + 4.165487467876769 + -2.003750834E7 2.0037508E7 + 256 + 256 + 134217728 + 134217728 + + + EPSG:900913:28 + 2.0827437339383845 + -2.003750834E7 2.0037508E7 + 256 + 256 + 268435456 + 268435456 + + + EPSG:900913:29 + 1.0413718669691923 + -2.003750834E7 2.0037508E7 + 256 + 256 + 536870912 + 536870912 + + + EPSG:900913:30 + 0.5206859334845961 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1073741824 + 1073741824 + + + + GlobalCRS84Scale + urn:ogc:def:crs:EPSG::4326 + + GlobalCRS84Scale:0 + 5.0E8 + 90.0 -180.0 + 256 + 256 + 2 + 1 + + + GlobalCRS84Scale:1 + 2.5E8 + 90.0 -180.0 + 256 + 256 + 3 + 2 + + + GlobalCRS84Scale:2 + 1.0E8 + 90.0 -180.0 + 256 + 256 + 6 + 3 + + + GlobalCRS84Scale:3 + 5.0E7 + 90.0 -180.0 + 256 + 256 + 12 + 6 + + + GlobalCRS84Scale:4 + 2.5E7 + 90.0 -180.0 + 256 + 256 + 23 + 12 + + + GlobalCRS84Scale:5 + 1.0E7 + 90.0 -180.0 + 256 + 256 + 56 + 28 + + + GlobalCRS84Scale:6 + 5000000.0 + 90.0 -180.0 + 256 + 256 + 112 + 56 + + + GlobalCRS84Scale:7 + 2500000.0 + 90.0 -180.0 + 256 + 256 + 224 + 112 + + + GlobalCRS84Scale:8 + 1000000.0 + 90.0 -180.0 + 256 + 256 + 560 + 280 + + + GlobalCRS84Scale:9 + 500000.0 + 90.0 -180.0 + 256 + 256 + 1119 + 560 + + + GlobalCRS84Scale:10 + 250000.0 + 90.0 -180.0 + 256 + 256 + 2237 + 1119 + + + GlobalCRS84Scale:11 + 100000.0 + 90.0 -180.0 + 256 + 256 + 5591 + 2796 + + + GlobalCRS84Scale:12 + 50000.0 + 90.0 -180.0 + 256 + 256 + 11182 + 5591 + + + GlobalCRS84Scale:13 + 25000.0 + 90.0 -180.0 + 256 + 256 + 22364 + 11182 + + + GlobalCRS84Scale:14 + 10000.0 + 90.0 -180.0 + 256 + 256 + 55909 + 27955 + + + GlobalCRS84Scale:15 + 5000.0 + 90.0 -180.0 + 256 + 256 + 111817 + 55909 + + + GlobalCRS84Scale:16 + 2500.0 + 90.0 -180.0 + 256 + 256 + 223633 + 111817 + + + GlobalCRS84Scale:17 + 1000.0 + 90.0 -180.0 + 256 + 256 + 559083 + 279542 + + + GlobalCRS84Scale:18 + 500.0 + 90.0 -180.0 + 256 + 256 + 1118165 + 559083 + + + GlobalCRS84Scale:19 + 250.0 + 90.0 -180.0 + 256 + 256 + 2236330 + 1118165 + + + GlobalCRS84Scale:20 + 100.0 + 90.0 -180.0 + 256 + 256 + 5590823 + 2795412 + + + + + diff --git a/web/client/utils/CatalogUtils.js b/web/client/utils/CatalogUtils.js index e54b267fe9..90542148d1 100644 --- a/web/client/utils/CatalogUtils.js +++ b/web/client/utils/CatalogUtils.js @@ -9,6 +9,8 @@ const assign = require('object-assign'); const {head, isArray, isString} = require('lodash'); const urlUtil = require('url'); +const CoordinatesUtils = require('./CoordinatesUtils'); +const castArray = require('lodash.castarray'); const getWMSBBox = (record) => { let layer = record; @@ -28,6 +30,18 @@ const getWMSBBox = (record) => { return bbox; }; +const getWMTSBBox = (record) => { + let layer = record; + let bbox = (layer["ows:WGS84BoundingBox"]); + if (!bbox) { + bbox = { + "ows:LowerCorner": "-180.0 -90.0", + "ows:UpperCorner": "180.0 90.0" + }; + } + return bbox; +}; + const converters = { csw: (records, options) => { let result = records; @@ -148,6 +162,62 @@ const converters = { }; }); } + }, + wmts: (records, options) => { + if (records && records.records) { + return records.records.map((record) => { + const bbox = getWMTSBBox(record); + return { + title: record["ows:Title"] || record["ows:Identifier"], + description: record["ows:Abstract"] || record["ows:Title"] || record["ows:Identifier"], + identifier: record["ows:Identifier"], + tags: "", + tileMatrixSet: record.TileMatrixSet, + matrixIds: castArray(record.TileMatrixSetLink).reduce((previous, current) => { + const tileMatrix = head(record.TileMatrixSet.filter((matrix) => matrix["ows:Identifier"] === current.TileMatrixSet)); + const tileMatrixSRS = CoordinatesUtils.getEPSGCode(tileMatrix["ows:SupportedCRS"]); + const levels = current.TileMatrixSetLimits && current.TileMatrixSetLimits.TileMatrixLimits.map((limit) => ({ + identifier: limit.TileMatrix, + ranges: { + cols: { + min: limit.MinTileCol, + max: limit.MaxTileCol + }, + rows: { + min: limit.MinTileRow, + max: limit.MaxTileRow + } + } + })) || tileMatrix.TileMatrix.map((matrix) => ({ + identifier: matrix["ows:Identifier"] + })); + + return assign(previous, { + [tileMatrix["ows:Identifier"]]: levels, + [tileMatrixSRS]: levels + }); + }, {}), + TileMatrixSetLink: castArray(record.TileMatrixSetLink), + boundingBox: { + extent: [ + bbox["ows:LowerCorner"].split(" ")[0], + bbox["ows:LowerCorner"].split(" ")[1], + bbox["ows:UpperCorner"].split(" ")[0], + bbox["ows:UpperCorner"].split(" ")[1] + ], + crs: "EPSG:4326" + }, + references: [{ + type: "OGC:WMTS", + url: record.GetTileUrl || options.url, + SRS: record.SRS || [], + params: { + name: record["ows:Identifier"] + } + }] + }; + }); + } } }; diff --git a/web/client/utils/CoordinatesUtils.js b/web/client/utils/CoordinatesUtils.js index 99d1e2e231..0ec4a3459f 100644 --- a/web/client/utils/CoordinatesUtils.js +++ b/web/client/utils/CoordinatesUtils.js @@ -164,6 +164,18 @@ const CoordinatesUtils = { } return srs; }, + getEquivalentSRS(srs) { + if (srs === 'EPSG:900913' || srs === 'EPSG:3857') { + return ['EPSG:3857', 'EPSG:900913']; + } + return [srs]; + }, + getEPSGCode(code) { + if (code.indexOf(':') !== -1) { + return 'EPSG:' + code.substring(code.lastIndexOf(':') + 1); + } + return code; + }, normalizeSRS: function(srs, allowedSRS) { const result = (srs === 'EPSG:900913' ? 'EPSG:3857' : srs); if (allowedSRS && !allowedSRS[result]) { diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index be655e5ca6..7c1701ab94 100644 --- a/web/client/utils/LayersUtils.js +++ b/web/client/utils/LayersUtils.js @@ -132,6 +132,33 @@ var LayersUtils = { return feature; }) }; + }, + saveLayer: (layer) => { + return { + features: layer.features, + format: layer.format, + group: layer.group, + search: layer.search, + source: layer.source, + name: layer.name, + opacity: layer.opacity, + provider: layer.provider, + styles: layer.styles, + style: layer.style, + availableStyles: layer.availableStyles, + capabilitiesURL: layer.capabilitiesURL, + title: layer.title, + transparent: layer.transparent, + type: layer.type, + url: layer.url, + bbox: layer.bbox, + visibility: layer.visibility, + singleTile: layer.singleTile || false, + allowedSRS: layer.allowedSRS, + matrixIds: layer.matrixIds, + tileMatrixSet: layer.tileMatrixSet, + ...assign({}, layer.params ? {params: layer.params} : {}) + }; } }; diff --git a/web/client/utils/WMTSUtils.js b/web/client/utils/WMTSUtils.js new file mode 100644 index 0000000000..3f4549652c --- /dev/null +++ b/web/client/utils/WMTSUtils.js @@ -0,0 +1,39 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +const CoordinatesUtils = require('./CoordinatesUtils'); +const {isString, isArray, isObject, head} = require('lodash'); + +const WMTSUtils = { + getTileMatrixSet: (tileMatrixSet, srs, allowedSRS) => { + if (tileMatrixSet && isString(tileMatrixSet)) { + return tileMatrixSet; + } + if (tileMatrixSet) { + return CoordinatesUtils.getEquivalentSRS(srs, allowedSRS).reduce((previous, current) => { + if (isArray(tileMatrixSet)) { + const matching = head(tileMatrixSet.filter((matrix) => (matrix["ows:Identifier"] === current || CoordinatesUtils.getEPSGCode(matrix["ows:SupportedCRS"]) === current))); + return matching && matching["ows:Identifier"] || previous; + } else if (isObject(tileMatrixSet)) { + return tileMatrixSet[current] || previous; + } + return previous; + }, srs); + } + if (tileMatrixSet && isArray(tileMatrixSet)) { + return CoordinatesUtils.getEquivalentSRS(srs, allowedSRS).reduce((previous, current) => { + return tileMatrixSet[current] || previous; + }, srs); + } + return srs; + } +}; + + +module.exports = WMTSUtils; diff --git a/web/client/utils/leaflet/WMSUtils.js b/web/client/utils/leaflet/WMSUtils.js index 30b83cfa28..7698bd7ea4 100644 --- a/web/client/utils/leaflet/WMSUtils.js +++ b/web/client/utils/leaflet/WMSUtils.js @@ -1,3 +1,10 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ var objectAssign = require('object-assign'); var WMSUtils = { diff --git a/web/client/utils/leaflet/WMTS.js b/web/client/utils/leaflet/WMTS.js new file mode 100644 index 0000000000..1cee07ee28 --- /dev/null +++ b/web/client/utils/leaflet/WMTS.js @@ -0,0 +1,105 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const L = require('leaflet'); + +var WMTS = L.TileLayer.extend({ + defaultWmtsParams: { + service: "WMTS", + request: "GetTile", + version: "1.3.0", + layer: "", + style: "", + tileMatrixSet: "", + format: "image/jpeg" + }, + initialize: function(urls, options, matrixOptions) { + this._url = urls[0]; + this._urls = urls; + this._urlsIndex = 0; + let wmtsParams = L.extend({}, this.defaultWmtsParams); + let tileSize = options.tileSize || this.options.tileSize; + if (options.detectRetina && L.Browser.retina) { + wmtsParams.width = wmtsParams.height = tileSize * 2; + } else { + wmtsParams.width = wmtsParams.height = tileSize; + } + for (let i in options) { + // all keys that are not TileLayer options go to WMS params + if (!this.options.hasOwnProperty(i) && i !== 'crs') { + wmtsParams[i] = options[i]; + } + } + this.wmtsParams = wmtsParams; + this.matrixIds = matrixOptions.matrixIds && this.getMatrix(matrixOptions.matrixIds, matrixOptions) || this.getDefaultMatrix(matrixOptions); + this.ignoreErrors = matrixOptions.ignoreErrors || false; + L.setOptions(this, options); + }, + isInRange: function(col, row, ranges) { + if ((col < ranges.cols.min) || (col > ranges.cols.max)) { + return false; + } + if ((row < ranges.rows.min) || (row > ranges.rows.max)) { + return false; + } + return true; + }, + getTileUrl: function(tilePoint) { // (Point, Number) -> String + let map = this._map; + let crs = map.options.crs; + let tileSize = this.options.tileSize; + let nwPoint = tilePoint.multiplyBy(tileSize); + nwPoint.x += 1; + nwPoint.y -= 1; + let sePoint = nwPoint.add([tileSize, tileSize]); + let nw = crs.project(map.unproject(nwPoint, tilePoint.z)); + let se = crs.project(map.unproject(sePoint, tilePoint.z)); + let tilewidth = se.x - nw.x; + let t = map.getZoom(); + let ident = this.matrixIds[t].identifier; + let X0 = this.matrixIds[t].topLeftCorner.lng; + let Y0 = this.matrixIds[t].topLeftCorner.lat; + let tilecol = Math.floor((nw.x - X0) / tilewidth); + let tilerow = -Math.floor((nw.y - Y0) / tilewidth); + if (this.matrixIds[t].ranges) { + if (!this.isInRange(tilecol, tilerow, this.matrixIds[t].ranges)) { + return "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + } + } + this._urlsIndex++; + if (this._urlsIndex === this._urls.length) { + this._urlsIndex = 0; + } + const url = L.Util.template(this._urls[this._urlsIndex], {s: this._getSubdomain(tilePoint)}); + return url + L.Util.getParamString(this.wmtsParams, url, true) + "&tilematrix=" + ident + "&tilerow=" + tilerow + "&tilecol=" + tilecol; + }, + getMatrix: function(matrix, options) { + return matrix.map((el) => { + return { + identifier: el.identifier, + topLeftCorner: new L.LatLng(options.originY, options.originX), + ranges: el.ranges || null + }; + }); + }, + getDefaultMatrix: function(options) { + var e = new Array(22); + // Lint gives an error here: All "var" declarations must be at the top of the function scope + for (let t = 0; t < 22; t++) { + e[t] = { + identifier: options.tileMatrixPrefix + t, + topLeftCorner: new L.LatLng(options.originY, options.originX) + }; + } + return e; + }, + onError: function() { + // let's allow ignoring WMTS errors if we don't configure ranges + return !this.ignoreErrors; + } +}); +module.exports = WMTS; diff --git a/web/client/utils/ogc/WMTS.js b/web/client/utils/ogc/WMTS.js new file mode 100644 index 0000000000..145b929a85 --- /dev/null +++ b/web/client/utils/ogc/WMTS.js @@ -0,0 +1,38 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +// Disable ESLint because some of the names to include are not in camel case +/*eslint-disable */ +// include base schemas and name spaces +const { + OWS_1_0_0, + WMTS_1_0_0 +} = require('ogc-schemas'); + +const XLink_1_0 = require('w3c-schemas').XLink_1_0; +const {Jsonix} = require('jsonix'); +const context = new Jsonix.Context([ + OWS_1_0_0, + XLink_1_0, + WMTS_1_0_0, + ], { + namespacePrefixes: { + "http://www.opengis.net/ogc": 'ogc', + "http://www.opengis.net/wmts": "wmts", + "http://purl.org/dc/elements/1.1/": "dc", + "http://www.opengis.net/ows": "ows", + "http://inspire.ec.europa.eu/schemas/inspire_vs/1.0": "inspire_vs", + "http://inspire.ec.europa.eu/schemas/common/1.0": "inspire_common" + } +}); +/*eslint-enable */ +const marshaller = context.createMarshaller(); +const unmarshaller = context.createUnmarshaller(); +const WMTS = {}; + + +module.exports = {WMTS, marshaller, unmarshaller};