001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation;
003
004import java.awt.geom.Area;
005import java.awt.geom.PathIterator;
006import java.text.MessageFormat;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.List;
012import java.util.Locale;
013import java.util.TreeSet;
014import java.util.function.Supplier;
015import java.util.stream.Collectors;
016import java.util.stream.Stream;
017
018import org.openstreetmap.josm.command.Command;
019import org.openstreetmap.josm.data.coor.EastNorth;
020import org.openstreetmap.josm.data.osm.Node;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.OsmUtils;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.Way;
025import org.openstreetmap.josm.data.osm.WaySegment;
026import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
027import org.openstreetmap.josm.tools.AlphanumComparator;
028import org.openstreetmap.josm.tools.CheckParameterUtil;
029import org.openstreetmap.josm.tools.I18n;
030
031/**
032 * Validation error
033 * @since 3669
034 */
035public class TestError implements Comparable<TestError> {
036    /** is this error on the ignore list */
037    private boolean ignored;
038    /** Severity */
039    private final Severity severity;
040    /** The error message */
041    private final String message;
042    /** Deeper error description */
043    private final String description;
044    private final String descriptionEn;
045    /** The affected primitives */
046    private final Collection<? extends OsmPrimitive> primitives;
047    /** The primitives or way segments to be highlighted */
048    private final Collection<?> highlighted;
049    /** The tester that raised this error */
050    private final Test tester;
051    /** Internal code used by testers to classify errors */
052    private final int code;
053    /** If this error is selected */
054    private boolean selected;
055    /** Supplying a command to fix the error */
056    private final Supplier<Command> fixingCommand;
057
058    /**
059     * A builder for a {@code TestError}.
060     * @since 11129
061     */
062    public static final class Builder {
063        private final Test tester;
064        private final Severity severity;
065        private final int code;
066        private String message;
067        private String description;
068        private String descriptionEn;
069        private Collection<? extends OsmPrimitive> primitives;
070        private Collection<?> highlighted;
071        private Supplier<Command> fixingCommand;
072
073        Builder(Test tester, Severity severity, int code) {
074            this.tester = tester;
075            this.severity = severity;
076            this.code = code;
077        }
078
079        /**
080         * Sets the error message.
081         *
082         * @param message The error message
083         * @return {@code this}
084         */
085        public Builder message(String message) {
086            this.message = message;
087            return this;
088        }
089
090        /**
091         * Sets the error message.
092         *
093         * @param message       The message of this error group
094         * @param description   The translated description of this error
095         * @param descriptionEn The English description (for ignoring errors)
096         * @return {@code this}
097         */
098        public Builder messageWithManuallyTranslatedDescription(String message, String description, String descriptionEn) {
099            this.message = message;
100            this.description = description;
101            this.descriptionEn = descriptionEn;
102            return this;
103        }
104
105        /**
106         * Sets the error message.
107         *
108         * @param message The message of this error group
109         * @param marktrDescription The {@linkplain I18n#marktr prepared for i18n} description of this error
110         * @param args The description arguments to be applied in {@link I18n#tr(String, Object...)}
111         * @return {@code this}
112         */
113        public Builder message(String message, String marktrDescription, Object... args) {
114            this.message = message;
115            this.description = I18n.tr(marktrDescription, args);
116            this.descriptionEn = new MessageFormat(marktrDescription, Locale.ENGLISH).format(args);
117            return this;
118        }
119
120        /**
121         * Sets the primitives affected by this error.
122         *
123         * @param primitives the primitives affected by this error
124         * @return {@code this}
125         */
126        public Builder primitives(OsmPrimitive... primitives) {
127            return primitives(Arrays.asList(primitives));
128        }
129
130        /**
131         * Sets the primitives affected by this error.
132         *
133         * @param primitives the primitives affected by this error
134         * @return {@code this}
135         */
136        public Builder primitives(Collection<? extends OsmPrimitive> primitives) {
137            CheckParameterUtil.ensureThat(this.primitives == null, "primitives already set");
138            CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
139            this.primitives = primitives;
140            if (this.highlighted == null) {
141                this.highlighted = primitives;
142            }
143            return this;
144        }
145
146        /**
147         * Sets the primitives to highlight when selecting this error.
148         *
149         * @param highlighted the primitives to highlight
150         * @return {@code this}
151         * @see ValidatorVisitor#visit(OsmPrimitive)
152         */
153        public Builder highlight(OsmPrimitive... highlighted) {
154            return highlight(Arrays.asList(highlighted));
155        }
156
157        /**
158         * Sets the primitives to highlight when selecting this error.
159         *
160         * @param highlighted the primitives to highlight
161         * @return {@code this}
162         * @see ValidatorVisitor#visit(OsmPrimitive)
163         */
164        public Builder highlight(Collection<? extends OsmPrimitive> highlighted) {
165            CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
166            this.highlighted = highlighted;
167            return this;
168        }
169
170        /**
171         * Sets the way segments to highlight when selecting this error.
172         *
173         * @param highlighted the way segments to highlight
174         * @return {@code this}
175         * @see ValidatorVisitor#visit(WaySegment)
176         */
177        public Builder highlightWaySegments(Collection<WaySegment> highlighted) {
178            CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
179            this.highlighted = highlighted;
180            return this;
181        }
182
183        /**
184         * Sets the node pairs to highlight when selecting this error.
185         *
186         * @param highlighted the node pairs to highlight
187         * @return {@code this}
188         * @see ValidatorVisitor#visit(List)
189         */
190        public Builder highlightNodePairs(Collection<List<Node>> highlighted) {
191            CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
192            this.highlighted = highlighted;
193            return this;
194        }
195
196        /**
197         * Sets an area to highlight when selecting this error.
198         *
199         * @param highlighted the area to highlight
200         * @return {@code this}
201         */
202        public Builder highlight(Area highlighted) {
203            CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
204            this.highlighted = Collections.singleton(highlighted);
205            return this;
206        }
207
208        /**
209         * Sets a supplier to obtain a command to fix the error.
210         *
211         * @param fixingCommand the fix supplier. Can be null
212         * @return {@code this}
213         */
214        public Builder fix(Supplier<Command> fixingCommand) {
215            CheckParameterUtil.ensureThat(this.fixingCommand == null, "fixingCommand already set");
216            this.fixingCommand = fixingCommand;
217            return this;
218        }
219
220        /**
221         * Returns a new test error with the specified values
222         *
223         * @return a new test error with the specified values
224         * @throws IllegalArgumentException when {@link #message} or {@link #primitives} is null.
225         */
226        public TestError build() {
227            CheckParameterUtil.ensureParameterNotNull(message, "message not set");
228            CheckParameterUtil.ensureParameterNotNull(primitives, "primitives not set");
229            if (this.highlighted == null) {
230                this.highlighted = Collections.emptySet();
231            }
232            return new TestError(this);
233        }
234    }
235
236    /**
237     * Starts building a new {@code TestError}
238     * @param tester The tester
239     * @param severity The severity of this error
240     * @param code The test error reference code
241     * @return a new test builder
242     * @since 11129
243     */
244    public static Builder builder(Test tester, Severity severity, int code) {
245        return new Builder(tester, severity, code);
246    }
247
248    TestError(Builder builder) {
249        this.tester = builder.tester;
250        this.severity = builder.severity;
251        this.message = builder.message;
252        this.description = builder.description;
253        this.descriptionEn = builder.descriptionEn;
254        this.primitives = builder.primitives;
255        this.highlighted = builder.highlighted;
256        this.code = builder.code;
257        this.fixingCommand = builder.fixingCommand;
258    }
259
260    /**
261     * Gets the error message
262     * @return the error message
263     */
264    public String getMessage() {
265        return message;
266    }
267
268    /**
269     * Gets the error message
270     * @return the error description
271     */
272    public String getDescription() {
273        return description;
274    }
275
276    /**
277     * Gets the list of primitives affected by this error
278     * @return the list of primitives affected by this error
279     */
280    public Collection<? extends OsmPrimitive> getPrimitives() {
281        return Collections.unmodifiableCollection(primitives);
282    }
283
284    /**
285     * Gets all primitives of the given type affected by this error
286     * @param type restrict primitives to subclasses
287     * @param <T> type of primitives
288     * @return the primitives as Stream
289     */
290    public final <T extends OsmPrimitive> Stream<T> primitives(Class<T> type) {
291        return primitives.stream()
292                .filter(type::isInstance)
293                .map(type::cast);
294    }
295
296    /**
297     * Gets the severity of this error
298     * @return the severity of this error
299     */
300    public Severity getSeverity() {
301        return severity;
302    }
303
304    /**
305     * Returns the ignore state for this error.
306     * @return the ignore state for this error or null if any primitive is new
307     */
308    public String getIgnoreState() {
309        Collection<String> strings = new TreeSet<>();
310        for (OsmPrimitive o : primitives) {
311            // ignore data not yet uploaded
312            if (o.isNew())
313                return null;
314            String type = "u";
315            if (o instanceof Way) {
316                type = "w";
317            } else if (o instanceof Relation) {
318                type = "r";
319            } else if (o instanceof Node) {
320                type = "n";
321            }
322            strings.add(type + '_' + o.getId());
323        }
324        return strings.stream().map(o -> ':' + o).collect(Collectors.joining("", getIgnoreSubGroup(), ""));
325    }
326
327    /**
328     * Check if this error matches an entry in the ignore list and
329     * set the ignored flag if it is.
330     * @return the new ignored state
331     */
332    public boolean updateIgnored() {
333        setIgnored(calcIgnored());
334        return isIgnored();
335    }
336
337    private boolean calcIgnored() {
338        if (OsmValidator.hasIgnoredError(getIgnoreGroup()))
339            return true;
340        if (OsmValidator.hasIgnoredError(getIgnoreSubGroup()))
341            return true;
342        String state = getIgnoreState();
343        return state != null && OsmValidator.hasIgnoredError(state);
344    }
345
346    /**
347     * Gets the ignores subgroup that is more specialized than {@link #getIgnoreGroup()}
348     * @return The ignore sub group
349     */
350    public String getIgnoreSubGroup() {
351        if (code == 3000) {
352            // see #19053
353            return "3000_" + (description == null ? message : description);
354        }
355        String ignorestring = getIgnoreGroup();
356        if (descriptionEn != null) {
357            ignorestring += '_' + descriptionEn;
358        }
359        return ignorestring;
360    }
361
362    /**
363     * Gets the ignore group ID that is used to allow the user to ignore all same errors
364     * @return The group id
365     * @see TestError#getIgnoreSubGroup()
366     */
367    public String getIgnoreGroup() {
368        if (code == 3000) {
369            // see #19053
370            return "3000_" + getMessage();
371        }
372        return Integer.toString(code);
373    }
374
375    /**
376     * Flags this error as ignored
377     * @param state The ignore flag
378     */
379    public void setIgnored(boolean state) {
380        ignored = state;
381    }
382
383    /**
384     * Checks if this error is ignored
385     * @return <code>true</code> if it is ignored
386     */
387    public boolean isIgnored() {
388        return ignored;
389    }
390
391    /**
392     * Gets the tester that raised this error
393     * @return the tester that raised this error
394     */
395    public Test getTester() {
396        return tester;
397    }
398
399    /**
400     * Gets the code
401     * @return the code
402     */
403    public int getCode() {
404        return code;
405    }
406
407    /**
408     * Returns true if the error can be fixed automatically
409     *
410     * @return true if the error can be fixed
411     */
412    public boolean isFixable() {
413        return (fixingCommand != null || ((tester != null) && tester.isFixable(this)))
414                && OsmUtils.isOsmCollectionEditable(primitives);
415    }
416
417    /**
418     * Fixes the error with the appropriate command
419     *
420     * @return The command to fix the error
421     */
422    public Command getFix() {
423        // obtain fix from the error
424        final Command fix = fixingCommand != null ? fixingCommand.get() : null;
425        if (fix != null) {
426            return fix;
427        }
428
429        // obtain fix from the tester
430        if (tester == null || !tester.isFixable(this) || primitives.isEmpty())
431            return null;
432
433        return tester.fixError(this);
434    }
435
436    /**
437     * Sets the selection flag of this error
438     * @param selected if this error is selected
439     */
440    public void setSelected(boolean selected) {
441        this.selected = selected;
442    }
443
444    /**
445     * Visits all highlighted validation elements
446     * @param v The visitor that should receive a visit-notification on all highlighted elements
447     */
448    @SuppressWarnings("unchecked")
449    public void visitHighlighted(ValidatorVisitor v) {
450        for (Object o : highlighted) {
451            if (o instanceof OsmPrimitive) {
452                v.visit((OsmPrimitive) o);
453            } else if (o instanceof WaySegment) {
454                v.visit((WaySegment) o);
455            } else if (o instanceof List<?>) {
456                v.visit((List<Node>) o);
457            } else if (o instanceof Area) {
458                for (List<Node> l : getHiliteNodesForArea((Area) o)) {
459                    v.visit(l);
460                }
461            }
462        }
463    }
464
465    /**
466     * Calculate list of node pairs describing the area.
467     * @param area the area
468     * @return list of node pairs describing the area
469     */
470    private static List<List<Node>> getHiliteNodesForArea(Area area) {
471        List<List<Node>> hilite = new ArrayList<>();
472        PathIterator pit = area.getPathIterator(null);
473        double[] res = new double[6];
474        List<Node> nodes = new ArrayList<>();
475        while (!pit.isDone()) {
476            int type = pit.currentSegment(res);
477            Node n = new Node(new EastNorth(res[0], res[1]));
478            switch (type) {
479            case PathIterator.SEG_MOVETO:
480                if (!nodes.isEmpty()) {
481                    hilite.add(nodes);
482                }
483                nodes = new ArrayList<>();
484                nodes.add(n);
485                break;
486            case PathIterator.SEG_LINETO:
487                nodes.add(n);
488                break;
489            case PathIterator.SEG_CLOSE:
490                if (!nodes.isEmpty()) {
491                    nodes.add(nodes.get(0));
492                    hilite.add(nodes);
493                    nodes = new ArrayList<>();
494                }
495                break;
496            default:
497                break;
498            }
499            pit.next();
500        }
501        if (nodes.size() > 1) {
502            hilite.add(nodes);
503        }
504        return hilite;
505    }
506
507    /**
508     * Returns the selection flag of this error
509     * @return true if this error is selected
510     * @since 5671
511     */
512    public boolean isSelected() {
513        return selected;
514    }
515
516    /**
517     * Returns The primitives or way segments to be highlighted
518     * @return The primitives or way segments to be highlighted
519     * @since 5671
520     */
521    public Collection<?> getHighlighted() {
522        return Collections.unmodifiableCollection(highlighted);
523    }
524
525    @Override
526    public int compareTo(TestError o) {
527        if (equals(o)) return 0;
528
529        return AlphanumComparator.getInstance().compare(getNameVisitor().toString(), o.getNameVisitor().toString());
530    }
531
532    /**
533     * Returns a new {@link MultipleNameVisitor} for the list of primitives affected by this error.
534     * @return Name visitor (used in cell renderer and for sorting)
535     */
536    public MultipleNameVisitor getNameVisitor() {
537        MultipleNameVisitor v = new MultipleNameVisitor();
538        v.visit(getPrimitives());
539        return v;
540    }
541
542    /**
543     * Tests if two errors are similar, i.e.,
544     * same code and description and same combination of primitives and same combination of highlighted objects, but maybe with different orders.
545     * @param other the other error to be compared
546     * @return true if two errors are similar
547     */
548    public boolean isSimilar(TestError other) {
549        return getCode() == other.getCode()
550                && getMessage().equals(other.getMessage())
551                && getPrimitives().size() == other.getPrimitives().size()
552                && getPrimitives().containsAll(other.getPrimitives())
553                && highlightedIsEqual(getHighlighted(), other.getHighlighted());
554    }
555
556    private static boolean highlightedIsEqual(Collection<?> highlighted, Collection<?> highlighted2) {
557        if (highlighted.size() == highlighted2.size()) {
558            if (!highlighted.isEmpty()) {
559                Object h1 = highlighted.iterator().next();
560                Object h2 = highlighted2.iterator().next();
561                if (h1 instanceof Area && h2 instanceof Area) {
562                    return ((Area) h1).equals((Area) h2);
563                }
564                return highlighted.containsAll(highlighted2);
565            }
566            return true;
567        }
568        return false;
569    }
570
571    @Override
572    public String toString() {
573        return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']';
574    }
575
576}