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