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