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}