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(OsmValidator.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 (ValidatorPreference.PREF_USE_IGNORE.get()) { 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 } 196 197 /** 198 * Fix selected errors 199 */ 200 @SuppressWarnings("unchecked") 201 private void fixErrors() { 202 TreePath[] selectionPaths = tree.getSelectionPaths(); 203 if (selectionPaths == null) 204 return; 205 206 Set<DefaultMutableTreeNode> processedNodes = new HashSet<>(); 207 208 List<TestError> errorsToFix = new LinkedList<>(); 209 for (TreePath path : selectionPaths) { 210 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 211 if (node == null) { 212 continue; 213 } 214 215 Enumeration<TreeNode> children = node.breadthFirstEnumeration(); 216 while (children.hasMoreElements()) { 217 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) 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 @SuppressWarnings("unchecked") 240 private void ignoreErrors() { 241 int asked = JOptionPane.DEFAULT_OPTION; 242 boolean changed = false; 243 TreePath[] selectionPaths = tree.getSelectionPaths(); 244 if (selectionPaths == null) 245 return; 246 247 Set<DefaultMutableTreeNode> processedNodes = new HashSet<>(); 248 for (TreePath path : selectionPaths) { 249 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 250 if (node == null) { 251 continue; 252 } 253 254 Object mainNodeInfo = node.getUserObject(); 255 if (!(mainNodeInfo instanceof TestError)) { 256 Set<String> state = new HashSet<>(); 257 // ask if the whole set should be ignored 258 if (asked == JOptionPane.DEFAULT_OPTION) { 259 String[] a = new String[] {tr("Whole group"), tr("Single elements"), tr("Nothing")}; 260 asked = JOptionPane.showOptionDialog(Main.parent, tr("Ignore whole group or individual elements?"), 261 tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, 262 a, a[1]); 263 } 264 if (asked == JOptionPane.YES_NO_OPTION) { 265 Enumeration<TreeNode> children = node.breadthFirstEnumeration(); 266 while (children.hasMoreElements()) { 267 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement(); 268 if (processedNodes.contains(childNode)) { 269 continue; 270 } 271 272 processedNodes.add(childNode); 273 Object nodeInfo = childNode.getUserObject(); 274 if (nodeInfo instanceof TestError) { 275 TestError err = (TestError) nodeInfo; 276 err.setIgnored(true); 277 changed = true; 278 state.add(node.getDepth() == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup()); 279 } 280 } 281 for (String s : state) { 282 OsmValidator.addIgnoredError(s); 283 } 284 continue; 285 } else if (asked == JOptionPane.CANCEL_OPTION || asked == JOptionPane.CLOSED_OPTION) { 286 continue; 287 } 288 } 289 290 Enumeration<TreeNode> children = node.breadthFirstEnumeration(); 291 while (children.hasMoreElements()) { 292 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement(); 293 if (processedNodes.contains(childNode)) { 294 continue; 295 } 296 297 processedNodes.add(childNode); 298 Object nodeInfo = childNode.getUserObject(); 299 if (nodeInfo instanceof TestError) { 300 TestError error = (TestError) nodeInfo; 301 String state = error.getIgnoreState(); 302 if (state != null) { 303 OsmValidator.addIgnoredError(state); 304 } 305 changed = true; 306 error.setIgnored(true); 307 } 308 } 309 } 310 if (changed) { 311 tree.resetErrors(); 312 OsmValidator.saveIgnoredErrors(); 313 Main.map.repaint(); 314 } 315 } 316 317 /** 318 * Sets the selection of the map to the current selected items. 319 */ 320 @SuppressWarnings("unchecked") 321 private void setSelectedItems() { 322 if (tree == null) 323 return; 324 325 Collection<OsmPrimitive> sel = new HashSet<>(40); 326 327 TreePath[] selectedPaths = tree.getSelectionPaths(); 328 if (selectedPaths == null) 329 return; 330 331 for (TreePath path : selectedPaths) { 332 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 333 Enumeration<TreeNode> children = node.breadthFirstEnumeration(); 334 while (children.hasMoreElements()) { 335 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement(); 336 Object nodeInfo = childNode.getUserObject(); 337 if (nodeInfo instanceof TestError) { 338 TestError error = (TestError) nodeInfo; 339 sel.addAll(error.getSelectablePrimitives()); 340 } 341 } 342 } 343 DataSet ds = Main.getLayerManager().getEditDataSet(); 344 if (ds != null) { 345 ds.setSelected(sel); 346 } 347 } 348 349 /** 350 * Checks for fixes in selected element and, if needed, adds to the sel 351 * parameter all selected elements 352 * 353 * @param sel 354 * The collection where to add all selected elements 355 * @param addSelected 356 * if true, add all selected elements to collection 357 * @return whether the selected elements has any fix 358 */ 359 @SuppressWarnings("unchecked") 360 private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) { 361 boolean hasFixes = false; 362 363 DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); 364 if (lastSelectedNode != null && !lastSelectedNode.equals(node)) { 365 Enumeration<TreeNode> children = lastSelectedNode.breadthFirstEnumeration(); 366 while (children.hasMoreElements()) { 367 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement(); 368 Object nodeInfo = childNode.getUserObject(); 369 if (nodeInfo instanceof TestError) { 370 TestError error = (TestError) nodeInfo; 371 error.setSelected(false); 372 } 373 } 374 } 375 376 lastSelectedNode = node; 377 if (node == null) 378 return hasFixes; 379 380 Enumeration<TreeNode> children = node.breadthFirstEnumeration(); 381 while (children.hasMoreElements()) { 382 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement(); 383 Object nodeInfo = childNode.getUserObject(); 384 if (nodeInfo instanceof TestError) { 385 TestError error = (TestError) nodeInfo; 386 error.setSelected(true); 387 388 hasFixes = hasFixes || error.isFixable(); 389 if (addSelected) { 390 sel.addAll(error.getSelectablePrimitives()); 391 } 392 } 393 } 394 selectButton.setEnabled(true); 395 if (ignoreButton != null) { 396 ignoreButton.setEnabled(true); 397 } 398 399 return hasFixes; 400 } 401 402 @Override 403 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 404 OsmDataLayer editLayer = e.getSource().getEditLayer(); 405 if (editLayer == null) { 406 tree.setErrorList(new ArrayList<TestError>()); 407 } else { 408 tree.setErrorList(editLayer.validationErrors); 409 } 410 } 411 412 /** 413 * Add a tree selection listener to the validator tree. 414 * @param listener the TreeSelectionListener 415 * @since 5958 416 */ 417 public void addTreeSelectionListener(TreeSelectionListener listener) { 418 tree.addTreeSelectionListener(listener); 419 } 420 421 /** 422 * Remove the given tree selection listener from the validator tree. 423 * @param listener the TreeSelectionListener 424 * @since 5958 425 */ 426 public void removeTreeSelectionListener(TreeSelectionListener listener) { 427 tree.removeTreeSelectionListener(listener); 428 } 429 430 /** 431 * Replies the popup menu handler. 432 * @return The popup menu handler 433 * @since 5958 434 */ 435 public PopupMenuHandler getPopupMenuHandler() { 436 return popupMenuHandler; 437 } 438 439 /** 440 * Replies the currently selected error, or {@code null}. 441 * @return The selected error, if any. 442 * @since 5958 443 */ 444 public TestError getSelectedError() { 445 Object comp = tree.getLastSelectedPathComponent(); 446 if (comp instanceof DefaultMutableTreeNode) { 447 Object object = ((DefaultMutableTreeNode) comp).getUserObject(); 448 if (object instanceof TestError) { 449 return (TestError) object; 450 } 451 } 452 return null; 453 } 454 455 /** 456 * Watches for double clicks and launches the popup menu. 457 */ 458 class MouseEventHandler extends PopupMenuLauncher { 459 460 MouseEventHandler() { 461 super(popupMenu); 462 } 463 464 @Override 465 public void mouseClicked(MouseEvent e) { 466 fixButton.setEnabled(false); 467 if (ignoreButton != null) { 468 ignoreButton.setEnabled(false); 469 } 470 selectButton.setEnabled(false); 471 472 boolean isDblClick = isDoubleClick(e); 473 474 Collection<OsmPrimitive> sel = isDblClick ? new HashSet<>(40) : null; 475 476 boolean hasFixes = setSelection(sel, isDblClick); 477 fixButton.setEnabled(hasFixes); 478 479 if (isDblClick) { 480 Main.getLayerManager().getEditDataSet().setSelected(sel); 481 if (Main.pref.getBoolean("validator.autozoom", false)) { 482 AutoScaleAction.zoomTo(sel); 483 } 484 } 485 } 486 487 @Override public void launch(MouseEvent e) { 488 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY()); 489 if (selPath == null) 490 return; 491 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1); 492 if (!(node.getUserObject() instanceof TestError)) 493 return; 494 super.launch(e); 495 } 496 497 } 498 499 /** 500 * Watches for tree selection. 501 */ 502 public class SelectionWatch implements TreeSelectionListener { 503 @Override 504 public void valueChanged(TreeSelectionEvent e) { 505 fixButton.setEnabled(false); 506 if (ignoreButton != null) { 507 ignoreButton.setEnabled(false); 508 } 509 selectButton.setEnabled(false); 510 511 Collection<OsmPrimitive> sel = new HashSet<>(); 512 boolean hasFixes = setSelection(sel, true); 513 fixButton.setEnabled(hasFixes); 514 popupMenuHandler.setPrimitives(sel); 515 if (Main.map != null) { 516 Main.map.repaint(); 517 } 518 } 519 } 520 521 public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor { 522 @Override 523 public void visit(OsmPrimitive p) { 524 if (p.isUsable()) { 525 p.accept(this); 526 } 527 } 528 529 @Override 530 public void visit(WaySegment ws) { 531 if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount()) 532 return; 533 visit(ws.way.getNodes().get(ws.lowerIndex)); 534 visit(ws.way.getNodes().get(ws.lowerIndex + 1)); 535 } 536 537 @Override 538 public void visit(List<Node> nodes) { 539 for (Node n: nodes) { 540 visit(n); 541 } 542 } 543 544 @Override 545 public void visit(TestError error) { 546 if (error != null) { 547 error.visitHighlighted(this); 548 } 549 } 550 } 551 552 public void updateSelection(Collection<? extends OsmPrimitive> newSelection) { 553 if (!Main.pref.getBoolean(ValidatorPreference.PREF_FILTER_BY_SELECTION, false)) 554 return; 555 if (newSelection.isEmpty()) { 556 tree.setFilter(null); 557 } 558 tree.setFilter(new HashSet<>(newSelection)); 559 } 560 561 @Override 562 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 563 updateSelection(newSelection); 564 } 565 566 /** 567 * Task for fixing a collection of {@link TestError}s. Can be run asynchronously. 568 * 569 * 570 */ 571 class FixTask extends PleaseWaitRunnable { 572 private final Collection<TestError> testErrors; 573 private boolean canceled; 574 575 FixTask(Collection<TestError> testErrors) { 576 super(tr("Fixing errors ..."), false /* don't ignore exceptions */); 577 this.testErrors = testErrors == null ? new ArrayList<>() : testErrors; 578 } 579 580 @Override 581 protected void cancel() { 582 this.canceled = true; 583 } 584 585 @Override 586 protected void finish() { 587 // do nothing 588 } 589 590 protected void fixError(TestError error) throws InterruptedException, InvocationTargetException { 591 if (error.isFixable()) { 592 final Command fixCommand = error.getFix(); 593 if (fixCommand != null) { 594 SwingUtilities.invokeAndWait(() -> Main.main.undoRedo.addNoRedraw(fixCommand)); 595 } 596 // It is wanted to ignore an error if it said fixable, even if fixCommand was null 597 // This is to fix #5764 and #5773: 598 // a delete command, for example, may be null if all concerned primitives have already been deleted 599 error.setIgnored(true); 600 } 601 } 602 603 @Override 604 protected void realRun() throws SAXException, IOException, OsmTransferException { 605 ProgressMonitor monitor = getProgressMonitor(); 606 try { 607 monitor.setTicksCount(testErrors.size()); 608 final DataSet ds = Main.getLayerManager().getEditDataSet(); 609 int i = 0; 610 SwingUtilities.invokeAndWait(ds::beginUpdate); 611 try { 612 for (TestError error: testErrors) { 613 i++; 614 monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(), error.getMessage())); 615 if (this.canceled) 616 return; 617 fixError(error); 618 monitor.worked(1); 619 } 620 } finally { 621 SwingUtilities.invokeAndWait(ds::endUpdate); 622 } 623 monitor.subTask(tr("Updating map ...")); 624 SwingUtilities.invokeAndWait(() -> { 625 Main.main.undoRedo.afterAdd(); 626 Main.map.repaint(); 627 tree.resetErrors(); 628 ds.fireSelectionChanged(); 629 }); 630 } catch (InterruptedException | InvocationTargetException e) { 631 // FIXME: signature of realRun should have a generic checked exception we could throw here 632 throw new RuntimeException(e); 633 } finally { 634 monitor.finishTask(); 635 } 636 } 637 } 638}