001// License: GPL. See LICENSE file for details.
002package org.openstreetmap.josm.data.validation;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.List;
008import java.util.TreeSet;
009
010import org.openstreetmap.josm.Main;
011import org.openstreetmap.josm.command.Command;
012import org.openstreetmap.josm.data.osm.Node;
013import org.openstreetmap.josm.data.osm.OsmPrimitive;
014import org.openstreetmap.josm.data.osm.Relation;
015import org.openstreetmap.josm.data.osm.Way;
016import org.openstreetmap.josm.data.osm.WaySegment;
017import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
018import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
019import org.openstreetmap.josm.data.osm.event.DataSetListener;
020import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
021import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
022import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
023import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
024import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
025import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
026import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
027
028/**
029 * Validation error
030 * @since 3669
031 */
032public class TestError implements Comparable<TestError>, DataSetListener {
033    /** is this error on the ignore list */
034    private Boolean ignored = false;
035    /** Severity */
036    private Severity severity;
037    /** The error message */
038    private String message;
039    /** Deeper error description */
040    private String description;
041    private String description_en;
042    /** The affected primitives */
043    private Collection<? extends OsmPrimitive> primitives;
044    /** The primitives or way segments to be highlighted */
045    private Collection<?> highlighted;
046    /** The tester that raised this error */
047    private Test tester;
048    /** Internal code used by testers to classify errors */
049    private int code;
050    /** If this error is selected */
051    private boolean selected;
052
053    /**
054     * Constructs a new {@code TestError}.
055     * @param tester The tester
056     * @param severity The severity of this error
057     * @param message The error message
058     * @param primitives The affected primitives
059     * @param code The test error reference code
060     */
061    public TestError(Test tester, Severity severity, String message, String description, String description_en,
062            int code, Collection<? extends OsmPrimitive> primitives, Collection<?> highlighted) {
063        this.tester = tester;
064        this.severity = severity;
065        this.message = message;
066        this.description = description;
067        this.description_en = description_en;
068        this.primitives = primitives;
069        this.highlighted = highlighted;
070        this.code = code;
071    }
072
073    public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives,
074            Collection<?> highlighted) {
075        this(tester, severity, message, null, null, code, primitives, highlighted);
076    }
077
078    public TestError(Test tester, Severity severity, String message, String description, String description_en,
079            int code, Collection<? extends OsmPrimitive> primitives) {
080        this(tester, severity, message, description, description_en, code, primitives, primitives);
081    }
082
083    public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives) {
084        this(tester, severity, message, null, null, code, primitives, primitives);
085    }
086
087    public TestError(Test tester, Severity severity, String message, int code, OsmPrimitive primitive) {
088        this(tester, severity, message, null, null, code, Collections.singletonList(primitive), Collections
089                .singletonList(primitive));
090    }
091
092    public TestError(Test tester, Severity severity, String message, String description, String description_en,
093            int code, OsmPrimitive primitive) {
094        this(tester, severity, message, description, description_en, code, Collections.singletonList(primitive));
095    }
096
097    /**
098     * Gets the error message
099     * @return the error message
100     */
101    public String getMessage() {
102        return message;
103    }
104
105    /**
106     * Gets the error message
107     * @return the error description
108     */
109    public String getDescription() {
110        return description;
111    }
112
113    /**
114     * Sets the error message
115     * @param message The error message
116     */
117    public void setMessage(String message) {
118        this.message = message;
119    }
120
121    /**
122     * Gets the list of primitives affected by this error
123     * @return the list of primitives affected by this error
124     */
125    public Collection<? extends OsmPrimitive> getPrimitives() {
126        return primitives;
127    }
128
129    /**
130     * Gets the list of primitives affected by this error and are selectable
131     * @return the list of selectable primitives affected by this error
132     */
133    public Collection<? extends OsmPrimitive> getSelectablePrimitives() {
134        List<OsmPrimitive> selectablePrimitives = new ArrayList<OsmPrimitive>(primitives.size());
135        for (OsmPrimitive o : primitives) {
136            if (o.isSelectable()) {
137                selectablePrimitives.add(o);
138            }
139        }
140        return selectablePrimitives;
141    }
142
143    /**
144     * Sets the list of primitives affected by this error
145     * @param primitives the list of primitives affected by this error
146     */
147    public void setPrimitives(List<OsmPrimitive> primitives) {
148        this.primitives = primitives;
149    }
150
151    /**
152     * Gets the severity of this error
153     * @return the severity of this error
154     */
155    public Severity getSeverity() {
156        return severity;
157    }
158
159    /**
160     * Sets the severity of this error
161     * @param severity the severity of this error
162     */
163    public void setSeverity(Severity severity) {
164        this.severity = severity;
165    }
166
167    /**
168     * Sets the ignore state for this error
169     */
170    public String getIgnoreState() {
171        Collection<String> strings = new TreeSet<String>();
172        StringBuilder ignorestring = new StringBuilder(getIgnoreSubGroup());
173        for (OsmPrimitive o : primitives) {
174            // ignore data not yet uploaded
175            if (o.isNew())
176                return null;
177            String type = "u";
178            if (o instanceof Way) {
179                type = "w";
180            } else if (o instanceof Relation) {
181                type = "r";
182            } else if (o instanceof Node) {
183                type = "n";
184            }
185            strings.add(type + "_" + o.getId());
186        }
187        for (String o : strings) {
188            ignorestring.append(":").append(o);
189        }
190        return ignorestring.toString();
191    }
192
193    public String getIgnoreSubGroup() {
194        String ignorestring = getIgnoreGroup();
195        if (description_en != null) {
196            ignorestring += "_" + description_en;
197        }
198        return ignorestring;
199    }
200
201    public String getIgnoreGroup() {
202        return Integer.toString(code);
203    }
204
205    public void setIgnored(boolean state) {
206        ignored = state;
207    }
208
209    public Boolean getIgnored() {
210        return ignored;
211    }
212
213    /**
214     * Gets the tester that raised this error
215     * @return the tester that raised this error
216     */
217    public Test getTester() {
218        return tester;
219    }
220
221    /**
222     * Gets the code
223     * @return the code
224     */
225    public int getCode() {
226        return code;
227    }
228
229    /**
230     * Returns true if the error can be fixed automatically
231     *
232     * @return true if the error can be fixed
233     */
234    public boolean isFixable() {
235        return tester != null && tester.isFixable(this);
236    }
237
238    /**
239     * Fixes the error with the appropriate command
240     *
241     * @return The command to fix the error
242     */
243    public Command getFix() {
244        if (tester == null || !tester.isFixable(this) || primitives.isEmpty())
245            return null;
246
247        return tester.fixError(this);
248    }
249
250    /**
251     * Sets the selection flag of this error
252     * @param selected if this error is selected
253     */
254    public void setSelected(boolean selected) {
255        this.selected = selected;
256    }
257
258    @SuppressWarnings("unchecked")
259    public void visitHighlighted(ValidatorVisitor v) {
260        for (Object o : highlighted) {
261            if (o instanceof OsmPrimitive) {
262                v.visit((OsmPrimitive) o);
263            } else if (o instanceof WaySegment) {
264                v.visit((WaySegment) o);
265            } else if (o instanceof List<?>) {
266                v.visit((List<Node>)o);
267            }
268        }
269    }
270
271    /**
272     * Returns the selection flag of this error
273     * @return true if this error is selected
274     * @since 5671
275     */
276    public boolean isSelected() {
277        return selected;
278    }
279
280    /**
281     * Returns The primitives or way segments to be highlighted
282     * @return The primitives or way segments to be highlighted
283     * @since 5671
284     */
285    public Collection<?> getHighlighted() {
286        return highlighted;
287    }
288
289    @Override
290    public int compareTo(TestError o) {
291        if (equals(o)) return 0;
292
293        MultipleNameVisitor v1 = new MultipleNameVisitor();
294        MultipleNameVisitor v2 = new MultipleNameVisitor();
295
296        v1.visit(getPrimitives());
297        v2.visit(o.getPrimitives());
298        return v1.toString().compareToIgnoreCase(v2.toString());
299    }
300
301    @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
302        // Remove purged primitives (fix #8639)
303        try {
304            primitives.removeAll(event.getPrimitives());
305        } catch (UnsupportedOperationException e) {
306            if (event.getPrimitives().containsAll(primitives)) {
307                primitives = Collections.emptyList();
308            } else {
309                Main.warn("Unable to remove primitives from "+this);
310            }
311        }
312    }
313
314    @Override public void primitivesAdded(PrimitivesAddedEvent event) {}
315    @Override public void tagsChanged(TagsChangedEvent event) {}
316    @Override public void nodeMoved(NodeMovedEvent event) {}
317    @Override public void wayNodesChanged(WayNodesChangedEvent event) {}
318    @Override public void relationMembersChanged(RelationMembersChangedEvent event) {}
319    @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {}
320    @Override public void dataChanged(DataChangedEvent event) {}
321
322    @Override
323    public String toString() {
324        return "TestError [tester=" + tester + ", code=" + code + "]";
325    }
326}