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