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

geosolutions-it / MapStore2 / 19735587487

27 Nov 2025 09:59AM UTC coverage: 76.667% (-0.3%) from 76.929%
19735587487

Pull #11119

github

web-flow
Fix: #11712 Support for template format on vector layers to visualize embedded conent (#11720)
Pull Request #11119: Layer Selection Plugin on ArcGIS, WFS & WMS layers

32268 of 50209 branches covered (64.27%)

7 of 13 new or added lines in 2 files covered. (53.85%)

3017 existing lines in 248 files now uncovered.

40158 of 52380 relevant lines covered (76.67%)

37.8 hits per line

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

85.76
/web/client/utils/PrintUtils.js
1
/*
2
 * Copyright 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 { reproject, getUnits, reprojectGeoJson, normalizeSRS } from './CoordinatesUtils';
10

11
import {addAuthenticationParameter} from './SecurityUtils';
12
import { calculateExtent, getGoogleMercatorScales, getResolutionsForProjection, getScales } from './MapUtils';
13
import { optionsToVendorParams } from './VendorParamsUtils';
14
import { colorToHexStr } from './ColorUtils';
15
import { getLayerConfig } from './TileConfigProvider';
16
import { extractValidBaseURL } from './TileProviderUtils';
17
import { getTileMatrix } from './WMTSUtils';
18
import { guessFormat } from './TMSUtils';
19
import { get as getProjection } from 'ol/proj';
20
import { isArray, filter, find, isEmpty, toNumber, castArray, reverse, includes } from 'lodash';
21
import { getFeature } from '../api/WFS';
22
import { generateEnvString } from './LayerLocalizationUtils';
23
import { ServerTypes } from './LayersUtils';
24
import PrintStyleParser from './styleparser/PrintStyleParser';
25
import url from 'url';
26

27
import { getStore } from "./StateUtils";
28
import { isLocalizedLayerStylesEnabledSelector, localizedLayerStylesEnvSelector } from '../selectors/localizedLayerStyles';
29
import { currentLocaleLanguageSelector } from '../selectors/locale';
30
import { printSpecificationSelector } from "../selectors/print";
31
import sortBy from "lodash/sortBy";
32
import head from "lodash/head";
33
import isNil from "lodash/isNil";
34
import get from "lodash/get";
35
import min from "lodash/min";
36
import trimEnd from 'lodash/trimEnd';
37
import React from 'react';
38
import { render, unmountComponentAtNode } from 'react-dom';
39
import { toPng } from 'html-to-image';
40
import VectorLegend from '../plugins/TOC/components/VectorLegend';
41

42
import { getGridGeoJson } from "./grids/MapGridsUtils";
43
import { isImageServerUrl } from './ArcGISUtils';
44
import { getWMSLegendConfig, LEGEND_FORMAT } from './LegendUtils';
45

46
const defaultScales = getGoogleMercatorScales(0, 21);
1✔
47
let PrintUtils;
48

49
const printStyleParser = new PrintStyleParser();
1✔
50

51
// For testing purposes
52
export const __internals__ = {
1✔
53
    toPng
54
};
55

56
/**
57
 * Renders a vector layer's legend to a base64 encoded PNG image.
58
 * It works by temporarily mounting a VectorLegend component to the DOM,
59
 * waiting for all its assets (like external images in rules) to load,
60
 * and then capturing the component's HTML as a PNG data URL.
61
 * @param {object} layer The MapStore layer object, with a geostyler style.
62
 * @returns {Promise<string|null>} A promise that resolves with the base64 data URL of the legend, or null if rendering fails.
63
 */
64
export function renderVectorLegendToBase64(layer) {
65
    if (!layer?.style || layer.style.format !== 'geostyler' || !layer.style.body?.rules || !layer.style.body.rules.length) {
5✔
66
        return Promise.resolve(null);
3✔
67
    }
68
    const container = typeof document !== 'undefined' && document.createElement('div');
2✔
69
    if (!container) {
2!
UNCOV
70
        return Promise.resolve(null);
×
71
    }
72

73
    document.body.appendChild(container);
2✔
74
    container.style.position = 'fixed';
2✔
75
    container.style.top = '0px';
2✔
76
    container.style.left = '0px';
2✔
77
    container.style.width = '200px';
2✔
78
    container.style.zIndex = -1;
2✔
79
    container.style.opacity = 0;
2✔
80
    container.style.fontSize = '10px';
2✔
81

82
    const styles = `
2✔
83
        .ms-legend-rule {
84
            display: flex;
85
            align-items: center;
86
            margin-bottom: 4px;
87
        }
88
        .ms-legend-icon {
89
            margin-right: 5px;
90
            flex-shrink: 0;
91
        }
92
    `;
93

94
    return new Promise(renderResolve => {
2✔
95
        render(
2✔
96
            <div>
97
                <style>{styles}</style>
98
                <VectorLegend
99
                    style={layer.style}
100
                    layer={layer}
101
                    interactive={false}
102
                    onChange={() => {}}
103
                />
104
            </div>,
105
            container,
106
            renderResolve
107
        );
108
    }).then(() => {
109
        const images = Array.from(container.querySelectorAll('img'));
2✔
110
        const promises = images.map(img => new Promise(imgResolve => {
2✔
UNCOV
111
            if (img.complete) {
×
UNCOV
112
                imgResolve();
×
UNCOV
113
                return;
×
114
            }
UNCOV
115
            img.onload = imgResolve;
×
UNCOV
116
            img.onerror = imgResolve;
×
117
        }));
118
        return Promise.all(promises);
2✔
119
    }).then(() => new Promise(resolve => setTimeout(resolve, 200)))
2✔
120
        .then(() => __internals__.toPng(container.querySelector('.ms-legend'), {
2✔
121
            quality: 3,
122
            pixelRatio: 1.2,
123
            skipFonts: true
124
        }))
125
        .then(dataUrl => {
126
            unmountComponentAtNode(container);
1✔
127
            document.body.removeChild(container);
1✔
128
            return dataUrl;
1✔
129
        })
130
        .catch(error => {
131
            if (container && document.body.contains(container)) {
1!
132
                unmountComponentAtNode(container);
1✔
133
                document.body.removeChild(container);
1✔
134
            }
135
            console.warn('Error rendering vector legend to base64:', error);
1✔
136
            return null;
1✔
137
        });
138
}
139

140
// Try to guess geomType, getting the first type available.
141
export const getGeomType = function(layer) {
1✔
142
    return layer.features && layer.features[0] && layer.features[0].geometry ? layer.features[0].geometry.type :
7✔
143
        layer.features && layer.features[0].features && layer.features[0].style && layer.features[0].style.type ? layer.features[0].style.type : undefined;
6!
144
};
145

146
/**
147
 * Utility functions for thumbnails
148
 * @memberof utils
149
 * @static
150
 * @name PrintUtils
151
 */
152

153
/**
154
 * Extracts the correct opacity from layer. if Undefined, the opacity is `1`.
155
 * @ignore
156
 * @param {object} layer the MapStore layer
157
 */
158
export const getOpacity = layer => layer.opacity || (layer.opacity === 0 ? 0 : 1.0);
57✔
159

160
/**
161
 * Preload data (e.g. WFS) before to sent it to the print tool.
162
 * @memberof utils.PrintUtils
163
 */
164
export const preloadData = (spec) => {
1✔
165
    // check if remote data
166
    const wfsLayers = filter(spec.layers, {type: "wfs"});
9✔
167
    if (wfsLayers.length > 0) {
9!
168
        // get data from WFS
UNCOV
169
        return Promise.all(
×
170
            wfsLayers.map(l =>
UNCOV
171
                getFeature(l.url, l.name, {
×
172
                    outputFormat: "application/json",
173
                    srsName: spec.projection,
174
                    ...(optionsToVendorParams(l) || {})
×
175
                })
UNCOV
176
                    .then(({data}) => ({
×
177
                        id: l.id,
178
                        geoJson: data
179
                    }))
180
            )
181
        // set geoJson in layer's spec
182
        ).then(replies => {
UNCOV
183
            return {
×
184
                ...spec,
185
                layers: (spec.layers || []).map(l => {
×
UNCOV
186
                    const layerData = find(replies, {id: l.id});
×
UNCOV
187
                    if (l.type === "wfs" && layerData) {
×
UNCOV
188
                        return {
×
189
                            ...l,
190
                            ...layerData
191

192
                        };
193
                    }
UNCOV
194
                    return l;
×
195
                })
196
            };
197
        });
198
    }
199
    return Promise.resolve(spec);
9✔
200
};
201
/**
202
 * Given a static resource, returns the resource's absolute
203
 * URL. Supports file paths with or without origin/protocol.
204
 * @param {string} uri the uri to transform
205
 * @param {string} [origin=window.location.origin] the origin to use
206
 * @memberof utils.PrintUtils
207
 */
208
export const toAbsoluteURL = (uri, origin) => {
1✔
209
    // Handle absolute URLs (with protocol-relative prefix)
210
    // Example: //domain.com/file.png
211
    if (uri.search(/^\/\//) !== -1) {
47✔
212
        return window.location.protocol + uri;
1✔
213
    }
214

215
    // Handle absolute URLs (with explicit origin)
216
    // Example: http://domain.com/file.png
217
    if (uri.search(/:\/\//) !== -1) {
46✔
218
        return uri;
27✔
219
    }
220

221
    // Handle absolute URLs (without explicit origin)
222
    // Example: /file.png
223
    if (uri.search(/^\//) !== -1) {
19!
224
        return (origin || window.location.origin) + uri;
19✔
225
    }
UNCOV
226
    return uri;
×
227
};
228
/**
229
 * Tranform the original URL configuration of the layer into a URL
230
 * usable for the print service.
231
 * @param  {string|array} input Original URL
232
 * @returns {string}       the URL modified as GeoServer requires
233
 * @memberof utils.PrintUtils
234
 */
235
export const normalizeUrl = (input) => {
1✔
236
    let result = isArray(input) ? input[0] : input;
45!
237
    if (result.indexOf('?') !== -1) {
45!
UNCOV
238
        result = result.substring(0, result.indexOf('?'));
×
239
    }
240
    return PrintUtils.toAbsoluteURL(result);
45✔
241
};
242
/**
243
 * Find the layout name for the given options.
244
 * The convention is: `PAGE_FORMAT + ("_2_pages_legend"|"_2_pages_legend"|"") + ("_landscape"|"")``
245
 * @param  {object} spec the spec with the options
246
 * @returns {string}      the layout name.
247
 */
248
export const getLayoutName = (spec) => {
1✔
249
    let layoutName = [spec.sheet];
512✔
250
    if (spec.includeLegend) {
512✔
251
        if (spec.twoPages) {
11✔
252
            layoutName.push('2_pages_legend');
10✔
253
        }
254
    } else {
255
        layoutName.push('no_legend');
501✔
256
    }
257
    if (spec.landscape) {
512✔
258
        layoutName.push('landscape');
1✔
259
    }
260
    return layoutName.join('_');
512✔
261
};
262
/**
263
 * Gets the print scales allowed from the capabilities of the print service.
264
 * @param  {capabilities} capabilities the capabilities of the print service
265
 * @returns {array}              the scales array
266
 * @memberof utils.PrintUtils
267
 */
268
export const getPrintScales = (capabilities) => {
1✔
269
    return capabilities.scales.slice(0).reverse().map((scale) => parseFloat(scale.value)) || [];
108!
270
};
271
/**
272
 * Guess the nearest zoom level in the allowed scales
273
 * @param  {number} zoom                      the zoom level
274
 * @param  {array} scales                    the allowed scales
275
 * @param  {array} [mapScales=defaultScales] the map scales
276
 * @returns {number}                          the index that best approximates the current map scale
277
 * @memberof utils.PrintUtils
278
 */
279
export const getNearestZoom = (zoom, scales, mapScales = defaultScales) => {
1✔
280
    const mapScale = mapScales[Math.round(zoom)];
21✔
281
    return scales.reduce((previous, current, index) => {
21✔
282
        return current < mapScale ? previous : index;
65✔
283
    }, 0);
284
};
285
/**
286
 * @memberof utils
287
 * Guess the map zoom level from print scale
288
 * @param  {number} zoom                      the zoom level
289
 * @param  {array} scales                    the allowed scales
290
 * @param  {array} [mapScales=defaultScales] the map scales
291
 * @returns {number}                          the index that best approximates the current map scale
292
 * @memberof utils.PrintUtils
293
 */
294
export const getMapZoom = (scaleZoom, scales, mapScales = defaultScales) => {
1!
UNCOV
295
    const scale = scales[Math.round(scaleZoom)];
×
UNCOV
296
    return mapScales.reduce((previous, current, index) => {
×
UNCOV
297
        return current < scale ? previous : index;
×
298
    }, 0) + 1;
299
};
300
/**
301
 * Get the mapSize for print preview, parsing the layout and limiting the width.
302
 * @param  {object} layout   the layout object
303
 * @param  {number} maxWidth the max width for the mapSize
304
 * @returns {object}          width and height of a map limited by the maxWidth and with the same ratio of the layout
305
 * @memberof utils.PrintUtils
306
 */
307
export const getMapSize = (layout, maxWidth) => {
1✔
308
    if (layout) {
3✔
309
        const width = layout.rotation ? layout.map.height : layout.map.width;
2✔
310
        const height = layout.rotation ? layout.map.width : layout.map.height;
2✔
311
        return {
2✔
312
            width: maxWidth,
313
            height: height / width * maxWidth
314
        };
315
    }
316
    return {
1✔
317
        width: 100,
318
        height: 100
319
    };
320
};
321

322
export const mapProjectionSelector = (state) => state?.print?.map?.projection ?? "EPSG:3857";
82✔
323

324
/**
325
 * Parse credit/attribution text by removing html tags within its text plus removing '|' symbol
326
 * @param  {string} creditText the layer credit/attribution text
327
 * @returns {string}       the parsed credit/attribution text after removing html tags plus '|' symbol within
328
 * @memberof utils.PrintUtils
329
 */
330
export function parseCreditRemovingTagsOrSymbol(creditText = "") {
×
331
    let parsedCredit = creditText;
7✔
332
    do {
7✔
333
        let tagStartIndex = parsedCredit.indexOf("<");
22✔
334
        let tagEndIndex = parsedCredit.indexOf(">");
22✔
335
        if (tagStartIndex !== -1 && tagEndIndex !== -1) {
22✔
336
            parsedCredit = parsedCredit.replace(parsedCredit.substring(tagStartIndex, tagEndIndex + 1), "");
18✔
337
        }
338
    } while (parsedCredit.includes("<") || parsedCredit.includes(">"));
29✔
339
    let hasOrSymbol = parsedCredit && parsedCredit.includes("|");
7✔
340
    if (hasOrSymbol) {
7!
341
        parsedCredit = parsedCredit?.replaceAll("|", "")?.replaceAll("  ", " ");
7✔
342
    }
343
    return parsedCredit;
7✔
344
}
345
/**
346
 * Gets the credits of layers in one text with '|' separated
347
 * @param  {object} layers the map layers for print
348
 * @returns {string}       the layers credits as a text '|' separated
349
 * @memberof utils.PrintUtils
350
 */
351
export const getLayersCredits = (layers) => {
1✔
352
    let layerCredits = layers.filter(lay => lay?.credits?.title || lay?.attribution).map((layer) => {
27✔
353
        const layerCreditTitle = layer?.credits?.title || layer?.attribution || '';
8!
354
        const hasOrSymbol = layerCreditTitle.includes('|');
8✔
355
        const hasHtmlTag = layerCreditTitle.includes('<');
8✔
356
        const layerCredit = (hasHtmlTag || hasOrSymbol)
8✔
357
            ? parseCreditRemovingTagsOrSymbol(layerCreditTitle)
358
            : layerCreditTitle;
359
        return layerCredit;
8✔
360
    });
361
    const uniqueCredits = [...new Set(layerCredits)];
19✔
362
    layerCredits = uniqueCredits.join(' | ');
19✔
363
    return layerCredits;
19✔
364
};
365
/**
366
 * Default screen DPI (96) to Print DPI (72). Used to calculate correct resolution for
367
 * screen preview and printed map.
368
 * @memberof utils.PrintUtils
369
 */
370
export const DEFAULT_PRINT_RATIO = 96.0 / 72.0;
1✔
371

372
/**
373
 * Returns the correct multiplier to sync the screen resolution and the printed map resolution.
374
 * @param {number} printSize printed map size (in print points (1/72"))
375
 * @param {number} screenSize screen preview size (in pixels)
376
 * @param {number} dpiRatio ratio screen_dpi / printed_dpi
377
 * @return {number} the resolution multiplier to apply to the screen preview
378
 * @memberof utils.PrintUtils
379
 */
380
export function getResolutionMultiplier(printSize, screenSize, dpiRatio = DEFAULT_PRINT_RATIO) {
1,068✔
381
    return printSize / screenSize * dpiRatio;
1,068✔
382
}
383

384
export function getScalesByResolutions(resolutions, ratio, projection = "EPSG:3857") {
×
385

386
    // Get the corresponding scales based on the resolutions
UNCOV
387
    const correspScales = (getScales(projection)).map(sc => sc * ratio);
×
388

389
    // Calculate scales for each resolution
UNCOV
390
    const scales = resolutions.map(res => {
×
UNCOV
391
        const firstRes = resolutions[0];
×
UNCOV
392
        const firstScale = correspScales[0];
×
393
        // Calculate the scale corresponding to the current resolution
UNCOV
394
        const correspondentScale = res * firstScale / firstRes;
×
UNCOV
395
        return correspondentScale / ratio;
×
396
    });
UNCOV
397
    return scales;
×
398
}
399

400
/**
401
 * Creates the mapfish print specification from the current configuration
402
 * @param  {object} spec the current configuration
403
 * @returns {object}      the mapfish print configuration to send to the server
404
 * @memberof utils.PrintUtils
405
 */
406
export const getMapfishPrintSpecification = (rawSpec, state) => {
1✔
407
    const {params, mergeableParams, excludeLayersFromLegend, ...baseSpec} = rawSpec;
17✔
408
    const spec = {...baseSpec, ...params};
17✔
409
    const printMap = state?.print?.map;
17✔
410
    const projectedCenter = reproject(spec.center, 'EPSG:4326', spec.projection);
17✔
411
    // * use [spec.zoom] the actual zoom in case useFixedScales = false else use [spec.scaleZoom] the fixed zoom scale not actual
412
    const projectedZoom = Math.round(printMap?.useFixedScales && !printMap?.editScale ? spec.scaleZoom : spec.zoom);
17✔
413
    const layout = head(state?.print?.capabilities?.layouts?.filter((l) => l.name === getLayoutName(spec)) || []);
17✔
414
    const ratio = getResolutionMultiplier(layout?.map?.width, 370) ?? 1;
17!
415
    const scales = printMap?.editScale ?
17✔
416
        printMap.mapPrintResolutions?.length ?
2!
417
            getScalesByResolutions(printMap.mapPrintResolutions, ratio, spec.projection) :
418
            getScales(spec.projection) : spec.scales || getScales(spec.projection);
27✔
419
    const reprojectedScale = printMap?.editScale ? scales[projectedZoom] : scales[projectedZoom] || defaultScales[projectedZoom];
17✔
420

421
    const projectedSpec = {
17✔
422
        ...spec,
423
        center: projectedCenter,
424
        scaleZoom: projectedZoom
425
    };
426
    const legendLayersList = spec.layers.filter(layer => !includes(excludeLayersFromLegend, layer.name));
17✔
427

428
    const legendLayersPromise = PrintUtils.getMapfishLayersSpecification(legendLayersList, projectedSpec, state, 'legend');
17✔
429

430
    return legendLayersPromise.then((legendLayers) => {
17✔
431
        const layersPromise = PrintUtils.getMapfishLayersSpecification(spec.layers, projectedSpec, state, 'map');
17✔
432
        return layersPromise.then((layers) => {
17✔
433
            return {
17✔
434
                "units": getUnits(spec.projection),
435
                "srs": normalizeSRS(spec.projection || 'EPSG:3857'),
17!
436
                "layout": PrintUtils.getLayoutName(projectedSpec),
437
                "dpi": parseInt(spec.resolution, 10),
438
                "outputFilename": "mapstore-print",
439
                "geodetic": false,
440
                "mapTitle": spec.name || '',
33✔
441
                "comment": spec.description || '',
33✔
442
                "layers": layers,
443
                "pages": [
444
                    {
445
                        "center": [
446
                            projectedCenter.x,
447
                            projectedCenter.y
448
                        ],
449
                        "scale": reprojectedScale,
450
                        "rotation": !isNil(spec.rotation) ? -Number(spec.rotation) : 0 // negate the rotation value to match rotation in map preview and printed output
17!
451
                    }
452
                ],
453
                "legends": legendLayers,
454
                "credits": getLayersCredits(spec.layers),
455
                ...(mergeableParams ? {mergeableParams} : {}),
17!
456
                ...params
457
            };
458
        });
459
    });
460
};
461

462
export const localizationFilter = (state, spec) => {
1✔
463
    const localizationEnabled = isLocalizedLayerStylesEnabledSelector(state);
9✔
464
    const localizationEnv = localizedLayerStylesEnvSelector(state);
9✔
465
    const localizedSpec = localizationEnabled ? {
9!
466
        ...spec,
467
        env: localizationEnv,
468
        currentLanguage: currentLocaleLanguageSelector(state)
469
    } : spec;
470

471
    return Promise.resolve(localizedSpec);
9✔
472
};
473
export const wfsPreloaderFilter = (state, spec) => preloadData(spec);
9✔
474
export const toMapfish = (state, spec) => Promise.resolve(getMapfishPrintSpecification(spec, state));
8✔
475

476
const defaultPrintingServiceTransformerChain = [
1✔
477
    {name: "localization", transformer: localizationFilter},
478
    {name: "wfspreloader", transformer: wfsPreloaderFilter},
479
    {name: "mapfishSpecCreator", transformer: toMapfish}
480
];
481

482
let userTransformerChain = [];
1✔
483
let mapTransformerChain = [];
1✔
484
let validatorsChain = [];
1✔
485

486
function addOrReplaceTransformers(chain, transformers) {
487
    return transformers.reduce((res, transformer) => {
88✔
488
        if (res.findIndex(t => t.name === transformer.name) === -1) {
88✔
489
            return [...res, transformer];
84✔
490
        }
491
        return res.map(t => t.name === transformer.name ? transformer : t);
8✔
492
    }, chain);
493
}
494

495
export function getSpecTransformerChain() {
496
    const userOffset = defaultPrintingServiceTransformerChain.length;
22✔
497
    return sortBy(addOrReplaceTransformers(
22✔
498
        defaultPrintingServiceTransformerChain.map((t, index) => ({...t, position: index})),
66✔
499
        userTransformerChain.map((t, index) => ({...t, position: t.position ?? index + userOffset}))
22✔
500
    ), ["position"]);
501
}
502

503
export function getMapTransformerChain() {
504
    return mapTransformerChain;
59✔
505
}
506

507
export function getValidatorsChain() {
508
    return validatorsChain;
52✔
509
}
510

511
/**
512
 * Resets the list of transformers and validators.
513
 * @memberof utils.PrintUtils
514
 */
515
export function resetDefaultPrintingService() {
516
    userTransformerChain = [];
53✔
517
    mapTransformerChain = [];
53✔
518
    validatorsChain = [];
53✔
519
}
520

521
/**
522
 * Adds/Updates a user custom transformer for the default printing service spec transformer chain.
523
 *
524
 * Transformers are called by the default printing service to enrich / change the spec payload for mapfish-print
525
 * before calling the remote service.
526
 *
527
 * Adding a new transformer allows adding new variables for a custom config.yaml, or process the default
528
 * ones to implement custom behaviour.
529
 *
530
 * @param {string} name name of the transformer (allows replacing one of the default ones, by specifying its name).
531
 *      default transformers are: `localization`, `wfspreloader`, `mapfishSpecCreator`.
532
 * @param {function} transformer (state, spec) => Promise<spec>
533
 * @param {int} position position in the chain (0-indexed), allows inserting a transformer between existing ones
534
 * @memberof utils.PrintUtils
535
 *
536
 * @example
537
 * // add a transformer to append a new property to the spec
538
 * addTransformer("mytransform", (state, spec) => ({...spec, newprop: state.print.myprop}))
539
 *
540
 * If you need to use addTransformer in an extension, use action ADD_PRINT_TRANSFORMER from print module
541
 * Otherwise, the let userTransformerChain are copy to your extension and not override the reference in the print module of MapStore2 framework
542
 */
543
export function addTransformer(name, transformer, position) {
544
    userTransformerChain = addOrReplaceTransformers(userTransformerChain, [{name, transformer, position}]);
36✔
545
}
546

547
/**
548
 * Adds/Updates a map custom transformer for the default printing service map object transformer chain.
549
 *
550
 * Map transformers can be used to implement custom behaviour that changes map related properties and
551
 * should be reflected on the printing plugin dialog (e.g. the map-preview).
552
 *
553
 * These are applied to the print state map fragment before being passed as a map property to the Print
554
 * plugin items.
555
 *
556
 * @param {string} name name of the transformer (allows replacing and existing one).
557
 * @param {function} transformer (state, map) => map
558
 * @example
559
 * // add a map transformer to increase the map zoom by 1
560
 * addMapTransformer("mymaptransform", (state, map) => ({...map, zoom: map.zoom + 1}))
561
 */
562
export function addMapTransformer(name, transformer) {
563
    mapTransformerChain = addOrReplaceTransformers(mapTransformerChain, [{name, transformer}]);
30✔
564
}
565

566
function addOrReplaceValidators(chain, list) {
567
    return list.reduce((res, validator) => {
29✔
568
        if (res.findIndex(v => v.id === validator.id) === -1) {
29✔
569
            return [...res, validator];
28✔
570
        }
571
        return res.map(v => v.id === validator.id ? validator : v);
1!
572
    }, chain);
573
}
574

575
/**
576
 * Adds a new validation function.
577
 * @param {string} id unique id of the validator (a validator with the same id will be replaced).
578
 * @param {string} name binding name of the validator (bind the validator result to a specific item / plugin, by item id).
579
 * @param {function} validator (state, current_validation) => { valid: true/false, errors: ["message", ...] }
580
 *
581
 * @example
582
 * // add a validator for the myplugin plugin, bound to the map-preview component
583
 * addValidator("myplugin", "map-preview", (state, current) => state.print.myprop ? {valid: true} : {valid: false, errors: ["myprop missing"]})
584
 */
585
export function addValidator(id, name, validator) {
586
    validatorsChain = addOrReplaceValidators(validatorsChain, [{id, name, validator}]);
29✔
587
}
588

589
/**
590
 * Returns the default printing service.
591
 *
592
 * A printing service implements all the basic functionalities of a printing engine.
593
 *
594
 *  - The print function, whose goal is to transform the Print plugin
595
 *    specification object into a specification for the chosen printing engine.
596
 *
597
 *    This service is compatible with the mapfish-2 printing engine and works by applying a chain of transformers,
598
 *    summing up the defaultPrintingServiceTransformerChain list, to eventual custom transformers,
599
 *    added with addTransformer.
600
 *
601
 *    Each transformer is a function reiceiving two parameters, the redux global state and the print
602
 *    specification object returned by the previous chain step, and returning a Promise of the transformed
603
 *    specification:
604
 *
605
 *     (state, spec) => Promise.resolve(<transformed spec>)
606
 *
607
 *    Project specific transformers can be added to the end of the chain using the addTransformer function.
608
 *
609
 *  - The validate function, that validates current user input in the printing dialog and outputs
610
 *    eventual validation error to be used by the UI items (to show errors, etc.).
611
 *
612
 *    It works by applying a chain of validators, that enrich the validation result object.
613
 *
614
 *    Each validator has a name, and a function reiceiving two parameters, the redux global state and the
615
 *    actual validation object for the name:
616
 *
617
 *     (state, validation) => {valid: true/false, errors: ["message", ...]}
618
 *
619
 *    Project specific validators can be added to the end of the chain using the addValidator function.
620
 *
621
 *  - The getMapConfiguration function, that returns a map configuration object for the UI items.
622
 *
623
 *    It works by applying a chain of map transformers, that transform the map configuration object.
624
 *
625
 *    Each transformer is a function reiceiving two parameters, the redux global state and the
626
 *    actual map configuration object:
627
 *
628
 *     (state, map) => <transformed map>
629
 *
630
 *    Project specific transformers can be added to the end of the chain using the addMapTransformer function.
631
 *
632
 * @returns {object} the default printint service.
633
 * @memberof utils.PrintUtils
634
 */
635
export const getDefaultPrintingService = () => {
1✔
636
    return {
24✔
637
        print: (extra) => {
638
            const state = getStore().getState();
9✔
639
            const printSpec = printSpecificationSelector(state);
9✔
640
            const intialSpec = extra ? {
9✔
641
                ...printSpec,
642
                ...extra
643
            } : printSpec;
644
            return getSpecTransformerChain().map(t => t.transformer).reduce((previous, f) => {
35✔
645
                return previous.then(spec=> f(state, spec));
35✔
646
            }, Promise.resolve(intialSpec));
647
        },
648
        validate: () => {
649
            const state = getStore().getState();
48✔
650
            return getValidatorsChain().reduce((acc, current) => {
48✔
651
                const previousValidation = acc[current.name] ?? {valid: true, errors: []};
37✔
652
                const validation = current.validator(state, previousValidation);
37✔
653
                return {
37✔
654
                    ...acc,
655
                    [current.name]: {
656
                        valid: previousValidation.valid && validation.valid,
73✔
657
                        errors: [...previousValidation.errors, ...(validation.errors || [])]
72✔
658
                    }
659
                };
660
            }, {});
661
        },
662
        getMapConfiguration: () => {
663
            const state = getStore().getState();
53✔
664
            return getMapTransformerChain().map(t => t.transformer).reduce((acc, t) => {
53✔
665
                return t(state, acc);
41✔
666
            }, state?.print?.map || {});
53!
667
        }
668
    };
669
};
670

671

672
/**
673
 * Returns vendor params that can be used when calling wms server for print requests
674
 * @param {layer} the layer object
675
 */
676
export const getPrintVendorParams = (layer) => {
1✔
677
    if (layer?.serverType === ServerTypes.NO_VENDOR) {
15✔
678
        return {};
1✔
679
    }
680
    return { "TILED": true };
14✔
681
};
682

683
export const getLegendIconsSize = (spec = {}, layer = {}) => {
1!
684
    const forceIconSize = (spec.forceIconsSize || layer.group === 'background');
11✔
685
    const width = forceIconSize ? spec.iconsWidth : get(layer, 'legendOptions.legendWidth', 12);
11✔
686
    const height = forceIconSize ? spec.iconsHeight : get(layer, 'legendOptions.legendHeight', 12);
11✔
687
    return {
11✔
688
        width,
689
        height,
690
        minSymbolSize: min([width, height])
691
    };
692
};
693

694
/**
695
 * Generate the layers (or legend) specification for print.
696
 * @param  {array} layers  the layers configurations
697
 * @param  {spec} spec    the print configurations
698
 * @param  {string} purpose allowed values: `map|legend`. Tells which spec to generate.
699
 * @returns {array}         the configuration array for layers (or legend) to send to the print service.
700
 * @memberof utils.PrintUtils
701
 */
702
export const getMapfishLayersSpecification = (layers, spec, state, purpose) => {
1✔
703
    const filtered = layers.filter(layer =>
45✔
704
        PrintUtils.specCreators[layer.type] &&
40✔
705
        PrintUtils.specCreators[layer.type][purpose]
706
    );
707

708
    return Promise.all(
45✔
709
        filtered.map(layer =>
710
            PrintUtils.specCreators[layer.type][purpose](layer, spec, state)
30✔
711
        )
712
    ).then(results => results.filter(r => r));
45✔
713
};
714

715
export const specCreators = {
1✔
716
    wms: {
717
        map: (layer, spec) => ({
13✔
718
            "baseURL": PrintUtils.normalizeUrl(layer.url) + '?',
719
            "opacity": getOpacity(layer),
720
            "singleTile": false,
721
            "type": "WMS",
722
            "layers": [
723
                layer.name
724
            ],
725
            "format": layer.format || "image/png",
23✔
726
            "styles": [
727
                layer.style || ''
24✔
728
            ],
729
            "customParams": addAuthenticationParameter(PrintUtils.normalizeUrl(layer.url), Object.assign({
730
                "TRANSPARENT": true,
731
                ...getPrintVendorParams(layer),
732
                "EXCEPTIONS": "application/vnd.ogc.se_inimage",
733
                "scaleMethod": "accurate",
734
                "ENV": generateEnvString(spec.env)
735
            }, layer.baseParams || {}, layer.params || {}, {
47✔
736
                ...optionsToVendorParams({
737
                    layerFilter: layer.layerFilter,
738
                    filterObj: layer.filterObj
739
                })
740
            }
741
            ))}),
742
        legend: (layer, spec) => {
743
            const legendOptions = "forceLabels:" + (spec.forceLabels ? "on" : "") + ";fontAntialiasing:" + spec.antiAliasing + ";dpi:" + spec.legendDpi + ";fontStyle:" + (spec.bold && "bold" || (spec.italic && "italic") || '') + ";fontName:" + spec.fontFamily + ";fontSize:" + spec.fontSize;
7!
744
            return {
7✔
745
                "name": layer.title || layer.name,
12✔
746
                "classes": [
747
                    {
748
                        "name": "",
749
                        "icons": [
750
                            PrintUtils.normalizeUrl(layer.url) + url.format({
751
                                query: addAuthenticationParameter(PrintUtils.normalizeUrl(layer.url), {
752
                                    ...getWMSLegendConfig({layer, legendOptions, mapBbox: spec.bbox, mapSize: spec.size, projection: spec.projection, format: LEGEND_FORMAT.IMAGE}),
753
                                    TRANSPARENT: true,
754
                                    EXCEPTIONS: "application/vnd.ogc.se_xml",
755
                                    VERSION: "1.1.1",
756
                                    SCALE: spec.scale,
757
                                    ...getLegendIconsSize(spec, layer),
758
                                    ...(spec.language ? {LANGUAGE: spec.language} : {})
7✔
759
                                })
760
                            })
761
                        ]
762
                    }
763
                ]
764
            };
765
        }
766
    },
767
    vector: {
768
        map: (layer, spec) => ({
6✔
769
            type: 'Vector',
770
            name: layer.name,
771
            "opacity": getOpacity(layer),
772
            styleProperty: "ms_style",
773
            styles: {
774
                1: PrintUtils.toOpenLayers2Style(layer, layer.style),
775
                "Polygon": PrintUtils.toOpenLayers2Style(layer, layer.style, "Polygon"),
776
                "LineString": PrintUtils.toOpenLayers2Style(layer, layer.style, "LineString"),
777
                "Point": PrintUtils.toOpenLayers2Style(layer, layer.style, "Point"),
778
                "FeatureCollection": PrintUtils.toOpenLayers2Style(layer, layer.style, "FeatureCollection")
779
            },
780
            geoJson: reprojectGeoJson({
781
                type: "FeatureCollection",
782
                features: layer?.style?.format === 'geostyler' && layer?.style?.body
14✔
783
                    ? printStyleParser.writeStyle(layer.style.body, true)({ layer, spec })
784
                    : layer.features.map( f => ({...f, properties: {...f.properties, ms_style: f && f.geometry && f.geometry.type && f.geometry.type.replace("Multi", "") || 1}}))
4!
785
            },
786
            "EPSG:4326",
787
            spec.projection)
788
        }
789
        ),
790
        legend: (layer) => {
UNCOV
791
            return renderVectorLegendToBase64(layer)
×
792
                .then(legendImage => {
UNCOV
793
                    if (legendImage) {
×
UNCOV
794
                        return {
×
795
                            name: layer?.title ?? layer?.name,
×
796
                            classes: [
797
                                {
798
                                    name: '',
799
                                    icons: [legendImage]
800
                                }
801
                            ]
802
                        };
803
                    }
UNCOV
804
                    return null;
×
805
                });
806
        }
807
    },
808
    graticule: {
809
        map: (layer, spec, state) => {
810
            const layout = head(state?.print?.capabilities.layouts.filter((l) => l.name === getLayoutName(spec)));
3✔
811
            const ratio = getResolutionMultiplier(layout?.map?.width, spec.size?.width ?? 370) ?? 1;
3!
812
            const resolutions = getResolutionsForProjection(spec.projection).map(r => r * ratio);
93✔
813
            const resolution = resolutions[spec.scaleZoom];
3✔
814
            return {
3✔
815
                type: 'Vector',
816
                name: layer.name || "graticule",
3!
817
                "opacity": getOpacity(layer),
818
                styleProperty: "ms_style",
819
                styles: {
820
                    "lines": PrintUtils.toOpenLayers2Style(layer, layer.style, "GraticuleLines"),
821
                    "xlabels": PrintUtils.toOpenLayers2TextStyle(layer, layer.labelXStyle, "GraticuleXLabels"),
822
                    "ylabels": PrintUtils.toOpenLayers2TextStyle(layer, layer.labelYStyle, "GraticuleYLabels"),
823
                    "frame": PrintUtils.toOpenLayers2Style(layer, layer.frameStyle, "GraticuleFrame")
824
                },
825
                geoJson: getGridGeoJson({
826
                    resolutions,
827
                    mapProjection: spec.projection,
828
                    gridProjection: layer.srs || spec.projection,
6✔
829
                    extent: calculateExtent(spec.center, resolution, spec.size, spec.projection),
830
                    zoom: spec.scaleZoom,
831
                    withLabels: true,
832
                    xLabelFormatter: layer.xLabelFormatter,
833
                    yLabelFormatter: layer.yLabelFormatter,
834
                    xLabelStyle: PrintUtils.toOpenLayers2TextStyle(layer, layer.labelXStyle, "GraticuleXLabels"),
835
                    yLabelStyle: PrintUtils.toOpenLayers2TextStyle(layer, layer.labelYStyle, "GraticuleYLabels"),
836
                    frameSize: layer.frameRatio
837
                })
838
            };
839
        }
840
    },
841
    wfs: {
842
        map: (layer) => ({
3✔
843
            type: 'Vector',
844
            name: layer.name,
845
            "opacity": getOpacity(layer),
846
            styleProperty: "ms_style",
847
            styles: {
848
                1: PrintUtils.toOpenLayers2Style(layer, layer.style),
849
                "Polygon": PrintUtils.toOpenLayers2Style(layer, layer.style, "Polygon"),
850
                "LineString": PrintUtils.toOpenLayers2Style(layer, layer.style, "LineString"),
851
                "Point": PrintUtils.toOpenLayers2Style(layer, layer.style, "Point"),
852
                "FeatureCollection": PrintUtils.toOpenLayers2Style(layer, layer.style, "FeatureCollection")
853
            },
854
            // NOTE: data in this case have to be pre-loaded, in the correct projection
855
            geoJson: layer.geoJson && {
3!
856
                type: "FeatureCollection",
857
                features: layer?.style?.format === 'geostyler' && layer?.style?.body
×
858
                    ? printStyleParser.writeStyle(layer.style.body, true)({ layer: { ...layer, features: layer.geoJson.features } })
UNCOV
859
                    : layer.geoJson.features.map(f => ({ ...f, properties: { ...f.properties, ms_style: f && f.geometry && f.geometry.type && f.geometry.type.replace("Multi", "") || 1 } }))
×
860
            }
861
        }
862
        )
863
    },
864
    osm: {
865
        map: (layer = {}) => ({
13!
866
            "baseURL": "http://a.tile.openstreetmap.org/",
867
            "opacity": getOpacity(layer),
868
            "singleTile": false,
869
            "type": "OSM",
870
            "maxExtent": [
871
                -20037508.3392,
872
                -20037508.3392,
873
                20037508.3392,
874
                20037508.3392
875
            ],
876
            "tileSize": [
877
                256,
878
                256
879
            ],
880
            "extension": "png",
881
            "resolutions": [
882
                156543.03390625,
883
                78271.516953125,
884
                39135.7584765625,
885
                19567.87923828125,
886
                9783.939619140625,
887
                4891.9698095703125,
888
                2445.9849047851562,
889
                1222.9924523925781,
890
                611.4962261962891,
891
                305.74811309814453,
892
                152.87405654907226,
893
                76.43702827453613,
894
                38.218514137268066,
895
                19.109257068634033,
896
                9.554628534317017,
897
                4.777314267158508,
898
                2.388657133579254,
899
                1.194328566789627,
900
                0.5971642833948135
901
            ]
902
        })
903
    },
904
    mapquest: {
905
        map: (layer = {}) => ({
3!
906
            "baseURL": "http://otile1.mqcdn.com/tiles/1.0.0/map/",
907
            "opacity": getOpacity(layer),
908
            "singleTile": false,
909
            "type": "OSM",
910
            "maxExtent": [
911
                -20037508.3392,
912
                -20037508.3392,
913
                20037508.3392,
914
                20037508.3392
915
            ],
916
            "tileSize": [
917
                256,
918
                256
919
            ],
920
            "extension": "png",
921
            "resolutions": [
922
                156543.03390625,
923
                78271.516953125,
924
                39135.7584765625,
925
                19567.87923828125,
926
                9783.939619140625,
927
                4891.9698095703125,
928
                2445.9849047851562,
929
                1222.9924523925781,
930
                611.4962261962891,
931
                305.74811309814453,
932
                152.87405654907226,
933
                76.43702827453613,
934
                38.218514137268066,
935
                19.109257068634033,
936
                9.554628534317017,
937
                4.777314267158508,
938
                2.388657133579254,
939
                1.194328566789627,
940
                0.5971642833948135
941
            ]
942
        })
943
    },
944
    wmts: {
945
        map: (layer, spec) => {
946
            const SRS =  spec.projection;
5✔
947
            const { tileMatrixSet, tileMatrixSetName} = getTileMatrix(layer, SRS); // TODO: use spec SRS.
5✔
948
            if (!tileMatrixSet) {
5!
UNCOV
949
                throw Error("tile matrix not found for pdf EPSG" + SRS);
×
950
            }
951
            const matrixIds = PrintUtils.getWMTSMatrixIds(layer, tileMatrixSet);
5✔
952
            const baseURL = PrintUtils.normalizeUrl(castArray(layer.url)[0]);
5✔
953
            let dimensionParams = {};
5✔
954
            if (baseURL.indexOf('{Style}') >= 0) {
5✔
955
                dimensionParams = {
1✔
956
                    "dimensions": ["Style"],
957
                    "params": {
958
                        "STYLE": layer.style
959
                    }
960
                };
961
            }
962
            return {
5✔
963
                "baseURL": encodeURI(baseURL),
964
                // "dimensions": isEmpty(layer.dimensions) && layer.dimensions || null,
965

966

967
                "format": layer.format || "image/png",
5!
968
                "type": "WMTS",
969
                "layer": layer.name,
970
                "customParams ": addAuthenticationParameter(layer.capabilitiesURL, Object.assign({
971
                    "TRANSPARENT": true
972
                })),
973
                // rest parameter style is not included
974
                // so simulate with dimensions and params
975
                ...dimensionParams,
976
                "matrixIds": matrixIds,
977
                "matrixSet": tileMatrixSetName,
978
                "style": layer.style,
979
                "name": layer.name,
980
                "requestEncoding": layer.requestEncoding === "RESTful" ? "REST" : layer.requestEncoding || "KVP",
9!
981
                "opacity": getOpacity(layer),
982
                "version": layer.version || "1.0.0"
10✔
983
            };
984
        }
985
    },
986
    tileprovider: {
987
        map: (layer) => {
988
            // details here: http://www.mapfish.org/doc/print/protocol.html#xyz
989
            const [providerURL, layerConfig] = getLayerConfig(layer.provider, layer);
7✔
990
            if (!isEmpty(layerConfig)) {
7!
991
                let validURL = extractValidBaseURL({ ...layerConfig, url: layerConfig?.url ?? providerURL });
7✔
992
                if (!validURL) {
7!
UNCOV
993
                    throw Error("No base URL found for this layer");
×
994
                }
995
                // transform in xyz format for mapfish-print.
996
                const queryIndex = validURL.indexOf("?");
7✔
997
                const firstBracketIndex = validURL.indexOf('{');
7✔
998
                const baseURL = validURL.slice(0, firstBracketIndex);
7✔
999
                const pathSection = queryIndex < 0
7✔
1000
                    ? validURL.slice(firstBracketIndex)
1001
                    : validURL.slice(firstBracketIndex, queryIndex);
1002
                const pathFormat = pathSection
7✔
1003
                    .replace("{x}", "${x}")
1004
                    .replace("{y}", "${y}")
1005
                    .replace("{z}", "${z}");
1006
                // TODO: support bounds
1007
                return {
7✔
1008
                    baseURL,
1009
                    path_format: pathFormat,
1010
                    "type": 'xyz',
1011
                    "extension": pathSection.split('.').pop() || "png",
7!
1012
                    "opacity": getOpacity(layer),
1013
                    "tileSize": [256, 256],
1014
                    "maxExtent": [-20037508.3392, -20037508.3392, 20037508.3392, 20037508.3392],
1015
                    "resolutions": [
1016
                        156543.03390625,
1017
                        78271.516953125,
1018
                        39135.7584765625,
1019
                        19567.87923828125,
1020
                        9783.939619140625,
1021
                        4891.9698095703125,
1022
                        2445.9849047851562,
1023
                        1222.9924523925781,
1024
                        611.4962261962891,
1025
                        305.74811309814453,
1026
                        152.87405654907226,
1027
                        76.43702827453613,
1028
                        38.218514137268066,
1029
                        19.109257068634033,
1030
                        9.554628534317017,
1031
                        4.777314267158508,
1032
                        2.388657133579254,
1033
                        1.194328566789627,
1034
                        0.5971642833948135
1035
                    ].filter( (_, i) => {
1036
                        let isIncluded = true;
133✔
1037
                        if (layerConfig.maxNativeZoom) {
133✔
1038
                            isIncluded = isIncluded && i <= layerConfig.maxNativeZoom;
95✔
1039
                        }
1040
                        return isIncluded;
133✔
1041
                    }),
1042
                    "customParams": Object.fromEntries((new URL(validURL)).searchParams)
1043
                };
1044
            }
UNCOV
1045
            return {};
×
1046
        }
1047
    },
1048
    tms: {
1049
        map: (layer) => {
1050
            // layer.tileMapService is like tileMapUrl, but with the layer name in the tail.
1051
            // e.g. "https://server.org/gwc/service/tms/1.0.0" - "https://server.org/gwc/service/tms/1.0.0/workspace%3Alayer@EPSG%3A3857@png"
1052
            const layerName = layer.tileMapUrl.split(layer.tileMapService + "/")[1];
4✔
1053
            return {
4✔
1054
                type: 'tms',
1055
                opacity: getOpacity(layer),
1056
                layer: layerName,
1057
                // baseURL for mapfish print required to remove the version
1058
                baseURL: layer.tileMapService.substring(0, layer.tileMapService.lastIndexOf("/1.0.0")),
1059
                tileSize: layer.tileSize,
1060
                format: guessFormat(layer.tileMapUrl),
1061
                "maxExtent": [
1062
                    -20037508.3392,
1063
                    -20037508.3392,
1064
                    20037508.3392,
1065
                    20037508.3392
1066
                ],
1067
                resolutions: layer.tileSets.map(({resolution}) => resolution)
124✔
1068
                // letters: ... to implement
1069

1070
            };
1071
        }
1072
    },
1073
    arcgis: {
1074
        map: (layer, spec, state) => {
1075
            const layout = head(state?.print?.capabilities.layouts.filter((l) => l.name === getLayoutName(spec)));
4✔
1076
            const ratio = getResolutionMultiplier(layout?.map?.width, spec.size?.width ?? 370) ?? 1;
4!
1077
            const resolutions = getResolutionsForProjection(spec.projection).map(r => r * ratio);
124✔
1078
            const resolution = resolutions[spec.scaleZoom];
4✔
1079
            const extent = calculateExtent(spec.center, resolution, spec.size, spec.projection);
4✔
1080
            const sr = spec.projection
4✔
1081
                .replace('EPSG:', '')
1082
                .replace('900913', '3857');
1083
            return {
4✔
1084
                type: 'Image',
1085
                opacity: layer.opacity ?? 1.0,
6✔
1086
                name: layer.name ?? -1,
4!
1087
                baseURL: url.format({
1088
                    ...url.parse(`${trimEnd(layer.url, '/')}/${isImageServerUrl(layer.url) ? 'exportImage' : 'export'}`),
4!
1089
                    query: {
1090
                        F: 'image',
1091
                        ...(layer.name !== undefined  && { LAYERS: `show:${layer.name}` }),
8✔
1092
                        FORMAT: layer.format || 'PNG32',
4!
1093
                        TRANSPARENT: true,
1094
                        SIZE: `${layout?.map?.width},${layout?.map?.height}`,
1095
                        bbox: extent.join(','),
1096
                        BBOXSR: sr,
1097
                        IMAGESR: sr,
1098
                        DPI: 90
1099
                    }
1100
                }),
1101
                extent
1102
            };
1103
        }
1104
    }
1105
};
1106

1107
export const getWMTSMatrixIds = (layer, tileMatrixSet) => {
1✔
1108
    let modifiedTileMatrixSet = [];
5✔
1109
    const srs = normalizeSRS(layer.srs || 'EPSG:3857', layer.allowedSRS);
5✔
1110
    const projection = getProjection(srs);
5✔
1111
    const identifierText = "ows:Identifier";
5✔
1112
    const metersPerUnit = projection.getMetersPerUnit();
5✔
1113
    const scaleToResolution = s => s * 0.28E-3 / metersPerUnit;
145✔
1114

1115
    tileMatrixSet && tileMatrixSet.TileMatrix.map(tileMatrix => {
5✔
1116
        const identifier = tileMatrix[identifierText];
145✔
1117
        const resolution = scaleToResolution(tileMatrix.ScaleDenominator);
145✔
1118
        const tileSize = [toNumber(tileMatrix.TileWidth), toNumber(tileMatrix.TileHeight)];
145✔
1119
        const topLeftCorner = tileMatrix.TopLeftCorner && tileMatrix.TopLeftCorner.split(" ").map(v => toNumber(v));
290✔
1120
        const matrixSize = [toNumber(tileMatrix.MatrixWidth), toNumber(tileMatrix.MatrixHeight)];
145✔
1121

1122
        return modifiedTileMatrixSet.push({ identifier, matrixSize, resolution, tileSize, topLeftCorner});
145✔
1123
    });
1124
    return modifiedTileMatrixSet;
5✔
1125
};
1126
export const rgbaTorgb = (rgba = "") => {
1!
1127
    return rgba.indexOf("rgba") !== -1 ? `rgb${rgba.slice(rgba.indexOf("("), rgba.lastIndexOf(","))})` : rgba;
1!
1128
};
1129

1130
function getLabelAlign(horizontal, vertical) {
UNCOV
1131
    const hAlign = horizontal === "start" ? "l" : (horizontal === "end" ? "r" : "c");
×
UNCOV
1132
    const vAlign = vertical === "top" ? "t" : (vertical === "bottom" ? "b" : "m");
×
UNCOV
1133
    return [hAlign, vAlign].join("");
×
1134
}
1135

1136
/**
1137
 *
1138
 * @param {*} layer
1139
 * @param {*} style
1140
 * @param {*} styleType
1141
 * @memberof utils.PrintUtils
1142
 */
1143
export const toOpenLayers2TextStyle = function(layer, style, styleType) {
1✔
1144
    if (!style) {
12!
1145
        return PrintUtils.getOlDefaultStyle(layer, styleType);
12✔
1146
    }
UNCOV
1147
    switch (styleType) {
×
1148
    case 'GraticuleXLabels': {
UNCOV
1149
        return {
×
1150
            "fontColor": style.color || "#000000",
×
1151
            "fontFamily": style.font || "12px Calibri,sans-serif",
×
1152
            "fontWeight": style.fontWeight || "bold",
×
1153
            "fontSize": style.fontSize || "14",
×
1154
            "label": "{properties.valueText}",
1155
            "labelAlign": getLabelAlign(style.textAlign || "center", style.verticalAlign || "bottom"),
×
1156
            "labelOutlineColor": style.labelOutlineColor || "#FFFFFF",
×
1157
            "labelOutlineWidth": style.labelOutlineWidth / 4.0 || 0.5,
×
1158
            "rotation": style.rotation ? -style.rotation : 0
×
1159
        };
1160
    }
1161
    case 'GraticuleYLabels': {
UNCOV
1162
        return {
×
1163
            "fontColor": style.color || "#000000",
×
1164
            "fontFamily": style.font || "12px Calibri,sans-serif",
×
1165
            "fontWeight": style.fontWeight || "bold",
×
1166
            "fontSize": style.fontSize || "14",
×
1167
            "label": "{properties.valueText}",
1168
            "labelAlign": getLabelAlign(style.textAlign || "end", style.verticalAlign || "middle"),
×
1169
            "labelOutlineColor": style.labelOutlineColor || "#FFFFFF",
×
1170
            "labelOutlineWidth": style.labelOutlineWidth / 4.0 || 0.5,
×
1171
            "rotation": style.rotation ? -style.rotation : 0
×
1172
        };
1173
    }
1174
    default: {
UNCOV
1175
        return {
×
1176
            "fontColor": "#000000",
1177
            "fontFamily": "12px Calibri,sans-serif",
1178
            "fontWeight": "bold",
1179
            "fontSize": "14",
1180
            "label": "{properties.valueText}",
1181
            "labelAlign": "cb",
1182
            "labelOutlineColor": "#FFFFFF",
1183
            "labelOutlineWidth": 0.5
1184
        };
1185
    }
1186
    }
1187
};
1188

1189
/**
1190
 * Useful for print (Or generic Openlayers 2 conversion style)
1191
 * http://dev.openlayers.org/docs/files/OpenLayers/Feature/Vector-js.html#OpenLayers.Feature.Vector.OpenLayers.Feature.Vector.style
1192
 * @memberof utils.PrintUtils
1193
 */
1194
export const toOpenLayers2Style = function(layer, style, styleType) {
1✔
1195
    if (!style || layer.styleName === "marker") {
54✔
1196
        return PrintUtils.getOlDefaultStyle(layer, styleType);
21✔
1197
    }
1198
    // TODO: add support for grid labels (x and y)
1199
    // commented the available options.
1200
    return {
33✔
1201
        "fillColor": colorToHexStr(style.fillColor),
1202
        "fillOpacity": style.fillOpacity,
1203
        // "rotation": "30",
1204
        "externalGraphic": style.iconUrl,
1205
        // "graphicName": "circle",
1206
        // "graphicOpacity": 0.4,
1207
        "pointRadius": style.radius,
1208
        "strokeColor": colorToHexStr(style.color),
1209
        "strokeOpacity": style.opacity,
1210
        "strokeWidth": style.weight,
1211
        "strokeDashstyle": style.lineDash ? reverse(style.lineDash).join(" ") : undefined
33!
1212
        // "strokeLinecap": "round",
1213
        // "strokeDashstyle": "dot",
1214
        // "fontColor": "#000000",
1215
        // "fontFamily": "sans-serif",
1216
        // "fontSize": "12px",
1217
        // "fontStyle": "normal",
1218
        // "fontWeight": "bold",
1219
        // "haloColor": "#123456",
1220
        // "haloOpacity": "0.7",
1221
        // "haloRadius": "3.0",
1222
        // "label": "${name}",
1223
        // "labelAlign": "cm",
1224
        // "labelRotation": "45",
1225
        // "labelXOffset": "-25.0",
1226
        // "labelYOffset": "-35.0"
1227
    };
1228
};
1229
/**
1230
 * Provides the default style for
1231
 * each vector type.
1232
 * @memberof utils.PrintUtils
1233
 */
1234
export const getOlDefaultStyle = (layer, styleType) => {
1✔
1235
    switch (styleType || getGeomType(layer) || "") {
37!
1236
    case 'Polygon':
1237
    case 'MultiPolygon': {
1238
        return {
5✔
1239
            "fillColor": "#0000FF",
1240
            "fillOpacity": 0.1,
1241
            "strokeColor": "#0000FF",
1242
            "strokeOpacity": 1,
1243
            "strokeWidth": 3,
1244
            "strokeDashstyle": "dash",
1245
            "strokeLinecap": "round"
1246
        };
1247
    }
1248
    case 'MultiLineString':
1249
    case 'LineString':
1250
        return {
5✔
1251
            "strokeColor": "#0000FF",
1252
            "strokeOpacity": 1,
1253
            "strokeWidth": 3
1254
        };
1255
    case 'Point':
1256
    case 'MultiPoint': {
1257
        return layer.styleName === "marker" ? {
5✔
1258
            "externalGraphic": "http://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/images/marker-icon.png",
1259
            "graphicWidth": 25,
1260
            "graphicHeight": 41,
1261
            "graphicXOffset": -12, // different offset
1262
            "graphicYOffset": -41
1263
        } : {
1264
            "fillColor": "#FF0000",
1265
            "fillOpacity": 0,
1266
            "strokeColor": "#FF0000",
1267
            "pointRadius": 5,
1268
            "strokeOpacity": 1,
1269
            "strokeWidth": 1
1270
        };
1271
    }
1272
    case 'GraticuleLines': {
UNCOV
1273
        return {
×
1274
            "strokeColor": '#ff7800',
1275
            "strokeOpacity": 0.9,
1276
            "strokeWidth": 2,
1277
            "strokeDashstyle": "4 0.5"
1278
        };
1279
    }
1280
    case 'GraticuleFrame': {
1281
        return {
3✔
1282
            "strokeColor": '#000000',
1283
            "strokeOpacity": 1.0,
1284
            "strokeWidth": 1,
1285
            "fillColor": "#FFFFFF",
1286
            "fillOpacity": 1.0
1287
        };
1288
    }
1289
    case 'GraticuleXLabels': {
1290
        return {
6✔
1291
            "fontColor": "#000000",
1292
            "fontFamily": "12px Calibri,sans-serif",
1293
            "fontWeight": "bold",
1294
            "fontSize": "14",
1295
            "label": "{properties.valueText}",
1296
            "labelAlign": "cb",
1297
            "labelOutlineColor": "#FFFFFF",
1298
            "labelOutlineWidth": 0.5
1299
        };
1300
    }
1301
    case 'GraticuleYLabels': {
1302
        return {
6✔
1303
            "fontColor": "#000000",
1304
            "fontFamily": "12px Calibri,sans-serif",
1305
            "fontWeight": "bold",
1306
            "fontSize": "14",
1307
            "label": "{properties.valueText}",
1308
            "labelAlign": "rm",
1309
            "labelOutlineColor": "#FFFFFF",
1310
            "labelOutlineWidth": 0.5
1311
        };
1312
    }
1313
    default: {
1314
        return {
7✔
1315
            "fillColor": "#0000FF",
1316
            "fillOpacity": 0.1,
1317
            "strokeColor": "#0000FF",
1318
            "pointRadius": 5,
1319
            "strokeOpacity": 1,
1320
            "strokeWidth": 1
1321
        };
1322
    }
1323
    }
1324
};
1325

1326

1327
PrintUtils = {
1✔
1328
    toAbsoluteURL,
1329
    getLayoutName,
1330
    getMapfishLayersSpecification,
1331
    specCreators,
1332
    normalizeUrl,
1333
    toOpenLayers2Style,
1334
    toOpenLayers2TextStyle,
1335
    getWMTSMatrixIds,
1336
    getOlDefaultStyle
1337
};
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