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