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 && scale <= previous.scale && scale >= current.scale) { 177 if (floor || previous.scale / scale < scale / current.scale) { 178 return new Scale(previous.scale, previous.isNative, i-1); 179 } else { 180 return new Scale(current.scale, current.isNative, i); 181 } 182 } 183 previous = current; 184 } 185 return null; 186 } 187 } 188 189 /** 190 * Get new scale for zoom in/out with a ratio at a number of times. 191 * Used by mousewheel zoom where wheel can step more than one between events. 192 * @param scale previois scale 193 * @param ratio user defined zoom ratio 194 * @param times number of times to zoom 195 * @return new {@link Scale} object from {@link ScaleList} or outside 196 */ 197 public Scale scaleZoomTimes(double scale, double ratio, int times) { 198 Scale next = getSnapScale(scale, ratio, false); 199 int abs = Math.abs(times); 200 for (int i = 0; i < abs; i++) { 201 if (times < 0) { 202 next = getNextIn(next, ratio); 203 } else { 204 next = getNextOut(next, ratio); 205 } 206 } 207 return next; 208 } 209 210 /** 211 * Get new scale for zoom in. 212 * @param scale previous scale 213 * @param ratio user defined zoom ratio 214 * @return next scale in list or a new scale when zoomed outside 215 */ 216 public Scale scaleZoomIn(double scale, double ratio) { 217 Scale snap = getSnapScale(scale, ratio, false); 218 return getNextIn(snap, ratio); 219 } 220 221 /** 222 * Get new scale for zoom out. 223 * @param scale previous scale 224 * @param ratio user defined zoom ratio 225 * @return next scale in list or a new scale when zoomed outside 226 */ 227 public Scale scaleZoomOut(double scale, double ratio) { 228 Scale snap = getSnapScale(scale, ratio, false); 229 return getNextOut(snap, ratio); 230 } 231 232 @Override 233 public String toString() { 234 StringBuilder stringBuilder = new StringBuilder(); 235 for (Scale s: this.scales) { 236 stringBuilder.append(s.toString() + '\n'); 237 } 238 return stringBuilder.toString(); 239 } 240 241 private Scale getNextIn(Scale scale, double ratio) { 242 if (scale == null) 243 return null; 244 int nextIndex = scale.getIndex() + 1; 245 if (nextIndex <= 0 || nextIndex > this.scales.size()-1) { 246 return new Scale(scale.scale / ratio, nextIndex == 0, nextIndex); 247 } else { 248 Scale nextScale = this.scales.get(nextIndex); 249 return new Scale(nextScale.scale, nextScale.isNative, nextIndex); 250 } 251 } 252 253 private Scale getNextOut(Scale scale, double ratio) { 254 if (scale == null) 255 return null; 256 int nextIndex = scale.getIndex() - 1; 257 if (nextIndex < 0 || nextIndex >= this.scales.size()-1) { 258 return new Scale(scale.scale * ratio, nextIndex == this.scales.size()-1, nextIndex); 259 } else { 260 Scale nextScale = this.scales.get(nextIndex); 261 return new Scale(nextScale.scale, nextScale.isNative, nextIndex); 262 } 263 } 264 } 265}