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