source: josm/trunk/src/org/openstreetmap/josm/gui/history/HistoryBrowserDialogManager.java@ 17921

Last change on this file since 17921 was 17471, checked in by GerdP, 3 years ago

see #17184: Memory leaks

  • use String.intern() for field role in RelationMemberData constructor.
  • clear HistoryDataSet when last HistoryBrowserDialog instance is closed

We have route relations with +1000 members and +1000 versions. It's probably not a good idea to create all the RelationMemberData instances in advance. Will open a separate ticket for that.

  • Property svn:eol-style set to native
File size: 9.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.history;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.Point;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Iterator;
13import java.util.LinkedHashMap;
14import java.util.LinkedList;
15import java.util.List;
16import java.util.Map.Entry;
17import java.util.Objects;
18
19import javax.swing.JOptionPane;
20import javax.swing.SwingUtilities;
21
22import org.openstreetmap.josm.data.osm.PrimitiveId;
23import org.openstreetmap.josm.data.osm.history.History;
24import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
25import org.openstreetmap.josm.gui.MainApplication;
26import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
27import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
28import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
29import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
30import org.openstreetmap.josm.gui.util.WindowGeometry;
31import org.openstreetmap.josm.spi.preferences.Config;
32import org.openstreetmap.josm.tools.JosmRuntimeException;
33import org.openstreetmap.josm.tools.Logging;
34import org.openstreetmap.josm.tools.SubclassFilteredCollection;
35import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
36
37/**
38 * Manager allowing to show/hide history dialogs.
39 * @since 2019
40 */
41public final class HistoryBrowserDialogManager implements LayerChangeListener {
42
43 private static boolean isUnloaded(PrimitiveId p) {
44 History h = HistoryDataSet.getInstance().getHistory(p);
45 if (h == null)
46 // reload if the history is not in the cache yet
47 return true;
48 else
49 // reload if the history object of the selected object is not in the cache yet
50 return !p.isNew() && h.getByVersion(p.getUniqueId()) == null;
51 }
52
53 private static final String WINDOW_GEOMETRY_PREF = HistoryBrowserDialogManager.class.getName() + ".geometry";
54
55 private static HistoryBrowserDialogManager instance;
56
57 private final LinkedHashMap<Long, HistoryBrowserDialog> dialogs = new LinkedHashMap<>();
58
59 private static final List<HistoryHook> hooks = new ArrayList<>();
60
61 private HistoryBrowserDialogManager() {
62 MainApplication.getLayerManager().addLayerChangeListener(this);
63 }
64
65 /**
66 * Replies the unique instance.
67 * @return the unique instance
68 */
69 public static synchronized HistoryBrowserDialogManager getInstance() {
70 if (instance == null) {
71 instance = new HistoryBrowserDialogManager();
72 }
73 return instance;
74 }
75
76 /**
77 * Determines if an history dialog exists for the given object id.
78 * @param id the object id
79 * @return {@code true} if an history dialog exists for the given object id, {@code false} otherwise
80 */
81 public boolean existsDialog(long id) {
82 return dialogs.containsKey(id);
83 }
84
85 private boolean hasDialogWithCloseUpperLeftCorner(Point p) {
86 return dialogs.values().stream()
87 .map(Component::getLocation)
88 .anyMatch(corner -> p.x >= corner.x - 5 && corner.x + 5 >= p.x && p.y >= corner.y - 5 && corner.y + 5 >= p.y);
89 }
90
91 private void placeOnScreen(HistoryBrowserDialog dialog) {
92 WindowGeometry geometry = new WindowGeometry(WINDOW_GEOMETRY_PREF, WindowGeometry.centerOnScreen(new Dimension(850, 500)));
93 geometry.applySafe(dialog);
94 Point p = dialog.getLocation();
95 while (hasDialogWithCloseUpperLeftCorner(p)) {
96 p.x += 20;
97 p.y += 20;
98 }
99 dialog.setLocation(p);
100 }
101
102 /**
103 * Hides the specified history dialog and cleans associated resources.
104 * @param dialog History dialog to hide
105 */
106 public void hide(HistoryBrowserDialog dialog) {
107 for (Iterator<Entry<Long, HistoryBrowserDialog>> it = dialogs.entrySet().iterator(); it.hasNext();) {
108 if (Objects.equals(it.next().getValue(), dialog)) {
109 it.remove();
110 if (dialogs.isEmpty()) {
111 new WindowGeometry(dialog).remember(WINDOW_GEOMETRY_PREF);
112 }
113 break;
114 }
115 }
116 dialog.setVisible(false);
117 dialog.dispose();
118
119 if (!dialogs.isEmpty()) {
120 // see #17270: set focus to last dialog
121 new LinkedList<>(dialogs.values()).getLast().toFront();
122 } else {
123 // we always reload the history, so there is no need to keep it in the cache.
124 HistoryDataSet.getInstance().clear();
125 }
126 }
127
128 /**
129 * Hides and destroys all currently visible history browser dialogs
130 * @since 2448
131 */
132 public void hideAll() {
133 new ArrayList<>(dialogs.values()).forEach(this::hide);
134 }
135
136 /**
137 * Show history dialog for the given history.
138 * @param h History to show
139 * @since 2448
140 */
141 public void show(History h) {
142 if (h == null)
143 return;
144 if (existsDialog(h.getId())) {
145 dialogs.get(h.getId()).toFront();
146 } else {
147 HistoryBrowserDialog dialog = new HistoryBrowserDialog(h);
148 placeOnScreen(dialog);
149 dialog.setVisible(true);
150 dialogs.put(h.getId(), dialog);
151 }
152 }
153
154 /* ----------------------------------------------------------------------------- */
155 /* LayerChangeListener */
156 /* ----------------------------------------------------------------------------- */
157 @Override
158 public void layerAdded(LayerAddEvent e) {
159 // Do nothing
160 }
161
162 @Override
163 public void layerRemoving(LayerRemoveEvent e) {
164 // remove all history browsers if the number of layers drops to 0
165 if (e.getSource().getLayers().isEmpty()) {
166 hideAll();
167 }
168 }
169
170 @Override
171 public void layerOrderChanged(LayerOrderChangeEvent e) {
172 // Do nothing
173 }
174
175 /**
176 * Adds a new {@code HistoryHook}.
177 * @param hook hook to add
178 * @return {@code true} (as specified by {@link Collection#add})
179 * @since 13947
180 */
181 public static boolean addHistoryHook(HistoryHook hook) {
182 return hooks.add(Objects.requireNonNull(hook));
183 }
184
185 /**
186 * Removes an existing {@code HistoryHook}.
187 * @param hook hook to remove
188 * @return {@code true} if this list contained the specified element
189 * @since 13947
190 */
191 public static boolean removeHistoryHook(HistoryHook hook) {
192 return hooks.remove(Objects.requireNonNull(hook));
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 showHistory(MainApplication.getMainFrame(), primitives);
201 }
202
203 /**
204 * Show history dialog(s) for the given primitive(s).
205 * @param parent Parent component for displayed dialog boxes
206 * @param primitives The primitive(s) for which history will be displayed
207 * @since 16123
208 */
209 public void showHistory(Component parent, final Collection<? extends PrimitiveId> primitives) {
210 final List<PrimitiveId> realPrimitives = new ArrayList<>(primitives);
211 hooks.forEach(h -> h.modifyRequestedIds(realPrimitives));
212 final Collection<? extends PrimitiveId> notNewPrimitives = SubclassFilteredCollection.filter(realPrimitives, p1 -> !p1.isNew());
213 if (notNewPrimitives.isEmpty()) {
214 JOptionPane.showMessageDialog(
215 parent,
216 tr("Please select at least one already uploaded node, way, or relation."),
217 tr("Warning"),
218 JOptionPane.WARNING_MESSAGE);
219 return;
220 }
221 if (notNewPrimitives.size() > Config.getPref().getInt("warn.open.maxhistory", 5) &&
222 /* I18N english text for value 1 makes no real sense, never called for values <= maxhistory (usually 5) */
223 JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(MainApplication.getMainFrame(),
224 "<html>" + trn(
225 "You are about to open <b>{0}</b> history dialog.<br/>Do you want to continue?",
226 "You are about to open <b>{0}</b> different history dialogs simultaneously.<br/>Do you want to continue?",
227 notNewPrimitives.size(), notNewPrimitives.size()) + "</html>",
228 tr("Confirmation"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE)) {
229 return;
230 }
231
232 Collection<? extends PrimitiveId> toLoad = SubclassFilteredCollection.filter(notNewPrimitives, HistoryBrowserDialogManager::isUnloaded);
233 if (!toLoad.isEmpty()) {
234 MainApplication.worker.submit(new HistoryLoadTask(parent).addPrimitiveIds(toLoad));
235 }
236
237 Runnable r = () -> {
238 try {
239 for (PrimitiveId p : notNewPrimitives) {
240 final History h = HistoryDataSet.getInstance().getHistory(p);
241 if (h == null) {
242 Logging.warn("{0} not found in HistoryDataSet", p);
243 continue;
244 }
245 SwingUtilities.invokeLater(() -> show(h));
246 }
247 } catch (final JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
248 BugReportExceptionHandler.handleException(e);
249 }
250 };
251 MainApplication.worker.submit(r);
252 }
253}
Note: See TracBrowser for help on using the repository browser.