source: josm/trunk/src/org/openstreetmap/josm/data/validation/TestError.java@ 18637

Last change on this file since 18637 was 18637, checked in by taylor.smock, 16 months ago

See #18230: Move RightAngleBuildingTest error code to 4101 from 3701.

This is due to clashing with an error code from pt_assistant.

See also #21423: Ensure validator codes are unique -- replace missed
since xxx with since 18636.

  • Property svn:eol-style set to native
File size: 22.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation;
3
4import java.awt.geom.Area;
5import java.awt.geom.PathIterator;
6import java.text.MessageFormat;
7import java.time.Instant;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.List;
13import java.util.Locale;
14import java.util.Map;
15import java.util.TreeSet;
16import java.util.function.Supplier;
17import java.util.stream.Collectors;
18import java.util.stream.Stream;
19
20import org.openstreetmap.josm.command.Command;
21import org.openstreetmap.josm.data.coor.EastNorth;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.OsmUtils;
25import org.openstreetmap.josm.data.osm.Relation;
26import org.openstreetmap.josm.data.osm.Way;
27import org.openstreetmap.josm.data.osm.WaySegment;
28import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
29import org.openstreetmap.josm.tools.AlphanumComparator;
30import org.openstreetmap.josm.tools.CheckParameterUtil;
31import org.openstreetmap.josm.tools.I18n;
32
33/**
34 * Validation error
35 * @since 3669
36 */
37public class TestError implements Comparable<TestError> {
38 /**
39 * Used to switch users over to new ignore system, UNIQUE_CODE_MESSAGE_STATE
40 * 1_704_067_200L -> 2024-01-01
41 * We can probably remove this and the supporting code in 2025.
42 */
43 private static boolean switchOver = Instant.now().isAfter(Instant.ofEpochMilli(1_704_067_200L));
44 /** is this error on the ignore list */
45 private boolean ignored;
46 /** Severity */
47 private final Severity severity;
48 /** The error message */
49 private final String message;
50 /** Deeper error description */
51 private final String description;
52 private final String descriptionEn;
53 /** The affected primitives */
54 private final Collection<? extends OsmPrimitive> primitives;
55 /** The primitives or way segments to be highlighted */
56 private final Collection<?> highlighted;
57 /** The tester that raised this error */
58 private final Test tester;
59 /** Internal code used by testers to classify errors */
60 private final int code;
61 /** Internal code used by testers to classify errors. Used for moving between JOSM versions. */
62 private final int uniqueCode;
63 /** If this error is selected */
64 private boolean selected;
65 /** Supplying a command to fix the error */
66 private final Supplier<Command> fixingCommand;
67
68 /**
69 * A builder for a {@code TestError}.
70 * @since 11129
71 */
72 public static final class Builder {
73 private final Test tester;
74 private final Severity severity;
75 private final int code;
76 private final int uniqueCode;
77 private String message;
78 private String description;
79 private String descriptionEn;
80 private Collection<? extends OsmPrimitive> primitives;
81 private Collection<?> highlighted;
82 private Supplier<Command> fixingCommand;
83
84 Builder(Test tester, Severity severity, int code) {
85 this.tester = tester;
86 this.severity = severity;
87 this.code = code;
88 this.uniqueCode = this.tester != null ? this.tester.getClass().getName().hashCode() : code;
89 }
90
91 /**
92 * Sets the error message.
93 *
94 * @param message The error message
95 * @return {@code this}
96 */
97 public Builder message(String message) {
98 this.message = message;
99 return this;
100 }
101
102 /**
103 * Sets the error message.
104 *
105 * @param message The message of this error group
106 * @param description The translated description of this error
107 * @param descriptionEn The English description (for ignoring errors)
108 * @return {@code this}
109 */
110 public Builder messageWithManuallyTranslatedDescription(String message, String description, String descriptionEn) {
111 this.message = message;
112 this.description = description;
113 this.descriptionEn = descriptionEn;
114 return this;
115 }
116
117 /**
118 * Sets the error message.
119 *
120 * @param message The message of this error group
121 * @param marktrDescription The {@linkplain I18n#marktr prepared for i18n} description of this error
122 * @param args The description arguments to be applied in {@link I18n#tr(String, Object...)}
123 * @return {@code this}
124 */
125 public Builder message(String message, String marktrDescription, Object... args) {
126 this.message = message;
127 this.description = I18n.tr(marktrDescription, args);
128 this.descriptionEn = new MessageFormat(marktrDescription, Locale.ENGLISH).format(args);
129 return this;
130 }
131
132 /**
133 * Sets the primitives affected by this error.
134 *
135 * @param primitives the primitives affected by this error
136 * @return {@code this}
137 */
138 public Builder primitives(OsmPrimitive... primitives) {
139 return primitives(Arrays.asList(primitives));
140 }
141
142 /**
143 * Sets the primitives affected by this error.
144 *
145 * @param primitives the primitives affected by this error
146 * @return {@code this}
147 */
148 public Builder primitives(Collection<? extends OsmPrimitive> primitives) {
149 CheckParameterUtil.ensureThat(this.primitives == null, "primitives already set");
150 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
151 this.primitives = primitives;
152 if (this.highlighted == null) {
153 this.highlighted = primitives;
154 }
155 return this;
156 }
157
158 /**
159 * Sets the primitives to highlight when selecting this error.
160 *
161 * @param highlighted the primitives to highlight
162 * @return {@code this}
163 * @see ValidatorVisitor#visit(OsmPrimitive)
164 */
165 public Builder highlight(OsmPrimitive... highlighted) {
166 return highlight(Arrays.asList(highlighted));
167 }
168
169 /**
170 * Sets the primitives to highlight when selecting this error.
171 *
172 * @param highlighted the primitives to highlight
173 * @return {@code this}
174 * @see ValidatorVisitor#visit(OsmPrimitive)
175 */
176 public Builder highlight(Collection<? extends OsmPrimitive> highlighted) {
177 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
178 this.highlighted = highlighted;
179 return this;
180 }
181
182 /**
183 * Sets the way segments to highlight when selecting this error.
184 *
185 * @param highlighted the way segments to highlight
186 * @return {@code this}
187 * @see ValidatorVisitor#visit(WaySegment)
188 */
189 public Builder highlightWaySegments(Collection<WaySegment> highlighted) {
190 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
191 this.highlighted = highlighted;
192 return this;
193 }
194
195 /**
196 * Sets the node pairs to highlight when selecting this error.
197 *
198 * @param highlighted the node pairs to highlight
199 * @return {@code this}
200 * @see ValidatorVisitor#visit(List)
201 */
202 public Builder highlightNodePairs(Collection<List<Node>> highlighted) {
203 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
204 this.highlighted = highlighted;
205 return this;
206 }
207
208 /**
209 * Sets an area to highlight when selecting this error.
210 *
211 * @param highlighted the area to highlight
212 * @return {@code this}
213 */
214 public Builder highlight(Area highlighted) {
215 CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
216 this.highlighted = Collections.singleton(highlighted);
217 return this;
218 }
219
220 /**
221 * Sets a supplier to obtain a command to fix the error.
222 *
223 * @param fixingCommand the fix supplier. Can be null
224 * @return {@code this}
225 */
226 public Builder fix(Supplier<Command> fixingCommand) {
227 CheckParameterUtil.ensureThat(this.fixingCommand == null, "fixingCommand already set");
228 this.fixingCommand = fixingCommand;
229 return this;
230 }
231
232 /**
233 * Returns a new test error with the specified values
234 *
235 * @return a new test error with the specified values
236 * @throws IllegalArgumentException when {@link #message} or {@link #primitives} is null.
237 */
238 public TestError build() {
239 CheckParameterUtil.ensureParameterNotNull(message, "message not set");
240 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives not set");
241 if (this.highlighted == null) {
242 this.highlighted = Collections.emptySet();
243 }
244 return new TestError(this);
245 }
246 }
247
248 /**
249 * Update error codes on read and save. Used for tests.
250 * @param updateErrorCodes {@code true} to update error codes. See {@link #switchOver} for default.
251 */
252 static void setUpdateErrorCodes(boolean updateErrorCodes) {
253 switchOver = updateErrorCodes;
254 }
255
256 /**
257 * Starts building a new {@code TestError}
258 * @param tester The tester
259 * @param severity The severity of this error
260 * @param code The test error reference code
261 * @return a new test builder
262 * @since 11129
263 */
264 public static Builder builder(Test tester, Severity severity, int code) {
265 return new Builder(tester, severity, code);
266 }
267
268 TestError(Builder builder) {
269 this.tester = builder.tester;
270 this.severity = builder.severity;
271 this.message = builder.message;
272 this.description = builder.description;
273 this.descriptionEn = builder.descriptionEn;
274 this.primitives = builder.primitives;
275 this.highlighted = builder.highlighted;
276 this.code = builder.code;
277 this.uniqueCode = builder.uniqueCode;
278 this.fixingCommand = builder.fixingCommand;
279 }
280
281 /**
282 * Gets the error message
283 * @return the error message
284 */
285 public String getMessage() {
286 return message;
287 }
288
289 /**
290 * Gets the error message
291 * @return the error description
292 */
293 public String getDescription() {
294 return description;
295 }
296
297 /**
298 * Gets the list of primitives affected by this error
299 * @return the list of primitives affected by this error
300 */
301 public Collection<? extends OsmPrimitive> getPrimitives() {
302 return Collections.unmodifiableCollection(primitives);
303 }
304
305 /**
306 * Gets all primitives of the given type affected by this error
307 * @param type restrict primitives to subclasses
308 * @param <T> type of primitives
309 * @return the primitives as Stream
310 */
311 public final <T extends OsmPrimitive> Stream<T> primitives(Class<T> type) {
312 return primitives.stream()
313 .filter(type::isInstance)
314 .map(type::cast);
315 }
316
317 /**
318 * Gets the severity of this error
319 * @return the severity of this error
320 */
321 public Severity getSeverity() {
322 return severity;
323 }
324
325 /**
326 * Returns the ignore state for this error.
327 * @return the ignore state for this error or null if any primitive is new
328 */
329 public String getIgnoreState() {
330 return getIgnoreState(false);
331 }
332
333 /**
334 * Get the ignore state
335 * @param useOriginal if {@code true}, use the original code to get the ignore state
336 * @return The ignore state ({@link #getIgnoreGroup} + ignored object list)
337 */
338 private String getIgnoreState(boolean useOriginal) {
339 Collection<String> strings = new TreeSet<>();
340 for (OsmPrimitive o : primitives) {
341 // ignore data not yet uploaded
342 if (o.isNew())
343 return null;
344 String type = "u";
345 if (o instanceof Way) {
346 type = "w";
347 } else if (o instanceof Relation) {
348 type = "r";
349 } else if (o instanceof Node) {
350 type = "n";
351 }
352 strings.add(type + '_' + o.getId());
353 }
354 return strings.stream().map(o -> ':' + o).collect(Collectors.joining("", getIgnoreSubGroup(useOriginal), ""));
355 }
356
357 /**
358 * Check if this error matches an entry in the ignore list and
359 * set the ignored flag if it is.
360 * @return the new ignored state
361 */
362 public boolean updateIgnored() {
363 setIgnored(calcIgnored());
364 return isIgnored();
365 }
366
367 private boolean calcIgnored() {
368 // Begin code removal section (backwards compatibility)
369 if (OsmValidator.hasIgnoredError(getIgnoreGroup(true))) {
370 updateIgnoreList(getIgnoreGroup(true), getIgnoreGroup(false));
371 return true;
372 }
373 if (OsmValidator.hasIgnoredError(getIgnoreSubGroup(true))) {
374 updateIgnoreList(getIgnoreSubGroup(true), getIgnoreSubGroup(false));
375 return true;
376 }
377 String oldState = getIgnoreState(true);
378 String state = getIgnoreState(false);
379 if (oldState != null && OsmValidator.hasIgnoredError(oldState)) {
380 updateIgnoreList(oldState, state);
381 return true;
382 }
383 // End code removal section
384 if (OsmValidator.hasIgnoredError(getIgnoreGroup()))
385 return true;
386 if (OsmValidator.hasIgnoredError(getIgnoreSubGroup()))
387 return true;
388 return state != null && OsmValidator.hasIgnoredError(state);
389 }
390
391 /**
392 * Convert old keys to new keys. Only takes effect when {@link #switchOver} is true
393 * @param oldKey The key to replace
394 * @param newKey The new key
395 */
396 private static void updateIgnoreList(String oldKey, String newKey) {
397 if (switchOver) {
398 Map<String, String> errors = OsmValidator.getIgnoredErrors();
399 if (errors.containsKey(oldKey)) {
400 String value = errors.remove(oldKey);
401 errors.put(newKey, value);
402 }
403 }
404 }
405
406 /**
407 * Gets the ignores subgroup that is more specialized than {@link #getIgnoreGroup()}
408 * @return The ignore sub group
409 */
410 public String getIgnoreSubGroup() {
411 return getIgnoreSubGroup(false);
412 }
413
414 /**
415 * Get the subgroup for the error
416 * @param useOriginal if {@code true}, use the original code instead of the new unique codes.
417 * @return The ignore subgroup
418 */
419 private String getIgnoreSubGroup(boolean useOriginal) {
420 if (code == 3000) {
421 // see #19053
422 return "3000_" + (description == null ? message : description);
423 }
424 String ignorestring = getIgnoreGroup(useOriginal);
425 if (descriptionEn != null) {
426 ignorestring += '_' + descriptionEn;
427 }
428 return ignorestring;
429 }
430
431 /**
432 * Gets the ignore group ID that is used to allow the user to ignore all same errors
433 * @return The group id
434 * @see TestError#getIgnoreSubGroup()
435 */
436 public String getIgnoreGroup() {
437 return getIgnoreGroup(false);
438 }
439
440 /**
441 * Get the ignore group
442 * @param useOriginal if {@code true}, use the original code instead of a unique code + original code.
443 * Used for reading and understanding old ignore groups.
444 * @return The ignore group.
445 */
446 private String getIgnoreGroup(boolean useOriginal) {
447 if (code == 3000) {
448 // see #19053
449 return "3000_" + getMessage();
450 }
451 if (useOriginal) {
452 return Integer.toString(this.code);
453 }
454 return this.uniqueCode + "_" + this.code;
455 }
456
457 /**
458 * Flags this error as ignored
459 * @param state The ignore flag
460 */
461 public void setIgnored(boolean state) {
462 ignored = state;
463 }
464
465 /**
466 * Checks if this error is ignored
467 * @return <code>true</code> if it is ignored
468 */
469 public boolean isIgnored() {
470 return ignored;
471 }
472
473 /**
474 * Gets the tester that raised this error
475 * @return the tester that raised this error
476 */
477 public Test getTester() {
478 return tester;
479 }
480
481 /**
482 * Gets the code
483 * @return the code
484 */
485 public int getCode() {
486 return code;
487 }
488
489 /**
490 * Get the unique code for this test. Used for ignore lists.
491 * @return The unique code (generated with {@code tester.getClass().getName().hashCode() + code}).
492 * @since 18636
493 */
494 public int getUniqueCode() {
495 return this.uniqueCode;
496 }
497
498 /**
499 * Returns true if the error can be fixed automatically
500 *
501 * @return true if the error can be fixed
502 */
503 public boolean isFixable() {
504 return (fixingCommand != null || ((tester != null) && tester.isFixable(this)))
505 && OsmUtils.isOsmCollectionEditable(primitives);
506 }
507
508 /**
509 * Fixes the error with the appropriate command
510 *
511 * @return The command to fix the error
512 */
513 public Command getFix() {
514 // obtain fix from the error
515 final Command fix = fixingCommand != null ? fixingCommand.get() : null;
516 if (fix != null) {
517 return fix;
518 }
519
520 // obtain fix from the tester
521 if (tester == null || !tester.isFixable(this) || primitives.isEmpty())
522 return null;
523
524 return tester.fixError(this);
525 }
526
527 /**
528 * Sets the selection flag of this error
529 * @param selected if this error is selected
530 */
531 public void setSelected(boolean selected) {
532 this.selected = selected;
533 }
534
535 /**
536 * Visits all highlighted validation elements
537 * @param v The visitor that should receive a visit-notification on all highlighted elements
538 */
539 @SuppressWarnings("unchecked")
540 public void visitHighlighted(ValidatorVisitor v) {
541 for (Object o : highlighted) {
542 if (o instanceof OsmPrimitive) {
543 v.visit((OsmPrimitive) o);
544 } else if (o instanceof WaySegment) {
545 v.visit((WaySegment) o);
546 } else if (o instanceof List<?>) {
547 v.visit((List<Node>) o);
548 } else if (o instanceof Area) {
549 for (List<Node> l : getHiliteNodesForArea((Area) o)) {
550 v.visit(l);
551 }
552 }
553 }
554 }
555
556 /**
557 * Calculate list of node pairs describing the area.
558 * @param area the area
559 * @return list of node pairs describing the area
560 */
561 private static List<List<Node>> getHiliteNodesForArea(Area area) {
562 List<List<Node>> hilite = new ArrayList<>();
563 PathIterator pit = area.getPathIterator(null);
564 double[] res = new double[6];
565 List<Node> nodes = new ArrayList<>();
566 while (!pit.isDone()) {
567 int type = pit.currentSegment(res);
568 Node n = new Node(new EastNorth(res[0], res[1]));
569 switch (type) {
570 case PathIterator.SEG_MOVETO:
571 if (!nodes.isEmpty()) {
572 hilite.add(nodes);
573 }
574 nodes = new ArrayList<>();
575 nodes.add(n);
576 break;
577 case PathIterator.SEG_LINETO:
578 nodes.add(n);
579 break;
580 case PathIterator.SEG_CLOSE:
581 if (!nodes.isEmpty()) {
582 nodes.add(nodes.get(0));
583 hilite.add(nodes);
584 nodes = new ArrayList<>();
585 }
586 break;
587 default:
588 break;
589 }
590 pit.next();
591 }
592 if (nodes.size() > 1) {
593 hilite.add(nodes);
594 }
595 return hilite;
596 }
597
598 /**
599 * Returns the selection flag of this error
600 * @return true if this error is selected
601 * @since 5671
602 */
603 public boolean isSelected() {
604 return selected;
605 }
606
607 /**
608 * Returns The primitives or way segments to be highlighted
609 * @return The primitives or way segments to be highlighted
610 * @since 5671
611 */
612 public Collection<?> getHighlighted() {
613 return Collections.unmodifiableCollection(highlighted);
614 }
615
616 @Override
617 public int compareTo(TestError o) {
618 if (equals(o)) return 0;
619
620 return AlphanumComparator.getInstance().compare(getNameVisitor().toString(), o.getNameVisitor().toString());
621 }
622
623 /**
624 * Returns a new {@link MultipleNameVisitor} for the list of primitives affected by this error.
625 * @return Name visitor (used in cell renderer and for sorting)
626 */
627 public MultipleNameVisitor getNameVisitor() {
628 MultipleNameVisitor v = new MultipleNameVisitor();
629 v.visit(getPrimitives());
630 return v;
631 }
632
633 /**
634 * Tests if two errors are similar, i.e.,
635 * same code and description and same combination of primitives and same combination of highlighted objects, but maybe with different orders.
636 * @param other the other error to be compared
637 * @return true if two errors are similar
638 */
639 public boolean isSimilar(TestError other) {
640 return getUniqueCode() == other.getUniqueCode()
641 && getCode() == other.getCode()
642 && getMessage().equals(other.getMessage())
643 && getPrimitives().size() == other.getPrimitives().size()
644 && getPrimitives().containsAll(other.getPrimitives())
645 && highlightedIsEqual(getHighlighted(), other.getHighlighted());
646 }
647
648 private static boolean highlightedIsEqual(Collection<?> highlighted, Collection<?> highlighted2) {
649 if (highlighted.size() == highlighted2.size()) {
650 if (!highlighted.isEmpty()) {
651 Object h1 = highlighted.iterator().next();
652 Object h2 = highlighted2.iterator().next();
653 if (h1 instanceof Area && h2 instanceof Area) {
654 return ((Area) h1).equals((Area) h2);
655 }
656 return highlighted.containsAll(highlighted2);
657 }
658 return true;
659 }
660 return false;
661 }
662
663 @Override
664 public String toString() {
665 return "TestError [tester=" + tester + ", unique code=" + this.uniqueCode +
666 ", code=" + code + ", message=" + message + ']';
667 }
668
669}
Note: See TracBrowser for help on using the repository browser.