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