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