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}