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