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

geosolutions-it / MapStore2 / 17906295220

22 Sep 2025 06:11AM UTC coverage: 77.02% (+0.004%) from 77.016%
17906295220

Pull #11503

github

web-flow
Merge 5f489990e into 0d6dfc135
Pull Request #11503: [Backport 2025.01.xx] - Fixes #11476 , #11435 : Performance Optimization: Refactor Pending Changes Logic and Save Resource Selector #11476 (#11484)

31090 of 48275 branches covered (64.4%)

131 of 148 new or added lines in 8 files covered. (88.51%)

2 existing lines in 1 file now uncovered.

38510 of 50000 relevant lines covered (77.02%)

36.7 hits per line

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

89.81
/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
    pick,
11
    get,
12
    find,
13
    mapKeys,
14
    mapValues,
15
    keys,
16
    uniq,
17
    uniqWith,
18
    isEqual,
19
    isEmpty,
20
    findIndex,
21
    cloneDeep,
22
    minBy,
23
    omit,
24
    isObject
25
} from 'lodash';
26
import { get as getProjectionOL, getPointResolution, transform } from 'ol/proj';
27
import { get as getExtent } from 'ol/proj/projections';
28

29
import uuidv1 from 'uuid/v1';
30

31
import { getUnits, normalizeSRS, reproject } from './CoordinatesUtils';
32

33
import { getProjection } from './ProjectionUtils';
34

35
import { set } from './ImmutableUtils';
36
import {
37
    saveLayer,
38
    getGroupNodes,
39
    getNode,
40
    extractSourcesFromLayers,
41
    updateAvailableTileMatrixSetsOptions,
42
    getTileMatrixSetLink,
43
    DEFAULT_GROUP_ID
44
} from './LayersUtils';
45
import assign from 'object-assign';
46

47
export const DEFAULT_SCREEN_DPI = 96;
1✔
48

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

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

62
export const EMPTY_MAP = 'EMPTY_MAP';
1✔
63

64
import proj4 from "proj4";
65

66
export const EXTENT_TO_ZOOM_HOOK = 'EXTENT_TO_ZOOM_HOOK';
1✔
67

68
/**
69
 * `ZOOM_TO_EXTENT_HOOK` hook takes 2 arguments:
70
 * - `extent`: array of the extent [minx, miny, maxx, maxy]
71
 * - `options` object, with the following attributes:
72
 *   - `crs`: crs of the extent
73
 *   - `maxZoom`: max zoom for the zoom to functionality.
74
 *   - `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
75
 */
76
export const ZOOM_TO_EXTENT_HOOK = 'ZOOM_TO_EXTENT_HOOK';
1✔
77
export const RESOLUTIONS_HOOK = 'RESOLUTIONS_HOOK';
1✔
78
export const RESOLUTION_HOOK = 'RESOLUTION_HOOK';
1✔
79
export const COMPUTE_BBOX_HOOK = 'COMPUTE_BBOX_HOOK';
1✔
80
export const GET_PIXEL_FROM_COORDINATES_HOOK = 'GET_PIXEL_FROM_COORDINATES_HOOK';
1✔
81
export const GET_COORDINATES_FROM_PIXEL_HOOK = 'GET_COORDINATES_FROM_PIXEL_HOOK';
1✔
82
export const CLICK_ON_MAP_HOOK = 'CLICK_ON_MAP_HOOK';
1✔
83

84
let hooks = {};
1✔
85

86

87
export function registerHook(name, hook) {
88
    hooks[name] = hook;
1,135✔
89
}
90

91
export function getHook(name) {
92
    return hooks[name];
970✔
93
}
94

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

106
export function clearHooks() {
107
    hooks = {};
47✔
108
}
109

110
/**
111
 * @param dpi {number} dot per inch resolution
112
 * @return {number} dot per meter resolution
113
 */
114
export function dpi2dpm(dpi) {
115
    return dpi * (100 / 2.54);
7,711✔
116
}
117

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

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

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

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

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

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

205
export function getGoogleMercatorResolutions(minZoom, maxZoom, dpi) {
206
    return getResolutionsForScales(getGoogleMercatorScales(minZoom, maxZoom, dpi), "EPSG:3857", dpi);
313✔
207
}
208

209
/**
210
 * Calculates resolutions accordingly with default algorithm in GeoWebCache.
211
 * See this: https://github.com/GeoWebCache/geowebcache/blob/5e913193ff50a61ef9dd63a87887189352fa6b21/geowebcache/core/src/main/java/org/geowebcache/grid/GridSetFactory.java#L196
212
 * It allows to have the resolutions aligned to the default generated grid sets on server side.
213
 * **NOTES**: this solution doesn't support:
214
 * - custom grid sets with `alignTopLeft=true` (e.g. GlobalCRS84Pixel). Custom resolutions will need to be configured as `mapOptions.view.resolutions`
215
 * - custom grid set with custom extent. You need to customize the projection definition extent to make it work.
216
 * - custom grid set is partially supported by mapOptions.view.resolutions but this is not managed by projection change yet
217
 * - custom tile sizes
218
 * @param {string} srs projection code
219
 * @param {object} options optional configuration
220
 * @param {number} options.minResolution minimum resolution of the tile grid pyramid, default computed based on minimum zoom
221
 * @param {number} options.maxResolution maximum resolution of the tile grid pyramid, default computed based on maximum zoom
222
 * @param {number} options.minZoom minimum zoom of the tile grid pyramid, default 0
223
 * @param {number} options.maxZoom maximum zoom of the tile grid pyramid, default 30
224
 * @param {number} options.zoomFactor zoom factor, default 2
225
 * @param {array} options.extent extent of the tile grid pyramid in the projection coordinates, [minx, miny, maxx, maxy], default maximum extent of the projection
226
 * @param {number} options.tileWidth tile width, default 256
227
 * @param {number} options.tileHeight tile height, default 256
228
 * @return {array} a list of resolution based on the selected projection
229
 */
230
export function getResolutionsForProjection(srs, {
78✔
231
    minResolution: minRes,
232
    maxResolution: maxRes,
233
    minZoom: minZ,
234
    maxZoom: maxZ,
235
    zoomFactor: zoomF,
236
    extent: ext,
237
    tileWidth = 256,
537✔
238
    tileHeight = 256
537✔
239
} = {}) {
240
    const defaultMaxZoom = 30;
583✔
241
    const defaultZoomFactor = 2;
583✔
242

243
    let minZoom = minZ ?? 0;
583✔
244

245
    let maxZoom = maxZ ?? defaultMaxZoom;
583✔
246

247
    let zoomFactor = zoomF ?? defaultZoomFactor;
583✔
248

249
    const projection = proj4.defs(srs);
583✔
250

251
    const extent = ext ?? getProjection(srs)?.extent;
583✔
252

253
    const extentWidth = !extent ? 360 * METERS_PER_UNIT.degrees /
583!
254
        METERS_PER_UNIT[projection.getUnits()] :
255
        extent[2] - extent[0];
256
    const extentHeight = !extent ? 360 * METERS_PER_UNIT.degrees /
583!
257
        METERS_PER_UNIT[projection.getUnits()] :
258
        extent[3] - extent[1];
259

260
    let resX = extentWidth / tileWidth;
583✔
261
    let resY = extentHeight / tileHeight;
583✔
262
    let tilesWide;
263
    let tilesHigh;
264
    if (resX <= resY) {
583✔
265
        // use one tile wide by N tiles high
266
        tilesWide = 1;
493✔
267
        tilesHigh = Math.round(resY / resX);
493✔
268
        // previous resY was assuming 1 tile high, recompute with the actual number of tiles
269
        // high
270
        resY = resY / tilesHigh;
493✔
271
    } else {
272
        // use one tile high by N tiles wide
273
        tilesHigh = 1;
90✔
274
        tilesWide = Math.round(resX / resY);
90✔
275
        // previous resX was assuming 1 tile wide, recompute with the actual number of tiles
276
        // wide
277
        resX = resX / tilesWide;
90✔
278
    }
279
    // the maximum of resX and resY is the one that adjusts better
280
    const res = Math.max(resX, resY);
583✔
281

282
    /*
283
        // TODO: this is how GWC creates the bbox adjusted.
284
        // We should calculate it to have the correct extent for a grid set
285
        const adjustedExtentWidth = tilesWide * tileWidth * res;
286
        const adjustedExtentHeight = tilesHigh * tileHeight * res;
287
        BoundingBox adjExtent = new BoundingBox(extent);
288
        adjExtent.setMaxX(adjExtent.getMinX() + adjustedExtentWidth);
289
        // Do we keep the top or the bottom fixed?
290
        if (alignTopLeft) {
291
            adjExtent.setMinY(adjExtent.getMaxY() - adjustedExtentHeight);
292
        } else {
293
            adjExtent.setMaxY(adjExtent.getMinY() + adjustedExtentHeight);
294

295
     */
296

297
    const defaultMaxResolution = res;
583✔
298

299
    const defaultMinResolution = defaultMaxResolution / Math.pow(
583✔
300
        defaultZoomFactor, defaultMaxZoom - 0);
301

302
    // user provided maxResolution takes precedence
303
    let maxResolution = maxRes;
583✔
304
    if (maxResolution !== undefined) {
583!
305
        minZoom = 0;
×
306
    } else {
307
        maxResolution = defaultMaxResolution / Math.pow(zoomFactor, minZoom);
583✔
308
    }
309

310
    // user provided minResolution takes precedence
311
    let minResolution = minRes;
583✔
312
    if (minResolution === undefined) {
583!
313
        if (maxZoom !== undefined) {
583!
314
            if (maxRes !== undefined) {
583!
315
                minResolution = maxResolution / Math.pow(zoomFactor, maxZoom);
×
316
            } else {
317
                minResolution = defaultMaxResolution / Math.pow(zoomFactor, maxZoom);
583✔
318
            }
319
        } else {
320
            minResolution = defaultMinResolution;
×
321
        }
322
    }
323

324
    // given discrete zoom levels, minResolution may be different than provided
325
    maxZoom = minZoom + Math.floor(
583✔
326
        Math.log(maxResolution / minResolution) / Math.log(zoomFactor));
327
    return Array.apply(0, Array(maxZoom - minZoom + 1)).map((x, y) => maxResolution / Math.pow(zoomFactor, y));
18,073✔
328
}
329

330
export function getResolutions(projection) {
331
    if (getHook('RESOLUTIONS_HOOK')) {
572✔
332
        return getHook('RESOLUTIONS_HOOK')(projection);
252✔
333
    }
334
    return projection && normalizeSRS(projection) !== "EPSG:3857" ? getResolutionsForProjection(projection) :
320✔
335
        getGoogleMercatorResolutions(0, 21, DEFAULT_SCREEN_DPI);
336
}
337

338
export function getScales(projection, dpi) {
339
    const dpu = dpi2dpu(dpi, projection);
112✔
340
    return getResolutions(projection).map((resolution) => resolution * dpu);
2,824✔
341
}
342

343
export function getScale(projection, dpi, resolution) {
344
    const dpu = dpi2dpu(dpi, projection);
×
345
    return resolution * dpu;
×
346
}
347
/**
348
 * get random coordinates within CRS extent
349
 * @param {string} crs the code of the projection for example EPSG:4346
350
 * @returns {number[]} the point in [x,y] [lon,lat]
351
 */
352
export function getRandomPointInCRS(crs) {
353
    const extent = getExtent(crs); // Get the projection's extent
6✔
354
    if (!extent) {
6!
355
        throw new Error(`Extent not available for CRS: ${crs}`);
×
356
    }
357
    const [minX, minY, maxX, maxY] = extent.extent_;
6✔
358

359
    // Check if the equator (latitude = 0) is within the CRS extent
360
    const isEquatorWithinExtent = minY <= 0 && maxY >= 0;
6✔
361

362
    // Generate a random X coordinate within the valid longitude range
363
    const randomX = Math.random() * (maxX - minX) + minX;
6✔
364

365
    // Set Y to 0 if the equator is within the extent, otherwise generate a random Y
366
    const randomY = isEquatorWithinExtent ? 0 : Math.random() * (maxY - minY) + minY;
6!
367

368
    return [randomX, randomY];
6✔
369
}
370

371
/**
372
 * convert resolution between CRSs
373
 * @param {string} sourceCRS the code of a projection
374
 * @param {string} targetCRS the code of a projection
375
 * @param {number} sourceResolution the resolution to convert
376
 * @returns the converted resolution
377
 */
378
export function convertResolution(sourceCRS, targetCRS, sourceResolution) {
379
    const sourceProjection = getProjectionOL(sourceCRS);
4✔
380
    const targetProjection = getProjectionOL(targetCRS);
4✔
381

382
    if (!sourceProjection || !targetProjection) {
4!
383
        throw new Error(`Invalid CRS: ${sourceCRS} or ${targetCRS}`);
×
384
    }
385

386
    // Get a random point in the extent of the source CRS
387
    const randomPoint = getRandomPointInCRS(sourceCRS);
4✔
388

389
    // Transform the resolution
390
    const transformedResolution = getPointResolution(
4✔
391
        sourceProjection,
392
        sourceResolution,
393
        transform(randomPoint, sourceCRS, targetCRS),
394
        targetProjection.getUnits()
395
    );
396

397
    return { randomPoint, transformedResolution };
4✔
398
}
399

400
/**
401
 * Convert a resolution to the nearest zoom
402
 * @param {number} targetResolution resolution to be converted in zoom
403
 * @param {array} resolutions list of all available resolutions
404
 */
405
export function getZoomFromResolution(targetResolution, resolutions = getResolutions()) {
1✔
406
    // compute the absolute difference for all resolutions
407
    // and store the idx as zoom
408
    const diffs = resolutions.map((resolution, zoom) => ({ diff: Math.abs(resolution - targetResolution), zoom }));
910✔
409
    // the minimum difference represents the nearest zoom to the target resolution
410
    const { zoom } = minBy(diffs, 'diff');
37✔
411
    return zoom;
37✔
412
}
413

414
/**
415
 * Calculate the exact zoom level corresponding to a given resolution
416
 *
417
 * @param {number} targetResolution resolution to be converted in zoom
418
 * @param {number[]} resolutions list of all available resolutions
419
 * @returns {number} - A floating-point number representing the exact zoom level that corresponds
420
 *                   to the provided resolution.
421
 *
422
 * @example
423
 * const resolutions = [2048, 1024, 512, 256];
424
 * const zoom = getExactZoom(600, resolutions);
425
 * console.log(zoom); // e.g., ~1.77
426
 */
427
export function getExactZoomFromResolution(targetResolution, resolutions = getResolutions()) {
×
428
    const maxResolution = resolutions[0]; // zoom level 0
3✔
429
    return Math.log2(maxResolution / targetResolution);
3✔
430
}
431

432
export function defaultGetZoomForExtent(extent, mapSize, minZoom, maxZoom, dpi, mapResolutions) {
433
    const wExtent = extent[2] - extent[0];
10✔
434
    const hExtent = extent[3] - extent[1];
10✔
435

436
    const xResolution = Math.abs(wExtent / mapSize.width);
10✔
437
    const yResolution = Math.abs(hExtent / mapSize.height);
10✔
438
    const extentResolution = Math.max(xResolution, yResolution);
10✔
439

440
    const resolutions = mapResolutions || getResolutionsForScales(getGoogleMercatorScales(
10✔
441
        minZoom, maxZoom, dpi || DEFAULT_SCREEN_DPI), "EPSG:3857", dpi);
11✔
442

443
    const {zoom} = resolutions.reduce((previous, resolution, index) => {
10✔
444
        const diff = Math.abs(resolution - extentResolution);
228✔
445
        return diff > previous.diff ? previous : {diff: diff, zoom: index};
228✔
446
    }, {diff: Number.POSITIVE_INFINITY, zoom: 0});
447

448
    return Math.max(0, Math.min(zoom, maxZoom));
10✔
449
}
450

451
/**
452
 * Calculates the best fitting zoom level for the given extent.
453
 *
454
 * @param extent {Array} [minx, miny, maxx, maxy]
455
 * @param mapSize {Object} current size of the map.
456
 * @param minZoom {number} min zoom level.
457
 * @param maxZoom {number} max zoom level.
458
 * @param dpi {number} screen resolution in dot per inch.
459
 * @return {Number} the zoom level fitting th extent
460
 */
461
export function getZoomForExtent(extent, mapSize, minZoom, maxZoom, dpi) {
462
    if (getHook("EXTENT_TO_ZOOM_HOOK")) {
11✔
463
        return getHook("EXTENT_TO_ZOOM_HOOK")(extent, mapSize, minZoom, maxZoom, dpi);
1✔
464
    }
465
    const resolutions = getHook("RESOLUTIONS_HOOK") ?
10✔
466
        getHook("RESOLUTIONS_HOOK")() : null;
467
    return defaultGetZoomForExtent(extent, mapSize, minZoom, maxZoom, dpi, resolutions);
10✔
468
}
469

470
/**
471
* It returns the current resolution.
472
*
473
* @param currentZoom {number} the current zoom
474
* @param minZoom {number} min zoom level.
475
* @param maxZoom {number} max zoom level.
476
* @param dpi {number} screen resolution in dot per inch.
477
* @return {Number} the actual resolution
478
*/
479
export function getCurrentResolution(currentZoom, minZoom, maxZoom, dpi) {
480
    if (getHook("RESOLUTION_HOOK")) {
63✔
481
        return getHook("RESOLUTION_HOOK")(currentZoom, minZoom, maxZoom, dpi);
2✔
482
    }
483
    /* if no hook is registered (leaflet) it is used the GoogleMercatorResolutions in
484
       in order to get the list of resolutions */
485
    return getGoogleMercatorResolutions(minZoom, maxZoom, dpi)[currentZoom];
61✔
486
}
487

488
/**
489
 * Calculates the center for for the given extent.
490
 *
491
 * @param  {Array} extent [minx, miny, maxx, maxy]
492
 * @param  {String} projection projection of the extent
493
 * @return {object} center object
494
 */
495
export function getCenterForExtent(extent, projection) {
496

497
    var wExtent = extent[2] - extent[0];
10✔
498
    var hExtent = extent[3] - extent[1];
10✔
499

500
    var w = wExtent / 2;
10✔
501
    var h = hExtent / 2;
10✔
502

503
    return {
10✔
504
        x: extent[0] + w,
505
        y: extent[1] + h,
506
        crs: projection
507
    };
508
}
509

510
/**
511
 * Calculates the bounding box for the given center and zoom.
512
 *
513
 * @param  {object} center object
514
 * @param  {number} zoom level
515
 */
516
export function getBbox(center, zoom) {
517
    return executeHook("COMPUTE_BBOX_HOOK",
21✔
518
        (hook) => {
519
            return hook(center, zoom);
14✔
520
        }
521
    );
522
}
523

524
export const isNearlyEqual = function(a, b) {
1✔
525
    if (a === undefined || b === undefined) {
14!
526
        return false;
×
527
    }
528
    return a.toFixed(12) - b.toFixed(12) === 0;
14✔
529
};
530

531
/**
532
 * checks if maps has changed by looking at center or zoom
533
 * @param {object} oldMap map object
534
 * @param {object} newMap map object
535
 */
536
export function mapUpdated(oldMap, newMap) {
537
    if (oldMap && !isEmpty(oldMap) &&
8✔
538
        newMap && !isEmpty(newMap)) {
539
        const centersEqual = isNearlyEqual(newMap?.center?.x, oldMap?.center?.x) &&
3✔
540
                              isNearlyEqual(newMap?.center?.y, oldMap?.center?.y);
541
        return !centersEqual || newMap?.zoom !== oldMap?.zoom;
3✔
542
    }
543
    return false;
5✔
544
}
545

546
/* Transform width and height specified in meters to the units of the specified projection */
547
export function transformExtent(projection, center, width, height) {
548
    let units = getUnits(projection);
×
549
    if (units === 'ft') {
×
550
        return {width: width / METERS_PER_UNIT.ft, height: height / METERS_PER_UNIT.ft};
×
551
    } else if (units === 'us-ft') {
×
552
        return {width: width / METERS_PER_UNIT['us-ft'], height: height / METERS_PER_UNIT['us-ft']};
×
553
    } else if (units === 'degrees') {
×
554
        return {
×
555
            width: width / (111132.92 - 559.82 * Math.cos(2 * center.y) + 1.175 * Math.cos(4 * center.y)),
556
            height: height / (111412.84 * Math.cos(center.y) - 93.5 * Math.cos(3 * center.y))
557
        };
558
    }
559
    return {width, height};
×
560
}
561

562
export const groupSaveFormatted = (node) => {
1✔
563
    return {
43✔
564
        id: node.id,
565
        title: node.title,
566
        description: node.description,
567
        tooltipOptions: node.tooltipOptions,
568
        tooltipPlacement: node.tooltipPlacement,
569
        expanded: node.expanded,
570
        visibility: node.visibility,
571
        nodesMutuallyExclusive: node.nodesMutuallyExclusive
572
    };
573
};
574

575

576
export function saveMapConfiguration(currentMap, currentLayers, currentGroups, currentBackgrounds, textSearchConfig, bookmarkSearchConfig, additionalOptions) {
577

578
    const map = {
42✔
579
        center: currentMap.center,
580
        maxExtent: currentMap.maxExtent,
581
        projection: currentMap.projection,
582
        units: currentMap.units,
583
        mapInfoControl: currentMap.mapInfoControl,
584
        zoom: currentMap.zoom,
585
        mapOptions: currentMap.mapOptions || {},
78✔
586
        ...(currentMap.visualizationMode && { visualizationMode: currentMap.visualizationMode }),
42✔
587
        ...(currentMap.viewerOptions && { viewerOptions: currentMap.viewerOptions })
42✔
588
    };
589

590
    const layers = currentLayers.map((layer) => {
41✔
591
        return saveLayer(layer);
69✔
592
    });
593

594
    const flatGroupId = currentGroups.reduce((a, b) => {
41✔
595
        const flatGroups = a.concat(getGroupNodes(b));
35✔
596
        return flatGroups;
35✔
597
    }, [].concat(currentGroups.map(g => g.id)));
35✔
598

599
    const groups = flatGroupId.map(g => {
41✔
600
        const node = getNode(currentGroups, g);
95✔
601
        return node && node.nodes ? groupSaveFormatted(node) : null;
95✔
602
    }).filter(g => g);
95✔
603

604
    const backgrounds = currentBackgrounds.filter(background => !!background.thumbnail);
41✔
605

606
    // extract sources map
607
    const sources = extractSourcesFromLayers(layers);
41✔
608

609
    // removes tile matrix set from layers and replace it with a link if available in sources
610
    const formattedLayers = layers.map(layer => {
41✔
611
        const { availableTileMatrixSets, ...updatedLayer } = updateAvailableTileMatrixSetsOptions(layer);
69✔
612
        return availableTileMatrixSets
69✔
613
            ? {
614
                ...updatedLayer,
615
                availableTileMatrixSets: Object.keys(availableTileMatrixSets)
616
                    .reduce((acc, tileMatrixSetId) => {
617
                        const tileMatrixSetLink = getTileMatrixSetLink(layer, tileMatrixSetId);
3✔
618
                        if (get({ sources }, tileMatrixSetLink)) {
3!
619
                            return {
3✔
620
                                ...acc,
621
                                [tileMatrixSetId]: {
622
                                    ...omit(availableTileMatrixSets[tileMatrixSetId], 'tileMatrixSet'),
623
                                    tileMatrixSetLink
624
                                }
625
                            };
626
                        }
627
                        return {
×
628
                            ...acc,
629
                            [tileMatrixSetId]: availableTileMatrixSets[tileMatrixSetId]
630
                        };
631
                    }, {})
632
            }
633
            : updatedLayer;
634
    });
635

636
    /* removes the geometryGeodesic property from the features in the annotations layer*/
637
    let annotationsLayerIndex = findIndex(formattedLayers, layer => layer.id === "annotations");
69✔
638
    if (annotationsLayerIndex !== -1) {
41✔
639
        let featuresLayer = formattedLayers[annotationsLayerIndex].features.map(feature => {
1✔
640
            if (feature.type === "FeatureCollection") {
1!
641
                return {
1✔
642
                    ...feature,
643
                    features: feature.features.map(f => {
644
                        if (f.properties.geometryGeodesic) {
1!
645
                            return set("properties.geometryGeodesic", null, f);
1✔
646
                        }
647
                        return f;
×
648
                    })
649
                };
650
            }
651
            if (feature.properties.geometryGeodesic) {
×
652
                return set("properties.geometryGeodesic", null, feature);
×
653
            }
654
            return {};
×
655
        });
656
        formattedLayers[annotationsLayerIndex] = set("features", featuresLayer, formattedLayers[annotationsLayerIndex]);
1✔
657
    }
658

659
    return {
41✔
660
        version: 2,
661
        // layers are defined inside the map object
662
        map: assign({}, map, {layers: formattedLayers, groups, backgrounds, text_search_config: textSearchConfig, bookmark_search_config: bookmarkSearchConfig},
663
            !isEmpty(sources) && {sources} || {}),
82✔
664
        ...additionalOptions
665
    };
666
}
667

668
export const generateNewUUIDs = (mapConfig = {}) => {
1!
669
    const newMapConfig = cloneDeep(mapConfig);
2✔
670

671
    const oldIdToNew = {
2✔
672
        ...get(mapConfig, 'map.layers', []).reduce((result, layer) => ({
4✔
673
            ...result,
674
            [layer.id]: layer.id === 'annotations' ? layer.id : uuidv1()
4!
675
        }), {}),
676
        ...get(mapConfig, 'widgetsConfig.widgets', []).reduce((result, widget) => ({...result, [widget.id]: uuidv1()}), {})
1✔
677
    };
678

679
    return set('map.backgrounds', get(mapConfig, 'map.backgrounds', []).map(background => ({...background, id: oldIdToNew[background.id]})),
2✔
680
        set('widgetsConfig', {
681
            collapsed: mapValues(mapKeys(get(mapConfig, 'widgetsConfig.collapsed', {}), (value, key) => oldIdToNew[key]), (value) =>
1✔
682
                ({...value, layouts: mapValues(value.layouts, (layout) => ({...layout, i: oldIdToNew[layout.i]}))})),
2✔
683
            layouts: mapValues(get(mapConfig, 'widgetsConfig.layouts', {}), (value) =>
684
                value.map(layout => ({...layout, i: oldIdToNew[layout.i]}))),
2✔
685
            widgets: get(mapConfig, 'widgetsConfig.widgets', [])
686
                .map(widget => ({
1✔
687
                    ...widget,
688
                    id: oldIdToNew[widget.id],
689
                    layer: ({...get(widget, 'layer', {}), id: oldIdToNew[get(widget, 'layer.id')]})
690
                }))
691
        },
692
        set('map.layers', get(mapConfig, 'map.layers', [])
693
            .map(layer => ({...layer, id: oldIdToNew[layer.id]})), newMapConfig)));
4✔
694
};
695

696
export const mergeMapConfigs = (cfg1 = {}, cfg2 = {}) => {
1!
697
    // removes empty props from layer as it can cause bugs
698
    const fixLayers = (layers = []) => layers.map(layer => pick(layer, keys(layer).filter(key => layer[key] !== undefined)));
72!
699

700
    const cfg2Fixed = generateNewUUIDs(cfg2);
2✔
701

702
    const backgrounds = [...get(cfg1, 'map.backgrounds', []), ...get(cfg2Fixed, 'map.backgrounds', [])];
2✔
703

704
    const layers1 = fixLayers(get(cfg1, 'map.layers', []));
2✔
705
    const layers2 = fixLayers(get(cfg2Fixed, 'map.layers', []));
2✔
706

707
    const annotationsLayer1 = find(layers1, layer => layer.id === 'annotations');
5✔
708
    const annotationsLayer2 = find(layers2, layer => layer.id === 'annotations');
4✔
709

710
    const layers = [
2✔
711
        ...layers2.filter(layer => layer.id !== 'annotations'),
4✔
712
        ...layers1.filter(layer => layer.id !== 'annotations'),
6✔
713
        ...(annotationsLayer1 || annotationsLayer2 ? [{
5✔
714
            ...(annotationsLayer1 || {}),
1!
715
            ...(annotationsLayer2 || {}),
2✔
716
            features: [
717
                ...get(annotationsLayer1, 'features', []), ...get(annotationsLayer2, 'features', [])
718
            ]
719
        }] : [])
720
    ];
721
    const toleratedFields = ['id', 'visibility'];
2✔
722
    const backgroundLayers = layers.filter(layer => layer.group === 'background')
10✔
723
        // remove duplication by comparing all fields with some level of tolerance
724
        .filter((l1, i, a) => findIndex(a, (l2) => isEqual(omit(l1, toleratedFields), omit(l2, toleratedFields))) === i);
5✔
725
    const firstVisible = findIndex(backgroundLayers, layer => layer.visibility);
2✔
726

727
    const sources1 = get(cfg1, 'map.sources', {});
2✔
728
    const sources2 = get(cfg2Fixed, 'map.sources', {});
2✔
729
    const sources = {...sources1, ...sources2};
2✔
730

731
    const widgetsConfig1 = get(cfg1, 'widgetsConfig', {});
2✔
732
    const widgetsConfig2 = get(cfg2Fixed, 'widgetsConfig', {});
2✔
733

734
    return {
2✔
735
        ...cfg2Fixed,
736
        ...cfg1,
737
        catalogServices: {
738
            ...get(cfg1, 'catalogServices', {}),
739
            services: {
740
                ...get(cfg1, 'catalogServices.services', {}),
741
                ...get(cfg2Fixed, 'catalogServices.services', {})
742
            }
743
        },
744
        map: {
745
            ...cfg2Fixed.map,
746
            ...cfg1.map,
747
            backgrounds,
748
            groups: uniqWith([...get(cfg1, 'map.groups', []), ...get(cfg2Fixed, 'map.groups', [])],
749
                (group1, group2) => group1.id === group2.id),
4✔
750
            layers: [
751
                ...backgroundLayers.slice(0, firstVisible + 1),
752
                ...backgroundLayers.slice(firstVisible + 1).map(layer => ({...layer, visibility: false})),
2✔
753
                ...layers.filter(layer => layer.group !== 'background')
10✔
754
            ],
755
            sources: !isEmpty(sources) ? sources : undefined
2!
756
        },
757
        widgetsConfig: {
758
            collapsed: {...widgetsConfig1.collapsed, ...widgetsConfig2.collapsed},
759
            layouts: uniq([...keys(widgetsConfig1.layouts), ...keys(widgetsConfig2.layouts)])
760
                .reduce((result, key) => ({
2✔
761
                    ...result,
762
                    [key]: [
763
                        ...get(widgetsConfig1, `layouts.${key}`, []),
764
                        ...get(widgetsConfig2, `layouts.${key}`, [])
765
                    ]
766
                }), {}),
767
            widgets: [...get(widgetsConfig1, 'widgets', []), ...get(widgetsConfig2, 'widgets', [])]
768
        },
769
        timelineData: {
770
            ...get(cfg1, 'timelineData', {}),
771
            ...get(cfg2Fixed, 'timelineData', {})
772
        },
773
        dimensionData: {
774
            ...get(cfg1, 'dimensionData', {}),
775
            ...get(cfg2Fixed, 'dimensionData', {})
776
        }
777
    };
778
};
779

780
export const addRootParentGroup = (cfg = {}, groupTitle = 'RootGroup') => {
1!
781
    const groups = get(cfg, 'map.groups', []);
2✔
782
    const groupsWithoutDefault = groups.filter(({id}) => id !== DEFAULT_GROUP_ID);
3✔
783
    const defaultGroup = find(groups, ({id}) => id === DEFAULT_GROUP_ID);
2✔
784
    const fixedDefaultGroup = defaultGroup && {
2✔
785
        id: uuidv1(),
786
        title: groupTitle,
787
        expanded: defaultGroup.expanded
788
    };
789
    const groupsWithFixedDefault = defaultGroup ?
2✔
790
        [
791
            ...groupsWithoutDefault.map(({id, ...other}) => ({
2✔
792
                id: `${fixedDefaultGroup.id}.${id}`,
793
                ...other
794
            })),
795
            fixedDefaultGroup
796
        ] :
797
        groupsWithoutDefault;
798

799
    return {
2✔
800
        ...cfg,
801
        map: {
802
            ...cfg.map,
803
            groups: groupsWithFixedDefault,
804
            layers: get(cfg, 'map.layers', []).map(({group, ...other}) => ({
6✔
805
                ...other,
806
                group: defaultGroup && group !== 'background' && (group === DEFAULT_GROUP_ID || !group) ? fixedDefaultGroup.id :
28✔
807
                    defaultGroup && find(groupsWithFixedDefault, ({id}) => id.slice(id.indexOf('.') + 1) === group)?.id || group
6✔
808
            }))
809
        }
810
    };
811
};
812

813
export function isSimpleGeomType(geomType) {
814
    switch (geomType) {
48✔
815
    case "MultiPoint": case "MultiLineString": case "MultiPolygon": case "GeometryCollection": case "Text": return false;
13✔
816
    case "Point": case "Circle": case "LineString": case "Polygon": default: return true;
35✔
817
    }
818
}
819
export function getSimpleGeomType(geomType = "Point") {
×
820
    switch (geomType) {
37✔
821
    case "Point": case "LineString": case "Polygon": case "Circle": return geomType;
22✔
822
    case "MultiPoint": case "Marker": return "Point";
4✔
823
    case "MultiLineString": return "LineString";
3✔
824
    case "MultiPolygon": return "Polygon";
3✔
825
    case "GeometryCollection": return "GeometryCollection";
3✔
826
    case "Text": return "Point";
1✔
827
    default: return geomType;
1✔
828
    }
829
}
830

831
export const getIdFromUri = (uri, regex = /data\/(\d+)/) => {
1✔
832
    // 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`
833
    const decodedUri = decodeURIComponent(uri);
9✔
834
    const findDataDigit = regex.exec(decodedUri);
9✔
835
    return findDataDigit && findDataDigit.length && findDataDigit.length > 1 ? findDataDigit[1] : null;
9✔
836
};
837

838

839
/**
840
 * Determines if a field should be included in the comparison based on picked fields and exclusion rules.
841
 * @param {string} path - The full path to the field (e.g., 'root.obj.key').
842
 * @param {string} key - The key of the field being checked.
843
 * @param {any} value - The value of the field.
844
 * @param {object} rules - The rules object containing pickedFields and excludes.
845
 * @param {string[]} rules.pickedFields - Array of field paths to include in the comparison.
846
 * @param {object} rules.excludes - Object mapping parent paths to arrays of keys to exclude.
847
 * @returns {boolean} True if the field should be included, false otherwise.
848
 */
849
export const filterFieldByRules = (path, key, value, { pickedFields = [], excludes = {} }) => {
1!
850
    // remove all empty objects, nill or false value to normalize comparison
851
    if (
389✔
852
        value === undefined
1,415✔
853
        || value === null
854
        || value === false
855
        || (isObject(value) && isEmpty(value))
856
    ) {
857
        return false;
197✔
858
    }
859
    if (pickedFields.some((field) => field.includes(path) || path.includes(field))) {
505✔
860
        // Fix: check parent path for excludes
861
        const parentPath = path.substring(0, path.lastIndexOf('.'));
151✔
862
        if (excludes[parentPath] === undefined) {
151✔
863
            return true;
86✔
864
        }
865
        if (excludes[parentPath] && excludes[parentPath].includes(key)) {
65✔
866
            return false;
8✔
867
        }
868
        return true;
57✔
869
    }
870
    return false;
41✔
871
};
872

873
/**
874
 * Apply a custom parser to a value based on the path
875
 * @param {string} path - The full path to the field (e.g., 'root.obj.key').
876
 * @param {string} key - The key of the field being checked.
877
 * @param {any} value - The value of the field.
878
 * @param {object} rules - The rules object containing pickedFields and excludes.
879
 * @param {object} rules.parsers - parsers configuration
880
 * @returns {any} parsed value
881
 */
882
export const parseFieldValue = (path, key, value, { parsers }) => {
1✔
883
    return parsers?.[path] ? parsers[path](value, key) : value;
389✔
884
};
885
/**
886
 * Prepares object entries for comparison by applying aliasing, filtering, and sorting.
887
 * @param {object} obj - The object whose entries are to be prepared.
888
 * @param {object} rules - The rules object containing aliases, pickedFields, and excludes.
889
 * @param {string} parentKey - The parent key path for the current object.
890
 * @returns {array} Array of [key, value] pairs, filtered and sorted for comparison.
891
 */
892
export const prepareObjectEntries = (obj, rules, parentKey) => {
1✔
893
    const safeObj = obj || {};
71!
894
    // First apply aliasing and parsing, then filter using the aliased keys
895
    return Object.entries(safeObj)
71✔
896
        .map(([originalKey, value]) => {
897
            const key = rules?.aliases?.[originalKey] || originalKey;
385✔
898
            return [key, parseFieldValue(`${parentKey}.${key}`, key, value, rules)];
385✔
899
        })
900
        .filter(([key, value]) => filterFieldByRules(`${parentKey}.${key}`, key, value, rules))
385✔
901
        .sort((a, b) => {
902
            if (a[0] < b[0]) { return -1; }
127✔
903
            if (a[0] > b[0]) { return 1; }
73!
904
            return 0;
×
905
        });
906
};
907

908
// function that checks if a field has changed ( also includes the rules to prepare object for comparision)
909
export const recursiveIsChangedWithRules = (a, b, rules, parentKey = 'root') => {
1!
910
    // strictly equal
911
    if (a === b) {
80✔
912
        return false;
32✔
913
    }
914

915
    // Handle arrays
916
    if (Array.isArray(a)) {
48✔
917
        if (!Array.isArray(b) || a.length !== b.length) {
10!
918
            return true;
×
919
        }
920
        // same reference
921
        if (a === b) {
10!
NEW
922
            return false;
×
923
        }
924
        for (let i = 0; i < a.length; i++) {
10✔
925
            if (recursiveIsChangedWithRules(a[i], b[i], rules, `${parentKey}[]`)) {
10✔
926
                return true;
3✔
927
            }
928
        }
929
        return false;
7✔
930
    }
931

932
    // Handle objects
933
    if (typeof a === 'object' && a !== null) {
38✔
934
        // Prepare entries only if needed
935
        const aEntries = prepareObjectEntries(a, rules, parentKey);
34✔
936
        const bEntries = prepareObjectEntries(b || {}, rules, parentKey);
34!
937
        if (aEntries.length !== bEntries.length) {
34✔
938
            return true;
3✔
939
        }
940
        for (let i = 0; i < aEntries.length; i++) {
31✔
941
            const [key, value] = aEntries[i];
55✔
942
            if (recursiveIsChangedWithRules(value, bEntries[i]?.[1], rules, `${parentKey}.${key}`)) {
55✔
943
                return true;
12✔
944
            }
945
        }
946
        return false;
19✔
947
    }
948
    // Fallback for primitives
949
    return a !== b;
4✔
950
};
951

952
/**
953
 * @param {object} map1 - The original map configuration object.
954
 * @param {object} map2 - The updated map configuration object.
955
 * @returns {boolean} True if the considered fields are equal, false otherwise.
956
 */
957
export const compareMapChanges = (map1 = {}, map2 = {}) => {
1!
958
    const pickedFields = [
8✔
959
        'root.map.layers',
960
        'root.map.backgrounds',
961
        'root.map.text_search_config',
962
        'root.map.bookmark_search_config',
963
        'root.map.text_serch_config',
964
        'root.map.zoom',
965
        'root.widgetsConfig',
966
        'root.swipe'
967
    ];
968
    const aliases = {
8✔
969
        text_serch_config: 'text_search_config'
970
    };
971
    const excludes = {
8✔
972
        'root.map.layers[]': ['apiKey', 'time', 'args', 'fixed']
973
    };
974
    const parsers = {
8✔
975
        // in some cases widgets have an empty configuration
976
        // we could exclude them if there are not widgets listed
977
        'root.widgetsConfig': (value) => {
978
            if (!value?.widgets?.length) {
12!
979
                return null;
12✔
980
            }
NEW
981
            return value;
×
982
        },
983
        // the ellipsoid layer is included by default from the background selector
984
        // we could exclude it because it's not currently configurable
985
        'root.map.layers': (value) => {
986
            return (value || []).filter(layer => !(layer.type === 'terrain' && layer.provider === 'ellipsoid'));
12!
987
        }
988
    };
989
    const isSame = !recursiveIsChangedWithRules(map1, map2, { pickedFields, aliases, excludes, parsers }, 'root');
8✔
990
    return isSame;
8✔
991
};
992
/**
993
 * creates utilities for registering, fetching, executing hooks
994
 * used to override default ones in order to have a local hooks object
995
 * one for each map widget
996
 */
997
export const createRegisterHooks = (id) => {
1✔
998
    let hooksCustom = {};
5✔
999
    return {
5✔
1000
        registerHook: (name, hook) => {
1001
            hooksCustom[name] = hook;
17✔
1002
        },
1003
        getHook: (name) => hooksCustom[name],
7✔
1004
        executeHook: (hookName, existCallback, dontExistCallback) => {
1005
            const hook = hooksCustom[hookName];
×
1006
            if (hook) {
×
1007
                return existCallback(hook);
×
1008
            }
1009
            if (dontExistCallback) {
×
1010
                return dontExistCallback();
×
1011
            }
1012
            return null;
×
1013
        },
1014
        id
1015
    };
1016
};
1017

1018
/**
1019
 * Detects if state has enabled Identify plugin for mapPopUps
1020
 * @param {object} state
1021
 * @returns {boolean}
1022
 */
1023
export const detectIdentifyInMapPopUp = (state)=>{
1✔
1024
    if (state.mapPopups?.popups) {
2!
1025
        let hasIdentify = state.mapPopups.popups.filter(plugin =>plugin?.component?.toLowerCase() === 'identify');
2✔
1026
        return hasIdentify && hasIdentify.length > 0 ? true : false;
2✔
1027
    }
1028
    return false;
×
1029
};
1030

1031
/**
1032
 * Derive resolution object with scale and zoom info
1033
 * based on visibility limit's type
1034
 * @param value {number} computed with dots per map unit to get resolution
1035
 * @param type {string} of visibility limit ex. scale
1036
 * @param projection {string} map projection
1037
 * @param resolutions {array} map resolutions
1038
 * @return {object} resolution object
1039
 */
1040
export const getResolutionObject = (value, type, {projection, resolutions} = {}) => {
1!
1041
    const dpu = dpi2dpu(DEFAULT_SCREEN_DPI, projection);
4✔
1042
    if (type === 'scale') {
4✔
1043
        const resolution = value / dpu;
3✔
1044
        return {
3✔
1045
            resolution: resolution,
1046
            scale: value,
1047
            zoom: getZoomFromResolution(resolution, resolutions)
1048
        };
1049
    }
1050
    return {
1✔
1051
        resolution: value,
1052
        scale: value * dpu,
1053
        zoom: getZoomFromResolution(value, resolutions)
1054
    };
1055
};
1056
window.__ = getResolutionObject;
1✔
1057

1058
export function calculateExtent(center = {x: 0, y: 0, crs: "EPSG:3857"}, resolution, size = {width: 100, height: 100}, projection = "EPSG:3857") {
12!
1059
    const {x, y} = reproject(center, center.crs ?? projection, projection);
7!
1060
    const dx = resolution * size.width / 2;
7✔
1061
    const dy = resolution * size.height / 2;
7✔
1062
    return [x - dx, y - dy, x + dx, y + dy];
7✔
1063

1064
}
1065

1066

1067
export const reprojectZoom = (zoom, mapProjection, printProjection) => {
1✔
1068
    const multiplier = METERS_PER_UNIT[getUnits(mapProjection)] / METERS_PER_UNIT[getUnits(printProjection)];
21✔
1069
    const mapResolution = getResolutions(mapProjection)[Math.round(zoom)] * multiplier;
21✔
1070
    const printResolutions = getResolutions(printProjection);
21✔
1071

1072
    const printResolution = printResolutions.reduce((nearest, current) => {
21✔
1073
        return Math.abs(current - mapResolution) < Math.abs(nearest - mapResolution) ? current : nearest;
525✔
1074
    }, printResolutions[0]);
1075
    return printResolutions.indexOf(printResolution);
21✔
1076
};
1077

1078

1079
export default {
1080
    createRegisterHooks,
1081
    EXTENT_TO_ZOOM_HOOK,
1082
    RESOLUTIONS_HOOK,
1083
    RESOLUTION_HOOK,
1084
    COMPUTE_BBOX_HOOK,
1085
    GET_PIXEL_FROM_COORDINATES_HOOK,
1086
    GET_COORDINATES_FROM_PIXEL_HOOK,
1087
    DEFAULT_SCREEN_DPI,
1088
    ZOOM_TO_EXTENT_HOOK,
1089
    CLICK_ON_MAP_HOOK,
1090
    EMPTY_MAP,
1091
    registerHook,
1092
    getHook,
1093
    dpi2dpm,
1094
    getSphericalMercatorScales,
1095
    getSphericalMercatorScale,
1096
    getGoogleMercatorScales,
1097
    getGoogleMercatorResolutions,
1098
    getGoogleMercatorScale,
1099
    getResolutionsForScales,
1100
    getZoomForExtent,
1101
    defaultGetZoomForExtent,
1102
    getCenterForExtent,
1103
    getResolutions,
1104
    getScales,
1105
    getBbox,
1106
    mapUpdated,
1107
    getCurrentResolution,
1108
    transformExtent,
1109
    saveMapConfiguration,
1110
    generateNewUUIDs,
1111
    mergeMapConfigs,
1112
    addRootParentGroup,
1113
    isSimpleGeomType,
1114
    getSimpleGeomType,
1115
    getIdFromUri,
1116
    compareMapChanges,
1117
    clearHooks,
1118
    getResolutionObject,
1119
    calculateExtent,
1120
    reprojectZoom
1121
};
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