From e77b06aecc60b03927c16b3d7f23d88632e4d422 Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Fri, 26 May 2017 16:17:02 +0200 Subject: [PATCH] Part of #1885. Add WFS-T insert and transaction. - Implemented insert and transaction - Moved and tested GML generation stuff from filterutils - Added WFS utilities to generate XML for GML features --- package.json | 1 + .../test-resources/wfs/describe-pois.json | 44 ++++ .../test-resources/wfs/describe-states.json | 196 ++++++++++++++++++ web/client/test-resources/wfs/museam.json | 29 +++ .../wfst/insert/Wyoming_1_1_0.xml | 1 + .../wfst/insert/museam_1_1_0.xml | 1 + web/client/utils/FilterUtils.jsx | 150 +------------- .../utils/__tests__/FilterUtils-test.js | 10 +- web/client/utils/ogc/GML/index.js | 168 +++++++++++++++ web/client/utils/ogc/WFS/base.js | 73 +++++++ web/client/utils/ogc/{WFS.js => WFS/index.js} | 3 +- web/client/utils/ogc/WFST/index.js | 1 + web/client/utils/ogc/WFST/v1_1_0/index.js | 14 ++ web/client/utils/ogc/WFST/v1_1_0/insert.js | 33 +++ .../utils/ogc/WFST/v1_1_0/transaction.js | 23 ++ web/client/utils/ogc/__tests__/GML-test.js | 108 ++++++++++ web/client/utils/ogc/__tests__/WFS-T-test.js | 39 ++++ 17 files changed, 744 insertions(+), 150 deletions(-) create mode 100644 web/client/test-resources/wfs/describe-pois.json create mode 100644 web/client/test-resources/wfs/describe-states.json create mode 100644 web/client/test-resources/wfs/museam.json create mode 100644 web/client/test-resources/wfst/insert/Wyoming_1_1_0.xml create mode 100644 web/client/test-resources/wfst/insert/museam_1_1_0.xml create mode 100644 web/client/utils/ogc/GML/index.js create mode 100644 web/client/utils/ogc/WFS/base.js rename web/client/utils/ogc/{WFS.js => WFS/index.js} (98%) create mode 100644 web/client/utils/ogc/WFST/index.js create mode 100644 web/client/utils/ogc/WFST/v1_1_0/index.js create mode 100644 web/client/utils/ogc/WFST/v1_1_0/insert.js create mode 100644 web/client/utils/ogc/WFST/v1_1_0/transaction.js create mode 100644 web/client/utils/ogc/__tests__/GML-test.js create mode 100644 web/client/utils/ogc/__tests__/WFS-T-test.js diff --git a/package.json b/package.json index d0adf45e65..4b53c30fbb 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "istanbul-instrumenter-loader": "2.0.0", "jsdoc": "3.4.3", "jsdoc-jsx": "0.1.0", + "json-loader": "0.5.4", "karma": "1.5.0", "karma-chrome-launcher": "2.0.0", "karma-cli": "1.0.1", diff --git a/web/client/test-resources/wfs/describe-pois.json b/web/client/test-resources/wfs/describe-pois.json new file mode 100644 index 0000000000..bf904fcfb9 --- /dev/null +++ b/web/client/test-resources/wfs/describe-pois.json @@ -0,0 +1,44 @@ +{ + "elementFormDefault":"qualified", + "targetNamespace":"http://www.census.gov", + "targetPrefix":"tiger", + "featureTypes":[ + { + "typeName":"poi", + "properties":[ + { + "name":"the_geom", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"gml:Point", + "localType":"Point" + }, + { + "name":"NAME", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:string", + "localType":"string" + }, + { + "name":"THUMBNAIL", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:string", + "localType":"string" + }, + { + "name":"MAINPAGE", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:string", + "localType":"string" + } + ] + } + ] +} diff --git a/web/client/test-resources/wfs/describe-states.json b/web/client/test-resources/wfs/describe-states.json new file mode 100644 index 0000000000..36254d7d89 --- /dev/null +++ b/web/client/test-resources/wfs/describe-states.json @@ -0,0 +1,196 @@ +{ + "elementFormDefault":"qualified", + "targetNamespace":"http://www.openplans.org/topp", + "targetPrefix":"topp", + "featureTypes":[ + { + "typeName":"states", + "properties":[ + { + "name":"the_geom", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"gml:MultiPolygon", + "localType":"MultiPolygon" + }, + { + "name":"STATE_NAME", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:string", + "localType":"string" + }, + { + "name":"STATE_FIPS", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:string", + "localType":"string" + }, + { + "name":"SUB_REGION", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:string", + "localType":"string" + }, + { + "name":"STATE_ABBR", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:string", + "localType":"string" + }, + { + "name":"LAND_KM", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"WATER_KM", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"PERSONS", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"FAMILIES", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"HOUSHOLD", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"MALE", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"FEMALE", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"WORKERS", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"DRVALONE", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"CARPOOL", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"PUBTRANS", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"EMPLOYED", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"UNEMPLOY", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"SERVICE", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"MANUAL", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"P_MALE", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"P_FEMALE", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + }, + { + "name":"SAMP_POP", + "maxOccurs":1, + "minOccurs":0, + "nillable":true, + "type":"xsd:number", + "localType":"number" + } + ] + } + ] +} diff --git a/web/client/test-resources/wfs/museam.json b/web/client/test-resources/wfs/museam.json new file mode 100644 index 0000000000..97e7d5c7a8 --- /dev/null +++ b/web/client/test-resources/wfs/museam.json @@ -0,0 +1,29 @@ +{ + "type":"FeatureCollection", + "totalFeatures":6, + "features":[ + { + "type":"Feature", + "id":"poi.1", + "geometry":{ + "type":"Point", + "coordinates":[ + -74.0104611, + 40.70758763 + ] + }, + "geometry_name":"the_geom", + "properties":{ + "NAME":"museam", + "THUMBNAIL":"pics/22037827-Ti.jpg", + "MAINPAGE":"pics/22037827-L.jpg" + } + } + ], + "crs":{ + "type":"name", + "properties":{ + "name":"urn:ogc:def:crs:EPSG::4326" + } + } +} diff --git a/web/client/test-resources/wfst/insert/Wyoming_1_1_0.xml b/web/client/test-resources/wfst/insert/Wyoming_1_1_0.xml new file mode 100644 index 0000000000..7344df0649 --- /dev/null +++ b/web/client/test-resources/wfst/insert/Wyoming_1_1_0.xml @@ -0,0 +1 @@ +Wyoming56MtnWY251500.8011848.1494535881198251688392270072265811645611536792810929632078681311271419291570.50.583202-104.053108 41.698246 -104.054993 41.564247 -104.053505 41.388107 -104.051201 41.003227 -104.933968 40.994305 -105.278259 40.996365 -106.202896 41.000111 -106.328545 41.001316 -106.864838 40.998489 -107.303436 41.000168 -107.918037 41.00341 -109.047638 40.998474 -110.001457 40.997646 -110.062477 40.99794 -111.050285 40.996635 -111.050911 41.25848 -111.050323 41.578648 -111.047951 41.996265 -111.046028 42.503323 -111.048447 43.019962 -111.04673 43.284813 -111.045998 43.515606 -111.049629 43.982632 -111.050789 44.473396 -111.050842 44.664562 -111.05265 44.995766 -110.428894 44.992348 -110.392006 44.998688 -109.994789 45.002853 -109.798653 44.99958 -108.624573 44.997643 -108.258568 45.00016 -107.893715 44.999813 -106.258644 44.996174 -106.020576 44.997227 -105.084465 44.999832 -105.04126 45.001091 -104.059349 44.997349 -104.058975 44.574368 -104.060547 44.181843 -104.059242 44.145844 -104.05899 43.852928 -104.057426 43.503738 -104.05867 43.47916 -104.05571 43.003094 -104.055725 42.614704 -104.053009 41.999851 -104.053108 41.698246 diff --git a/web/client/test-resources/wfst/insert/museam_1_1_0.xml b/web/client/test-resources/wfst/insert/museam_1_1_0.xml new file mode 100644 index 0000000000..7bde341869 --- /dev/null +++ b/web/client/test-resources/wfst/insert/museam_1_1_0.xml @@ -0,0 +1 @@ +museampics/22037827-Ti.jpgpics/22037827-L.jpg-74.0104611 40.70758763 diff --git a/web/client/utils/FilterUtils.jsx b/web/client/utils/FilterUtils.jsx index a6f33dea16..f8e9461caf 100644 --- a/web/client/utils/FilterUtils.jsx +++ b/web/client/utils/FilterUtils.jsx @@ -6,9 +6,7 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ - -const {isArray} = require('lodash'); - +const {processOGCGeometry, closePolygon, pointElement, polygonElement, lineStringElement } = require("./ogc/GML"); const normalizeVersion = (version) => { if (!version) { return "2.0"; @@ -359,124 +357,11 @@ const FilterUtils = { } return filter; }, - closePolygon: function(coords) { - if (coords.length >= 3) { - const first = coords[0]; - const last = coords[coords.length - 1]; - if ((first[0] !== last[0]) || (first[1] !== last[1])) { - return coords.concat([coords[0]]); - } - } - return coords; - }, - getGmlPolygonElement: function(coordinates, srsName, version) { - let gmlPolygon = '' : '>'; - - // /////////////////////////////////////////////////////////////////////////////////////////////////////// - // Array of LinearRing coordinate array. The first element in the array represents the exterior ring. - // Any subsequent elements represent interior rings (or holes). - // /////////////////////////////////////////////////////////////////////////////////////////////////////// - - const normalizedCoords = coordinates.length && isArray(coordinates[0]) && coordinates[0].length && isArray(coordinates[0][0]) ? coordinates : [coordinates]; - normalizedCoords.forEach((element, index) => { - let coords = this.closePolygon(element).map((coordinate) => { - return coordinate[0] + (version === "1.0.0" ? "," : " ") + coordinate[1]; - }); - const exterior = (version === "1.0.0" ? "outerBoundaryIs" : "exterior"); - const interior = (version === "1.0.0" ? "innerBoundaryIs" : "exterior"); - gmlPolygon += - (index < 1 ? '' : '') + - '' + - (version === "1.0.0" ? '' : '') + - coords.join(" ") + - (version === "1.0.0" ? '' : '') + - '' + - (index < 1 ? '' : ''); - }); - - gmlPolygon += ''; - return gmlPolygon; - }, - getGmlLineStringElement: function(coordinates, srsName, version) { - let gml = '' : '>'; - - // /////////////////////////////////////////////////////////////////////////////////////////////////////// - // Array of LinearRing coordinate array. The first element in the array represents the exterior ring. - // Any subsequent elements represent interior rings (or holes). - // /////////////////////////////////////////////////////////////////////////////////////////////////////// - coordinates.forEach((element) => { - let coords = element.map((coordinate) => { - return coordinate[0] + (version === "1.0.0" ? "," : " ") + coordinate[1]; - }); - gml += (version === "1.0.0" ? '' : '') + - coords.join(" ") + - (version === "1.0.0" ? '' : ''); - }); - - gml += ''; - return gml; - }, - processOGCGeometry: function(version, geometry) { - let ogc = ''; - switch (geometry.type) { - case "Point": - ogc += this.getGmlPointElement(geometry.coordinates, - geometry.projection || "EPSG:4326", version); - break; - case "MultiPoint": - ogc += ''; - - // ////////////////////////////////////////////////////////////////////////// - // Coordinates of a MultiPoint are an array of positions - // ////////////////////////////////////////////////////////////////////////// - geometry.coordinates.forEach((element) => { - let point = element; - if (point) { - ogc += ""; - ogc += this.getGmlPointElement(point, version); - ogc += ""; - } - }); - - ogc += ''; - break; - case "LineString": - ogc += this.getGmlLineStringElement(geometry.coordinates, - geometry.projection || "EPSG:4326", version); - break; - case "Polygon": - ogc += this.getGmlPolygonElement(geometry.coordinates, - geometry.projection || "EPSG:4326", version); - break; - case "MultiPolygon": - const multyPolygonTagName = version === "2.0" ? "MultiSurface" : "MultiPolygon"; - const polygonMemberTagName = version === "2.0" ? "surfaceMembers" : "polygonMember"; - - ogc += ''; - - // ////////////////////////////////////////////////////////////////////////// - // Coordinates of a MultiPolygon are an array of Polygon coordinate arrays - // ////////////////////////////////////////////////////////////////////////// - geometry.coordinates.forEach((element) => { - let polygon = element; - if (polygon) { - ogc += ""; - ogc += this.getGmlPolygonElement(polygon, version); - ogc += ""; - } - }); - - ogc += ''; - break; - default: - break; - } - return ogc; - }, + closePolygon: closePolygon, + getGmlPointElement: pointElement, + getGmlPolygonElement: polygonElement, + getGmlLineStringElement: lineStringElement, + processOGCGeometry, processOGCSpatialFilter: function(version, objFilter, nsplaceholder) { let ogc = ogcSpatialOperator[objFilter.spatialField.operation].startTag; ogc += @@ -594,29 +479,6 @@ const FilterUtils = { * }} * } */ - getGmlPointElement: function(coordinates, srsName, version) { - let gmlPoint = '' : '>'; - const normalizedCoords = coordinates.length && isArray(coordinates[0]) ? coordinates : [coordinates]; - // /////////////////////////////////////////////////////////////////////////////////////////////////////// - // Array of LinearRing coordinate array. The first element in the array represents the exterior ring. - // Any subsequent elements represent interior rings (or holes). - // /////////////////////////////////////////////////////////////////////////////////////////////////////// - normalizedCoords.forEach((element) => { - let coords = element.map((coordinate) => { - return coordinate[0] + " " + coordinate[1]; - }); - if (version === "1.0.0") { - gmlPoint += '' + element[0][0] + '' + element[0][1] + ''; - } else { - gmlPoint += '' + coords.join(" ") + ''; - } - }); - - gmlPoint += ''; - return gmlPoint; - }, processOGCCrossLayerFilter: function(crossLayerFilter, nsplaceholderparams) { let ogc = ogcSpatialOperator[crossLayerFilter.operation].startTag; let nsplaceholder = nsplaceholderparams || "ogc"; diff --git a/web/client/utils/__tests__/FilterUtils-test.js b/web/client/utils/__tests__/FilterUtils-test.js index fb230b79f2..9ebea05edf 100644 --- a/web/client/utils/__tests__/FilterUtils-test.js +++ b/web/client/utils/__tests__/FilterUtils-test.js @@ -332,7 +332,7 @@ describe('FilterUtils', () => { "geometry": { "type": "Point", "projection": "EPSG:4326", - "coordinates": [[[1, 1]]] + "coordinates": [1, 1] } } }; @@ -348,7 +348,7 @@ describe('FilterUtils', () => { "geometry": { "type": "Point", "projection": "EPSG:4326", - "coordinates": [[[1, 1]]] + "coordinates": [1, 1] } } }; @@ -380,7 +380,7 @@ describe('FilterUtils', () => { "geometry": { "type": "Point", "projection": "EPSG:4326", - "coordinates": [[[1, 1]]] + "coordinates": [1, 1] } } }; @@ -396,7 +396,7 @@ describe('FilterUtils', () => { "geometry": { "type": "Point", "projection": "EPSG:4326", - "coordinates": [[[1, 1]]] + "coordinates": [1, 1] } } }; @@ -428,7 +428,7 @@ describe('FilterUtils', () => { "geometry": { "type": "Point", "projection": "EPSG:4326", - "coordinates": [[[1, 1]]] + "coordinates": [1, 1] } } }; diff --git a/web/client/utils/ogc/GML/index.js b/web/client/utils/ogc/GML/index.js new file mode 100644 index 0000000000..9056a37b76 --- /dev/null +++ b/web/client/utils/ogc/GML/index.js @@ -0,0 +1,168 @@ +const {isArray} = require('lodash'); +const isGML1 = (version) => version === "1.0.0"; +const closePolygon = (coords) => { + if (coords.length >= 3) { + const first = coords[0]; + const last = coords[coords.length - 1]; + if ((first[0] !== last[0]) || (first[1] !== last[1])) { + return coords.concat([coords[0]]); + } + } + return coords; +}; +const pointElement = (coordinates, srsName, version) => { + let gmlPoint = '' : '>'; + if (gml1) { + gmlPoint += '' + coordinates[0] + '' + coordinates[1] + ''; + } else { + gmlPoint += '' + coordinates.join(" ") + ''; + } + + + gmlPoint += ''; + return gmlPoint; +}; + +const polygonElement = (coordinates, srsName, version) => { + const gml1 = isGML1(version); + let gmlPolygon = '' : '>'; + + // /////////////////////////////////////////////////////////////////////////////////////////////////////// + // Array of LinearRing coordinate array. The first element in the array represents the exterior ring. + // Any subsequent elements represent interior rings (or holes). + // /////////////////////////////////////////////////////////////////////////////////////////////////////// + + const normalizedCoords = coordinates.length && isArray(coordinates[0]) && coordinates[0].length && isArray(coordinates[0][0]) ? coordinates : [coordinates]; + normalizedCoords.forEach((element, index) => { + let coords = closePolygon(element).map((coordinate) => { + return coordinate[0] + (gml1 ? "," : " ") + coordinate[1]; + }); + const exterior = (gml1 ? "outerBoundaryIs" : "exterior"); + const interior = (gml1 ? "innerBoundaryIs" : "exterior"); + gmlPolygon += + (index < 1 ? '' : '') + + '' + + (gml1 ? '' : '') + + coords.join(" ") + + (gml1 ? '' : '') + + '' + + (index < 1 ? '' : ''); + }); + + gmlPolygon += ''; + return gmlPolygon; +}; +const lineStringElement = (coordinates, srsName, version) => { + const gml1 = isGML1(version); + let gml = '' : '>'; + + // /////////////////////////////////////////////////////////////////////////////////////////////////////// + // Array of LinearRing coordinate array. The first element in the array represents the exterior ring. + // Any subsequent elements represent interior rings (or holes). + // /////////////////////////////////////////////////////////////////////////////////////////////////////// + + let coords = coordinates.map((coordinate) => { + return coordinate[0] + (gml1 ? "," : " ") + coordinate[1]; + }); + gml += (gml1 ? '' : '') + + coords.join(" ") + + (gml1 ? '' : ''); + + gml += ''; + return gml; +}; + + +/** + * Processes the geometry in geojson format to provide the GML version of it + * @param {string} version GML version + * @param {object} geometry the geometry in GeoJSON format + * @return {string} the GML version of the Geometry + */ +const processOGCGeometry = (version, geometry) => { + let ogc = ''; + const srsName = geometry.projection || "EPSG:4326"; + switch (geometry.type) { + case "Point": + ogc += pointElement(geometry.coordinates, + srsName, version); + break; + case "MultiPoint": + ogc += ''; + + // ////////////////////////////////////////////////////////////////////////// + // Coordinates of a MultiPoint are an array of positions + // ////////////////////////////////////////////////////////////////////////// + geometry.coordinates.forEach((element) => { + let point = element; + if (point) { + ogc += ""; + ogc += pointElement(point, version); + ogc += ""; + } + }); + + ogc += ''; + break; + case "LineString": + ogc += lineStringElement(geometry.coordinates, + srsName, version); + break; + case "MultiLineString": + const multyLineTagName = version === "2.0" ? "MultiCurve" : "MultiLineString"; + const lineMemberTagName = version === "2.0" ? "curveMember" : "lineStringMember"; + + ogc += ``; + + // ////////////////////////////////////////////////////////////////////////// + // Coordinates of a MultiPolygon are an array of Polygon coordinate arrays + // ////////////////////////////////////////////////////////////////////////// + geometry.coordinates.forEach((element) => { + if (element) { + ogc += ""; + ogc += lineStringElement(element, srsName, version); + ogc += ""; + } + }); + ogc += ''; + break; + case "Polygon": + ogc += polygonElement(geometry.coordinates, + srsName, version); + break; + case "MultiPolygon": + const multyPolygonTagName = version === "2.0" ? "MultiSurface" : "MultiPolygon"; + const polygonMemberTagName = version === "2.0" ? "surfaceMembers" : "polygonMember"; + + ogc += ``; + + // ////////////////////////////////////////////////////////////////////////// + // Coordinates of a MultiPolygon are an array of Polygon coordinate arrays + // ////////////////////////////////////////////////////////////////////////// + geometry.coordinates.forEach((element) => { + let polygon = element; + if (polygon) { + ogc += ""; + ogc += polygonElement(polygon, srsName, version); + ogc += ""; + } + }); + ogc += ''; + break; + default: + break; + } + return ogc; +}; + +module.exports = { + closePolygon, + pointElement, + polygonElement, + lineStringElement, + processOGCGeometry +}; diff --git a/web/client/utils/ogc/WFS/base.js b/web/client/utils/ogc/WFS/base.js new file mode 100644 index 0000000000..248b52abf6 --- /dev/null +++ b/web/client/utils/ogc/WFS/base.js @@ -0,0 +1,73 @@ +/* + * 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 {head, get} = require('lodash'); +const {processOGCGeometry} = require("../GML"); +/** + * Retrives the descriptor for a property in the describeFeatureType (supports single featureTypes) + * @memberof utils.ogc.WFS + * @param {string} propName the name of the property to get + * @param {object} describeFeatureType the describeFeatureType object + * @return {object} the property descriptor + */ +const getPropertyDesciptor = (propName, describeFeatureType) => + head( + get(describeFeatureType, "featureTypes[0].properties").filter(d => d.name === propName) + ); +/** + * @name schemaLocation + * @memberof utils.ogc.WFS + * @param {object} describeFeatureType schemaLocation + * @return {string} url of the schemaLocation + */ +const schemaLocation = (d) => d.targetNamespace; + +/** + * Base utilities for WFS. + * @name WFS + * @memberof utils.ogc + */ +module.exports = { + schemaLocation, + /** + * retrieves the featureTypeSchema entry for XML from describeFeatureType + * @param {object} d describeFeatureType + * @return {string} the attribute. e.g. xmlns:topp="http://example.com/topp" + */ + featureTypeSchema: (d) => `xmlns:${d.targetPrefix}="${schemaLocation(d)}"`, + /** + * Retrieve the value of the feature in GeoJSON to output the Geometry + * @param {object|number|string} value the value + * @param {string} key the attribute name + * @param {object} describeFeatureType the describeFeatureType object + * @return {string} the attribute value or a gml geometry + */ + getValue: (value, key, describeFeatureType, version = "1.1.0") => { + // TODO implement normal attributes; + const isGeometryType = getPropertyDesciptor(key, describeFeatureType).type.indexOf("gml:") === 0; + if (isGeometryType) { + return processOGCGeometry(version, { + type: value.type, + coordinates: value.coordinates + }); + } + return value; + }, + getPropertyDesciptor, + /** + * retrives the featureTypeName from the describeFeatureType json object. + * It prepends the targetPrefix to the first typename found in the featureTypes array. + * Doesn't support multiple feature types + * @param {object} describeFeatureType the json object for describeFeatureType + * @return {string} the prefixed typenName + * @example + * getTypeName({targetPrefix: "topp",featureTypes: [{typeName: "states"}]); // --> topp:states + */ + getTypeName: (dft) => dft.targetPrefix ? dft.targetPrefix + ":" + dft.featureTypes[0].typeName : dft.featureTypes[0].typeName, + processOGCGeometry +}; diff --git a/web/client/utils/ogc/WFS.js b/web/client/utils/ogc/WFS/index.js similarity index 98% rename from web/client/utils/ogc/WFS.js rename to web/client/utils/ogc/WFS/index.js index e794b385b6..506b9fa977 100644 --- a/web/client/utils/ogc/WFS.js +++ b/web/client/utils/ogc/WFS/index.js @@ -39,7 +39,8 @@ const context = new Jsonix.Context([ /*eslint-enable */ const marshaller = context.createMarshaller(); const unmarshaller = context.createUnmarshaller(); -const WFS = {}; +const WFS = { +}; module.exports = {WFS, marshaller, unmarshaller}; diff --git a/web/client/utils/ogc/WFST/index.js b/web/client/utils/ogc/WFST/index.js new file mode 100644 index 0000000000..35afbbc7dd --- /dev/null +++ b/web/client/utils/ogc/WFST/index.js @@ -0,0 +1 @@ +module.exports = require("./v1_1_0"); diff --git a/web/client/utils/ogc/WFST/v1_1_0/index.js b/web/client/utils/ogc/WFST/v1_1_0/index.js new file mode 100644 index 0000000000..a58fe1e69d --- /dev/null +++ b/web/client/utils/ogc/WFST/v1_1_0/index.js @@ -0,0 +1,14 @@ +/* + * 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 {transaction} = require('./transaction'); +const {insert} = require('./insert'); + +module.exports = { + insert, + transaction +}; diff --git a/web/client/utils/ogc/WFST/v1_1_0/insert.js b/web/client/utils/ogc/WFST/v1_1_0/insert.js new file mode 100644 index 0000000000..4dc2db2420 --- /dev/null +++ b/web/client/utils/ogc/WFST/v1_1_0/insert.js @@ -0,0 +1,33 @@ +/* + * 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 getAttributeName = (k, d) => d.targetPrefix ? d.targetPrefix + ":" + k : k; +const {getValue, getTypeName, getPropertyDesciptor} = require("../../WFS/base"); +const attribute = (key, value) => `<${key}>${value}`; +const attributes = (f, describeFeatureType) => + Object.keys(f.properties) + .filter(k => getPropertyDesciptor(k, describeFeatureType)) + .map((key) => attribute(getAttributeName(key, describeFeatureType), getValue(f.properties[key], key, describeFeatureType))); +const geometryAttribute = (f, describeFeatureType) => + attribute(getAttributeName(f.geometry_name, describeFeatureType), getValue(f.geometry, f.geometry_name, describeFeatureType)); + +const feature = (f, describeFeatureType) => `<${getTypeName(describeFeatureType)}>` + + (attributes(f, describeFeatureType) + .concat(geometryAttribute(f, describeFeatureType)) + ).join("") + + ``; +const features = (fs, describeFeatureType) => fs.map(f => feature(f, describeFeatureType)).join(""); + +const insert = (fs, describeFeatureType) => '' + + `${features(fs.features, describeFeatureType)}` + + ''; + +module.exports = { + insert, + feature +}; diff --git a/web/client/utils/ogc/WFST/v1_1_0/transaction.js b/web/client/utils/ogc/WFST/v1_1_0/transaction.js new file mode 100644 index 0000000000..6b767ed6cb --- /dev/null +++ b/web/client/utils/ogc/WFST/v1_1_0/transaction.js @@ -0,0 +1,23 @@ +/* + * 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 transaction = (operations, schemaLocation) => '` + + `${operations.map((o)=> o)}` + + ''; +module.exports = { + transaction +}; diff --git a/web/client/utils/ogc/__tests__/GML-test.js b/web/client/utils/ogc/__tests__/GML-test.js new file mode 100644 index 0000000000..acd3c25e1d --- /dev/null +++ b/web/client/utils/ogc/__tests__/GML-test.js @@ -0,0 +1,108 @@ +/* + * 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 expect = require('expect'); +const {processOGCGeometry} = require('../GML'); +const V1_1_0 = "1.1.0"; +const point = {"type": "Point", "coordinates": [100.0, 0.0] }; +const lineString = { "type": "LineString", + "coordinates": [ [100.0, 0.0], [101.0, 1.0] ] +}; +const polygon = { + "type": "Polygon", + "coordinates": [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]] +}; +const multiPolygon = { + "type": "MultiPolygon", + "coordinates": [ + [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]], + [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], + [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]] + ] +}; +const multiLineString = { "type": "MultiLineString", + "coordinates": [ + [ [100.0, 0.0], [101.0, 1.0] ], + [ [102.0, 2.0], [103.0, 3.0] ] + ] + }; +const EXPECTED_RESULTS = { + V1_1_0: { + point: '100 0', + multiLineString: '' + + '' + + '' + + '100 0 101 1' + + '' + + '' + + '' + + '' + + '102 2 103 3' + + '' + + '' + + '', + lineString: '' + + '100 0 101 1' + + '', + polygon: '' + + '' + + '' + + '102 2 103 2 103 3 102 3 102 2' + + '' + + '' + + '', + multiPolygon: '' + + '' + + '' + + '' + + '' + + '102 2 103 2 103 3 102 3 102 2' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '100 0 101 0 101 1 100 1 100 0' + + '' + + '' + + '' + + '' + + '100.2 0.2 100.8 0.2 100.8 0.8 100.2 0.8 100.2 0.2' + + '' + + '' + + '' + + '' + + '' + } +}; +describe('Test GeoJSON/GML geometry conversion', () => { + it('Point to GML 1.1.0', () => { + const gmlPoint = processOGCGeometry(V1_1_0, point); + expect(gmlPoint).toEqual(EXPECTED_RESULTS.V1_1_0.point); + }); + + it('LineString to GML 1.1.0', () => { + const gmlLineString = processOGCGeometry(V1_1_0, lineString); + expect(gmlLineString).toBe(EXPECTED_RESULTS.V1_1_0.lineString); + }); + it('MultiLineString to GML 1.1.0', () => { + const gmlLineString = processOGCGeometry(V1_1_0, multiLineString); + expect(gmlLineString).toBe(EXPECTED_RESULTS.V1_1_0.multiLineString); + }); + it('Polygon to GML 1.1.0', () => { + const gmlPolygon = processOGCGeometry(V1_1_0, polygon); + expect(gmlPolygon).toBe(EXPECTED_RESULTS.V1_1_0.polygon); + }); + it('MultiPolygon to GML 1.1.0', () => { + const gmlMultiPolygon = processOGCGeometry(V1_1_0, multiPolygon); + expect(gmlMultiPolygon).toBe(EXPECTED_RESULTS.V1_1_0.multiPolygon); + }); +}); diff --git a/web/client/utils/ogc/__tests__/WFS-T-test.js b/web/client/utils/ogc/__tests__/WFS-T-test.js new file mode 100644 index 0000000000..c8bb227883 --- /dev/null +++ b/web/client/utils/ogc/__tests__/WFS-T-test.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 expect = require('expect'); +const {insert, transaction} = require('../WFST'); +const {featureTypeSchema} = require('../WFS/base'); +const describeStates = require('json-loader!../../../test-resources/wfs/describe-states.json'); +const describePois = require('json-loader!../../../test-resources/wfs/describe-pois.json'); +const wyoming = require('json-loader!../../../test-resources/wfs/Wyoming.json'); +const museam = require('json-loader!../../../test-resources/wfs/museam.json'); +const expectedInsertWyoming = require('raw-loader!../../../test-resources/wfst/insert/Wyoming_1_1_0.xml'); +const expectedInsertmuseam = require('raw-loader!../../../test-resources/wfst/insert/museam_1_1_0.xml'); + +describe('Test WFS-T request bodies generation', () => { + it('WFS-T insert', () => { + const result = insert(wyoming, describeStates); + expect(result).toExist(); + }); + it('WFS-T transaction with insert polygon', () => { + const result = transaction([insert(wyoming, describeStates)], featureTypeSchema(describeStates)); + expect(result).toExist(); + expect(result + '\n').toEqual(expectedInsertWyoming); + }); + it('WFS-T transaction with insert multypolygon', () => { + const result = transaction([insert(wyoming, describeStates)], featureTypeSchema(describeStates)); + expect(result).toExist(); + expect(result + '\n').toEqual(expectedInsertWyoming); + }); + it('WFS-T transaction with insert point', () => { + const result = transaction([insert(museam, describePois)], featureTypeSchema(describePois)); + expect(result).toExist(); + expect(result + '\n').toEqual(expectedInsertmuseam); + }); +});