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}