• 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

63.83
/web/client/utils/styleparser/LeafletStyleParser.js
1
/*
2
 * Copyright 2022, 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 L from 'leaflet';
10
import { castArray, flatten } from 'lodash';
11
import 'ol/geom/Polygon';
12
import {
13
    resolveAttributeTemplate,
14
    geoStylerStyleFilter,
15
    getImageIdFromSymbolizer,
16
    parseSymbolizerExpressions
17
} from './StyleParserUtils';
18
import { drawIcons } from './IconUtils';
19

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

23
const geometryTypeToKind = {
1✔
24
    'Point': ['Mark', 'Icon', 'Text', 'Circle'],
25
    'MultiPoint': ['Mark', 'Icon', 'Text'],
26
    'LineString': ['Line'],
27
    'MultiLineString': ['Line'],
28
    'Polygon': ['Fill'],
29
    'MultiPolygon': ['Fill']
30
};
31

32
const getGeometryFunction = geometryFunctionsLibrary.geojson();
1✔
33

34
const anchorToPoint = (anchor, width, height) => {
1✔
35
    switch (anchor) {
1!
36
    case 'top-left':
UNCOV
37
        return [0, 0];
×
38
    case 'top':
UNCOV
39
        return [width / 2, 0];
×
40
    case 'top-right':
41
        return [width, 0];
1✔
42
    case 'left':
UNCOV
43
        return [0, height / 2];
×
44
    case 'center':
UNCOV
45
        return [width / 2, height / 2];
×
46
    case 'right':
UNCOV
47
        return [width, height / 2];
×
48
    case 'bottom-left':
UNCOV
49
        return [0, height];
×
50
    case 'bottom':
UNCOV
51
        return [width / 2, height];
×
52
    case 'bottom-right':
UNCOV
53
        return [width, height];
×
54
    default:
UNCOV
55
        return [width / 2, height / 2];
×
56
    }
57
};
58

59
const anchorToTransform = (anchor) => {
1✔
60
    switch (anchor) {
1!
61
    case 'top-left':
UNCOV
62
        return ['0px', '0px'];
×
63
    case 'top':
UNCOV
64
        return ['-50%', '0px'];
×
65
    case 'top-right':
UNCOV
66
        return ['-100%', '0px'];
×
67
    case 'left':
UNCOV
68
        return ['0px', '-50%'];
×
69
    case 'center':
UNCOV
70
        return ['-50%', '-50%'];
×
71
    case 'right':
UNCOV
72
        return ['-100%', '-50%'];
×
73
    case 'bottom-left':
UNCOV
74
        return ['0px', '-100%'];
×
75
    case 'bottom':
76
        return ['-50%', '-100%'];
1✔
77
    case 'bottom-right':
UNCOV
78
        return ['-100%', '-100%'];
×
79
    default:
UNCOV
80
        return ['-50%', '-50%'];
×
81
    }
82
};
83

84
function getStyleFuncFromRules({ rules: geoStylerStyleRules = [] }) {
×
85

86
    // the last rules of the array should the one we'll apply
87
    // in case we have multiple symbolizers on the same features
88
    // we ensure to find the last symbolizer matching the filter and geometry type
89
    // by reversing all the rules
90
    const rules = [...geoStylerStyleRules].reverse();
13✔
91
    return ({
13✔
92
        opacity: globalOpacity = 1,
13✔
93
        layer = {},
8✔
94
        features
95
    } = {}) => drawIcons({ rules: geoStylerStyleRules }, { features }).then((images = []) =>  {
13!
96

97
        if (layer._msAdditionalLayers) {
13✔
98
            layer._msAdditionalLayers.forEach((additionalLayer) => {
1✔
UNCOV
99
                additionalLayer.remove();
×
100
            });
101
        }
102

103
        layer._msAdditionalLayers = [];
13✔
104

105
        const pointToLayer = ({ symbolizer: _symbolizer, latlng, feature }) => {
13✔
106
            const symbolizer = parseSymbolizerExpressions(_symbolizer, feature);
7✔
107
            if (symbolizer.kind === 'Mark') {
7✔
108
                const { image, src, width, height } = images.find(({ id }) => id === getImageIdFromSymbolizer(symbolizer, _symbolizer)) || {};
4!
109
                if (image) {
4!
110
                    const aspect = width / height;
4✔
111
                    const size = symbolizer.radius * 2;
4✔
112
                    let iconSizeW = size;
4✔
113
                    let iconSizeH = iconSizeW / aspect;
4✔
114
                    if (height > width) {
4!
115
                        iconSizeH = size;
×
UNCOV
116
                        iconSizeW = iconSizeH * aspect;
×
117
                    }
118
                    return L.marker(latlng, {
4✔
119
                        icon: L.icon({
120
                            iconUrl: src,
121
                            iconSize: [iconSizeW, iconSizeH],
122
                            iconAnchor: [iconSizeW / 2, iconSizeH / 2]
123
                        }),
124
                        opacity: 1 * globalOpacity
125
                    });
126
                }
127
            }
128
            if (symbolizer.kind === 'Icon') {
3✔
129
                const { image, src, width, height } = images.find(({ id }) => id === getImageIdFromSymbolizer(symbolizer, _symbolizer)) || {};
1!
130
                if (image) {
1!
131
                    const aspect = width / height;
1✔
132
                    let iconSizeW = symbolizer.size;
1✔
133
                    let iconSizeH = iconSizeW / aspect;
1✔
134
                    if (height > width) {
1!
135
                        iconSizeH = symbolizer.size;
×
UNCOV
136
                        iconSizeW = iconSizeH * aspect;
×
137
                    }
138
                    return L.marker(latlng, {
1✔
139
                        icon: L.icon({
140
                            iconUrl: src,
141
                            iconSize: [iconSizeW, iconSizeH],
142
                            iconAnchor: anchorToPoint(symbolizer.anchor, iconSizeW, iconSizeH)
143
                        }),
144
                        opacity: symbolizer.opacity * globalOpacity
145
                    });
146
                }
147
            }
148
            if (symbolizer.kind === 'Text') {
2✔
149
                const label = resolveAttributeTemplate(feature, symbolizer.label, '');
1✔
150
                const haloProperties = `
1✔
151
                    -webkit-text-stroke-width:${symbolizer.haloWidth}px;
152
                    -webkit-text-stroke-color:${symbolizer.haloColor || ''};
1!
153
                `;
154
                const [anchorH, anchorV] = anchorToTransform(symbolizer.anchor);
1✔
155
                const textIcon = L.divIcon({
1✔
156
                    html: `<div style="
157
                        color:${symbolizer.color};
158
                        font-family: ${castArray(symbolizer.font || []).join(', ')};
1!
159
                        font-style: ${symbolizer.fontStyle || 'normal'};
1!
160
                        font-weight: ${symbolizer.fontWeight || 'normal'};
1!
161
                        font-size: ${symbolizer.size}px;
162

163
                        position: absolute;
164
                        transform: translate(calc(${anchorH} + ${symbolizer?.offset?.[0] ?? 0}px), calc(${anchorV} + ${symbolizer?.offset?.[1] ?? 0}px)) rotateZ(${symbolizer?.rotate ?? 0}deg);
3!
165

166
                        ${symbolizer.haloWidth > 0 ? haloProperties : ''}
1!
167
                    ">
168
                        ${label}
169
                        </div>`,
170
                    className: ''
171
                });
172
                return L.marker(latlng, {
1✔
173
                    icon: textIcon,
174
                    opacity: 1 * globalOpacity
175
                });
176
            }
177
            if (symbolizer.kind === 'Circle') {
1!
178
                const radius = symbolizer.radius;
1✔
179
                const geodesic = symbolizer.geodesic;
1✔
180
                const geoJSONLayer = L.geoJSON({
1✔
181
                    ...feature,
182
                    geometry: circleToPolygon(feature.geometry.coordinates, radius, geodesic)
183
                });
184
                geoJSONLayer.setStyle({
1✔
185
                    fill: true,
186
                    stroke: true,
187
                    fillColor: symbolizer.color,
188
                    fillOpacity: symbolizer.opacity * globalOpacity,
189
                    color: symbolizer.outlineColor,
190
                    opacity: (symbolizer.outlineOpacity ?? 0) * globalOpacity,
1!
191
                    weight: symbolizer.outlineWidth ?? 0,
1!
192
                    ...(symbolizer.outlineDasharray && { dashArray: symbolizer.outlineDasharray.join(' ') })
2✔
193
                });
194
                return geoJSONLayer;
1✔
195
            }
UNCOV
196
            return null;
×
197
        };
198
        return {
13✔
199
            filter: (feature) => {
200
                const geometryType = feature?.geometry?.type;
5✔
201
                if (rules.length === 0) {
5✔
202
                    return false;
2✔
203
                }
204
                const supportedKinds = geometryTypeToKind[geometryType] || [];
3!
205
                if (rules
3!
206
                    .find(rule =>
207
                        // the symbolizer should be included in the supported ones
208
                        rule?.symbolizers?.find(symbolizer => ['Fill', 'Line'].includes(symbolizer.kind) || supportedKinds.includes(symbolizer.kind))
3!
209
                        // the filter should match the expression or be undefined
210
                        && (!rule.filter || geoStylerStyleFilter(feature, rule.filter))
211
                    )
212
                ) {
213
                    return true;
3✔
214
                }
UNCOV
215
                return false;
×
216
            },
217
            pointToLayer: (feature, latlng) => {
218
                const geometryType = feature?.geometry?.type;
7✔
219
                const supportedKinds = geometryTypeToKind[geometryType] || [];
7!
220
                const validRules = rules
7!
221
                    .filter(rule =>
222
                        // the symbolizer should be included in the supported ones
223
                        rule?.symbolizers?.find(symbolizer => supportedKinds.includes(symbolizer.kind))
7!
224
                        // the filter should match the expression or be undefined
225
                        && (!rule.filter || geoStylerStyleFilter(feature, rule.filter))
226
                    ) || {};
227
                const symbolizers = flatten(validRules.map((rule) => rule.symbolizers.filter(({ kind }) => supportedKinds.includes(kind))));
7✔
228
                symbolizers.forEach((symbolizer, idx) => {
7✔
229
                    if (idx > 0) {
7!
230
                        const pointLayer = pointToLayer({ symbolizer, latlng, feature });
×
231
                        layer._msAdditionalLayers.push(pointLayer);
×
UNCOV
232
                        layer.addLayer(pointLayer);
×
233
                    }
234
                });
235
                const firstValidSymbolizer = symbolizers[0];
7✔
236
                return pointToLayer({
7✔
237
                    symbolizer: firstValidSymbolizer,
238
                    latlng,
239
                    feature
240
                });
241
            },
242
            style: (feature) => {
243
                if (feature?.geometry?.type === 'Point') {
7✔
244
                    return null;
3✔
245
                }
246
                const geometryType = feature?.geometry?.type;
4✔
247
                const supportedKinds = geometryTypeToKind[geometryType] || [];
4!
248
                const validRules = rules
4!
249
                    .filter(rule =>
250
                        // the filter should match the expression or be undefined
251
                        (!rule.filter || geoStylerStyleFilter(feature, rule.filter))
4!
252
                    ) || {};
253

254
                (['LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'].includes(geometryType)
4!
255
                    ? flatten(validRules.map((rule) => rule.symbolizers.filter(({ kind }) => ['Mark', 'Icon', 'Text'].includes(kind))))
4✔
256
                    : [])
257
                    .forEach((_symbolizer) => {
258
                        const symbolizer = parseSymbolizerExpressions(_symbolizer, feature);
×
259
                        const geometryFunction = getGeometryFunction({ msGeometry: { name: 'centerPoint' }, ...symbolizer});
×
260
                        if (geometryFunction) {
×
261
                            const coordinates = geometryFunction(feature);
×
262
                            if (coordinates) {
×
263
                                const latlng = L.latLng(coordinates[1], coordinates[0]);
×
264
                                const pointLayer = pointToLayer({ symbolizer, latlng, feature });
×
265
                                layer._msAdditionalLayers.push(pointLayer);
×
UNCOV
266
                                layer.addLayer(pointLayer);
×
267
                            }
268
                        }
269
                    });
270

271
                const firstValidRule = validRules
4!
272
                    .find(rule =>
273
                        // the symbolizer should be included in the supported ones
274
                        rule?.symbolizers?.find(symbolizer => supportedKinds.includes(symbolizer.kind))
4✔
275
                    ) || {};
276
                const firstValidSymbolizer = parseSymbolizerExpressions(firstValidRule?.symbolizers?.find(symbolizer => supportedKinds.includes(symbolizer.kind)) || {}, feature);
4!
277
                if (firstValidSymbolizer.kind === 'Line') {
4✔
278
                    const geometryFunction = getGeometryFunction(firstValidSymbolizer);
2✔
279
                    const style = {
2✔
280
                        stroke: true,
281
                        fill: false,
282
                        color: firstValidSymbolizer.color,
283
                        opacity: firstValidSymbolizer.opacity * globalOpacity,
284
                        weight: firstValidSymbolizer.width,
285
                        ...(firstValidSymbolizer.dasharray && { dashArray: firstValidSymbolizer.dasharray.join(' ') }),
3✔
286
                        ...(firstValidSymbolizer.cap && { lineCap: firstValidSymbolizer.cap }),
2!
287
                        ...(firstValidSymbolizer.join && { lineJoin: firstValidSymbolizer.join })
2!
288
                    };
289
                    if (geometryFunction && feature.geometry.type === 'LineString') {
2!
290
                        const coordinates = geometryFunction(feature);
×
291
                        const geoJSONLayer = L.geoJSON({ ...feature, geometry: { type: 'LineString', coordinates }});
×
292
                        geoJSONLayer.setStyle(style);
×
293
                        layer._msAdditionalLayers.push(geoJSONLayer);
×
294
                        layer.addLayer(geoJSONLayer);
×
UNCOV
295
                        return {
×
296
                            stroke: false,
297
                            fill: false
298
                        };
299
                    }
300

301
                    return style;
2✔
302
                }
303
                if (firstValidSymbolizer.kind === 'Fill') {
2!
304
                    const geometryFunction = getGeometryFunction(firstValidSymbolizer);
2✔
305
                    const style = {
2✔
306
                        fill: true,
307
                        stroke: true,
308
                        fillColor: firstValidSymbolizer.color,
309
                        fillOpacity: firstValidSymbolizer.fillOpacity * globalOpacity,
310
                        color: firstValidSymbolizer.outlineColor,
311
                        opacity: (firstValidSymbolizer.outlineOpacity ?? 0) * globalOpacity,
2!
312
                        weight: firstValidSymbolizer.outlineWidth ?? 0,
2!
313
                        ...(firstValidSymbolizer.outlineDasharray && { dashArray: firstValidSymbolizer.outlineDasharray.join(' ') })
4✔
314
                    };
315
                    if (geometryFunction && feature.geometry.type === 'Polygon') {
2!
316
                        const coordinates = geometryFunction(feature);
×
317
                        const geoJSONLayer = L.geoJSON({ ...feature, geometry: { type: 'Polygon', coordinates }});
×
318
                        geoJSONLayer.setStyle(style);
×
319
                        layer._msAdditionalLayers.push(geoJSONLayer);
×
320
                        layer.addLayer(geoJSONLayer);
×
UNCOV
321
                        return {
×
322
                            stroke: false,
323
                            fill: false
324
                        };
325
                    }
326
                }
327
                return {
2✔
328
                    stroke: false,
329
                    fill: false
330
                };
331
            }
332
        };
333
    });
334
}
335

336
class LeafletStyleParser {
337

338
    readStyle() {
339
        return new Promise((resolve, reject) => {
1✔
340
            try {
1✔
341
                resolve(null);
1✔
342
            } catch (error) {
UNCOV
343
                reject(error);
×
344
            }
345
        });
346
    }
347

348
    writeStyle(geoStylerStyle) {
349
        return new Promise((resolve, reject) => {
13✔
350
            try {
13✔
351
                const styleFunc = getStyleFuncFromRules(geoStylerStyle);
13✔
352
                resolve(styleFunc);
13✔
353
            } catch (error) {
UNCOV
354
                reject(error);
×
355
            }
356
        });
357
    }
358
}
359

360
export default LeafletStyleParser;
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