Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…tions-it#1304 adding WMTS support to Leaflet, Openlayers and Catalog
  • Loading branch information
mbarto committed Jan 24, 2017
1 parent a56dc1a commit 0e2da7e
Show file tree
Hide file tree
Showing 27 changed files with 2,700 additions and 56 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion web/client/actions/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
98 changes: 98 additions & 0 deletions web/client/api/WMTS.js
Original file line number Diff line number Diff line change
@@ -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;
24 changes: 24 additions & 0 deletions web/client/api/__tests__/WMTS-test.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
});
60 changes: 59 additions & 1 deletion web/client/components/catalog/RecordItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('=');
Expand Down Expand Up @@ -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({
Expand All @@ -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",
Expand Down Expand Up @@ -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) {
Expand All @@ -131,6 +142,19 @@ const RecordItem = React.createClass({
</Button>
);
}
if (wmts) {
buttons.push(
<Button
key="wmts-button"
className="record-button"
bsStyle="success"
bsSize={this.props.buttonSize}
onClick={() => { this.addwmtsLayer(wmts); }}
key="addwmtsLayer">
<Glyphicon glyph="plus" />&nbsp;<Message msgId="catalog.addToMap"/>
</Button>
);
}
// creating get capbilities links that will be used to share layers info
if (this.props.showGetCapLinks) {
let links = this.getLinks(record);
Expand Down Expand Up @@ -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);
}
}
}
});

Expand Down
50 changes: 50 additions & 0 deletions web/client/components/catalog/__tests__/RecordItem-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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&",
Expand Down Expand Up @@ -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((<ReactItem
record={sampleRecord3}
onLayerAdd={actions.onLayerAdd}
onZoomToExtent={actions.onZoomToExtent}/>), 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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
8 changes: 7 additions & 1 deletion web/client/components/map/leaflet/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
});

Expand Down
Loading

0 comments on commit 0e2da7e

Please sign in to comment.