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 double scale;
030
031        /**
032         * True if this scale is native resolution for data source.
033         */
034        private boolean isNative;
035
036        private 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
081     * between native resolutions
082     */
083    class ScaleList  {
084        private List<Scale> scales = new ArrayList<>();
085
086        protected ScaleList(double[] scales) {
087            for (int i = 0; i < scales.length; i++) {
088                this.scales.add(new Scale(scales[i], i));
089            }
090        }
091
092        protected ScaleList() {
093        }
094
095        public ScaleList(Collection<Double> scales) {
096            int i = 0;
097            for (Double scale: scales) {
098                this.scales.add(new Scale(scale, i++));
099            }
100        }
101
102        protected void addScale(Scale scale) {
103            scales.add(scale);
104        }
105
106        /**
107         * Returns a ScaleList that has intermediate steps between native scales.
108         * Native steps are split to equal steps near given ratio.
109         * @param ratio user defined zoom ratio
110         * @return a {@link ScaleList} with intermediate steps
111         */
112        public ScaleList withIntermediateSteps(double ratio) {
113            ScaleList result = new ScaleList();
114            Scale previous = null;
115            for (Scale current: this.scales) {
116                if (previous != null) {
117                    double step = previous.scale / current.scale;
118                    double factor = Math.log(step) / Math.log(ratio);
119                    int steps = (int) Math.round(factor);
120                    if (steps != 0) {
121                        double smallStep = Math.pow(step, 1.0/steps);
122                        for (int j = 1; j < steps; j++) {
123                            double intermediate = previous.scale / Math.pow(smallStep, j);
124                            result.addScale(new Scale(intermediate, false, current.index));
125                        }
126                    }
127                }
128                result.addScale(current);
129                previous = current;
130            }
131            return result;
132        }
133
134        /**
135         * Get a scale from this ScaleList or a new scale if zoomed outside.
136         * @param scale previous scale
137         * @param floor use floor instead of round, set true when fitting view to objects
138         * @return new {@link Scale}
139         */
140        public Scale getSnapScale(double scale, boolean floor) {
141            return getSnapScale(scale, NavigatableComponent.PROP_ZOOM_RATIO.get(), floor);
142        }
143
144        /**
145         * Get a scale from this ScaleList or a new scale if zoomed outside.
146         * @param scale previous scale
147         * @param ratio zoom ratio from starting from previous scale
148         * @param floor use floor instead of round, set true when fitting view to objects
149         * @return new {@link Scale}
150         */
151        public Scale getSnapScale(double scale, double ratio, boolean floor) {
152            if (scales.isEmpty())
153                return null;
154            int size = scales.size();
155            Scale first = scales.get(0);
156            Scale last = scales.get(size-1);
157
158            if (scale > first.scale) {
159                double step = scale / first.scale;
160                double factor = Math.log(step) / Math.log(ratio);
161                int steps = (int) (floor ? Math.floor(factor) : Math.round(factor));
162                if (steps == 0) {
163                    return new Scale(first.scale, first.isNative, steps);
164                } else {
165                    return new Scale(first.scale * Math.pow(ratio, steps), false, steps);
166                }
167            } else if (scale < last.scale) {
168                double step = last.scale / scale;
169                double factor = Math.log(step) / Math.log(ratio);
170                int steps = (int) (floor ? Math.floor(factor) : Math.round(factor));
171                if (steps == 0) {
172                    return new Scale(last.scale, last.isNative, size-1+steps);
173                } else {
174                    return new Scale(last.scale / Math.pow(ratio, steps), false, size-1+steps);
175                }
176            } else {
177                Scale previous = null;
178                for (int i = 0; i < size; i++) {
179                    Scale current = this.scales.get(i);
180                    if (previous != null) {
181                        if (scale <= previous.scale && scale >= current.scale) {
182                            if (floor || previous.scale / scale < scale / current.scale) {
183                                return new Scale(previous.scale, previous.isNative, i-1);
184                            } else {
185                                return new Scale(current.scale, current.isNative, i);
186                            }
187                        }
188                    }
189                    previous = current;
190                }
191                return null;
192            }
193        }
194
195        /**
196         * Get new scale for zoom in/out with a ratio at a number of times.
197         * Used by mousewheel zoom where wheel can step more than one between events.
198         * @param scale previois scale
199         * @param ratio user defined zoom ratio
200         * @param times number of times to zoom
201         * @return new {@link Scale} object from {@link ScaleList} or outside
202         */
203        public Scale scaleZoomTimes(double scale, double ratio, int times) {
204            Scale next = getSnapScale(scale, ratio, false);
205            int abs = Math.abs(times);
206            for (int i = 0; i < abs; i++) {
207                if (times < 0) {
208                    next = getNextIn(next, ratio);
209                } else {
210                    next = getNextOut(next, ratio);
211                }
212            }
213            return next;
214        }
215
216        /**
217         * Get new scale for zoom in.
218         * @param scale previous scale
219         * @param ratio user defined zoom ratio
220         * @return next scale in list or a new scale when zoomed outside
221         */
222        public Scale scaleZoomIn(double scale, double ratio) {
223            Scale snap = getSnapScale(scale, ratio, false);
224            return getNextIn(snap, ratio);
225        }
226
227        /**
228         * Get new scale for zoom out.
229         * @param scale previous scale
230         * @param ratio user defined zoom ratio
231         * @return next scale in list or a new scale when zoomed outside
232         */
233        public Scale scaleZoomOut(double scale, double ratio) {
234            Scale snap = getSnapScale(scale, ratio, false);
235            return getNextOut(snap, ratio);
236        }
237
238        @Override
239        public String toString() {
240            StringBuilder stringBuilder = new StringBuilder();
241            for (Scale s: this.scales) {
242                stringBuilder.append(s + "\n");
243            }
244            return stringBuilder.toString();
245        }
246
247        private Scale getNextIn(Scale scale, double ratio) {
248            if (scale == null)
249                return null;
250            int nextIndex = scale.getIndex() + 1;
251            if (nextIndex <= 0 || nextIndex > this.scales.size()-1) {
252                return new Scale(scale.scale / ratio, nextIndex == 0, nextIndex);
253            } else {
254                Scale nextScale = this.scales.get(nextIndex);
255                return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
256            }
257        }
258
259        private Scale getNextOut(Scale scale, double ratio) {
260            if (scale == null)
261                return null;
262            int nextIndex = scale.getIndex() - 1;
263            if (nextIndex < 0 || nextIndex >= this.scales.size()-1) {
264                return new Scale(scale.scale * ratio, nextIndex == this.scales.size()-1, nextIndex);
265            } else {
266                Scale nextScale = this.scales.get(nextIndex);
267                return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
268            }
269        }
270    }
271}