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

geosolutions-it / MapStore2 / 30263629182

27 Jul 2026 11:52AM UTC coverage: 75.764% (-0.03%) from 75.789%
30263629182

Pull #12664

github

web-flow
Merge d4ed16c34 into 85310a94b
Pull Request #12664: Multi View Identify #12595

36066 of 56768 branches covered (63.53%)

165 of 229 new or added lines in 8 files covered. (72.05%)

116 existing lines in 5 files now uncovered.

44874 of 59229 relevant lines covered (75.76%)

123.14 hits per line

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

77.92
/web/client/utils/GeoNodeUtils.js
1
/*
2
 * Copyright 2025, 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

10
import { v4 as uuid } from 'uuid';
11
import { isEmpty } from 'lodash';
12
import queryString from 'query-string';
13
import url from 'url';
14
import turfCenter from '@turf/center';
15

16
import { isImageServerUrl } from './ArcGISUtils';
17
import { getConfigProp } from './ConfigUtils';
18
import { getSupportedLocales, shortLocale, getMessageById } from './LocaleUtils';
19
import { ServerTypes } from './LayersUtils';
20
import { getPolygonFromExtent } from './CoordinatesUtils';
21

22
export const SOURCE_TYPES = {
3✔
23
    LOCAL: 'LOCAL',
24
    REMOTE: 'REMOTE'
25
};
26

27
export const FEATURE_INFO_FORMAT = 'TEMPLATE';
3✔
28

29
export const GXP_PTYPES = {
3✔
30
    'AUTO': 'gxp_wmscsource',
31
    'OWS': 'gxp_wmscsource',
32
    'WMS': 'gxp_wmscsource',
33
    'WFS': 'gxp_wmscsource',
34
    'WCS': 'gxp_wmscsource',
35
    'REST_MAP': 'gxp_arcrestsource',
36
    'REST_IMG': 'gxp_arcrestsource',
37
    'HGL': 'gxp_hglsource',
38
    'GN_WMS': 'gxp_geonodecataloguesource'
39
};
40

41
export const ResourceTypes = {
3✔
42
    DATASET: 'dataset',
43
    MAP: 'map',
44
    DOCUMENT: 'document',
45
    GEOSTORY: 'geostory',
46
    DASHBOARD: 'dashboard',
47
    VIEWER: 'mapviewer'
48
};
49

50
export const GEONODE_KEYWORDS_FILTER = 'filter{keywords.slug.in}';
3✔
51
export const GEONODE_CATEGORY_FILTER = 'filter{category.identifier.in}';
3✔
52
export const GEONODE_DOCUMENTS_ROW_VIEWER = 'GEONODE_DOCUMENTS_ROW_VIEWER';
3✔
53

54
const DEFAULT_PRESET_KEYS = {
3✔
55
    CATALOGS: 'catalog_list',
56
    DATASETS: 'dataset_list',
57
    DOCUMENTS: 'document_list',
58
    MAPS: 'map_list',
59
    VIEWER_COMMON: 'viewer_common',
60
    DATASET: 'dataset_viewer',
61
    DOCUMENT: 'document_viewer',
62
    MAP: 'map_viewer',
63
    MAP_DETAILS: 'map_details'
64
};
65

66
const DEFAULT_REST_API_PRESETS = {
3✔
67
    [DEFAULT_PRESET_KEYS.CATALOGS]: {
68
        "exclude": ["*"],
69
        "include": [
70
            "advertised", "category", "detail_url", "is_approved", "is_copyable", "is_published",
71
            "keywords", "owner", "perms", "pk", "raw_abstract", "resource_type", "subtype",
72
            "title", "executions", "thumbnail_url", "created", "favorite"
73
        ]
74
    },
75
    [DEFAULT_PRESET_KEYS.DATASETS]: {
76
        "exclude": ["*"],
77
        "include": [
78
            "advertised", "detail_url", "owner", "perms", "pk", "raw_abstract",
79
            "resource_type", "subtype", "title", "data", "executions", "thumbnail_url",
80
            "alternate", "links", "featureinfo_custom_template", "has_time",
81
            "default_style", "ptype", "extent", "is_approved", "is_published"
82
        ]
83
    },
84
    [DEFAULT_PRESET_KEYS.MAPS]: {
85
        "exclude": ["*"],
86
        "include": [
87
            "advertised", "detail_url", "data", "is_approved", "is_copyable",
88
            "is_published", "owner", "perms", "pk", "raw_abstract", "resource_type",
89
            "subtype", "title", "executions", "thumbnail_url"
90
        ]
91
    },
92
    [DEFAULT_PRESET_KEYS.DOCUMENTS]: {
93
        "exclude": ["*"],
94
        "include": [
95
            "pk", "raw_abstract", "resource_type", "subtype", "title", "data",
96
            "executions", "thumbnail_url", "alternate", "attribution", "href", "extension"
97
        ]
98
    },
99
    [DEFAULT_PRESET_KEYS.VIEWER_COMMON]: {
100
        "exclude": ["*"],
101
        "include": [
102
            "abstract", "advertised", "alternate", "attribution", "category", "created",
103
            "date", "date_type", "detail_url", "download_urls", "embed_url", "executions",
104
            "extent", "favorite", "group", "is_approved", "is_copyable", "is_published",
105
            "keywords", "language", "last_updated", "linked_resources", "links", "owner",
106
            "perms", "pk", "poc", "raw_abstract", "regions", "resource_type", "sourcetype",
107
            "subtype", "supplemental_information", "temporal_extent_end", "temporal_extent_start",
108
            "thumbnail_url", "title", "uuid", "metadata_uploaded_preserve", "featured"
109
        ]
110
    },
111
    [DEFAULT_PRESET_KEYS.MAP_DETAILS]: {
112
        "include": ["maplayers"]
113
    },
114
    [DEFAULT_PRESET_KEYS.MAP]: {
115
        "include": ["data", "maplayers"]
116
    },
117
    [DEFAULT_PRESET_KEYS.DOCUMENT]: {
118
        "include": ["href", "extension", "ll_bbox_polygon"]
119
    },
120
    [DEFAULT_PRESET_KEYS.DATASET]: {
121
        "include": [
122
            "featureinfo_custom_template", "dataset_ows_url", "default_style", "ptype",
123
            "store", "has_time", "attribute_set", "data"
124
        ]
125
    }
126
};
127

128
let apiPresets = {};
3✔
129

130
/**
131
 * Override API presets from a downstream project.
132
 * @param {object} presets - key/value pairs, e.g. { CATALOGS: 'catalog_lists' }
133
 */
134
export const setApiPreset = (presets) => {
3✔
UNCOV
135
    apiPresets = { ...apiPresets, ...presets };
×
136
};
137

138
/**
139
 * Get the current override value for a preset key, or undefined if not overridden.
140
 * @param {string} key - preset key (e.g. 'CATALOGS', 'VIEWER_COMMON')
141
 * @returns {string|undefined}
142
 */
143
export const getApiPreset = (key) => {
3✔
144
    return apiPresets[key];
57✔
145
};
146

147
/**
148
 * Resolve the query params to send for a given preset key.
149
 * Uses api_preset override if set, else falls back to DEFAULT_REST_API_PRESETS.
150
 * @param {string} key - preset key (e.g. 'CATALOGS', 'VIEWER_COMMON')
151
 * @returns {object} query params to spread into the request
152
 */
153
export const resolveApiPresetParams = (key) => {
3✔
154
    const override = getApiPreset(key);
57✔
155
    if (override) {
57!
UNCOV
156
        return { api_preset: override };
×
157
    }
158
    const defaultName = DEFAULT_PRESET_KEYS[key];
57✔
159
    const fallback = defaultName && DEFAULT_REST_API_PRESETS[defaultName];
57✔
160
    if (!fallback) {
57!
UNCOV
161
        return {};
×
162
    }
163
    const params = {};
57✔
164
    if (fallback.include) {
57!
165
        params.include = fallback.include;
57✔
166
    }
167
    if (fallback.exclude) {
57✔
168
        params.exclude = fallback.exclude;
30✔
169
    }
170
    return params;
57✔
171
};
172

173
/**
174
 * Params serializer to format include/exclude/sort as bracket arrays.
175
 * @returns {object} axios config with paramsSerializer
176
 */
177
export const paramsSerializer = () => {
3✔
178
    return {
30✔
179
        paramsSerializer: {
180
            serialize: params => {
181
                const {include, exclude, sort, ...rest} = params ?? {};
3!
182
                let queryParams = '';
3✔
183
                if (!isEmpty(include) || !isEmpty(exclude) || !isEmpty(sort)) {
3!
184
                    queryParams = queryString.stringify({include, exclude, sort}, { arrayFormat: 'bracket'});
3✔
185
                }
186
                if (!isEmpty(rest)) {
3!
187
                    queryParams = (isEmpty(queryParams) ? '' : `${queryParams}&`) + queryString.stringify(rest);
3!
188
                }
189
                return queryParams;
3✔
190
            }
191
        }
192
    };
193
};
194

195
/**
196
 * Merge resolved preset params for one or more preset keys.
197
 * When all presets resolve to api_preset strings, combines them into an array.
198
 * When any preset resolves to include/exclude, merges the arrays.
199
 * @param {...string} keys - preset keys (e.g. 'VIEWER_COMMON', 'DATASET')
200
 * @returns {object} merged query params
201
 */
202
export const mergePresetParams = (...keys) => {
3✔
203
    const allParams = keys.map(key => resolveApiPresetParams(key));
36✔
204
    const hasApiPreset = allParams.every(p => p.api_preset);
18✔
205
    if (hasApiPreset) {
18!
UNCOV
206
        return { api_preset: allParams.reduce((acc, p) => acc.concat(p.api_preset), []) };
×
207
    }
208
    const include = allParams.reduce((acc, p) => acc.concat(p.include || []), []);
36!
209
    const exclude = allParams.reduce((acc, p) => acc.concat(p.exclude || []), []);
36✔
210
    const merged = {};
18✔
211
    if (include.length) merged.include = include;
18!
212
    if (exclude.length) merged.exclude = exclude;
18!
213
    return merged;
18✔
214
};
215

216
/**
217
 * Returns the filter key and tag property used for filter values by tagFilterType.
218
 */
219
export const getTagConfig = (tagFilterType) => {
3✔
220
    if (tagFilterType === 'keyword') {
60!
UNCOV
221
        return { filterKey: GEONODE_KEYWORDS_FILTER, filterProp: 'slug' };
×
222
    }
223
    if (tagFilterType === 'category') {
60✔
224
        return { filterKey: GEONODE_CATEGORY_FILTER, filterProp: 'identifier' };
3✔
225
    }
226
    return { filterKey: GEONODE_KEYWORDS_FILTER, filterProp: 'slug' };
57✔
227
};
228

229
export const isDefaultDatasetSubtype = (subtype) => !subtype || ['vector', 'raster', 'remote', 'vector_time'].includes(subtype);
3✔
230

231
const datasetAttributeSetToFields = ({ attribute_set: attributeSet = [] }) => {
3✔
232
    return attributeSet
30✔
UNCOV
233
        .filter(({ attribute_type: type }) => !type.includes('gml:'))
×
234
        .map(({
235
            attribute,
236
            attribute_label: alias,
237
            attribute_type: type
238
        }) => {
UNCOV
239
            return {
×
240
                name: attribute,
241
                alias: alias,
242
                type: type
243
            };
244
        });
245
};
246

247
export const getDimensions = ({links, has_time: hasTime} = {}) => {
3✔
248
    const { url: wmsUrl } = links?.find(({ link_type: linkType }) => linkType === 'OGC:WMS') || {};
45✔
249
    const { url: wmtsUrl } = links?.find(({ link_type: linkType }) => linkType === 'OGC:WMTS') || {};
45✔
250
    const dimensions = [
45✔
251
        ...(hasTime ? [{
45✔
252
            name: 'time',
253
            source: {
254
                type: 'multidim-extension',
255
                url: wmtsUrl || (wmsUrl || '').split('/geoserver/')[0] + '/geoserver/gwc/service/wmts'
24✔
256
            }
257
        }] : [])
258
    ];
259
    return dimensions;
45✔
260
};
261

262
function getExtentFromResource({ extent }) {
263
    if (isEmpty(extent?.coords)) {
48!
264
        return null;
48✔
265
    }
266
    const [minx, miny, maxx, maxy] = extent.coords;
×
267

UNCOV
268
    const WGS84_MAX_EXTENT = [-180, -90, 180, 90];
×
UNCOV
269
    if (minx < WGS84_MAX_EXTENT[0] || miny < WGS84_MAX_EXTENT[1] || maxx > WGS84_MAX_EXTENT[2] || maxy > WGS84_MAX_EXTENT[3]) {
×
UNCOV
270
        return {
×
271
            crs: 'EPSG:4326',
272
            bounds: {
273
                minx: WGS84_MAX_EXTENT[0],
274
                miny: WGS84_MAX_EXTENT[1],
275
                maxx: WGS84_MAX_EXTENT[2],
276
                maxy: WGS84_MAX_EXTENT[3]
277
            }
278
        };
279
    }
280
    const bbox = {
×
281
        crs: 'EPSG:4326',
282
        bounds: { minx, miny, maxx, maxy }
283
    };
UNCOV
284
    return bbox;
×
285
}
286

287
const getLocalizedValue = (resource, key, locale = '') => {
3!
288
    if (resource[`${key}_${locale}`]) {
291!
UNCOV
289
        return resource[`${key}_${locale}`];
×
290
    }
291
    const lang = shortLocale(locale);
291✔
292
    if (lang && resource[`${key}_${lang}`]) {
291✔
293
        return resource[`${key}_${lang}`];
9✔
294
    }
295
    return null;
282✔
296
};
297

298
const getLocalizedValues = (resource, key, defaultValue) => {
3✔
299
    const supportedLocales = getSupportedLocales() || {};
48!
300
    const translations = Object.values(supportedLocales)
48✔
301
        .map(({ code }) => {
302
            const value = getLocalizedValue(resource, key, code);
291✔
303
            return value ? [code, value] : null;
291✔
304
        })
305
        .filter(value => value !== null);
291✔
306
    if (translations.length) {
48✔
307
        return { ...Object.fromEntries(translations), 'default': defaultValue };
3✔
308
    }
309
    return defaultValue;
45✔
310
};
311

312
const calculateBbox = (coordinates) => {
3✔
313
    const validCoords = (coordinates || []).filter(coord => coord && coord.length === 2);
15!
314
    if (validCoords.length === 0) {
15!
UNCOV
315
        return null;
×
316
    }
317
    const lons = validCoords.map(coord => coord[0]);
15✔
318
    const lats = validCoords.map(coord => coord[1]);
15✔
319
    return {
15✔
320
        bounds: {
321
            minx: Math.min(...lons),
322
            miny: Math.min(...lats),
323
            maxx: Math.max(...lons),
324
            maxy: Math.max(...lats)
325
        },
326
        crs: 'EPSG:4326'
327
    };
328
};
329

330
const documentMarkerSymbolizer = (glyph) => [{
45✔
331
    kind: 'Icon',
332
    size: 46,
333
    image: { args: [{ color: 'blue', glyph, shape: 'circle' }], name: 'msMarkerIcon' },
334
    anchor: 'bottom',
335
    rotate: 0,
336
    opacity: 1,
337
    symbolizerId: '01',
338
    msBringToFront: false,
339
    msHeightReference: 'none'
340
}];
341

342
const getDocumentsStyle = (locales) => ({
15✔
343
    format: 'geostyler',
344
    metadata: { editorType: 'visual' },
345
    body: {
346
        rules: [
347
            {   name: getMessageById(locales, 'catalog.subtypes.video'),
348
                ruleId: '01',
349
                mandatory: false,
350
                filter: ['&&', ['==', 'subtype', 'video']],
351
                symbolizers: documentMarkerSymbolizer('video-camera')
352
            },
353
            {   name: getMessageById(locales, 'catalog.subtypes.image'),
354
                ruleId: '02',
355
                mandatory: false,
356
                filter: ['&&', ['==', 'subtype', 'image']],
357
                symbolizers: documentMarkerSymbolizer('camera')
358
            },
359
            {   name: getMessageById(locales, 'catalog.subtypes.file'),
360
                ruleId: '03',
361
                mandatory: false,
362
                filter: ['&&', ['!=', 'subtype', 'image'], ['!=', 'subtype', 'video']],
363
                symbolizers: documentMarkerSymbolizer('file')
364
            }
365
        ]
366
    }
367
});
368

369
/**
370
* convert resource layer configuration to a mapstore layer object
371
* @param {object} resource geonode layer resource
372
* @return {object}
373
*/
374
export const resourceToLayerConfig = (resource, options) => {
3✔
375

376
    const {
377
        alternate,
378
        links = [],
3✔
379
        featureinfo_custom_template: template,
380
        title: defaultTitle,
381
        perms,
382
        pk,
383
        default_style: defaultStyle,
384
        ptype,
385
        subtype,
386
        sourcetype,
387
        data
388
    } = resource;
48✔
389

390
    const layerSettings = data?.layerSettings ?? data;
48✔
391

392
    const title = getLocalizedValues(resource, 'title', defaultTitle);
48✔
393

394
    const bbox = getExtentFromResource(resource);
48✔
395
    const defaultStyleParams = defaultStyle && {
48!
396
        defaultStyle: {
397
            title: defaultStyle.sld_title,
398
            name: defaultStyle.workspace ? `${defaultStyle.workspace}:${defaultStyle.name}` : defaultStyle.name
×
399
        }
400
    };
401

402
    const extendedParams = {
48✔
403
        pk,
404
        alternate
405
    };
406

407
    if (subtype === '3dtiles') {
48✔
408
        const { url: tilesetUrl } = links.find(({ extension }) => (extension === '3dtiles')) || {};
3!
409
        const { enableImageryOverlay } = options || {};
3✔
410
        return {
3✔
411
            id: uuid(),
412
            type: '3dtiles',
413
            title,
414
            url: tilesetUrl,
415
            ...(bbox && { bbox }),
3!
416
            visibility: true,
417
            ...(enableImageryOverlay && { enableImageryOverlay }),
3!
418
            ...layerSettings,
419
            extendedParams
420
        };
421
    }
422
    if (subtype === 'cog') {
45✔
423
        const { url: cogUrl } = links.find(({ extension }) => (extension === 'cog')) || {};
6!
424
        return {
6✔
425
            perms,
426
            id: uuid(),
427
            type: 'cog',
428
            title,
429
            url: cogUrl,
430
            sources: [{
431
                url: cogUrl,
432
                nodata: 0
433
            }],
434
            ...(bbox && { bbox }),
6!
435
            visibility: true,
436
            ...layerSettings,
437
            extendedParams
438
        };
439
    }
440
    if (subtype === 'flatgeobuf') {
39✔
441
        const { url: fgbUrl } = links.find(({ extension }) => (extension === 'flatgeobuf')) || {};
3!
442
        return {
3✔
443
            perms,
444
            id: uuid(),
445
            type: 'flatgeobuf',
446
            title,
447
            url: fgbUrl,
448
            ...(bbox && { bbox }),
3!
449
            visibility: true,
450
            ...layerSettings,
451
            extendedParams
452
        };
453
    }
454

455
    switch (ptype) {
36✔
456
    case GXP_PTYPES.REST_MAP:
457
    case GXP_PTYPES.REST_IMG: {
458
        const { url: arcgisUrl } = links.find(({ mime, link_type: linkType }) => (mime === 'text/html' && linkType === 'image')) || {};
6!
459
        return {
6✔
460
            perms,
461
            id: uuid(),
462
            pk,
463
            type: 'arcgis',
464
            ...(isImageServerUrl(arcgisUrl)
6!
465
                ? { queryable: false }
466
                : { name: alternate.replace('remoteWorkspace:', '') }),
467
            url: arcgisUrl,
468
            ...(bbox && { bbox }),
6!
469
            title,
470
            visibility: true,
471
            ...layerSettings,
472
            extendedParams
473
        };
474
    }
475
    default:
476
        const { url: wfsUrl } = links.find(({ link_type: linkType }) => linkType === 'OGC:WFS') || {};
30✔
477
        const { url: wmsUrl } = links.find(({ link_type: linkType }) => linkType === 'OGC:WMS') || {};
30✔
478

479
        const dimensions = getDimensions(resource);
30✔
480

481
        const params = wmsUrl && url.parse(wmsUrl, true).query;
30✔
482
        const {
483
            defaultLayerFormat = 'image/png',
30✔
484
            defaultTileSize = 512
30✔
485
        } = getConfigProp('geoNodeSettings') || {};
30✔
486
        const fields = datasetAttributeSetToFields(resource);
30✔
487
        return {
30✔
488
            perms,
489
            id: uuid(),
490
            pk,
491
            type: 'wms',
492
            name: alternate,
493
            url: wmsUrl || '',
33✔
494
            format: defaultLayerFormat,
495
            ...(wfsUrl && {
30!
496
                search: {
497
                    type: 'wfs',
498
                    url: wfsUrl
499
                }
500
            }),
501
            ...(bbox ? { bbox } : { bboxError: true }),
30!
502
            ...(template && {
30!
503
                featureInfo: {
504
                    format: FEATURE_INFO_FORMAT,
505
                    template
506
                }
507
            }),
508
            style: defaultStyleParams?.defaultStyle?.name || '',
60✔
509
            title,
510
            tileSize: defaultTileSize,
511
            visibility: true,
512
            ...(params && { params }),
57✔
513
            ...(dimensions.length > 0 && ({ dimensions })),
30!
514
            ...(fields && { fields }),
60✔
515
            ...(sourcetype === SOURCE_TYPES.REMOTE && !wmsUrl.includes('/geoserver/') && {
30!
516
                serverType: ServerTypes.NO_VENDOR
517
            }),
518
            ...layerSettings,
519
            extendedParams
520
        };
521
    }
522
};
523

524

525
/**
526
 * Build a single MapStore vector layer that collects the given GeoNode documents
527
 * as point features (located at the center of each document extent). Documents
528
 * without an extent are skipped.
529
 * @param {array} documents - array of GeoNode document resources
530
 * @param {object} options - options object, may include locales
531
 * @returns {object} a MapStore vector layer config
532
 */
533
export const documentsToLayerConfig = (documents = [], locales) => {
3!
534
    const features = documents
15✔
535
        .map((doc) => {
536
            const extent = doc?.extent?.coords;
21✔
537
            const polygon = !isEmpty(extent) ? getPolygonFromExtent(extent) : null;
21✔
538
            const center = polygon ? turfCenter(polygon) : null;
21✔
539
            if (!center) {
21✔
540
                return null;
6✔
541
            }
542
            return {
15✔
543
                type: 'Feature',
544
                properties: doc,
545
                geometry: {
546
                    type: 'Point',
547
                    coordinates: center.geometry.coordinates
548
                },
549
                id: doc.pk
550
            };
551
        })
552
        .filter(Boolean);
553
    const bbox = calculateBbox(features.map(feature => feature.geometry.coordinates));
15✔
554
    return {
15✔
555
        id: uuid(),
556
        type: 'vector',
557
        visibility: true,
558
        name: 'Documents',
559
        title: `${getMessageById(locales, 'catalog.resourceTypes.document')} (${features.length})`,
560
        ...(bbox && { bbox }),
30✔
561
        features,
562
        style: getDocumentsStyle(locales),
563
        rowViewer: GEONODE_DOCUMENTS_ROW_VIEWER
564
    };
565
};
566

567
/**
568
* Convert a GeoNode map resource into structure `{ layers, groups }` ready for the catalog container:
569
* all the map layers exclude backgrounds, nested under a new parent group titled how the map name.
570
* @param {object} - GeoNode map resource
571
* @returns {object} - `{ layers, groups }` where `layers` is an array of layer objects and `groups` is an array of group objects
572
*/
573
export const resourceMapToLayerGroup = (mapResource) => {
3✔
UNCOV
574
    const mapConfig = mapResource?.data?.map || {};
×
UNCOV
575
    const parentId = `Default.${uuid()}`;
×
UNCOV
576
    const parentGroup = {
×
577
        title: mapResource?.title,
578
        parent: 'Default',
579
        options: { id: parentId },
580
        asFirst: true
581
    };
UNCOV
582
    const oldToNewPath = { Default: parentId };
×
UNCOV
583
    const groups = (mapConfig.groups || [])
×
UNCOV
584
        .filter((group) => group?.id && group.id !== 'Default')
×
UNCOV
585
        .sort((a, b) => a.id.split('.').length - b.id.split('.').length)
×
586
        .map((group) => {
UNCOV
587
            const segments = group.id.split('.');
×
UNCOV
588
            const oldParentPath = segments.slice(0, -1).join('.');
×
UNCOV
589
            const newParentPath = oldToNewPath[oldParentPath] || parentId;
×
UNCOV
590
            const newSegment = uuid();
×
UNCOV
591
            const newPath = `${newParentPath}.${newSegment}`;
×
UNCOV
592
            oldToNewPath[group.id] = newPath;
×
UNCOV
593
            const { id, title, ...groupOptions } = group;
×
UNCOV
594
            return {
×
595
                title,
596
                parent: newParentPath,
597
                options: { ...groupOptions, id: newPath, name: newSegment },
598
                asFirst: false
599
            };
600
        });
UNCOV
601
    const layers = (mapConfig.layers || [])
×
UNCOV
602
        .filter((layer) => layer?.group !== 'background')
×
UNCOV
603
        .map((layer) => ({
×
604
            ...layer,
605
            id: uuid(),
606
            group: oldToNewPath[layer?.group] || parentId
×
607
        }));
UNCOV
608
    return { layers, groups: [parentGroup, ...groups] };
×
609
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc