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