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