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