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}