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

geosolutions-it / MapStore2 / 15811185418

22 Jun 2025 09:57PM UTC coverage: 76.901% (-0.03%) from 76.934%
15811185418

Pull #11130

github

web-flow
Merge 0b5467418 into 7cde38ac9
Pull Request #11130: #10839: Allow printing by freely setting the scale factor

31173 of 48566 branches covered (64.19%)

39 of 103 new or added lines in 7 files covered. (37.86%)

1644 existing lines in 149 files now uncovered.

38769 of 50414 relevant lines covered (76.9%)

36.38 hits per line

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

90.23
/web/client/utils/QueryParamsUtils.js
1
/**
2
 * Copyright 2022, 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 url from "url";
10
import {every, get, has, includes, inRange, isEmpty, isNaN, isNil, isObject, partial, toNumber} from "lodash";
11

12
import {getBbox} from "./MapUtils";
13
import {isValidExtent} from "./CoordinatesUtils";
14
import {getCenter, getConfigProp} from "./ConfigUtils";
15
import {updatePointWithGeometricFilter} from "./IdentifyUtils";
16
import {mapProjectionSelector} from "./PrintUtils";
17
import {ADD_LAYERS_FROM_CATALOGS, addLayersMapViewerUrl} from "../actions/catalog";
18
import {changeMapView, orientateMap, ZOOM_TO_EXTENT, zoomToExtent} from "../actions/map";
19
import {mapSelector} from "../selectors/map";
20
import {featureInfoClick} from "../actions/mapInfo";
21
import {warning} from "../actions/notifications";
22
import {
23
    addMarker,
24
    scheduleSearchLayerWithFilter,
25
    searchLayerWithFilter,
26
    SEARCH_LAYER_WITH_FILTER,
27
    SCHEDULE_SEARCH_LAYER_WITH_FILTER
28
} from "../actions/search";
29
import uuid from "uuid/v1";
30
import {syncActiveBackgroundLayer} from "../actions/backgroundselector";
31
import {selectedServiceSelector} from "../selectors/catalog";
32
import {mapTypeSelector} from "../selectors/maptype";
33

34
/**
35
 * Retrieves parameters from hash "query string" of react router
36
 * Example: `#/viewer/openlayers/0?center=0,0&zoom=5
37
 * @param {string|number} name - name of the parameter to get
38
 * @param state - state of the app
39
 */
40
export const getRequestLoadValue = (name, state) => {
1✔
41
    const search = get(state, 'router.location.search') || '';
1,932✔
42
    const { query = {} } = url.parse(search, true) || {};
1,932!
43
    if (query[name]) {
1,932✔
44
        try {
96✔
45
            return JSON.parse(query[name]);
96✔
46
        } catch (e) {
47
            if (query[name].length) {
47!
48
                return query[name];
47✔
49
            }
50
            return null;
×
51
        }
52
    }
53
    return null;
1,836✔
54
};
55

56
/**
57
 * Retrieves parameters from the `queryParams` entry (JSON) of the passed storage (by default `sessionStorage`).
58
 * Example:
59
 * <pre>
60
 * {
61
 *      "map": {"Contents of exported map"},
62
 *      "featureinfo": {"lat": 0, "lng": 0, "filterNameList": []},
63
 *      "bbox": "-177.84667968750014,-1.8234225930143395,-9.096679687500114,61.700290838326204",
64
 *      "center": "0,0",
65
 *      "zoom": 5,
66
 *      "actions": [],
67
 *      "page": "#/viewer/openlayers/config"
68
 * }
69
 * </pre>
70
 * @param {string} name - name of the parameter to get
71
 * @param queryParamsID - unique identifier of the request
72
 * @param {Storage} storage - sessionStorage or localStorage
73
 */
74
export const postRequestLoadValue = (name, queryParamsID, storage = sessionStorage) => {
1!
75
    const itemName = queryParamsID ? `queryParams-${queryParamsID}` : 'queryParams';
920✔
76
    const queryParams = storage.getItem(itemName) ?? null;
920✔
77
    if (queryParams) {
920✔
78
        try {
50✔
79
            const params = JSON.parse(queryParams);
50✔
80
            const { [name]: item, ...rest } = params;
50✔
81
            if (item && typeof params === 'object') {
50✔
82
                const { length } = Object.keys(params);
15✔
83
                length > 1 && storage.setItem(itemName, JSON.stringify(rest));
15✔
84
                length === 1 && storage.removeItem(itemName);
15✔
85
            }
86
            return item;
50✔
87
        } catch (e) {
88
            // eslint-disable-next-line no-console
89
            console.error(`Unable to parse query parameters from sessionStorage`);
×
90
            storage.removeItem(itemName);
×
91
            return null;
×
92
        }
93
    }
94
    return null;
870✔
95
};
96

97

98
/**
99
 * Retrieves parameter from two available sources:
100
 * - from hash "query string" of react router
101
 * - from the `queryParams` entry (JSON) of the passed storage
102
 * Data from query string has higher priority if parameter is available in both sources.
103
 * @param {string} name - name of the parameter to get
104
 * @param {*} state - app state
105
 * @param {Storage} storage - sessionStorage or localStorage
106
 */
107
export const getRequestParameterValue = (name, state, storage = sessionStorage) => {
1✔
108
    // Check if `queryParamsID` passed in query parameters. If so, use it as a key to retrieve data for POST method
109
    return getRequestLoadValue(name, state) ?? postRequestLoadValue(name, getRequestLoadValue('queryParamsID', state), storage);
1,009✔
110
};
111

112

113
/**
114
 * Map a set of URL querystrings to a KVP object
115
 * where each is single key is the parameter and the value is the parameter value
116
 * mapping is based on an object that maps each query string param to a redux action
117
 * @param {object} paramActions objects that maps each parameter to its respective action to trigger
118
 * @param {object} state the application state
119
 * @returns {object} { param: value } KVP object
120
 */
121
export const getParametersValues = (paramActions, state) => (
1✔
122
    Object.keys(paramActions)
39✔
123
        .reduce((params, parameter) => {
124
            const lowercase = parameter.toLowerCase();
546✔
125
            const value = getRequestParameterValue(parameter, state, sessionStorage) ?? getRequestParameterValue(lowercase, state, sessionStorage);
546✔
126
            return {
546✔
127
                ...params,
128
                ...(!isNil(value) ? { [parameter]: value } : {})
546✔
129
            };
130
        }, {})
131
);
132

133
/**
134
 * On a basis of a {param: action} object
135
 * map a KVP object in the form of {param: value}
136
 * to an array actions, each one corresponding to a param
137
 * @param {object} parameters objects that maps each parameter to its respective value
138
 * @param {object} paramActions objects that maps each parameter to its respective action to trigger
139
 * @param {object} state the application state
140
 * @returns {Function[]} array containing the functions to be triggered
141
 */
142
export const getQueryActions = (parameters, paramActions, state) => (
1✔
143
    Object.keys(parameters)
28✔
144
        .reduce((actions, param) => {
145
            return [
67✔
146
                ...actions,
147
                ...(paramActions[param](parameters, state) || [])
102✔
148
            ];
149
        }, [])
150
);
151

152
/**
153
 * From querystring params gets the specific cesium maps params (heading, pitch, roll)
154
 * and output and object with the param as key and the param value as object value
155
 * @param {String} parameters the querystring params from the URL request
156
 * @param {Object} map the slice of state having information about the current map
157
 * @returns {Object} the specific cesium map params in form of KVP object
158
 */
159
export const getCesiumViewerOptions = (parameters, map) => {
1✔
160
    const { heading, pitch, roll = 0 } = parameters;
12✔
161
    const validViewerOptions = [heading, pitch].map(val => typeof(val) !== 'undefined');
24✔
162
    return validViewerOptions && validViewerOptions.indexOf(false) === -1 ? {heading, pitch, roll} : map && map.viewerOptions;
12✔
163
};
164

165
/*
166
it maps params key to function.
167
functions must return an array of actions or and empty array
168
*/
169
export const paramActions = {
1✔
170
    bbox: (parameters) => {
171
        const extent = String(parameters.bbox).split(',')
4✔
172
            .map(val => parseFloat(val))
16✔
173
            .filter((val, idx) => idx % 2 === 0
16✔
174
                ? val > -180.5 && val < 180.5
15✔
175
                : val >= -90 && val <= 90)
16✔
176
            .filter(val => !isNaN(val));
15✔
177
        if (extent && extent.length === 4 && isValidExtent(extent)) {
4✔
178
            return [
3✔
179
                zoomToExtent(extent, 'EPSG:4326', undefined, {nearest: true})
180
            ];
181
        }
182
        return [
1✔
183
            warning({
184
                title: "share.wrongBboxParamTitle",
185
                message: "share.wrongBboxParamMessage",
186
                position: "tc"
187
            })
188
        ];
189
    },
190
    // In 2D mode the camera position and the center match the same latitude and longitude
191
    // while in 3D mode the center represents the position of the camera and not the actual targeted center on the globe
192
    // because it could differ based on the camera orientation (heading, pitch and roll)
193
    center: (parameters, state) => {
194
        const map = mapSelector(state);
8✔
195
        const mapType = mapTypeSelector(state);
8✔
196
        const validCenter = parameters && !isEmpty(parameters.center) && parameters.center.split(',').map(val => !isEmpty(val) && toNumber(val));
16✔
197

198
        if (mapType === 'cesium') {
8✔
199
            // if there is no bbox parameter and 'zoom', 'heading', 'pitch', 'roll' are presented - use orientate action
200
            if (parameters?.bbox) {
3!
201
                return [];
×
202
            }
203
            const requiredKeys = ['center', 'zoom', 'heading', 'pitch', 'roll'];
3✔
204
            if (every(requiredKeys, partial(has, parameters))) {
3✔
205
                return [orientateMap(parameters)];
2✔
206
            }
207
            return [];
1✔
208
        }
209

210
        const center = validCenter && validCenter.indexOf(false) === -1 && getCenter(validCenter);
5✔
211
        // if mapInfo query param is used --> use map zoom level if zoom q param not provided
212
        const isWithinMapInfo = parameters.mapInfo;
5✔
213
        const zoom = isWithinMapInfo ? toNumber(parameters.zoom) || map.zoom : toNumber(parameters.zoom);
5✔
214
        const bbox = getBbox(center, zoom);
5✔
215
        const mapSize = map && map.size;
5✔
216
        const projection = map && map.projection;
5✔
217
        const viewerOptions = getCesiumViewerOptions(parameters, map);
5✔
218
        const isValid = center && isObject(center) && inRange(center.y, -90, 91) && inRange(center.x, -180, 181) && inRange(zoom, 1, 36);
5✔
219
        if (isValid) {
5✔
220
            return [changeMapView(center, zoom, bbox, mapSize, null, projection, viewerOptions)];
4✔
221
        }
222
        return [
1✔
223
            warning({
224
                title: "share.wrongCenterAndZoomParamTitle",
225
                message: "share.wrongCenterAndZoomParamMessage",
226
                position: "tc"
227
            })
228
        ];
229

230
    },
231
    marker: (parameters, state) => {
232
        const map = mapSelector(state);
4✔
233
        const marker = !isEmpty(parameters.marker) && parameters.marker.split(',').map(val => !isEmpty(val) && toNumber(val));
8✔
234
        const center = marker && marker.length === 2 && marker.indexOf(false) === -1 && getCenter(marker);
4✔
235
        // if mapInfo query param is used --> use map zoom level if zoom q param not provided
236
        const isWithinMapInfo = parameters.mapInfo;
4✔
237
        const zoom = isWithinMapInfo ? toNumber(parameters.zoom) || map.zoom : toNumber(parameters.zoom);
4✔
238
        const bbox = getBbox(center, zoom);
4✔
239
        const lng = marker && marker[0];
4✔
240
        const lat = marker && marker[1];
4✔
241
        const mapSize = map && map.size;
4✔
242
        const projection = map && map.projection;
4✔
243
        const isValid = center && marker && isObject(marker) && (inRange(lat, -90, 91) && inRange(lng, -180, 181)) && inRange(zoom, 1, 36);
4✔
244

245
        if (isValid) {
4✔
246
            return [changeMapView(center, zoom, bbox, mapSize, null, projection),
3✔
247
                addMarker({lat, lng})
248
            ];
249
        }
250
        return [
1✔
251
            warning({
252
                title: "share.wrongMarkerAndZoomParamTitle",
253
                message: "share.wrongMarkerAndZoomParamMessage",
254
                position: "tc"
255
            })
256
        ];
257
    },
258
    featureInfo: (parameters, state) => {
259
        const value = parameters.featureInfo;
4✔
260
        const {lat, lng, filterNameList} = value;
4✔
261
        const projection = mapProjectionSelector(state);
4✔
262
        if (typeof lat !== 'undefined' && typeof lng !== 'undefined') {
4✔
263
            return [featureInfoClick(updatePointWithGeometricFilter({
2✔
264
                latlng: {
265
                    lat,
266
                    lng
267
                }
268
            }, projection), false, filterNameList ?? [])];
3✔
269
        } else if (typeof value === 'string') {
2!
270
            const [longitude, latitude] = value.split(',');
2✔
271
            if (typeof latitude !== 'undefined' && typeof longitude !== 'undefined') {
2!
272
                return [featureInfoClick(updatePointWithGeometricFilter({
2✔
273
                    latlng: {
274
                        lat: latitude,
275
                        lng: longitude
276
                    }
277
                }, projection), false, [])];
278
            }
279
        }
UNCOV
280
        return [];
×
281
    },
282
    mapInfo: (parameters) => {
283
        const value = parameters.mapInfo;
7✔
284
        const filterValue = parameters.mapInfoFilter;
7✔
285
        if (typeof value === 'string') {
7!
286
            const isCoordsProvided = parameters.marker || parameters.center || parameters.bbox;
7✔
287
            const zoom = toNumber(parameters?.zoom);
7✔
288
            const isZoomValid = zoom && inRange(zoom, 0, 36);
7✔
289
            // if zoom provided --> use it to override map zoo, level
290
            const overrideZoomLvl = zoom && isZoomValid ? zoom : null;
7✔
291
            const queryParamZoomOption = {overrideZoomLvl, isCoordsProvided: !!isCoordsProvided};
7✔
292
            const layers = parameters.addLayers;
7✔
293
            if (typeof layers === 'string') {
7✔
294
                const parsed = layers.split(',');
1✔
295
                const pairs = parsed.map(el => el.split(";"));
1✔
296
                if (pairs.find(el => el[0] === value)) {
1!
297
                    return [
1✔
298
                        scheduleSearchLayerWithFilter({layer: value, cql_filter: filterValue ?? '', queryParamZoomOption })
1!
299
                    ];
300
                }
301
            }
302
            return [
6✔
303
                searchLayerWithFilter({layer: value, cql_filter: filterValue ?? '', queryParamZoomOption})
6!
304
            ];
305
        }
UNCOV
306
        return [];
×
307
    },
308
    addLayers: (parameters, state) => {
309
        const layers = parameters.addLayers;
3✔
310
        if (typeof layers === 'string') {
3!
311
            const parsed = layers.split(',');
3✔
312
            if (parsed.length) {
3!
313
                const defaultSource = selectedServiceSelector(state);
3✔
314
                const pairs = parsed.map(el => el.split(";"));
5✔
315
                const layerFilters = (parameters.layerFilters ?? '').split(';') ?? [];
3!
316
                return [
3✔
317
                    addLayersMapViewerUrl(
318
                        pairs.map(el => el[0]),
5✔
319
                        pairs.map(el => el[1] ?? defaultSource),
5✔
320
                        layerFilters.map(filter => {
321
                            return filter.length ? ({
3✔
322
                                params: {
323
                                    CQL_FILTER: filter
324
                                }
325
                            }) : {};
326
                        })
327
                    )
328
                ];
329
            }
330
        }
UNCOV
331
        return [];
×
332
    },
333
    background: (parameters, state) => {
334
        const background = parameters.background;
2✔
335
        if (typeof background === 'string') {
2!
336
            const defaultSource = selectedServiceSelector(state);
2✔
337
            const pair = background.split(";");
2✔
338
            const id = uuid();
2✔
339
            return [
2✔
340
                addLayersMapViewerUrl(
341
                    [pair[0]],
342
                    [pair[1] ?? defaultSource],
4✔
343
                    [{
344
                        id,
345
                        'group': 'background',
346
                        visibility: true
347
                    }]
348
                ),
349
                syncActiveBackgroundLayer(id)
350
            ];
351
        }
352
        return [];
×
353
    },
354
    actions: (parameters) => {
355
        const whiteList = (getConfigProp("initialActionsWhiteList") || []).concat([
×
356
            SEARCH_LAYER_WITH_FILTER,
357
            SCHEDULE_SEARCH_LAYER_WITH_FILTER,
358
            ZOOM_TO_EXTENT,
359
            ADD_LAYERS_FROM_CATALOGS
360
        ]);
UNCOV
361
        if (parameters.actions) {
×
UNCOV
362
            return parameters.actions.filter(a => includes(whiteList, a.type));
×
363
        }
UNCOV
364
        return [];
×
365
    },
366
    ...([
367
        // supplementary parameter types with no processing callback
368
        'zoom', 'heading', 'pitch', 'roll',
369
        'layerFilters', 'mapInfoFilter'
370
    ]
371
        .reduce((prev, cur) => ({...prev, [cur]: () => {}}), {})
6✔
372
    )
373
};
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