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

geosolutions-it / MapStore2 / 16061365012

03 Jul 2025 10:03PM UTC coverage: 76.877% (-0.1%) from 76.972%
16061365012

Pull #11087

github

web-flow
Merge f7c173b17 into 01d598372
Pull Request #11087: #8338: Implement a terrain layer selector

31243 of 48671 branches covered (64.19%)

124 of 152 new or added lines in 10 files covered. (81.58%)

240 existing lines in 17 files now uncovered.

38832 of 50512 relevant lines covered (76.88%)

36.55 hits per line

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

89.24
/web/client/plugins/Print.jsx
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 './print/print.css';
10

11
import head from 'lodash/head';
12
import castArray from "lodash/castArray";
13
import isNil from "lodash/isNil";
14
import PropTypes from 'prop-types';
15
import React from 'react';
16
import { PanelGroup, Col, Glyphicon, Grid, Panel, Row } from 'react-bootstrap';
17
import { connect } from '../utils/PluginsUtils';
18
import { createSelector } from 'reselect';
19

20
import { setControlProperty, toggleControl } from '../actions/controls';
21
import { configurePrintMap, printError, printSubmit, printSubmitting, addPrintParameter } from '../actions/print';
22
import Message from '../components/I18N/Message';
23
import Dialog from '../components/misc/Dialog';
24
import printReducers from '../reducers/print';
25
import printEpics from '../epics/print';
26
import { printSpecificationSelector } from "../selectors/print";
27
import { layersSelector, rawGroupsSelector } from '../selectors/layers';
28
import { currentLocaleSelector } from '../selectors/locale';
29
import { mapSelector, scalesSelector } from '../selectors/map';
30
import { mapTypeSelector } from '../selectors/maptype';
31
import { normalizeSRS, convertDegreesToRadian } from '../utils/CoordinatesUtils';
32
import { getMessageById } from '../utils/LocaleUtils';
33
import { defaultGetZoomForExtent, getResolutions, mapUpdated, dpi2dpu, DEFAULT_SCREEN_DPI, getScales, reprojectZoom } from '../utils/MapUtils';
34
import { getDerivedLayersVisibility, isInsideResolutionsLimits } from '../utils/LayersUtils';
35
import { has, includes } from 'lodash';
36
import {additionalLayersSelector} from "../selectors/additionallayers";
37
import { MapLibraries } from '../utils/MapTypeUtils';
38
import FlexBox from '../components/layout/FlexBox';
39
import Text from '../components/layout/Text';
40
import Button from '../components/layout/Button';
41
import { getResolutionMultiplier } from '../utils/PrintUtils';
42

43
/**
44
 * Print plugin. This plugin allows to print current map view. **note**: this plugin requires the  **printing module** to work.
45
 * Please look at mapstore documentation about how to add and configure the printing module in your installation.
46
 *
47
 * It also works as a container for other plugins, usable to customize the UI of the parameters dialog.
48
 *
49
 * The UI supports different targets for adding new plugins:
50
 *  - `left-panel` (controls/widgets to be added to the left column, before the accordion)
51
 *  - `left-panel-accordion` (controls/widgets to be added to the left column, as subpanels of the accordion)
52
 *  - `right-panel` (controls/widgets to be added to the right column, before the buttons bar)
53
 *  - `buttons` (controls/widgets to be added to the right column, in the buttons bar)
54
 *  - `preview-panel` (controls/widgets to be added to the printed pdf preview panel)
55
 *
56
 * In addition it is also possibile to use specific targets that override a standard widget, to replace it
57
 * with a custom one. They are (in order, from left to right and top to bottom in the UI):
58
 *  - `name` (`left-panel`, `position`: `1`)
59
 *  - `description` (`left-panel`, `position`: `2`)
60
 *  - `outputFormat` (`left-panel`, `position`: `3`)
61
 *  - `projection` (`left-panel`, `position`: `4`)
62
 *  - `layout` (`left-panel-accordion`, `position`: `1`)
63
 *  - `legend-options` (`left-panel-accordion`, `position`: `2`)
64
 *  - `resolution` (`right-panel`, `position`: `1`)
65
 *  - `map-preview` (`right-panel`, `position`: `2`)
66
 *  - `default-background-ignore` (`right-panel`, `position`: `3`)
67
 *  - `submit` (`buttons`, `position`: `1`)
68
 *  - `print-preview` (`preview-panel`, `position`: `1`)
69
 *
70
 * To remove a widget, you have to include a Null plugin with the desired target.
71
 * You can use the position to sort existing and custom items.
72
 *
73
 * Standard widgets can be configured by providing an options object as a configuration property
74
 * of this (Print) plugin. The options object of a widget is named `<widget_id>Options`
75
 * (e.g. `outputFormatOptions`).
76
 *
77
 * You can customize Print plugin by creating one custom plugin (or more) that modifies the existing
78
 * components with your own ones. You can configure this plugin in `localConfig.json` as usual.
79
 *
80
 * It delegates to a printingService the creation of the final print. The default printingService
81
 * implements a mapfish-print v2 compatible workflow. It is possible to override the printingService to
82
 * use, via a specific property (printingService).
83
 *
84
 * It is also possible to customize the payload of the spec sent to the mapfish-print engine, by
85
 * adding new transformers to the default chain.
86
 *
87
 * Each transformer is a function that can add / replace / remove fragments from the JSON payload.
88
 *
89
 * @class Print
90
 * @memberof plugins
91
 * @static
92
 *
93
 * @prop {boolean} cfg.useFixedScales if true, the printing scale is constrained to the nearest scale of the ones configured
94
 * in the `config.yml` file, if false the current scale is used
95
 * Note: If `disableScaleLocking` in `config.yml` is false, `useFixedScales` must be true to avoid errors due to disallowed scales.
96
 * @prop {boolean} cfg.editScale if true, the scale input field in the print preview panel will be editable allowing users to
97
 * freely enter a desired scale value. This allows the print service to accept custom scale values rather than only those from
98
 * its capabilities in case it is configured in the `config.yml` file.
99
 * If false, the default behavior is to take a scale within the capabilities scales.
100
 * <br>
101
 * if useFixedScales = true and editScale = true --> the editScale setting will override the fixed scales
102
 * **Important Note:** This functionality relies on `disableScaleLocking` being `true` in the `config.yml` file.
103
 * If `disableScaleLocking` in `config.yml` is `false` (meaning the backend requires fixed scales),
104
 * then `editScale` **must be `false`** to prevent scale-not-allowed errors.
105
 * <br>
106
 * @prop {object} cfg.overrideOptions overrides print options, this will override options created from current state of map
107
 * @prop {boolean} cfg.overrideOptions.geodetic prints in geodetic mode: in geodetic mode scale calculation is more precise on
108
 * printed maps, but the preview is not accurate
109
 * @prop {string} cfg.overrideOptions.outputFilename name of output file
110
 * @prop {object} cfg.mapPreviewOptions options for the map preview tool
111
 * @prop {string[]} cfg.ignoreLayers list of layer types to ignore in preview and when printing, default ["google", "bing"]
112
 * @prop {boolean} cfg.mapPreviewOptions.enableScalebox if true a combobox to select the printing scale is shown over the preview
113
 * this is particularly useful if useFixedScales is also true, to show the real printing scales
114
 * @prop {boolean} cfg.mapPreviewOptions.enableRefresh true by default, if false the preview is not updated if the user pans or zooms the main map
115
 * @prop {object} cfg.outputFormatOptions options for the output formats
116
 * @prop {object[]} cfg.outputFormatOptions.allowedFormats array of allowed formats, e.g. [{"name": "PDF", "value": "pdf"}]
117
 * @prop {object} cfg.projectionOptions options for the projections
118
 * @prop {string[]} cfg.excludeLayersFromLegend list of layer names e.g. ["workspace:layerName"] to exclude from printed document
119
 * @prop {object} cfg.mergeableParams object to pass to mapfish-print v2 to merge params, example here https://github.com/mapfish/mapfish-print-v2/blob/main/docs/protocol.rst#printpdf
120
 * @prop {object[]} cfg.projectionOptions.projections array of available projections, e.g. [{"name": "EPSG:3857", "value": "EPSG:3857"}]
121
 * @prop {object} cfg.overlayLayersOptions options for overlay layers
122
 * @prop {boolean} cfg.overlayLayersOptions.enabled if true a checkbox will be shown to exclude or include overlay layers to the print
123
 *
124
 * @example
125
 * // printing in geodetic mode
126
 * {
127
 *   "name": "Print",
128
 *   "cfg": {
129
 *       "overrideOptions": {
130
 *          "geodetic": true
131
 *       }
132
 *    }
133
 * }
134
 *
135
 * @example
136
 * // Using a list of fixed scales with scale selector
137
 * {
138
 *   "name": "Print",
139
 *   "cfg": {
140
 *       "ignoreLayers": ["google", "bing"],
141
 *       "useFixedScales": true,
142
 *       "mapPreviewOptions": {
143
 *          "enableScalebox": true
144
 *       }
145
 *    }
146
 * }
147
 *
148
 * @example
149
 * // Default behavior (scale editing enabled, no fixed scales)
150
 * // Assumes disableScaleLocking = true in config.yml
151
 * {
152
 *   "name": "Print",
153
 *   "cfg": {
154
 *     "useFixedScales": false,
155
 *     "editScale": true
156
 *   }
157
 * }
158
 *
159
 * @example
160
 * // Configuration when disableScaleLocking = false in config.yml
161
 * {
162
 *   "name": "Print",
163
 *   "cfg": {
164
 *     "useFixedScales": true,
165
 *     "editScale": false
166
 *   }
167
 * }
168
 *
169
 * @example
170
 * // Priority override: editScale overrides useFixedScales when both are true
171
 * // (only valid when disableScaleLocking = true in config.yml)
172
 * {
173
 *   "name": "Print",
174
 *   "cfg": {
175
 *     "useFixedScales": true,
176
 *     "editScale": true
177
 *   }
178
 * }
179
 *
180
 *
181
 * @example
182
 * // restrict allowed output formats
183
 * {
184
 *   "name": "Print",
185
 *   "cfg": {
186
 *       "outputFormatOptions": {
187
 *          "allowedFormats": [{"name": "PDF", "value": "pdf"}, {"name": "PNG", "value": "png"}]
188
 *       }
189
 *    }
190
 * }
191
 *
192
 * @example
193
 * // enable custom projections for printing
194
 * "projectionDefs": [{
195
 *    "code": "EPSG:23032",
196
 *    "def": "+proj=utm +zone=32 +ellps=intl +towgs84=-87,-98,-121,0,0,0,0 +units=m +no_defs",
197
 *    "extent": [-1206118.71, 4021309.92, 1295389.0, 8051813.28],
198
 *    "worldExtent": [-9.56, 34.88, 31.59, 71.21]
199
 * }]
200
 * ...
201
 * {
202
 *   "name": "Print",
203
 *   "cfg": {
204
 *       "projectionOptions": {
205
 *          "projections": [{"name": "UTM32N", "value": "EPSG:23032"}, {"name": "EPSG:3857", "value": "EPSG:3857"}, {"name": "EPSG:4326", "value": "EPSG:4326"}]
206
 *       }
207
 *    }
208
 * }
209
 *
210
 * @example
211
 * // customize the printing UI via plugin(s)
212
 * import React from "react";
213
 * import {createPlugin} from "../../utils/PluginsUtils";
214
 * import { connect } from "react-redux";
215
 *
216
 * const MyCustomPanel = () => <div>Hello, I am a custom component</div>;
217
 *
218
 * const MyCustomLayout = ({sheet}) => <div>Hello, I am a custom layout, the sheet is {sheet}</div>;
219
 * const MyConnectedCustomLayout = connect((state) => ({sheet: state.print?.spec.sheet}))(MyCustomLayout);
220
 *
221
 * export default createPlugin('PrintCustomizations', {
222
 *     component: () => null,
223
 *     containers: {
224
 *         Print: [
225
 *             // this entry add a panel between title and description
226
 *             {
227
 *                 target: "left-panel",
228
 *                 position: 1.5,
229
 *                 component: MyCustomPanel
230
 *             },
231
 *             // this entry replaces the layout panel
232
 *             {
233
 *                 target: "layout",
234
 *                 component: MyConnectedCustomLayout,
235
 *                 title: "MyLayout"
236
 *             },
237
 *             // To remove one component, simply create a component that returns null.
238
 *             {
239
 *                 target: "map-preview",
240
 *                 component: () => null
241
 *             }
242
 *         ]
243
 *     }
244
 * });
245
 * @example
246
 * // adds a transformer to the printingService chain
247
 * import {addTransformer} from "@js/utils/PrintUtils";
248
 *
249
 * addTransformer("mytranform", (state, spec) => Promise.resolve({
250
 *      ...spec,
251
 *      custom: "some value"
252
 * }));
253
 */
254

255
function overrideItem(item, overrides = []) {
×
256
    const replacement = overrides.find(i => i.target === item.id);
734✔
257
    return replacement ?? item;
734✔
258
}
259

260
const EmptyComponent = () => {
1✔
UNCOV
261
    return null;
×
262
};
263

264
function handleRemoved(item) {
265
    return item.plugin ? item : {
734!
266
        ...item,
267
        plugin: EmptyComponent
268
    };
269
}
270

271
function mergeItems(standard, overrides) {
272
    return standard
246✔
273
        .map(item => overrideItem(item, overrides))
734✔
274
        .map(handleRemoved);
275
}
276

277
function filterLayer(layer = {}) {
×
278
    // Skip layer with error and type cog
279
    return !layer.loadingError && layer.type !== "cog";
33✔
280
}
281

282
export default {
283
    PrintPlugin: Object.assign({
284
        loadPlugin: (resolve) => {
285
            Promise.all([
18✔
286
                import('./print/index'),
287
                import('../utils/PrintUtils')
288
            ]).then(([printMod, utilsMod]) => {
289

290
                const {
291
                    standardItems
292
                } = printMod.default;
18✔
293

294
                const {
295
                    getDefaultPrintingService,
296
                    getLayoutName,
297
                    getPrintScales,
298
                    getNearestZoom
299
                } = utilsMod;
18✔
300
                class Print extends React.Component {
301
                    static propTypes = {
18✔
302
                        map: PropTypes.object,
303
                        layers: PropTypes.array,
304
                        capabilities: PropTypes.object,
305
                        printSpec: PropTypes.object,
306
                        printSpecTemplate: PropTypes.object,
307
                        withContainer: PropTypes.bool,
308
                        withPanelAsContainer: PropTypes.bool,
309
                        open: PropTypes.bool,
310
                        pdfUrl: PropTypes.string,
311
                        title: PropTypes.string,
312
                        style: PropTypes.object,
313
                        mapWidth: PropTypes.number,
314
                        mapType: PropTypes.string,
315
                        alternatives: PropTypes.array,
316
                        toggleControl: PropTypes.func,
317
                        onBeforePrint: PropTypes.func,
318
                        setPage: PropTypes.func,
319
                        onPrint: PropTypes.func,
320
                        printError: PropTypes.func,
321
                        configurePrintMap: PropTypes.func,
322
                        getLayoutName: PropTypes.func,
323
                        error: PropTypes.string,
324
                        getZoomForExtent: PropTypes.func,
325
                        minZoom: PropTypes.number,
326
                        maxZoom: PropTypes.number,
327
                        usePreview: PropTypes.bool,
328
                        mapPreviewOptions: PropTypes.object,
329
                        syncMapPreview: PropTypes.bool,
330
                        useFixedScales: PropTypes.bool,
331
                        scales: PropTypes.array,
332
                        ignoreLayers: PropTypes.array,
333
                        defaultBackground: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
334
                        closeGlyph: PropTypes.string,
335
                        submitConfig: PropTypes.object,
336
                        previewOptions: PropTypes.object,
337
                        currentLocale: PropTypes.string,
338
                        overrideOptions: PropTypes.object,
339
                        items: PropTypes.array,
340
                        excludeLayersFromLegend: PropTypes.array,
341
                        mergeableParams: PropTypes.object,
342
                        addPrintParameter: PropTypes.func,
343
                        printingService: PropTypes.object,
344
                        printMap: PropTypes.object
345
                    };
346

347
                    static contextTypes = {
18✔
348
                        messages: PropTypes.object,
349
                        plugins: PropTypes.object,
350
                        loadedPlugins: PropTypes.object
351
                    };
352

353
                    static defaultProps = {
18✔
354
                        withContainer: true,
355
                        withPanelAsContainer: false,
356
                        title: 'print.paneltitle',
357
                        toggleControl: () => {},
358
                        onBeforePrint: () => {},
359
                        setPage: () => {},
360
                        onPrint: () => {},
361
                        configurePrintMap: () => {},
362
                        printSpecTemplate: {},
363
                        excludeLayersFromLegend: [],
364
                        getLayoutName: getLayoutName,
365
                        getZoomForExtent: defaultGetZoomForExtent,
366
                        pdfUrl: null,
367
                        mapWidth: 370,
368
                        mapType: MapLibraries.OPENLAYERS,
369
                        minZoom: 1,
370
                        maxZoom: 23,
371
                        usePreview: true,
372
                        mapPreviewOptions: {
373
                            enableScalebox: false,
374
                            enableRefresh: true
375
                        },
376
                        syncMapPreview: false,      // make it false to prevent map sync
377
                        useFixedScales: false,
378
                        scales: [],
379
                        ignoreLayers: ["google", "bing"],
380
                        defaultBackground: ["osm", "wms", "empty"],
381
                        closeGlyph: "1-close",
382
                        submitConfig: {
383
                            buttonConfig: {
384
                                bsSize: "small",
385
                                bsStyle: "primary"
386
                            },
387
                            glyph: ""
388
                        },
389
                        previewOptions: {
390
                            buttonStyle: "primary"
391
                        },
392
                        style: {},
393
                        currentLocale: 'en-US',
394
                        overrideOptions: {},
395
                        items: [],
396
                        printingService: getDefaultPrintingService(),
397
                        printMap: {},
398
                        editScale: false
399
                    };
400
                    constructor(props) {
401
                        super(props);
18✔
402
                        // Calling configurePrintMap here to replace calling in in UNSAFE_componentWillMount
403
                        this.configurePrintMap();
18✔
404
                        this.state = {
18✔
405
                            activeAccordionPanel: 0
406
                        };
407
                    }
408

409
                    UNSAFE_componentWillReceiveProps(nextProps) {
410
                        const hasBeenOpened = nextProps.open && !this.props.open;
45✔
411
                        const mapHasChanged = this.props.open && this.props.syncMapPreview && mapUpdated(this.props.map, nextProps.map);
45!
412
                        const specHasChanged = (
413
                            nextProps.printSpec.defaultBackground !== this.props.printSpec.defaultBackground ||
45✔
414
                                nextProps.printSpec.additionalLayers !== this.props.printSpec.additionalLayers
415
                        );
416
                        if (hasBeenOpened || mapHasChanged || specHasChanged) {
45✔
417
                            this.configurePrintMap(nextProps);
1✔
418
                        }
419
                    }
420

421
                    getAlternativeBackground = (layers, defaultBackground, projection) => {
18✔
422
                        const allowedBackground = head(castArray(defaultBackground).map(type => ({
10✔
423
                            type
424
                        })).filter(l => this.isAllowed(l, projection)));
10✔
425
                        if (allowedBackground) {
5!
426
                            return head(layers.filter(l => l.type === allowedBackground.type));
5✔
427
                        }
UNCOV
428
                        return null;
×
429
                    };
430

431
                    getItems = (target) => {
18✔
432
                        const filtered = this.props.items.filter(i => !target || i.target === target);
246✔
433
                        const merged = mergeItems(standardItems[target], this.props.items)
246✔
434
                            .map(item => ({
734✔
435
                                ...item,
436
                                target
437
                            }));
438
                        return [...merged, ...filtered]
246✔
439
                            .sort((i1, i2) => (i1.position ?? 0) - (i2.position ?? 0));
512✔
440
                    };
441
                    getMapConfiguration = () => {
18✔
442
                        const map = this.props.printingService.getMapConfiguration();
69✔
443
                        return {
69✔
444
                            ...map,
445
                            layers: this.filterLayers(map.layers, this.props.useFixedScales && !this.props.editScale ? map.scaleZoom : map.zoom, map.projection)
153✔
446
                        };
447
                    };
448
                    getMapSize = (layout) => {
18✔
449
                        const currentLayout = layout || this.getLayout();
61!
450
                        return {
61✔
451
                            width: this.props.mapWidth,
452
                            height: currentLayout && currentLayout.map.height / currentLayout.map.width * this.props.mapWidth || 270
122!
453
                        };
454
                    };
455
                    getPreviewResolution = (zoom, projection) => {
18✔
456
                        const dpu = dpi2dpu(DEFAULT_SCREEN_DPI, projection);
69✔
457
                        const roundZoom = Math.round(zoom);
69✔
458
                        const scale = this.props.useFixedScales && !this.props.editScale
69✔
459
                            ? getPrintScales(this.props.capabilities)[roundZoom]
460
                            : this.props.scales[roundZoom];
461
                        return scale / dpu;
69✔
462
                    };
463
                    getLayout = (props) => {
18✔
464
                        const { getLayoutName: getLayoutNameProp, printSpec, capabilities } = props || this.props;
427✔
465
                        const layoutName = getLayoutNameProp(printSpec);
427✔
466
                        return head(capabilities.layouts.filter((l) => l.name === layoutName));
427✔
467
                    };
468

469
                    renderWarning = (layout) => {
18✔
470
                        if (!layout) {
61!
UNCOV
471
                            return <Row><Col xs={12}><div className="print-warning"><span><Message msgId="print.layoutWarning"/></span></div></Col></Row>;
×
472
                        }
473
                        return null;
61✔
474
                    };
475
                    renderItem = (item, opts) => {
18✔
476
                        const {validations, ...options } = opts;
740✔
477
                        const Comp = item.component ?? item.plugin;
740✔
478
                        const {style, ...other} = this.props;
740✔
479
                        const itemOptions = this.props[item.id + "Options"];
740✔
480
                        return <Comp role="body" {...other} {...item.cfg} {...options} {...itemOptions} validation={validations?.[item.id ?? item.name]}/>;
740✔
481
                    };
482
                    renderItems = (target, options) => {
18✔
483
                        return this.getItems(target)
185✔
484
                            .map(item => this.renderItem(item, options));
618✔
485
                    };
486
                    renderAccordion = (target, options) => {
18✔
487
                        const items = this.getItems(target);
61✔
488
                        return (<PanelGroup accordion activeKey={this.state.activeAccordionPanel} onSelect={(key) => {
61✔
UNCOV
489
                            this.setState({
×
490
                                activeAccordionPanel: key
491
                            });
492
                        }}>
493
                            {items.map((item, pos) => (
494
                                <Panel header={getMessageById(this.context.messages, item.cfg?.title ?? item.title ?? "")} eventKey={pos} collapsible>
122!
495
                                    {this.renderItem(item, options)}
496
                                </Panel>
497
                            ))}
498
                        </PanelGroup>);
499
                    };
500
                    renderPrintPanel = () => {
18✔
501
                        const layout = this.getLayout();
61✔
502
                        const map = this.getMapConfiguration();
61✔
503
                        const options = {
61✔
504
                            layout,
505
                            map,
506
                            layoutName: this.props.getLayoutName(this.props.printSpec),
507
                            mapSize: this.getMapSize(layout),
508
                            resolutions: getResolutions(map?.projection),
UNCOV
509
                            onRefresh: () => this.configurePrintMap(),
×
510
                            notAllowedLayers: this.isBackgroundIgnored(this.props.layers, map?.projection),
511
                            actionConfig: this.props.submitConfig,
512
                            validations: this.props.printingService.validate(),
513
                            rotation: !isNil(this.props.printSpec.rotation) ? convertDegreesToRadian(Number(this.props.printSpec.rotation)) : 0,
61!
514
                            actions: {
515
                                print: this.print,
516
                                addParameter: this.addParameter
517
                            }
518
                        };
519
                        return (
61✔
520
                            <Grid fluid role="body">
521
                                {this.renderError()}
522
                                {this.renderWarning(layout)}
523
                                <Row>
524
                                    <Col xs={12} md={6}>
525
                                        {this.renderItems("left-panel", options)}
526
                                        {this.renderAccordion("left-panel-accordion", options)}
527
                                    </Col>
528
                                    <Col xs={12} md={6} style={{textAlign: "center"}}>
529
                                        {this.renderItems("right-panel", options)}
530
                                        {this.renderItems("buttons", options)}
531
                                        {this.renderDownload()}
532
                                    </Col>
533
                                </Row>
534
                            </Grid>
535
                        );
536
                    };
537

538
                    renderDownload = () => {
18✔
539
                        if (this.props.pdfUrl && !this.props.usePreview) {
61!
UNCOV
540
                            return <iframe src={this.props.pdfUrl} style={{visibility: "hidden", display: "none"}}/>;
×
541
                        }
542
                        return null;
61✔
543
                    };
544

545
                    renderError = () => {
18✔
546
                        if (this.props.error) {
61✔
547
                            return <Row><Col xs={12}><div className="print-error"><span>{this.props.error}</span></div></Col></Row>;
2✔
548
                        }
549
                        return null;
59✔
550
                    };
551

552
                    renderPreviewPanel = () => {
18✔
553
                        return this.renderItems("preview-panel", this.props.previewOptions);
2✔
554
                    };
555

556
                    renderBody = () => {
18✔
557
                        if (this.props.pdfUrl && this.props.usePreview) {
63✔
558
                            return this.renderPreviewPanel();
2✔
559
                        }
560
                        return this.renderPrintPanel();
61✔
561
                    };
562

563
                    render() {
564
                        if ((this.props.capabilities || this.props.error) && this.props.open) {
63!
565
                            if (this.props.withContainer) {
63!
566
                                if (this.props.withPanelAsContainer) {
63!
UNCOV
567
                                    return (<Panel className="mapstore-print-panel" header={<span><span className="print-panel-title"><Message msgId="print.paneltitle"/></span><span className="print-panel-close panel-close" onClick={this.props.toggleControl}></span></span>} style={this.props.style}>
×
568
                                        {this.renderBody()}
569
                                    </Panel>);
570
                                }
571
                                return (<Dialog start={{x: 0, y: 80}} id="mapstore-print-panel" style={{ zIndex: 1990, ...this.props.style}}>
63✔
572
                                    <FlexBox role="header" centerChildrenVertically gap="sm">
573
                                        <FlexBox.Fill component={Text} ellipsis fontSize="md" className="print-panel-title _padding-lr-sm">
574
                                            <Message msgId="print.paneltitle"/>
575
                                        </FlexBox.Fill>
576
                                        <Button onClick={this.props.toggleControl} square borderTransparent className="print-panel-close">
577
                                            {this.props.closeGlyph ? <Glyphicon glyph={this.props.closeGlyph}/> : <span>×</span>}
63!
578
                                        </Button>
579
                                    </FlexBox>
580
                                    {this.renderBody()}
581
                                </Dialog>);
582
                            }
UNCOV
583
                            return this.renderBody();
×
584
                        }
UNCOV
585
                        return null;
×
586
                    }
587
                    addParameter = (name, value) => {
18✔
588
                        this.props.addPrintParameter("params." + name, value);
3✔
589
                    };
590
                    isCompatibleWithSRS = (projection, layer) => {
18✔
591
                        return projection === "EPSG:3857" || includes([
42!
592
                            "wms",
593
                            "wfs",
594
                            "vector",
595
                            "graticule",
596
                            "empty",
597
                            "arcgis"
598
                        ], layer.type) || layer.type === "wmts" && has(layer.allowedSRS, projection);
599
                    };
600
                    isAllowed = (layer, projection) => {
18✔
601
                        return this.props.ignoreLayers.indexOf(layer.type) === -1 &&
45✔
602
                            this.isCompatibleWithSRS(normalizeSRS(projection), layer);
603
                    };
604

605
                    isBackgroundIgnored = (layers, projection) => {
18✔
606
                        const background = head((layers || this.props.layers)
130!
607
                            .filter(layer => layer.group === "background" && layer.visibility && this.isAllowed(layer, projection)));
73!
608
                        return !background;
130✔
609
                    };
610
                    filterLayers = (layers, zoom, projection) => {
18✔
611
                        const resolution = this.getPreviewResolution(zoom, projection);
69✔
612

613
                        const filtered = layers.filter((layer) =>
69✔
614
                            layer.visibility &&
40✔
615
                            isInsideResolutionsLimits(layer, resolution) &&
616
                            this.isAllowed(layer, projection)
617
                        );
618
                        if (this.isBackgroundIgnored(layers, projection) && this.props.defaultBackground && this.props.printSpec.defaultBackground) {
69✔
619
                            const defaultBackground = this.getAlternativeBackground(layers, this.props.defaultBackground);
5✔
620
                            if (defaultBackground) {
5!
UNCOV
621
                                return [{
×
622
                                    ...defaultBackground,
623
                                    visibility: true
624
                                }, ...filtered];
625
                            }
626
                            return filtered;
5✔
627
                        }
628
                        return filtered;
64✔
629
                    };
630
                    getRatio = () => {
18✔
631
                        let mapPrintLayout = this.getLayout();
366✔
632
                        if (this.props?.mapWidth && mapPrintLayout) {
366!
633
                            return getResolutionMultiplier(mapPrintLayout?.map?.width || 0, this?.props?.mapWidth || 0, this.props.printRatio);
366!
634
                        }
UNCOV
635
                        return 1;
×
636
                    };
637
                    configurePrintMap = (props) => {
18✔
638
                        const {
639
                            map: newMap,
640
                            capabilities,
641
                            configurePrintMap: configurePrintMapProp,
642
                            useFixedScales,
643
                            currentLocale,
644
                            layers,
645
                            printMap,
646
                            printSpec,
647
                            editScale
648
                        } = props || this.props;
19✔
649
                        if (newMap && newMap.bbox && capabilities) {
19!
650
                            const selectedPrintProjection = (printSpec && printSpec?.params?.projection) || (printSpec && printSpec?.projection) || (printMap && printMap.projection) || 'EPSG:3857';
19!
651
                            const printSrs = normalizeSRS(selectedPrintProjection);
19✔
652
                            const mapProjection = newMap.projection;
19✔
653
                            const mapSrs = normalizeSRS(mapProjection);
19✔
654
                            const zoom = reprojectZoom(newMap.zoom, mapSrs, printSrs);
19✔
655
                            const scales = getPrintScales(capabilities);
19✔
656
                            const printMapScales = getScales(printSrs);
19✔
657
                            const scaleZoom = getNearestZoom(zoom, scales, printMapScales);
19✔
658
                            if (useFixedScales && !editScale) {
19✔
659
                                const scale = scales[scaleZoom];
4✔
660
                                configurePrintMapProp(newMap.center, zoom, scaleZoom, scale,
4✔
661
                                    layers, newMap.projection, currentLocale, useFixedScales);
662
                            } else {
663
                                const scale = printMapScales[zoom];
15✔
664
                                let resolutions = getResolutions(printSrs).map((resolution) => resolution * this.getRatio());
366✔
665
                                const reqScaleZoom = editScale ? zoom : scaleZoom;
15✔
666
                                configurePrintMapProp(newMap.center, zoom, reqScaleZoom, scale,
15✔
667
                                    layers, newMap.projection, currentLocale, useFixedScales, {
668
                                        editScale,
669
                                        mapResolution: resolutions[zoom]
670
                                    });
671
                            }
672
                        }
673
                    };
674

675
                    print = () => {
18✔
676
                        this.props.setPage(0);
8✔
677
                        this.props.onBeforePrint();
8✔
678
                        this.props.printingService.print({
8✔
679
                            excludeLayersFromLegend: this.props.excludeLayersFromLegend,
680
                            mergeableParams: this.props.mergeableParams,
681
                            layers: this.getMapConfiguration()?.layers,
682
                            scales: this.props.useFixedScales && !this.props.editScale ? getPrintScales(this.props.capabilities) : undefined,
18✔
683
                            bbox: this.props.map?.bbox
684
                        })
685
                            .then((spec) =>
686
                                this.props.onPrint(this.props.capabilities.createURL, { ...spec, ...this.props.overrideOptions })
6✔
687
                            )
688
                            .catch(e => {
UNCOV
689
                                this.props.printError("Error in printing:" + e.message);
×
690
                            });
691
                    };
692
                }
693

694
                const selector = createSelector([
18✔
695
                    (state) => state.controls.print && state.controls.print.enabled || state.controls.toolbar && state.controls.toolbar.active === 'print',
63!
696
                    (state) => state.print && state.print.capabilities,
63✔
697
                    printSpecificationSelector,
698
                    (state) => state.print && state.print.pdfUrl,
63✔
699
                    (state) => state.print && state.print.error,
63✔
700
                    mapSelector,
701
                    layersSelector,
702
                    additionalLayersSelector,
703
                    scalesSelector,
704
                    (state) => state.browser && (!state.browser.ie || state.browser.ie11),
63!
705
                    currentLocaleSelector,
706
                    mapTypeSelector,
707
                    (state) => state.print.map,
63✔
708
                    rawGroupsSelector
709
                ], (open, capabilities, printSpec, pdfUrl, error, map, layers, additionalLayers, scales, usePreview, currentLocale, mapType, printMap, groups) => ({
63✔
710
                    open,
711
                    capabilities,
712
                    printSpec,
713
                    pdfUrl,
714
                    error,
715
                    map,
716
                    layers: [
717
                        ...getDerivedLayersVisibility(layers, groups).filter(filterLayer),
UNCOV
718
                        ...(printSpec?.additionalLayers ? additionalLayers.map(l => l.options).filter(
×
719
                            l => {
UNCOV
720
                                const isVector = l.type === 'vector';
×
UNCOV
721
                                const hasFeatures = Array.isArray(l.features) && l.features.length > 0;
×
UNCOV
722
                                return !l.loadingError && (!isVector || (isVector && hasFeatures));
×
723
                            }
724
                        ) : [])
725
                    ],
726
                    scales,
727
                    usePreview,
728
                    currentLocale,
729
                    mapType,
730
                    printMap
731
                }));
732

733
                const PrintPlugin = connect(selector, {
18✔
734
                    toggleControl: toggleControl.bind(null, 'print', null),
735
                    onPrint: printSubmit,
736
                    printError: printError,
737
                    onBeforePrint: printSubmitting,
738
                    setPage: setControlProperty.bind(null, 'print', 'currentPage'),
739
                    configurePrintMap,
740
                    addPrintParameter
741
                })(Print);
742
                resolve(PrintPlugin);
18✔
743
            });
744
        },
UNCOV
745
        enabler: (state) => state.print && state.print.enabled || state.toolbar && state.toolbar.active === 'print'
×
746
    },
747
    {
748
        disablePluginIf: "{state('mapType') === 'cesium' || !state('printEnabled')}",
749
        Toolbar: {
750
            name: 'print',
751
            position: 7,
752
            help: <Message msgId="helptexts.print"/>,
753
            tooltip: "printbutton",
754
            icon: <Glyphicon glyph="print"/>,
755
            exclusive: true,
756
            panel: true,
757
            priority: 1
758
        },
759
        BurgerMenu: {
760
            name: 'print',
761
            position: 2,
762
            tooltip: "printToolTip",
763
            text: <Message msgId="printbutton"/>,
764
            icon: <Glyphicon glyph="print"/>,
765
            action: toggleControl.bind(null, 'print', null),
766
            priority: 3,
767
            doNotHide: true
768
        },
769
        SidebarMenu: {
770
            name: "print",
771
            position: 3,
772
            tooltip: "printbutton",
773
            text: <Message msgId="printbutton"/>,
774
            icon: <Glyphicon glyph="print"/>,
775
            action: toggleControl.bind(null, 'print', null),
776
            doNotHide: true,
777
            toggle: true,
778
            priority: 2
779
        }
780
    }),
781
    reducers: {print: printReducers},
782
    epics: {...printEpics}
783
};
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