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 = ImageProvider.get("dialogs", "bookmark"); 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 = ImageProvider.get("dialogs", "bookmark"); 105 this.name = name; 106 this.area = area; 107 } 108 109 @Override 110 public String toString() { 111 return name; 112 } 113 114 @Override 115 public int compareTo(Bookmark b) { 116 return name.toLowerCase(Locale.ENGLISH).compareTo(b.name.toLowerCase(Locale.ENGLISH)); 117 } 118 119 @Override 120 public int hashCode() { 121 return Objects.hash(name, area); 122 } 123 124 @Override 125 public boolean equals(Object obj) { 126 if (this == obj) return true; 127 if (obj == null || getClass() != obj.getClass()) return false; 128 Bookmark bookmark = (Bookmark) obj; 129 return Objects.equals(name, bookmark.name) && 130 Objects.equals(area, bookmark.area); 131 } 132 133 /** 134 * Returns the bookmark area 135 * @return The bookmark area 136 */ 137 public Bounds getArea() { 138 return area; 139 } 140 141 /** 142 * Returns the bookmark name 143 * @return The bookmark name 144 */ 145 public String getName() { 146 return name; 147 } 148 149 /** 150 * Sets the bookmark name 151 * @param name The bookmark name 152 */ 153 public void setName(String name) { 154 this.name = name; 155 } 156 157 /** 158 * Sets the bookmark area 159 * @param area The bookmark area 160 */ 161 public void setArea(Bounds area) { 162 this.area = area; 163 } 164 165 /** 166 * Returns the bookmark icon. 167 * @return the bookmark icon 168 * @since 12495 169 */ 170 public ImageIcon getIcon() { 171 return icon; 172 } 173 174 /** 175 * Sets the bookmark icon. 176 * @param icon the bookmark icon 177 * @since 12495 178 */ 179 public void setIcon(ImageIcon icon) { 180 this.icon = icon; 181 } 182 } 183 184 /** 185 * A specific optional bookmark for the "home location" configured on osm.org website. 186 * @since 12495 187 */ 188 public static class HomeLocationBookmark extends Bookmark { 189 /** 190 * Constructs a new {@code HomeLocationBookmark}. 191 */ 192 public HomeLocationBookmark() { 193 setName(tr("Home location")); 194 setIcon(ImageProvider.get("help", "home", ImageSizes.SMALLICON)); 195 UserInfo info = UserIdentityManager.getInstance().getUserInfo(); 196 if (info == null) { 197 throw new IllegalStateException("User not identified"); 198 } 199 LatLon home = info.getHome(); 200 if (home == null) { 201 throw new IllegalStateException("User home location not set"); 202 } 203 int zoom = info.getHomeZoom(); 204 if (zoom <= 3) { 205 // 3 is the default zoom level in OSM database, but the real zoom level was not correct 206 // for a long time, see https://github.com/openstreetmap/openstreetmap-website/issues/1592 207 zoom = 15; 208 } 209 Projection mercator = Projections.getProjectionByCode("EPSG:3857"); 210 setArea(MapViewState.createDefaultState(430, 400) // Size of map on osm.org user profile settings 211 .usingProjection(mercator) 212 .usingScale(Selector.GeneralSelector.level2scale(zoom) / 100) 213 .usingCenter(mercator.latlon2eastNorth(home)) 214 .getViewArea() 215 .getLatLonBoundsBox()); 216 } 217 } 218 219 /** 220 * A specific optional bookmark for the boundaries of recent changesets. 221 * @since 12495 222 */ 223 public static class ChangesetBookmark extends Bookmark { 224 /** 225 * Constructs a new {@code ChangesetBookmark}. 226 * @param cs changeset from which the boundaries are read. Its id, name and comment are used to name the bookmark 227 */ 228 public ChangesetBookmark(Changeset cs) { 229 setName(String.format("%d - %tF - %s", cs.getId(), cs.getCreatedAt(), cs.getComment())); 230 setIcon(ImageProvider.get("data", "changeset", ImageSizes.SMALLICON)); 231 setArea(cs.getBounds()); 232 } 233 } 234 235 /** 236 * Creates a bookmark list as well as the Buttons add and remove. 237 */ 238 public BookmarkList() { 239 setModel(new DefaultListModel<Bookmark>()); 240 load(); 241 setVisibleRowCount(7); 242 setCellRenderer(new BookmarkCellRenderer()); 243 } 244 245 /** 246 * Loads the home location bookmark from OSM API, 247 * the manual bookmarks from preferences file, 248 * the changeset bookmarks from changeset cache. 249 */ 250 public final void load() { 251 final DefaultListModel<Bookmark> model = (DefaultListModel<Bookmark>) getModel(); 252 model.removeAllElements(); 253 UserIdentityManager im = UserIdentityManager.getInstance(); 254 // Add home location bookmark first, if user fully identified 255 if (im.isFullyIdentified()) { 256 try { 257 model.addElement(new HomeLocationBookmark()); 258 } catch (IllegalStateException e) { 259 Logging.info(e.getMessage()); 260 Logging.trace(e); 261 } 262 } 263 // Then add manual bookmarks previously saved in local preferences 264 List<List<String>> args = Config.getPref().getListOfLists("bookmarks", null); 265 if (args != null) { 266 List<Bookmark> bookmarks = new LinkedList<>(); 267 for (Collection<String> entry : args) { 268 try { 269 bookmarks.add(new Bookmark(entry)); 270 } catch (IllegalArgumentException e) { 271 Logging.log(Logging.LEVEL_ERROR, tr("Error reading bookmark entry: %s", e.getMessage()), e); 272 } 273 } 274 Collections.sort(bookmarks); 275 for (Bookmark b : bookmarks) { 276 model.addElement(b); 277 } 278 } 279 // Finally add recent changeset bookmarks, if user name is known 280 final int n = MAX_CHANGESET_BOOKMARKS.get(); 281 if (n > 0 && !im.isAnonymous()) { 282 final UserInfo userInfo = im.getUserInfo(); 283 if (userInfo != null) { 284 final ChangesetCacheManager ccm = ChangesetCacheManager.getInstance(); 285 final int userId = userInfo.getId(); 286 int found = 0; 287 for (int i = 0; i < ccm.getModel().getRowCount() && found < n; i++) { 288 Changeset cs = ccm.getModel().getValueAt(i, 0); 289 if (cs.getUser().getId() == userId && cs.getBounds() != null) { 290 model.addElement(new ChangesetBookmark(cs)); 291 found++; 292 } 293 } 294 } 295 } 296 } 297 298 /** 299 * Saves all manual bookmarks to the preferences file. 300 */ 301 public final void save() { 302 List<List<String>> coll = new LinkedList<>(); 303 for (Object o : ((DefaultListModel<Bookmark>) getModel()).toArray()) { 304 if (o instanceof HomeLocationBookmark || o instanceof ChangesetBookmark) { 305 continue; 306 } 307 String[] array = new String[5]; 308 Bookmark b = (Bookmark) o; 309 array[0] = b.getName(); 310 Bounds area = b.getArea(); 311 array[1] = String.valueOf(area.getMinLat()); 312 array[2] = String.valueOf(area.getMinLon()); 313 array[3] = String.valueOf(area.getMaxLat()); 314 array[4] = String.valueOf(area.getMaxLon()); 315 coll.add(Arrays.asList(array)); 316 } 317 Config.getPref().putListOfLists("bookmarks", coll); 318 } 319 320 /** 321 * Refreshes the changeset bookmarks. 322 * @since 12495 323 */ 324 public void refreshChangesetBookmarks() { 325 final int n = MAX_CHANGESET_BOOKMARKS.get(); 326 if (n > 0) { 327 final DefaultListModel<Bookmark> model = (DefaultListModel<Bookmark>) getModel(); 328 for (int i = model.getSize() - 1; i >= 0; i--) { 329 if (model.get(i) instanceof ChangesetBookmark) { 330 model.remove(i); 331 } 332 } 333 ChangesetQuery query = ChangesetQuery.forCurrentUser(); 334 if (!GraphicsEnvironment.isHeadless()) { 335 final ChangesetQueryTask task = new ChangesetQueryTask(this, query); 336 ChangesetCacheManager.getInstance().runDownloadTask(task); 337 MainApplication.worker.submit(() -> { 338 if (task.isCanceled() || task.isFailed()) 339 return; 340 GuiHelper.runInEDT(() -> task.getDownloadedData().stream() 341 .filter(cs -> cs.getBounds() != null) 342 .sorted(Comparator.reverseOrder()) 343 .limit(n) 344 .forEachOrdered(cs -> model.addElement(new ChangesetBookmark(cs)))); 345 }); 346 } 347 } 348 } 349 350 static class BookmarkCellRenderer extends JLabel implements ListCellRenderer<BookmarkList.Bookmark> { 351 352 /** 353 * Constructs a new {@code BookmarkCellRenderer}. 354 */ 355 BookmarkCellRenderer() { 356 setOpaque(true); 357 } 358 359 protected void renderColor(boolean selected) { 360 if (selected) { 361 setBackground(UIManager.getColor("List.selectionBackground")); 362 setForeground(UIManager.getColor("List.selectionForeground")); 363 } else { 364 setBackground(UIManager.getColor("List.background")); 365 setForeground(UIManager.getColor("List.foreground")); 366 } 367 } 368 369 protected String buildToolTipText(Bookmark b) { 370 Bounds area = b.getArea(); 371 StringBuilder sb = new StringBuilder(128); 372 if (area != null) { 373 sb.append("<html>min[latitude,longitude]=<strong>[") 374 .append(area.getMinLat()).append(',').append(area.getMinLon()).append("]</strong>"+ 375 "<br>max[latitude,longitude]=<strong>[") 376 .append(area.getMaxLat()).append(',').append(area.getMaxLon()).append("]</strong>"+ 377 "</html>"); 378 } 379 return sb.toString(); 380 } 381 382 @Override 383 public Component getListCellRendererComponent(JList<? extends Bookmark> list, Bookmark value, int index, boolean isSelected, 384 boolean cellHasFocus) { 385 renderColor(isSelected); 386 setIcon(value.getIcon()); 387 setText(value.getName()); 388 setToolTipText(buildToolTipText(value)); 389 return this; 390 } 391 } 392}