// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.history; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Dimension; import java.awt.Point; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.function.Predicate; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.osm.PrimitiveId; import org.openstreetmap.josm.data.osm.history.History; import org.openstreetmap.josm.data.osm.history.HistoryDataSet; import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; import org.openstreetmap.josm.tools.JosmRuntimeException; import org.openstreetmap.josm.tools.SubclassFilteredCollection; import org.openstreetmap.josm.tools.WindowGeometry; import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler; /** * Manager allowing to show/hide history dialogs. * @since 2019 */ public final class HistoryBrowserDialogManager implements LayerChangeListener { static final class UnloadedHistoryPredicate implements Predicate<PrimitiveId> { private final HistoryDataSet hds = HistoryDataSet.getInstance(); @Override public boolean test(PrimitiveId p) { History h = hds.getHistory(p); if (h == null) // reload if the history is not in the cache yet return true; else // reload if the history object of the selected object is not in the cache yet return !p.isNew() && h.getByVersion(p.getUniqueId()) == null; } } private static final String WINDOW_GEOMETRY_PREF = HistoryBrowserDialogManager.class.getName() + ".geometry"; private static HistoryBrowserDialogManager instance; private final Map<Long, HistoryBrowserDialog> dialogs; private final Predicate<PrimitiveId> unloadedHistoryPredicate = new UnloadedHistoryPredicate(); private final Predicate<PrimitiveId> notNewPredicate = p -> !p.isNew(); protected HistoryBrowserDialogManager() { dialogs = new HashMap<>(); Main.getLayerManager().addLayerChangeListener(this); } /** * Replies the unique instance. * @return the unique instance */ public static synchronized HistoryBrowserDialogManager getInstance() { if (instance == null) { instance = new HistoryBrowserDialogManager(); } return instance; } /** * Determines if an history dialog exists for the given object id. * @param id the object id * @return {@code true} if an history dialog exists for the given object id, {@code false} otherwise */ public boolean existsDialog(long id) { return dialogs.containsKey(id); } private void show(long id, HistoryBrowserDialog dialog) { if (dialogs.containsValue(dialog)) { show(id); } else { placeOnScreen(dialog); dialog.setVisible(true); dialogs.put(id, dialog); } } private void show(long id) { if (dialogs.containsKey(id)) { dialogs.get(id).toFront(); } } private boolean hasDialogWithCloseUpperLeftCorner(Point p) { for (HistoryBrowserDialog dialog: dialogs.values()) { Point corner = dialog.getLocation(); if (p.x >= corner.x -5 && corner.x + 5 >= p.x && p.y >= corner.y -5 && corner.y + 5 >= p.y) return true; } return false; } private void placeOnScreen(HistoryBrowserDialog dialog) { WindowGeometry geometry = new WindowGeometry(WINDOW_GEOMETRY_PREF, WindowGeometry.centerOnScreen(new Dimension(850, 500))); geometry.applySafe(dialog); Point p = dialog.getLocation(); while (hasDialogWithCloseUpperLeftCorner(p)) { p.x += 20; p.y += 20; } dialog.setLocation(p); } /** * Hides the specified history dialog and cleans associated resources. * @param dialog History dialog to hide */ public void hide(HistoryBrowserDialog dialog) { for (Iterator<Entry<Long, HistoryBrowserDialog>> it = dialogs.entrySet().iterator(); it.hasNext();) { if (Objects.equals(it.next().getValue(), dialog)) { it.remove(); if (dialogs.isEmpty()) { new WindowGeometry(dialog).remember(WINDOW_GEOMETRY_PREF); } break; } } dialog.setVisible(false); dialog.dispose(); } /** * Hides and destroys all currently visible history browser dialogs * */ public void hideAll() { List<HistoryBrowserDialog> dialogs = new ArrayList<>(); dialogs.addAll(this.dialogs.values()); for (HistoryBrowserDialog dialog: dialogs) { dialog.unlinkAsListener(); hide(dialog); } } /** * Show history dialog for the given history. * @param h History to show */ public void show(History h) { if (h == null) return; if (existsDialog(h.getId())) { show(h.getId()); } else { HistoryBrowserDialog dialog = new HistoryBrowserDialog(h); show(h.getId(), dialog); } } /* ----------------------------------------------------------------------------- */ /* LayerChangeListener */ /* ----------------------------------------------------------------------------- */ @Override public void layerAdded(LayerAddEvent e) { // Do nothing } @Override public void layerRemoving(LayerRemoveEvent e) { // remove all history browsers if the number of layers drops to 0 if (e.getSource().getLayers().isEmpty()) { hideAll(); } } @Override public void layerOrderChanged(LayerOrderChangeEvent e) { // Do nothing } /** * Show history dialog(s) for the given primitive(s). * @param primitives The primitive(s) for which history will be displayed */ public void showHistory(final Collection<? extends PrimitiveId> primitives) { final Collection<? extends PrimitiveId> notNewPrimitives = SubclassFilteredCollection.filter(primitives, notNewPredicate); if (notNewPrimitives.isEmpty()) { JOptionPane.showMessageDialog( Main.parent, tr("Please select at least one already uploaded node, way, or relation."), tr("Warning"), JOptionPane.WARNING_MESSAGE); return; } Collection<? extends PrimitiveId> toLoad = SubclassFilteredCollection.filter(primitives, unloadedHistoryPredicate); if (!toLoad.isEmpty()) { HistoryLoadTask task = new HistoryLoadTask(); for (PrimitiveId p : notNewPrimitives) { task.add(p); } Main.worker.submit(task); } Runnable r = () -> { try { for (PrimitiveId p : notNewPrimitives) { final History h = HistoryDataSet.getInstance().getHistory(p); if (h == null) { continue; } SwingUtilities.invokeLater(() -> show(h)); } } catch (final JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { BugReportExceptionHandler.handleException(e); } }; Main.worker.submit(r); } }