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