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}