001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.imagery; 003 004import java.util.HashMap; 005import java.util.Locale; 006import java.util.Map; 007import java.util.concurrent.CopyOnWriteArrayList; 008 009import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 010import org.openstreetmap.josm.data.coor.EastNorth; 011import org.openstreetmap.josm.data.imagery.OffsetBookmark; 012import org.openstreetmap.josm.data.preferences.BooleanProperty; 013import org.openstreetmap.josm.data.projection.ProjectionRegistry; 014import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 015import org.openstreetmap.josm.io.session.SessionAwareReadApply; 016import org.openstreetmap.josm.spi.preferences.Config; 017import org.openstreetmap.josm.tools.CheckParameterUtil; 018import org.openstreetmap.josm.tools.JosmRuntimeException; 019import org.openstreetmap.josm.tools.bugreport.BugReport; 020 021/** 022 * This are the preferences of how to display a {@link TileSource}. 023 * <p> 024 * They have been extracted from the {@link AbstractTileSourceLayer}. Each layer has one set of such settings. 025 * @author michael 026 * @since 10568 027 */ 028public class TileSourceDisplaySettings implements SessionAwareReadApply { 029 /** 030 * A string returned by {@link DisplaySettingsChangeEvent#getChangedSetting()} if auto load was changed. 031 * @see TileSourceDisplaySettings#isAutoLoad() 032 */ 033 public static final String AUTO_LOAD = "automatic-downloading"; 034 035 /** 036 * A string returned by {@link DisplaySettingsChangeEvent#getChangedSetting()} if auto zoom was changed. 037 * @see TileSourceDisplaySettings#isAutoZoom() 038 */ 039 public static final String AUTO_ZOOM = "automatically-change-resolution"; 040 041 /** 042 * A string returned by {@link DisplaySettingsChangeEvent#getChangedSetting()} if the show errors property was changed. 043 * @see TileSourceDisplaySettings#isShowErrors() 044 */ 045 private static final String SHOW_ERRORS = "show-errors"; 046 047 private static final String DISPLACEMENT = "displacement"; 048 049 private static final String PREFERENCE_PREFIX = "imagery.generic"; 050 051 /** 052 * The default auto load property 053 */ 054 public static final BooleanProperty PROP_AUTO_LOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true); 055 056 /** 057 * The default auto zoom property 058 */ 059 public static final BooleanProperty PROP_AUTO_ZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true); 060 061 062 /** if layers changes automatically, when user zooms in */ 063 private boolean autoZoom; 064 /** if layer automatically loads new tiles */ 065 private boolean autoLoad; 066 /** if layer should show errors on tiles */ 067 private boolean showErrors; 068 069 private OffsetBookmark previousOffsetBookmark; 070 private OffsetBookmark offsetBookmark; 071 /** 072 * the displacement (basically caches the displacement from the offsetBookmark 073 * in the current projection) 074 */ 075 private EastNorth displacement = EastNorth.ZERO; 076 077 private final CopyOnWriteArrayList<DisplaySettingsChangeListener> listeners = new CopyOnWriteArrayList<>(); 078 079 /** 080 * Create a new {@link TileSourceDisplaySettings} 081 */ 082 public TileSourceDisplaySettings() { 083 this(new String[] {PREFERENCE_PREFIX}); 084 } 085 086 /** 087 * Create a new {@link TileSourceDisplaySettings} 088 * @param preferencePrefix The additional prefix to scan for preferences. 089 */ 090 public TileSourceDisplaySettings(String preferencePrefix) { 091 this(PREFERENCE_PREFIX, preferencePrefix); 092 } 093 094 private TileSourceDisplaySettings(String... prefixes) { 095 autoZoom = getProperty(prefixes, "default_autozoom", PROP_AUTO_ZOOM.getDefaultValue()); 096 autoLoad = getProperty(prefixes, "default_autoload", PROP_AUTO_LOAD.getDefaultValue()); 097 showErrors = getProperty(prefixes, "default_showerrors", Boolean.TRUE); 098 } 099 100 private static boolean getProperty(String[] prefixes, String name, Boolean def) { 101 // iterate through all values to force the preferences to receive the default value. 102 // we only support a default value of true. 103 boolean value = true; 104 for (String p : prefixes) { 105 String key = p + "." + name; 106 boolean currentValue = Config.getPref().getBoolean(key, true); 107 if (!Config.getPref().get(key, def.toString()).isEmpty()) { 108 value = currentValue; 109 } 110 } 111 return value; 112 } 113 114 /** 115 * Let the layer zoom automatically if the user zooms in 116 * @return auto zoom 117 */ 118 public boolean isAutoZoom() { 119 return autoZoom; 120 } 121 122 /** 123 * Sets the auto zoom property 124 * @param autoZoom {@code true} to let the layer zoom automatically if the user zooms in 125 * @see #isAutoZoom() 126 * @see #AUTO_ZOOM 127 */ 128 public void setAutoZoom(boolean autoZoom) { 129 this.autoZoom = autoZoom; 130 fireSettingsChange(AUTO_ZOOM); 131 } 132 133 /** 134 * Gets if the layer should automatically load new tiles. 135 * @return <code>true</code> if it should 136 */ 137 public boolean isAutoLoad() { 138 return autoLoad; 139 } 140 141 /** 142 * Sets the auto load property 143 * @param autoLoad {@code true} if the layer should automatically load new tiles 144 * @see #isAutoLoad() 145 * @see #AUTO_LOAD 146 */ 147 public void setAutoLoad(boolean autoLoad) { 148 this.autoLoad = autoLoad; 149 fireSettingsChange(AUTO_LOAD); 150 } 151 152 /** 153 * If the layer should display the errors it encountered while loading the tiles. 154 * @return <code>true</code> to show errors. 155 */ 156 public boolean isShowErrors() { 157 return showErrors; 158 } 159 160 /** 161 * Sets the show errors property. Fires a change event. 162 * @param showErrors {@code true} if the layer should display the errors it encountered while loading the tiles 163 * @see #isShowErrors() 164 * @see #SHOW_ERRORS 165 */ 166 public void setShowErrors(boolean showErrors) { 167 this.showErrors = showErrors; 168 fireSettingsChange(SHOW_ERRORS); 169 } 170 171 /** 172 * Gets the displacement in x (east) direction 173 * @return The displacement. 174 * @see #getDisplacement() 175 * @since 10571 176 */ 177 public double getDx() { 178 return getDisplacement().east(); 179 } 180 181 /** 182 * Gets the displacement in y (north) direction 183 * @return The displacement. 184 * @see #getDisplacement() 185 * @since 10571 186 */ 187 public double getDy() { 188 return getDisplacement().north(); 189 } 190 191 /** 192 * Gets the displacement of the image 193 * @return The displacement. 194 * @since 10571 195 */ 196 public EastNorth getDisplacement() { 197 return displacement; 198 } 199 200 /** 201 * Gets the displacement of the image formatted as a string 202 * @param locale the locale used to format the decimals 203 * @return the displacement string 204 * @see #getDisplacement() 205 * @since 15733 206 */ 207 public String getDisplacementString(final Locale locale) { 208 // Support projections with very small numbers (e.g. 4326) 209 int precision = ProjectionRegistry.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7; 210 return String.format(locale, "%1." + precision + "f; %1." + precision + "f", getDx(), getDy()); 211 } 212 213 /** 214 * Sets an offset bookmark to use. Loads the displacement from the bookmark. 215 * 216 * @param offsetBookmark the offset bookmark, may be null 217 */ 218 public void setOffsetBookmark(OffsetBookmark offsetBookmark) { 219 if (this.offsetBookmark != null) { 220 this.previousOffsetBookmark = this.offsetBookmark; 221 } 222 this.offsetBookmark = offsetBookmark; 223 if (offsetBookmark == null) { 224 setDisplacement(EastNorth.ZERO); 225 } else { 226 setDisplacement(offsetBookmark.getDisplacement(ProjectionRegistry.getProjection())); 227 } 228 } 229 230 /** 231 * Gets the offset bookmark in use. 232 * @return the offset bookmark, may be null 233 */ 234 public OffsetBookmark getOffsetBookmark() { 235 return this.offsetBookmark; 236 } 237 238 /** 239 * Gets the offset bookmark previously in use. 240 * @return the previously used offset bookmark, may be null 241 */ 242 public OffsetBookmark getPreviousOffsetBookmark() { 243 return previousOffsetBookmark; 244 } 245 246 private void setDisplacement(EastNorth displacement) { 247 CheckParameterUtil.ensure(displacement, "displacement", EastNorth::isValid); 248 this.displacement = displacement; 249 fireSettingsChange(DISPLACEMENT); 250 } 251 252 /** 253 * Notifies all listeners that the paint settings have changed 254 * @param changedSetting The setting name 255 */ 256 private void fireSettingsChange(String changedSetting) { 257 DisplaySettingsChangeEvent e = new DisplaySettingsChangeEvent(changedSetting); 258 for (DisplaySettingsChangeListener l : listeners) { 259 l.displaySettingsChanged(e); 260 } 261 } 262 263 /** 264 * Add a listener that listens to display settings changes. 265 * @param l The listener 266 */ 267 public void addSettingsChangeListener(DisplaySettingsChangeListener l) { 268 listeners.add(l); 269 } 270 271 /** 272 * Remove a listener that listens to display settings changes. 273 * @param l The listener 274 */ 275 public void removeSettingsChangeListener(DisplaySettingsChangeListener l) { 276 listeners.remove(l); 277 } 278 279 /** 280 * Stores the current settings object to the given hashmap. 281 * The offset data is not stored and needs to be handled separately. 282 * @see #applyFromPropertiesMap(Map) 283 * @see OffsetBookmark#toPropertiesMap() 284 */ 285 @Override 286 public Map<String, String> toPropertiesMap() { 287 Map<String, String> data = new HashMap<>(); 288 data.put(AUTO_LOAD, Boolean.toString(autoLoad)); 289 data.put(AUTO_ZOOM, Boolean.toString(autoZoom)); 290 data.put(SHOW_ERRORS, Boolean.toString(showErrors)); 291 return data; 292 } 293 294 /** 295 * Load the settings from the given data instance. 296 * The offset data is not loaded and needs to be handled separately. 297 * @param data The data 298 * @see #toPropertiesMap() 299 * @see OffsetBookmark#fromPropertiesMap(java.util.Map) 300 */ 301 @Override 302 public void applyFromPropertiesMap(Map<String, String> data) { 303 try { 304 String doAutoLoad = data.get(AUTO_LOAD); 305 if (doAutoLoad != null) { 306 setAutoLoad(Boolean.parseBoolean(doAutoLoad)); 307 } 308 309 String doAutoZoom = data.get(AUTO_ZOOM); 310 if (doAutoZoom != null) { 311 setAutoZoom(Boolean.parseBoolean(doAutoZoom)); 312 } 313 314 String doShowErrors = data.get(SHOW_ERRORS); 315 if (doShowErrors != null) { 316 setShowErrors(Boolean.parseBoolean(doShowErrors)); 317 } 318 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 319 throw BugReport.intercept(e).put("data", data); 320 } 321 } 322 323 @Override 324 public int hashCode() { 325 final int prime = 31; 326 int result = 1; 327 result = prime * result + (autoLoad ? 1231 : 1237); 328 result = prime * result + (autoZoom ? 1231 : 1237); 329 result = prime * result + (showErrors ? 1231 : 1237); 330 return result; 331 } 332 333 @Override 334 public boolean equals(Object obj) { 335 if (this == obj) 336 return true; 337 if (obj == null || getClass() != obj.getClass()) 338 return false; 339 TileSourceDisplaySettings other = (TileSourceDisplaySettings) obj; 340 return autoLoad == other.autoLoad 341 && autoZoom == other.autoZoom 342 && showErrors == other.showErrors; 343 } 344 345 @Override 346 public String toString() { 347 return "TileSourceDisplaySettings [autoZoom=" + autoZoom + ", autoLoad=" + autoLoad + ", showErrors=" 348 + showErrors + ']'; 349 } 350 351 /** 352 * A listener that listens to changes to the {@link TileSourceDisplaySettings} object. 353 * @author Michael Zangl 354 * @since 10600 (functional interface) 355 */ 356 @FunctionalInterface 357 public interface DisplaySettingsChangeListener { 358 /** 359 * Called whenever the display settings have changed. 360 * @param e The change event. 361 */ 362 void displaySettingsChanged(DisplaySettingsChangeEvent e); 363 } 364 365 /** 366 * An event that is created whenever the display settings change. 367 * @author Michael Zangl 368 */ 369 public static final class DisplaySettingsChangeEvent { 370 private final String changedSetting; 371 372 DisplaySettingsChangeEvent(String changedSetting) { 373 this.changedSetting = changedSetting; 374 } 375 376 /** 377 * Gets the setting that was changed 378 * @return The name of the changed setting. 379 */ 380 public String getChangedSetting() { 381 return changedSetting; 382 } 383 384 @Override 385 public String toString() { 386 return "DisplaySettingsChangeEvent [changedSetting=" + changedSetting + ']'; 387 } 388 } 389}