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