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