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

geosolutions-it / MapStore2 / 17819991521

18 Sep 2025 06:24AM UTC coverage: 76.76% (+0.007%) from 76.753%
17819991521

Pull #11484

github

web-flow
Merge 96ab2ea99 into df98a9847
Pull Request #11484: Performance Optimization: Refactor Pending Changes Logic and Save Resource Selector #11476

31590 of 49204 branches covered (64.2%)

128 of 148 new or added lines in 8 files covered. (86.49%)

2 existing lines in 1 file now uncovered.

39265 of 51153 relevant lines covered (76.76%)

37.15 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

46
export const DEFAULT_SCREEN_DPI = 96;
1✔
47

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

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

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

63
import proj4 from "proj4";
64

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

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

83
let hooks = {};
1✔
84

85

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

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

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

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

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

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

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

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

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

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

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

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

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

242
    let minZoom = minZ ?? 0;
598✔
243

244
    let maxZoom = maxZ ?? defaultMaxZoom;
598✔
245

246
    let zoomFactor = zoomF ?? defaultZoomFactor;
598✔
247

248
    const projection = proj4.defs(srs);
598✔
249

250
    const extent = ext ?? getProjection(srs)?.extent;
598✔
251

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

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

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

294
     */
295

296
    const defaultMaxResolution = res;
598✔
297

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

574

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

837

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

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

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

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

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

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

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

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

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

1063
}
1064

1065

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

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

1077

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