source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ChildRelationBrowser.java@ 14713

Last change on this file since 14713 was 14713, checked in by simon04, 5 years ago

see #14462 - ChildRelationBrowser: add popup menu

  • Property svn:eol-style set to native
File size: 18.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.relation;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.Dialog;
10import java.awt.FlowLayout;
11import java.awt.Point;
12import java.awt.event.ActionEvent;
13import java.awt.event.MouseEvent;
14import java.io.IOException;
15import java.net.HttpURLConnection;
16import java.util.Arrays;
17import java.util.HashSet;
18import java.util.Iterator;
19import java.util.List;
20import java.util.Set;
21import java.util.Stack;
22import java.util.stream.Collectors;
23
24import javax.swing.AbstractAction;
25import javax.swing.JButton;
26import javax.swing.JOptionPane;
27import javax.swing.JPanel;
28import javax.swing.JPopupMenu;
29import javax.swing.JScrollPane;
30import javax.swing.JTree;
31import javax.swing.SwingUtilities;
32import javax.swing.event.TreeSelectionEvent;
33import javax.swing.event.TreeSelectionListener;
34import javax.swing.tree.TreePath;
35
36import org.openstreetmap.josm.data.osm.DataSet;
37import org.openstreetmap.josm.data.osm.DataSetMerger;
38import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
39import org.openstreetmap.josm.data.osm.OsmPrimitive;
40import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
41import org.openstreetmap.josm.data.osm.Relation;
42import org.openstreetmap.josm.data.osm.RelationMember;
43import org.openstreetmap.josm.gui.ExceptionDialogUtil;
44import org.openstreetmap.josm.gui.MainApplication;
45import org.openstreetmap.josm.gui.PleaseWaitRunnable;
46import org.openstreetmap.josm.gui.PopupMenuHandler;
47import org.openstreetmap.josm.gui.layer.OsmDataLayer;
48import org.openstreetmap.josm.gui.progress.ProgressMonitor;
49import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
50import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
51import org.openstreetmap.josm.io.OsmApi;
52import org.openstreetmap.josm.io.OsmApiException;
53import org.openstreetmap.josm.io.OsmServerObjectReader;
54import org.openstreetmap.josm.io.OsmTransferException;
55import org.openstreetmap.josm.tools.CheckParameterUtil;
56import org.openstreetmap.josm.tools.ImageProvider;
57import org.openstreetmap.josm.tools.Logging;
58import org.openstreetmap.josm.tools.Utils;
59import org.xml.sax.SAXException;
60
61/**
62 * ChildRelationBrowser is a UI component which provides a tree-like view on the hierarchical
63 * structure of relations.
64 *
65 * @since 1828
66 */
67public class ChildRelationBrowser extends JPanel {
68 /** the tree with relation children */
69 private RelationTree childTree;
70 /** the tree model */
71 private transient RelationTreeModel model;
72
73 /** the osm data layer this browser is related to */
74 private transient OsmDataLayer layer;
75
76 /** the editAction used in the bottom panel and for doubleClick */
77 private EditAction editAction;
78
79 /**
80 * Replies the {@link OsmDataLayer} this editor is related to
81 *
82 * @return the osm data layer
83 */
84 protected OsmDataLayer getLayer() {
85 return layer;
86 }
87
88 /**
89 * builds the UI
90 */
91 protected void build() {
92 setLayout(new BorderLayout());
93 childTree = new RelationTree(model);
94 JScrollPane pane = new JScrollPane(childTree);
95 add(pane, BorderLayout.CENTER);
96
97 final JPopupMenu popupMenu = new JPopupMenu();
98 final PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
99 RelationPopupMenus.setupHandler(popupMenuHandler);
100
101 add(buildButtonPanel(), BorderLayout.SOUTH);
102 childTree.setToggleClickCount(0);
103 childTree.addMouseListener(new PopupMenuLauncher(popupMenu) {
104 @Override
105 public void mouseClicked(MouseEvent e) {
106 if (e.getClickCount() == 2
107 && !e.isAltDown() && !e.isAltGraphDown() && !e.isControlDown() && !e.isMetaDown() && !e.isShiftDown()
108 && childTree.getRowForLocation(e.getX(), e.getY()) == childTree.getMinSelectionRow()) {
109 Relation r = (Relation) childTree.getLastSelectedPathComponent();
110 if (r != null && r.isIncomplete()) {
111 childTree.expandPath(childTree.getSelectionPath());
112 } else {
113 editAction.actionPerformed(new ActionEvent(e.getSource(), ActionEvent.ACTION_PERFORMED, null));
114 }
115 }
116 }
117
118 @Override
119 protected TreePath checkTreeSelection(JTree tree, Point p) {
120 final TreePath treeSelection = super.checkTreeSelection(tree, p);
121 final TreePath[] selectionPaths = tree.getSelectionPaths();
122 if (selectionPaths == null) {
123 return treeSelection;
124 }
125 final List<OsmPrimitive> relations = Arrays.stream(selectionPaths)
126 .map(TreePath::getLastPathComponent)
127 .map(OsmPrimitive.class::cast)
128 .collect(Collectors.toList());
129 popupMenuHandler.setPrimitives(relations);
130 return treeSelection;
131 }
132 });
133 }
134
135 /**
136 * builds the panel with the command buttons
137 *
138 * @return the button panel
139 */
140 protected JPanel buildButtonPanel() {
141 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
142
143 // ---
144 DownloadAllChildRelationsAction downloadAction = new DownloadAllChildRelationsAction();
145 pnl.add(new JButton(downloadAction));
146
147 // ---
148 DownloadSelectedAction downloadSelectedAction = new DownloadSelectedAction();
149 childTree.addTreeSelectionListener(downloadSelectedAction);
150 pnl.add(new JButton(downloadSelectedAction));
151
152 // ---
153 editAction = new EditAction();
154 childTree.addTreeSelectionListener(editAction);
155 pnl.add(new JButton(editAction));
156
157 return pnl;
158 }
159
160 /**
161 * constructor
162 *
163 * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null.
164 * @throws IllegalArgumentException if layer is null
165 */
166 public ChildRelationBrowser(OsmDataLayer layer) {
167 CheckParameterUtil.ensureParameterNotNull(layer, "layer");
168 this.layer = layer;
169 model = new RelationTreeModel();
170 build();
171 }
172
173 /**
174 * constructor
175 *
176 * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null.
177 * @param root the root relation
178 * @throws IllegalArgumentException if layer is null
179 */
180 public ChildRelationBrowser(OsmDataLayer layer, Relation root) {
181 this(layer);
182 populate(root);
183 }
184
185 /**
186 * populates the browser with a relation
187 *
188 * @param r the relation
189 */
190 public void populate(Relation r) {
191 model.populate(r);
192 }
193
194 /**
195 * populates the browser with a list of relation members
196 *
197 * @param members the list of relation members
198 */
199
200 public void populate(List<RelationMember> members) {
201 model.populate(members);
202 }
203
204 /**
205 * replies the parent dialog this browser is embedded in
206 *
207 * @return the parent dialog; null, if there is no {@link Dialog} as parent dialog
208 */
209 protected Dialog getParentDialog() {
210 Component c = this;
211 while (c != null && !(c instanceof Dialog)) {
212 c = c.getParent();
213 }
214 return (Dialog) c;
215 }
216
217 /**
218 * Action for editing the currently selected relation
219 *
220 *
221 */
222 class EditAction extends AbstractAction implements TreeSelectionListener {
223 EditAction() {
224 putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to"));
225 new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true);
226 putValue(NAME, tr("Edit"));
227 refreshEnabled();
228 }
229
230 protected void refreshEnabled() {
231 TreePath[] selection = childTree.getSelectionPaths();
232 setEnabled(selection != null && selection.length > 0);
233 }
234
235 public void run() {
236 TreePath[] selection = childTree.getSelectionPaths();
237 if (selection == null || selection.length == 0) return;
238 // do not launch more than 10 relation editors in parallel
239 //
240 for (int i = 0; i < Math.min(selection.length, 10); i++) {
241 Relation r = (Relation) selection[i].getLastPathComponent();
242 if (r.isIncomplete()) {
243 continue;
244 }
245 RelationEditor editor = RelationEditor.getEditor(getLayer(), r, null);
246 editor.setVisible(true);
247 }
248 }
249
250 @Override
251 public void actionPerformed(ActionEvent e) {
252 if (!isEnabled())
253 return;
254 run();
255 }
256
257 @Override
258 public void valueChanged(TreeSelectionEvent e) {
259 refreshEnabled();
260 }
261 }
262
263 /**
264 * Action for downloading all child relations for a given parent relation.
265 * Recursively.
266 */
267 class DownloadAllChildRelationsAction extends AbstractAction {
268 DownloadAllChildRelationsAction() {
269 putValue(SHORT_DESCRIPTION, tr("Download all child relations (recursively)"));
270 new ImageProvider("download").getResource().attachImageIcon(this, true);
271 putValue(NAME, tr("Download All Children"));
272 }
273
274 public void run() {
275 MainApplication.worker.submit(new DownloadAllChildrenTask(getParentDialog(), (Relation) model.getRoot()));
276 }
277
278 @Override
279 public void actionPerformed(ActionEvent e) {
280 if (!isEnabled())
281 return;
282 run();
283 }
284 }
285
286 /**
287 * Action for downloading all selected relations
288 */
289 class DownloadSelectedAction extends AbstractAction implements TreeSelectionListener {
290 DownloadSelectedAction() {
291 putValue(SHORT_DESCRIPTION, tr("Download selected relations"));
292 // FIXME: replace with better icon
293 new ImageProvider("download").getResource().attachImageIcon(this, true);
294 putValue(NAME, tr("Download Selected Children"));
295 updateEnabledState();
296 }
297
298 protected void updateEnabledState() {
299 TreePath[] selection = childTree.getSelectionPaths();
300 setEnabled(selection != null && selection.length > 0);
301 }
302
303 public void run() {
304 TreePath[] selection = childTree.getSelectionPaths();
305 if (selection == null || selection.length == 0)
306 return;
307 Set<Relation> relations = new HashSet<>();
308 for (TreePath aSelection : selection) {
309 relations.add((Relation) aSelection.getLastPathComponent());
310 }
311 MainApplication.worker.submit(new DownloadRelationSetTask(getParentDialog(), relations));
312 }
313
314 @Override
315 public void actionPerformed(ActionEvent e) {
316 if (!isEnabled())
317 return;
318 run();
319 }
320
321 @Override
322 public void valueChanged(TreeSelectionEvent e) {
323 updateEnabledState();
324 }
325 }
326
327 abstract class DownloadTask extends PleaseWaitRunnable {
328 protected boolean canceled;
329 protected int conflictsCount;
330 protected Exception lastException;
331
332 DownloadTask(String title, Dialog parent) {
333 super(title, new PleaseWaitProgressMonitor(parent), false);
334 }
335
336 @Override
337 protected void cancel() {
338 canceled = true;
339 OsmApi.getOsmApi().cancel();
340 }
341
342 protected void refreshView(Relation relation) {
343 for (int i = 0; i < childTree.getRowCount(); i++) {
344 Relation reference = (Relation) childTree.getPathForRow(i).getLastPathComponent();
345 if (reference == relation) {
346 model.refreshNode(childTree.getPathForRow(i));
347 }
348 }
349 }
350
351 @Override
352 protected void finish() {
353 if (canceled)
354 return;
355 if (lastException != null) {
356 ExceptionDialogUtil.explainException(lastException);
357 return;
358 }
359
360 if (conflictsCount > 0) {
361 JOptionPane.showMessageDialog(
362 MainApplication.getMainFrame(),
363 trn("There was {0} conflict during import.",
364 "There were {0} conflicts during import.",
365 conflictsCount, conflictsCount),
366 trn("Conflict in data", "Conflicts in data", conflictsCount),
367 JOptionPane.WARNING_MESSAGE
368 );
369 }
370 }
371 }
372
373 /**
374 * The asynchronous task for downloading relation members.
375 */
376 class DownloadAllChildrenTask extends DownloadTask {
377 private final Stack<Relation> relationsToDownload;
378 private final Set<Long> downloadedRelationIds;
379
380 DownloadAllChildrenTask(Dialog parent, Relation r) {
381 super(tr("Download relation members"), parent);
382 relationsToDownload = new Stack<>();
383 downloadedRelationIds = new HashSet<>();
384 relationsToDownload.push(r);
385 }
386
387 /**
388 * warns the user if a relation couldn't be loaded because it was deleted on
389 * the server (the server replied a HTTP code 410)
390 *
391 * @param r the relation
392 */
393 protected void warnBecauseOfDeletedRelation(Relation r) {
394 String message = tr("<html>The child relation<br>"
395 + "{0}<br>"
396 + "is deleted on the server. It cannot be loaded</html>",
397 Utils.escapeReservedCharactersHTML(r.getDisplayName(DefaultNameFormatter.getInstance()))
398 );
399
400 JOptionPane.showMessageDialog(
401 MainApplication.getMainFrame(),
402 message,
403 tr("Relation is deleted"),
404 JOptionPane.WARNING_MESSAGE
405 );
406 }
407
408 /**
409 * Remembers the child relations to download
410 *
411 * @param parent the parent relation
412 */
413 protected void rememberChildRelationsToDownload(Relation parent) {
414 downloadedRelationIds.add(parent.getId());
415 for (RelationMember member: parent.getMembers()) {
416 if (member.isRelation()) {
417 Relation child = member.getRelation();
418 if (!downloadedRelationIds.contains(child.getId())) {
419 relationsToDownload.push(child);
420 }
421 }
422 }
423 }
424
425 /**
426 * Merges the primitives in <code>ds</code> to the dataset of the edit layer
427 *
428 * @param ds the data set
429 */
430 protected void mergeDataSet(DataSet ds) {
431 if (ds != null) {
432 final DataSetMerger visitor = new DataSetMerger(getLayer().getDataSet(), ds);
433 visitor.merge();
434 if (!visitor.getConflicts().isEmpty()) {
435 getLayer().getConflicts().add(visitor.getConflicts());
436 conflictsCount += visitor.getConflicts().size();
437 }
438 }
439 }
440
441 @Override
442 protected void realRun() throws SAXException, IOException, OsmTransferException {
443 try {
444 while (!relationsToDownload.isEmpty() && !canceled) {
445 Relation r = relationsToDownload.pop();
446 if (r.isNew()) {
447 continue;
448 }
449 rememberChildRelationsToDownload(r);
450 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance())));
451 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION,
452 true);
453 DataSet dataSet = null;
454 try {
455 dataSet = reader.parseOsm(progressMonitor
456 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
457 } catch (OsmApiException e) {
458 if (e.getResponseCode() == HttpURLConnection.HTTP_GONE) {
459 warnBecauseOfDeletedRelation(r);
460 continue;
461 }
462 throw e;
463 }
464 mergeDataSet(dataSet);
465 refreshView(r);
466 }
467 SwingUtilities.invokeLater(MainApplication.getMap()::repaint);
468 } catch (OsmTransferException e) {
469 if (canceled) {
470 Logging.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString()));
471 return;
472 }
473 lastException = e;
474 }
475 }
476 }
477
478 /**
479 * The asynchronous task for downloading a set of relations
480 */
481 class DownloadRelationSetTask extends DownloadTask {
482 private final Set<Relation> relations;
483
484 DownloadRelationSetTask(Dialog parent, Set<Relation> relations) {
485 super(tr("Download relation members"), parent);
486 this.relations = relations;
487 }
488
489 protected void mergeDataSet(DataSet dataSet) {
490 if (dataSet != null) {
491 final DataSetMerger visitor = new DataSetMerger(getLayer().getDataSet(), dataSet);
492 visitor.merge();
493 if (!visitor.getConflicts().isEmpty()) {
494 getLayer().getConflicts().add(visitor.getConflicts());
495 conflictsCount += visitor.getConflicts().size();
496 }
497 }
498 }
499
500 @Override
501 protected void realRun() throws SAXException, IOException, OsmTransferException {
502 try {
503 Iterator<Relation> it = relations.iterator();
504 while (it.hasNext() && !canceled) {
505 Relation r = it.next();
506 if (r.isNew()) {
507 continue;
508 }
509 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance())));
510 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION,
511 true);
512 DataSet dataSet = reader.parseOsm(progressMonitor
513 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
514 mergeDataSet(dataSet);
515 refreshView(r);
516 }
517 } catch (OsmTransferException e) {
518 if (canceled) {
519 Logging.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString()));
520 return;
521 }
522 lastException = e;
523 }
524 }
525 }
526}
Note: See TracBrowser for help on using the repository browser.