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