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

geosolutions-it / MapStore2 / 15422118818

03 Jun 2025 03:58PM UTC coverage: 76.946% (-0.05%) from 76.993%
15422118818

Pull #11030

github

web-flow
Merge a82ef5c64 into f38b5e770
Pull Request #11030: Improve GeoServer user integration doc

31016 of 48282 branches covered (64.24%)

38626 of 50199 relevant lines covered (76.95%)

36.03 hits per line

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

95.0
/web/client/api/WMS.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
import { isArray, castArray, get } from 'lodash';
11
import xml2js from 'xml2js';
12
import axios from '../libs/ajax';
13
import { getConfigProp } from '../utils/ConfigUtils';
14
import { getWMSBoundingBox } from '../utils/CoordinatesUtils';
15
import { isValidGetMapFormat, isValidGetFeatureInfoFormat } from '../utils/WMSUtils';
16
import { getAuthorizationBasic } from '../utils/SecurityUtils';
17
const capabilitiesCache = {};
1✔
18

19
export const WMS_GET_CAPABILITIES_VERSION = '1.3.0';
1✔
20
// The describe layer request is used to detect the OWS type, WFS or WCS type (eg: get additional information for the styling)
21
// The default version is 1.1.1 because GeoServer is not fully supporting the version 1.3.0
22
export const WMS_DESCRIBE_LAYER_VERSION = '1.1.1';
1✔
23

24
export const parseUrl = (
1✔
25
    urls,
26
    query = {
2✔
27
        service: "WMS",
28
        version: WMS_GET_CAPABILITIES_VERSION,
29
        request: "GetCapabilities"
30
    },
31
    options
32
) => {
33
    const url = (isArray(urls) && urls || urls.split(','))[0];
44✔
34
    const parsed = urlUtil.parse(url, true);
44✔
35
    return urlUtil.format({
44✔
36
        ...parsed,
37
        search: null,
38
        query: {
39
            ...query,
40
            ...parsed.query,
41
            ...(options?.query || {})
87✔
42
        }
43
    });
44
};
45

46
/**
47
 * Extract `credits` property from layer's Attribution
48
 * (Web Map Service Implementation Specification OGC 01-068r3 - 7.1.4.5.9 )
49
 * @param {object} attribution Attribution object of WMS Capabilities (parsed with xml2js default format)
50
 * @return {object} an object to place in `credits` attribute of layer with the structure in the example.
51
 * @example
52
 * {
53
 *     title: "content of <Title></Title>",
54
 *     imageUrl: "url of the image linked as LogoURL",
55
 *     link: "http://some.site.of.reference",
56
 *     logo: { // more info about the logo
57
 *         format: "image/png",
58
 *         width: "200",
59
 *         height: "100"
60
 *     }
61
 * }
62
 *
63
 */
64
export const extractCredits = attribution => {
1✔
65
    const title = attribution && attribution.Title;
3✔
66
    const logo = attribution.LogoURL && {
3✔
67
        ...(get(attribution, 'LogoURL.$') || {}),
3!
68
        format: get(attribution, 'LogoURL.Format') // e.g. image/png
69
    };
70
    const link = get(attribution, 'OnlineResource.$["xlink:href"]');
3✔
71
    return {
3✔
72
        title,
73
        logo,
74
        imageUrl: get(attribution, 'LogoURL.OnlineResource.$["xlink:href"]'),
75
        link
76
    };
77
};
78

79
export const flatLayers = (root) => {
1✔
80
    const rootLayer = root?.Layer ?? root?.layer;
212✔
81
    const rootName = root?.Name ?? root?.name;
212✔
82
    return rootLayer
212✔
83
        ? (castArray(rootLayer))
84
            .reduce((acc, current) => {
85
                const currentLayer = current.Layer ?? current.layer;
186✔
86
                const currentName = current.Name ?? current.name;
186✔
87
                return [
186✔
88
                    ...acc,
89
                    ...flatLayers(current),
90
                    ...(currentLayer && currentName ? [current] : [])
419✔
91
                ];
92
            }, [])
93
        : rootName && [root] || [];
284✔
94
};
95

96
const getFormats = (response) => {
1✔
97
    const root = response.Capability;
9✔
98
    const imageFormats = castArray(root?.Request?.GetMap?.Format || []).filter(isValidGetMapFormat);
9✔
99
    const infoFormats = castArray(root?.Request?.GetFeatureInfo?.Format || []).filter(isValidGetFeatureInfoFormat);
9✔
100
    return { imageFormats, infoFormats };
9✔
101
};
102

103
export const getOnlineResource = (c) => {
1✔
104
    return c.Request && c.Request.GetMap && c.Request.GetMap.DCPType && c.Request.GetMap.DCPType.HTTP && c.Request.GetMap.DCPType.HTTP.Get && c.Request.GetMap.DCPType.HTTP.Get.OnlineResource && c.Request.GetMap.DCPType.HTTP.Get.OnlineResource.$ || undefined;
8!
105
};
106
export const searchAndPaginate = (json = {}, startPosition, maxRecords, text) => {
1!
107
    const root = json.Capability;
8✔
108
    const service = json.Service;
8✔
109
    const onlineResource = getOnlineResource(root);
8✔
110
    const SRSList = root.Layer && castArray(root.Layer.SRS || root.Layer.CRS)?.map((crs) => crs.toUpperCase()) || [];
44!
111
    const credits = root.Layer && root.Layer.Attribution && extractCredits(root.Layer.Attribution);
8✔
112
    const getMapFormats = castArray(root?.Request?.GetMap?.Format || []);
8!
113
    const getFeatureInfoFormats = castArray(root?.Request?.GetFeatureInfo?.Format || []);
8!
114
    const layersObj = flatLayers(root);
8✔
115
    const layers = isArray(layersObj) ? layersObj : [layersObj];
8!
116
    const filteredLayers = layers
8✔
117
        .filter((layer) => !text || layer.Name.toLowerCase().indexOf(text.toLowerCase()) !== -1 || layer.Title && layer.Title.toLowerCase().indexOf(text.toLowerCase()) !== -1 || layer.Abstract && layer.Abstract.toLowerCase().indexOf(text.toLowerCase()) !== -1);
31✔
118
    return {
8✔
119
        numberOfRecordsMatched: filteredLayers.length,
120
        numberOfRecordsReturned: Math.min(maxRecords, filteredLayers.length),
121
        nextRecord: startPosition + Math.min(maxRecords, filteredLayers.length) + 1,
122
        service,
123
        layerOptions: {
124
            version: json?.$?.version || WMS_GET_CAPABILITIES_VERSION
8!
125
        },
126
        records: filteredLayers
127
            .filter((layer, index) => index >= startPosition - 1 && index < startPosition - 1 + maxRecords)
22✔
128
            .map((layer) => ({
7✔
129
                ...layer,
130
                getMapFormats,
131
                getFeatureInfoFormats,
132
                onlineResource,
133
                SRS: SRSList,
134
                credits: layer.Attribution ? extractCredits(layer.Attribution) : credits
7!
135
            }))
136
    };
137
};
138

139
export const getDimensions = (layer) => {
1✔
140
    return castArray(layer.Dimension || layer.dimension || []).map((dim, index) => {
1✔
141
        const extent = (layer.Extent && castArray(layer.Extent)[index] || layer.extent && castArray(layer.extent)[index]);
×
142
        return {
×
143
            name: dim.$.name,
144
            units: dim.$.units,
145
            unitSymbol: dim.$.unitSymbol,
146
            "default": dim.$.default || (extent && extent.$.default),
×
147
            values: dim._ && dim._.split(',') || extent && extent._ && extent._.split(',')
×
148
        };
149
    });
150
};
151
/**
152
 * Get the WMS capabilities given a valid url endpoint
153
 * @param {string} url WMS endpoint
154
 * @return {object} root object of the capabilities
155
 * - `$`: object with additional information of the capability (eg: $.version)
156
 * - `Capability`: capability object that contains layers and requests formats
157
 * - `Service`: service information object
158
 */
159
export const getCapabilities = (url, headers = {}) => {
1✔
160
    return axios.get(parseUrl(url, {
32✔
161
        service: "WMS",
162
        version: WMS_GET_CAPABILITIES_VERSION,
163
        request: "GetCapabilities"
164
    }), {headers}).then((response) => {
165
        let json;
166
        xml2js.parseString(response.data, {explicitArray: false}, (ignore, result) => {
29✔
167
            json = result;
29✔
168
        });
169
        return (json.WMS_Capabilities || json.WMT_MS_Capabilities || {});
29✔
170
    });
171
};
172

173
export const describeLayer = (url, layer, options = {}) => {
1✔
174
    return axios.get(parseUrl(url, {
5✔
175
        service: "WMS",
176
        version: WMS_DESCRIBE_LAYER_VERSION,
177
        layers: layer,
178
        request: "DescribeLayer"
179
    }, options)).then((response) => {
180
        let json;
181
        xml2js.parseString(response.data, {explicitArray: false}, (ignore, result) => {
5✔
182
            json = result;
5✔
183
        });
184
        const layerDescription = json?.WMS_DescribeLayerResponse?.LayerDescription && castArray(json.WMS_DescribeLayerResponse.LayerDescription)[0];
5✔
185
        return layerDescription && {
5✔
186
            ...layerDescription?.$,
187
            ...(layerDescription?.Query && {
2✔
188
                query: castArray(layerDescription.Query).map(query => ({
1✔
189
                    ...query?.$
190
                }))
191
            })
192
        };
193
    });
194
};
195
export const getRecords = (url, startPosition, maxRecords, text, options) => {
1✔
196
    const cached = capabilitiesCache[url];
8✔
197
    if (cached && new Date().getTime() < cached.timestamp + (getConfigProp('cacheExpire') || 60) * 1000) {
8✔
198
        return new Promise((resolve) => {
1✔
199
            resolve(searchAndPaginate(cached.data, startPosition, maxRecords, text));
1✔
200
        });
201
    }
202
    const protectedId = options?.options?.service?.protectedId;
7✔
203
    let headers = getAuthorizationBasic(protectedId);
7✔
204
    return getCapabilities(url, headers)
7✔
205
        .then((json) => {
206
            capabilitiesCache[url] = {
7✔
207
                timestamp: new Date().getTime(),
208
                data: json
209
            };
210
            return searchAndPaginate(json, startPosition, maxRecords, text);
7✔
211
        });
212
};
213
export const describeLayers = (url, layers) => {
1✔
214
    return axios.get(parseUrl(url, {
5✔
215
        service: "WMS",
216
        version: WMS_DESCRIBE_LAYER_VERSION,
217
        layers: layers,
218
        request: "DescribeLayer"
219
    })).then((response) => {
220
        let descriptions;
221
        xml2js.parseString(response.data, {explicitArray: false}, (ignore, result) => {
4✔
222
            descriptions = result && result.WMS_DescribeLayerResponse && result.WMS_DescribeLayerResponse.LayerDescription;
4✔
223
        });
224
        descriptions = Array.isArray(descriptions) ? descriptions : [descriptions];
4!
225
        // make it compatible with json format of describe layer
226
        return descriptions.map(desc => ({
8✔
227
            ...(desc && desc.$ || {}),
16!
228
            layerName: desc && desc.$ && desc.$.name,
24✔
229
            query: {
230
                ...(desc && desc.query && desc.query.$ || {})
24!
231
            }
232
        }));
233
    });
234
};
235
export const textSearch = (url, startPosition, maxRecords, text, options) => {
1✔
236
    return getRecords(url, startPosition, maxRecords, text, options);
3✔
237
};
238
export const parseLayerCapabilities = (json, layer) => {
1✔
239
    const root = json.Capability;
10✔
240
    const layersCapabilities = flatLayers(root);
10✔
241
    const capabilities = layersCapabilities.find((layerCapability) => {
10✔
242
        const capabilityName = layerCapability.Name;
16✔
243
        if (layer.name.split(":").length === 2 && capabilityName && capabilityName.split(":").length === 2) {
16✔
244
            return layer.name === capabilityName && layerCapability;
3✔
245
        }
246
        if (capabilityName && capabilityName.split(":").length === 2) {
13!
247
            return (layer.name === capabilityName.split(":")[1]) && layerCapability;
×
248
        }
249
        if (layer.name.split(":").length === 2) {
13✔
250
            return layer.name.split(":")[1] === capabilityName && layerCapability;
10✔
251
        }
252
        return layer.name === capabilityName && layerCapability;
3✔
253
    });
254
    if (capabilities) {
10✔
255
        const { imageFormats, infoFormats } = getFormats(json);
6✔
256
        return {
6✔
257
            ...capabilities,
258
            layerOptions: {
259
                imageFormats,
260
                infoFormats
261
            }
262
        };
263
    }
264
    return null;
4✔
265
};
266
export const getBBox = (record, bounds) => {
1✔
267
    let layer = record;
19✔
268
    let bbox = (layer.EX_GeographicBoundingBox || layer.exGeographicBoundingBox || getWMSBoundingBox(layer.BoundingBox) || (layer.LatLonBoundingBox && layer.LatLonBoundingBox.$) || layer.latLonBoundingBox);
19✔
269
    while (!bbox && layer.Layer && layer.Layer.length) {
19!
270
        layer = layer.Layer[0];
×
271
        bbox = (layer.EX_GeographicBoundingBox || layer.exGeographicBoundingBox || getWMSBoundingBox(layer.BoundingBox) || (layer.LatLonBoundingBox && layer.LatLonBoundingBox.$) || layer.latLonBoundingBox);
×
272
    }
273
    if (!bbox) {
19✔
274
        bbox = {
14✔
275
            westBoundLongitude: -180.0,
276
            southBoundLatitude: -90.0,
277
            eastBoundLongitude: 180.0,
278
            northBoundLatitude: 90.0
279
        };
280
    }
281
    const catalogBounds = {
19✔
282
        extent: [
283
            bbox.westBoundLongitude || bbox.minx,
23✔
284
            bbox.southBoundLatitude || bbox.miny,
23✔
285
            bbox.eastBoundLongitude || bbox.maxx,
23✔
286
            bbox.northBoundLatitude || bbox.maxy
23✔
287
        ],
288
        crs: "EPSG:4326"
289
    };
290
    if (bounds) {
19✔
291
        return {
2✔
292
            crs: catalogBounds.crs,
293
            bounds: {
294
                minx: catalogBounds.extent[0],
295
                miny: catalogBounds.extent[1],
296
                maxx: catalogBounds.extent[2],
297
                maxy: catalogBounds.extent[3]
298
            }
299
        };
300
    }
301
    return catalogBounds;
17✔
302
};
303
export const reset = () => {
1✔
304
    Object.keys(capabilitiesCache).forEach(key => {
1✔
305
        delete capabilitiesCache[key];
5✔
306
    });
307
};
308

309
/**
310
 * Fetch the supported formats of the WMS service
311
 * @param url
312
 * @param includeGFIFormats
313
 * @return {object|string} formats
314
 */
315
export const getSupportedFormat = (url, includeGFIFormats = false) => {
1✔
316
    return getCapabilities(url)
4✔
317
        .then((response) => {
318
            const { imageFormats, infoFormats } = getFormats(response);
3✔
319
            if (includeGFIFormats) {
3✔
320
                return { imageFormats, infoFormats };
1✔
321
            }
322
            return imageFormats;
2✔
323
        })
324
        .catch(() => includeGFIFormats ? { imageFormats: [], infoFormats: [] } : []);
1!
325
};
326

327
export const getJsonWMSLegend = (url) => {
1✔
328
    return axios.get(url)
8✔
329
        .then((response) => {
330
            if (typeof response?.data === 'string' && response.data.includes("Exception")) {
6!
331
                throw new Error("Faild to get json legend");
×
332
            }
333
            return response?.data?.Legend || [];
6!
334
        }).catch(err => { throw err; });
2✔
335
};
336

337
const Api = {
1✔
338
    flatLayers,
339
    parseUrl,
340
    getDimensions,
341
    getCapabilities,
342
    describeLayer,
343
    getRecords,
344
    describeLayers,
345
    textSearch,
346
    parseLayerCapabilities,
347
    getBBox,
348
    reset,
349
    getSupportedFormat
350
};
351

352
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