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) { 177 if (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 } 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 previois 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 StringBuilder stringBuilder = new StringBuilder(); 237 for (Scale s: this.scales) { 238 stringBuilder.append(s.toString() + '\n'); 239 } 240 return stringBuilder.toString(); 241 } 242 243 private Scale getNextIn(Scale scale, double ratio) { 244 if (scale == null) 245 return null; 246 int nextIndex = scale.getIndex() + 1; 247 if (nextIndex <= 0 || nextIndex > this.scales.size()-1) { 248 return new Scale(scale.scale / ratio, nextIndex == 0, nextIndex); 249 } else { 250 Scale nextScale = this.scales.get(nextIndex); 251 return new Scale(nextScale.scale, nextScale.isNative, nextIndex); 252 } 253 } 254 255 private Scale getNextOut(Scale scale, double ratio) { 256 if (scale == null) 257 return null; 258 int nextIndex = scale.getIndex() - 1; 259 if (nextIndex < 0 || nextIndex >= this.scales.size()-1) { 260 return new Scale(scale.scale * ratio, nextIndex == this.scales.size()-1, nextIndex); 261 } else { 262 Scale nextScale = this.scales.get(nextIndex); 263 return new Scale(nextScale.scale, nextScale.isNative, nextIndex); 264 } 265 } 266 } 267}