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