001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Graphics2D;
007import java.io.File;
008import java.util.Arrays;
009import java.util.Collections;
010import java.util.List;
011import java.util.stream.Collectors;
012
013import javax.swing.Action;
014import javax.swing.Icon;
015import javax.swing.tree.DefaultMutableTreeNode;
016
017import org.openstreetmap.josm.actions.RenameLayerAction;
018import org.openstreetmap.josm.actions.SaveActionBase;
019import org.openstreetmap.josm.data.Bounds;
020import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
021import org.openstreetmap.josm.data.validation.OsmValidator;
022import org.openstreetmap.josm.data.validation.Severity;
023import org.openstreetmap.josm.data.validation.TestError;
024import org.openstreetmap.josm.gui.MainApplication;
025import org.openstreetmap.josm.gui.MapView;
026import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
027import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
028import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
029import org.openstreetmap.josm.gui.io.importexport.ValidatorErrorExporter;
030import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
031import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
032import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
033import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
034import org.openstreetmap.josm.gui.layer.validation.PaintVisitor;
035import org.openstreetmap.josm.tools.ImageProvider;
036import org.openstreetmap.josm.tools.MultiMap;
037
038/**
039 * A layer showing error messages.
040 *
041 * @author frsantos
042 *
043 * @since  3669 (creation)
044 * @since 10386 (new LayerChangeListener interface)
045 */
046public class ValidatorLayer extends Layer implements LayerChangeListener {
047    private final Runnable invalidator = this::invalidate;
048
049    /**
050     * Constructs a new Validator layer
051     */
052    public ValidatorLayer() {
053        super(tr("Validation errors"));
054        MainApplication.getLayerManager().addLayerChangeListener(this);
055        MainApplication.getMap().validatorDialog.tree.addInvalidationListener(invalidator);
056    }
057
058    /**
059     * Return a static icon.
060     */
061    @Override
062    public Icon getIcon() {
063        return ImageProvider.get("layer", "validator_small");
064    }
065
066    /**
067     * Draw all primitives in this layer but do not draw modified ones (they
068     * are drawn by the edit layer).
069     * Draw nodes last to overlap the ways they belong to.
070     */
071    @Override
072    public void paint(final Graphics2D g, final MapView mv, Bounds bounds) {
073        DefaultMutableTreeNode root = MainApplication.getMap().validatorDialog.tree.getRoot();
074        if (root == null || root.getChildCount() == 0)
075            return;
076
077        PaintVisitor paintVisitor = new PaintVisitor(g, mv);
078
079        DefaultMutableTreeNode severity = (DefaultMutableTreeNode) root.getLastChild();
080        while (severity != null) {
081            ValidatorTreePanel.visitTestErrors(severity, paintVisitor::visit);
082
083            // Severities in inverse order
084            severity = severity.getPreviousSibling();
085        }
086
087        paintVisitor.clearPaintedObjects();
088    }
089
090    @Override
091    public String getToolTipText() {
092        MultiMap<Severity, TestError> errorTree = new MultiMap<>();
093        List<TestError> errors = MainApplication.getMap().validatorDialog.tree.getErrors();
094        for (TestError e : errors) {
095            errorTree.put(e.getSeverity(), e);
096        }
097
098        String b = Arrays.stream(Severity.values())
099                .filter(errorTree::containsKey)
100                .map(s -> tr(s.toString()) + ": " + errorTree.get(s).size() + "<br>")
101                .collect(Collectors.joining());
102
103        if (b.length() == 0)
104            return "<html>" + tr("No validation errors") + "</html>";
105        else
106            return "<html>" + tr("Validation errors") + ":<br>" + b + "</html>";
107    }
108
109    @Override
110    public void mergeFrom(Layer from) {
111        // Do nothing
112    }
113
114    @Override
115    public boolean isMergable(Layer other) {
116        return false;
117    }
118
119    @Override
120    public void visitBoundingBox(BoundingXYVisitor v) {
121        // Do nothing
122    }
123
124    @Override
125    public Object getInfoComponent() {
126        return getToolTipText();
127    }
128
129    @Override
130    public Action[] getMenuEntries() {
131        return new Action[] {
132                LayerListDialog.getInstance().createShowHideLayerAction(),
133                LayerListDialog.getInstance().createDeleteLayerAction(),
134                SeparatorLayerAction.INSTANCE,
135                new RenameLayerAction(null, this),
136                SeparatorLayerAction.INSTANCE,
137                new LayerListPopup.InfoAction(this),
138                new LayerSaveAsAction(this)
139                };
140    }
141
142    @Override
143    public boolean isSavable() {
144        return true; // With ValidatorErrorExporter
145    }
146
147    @Override
148    public boolean checkSaveConditions() {
149        return true;
150    }
151
152    @Override
153    public File createAndOpenSaveFileChooser() {
154        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Validation errors file"), ValidatorErrorExporter.FILE_FILTER);
155    }
156
157    @Override
158    public void layerOrderChanged(LayerOrderChangeEvent e) {
159        // Do nothing
160    }
161
162    @Override
163    public void layerAdded(LayerAddEvent e) {
164        // Do nothing
165    }
166
167    /**
168     * If layer is the OSM Data layer, remove all errors
169     */
170    @Override
171    public void layerRemoving(LayerRemoveEvent e) {
172        // Removed layer is still in that list.
173        if (e.getRemovedLayer() instanceof OsmDataLayer && e.getSource().getLayersOfType(OsmDataLayer.class).size() <= 1) {
174            e.scheduleRemoval(Collections.singleton(this));
175        } else if (e.getRemovedLayer() == this) {
176            OsmValidator.resetErrorLayer();
177        }
178    }
179
180    @Override
181    public LayerPositionStrategy getDefaultLayerPosition() {
182        return LayerPositionStrategy.IN_FRONT;
183    }
184
185    @Override
186    public synchronized void destroy() {
187        MainApplication.getMap().validatorDialog.tree.removeInvalidationListener(invalidator);
188        MainApplication.getLayerManager().removeLayerChangeListener(this);
189        super.destroy();
190    }
191}