001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.validator;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.KeyListener;
007import java.awt.event.MouseEvent;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.Enumeration;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Map;
015import java.util.Set;
016import java.util.function.Consumer;
017import java.util.function.Predicate;
018
019import javax.swing.JTree;
020import javax.swing.ToolTipManager;
021import javax.swing.tree.DefaultMutableTreeNode;
022import javax.swing.tree.DefaultTreeModel;
023import javax.swing.tree.TreeNode;
024import javax.swing.tree.TreePath;
025import javax.swing.tree.TreeSelectionModel;
026
027import org.openstreetmap.josm.data.osm.OsmPrimitive;
028import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
029import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
030import org.openstreetmap.josm.data.osm.event.DataSetListener;
031import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
032import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
033import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
034import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
035import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
036import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
037import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
038import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
039import org.openstreetmap.josm.data.validation.OsmValidator;
040import org.openstreetmap.josm.data.validation.Severity;
041import org.openstreetmap.josm.data.validation.TestError;
042import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
043import org.openstreetmap.josm.gui.util.GuiHelper;
044import org.openstreetmap.josm.tools.Destroyable;
045import org.openstreetmap.josm.tools.ListenerList;
046
047/**
048 * A panel that displays the error tree. The selection manager
049 * respects clicks into the selection list. Ctrl-click will remove entries from
050 * the list while single click will make the clicked entry the only selection.
051 *
052 * @author frsantos
053 */
054public class ValidatorTreePanel extends JTree implements Destroyable, DataSetListener {
055
056    private static final class GroupTreeNode extends DefaultMutableTreeNode {
057
058        GroupTreeNode(Object userObject) {
059            super(userObject);
060        }
061
062        @Override
063        public String toString() {
064            return tr("{0} ({1})", super.toString(), getLeafCount());
065        }
066    }
067
068    /**
069     * The validation data.
070     */
071    protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
072
073    /** The list of errors shown in the tree */
074    private transient List<TestError> errors = new ArrayList<>();
075
076    /**
077     * If {@link #filter} is not <code>null</code> only errors are displayed
078     * that refer to one of the primitives in the filter.
079     */
080    private transient Set<? extends OsmPrimitive> filter;
081
082    private final ListenerList<Runnable> invalidationListeners = ListenerList.create();
083
084    /**
085     * Constructor
086     * @param errors The list of errors
087     */
088    public ValidatorTreePanel(List<TestError> errors) {
089        ToolTipManager.sharedInstance().registerComponent(this);
090        this.setModel(valTreeModel);
091        this.setRootVisible(false);
092        this.setShowsRootHandles(true);
093        this.expandRow(0);
094        this.setVisibleRowCount(8);
095        this.setCellRenderer(new ValidatorTreeRenderer());
096        this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
097        setErrorList(errors);
098        for (KeyListener keyListener : getKeyListeners()) {
099            // Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands
100            if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) {
101                removeKeyListener(keyListener);
102            }
103        }
104        DatasetEventManager.getInstance().addDatasetListener(this, DatasetEventManager.FireMode.IN_EDT);
105    }
106
107    @Override
108    public String getToolTipText(MouseEvent e) {
109        String res = null;
110        TreePath path = getPathForLocation(e.getX(), e.getY());
111        if (path != null) {
112            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
113            Object nodeInfo = node.getUserObject();
114
115            if (nodeInfo instanceof TestError) {
116                TestError error = (TestError) nodeInfo;
117                MultipleNameVisitor v = new MultipleNameVisitor();
118                v.visit(error.getPrimitives());
119                res = "<html>" + v.getText() + "<br>" + error.getMessage();
120                String d = error.getDescription();
121                if (d != null)
122                    res += "<br>" + d;
123                res += "</html>";
124            } else {
125                res = node.toString();
126            }
127        }
128        return res;
129    }
130
131    /** Constructor */
132    public ValidatorTreePanel() {
133        this(null);
134    }
135
136    @Override
137    public void setVisible(boolean v) {
138        if (v) {
139            buildTree();
140        } else {
141            valTreeModel.setRoot(new DefaultMutableTreeNode());
142        }
143        super.setVisible(v);
144        invalidationListeners.fireEvent(Runnable::run);
145    }
146
147    /**
148     * Builds the errors tree
149     */
150    public void buildTree() {
151        final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
152
153        if (errors == null || errors.isEmpty()) {
154            GuiHelper.runInEDTAndWait(() -> valTreeModel.setRoot(rootNode));
155            return;
156        }
157        // Sort validation errors - #8517
158        Collections.sort(errors);
159
160        // Remember the currently expanded rows
161        Set<Object> oldSelectedRows = new HashSet<>();
162        Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
163        if (expanded != null) {
164            while (expanded.hasMoreElements()) {
165                TreePath path = expanded.nextElement();
166                DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
167                Object userObject = node.getUserObject();
168                if (userObject instanceof Severity) {
169                    oldSelectedRows.add(userObject);
170                } else if (userObject instanceof String) {
171                    String msg = (String) userObject;
172                    int index = msg.lastIndexOf(" (");
173                    if (index > 0) {
174                        msg = msg.substring(0, index);
175                    }
176                    oldSelectedRows.add(msg);
177                }
178            }
179        }
180
181        Predicate<TestError> filterToUse = e -> !e.isIgnored();
182        if (!ValidatorPrefHelper.PREF_OTHER.get()) {
183            filterToUse = filterToUse.and(e -> e.getSeverity() != Severity.OTHER);
184        }
185        if (filter != null) {
186            filterToUse = filterToUse.and(e -> e.getPrimitives().stream().anyMatch(filter::contains));
187        }
188        Map<Severity, Map<String, Map<String, List<TestError>>>> errorsBySeverityMessageDescription
189            = OsmValidator.getErrorsBySeverityMessageDescription(errors, filterToUse);
190
191        final List<TreePath> expandedPaths = new ArrayList<>();
192        errorsBySeverityMessageDescription.forEach((severity, errorsByMessageDescription) -> {
193            // Severity node
194            final DefaultMutableTreeNode severityNode = new GroupTreeNode(severity);
195            rootNode.add(severityNode);
196
197            if (oldSelectedRows.contains(severity)) {
198                expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode}));
199            }
200
201            final Map<String, List<TestError>> errorsWithEmptyMessageByDescription = errorsByMessageDescription.get("");
202            if (errorsWithEmptyMessageByDescription != null) {
203                errorsWithEmptyMessageByDescription.forEach((description, errors) -> {
204                    final String msg = tr("{0} ({1})", description, errors.size());
205                    final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
206                    severityNode.add(messageNode);
207
208                    if (oldSelectedRows.contains(description)) {
209                        expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
210                    }
211
212                    errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
213                });
214            }
215
216            errorsByMessageDescription.forEach((message, errorsByDescription) -> {
217                if (message.isEmpty()) {
218                    return;
219                }
220                // Group node
221                final DefaultMutableTreeNode groupNode;
222                if (errorsByDescription.size() > 1) {
223                    groupNode = new GroupTreeNode(message);
224                    severityNode.add(groupNode);
225                    if (oldSelectedRows.contains(message)) {
226                        expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode}));
227                    }
228                } else {
229                    groupNode = null;
230                }
231
232                errorsByDescription.forEach((description, errors) -> {
233                    boolean emptyDescription = description == null || description.isEmpty();
234                    // Message node
235                    final String msg;
236                    if (groupNode != null) {
237                        msg = tr("{0} ({1})", description, errors.size());
238                    } else if (emptyDescription) {
239                        msg = tr("{0} ({1})", message, errors.size());
240                    } else {
241                        msg = tr("{0} - {1} ({2})", message, description, errors.size());
242                    }
243                    final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
244                    if (groupNode != null) {
245                        groupNode.add(messageNode);
246                    } else {
247                        severityNode.add(messageNode);
248                    }
249
250                    if (oldSelectedRows.contains(description) || (emptyDescription && oldSelectedRows.contains(message))) {
251                        if (groupNode != null) {
252                            expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode, messageNode}));
253                        } else {
254                            expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
255                        }
256                    }
257
258                    errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
259                });
260            });
261        });
262
263        valTreeModel.setRoot(rootNode);
264        for (TreePath path : expandedPaths) {
265            this.expandPath(path);
266        }
267
268        invalidationListeners.fireEvent(Runnable::run);
269    }
270
271    /**
272     * Add a new invalidation listener
273     * @param listener The listener
274     */
275    public void addInvalidationListener(Runnable listener) {
276        invalidationListeners.addListener(listener);
277    }
278
279    /**
280     * Remove an invalidation listener
281     * @param listener The listener
282     * @since 10880
283     */
284    public void removeInvalidationListener(Runnable listener) {
285        invalidationListeners.removeListener(listener);
286    }
287
288    /**
289     * Sets the errors list used by a data layer
290     * @param errors The error list that is used by a data layer
291     */
292    public final void setErrorList(List<TestError> errors) {
293        this.errors = errors;
294        if (isVisible()) {
295            buildTree();
296        }
297    }
298
299    /**
300     * Clears the current error list and adds these errors to it
301     * @param newerrors The validation errors
302     */
303    public void setErrors(List<TestError> newerrors) {
304        if (errors == null)
305            return;
306        clearErrors();
307        for (TestError error : newerrors) {
308            if (!error.isIgnored()) {
309                errors.add(error);
310            }
311        }
312        if (isVisible()) {
313            buildTree();
314        }
315    }
316
317    /**
318     * Returns the errors of the tree
319     * @return the errors of the tree
320     */
321    public List<TestError> getErrors() {
322        return errors != null ? errors : Collections.<TestError>emptyList();
323    }
324
325    /**
326     * Selects all errors related to the specified {@code primitives}, i.e. where {@link TestError#getPrimitives()}
327     * returns a primitive present in {@code primitives}.
328     * @param primitives collection of primitives
329     */
330    public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) {
331        final Collection<TreePath> paths = new ArrayList<>();
332        walkAndSelectRelatedErrors(new TreePath(getRoot()), new HashSet<>(primitives)::contains, paths);
333        getSelectionModel().clearSelection();
334        for (TreePath path : paths) {
335            expandPath(path);
336            getSelectionModel().addSelectionPath(path);
337        }
338    }
339
340    private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) {
341        final int count = getModel().getChildCount(p.getLastPathComponent());
342        for (int i = 0; i < count; i++) {
343            final Object child = getModel().getChild(p.getLastPathComponent(), i);
344            if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode
345                    && ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) {
346                final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject();
347                if (error.getPrimitives().stream().anyMatch(isRelevant)) {
348                    paths.add(p.pathByAddingChild(child));
349                }
350            } else {
351                walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths);
352            }
353        }
354    }
355
356    /**
357     * Returns the filter list
358     * @return the list of primitives used for filtering
359     */
360    public Set<? extends OsmPrimitive> getFilter() {
361        return filter;
362    }
363
364    /**
365     * Set the filter list to a set of primitives
366     * @param filter the list of primitives used for filtering
367     */
368    public void setFilter(Set<? extends OsmPrimitive> filter) {
369        if (filter != null && filter.isEmpty()) {
370            this.filter = null;
371        } else {
372            this.filter = filter;
373        }
374        if (isVisible()) {
375            buildTree();
376        }
377    }
378
379    /**
380     * Updates the current errors list
381     */
382    public void resetErrors() {
383        setErrors(new ArrayList<>(errors));
384    }
385
386    /**
387     * Expands complete tree
388     */
389    public void expandAll() {
390        visitTreeNodes(getRoot(), x -> expandPath(new TreePath(x.getPath())));
391    }
392
393    /**
394     * Returns the root node model.
395     * @return The root node model
396     */
397    public DefaultMutableTreeNode getRoot() {
398        return (DefaultMutableTreeNode) valTreeModel.getRoot();
399    }
400
401    private void clearErrors() {
402        if (errors != null) {
403            errors.clear();
404        }
405    }
406
407    @Override
408    public void destroy() {
409        DatasetEventManager.getInstance().removeDatasetListener(this);
410        ToolTipManager.sharedInstance().unregisterComponent(this);
411        clearErrors();
412    }
413
414    /**
415     * Visitor call for all tree nodes children of root, in breadth-first order.
416     * @param root Root node
417     * @param visitor Visitor
418     * @since 13940
419     */
420    public static void visitTreeNodes(DefaultMutableTreeNode root, Consumer<DefaultMutableTreeNode> visitor) {
421        @SuppressWarnings("unchecked")
422        Enumeration<TreeNode> errorMessages = root.breadthFirstEnumeration();
423        while (errorMessages.hasMoreElements()) {
424            visitor.accept(((DefaultMutableTreeNode) errorMessages.nextElement()));
425        }
426    }
427
428    /**
429     * Visitor call for all {@link TestError} nodes children of root, in breadth-first order.
430     * @param root Root node
431     * @param visitor Visitor
432     * @since 13940
433     */
434    public static void visitTestErrors(DefaultMutableTreeNode root, Consumer<TestError> visitor) {
435        visitTestErrors(root, visitor, null);
436    }
437
438    /**
439     * Visitor call for all {@link TestError} nodes children of root, in breadth-first order.
440     * @param root Root node
441     * @param visitor Visitor
442     * @param processedNodes Set of already visited nodes (optional)
443     * @since 13940
444     */
445    public static void visitTestErrors(DefaultMutableTreeNode root, Consumer<TestError> visitor,
446            Set<DefaultMutableTreeNode> processedNodes) {
447        visitTreeNodes(root, n -> {
448            if (processedNodes == null || !processedNodes.contains(n)) {
449                if (processedNodes != null) {
450                    processedNodes.add(n);
451                }
452                Object o = n.getUserObject();
453                if (o instanceof TestError) {
454                    visitor.accept((TestError) o);
455                }
456            }
457        });
458    }
459
460    @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
461        // Remove purged primitives (fix #8639)
462        if (errors != null) {
463            final Set<? extends OsmPrimitive> deletedPrimitives = new HashSet<>(event.getPrimitives());
464            errors.removeIf(error -> error.getPrimitives().stream().anyMatch(deletedPrimitives::contains));
465        }
466    }
467
468    @Override public void primitivesAdded(PrimitivesAddedEvent event) {
469        // Do nothing
470    }
471
472    @Override public void tagsChanged(TagsChangedEvent event) {
473        // Do nothing
474    }
475
476    @Override public void nodeMoved(NodeMovedEvent event) {
477        // Do nothing
478    }
479
480    @Override public void wayNodesChanged(WayNodesChangedEvent event) {
481        // Do nothing
482    }
483
484    @Override public void relationMembersChanged(RelationMembersChangedEvent event) {
485        // Do nothing
486    }
487
488    @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
489        // Do nothing
490    }
491
492    @Override public void dataChanged(DataChangedEvent event) {
493        // Do nothing
494    }
495}