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) {
177                        if (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                    }
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 previois 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            StringBuilder stringBuilder = new StringBuilder();
237            for (Scale s: this.scales) {
238                stringBuilder.append(s.toString() + '\n');
239            }
240            return stringBuilder.toString();
241        }
242
243        private Scale getNextIn(Scale scale, double ratio) {
244            if (scale == null)
245                return null;
246            int nextIndex = scale.getIndex() + 1;
247            if (nextIndex <= 0 || nextIndex > this.scales.size()-1) {
248                return new Scale(scale.scale / ratio, nextIndex == 0, nextIndex);
249            } else {
250                Scale nextScale = this.scales.get(nextIndex);
251                return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
252            }
253        }
254
255        private Scale getNextOut(Scale scale, double ratio) {
256            if (scale == null)
257                return null;
258            int nextIndex = scale.getIndex() - 1;
259            if (nextIndex < 0 || nextIndex >= this.scales.size()-1) {
260                return new Scale(scale.scale * ratio, nextIndex == this.scales.size()-1, nextIndex);
261            } else {
262                Scale nextScale = this.scales.get(nextIndex);
263                return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
264            }
265        }
266    }
267}