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}