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}