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