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}