-
Notifications
You must be signed in to change notification settings - Fork 446
Expand file tree
/
Copy pathPrintUtils.js
More file actions
1363 lines (1304 loc) · 53.9 KB
/
PrintUtils.js
File metadata and controls
1363 lines (1304 loc) · 53.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2016, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import { reproject, getUnits, reprojectGeoJson, normalizeSRS } from './CoordinatesUtils';
import {addAuthenticationParameter} from './SecurityUtils';
import { calculateExtent, getGoogleMercatorScales, getResolutionsForProjection, getScales } from './MapUtils';
import { optionsToVendorParams } from './VendorParamsUtils';
import { colorToHexStr } from './ColorUtils';
import { getLayerConfig } from './TileConfigProvider';
import { extractValidBaseURL } from './TileProviderUtils';
import { getTileMatrix } from './WMTSUtils';
import { guessFormat } from './TMSUtils';
import { get as getProjection } from 'ol/proj';
import { isArray, filter, find, isEmpty, toNumber, castArray, reverse, includes } from 'lodash';
import { getFeature } from '../api/WFS';
import { generateEnvString } from './LayerLocalizationUtils';
import { ServerTypes } from './LayersUtils';
import PrintStyleParser from './styleparser/PrintStyleParser';
import url from 'url';
import { getStore } from "./StateUtils";
import { isLocalizedLayerStylesEnabledSelector, localizedLayerStylesEnvSelector } from '../selectors/localizedLayerStyles';
import { currentLocaleLanguageSelector } from '../selectors/locale';
import { printSpecificationSelector } from "../selectors/print";
import sortBy from "lodash/sortBy";
import head from "lodash/head";
import isNil from "lodash/isNil";
import get from "lodash/get";
import min from "lodash/min";
import trimEnd from 'lodash/trimEnd';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { toPng } from 'html-to-image';
import VectorLegend from '../plugins/TOC/components/VectorLegend';
import { getGridGeoJson } from "./grids/MapGridsUtils";
import { isImageServerUrl } from './ArcGISUtils';
import { getWMSLegendConfig, LEGEND_FORMAT } from './LegendUtils';
const defaultScales = getGoogleMercatorScales(0, 21);
let PrintUtils;
const printStyleParser = new PrintStyleParser();
// For testing purposes
export const __internals__ = {
toPng
};
/**
* Renders a vector layer's legend to a base64 encoded PNG image.
* It works by temporarily mounting a VectorLegend component to the DOM,
* waiting for all its assets (like external images in rules) to load,
* and then capturing the component's HTML as a PNG data URL.
* @param {object} layer The MapStore layer object, with a geostyler style.
* @returns {Promise<string|null>} A promise that resolves with the base64 data URL of the legend, or null if rendering fails.
*/
export function renderVectorLegendToBase64(layer) {
if (!layer?.style || layer.style.format !== 'geostyler' || !layer.style.body?.rules || !layer.style.body.rules.length) {
return Promise.resolve(null);
}
const container = typeof document !== 'undefined' && document.createElement('div');
if (!container) {
return Promise.resolve(null);
}
document.body.appendChild(container);
container.style.position = 'fixed';
container.style.top = '0px';
container.style.left = '0px';
container.style.width = '200px';
container.style.zIndex = -1;
container.style.opacity = 0;
container.style.fontSize = '10px';
const styles = `
.ms-legend-rule {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.ms-legend-icon {
margin-right: 5px;
flex-shrink: 0;
}
`;
return new Promise(renderResolve => {
render(
<div>
<style>{styles}</style>
<VectorLegend
style={layer.style}
layer={layer}
interactive={false}
onChange={() => {}}
/>
</div>,
container,
renderResolve
);
}).then(() => {
const images = Array.from(container.querySelectorAll('img'));
const promises = images.map(img => new Promise(imgResolve => {
if (img.complete) {
imgResolve();
return;
}
img.onload = imgResolve;
img.onerror = imgResolve;
}));
return Promise.all(promises);
}).then(() => new Promise(resolve => setTimeout(resolve, 200)))
.then(() => __internals__.toPng(container.querySelector('.ms-legend'), {
quality: 3,
pixelRatio: 1.2,
skipFonts: true
}))
.then(dataUrl => {
unmountComponentAtNode(container);
document.body.removeChild(container);
return dataUrl;
})
.catch(error => {
if (container && document.body.contains(container)) {
unmountComponentAtNode(container);
document.body.removeChild(container);
}
console.warn('Error rendering vector legend to base64:', error);
return null;
});
}
// Try to guess geomType, getting the first type available.
export const getGeomType = function(layer) {
return layer.features && layer.features[0] && layer.features[0].geometry ? layer.features[0].geometry.type :
layer.features && layer.features[0].features && layer.features[0].style && layer.features[0].style.type ? layer.features[0].style.type : undefined;
};
/**
* Utility functions for thumbnails
* @memberof utils
* @static
* @name PrintUtils
*/
/**
* Extracts the correct opacity from layer. if Undefined, the opacity is `1`.
* @ignore
* @param {object} layer the MapStore layer
*/
export const getOpacity = layer => layer.opacity || (layer.opacity === 0 ? 0 : 1.0);
/**
* Preload data (e.g. WFS) before to sent it to the print tool.
* @memberof utils.PrintUtils
*/
export const preloadData = (spec) => {
// check if remote data
const wfsLayers = filter(spec.layers, {type: "wfs"});
if (wfsLayers.length > 0) {
// get data from WFS
return Promise.all(
wfsLayers.map(l =>
getFeature(l.url, l.name, {
outputFormat: "application/json",
srsName: spec.projection,
...(optionsToVendorParams(l) || {})
})
.then(({data}) => ({
id: l.id,
geoJson: data
}))
)
// set geoJson in layer's spec
).then(replies => {
return {
...spec,
layers: (spec.layers || []).map(l => {
const layerData = find(replies, {id: l.id});
if (l.type === "wfs" && layerData) {
return {
...l,
...layerData
};
}
return l;
})
};
});
}
return Promise.resolve(spec);
};
/**
* Given a static resource, returns the resource's absolute
* URL. Supports file paths with or without origin/protocol.
* @param {string} uri the uri to transform
* @param {string} [origin=window.location.origin] the origin to use
* @memberof utils.PrintUtils
*/
export const toAbsoluteURL = (uri, origin) => {
// Handle absolute URLs (with protocol-relative prefix)
// Example: //domain.com/file.png
if (uri.search(/^\/\//) !== -1) {
return window.location.protocol + uri;
}
// Handle absolute URLs (with explicit origin)
// Example: http://domain.com/file.png
if (uri.search(/:\/\//) !== -1) {
return uri;
}
// Handle absolute URLs (without explicit origin)
// Example: /file.png
if (uri.search(/^\//) !== -1) {
return (origin || window.location.origin) + uri;
}
return uri;
};
/**
* Tranform the original URL configuration of the layer into a URL
* usable for the print service.
* @param {string|array} input Original URL
* @returns {string} the URL modified as GeoServer requires
* @memberof utils.PrintUtils
*/
export const normalizeUrl = (input) => {
let result = isArray(input) ? input[0] : input;
if (result.indexOf('?') !== -1) {
result = result.substring(0, result.indexOf('?'));
}
return PrintUtils.toAbsoluteURL(result);
};
/**
* Find the layout name for the given options.
* The convention is: `PAGE_FORMAT + ("_2_pages_legend"|"_2_pages_legend"|"") + ("_landscape"|"")``
* @param {object} spec the spec with the options
* @returns {string} the layout name.
*/
export const getLayoutName = (spec) => {
let layoutName = [spec.sheet];
if (spec.includeLegend) {
if (spec.twoPages) {
layoutName.push('2_pages_legend');
}
} else {
layoutName.push('no_legend');
}
if (spec.landscape) {
layoutName.push('landscape');
}
return layoutName.join('_');
};
/**
* Gets the print scales allowed from the capabilities of the print service.
* @param {capabilities} capabilities the capabilities of the print service
* @returns {array} the scales array
* @memberof utils.PrintUtils
*/
export const getPrintScales = (capabilities) => {
return capabilities.scales.slice(0).reverse().map((scale) => parseFloat(scale.value)) || [];
};
/**
* Guess the nearest zoom level in the allowed scales
* @param {number} zoom the zoom level
* @param {array} scales the allowed scales
* @param {array} [mapScales=defaultScales] the map scales
* @returns {number} the index that best approximates the current map scale
* @memberof utils.PrintUtils
*/
export const getNearestZoom = (zoom, scales, mapScales = defaultScales) => {
const mapScale = mapScales[Math.round(zoom)];
return scales.reduce((previous, current, index) => {
return current < mapScale ? previous : index;
}, 0);
};
/**
* @memberof utils
* Guess the map zoom level from print scale
* @param {number} zoom the zoom level
* @param {array} scales the allowed scales
* @param {array} [mapScales=defaultScales] the map scales
* @returns {number} the index that best approximates the current map scale
* @memberof utils.PrintUtils
*/
export const getMapZoom = (scaleZoom, scales, mapScales = defaultScales) => {
const scale = scales[Math.round(scaleZoom)];
return mapScales.reduce((previous, current, index) => {
return current < scale ? previous : index;
}, 0) + 1;
};
/**
* Get the mapSize for print preview, parsing the layout and limiting the width.
* @param {object} layout the layout object
* @param {number} maxWidth the max width for the mapSize
* @returns {object} width and height of a map limited by the maxWidth and with the same ratio of the layout
* @memberof utils.PrintUtils
*/
export const getMapSize = (layout, maxWidth) => {
if (layout) {
const width = layout.rotation ? layout.map.height : layout.map.width;
const height = layout.rotation ? layout.map.width : layout.map.height;
return {
width: maxWidth,
height: height / width * maxWidth
};
}
return {
width: 100,
height: 100
};
};
export const mapProjectionSelector = (state) => state?.print?.map?.projection ?? "EPSG:3857";
/**
* Parse credit/attribution text by removing html tags within its text plus removing '|' symbol
* @param {string} creditText the layer credit/attribution text
* @returns {string} the parsed credit/attribution text after removing html tags plus '|' symbol within
* @memberof utils.PrintUtils
*/
export function parseCreditRemovingTagsOrSymbol(creditText = "") {
let parsedCredit = creditText;
do {
let tagStartIndex = parsedCredit.indexOf("<");
let tagEndIndex = parsedCredit.indexOf(">");
if (tagStartIndex !== -1 && tagEndIndex !== -1) {
parsedCredit = parsedCredit.replace(parsedCredit.substring(tagStartIndex, tagEndIndex + 1), "");
}
} while (parsedCredit.includes("<") || parsedCredit.includes(">"));
let hasOrSymbol = parsedCredit && parsedCredit.includes("|");
if (hasOrSymbol) {
parsedCredit = parsedCredit?.replaceAll("|", "")?.replaceAll(" ", " ");
}
return parsedCredit;
}
/**
* Gets the credits of layers in one text with '|' separated
* @param {object} layers the map layers for print
* @returns {string} the layers credits as a text '|' separated
* @memberof utils.PrintUtils
*/
export const getLayersCredits = (layers) => {
let layerCredits = layers.filter(lay => lay?.credits?.title || lay?.attribution).map((layer) => {
const layerCreditTitle = layer?.credits?.title || layer?.attribution || '';
const hasOrSymbol = layerCreditTitle.includes('|');
const hasHtmlTag = layerCreditTitle.includes('<');
const layerCredit = (hasHtmlTag || hasOrSymbol)
? parseCreditRemovingTagsOrSymbol(layerCreditTitle)
: layerCreditTitle;
return layerCredit;
});
const uniqueCredits = [...new Set(layerCredits)];
layerCredits = uniqueCredits.join(' | ');
return layerCredits;
};
/**
* Default screen DPI (96) to Print DPI (72). Used to calculate correct resolution for
* screen preview and printed map.
* @memberof utils.PrintUtils
*/
export const DEFAULT_PRINT_RATIO = 96.0 / 72.0;
/**
* Returns the correct multiplier to sync the screen resolution and the printed map resolution.
* @param {number} printSize printed map size (in print points (1/72"))
* @param {number} screenSize screen preview size (in pixels)
* @param {number} dpiRatio ratio screen_dpi / printed_dpi
* @return {number} the resolution multiplier to apply to the screen preview
* @memberof utils.PrintUtils
*/
export function getResolutionMultiplier(printSize, screenSize, dpiRatio = DEFAULT_PRINT_RATIO) {
return printSize / screenSize * dpiRatio;
}
export function getScalesByResolutions(resolutions, ratio, projection = "EPSG:3857") {
// Get the corresponding scales based on the resolutions
const correspScales = (getScales(projection)).map(sc => sc * ratio);
// Calculate scales for each resolution
const scales = resolutions.map(res => {
const firstRes = resolutions[0];
const firstScale = correspScales[0];
// Calculate the scale corresponding to the current resolution
const correspondentScale = res * firstScale / firstRes;
return correspondentScale / ratio;
});
return scales;
}
/**
* Calculates the map print scale denominator (1 : X) for a given print specification.
*
* Handles multiple scale sources:
* - Fixed scales from spec.scales
* - Dynamic scales from projection (via getScales)
* - Custom scales from resolutions (via getScalesByResolutions) when editScale is enabled
*
* @param {Object} rawSpec - Raw print specification object
* @returns {number} Rounded scale denominator (e.g., 5000 for 1:5000 scale)
**/
export const getMapPrintScale = (rawSpec, state) => {
const {params, mergeableParams, excludeLayersFromLegend, ...baseSpec} = rawSpec;
const spec = {...baseSpec, ...params};
const printMap = state?.print?.map;
// * use [spec.zoom] the actual zoom in case useFixedScales = false else use [spec.scaleZoom] the fixed zoom scale not actual
const projectedZoom = Math.round(printMap?.useFixedScales && !printMap?.editScale ? spec.scaleZoom : spec.zoom);
const layout = head(state?.print?.capabilities?.layouts?.filter((l) => l.name === getLayoutName(spec)) || []);
const ratio = getResolutionMultiplier(layout?.map?.width, 370) ?? 1;
const scales = printMap?.editScale ?
printMap.mapPrintResolutions?.length ?
getScalesByResolutions(printMap.mapPrintResolutions, ratio, spec.projection) :
getScales(spec.projection) : spec.scales || getScales(spec.projection);
const reprojectedScale = printMap?.editScale ? scales[projectedZoom] : scales[projectedZoom] || defaultScales[projectedZoom];
return Math.round(reprojectedScale);
};
/**
* Creates the mapfish print specification from the current configuration
* @param {object} spec the current configuration
* @returns {object} the mapfish print configuration to send to the server
* @memberof utils.PrintUtils
*/
export const getMapfishPrintSpecification = (rawSpec, state) => {
const {params, mergeableParams, excludeLayersFromLegend, ...baseSpec} = rawSpec;
const spec = {...baseSpec, ...params};
const printMap = state?.print?.map;
const projectedCenter = reproject(spec.center, 'EPSG:4326', spec.projection);
// * use [spec.zoom] the actual zoom in case useFixedScales = false else use [spec.scaleZoom] the fixed zoom scale not actual
const projectedZoom = Math.round(printMap?.useFixedScales && !printMap?.editScale ? spec.scaleZoom : spec.zoom);
const layout = head(state?.print?.capabilities?.layouts?.filter((l) => l.name === getLayoutName(spec)) || []);
const ratio = getResolutionMultiplier(layout?.map?.width, 370) ?? 1;
const scales = printMap?.editScale ?
printMap.mapPrintResolutions?.length ?
getScalesByResolutions(printMap.mapPrintResolutions, ratio, spec.projection) :
getScales(spec.projection) : spec.scales || getScales(spec.projection);
const reprojectedScale = printMap?.editScale ? scales[projectedZoom] : scales[projectedZoom] || defaultScales[projectedZoom];
const projectedSpec = {
...spec,
center: projectedCenter,
scaleZoom: projectedZoom
};
const legendLayersList = spec.layers.filter(layer => !includes(excludeLayersFromLegend, layer.name));
const legendLayersPromise = PrintUtils.getMapfishLayersSpecification(legendLayersList, projectedSpec, state, 'legend');
return legendLayersPromise.then((legendLayers) => {
const layersPromise = PrintUtils.getMapfishLayersSpecification(spec.layers, projectedSpec, state, 'map');
return layersPromise.then((layers) => {
return {
"units": getUnits(spec.projection),
"srs": normalizeSRS(spec.projection || 'EPSG:3857'),
"layout": PrintUtils.getLayoutName(projectedSpec),
"dpi": parseInt(spec.resolution, 10),
"outputFilename": "mapstore-print",
"geodetic": false,
"mapTitle": spec.name || '',
"comment": spec.description || '',
"layers": layers,
"pages": [
{
"center": [
projectedCenter.x,
projectedCenter.y
],
"scale": reprojectedScale,
"rotation": !isNil(spec.rotation) ? -Number(spec.rotation) : 0 // negate the rotation value to match rotation in map preview and printed output
}
],
"legends": legendLayers,
"credits": getLayersCredits(spec.layers),
...(mergeableParams ? {mergeableParams} : {}),
...params
};
});
});
};
export const localizationFilter = (state, spec) => {
const localizationEnabled = isLocalizedLayerStylesEnabledSelector(state);
const localizationEnv = localizedLayerStylesEnvSelector(state);
const localizedSpec = localizationEnabled ? {
...spec,
env: localizationEnv,
currentLanguage: currentLocaleLanguageSelector(state)
} : spec;
return Promise.resolve(localizedSpec);
};
export const wfsPreloaderFilter = (state, spec) => preloadData(spec);
export const toMapfish = (state, spec) => Promise.resolve(getMapfishPrintSpecification(spec, state));
const defaultPrintingServiceTransformerChain = [
{name: "localization", transformer: localizationFilter},
{name: "wfspreloader", transformer: wfsPreloaderFilter},
{name: "mapfishSpecCreator", transformer: toMapfish}
];
let userTransformerChain = [];
let mapTransformerChain = [];
let validatorsChain = [];
function addOrReplaceTransformers(chain, transformers) {
return transformers.reduce((res, transformer) => {
if (res.findIndex(t => t.name === transformer.name) === -1) {
return [...res, transformer];
}
return res.map(t => t.name === transformer.name ? transformer : t);
}, chain);
}
export function getSpecTransformerChain() {
const userOffset = defaultPrintingServiceTransformerChain.length;
return sortBy(addOrReplaceTransformers(
defaultPrintingServiceTransformerChain.map((t, index) => ({...t, position: index})),
userTransformerChain.map((t, index) => ({...t, position: t.position ?? index + userOffset}))
), ["position"]);
}
export function getMapTransformerChain() {
return mapTransformerChain;
}
export function getValidatorsChain() {
return validatorsChain;
}
/**
* Resets the list of transformers and validators.
* @memberof utils.PrintUtils
*/
export function resetDefaultPrintingService() {
userTransformerChain = [];
mapTransformerChain = [];
validatorsChain = [];
}
/**
* Adds/Updates a user custom transformer for the default printing service spec transformer chain.
*
* Transformers are called by the default printing service to enrich / change the spec payload for mapfish-print
* before calling the remote service.
*
* Adding a new transformer allows adding new variables for a custom config.yaml, or process the default
* ones to implement custom behaviour.
*
* @param {string} name name of the transformer (allows replacing one of the default ones, by specifying its name).
* default transformers are: `localization`, `wfspreloader`, `mapfishSpecCreator`.
* @param {function} transformer (state, spec) => Promise<spec>
* @param {int} position position in the chain (0-indexed), allows inserting a transformer between existing ones
* @memberof utils.PrintUtils
*
* @example
* // add a transformer to append a new property to the spec
* addTransformer("mytransform", (state, spec) => ({...spec, newprop: state.print.myprop}))
*
* If you need to use addTransformer in an extension, use action ADD_PRINT_TRANSFORMER from print module
* Otherwise, the let userTransformerChain are copy to your extension and not override the reference in the print module of MapStore2 framework
*/
export function addTransformer(name, transformer, position) {
userTransformerChain = addOrReplaceTransformers(userTransformerChain, [{name, transformer, position}]);
}
/**
* Adds/Updates a map custom transformer for the default printing service map object transformer chain.
*
* Map transformers can be used to implement custom behaviour that changes map related properties and
* should be reflected on the printing plugin dialog (e.g. the map-preview).
*
* These are applied to the print state map fragment before being passed as a map property to the Print
* plugin items.
*
* @param {string} name name of the transformer (allows replacing and existing one).
* @param {function} transformer (state, map) => map
* @example
* // add a map transformer to increase the map zoom by 1
* addMapTransformer("mymaptransform", (state, map) => ({...map, zoom: map.zoom + 1}))
*/
export function addMapTransformer(name, transformer) {
mapTransformerChain = addOrReplaceTransformers(mapTransformerChain, [{name, transformer}]);
}
function addOrReplaceValidators(chain, list) {
return list.reduce((res, validator) => {
if (res.findIndex(v => v.id === validator.id) === -1) {
return [...res, validator];
}
return res.map(v => v.id === validator.id ? validator : v);
}, chain);
}
/**
* Adds a new validation function.
* @param {string} id unique id of the validator (a validator with the same id will be replaced).
* @param {string} name binding name of the validator (bind the validator result to a specific item / plugin, by item id).
* @param {function} validator (state, current_validation) => { valid: true/false, errors: ["message", ...] }
*
* @example
* // add a validator for the myplugin plugin, bound to the map-preview component
* addValidator("myplugin", "map-preview", (state, current) => state.print.myprop ? {valid: true} : {valid: false, errors: ["myprop missing"]})
*/
export function addValidator(id, name, validator) {
validatorsChain = addOrReplaceValidators(validatorsChain, [{id, name, validator}]);
}
/**
* Returns the default printing service.
*
* A printing service implements all the basic functionalities of a printing engine.
*
* - The print function, whose goal is to transform the Print plugin
* specification object into a specification for the chosen printing engine.
*
* This service is compatible with the mapfish-2 printing engine and works by applying a chain of transformers,
* summing up the defaultPrintingServiceTransformerChain list, to eventual custom transformers,
* added with addTransformer.
*
* Each transformer is a function reiceiving two parameters, the redux global state and the print
* specification object returned by the previous chain step, and returning a Promise of the transformed
* specification:
*
* (state, spec) => Promise.resolve(<transformed spec>)
*
* Project specific transformers can be added to the end of the chain using the addTransformer function.
*
* - The validate function, that validates current user input in the printing dialog and outputs
* eventual validation error to be used by the UI items (to show errors, etc.).
*
* It works by applying a chain of validators, that enrich the validation result object.
*
* Each validator has a name, and a function reiceiving two parameters, the redux global state and the
* actual validation object for the name:
*
* (state, validation) => {valid: true/false, errors: ["message", ...]}
*
* Project specific validators can be added to the end of the chain using the addValidator function.
*
* - The getMapConfiguration function, that returns a map configuration object for the UI items.
*
* It works by applying a chain of map transformers, that transform the map configuration object.
*
* Each transformer is a function reiceiving two parameters, the redux global state and the
* actual map configuration object:
*
* (state, map) => <transformed map>
*
* Project specific transformers can be added to the end of the chain using the addMapTransformer function.
*
* @returns {object} the default printint service.
* @memberof utils.PrintUtils
*/
export const getDefaultPrintingService = () => {
return {
print: (extra) => {
const state = getStore().getState();
const printSpec = printSpecificationSelector(state);
const intialSpec = extra ? {
...printSpec,
...extra
} : printSpec;
return getSpecTransformerChain().map(t => t.transformer).reduce((previous, f) => {
return previous.then(spec=> f(state, spec));
}, Promise.resolve(intialSpec));
},
validate: () => {
const state = getStore().getState();
return getValidatorsChain().reduce((acc, current) => {
const previousValidation = acc[current.name] ?? {valid: true, errors: []};
const validation = current.validator(state, previousValidation);
return {
...acc,
[current.name]: {
valid: previousValidation.valid && validation.valid,
errors: [...previousValidation.errors, ...(validation.errors || [])]
}
};
}, {});
},
getMapConfiguration: () => {
const state = getStore().getState();
return getMapTransformerChain().map(t => t.transformer).reduce((acc, t) => {
return t(state, acc);
}, state?.print?.map || {});
}
};
};
/**
* Returns vendor params that can be used when calling wms server for print requests
* @param {layer} the layer object
*/
export const getPrintVendorParams = (layer) => {
if (layer?.serverType === ServerTypes.NO_VENDOR) {
return {};
}
return { "TILED": true };
};
export const getLegendIconsSize = (spec = {}, layer = {}) => {
const forceIconSize = (spec.forceIconsSize || layer.group === 'background');
const width = forceIconSize ? spec.iconsWidth : get(layer, 'legendOptions.legendWidth', 12);
const height = forceIconSize ? spec.iconsHeight : get(layer, 'legendOptions.legendHeight', 12);
return {
width,
height,
minSymbolSize: min([width, height])
};
};
/**
* Generate the layers (or legend) specification for print.
* @param {array} layers the layers configurations
* @param {spec} spec the print configurations
* @param {string} purpose allowed values: `map|legend`. Tells which spec to generate.
* @returns {array} the configuration array for layers (or legend) to send to the print service.
* @memberof utils.PrintUtils
*/
export const getMapfishLayersSpecification = (layers, spec, state, purpose) => {
const filtered = layers.filter(layer =>
PrintUtils.specCreators[layer.type] &&
PrintUtils.specCreators[layer.type][purpose]
);
return Promise.all(
filtered.map(layer =>
PrintUtils.specCreators[layer.type][purpose](layer, spec, state)
)
).then(results => results.filter(r => r));
};
export const specCreators = {
wms: {
map: (layer, spec) => ({
"baseURL": PrintUtils.normalizeUrl(layer.url) + '?',
"opacity": getOpacity(layer),
"singleTile": false,
"type": "WMS",
"layers": [
layer.name
],
"format": layer.format || "image/png",
"styles": [
layer.style || ''
],
"customParams": addAuthenticationParameter(PrintUtils.normalizeUrl(layer.url), Object.assign({
"TRANSPARENT": true,
...getPrintVendorParams(layer),
"EXCEPTIONS": "application/vnd.ogc.se_inimage",
"scaleMethod": "accurate",
"ENV": generateEnvString(spec.env)
}, layer.baseParams || {}, layer.params || {}, {
...optionsToVendorParams({
layerFilter: layer.layerFilter,
filterObj: layer.filterObj
})
}
))}),
legend: (layer, spec) => {
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;
return {
"name": layer.title || layer.name,
"classes": [
{
"name": "",
"icons": [
PrintUtils.normalizeUrl(layer.url) + url.format({
query: addAuthenticationParameter(PrintUtils.normalizeUrl(layer.url), {
...getWMSLegendConfig({layer, legendOptions, mapBbox: spec.bbox, mapSize: spec.size, projection: spec.projection, format: LEGEND_FORMAT.IMAGE}),
TRANSPARENT: true,
EXCEPTIONS: "application/vnd.ogc.se_xml",
VERSION: "1.1.1",
SCALE: spec.scale,
...getLegendIconsSize(spec, layer),
...(spec.language ? {LANGUAGE: spec.language} : {})
})
})
]
}
]
};
}
},
vector: {
map: (layer, spec, state) => ({
type: 'Vector',
name: layer.name,
"opacity": getOpacity(layer),
styleProperty: "ms_style",
styles: {
1: PrintUtils.toOpenLayers2Style(layer, layer.style),
"Polygon": PrintUtils.toOpenLayers2Style(layer, layer.style, "Polygon"),
"LineString": PrintUtils.toOpenLayers2Style(layer, layer.style, "LineString"),
"Point": PrintUtils.toOpenLayers2Style(layer, layer.style, "Point"),
"FeatureCollection": PrintUtils.toOpenLayers2Style(layer, layer.style, "FeatureCollection")
},
geoJson: reprojectGeoJson({
type: "FeatureCollection",
features: layer?.style?.format === 'geostyler' && layer?.style?.body
? printStyleParser.writeStyle(layer.style.body, true)({ layer, spec, mapPrintScale: getMapPrintScale(spec, state) })
: layer.features.map( f => ({...f, properties: {...f.properties, ms_style: f && f.geometry && f.geometry.type && f.geometry.type.replace("Multi", "") || 1}}))
},
"EPSG:4326",
spec.projection)
}
),
legend: (layer) => {
return renderVectorLegendToBase64(layer)
.then(legendImage => {
if (legendImage) {
return {
name: layer?.title ?? layer?.name,
classes: [
{
name: '',
icons: [legendImage]
}
]
};
}
return null;
});
}
},
graticule: {
map: (layer, spec, state) => {
const layout = head(state?.print?.capabilities.layouts.filter((l) => l.name === getLayoutName(spec)));
const ratio = getResolutionMultiplier(layout?.map?.width, spec.size?.width ?? 370) ?? 1;
const resolutions = getResolutionsForProjection(spec.projection).map(r => r * ratio);
const resolution = resolutions[spec.scaleZoom];
return {
type: 'Vector',
name: layer.name || "graticule",
"opacity": getOpacity(layer),
styleProperty: "ms_style",
styles: {
"lines": PrintUtils.toOpenLayers2Style(layer, layer.style, "GraticuleLines"),
"xlabels": PrintUtils.toOpenLayers2TextStyle(layer, layer.labelXStyle, "GraticuleXLabels"),
"ylabels": PrintUtils.toOpenLayers2TextStyle(layer, layer.labelYStyle, "GraticuleYLabels"),
"frame": PrintUtils.toOpenLayers2Style(layer, layer.frameStyle, "GraticuleFrame")
},
geoJson: getGridGeoJson({
resolutions,
mapProjection: spec.projection,
gridProjection: layer.srs || spec.projection,
extent: calculateExtent(spec.center, resolution, spec.size, spec.projection),
zoom: spec.scaleZoom,
withLabels: true,
xLabelFormatter: layer.xLabelFormatter,
yLabelFormatter: layer.yLabelFormatter,
xLabelStyle: PrintUtils.toOpenLayers2TextStyle(layer, layer.labelXStyle, "GraticuleXLabels"),
yLabelStyle: PrintUtils.toOpenLayers2TextStyle(layer, layer.labelYStyle, "GraticuleYLabels"),
frameSize: layer.frameRatio
})
};
}
},
wfs: {
map: (layer, spec, state) => ({
type: 'Vector',
name: layer.name,
"opacity": getOpacity(layer),
styleProperty: "ms_style",
styles: {
1: PrintUtils.toOpenLayers2Style(layer, layer.style),
"Polygon": PrintUtils.toOpenLayers2Style(layer, layer.style, "Polygon"),
"LineString": PrintUtils.toOpenLayers2Style(layer, layer.style, "LineString"),
"Point": PrintUtils.toOpenLayers2Style(layer, layer.style, "Point"),
"FeatureCollection": PrintUtils.toOpenLayers2Style(layer, layer.style, "FeatureCollection")
},
// NOTE: data in this case have to be pre-loaded, in the correct projection
geoJson: layer.geoJson && {
type: "FeatureCollection",
features: layer?.style?.format === 'geostyler' && layer?.style?.body
? printStyleParser.writeStyle(layer.style.body, true)({ layer: { ...layer, features: layer.geoJson.features }, spec: {...spec, projection: 'EPSG:3857', mapPrintScale: getMapPrintScale(spec, state)}})
: layer.geoJson.features.map(f => ({ ...f, properties: { ...f.properties, ms_style: f && f.geometry && f.geometry.type && f.geometry.type.replace("Multi", "") || 1 } }))
}
}
)
},
osm: {
map: (layer = {}) => ({
"baseURL": "http://a.tile.openstreetmap.org/",
"opacity": getOpacity(layer),
"singleTile": false,
"type": "OSM",
"maxExtent": [
-20037508.3392,
-20037508.3392,
20037508.3392,
20037508.3392
],
"tileSize": [
256,
256
],
"extension": "png",
"resolutions": [
156543.03390625,
78271.516953125,
39135.7584765625,
19567.87923828125,
9783.939619140625,
4891.9698095703125,
2445.9849047851562,
1222.9924523925781,
611.4962261962891,
305.74811309814453,
152.87405654907226,
76.43702827453613,
38.218514137268066,
19.109257068634033,
9.554628534317017,
4.777314267158508,
2.388657133579254,
1.194328566789627,
0.5971642833948135
]
})
},
mapquest: {
map: (layer = {}) => ({
"baseURL": "http://otile1.mqcdn.com/tiles/1.0.0/map/",
"opacity": getOpacity(layer),
"singleTile": false,
"type": "OSM",
"maxExtent": [
-20037508.3392,
-20037508.3392,
20037508.3392,
20037508.3392
],
"tileSize": [
256,
256
],
"extension": "png",
"resolutions": [
156543.03390625,
78271.516953125,
39135.7584765625,
19567.87923828125,
9783.939619140625,
4891.9698095703125,
2445.9849047851562,
1222.9924523925781,
611.4962261962891,
305.74811309814453,
152.87405654907226,
76.43702827453613,
38.218514137268066,
19.109257068634033,
9.554628534317017,
4.777314267158508,
2.388657133579254,
1.194328566789627,
0.5971642833948135
]
})
},
wmts: {
map: (layer, spec) => {
const SRS = spec.projection;
const { tileMatrixSet, tileMatrixSetName} = getTileMatrix(layer, SRS); // TODO: use spec SRS.
if (!tileMatrixSet) {
throw Error("tile matrix not found for pdf EPSG" + SRS);
}
const matrixIds = PrintUtils.getWMTSMatrixIds(layer, tileMatrixSet);
const baseURL = PrintUtils.normalizeUrl(castArray(layer.url)[0]);
let dimensionParams = {};
if (baseURL.indexOf('{Style}') >= 0) {
dimensionParams = {
"dimensions": ["Style"],
"params": {
"STYLE": layer.style
}
};
}
return {
"baseURL": encodeURI(baseURL),
// "dimensions": isEmpty(layer.dimensions) && layer.dimensions || null,
"format": layer.format || "image/png",
"type": "WMTS",
"layer": layer.name,
"customParams ": addAuthenticationParameter(layer.capabilitiesURL, Object.assign({
"TRANSPARENT": true
})),
// rest parameter style is not included
// so simulate with dimensions and params