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

geosolutions-it / MapStore2 / 15263113290

26 May 2025 10:56PM UTC coverage: 76.844% (-0.09%) from 76.934%
15263113290

Pull #11130

github

web-flow
Merge c660b1ef6 into 94d9822b2
Pull Request #11130: #10839: Allow printing by freely setting the scale factor

31045 of 48413 branches covered (64.13%)

48 of 114 new or added lines in 9 files covered. (42.11%)

2 existing lines in 2 files now uncovered.

38641 of 50285 relevant lines covered (76.84%)

36.4 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 assign from 'object-assign';
15
import PropTypes from 'prop-types';
16
import React from 'react';
17
import { PanelGroup, Col, Glyphicon, Grid, Panel, Row } from 'react-bootstrap';
18
import { connect } from '../utils/PluginsUtils';
19
import { createSelector } from 'reselect';
20

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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