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}