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.JosmRuntimeException; 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, ActiveLayerChangeListener { 069 070 /** The display tree */ 071 public ValidatorTreePanel tree; 072 073 /** The fix button */ 074 private final SideButton fixButton; 075 /** The ignore button */ 076 private final SideButton ignoreButton; 077 /** The select button */ 078 private final SideButton selectButton; 079 /** The lookup button */ 080 private final 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 new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true); 124 } 125 126 @Override 127 public void actionPerformed(ActionEvent e) { 128 final DataSet ds = Main.getLayerManager().getEditDataSet(); 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(OsmValidator.validateAction)); 139 140 fixButton = new SideButton(new AbstractAction() { 141 { 142 putValue(NAME, tr("Fix")); 143 putValue(SHORT_DESCRIPTION, tr("Fix the selected issue.")); 144 new ImageProvider("dialogs", "fix").getResource().attachImageIcon(this, true); 145 } 146 @Override 147 public void actionPerformed(ActionEvent e) { 148 fixErrors(); 149 } 150 }); 151 fixButton.setEnabled(false); 152 buttons.add(fixButton); 153 154 if (ValidatorPreference.PREF_USE_IGNORE.get()) { 155 ignoreButton = new SideButton(new AbstractAction() { 156 { 157 putValue(NAME, tr("Ignore")); 158 putValue(SHORT_DESCRIPTION, tr("Ignore the selected issue next time.")); 159 new ImageProvider("dialogs", "fix").getResource().attachImageIcon(this, true); 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.getLayerManager().getEditDataSet(); 178 if (ds != null) { 179 updateSelection(ds.getAllSelected()); 180 } 181 Main.getLayerManager().addAndFireActiveLayerChangeListener(this); 182 } 183 184 @Override 185 public void hideNotify() { 186 Main.getLayerManager().removeActiveLayerChangeListener(this); 187 DataSet.removeSelectionListener(this); 188 } 189 190 @Override 191 public void setVisible(boolean v) { 192 if (tree != null) { 193 tree.setVisible(v); 194 } 195 super.setVisible(v); 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 error.getPrimitives().stream() 341 .filter(OsmPrimitive::isSelectable) 342 .forEach(sel::add); 343 } 344 } 345 } 346 DataSet ds = Main.getLayerManager().getEditDataSet(); 347 if (ds != null) { 348 ds.setSelected(sel); 349 } 350 } 351 352 /** 353 * Checks for fixes in selected element and, if needed, adds to the sel 354 * parameter all selected elements 355 * 356 * @param sel 357 * The collection where to add all selected elements 358 * @param addSelected 359 * if true, add all selected elements to collection 360 * @return whether the selected elements has any fix 361 */ 362 @SuppressWarnings("unchecked") 363 private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) { 364 boolean hasFixes = false; 365 366 DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); 367 if (lastSelectedNode != null && !lastSelectedNode.equals(node)) { 368 Enumeration<TreeNode> children = lastSelectedNode.breadthFirstEnumeration(); 369 while (children.hasMoreElements()) { 370 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement(); 371 Object nodeInfo = childNode.getUserObject(); 372 if (nodeInfo instanceof TestError) { 373 TestError error = (TestError) nodeInfo; 374 error.setSelected(false); 375 } 376 } 377 } 378 379 lastSelectedNode = node; 380 if (node == null) 381 return hasFixes; 382 383 Enumeration<TreeNode> children = node.breadthFirstEnumeration(); 384 while (children.hasMoreElements()) { 385 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement(); 386 Object nodeInfo = childNode.getUserObject(); 387 if (nodeInfo instanceof TestError) { 388 TestError error = (TestError) nodeInfo; 389 error.setSelected(true); 390 391 hasFixes = hasFixes || error.isFixable(); 392 if (addSelected) { 393 error.getPrimitives().stream() 394 .filter(OsmPrimitive::isSelectable) 395 .forEach(sel::add); 396 } 397 } 398 } 399 selectButton.setEnabled(true); 400 if (ignoreButton != null) { 401 ignoreButton.setEnabled(true); 402 } 403 404 return hasFixes; 405 } 406 407 @Override 408 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 409 OsmDataLayer editLayer = e.getSource().getEditLayer(); 410 if (editLayer == null) { 411 tree.setErrorList(new ArrayList<TestError>()); 412 } else { 413 tree.setErrorList(editLayer.validationErrors); 414 } 415 } 416 417 /** 418 * Add a tree selection listener to the validator tree. 419 * @param listener the TreeSelectionListener 420 * @since 5958 421 */ 422 public void addTreeSelectionListener(TreeSelectionListener listener) { 423 tree.addTreeSelectionListener(listener); 424 } 425 426 /** 427 * Remove the given tree selection listener from the validator tree. 428 * @param listener the TreeSelectionListener 429 * @since 5958 430 */ 431 public void removeTreeSelectionListener(TreeSelectionListener listener) { 432 tree.removeTreeSelectionListener(listener); 433 } 434 435 /** 436 * Replies the popup menu handler. 437 * @return The popup menu handler 438 * @since 5958 439 */ 440 public PopupMenuHandler getPopupMenuHandler() { 441 return popupMenuHandler; 442 } 443 444 /** 445 * Replies the currently selected error, or {@code null}. 446 * @return The selected error, if any. 447 * @since 5958 448 */ 449 public TestError getSelectedError() { 450 Object comp = tree.getLastSelectedPathComponent(); 451 if (comp instanceof DefaultMutableTreeNode) { 452 Object object = ((DefaultMutableTreeNode) comp).getUserObject(); 453 if (object instanceof TestError) { 454 return (TestError) object; 455 } 456 } 457 return null; 458 } 459 460 /** 461 * Watches for double clicks and launches the popup menu. 462 */ 463 class MouseEventHandler extends PopupMenuLauncher { 464 465 MouseEventHandler() { 466 super(popupMenu); 467 } 468 469 @Override 470 public void mouseClicked(MouseEvent e) { 471 fixButton.setEnabled(false); 472 if (ignoreButton != null) { 473 ignoreButton.setEnabled(false); 474 } 475 selectButton.setEnabled(false); 476 477 boolean isDblClick = isDoubleClick(e); 478 479 Collection<OsmPrimitive> sel = isDblClick ? new HashSet<>(40) : null; 480 481 boolean hasFixes = setSelection(sel, isDblClick); 482 fixButton.setEnabled(hasFixes); 483 484 if (isDblClick) { 485 Main.getLayerManager().getEditDataSet().setSelected(sel); 486 if (Main.pref.getBoolean("validator.autozoom", false)) { 487 AutoScaleAction.zoomTo(sel); 488 } 489 } 490 } 491 492 @Override public void launch(MouseEvent e) { 493 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY()); 494 if (selPath == null) 495 return; 496 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1); 497 if (!(node.getUserObject() instanceof TestError)) 498 return; 499 super.launch(e); 500 } 501 502 } 503 504 /** 505 * Watches for tree selection. 506 */ 507 public class SelectionWatch implements TreeSelectionListener { 508 @Override 509 public void valueChanged(TreeSelectionEvent e) { 510 fixButton.setEnabled(false); 511 if (ignoreButton != null) { 512 ignoreButton.setEnabled(false); 513 } 514 selectButton.setEnabled(false); 515 516 Collection<OsmPrimitive> sel = new HashSet<>(); 517 boolean hasFixes = setSelection(sel, true); 518 fixButton.setEnabled(hasFixes); 519 popupMenuHandler.setPrimitives(sel); 520 if (Main.map != null) { 521 Main.map.repaint(); 522 } 523 } 524 } 525 526 public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor { 527 @Override 528 public void visit(OsmPrimitive p) { 529 if (p.isUsable()) { 530 p.accept(this); 531 } 532 } 533 534 @Override 535 public void visit(WaySegment ws) { 536 if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount()) 537 return; 538 visit(ws.way.getNodes().get(ws.lowerIndex)); 539 visit(ws.way.getNodes().get(ws.lowerIndex + 1)); 540 } 541 542 @Override 543 public void visit(List<Node> nodes) { 544 for (Node n: nodes) { 545 visit(n); 546 } 547 } 548 549 @Override 550 public void visit(TestError error) { 551 if (error != null) { 552 error.visitHighlighted(this); 553 } 554 } 555 } 556 557 public void updateSelection(Collection<? extends OsmPrimitive> newSelection) { 558 if (!Main.pref.getBoolean(ValidatorPreference.PREF_FILTER_BY_SELECTION, false)) 559 return; 560 if (newSelection.isEmpty()) { 561 tree.setFilter(null); 562 } 563 tree.setFilter(new HashSet<>(newSelection)); 564 } 565 566 @Override 567 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 568 updateSelection(newSelection); 569 } 570 571 /** 572 * Task for fixing a collection of {@link TestError}s. Can be run asynchronously. 573 * 574 * 575 */ 576 class FixTask extends PleaseWaitRunnable { 577 private final Collection<TestError> testErrors; 578 private boolean canceled; 579 580 FixTask(Collection<TestError> testErrors) { 581 super(tr("Fixing errors ..."), false /* don't ignore exceptions */); 582 this.testErrors = testErrors == null ? new ArrayList<>() : testErrors; 583 } 584 585 @Override 586 protected void cancel() { 587 this.canceled = true; 588 } 589 590 @Override 591 protected void finish() { 592 // do nothing 593 } 594 595 protected void fixError(TestError error) throws InterruptedException, InvocationTargetException { 596 if (error.isFixable()) { 597 final Command fixCommand = error.getFix(); 598 if (fixCommand != null) { 599 SwingUtilities.invokeAndWait(() -> Main.main.undoRedo.addNoRedraw(fixCommand)); 600 } 601 // It is wanted to ignore an error if it said fixable, even if fixCommand was null 602 // This is to fix #5764 and #5773: 603 // a delete command, for example, may be null if all concerned primitives have already been deleted 604 error.setIgnored(true); 605 } 606 } 607 608 @Override 609 protected void realRun() throws SAXException, IOException, OsmTransferException { 610 ProgressMonitor monitor = getProgressMonitor(); 611 try { 612 monitor.setTicksCount(testErrors.size()); 613 final DataSet ds = Main.getLayerManager().getEditDataSet(); 614 int i = 0; 615 SwingUtilities.invokeAndWait(ds::beginUpdate); 616 try { 617 for (TestError error: testErrors) { 618 i++; 619 monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(), error.getMessage())); 620 if (this.canceled) 621 return; 622 fixError(error); 623 monitor.worked(1); 624 } 625 } finally { 626 SwingUtilities.invokeAndWait(ds::endUpdate); 627 } 628 monitor.subTask(tr("Updating map ...")); 629 SwingUtilities.invokeAndWait(() -> { 630 Main.main.undoRedo.afterAdd(); 631 Main.map.repaint(); 632 tree.resetErrors(); 633 ds.fireSelectionChanged(); 634 }); 635 } catch (InterruptedException | InvocationTargetException e) { 636 // FIXME: signature of realRun should have a generic checked exception we could throw here 637 throw new JosmRuntimeException(e); 638 } finally { 639 monitor.finishTask(); 640 } 641 } 642 } 643}