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; 016 017import javax.swing.JOptionPane; 018import javax.swing.SwingUtilities; 019 020import org.openstreetmap.josm.Main; 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.layer.LayerManager.LayerAddEvent; 025import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 026import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 027import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 028import org.openstreetmap.josm.tools.Predicate; 029import org.openstreetmap.josm.tools.Utils; 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 protected 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 protected void show(long id) { 081 if (dialogs.keySet().contains(id)) { 082 dialogs.get(id).toFront(); 083 } 084 } 085 086 protected 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 protected 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 = Utils.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 = Utils.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 = new Runnable() { 199 200 @Override 201 public void run() { 202 try { 203 for (PrimitiveId p : notNewPrimitives) { 204 final History h = HistoryDataSet.getInstance().getHistory(p); 205 if (h == null) { 206 continue; 207 } 208 SwingUtilities.invokeLater(new Runnable() { 209 @Override 210 public void run() { 211 show(h); 212 } 213 }); 214 } 215 } catch (final RuntimeException e) { 216 BugReportExceptionHandler.handleException(e); 217 } 218 } 219 }; 220 Main.worker.submit(r); 221 } 222 223 private final Predicate<PrimitiveId> unloadedHistoryPredicate = new Predicate<PrimitiveId>() { 224 225 private HistoryDataSet hds = HistoryDataSet.getInstance(); 226 227 @Override 228 public boolean evaluate(PrimitiveId p) { 229 History h = hds.getHistory(p); 230 if (h == null) 231 // reload if the history is not in the cache yet 232 return true; 233 else 234 // reload if the history object of the selected object is not in the cache yet 235 return !p.isNew() && h.getByVersion(p.getUniqueId()) == null; 236 } 237 }; 238 239 private final Predicate<PrimitiveId> notNewPredicate = new Predicate<PrimitiveId>() { 240 241 @Override 242 public boolean evaluate(PrimitiveId p) { 243 return !p.isNew(); 244 } 245 }; 246}