• 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

30.86
/web/client/components/mapcontrols/scale/ScaleBox.jsx
1
/**
2
 * Copyright 2015, 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 { isEqual } from 'lodash';
10
import PropTypes from 'prop-types';
11
import React from 'react';
12
import { ControlLabel, Form, FormControl, FormGroup } from 'react-bootstrap';
13
import {Creatable} from 'react-select';
14
import { getExactZoomFromResolution, getGoogleMercatorScales, getResolutions } from '../../../utils/MapUtils';
15
import localizedProps from '../../misc/enhancers/localizedProps';
16
import Message from '../../I18N/Message';
17
const ReactSelectCreatable = localizedProps(['placeholder', 'noResultsText'])(Creatable);
1✔
18

19
class ScaleBox extends React.Component {
20
    static propTypes = {
1✔
21
        id: PropTypes.string,
22
        style: PropTypes.object,
23
        scales: PropTypes.array,
24
        currentZoomLvl: PropTypes.number,
25
        minZoom: PropTypes.number,
26
        onChange: PropTypes.func,
27
        readOnly: PropTypes.bool,
28
        label: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.object]),
29
        template: PropTypes.func,
30
        useRawInput: PropTypes.bool,
31
        disableScaleLockingParms: PropTypes.object
32
    };
33

34
    static defaultProps = {
1✔
35
        id: 'mapstore-scalebox',
36
        scales: getGoogleMercatorScales(0, 28),
37
        currentZoomLvl: 0,
38
        minZoom: 0,
39
        scale: 0,
40
        onChange() {},
41
        readOnly: false,
42
        template: scale => scale < 1
261!
43
            ? Math.round(1 / scale) + " : 1"
44
            : "1 : " + Math.round(scale),
45
        useRawInput: false,
46
        disableScaleLockingParms: {}
47
    };
48
    constructor(props) {
49
        super(props);
16✔
50
        this.state = {
16✔
51
            scales: this.props.disableScaleLockingParms?.resolutions?.length ?
16!
52
                this.getScalesFromResolutions(this.props.disableScaleLockingParms?.resolutions) :
53
                this.props.scales.map((scale, idx) => ({
309✔
54
                    value: scale,
55
                    zoom: idx
56
                }))
57
        };
58
    }
59

60
    shouldComponentUpdate(nextProps) {
61
        return !isEqual(nextProps, this.props);
4✔
62
    }
63
    onComboChange = (event) => {
16✔
64
        let selectedZoomLvl = parseInt(event.nativeEvent.target.value, 10);
1✔
65
        this.props.onChange(selectedZoomLvl, this.props.scales[selectedZoomLvl]);
1✔
66
    };
67

68
    getOptions = () => {
16✔
69
        return this.state.scales.map((item) => {
17✔
70
            return (
260✔
71
                <option value={item.zoom} key={item.zoom}>{this.props.template(item.value, item.zoom)}</option>
72
            );
73
        }).filter((element, index) => index >= this.props.minZoom);
260✔
74
    };
75
    // for print in case editScale = true
76
    updateScales(scales, newValue) {
NEW
77
        const scalesValues = scales.map(scale => scale.value);
×
78

NEW
79
        const isMissing = newValue && scalesValues.indexOf(newValue.value) === -1;
×
80

NEW
81
        return isMissing
×
NEW
82
            ? [ {value: newValue.value, zoom: newValue.zoom}, ...scales ].sort((a, b) => (a.value > b.value ? -1 : 1))
×
83
            : scales;
84
    }
85
    handleUpdateScales = (newScaleValue) => {
16✔
NEW
86
        this.setState((prevState) => ({
×
87
            scales: this.updateScales(prevState.scales, newScaleValue)
88
        }));
89
    };
90
    getResolutionByScale(scale, resolutions) {
NEW
91
        const { disableScaleLockingParms } = this.props;
×
NEW
92
        let correspScales = this.props.scales.map(sc => sc * disableScaleLockingParms.ratio);
×
NEW
93
        const firstRes = resolutions[0];
×
NEW
94
        const firstScale = correspScales[0];
×
NEW
95
        const corresEnteredScale = scale * disableScaleLockingParms?.ratio || 1;
×
NEW
96
        const correspondentResolution = corresEnteredScale * firstRes / firstScale;
×
NEW
97
        return correspondentResolution;
×
98
    }
99

100
    getZoomLevelByResolution(resolution) {
NEW
101
        const resolutions = this.props.disableScaleLockingParms?.resolutions || getResolutions(this.props.disableScaleLockingParms?.projection);
×
NEW
102
        const corresZoom = getExactZoomFromResolution(resolution, resolutions) ?? 0;
×
NEW
103
        return parseFloat(corresZoom.toFixed(2));
×
104
    }
105
    getScalesFromResolutions(resolutions) {
NEW
106
        const { disableScaleLockingParms } = this.props;
×
107

108
        // Calculate the corresponding scales based on the ratio
NEW
109
        let correspScales = this.props.scales.map(sc => sc * disableScaleLockingParms.ratio).map((sc, idx) => ({value: sc, zoom: idx}));
×
110

111
        // Get the first resolution and the first corresponding scale
NEW
112
        const firstRes = resolutions[0];
×
NEW
113
        const firstScale = correspScales[0].value;
×
114

115
        // Calculate the corresponding scales for each resolution
NEW
116
        const correspondentScales = resolutions.map(res => {
×
NEW
117
            return res * firstScale / firstRes;
×
118
        });
NEW
119
        return correspondentScales.map(sc => sc / disableScaleLockingParms.ratio).map((sc, idx) => ({value: sc, zoom: this.getZoomLevelByResolution(resolutions[idx])}));
×
120
    }
121
    handleChangeEditableScaleList(option) {
NEW
122
        const {disableScaleLockingParms} = this.props;
×
123

NEW
124
        const newValue = option?.value && parseFloat(option.value);
×
NEW
125
        const newScale = option?.scale && parseFloat(option.scale) || newValue;
×
NEW
126
        const newLabel = option?.label && parseFloat(option.label);
×
NEW
127
        const newZoom = option?.zoom && parseFloat(option.zoom);
×
128

NEW
129
        const baseResolutions  = disableScaleLockingParms?.resolutions || getResolutions(this.props.disableScaleLockingParms?.projection);
×
NEW
130
        const currentResolutions = [...baseResolutions];
×
NEW
131
        const correspondentResolution = this.getResolutionByScale(newScale, baseResolutions );
×
132
        // 1. Validate if newResolution is a valid number greater than 0
NEW
133
        if (typeof correspondentResolution === 'number' && correspondentResolution > 0) {
×
134
            // 2. Check if the resolution already exists in the array
NEW
135
            const resolutionSet = new Set(baseResolutions );
×
NEW
136
            if (!resolutionSet.has(correspondentResolution)) {
×
137
                // 3. Add the new resolution to the array
NEW
138
                currentResolutions .push(correspondentResolution);
×
139
                // 4. Sort the array to maintain order
NEW
140
                currentResolutions .sort((a, b) => b - a); // Sorts numerically in ascending order
×
141
            }
142
        }
NEW
143
        const corresZoom = getExactZoomFromResolution(correspondentResolution, currentResolutions) ?? 0;
×
NEW
144
        const roundedZoom = parseFloat(corresZoom.toFixed(2));
×
145
        // new created option
NEW
146
        if (newScale === newLabel && newScale && !newZoom) {
×
NEW
147
            this.handleUpdateScales({
×
148
                value: newScale, zoom: roundedZoom
149
            });
NEW
150
            this.props.onChange(roundedZoom, newScale, correspondentResolution, currentResolutions);
×
151
            // add this resolution to the resolutions list of map viewer
NEW
152
            return;
×
153
        }
NEW
154
        const scaleLevelToSet = isNaN(newScale) ? undefined : newScale;
×
NEW
155
        const zoomLevelToSet = isNaN(newZoom) ? undefined : newZoom;
×
NEW
156
        const selectedZoomLvl = this.state.scales.find( sc => sc.value === scaleLevelToSet) ||
×
157
                            this.props.scales[this.props.currentZoomLvl];
NEW
158
        this.props.onChange(zoomLevelToSet, selectedZoomLvl?.value, correspondentResolution, currentResolutions);
×
159
    }
160
    render() {
161
        let control = null;
19✔
162
        let currentZoomLvl = Math.round(this.props.currentZoomLvl);
19✔
163
        const {disableScaleLockingParms} = this.props;
19✔
164
        if (this.props.readOnly) {
19✔
165
            control =
2✔
166
                <label>{this.props.template(this.props.scales[currentZoomLvl], currentZoomLvl)}</label>
167
            ;
168
        } else if (this.props.useRawInput) {
17!
169
            control =
×
170
                (<select label={this.props.label} onChange={this.onComboChange} bsSize="small" value={currentZoomLvl || ""}>
×
171
                    {this.getOptions()}
172
                </select>)
173
            ;
174
        } else if (disableScaleLockingParms?.editScale) {
17!
NEW
175
            const {scales} = this.state;
×
NEW
176
            currentZoomLvl = disableScaleLockingParms?.resolution ? this.getZoomLevelByResolution(disableScaleLockingParms?.resolution) : Math.round(this.props.currentZoomLvl) > (scales.length - 1) ? (scales.length - 1) : Math.round(this.props.currentZoomLvl);
×
177

NEW
178
            control =
×
179
                (<Form inline><FormGroup bsSize="small">
180
                    <ReactSelectCreatable
181
                        id="scaleBox"
182
                        className="scale-box-create-select"
183
                        value={currentZoomLvl}
NEW
184
                        options={scales.map((item) => ({scale: item.value, zoom: item.zoom, value: item.zoom, label: this.props.template(item.value, item.zoom)}))}
×
185
                        promptTextCreator={(label) => {
NEW
186
                            return <Message msgId={"print.createScaleOption"} msgParams={{ label }}/>;
×
187

188
                        }}
189
                        isValidNewOption={(option) => {
NEW
190
                            if (option.label) {
×
NEW
191
                                const newValue = parseFloat(option.label);
×
NEW
192
                                return !isNaN(newValue) && newValue > Math.min(...(this.props.scales || [])) && newValue < Math.max(...(this.props.scales || []));
×
193
                            }
NEW
194
                            return false;
×
195
                        }}
NEW
196
                        onChange={(op) => this.handleChangeEditableScaleList(op)}
×
197
                    />
198
                </FormGroup></Form>)
199
            ;
200
        } else {
201
            control =
17✔
202
                (<Form inline><FormGroup bsSize="small">
203
                    <ControlLabel htmlFor="scaleBox">{this.props.label}</ControlLabel>
204
                    <FormControl id="scaleBox" componentClass="select" onChange={this.onComboChange} value={currentZoomLvl || ""}>
25✔
205
                        {this.getOptions()}
206
                    </FormControl>
207
                </FormGroup></Form>)
208
            ;
209
        }
210
        return (
19✔
211

212
            <div id={this.props.id} style={this.props.style}>
213
                {control}
214
            </div>
215
        );
216
    }
217
}
218

219
export default ScaleBox;
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