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