source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java@ 7047

Last change on this file since 7047 was 7047, checked in by Don-vip, 10 years ago

fix #7042 - Add "call relation editor" to context menu of validator dialog

  • Property svn:eol-style set to native
File size: 23.2 KB
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.ActionEvent;
7import java.awt.event.KeyEvent;
8import java.awt.event.MouseEvent;
9import java.io.IOException;
10import java.lang.reflect.InvocationTargetException;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Enumeration;
14import java.util.HashSet;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.AbstractAction;
20import javax.swing.JComponent;
21import javax.swing.JOptionPane;
22import javax.swing.JPopupMenu;
23import javax.swing.SwingUtilities;
24import javax.swing.event.TreeSelectionEvent;
25import javax.swing.event.TreeSelectionListener;
26import javax.swing.tree.DefaultMutableTreeNode;
27import javax.swing.tree.TreePath;
28
29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.actions.AutoScaleAction;
31import org.openstreetmap.josm.actions.relation.EditRelationAction;
32import org.openstreetmap.josm.command.Command;
33import org.openstreetmap.josm.data.SelectionChangedListener;
34import org.openstreetmap.josm.data.osm.DataSet;
35import org.openstreetmap.josm.data.osm.Node;
36import org.openstreetmap.josm.data.osm.OsmPrimitive;
37import org.openstreetmap.josm.data.osm.WaySegment;
38import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
39import org.openstreetmap.josm.data.validation.OsmValidator;
40import org.openstreetmap.josm.data.validation.TestError;
41import org.openstreetmap.josm.data.validation.ValidatorVisitor;
42import org.openstreetmap.josm.gui.MapView;
43import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
44import org.openstreetmap.josm.gui.PleaseWaitRunnable;
45import org.openstreetmap.josm.gui.PopupMenuHandler;
46import org.openstreetmap.josm.gui.SideButton;
47import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
48import org.openstreetmap.josm.gui.layer.Layer;
49import org.openstreetmap.josm.gui.layer.OsmDataLayer;
50import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
51import org.openstreetmap.josm.gui.progress.ProgressMonitor;
52import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
53import org.openstreetmap.josm.io.OsmTransferException;
54import org.openstreetmap.josm.tools.ImageProvider;
55import org.openstreetmap.josm.tools.InputMapUtils;
56import org.openstreetmap.josm.tools.Shortcut;
57import org.xml.sax.SAXException;
58
59/**
60 * A small tool dialog for displaying the current errors. The selection manager
61 * respects clicks into the selection list. Ctrl-click will remove entries from
62 * the list while single click will make the clicked entry the only selection.
63 *
64 * @author frsantos
65 */
66public class ValidatorDialog extends ToggleDialog implements SelectionChangedListener, LayerChangeListener {
67
68 /** The display tree */
69 public ValidatorTreePanel tree;
70
71 /** The fix button */
72 private SideButton fixButton;
73 /** The ignore button */
74 private SideButton ignoreButton;
75 /** The select button */
76 private SideButton selectButton;
77
78 private final JPopupMenu popupMenu = new JPopupMenu();
79 private final PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
80
81 /** Last selected element */
82 private DefaultMutableTreeNode lastSelectedNode = null;
83
84 private OsmDataLayer linkedLayer;
85
86 /**
87 * Constructor
88 */
89 public ValidatorDialog() {
90 super(tr("Validation Results"), "validator", tr("Open the validation window."),
91 Shortcut.registerShortcut("subwindow:validator", tr("Toggle: {0}", tr("Validation results")),
92 KeyEvent.VK_V, Shortcut.ALT_SHIFT), 150, false, ValidatorPreference.class);
93
94 popupMenuHandler.addAction(Main.main.menu.autoScaleActions.get("problem"));
95 popupMenuHandler.addAction(new EditRelationAction());
96
97 tree = new ValidatorTreePanel();
98 tree.addMouseListener(new MouseEventHandler());
99 addTreeSelectionListener(new SelectionWatch());
100 InputMapUtils.unassignCtrlShiftUpDown(tree, JComponent.WHEN_FOCUSED);
101
102 List<SideButton> buttons = new LinkedList<>();
103
104 selectButton = new SideButton(new AbstractAction() {
105 {
106 putValue(NAME, tr("Select"));
107 putValue(SHORT_DESCRIPTION, tr("Set the selected elements on the map to the selected items in the list above."));
108 putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
109 }
110 @Override
111 public void actionPerformed(ActionEvent e) {
112 setSelectedItems();
113 }
114 });
115 InputMapUtils.addEnterAction(tree, selectButton.getAction());
116
117 selectButton.setEnabled(false);
118 buttons.add(selectButton);
119
120 buttons.add(new SideButton(Main.main.validator.validateAction));
121
122 fixButton = new SideButton(new AbstractAction() {
123 {
124 putValue(NAME, tr("Fix"));
125 putValue(SHORT_DESCRIPTION, tr("Fix the selected issue."));
126 putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
127 }
128 @Override
129 public void actionPerformed(ActionEvent e) {
130 fixErrors();
131 }
132 });
133 fixButton.setEnabled(false);
134 buttons.add(fixButton);
135
136 if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
137 ignoreButton = new SideButton(new AbstractAction() {
138 {
139 putValue(NAME, tr("Ignore"));
140 putValue(SHORT_DESCRIPTION, tr("Ignore the selected issue next time."));
141 putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
142 }
143 @Override
144 public void actionPerformed(ActionEvent e) {
145 ignoreErrors();
146 }
147 });
148 ignoreButton.setEnabled(false);
149 buttons.add(ignoreButton);
150 } else {
151 ignoreButton = null;
152 }
153 createLayout(tree, true, buttons);
154 }
155
156 @Override
157 public void showNotify() {
158 DataSet.addSelectionListener(this);
159 DataSet ds = Main.main.getCurrentDataSet();
160 if (ds != null) {
161 updateSelection(ds.getAllSelected());
162 }
163 MapView.addLayerChangeListener(this);
164 Layer activeLayer = Main.map.mapView.getActiveLayer();
165 if (activeLayer != null) {
166 activeLayerChange(null, activeLayer);
167 }
168 }
169
170 @Override
171 public void hideNotify() {
172 MapView.removeLayerChangeListener(this);
173 DataSet.removeSelectionListener(this);
174 }
175
176 @Override
177 public void setVisible(boolean v) {
178 if (tree != null) {
179 tree.setVisible(v);
180 }
181 super.setVisible(v);
182 Main.map.repaint();
183 }
184
185 /**
186 * Fix selected errors
187 */
188 @SuppressWarnings("unchecked")
189 private void fixErrors() {
190 TreePath[] selectionPaths = tree.getSelectionPaths();
191 if (selectionPaths == null)
192 return;
193
194 Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
195
196 LinkedList<TestError> errorsToFix = new LinkedList<>();
197 for (TreePath path : selectionPaths) {
198 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
199 if (node == null) {
200 continue;
201 }
202
203 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
204 while (children.hasMoreElements()) {
205 DefaultMutableTreeNode childNode = children.nextElement();
206 if (processedNodes.contains(childNode)) {
207 continue;
208 }
209
210 processedNodes.add(childNode);
211 Object nodeInfo = childNode.getUserObject();
212 if (nodeInfo instanceof TestError) {
213 errorsToFix.add((TestError)nodeInfo);
214 }
215 }
216 }
217
218 // run fix task asynchronously
219 //
220 FixTask fixTask = new FixTask(errorsToFix);
221 Main.worker.submit(fixTask);
222 }
223
224 /**
225 * Set selected errors to ignore state
226 */
227 @SuppressWarnings("unchecked")
228 private void ignoreErrors() {
229 int asked = JOptionPane.DEFAULT_OPTION;
230 boolean changed = false;
231 TreePath[] selectionPaths = tree.getSelectionPaths();
232 if (selectionPaths == null)
233 return;
234
235 Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
236 for (TreePath path : selectionPaths) {
237 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
238 if (node == null) {
239 continue;
240 }
241
242 Object mainNodeInfo = node.getUserObject();
243 if (!(mainNodeInfo instanceof TestError)) {
244 Set<String> state = new HashSet<>();
245 // ask if the whole set should be ignored
246 if (asked == JOptionPane.DEFAULT_OPTION) {
247 String[] a = new String[] { tr("Whole group"), tr("Single elements"), tr("Nothing") };
248 asked = JOptionPane.showOptionDialog(Main.parent, tr("Ignore whole group or individual elements?"),
249 tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
250 a, a[1]);
251 }
252 if (asked == JOptionPane.YES_NO_OPTION) {
253 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
254 while (children.hasMoreElements()) {
255 DefaultMutableTreeNode childNode = children.nextElement();
256 if (processedNodes.contains(childNode)) {
257 continue;
258 }
259
260 processedNodes.add(childNode);
261 Object nodeInfo = childNode.getUserObject();
262 if (nodeInfo instanceof TestError) {
263 TestError err = (TestError) nodeInfo;
264 err.setIgnored(true);
265 changed = true;
266 state.add(node.getDepth() == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup());
267 }
268 }
269 for (String s : state) {
270 OsmValidator.addIgnoredError(s);
271 }
272 continue;
273 } else if (asked == JOptionPane.CANCEL_OPTION) {
274 continue;
275 }
276 }
277
278 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
279 while (children.hasMoreElements()) {
280 DefaultMutableTreeNode childNode = children.nextElement();
281 if (processedNodes.contains(childNode)) {
282 continue;
283 }
284
285 processedNodes.add(childNode);
286 Object nodeInfo = childNode.getUserObject();
287 if (nodeInfo instanceof TestError) {
288 TestError error = (TestError) nodeInfo;
289 String state = error.getIgnoreState();
290 if (state != null) {
291 OsmValidator.addIgnoredError(state);
292 }
293 changed = true;
294 error.setIgnored(true);
295 }
296 }
297 }
298 if (changed) {
299 tree.resetErrors();
300 OsmValidator.saveIgnoredErrors();
301 Main.map.repaint();
302 }
303 }
304
305 /**
306 * Sets the selection of the map to the current selected items.
307 */
308 @SuppressWarnings("unchecked")
309 private void setSelectedItems() {
310 if (tree == null)
311 return;
312
313 Collection<OsmPrimitive> sel = new HashSet<>(40);
314
315 TreePath[] selectedPaths = tree.getSelectionPaths();
316 if (selectedPaths == null)
317 return;
318
319 for (TreePath path : selectedPaths) {
320 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
321 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
322 while (children.hasMoreElements()) {
323 DefaultMutableTreeNode childNode = children.nextElement();
324 Object nodeInfo = childNode.getUserObject();
325 if (nodeInfo instanceof TestError) {
326 TestError error = (TestError) nodeInfo;
327 sel.addAll(error.getSelectablePrimitives());
328 }
329 }
330 }
331 DataSet ds = Main.main.getCurrentDataSet();
332 if (ds != null) {
333 ds.setSelected(sel);
334 }
335 }
336
337 /**
338 * Checks for fixes in selected element and, if needed, adds to the sel
339 * parameter all selected elements
340 *
341 * @param sel
342 * The collection where to add all selected elements
343 * @param addSelected
344 * if true, add all selected elements to collection
345 * @return whether the selected elements has any fix
346 */
347 @SuppressWarnings("unchecked")
348 private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
349 boolean hasFixes = false;
350
351 DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
352 if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
353 Enumeration<DefaultMutableTreeNode> children = lastSelectedNode.breadthFirstEnumeration();
354 while (children.hasMoreElements()) {
355 DefaultMutableTreeNode childNode = children.nextElement();
356 Object nodeInfo = childNode.getUserObject();
357 if (nodeInfo instanceof TestError) {
358 TestError error = (TestError) nodeInfo;
359 error.setSelected(false);
360 }
361 }
362 }
363
364 lastSelectedNode = node;
365 if (node == null)
366 return hasFixes;
367
368 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
369 while (children.hasMoreElements()) {
370 DefaultMutableTreeNode childNode = children.nextElement();
371 Object nodeInfo = childNode.getUserObject();
372 if (nodeInfo instanceof TestError) {
373 TestError error = (TestError) nodeInfo;
374 error.setSelected(true);
375
376 hasFixes = hasFixes || error.isFixable();
377 if (addSelected) {
378 sel.addAll(error.getSelectablePrimitives());
379 }
380 }
381 }
382 selectButton.setEnabled(true);
383 if (ignoreButton != null) {
384 ignoreButton.setEnabled(true);
385 }
386
387 return hasFixes;
388 }
389
390 @Override
391 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
392 if (newLayer instanceof OsmDataLayer) {
393 linkedLayer = (OsmDataLayer)newLayer;
394 tree.setErrorList(linkedLayer.validationErrors);
395 }
396 }
397
398 @Override
399 public void layerAdded(Layer newLayer) {}
400
401 @Override
402 public void layerRemoved(Layer oldLayer) {
403 if (oldLayer == linkedLayer) {
404 tree.setErrorList(new ArrayList<TestError>());
405 }
406 }
407
408 /**
409 * Add a tree selection listener to the validator tree.
410 * @param listener the TreeSelectionListener
411 * @since 5958
412 */
413 public void addTreeSelectionListener(TreeSelectionListener listener) {
414 tree.addTreeSelectionListener(listener);
415 }
416
417 /**
418 * Remove the given tree selection listener from the validator tree.
419 * @param listener the TreeSelectionListener
420 * @since 5958
421 */
422 public void removeTreeSelectionListener(TreeSelectionListener listener) {
423 tree.removeTreeSelectionListener(listener);
424 }
425
426 /**
427 * Replies the popup menu handler.
428 * @return The popup menu handler
429 * @since 5958
430 */
431 public PopupMenuHandler getPopupMenuHandler() {
432 return popupMenuHandler;
433 }
434
435 /**
436 * Replies the currently selected error, or {@code null}.
437 * @return The selected error, if any.
438 * @since 5958
439 */
440 public TestError getSelectedError() {
441 Object comp = tree.getLastSelectedPathComponent();
442 if (comp instanceof DefaultMutableTreeNode) {
443 Object object = ((DefaultMutableTreeNode)comp).getUserObject();
444 if (object instanceof TestError) {
445 return (TestError) object;
446 }
447 }
448 return null;
449 }
450
451 /**
452 * Watches for double clicks and launches the popup menu.
453 */
454 class MouseEventHandler extends PopupMenuLauncher {
455
456 public MouseEventHandler() {
457 super(popupMenu);
458 }
459
460 @Override
461 public void mouseClicked(MouseEvent e) {
462 fixButton.setEnabled(false);
463 if (ignoreButton != null) {
464 ignoreButton.setEnabled(false);
465 }
466 selectButton.setEnabled(false);
467
468 boolean isDblClick = isDoubleClick(e);
469
470 Collection<OsmPrimitive> sel = isDblClick ? new HashSet<OsmPrimitive>(40) : null;
471
472 boolean hasFixes = setSelection(sel, isDblClick);
473 fixButton.setEnabled(hasFixes);
474
475 if (isDblClick) {
476 Main.main.getCurrentDataSet().setSelected(sel);
477 if (Main.pref.getBoolean("validator.autozoom", false)) {
478 AutoScaleAction.zoomTo(sel);
479 }
480 }
481 }
482
483 @Override public void launch(MouseEvent e) {
484 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
485 if (selPath == null)
486 return;
487 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
488 if (!(node.getUserObject() instanceof TestError))
489 return;
490 super.launch(e);
491 }
492
493 }
494
495 /**
496 * Watches for tree selection.
497 */
498 public class SelectionWatch implements TreeSelectionListener {
499 @Override
500 public void valueChanged(TreeSelectionEvent e) {
501 fixButton.setEnabled(false);
502 if (ignoreButton != null) {
503 ignoreButton.setEnabled(false);
504 }
505 selectButton.setEnabled(false);
506
507 Collection<OsmPrimitive> sel = new HashSet<>();
508 boolean hasFixes = setSelection(sel, true);
509 fixButton.setEnabled(hasFixes);
510 popupMenuHandler.setPrimitives(sel);
511 if (Main.map != null) {
512 Main.map.repaint();
513 }
514 }
515 }
516
517 public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor {
518 @Override
519 public void visit(OsmPrimitive p) {
520 if (p.isUsable()) {
521 p.accept(this);
522 }
523 }
524
525 @Override
526 public void visit(WaySegment ws) {
527 if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
528 return;
529 visit(ws.way.getNodes().get(ws.lowerIndex));
530 visit(ws.way.getNodes().get(ws.lowerIndex + 1));
531 }
532
533 @Override
534 public void visit(List<Node> nodes) {
535 for (Node n: nodes) {
536 visit(n);
537 }
538 }
539
540 @Override
541 public void visit(TestError error) {
542 if (error != null) {
543 error.visitHighlighted(this);
544 }
545 }
546 }
547
548 public void updateSelection(Collection<? extends OsmPrimitive> newSelection) {
549 if (!Main.pref.getBoolean(ValidatorPreference.PREF_FILTER_BY_SELECTION, false))
550 return;
551 if (newSelection.isEmpty()) {
552 tree.setFilter(null);
553 }
554 HashSet<OsmPrimitive> filter = new HashSet<>(newSelection);
555 tree.setFilter(filter);
556 }
557
558 @Override
559 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
560 updateSelection(newSelection);
561 }
562
563 /**
564 * Task for fixing a collection of {@link TestError}s. Can be run asynchronously.
565 *
566 *
567 */
568 class FixTask extends PleaseWaitRunnable {
569 private Collection<TestError> testErrors;
570 private boolean canceled;
571
572 public FixTask(Collection<TestError> testErrors) {
573 super(tr("Fixing errors ..."), false /* don't ignore exceptions */);
574 this.testErrors = testErrors == null ? new ArrayList<TestError> (): testErrors;
575 }
576
577 @Override
578 protected void cancel() {
579 this.canceled = true;
580 }
581
582 @Override
583 protected void finish() {
584 // do nothing
585 }
586
587 protected void fixError(TestError error) throws InterruptedException, InvocationTargetException {
588 if (error.isFixable()) {
589 final Command fixCommand = error.getFix();
590 if (fixCommand != null) {
591 SwingUtilities.invokeAndWait(new Runnable() {
592 @Override
593 public void run() {
594 Main.main.undoRedo.addNoRedraw(fixCommand);
595 }
596 });
597 }
598 // It is wanted to ignore an error if it said fixable, even if fixCommand was null
599 // This is to fix #5764 and #5773: a delete command, for example, may be null if all concerned primitives have already been deleted
600 error.setIgnored(true);
601 }
602 }
603
604 @Override
605 protected void realRun() throws SAXException, IOException,
606 OsmTransferException {
607 ProgressMonitor monitor = getProgressMonitor();
608 try {
609 monitor.setTicksCount(testErrors.size());
610 int i=0;
611 SwingUtilities.invokeAndWait(new Runnable() {
612 @Override
613 public void run() {
614 Main.main.getCurrentDataSet().beginUpdate();
615 }
616 });
617 try {
618 for (TestError error: testErrors) {
619 i++;
620 monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(),error.getMessage()));
621 if (this.canceled)
622 return;
623 fixError(error);
624 monitor.worked(1);
625 }
626 } finally {
627 SwingUtilities.invokeAndWait(new Runnable() {
628 @Override
629 public void run() {
630 Main.main.getCurrentDataSet().endUpdate();
631 }
632 });
633 }
634 monitor.subTask(tr("Updating map ..."));
635 SwingUtilities.invokeAndWait(new Runnable() {
636 @Override
637 public void run() {
638 Main.main.undoRedo.afterAdd();
639 Main.map.repaint();
640 tree.resetErrors();
641 Main.main.getCurrentDataSet().fireSelectionChanged();
642 }
643 });
644 } catch(InterruptedException | InvocationTargetException e) {
645 // FIXME: signature of realRun should have a generic checked exception we
646 // could throw here
647 throw new RuntimeException(e);
648 } finally {
649 monitor.finishTask();
650 }
651 }
652 }
653}
Note: See TracBrowser for help on using the repository browser.