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 protected HistoryBrowserDialogManager() { 067 dialogs = new HashMap<>(); 068 MainApplication.getLayerManager().addLayerChangeListener(this); 069 } 070 071 /** 072 * Replies the unique instance. 073 * @return the unique instance 074 */ 075 public static synchronized HistoryBrowserDialogManager getInstance() { 076 if (instance == null) { 077 instance = new HistoryBrowserDialogManager(); 078 } 079 return instance; 080 } 081 082 /** 083 * Determines if an history dialog exists for the given object id. 084 * @param id the object id 085 * @return {@code true} if an history dialog exists for the given object id, {@code false} otherwise 086 */ 087 public boolean existsDialog(long id) { 088 return dialogs.containsKey(id); 089 } 090 091 private void show(long id, HistoryBrowserDialog dialog) { 092 if (dialogs.containsValue(dialog)) { 093 show(id); 094 } else { 095 placeOnScreen(dialog); 096 dialog.setVisible(true); 097 dialogs.put(id, dialog); 098 } 099 } 100 101 private void show(long id) { 102 if (dialogs.containsKey(id)) { 103 dialogs.get(id).toFront(); 104 } 105 } 106 107 private boolean hasDialogWithCloseUpperLeftCorner(Point p) { 108 for (HistoryBrowserDialog dialog: dialogs.values()) { 109 Point corner = dialog.getLocation(); 110 if (p.x >= corner.x -5 && corner.x + 5 >= p.x 111 && p.y >= corner.y -5 && corner.y + 5 >= p.y) 112 return true; 113 } 114 return false; 115 } 116 117 private void placeOnScreen(HistoryBrowserDialog dialog) { 118 WindowGeometry geometry = new WindowGeometry(WINDOW_GEOMETRY_PREF, WindowGeometry.centerOnScreen(new Dimension(850, 500))); 119 geometry.applySafe(dialog); 120 Point p = dialog.getLocation(); 121 while (hasDialogWithCloseUpperLeftCorner(p)) { 122 p.x += 20; 123 p.y += 20; 124 } 125 dialog.setLocation(p); 126 } 127 128 /** 129 * Hides the specified history dialog and cleans associated resources. 130 * @param dialog History dialog to hide 131 */ 132 public void hide(HistoryBrowserDialog dialog) { 133 for (Iterator<Entry<Long, HistoryBrowserDialog>> it = dialogs.entrySet().iterator(); it.hasNext();) { 134 if (Objects.equals(it.next().getValue(), dialog)) { 135 it.remove(); 136 if (dialogs.isEmpty()) { 137 new WindowGeometry(dialog).remember(WINDOW_GEOMETRY_PREF); 138 } 139 break; 140 } 141 } 142 dialog.setVisible(false); 143 dialog.dispose(); 144 } 145 146 /** 147 * Hides and destroys all currently visible history browser dialogs 148 * 149 */ 150 public void hideAll() { 151 List<HistoryBrowserDialog> dialogs = new ArrayList<>(); 152 dialogs.addAll(this.dialogs.values()); 153 for (HistoryBrowserDialog dialog: dialogs) { 154 dialog.unlinkAsListener(); 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 * Show history dialog(s) for the given primitive(s). 197 * @param primitives The primitive(s) for which history will be displayed 198 */ 199 public void showHistory(final Collection<? extends PrimitiveId> primitives) { 200 final Collection<? extends PrimitiveId> notNewPrimitives = SubclassFilteredCollection.filter(primitives, notNewPredicate); 201 if (notNewPrimitives.isEmpty()) { 202 JOptionPane.showMessageDialog( 203 Main.parent, 204 tr("Please select at least one already uploaded node, way, or relation."), 205 tr("Warning"), 206 JOptionPane.WARNING_MESSAGE); 207 return; 208 } 209 210 Collection<? extends PrimitiveId> toLoad = SubclassFilteredCollection.filter(primitives, unloadedHistoryPredicate); 211 if (!toLoad.isEmpty()) { 212 HistoryLoadTask task = new HistoryLoadTask(); 213 for (PrimitiveId p : notNewPrimitives) { 214 task.add(p); 215 } 216 MainApplication.worker.submit(task); 217 } 218 219 Runnable r = () -> { 220 try { 221 for (PrimitiveId p : notNewPrimitives) { 222 final History h = HistoryDataSet.getInstance().getHistory(p); 223 if (h == null) { 224 continue; 225 } 226 SwingUtilities.invokeLater(() -> show(h)); 227 } 228 } catch (final JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 229 BugReportExceptionHandler.handleException(e); 230 } 231 }; 232 MainApplication.worker.submit(r); 233 } 234}