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

geosolutions-it / MapStore2 / 15022192473

14 May 2025 01:37PM UTC coverage: 76.899% (-0.09%) from 76.993%
15022192473

Pull #10515

github

web-flow
Merge 76310647b into d76ffd67a
Pull Request #10515: #10514 - FeatureEditor filter by geometric area

30971 of 48268 branches covered (64.16%)

27 of 42 new or added lines in 6 files covered. (64.29%)

532 existing lines in 55 files now uncovered.

38582 of 50172 relevant lines covered (76.9%)

35.92 hits per line

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

89.31
/web/client/utils/LayersUtils.js
1
/*
2
 * Copyright 2017, 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 assign from 'object-assign';
10
import toBbox from 'turf-bbox';
11
import uuidv1 from 'uuid/v1';
12
import isString from 'lodash/isString';
13
import isObject from 'lodash/isObject';
14
import isArray from 'lodash/isArray';
15
import head from 'lodash/head';
16
import castArray from 'lodash/castArray';
17
import isEmpty from 'lodash/isEmpty';
18
import findIndex from 'lodash/findIndex';
19
import pick from 'lodash/pick';
20
import isNil from 'lodash/isNil';
21
import get from 'lodash/get';
22
import { addAuthenticationParameter } from './SecurityUtils';
23
import { getEPSGCode } from './CoordinatesUtils';
24
import { ANNOTATIONS, updateAnnotationsLayer, isAnnotationLayer } from '../plugins/Annotations/utils/AnnotationsUtils';
25
import { getLocale } from './LocaleUtils';
26

27
let LayersUtils;
28

29
let regGeoServerRule = /\/[\w- ]*geoserver[\w- ]*\//;
1✔
30

31
export const NodeTypes = {
1✔
32
    LAYER: 'layers',
33
    GROUP: 'groups'
34
};
35

36
export const DEFAULT_GROUP_ID = 'Default';
1✔
37
export const ROOT_GROUP_ID = 'root';
1✔
38

39
const getGroup = (groupId, groups) => {
1✔
40
    return head(groups.filter((subGroup) => isObject(subGroup) && subGroup.id === groupId));
77✔
41
};
42
const getLayer = (layerName, allLayers) => {
1✔
43
    return head(allLayers.filter((layer) => layer.id === layerName));
222✔
44
};
45
const getLayersId = (groupId, allLayers) => {
1✔
46
    return allLayers.filter((layer) => (layer.group || DEFAULT_GROUP_ID) === groupId).map((layer) => layer.id).reverse();
125✔
47
};
48
/**
49
 * utility to check
50
 * @param {object} l layer data
51
 * @returns {string} wps url or fallback to other layer urls
52
 */
53
export const getWpsUrl = l => l && l.wpsUrl || (l.search && l.search.url) || l.url;
12!
54
const initialReorderLayers = (groups, allLayers) => {
1✔
55
    return groups.slice(0).reverse().reduce((previous, group) => {
83✔
56
        return previous.concat(
82✔
57
            group.nodes.slice(0).reverse().reduce((layers, node) => {
58
                if (isObject(node)) {
115✔
59
                    return layers.concat(initialReorderLayers([node], allLayers));
4✔
60
                }
61
                return layers.concat(getLayer(node, allLayers));
111✔
62
            }, [])
63
        );
64
    }, []);
65
};
66
const reorderLayers = (groups, allLayers) => {
1✔
67
    return initialReorderLayers(groups, allLayers);
79✔
68
};
69
const createGroup = (groupId, groupTitle, groupName, layers, addLayers) => {
1✔
70
    return assign({}, {
74✔
71
        id: groupId,
72
        title: groupTitle ?? (groupName || "").replace(/\${dot}/g, "."),
74!
73
        name: groupName,
74
        nodes: addLayers ? getLayersId(groupId, layers) : [],
74✔
75
        expanded: true
76
    });
77
};
78

79
const getElevationDimension = (dimensions = []) => {
1!
80
    return dimensions.reduce((previous, dim) => {
1✔
81
        return dim.name.toLowerCase() === 'elevation' || dim.name.toLowerCase() === 'depth' ?
1!
82
            assign({
83
                positive: dim.name.toLowerCase() === 'elevation'
84
            }, dim, {
85
                name: dim.name.toLowerCase() === 'elevation' ? dim.name : 'DIM_' + dim.name
1!
86
            }) : previous;
87
    }, null);
88
};
89

90
const addBaseParams = (url, params) => {
1✔
91
    const query = Object.keys(params).map((key) => key + '=' + encodeURIComponent(params[key])).join('&');
26✔
92
    return url.indexOf('?') === -1 ? (url + '?' + query) : (url + '&' + query);
26✔
93
};
94

95
const isSupportedLayerFunc = (layer, maptype) => {
1✔
96
    const LayersUtil = require('./' + maptype + '/Layers');
11✔
97
    const Layers = LayersUtil.default || LayersUtil;
11✔
98
    if (layer.type === "mapquest" || layer.type === "bing") {
11✔
99
        return Layers.isSupported(layer.type) && layer.apiKey && layer.apiKey !== "__API_KEY_MAPQUEST__" && !layer.invalid;
5✔
100
    }
101

102
    /*
103
     * type 'empty' represents 'No background' layer
104
     * previously was checking for types
105
    */
106
    if (layer.type === 'empty') {
6✔
107
        return true;
3✔
108
    }
109
    return Layers.isSupported(layer.type) && !layer.invalid;
3✔
110
};
111

112

113
const checkInvalidParam = (layer) => {
1✔
114
    return layer && layer.invalid ? assign({}, layer, {invalid: false}) : layer;
1!
115
};
116

117
export const getNode = (nodes, id) => {
1✔
118
    if (nodes && isArray(nodes)) {
235!
119
        return nodes.reduce((previous, node) => {
235✔
120
            if (previous) {
358✔
121
                return previous;
51✔
122
            }
123
            if (node && (node.name === id || node.id === id || node === id)) {
307✔
124
                return node;
109✔
125
            }
126
            if (node && node.nodes && node.nodes.length > 0) {
198✔
127
                return getNode(node.nodes, id);
107✔
128
            }
129
            return previous;
91✔
130
        }, null);
131
    }
132
    return null;
×
133
};
134

135
export const getGroupNodes = (node) => {
1✔
136
    if (node && node.nodes) {
48✔
137
        return node.nodes.reduce((a, b) => {
47✔
138
            let nodes = [].concat(a);
67✔
139
            if (b.nodes) {
67✔
140
                nodes = a.concat(getGroupNodes(b));
13✔
141
            }
142
            if (isString(b)) {
67✔
143
                return [...nodes, b];
45✔
144
            }
145
            return [...nodes, b.id];
22✔
146
        }, []);
147
    }
148
    return [];
1✔
149
};
150

151
/**
152
 * Gets title of nested groups from Default
153
 * @param {string} id of group
154
 * @param {array} groups groups of map
155
 * @return {string} title of the group
156
 */
157
export const getNestedGroupTitle = (id, groups = []) => {
1!
158
    return isArray(groups) && head(groups.map(group => {
1✔
159
        const groupObj = group.id === id ? group : null;
1!
160
        if (groupObj) {
1!
161
            return groupObj.title;
×
162
        }
163
        const nodeObj = getNode(group.nodes, id);
1✔
164
        return nodeObj ? nodeObj.title : null;
1!
165
    }));
166
};
167

168
/**
169
 * Flatten nested groupDetails to a one-level groupDetails
170
 * @param {(Object[]|Object)} groupDetails of objects
171
 * @returns {Object[]} flattened groupDetails
172
 */
173
export const flattenArrayOfObjects = (groupDetails) => {
1✔
174
    let result = [];
432✔
175
    groupDetails && castArray(groupDetails).forEach((a) => {
432✔
176
        result.push(a);
319✔
177
        if (a.nodes && Array.isArray(groupDetails) && Array.isArray(a.nodes)) {
319✔
178
            result = result.concat(flattenArrayOfObjects(a.nodes));
106✔
179
        }
180
    });
181
    return result;
432✔
182
};
183

184
/**
185
 * Gets group title by id
186
 * @param {string} id of group
187
 * @param {object[]} groups groups of map
188
 * @returns {string} title of the group
189
 */
190

191
export const displayTitle = (id, groups) => {
1✔
192
    if (groups && Array.isArray(groups)) {
76!
193
        for (let group of groups) {
76✔
194
            if (group?.id === id) {
61✔
195
                return group.title;
46✔
196
            }
197
        }
198
    }
199
    return DEFAULT_GROUP_ID;
30✔
200
};
201
/**
202
 * adds or update node property in a nested node
203
 * if propName is an object it overrides a whole group of options instead of one
204
 */
205
export const deepChange = (nodes, findValue, propName, propValue) => {
1✔
206
    if (nodes && isArray(nodes) && nodes.length > 0) {
96✔
207
        return nodes.map((node) => {
94✔
208
            if (isObject(node)) {
125✔
209
                if (node.id === findValue) {
95✔
210
                    return {...node, ...(isObject(propName) ? propName : {[propName]: propValue})};
60✔
211
                } else if (node.nodes) {
35!
212
                    return {...node, nodes: deepChange(node.nodes, findValue, propName, propValue)};
35✔
213
                }
214
            }
215
            return node;
30✔
216
        });
217
    }
218
    return [];
2✔
219
};
220

221
export const updateAvailableTileMatrixSetsOptions = ({ tileMatrixSet, matrixIds,  ...layer }) => {
1✔
222
    if (!layer.availableTileMatrixSets && tileMatrixSet && matrixIds) {
86✔
223
        const matrixIdsKeys = isArray(matrixIds) ? matrixIds : Object.keys(matrixIds);
17!
224
        const availableTileMatrixSets = matrixIdsKeys
17✔
225
            .reduce((acc, key) => {
226
                const tileMatrix = (tileMatrixSet || []).find((matrix) => matrix['ows:Identifier'] === key);
29!
227
                if (!tileMatrix) {
21!
228
                    return acc;
×
229
                }
230
                const limits = isObject(matrixIds) ? matrixIds[key] : null;
21!
231
                const isLayerLimit = !!(limits || []).find(({ ranges }) => !!ranges);
21!
232
                const tileMatrixCRS = getEPSGCode(tileMatrix['ows:SupportedCRS'] || '');
21!
233
                return {
21✔
234
                    ...acc,
235
                    [key]: {
236
                        crs: tileMatrixCRS,
237
                        ...(isLayerLimit && { limits }),
23✔
238
                        tileMatrixSet: tileMatrix
239
                    }
240
                };
241
            }, {});
242
        return { ...layer, availableTileMatrixSets };
17✔
243
    }
244
    return layer;
69✔
245
};
246

247
/**
248
 * Extracts the sourceID of a layer.
249
 * @param {object} layer the layer object
250
 */
251
export const getSourceId = (layer = {}) => layer.capabilitiesURL || head(castArray(layer.url));
15!
252
export const getTileMatrixSetLink = (layer = {}, tileMatrixSetId) => `sources['${getSourceId(layer)}'].tileMatrixSet['${tileMatrixSetId}']`;
4!
253
/**
254
 * It extracts tile matrix set from sources and add them to the layer
255
 *
256
 * @param sources {object} sources object from state or configuration
257
 * @param layer {object} layer to check
258
 * @return {object} new layers with tileMatrixSet and matrixIds (if needed)
259
 */
260
export const extractTileMatrixFromSources = (sources, layer) => {
1✔
261
    if (!sources || !layer) {
12✔
262
        return {};
3✔
263
    }
264
    if (layer.availableTileMatrixSets) {
9✔
265
        const availableTileMatrixSets =  Object.keys(layer.availableTileMatrixSets)
1✔
266
            .reduce((acc, tileMatrixSetId) => {
267
                const tileMatrixSetLink = getTileMatrixSetLink(layer, tileMatrixSetId);
1✔
268
                const tileMatrixSet = get({ sources }, tileMatrixSetLink);
1✔
269
                if (tileMatrixSet) {
1!
270
                    return {
1✔
271
                        ...acc,
272
                        [tileMatrixSetId]: {
273
                            ...layer.availableTileMatrixSets[tileMatrixSetId],
274
                            tileMatrixSet
275
                        }
276
                    };
277
                }
278
                return {
×
279
                    ...acc,
280
                    [tileMatrixSetId]: layer.availableTileMatrixSets[tileMatrixSetId]
281
                };
282
            }, {});
283
        return { availableTileMatrixSets };
1✔
284
    }
285
    if (!isArray(layer.matrixIds) && isObject(layer.matrixIds)) {
8✔
286
        layer.matrixIds = [...Object.keys(layer.matrixIds)];
1✔
287
    }
288
    const sourceId = getSourceId(layer);
8✔
289
    const matrixIds = layer.matrixIds && layer.matrixIds.reduce((acc, mI) => {
8✔
290
        const ids = (sources?.[sourceId]?.tileMatrixSet?.[mI]?.TileMatrix || [])
10!
291
            .map(i => ({
10✔
292
                identifier: i['ows:Identifier'],
293
                ranges: i.ranges
294
            }));
295
        return ids.length === 0 ? acc : { ...acc, [mI]: [...ids] };
10!
296
    }, {}) || null;
297
    const tileMatrixSet = layer.tileMatrixSet && layer.matrixIds.map(mI => sources[sourceId].tileMatrixSet[mI]).filter(v => v) || null;
9✔
298
    const newTileMatrixOptions = updateAvailableTileMatrixSetsOptions((tileMatrixSet && matrixIds) ? { tileMatrixSet, matrixIds } : {});
8✔
299
    return newTileMatrixOptions;
8✔
300
};
301

302
/**
303
 * It extracts tile matrix set from layers and add them to sources map object
304
 *
305
 * @param  {object} groupedLayersByUrl layers grouped by url
306
 * @param {object} [sources] current sources map object
307
 * @return {object} new sources object with data from layers
308
 */
309
export const extractTileMatrixSetFromLayers = (groupedLayersByUrl, sources = {}) => {
1✔
310
    return Object.keys(groupedLayersByUrl || [])
46✔
311
        .reduce((acc, url) => {
312
            const layers = groupedLayersByUrl[url];
8✔
313
            const tileMatrixSet = layers.reduce((layerAcc, layer) => {
8✔
314
                const { availableTileMatrixSets } = updateAvailableTileMatrixSetsOptions(layer);
10✔
315
                return {
10✔
316
                    ...layerAcc,
317
                    ...Object.keys(availableTileMatrixSets).reduce((tileMatrixSetAcc, tileMatrixSetId) => ({
11✔
318
                        ...tileMatrixSetAcc,
319
                        [tileMatrixSetId]: availableTileMatrixSets[tileMatrixSetId].tileMatrixSet
320
                    }), {})
321
                };
322
            }, {});
323
            return {
8✔
324
                ...acc,
325
                [url]: {
326
                    ...sources?.[url],
327
                    tileMatrixSet: {
328
                        ...sources?.[url]?.tileMatrixSet,
329
                        ...tileMatrixSet
330
                    }
331
                }
332
            };
333
        }, { ...sources });
334
};
335

336
/**
337
 * Creates a map of `sourceId: sourceObject` from the layers array.
338
 * @param {object[]} layers array of layer objects
339
 */
340
export const extractSourcesFromLayers = layers => {
1✔
341
    /* layers grouped by url to create the source object */
342
    const groupByUrl = layers
40✔
343
        .filter(layer => layer.tileMatrixSet || layer.availableTileMatrixSets)
68✔
344
        .reduce((acc, layer) => {
345
            const sourceId = getSourceId(layer);
3✔
346
            return {
3✔
347
                ...acc,
348
                [sourceId]: acc[sourceId]
3!
349
                    ? [...acc[sourceId], layer]
350
                    : [layer]
351
            };
352
        }, {});
353

354
    /* extract and add tile matrix sets to sources object  */
355
    return extractTileMatrixSetFromLayers(groupByUrl);
40✔
356
};
357

358
/**
359
 * It extracts data from configuration sources and add them to the layers
360
 *
361
 * @param mapState {object} state of map, must contains layers array
362
 * @return {object} new sources object with data from layers
363
 */
364

365
export const extractDataFromSources = mapState => {
1✔
366
    if (!mapState || !mapState.layers || !isArray(mapState.layers)) {
71✔
367
        return null;
2✔
368
    }
369
    const sources = mapState.mapInitialConfig && mapState.mapInitialConfig.sources && assign({}, mapState.mapInitialConfig.sources) || {};
69✔
370

371
    return !isEmpty(sources) ? mapState.layers.map(l => {
69✔
372

373
        const tileMatrix = extractTileMatrixFromSources(sources, l);
1✔
374

375
        return assign({}, l, tileMatrix);
1✔
376
    }) : [...mapState.layers];
377
};
378

379
export const getURLs = (urls, queryParametersString = '') => {
1✔
380
    return urls.map((url) => url.split("\?")[0] + queryParametersString);
199✔
381
};
382

383

384
const LayerCustomUtils = {};
1✔
385
/**
386
 * Return a base url for the given layer.
387
 * Supports multiple urls.
388
 */
389

390
export const getLayerUrl = (layer) => {
1✔
391
    return isArray(layer.url) ? layer.url[0] : layer.url;
134✔
392
};
393

394
export const getGroupByName = (groupName, groups = []) => {
1!
395
    const result = head(groups.filter(g => g.name === groupName));
×
396
    return result || groups.reduce((prev, g) => prev || !!g.nodes && LayersUtils.getGroupByName(groupName, g.nodes), undefined);
×
397
};
398
export const getDimension = (dimensions, dimension) => {
1✔
399
    switch (dimension.toLowerCase()) {
1!
400
    case 'elevation':
401
        return getElevationDimension(dimensions);
1✔
402
    default:
403
        return null;
×
404
    }
405
};
406
/**
407
 * Returns an id for the layer. If the layer has layer.id returns it, otherwise it will return a generated id.
408
 * If the layer doesn't have any layer and if the 2nd argument is passed (it should be an array),
409
 * the layer id will returned will be something like `layerName__2` when 2 is the layer size (for retro compatibility, it should be removed in the future).
410
 * Otherwise a random string will be appended to the layer name.
411
 * @param {object} layer the layer
412
 * @returns {string} the id of the layer, or a generated one
413
 */
414
export const getLayerId = (layerObj) => {
1✔
415
    return layerObj && layerObj.id || `${layerObj.name ? `${layerObj.name}__` : ''}${uuidv1()}`;
12✔
416
};
417

418
/**
419
 * it creates an id of a feature if not existing
420
 * @param {object} feature list of layers to check
421
  * @return {string} the id
422
 */
423
export const createFeatureId = (feature = {}) => {
1!
424
    return {
6✔
425
        ...feature,
426
        id: feature.id || feature.properties?.id || uuidv1()
13✔
427
    };
428
};
429
/**
430
 * Normalizes the layer to assign missing Ids and features for vector layers
431
 * @param {object} layer the layer to normalize
432
 * @returns {object} the normalized layer
433
 */
434

435
export const normalizeLayer = (layer) => {
1✔
436
    // con uuid
437
    let _layer = layer;
37✔
438
    if (layer.type === "vector") {
37✔
439
        _layer = _layer?.features?.length ? {
3!
440
            ..._layer,
441
            features: _layer?.features?.map(createFeatureId)
442
        } : layer;
443
    }
444
    // regenerate geodesic lines as property since that info has not been saved
445
    if (_layer.id === ANNOTATIONS) {
37✔
446
        _layer = updateAnnotationsLayer(_layer)[0];
1✔
447
    }
448

449
    return {
37✔
450
        ..._layer,
451
        id: _layer.id || LayersUtils.getLayerId(_layer)};
39✔
452

453
};
454
/**
455
 * Normalizes the map adding missing ids, default groups.
456
 * @param {object} map the map
457
 * @param {object} the normalized map
458
 */
459
export const normalizeMap = (rawMap = {}) =>
1!
460
    [
×
461
        (map) => (map.layers || []).filter(({ id } = {}) => !id).length > 0 ? {...map, layers: (map.layers || []).map(l => LayersUtils.normalizeLayer(l))} : map,
×
462
        (map) => map.groups ? map : {...map, groups: {id: DEFAULT_GROUP_ID, expanded: true}}
×
463
        // this is basically a compose
464
    ].reduce((f, g) => (...args) => f(g(...args)))(rawMap);
×
465
/**
466
 * @param gid
467
 * @return function that filter by group
468
 */
469
export const belongsToGroup = (gid) => l => (l.group || DEFAULT_GROUP_ID) === gid || (l.group || "").indexOf(`${gid}.`) === 0;
2!
470
export const getLayersByGroup = (configLayers, configGroups) => {
1✔
471
    let i = 0;
75✔
472
    let mapLayers = configLayers.map((layer) => assign({}, layer, {storeIndex: i++}));
122✔
473
    let groupNames = mapLayers.reduce((groups, layer) => {
75✔
474
        return groups.indexOf(layer.group || DEFAULT_GROUP_ID) === -1 ? groups.concat([layer.group || DEFAULT_GROUP_ID]) : groups;
122✔
475
    }, []).filter((group) => group !== 'background').reverse();
77✔
476
    return groupNames.reduce((groups, names)=> {
75✔
477
        let name = names || DEFAULT_GROUP_ID;
73!
478
        name.split('.').reduce((subGroups, groupName, idx, array)=> {
73✔
479
            const groupId = name.split(".", idx + 1).join('.');
77✔
480
            let group = getGroup(groupId, subGroups);
77✔
481
            const addLayers = idx === array.length - 1;
77✔
482
            if (!group) {
77✔
483
                const flattenGroups = flattenArrayOfObjects(configGroups);
74✔
484
                const groupTitle = displayTitle(groupId, flattenGroups);
74✔
485
                group = createGroup(groupId, groupTitle || groupName, groupName, mapLayers, addLayers);
74✔
486
                subGroups.push(group);
74✔
487
            } else if (addLayers) {
3✔
488
                group.nodes = getLayersId(groupId, mapLayers).concat(group.nodes)
2✔
489
                    .reduce((arr, cur) => {
490
                        isObject(cur)
4✔
491
                            ? arr.push({node: cur, order: mapLayers.find((el) => el.group === cur.id)?.storeIndex})
6✔
492
                            : arr.push({node: cur, order: mapLayers.find((el) => el.id === cur)?.storeIndex});
4✔
493
                        return arr;
4✔
494
                    }, []).sort((a, b) => b.order - a.order).map(e => e.node);
4✔
495
            }
496
            return group.nodes;
77✔
497
        }, groups);
498
        return groups;
73✔
499
    }, []);
500
};
501
export const removeEmptyGroups = (groups) => {
1✔
502
    return groups.reduce((acc, group) => {
1✔
503
        return acc.concat(LayersUtils.getNotEmptyGroup(group));
2✔
504
    }, []);
505
};
506
export const getNotEmptyGroup = (group) => {
1✔
507
    const nodes = group.nodes.reduce((gNodes, node) => {
2✔
508
        return node.nodes ? gNodes.concat(LayersUtils.getNotEmptyGroup(node)) : gNodes.concat(node);
1!
509
    }, []);
510
    return nodes.length > 0 ? assign({}, group, {nodes: nodes}) : [];
2✔
511
};
512
export const reorderFunc = (groups, allLayers) => {
1✔
513
    return allLayers.filter((layer) => layer.group === 'background')
114✔
514
        .concat(reorderLayers(groups, allLayers));
515
};
516

517
export const getInactiveNode = (groupId, groups, nodeId) => {
1✔
518
    const groupIds = groupId
327✔
519
        .split('.')
520
        .reverse()
521
        .map((val, idx, arr) => [val, ...arr.filter((v, jdx) => jdx > idx)].reverse().join('.'));
383✔
522
    const inactive = !!groups
327✔
523
        .find((group) => nodeId !== group?.id && groupIds.includes(group?.id) && group.visibility === false);
269✔
524
    return inactive;
327✔
525
};
526

527
export const getDerivedLayersVisibility = (layers = [], groups = []) => {
1✔
528
    const flattenGroups = flattenArrayOfObjects(groups).filter(isObject);
154✔
529
    return layers.map((layer) => {
154✔
530
        const inactive = getInactiveNode(layer?.group || DEFAULT_GROUP_ID, flattenGroups);
105✔
531
        return inactive ? { ...layer, visibility: false } : layer;
105✔
532
    });
533
};
534

535
export const denormalizeGroups = (allLayers, groups) => {
1✔
536
    const flattenGroups = flattenArrayOfObjects(groups).filter(isObject);
94✔
537
    let getNormalizedGroup = (group, layers) => {
94✔
538
        const nodes = group?.nodes?.map((node) => {
96✔
539
            if (isObject(node)) {
125✔
540
                return getNormalizedGroup(node, layers);
5✔
541
            }
542
            return layers.find((layer) => layer.id === node);
166✔
543
        });
544
        return {
96✔
545
            ...group,
546
            nodes,
547
            inactive: getInactiveNode(group?.id || '', flattenGroups, group?.id),
100✔
548
            visibility: group?.visibility === undefined ? true : group.visibility
96✔
549
        };
550
    };
551
    let normalizedLayers = allLayers.map((layer) => ({
121✔
552
        ...layer,
553
        inactive: getInactiveNode(layer?.group || DEFAULT_GROUP_ID, flattenGroups),
200✔
554
        expanded: layer.expanded || false
237✔
555
    }));
556
    return {
94✔
557
        flat: normalizedLayers,
558
        groups: groups.map((group) => getNormalizedGroup(group, normalizedLayers))
91✔
559
    };
560
};
561

562
export const sortLayers = (groups, allLayers) => {
1✔
563
    return allLayers.filter((layer) => layer.group === 'background')
18✔
564
        .concat(reorderLayers(groups, allLayers));
565
};
566
export const toggleByType = (type, toggleFun) => {
1✔
567
    return (node, status) => {
×
568
        return toggleFun(node, type, status);
×
569
    };
570
};
571
export const sortUsing = (sortFun, action) => {
1✔
572
    return (node, reorder) => {
×
573
        return action(node, reorder, sortFun);
×
574
    };
575
};
576
export const splitMapAndLayers = (mapState) => {
1✔
577
    if (mapState && isArray(mapState.layers)) {
126✔
578
        let groups = LayersUtils.getLayersByGroup(mapState.layers, mapState.groups);
67✔
579
        // additional params from saved configuration
580
        if (isArray(mapState.groups)) {
67✔
581
            groups = mapState.groups.reduce((g, group) => {
60✔
582
                let newGroups = g;
45✔
583
                let groupMetadata = {
45✔
584
                    expanded: group.expanded,
585
                    visibility: group.visibility,
586
                    nodesMutuallyExclusive: group.nodesMutuallyExclusive
587
                };
588
                if (group.title) {
45✔
589
                    groupMetadata = {
16✔
590
                        ...groupMetadata,
591
                        title: group.title,
592
                        description: group.description,
593
                        tooltipOptions: group.tooltipOptions,
594
                        tooltipPlacement: group.tooltipPlacement
595
                    };
596
                }
597
                newGroups = LayersUtils.deepChange(newGroups, group.id, groupMetadata);
45✔
598
                return newGroups;
45✔
599
            }, [].concat(groups));
600
        }
601

602
        let layers = extractDataFromSources(mapState);
67✔
603

604
        return assign({}, mapState, {
67✔
605
            layers: {
606
                flat: LayersUtils.reorder(groups, layers),
607
                groups: groups
608
            }
609
        });
610
    }
611
    return mapState;
59✔
612
};
613
/**
614
 * used for converting a geojson file with fileName into a vector layer
615
 * it supports FeatureCollection or Feature
616
 * @param {object} geoJSON object to put into features
617
 * @param {string} id layer id
618
 * @return {object} vector layer containing the geojson in features array
619
 */
620
export const geoJSONToLayer = (geoJSON, id) => {
1✔
621
    const bbox = toBbox(geoJSON);
11✔
622
    let features = [];
11✔
623
    if (geoJSON.type === "FeatureCollection") {
11✔
624
        features = geoJSON.features.map((feature, idx) => {
8✔
625
            if (!feature.id) {
10✔
626
                feature.id = idx;
9✔
627
            }
628
            if (feature.geometry && feature.geometry.bbox && isNaN(feature.geometry.bbox[0])) {
10!
629
                feature.geometry.bbox = [null, null, null, null];
×
630
            }
631
            return feature;
10✔
632
        });
633
    } else {
634
        features = [pick({...geoJSON, id: isNil(geoJSON.id) ? uuidv1() : geoJSON.id}, ["geometry", "type", "style", "id"])];
3✔
635
    }
636
    return {
11✔
637
        type: 'vector',
638
        visibility: true,
639
        id,
640
        name: geoJSON.fileName,
641
        hideLoading: true,
642
        bbox: {
643
            bounds: {
644
                minx: bbox[0],
645
                miny: bbox[1],
646
                maxx: bbox[2],
647
                maxy: bbox[3]
648
            },
649
            crs: "EPSG:4326"
650
        },
651
        features,
652
        ...(['geostyler'].includes(geoJSON?.style?.format) && geoJSON?.style?.body && {
11!
653
            style: geoJSON.style
654
        })
655
    };
656
};
657
export const saveLayer = (layer) => {
1✔
658
    return assign({
81✔
659
        id: layer.id,
660
        features: layer.features,
661
        format: layer.format,
662
        thumbURL: layer.thumbURL && layer.thumbURL.split(':')[0] === 'blob' ? undefined : layer.thumbURL,
173✔
663
        group: layer.group,
664
        search: layer.search,
665
        fields: layer.fields,
666
        source: layer.source,
667
        name: layer.name,
668
        opacity: layer.opacity,
669
        provider: layer.provider,
670
        description: layer.description,
671
        styles: layer.styles,
672
        style: layer.style,
673
        styleName: layer.styleName,
674
        layerFilter: layer.layerFilter,
675
        title: layer.title,
676
        transparent: layer.transparent,
677
        tiled: layer.tiled,
678
        type: layer.type,
679
        url: layer.url,
680
        bbox: layer.bbox,
681
        visibility: layer.visibility,
682
        singleTile: layer.singleTile || false,
161✔
683
        allowedSRS: layer.allowedSRS,
684
        matrixIds: layer.matrixIds,
685
        tileMatrixSet: layer.tileMatrixSet,
686
        availableTileMatrixSets: layer.availableTileMatrixSets,
687
        requestEncoding: layer.requestEncoding,
688
        dimensions: layer.dimensions || [],
154✔
689
        maxZoom: layer.maxZoom,
690
        maxNativeZoom: layer.maxNativeZoom,
691
        maxResolution: layer.maxResolution,
692
        minResolution: layer.minResolution,
693
        disableResolutionLimits: layer.disableResolutionLimits,
694
        hideLoading: layer.hideLoading || false,
162✔
695
        handleClickOnLayer: layer.handleClickOnLayer || false,
162✔
696
        queryable: layer.queryable,
697
        featureInfo: layer.featureInfo,
698
        catalogURL: layer.catalogURL,
699
        capabilitiesURL: layer.capabilitiesURL,
700
        serverType: layer.serverType,
701
        useForElevation: layer.useForElevation || false,
162✔
702
        hidden: layer.hidden || false,
162✔
703
        origin: layer.origin,
704
        thematic: layer.thematic,
705
        tooltipOptions: layer.tooltipOptions,
706
        tooltipPlacement: layer.tooltipPlacement,
707
        legendOptions: layer.legendOptions,
708
        tileSize: layer.tileSize,
709
        version: layer.version,
710
        expanded: layer.expanded || false
160✔
711
    },
712
    layer?.enableInteractiveLegend !== undefined ? { enableInteractiveLegend: layer?.enableInteractiveLegend } : {},
81!
713
    layer.sources ? { sources: layer.sources } : {},
81✔
714
    layer.heightOffset ? { heightOffset: layer.heightOffset } : {},
81✔
715
    layer.params ? { params: layer.params } : {},
81✔
716
    layer.extendedParams ? { extendedParams: layer.extendedParams } : {},
81✔
717
    layer.localizedLayerStyles ? { localizedLayerStyles: layer.localizedLayerStyles } : {},
81!
718
    layer.options ? { options: layer.options } : {},
81✔
719
    layer.credits ? { credits: layer.credits } : {},
81✔
720
    layer.security ? { security: layer.security } : {},
81✔
721
    layer.tileGrids ? { tileGrids: layer.tileGrids } : {},
81✔
722
    layer.tileGridStrategy ? { tileGridStrategy: layer.tileGridStrategy } : {},
81✔
723
    layer.tileGridCacheSupport ? { tileGridCacheSupport: layer.tileGridCacheSupport } : {},
81✔
724
    isString(layer.rowViewer) ? { rowViewer: layer.rowViewer } : {},
81!
725
    !isNil(layer.forceProxy) ? { forceProxy: layer.forceProxy } : {},
81✔
726
    !isNil(layer.disableFeaturesEditing) ? { disableFeaturesEditing: layer.disableFeaturesEditing } : {},
81✔
727
    layer.pointCloudShading ? { pointCloudShading: layer.pointCloudShading } : {},
81✔
728
    !isNil(layer.sourceMetadata) ? { sourceMetadata: layer.sourceMetadata } : {});
81✔
729
};
730

731
/**
732
 * constants to specify whether we can use some geoserver vendor extensions of if they
733
 * should rather be avoided
734
*/
735
export const ServerTypes = {
1✔
736
    GEOSERVER: 'geoserver',
737
    NO_VENDOR: 'no-vendor'
738
};
739

740
/**
741
 * default initial constant regex rule for searching for a /geoserver/ string in a url
742
 * useful for a reset to an initial state of the rule
743
 */
744
export const REG_GEOSERVER_RULE = regGeoServerRule;
1✔
745
/**
746
 * Override default REG_GEOSERVER_RULE variable
747
 * @param {regex} regex custom regex to override
748
 */
749
export const setRegGeoserverRule = (regex) => {
1✔
750
    regGeoServerRule = regex;
2✔
751
};
752
/**
753
 * Get REG_GEOSERVER_RULE regex variable
754
 */
755
export const getRegGeoserverRule = () => regGeoServerRule;
116✔
756
/**
757
 * it tests if a url is matched by a regex,
758
 * if so it returns the matched string
759
 * otherwise returns null
760
 * @param object.regex the regex to use for parsing the url
761
 * @param object.url the url to test
762
 */
763
export const findGeoServerName = ({url, regexRule}) => {
1✔
764
    const regex = regexRule || LayersUtils.getRegGeoserverRule();
117✔
765
    const location = isArray(url) ? url[0] : url;
117✔
766
    return regex.test(location) && location.match(regex)[0] || null;
117✔
767
};
768

769
/**
770
 * This method search for a /geoserver/  string inside the url
771
 * if it finds it returns a getCapabilitiesUrl to a single layer if it has a name like WORKSPACE:layerName
772
 * otherwise it returns the default getCapabilitiesUrl
773
 */
774
export const getCapabilitiesUrl = (layer) => {
1✔
775
    const matchedGeoServerName = LayersUtils.findGeoServerName({url: layer.url});
26✔
776
    let reqUrl = getLayerUrl(layer);
26✔
777
    if (!!matchedGeoServerName) {
26✔
778
        let urlParts = reqUrl.split(matchedGeoServerName);
12✔
779
        if (urlParts.length === 2) {
12!
780
            let layerParts = layer.name.split(":");
12✔
781
            if (layerParts.length === 2) {
12✔
782
                reqUrl = urlParts[0] + matchedGeoServerName + layerParts [0] + "/" + layerParts[1] + "/" + urlParts[1];
8✔
783
            }
784
        }
785
    }
786
    const params = {
26✔
787
        ...layer.baseParams,
788
        ...layer.params
789
    };
790
    return addBaseParams(reqUrl, params);
26✔
791
};
792
/**
793
 * Gets the layer search url or the current url
794
 *
795
 * @memberof utils.LayerUtils
796
 * @param {Object} layer
797
 * @returns {string} layer url
798
 */
799
export const getSearchUrl = (l = {}) => l.search && l.search.url || l.url;
11!
800
export const invalidateUnsupportedLayer = (layer, maptype) => {
1✔
801
    return isSupportedLayerFunc(layer, maptype) ? checkInvalidParam(layer) : assign({}, layer, {invalid: true});
1!
802
};
803
/**
804
 * Establish if a layer is supported or not
805
 * @return {boolean} value
806
 */
807
export const isSupportedLayer = (layer, maptype) => {
1✔
808
    return !!isSupportedLayerFunc(layer, maptype);
10✔
809
};
810
export const getLayerTitleTranslations = (capabilities) => {
1✔
811
    return !!LayerCustomUtils.getLayerTitleTranslations ? LayerCustomUtils.getLayerTitleTranslations(capabilities) : capabilities.Title;
17!
812
};
813
export const setCustomUtils = (type, fun) => {
1✔
UNCOV
814
    LayerCustomUtils[type] = fun;
×
815
};
816

817
export const getAuthenticationParam = options => {
1✔
818
    const urls = getURLs(isArray(options.url) ? options.url : [options.url]);
117✔
819
    let authenticationParam = {};
117✔
820
    urls.forEach(url => {
117✔
821
        addAuthenticationParameter(url, authenticationParam, options.securityToken);
119✔
822
    });
823
    return authenticationParam;
117✔
824
};
825
/**
826
 * Removes google backgrounds and select an alternative one as visible
827
 * returns a new list of layers modified accordingly
828
 */
829
export const excludeGoogleBackground = ll => {
1✔
830
    const hasVisibleGoogleBackground = ll.filter(({ type, group, visibility } = {}) => group === 'background' && type === 'google' && visibility).length > 0;
14!
831
    const layers = ll.filter(({ type } = {}) => type !== 'google');
14!
832
    const backgrounds = layers.filter(({ group } = {}) => group === 'background');
10!
833

834
    // check if the selection of a new background is required
835
    if (hasVisibleGoogleBackground && backgrounds.filter(({ visibility } = {}) => visibility).length === 0) {
8!
836
        // select the first available
837
        if (backgrounds.length > 0) {
3✔
838
            const candidate = findIndex(layers, {group: 'background'});
1✔
839
            return layers.map((l, i) => i === candidate ? {...l, visibility: true} : l);
2✔
840
        }
841
        // add osm if any other background is missing
842
        return [{
2✔
843
            "type": "osm",
844
            "title": "Open Street Map",
845
            "name": "mapnik",
846
            "source": "osm",
847
            "group": "background",
848
            "visibility": true
849
        }, ...layers];
850

851

852
    }
853
    return layers;
5✔
854
};
855
export const creditsToAttribution = ({ imageUrl, link, title, text }) => {
1✔
856
    // TODO: check if format is valid for an img (svg, for instance, may not work)
857
    const html = imageUrl ? `<img src="${imageUrl}" ${title ? `title="${title}"` : ``}>` : title || text || "credits";
23✔
858
    return link && html ? `<a href="${link}" target="_blank">${html}</a>` : html;
23✔
859
};
860

861
export const getLayerTitle = ({title, name}, currentLocale = 'default') => title?.[currentLocale] || title?.default || title || name;
6!
862

863
/**
864
 * Check if a resolution is inside of the min and max resolution limits of a layer
865
 * @param {object} layer layer object
866
 * @param {number} resolution resolutions of the current view
867
 */
868
export const isInsideResolutionsLimits = (layer, resolution) => {
1✔
869
    if (layer.disableResolutionLimits) {
68✔
870
        return true;
1✔
871
    }
872
    const minResolution = layer.minResolution || -Infinity;
67✔
873
    const maxResolution = layer.maxResolution || Infinity;
67✔
874
    return resolution !== undefined
67✔
875
        ? resolution < maxResolution && resolution >= minResolution
124✔
876
        : true;
877
};
878

879
/**
880
 * Filter array of layers to return layers with visibility key set to true
881
 * @param {Array} layers
882
 * @param {Array} timelineLayers
883
 * @returns {Array}
884
 */
885
export const visibleTimelineLayers = (layers, timelineLayers) => {
1✔
886
    return layers.filter(layer => {
9✔
887
        let timelineLayer = timelineLayers?.find(item => item.id === layer.id);
×
UNCOV
888
        return timelineLayer?.visibility ? layer : null;
×
889
    });
890
};
891

892
/**
893
 * Loop through array of timeline layers to determine if any of the layers is visible
894
 * @param {Array} layers
895
 * @returns {boolean}
896
 */
897
export const isTimelineVisible = (layers)=>{
1✔
898
    for (let layer of layers) {
6✔
899
        if (layer?.visibility) {
8✔
900
            return true;
4✔
901
        }
902
    }
903
    return false;
2✔
904
};
905

906
/**
907
 * Remove the workspace prefix from a geoserver layer name
908
 * @param {string} full layer name with workspace
909
 * @returns {string} layer name without workspace prefix
910
 */
911
export const removeWorkspace = (layer) => {
1✔
912
    if (layer.indexOf(':') !== -1) {
163✔
913
        return layer.split(':')[1];
1✔
914
    }
915
    return layer;
162✔
916
};
917

918
/**
919
 * Returns vendor params that can be used when calling wms server for display requests
920
 * @param {layer} the layer object
921
 */
922
export const getWMSVendorParams = (layer) =>  {
1✔
923
    if (layer?.serverType === ServerTypes.NO_VENDOR) {
170✔
924
        return {};
1✔
925
    }
926
    return { TILED: layer.singleTile ? false : (!isNil(layer.tiled) ? layer.tiled : true)};
169✔
927
};
928

929
/**
930
 * Utility function to check if the node allows to show fields tab
931
 * @param {object} node the node of the TOC (including layer properties)
932
 * @returns {boolean} true if the node allows to show fields
933
 */
934
export const hasWFSService = ({type, search = {}} = {}) =>
1!
935
    type === 'wfs' // pure WFS layer
45✔
936
        || (type === 'wms' && search.type === 'wfs'); // WMS backed by WFS (search)
937

938
export const getLayerTypeGlyph = (layer) => {
1✔
939
    if (isAnnotationLayer(layer)) {
103!
UNCOV
940
        return 'comment';
×
941
    }
942
    return '1-layer';
103✔
943
};
944

945
/**
946
Removes a group even if it is nested
947
It works for layers too
948
**/
949
export const deepRemove = (nodes, findValue) => {
1✔
950
    if (nodes && isArray(nodes) && nodes.length > 0) {
34✔
951
        return nodes.filter((node) => (node.id && node.id !== findValue) || (isString(node) && node !== findValue )).map((node) => isObject(node) ? assign({}, node, node.nodes ? {
36✔
952
            nodes: deepRemove(node.nodes, findValue)
953
        } : {}) : node);
954
    }
955
    return nodes;
9✔
956
};
957

958
const updateGroupIds = (node, parentGroupId, newLayers) => {
1✔
959
    if (node) {
1!
960
        if (isString(node.id)) {
1!
961
            const lastDot = node.id.lastIndexOf('.');
1✔
962
            const newId = lastDot !== -1 ?
1!
963
                parentGroupId + node.id.slice(lastDot + (parentGroupId === '' ? 1 : 0)) :
1!
964
                parentGroupId + (parentGroupId === '' ? '' : '.') + node.id;
×
965
            return assign({}, node, {id: newId, nodes: node.nodes.map(x => updateGroupIds(x, newId, newLayers))});
1✔
UNCOV
966
        } else if (isString(node)) {
×
967
            // if it's just a string it means it is a layer id
968
            for (let layer of newLayers) {
×
969
                if (layer.id === node) {
×
UNCOV
970
                    layer.group = parentGroupId;
×
971
                }
972
            }
UNCOV
973
            return node;
×
974
        }
975
    }
UNCOV
976
    return node;
×
977
};
978

979
export const sortGroups = (
1✔
980
    {
981
        groups: _groups,
982
        layers: _layers
983
    },
984
    {
985
        node: _node,
986
        index: _index,
987
        groupId: _groupId
988
    }
989
) => {
990
    const node = getNode(_groups || [], _node);
1!
991
    const layerNode = getNode(_layers, _node);
1✔
992
    if (node && _index >= 0 && node.id !== ROOT_GROUP_ID && node.id !== DEFAULT_GROUP_ID && !(!!layerNode && _groupId === ROOT_GROUP_ID)) {
1!
993
        const groupId = _groupId || DEFAULT_GROUP_ID;
1!
994
        const curGroupId = layerNode ? (layerNode.group || DEFAULT_GROUP_ID) : (() => {
1!
995
            const groups = node.id.split('.');
1✔
996
            return groups[groups.length - 2] || ROOT_GROUP_ID;
1!
997
        })();
998

999
        if (groupId === curGroupId) {
1!
1000
            const curGroupNode = curGroupId === ROOT_GROUP_ID ? {nodes: _groups} : getNode(_groups, curGroupId);
×
1001
            let nodes = (curGroupNode && curGroupNode.nodes || []).slice();
×
UNCOV
1002
            const nodeIndex = nodes.findIndex(x => (x.id || x) === (node.id || node));
×
1003

1004
            if (nodeIndex !== -1 && nodeIndex !== _index) {
×
1005
                const swapCnt = Math.abs(_index - nodeIndex);
×
1006
                const delta = nodeIndex < _index ? 1 : -1;
×
1007
                let pos = nodeIndex;
×
1008
                for (let i = 0; i < swapCnt; ++i, pos += delta) {
×
1009
                    const tmp = nodes[pos];
×
1010
                    nodes[pos] = nodes[pos + delta];
×
UNCOV
1011
                    nodes[pos + delta] = tmp;
×
1012
                }
1013

UNCOV
1014
                const newGroups = curGroupId === ROOT_GROUP_ID ? nodes : deepChange(_groups, _groupId, 'nodes', nodes);
×
1015

UNCOV
1016
                return {
×
1017
                    layers: sortLayers(newGroups, _layers),
1018
                    groups: newGroups
1019
                };
1020
            }
1021
        }
1022
        const groupsWithRemovedNode = deepRemove(_groups, node.id || node);
1!
1023
        const dstGroup = groupId === ROOT_GROUP_ID ? {nodes: groupsWithRemovedNode} : getNode(groupsWithRemovedNode, _groupId);
1!
1024
        if (dstGroup) {
1!
1025
            const newLayers = _layers.map(layer => ({ ...layer }));
1✔
1026
            const newNode = updateGroupIds(node, groupId === ROOT_GROUP_ID ? '' : groupId, newLayers);
1!
1027
            let newDestNodes = dstGroup.nodes.slice();
1✔
1028
            newDestNodes.splice(_index, 0, newNode);
1✔
1029
            const newGroups = groupId === ROOT_GROUP_ID ?
1!
1030
                newDestNodes :
1031
                deepChange(groupsWithRemovedNode.slice(), dstGroup.id, 'nodes', newDestNodes);
1032

1033
            return {
1✔
1034
                layers: sortLayers(newGroups, newLayers),
1035
                groups: newGroups
1036
            };
1037
        }
1038
    }
UNCOV
1039
    return null;
×
1040
};
1041

1042
export const moveNode = (groups, node, groupId, newLayers, foreground = true) => {
1✔
1043
    // Remove node from old group
1044
    let newGroups = deepRemove(groups, node);
11✔
1045
    // Check if group to move to exists
1046
    let group = getNode(newGroups, groupId);
11✔
1047
    if (!group) {
11✔
1048
        // Create missing group
1049
        group = head(getLayersByGroup([getNode(newLayers, node)]));
8✔
1050
        // check for parent group if exist
1051
        const parentGroup = groupId.split('.').reduce((tree, gName, idx) => {
8✔
1052
            const gId = groupId.split(".", idx + 1).join('.');
8✔
1053
            const parent = getNode(newGroups, gId);
8✔
1054
            return parent ? tree.concat(parent) : tree;
8!
1055
        }, []).pop();
1056
        if (parentGroup) {
8!
1057
            group = getNode([group], parentGroup.id).nodes[0];
×
UNCOV
1058
            newGroups = deepChange(newGroups, parentGroup.id, 'nodes', foreground ? [group].concat(parentGroup.nodes) : parentGroup.nodes.concat(group));
×
1059
        } else {
1060
            newGroups = [group].concat(newGroups);
8✔
1061
        }
1062
    } else {
1063
        newGroups = deepChange(newGroups, group.id, 'nodes', foreground ? [node].concat(group.nodes.slice(0)) : group.nodes.concat(node));
3✔
1064
    }
1065
    return newGroups;
11✔
1066
};
1067

1068
export const changeNodeConfiguration = ({
1✔
1069
    layers: _layers,
1070
    groups: _groups
1071
}, {
1072
    node,
1073
    nodeType,
1074
    options
1075
}) => {
1076
    const selector = nodeType === 'groups' ? 'group' : 'id';
18✔
1077
    if (selector === 'group') {
18✔
1078
        const groups = _groups ? [].concat(_groups) : [];
5!
1079
        // updating correctly options in a (deep) subgroup
1080
        const newGroups = deepChange(groups, node, options);
5✔
1081
        return { groups: newGroups };
5✔
1082
    }
1083

1084
    const flatLayers = (_layers || []);
13!
1085

1086
    // const newGroups = action.options && action.options.group && action.options.group !== layer;
1087
    let sameGroup = options.hasOwnProperty("group") ? false : true;
13!
1088

1089
    const newLayers = flatLayers.map((layer) => {
13✔
1090
        if (layer[selector] === node || layer[selector].indexOf(node + '.') === 0) {
19✔
1091
            if (layer.group === (options.group || DEFAULT_GROUP_ID)) {
13!
1092
                // If the layer didn't change group, raise a flag to prevent groups update
UNCOV
1093
                sameGroup = true;
×
1094
            }
1095
            // Edit the layer with the new options
1096
            return { ...layer, ...options };
13✔
1097
        }
1098
        return layer;
6✔
1099
    });
1100
    let originalNode = head(flatLayers.filter((layer) => { return (layer[selector] === node || layer[selector].indexOf(node + '.') === 0); }));
19✔
1101
    if (!sameGroup && originalNode ) {
13!
1102
        // Remove layers from old group
1103
        const groupId = (options.group || DEFAULT_GROUP_ID);
×
UNCOV
1104
        const newGroups = moveNode(_groups, node, groupId, newLayers);
×
1105

1106
        let orderedNewLayers = sortLayers ? sortLayers(newGroups, newLayers) : newLayers;
×
UNCOV
1107
        return {
×
1108
            layers: orderedNewLayers,
1109
            groups: newGroups
1110
        };
1111
    }
1112
    return { layers: newLayers };
13✔
1113
};
1114

1115
export const getSelectedNodes = (selectedIds = [], id, ctrlKey) => {
1!
1116
    if (!id) {
64✔
1117
        return [];
1✔
1118
    }
1119
    if (ctrlKey) {
63✔
1120
        return selectedIds.includes(id)
4✔
1121
            ? selectedIds.filter((selectedId) => selectedId !== id)
2✔
1122
            : [...selectedIds, id];
1123
    }
1124
    return selectedIds.includes(id)
59✔
1125
        ? []
1126
        : [id];
1127
};
1128

1129

1130
/**
1131
 * Returns a parsed title
1132
 * @param {string/object} title title of the group
1133
 * @param {string} locale
1134
 */
1135
export const getTitle = (title, locale = '') => {
1✔
1136
    let _title = title || '';
71✔
1137
    if (isObject(title)) {
71✔
1138
        const _locale = locale || getLocale();
4✔
1139
        _title = title[_locale] || title.default;
4✔
1140
    }
1141
    return _title.replace(/\./g, '/').replace(/\${dot}/g, '.');
71✔
1142
};
1143
/**
1144
 * flatten groups and subgroups in a single array
1145
 * @param {object[]} groups node to get the groups and subgroups
1146
 * @param {number} idx
1147
 * @params {boolean} wholeGroup, if true it returns the whole node
1148
 * @return {object[]} array of nodes (groups and subgroups)
1149
*/
1150
export const flattenGroups = (groups, idx = 0, wholeGroup = false) => {
1✔
1151
    return groups.filter((group) => group.nodes).reduce((acc, g) => {
44✔
1152
        acc.push(wholeGroup ? g : {label: g.title, value: g.id});
28✔
1153
        if (g.nodes.length > 0) {
28!
1154
            return acc.concat(flattenGroups(g.nodes, idx + 1, wholeGroup));
28✔
1155
        }
UNCOV
1156
        return acc;
×
1157
    }, []);
1158
};
1159

1160
LayersUtils = {
1✔
1161
    getGroupByName,
1162
    getLayerId,
1163
    hasWFSService,
1164
    normalizeLayer,
1165
    getNotEmptyGroup,
1166
    getLayersByGroup,
1167
    deepChange,
1168
    reorder: reorderFunc,
1169
    getRegGeoserverRule,
1170
    findGeoServerName,
1171
    isInsideResolutionsLimits,
1172
    visibleTimelineLayers
1173
};
1174

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