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