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

geosolutions-it / MapStore2 / 13814213809

12 Mar 2025 02:43PM UTC coverage: 76.97% (-0.005%) from 76.975%
13814213809

Pull #10927

github

web-flow
Merge f33277ed6 into a2e8f16d1
Pull Request #10927: wip for better 3dtiles support via CSW

30977 of 48217 branches covered (64.24%)

7 of 8 new or added lines in 2 files covered. (87.5%)

3 existing lines in 2 files now uncovered.

38491 of 50008 relevant lines covered (76.97%)

35.71 hits per line

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

95.39
/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, isArray } 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

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

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

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

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

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

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

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

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

392
let capabilitiesCache = {};
1✔
393

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

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

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

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

562
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