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 < 0 || index >= 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 for (OffsetBookmark b : allBookmarks) { 351 if (b.isUsable(layer) && name.equals(b.name)) 352 return b; 353 } 354 return null; 355 } 356 357 /** 358 * Add a bookmark for the displacement of that layer 359 * @param name The bookmark name 360 * @param layer The layer to store the bookmark for 361 */ 362 public static void bookmarkOffset(String name, AbstractTileSourceLayer<?> layer) { 363 LatLon center; 364 if (MainApplication.isDisplayingMapView()) { 365 center = ProjectionRegistry.getProjection().eastNorth2latlon(MainApplication.getMap().mapView.getCenter()); 366 } else { 367 center = LatLon.ZERO; 368 } 369 OffsetBookmark nb = new OffsetBookmark( 370 ProjectionRegistry.getProjection().toCode(), layer.getInfo().getId(), layer.getInfo().getName(), 371 name, layer.getDisplaySettings().getDisplacement(), center); 372 for (ListIterator<OffsetBookmark> it = allBookmarks.listIterator(); it.hasNext();) { 373 OffsetBookmark b = it.next(); 374 if (b.isUsable(layer) && name.equals(b.name)) { 375 it.set(nb); 376 saveBookmarks(); 377 return; 378 } 379 } 380 allBookmarks.add(nb); 381 saveBookmarks(); 382 } 383 384 /** 385 * Converts the offset bookmark to a properties map. 386 * 387 * The map contains all the information to restore the offset bookmark. 388 * @return properties map of all data 389 * @see #fromPropertiesMap(java.util.Map) 390 * @since 12134 391 */ 392 public Map<String, String> toPropertiesMap() { 393 return StructUtils.serializeStruct(this, OffsetBookmark.class); 394 } 395 396 /** 397 * Creates an offset bookmark from a properties map. 398 * @param properties the properties map 399 * @return corresponding offset bookmark 400 * @see #toPropertiesMap() 401 * @since 12134 402 */ 403 public static OffsetBookmark fromPropertiesMap(Map<String, String> properties) { 404 return StructUtils.deserializeStruct(properties, OffsetBookmark.class); 405 } 406}