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

geosolutions-it / MapStore2 / 3756959745

pending completion
3756959745

push

github

allyoucanmap
#8872 WMTS - No persistency for TileMatrixSetLimits il TileMatrixSet is within a layer (#8881)

26127 of 40140 branches covered (65.09%)

94 of 94 new or added lines in 8 files covered. (100.0%)

32411 of 41698 relevant lines covered (77.73%)

42.91 hits per line

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

85.92
/web/client/utils/MapUtils.js
1
/*
2
 * Copyright 2015-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 {
10
    isString,
11
    trim,
12
    isNumber,
13
    pick,
14
    get,
15
    find,
16
    mapKeys,
17
    mapValues,
18
    keys,
19
    uniq,
20
    uniqWith,
21
    isEqual,
22
    isEmpty,
23
    findIndex,
24
    cloneDeep,
25
    minBy,
26
    omit
27
} from 'lodash';
28

29
import uuidv1 from 'uuid/v1';
30

31
import { getExtentForProjection, getUnits, normalizeSRS, reproject } from './CoordinatesUtils';
32
import { set } from './ImmutableUtils';
33
import {
34
    saveLayer,
35
    getGroupNodes,
36
    getNode,
37
    extractSourcesFromLayers,
38
    updateAvailableTileMatrixSetsOptions,
39
    getTileMatrixSetLink
40
} from './LayersUtils';
41
import assign from 'object-assign';
42

43
export const DEFAULT_MAP_LAYOUT = {left: {sm: 300, md: 500, lg: 600}, right: {md: 548}, bottom: {sm: 30}};
1✔
44

45
export const DEFAULT_SCREEN_DPI = 96;
1✔
46

47
export const METERS_PER_UNIT = {
1✔
48
    'm': 1,
49
    'degrees': 111194.87428468118,
50
    'ft': 0.3048,
51
    'us-ft': 1200 / 3937
52
};
53

54
export const GOOGLE_MERCATOR = {
1✔
55
    RADIUS: 6378137,
56
    TILE_WIDTH: 256,
57
    ZOOM_FACTOR: 2
58
};
59

60
export const EMPTY_MAP = 'EMPTY_MAP';
1✔
61

62
import proj4 from "proj4";
63

64
export const EXTENT_TO_ZOOM_HOOK = 'EXTENT_TO_ZOOM_HOOK';
1✔
65

66
/**
67
 * `ZOOM_TO_EXTENT_HOOK` hook takes 2 arguments:
68
 * - `extent`: array of the extent [minx, miny, maxx, maxy]
69
 * - `options` object, with the following attributes:
70
 *   - `crs`: crs of the extent
71
 *   - `maxZoom`: max zoom for the zoom to functionality.
72
 *   - `padding`: object with attributes, `top`, `right`, `bottom` and `top` with the size, in pixels of the padding for the visible part of the map. When supported by the mapping lib, it will zoom to visible area
73
 */
74
export const ZOOM_TO_EXTENT_HOOK = 'ZOOM_TO_EXTENT_HOOK';
1✔
75
export const RESOLUTIONS_HOOK = 'RESOLUTIONS_HOOK';
1✔
76
export const RESOLUTION_HOOK = 'RESOLUTION_HOOK';
1✔
77
export const COMPUTE_BBOX_HOOK = 'COMPUTE_BBOX_HOOK';
1✔
78
export const GET_PIXEL_FROM_COORDINATES_HOOK = 'GET_PIXEL_FROM_COORDINATES_HOOK';
1✔
79
export const GET_COORDINATES_FROM_PIXEL_HOOK = 'GET_COORDINATES_FROM_PIXEL_HOOK';
1✔
80
export const CLICK_ON_MAP_HOOK = 'CLICK_ON_MAP_HOOK';
1✔
81

82
let hooks = {};
1✔
83

84

85
export function registerHook(name, hook) {
86
    hooks[name] = hook;
803✔
87
}
88

89
export function getHook(name) {
90
    return hooks[name];
639✔
91
}
92

93
export function executeHook(hookName, existCallback, dontExistCallback) {
94
    const hook = getHook(hookName);
18✔
95
    if (hook) {
18✔
96
        return existCallback(hook);
11✔
97
    }
98
    if (dontExistCallback) {
7!
99
        return dontExistCallback();
×
100
    }
101
    return null;
7✔
102
}
103

104
export function clearHooks() {
105
    hooks = {};
39✔
106
}
107

108
/**
109
 * @param dpi {number} dot per inch resolution
110
 * @return {number} dot per meter resolution
111
 */
112
export function dpi2dpm(dpi) {
113
    return dpi * (100 / 2.54);
6,561✔
114
}
115

116
/**
117
 * @param dpi {number} screen resolution in dots per inch.
118
 * @param projection {string} map projection.
119
 * @return {number} dots per map unit.
120
 */
121
export function dpi2dpu(dpi, projection) {
122
    const units = getUnits(projection || "EPSG:3857");
417✔
123
    return METERS_PER_UNIT[units] * dpi2dpm(dpi || DEFAULT_SCREEN_DPI);
417✔
124
}
125

126
/**
127
 * @param radius {number} Earth's radius of the model in meters.
128
 * @param tileWidth {number} width of the tiles used to draw the map.
129
 * @param zoomFactor {number} zoom factor.
130
 * @param zoomLvl {number} target zoom level.
131
 * @param dpi {number} screen resolution in dot per inch.
132
 * @return {number} the scale of the showed map.
133
 */
134
export function getSphericalMercatorScale(radius, tileWidth, zoomFactor, zoomLvl, dpi) {
135
    return 2 * Math.PI * radius / (tileWidth * Math.pow(zoomFactor, zoomLvl) / dpi2dpm(dpi || DEFAULT_SCREEN_DPI));
6,141✔
136
}
137

138
/**
139
 * @param zoomLvl {number} target zoom level.
140
 * @param dpi {number} screen resolution in dot per inch.
141
 * @return {number} the scale of the showed map.
142
 */
143
export function getGoogleMercatorScale(zoomLvl, dpi) {
144
    return getSphericalMercatorScale(GOOGLE_MERCATOR.RADIUS, GOOGLE_MERCATOR.TILE_WIDTH, GOOGLE_MERCATOR.ZOOM_FACTOR, zoomLvl, dpi);
31✔
145
}
146

147
/**
148
 * @param radius {number} Earth's radius of the model in meters.
149
 * @param tileWidth {number} width of the tiles used to draw the map.
150
 * @param zoomFactor {number} zoom factor.
151
 * @param minZoom {number} min zoom level.
152
 * @param maxZoom {number} max zoom level.
153
 * @param dpi {number} screen resolution in dot per inch.
154
 * @return {array} a list of scale for each zoom level in the given interval.
155
 */
156
export function getSphericalMercatorScales(radius, tileWidth, zoomFactor, minZoom, maxZoom, dpi) {
157
    var retval = [];
279✔
158
    for (let l = minZoom; l <= maxZoom; l++) {
279✔
159
        retval.push(
6,106✔
160
            getSphericalMercatorScale(
161
                radius,
162
                tileWidth,
163
                zoomFactor,
164
                l,
165
                dpi
166
            )
167
        );
168
    }
169
    return retval;
279✔
170
}
171

172
/**
173
 * Get a list of scales for each zoom level of the Google Mercator.
174
 * @param minZoom {number} min zoom level.
175
 * @param maxZoom {number} max zoom level.
176
 * @return {array} a list of scale for each zoom level in the given interval.
177
 */
178
export function getGoogleMercatorScales(minZoom, maxZoom, dpi) {
179
    return getSphericalMercatorScales(
278✔
180
        GOOGLE_MERCATOR.RADIUS,
181
        GOOGLE_MERCATOR.TILE_WIDTH,
182
        GOOGLE_MERCATOR.ZOOM_FACTOR,
183
        minZoom,
184
        maxZoom,
185
        dpi
186
    );
187
}
188

189
/**
190
 * @param scales {array} list of scales.
191
 * @param projection {string} map projection.
192
 * @param dpi {number} screen resolution in dots per inch.
193
 * @return {array} a list of resolutions corresponding to the given scales, projection and dpi.
194
 */
195
export function getResolutionsForScales(scales, projection, dpi) {
196
    const dpu = dpi2dpu(dpi, projection);
285✔
197
    const resolutions = scales.map((scale) => {
285✔
198
        return scale / dpu;
6,068✔
199
    });
200
    return resolutions;
285✔
201
}
202

203
export function getGoogleMercatorResolutions(minZoom, maxZoom, dpi) {
204
    return getResolutionsForScales(getGoogleMercatorScales(minZoom, maxZoom, dpi), "EPSG:3857", dpi);
267✔
205
}
206

207
/**
208
 * Calculates resolutions accordingly with default algorithm in GeoWebCache.
209
 * See this: https://github.com/GeoWebCache/geowebcache/blob/5e913193ff50a61ef9dd63a87887189352fa6b21/geowebcache/core/src/main/java/org/geowebcache/grid/GridSetFactory.java#L196
210
 * It allows to have the resolutions aligned to the default generated grid sets on server side.
211
 * **NOTES**: this solution doesn't support:
212
 * - custom grid sets with `alignTopLeft=true` (e.g. GlobalCRS84Pixel). Custom resolutions will need to be configured as `mapOptions.view.resolutions`
213
 * - custom grid set with custom extent. You need to customize the projection definition extent to make it work.
214
 * - custom grid set is partially supported by mapOptions.view.resolutions but this is not managed by projection change yet
215
 * - custom tile sizes
216
 *
217
 */
218
export function getResolutionsForProjection(srs, minRes, maxRes, minZ, maxZ, zoomF, ext) {
219
    const tileWidth = 256; // TODO: pass as parameters
118✔
220
    const tileHeight = 256; // TODO: pass as parameters - allow different from tileWidth
118✔
221

222
    const defaultMaxZoom = 28;
118✔
223
    const defaultZoomFactor = 2;
118✔
224

225
    let minZoom = minZ ?? 0;
118✔
226

227
    let maxZoom = maxZ ?? defaultMaxZoom;
118✔
228

229
    let zoomFactor = zoomF ?? defaultZoomFactor;
118✔
230

231
    const projection = proj4.defs(srs);
118✔
232

233
    const extent = ext ?? getExtentForProjection(srs)?.extent;
118✔
234

235
    const extentWidth = !extent ? 360 * METERS_PER_UNIT.degrees /
118!
236
        METERS_PER_UNIT[projection.getUnits()] :
237
        extent[2] - extent[0];
238
    const extentHeight = !extent ? 360 * METERS_PER_UNIT.degrees /
118!
239
        METERS_PER_UNIT[projection.getUnits()] :
240
        extent[3] - extent[1];
241

242
    let resX = extentWidth / tileWidth;
118✔
243
    let resY = extentHeight / tileHeight;
118✔
244
    let tilesWide;
245
    let tilesHigh;
246
    if (resX <= resY) {
118✔
247
        // use one tile wide by N tiles high
248
        tilesWide = 1;
97✔
249
        tilesHigh = Math.round(resY / resX);
97✔
250
        // previous resY was assuming 1 tile high, recompute with the actual number of tiles
251
        // high
252
        resY = resY / tilesHigh;
97✔
253
    } else {
254
        // use one tile high by N tiles wide
255
        tilesHigh = 1;
21✔
256
        tilesWide = Math.round(resX / resY);
21✔
257
        // previous resX was assuming 1 tile wide, recompute with the actual number of tiles
258
        // wide
259
        resX = resX / tilesWide;
21✔
260
    }
261
    // the maximum of resX and resY is the one that adjusts better
262
    const res = Math.max(resX, resY);
118✔
263

264
    /*
265
        // TODO: this is how GWC creates the bbox adjusted.
266
        // We should calculate it to have the correct extent for a grid set
267
        const adjustedExtentWidth = tilesWide * tileWidth * res;
268
        const adjustedExtentHeight = tilesHigh * tileHeight * res;
269
        BoundingBox adjExtent = new BoundingBox(extent);
270
        adjExtent.setMaxX(adjExtent.getMinX() + adjustedExtentWidth);
271
        // Do we keep the top or the bottom fixed?
272
        if (alignTopLeft) {
273
            adjExtent.setMinY(adjExtent.getMaxY() - adjustedExtentHeight);
274
        } else {
275
            adjExtent.setMaxY(adjExtent.getMinY() + adjustedExtentHeight);
276

277
     */
278

279
    const defaultMaxResolution = res;
118✔
280

281
    const defaultMinResolution = defaultMaxResolution / Math.pow(
118✔
282
        defaultZoomFactor, defaultMaxZoom - 0);
283

284
    // user provided maxResolution takes precedence
285
    let maxResolution = maxRes;
118✔
286
    if (maxResolution !== undefined) {
118!
287
        minZoom = 0;
×
288
    } else {
289
        maxResolution = defaultMaxResolution / Math.pow(zoomFactor, minZoom);
118✔
290
    }
291

292
    // user provided minResolution takes precedence
293
    let minResolution = minRes;
118✔
294
    if (minResolution === undefined) {
118!
295
        if (maxZoom !== undefined) {
118!
296
            if (maxRes !== undefined) {
118!
297
                minResolution = maxResolution / Math.pow(zoomFactor, maxZoom);
×
298
            } else {
299
                minResolution = defaultMaxResolution / Math.pow(zoomFactor, maxZoom);
118✔
300
            }
301
        } else {
302
            minResolution = defaultMinResolution;
×
303
        }
304
    }
305

306
    // given discrete zoom levels, minResolution may be different than provided
307
    maxZoom = minZoom + Math.floor(
118✔
308
        Math.log(maxResolution / minResolution) / Math.log(zoomFactor));
309
    return Array.apply(0, Array(maxZoom - minZoom + 1)).map((x, y) => maxResolution / Math.pow(zoomFactor, y));
3,422✔
310
}
311

312
export function getResolutions(projection) {
313
    if (getHook('RESOLUTIONS_HOOK')) {
357✔
314
        return getHook('RESOLUTIONS_HOOK')(projection);
139✔
315
    }
316
    return projection && normalizeSRS(projection) !== "EPSG:3857" ? getResolutionsForProjection(projection) :
218✔
317
        getGoogleMercatorResolutions(0, 21, DEFAULT_SCREEN_DPI);
318
}
319

320
export function getScales(projection, dpi) {
321
    const dpu = dpi2dpu(dpi, projection);
40✔
322
    return getResolutions(projection).map((resolution) => resolution * dpu);
961✔
323
}
324
/**
325
 * Convert a resolution to the nearest zoom
326
 * @param {number} targetResolution resolution to be converted in zoom
327
 * @param {array} resolutions list of all available resolutions
328
 */
329
export function getZoomFromResolution(targetResolution, resolutions = getResolutions()) {
1✔
330
    // compute the absolute difference for all resolutions
331
    // and store the idx as zoom
332
    const diffs = resolutions.map((resolution, zoom) => ({ diff: Math.abs(resolution - targetResolution), zoom }));
714✔
333
    // the minimum difference represents the nearest zoom to the target resolution
334
    const { zoom } = minBy(diffs, 'diff');
35✔
335
    return zoom;
35✔
336
}
337

338
export function defaultGetZoomForExtent(extent, mapSize, minZoom, maxZoom, dpi, mapResolutions) {
339
    const wExtent = extent[2] - extent[0];
11✔
340
    const hExtent = extent[3] - extent[1];
11✔
341

342
    const xResolution = Math.abs(wExtent / mapSize.width);
11✔
343
    const yResolution = Math.abs(hExtent / mapSize.height);
11✔
344
    const extentResolution = Math.max(xResolution, yResolution);
11✔
345

346
    const resolutions = mapResolutions || getResolutionsForScales(getGoogleMercatorScales(
11✔
347
        minZoom, maxZoom, dpi || DEFAULT_SCREEN_DPI), "EPSG:3857", dpi);
11✔
348

349
    const {zoom} = resolutions.reduce((previous, resolution, index) => {
11✔
350
        const diff = Math.abs(resolution - extentResolution);
252✔
351
        return diff > previous.diff ? previous : {diff: diff, zoom: index};
252✔
352
    }, {diff: Number.POSITIVE_INFINITY, zoom: 0});
353

354
    return Math.max(0, Math.min(zoom, maxZoom));
11✔
355
}
356

357
/**
358
 * Calculates the best fitting zoom level for the given extent.
359
 *
360
 * @param extent {Array} [minx, miny, maxx, maxy]
361
 * @param mapSize {Object} current size of the map.
362
 * @param minZoom {number} min zoom level.
363
 * @param maxZoom {number} max zoom level.
364
 * @param dpi {number} screen resolution in dot per inch.
365
 * @return {Number} the zoom level fitting th extent
366
 */
367
export function getZoomForExtent(extent, mapSize, minZoom, maxZoom, dpi) {
368
    if (getHook("EXTENT_TO_ZOOM_HOOK")) {
11✔
369
        return getHook("EXTENT_TO_ZOOM_HOOK")(extent, mapSize, minZoom, maxZoom, dpi);
1✔
370
    }
371
    const resolutions = getHook("RESOLUTIONS_HOOK") ?
10✔
372
        getHook("RESOLUTIONS_HOOK")() : null;
373
    return defaultGetZoomForExtent(extent, mapSize, minZoom, maxZoom, dpi, resolutions);
10✔
374
}
375

376
/**
377
* It returns the current resolution.
378
*
379
* @param currentZoom {number} the current zoom
380
* @param minZoom {number} min zoom level.
381
* @param maxZoom {number} max zoom level.
382
* @param dpi {number} screen resolution in dot per inch.
383
* @return {Number} the actual resolution
384
*/
385
export function getCurrentResolution(currentZoom, minZoom, maxZoom, dpi) {
386
    if (getHook("RESOLUTION_HOOK")) {
62✔
387
        return getHook("RESOLUTION_HOOK")(currentZoom, minZoom, maxZoom, dpi);
2✔
388
    }
389
    /* if no hook is registered (leaflet) it is used the GoogleMercatorResolutions in
390
       in order to get the list of resolutions */
391
    return getGoogleMercatorResolutions(minZoom, maxZoom, dpi)[currentZoom];
60✔
392
}
393

394
/**
395
 * Calculates the center for for the given extent.
396
 *
397
 * @param  {Array} extent [minx, miny, maxx, maxy]
398
 * @param  {String} projection projection of the extent
399
 * @return {object} center object
400
 */
401
export function getCenterForExtent(extent, projection) {
402

403
    var wExtent = extent[2] - extent[0];
10✔
404
    var hExtent = extent[3] - extent[1];
10✔
405

406
    var w = wExtent / 2;
10✔
407
    var h = hExtent / 2;
10✔
408

409
    return {
10✔
410
        x: extent[0] + w,
411
        y: extent[1] + h,
412
        crs: projection
413
    };
414
}
415

416
/**
417
 * Calculates the bounding box for the given center and zoom.
418
 *
419
 * @param  {object} center object
420
 * @param  {number} zoom level
421
 */
422
export function getBbox(center, zoom) {
423
    return executeHook("COMPUTE_BBOX_HOOK",
18✔
424
        (hook) => {
425
            return hook(center, zoom);
11✔
426
        }
427
    );
428
}
429

430
export const isNearlyEqual = function(a, b) {
1✔
431
    if (a === undefined || b === undefined) {
74!
432
        return false;
×
433
    }
434
    return a.toFixed(12) - b.toFixed(12) === 0;
74✔
435
};
436

437
/**
438
 * checks if maps has changed by looking at center or zoom
439
 * @param {object} oldMap map object
440
 * @param {object} newMap map object
441
 */
442
export function mapUpdated(oldMap, newMap) {
443
    if (oldMap && !isEmpty(oldMap) &&
38✔
444
        newMap && !isEmpty(newMap)) {
445
        const centersEqual = isNearlyEqual(newMap?.center?.x, oldMap?.center?.x) &&
33✔
446
                              isNearlyEqual(newMap?.center?.y, oldMap?.center?.y);
447
        return !centersEqual || newMap?.zoom !== oldMap?.zoom;
33✔
448
    }
449
    return false;
5✔
450
}
451

452
/* Transform width and height specified in meters to the units of the specified projection */
453
export function transformExtent(projection, center, width, height) {
454
    let units = getUnits(projection);
×
455
    if (units === 'ft') {
×
456
        return {width: width / METERS_PER_UNIT.ft, height: height / METERS_PER_UNIT.ft};
×
457
    } else if (units === 'us-ft') {
×
458
        return {width: width / METERS_PER_UNIT['us-ft'], height: height / METERS_PER_UNIT['us-ft']};
×
459
    } else if (units === 'degrees') {
×
460
        return {
×
461
            width: width / (111132.92 - 559.82 * Math.cos(2 * center.y) + 1.175 * Math.cos(4 * center.y)),
462
            height: height / (111412.84 * Math.cos(center.y) - 93.5 * Math.cos(3 * center.y))
463
        };
464
    }
465
    return {width, height};
×
466
}
467

468
export const groupSaveFormatted = (node) => {
1✔
469
    return {
24✔
470
        id: node.id,
471
        title: node.title,
472
        description: node.description,
473
        tooltipOptions: node.tooltipOptions,
474
        tooltipPlacement: node.tooltipPlacement,
475
        expanded: node.expanded
476
    };
477
};
478

479

480
export function saveMapConfiguration(currentMap, currentLayers, currentGroups, currentBackgrounds, textSearchConfig, bookmarkSearchConfig, additionalOptions) {
481

482
    const map = {
19✔
483
        center: currentMap.center,
484
        maxExtent: currentMap.maxExtent,
485
        projection: currentMap.projection,
486
        units: currentMap.units,
487
        mapInfoControl: currentMap.mapInfoControl,
488
        zoom: currentMap.zoom,
489
        mapOptions: currentMap.mapOptions || {}
34✔
490
    };
491

492
    const layers = currentLayers.map((layer) => {
18✔
493
        return saveLayer(layer);
28✔
494
    });
495

496
    const flatGroupId = currentGroups.reduce((a, b) => {
18✔
497
        const flatGroups = a.concat(getGroupNodes(b));
16✔
498
        return flatGroups;
16✔
499
    }, [].concat(currentGroups.map(g => g.id)));
16✔
500

501
    const groups = flatGroupId.map(g => {
18✔
502
        const node = getNode(currentGroups, g);
48✔
503
        return node && node.nodes ? groupSaveFormatted(node) : null;
48✔
504
    }).filter(g => g);
48✔
505

506
    const backgrounds = currentBackgrounds.filter(background => !!background.thumbnail);
18✔
507

508
    // extract sources map
509
    const sources = extractSourcesFromLayers(layers);
18✔
510

511
    // removes tile matrix set from layers and replace it with a link if available in sources
512
    const formattedLayers = layers.map(layer => {
18✔
513
        const { availableTileMatrixSets, ...updatedLayer } = updateAvailableTileMatrixSetsOptions(layer);
28✔
514
        return availableTileMatrixSets
28✔
515
            ? {
516
                ...updatedLayer,
517
                availableTileMatrixSets: Object.keys(availableTileMatrixSets)
518
                    .reduce((acc, tileMatrixSetId) => {
519
                        const tileMatrixSetLink = getTileMatrixSetLink(layer, tileMatrixSetId);
3✔
520
                        if (get({ sources }, tileMatrixSetLink)) {
3!
521
                            return {
3✔
522
                                ...acc,
523
                                [tileMatrixSetId]: {
524
                                    ...omit(availableTileMatrixSets[tileMatrixSetId], 'tileMatrixSet'),
525
                                    tileMatrixSetLink
526
                                }
527
                            };
528
                        }
529
                        return {
×
530
                            ...acc,
531
                            [tileMatrixSetId]: availableTileMatrixSets[tileMatrixSetId]
532
                        };
533
                    }, {})
534
            }
535
            : updatedLayer;
536
    });
537

538
    /* removes the geometryGeodesic property from the features in the annotations layer*/
539
    let annotationsLayerIndex = findIndex(formattedLayers, layer => layer.id === "annotations");
28✔
540
    if (annotationsLayerIndex !== -1) {
18✔
541
        let featuresLayer = formattedLayers[annotationsLayerIndex].features.map(feature => {
1✔
542
            if (feature.type === "FeatureCollection") {
1!
543
                return {
1✔
544
                    ...feature,
545
                    features: feature.features.map(f => {
546
                        if (f.properties.geometryGeodesic) {
1!
547
                            return set("properties.geometryGeodesic", null, f);
1✔
548
                        }
549
                        return f;
×
550
                    })
551
                };
552
            }
553
            if (feature.properties.geometryGeodesic) {
×
554
                return set("properties.geometryGeodesic", null, feature);
×
555
            }
556
            return {};
×
557
        });
558
        formattedLayers[annotationsLayerIndex] = set("features", featuresLayer, formattedLayers[annotationsLayerIndex]);
1✔
559
    }
560

561
    return {
18✔
562
        version: 2,
563
        // layers are defined inside the map object
564
        map: assign({}, map, {layers: formattedLayers, groups, backgrounds, text_search_config: textSearchConfig, bookmark_search_config: bookmarkSearchConfig},
565
            !isEmpty(sources) && {sources} || {}),
36✔
566
        ...additionalOptions
567
    };
568
}
569

570
export const generateNewUUIDs = (mapConfig = {}) => {
1!
571
    const newMapConfig = cloneDeep(mapConfig);
1✔
572

573
    const oldIdToNew = {
1✔
574
        ...get(mapConfig, 'map.layers', []).reduce((result, layer) => ({
4✔
575
            ...result,
576
            [layer.id]: layer.id === 'annotations' ? layer.id : uuidv1()
4!
577
        }), {}),
578
        ...get(mapConfig, 'widgetsConfig.widgets', []).reduce((result, widget) => ({...result, [widget.id]: uuidv1()}), {})
1✔
579
    };
580

581
    return set('map.backgrounds', get(mapConfig, 'map.backgrounds', []).map(background => ({...background, id: oldIdToNew[background.id]})),
1✔
582
        set('widgetsConfig', {
583
            collapsed: mapValues(mapKeys(get(mapConfig, 'widgetsConfig.collapsed', {}), (value, key) => oldIdToNew[key]), (value) =>
1✔
584
                ({...value, layouts: mapValues(value.layouts, (layout) => ({...layout, i: oldIdToNew[layout.i]}))})),
2✔
585
            layouts: mapValues(get(mapConfig, 'widgetsConfig.layouts', {}), (value) =>
586
                value.map(layout => ({...layout, i: oldIdToNew[layout.i]}))),
2✔
587
            widgets: get(mapConfig, 'widgetsConfig.widgets', [])
588
                .map(widget => ({
1✔
589
                    ...widget,
590
                    id: oldIdToNew[widget.id],
591
                    layer: ({...get(widget, 'layer', {}), id: oldIdToNew[get(widget, 'layer.id')]})
592
                }))
593
        },
594
        set('map.layers', get(mapConfig, 'map.layers', [])
595
            .map(layer => ({...layer, id: oldIdToNew[layer.id]})), newMapConfig)));
4✔
596
};
597

598
export const mergeMapConfigs = (cfg1 = {}, cfg2 = {}) => {
1!
599
    // removes empty props from layer as it can cause bugs
600
    const fixLayers = (layers = []) => layers.map(layer => pick(layer, keys(layer).filter(key => layer[key] !== undefined)));
23!
601

602
    const cfg2Fixed = generateNewUUIDs(cfg2);
1✔
603

604
    const backgrounds = [...get(cfg1, 'map.backgrounds', []), ...get(cfg2Fixed, 'map.backgrounds', [])];
1✔
605

606
    const layers1 = fixLayers(get(cfg1, 'map.layers', []));
1✔
607
    const layers2 = fixLayers(get(cfg2Fixed, 'map.layers', []));
1✔
608

609
    const annotationsLayer1 = find(layers1, layer => layer.id === 'annotations');
4✔
610
    const annotationsLayer2 = find(layers2, layer => layer.id === 'annotations');
4✔
611

612
    const layers = [
1✔
613
        ...layers2.filter(layer => layer.id !== 'annotations'),
4✔
614
        ...layers1.filter(layer => layer.id !== 'annotations'),
5✔
615
        ...(annotationsLayer1 || annotationsLayer2 ? [{
2!
616
            ...(annotationsLayer1 || {}),
1!
617
            ...(annotationsLayer2 || {}),
2✔
618
            features: [
619
                ...get(annotationsLayer1, 'features', []), ...get(annotationsLayer2, 'features', [])
620
            ]
621
        }] : [])
622
    ];
623
    const toleratedFields = ['id', 'visibility'];
1✔
624
    const backgroundLayers = layers.filter(layer => layer.group === 'background')
9✔
625
        // remove duplication by comparing all fields with some level of tolerance
626
        .filter((l1, i, a) => findIndex(a, (l2) => isEqual(omit(l1, toleratedFields), omit(l2, toleratedFields))) === i);
5✔
627
    const firstVisible = findIndex(backgroundLayers, layer => layer.visibility);
2✔
628

629
    const sources1 = get(cfg1, 'map.sources', {});
1✔
630
    const sources2 = get(cfg2Fixed, 'map.sources', {});
1✔
631
    const sources = {...sources1, ...sources2};
1✔
632

633
    const widgetsConfig1 = get(cfg1, 'widgetsConfig', {});
1✔
634
    const widgetsConfig2 = get(cfg2Fixed, 'widgetsConfig', {});
1✔
635

636
    return {
1✔
637
        ...cfg2Fixed,
638
        ...cfg1,
639
        catalogServices: {
640
            ...get(cfg1, 'catalogServices', {}),
641
            services: {
642
                ...get(cfg1, 'catalogServices.services', {}),
643
                ...get(cfg2Fixed, 'catalogServices.services', {})
644
            }
645
        },
646
        map: {
647
            ...cfg2Fixed.map,
648
            ...cfg1.map,
649
            backgrounds,
650
            groups: uniqWith([...get(cfg1, 'map.groups', []), ...get(cfg2Fixed, 'map.groups', [])],
651
                (group1, group2) => group1.id === group2.id),
4✔
652
            layers: [
653
                ...backgroundLayers.slice(0, firstVisible + 1),
654
                ...backgroundLayers.slice(firstVisible + 1).map(layer => ({...layer, visibility: false})),
2✔
655
                ...layers.filter(layer => layer.group !== 'background')
9✔
656
            ],
657
            sources: !isEmpty(sources) ? sources : undefined
1!
658
        },
659
        widgetsConfig: {
660
            collapsed: {...widgetsConfig1.collapsed, ...widgetsConfig2.collapsed},
661
            layouts: uniq([...keys(widgetsConfig1.layouts), ...keys(widgetsConfig2.layouts)])
662
                .reduce((result, key) => ({
2✔
663
                    ...result,
664
                    [key]: [
665
                        ...get(widgetsConfig1, `layouts.${key}`, []),
666
                        ...get(widgetsConfig2, `layouts.${key}`, [])
667
                    ]
668
                }), {}),
669
            widgets: [...get(widgetsConfig1, 'widgets', []), ...get(widgetsConfig2, 'widgets', [])]
670
        },
671
        timelineData: {
672
            ...get(cfg1, 'timelineData', {}),
673
            ...get(cfg2Fixed, 'timelineData', {})
674
        },
675
        dimensionData: {
676
            ...get(cfg1, 'dimensionData', {}),
677
            ...get(cfg2Fixed, 'dimensionData', {})
678
        }
679
    };
680
};
681

682
export const addRootParentGroup = (cfg = {}, groupTitle = 'RootGroup') => {
1!
683
    const groups = get(cfg, 'map.groups', []);
1✔
684
    const groupsWithoutDefault = groups.filter(({id}) => id !== 'Default');
3✔
685
    const defaultGroup = find(groups, ({id}) => id === 'Default');
1✔
686
    const fixedDefaultGroup = defaultGroup && {
1✔
687
        id: uuidv1(),
688
        title: groupTitle,
689
        expanded: defaultGroup.expanded
690
    };
691
    const groupsWithFixedDefault = defaultGroup ?
1!
692
        [
693
            ...groupsWithoutDefault.map(({id, ...other}) => ({
2✔
694
                id: `${fixedDefaultGroup.id}.${id}`,
695
                ...other
696
            })),
697
            fixedDefaultGroup
698
        ] :
699
        groupsWithoutDefault;
700

701
    return {
1✔
702
        ...cfg,
703
        map: {
704
            ...cfg.map,
705
            groups: groupsWithFixedDefault,
706
            layers: get(cfg, 'map.layers', []).map(({group, ...other}) => ({
6✔
707
                ...other,
708
                group: defaultGroup && group !== 'background' && (group === 'Default' || !group) ? fixedDefaultGroup.id :
28✔
709
                    defaultGroup && find(groupsWithFixedDefault, ({id}) => id.slice(id.indexOf('.') + 1) === group)?.id || group
6✔
710
            }))
711
        }
712
    };
713
};
714

715
export function isSimpleGeomType(geomType) {
716
    switch (geomType) {
50✔
717
    case "MultiPoint": case "MultiLineString": case "MultiPolygon": case "GeometryCollection": case "Text": return false;
13✔
718
    case "Point": case "Circle": case "LineString": case "Polygon": default: return true;
37✔
719
    }
720
}
721
export function getSimpleGeomType(geomType = "Point") {
×
722
    switch (geomType) {
34✔
723
    case "Point": case "LineString": case "Polygon": case "Circle": return geomType;
19✔
724
    case "MultiPoint": case "Marker": return "Point";
4✔
725
    case "MultiLineString": return "LineString";
3✔
726
    case "MultiPolygon": return "Polygon";
3✔
727
    case "GeometryCollection": return "GeometryCollection";
3✔
728
    case "Text": return "Point";
1✔
729
    default: return geomType;
1✔
730
    }
731
}
732

733
export const getIdFromUri = (uri, regex = /data\/(\d+)/) => {
1✔
734
    // this decode is for backward compatibility with old linked resources`rest%2Fgeostore%2Fdata%2F2%2Fraw%3Fdecode%3Ddatauri` not needed for new ones `rest/geostore/data/2/raw?decode=datauri`
735
    const decodedUri = decodeURIComponent(uri);
15✔
736
    const findDataDigit = regex.exec(decodedUri);
15✔
737
    return findDataDigit && findDataDigit.length && findDataDigit.length > 1 ? findDataDigit[1] : null;
15✔
738
};
739

740
/**
741
 * Return parsed number from layout value
742
 * if percentage returns percentage of second argument that should be a number
743
 * eg. 20% of map height parseLayoutValue(20%, map.size.height)
744
 * but if value is stored as number it will return the number
745
 * eg. parseLayoutValue(50, map.size.height) returns 50
746
 * @param value {number|string} number or percentage value string
747
 * @param size {number} only in case of percentage
748
 * @return {number}
749
 */
750
export const parseLayoutValue = (value, size = 0) => {
1✔
751
    if (isString(value) && value.indexOf('%') !== -1) {
35✔
752
        return parseFloat(trim(value)) * size / 100;
6✔
753
    }
754
    return isNumber(value) ? value : 0;
29✔
755
};
756

757
/**
758
 * Method for cleanup map object from uneseccary fields which
759
 * updated map contains and were set on map render
760
 * @param {object} obj
761
 */
762

763
export const prepareMapObjectToCompare = obj => {
1✔
764
    const skippedKeys = ['apiKey', 'time', 'args', 'fixed'];
155✔
765
    const shouldBeSkipped = (key) => skippedKeys.reduce((p, n) => p || key === n, false);
1,008✔
766
    Object.keys(obj).forEach(key => {
155✔
767
        const value = obj[key];
467✔
768
        const type = typeof value;
467✔
769
        if (type === "object" && value !== null && !shouldBeSkipped(key)) {
467✔
770
            prepareMapObjectToCompare(value);
135✔
771
            if (!Object.keys(value).length) {
135✔
772
                delete obj[key];
73✔
773
            }
774
        } else if (type === "undefined" || !value || shouldBeSkipped(key)) {
332✔
775
            delete obj[key];
219✔
776
        }
777
    });
778
};
779

780
/**
781
 * Method added for support old key with objects provided for compareMapChanges feature
782
 * like text_serch_config
783
 * @param {object} obj
784
 * @param {string} oldKey
785
 * @param {string} newKey
786
 */
787
export const updateObjectFieldKey = (obj, oldKey, newKey) => {
1✔
788
    if (obj[oldKey]) {
15✔
789
        Object.defineProperty(obj, newKey, Object.getOwnPropertyDescriptor(obj, oldKey));
1✔
790
        delete obj[oldKey];
1✔
791
    }
792
};
793

794
/**
795
 * Feature for map change recognition. Returns value of isEqual method from lodash
796
 * @param {object} map1 original map before changes
797
 * @param {object} map2 updated map
798
 * @returns {boolean}
799
 */
800
export const compareMapChanges = (map1 = {}, map2 = {}) => {
1!
801
    const pickedFields = [
6✔
802
        'map.layers',
803
        'map.backgrounds',
804
        'map.text_search_config',
805
        'map.bookmark_search_config',
806
        'map.text_serch_config',
807
        'map.zoom',
808
        'widgetsConfig'
809
    ];
810
    const filteredMap1 = pick(cloneDeep(map1), pickedFields);
6✔
811
    const filteredMap2 = pick(cloneDeep(map2), pickedFields);
6✔
812
    // ABOUT: used for support text_serch_config field in old maps
813
    updateObjectFieldKey(filteredMap1.map, 'text_serch_config', 'text_search_config');
6✔
814
    updateObjectFieldKey(filteredMap2.map, 'text_serch_config', 'text_search_config');
6✔
815

816
    prepareMapObjectToCompare(filteredMap1);
6✔
817
    prepareMapObjectToCompare(filteredMap2);
6✔
818
    return isEqual(filteredMap1, filteredMap2);
6✔
819
};
820

821
/**
822
 * creates utilities for registering, fetching, executing hooks
823
 * used to override default ones in order to have a local hooks object
824
 * one for each map widget
825
 */
826
export const createRegisterHooks = () => {
1✔
827
    let hooksCustom = {};
5✔
828
    return {
5✔
829
        registerHook: (name, hook) => {
830
            hooksCustom[name] = hook;
17✔
831
        },
832
        getHook: (name) => hooksCustom[name],
7✔
833
        executeHook: (hookName, existCallback, dontExistCallback) => {
834
            const hook = hooksCustom[hookName];
×
835
            if (hook) {
×
836
                return existCallback(hook);
×
837
            }
838
            if (dontExistCallback) {
×
839
                return dontExistCallback();
×
840
            }
841
            return null;
×
842
        }
843
    };
844
};
845

846
/**
847
 * Detects if state has enabled Identify plugin for mapPopUps
848
 * @param {object} state
849
 * @returns {boolean}
850
 */
851
export const detectIdentifyInMapPopUp = (state)=>{
1✔
852
    if (state.mapPopups?.popups) {
2!
853
        let hasIdentify = state.mapPopups.popups.filter(plugin =>plugin?.component?.toLowerCase() === 'identify');
2✔
854
        return hasIdentify && hasIdentify.length > 0 ? true : false;
2✔
855
    }
856
    return false;
×
857
};
858

859
/**
860
 * Derive resolution object with scale and zoom info
861
 * based on visibility limit's type
862
 * @param value {number} computed with dots per map unit to get resolution
863
 * @param type {string} of visibility limit ex. scale
864
 * @param projection {string} map projection
865
 * @param resolutions {array} map resolutions
866
 * @return {object} resolution object
867
 */
868
export const getResolutionObject = (value, type, {projection, resolutions} = {}) => {
1!
869
    const dpu = dpi2dpu(DEFAULT_SCREEN_DPI, projection);
4✔
870
    if (type === 'scale') {
4✔
871
        const resolution = value / dpu;
3✔
872
        return {
3✔
873
            resolution: resolution,
874
            scale: value,
875
            zoom: getZoomFromResolution(resolution, resolutions)
876
        };
877
    }
878
    return {
1✔
879
        resolution: value,
880
        scale: value * dpu,
881
        zoom: getZoomFromResolution(value, resolutions)
882
    };
883
};
884

885
export function calculateExtent(center = {x: 0, y: 0, crs: "EPSG:3857"}, resolution, size = {width: 100, height: 100}, projection = "EPSG:3857") {
6!
886
    const {x, y} = reproject(center, center.crs ?? projection, projection);
3!
887
    const dx = resolution * size.width / 2;
3✔
888
    const dy = resolution * size.height / 2;
3✔
889
    return [x - dx, y - dy, x + dx, y + dy];
3✔
890

891
}
892

893

894
export const reprojectZoom = (zoom, mapProjection, printProjection) => {
1✔
895
    const multiplier = METERS_PER_UNIT[getUnits(mapProjection)] / METERS_PER_UNIT[getUnits(printProjection)];
10✔
896
    const mapResolution = getResolutions(mapProjection)[zoom] * multiplier;
10✔
897
    const printResolutions = getResolutions(printProjection);
10✔
898

899
    const printResolution = printResolutions.reduce((nearest, current) => {
10✔
900
        return Math.abs(current - mapResolution) < Math.abs(nearest - mapResolution) ? current : nearest;
234✔
901
    }, printResolutions[0]);
902
    return printResolutions.indexOf(printResolution);
10✔
903
};
904

905

906
export default {
907
    createRegisterHooks,
908
    EXTENT_TO_ZOOM_HOOK,
909
    RESOLUTIONS_HOOK,
910
    RESOLUTION_HOOK,
911
    COMPUTE_BBOX_HOOK,
912
    GET_PIXEL_FROM_COORDINATES_HOOK,
913
    GET_COORDINATES_FROM_PIXEL_HOOK,
914
    DEFAULT_SCREEN_DPI,
915
    ZOOM_TO_EXTENT_HOOK,
916
    CLICK_ON_MAP_HOOK,
917
    EMPTY_MAP,
918
    registerHook,
919
    getHook,
920
    dpi2dpm,
921
    getSphericalMercatorScales,
922
    getSphericalMercatorScale,
923
    getGoogleMercatorScales,
924
    getGoogleMercatorResolutions,
925
    getGoogleMercatorScale,
926
    getResolutionsForScales,
927
    getZoomForExtent,
928
    defaultGetZoomForExtent,
929
    getCenterForExtent,
930
    getResolutions,
931
    getScales,
932
    getBbox,
933
    mapUpdated,
934
    getCurrentResolution,
935
    transformExtent,
936
    saveMapConfiguration,
937
    generateNewUUIDs,
938
    mergeMapConfigs,
939
    addRootParentGroup,
940
    isSimpleGeomType,
941
    getSimpleGeomType,
942
    getIdFromUri,
943
    parseLayoutValue,
944
    prepareMapObjectToCompare,
945
    updateObjectFieldKey,
946
    compareMapChanges,
947
    clearHooks,
948
    getResolutionObject,
949
    calculateExtent,
950
    reprojectZoom
951
};
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