001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.List;
010import java.util.ListIterator;
011import java.util.Map;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.data.StructUtils;
015import org.openstreetmap.josm.data.StructUtils.StructEntry;
016import org.openstreetmap.josm.data.StructUtils.WriteExplicitly;
017import org.openstreetmap.josm.data.coor.EastNorth;
018import org.openstreetmap.josm.data.coor.ILatLon;
019import org.openstreetmap.josm.data.coor.LatLon;
020import org.openstreetmap.josm.data.projection.Projection;
021import org.openstreetmap.josm.data.projection.Projections;
022import org.openstreetmap.josm.gui.MainApplication;
023import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
024import org.openstreetmap.josm.gui.layer.ImageryLayer;
025import org.openstreetmap.josm.spi.preferences.Config;
026import org.openstreetmap.josm.tools.Logging;
027
028/**
029 * Class to save a displacement of background imagery as a bookmark.
030 *
031 * Known offset bookmarks will be stored in the preferences and can be
032 * restored by the user in later sessions.
033 */
034public class OffsetBookmark {
035    private static final List<OffsetBookmark> allBookmarks = new ArrayList<>();
036
037    @StructEntry private String projection_code;
038    @StructEntry private String imagery_name;
039    @StructEntry private String name;
040    @StructEntry @WriteExplicitly private double dx, dy;
041    @StructEntry private double center_lon, center_lat;
042
043    /**
044     * Test if an image is usable for the given imagery layer.
045     * @param layer The layer to use the image at
046     * @return <code>true</code> if it is usable on the projection of the layer and the imagery name matches.
047     */
048    public boolean isUsable(ImageryLayer layer) {
049        if (projection_code == null) return false;
050        if (!Main.getProjection().toCode().equals(projection_code) && !hasCenter()) return false;
051        return layer.getInfo().getName().equals(imagery_name);
052    }
053
054    /**
055     * Construct new empty OffsetBookmark.
056     *
057     * Only used for preferences handling.
058     */
059    public OffsetBookmark() {
060        // do nothing
061    }
062
063    /**
064     * Create a new {@link OffsetBookmark} object using (0, 0) as center
065     * <p>
066     * The use of the {@link #OffsetBookmark(String, String, String, EastNorth, ILatLon)} constructor is preferred.
067     * @param projectionCode The projection for which this object was created
068     * @param imageryName The name of the imagery on the layer
069     * @param name The name of the new bookmark
070     * @param dx The x displacement
071     * @param dy The y displacement
072     */
073    public OffsetBookmark(String projectionCode, String imageryName, String name, double dx, double dy) {
074        this(projectionCode, imageryName, name, dx, dy, 0, 0);
075    }
076
077    /**
078     * Create a new {@link OffsetBookmark} object
079     * @param projectionCode The projection for which this object was created
080     * @param imageryName The name of the imagery on the layer
081     * @param name The name of the new bookmark
082     * @param displacement The displacement in east/north space.
083     * @param center The point on earth that was used as reference to align the image.
084     * @since 13243
085     */
086    public OffsetBookmark(String projectionCode, String imageryName, String name, EastNorth displacement, ILatLon center) {
087        this(projectionCode, imageryName, name, displacement.east(), displacement.north(), center.lon(), center.lat());
088    }
089
090    /**
091     * Create a new {@link OffsetBookmark} by specifying all values.
092     * <p>
093     * The use of the {@link #OffsetBookmark(String, String, String, EastNorth, ILatLon)} constructor is preferred.
094     * @param projectionCode The projection for which this object was created
095     * @param imageryName The name of the imagery on the layer
096     * @param name The name of the new bookmark
097     * @param dx The x displacement
098     * @param dy The y displacement
099     * @param centerLon The point on earth that was used as reference to align the image.
100     * @param centerLat The point on earth that was used as reference to align the image.
101     */
102    public OffsetBookmark(String projectionCode, String imageryName, String name, double dx, double dy, double centerLon, double centerLat) {
103        this.projection_code = projectionCode;
104        this.imagery_name = imageryName;
105        this.name = name;
106        this.dx = dx;
107        this.dy = dy;
108        this.center_lon = centerLon;
109        this.center_lat = centerLat;
110    }
111
112    /**
113     * Loads an old bookmark. For backward compatibility with settings. Do not use.
114     * @param list The settings that were read
115     */
116    public OffsetBookmark(Collection<String> list) {
117        List<String> array = new ArrayList<>(list);
118        this.projection_code = array.get(0);
119        this.imagery_name = array.get(1);
120        this.name = array.get(2);
121        this.dx = Double.parseDouble(array.get(3));
122        this.dy = Double.parseDouble(array.get(4));
123        if (array.size() >= 7) {
124            this.center_lon = Double.parseDouble(array.get(5));
125            this.center_lat = Double.parseDouble(array.get(6));
126        }
127        if (projection_code == null) {
128            Logging.error(tr("Projection ''{0}'' is not found, bookmark ''{1}'' is not usable", projection_code, name));
129        }
130    }
131
132    /**
133     * Get the projection code for which this bookmark was created.
134     * @return The projection.
135     */
136    public String getProjectionCode() {
137        return projection_code;
138    }
139
140    /**
141     * Get the name of this bookmark. This name can e.g. be displayed in menus.
142     * @return The name
143     */
144    public String getName() {
145        return name;
146    }
147
148    /**
149     * Get the name of the imagery for which this bookmark was created. It is used to match the bookmark to the right layers.
150     * @return The name
151     */
152    public String getImageryName() {
153        return imagery_name;
154    }
155
156    /**
157     * Get displacement in EastNorth coordinates of the original projection.
158     *
159     * @return the displacement
160     * @see #getProjectionCode()
161     */
162    public EastNorth getDisplacement() {
163        return new EastNorth(dx, dy);
164    }
165
166    /**
167     * Get displacement in EastNorth coordinates of a given projection.
168     *
169     * Displacement will be converted to the given projection, with respect to the
170     * center (reference point) of this bookmark.
171     * @param proj the projection
172     * @return the displacement, converted to that projection
173     */
174    public EastNorth getDisplacement(Projection proj) {
175        if (proj.toCode().equals(projection_code)) {
176            return getDisplacement();
177        }
178        LatLon center = getCenter();
179        Projection offsetProj = Projections.getProjectionByCode(projection_code);
180        EastNorth centerEN = center.getEastNorth(offsetProj);
181        EastNorth shiftedEN = centerEN.add(getDisplacement());
182        LatLon shifted = offsetProj.eastNorth2latlon(shiftedEN);
183        EastNorth centerEN2 = center.getEastNorth(proj);
184        EastNorth shiftedEN2 = shifted.getEastNorth(proj);
185        return shiftedEN2.subtract(centerEN2);
186    }
187
188    /**
189     * Get center/reference point of the bookmark.
190     *
191     * Basically this is the place where it was created and is valid.
192     * The center may be unrecorded (see {@link #hasCenter()}, in which
193     * case a dummy center (0,0) will be returned.
194     * @return the center
195     */
196    public LatLon getCenter() {
197        return new LatLon(center_lat, center_lon);
198    }
199
200    /**
201     * Check if bookmark has a valid center.
202     * @return true if bookmark has a valid center
203     */
204    public boolean hasCenter() {
205        return center_lat != 0 || center_lon != 0;
206    }
207
208    /**
209     * Set the projection code for which this bookmark was created
210     * @param projectionCode The projection
211     */
212    public void setProjectionCode(String projectionCode) {
213        this.projection_code = projectionCode;
214    }
215
216    /**
217     * Set the name of the bookmark
218     * @param name The name
219     * @see #getName()
220     */
221    public void setName(String name) {
222        this.name = name;
223    }
224
225    /**
226     * Sets the name of the imagery
227     * @param imageryName The name
228     * @see #getImageryName()
229     */
230    public void setImageryName(String imageryName) {
231        this.imagery_name = imageryName;
232    }
233
234    /**
235     * Update the displacement of this imagery.
236     * @param displacement The displacement
237     */
238    public void setDisplacement(EastNorth displacement) {
239        this.dx = displacement.east();
240        this.dy = displacement.north();
241    }
242
243    /**
244     * Load the global list of bookmarks from preferences.
245     */
246    public static void loadBookmarks() {
247        List<OffsetBookmark> bookmarks = StructUtils.getListOfStructs(
248                Config.getPref(), "imagery.offsetbookmarks", null, OffsetBookmark.class);
249        if (bookmarks == null) {
250            loadBookmarksOld();
251            saveBookmarks();
252        } else {
253            allBookmarks.addAll(bookmarks);
254        }
255    }
256
257    // migration code - remove Nov. 2017
258    private static void loadBookmarksOld() {
259        for (Collection<String> c : Config.getPref().getListOfLists("imagery.offsets")) {
260            allBookmarks.add(new OffsetBookmark(c));
261        }
262    }
263
264    /**
265     * Stores the bookmakrs in the settings.
266     */
267    public static void saveBookmarks() {
268        StructUtils.putListOfStructs(Config.getPref(), "imagery.offsetbookmarks", allBookmarks, OffsetBookmark.class);
269    }
270
271    /**
272     * Returns all bookmarks.
273     * @return all bookmarks (unmodifiable collection)
274     * @since 11651
275     */
276    public static List<OffsetBookmark> getBookmarks() {
277        return Collections.unmodifiableList(allBookmarks);
278    }
279
280    /**
281     * Returns the number of bookmarks.
282     * @return the number of bookmarks
283     * @since 11651
284     */
285    public static int getBookmarksSize() {
286        return allBookmarks.size();
287    }
288
289    /**
290     * Adds a bookmark.
291     * @param ob bookmark to add
292     * @return {@code true}
293     * @since 11651
294     */
295    public static boolean addBookmark(OffsetBookmark ob) {
296        return allBookmarks.add(ob);
297    }
298
299    /**
300     * Removes a bookmark.
301     * @param ob bookmark to remove
302     * @return {@code true} if this list contained the specified element
303     * @since 11651
304     */
305    public static boolean removeBookmark(OffsetBookmark ob) {
306        return allBookmarks.remove(ob);
307    }
308
309    /**
310     * Returns the bookmark at the given index.
311     * @param index bookmark index
312     * @return the bookmark at the given index
313     * @throws IndexOutOfBoundsException if the index is out of range
314     *         (<code>index &lt; 0 || index &gt;= size()</code>)
315     * @since 11651
316     */
317    public static OffsetBookmark getBookmarkByIndex(int index) {
318        return allBookmarks.get(index);
319    }
320
321    /**
322     * Gets a bookmark that is usable on the given layer by it's name.
323     * @param layer The layer to use the bookmark at
324     * @param name The name of the bookmark
325     * @return The bookmark if found, <code>null</code> if not.
326     */
327    public static OffsetBookmark getBookmarkByName(ImageryLayer layer, String name) {
328        for (OffsetBookmark b : allBookmarks) {
329            if (b.isUsable(layer) && name.equals(b.name))
330                return b;
331        }
332        return null;
333    }
334
335    /**
336     * Add a bookmark for the displacement of that layer
337     * @param name The bookmark name
338     * @param layer The layer to store the bookmark for
339     */
340    public static void bookmarkOffset(String name, AbstractTileSourceLayer<?> layer) {
341        LatLon center;
342        if (MainApplication.isDisplayingMapView()) {
343            center = Main.getProjection().eastNorth2latlon(MainApplication.getMap().mapView.getCenter());
344        } else {
345            center = LatLon.ZERO;
346        }
347        OffsetBookmark nb = new OffsetBookmark(
348                Main.getProjection().toCode(), layer.getInfo().getName(),
349                name, layer.getDisplaySettings().getDisplacement(), center);
350        for (ListIterator<OffsetBookmark> it = allBookmarks.listIterator(); it.hasNext();) {
351            OffsetBookmark b = it.next();
352            if (b.isUsable(layer) && name.equals(b.name)) {
353                it.set(nb);
354                saveBookmarks();
355                return;
356            }
357        }
358        allBookmarks.add(nb);
359        saveBookmarks();
360    }
361
362    /**
363     * Converts the offset bookmark to a properties map.
364     *
365     * The map contains all the information to restore the offset bookmark.
366     * @return properties map of all data
367     * @see #fromPropertiesMap(java.util.Map)
368     * @since 12134
369     */
370    public Map<String, String> toPropertiesMap() {
371        return StructUtils.serializeStruct(this, OffsetBookmark.class);
372    }
373
374    /**
375     * Creates an offset bookmark from a properties map.
376     * @param properties the properties map
377     * @return corresponding offset bookmark
378     * @see #toPropertiesMap()
379     * @since 12134
380     */
381    public static OffsetBookmark fromPropertiesMap(Map<String, String> properties) {
382        return StructUtils.deserializeStruct(properties, OffsetBookmark.class);
383    }
384}