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