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

geosolutions-it / MapStore2 / 12669623181

08 Jan 2025 11:25AM UTC coverage: 76.812% (-0.3%) from 77.108%
12669623181

Pull #10731

github

web-flow
Merge f1474727f into d1ebec1ff
Pull Request #10731: Fix #10631 New MapStore Home Page

30930 of 48379 branches covered (63.93%)

827 of 1286 new or added lines in 70 files covered. (64.31%)

161 existing lines in 14 files now uncovered.

38448 of 50055 relevant lines covered (76.81%)

34.24 hits per line

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

93.55
/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 ConfigUtils, { getConfigProp } from '../utils/ConfigUtils';
14
import { getWMSBoundingBox } from '../utils/CoordinatesUtils';
15
import { isValidGetMapFormat, isValidGetFeatureInfoFormat } from '../utils/WMSUtils';
16
const capabilitiesCache = {};
1✔
17

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

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

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

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

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

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

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

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

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

324
let layerLegendJsonData = {};
1✔
325
export const getJsonWMSLegend = (url) => {
1✔
326
    let request;
327

328
    // enables caching of the JSON legend for a specified duration,
329
    // while providing the possibility of re-fetching the legend data in case of external modifications
330
    const cached = layerLegendJsonData[url];
6✔
331
    if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) {
6!
UNCOV
332
        request = () => Promise.resolve(cached.data);
×
333
    } else {
334
        request = () => axios.get(url).then((response) => {
6✔
335
            if (typeof response?.data === 'string' && response.data.includes("Exception")) {
6!
336
                throw new Error("Faild to get json legend");
×
337
            }
338
            layerLegendJsonData[url] = {
6✔
339
                timestamp: new Date().getTime(),
340
                data: response?.data?.Legend
341
            };
342
            return response?.data?.Legend || [];
6!
343
        });
344
    }
345
    return request().then((data) => data).catch(err => {
6✔
UNCOV
346
        throw err;
×
347
    });
348
};
349

350
const Api = {
1✔
351
    flatLayers,
352
    parseUrl,
353
    getDimensions,
354
    getCapabilities,
355
    describeLayer,
356
    getRecords,
357
    describeLayers,
358
    textSearch,
359
    parseLayerCapabilities,
360
    getBBox,
361
    reset,
362
    getSupportedFormat
363
};
364

365
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