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

geosolutions-it / MapStore2 / 18968041869

31 Oct 2025 09:13AM UTC coverage: 76.918% (-0.01%) from 76.932%
18968041869

Pull #11611

github

web-flow
Merge 1b2ac4428 into f5928b825
Pull Request #11611: #11577 Doc build fixed for node 22. Build strategy

31983 of 49693 branches covered (64.36%)

39738 of 51663 relevant lines covered (76.92%)

37.87 hits per line

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

85.44
/web/client/components/widgets/enhancers/multiProtocolChart.js
1
/*
2
 * Copyright 2020, 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 React, { useEffect, useRef, useState, memo } from 'react';
10
import { castArray, isObject, isNil, sortBy, debounce } from 'lodash';
11
import wpsAggregate from '../../../observables/wps/aggregate';
12
import { getLayerJSONFeature } from '../../../observables/wfs';
13
import { getWpsUrl, getSearchUrl } from '../../../utils/LayersUtils';
14
import axios from '../../../libs/ajax';
15
const CancelToken = axios.CancelToken;
1✔
16

17
/**
18
 * Extracts null handling configuration from options
19
 * @param {object} options - Widget options
20
 * @returns {object} Object containing strategy and placeholder
21
 */
22
const getNullHandlingConfig = (options = {}) => {
1!
23
    const nullHandling = options.nullHandling?.groupByAttributes || {};
6✔
24
    const strategy = nullHandling.strategy || 'default';
6✔
25
    const placeholder = nullHandling.placeholder;
6✔
26
    return { strategy, placeholder };
6✔
27
};
28

29
export const wfsToChartData = ({ features } = {}, options = {}) => {
1!
30
    const { groupByAttributes } = options;
3✔
31
    const { strategy, placeholder } = getNullHandlingConfig(options);
3✔
32

33
    return sortBy(
3✔
34
        features
35
            .filter(({ properties }) => {
36
                if (strategy !== 'exclude') {
24✔
37
                    return true;
21✔
38
                }
39
                return properties[groupByAttributes] !== null;
3✔
40
            })
41
            .map(({ properties }) => {
42
                // Replace null in groupByAttributes field with placeholder if strategy is placeholder and placeholder is provided
43
                if (properties[groupByAttributes] === null && strategy === 'placeholder' && placeholder) {
23✔
44
                    return {
1✔
45
                        ...properties,
46
                        [groupByAttributes]: placeholder
47
                    };
48
                }
49
                return properties;
22✔
50
            }),
51
        groupByAttributes
52
    );
53
};
54

55
export const wpsAggregateToChartData = ({AggregationResults = [], GroupByAttributes = [], AggregationAttribute, AggregationFunctions} = {}, options = {}) => {
1!
56
    const { strategy, placeholder } = getNullHandlingConfig(options);
3✔
57

58
    return AggregationResults
3✔
59
        .filter(res => {
60
            if (strategy !== 'exclude') {
12✔
61
                return true;
9✔
62
            }
63
            return res[0] !== null;
3✔
64
        })
65
        .map((res) => ({
11✔
66
            ...GroupByAttributes.reduce((a, p, i) => {
67
                let value = res[i];
17✔
68
                // Replace null with placeholder if strategy is placeholder and placeholder is provided
69
                if (i === 0 && value === null && strategy === 'placeholder' && placeholder) {
17✔
70
                    value = placeholder;
1✔
71
                } else if (isObject(value)) {
16!
72
                    if (!isNil(value.time)) {
×
73
                        value = (new Date(value.time)).toISOString();
×
74
                    } else {
75
                        throw new Error('Unknown response format from server');
×
76
                    }
77
                }
78
                return {
17✔
79
                    ...a,
80
                    [p]: value
81
                };
82
            }, {}),
83
            [`${AggregationFunctions[0]}(${AggregationAttribute})`]: res[res.length - 1]
84
        })).sort( (e1, e2) => {
85
            const n1 = parseFloat(e1[GroupByAttributes]);
11✔
86
            const n2 = parseFloat(e2[GroupByAttributes]);
11✔
87
            if (!isNaN(n1) && !isNaN(n2) ) {
11!
88
                return n1 - n2;
×
89
            }
90
            if (e1 < e2) {
11!
91
                return -1;
×
92
            }
93
            if (e1 > e2) {
11!
94
                return 1;
×
95
            }
96
            return 0;
11✔
97
        });
98
};
99

100
const sameFilter = (f1, f2) => f1 === f2;
4✔
101
const sameOptions = (o1 = {}, o2 = {}) =>
1!
102
    o1.aggregateFunction === o2.aggregateFunction
4✔
103
    && o1.aggregationAttribute === o2.aggregationAttribute
104
    && o1.groupByAttributes === o2.groupByAttributes
105
    && o1.classificationAttribute === o2.classificationAttribute
106
    && o1.viewParams === o2.viewParams
107
    && o1.nullHandling?.groupByAttributes?.strategy === o2.nullHandling?.groupByAttributes?.strategy
108
    && o1.nullHandling?.groupByAttributes?.placeholder === o2.nullHandling?.groupByAttributes?.placeholder;
109

110

111
const dataServiceRequests = {
1✔
112
    wfs: ({ layer, options, filter }, { cancelToken }) => getLayerJSONFeature(
1✔
113
        layer,
114
        filter,
115
        {
116
            propertyName: options.classificationAttribute
1!
117
                ? [
118
                    ...castArray(options.aggregationAttribute),
119
                    ...castArray(options.groupByAttributes),
120
                    ...castArray(options.classificationAttribute)
121
                ]
122
                : [
123
                    ...castArray(options.aggregationAttribute),
124
                    ...castArray(options.groupByAttributes)
125
                ],
126
            requestOptions: {
127
                cancelToken
128
            }
129
        }
130
    )
131
        .toPromise()
132
        .then((response) => wfsToChartData(response, options)),
1✔
133
    wps: ({ layer, options, filter }, { cancelToken }) => wpsAggregate(
1✔
134
        getWpsUrl(layer),
135
        {featureType: layer.name, ...options, filter}, {
136
            timeout: 15000,
137
            cancelToken
138
        }, layer)
139
        .toPromise()
140
        .then((data) => wpsAggregateToChartData(data, options))
1✔
141
};
142

143
const getDataServiceType = ({ layer, options }) => {
1✔
144
    if (!options) {
2!
145
        return '';
×
146
    }
147
    if (!options.aggregateFunction || options.aggregateFunction === "None") {
2✔
148
        return layer.name && getSearchUrl(layer)
1!
149
            // maybe another attribute
150
            && options?.aggregationAttribute
151
            // TODO: not needed
152
            && options?.groupByAttributes ? 'wfs' : '';
153
    }
154
    if (layer.name && getWpsUrl(layer) && options && options.aggregateFunction && options.aggregationAttribute && options.groupByAttributes || options.classificationAttribute) {
1!
155
        return 'wps';
1✔
156
    }
157
    return '';
×
158
};
159

160
const arePropsEqual = (prevProps, nextProps) =>
1✔
161
    (nextProps.layer && prevProps.layer.name === nextProps.layer.name && prevProps.layer.loadingError === nextProps.layer.loadingError)
4✔
162
    && sameOptions(prevProps.options, nextProps.options)
163
    && sameFilter(prevProps.filter, nextProps.filter);
164

165
const multiProtocolChart = (Component) => {
1✔
166
    function MultiProtocolChart({ traces = [], ...props }) {
11✔
167
        const [state, setState] = useState({ loading: true });
17✔
168
        const prevTraces = useRef([]);
17✔
169
        const cancelTokens = useRef([]);
17✔
170
        const isMounted = useRef(true);
17✔
171
        const updateRequest = useRef();
17✔
172

173
        function handleLoad(newState) {
174
            setState(newState);
2✔
175
            if (props.onLoad) {
2!
176
                props.onLoad(newState);
×
177
            }
178
        }
179

180
        function handleLoadError(newState) {
181
            setState(newState);
×
182
            if (props.onLoadError) {
×
183
                props.onLoadError(newState);
×
184
            }
185
        }
186
        function clearRequests() {
187
            cancelTokens.current.forEach((cancel) => {
14✔
188
                cancel();
×
189
            });
190
            cancelTokens.current = [];
14✔
191
            updateRequest.current.cancel();
14✔
192
        }
193

194
        useEffect(() => {
17✔
195
            isMounted.current = true;
12✔
196
            updateRequest.current = debounce((dataRequests) => {
12✔
197
                axios.all(
2✔
198
                    dataRequests.map(({ dataServiceRequest, trace }) =>
199
                        dataServiceRequest(trace, {
2✔
200
                            cancelToken: new CancelToken((cancel) => {
201
                                cancelTokens.current.push(cancel);
2✔
202
                            })
203
                        })
204
                    )
205
                )
206
                    .then((data) => {
207
                        if (isMounted.current) {
2!
208
                            handleLoad({
2✔
209
                                data,
210
                                loading: false,
211
                                isAnimationActive: false,
212
                                error: undefined
213
                            });
214
                        }
215
                    })
216
                    .catch((error) => {
217
                        if (isMounted.current && !error.__CANCEL__) {
×
218
                            handleLoadError({
×
219
                                loading: false,
220
                                error,
221
                                data: []
222
                            });
223
                        }
224
                    })
225
                    .finally(() => {
226
                        cancelTokens.current = [];
2✔
227
                    });
228
            }, props.debounceTime || 300);
24✔
229
            return () => {
12✔
230
                isMounted.current = false;
12✔
231
                clearRequests();
12✔
232
            };
233
        }, []);
234
        useEffect(() => {
17✔
235
            if (traces.length !== prevTraces.current.length || traces.some((trace, idx) => {
17✔
236
                const prevTrace = prevTraces.current[idx];
4✔
237
                return !prevTrace || !arePropsEqual(prevTrace, trace);
4✔
238
            })) {
239
                const dataRequests = traces.map((trace) => {
2✔
240
                    const serviceType = getDataServiceType(trace);
2✔
241
                    // return an empty data array if the single trace is not correctly configured
242
                    const notSupportedService = () => Promise.resolve([]);
2✔
243
                    const dataServiceRequest = dataServiceRequests[serviceType] || notSupportedService;
2!
244
                    return { trace, dataServiceRequest };
2✔
245
                });
246
                clearRequests();
2✔
247
                setState({ loading: true, error: undefined });
2✔
248
                updateRequest.current(dataRequests);
2✔
249
            }
250
            prevTraces.current = [...traces];
17✔
251
        });
252
        return (<Component
17✔
253
            {...props}
254
            {...state}
255
            traces={traces}
256
        />);
257
    }
258
    // it's not possible to use hook without adding the memo
259
    // probably the chain of recompose enhancers
260
    // provide a react component not compatible with hooks
261
    // we can remove this memo once we remove the chain of recompose enhancers
262
    return memo(MultiProtocolChart);
5✔
263
};
264

265
export default multiProtocolChart;
266

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