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