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

geosolutions-it / MapStore2 / 5278726914

pending completion
5278726914

push

github

web-flow
[Backport c047-2023.01.xx] Fix #9025 & #9193 WMS caching with custom scales (#9222)

* #9025 WMS caching with custom scales (projection resolutions strategy) (#9168)

* Fix #9025 WMS caching with custom scales (custom resolutions strategy from WMTS) (#9184)

---------

Co-authored-by: Lorenzo Natali <lorenzo.natali@geosolutionsgroup.com>

* Fix #9025 Available tile grids popup always reports mismatch in geostories and dashboards (#9196)

* Fix #9193 Add a cache options checks/info also for default WMS tile grid (#9195)

* Fix #9193 failing test (#9207)

* #9025 add caching options to wms background settings (#9213)

* fix failing tests

---------

Co-authored-by: Lorenzo Natali <lorenzo.natali@geosolutionsgroup.com>

27722 of 42099 branches covered (65.85%)

133 of 133 new or added lines in 12 files covered. (100.0%)

34374 of 43748 relevant lines covered (78.57%)

42.93 hits per line

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

81.74
/web/client/components/map/openlayers/plugins/WMSLayer.js
1
/*
2
 * Copyright 2017, 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
import React from 'react';
9
import Message from '../../../../components/I18N/Message';
10
import Layers from '../../../../utils/openlayers/Layers';
11
import isNil from 'lodash/isNil';
12
import isEqual from 'lodash/isEqual';
13
import union from 'lodash/union';
14
import isArray from 'lodash/isArray';
15
import assign from 'object-assign';
16
import axios from '../../../../libs/ajax';
17
import CoordinatesUtils from '../../../../utils/CoordinatesUtils';
18
import { getProjection } from '../../../../utils/ProjectionUtils';
19
import {needProxy, getProxyUrl} from '../../../../utils/ProxyUtils';
20
import { getConfigProp } from '../../../../utils/ConfigUtils';
21

22
import {optionsToVendorParams} from '../../../../utils/VendorParamsUtils';
23
import {addAuthenticationToSLD, addAuthenticationParameter, getAuthenticationHeaders} from '../../../../utils/SecurityUtils';
24
import { creditsToAttribution, getWMSVendorParams } from '../../../../utils/LayersUtils';
25

26
import { getResolutionsForProjection } from '../../../../utils/MapUtils';
27
import  {loadTile, getElevation as getElevationFunc} from '../../../../utils/ElevationUtils';
28

29
import ImageLayer from 'ol/layer/Image';
30
import ImageWMS from 'ol/source/ImageWMS';
31
import {get} from 'ol/proj';
32
import TileGrid from 'ol/tilegrid/TileGrid';
33
import TileLayer from 'ol/layer/Tile';
34
import TileWMS from 'ol/source/TileWMS';
35

36
import VectorTileSource from 'ol/source/VectorTile';
37
import VectorTileLayer from 'ol/layer/VectorTile';
38

39
import { isVectorFormat } from '../../../../utils/VectorTileUtils';
40
import { OL_VECTOR_FORMATS, applyStyle } from '../../../../utils/openlayers/VectorTileUtils';
41
import { generateEnvString } from '../../../../utils/LayerLocalizationUtils';
42
import { getTileGridFromLayerOptions } from '../../../../utils/WMSUtils';
43

44
/**
45
 * Check source and apply proxy
46
 * when `forceProxy` is set on layer options
47
 * @param {boolean} forceProxy
48
 * @param {string} src
49
 * @returns {string}
50
 */
51
const proxySource = (forceProxy, src) => {
1✔
52
    let newSrc = src;
42✔
53
    if (forceProxy && needProxy(src)) {
42!
54
        let proxyUrl = getProxyUrl();
×
55
        newSrc = proxyUrl + encodeURIComponent(src);
×
56
    }
57
    return newSrc;
42✔
58
};
59

60
const loadFunction = (options, headers) => function(image, src) {
47✔
61
    // fixes #3916, see https://gis.stackexchange.com/questions/175057/openlayers-3-wms-styling-using-sld-body-and-post-request
62
    let img = image.getImage();
34✔
63
    let newSrc = proxySource(options.forceProxy, src);
33✔
64

65
    if (typeof window.btoa === 'function' && src.length >= (options.maxLengthUrl || getConfigProp('miscSettings')?.maxURLLength || Infinity)) {
33✔
66
        // GET ALL THE PARAMETERS OUT OF THE SOURCE URL**
67
        let [url, ...dataEntries] = src.split("&");
9✔
68
        url = proxySource(options.forceProxy, url);
9✔
69

70
        // SET THE PROPER HEADERS AND FINALLY SEND THE PARAMETERS
71
        axios.post(url, "&" + dataEntries.join("&"), {
9✔
72
            headers: {
73
                "Content-type": "application/x-www-form-urlencoded;charset=utf-8",
74
                ...headers
75
            },
76
            responseType: 'arraybuffer'
77
        }).then(response => {
78
            if (response.status === 200) {
9!
79
                const uInt8Array = new Uint8Array(response.data);
9✔
80
                let i = uInt8Array.length;
9✔
81
                const binaryString = new Array(i);
9✔
82
                while (i--) {
9✔
83
                    binaryString[i] = String.fromCharCode(uInt8Array[i]);
×
84
                }
85
                const dataImg = binaryString.join('');
9✔
86
                const type = response.headers['content-type'];
9✔
87
                if (type.indexOf('image') === 0) {
9!
88
                    img.src = 'data:' + type + ';base64,' + window.btoa(dataImg);
9✔
89
                }
90
            }
91
        }).catch(e => {
92
            console.error(e);
×
93
        });
94
    } else {
95
        if (headers) {
24✔
96
            axios.get(newSrc, {
8✔
97
                headers,
98
                responseType: 'blob'
99
            }).then(response => {
100
                if (response.status === 200 && response.data) {
8!
101
                    image.getImage().src = URL.createObjectURL(response.data);
8✔
102
                } else {
103
                    console.error("Status code: " + response.status);
×
104
                }
105
            }).catch(e => {
106
                console.error(e);
8✔
107
            });
108
        } else {
109
            img.src = newSrc;
16✔
110
        }
111
    }
112
};
113
/**
114
    @param {object} options of the layer
115
    @return the Openlayers options from the layers ones and/or default.
116
    tiled params must be tru if not defined
117
*/
118
function wmsToOpenlayersOptions(options) {
119
    const params = optionsToVendorParams(options);
69✔
120
    // NOTE: can we use opacity to manage visibility?
121
    const result = assign({}, options.baseParams, {
69✔
122
        LAYERS: options.name,
123
        STYLES: options.style || "",
138✔
124
        FORMAT: options.format || 'image/png',
75✔
125
        TRANSPARENT: options.transparent !== undefined ? options.transparent : true,
69!
126
        SRS: CoordinatesUtils.normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS),
97✔
127
        CRS: CoordinatesUtils.normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS),
97✔
128
        ...getWMSVendorParams(options),
129
        VERSION: options.version || "1.3.0"
138✔
130
    }, assign(
131
        {},
132
        (options._v_ ? {_v_: options._v_} : {}),
69!
133
        (params || {}),
115✔
134
        (options.localizedLayerStyles &&
138!
135
            options.env && options.env.length &&
136
            options.group !== 'background' ? {ENV: generateEnvString(options.env) } : {})
137
    ));
138
    return addAuthenticationToSLD(result, options);
69✔
139
}
140

141
function getWMSURLs( urls ) {
142
    return urls.map((url) => url.split("\?")[0]);
49✔
143
}
144

145
function tileCoordsToKey(coords) {
146
    return coords.join(':');
3✔
147
}
148

149
function elevationLoadFunction(forceProxy, imageTile, src) {
150
    let newSrc = proxySource(forceProxy, src);
×
151
    const coords = imageTile.getTileCoord();
×
152
    imageTile.getImage().src = "";
×
153
    loadTile(newSrc, coords, tileCoordsToKey(coords));
×
154
}
155

156
function addTileLoadFunction(sourceOptions, options) {
157
    if (options.useForElevation) {
40✔
158
        return assign({}, sourceOptions, { tileLoadFunction: elevationLoadFunction.bind(null, [options.forceProxy]) });
4✔
159
    }
160
    return sourceOptions;
36✔
161
}
162

163
function getTileFromCoords(layer, pos) {
164
    const map = layer.get('map');
3✔
165
    const tileGrid = layer.getSource().getTileGrid();
3✔
166
    return tileGrid.getTileCoordForCoordAndZ(pos, map.getView().getZoom());
3✔
167
}
168

169

170
function getTileRelativePixel(layer, pos, tilePoint) {
171
    const tileGrid = layer.getSource().getTileGrid();
3✔
172
    const extent = tileGrid.getTileCoordExtent(tilePoint);
3✔
173
    const ratio = tileGrid.getTileSize() / (extent[2] - extent[0]);
3✔
174
    const x = Math.floor((pos[0] - extent[0]) * ratio);
3✔
175
    const y = Math.floor((extent[3] - pos[1]) * ratio);
3✔
176
    return { x, y };
3✔
177
}
178

179
function getElevation(pos) {
180
    try {
3✔
181
        const tilePoint = getTileFromCoords(this, pos);
3✔
182
        const tileSize = this.getSource().getTileGrid().getTileSize();
3✔
183
        const elevation = getElevationFunc(tileCoordsToKey(tilePoint), getTileRelativePixel(this, pos, tilePoint), tileSize, this.get('nodata'), this.get('littleEndian'));
3✔
184
        if (elevation.available) {
3!
185
            return elevation.value;
×
186
        }
187
        return <Message msgId={elevation.message} />;
3✔
188
    } catch (e) {
189
        return <Message msgId="elevationLoadingError" />;
×
190
    }
191
}
192
const toOLAttributions = credits => credits && creditsToAttribution(credits) || undefined;
50✔
193

194
const generateTileGrid = (options, map) => {
1✔
195
    const mapSrs = map?.getView()?.getProjection()?.getCode() || 'EPSG:3857';
40!
196
    const normalizedSrs = CoordinatesUtils.normalizeSRS(options.srs || mapSrs, options.allowedSRS);
40✔
197
    const tileSize = options.tileSize ? options.tileSize : 256;
40✔
198
    const extent = get(normalizedSrs).getExtent() || getProjection(normalizedSrs).extent;
40!
199
    const { TILED } = getWMSVendorParams(options);
40✔
200
    const customTileGrid = TILED && options.tileGridStrategy === 'custom' && options.tileGrids
40✔
201
        ? getTileGridFromLayerOptions({ tileSize, projection: normalizedSrs, tileGrids: options.tileGrids })
202
        : null;
203
    if (customTileGrid
40✔
204
        && (customTileGrid.resolutions || customTileGrid.scales)
205
        && (customTileGrid.origins || customTileGrid.origin)
206
        && (customTileGrid.tileSizes || customTileGrid.tileSize)) {
207
        const {
208
            resolutions: customTileGridResolutions,
209
            scales,
210
            origin,
211
            origins,
212
            tileSize: customTileGridTileSize,
213
            tileSizes
214
        } = customTileGrid;
1✔
215
        const projection = get(normalizedSrs);
1✔
216
        const metersPerUnit = projection.getMetersPerUnit();
1✔
217
        const scaleToResolution = s => s * 0.28E-3 / metersPerUnit;
3✔
218
        const resolutions = customTileGridResolutions
1!
219
            ? customTileGridResolutions
220
            : scales.map(scale => scaleToResolution(scale));
3✔
221
        return new TileGrid({
1✔
222
            extent,
223
            resolutions,
224
            tileSizes,
225
            tileSize: customTileGridTileSize,
226
            origin,
227
            origins
228
        });
229
    }
230
    const resolutions = options.resolutions || getResolutionsForProjection(normalizedSrs, {
39✔
231
        tileWidth: tileSize,
232
        tileHeight: tileSize,
233
        extent
234
    });
235
    const origin = options.origin ? options.origin : [extent[0], extent[1]];
39✔
236
    return new TileGrid({
39✔
237
        extent,
238
        resolutions,
239
        tileSize,
240
        origin
241
    });
242
};
243

244
const createLayer = (options, map) => {
1✔
245
    const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]);
47✔
246
    const queryParameters = wmsToOpenlayersOptions(options) || {};
47!
247
    urls.forEach(url => addAuthenticationParameter(url, queryParameters, options.securityToken));
49✔
248
    const headers = getAuthenticationHeaders(urls[0], options.securityToken);
47✔
249
    const vectorFormat = isVectorFormat(options.format);
47✔
250

251
    if (options.singleTile && !vectorFormat) {
47✔
252
        return new ImageLayer({
7✔
253
            msId: options.id,
254
            opacity: options.opacity !== undefined ? options.opacity : 1,
7✔
255
            visible: options.visibility !== false,
256
            zIndex: options.zIndex,
257
            minResolution: options.minResolution,
258
            maxResolution: options.maxResolution,
259
            source: new ImageWMS({
260
                url: urls[0],
261
                crossOrigin: options.crossOrigin,
262
                attributions: toOLAttributions(options.credits),
263
                params: queryParameters,
264
                ratio: options.ratio || 1,
13✔
265
                imageLoadFunction: loadFunction(options, headers)
266
            })
267
        });
268
    }
269
    const sourceOptions = addTileLoadFunction({
40✔
270
        attributions: toOLAttributions(options.credits),
271
        urls: urls,
272
        crossOrigin: options.crossOrigin,
273
        params: queryParameters,
274
        tileGrid: generateTileGrid(options, map),
275
        tileLoadFunction: loadFunction(options, headers)
276
    }, options);
277
    const wmsSource = new TileWMS({ ...sourceOptions });
40✔
278
    const layerConfig = {
40✔
279
        msId: options.id,
280
        opacity: options.opacity !== undefined ? options.opacity : 1,
40✔
281
        visible: options.visibility !== false,
282
        zIndex: options.zIndex,
283
        minResolution: options.minResolution,
284
        maxResolution: options.maxResolution
285
    };
286
    let layer;
287
    if (vectorFormat) {
40✔
288
        layer = new VectorTileLayer({
5✔
289
            ...layerConfig,
290
            source: new VectorTileSource({
291
                ...sourceOptions,
292
                format: new OL_VECTOR_FORMATS[options.format]({
293
                    layerName: '_layer_'
294
                }),
295
                tileUrlFunction: (tileCoord, pixelRatio, projection) => wmsSource.tileUrlFunction(tileCoord, pixelRatio, projection)
8✔
296
            })
297
        });
298
    } else {
299

300
        layer = new TileLayer({
35✔
301
            ...layerConfig,
302
            source: wmsSource
303
        });
304
    }
305
    layer.set('map', map);
40✔
306
    if (vectorFormat) {
40✔
307
        layer.set('wmsSource', wmsSource);
5✔
308
        if (options.vectorStyle) {
5✔
309
            applyStyle(options.vectorStyle, layer);
2✔
310
        }
311
    }
312
    if (options.useForElevation) {
40✔
313
        layer.set('nodata', options.nodata);
4✔
314
        layer.set('littleEndian', options.littleendian ?? false);
4✔
315
        layer.set('getElevation', getElevation.bind(layer));
4✔
316
    }
317
    return layer;
40✔
318
};
319

320
const mustCreateNewLayer = (oldOptions, newOptions) => {
1✔
321
    return (oldOptions.singleTile !== newOptions.singleTile
17✔
322
        || oldOptions.securityToken !== newOptions.securityToken
323
        || oldOptions.ratio !== newOptions.ratio
324
        // no way to remove attribution when credits are removed, so have re-create the layer is needed. Seems to be solved in OL v5.3.0, due to the ol commit 9b8232f65b391d5d381d7a99a7cd070fc36696e9 (https://github.com/openlayers/openlayers/pull/7329)
325
        || oldOptions.credits !== newOptions.credits && !newOptions.credits
326
        || isVectorFormat(oldOptions.format) !== isVectorFormat(newOptions.format)
327
        || isVectorFormat(oldOptions.format) && isVectorFormat(newOptions.format) && oldOptions.format !== newOptions.format
328
        || oldOptions.localizedLayerStyles !== newOptions.localizedLayerStyles
329
        || oldOptions.tileSize !== newOptions.tileSize
330
        || oldOptions.forceProxy !== newOptions.forceProxy
331
        || oldOptions.tileGridStrategy !== newOptions.tileGridStrategy
332
        || !isEqual(oldOptions.tileGrids, newOptions.tileGrids)
333
    );
334
};
335

336
Layers.registerType('wms', {
1✔
337
    create: createLayer,
338
    update: (layer, newOptions, oldOptions, map) => {
339
        const newIsVector = isVectorFormat(newOptions.format);
17✔
340

341
        if (mustCreateNewLayer(oldOptions, newOptions)) {
17✔
342
            // TODO: do we need to clean anything before re-creating stuff from scratch?
343
            return createLayer(newOptions, map);
6✔
344
        }
345
        let needsRefresh = false;
11✔
346
        if (newIsVector && newOptions.vectorStyle && !isEqual(newOptions.vectorStyle, oldOptions.vectorStyle || {})) {
11!
347
            applyStyle(newOptions.vectorStyle, layer);
×
348
            needsRefresh = true;
×
349
        }
350

351
        const wmsSource = layer.get('wmsSource') || layer.getSource();
11✔
352
        const vectorSource = newIsVector ? layer.getSource() : null;
11!
353

354
        if (oldOptions.srs !== newOptions.srs) {
11!
355
            const normalizedSrs = CoordinatesUtils.normalizeSRS(newOptions.srs, newOptions.allowedSRS);
×
356
            const extent = get(normalizedSrs).getExtent() || getProjection(normalizedSrs).extent;
×
357
            if (newOptions.singleTile && !newIsVector) {
×
358
                layer.setExtent(extent);
×
359
            } else {
360
                const tileGrid = generateTileGrid(newOptions, map);
×
361
                wmsSource.tileGrid = tileGrid;
×
362
                if (vectorSource) {
×
363
                    vectorSource.tileGrid = tileGrid;
×
364
                }
365
            }
366
            needsRefresh = true;
×
367
        }
368

369
        if (oldOptions.credits !== newOptions.credits && newOptions.credits) {
11✔
370
            wmsSource.setAttributions(toOLAttributions(newOptions.credits));
3✔
371
            needsRefresh = true;
3✔
372
        }
373

374
        let changed = false;
11✔
375
        let oldParams;
376
        let newParams;
377
        if (oldOptions && wmsSource && wmsSource.updateParams) {
11!
378
            if (oldOptions.params && newOptions.params) {
11✔
379
                changed = union(
4✔
380
                    Object.keys(oldOptions.params),
381
                    Object.keys(newOptions.params)
382
                ).reduce((found, param) => {
383
                    if (newOptions.params[param] !== oldOptions.params[param]) {
5✔
384
                        return true;
3✔
385
                    }
386
                    return found;
2✔
387
                }, false);
388
            } else if ((!oldOptions.params && newOptions.params) || (oldOptions.params && !newOptions.params)) {
7!
389
                changed = true;
×
390
            }
391
            oldParams = wmsToOpenlayersOptions(oldOptions);
11✔
392
            newParams = wmsToOpenlayersOptions(newOptions);
11✔
393
            changed = changed || ["LAYERS", "STYLES", "FORMAT", "TRANSPARENT", "TILED", "VERSION", "_v_", "CQL_FILTER", "SLD", "VIEWPARAMS"].reduce((found, param) => {
11✔
394
                if (oldParams[param] !== newParams[param]) {
80✔
395
                    return true;
2✔
396
                }
397
                return found;
78✔
398
            }, false);
399

400
            needsRefresh = needsRefresh || changed;
11✔
401
        }
402

403
        if (oldOptions.minResolution !== newOptions.minResolution) {
11✔
404
            layer.setMinResolution(newOptions.minResolution === undefined ? 0 : newOptions.minResolution);
1!
405
        }
406
        if (oldOptions.maxResolution !== newOptions.maxResolution) {
11✔
407
            layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution);
1!
408
        }
409
        if (needsRefresh) {
11✔
410
            // forces tile cache drop
411
            // this prevents old cached tiles at lower zoom levels to be
412
            // rendered during new params load
413
            wmsSource?.tileCache?.pruneExceptNewestZ?.();
8✔
414
            if (vectorSource) {
8!
415
                vectorSource.clear();
×
416
                vectorSource.refresh();
×
417
            }
418

419
            if (changed) {
8✔
420
                const params = assign(newParams, addAuthenticationToSLD(optionsToVendorParams(newOptions) || {}, newOptions));
5✔
421

422
                wmsSource.updateParams(assign(params, Object.keys(oldParams || {}).reduce((previous, key) => {
5!
423
                    return !isNil(params[key]) ? previous : assign(previous, {
45✔
424
                        [key]: undefined
425
                    });
426
                }, {})));
427
            }
428
        }
429
        return null;
11✔
430
    }
431
});
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