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