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