001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.history;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Dimension;
007import java.awt.Point;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.HashMap;
011import java.util.Iterator;
012import java.util.List;
013import java.util.Map;
014import java.util.Map.Entry;
015import java.util.Objects;
016import java.util.function.Predicate;
017
018import javax.swing.JOptionPane;
019import javax.swing.SwingUtilities;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.data.osm.PrimitiveId;
023import org.openstreetmap.josm.data.osm.history.History;
024import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
025import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
026import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
027import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
028import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
029import org.openstreetmap.josm.tools.SubclassFilteredCollection;
030import org.openstreetmap.josm.tools.WindowGeometry;
031import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
032
033/**
034 * Manager allowing to show/hide history dialogs.
035 * @since 2019
036 */
037public final class HistoryBrowserDialogManager implements LayerChangeListener {
038
039    static final class UnloadedHistoryPredicate implements Predicate<PrimitiveId> {
040        private final HistoryDataSet hds = HistoryDataSet.getInstance();
041
042        @Override
043        public boolean test(PrimitiveId p) {
044            History h = hds.getHistory(p);
045            if (h == null)
046                // reload if the history is not in the cache yet
047                return true;
048            else
049                // reload if the history object of the selected object is not in the cache yet
050                return !p.isNew() && h.getByVersion(p.getUniqueId()) == null;
051        }
052    }
053
054    private static final String WINDOW_GEOMETRY_PREF = HistoryBrowserDialogManager.class.getName() + ".geometry";
055
056    private static HistoryBrowserDialogManager instance;
057
058    private final Map<Long, HistoryBrowserDialog> dialogs;
059
060    private final Predicate<PrimitiveId> unloadedHistoryPredicate = new UnloadedHistoryPredicate();
061
062    private final Predicate<PrimitiveId> notNewPredicate = p -> !p.isNew();
063
064    protected HistoryBrowserDialogManager() {
065        dialogs = new HashMap<>();
066        Main.getLayerManager().addLayerChangeListener(this);
067    }
068
069    /**
070     * Replies the unique instance.
071     * @return the unique instance
072     */
073    public static synchronized HistoryBrowserDialogManager getInstance() {
074        if (instance == null) {
075            instance = new HistoryBrowserDialogManager();
076        }
077        return instance;
078    }
079
080    /**
081     * Determines if an history dialog exists for the given object id.
082     * @param id the object id
083     * @return {@code true} if an history dialog exists for the given object id, {@code false} otherwise
084     */
085    public boolean existsDialog(long id) {
086        return dialogs.containsKey(id);
087    }
088
089    private void show(long id, HistoryBrowserDialog dialog) {
090        if (dialogs.values().contains(dialog)) {
091            show(id);
092        } else {
093            placeOnScreen(dialog);
094            dialog.setVisible(true);
095            dialogs.put(id, dialog);
096        }
097    }
098
099    private void show(long id) {
100        if (dialogs.keySet().contains(id)) {
101            dialogs.get(id).toFront();
102        }
103    }
104
105    private boolean hasDialogWithCloseUpperLeftCorner(Point p) {
106        for (HistoryBrowserDialog dialog: dialogs.values()) {
107            Point corner = dialog.getLocation();
108            if (p.x >= corner.x -5 && corner.x + 5 >= p.x
109                    && p.y >= corner.y -5 && corner.y + 5 >= p.y)
110                return true;
111        }
112        return false;
113    }
114
115    private void placeOnScreen(HistoryBrowserDialog dialog) {
116        WindowGeometry geometry = new WindowGeometry(WINDOW_GEOMETRY_PREF, WindowGeometry.centerOnScreen(new Dimension(850, 500)));
117        geometry.applySafe(dialog);
118        Point p = dialog.getLocation();
119        while (hasDialogWithCloseUpperLeftCorner(p)) {
120            p.x += 20;
121            p.y += 20;
122        }
123        dialog.setLocation(p);
124    }
125
126    /**
127     * Hides the specified history dialog and cleans associated resources.
128     * @param dialog History dialog to hide
129     */
130    public void hide(HistoryBrowserDialog dialog) {
131        for (Iterator<Entry<Long, HistoryBrowserDialog>> it = dialogs.entrySet().iterator(); it.hasNext();) {
132            if (Objects.equals(it.next().getValue(), dialog)) {
133                it.remove();
134                if (dialogs.isEmpty()) {
135                    new WindowGeometry(dialog).remember(WINDOW_GEOMETRY_PREF);
136                }
137                break;
138            }
139        }
140        dialog.setVisible(false);
141        dialog.dispose();
142    }
143
144    /**
145     * Hides and destroys all currently visible history browser dialogs
146     *
147     */
148    public void hideAll() {
149        List<HistoryBrowserDialog> dialogs = new ArrayList<>();
150        dialogs.addAll(this.dialogs.values());
151        for (HistoryBrowserDialog dialog: dialogs) {
152            dialog.unlinkAsListener();
153            hide(dialog);
154        }
155    }
156
157    /**
158     * Show history dialog for the given history.
159     * @param h History to show
160     */
161    public void show(History h) {
162        if (h == null)
163            return;
164        if (existsDialog(h.getId())) {
165            show(h.getId());
166        } else {
167            HistoryBrowserDialog dialog = new HistoryBrowserDialog(h);
168            show(h.getId(), dialog);
169        }
170    }
171
172    /* ----------------------------------------------------------------------------- */
173    /* LayerChangeListener                                                           */
174    /* ----------------------------------------------------------------------------- */
175    @Override
176    public void layerAdded(LayerAddEvent e) {
177        // Do nothing
178    }
179
180    @Override
181    public void layerRemoving(LayerRemoveEvent e) {
182        // remove all history browsers if the number of layers drops to 0
183        if (e.getSource().getLayers().isEmpty()) {
184            hideAll();
185        }
186    }
187
188    @Override
189    public void layerOrderChanged(LayerOrderChangeEvent e) {
190        // Do nothing
191    }
192
193    /**
194     * Show history dialog(s) for the given primitive(s).
195     * @param primitives The primitive(s) for which history will be displayed
196     */
197    public void showHistory(final Collection<? extends PrimitiveId> primitives) {
198        final Collection<? extends PrimitiveId> notNewPrimitives = SubclassFilteredCollection.filter(primitives, notNewPredicate);
199        if (notNewPrimitives.isEmpty()) {
200            JOptionPane.showMessageDialog(
201                    Main.parent,
202                    tr("Please select at least one already uploaded node, way, or relation."),
203                    tr("Warning"),
204                    JOptionPane.WARNING_MESSAGE);
205            return;
206        }
207
208        Collection<? extends PrimitiveId> toLoad = SubclassFilteredCollection.filter(primitives, unloadedHistoryPredicate);
209        if (!toLoad.isEmpty()) {
210            HistoryLoadTask task = new HistoryLoadTask();
211            for (PrimitiveId p : notNewPrimitives) {
212                task.add(p);
213            }
214            Main.worker.submit(task);
215        }
216
217        Runnable r = () -> {
218            try {
219                for (PrimitiveId p : notNewPrimitives) {
220                    final History h = HistoryDataSet.getInstance().getHistory(p);
221                    if (h == null) {
222                        continue;
223                    }
224                    SwingUtilities.invokeLater(() -> show(h));
225                }
226            } catch (final RuntimeException e) {
227                BugReportExceptionHandler.handleException(e);
228            }
229        };
230        Main.worker.submit(r);
231    }
232}