// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs.validator;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.swing.JTree;
import javax.swing.ToolTipManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataSetListener;
import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
import org.openstreetmap.josm.data.validation.Severity;
import org.openstreetmap.josm.data.validation.TestError;
import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.tools.AlphanumComparator;
import org.openstreetmap.josm.tools.Destroyable;
import org.openstreetmap.josm.tools.ListenerList;
/**
* A panel that displays the error tree. The selection manager
* respects clicks into the selection list. Ctrl-click will remove entries from
* the list while single click will make the clicked entry the only selection.
*
* @author frsantos
*/
public class ValidatorTreePanel extends JTree implements Destroyable, DataSetListener {
private static final class GroupTreeNode extends DefaultMutableTreeNode {
GroupTreeNode(Object userObject) {
super(userObject);
}
@Override
public String toString() {
return tr("{0} ({1})", super.toString(), getLeafCount());
}
}
/**
* The validation data.
*/
protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
/** The list of errors shown in the tree */
private transient List<TestError> errors = new ArrayList<>();
/**
* If {@link #filter} is not <code>null</code> only errors are displayed
* that refer to one of the primitives in the filter.
*/
private transient Set<? extends OsmPrimitive> filter;
private final ListenerList<Runnable> invalidationListeners = ListenerList.create();
/**
* Constructor
* @param errors The list of errors
*/
public ValidatorTreePanel(List<TestError> errors) {
ToolTipManager.sharedInstance().registerComponent(this);
this.setModel(valTreeModel);
this.setRootVisible(false);
this.setShowsRootHandles(true);
this.expandRow(0);
this.setVisibleRowCount(8);
this.setCellRenderer(new ValidatorTreeRenderer());
this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
setErrorList(errors);
for (KeyListener keyListener : getKeyListeners()) {
// Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands
if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) {
removeKeyListener(keyListener);
}
}
DatasetEventManager.getInstance().addDatasetListener(this, DatasetEventManager.FireMode.IN_EDT);
}
@Override
public String getToolTipText(MouseEvent e) {
String res = null;
TreePath path = getPathForLocation(e.getX(), e.getY());
if (path != null) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
Object nodeInfo = node.getUserObject();
if (nodeInfo instanceof TestError) {
TestError error = (TestError) nodeInfo;
MultipleNameVisitor v = new MultipleNameVisitor();
v.visit(error.getPrimitives());
res = "<html>" + v.getText() + "<br>" + error.getMessage();
String d = error.getDescription();
if (d != null)
res += "<br>" + d;
res += "</html>";
} else {
res = node.toString();
}
}
return res;
}
/** Constructor */
public ValidatorTreePanel() {
this(null);
}
@Override
public void setVisible(boolean v) {
if (v) {
buildTree();
} else {
valTreeModel.setRoot(new DefaultMutableTreeNode());
}
super.setVisible(v);
invalidationListeners.fireEvent(Runnable::run);
}
/**
* Builds the errors tree
*/
public void buildTree() {
final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
if (errors == null || errors.isEmpty()) {
GuiHelper.runInEDTAndWait(() -> valTreeModel.setRoot(rootNode));
return;
}
// Sort validation errors - #8517
Collections.sort(errors);
// Remember the currently expanded rows
Set<Object> oldSelectedRows = new HashSet<>();
Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
if (expanded != null) {
while (expanded.hasMoreElements()) {
TreePath path = expanded.nextElement();
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
Object userObject = node.getUserObject();
if (userObject instanceof Severity) {
oldSelectedRows.add(userObject);
} else if (userObject instanceof String) {
String msg = (String) userObject;
int index = msg.lastIndexOf(" (");
if (index > 0) {
msg = msg.substring(0, index);
}
oldSelectedRows.add(msg);
}
}
}
Predicate<TestError> filterToUse = e -> !e.isIgnored();
if (!ValidatorPreference.PREF_OTHER.get()) {
filterToUse = filterToUse.and(e -> e.getSeverity() != Severity.OTHER);
}
if (filter != null) {
filterToUse = filterToUse.and(e -> e.getPrimitives().stream().anyMatch(filter::contains));
}
Map<Severity, Map<String, Map<String, List<TestError>>>> errorsBySeverityMessageDescription
= errors.stream().filter(filterToUse).collect(
Collectors.groupingBy(TestError::getSeverity, () -> new EnumMap<>(Severity.class),
Collectors.groupingBy(TestError::getMessage, () -> new TreeMap<>(AlphanumComparator.getInstance()),
Collectors.groupingBy(e -> e.getDescription() == null ? "" : e.getDescription(),
() -> new TreeMap<>(AlphanumComparator.getInstance()),
Collectors.toList()
))));
final List<TreePath> expandedPaths = new ArrayList<>();
errorsBySeverityMessageDescription.forEach((severity, errorsByMessageDescription) -> {
// Severity node
final DefaultMutableTreeNode severityNode = new GroupTreeNode(severity);
rootNode.add(severityNode);
if (oldSelectedRows.contains(severity)) {
expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode}));
}
final Map<String, List<TestError>> errorsWithEmptyMessageByDescription = errorsByMessageDescription.get("");
if (errorsWithEmptyMessageByDescription != null) {
errorsWithEmptyMessageByDescription.forEach((description, errors) -> {
final String msg = tr("{0} ({1})", description, errors.size());
final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
severityNode.add(messageNode);
if (oldSelectedRows.contains(description)) {
expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
}
errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
});
}
errorsByMessageDescription.forEach((message, errorsByDescription) -> {
if (message.isEmpty()) {
return;
}
// Group node
final DefaultMutableTreeNode groupNode;
if (errorsByDescription.size() > 1) {
groupNode = new GroupTreeNode(message);
severityNode.add(groupNode);
if (oldSelectedRows.contains(message)) {
expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode}));
}
} else {
groupNode = null;
}
errorsByDescription.forEach((description, errors) -> {
// Message node
final String msg;
if (groupNode != null) {
msg = tr("{0} ({1})", description, errors.size());
} else if (description == null || description.isEmpty()) {
msg = tr("{0} ({1})", message, errors.size());
} else {
msg = tr("{0} - {1} ({2})", message, description, errors.size());
}
final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
if (groupNode != null) {
groupNode.add(messageNode);
} else {
severityNode.add(messageNode);
}
if (oldSelectedRows.contains(description)) {
if (groupNode != null) {
expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode, messageNode}));
} else {
expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
}
}
errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
});
});
});
valTreeModel.setRoot(rootNode);
for (TreePath path : expandedPaths) {
this.expandPath(path);
}
invalidationListeners.fireEvent(Runnable::run);
}
/**
* Add a new invalidation listener
* @param listener The listener
*/
public void addInvalidationListener(Runnable listener) {
invalidationListeners.addListener(listener);
}
/**
* Remove an invalidation listener
* @param listener The listener
* @since 10880
*/
public void removeInvalidationListener(Runnable listener) {
invalidationListeners.removeListener(listener);
}
/**
* Sets the errors list used by a data layer
* @param errors The error list that is used by a data layer
*/
public final void setErrorList(List<TestError> errors) {
this.errors = errors;
if (isVisible()) {
buildTree();
}
}
/**
* Clears the current error list and adds these errors to it
* @param newerrors The validation errors
*/
public void setErrors(List<TestError> newerrors) {
if (errors == null)
return;
clearErrors();
for (TestError error : newerrors) {
if (!error.isIgnored()) {
errors.add(error);
}
}
if (isVisible()) {
buildTree();
}
}
/**
* Returns the errors of the tree
* @return the errors of the tree
*/
public List<TestError> getErrors() {
return errors != null ? errors : Collections.<TestError>emptyList();
}
/**
* Selects all errors related to the specified {@code primitives}, i.e. where {@link TestError#getPrimitives()}
* returns a primitive present in {@code primitives}.
* @param primitives collection of primitives
*/
public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) {
final Collection<TreePath> paths = new ArrayList<>();
walkAndSelectRelatedErrors(new TreePath(getRoot()), new HashSet<>(primitives)::contains, paths);
getSelectionModel().clearSelection();
for (TreePath path : paths) {
expandPath(path);
getSelectionModel().addSelectionPath(path);
}
}
private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) {
final int count = getModel().getChildCount(p.getLastPathComponent());
for (int i = 0; i < count; i++) {
final Object child = getModel().getChild(p.getLastPathComponent(), i);
if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode
&& ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) {
final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject();
if (error.getPrimitives().stream().anyMatch(isRelevant)) {
paths.add(p.pathByAddingChild(child));
}
} else {
walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths);
}
}
}
/**
* Returns the filter list
* @return the list of primitives used for filtering
*/
public Set<? extends OsmPrimitive> getFilter() {
return filter;
}
/**
* Set the filter list to a set of primitives
* @param filter the list of primitives used for filtering
*/
public void setFilter(Set<? extends OsmPrimitive> filter) {
if (filter != null && filter.isEmpty()) {
this.filter = null;
} else {
this.filter = filter;
}
if (isVisible()) {
buildTree();
}
}
/**
* Updates the current errors list
*/
public void resetErrors() {
List<TestError> e = new ArrayList<>(errors);
setErrors(e);
}
/**
* Expands complete tree
*/
@SuppressWarnings("unchecked")
public void expandAll() {
DefaultMutableTreeNode root = getRoot();
int row = 0;
Enumeration<TreeNode> children = root.breadthFirstEnumeration();
while (children.hasMoreElements()) {
children.nextElement();
expandRow(row++);
}
}
/**
* Returns the root node model.
* @return The root node model
*/
public DefaultMutableTreeNode getRoot() {
return (DefaultMutableTreeNode) valTreeModel.getRoot();
}
private void clearErrors() {
if (errors != null) {
errors.clear();
}
}
@Override
public void destroy() {
DataSet ds = Main.getLayerManager().getEditDataSet();
if (ds != null) {
ds.removeDataSetListener(this);
}
clearErrors();
}
@Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
// Remove purged primitives (fix #8639)
if (errors != null) {
final Set<? extends OsmPrimitive> deletedPrimitives = new HashSet<>(event.getPrimitives());
errors.removeIf(error -> error.getPrimitives().stream().anyMatch(deletedPrimitives::contains));
}
}
@Override public void primitivesAdded(PrimitivesAddedEvent event) {
// Do nothing
}
@Override public void tagsChanged(TagsChangedEvent event) {
// Do nothing
}
@Override public void nodeMoved(NodeMovedEvent event) {
// Do nothing
}
@Override public void wayNodesChanged(WayNodesChangedEvent event) {
// Do nothing
}
@Override public void relationMembersChanged(RelationMembersChangedEvent event) {
// Do nothing
}
@Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
// Do nothing
}
@Override public void dataChanged(DataChangedEvent event) {
// Do nothing
}
}