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

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

fix #4304 - Improve error fixing performance by calling DataSet.beginUpdate/endUpdate

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