001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.download; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.awt.GraphicsEnvironment; 008import java.util.ArrayList; 009import java.util.Arrays; 010import java.util.Collection; 011import java.util.Collections; 012import java.util.Comparator; 013import java.util.LinkedList; 014import java.util.List; 015import java.util.Locale; 016import java.util.Objects; 017 018import javax.swing.DefaultListModel; 019import javax.swing.ImageIcon; 020import javax.swing.JLabel; 021import javax.swing.JList; 022import javax.swing.ListCellRenderer; 023import javax.swing.UIManager; 024 025import org.openstreetmap.josm.actions.downloadtasks.ChangesetQueryTask; 026import org.openstreetmap.josm.data.Bounds; 027import org.openstreetmap.josm.data.UserIdentityManager; 028import org.openstreetmap.josm.data.coor.LatLon; 029import org.openstreetmap.josm.data.osm.Changeset; 030import org.openstreetmap.josm.data.osm.UserInfo; 031import org.openstreetmap.josm.data.preferences.IntegerProperty; 032import org.openstreetmap.josm.data.projection.Projection; 033import org.openstreetmap.josm.data.projection.Projections; 034import org.openstreetmap.josm.gui.MainApplication; 035import org.openstreetmap.josm.gui.MapViewState; 036import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetCacheManager; 037import org.openstreetmap.josm.gui.mappaint.mapcss.Selector; 038import org.openstreetmap.josm.gui.util.GuiHelper; 039import org.openstreetmap.josm.io.ChangesetQuery; 040import org.openstreetmap.josm.spi.preferences.Config; 041import org.openstreetmap.josm.tools.ImageProvider; 042import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 043import org.openstreetmap.josm.tools.Logging; 044 045/** 046 * List class that read and save its content from the bookmark file. 047 * @since 6340 048 */ 049public class BookmarkList extends JList<BookmarkList.Bookmark> { 050 051 /** 052 * The maximum number of changeset bookmarks to maintain in list. 053 * @since 12495 054 */ 055 public static final IntegerProperty MAX_CHANGESET_BOOKMARKS = new IntegerProperty("bookmarks.changesets.max-entries", 15); 056 057 /** 058 * Class holding one bookmarkentry. 059 */ 060 public static class Bookmark implements Comparable<Bookmark> { 061 private String name; 062 private Bounds area; 063 private ImageIcon icon; 064 065 /** 066 * Constructs a new {@code Bookmark} with the given contents. 067 * @param list Bookmark contents as a list of 5 elements. 068 * First item is the name, then come bounds arguments (minlat, minlon, maxlat, maxlon) 069 * @throws NumberFormatException if the bounds arguments are not numbers 070 * @throws IllegalArgumentException if list contain less than 5 elements 071 */ 072 public Bookmark(Collection<String> list) { 073 List<String> array = new ArrayList<>(list); 074 if (array.size() < 5) 075 throw new IllegalArgumentException(tr("Wrong number of arguments for bookmark")); 076 icon = getDefaultIcon(); 077 name = array.get(0); 078 area = new Bounds(Double.parseDouble(array.get(1)), Double.parseDouble(array.get(2)), 079 Double.parseDouble(array.get(3)), Double.parseDouble(array.get(4))); 080 } 081 082 /** 083 * Constructs a new empty {@code Bookmark}. 084 */ 085 public Bookmark() { 086 this(null, null); 087 } 088 089 /** 090 * Constructs a new unamed {@code Bookmark} for the given area. 091 * @param area The bookmark area 092 */ 093 public Bookmark(Bounds area) { 094 this(null, area); 095 } 096 097 /** 098 * Constructs a new {@code Bookmark} for the given name and area. 099 * @param name The bookmark name 100 * @param area The bookmark area 101 * @since 12495 102 */ 103 protected Bookmark(String name, Bounds area) { 104 this.icon = getDefaultIcon(); 105 this.name = name; 106 this.area = area; 107 } 108 109 static ImageIcon getDefaultIcon() { 110 return ImageProvider.get("dialogs", "bookmark", ImageSizes.SMALLICON); 111 } 112 113 @Override 114 public String toString() { 115 return name; 116 } 117 118 @Override 119 public int compareTo(Bookmark b) { 120 return name.toLowerCase(Locale.ENGLISH).compareTo(b.name.toLowerCase(Locale.ENGLISH)); 121 } 122 123 @Override 124 public int hashCode() { 125 return Objects.hash(name, area); 126 } 127 128 @Override 129 public boolean equals(Object obj) { 130 if (this == obj) return true; 131 if (obj == null || getClass() != obj.getClass()) return false; 132 Bookmark bookmark = (Bookmark) obj; 133 return Objects.equals(name, bookmark.name) && 134 Objects.equals(area, bookmark.area); 135 } 136 137 /** 138 * Returns the bookmark area 139 * @return The bookmark area 140 */ 141 public Bounds getArea() { 142 return area; 143 } 144 145 /** 146 * Returns the bookmark name 147 * @return The bookmark name 148 */ 149 public String getName() { 150 return name; 151 } 152 153 /** 154 * Sets the bookmark name 155 * @param name The bookmark name 156 */ 157 public void setName(String name) { 158 this.name = name; 159 } 160 161 /** 162 * Sets the bookmark area 163 * @param area The bookmark area 164 */ 165 public void setArea(Bounds area) { 166 this.area = area; 167 } 168 169 /** 170 * Returns the bookmark icon. 171 * @return the bookmark icon 172 * @since 12495 173 */ 174 public ImageIcon getIcon() { 175 return icon; 176 } 177 178 /** 179 * Sets the bookmark icon. 180 * @param icon the bookmark icon 181 * @since 12495 182 */ 183 public void setIcon(ImageIcon icon) { 184 this.icon = icon; 185 } 186 } 187 188 /** 189 * A specific optional bookmark for the "home location" configured on osm.org website. 190 * @since 12495 191 */ 192 public static class HomeLocationBookmark extends Bookmark { 193 /** 194 * Constructs a new {@code HomeLocationBookmark}. 195 */ 196 public HomeLocationBookmark() { 197 setName(tr("Home location")); 198 setIcon(ImageProvider.get("help", "home", ImageSizes.SMALLICON)); 199 UserInfo info = UserIdentityManager.getInstance().getUserInfo(); 200 if (info == null) { 201 throw new IllegalStateException("User not identified"); 202 } 203 LatLon home = info.getHome(); 204 if (home == null) { 205 throw new IllegalStateException("User home location not set"); 206 } 207 int zoom = info.getHomeZoom(); 208 if (zoom <= 3) { 209 // 3 is the default zoom level in OSM database, but the real zoom level was not correct 210 // for a long time, see https://github.com/openstreetmap/openstreetmap-website/issues/1592 211 zoom = 15; 212 } 213 Projection mercator = Projections.getProjectionByCode("EPSG:3857"); 214 setArea(MapViewState.createDefaultState(430, 400) // Size of map on osm.org user profile settings 215 .usingProjection(mercator) 216 .usingScale(Selector.GeneralSelector.level2scale(zoom) / 100) 217 .usingCenter(mercator.latlon2eastNorth(home)) 218 .getViewArea() 219 .getLatLonBoundsBox()); 220 } 221 } 222 223 /** 224 * A specific optional bookmark for the boundaries of recent changesets. 225 * @since 12495 226 */ 227 public static class ChangesetBookmark extends Bookmark { 228 /** 229 * Constructs a new {@code ChangesetBookmark}. 230 * @param cs changeset from which the boundaries are read. Its id, name and comment are used to name the bookmark 231 */ 232 public ChangesetBookmark(Changeset cs) { 233 setName(String.format("%d - %tF - %s", cs.getId(), cs.getCreatedAt(), cs.getComment())); 234 setIcon(ImageProvider.get("data", "changeset", ImageSizes.SMALLICON)); 235 setArea(cs.getBounds()); 236 } 237 } 238 239 /** 240 * Creates a bookmark list as well as the Buttons add and remove. 241 */ 242 public BookmarkList() { 243 setModel(new DefaultListModel<Bookmark>()); 244 load(); 245 setVisibleRowCount(7); 246 setCellRenderer(new BookmarkCellRenderer()); 247 } 248 249 /** 250 * Loads the home location bookmark from OSM API, 251 * the manual bookmarks from preferences file, 252 * the changeset bookmarks from changeset cache. 253 */ 254 public final void load() { 255 final DefaultListModel<Bookmark> model = (DefaultListModel<Bookmark>) getModel(); 256 model.removeAllElements(); 257 UserIdentityManager im = UserIdentityManager.getInstance(); 258 // Add home location bookmark first, if user fully identified 259 if (im.isFullyIdentified()) { 260 try { 261 model.addElement(new HomeLocationBookmark()); 262 } catch (IllegalStateException e) { 263 Logging.info(e.getMessage()); 264 Logging.trace(e); 265 } 266 } 267 // Then add manual bookmarks previously saved in local preferences 268 List<List<String>> args = Config.getPref().getListOfLists("bookmarks", null); 269 if (args != null) { 270 List<Bookmark> bookmarks = new LinkedList<>(); 271 for (Collection<String> entry : args) { 272 try { 273 bookmarks.add(new Bookmark(entry)); 274 } catch (IllegalArgumentException e) { 275 Logging.log(Logging.LEVEL_ERROR, tr("Error reading bookmark entry: %s", e.getMessage()), e); 276 } 277 } 278 Collections.sort(bookmarks); 279 for (Bookmark b : bookmarks) { 280 model.addElement(b); 281 } 282 } 283 // Finally add recent changeset bookmarks, if user name is known 284 final int n = MAX_CHANGESET_BOOKMARKS.get(); 285 if (n > 0 && !im.isAnonymous()) { 286 final UserInfo userInfo = im.getUserInfo(); 287 if (userInfo != null) { 288 final ChangesetCacheManager ccm = ChangesetCacheManager.getInstance(); 289 final int userId = userInfo.getId(); 290 int found = 0; 291 for (int i = 0; i < ccm.getModel().getRowCount() && found < n; i++) { 292 Changeset cs = ccm.getModel().getValueAt(i, 0); 293 if (cs.getUser().getId() == userId && cs.getBounds() != null) { 294 model.addElement(new ChangesetBookmark(cs)); 295 found++; 296 } 297 } 298 } 299 } 300 } 301 302 /** 303 * Saves all manual bookmarks to the preferences file. 304 */ 305 public final void save() { 306 List<List<String>> coll = new LinkedList<>(); 307 for (Object o : ((DefaultListModel<Bookmark>) getModel()).toArray()) { 308 if (o instanceof HomeLocationBookmark || o instanceof ChangesetBookmark) { 309 continue; 310 } 311 String[] array = new String[5]; 312 Bookmark b = (Bookmark) o; 313 array[0] = b.getName(); 314 Bounds area = b.getArea(); 315 array[1] = String.valueOf(area.getMinLat()); 316 array[2] = String.valueOf(area.getMinLon()); 317 array[3] = String.valueOf(area.getMaxLat()); 318 array[4] = String.valueOf(area.getMaxLon()); 319 coll.add(Arrays.asList(array)); 320 } 321 Config.getPref().putListOfLists("bookmarks", coll); 322 } 323 324 /** 325 * Refreshes the changeset bookmarks. 326 * @since 12495 327 */ 328 public void refreshChangesetBookmarks() { 329 final int n = MAX_CHANGESET_BOOKMARKS.get(); 330 if (n > 0) { 331 final DefaultListModel<Bookmark> model = (DefaultListModel<Bookmark>) getModel(); 332 for (int i = model.getSize() - 1; i >= 0; i--) { 333 if (model.get(i) instanceof ChangesetBookmark) { 334 model.remove(i); 335 } 336 } 337 ChangesetQuery query = ChangesetQuery.forCurrentUser(); 338 if (!GraphicsEnvironment.isHeadless()) { 339 final ChangesetQueryTask task = new ChangesetQueryTask(this, query); 340 ChangesetCacheManager.getInstance().runDownloadTask(task); 341 MainApplication.worker.submit(() -> { 342 if (task.isCanceled() || task.isFailed()) 343 return; 344 GuiHelper.runInEDT(() -> task.getDownloadedData().stream() 345 .filter(cs -> cs.getBounds() != null) 346 .sorted(Comparator.reverseOrder()) 347 .limit(n) 348 .forEachOrdered(cs -> model.addElement(new ChangesetBookmark(cs)))); 349 }); 350 } 351 } 352 } 353 354 static class BookmarkCellRenderer extends JLabel implements ListCellRenderer<BookmarkList.Bookmark> { 355 356 /** 357 * Constructs a new {@code BookmarkCellRenderer}. 358 */ 359 BookmarkCellRenderer() { 360 setOpaque(true); 361 } 362 363 protected void renderColor(boolean selected) { 364 if (selected) { 365 setBackground(UIManager.getColor("List.selectionBackground")); 366 setForeground(UIManager.getColor("List.selectionForeground")); 367 } else { 368 setBackground(UIManager.getColor("List.background")); 369 setForeground(UIManager.getColor("List.foreground")); 370 } 371 } 372 373 protected String buildToolTipText(Bookmark b) { 374 Bounds area = b.getArea(); 375 StringBuilder sb = new StringBuilder(128); 376 if (area != null) { 377 sb.append("<html>min[latitude,longitude]=<strong>[") 378 .append(area.getMinLat()).append(',').append(area.getMinLon()).append("]</strong>"+ 379 "<br>max[latitude,longitude]=<strong>[") 380 .append(area.getMaxLat()).append(',').append(area.getMaxLon()).append("]</strong>"+ 381 "</html>"); 382 } 383 return sb.toString(); 384 } 385 386 @Override 387 public Component getListCellRendererComponent(JList<? extends Bookmark> list, Bookmark value, int index, boolean isSelected, 388 boolean cellHasFocus) { 389 renderColor(isSelected); 390 setIcon(value.getIcon()); 391 setText(value.getName()); 392 setToolTipText(buildToolTipText(value)); 393 return this; 394 } 395 } 396}