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