[3669] | 1 | // License: GPL. See LICENSE file for details.
|
---|
| 2 | package org.openstreetmap.josm.gui.dialogs.validator;
|
---|
| 3 |
|
---|
[6469] | 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
| 5 |
|
---|
[5672] | 6 | import java.awt.event.KeyListener;
|
---|
[3669] | 7 | import java.awt.event.MouseEvent;
|
---|
| 8 | import java.util.ArrayList;
|
---|
| 9 | import java.util.Collections;
|
---|
| 10 | import java.util.Enumeration;
|
---|
| 11 | import java.util.HashMap;
|
---|
| 12 | import java.util.HashSet;
|
---|
| 13 | import java.util.List;
|
---|
| 14 | import java.util.Map;
|
---|
[5672] | 15 | import java.util.Map.Entry;
|
---|
[3669] | 16 | import java.util.Set;
|
---|
| 17 |
|
---|
| 18 | import javax.swing.JTree;
|
---|
| 19 | import javax.swing.ToolTipManager;
|
---|
| 20 | import javax.swing.tree.DefaultMutableTreeNode;
|
---|
| 21 | import javax.swing.tree.DefaultTreeModel;
|
---|
| 22 | import javax.swing.tree.TreePath;
|
---|
| 23 | import javax.swing.tree.TreeSelectionModel;
|
---|
| 24 |
|
---|
[5672] | 25 | import org.openstreetmap.josm.Main;
|
---|
[5911] | 26 | import org.openstreetmap.josm.data.osm.DataSet;
|
---|
[3669] | 27 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
| 28 | import org.openstreetmap.josm.data.validation.Severity;
|
---|
| 29 | import org.openstreetmap.josm.data.validation.TestError;
|
---|
| 30 | import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
|
---|
[6529] | 31 | import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
|
---|
[6706] | 32 | import org.openstreetmap.josm.gui.util.GuiHelper;
|
---|
[5911] | 33 | import org.openstreetmap.josm.tools.Destroyable;
|
---|
[3674] | 34 | import org.openstreetmap.josm.tools.MultiMap;
|
---|
[3669] | 35 |
|
---|
| 36 | /**
|
---|
| 37 | * A panel that displays the error tree. The selection manager
|
---|
| 38 | * respects clicks into the selection list. Ctrl-click will remove entries from
|
---|
| 39 | * the list while single click will make the clicked entry the only selection.
|
---|
| 40 | *
|
---|
| 41 | * @author frsantos
|
---|
| 42 | */
|
---|
[5911] | 43 | public class ValidatorTreePanel extends JTree implements Destroyable {
|
---|
[3669] | 44 |
|
---|
[7266] | 45 | private static final class GroupTreeNode extends DefaultMutableTreeNode {
|
---|
| 46 |
|
---|
| 47 | public GroupTreeNode(Object userObject) {
|
---|
| 48 | super(userObject);
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 | @Override
|
---|
| 52 | public String toString() {
|
---|
| 53 | return tr("{0} ({1})", super.toString(), getLeafCount());
|
---|
| 54 | }
|
---|
| 55 | }
|
---|
| 56 |
|
---|
[3669] | 57 | /**
|
---|
| 58 | * The validation data.
|
---|
| 59 | */
|
---|
[3671] | 60 | protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
|
---|
[3669] | 61 |
|
---|
| 62 | /** The list of errors shown in the tree */
|
---|
[8308] | 63 | private transient List<TestError> errors = new ArrayList<>();
|
---|
[3669] | 64 |
|
---|
| 65 | /**
|
---|
| 66 | * If {@link #filter} is not <code>null</code> only errors are displayed
|
---|
| 67 | * that refer to one of the primitives in the filter.
|
---|
| 68 | */
|
---|
[8308] | 69 | private transient Set<OsmPrimitive> filter = null;
|
---|
[3669] | 70 |
|
---|
[5891] | 71 | /** a counter to check if tree has been rebuild */
|
---|
[3669] | 72 | private int updateCount;
|
---|
| 73 |
|
---|
| 74 | /**
|
---|
| 75 | * Constructor
|
---|
| 76 | * @param errors The list of errors
|
---|
| 77 | */
|
---|
| 78 | public ValidatorTreePanel(List<TestError> errors) {
|
---|
| 79 | ToolTipManager.sharedInstance().registerComponent(this);
|
---|
[3671] | 80 | this.setModel(valTreeModel);
|
---|
[3669] | 81 | this.setRootVisible(false);
|
---|
| 82 | this.setShowsRootHandles(true);
|
---|
| 83 | this.expandRow(0);
|
---|
| 84 | this.setVisibleRowCount(8);
|
---|
| 85 | this.setCellRenderer(new ValidatorTreeRenderer());
|
---|
| 86 | this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
|
---|
| 87 | setErrorList(errors);
|
---|
[5672] | 88 | for (KeyListener keyListener : getKeyListeners()) {
|
---|
| 89 | // Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands
|
---|
[6990] | 90 | if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) {
|
---|
[5672] | 91 | removeKeyListener(keyListener);
|
---|
| 92 | }
|
---|
| 93 | }
|
---|
[3669] | 94 | }
|
---|
| 95 |
|
---|
| 96 | @Override
|
---|
| 97 | public String getToolTipText(MouseEvent e) {
|
---|
| 98 | String res = null;
|
---|
| 99 | TreePath path = getPathForLocation(e.getX(), e.getY());
|
---|
| 100 | if (path != null) {
|
---|
| 101 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
|
---|
| 102 | Object nodeInfo = node.getUserObject();
|
---|
| 103 |
|
---|
| 104 | if (nodeInfo instanceof TestError) {
|
---|
| 105 | TestError error = (TestError) nodeInfo;
|
---|
| 106 | MultipleNameVisitor v = new MultipleNameVisitor();
|
---|
| 107 | v.visit(error.getPrimitives());
|
---|
| 108 | res = "<html>" + v.getText() + "<br>" + error.getMessage();
|
---|
| 109 | String d = error.getDescription();
|
---|
| 110 | if (d != null)
|
---|
| 111 | res += "<br>" + d;
|
---|
| 112 | res += "</html>";
|
---|
[3671] | 113 | } else {
|
---|
[3669] | 114 | res = node.toString();
|
---|
[3671] | 115 | }
|
---|
[3669] | 116 | }
|
---|
| 117 | return res;
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | /** Constructor */
|
---|
| 121 | public ValidatorTreePanel() {
|
---|
| 122 | this(null);
|
---|
| 123 | }
|
---|
| 124 |
|
---|
| 125 | @Override
|
---|
| 126 | public void setVisible(boolean v) {
|
---|
[3671] | 127 | if (v) {
|
---|
[3669] | 128 | buildTree();
|
---|
[3671] | 129 | } else {
|
---|
| 130 | valTreeModel.setRoot(new DefaultMutableTreeNode());
|
---|
| 131 | }
|
---|
[3669] | 132 | super.setVisible(v);
|
---|
| 133 | }
|
---|
| 134 |
|
---|
| 135 | /**
|
---|
| 136 | * Builds the errors tree
|
---|
| 137 | */
|
---|
| 138 | public void buildTree() {
|
---|
| 139 | updateCount++;
|
---|
[6706] | 140 | final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
|
---|
[3669] | 141 |
|
---|
| 142 | if (errors == null || errors.isEmpty()) {
|
---|
[6706] | 143 | GuiHelper.runInEDTAndWait(new Runnable() {
|
---|
| 144 | @Override
|
---|
| 145 | public void run() {
|
---|
| 146 | valTreeModel.setRoot(rootNode);
|
---|
| 147 | }
|
---|
| 148 | });
|
---|
[3669] | 149 | return;
|
---|
| 150 | }
|
---|
[6468] | 151 | // Sort validation errors - #8517
|
---|
| 152 | Collections.sort(errors);
|
---|
[3669] | 153 |
|
---|
| 154 | // Remember the currently expanded rows
|
---|
[7005] | 155 | Set<Object> oldSelectedRows = new HashSet<>();
|
---|
[3669] | 156 | Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
|
---|
| 157 | if (expanded != null) {
|
---|
| 158 | while (expanded.hasMoreElements()) {
|
---|
| 159 | TreePath path = expanded.nextElement();
|
---|
| 160 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
|
---|
| 161 | Object userObject = node.getUserObject();
|
---|
[3671] | 162 | if (userObject instanceof Severity) {
|
---|
[3669] | 163 | oldSelectedRows.add(userObject);
|
---|
[3671] | 164 | } else if (userObject instanceof String) {
|
---|
[3669] | 165 | String msg = (String) userObject;
|
---|
[7270] | 166 | int index = msg.lastIndexOf(" (");
|
---|
| 167 | if (index > 0) {
|
---|
| 168 | msg = msg.substring(0, index);
|
---|
| 169 | }
|
---|
[3669] | 170 | oldSelectedRows.add(msg);
|
---|
| 171 | }
|
---|
| 172 | }
|
---|
| 173 | }
|
---|
| 174 |
|
---|
[7005] | 175 | Map<Severity, MultiMap<String, TestError>> errorTree = new HashMap<>();
|
---|
| 176 | Map<Severity, HashMap<String, MultiMap<String, TestError>>> errorTreeDeep = new HashMap<>();
|
---|
[3669] | 177 | for (Severity s : Severity.values()) {
|
---|
[3674] | 178 | errorTree.put(s, new MultiMap<String, TestError>(20));
|
---|
| 179 | errorTreeDeep.put(s, new HashMap<String, MultiMap<String, TestError>>());
|
---|
[3669] | 180 | }
|
---|
| 181 |
|
---|
[6636] | 182 | final Boolean other = ValidatorPreference.PREF_OTHER.get();
|
---|
[3669] | 183 | for (TestError e : errors) {
|
---|
[3671] | 184 | if (e.getIgnored()) {
|
---|
[3669] | 185 | continue;
|
---|
[3671] | 186 | }
|
---|
[3669] | 187 | Severity s = e.getSeverity();
|
---|
[3723] | 188 | if(!other && s == Severity.OTHER) {
|
---|
| 189 | continue;
|
---|
| 190 | }
|
---|
[3669] | 191 | String d = e.getDescription();
|
---|
| 192 | String m = e.getMessage();
|
---|
| 193 | if (filter != null) {
|
---|
| 194 | boolean found = false;
|
---|
| 195 | for (OsmPrimitive p : e.getPrimitives()) {
|
---|
| 196 | if (filter.contains(p)) {
|
---|
| 197 | found = true;
|
---|
| 198 | break;
|
---|
| 199 | }
|
---|
| 200 | }
|
---|
[3671] | 201 | if (!found) {
|
---|
[3669] | 202 | continue;
|
---|
[3671] | 203 | }
|
---|
[3669] | 204 | }
|
---|
| 205 | if (d != null) {
|
---|
[3674] | 206 | MultiMap<String, TestError> b = errorTreeDeep.get(s).get(m);
|
---|
[3669] | 207 | if (b == null) {
|
---|
[7005] | 208 | b = new MultiMap<>(20);
|
---|
[3669] | 209 | errorTreeDeep.get(s).put(m, b);
|
---|
| 210 | }
|
---|
[3674] | 211 | b.put(d, e);
|
---|
[3671] | 212 | } else {
|
---|
[3674] | 213 | errorTree.get(s).put(m, e);
|
---|
[3671] | 214 | }
|
---|
[3669] | 215 | }
|
---|
| 216 |
|
---|
[7005] | 217 | List<TreePath> expandedPaths = new ArrayList<>();
|
---|
[3669] | 218 | for (Severity s : Severity.values()) {
|
---|
[3674] | 219 | MultiMap<String, TestError> severityErrors = errorTree.get(s);
|
---|
| 220 | Map<String, MultiMap<String, TestError>> severityErrorsDeep = errorTreeDeep.get(s);
|
---|
[3671] | 221 | if (severityErrors.isEmpty() && severityErrorsDeep.isEmpty()) {
|
---|
[3669] | 222 | continue;
|
---|
[3671] | 223 | }
|
---|
[3669] | 224 |
|
---|
| 225 | // Severity node
|
---|
[7266] | 226 | DefaultMutableTreeNode severityNode = new GroupTreeNode(s);
|
---|
[3669] | 227 | rootNode.add(severityNode);
|
---|
| 228 |
|
---|
[3671] | 229 | if (oldSelectedRows.contains(s)) {
|
---|
[3669] | 230 | expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode }));
|
---|
[3671] | 231 | }
|
---|
[3669] | 232 |
|
---|
[5783] | 233 | for (Entry<String, Set<TestError>> msgErrors : severityErrors.entrySet()) {
|
---|
[3669] | 234 | // Message node
|
---|
[3674] | 235 | Set<TestError> errs = msgErrors.getValue();
|
---|
[6469] | 236 | String msg = tr("{0} ({1})", msgErrors.getKey(), errs.size());
|
---|
[3669] | 237 | DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
|
---|
| 238 | severityNode.add(messageNode);
|
---|
| 239 |
|
---|
| 240 | if (oldSelectedRows.contains(msgErrors.getKey())) {
|
---|
| 241 | expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode }));
|
---|
| 242 | }
|
---|
| 243 |
|
---|
[3671] | 244 | for (TestError error : errs) {
|
---|
[3669] | 245 | // Error node
|
---|
| 246 | DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error);
|
---|
| 247 | messageNode.add(errorNode);
|
---|
| 248 | }
|
---|
| 249 | }
|
---|
[3674] | 250 | for (Entry<String, MultiMap<String, TestError>> bag : severityErrorsDeep.entrySet()) {
|
---|
[3669] | 251 | // Group node
|
---|
[3674] | 252 | MultiMap<String, TestError> errorlist = bag.getValue();
|
---|
[3669] | 253 | DefaultMutableTreeNode groupNode = null;
|
---|
| 254 | if (errorlist.size() > 1) {
|
---|
[7266] | 255 | groupNode = new GroupTreeNode(bag.getKey());
|
---|
[3669] | 256 | severityNode.add(groupNode);
|
---|
| 257 | if (oldSelectedRows.contains(bag.getKey())) {
|
---|
| 258 | expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode }));
|
---|
| 259 | }
|
---|
| 260 | }
|
---|
| 261 |
|
---|
[5783] | 262 | for (Entry<String, Set<TestError>> msgErrors : errorlist.entrySet()) {
|
---|
[3669] | 263 | // Message node
|
---|
[3674] | 264 | Set<TestError> errs = msgErrors.getValue();
|
---|
[3669] | 265 | String msg;
|
---|
[3671] | 266 | if (groupNode != null) {
|
---|
[6469] | 267 | msg = tr("{0} ({1})", msgErrors.getKey(), errs.size());
|
---|
[3671] | 268 | } else {
|
---|
[6469] | 269 | msg = tr("{0} - {1} ({2})", msgErrors.getKey(), bag.getKey(), errs.size());
|
---|
[3671] | 270 | }
|
---|
[3669] | 271 | DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
|
---|
[3671] | 272 | if (groupNode != null) {
|
---|
[3669] | 273 | groupNode.add(messageNode);
|
---|
[3671] | 274 | } else {
|
---|
[3669] | 275 | severityNode.add(messageNode);
|
---|
[3671] | 276 | }
|
---|
[3669] | 277 |
|
---|
| 278 | if (oldSelectedRows.contains(msgErrors.getKey())) {
|
---|
| 279 | if (groupNode != null) {
|
---|
| 280 | expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode,
|
---|
| 281 | messageNode }));
|
---|
| 282 | } else {
|
---|
| 283 | expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode }));
|
---|
| 284 | }
|
---|
| 285 | }
|
---|
| 286 |
|
---|
[3671] | 287 | for (TestError error : errs) {
|
---|
[3669] | 288 | // Error node
|
---|
| 289 | DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error);
|
---|
| 290 | messageNode.add(errorNode);
|
---|
| 291 | }
|
---|
| 292 | }
|
---|
| 293 | }
|
---|
| 294 | }
|
---|
| 295 |
|
---|
[3671] | 296 | valTreeModel.setRoot(rootNode);
|
---|
[3669] | 297 | for (TreePath path : expandedPaths) {
|
---|
| 298 | this.expandPath(path);
|
---|
| 299 | }
|
---|
| 300 | }
|
---|
| 301 |
|
---|
| 302 | /**
|
---|
| 303 | * Sets the errors list used by a data layer
|
---|
| 304 | * @param errors The error list that is used by a data layer
|
---|
| 305 | */
|
---|
[6890] | 306 | public final void setErrorList(List<TestError> errors) {
|
---|
[3669] | 307 | this.errors = errors;
|
---|
[3671] | 308 | if (isVisible()) {
|
---|
[3669] | 309 | buildTree();
|
---|
[3671] | 310 | }
|
---|
[3669] | 311 | }
|
---|
| 312 |
|
---|
| 313 | /**
|
---|
| 314 | * Clears the current error list and adds these errors to it
|
---|
[5891] | 315 | * @param newerrors The validation errors
|
---|
[3669] | 316 | */
|
---|
| 317 | public void setErrors(List<TestError> newerrors) {
|
---|
| 318 | if (errors == null)
|
---|
| 319 | return;
|
---|
[5911] | 320 | clearErrors();
|
---|
| 321 | DataSet ds = Main.main.getCurrentDataSet();
|
---|
[3669] | 322 | for (TestError error : newerrors) {
|
---|
[3671] | 323 | if (!error.getIgnored()) {
|
---|
[3669] | 324 | errors.add(error);
|
---|
[5911] | 325 | if (ds != null) {
|
---|
| 326 | ds.addDataSetListener(error);
|
---|
| 327 | }
|
---|
[3671] | 328 | }
|
---|
[3669] | 329 | }
|
---|
[3671] | 330 | if (isVisible()) {
|
---|
[3669] | 331 | buildTree();
|
---|
[3671] | 332 | }
|
---|
[3669] | 333 | }
|
---|
| 334 |
|
---|
| 335 | /**
|
---|
| 336 | * Returns the errors of the tree
|
---|
[5891] | 337 | * @return the errors of the tree
|
---|
[3669] | 338 | */
|
---|
| 339 | public List<TestError> getErrors() {
|
---|
| 340 | return errors != null ? errors : Collections.<TestError> emptyList();
|
---|
| 341 | }
|
---|
| 342 |
|
---|
[5891] | 343 | /**
|
---|
| 344 | * Returns the filter list
|
---|
| 345 | * @return the list of primitives used for filtering
|
---|
| 346 | */
|
---|
[3669] | 347 | public Set<OsmPrimitive> getFilter() {
|
---|
| 348 | return filter;
|
---|
| 349 | }
|
---|
| 350 |
|
---|
[5891] | 351 | /**
|
---|
| 352 | * Set the filter list to a set of primitives
|
---|
| 353 | * @param filter the list of primitives used for filtering
|
---|
| 354 | */
|
---|
[3669] | 355 | public void setFilter(Set<OsmPrimitive> filter) {
|
---|
[3671] | 356 | if (filter != null && filter.isEmpty()) {
|
---|
[3669] | 357 | this.filter = null;
|
---|
[3671] | 358 | } else {
|
---|
[3669] | 359 | this.filter = filter;
|
---|
[3671] | 360 | }
|
---|
| 361 | if (isVisible()) {
|
---|
[3669] | 362 | buildTree();
|
---|
[3671] | 363 | }
|
---|
[3669] | 364 | }
|
---|
| 365 |
|
---|
| 366 | /**
|
---|
| 367 | * Updates the current errors list
|
---|
| 368 | */
|
---|
| 369 | public void resetErrors() {
|
---|
[7005] | 370 | List<TestError> e = new ArrayList<>(errors);
|
---|
[3669] | 371 | setErrors(e);
|
---|
| 372 | }
|
---|
| 373 |
|
---|
| 374 | /**
|
---|
[5891] | 375 | * Expands complete tree
|
---|
[3669] | 376 | */
|
---|
| 377 | @SuppressWarnings("unchecked")
|
---|
| 378 | public void expandAll() {
|
---|
| 379 | DefaultMutableTreeNode root = getRoot();
|
---|
| 380 |
|
---|
| 381 | int row = 0;
|
---|
| 382 | Enumeration<DefaultMutableTreeNode> children = root.breadthFirstEnumeration();
|
---|
| 383 | while (children.hasMoreElements()) {
|
---|
| 384 | children.nextElement();
|
---|
| 385 | expandRow(row++);
|
---|
| 386 | }
|
---|
| 387 | }
|
---|
| 388 |
|
---|
| 389 | /**
|
---|
| 390 | * Returns the root node model.
|
---|
| 391 | * @return The root node model
|
---|
| 392 | */
|
---|
| 393 | public DefaultMutableTreeNode getRoot() {
|
---|
[3671] | 394 | return (DefaultMutableTreeNode) valTreeModel.getRoot();
|
---|
[3669] | 395 | }
|
---|
| 396 |
|
---|
[5891] | 397 | /**
|
---|
| 398 | * Returns a value to check if tree has been rebuild
|
---|
| 399 | * @return the current counter
|
---|
| 400 | */
|
---|
[3669] | 401 | public int getUpdateCount() {
|
---|
| 402 | return updateCount;
|
---|
| 403 | }
|
---|
[6070] | 404 |
|
---|
[5911] | 405 | private void clearErrors() {
|
---|
| 406 | if (errors != null) {
|
---|
| 407 | DataSet ds = Main.main.getCurrentDataSet();
|
---|
| 408 | if (ds != null) {
|
---|
| 409 | for (TestError e : errors) {
|
---|
| 410 | ds.removeDataSetListener(e);
|
---|
| 411 | }
|
---|
| 412 | }
|
---|
| 413 | errors.clear();
|
---|
| 414 | }
|
---|
| 415 | }
|
---|
| 416 |
|
---|
| 417 | @Override
|
---|
| 418 | public void destroy() {
|
---|
| 419 | clearErrors();
|
---|
| 420 | }
|
---|
[3669] | 421 | }
|
---|