001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.List;
007
008import org.openstreetmap.josm.gui.NavigatableComponent;
009
010/**
011 * Represents a layer that has native scales.
012 * @author András Kolesár
013 */
014public interface NativeScaleLayer {
015
016    /**
017     * Get native scales of this layer.
018     * @return {@link ScaleList} of native scales
019     */
020    ScaleList getNativeScales();
021
022    /**
023     * Represents a scale with native flag, used in {@link ScaleList}
024     */
025    class Scale {
026        /**
027         * Scale factor, same unit as in {@link NavigatableComponent}
028         */
029        private final double scale;
030
031        /**
032         * True if this scale is native resolution for data source.
033         */
034        private final boolean isNative;
035
036        private final int index;
037
038        /**
039         * Constructs a new Scale with given scale, native defaults to true.
040         * @param scale as defined in WMTS (scaleDenominator)
041         * @param index zoom index for this scale
042         */
043        public Scale(double scale, int index) {
044            this.scale = scale;
045            this.isNative = true;
046            this.index = index;
047        }
048
049        /**
050         * Constructs a new Scale with given scale, native and index values.
051         * @param scale as defined in WMTS (scaleDenominator)
052         * @param isNative is this scale native to the source or not
053         * @param index zoom index for this scale
054         */
055        public Scale(double scale, boolean isNative, int index) {
056            this.scale = scale;
057            this.isNative = isNative;
058            this.index = index;
059        }
060
061        @Override
062        public String toString() {
063            return String.format("%f [%s]", scale, isNative);
064        }
065
066        /**
067         * Get index of this scale in a {@link ScaleList}
068         * @return index
069         */
070        public int getIndex() {
071            return index;
072        }
073
074        public double getScale() {
075            return scale;
076        }
077    }
078
079    /**
080     * List of scales, may include intermediate steps between native resolutions
081     */
082    class ScaleList {
083        private final List<Scale> scales = new ArrayList<>();
084
085        protected ScaleList() {
086        }
087
088        public ScaleList(Collection<Double> scales) {
089            int i = 0;
090            for (Double scale: scales) {
091                this.scales.add(new Scale(scale, i++));
092            }
093        }
094
095        protected void addScale(Scale scale) {
096            scales.add(scale);
097        }
098
099        /**
100         * Returns a ScaleList that has intermediate steps between native scales.
101         * Native steps are split to equal steps near given ratio.
102         * @param ratio user defined zoom ratio
103         * @return a {@link ScaleList} with intermediate steps
104         */
105        public ScaleList withIntermediateSteps(double ratio) {
106            ScaleList result = new ScaleList();
107            Scale previous = null;
108            for (Scale current: this.scales) {
109                if (previous != null) {
110                    double step = previous.scale / current.scale;
111                    double factor = Math.log(step) / Math.log(ratio);
112                    int steps = (int) Math.round(factor);
113                    if (steps != 0) {
114                        double smallStep = Math.pow(step, 1.0/steps);
115                        for (int j = 1; j < steps; j++) {
116                            double intermediate = previous.scale / Math.pow(smallStep, j);
117                            result.addScale(new Scale(intermediate, false, current.index));
118                        }
119                    }
120                }
121                result.addScale(current);
122                previous = current;
123            }
124            return result;
125        }
126
127        /**
128         * Get a scale from this ScaleList or a new scale if zoomed outside.
129         * @param scale previous scale
130         * @param floor use floor instead of round, set true when fitting view to objects
131         * @return new {@link Scale}
132         */
133        public Scale getSnapScale(double scale, boolean floor) {
134            return getSnapScale(scale, NavigatableComponent.PROP_ZOOM_RATIO.get(), floor);
135        }
136
137        /**
138         * Get a scale from this ScaleList or a new scale if zoomed outside.
139         * @param scale previous scale
140         * @param ratio zoom ratio from starting from previous scale
141         * @param floor use floor instead of round, set true when fitting view to objects
142         * @return new {@link Scale}
143         */
144        public Scale getSnapScale(double scale, double ratio, boolean floor) {
145            if (scales.isEmpty())
146                return null;
147            int size = scales.size();
148            Scale first = scales.get(0);
149            Scale last = scales.get(size-1);
150
151            if (scale > first.scale) {
152                double step = scale / first.scale;
153                double factor = Math.log(step) / Math.log(ratio);
154                int steps = (int) (floor ? Math.floor(factor) : Math.round(factor));
155                if (steps == 0) {
156                    return new Scale(first.scale, first.isNative, steps);
157                } else {
158                    return new Scale(first.scale * Math.pow(ratio, steps), false, steps);
159                }
160            } else if (scale < last.scale) {
161                double step = last.scale / scale;
162                double factor = Math.log(step) / Math.log(ratio);
163                int steps = (int) (floor ? Math.floor(factor) : Math.round(factor));
164                if (steps == 0) {
165                    return new Scale(last.scale, last.isNative, size-1+steps);
166                } else {
167                    return new Scale(last.scale / Math.pow(ratio, steps), false, size-1+steps);
168                }
169            } else {
170                Scale previous = null;
171                for (int i = 0; i < size; i++) {
172                    Scale current = this.scales.get(i);
173                    if (previous != null) {
174                        if (scale <= previous.scale && scale >= current.scale) {
175                            if (floor || previous.scale / scale < scale / current.scale) {
176                                return new Scale(previous.scale, previous.isNative, i-1);
177                            } else {
178                                return new Scale(current.scale, current.isNative, i);
179                            }
180                        }
181                    }
182                    previous = current;
183                }
184                return null;
185            }
186        }
187
188        /**
189         * Get new scale for zoom in/out with a ratio at a number of times.
190         * Used by mousewheel zoom where wheel can step more than one between events.
191         * @param scale previois scale
192         * @param ratio user defined zoom ratio
193         * @param times number of times to zoom
194         * @return new {@link Scale} object from {@link ScaleList} or outside
195         */
196        public Scale scaleZoomTimes(double scale, double ratio, int times) {
197            Scale next = getSnapScale(scale, ratio, false);
198            int abs = Math.abs(times);
199            for (int i = 0; i < abs; i++) {
200                if (times < 0) {
201                    next = getNextIn(next, ratio);
202                } else {
203                    next = getNextOut(next, ratio);
204                }
205            }
206            return next;
207        }
208
209        /**
210         * Get new scale for zoom in.
211         * @param scale previous scale
212         * @param ratio user defined zoom ratio
213         * @return next scale in list or a new scale when zoomed outside
214         */
215        public Scale scaleZoomIn(double scale, double ratio) {
216            Scale snap = getSnapScale(scale, ratio, false);
217            return getNextIn(snap, ratio);
218        }
219
220        /**
221         * Get new scale for zoom out.
222         * @param scale previous scale
223         * @param ratio user defined zoom ratio
224         * @return next scale in list or a new scale when zoomed outside
225         */
226        public Scale scaleZoomOut(double scale, double ratio) {
227            Scale snap = getSnapScale(scale, ratio, false);
228            return getNextOut(snap, ratio);
229        }
230
231        @Override
232        public String toString() {
233            StringBuilder stringBuilder = new StringBuilder();
234            for (Scale s: this.scales) {
235                stringBuilder.append(s.toString() + '\n');
236            }
237            return stringBuilder.toString();
238        }
239
240        private Scale getNextIn(Scale scale, double ratio) {
241            if (scale == null)
242                return null;
243            int nextIndex = scale.getIndex() + 1;
244            if (nextIndex <= 0 || nextIndex > this.scales.size()-1) {
245                return new Scale(scale.scale / ratio, nextIndex == 0, nextIndex);
246            } else {
247                Scale nextScale = this.scales.get(nextIndex);
248                return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
249            }
250        }
251
252        private Scale getNextOut(Scale scale, double ratio) {
253            if (scale == null)
254                return null;
255            int nextIndex = scale.getIndex() - 1;
256            if (nextIndex < 0 || nextIndex >= this.scales.size()-1) {
257                return new Scale(scale.scale * ratio, nextIndex == this.scales.size()-1, nextIndex);
258            } else {
259                Scale nextScale = this.scales.get(nextIndex);
260                return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
261            }
262        }
263    }
264}