• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 15422327504

03 Jun 2025 04:08PM UTC coverage: 76.952% (-0.04%) from 76.993%
15422327504

Pull #11024

github

web-flow
Merge 2ddc9a6d7 into 2dbe8dab2
Pull Request #11024: Update User Guide - Upload image on Text Widget

31021 of 48282 branches covered (64.25%)

38629 of 50199 relevant lines covered (76.95%)

36.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.79
/web/client/api/CSW.js
1
/**
2
 * Copyright 2016, GeoSolutions Sas.
3
 * All rights reserved.
4
 *
5
 * This source code is licensed under the BSD-style license found in the
6
 * LICENSE file in the root directory of this source tree.
7
 */
8

9
import urlUtil from 'url';
10

11
import { get, head, last, template, isNil, castArray, isEmpty } from 'lodash';
12
import assign from 'object-assign';
13
import xml2js from 'xml2js';
14
import axios from '../libs/ajax';
15
import { cleanDuplicatedQuestionMarks } from '../utils/ConfigUtils';
16
import { extractCrsFromURN, makeBboxFromOWS, makeNumericEPSG, getExtentFromNormalized } from '../utils/CoordinatesUtils';
17
import WMS from "../api/WMS";
18
import { THREE_D_TILES, getCapabilities } from './ThreeDTiles';
19
import { getDefaultUrl } from '../utils/URLUtils';
20
import { getAuthorizationBasic } from '../utils/SecurityUtils';
21

22
export const parseUrl = (url) => {
1✔
23
    const parsed = urlUtil.parse(getDefaultUrl(url), true);
24✔
24
    return urlUtil.format(assign({}, parsed, { search: null }, {
24✔
25
        query: assign({
26
            service: "CSW",
27
            version: "2.0.2"
28
        }, parsed.query, { request: undefined })
29
    }));
30
};
31

32
const defaultStaticFilter =
33
    `<ogc:Or>
1✔
34
            <ogc:PropertyIsEqualTo>
35
                <ogc:PropertyName>dc:type</ogc:PropertyName>
36
                <ogc:Literal>dataset</ogc:Literal>
37
            </ogc:PropertyIsEqualTo>
38
            <ogc:PropertyIsEqualTo>
39
                <ogc:PropertyName>dc:type</ogc:PropertyName>
40
                <ogc:Literal>http://purl.org/dc/dcmitype/Dataset</ogc:Literal>
41
            </ogc:PropertyIsEqualTo>
42
       </ogc:Or>`;
43

44
const defaultDynamicFilter = "<ogc:PropertyIsLike wildCard='%' singleChar='_' escapeChar='\\'>" +
1✔
45
    "<ogc:PropertyName>csw:AnyText</ogc:PropertyName> " +
46
    "<ogc:Literal>%${searchText}%</ogc:Literal> " +
47
    "</ogc:PropertyIsLike> ";
48
/**
49
 * Create the SortProperty xml definition
50
 * @param {options}
51
 * @param {string} options.name property name to order
52
 * @param {string} options.order order type
53
 * @returns {string} sort by definition in xml format
54
 */
55
const sortByXml = ({ name, order }) => "<ogc:SortBy>" +
1✔
56
"<ogc:SortProperty>" +
57
  `<ogc:PropertyName>${name}</ogc:PropertyName>` +
58
  `<ogc:SortOrder>${order}</ogc:SortOrder>` +
59
"</ogc:SortProperty>" +
60
"</ogc:SortBy>";
61
/**
62
 * Create the GetRecords xml body
63
 * @param {options}
64
 * @param {number} options.startPosition staring index of record in catalog
65
 * @param {number} options.maxRecords maximum number of records returned
66
 * @param {number} options.filterXml a filter definition in xml format
67
 * @param {number} options.sortBy sort by definition in xml format
68
 * @returns {string} get record xml body
69
 */
70
export const cswGetRecordsXml = (options) => '<csw:GetRecords xmlns:csw="http://www.opengis.net/cat/csw/2.0.2" ' +
54✔
71
    'xmlns:ogc="http://www.opengis.net/ogc" ' +
72
    'xmlns:gml="http://www.opengis.net/gml" ' +
73
    'xmlns:dc="http://purl.org/dc/elements/1.1/" ' +
74
    'xmlns:dct="http://purl.org/dc/terms/" ' +
75
    'xmlns:gmd="http://www.isotc211.org/2005/gmd" ' +
76
    'xmlns:gco="http://www.isotc211.org/2005/gco" ' +
77
    'xmlns:gmi="http://www.isotc211.org/2005/gmi" ' +
78
    `xmlns:ows="http://www.opengis.net/ows" service="CSW" version="2.0.2" resultType="results" startPosition="${options.startPosition}" maxRecords="${options.maxRecords}">` +
79
    '<csw:Query typeNames="csw:Record">' +
80
    '<csw:ElementSetName>full</csw:ElementSetName>' +
81
    '<csw:Constraint version="1.1.0">' +
82
    '<ogc:Filter>' +
83
    (options.filterXml || '') +
74✔
84
    '</ogc:Filter>' +
85
    '</csw:Constraint>' +
86
    (options.sortBy || '') +
107✔
87
    '</csw:Query>' +
88
    '</csw:GetRecords>';
89
/**
90
 * Get crs information given a CSW record bounding box
91
 * @param {object} boundingBox CSW bounding box in json format
92
 * @returns {object} { crs, extractedCrs }
93
 */
94
const getCRSFromCSWBoundingBox = (boundingBox) => {
1✔
95
    const crsValue = boundingBox?.$?.crs ?? '';
54!
96
    const urn = crsValue.match(/[\w-]*:[\w-]*:[\w-]*:[\w-]*:[\w-]*:[^:]*:(([\w-]+\s[\w-]+)|[\w-]*)/)?.[0];
54✔
97
    const epsg = makeNumericEPSG(crsValue.match(/EPSG:[0-9]+/)?.[0]);
54✔
98
    const extractedCrs = epsg || (extractCrsFromURN(urn) || last(crsValue.split(':')));
54✔
99
    if (!extractedCrs) {
54!
100
        return { crs: 'EPSG:4326', extractedCrs };
×
101
    }
102
    if (extractedCrs.slice(0, 5) === 'EPSG:') {
54✔
103
        return { crs: makeNumericEPSG(extractedCrs), extractedCrs };
28✔
104
    }
105
    return { crs: makeNumericEPSG(`EPSG:${extractedCrs}`), extractedCrs };
26✔
106
};
107
/**
108
 * Get bounding box information given a CSW record
109
 * @param {object} cswRecord CSW record in json format
110
 * @returns {object} { crs, extent }
111
 */
112
const getBoundingBoxFromCSWRecord = (cswRecord) => {
1✔
113
    if (cswRecord?.['ows:BoundingBox']) {
56✔
114
        const boundingBox = castArray(cswRecord['ows:BoundingBox'])[0];
54✔
115
        const { crs, extractedCrs } = getCRSFromCSWBoundingBox(boundingBox);
54✔
116
        let lc = (boundingBox?.['ows:LowerCorner'] || '-180 -90').split(' ').map(parseFloat);
54!
117
        let uc = (boundingBox?.['ows:UpperCorner'] || '180 90').split(' ').map(parseFloat);
54!
118
        // Usually switched, GeoServer sometimes doesn't.
119
        // See https://docs.geoserver.org/latest/en/user/services/wfs/axis_order.html#axis-ordering
120
        if (crs === 'EPSG:4326' && extractedCrs !== 'CRS84' && extractedCrs !== 'OGC:CRS84') {
54✔
121
            lc = [lc[1], lc[0]];
19✔
122
            uc = [uc[1], uc[0]];
19✔
123
        }
124
        return {
54✔
125
            extent: makeBboxFromOWS(lc, uc),
126
            crs: 'EPSG:4326'
127
        };
128
    }
129
    return null;
2✔
130
};
131
/**
132
 * Get dc properties given a CSW record
133
 * @param {object} cswRecord CSW record in json format
134
 * @returns {object} dc
135
 */
136
const getDCFromCSWRecord = (cswRecord) => {
1✔
137
    // extract each dc or dct tag item in the XML
138
    const dc = Object.keys(cswRecord || {}).reduce((acc, key) => {
59✔
139
        const isDCElement = key.indexOf('dc:') === 0;
584✔
140
        const isDCTElement = key.indexOf('dct:') === 0;
584✔
141
        if (isDCElement || isDCTElement) {
584✔
142
            const name = isDCElement ? key.replace('dc:', '') :  key.replace('dct:', '');
465✔
143
            let value = cswRecord[key];
465✔
144
            if (name === 'references') {
465✔
145
                value = castArray(cswRecord[key]).map((reference) => {
13✔
146
                    return {
13✔
147
                        value: cleanDuplicatedQuestionMarks(reference?._),
148
                        scheme: reference?.$?.scheme
149
                    };
150
                });
151
            }
152
            if (name === 'URI') {
465✔
153
                value = castArray(cswRecord[key]).map((uri) => {
42✔
154
                    return {
81✔
155
                        description: uri?.$?.description,
156
                        name: uri?.$?.name,
157
                        protocol: uri?.$?.protocol,
158
                        value: uri?._
159
                    };
160
                });
161
            }
162
            return {
465✔
163
                ...acc,
164
                [name]: value
165
            };
166
        }
167
        return acc;
119✔
168
    }, {});
169
    return isEmpty(dc) ? null : dc;
59✔
170
};
171
/**
172
 * Get error message given the parsed response from a CSW request
173
 * @param {object} cswResponse CSW response in json format
174
 * @returns {string} error message
175
 */
176
const getCSWError = (cswResponse) => {
1✔
177
    const exceptionReport = cswResponse?.['ows:ExceptionReport'];
4✔
178
    if (exceptionReport) {
4!
179
        const exceptionText = exceptionReport?.['ows:Exception']?.['ows:ExceptionText'];
4✔
180
        return exceptionText || 'GenericError';
4!
181
    }
182
    return '';
×
183
};
184
/**
185
 * Parse a CSW axios response
186
 * @param {object} response axios response
187
 * @returns {object} it could return the parsed result, an error or null
188
 */
189
const parseCSWResponse = (response) => {
1✔
190
    if (!response) {
21!
191
        return null;
×
192
    }
193
    let json;
194
    xml2js.parseString(response.data, { explicitArray: false }, (ignore, result) => {
21✔
195
        json = result;
21✔
196
    });
197
    const searchResults = json?.['csw:GetRecordsResponse']?.['csw:SearchResults'];
21✔
198
    if (searchResults) {
21✔
199
        const cswRecords = searchResults?.['csw:Record'] || searchResults?.['gmd:MD_Metadata'];
18✔
200
        const records = !cswRecords ? null : castArray(cswRecords).map((cswRecord) => {
18!
201
            const boundingBox = getBoundingBoxFromCSWRecord(cswRecord);
56✔
202
            const dc = getDCFromCSWRecord(cswRecord);
56✔
203
            return {
56✔
204
                dateStamp: cswRecord?.['gmd:dateStamp']?.['gco:Date'],
205
                fileIdentifier: cswRecord?.['gmd:fileIdentifier']?.['gco:CharacterString'],
206
                identificationInfo: cswRecord?.['gmd:identificationInfo']?.['gmd:AbstractMD_Identification'],
207
                ...(boundingBox && { boundingBox }),
110✔
208
                ...(dc && { dc })
110✔
209
            };
210
        });
211
        const _dcRef = records ? records.map(({ dc = {} }) => {
18!
212
            const URIs = castArray(dc?.references?.length > 0 ? dc.references : dc.URI);
56✔
213
            return URIs;
56✔
214
        }).flat() : undefined;
215
        return {
18✔
216
            result: {
217
                numberOfRecordsMatched: parseFloat(searchResults?.$?.numberOfRecordsMatched),
218
                numberOfRecordsReturned: parseFloat(searchResults?.$?.numberOfRecordsReturned),
219
                nextRecord: parseFloat(searchResults?.$?.nextRecord),
220
                ...(records && { records })
36✔
221
            },
222
            _dcRef
223
        };
224
    }
225
    const error = getCSWError(json);
3✔
226
    if (error) {
3!
227
        return { error };
3✔
228
    }
229
    return null;
×
230
};
231
/**
232
 * Construct XML body to get records from the CSW service
233
 * @param {object} [options] the options to pass to withIntersectionObserver enhancer.
234
 * @param {number} startPosition
235
 * @param {number} maxRecords
236
 * @param {string} searchText
237
 * @param {object} filter object holds static and dynamic filter configured for the CSW service
238
 * @param {object} filter.staticFilter filter to fetch all record applied always i.e even when no search text is present
239
 * @param {object} filter.dynamicFilter filter when search text is present and is applied in conjunction with static filter
240
 * @return {string} constructed xml string
241
 */
242
export const constructXMLBody = (startPosition, maxRecords, searchText, { options: { service } = {} } = {}) => {
1✔
243
    const { filter, sortBy: sortObj } = service ?? {};
30✔
244
    const staticFilter = filter?.staticFilter || defaultStaticFilter;
30✔
245
    const dynamicFilter = `<ogc:And>
30✔
246
        ${template(filter?.dynamicFilter || defaultDynamicFilter)({ searchText })}
59✔
247
        ${staticFilter}
248
    </ogc:And>`;
249
    const sortExp = sortObj?.name ? sortByXml({ name: sortObj?.name, order: sortObj?.order ?? "ASC"}) : '';
30!
250
    return cswGetRecordsXml({ filterXml: !searchText ? staticFilter : dynamicFilter, startPosition, maxRecords, sortBy: sortExp});
30✔
251
};
252

253
// Extract the relevant information from the wms URL for (RNDT / INSPIRE)
254
const extractWMSParamsFromURL = wms => {
1✔
255
    const lowerCaseParams = new Map(Array.from(new URLSearchParams(wms.value)).map(([key, value]) => [key.toLowerCase(), value]));
9✔
256
    const layerName = lowerCaseParams.get('layers');
3✔
257
    const wmsVersion = lowerCaseParams.get('version');
3✔
258
    if (layerName) {
3✔
259
        return {
2✔
260
            ...wms,
261
            protocol: 'OGC:WMS',
262
            name: layerName,
263
            value: `${wms.value.match(/[^\?]+[\?]+/g)}SERVICE=WMS${wmsVersion && `&VERSION=${wmsVersion}`}`
4✔
264
        };
265
    }
266
    return false;
1✔
267
};
268

269
// Extract the relevant information from the wfs URL for (RNDT / INSPIRE)
270
const extractWFSParamsFromURL = wfs => {
1✔
271
    const lowerCaseParams = new Map(Array.from(new URLSearchParams(wfs.value)).map(([key, value]) => [key.toLowerCase(), value]));
3✔
272
    const layerName = lowerCaseParams.get('typename');
1✔
273
    if (layerName) {
1!
274
        return {
1✔
275
            ...wfs,
276
            protocol: 'OGC:WFS',
277
            name: layerName,
278
            value: `${wfs.value.match(/[^\?]+[\?]+/g)}service=WFS`
279
        };
280
    }
281
    return false;
×
282
};
283

284
const toReference = (layerType, data, options) => {
1✔
285
    if (!data.name) {
45✔
286
        return null;
8✔
287
    }
288
    switch (layerType) {
37!
289
    case 'wms':
290
    case 'wfs':
291
        const urlValue = !(data.value.indexOf("http") === 0)
34✔
292
            ? (options && options.catalogURL || "") + "/" + data.value
21✔
293
            : data.value;
294
        return {
34✔
295
            type: data.protocol || data.scheme,
46✔
296
            url: urlValue,
297
            SRS: [],
298
            params: {
299
                name: data.name
300
            }
301
        };
302
    case 'arcgis':
303
        return {
3✔
304
            type: 'arcgis',
305
            url: data.value,
306
            SRS: [],
307
            params: {
308
                name: data.name
309
            }
310
        };
311
    default:
312
        return null;
×
313
    }
314
};
315

316
const REGEX_WMS_EXPLICIT = [/^OGC:WMS-(.*)-http-get-map/g, /^OGC:WMS/g];
1✔
317
const REGEX_WFS_EXPLICIT = [/^OGC:WFS-(.*)-http-get-(capabilities|feature)/g, /^OGC:WFS/g];
1✔
318
const REGEX_WMS_EXTRACT = /serviceType\/ogc\/wms/g;
1✔
319
const REGEX_WFS_EXTRACT = /serviceType\/ogc\/wfs/g;
1✔
320
const REGEX_WMS_ALL = REGEX_WMS_EXPLICIT.concat(REGEX_WMS_EXTRACT);
1✔
321

322
export const getLayerReferenceFromDc = (dc, options, checkEsri = true) => {
1✔
323
    const URI = dc?.URI && castArray(dc.URI);
92✔
324
    const isWMS = isNil(options?.type) || options?.type === "wms";
92✔
325
    const isWFS = options?.type === "wfs";
92✔
326
    // look in URI objects for wms and thumbnail
327
    if (URI) {
92✔
328
        if (isWMS) {
51✔
329
            const wms = head(URI.map(uri => {
32✔
330
                if (uri.protocol) {
53✔
331
                    if (REGEX_WMS_EXPLICIT.some(regex => uri.protocol.match(regex))) {
79✔
332
                    /** wms protocol params are explicitly defined as attributes (INSPIRE)*/
333
                        return uri;
18✔
334
                    }
335
                    if (uri.protocol.match(REGEX_WMS_EXTRACT)) {
26✔
336
                    /** wms protocol params must be extracted from the element text (RNDT / INSPIRE) */
337
                        return extractWMSParamsFromURL(uri);
3✔
338
                    }
339
                }
340
                return false;
32✔
341
            }).filter(item => item));
53✔
342
            if (wms) {
32✔
343
                return toReference('wms', wms, options);
20✔
344
            }
345
        }
346
        if (isWFS) {
31✔
347
            const wfs = head(URI.map(uri => {
19✔
348
                if (uri.protocol) {
28✔
349
                    if (REGEX_WFS_EXPLICIT.some(regex => uri.protocol.match(regex))) {
38✔
350
                    /** wfs protocol params are explicitly defined as attributes (INSPIRE)*/
351
                        return uri;
1✔
352
                    }
353
                    if (uri.protocol.match(REGEX_WFS_EXTRACT)) {
18✔
354
                    /** wfs protocol params must be extracted from the element text (RNDT / INSPIRE) */
355
                        return extractWFSParamsFromURL(uri);
1✔
356
                    }
357
                }
358
                return false;
26✔
359
            }).filter(item => item));
28✔
360
            if (wfs) {
19✔
361
                return toReference('wfs', wfs, options);
2✔
362
            }
363
        }
364
    }
365
    // look in references objects
366
    if (dc?.references?.length) {
70✔
367
        const refs = castArray(dc.references);
37✔
368
        const wms = head(refs.filter((ref) => { return ref.scheme && REGEX_WMS_EXPLICIT.some(regex => ref.scheme.match(regex)); }));
108✔
369
        const wfs = head(refs.filter((ref) => { return ref.scheme && REGEX_WFS_EXPLICIT.some(regex => ref.scheme.match(regex)); }));
124✔
370
        if (isWMS && wms) {
37✔
371
            let urlObj = urlUtil.parse(getDefaultUrl(wms.value), true);
16✔
372
            let layerName = urlObj.query && urlObj.query.layers || dc.alternative;
16✔
373
            return toReference('wms', { ...wms, value: urlUtil.format(urlObj), name: layerName }, options);
16✔
374
        }
375
        if (isWFS && wfs) {
21✔
376
            let urlObj = urlUtil.parse(getDefaultUrl(wfs.value), true);
4✔
377
            let layerName = urlObj.query && urlObj.query.layers || dc.alternative;
4✔
378
            return toReference('wfs', { ...wfs, value: urlUtil.format(urlObj), name: layerName }, options);
4✔
379
        }
380
        if (checkEsri) {
17!
381
            // checks for esri arcgis in geonode csw
382
            const esri = head(refs.filter((ref) => {
17✔
383
                return ref.scheme && ref.scheme === "WWW:DOWNLOAD-REST_MAP";
27✔
384
            }));
385
            if (esri) {
17✔
386
                return toReference('arcgis', { ...esri, name: dc.alternative }, options);
3✔
387
            }
388
        }
389
    }
390
    return null;
47✔
391
};
392

393
let capabilitiesCache = {};
1✔
394

395
/**
396
 * Add capabilities data to CSW records
397
 * if corresponding capability flag is enabled and WMS url is found
398
 * Currently limited to only scale denominators (visibility limits)
399
 * @param {object[]} _dcRef dc.reference or dc.URI
400
 * @param {object} result csw results object
401
 * @param {object} options csw service options
402
 * @return {object} csw records
403
 */
404
const addCapabilitiesToRecords = (_dcRef, result, options) => {
1✔
405
    // Currently, visibility limits is the only capability info added to the records
406
    // hence `autoSetVisibilityLimits` flag is used to determine if `getCapabilities` is required
407
    // This should be modified when additional capability info is required
408
    const invokeCapabilities = get(options, "options.service.autoSetVisibilityLimits", false);
18✔
409

410
    if (!invokeCapabilities) {
18✔
411
        return result;
14✔
412
    }
413
    const { value: _url } = _dcRef?.find(t =>
4✔
414
        REGEX_WMS_ALL.some(regex => t?.scheme?.match(regex) || t?.protocol?.match(regex))) || {}; // Get WMS URL from references
28✔
415
    const [parsedUrl] = _url && _url.split('?') || [];
4✔
416
    if (!parsedUrl) return { ...result }; // Return record when no url found
4✔
417

418
    const cached = capabilitiesCache[parsedUrl];
3✔
419
    const isCached = !isEmpty(cached);
3✔
420
    return Promise.resolve(
3✔
421
        isCached
3✔
422
            ? cached
423
            : WMS.getCapabilities(parsedUrl + '?version=')
424
                .then((caps) => WMS.flatLayers(caps.Capability))
1✔
425
                .catch(() => []))
×
426
        .then((layers) => {
427
            if (!isCached) {
3✔
428
                capabilitiesCache[parsedUrl] = layers;
1✔
429
            }
430
            // Add visibility limits scale data of the layer to the record
431
            return {
3✔
432
                ...result,
433
                records: result?.records?.map(record => {
434
                    const name = get(getLayerReferenceFromDc(record?.dc, null, false), 'params.name', '');
12✔
435
                    const {
436
                        MinScaleDenominator,
437
                        MaxScaleDenominator
438
                    } = layers.find(l => l.Name === name) || {};
17✔
439
                    return {
12✔
440
                        ...record,
441
                        ...((!isNil(MinScaleDenominator) || !isNil(MaxScaleDenominator))
24✔
442
                            && { capabilities: { MaxScaleDenominator, MinScaleDenominator } })
443
                    };
444
                })
445
            };
446
        });
447
};
448
/**
449
 * handle getting bbox from capabilities in case of 3D tile layer for CSW records
450
 * @param {object} result csw results object
451
 * @return {object} csw records
452
 */
453
const getBboxFor3DLayersToRecords = async(result)=> {
1✔
454
    if (!result) return result;
21!
455
    let { records } = result;
21✔
456
    if (records?.length) {
21✔
457
        let records3DPromisesForCapabilities = records.map((rec)=>{
18✔
458
            if (rec?.dc?.format === THREE_D_TILES) {
56✔
459
                let tilesetJsonURL = rec.dc?.URI?.value;
3✔
460
                return getCapabilities(tilesetJsonURL);
3✔
461
            }
462
            return Promise.resolve(null);
53✔
463
        });
464
        let allPromises = await Promise.all(records3DPromisesForCapabilities);
18✔
465

466
        const newRecords = records.map(
18✔
467
            (record, idx) => {
468
                const capabilityResult = allPromises[idx];
56✔
469
                if (!capabilityResult || !capabilityResult?.bbox?.bounds || !capabilityResult?.bbox?.crs) {
56✔
470
                    return record;
53✔
471
                }
472
                let bbox = getExtentFromNormalized(capabilityResult.bbox.bounds, capabilityResult.bbox.crs);
3✔
473
                return {
3✔
474
                    ...record,
475
                    boundingBox: {
476
                        extent: bbox.extent,
477
                        crs: capabilityResult.bbox.crs
478
                    }
479
                };
480
            });
481
        return {
18✔
482
            ...result,
483
            records: newRecords
484
        };
485
    }
486
    return result;
3✔
487
};
488
/**
489
 * API for local config
490
 */
491
const Api = {
1✔
492
    parseUrl,
493
    getRecordById: function(catalogURL) {
494
        return axios.get(catalogURL)
3✔
495
            .then((response) => {
496
                if (response) {
3!
497
                    let json;
498
                    xml2js.parseString(response.data, { explicitArray: false }, (ignore, result) => {
3✔
499
                        json = result;
3✔
500
                    });
501
                    const record = json?.['csw:GetRecordByIdResponse']?.['csw:Record'];
3✔
502
                    const dc = getDCFromCSWRecord(record);
3✔
503
                    if (dc) {
3✔
504
                        return { dc };
2✔
505
                    }
506
                    const error = getCSWError(json);
1✔
507
                    if (error) {
1!
508
                        return { error };
1✔
509
                    }
510
                }
511
                return null;
×
512
            });
513
    },
514
    getRecords: function(url, startPosition, maxRecords, text, options) {
515
        const body = constructXMLBody(startPosition, maxRecords, text, options);
23✔
516
        const protectedId = options?.options?.service?.protectedId;
23✔
517
        let headers = getAuthorizationBasic(protectedId);
23✔
518
        return axios.post(parseUrl(url), body, {
23✔
519
            headers: {
520
                'Content-Type': 'application/xml',
521
                ...headers
522
            }
523
        }).then((response) => {
524
            const { error, _dcRef, result } = parseCSWResponse(response) || {};
21!
525
            if (result) {
21✔
526
                return addCapabilitiesToRecords(_dcRef, result, options);
18✔
527
            }
528
            if (error) {
3!
529
                return { error };
3✔
530
            }
531
            return null;
×
532
        }).then(results => getBboxFor3DLayersToRecords(results)); // handle getting bbox from capabilities in case of 3D tile layer
21✔
533
    },
534
    textSearch: function(url, startPosition, maxRecords, text, options) {
535
        return new Promise((resolve) => {
8✔
536
            resolve(Api.getRecords(url, startPosition, maxRecords, text, options));
8✔
537
        });
538
    },
539
    workspaceSearch: function(url, startPosition, maxRecords, text, workspace) {
540
        const workspaceTerm = workspace || "%";
1!
541
        const layerNameTerm = text && "%" + text + "%" || "%";
1!
542
        return Api.getRecords(url, startPosition, maxRecords, '', {
1✔
543
            options: {
544
                service: {
545
                    filter: {
546
                        staticFilter: "<ogc:PropertyIsLike wildCard=\"%\" singleChar=\"_\" escapeChar=\"\\\\\">" +
547
                            "<ogc:PropertyName>dc:identifier</ogc:PropertyName>" +
548
                            `<ogc:Literal>${workspaceTerm + ":" + layerNameTerm}</ogc:Literal>` +
549
                        "</ogc:PropertyIsLike>"
550
                    }
551
                }
552
            }
553
        });
554
    },
555
    reset: () => { }
556
};
557

558
export default Api;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc