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.List;
012import java.util.Map;
013
014import javax.swing.JOptionPane;
015import javax.swing.SwingUtilities;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.osm.PrimitiveId;
019import org.openstreetmap.josm.data.osm.history.History;
020import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
021import org.openstreetmap.josm.gui.MapView;
022import org.openstreetmap.josm.gui.layer.Layer;
023import org.openstreetmap.josm.tools.BugReportExceptionHandler;
024import org.openstreetmap.josm.tools.Predicate;
025import org.openstreetmap.josm.tools.Utils;
026import org.openstreetmap.josm.tools.WindowGeometry;
027
028/**
029 * Manager allowing to show/hide history dialogs.
030 * @since 2019
031 */
032public class HistoryBrowserDialogManager implements MapView.LayerChangeListener {
033
034    private static HistoryBrowserDialogManager instance;
035
036    /**
037     * Replies the unique instance.
038     * @return the unique instance
039     */
040    public static HistoryBrowserDialogManager getInstance() {
041        if (instance == null) {
042            instance = new HistoryBrowserDialogManager();
043        }
044        return instance;
045    }
046
047    private Map<Long, HistoryBrowserDialog> dialogs;
048
049    protected HistoryBrowserDialogManager() {
050        dialogs = new HashMap<>();
051        MapView.addLayerChangeListener(this);
052    }
053
054    /**
055     * Determines if an history dialog exists for the given object id.
056     * @param id the object id
057     * @return {@code true} if an history dialog exists for the given object id, {@code false} otherwise
058     */
059    public boolean existsDialog(long id) {
060        return dialogs.containsKey(id);
061    }
062
063    protected void show(long id, HistoryBrowserDialog dialog) {
064        if (dialogs.values().contains(dialog)) {
065            show(id);
066        } else {
067            placeOnScreen(dialog);
068            dialog.setVisible(true);
069            dialogs.put(id, dialog);
070        }
071    }
072
073    protected void show(long id) {
074        if (dialogs.keySet().contains(id)) {
075            dialogs.get(id).toFront();
076        }
077    }
078
079    protected boolean hasDialogWithCloseUpperLeftCorner(Point p) {
080        for (HistoryBrowserDialog dialog: dialogs.values()) {
081            Point corner = dialog.getLocation();
082            if (p.x >= corner.x -5 && corner.x + 5 >= p.x
083                    && p.y >= corner.y -5 && corner.y + 5 >= p.y)
084                return true;
085        }
086        return false;
087    }
088
089    final String WINDOW_GEOMETRY_PREF = getClass().getName() + ".geometry";
090
091    protected void placeOnScreen(HistoryBrowserDialog dialog) {
092        WindowGeometry geometry = new WindowGeometry(WINDOW_GEOMETRY_PREF, WindowGeometry.centerOnScreen(new Dimension(850, 500)));
093        geometry.applySafe(dialog);
094        Point p = dialog.getLocation();
095        while (hasDialogWithCloseUpperLeftCorner(p)) {
096            p.x += 20;
097            p.y += 20;
098        }
099        dialog.setLocation(p);
100    }
101
102    /**
103     * Hides the specified history dialog and cleans associated resources.
104     * @param dialog History dialog to hide
105     */
106    public void hide(HistoryBrowserDialog dialog) {
107        long id = 0;
108        for (long i: dialogs.keySet()) {
109            if (dialogs.get(i) == dialog) {
110                id = i;
111                break;
112            }
113        }
114        if (id > 0) {
115            dialogs.remove(id);
116            if (dialogs.isEmpty()) {
117                new WindowGeometry(dialog).remember(WINDOW_GEOMETRY_PREF);
118            }
119        }
120        dialog.setVisible(false);
121        dialog.dispose();
122    }
123
124    /**
125     * Hides and destroys all currently visible history browser dialogs
126     *
127     */
128    public void hideAll() {
129        List<HistoryBrowserDialog> dialogs = new ArrayList<>();
130        dialogs.addAll(this.dialogs.values());
131        for (HistoryBrowserDialog dialog: dialogs) {
132            dialog.unlinkAsListener();
133            hide(dialog);
134        }
135    }
136
137    /**
138     * Show history dialog for the given history.
139     * @param h History to show
140     */
141    public void show(History h) {
142        if (h == null)
143            return;
144        if (existsDialog(h.getId())) {
145            show(h.getId());
146        } else {
147            HistoryBrowserDialog dialog = new HistoryBrowserDialog(h);
148            show(h.getId(), dialog);
149        }
150    }
151
152    /* ----------------------------------------------------------------------------- */
153    /* LayerChangeListener                                                           */
154    /* ----------------------------------------------------------------------------- */
155    @Override
156    public void activeLayerChange(Layer oldLayer, Layer newLayer) {}
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<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        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}