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

geosolutions-it / MapStore2 / 15811185418

22 Jun 2025 09:57PM UTC coverage: 76.901% (-0.03%) from 76.934%
15811185418

Pull #11130

github

web-flow
Merge 0b5467418 into 7cde38ac9
Pull Request #11130: #10839: Allow printing by freely setting the scale factor

31173 of 48566 branches covered (64.19%)

39 of 103 new or added lines in 7 files covered. (37.86%)

1644 existing lines in 149 files now uncovered.

38769 of 50414 relevant lines covered (76.9%)

36.38 hits per line

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

60.61
/web/client/utils/styleparser/PrintStyleParser.js
1
/*
2
 * Copyright 2023, 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 { flatten } from 'lodash';
10
import turfFlatten from '@turf/flatten';
11
import {
12
    resolveAttributeTemplate,
13
    geoStylerStyleFilter,
14
    drawWellKnownNameImageFromSymbolizer,
15
    parseSymbolizerExpressions,
16
    getCachedImageById
17
} from './StyleParserUtils';
18
import { drawIcons } from './IconUtils';
19

20
import { geometryFunctionsLibrary } from './GeometryFunctionsUtils';
21
import { circleToPolygon } from '../DrawGeometryUtils';
22

23
const getGeometryFunction = geometryFunctionsLibrary.geojson();
1✔
24

25
const anchorToGraphicOffset = (anchor, width, height) => {
1✔
26
    switch (anchor) {
1!
27
    case 'top-left':
28
        return [0, 0];
1✔
29
    case 'top':
UNCOV
30
        return [-(width / 2), 0];
×
31
    case 'top-right':
UNCOV
32
        return [-width, 0];
×
33
    case 'left':
UNCOV
34
        return [0, -(height / 2)];
×
35
    case 'center':
UNCOV
36
        return [-(width / 2), -(height / 2)];
×
37
    case 'right':
UNCOV
38
        return [-width, -(height / 2)];
×
39
    case 'bottom-left':
UNCOV
40
        return [0, -height];
×
41
    case 'bottom':
UNCOV
42
        return [-(width / 2), -height];
×
43
    case 'bottom-right':
UNCOV
44
        return [-width, -height];
×
45
    default:
UNCOV
46
        return [-(width / 2), -(height / 2)];
×
47
    }
48
};
49

50
const anchorToLabelAlign = (anchor) => {
1✔
51
    switch (anchor) {
1!
52
    case 'top-left':
UNCOV
53
        return 'lt';
×
54
    case 'top':
UNCOV
55
        return 'ct';
×
56
    case 'top-right':
UNCOV
57
        return 'rt';
×
58
    case 'left':
UNCOV
59
        return 'lm';
×
60
    case 'center':
UNCOV
61
        return 'cm';
×
62
    case 'right':
UNCOV
63
        return 'rm';
×
64
    case 'bottom-left':
UNCOV
65
        return 'lb';
×
66
    case 'bottom':
67
        return 'cb';
1✔
68
    case 'bottom-right':
UNCOV
69
        return 'rb';
×
70
    default:
UNCOV
71
        return 'cm';
×
72
    }
73
};
74

75
const symbolizerToPrintMSStyle = (symbolizer, feature, layer, originalSymbolizer) => {
1✔
76
    const globalOpacity = layer.opacity === undefined ? 1 : layer.opacity;
10!
77
    if (symbolizer.kind === 'Mark') {
10✔
78
        const { width, height, canvas }  = drawWellKnownNameImageFromSymbolizer(symbolizer);
1✔
79
        return {
1✔
80
            graphicWidth: width,
81
            graphicHeight: height,
82
            externalGraphic: canvas.toDataURL(),
83
            graphicXOffset: -width / 2,
84
            graphicYOffset: -height / 2,
85
            rotation: symbolizer.rotate || 0,
1!
86
            graphicOpacity: globalOpacity
87
        };
88
    }
89
    if (symbolizer.kind === 'Icon') {
9✔
90
        const { width = symbolizer.size, height = symbolizer.size }  = getCachedImageById(originalSymbolizer);
1!
91
        const aspect = width / height;
1✔
92
        let iconSizeW = symbolizer.size;
1✔
93
        let iconSizeH = iconSizeW / aspect;
1✔
94
        if (height > width) {
1!
95
            iconSizeH = symbolizer.size;
×
UNCOV
96
            iconSizeW = iconSizeH * aspect;
×
97
        }
98
        const [graphicXOffset, graphicYOffset] = anchorToGraphicOffset(symbolizer.anchor, iconSizeW, iconSizeH);
1✔
99
        return {
1✔
100
            graphicWidth: iconSizeW,
101
            graphicHeight: iconSizeH,
102
            externalGraphic: symbolizer.image,
103
            graphicXOffset,
104
            graphicYOffset,
105
            rotation: symbolizer.rotate || 0,
1!
106
            graphicOpacity: symbolizer.opacity * globalOpacity
107
        };
108
    }
109
    if (symbolizer.kind === 'Text') {
8✔
110
        return {
1✔
111
            // not supported
112
            // fontStyle: symbolizer.fontStyle,
113
            fontSize: symbolizer.size, // in mapfish is in px
114
            // Supported itext fonts: COURIER, HELVETICA, TIMES_ROMAN
115
            fontFamily: (symbolizer.font || ['TIMES_ROMAN'])[0],
1!
116
            fontWeight: symbolizer.fontWeight,
117
            labelAlign: anchorToLabelAlign(symbolizer.anchor),
118
            labelXOffset: symbolizer?.offset?.[0] || 0,
1!
119
            labelYOffset: -(symbolizer?.offset?.[1] || 0),
1!
120
            rotation: -(symbolizer.rotate || 0),
1!
121
            fontColor: symbolizer.color,
122
            fontOpacity: 1 * globalOpacity,
123
            label: resolveAttributeTemplate(feature, symbolizer.label, ''),
124
            // does not work
125
            // the halo color cover the text
126
            /*
127
            ...(symbolizer.haloWidth > 0 && {
128
                labelOutlineColor: symbolizer.haloColor,
129
                labelOutlineOpacity: 1 * globalOpacity,
130
                labelOutlineWidth: symbolizer.haloWidth
131
            }),
132
            */
133
            // hide default point
134
            fillOpacity: 0,
135
            pointRadius: 0,
136
            strokeOpacity: 0,
137
            strokeWidth: 0
138
        };
139
    }
140
    if (symbolizer.kind === 'Line') {
7✔
141
        return {
4✔
142
            strokeColor: symbolizer.color,
143
            strokeOpacity: symbolizer.opacity * globalOpacity,
144
            strokeWidth: symbolizer.width,
145
            ...(symbolizer.dasharray && { strokeDashstyle: symbolizer.dasharray.join(" ") })
5✔
146
        };
147
    }
148
    if (symbolizer.kind === 'Fill') {
3✔
149
        return {
2✔
150
            strokeColor: symbolizer.outlineColor,
151
            strokeOpacity: (symbolizer.outlineOpacity ?? 0) * globalOpacity,
2!
152
            strokeWidth: symbolizer.outlineWidth ?? 0,
2!
153
            ...(symbolizer.outlineDasharray && { strokeDashstyle: symbolizer.outlineDasharray.join(" ") }),
4✔
154
            fillColor: symbolizer.color,
155
            fillOpacity: symbolizer.fillOpacity * globalOpacity
156
        };
157
    }
158
    if (symbolizer.kind === 'Circle') {
1!
159
        return {
1✔
160
            strokeColor: symbolizer.outlineColor,
161
            strokeOpacity: (symbolizer.outlineOpacity ?? 0) * globalOpacity,
1!
162
            strokeWidth: symbolizer.outlineWidth ?? 0,
1!
163
            ...(symbolizer.outlineDasharray && { strokeDashstyle: symbolizer.outlineDasharray.join(" ") }),
2✔
164
            fillColor: symbolizer.color,
165
            fillOpacity: symbolizer.opacity * globalOpacity
166
        };
167
    }
UNCOV
168
    return {
×
169
        display: 'none'
170
    };
171
};
172

173
export const getPrintStyleFuncFromRules = (geoStylerStyle) => {
1✔
174
    return ({
10✔
175
        layer,
176
        spec = { projection: 'EPSG:3857' }
8✔
177
    }) => {
178
        if (!layer?.features) {
10!
UNCOV
179
            return [];
×
180
        }
181
        const collection = turfFlatten({ type: 'FeatureCollection', features: layer.features});
10✔
182
        return flatten(collection.features
10✔
183
            .map((feature) => {
184
                const validRules = geoStylerStyle?.rules?.filter((rule) => !rule.filter || geoStylerStyleFilter(feature, rule.filter));
10✔
185
                if (validRules.length > 0) {
10!
186
                    const geometryType = feature.geometry.type;
10✔
187
                    const symbolizers = validRules.reduce((acc, rule) => [...acc, ...rule?.symbolizers], []);
10✔
188
                    const pointGeometrySymbolizers = symbolizers.filter((symbolizer) =>
10✔
189
                        ['Mark', 'Icon', 'Text', 'Model'].includes(symbolizer.kind) && ['Point'].includes(geometryType)
10✔
190
                    );
191
                    const polylineGeometrySymbolizers = symbolizers.filter((symbolizer) =>
10✔
192
                        symbolizer.kind === 'Line' && ['LineString'].includes(geometryType)
10✔
193
                    );
194
                    const polygonGeometrySymbolizers = symbolizers.filter((symbolizer) =>
10✔
195
                        symbolizer.kind === 'Fill' && ['Polygon'].includes(geometryType)
10✔
196
                    );
197

198
                    const circleGeometrySymbolizers = symbolizers.filter((symbolizer) =>
10✔
199
                        symbolizer.kind === 'Circle' && ['Point'].includes(geometryType)
10✔
200
                    );
201

202
                    const additionalPointSymbolizers = symbolizers.filter((symbolizer, idx) =>
10✔
203
                        ['Mark', 'Icon', 'Text', 'Model'].includes(symbolizer.kind)
10✔
204
                        && (
205
                            ['Polygon'].includes(geometryType)
206
                            || ['LineString'].includes(geometryType)
207
                            || ['Point'].includes(geometryType) && (circleGeometrySymbolizers.length === 0
3!
208
                                ? idx < pointGeometrySymbolizers.length - 1
209
                                : true)
210
                        )
211
                    );
212
                    const originalSymbolizer = circleGeometrySymbolizers[circleGeometrySymbolizers.length - 1]
10✔
213
                    || pointGeometrySymbolizers[pointGeometrySymbolizers.length - 1]
214
                    || polylineGeometrySymbolizers[polylineGeometrySymbolizers.length - 1]
215
                    || polygonGeometrySymbolizers[polygonGeometrySymbolizers.length - 1];
216

217
                    const symbolizer = parseSymbolizerExpressions(originalSymbolizer, feature);
10✔
218

219
                    let geometry = feature.geometry;
10✔
220
                    const geometryFunction = getGeometryFunction(symbolizer);
10✔
221
                    if (geometryFunction && (geometryType === 'LineString' || geometryType === 'Polygon')) {
10!
UNCOV
222
                        geometry = {
×
223
                            type: geometryType,
224
                            coordinates: geometryFunction(feature)
225
                        };
226
                    }
227
                    if (geometryType === 'Point' && circleGeometrySymbolizers.length) {
10✔
228
                        geometry = circleToPolygon(feature.geometry.coordinates, symbolizer.radius, symbolizer.geodesic, {
1✔
229
                            projection: spec.projection
230
                        });
231
                    }
232

233
                    return [
10✔
234
                        {
235
                            ...feature,
236
                            geometry,
237
                            properties: {
238
                                ...feature?.properties,
239
                                ms_style: symbolizerToPrintMSStyle(symbolizer, feature, layer, originalSymbolizer)
240
                            }
241
                        },
242
                        ...additionalPointSymbolizers.map((_additionalSymbolizer) => {
243
                            const additionalSymbolizer = parseSymbolizerExpressions(_additionalSymbolizer, feature);
×
244
                            const geomFunction = getGeometryFunction({ msGeometry: { name: 'centerPoint' }, ...additionalSymbolizer});
×
245
                            if (geomFunction) {
×
246
                                const coordinates = geomFunction(feature);
×
247
                                if (coordinates) {
×
UNCOV
248
                                    return {
×
249
                                        ...feature,
250
                                        geometry: {
251
                                            type: 'Point',
252
                                            coordinates
253
                                        },
254
                                        properties: {
255
                                            ...feature?.properties,
256
                                            ms_style: symbolizerToPrintMSStyle(additionalSymbolizer, feature, layer, _additionalSymbolizer)
257
                                        }
258
                                    };
259
                                }
260
                            }
261
                            return null;
×
UNCOV
262
                        }).filter((feat) => !!feat)
×
263
                    ];
264
                }
UNCOV
265
                return [];
×
266
            }));
267
    };
268
};
269

270
class PrintStyleParser {
271

272
    readStyle() {
273
        return new Promise((resolve, reject) => {
1✔
274
            try {
1✔
275
                resolve(null);
1✔
276
            } catch (error) {
UNCOV
277
                reject(error);
×
278
            }
279
        });
280
    }
281

282
    writeStyle(geoStylerStyle, sync) {
283
        if (sync) {
10!
284
            return getPrintStyleFuncFromRules(geoStylerStyle);
10✔
285
        }
286
        return new Promise((resolve, reject) => {
×
287
            try {
×
UNCOV
288
                const styleFunc = (options) => drawIcons(geoStylerStyle)
×
289
                    .then((images = []) => {
×
UNCOV
290
                        return getPrintStyleFuncFromRules(geoStylerStyle, { images })(options);
×
291
                    });
UNCOV
292
                resolve(styleFunc);
×
293
            } catch (error) {
UNCOV
294
                reject(error);
×
295
            }
296
        });
297
    }
298
}
299

300
export default PrintStyleParser;
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