diff --git a/web/client/api/CSW.js b/web/client/api/CSW.js index 613576e78a..a9572c696b 100644 --- a/web/client/api/CSW.js +++ b/web/client/api/CSW.js @@ -110,12 +110,28 @@ const extractWMSParamsFromURL = wms => { return false; }; +// Extract the relevant information from the wfs URL for (RNDT / INSPIRE) +const extractWFSParamsFromURL = wfs => { + const lowerCaseParams = new Map(Array.from(new URLSearchParams(wfs.value)).map(([key, value]) => [key.toLowerCase(), value])); + const layerName = lowerCaseParams.get('typename'); + if (layerName) { + return { + ...wfs, + protocol: 'OGC:WFS', + name: layerName, + value: `${wfs.value.match(/[^\?]+[\?]+/g)}service=WFS` + }; + } + return false; +}; + const toReference = (layerType, data, options) => { if (!data.name) { return null; } switch (layerType) { case 'wms': + case 'wfs': const urlValue = !(data.value.indexOf("http") === 0) ? (options && options.catalogURL || "") + "/" + data.value : data.value; @@ -142,39 +158,69 @@ const toReference = (layerType, data, options) => { }; const REGEX_WMS_EXPLICIT = [/^OGC:WMS-(.*)-http-get-map/g, /^OGC:WMS/g]; +const REGEX_WFS_EXPLICIT = [/^OGC:WFS-(.*)-http-get-(capabilities|feature)/g, /^OGC:WFS/g]; const REGEX_WMS_EXTRACT = /serviceType\/ogc\/wms/g; +const REGEX_WFS_EXTRACT = /serviceType\/ogc\/wfs/g; const REGEX_WMS_ALL = REGEX_WMS_EXPLICIT.concat(REGEX_WMS_EXTRACT); export const getLayerReferenceFromDc = (dc, options, checkEsri = true) => { const URI = dc?.URI && castArray(dc.URI); + const isWMS = isNil(options?.type) || options?.type === "wms"; + const isWFS = options?.type === "wfs"; // look in URI objects for wms and thumbnail if (URI) { - const wms = head(URI.map(uri => { - if (uri.protocol) { - if (REGEX_WMS_EXPLICIT.some(regex => uri.protocol.match(regex))) { + if (isWMS) { + const wms = head(URI.map(uri => { + if (uri.protocol) { + if (REGEX_WMS_EXPLICIT.some(regex => uri.protocol.match(regex))) { /** wms protocol params are explicitly defined as attributes (INSPIRE)*/ - return uri; - } - if (uri.protocol.match(REGEX_WMS_EXTRACT)) { + return uri; + } + if (uri.protocol.match(REGEX_WMS_EXTRACT)) { /** wms protocol params must be extracted from the element text (RNDT / INSPIRE) */ - return extractWMSParamsFromURL(uri); + return extractWMSParamsFromURL(uri); + } } + return false; + }).filter(item => item)); + if (wms) { + return toReference('wms', wms, options); + } + } + if (isWFS) { + const wfs = head(URI.map(uri => { + if (uri.protocol) { + if (REGEX_WFS_EXPLICIT.some(regex => uri.protocol.match(regex))) { + /** wfs protocol params are explicitly defined as attributes (INSPIRE)*/ + return uri; + } + if (uri.protocol.match(REGEX_WFS_EXTRACT)) { + /** wfs protocol params must be extracted from the element text (RNDT / INSPIRE) */ + return extractWFSParamsFromURL(uri); + } + } + return false; + }).filter(item => item)); + if (wfs) { + return toReference('wfs', wfs, options); } - return false; - }).filter(item => item)); - if (wms) { - return toReference('wms', wms, options); } } // look in references objects if (dc?.references?.length) { const refs = castArray(dc.references); const wms = head(refs.filter((ref) => { return ref.scheme && REGEX_WMS_EXPLICIT.some(regex => ref.scheme.match(regex)); })); - if (wms) { + const wfs = head(refs.filter((ref) => { return ref.scheme && REGEX_WFS_EXPLICIT.some(regex => ref.scheme.match(regex)); })); + if (isWMS && wms) { let urlObj = urlUtil.parse(getDefaultUrl(wms.value), true); let layerName = urlObj.query && urlObj.query.layers || dc.alternative; return toReference('wms', { ...wms, value: urlUtil.format(urlObj), name: layerName }, options); } + if (isWFS && wfs) { + let urlObj = urlUtil.parse(getDefaultUrl(wfs.value), true); + let layerName = urlObj.query && urlObj.query.layers || dc.alternative; + return toReference('wfs', { ...wfs, value: urlUtil.format(urlObj), name: layerName }, options); + } if (checkEsri) { // checks for esri arcgis in geonode csw const esri = head(refs.filter((ref) => { diff --git a/web/client/api/__tests__/CSW-test.js b/web/client/api/__tests__/CSW-test.js index 45069874e2..5856bea050 100644 --- a/web/client/api/__tests__/CSW-test.js +++ b/web/client/api/__tests__/CSW-test.js @@ -440,6 +440,34 @@ describe("getLayerReferenceFromDc", () => { expect(layerRef.type).toBe('OGC:WMS'); expect(layerRef.url).toBe('catalog_url/wmsurl?SERVICE=WMS&VERSION=1.3.0'); }); + it("test layer reference with dc.references of scheme OGC:WFS", () => { + const dc = {references: [{value: "http://wmsurl", scheme: 'OGC:WMS'}, {value: "http://wfsurl", scheme: 'OGC:WFS'}], alternative: "some_layer"}; + const layerRef = getLayerReferenceFromDc(dc, {type: "wfs"}); + expect(layerRef.params.name).toBe('some_layer'); + expect(layerRef.type).toBe('OGC:WFS'); + expect(layerRef.url).toBe('http://wfsurl/'); + }); + it("test layer reference with dc.references of scheme OGC:WMS-http-get-capabilities", () => { + const dc = {references: [{value: "http://wmsurl", scheme: 'OGC:WMS-http-get-map'}, {value: "http://wfsurl", scheme: 'OGC:WFS-http-get-capabilities'}], alternative: "some_layer"}; + const layerRef = getLayerReferenceFromDc(dc, {type: "wfs"}); + expect(layerRef.params.name).toBe('some_layer'); + expect(layerRef.type).toBe('OGC:WFS-http-get-capabilities'); + expect(layerRef.url).toBe('http://wfsurl/'); + }); + it("test layer reference with dc.references of scheme OGC:WMS-http-get-feature", () => { + const dc = {references: [{value: "http://wmsurl", scheme: 'OGC:WMS-http-get-map'}, {value: "http://wfsurl", scheme: 'OGC:WFS-http-get-feature'}], alternative: "some_layer"}; + const layerRef = getLayerReferenceFromDc(dc, {type: "wfs"}); + expect(layerRef.params.name).toBe('some_layer'); + expect(layerRef.type).toBe('OGC:WFS-http-get-feature'); + expect(layerRef.url).toBe('http://wfsurl/'); + }); + it("test layer reference with dc.URI of scheme serviceType/ogc/wfs and options", () => { + const dc = {URI: [{value: "wfsurl?service=wfs&typename=some_layer&version=1.3.0", protocol: 'serviceType/ogc/wfs'}, {value: "wmsurl", protocol: 'OGC:WMS'}]}; + const layerRef = getLayerReferenceFromDc(dc, {type: "wfs", catalogURL: "catalog_url"}); + expect(layerRef.params.name).toBe('some_layer'); + expect(layerRef.type).toBe('OGC:WFS'); + expect(layerRef.url).toBe('catalog_url/wfsurl?service=WFS'); + }); it("test layer reference with multiple dc.URI of scheme OGC:WMS", () => { const dc = {URI: [{value: "http://wmsurl", protocol: 'OGC:WMS', name: 'some_layer'}, {value: "wfsurl", protocol: 'OGC:WFS'}]}; const layerRef = getLayerReferenceFromDc(dc); @@ -480,6 +508,15 @@ describe("getLayerReferenceFromDc", () => { expect(getLayerReferenceFromDc(dc).url).toBe(_url[0]); }); + it('getLayerReferenceFromDc for service type WFS', () => { + const _url = [ + 'http://gs-stable.geosolutionsgroup.com:443/geoserver1', + 'http://gs-stable.geosolutionsgroup.com:443/geoserver2', + 'http://gs-stable.geosolutionsgroup.com:443/geoserver3' + ]; + const dc = {references: [{value: 'wmsurl', scheme: 'OGC:WMS'}, {value: _url, scheme: 'OGC:WFS'}], alternative: "some_layer"}; + expect(getLayerReferenceFromDc(dc, {type: "wfs"}).url).toBe(_url[0]); + }); }); diff --git a/web/client/api/catalog/CSW.js b/web/client/api/catalog/CSW.js index 9e00062de2..abc788a567 100644 --- a/web/client/api/catalog/CSW.js +++ b/web/client/api/catalog/CSW.js @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -import { head, isString, includes, castArray, sortBy, uniq } from 'lodash'; +import { head, isString, includes, castArray, sortBy, uniq, isEmpty } from 'lodash'; import { getLayerFromRecord as getLayerFromWMSRecord } from './WMS'; import { getMessageById } from '../../utils/LocaleUtils'; import { transformExtentToObj} from '../../utils/CoordinatesUtils'; @@ -104,12 +104,22 @@ function getThumbnailFromDc(dc, options) { } return thumbURL; } + +/** + * Extract bounding box object from the record + * @param {Object} record from OGC service + */ +function getBoundingBox(record) { + if (isEmpty(record.boundingBox?.crs) || isEmpty(record.boundingBox?.extent)) { + return null; + } + return { + crs: record.boundingBox?.crs, + bounds: transformExtentToObj(record.boundingBox?.extent) + }; +} function getCatalogRecord3DTiles(record, metadata) { const dc = record.dc; - let bbox = { - crs: record.boundingBox.crs, - bounds: transformExtentToObj(record.boundingBox.extent) - }; return { serviceType: '3dtiles', isValid: true, @@ -118,7 +128,7 @@ function getCatalogRecord3DTiles(record, metadata) { identifier: dc && isString(dc.identifier) && dc.identifier || '', url: dc?.URI?.value || "", thumbnail: null, - bbox, + bbox: getBoundingBox(record), format: dc && dc.format || "", references: [], catalogType: 'csw', @@ -136,6 +146,28 @@ const recordToLayer = (record, options) => { return null; } }; +const ADDITIONAL_OGC_SERVICES = ['wfs']; // Add services when support is provided +const getAdditionalOGCService = (record, references, parsedReferences = {}) => { + const hasAdditionalService = ADDITIONAL_OGC_SERVICES.some(serviceType => !isEmpty(parsedReferences[serviceType])); + if (hasAdditionalService) { + return { + additionalOGCServices: { + ...ADDITIONAL_OGC_SERVICES + .map(serviceType => { + const ogcReferences = parsedReferences[serviceType] ?? {}; + const {url, params: {name} = {}} = ogcReferences; + return {[serviceType]: { + url, name, references, ogcReferences, fetchCapabilities: true, + boundingBox: getBoundingBox(record) + }}; + }) + .flat() + .reduce((a, c) => ({...c, ...a}), {}) + } + }; + } + return null; +}; export const preprocess = commonPreprocess; export const validate = commonValidate; @@ -168,9 +200,11 @@ export const getCatalogRecords = (records, options, locales) => { }); } - const layerReference = getLayerReferenceFromDc(dc, options); - if (layerReference) { - references.push(layerReference); + const layerReferences = ['wms', ...ADDITIONAL_OGC_SERVICES].map(serviceType => { + return getLayerReferenceFromDc(dc, {...options, type: serviceType}); + }).filter(ref => ref); + if (!isEmpty(layerReferences)) { + references = references.concat(layerReferences); } // create the references array (now only wms is supported) @@ -229,16 +263,16 @@ export const getCatalogRecords = (records, options, locales) => { ...extractEsriReferences({ references }) }; - const layerType = Object.keys(parsedReferences).find(key => parsedReferences[key]); - const ogcReferences = layerType && layerType !== 'esri' - ? parsedReferences[layerType] - : undefined; let catRecord; if (dc && dc.format === THREE_D_TILES) { catRecord = getCatalogRecord3DTiles(record, metadata); } else if (dc && dc.format === MODEL) { // todo: handle get catalog record for ifc } else { + const layerType = Object.keys(parsedReferences).filter(key => !ADDITIONAL_OGC_SERVICES.includes(key)).find(key => parsedReferences[key]); + const ogcReferences = layerType && layerType !== 'esri' + ? parsedReferences[layerType] + : undefined; catRecord = { serviceType: 'csw', layerType, @@ -253,7 +287,8 @@ export const getCatalogRecords = (records, options, locales) => { tags: dc && dc.tags || '', metadata, capabilities: record.capabilities, - ogcReferences + ogcReferences, + ...getAdditionalOGCService(record, references, parsedReferences) }; } return catRecord; diff --git a/web/client/api/catalog/WFS.js b/web/client/api/catalog/WFS.js index 26f5225860..f436422cc0 100644 --- a/web/client/api/catalog/WFS.js +++ b/web/client/api/catalog/WFS.js @@ -15,7 +15,7 @@ import { testService as commonTestService, preprocess as commonPreprocess } from './common'; -import { get, castArray } from 'lodash'; +import { get, castArray, isEmpty } from 'lodash'; const searchAndPaginate = (json = {}, startPosition, maxRecords, text) => { @@ -123,7 +123,25 @@ export const getCatalogRecords = ({records} = {}) => { return null; }; +/** + * Formulate WFS layer data from record + * and fetch capabilities if needed to add capibilities specific data + * @param {Object} record data obtained from catalog service + * @param {Object} options props specific to wfs + * @returns {Promise} promise that resolves to formulated layer data + */ +const getLayerData = (record, options) => { + const layer = recordToLayer(record, options); + return getRecords(record.url, 1, 1, record.name).then((result)=> { + const [newRecord] = result?.records ?? []; + return isEmpty(newRecord) ? layer : recordToLayer(newRecord, options); + }).catch(() => layer); +}; + export const getLayerFromRecord = (record, options, asPromise) => { + if (options.fetchCapabilities && asPromise) { + return getLayerData(record, options); + } const layer = recordToLayer(record, options); return asPromise ? Promise.resolve(layer) : layer; }; diff --git a/web/client/api/catalog/__tests__/CSW-test.js b/web/client/api/catalog/__tests__/CSW-test.js index 04e7f6482a..0a846d313e 100644 --- a/web/client/api/catalog/__tests__/CSW-test.js +++ b/web/client/api/catalog/__tests__/CSW-test.js @@ -172,6 +172,137 @@ describe('Test correctness of the CSW catalog APIs', () => { expect(records[0].metadata.uri).toEqual(['
']); }); + it('csw with additional OGC services', () => { + const records = getCatalogRecords({ + records: [{ + boundingBox: { + extent: [43.718, 11.348, 43.84, 11.145], + crs: 'EPSG:3003' + }, + dc: { + references: [], + identifier: 'c_d612:sha-identifier', + title: 'title', + type: 'dataset', + subject: [ + 'web', + 'world', + 'sport', + 'transportation' + ], + format: [ + 'ESRI Shapefile', + 'KML' + ], + contributor: 'contributor', + rights: [ + 'otherRestrictions', + 'otherRestrictions' + ], + source: 'source', + relation: { + TYPE_NAME: 'DC_1_1.SimpleLiteral' + }, + URI: [{ + TYPE_NAME: "DC_1_1.URI", + description: "layer1 description", + name: "layer1", + protocol: "OGC:WMS", + value: "https://geoserver/wms?SERVICE=WMS&REQUEST=GetCapabilities" + }, + { + TYPE_NAME: "DC_1_1.URI", + description: "layer2 description", + name: "layer2", + protocol: "OGC:WFS", + value: "https://geoserver/wfs?SERVICE=WFS&REQUEST=GetCapabilities" + }] + } + }] + }, {}); + expect(records.length).toEqual(1); + expect(records[0].boundingBox).toEqual({ extent: [43.718, 11.348, 43.84, 11.145], crs: 'EPSG:3003' }); + expect(records[0].description).toEqual(""); + expect(records[0].identifier).toEqual("c_d612:sha-identifier"); + expect(records[0].thumbnail).toEqual(null); + expect(records[0].title).toEqual('title'); + expect(records[0].tags).toEqual(''); + expect(records[0].metadata.boundingBox).toEqual(['43.718,11.348,43.84,11.145']); + expect(records[0].metadata.contributor).toEqual(['contributor']); + expect(records[0].metadata.format).toEqual(['ESRI Shapefile', 'KML']); + expect(records[0].metadata.identifier).toEqual(['c_d612:sha-identifier']); + expect(records[0].metadata.relation).toEqual([{ TYPE_NAME: 'DC_1_1.SimpleLiteral' }]); + expect(records[0].metadata.rights).toEqual(['otherRestrictions']); + expect(records[0].metadata.references).toEqual(['']); + expect(records[0].metadata.source).toEqual(['source']); + expect(records[0].metadata.subject).toEqual(["