Changeset 15943 in josm
- Timestamp:
- 2020-02-26T20:09:08+01:00 (5 years ago)
- Location:
- trunk
- Files:
-
- 4 added
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/actions/SplitWayAction.java
r15728 r15943 15 15 import java.util.Iterator; 16 16 import java.util.List; 17 import java.util.Optional; 17 18 import java.util.concurrent.atomic.AtomicInteger; 18 19 import java.util.stream.Collectors; … … 220 221 list.setSelectedValue(wayToKeep, true); 221 222 } else { 222 setHighlightedWaySegments(Collections. <WaySegment>emptyList());223 setHighlightedWaySegments(Collections.emptyList()); 223 224 DISPLAY_COUNT.decrementAndGet(); 224 225 } … … 287 288 final MapFrame map = MainApplication.getMap(); 288 289 final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw; 289 final SplitWayCommand result = SplitWayCommand.doSplitWay(way, wayToKeep, newWays, !isMapModeDraw ? newSelection : null); 290 UndoRedoHandler.getInstance().add(result); 291 List<? extends PrimitiveId> newSel = result.getNewSelection(); 292 if (newSel != null && !newSel.isEmpty()) { 293 way.getDataSet().setSelected(newSel); 294 } 290 291 Optional<SplitWayCommand> splitWayCommand = SplitWayCommand.doSplitWay( 292 way, 293 wayToKeep, 294 newWays, 295 !isMapModeDraw ? newSelection : null, 296 SplitWayCommand.WhenRelationOrderUncertain.ASK_USER_FOR_CONSENT_TO_DOWNLOAD 297 ); 298 299 splitWayCommand.ifPresent(result -> { 300 UndoRedoHandler.getInstance().add(result); 301 List<? extends PrimitiveId> newSel = result.getNewSelection(); 302 if (newSel != null && !newSel.isEmpty()) { 303 way.getDataSet().setSelected(newSel); 304 } 305 }); 295 306 } 296 307 -
trunk/src/org/openstreetmap/josm/command/SplitWayCommand.java
r15418 r15943 2 2 package org.openstreetmap.josm.command; 3 3 4 import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.GO_AHEAD_WITHOUT_DOWNLOADS; 5 import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.GO_AHEAD_WITH_DOWNLOADS; 6 import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.USER_ABORTED; 7 import static org.openstreetmap.josm.command.SplitWayCommand.WhenRelationOrderUncertain.ASK_USER_FOR_CONSENT_TO_DOWNLOAD; 4 8 import static org.openstreetmap.josm.tools.I18n.tr; 5 9 import static org.openstreetmap.josm.tools.I18n.trn; … … 9 13 import java.util.Collection; 10 14 import java.util.Collections; 15 import java.util.EnumSet; 11 16 import java.util.HashMap; 12 17 import java.util.HashSet; … … 20 25 import java.util.function.Consumer; 21 26 27 import javax.swing.JOptionPane; 28 29 import org.openstreetmap.josm.data.osm.DataSet; 22 30 import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 23 31 import org.openstreetmap.josm.data.osm.Node; … … 27 35 import org.openstreetmap.josm.data.osm.RelationMember; 28 36 import org.openstreetmap.josm.data.osm.Way; 37 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 38 import org.openstreetmap.josm.gui.ExceptionDialogUtil; 39 import org.openstreetmap.josm.gui.MainApplication; 40 import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 41 import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 42 import org.openstreetmap.josm.io.MultiFetchServerObjectReader; 43 import org.openstreetmap.josm.io.OsmTransferException; 29 44 import org.openstreetmap.josm.spi.preferences.Config; 30 45 import org.openstreetmap.josm.tools.CheckParameterUtil; 31 46 import org.openstreetmap.josm.tools.Logging; 32 33 47 /** 34 48 * Splits a way into multiple ways (all identical except for their node list). … … 42 56 43 57 private static volatile Consumer<String> warningNotifier = Logging::warn; 58 private static final String DOWNLOAD_MISSING_PREF_KEY = "split_way_download_missing_members"; 44 59 45 60 private static final class RelationInformation { … … 60 75 private final Way originalWay; 61 76 private final List<Way> newWays; 77 62 78 /** Map<Restriction type, type to treat it as> */ 63 79 private static final Map<String, String> relationSpecialTypes = new HashMap<>(); … … 247 263 248 264 /** 249 * Splits the way {@code way} into chunks of {@code wayChunks} and replies 250 * the result of this process in an instance of {@link SplitWayCommand}. 251 * The {@link SplitWayCommand.Strategy} is used to determine which 252 * way chunk should reuse the old id and its history. 253 * 254 * Note that changes are not applied to the data yet. You have to 255 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}. 256 * 257 * @param way the way to split. Must not be null. 258 * @param wayChunks the list of way chunks into the way is split. Must not be null. 259 * @param selection The list of currently selected primitives 265 * Splits the way {@code way} into chunks of {@code wayChunks} and replies the result of this process in an instance 266 * of {@link SplitWayCommand}. The {@link SplitWayCommand.Strategy} is used to determine which way chunk should 267 * reuse the old id and its history. 268 * <p> 269 * If the split way is part of relations, and the order of the new parts in these relations cannot be determined due 270 * to missing relation members, the user will be asked to consent to downloading these missing members. 271 * <p> 272 * Note that changes are not applied to the data yet. You have to submit the command first, i.e. {@code 273 * UndoRedoHandler.getInstance().add(result)}. 274 * 275 * @param way the way to split. Must not be null. 276 * @param wayChunks the list of way chunks into the way is split. Must not be null. 277 * @param selection The list of currently selected primitives 260 278 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history 261 279 * @return the result from the split operation 262 280 */ 263 public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks, 264 Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) { 281 public static SplitWayCommand splitWay(Way way, 282 List<List<Node>> wayChunks, 283 Collection<? extends OsmPrimitive> selection, 284 Strategy splitStrategy) { 285 286 // This method could be refactored to use an Optional in the future, but would need to be deprecated first 287 // to phase out use by plugins. 288 return splitWay(way, wayChunks, selection, splitStrategy, ASK_USER_FOR_CONSENT_TO_DOWNLOAD).orElse(null); 289 } 290 291 /** 292 * Splits the way {@code way} into chunks of {@code wayChunks} and replies the result of this process in an instance 293 * of {@link SplitWayCommand}. The {@link SplitWayCommand.Strategy} is used to determine which way chunk should 294 * reuse the old id and its history. 295 * <p> 296 * Note that changes are not applied to the data yet. You have to submit the command first, i.e. {@code 297 * UndoRedoHandler.getInstance().add(result)}. 298 * 299 * @param way the way to split. Must not be null. 300 * @param wayChunks the list of way chunks into the way is split. Must not be null. 301 * @param selection The list of currently selected primitives 302 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its 303 * history 304 * @param whenRelationOrderUncertain What to do when the split way is part of relations, and the order of the new 305 * parts in the relation cannot be determined without downloading missing relation 306 * members. 307 * @return The result from the split operation, may be an empty {@link Optional} if the operation is aborted. 308 */ 309 public static Optional<SplitWayCommand> splitWay(Way way, 310 List<List<Node>> wayChunks, 311 Collection<? extends OsmPrimitive> selection, 312 Strategy splitStrategy, 313 WhenRelationOrderUncertain whenRelationOrderUncertain) { 265 314 // build a list of commands, and also a new selection list 266 315 final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size()); … … 273 322 final Way wayToKeep = splitStrategy.determineWayToKeep(newWays); 274 323 275 return wayToKeep != null ? doSplitWay(way, wayToKeep, newWays, newSelection) : null; 324 return wayToKeep != null 325 ? doSplitWay(way, wayToKeep, newWays, newSelection, whenRelationOrderUncertain) 326 : Optional.empty(); 276 327 } 277 328 … … 284 335 * @param newWays potential new ways 285 336 * @param newSelection new selection list to update (optional: can be null) 337 * @param whenRelationOrderUncertain Action to perform when the order of the new parts in relations the way is 338 * member of could not be reliably determined. See 339 * {@link WhenRelationOrderUncertain}. 286 340 * @return the {@code SplitWayCommand} 287 341 */ 288 public static SplitWayCommand doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) { 289 290 Collection<Command> commandList = new ArrayList<>(newWays.size()); 342 public static Optional<SplitWayCommand> doSplitWay(Way way, 343 Way wayToKeep, 344 List<Way> newWays, 345 List<OsmPrimitive> newSelection, 346 WhenRelationOrderUncertain whenRelationOrderUncertain) { 347 if (whenRelationOrderUncertain == null) whenRelationOrderUncertain = ASK_USER_FOR_CONSENT_TO_DOWNLOAD; 348 349 final int indexOfWayToKeep = newWays.indexOf(wayToKeep); 350 newWays.remove(wayToKeep); 351 352 // Figure out the order of relation members (if any). 353 Analysis analysis = analyseSplit(way, wayToKeep, newWays, indexOfWayToKeep); 354 355 // If there are relations that cannot be split properly without downloading more members, 356 // present the user with an option to do so, or to abort the split. 357 List<Relation> relationsNeedingMoreMembers = new ArrayList<>(); 358 Set<OsmPrimitive> incompleteMembers = new HashSet<>(); 359 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) { 360 if (!relationAnalysis.needsMoreMembers()) continue; 361 362 Relation relation = relationAnalysis.getRelation(); 363 int position = relationAnalysis.getPosition(); 364 int membersCount = relation.getMembersCount(); 365 366 // Mark the neighbouring members for downloading if these are ways too. 367 relationsNeedingMoreMembers.add(relation); 368 RelationMember rmPrev = position == 0 369 ? relation.getMember(membersCount - 1) 370 : relation.getMember(position - 1); 371 RelationMember rmNext = position == membersCount - 1 372 ? relation.getMember(0) 373 : relation.getMember(position + 1); 374 375 if (rmPrev != null && rmPrev.isWay()) { 376 incompleteMembers.add(rmPrev.getWay()); 377 } 378 if (rmNext != null && rmNext.isWay()) { 379 incompleteMembers.add(rmNext.getWay()); 380 } 381 } 382 383 MissingMemberStrategy missingMemberStrategy; 384 if (relationsNeedingMoreMembers.isEmpty()) { 385 // The split can be performed without any extra downloads. 386 missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS; 387 } else { 388 switch (whenRelationOrderUncertain) { 389 case ASK_USER_FOR_CONSENT_TO_DOWNLOAD: 390 // If the analysis shows that for some relations missing members should be downloaded, offer the user the 391 // chance to consent to this. 392 393 // Only ask the user about downloading missing members when they haven't consented to this before. 394 if (ConditionalOptionPaneUtil.getDialogReturnValue(DOWNLOAD_MISSING_PREF_KEY) == Integer.MAX_VALUE) { 395 // User has previously told us downloading missing relation members is fine. 396 missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS; 397 } else { 398 // Ask the user. 399 missingMemberStrategy = offerToDownloadMissingMembersIfNeeded(analysis, relationsNeedingMoreMembers); 400 } 401 break; 402 case SPLIT_ANYWAY: 403 missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS; 404 break; 405 case DOWNLOAD_MISSING_MEMBERS: 406 missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS; 407 break; 408 case ABORT: 409 default: 410 missingMemberStrategy = USER_ABORTED; 411 break; 412 } 413 } 414 415 switch (missingMemberStrategy) { 416 case GO_AHEAD_WITH_DOWNLOADS: 417 try { 418 downloadMissingMembers(incompleteMembers); 419 } catch (OsmTransferException e) { 420 ExceptionDialogUtil.explainException(e); 421 return Optional.empty(); 422 } 423 // If missing relation members were downloaded, perform the analysis again to find the relation 424 // member order for all relations. 425 analysis = analyseSplit(way, wayToKeep, newWays, indexOfWayToKeep); 426 return Optional.of(splitBasedOnAnalyses(way, newWays, newSelection, analysis, indexOfWayToKeep)); 427 case GO_AHEAD_WITHOUT_DOWNLOADS: 428 // Proceed with the split with the information we have. 429 // This can mean that there are no missing members we want, or that the user chooses to continue 430 // the split without downloading them. 431 return Optional.of(splitBasedOnAnalyses(way, newWays, newSelection, analysis, indexOfWayToKeep)); 432 case USER_ABORTED: 433 default: 434 return Optional.empty(); 435 } 436 } 437 438 static Analysis analyseSplit(Way way, 439 Way wayToKeep, 440 List<Way> newWays, 441 int indexOfWayToKeep) { 442 Collection<Command> commandList = new ArrayList<>(); 291 443 Collection<String> nowarnroles = Config.getPref().getList("way.split.roles.nowarn", 292 444 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west")); … … 296 448 changedWay.setNodes(wayToKeep.getNodes()); 297 449 commandList.add(new ChangeCommand(way, changedWay)); 298 if (/*!isMapModeDraw &&*/ newSelection != null && !newSelection.contains(way)) { 299 newSelection.add(way); 300 } 301 final int indexOfWayToKeep = newWays.indexOf(wayToKeep); 302 newWays.remove(wayToKeep); 303 304 if (/*!isMapModeDraw &&*/ newSelection != null) { 305 newSelection.addAll(newWays); 306 } 450 307 451 for (Way wayToAdd : newWays) { 308 452 commandList.add(new AddCommand(way.getDataSet(), wayToAdd)); 309 453 } 310 454 311 boolean warnmerole = false;312 boolean warnme = false;313 // now copy all relations to new way also455 List<RelationAnalysis> relationAnalyses = new ArrayList<>(); 456 EnumSet<WarningType> warnings = EnumSet.noneOf(WarningType.class); 457 int numberOfRelations = 0; 314 458 315 459 for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(way))) { … … 317 461 continue; 318 462 } 463 464 numberOfRelations++; 465 319 466 Relation c = null; 320 467 String type = Optional.ofNullable(r.get("type")).orElse(""); 468 // Known types of ordered relations. 469 boolean isOrderedRelation = "route".equals(type) || "multipolygon".equals(type) || "boundary".equals(type); 321 470 322 471 int ic = 0; 323 472 int ir = 0; 324 473 List<RelationMember> relationMembers = r.getMembers(); 325 for (RelationMember rm : relationMembers) {474 for (RelationMember rm : relationMembers) { 326 475 if (rm.isWay() && rm.getMember() == way) { 327 476 boolean insert = true; 328 477 if (relationSpecialTypes.containsKey(type) && "restriction".equals(relationSpecialTypes.get(type))) { 329 478 RelationInformation rValue = treatAsRestriction(r, rm, c, newWays, way, changedWay); 330 warnme = rValue.warnme;479 if (rValue.warnme) warnings.add(WarningType.GENERIC); 331 480 insert = rValue.insert; 332 481 c = rValue.relation; 333 } else if (!("route".equals(type)) && !("multipolygon".equals(type))) { 334 warnme = true; 482 } else if (!isOrderedRelation) { 483 // Warn the user when relations that are not a route or multipolygon are modified as a result 484 // of splitting up the way, because we can't tell if this might break anything. 485 warnings.add(WarningType.GENERIC); 335 486 } 336 487 if (c == null) { … … 340 491 if (insert) { 341 492 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) { 342 warn merole = true;493 warnings.add(WarningType.ROLE); 343 494 } 344 495 345 Boolean backwards = null; 346 int k = 1; 347 while (ir - k >= 0 || ir + k < relationMembers.size()) { 348 if ((ir - k >= 0) && relationMembers.get(ir - k).isWay()) { 349 Way w = relationMembers.get(ir - k).getWay(); 350 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) { 351 backwards = Boolean.FALSE; 352 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) { 353 backwards = Boolean.TRUE; 496 // Attempt to determine the direction the ways in the relation are ordered. 497 Direction direction = Direction.UNKNOWN; 498 if (isOrderedRelation) { 499 if (way.lastNode() == way.firstNode()) { 500 // Self-closing way. 501 direction = Direction.IRRELEVANT; 502 } else { 503 // For ordered relations, looking beyond the nearest neighbour members is not required, 504 // and can even cause the wrong direction to be guessed (with closed loops). 505 if (ir - 1 >= 0 && relationMembers.get(ir - 1).isWay()) { 506 Way w = relationMembers.get(ir - 1).getWay(); 507 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) { 508 direction = Direction.FORWARDS; 509 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) { 510 direction = Direction.BACKWARDS; 511 } 354 512 } 355 break; 513 if (ir + 1 < relationMembers.size() && relationMembers.get(ir + 1).isWay()) { 514 Way w = relationMembers.get(ir + 1).getWay(); 515 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) { 516 direction = Direction.BACKWARDS; 517 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) { 518 direction = Direction.FORWARDS; 519 } 520 } 356 521 } 357 if ((ir + k < relationMembers.size()) && relationMembers.get(ir + k).isWay()) { 358 Way w = relationMembers.get(ir + k).getWay(); 359 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) { 360 backwards = Boolean.TRUE; 361 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) { 362 backwards = Boolean.FALSE; 522 } else { 523 int k = 1; 524 while (ir - k >= 0 || ir + k < relationMembers.size()) { 525 if (ir - k >= 0 && relationMembers.get(ir - k).isWay()) { 526 Way w = relationMembers.get(ir - k).getWay(); 527 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) { 528 direction = Direction.FORWARDS; 529 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) { 530 direction = Direction.BACKWARDS; 531 } 532 break; 363 533 } 364 break; 365 } 366 k++; 367 } 368 369 int j = ic; 370 final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep); 371 for (Way wayToAdd : waysToAddBefore) { 372 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 373 j++; 374 if (Boolean.TRUE.equals(backwards)) { 375 c.addMember(ic + 1, em); 376 } else { 377 c.addMember(j - 1, em); 534 if (ir + k < relationMembers.size() && relationMembers.get(ir + k).isWay()) { 535 Way w = relationMembers.get(ir + k).getWay(); 536 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) { 537 direction = Direction.BACKWARDS; 538 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) { 539 direction = Direction.FORWARDS; 540 } 541 break; 542 } 543 k++; 378 544 } 379 545 } 380 final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size()); 381 for (Way wayToAdd : waysToAddAfter) { 382 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 383 j++; 384 if (Boolean.TRUE.equals(backwards)) { 385 c.addMember(ic, em); 386 } else { 387 c.addMember(j, em); 388 } 389 } 390 ic = j; 546 547 // We don't have enough information to determine the order of the new ways in this relation. 548 // This may cause relations to be saved with the two new way sections in reverse order. 549 // 550 // This often breaks routes. 551 // 552 // The user should be asked to download more members, or to abort the split operation. 553 boolean needsMoreMembers = isOrderedRelation 554 && direction == Direction.UNKNOWN 555 && relationMembers.size() > 1 556 && r.hasIncompleteMembers(); 557 558 relationAnalyses.add(new RelationAnalysis(c, rm, direction, needsMoreMembers, ic)); 559 ic += indexOfWayToKeep; 391 560 } 392 561 } … … 399 568 } 400 569 } 401 if (warnmerole) { 570 571 return new Analysis(relationAnalyses, commandList, warnings, numberOfRelations); 572 } 573 574 static class Analysis { 575 List<RelationAnalysis> relationAnalyses; 576 Collection<Command> commands; 577 EnumSet<WarningType> warningTypes; 578 private final int numberOfRelations; 579 580 Analysis(List<RelationAnalysis> relationAnalyses, 581 Collection<Command> commandList, 582 EnumSet<WarningType> warnings, 583 int numberOfRelations) { 584 this.relationAnalyses = relationAnalyses; 585 commands = commandList; 586 warningTypes = warnings; 587 this.numberOfRelations = numberOfRelations; 588 } 589 590 List<RelationAnalysis> getRelationAnalyses() { 591 return relationAnalyses; 592 } 593 594 Collection<Command> getCommands() { 595 return commands; 596 } 597 598 EnumSet<WarningType> getWarningTypes() { 599 return warningTypes; 600 } 601 602 public int getNumberOfRelations() { 603 return numberOfRelations; 604 } 605 } 606 607 static MissingMemberStrategy offerToDownloadMissingMembersIfNeeded(Analysis analysis, 608 List<Relation> relationsNeedingMoreMembers) { 609 String[] options = { 610 tr("Yes, download the missing members"), 611 tr("No, abort the split operation"), 612 tr("No, perform the split without downloading") 613 }; 614 615 String msgMemberOfRelations = trn( 616 "This way is part of a relation.", 617 "This way is part of {0} relations.", 618 analysis.getNumberOfRelations(), 619 analysis.getNumberOfRelations() 620 ); 621 622 String msgReferToRelations; 623 if (analysis.getNumberOfRelations() == 1) { 624 msgReferToRelations = tr("this relation"); 625 } else if (analysis.getNumberOfRelations() == relationsNeedingMoreMembers.size()) { 626 msgReferToRelations = tr("these relations"); 627 } else { 628 msgReferToRelations = trn( 629 "one relation", 630 "{0} relations", 631 relationsNeedingMoreMembers.size(), 632 relationsNeedingMoreMembers.size() 633 ); 634 } 635 636 String msgRelationsMissingData = tr( 637 "For {0} the correct order of the new way parts could not be determined. " + 638 "To fix this, some missing relation members should be downloaded first.", 639 msgReferToRelations 640 ); 641 642 JMultilineLabel msg = new JMultilineLabel(msgMemberOfRelations + " " + msgRelationsMissingData); 643 msg.setMaxWidth(600); 644 645 int ret = JOptionPane.showOptionDialog( 646 MainApplication.getMainFrame(), 647 msg, 648 tr("Download missing relation members?"), 649 JOptionPane.OK_CANCEL_OPTION, 650 JOptionPane.QUESTION_MESSAGE, 651 null, 652 options, 653 options[0] 654 ); 655 656 switch (ret) { 657 case JOptionPane.OK_OPTION: 658 // Ask the user if they want to do this automatically from now on. We only ask this for the download 659 // action, because automatically cancelling is confusing (the user can't tell why this happened), and 660 // automatically performing the split without downloading missing members despite needing them is 661 // likely to break a lot of routes. The user also can't tell the difference between a split that needs 662 // no downloads at all, and this special case where downloading missing relation members will prevent 663 // broken relations. 664 ConditionalOptionPaneUtil.showMessageDialog( 665 DOWNLOAD_MISSING_PREF_KEY, 666 MainApplication.getMainFrame(), 667 tr("Missing relation members will be downloaded. Should this be done automatically from now on?"), 668 tr("Downloading missing relation members"), 669 JOptionPane.INFORMATION_MESSAGE 670 ); 671 return GO_AHEAD_WITH_DOWNLOADS; 672 case JOptionPane.CANCEL_OPTION: 673 return GO_AHEAD_WITHOUT_DOWNLOADS; 674 default: 675 return USER_ABORTED; 676 } 677 } 678 679 static void downloadMissingMembers(Set<OsmPrimitive> incompleteMembers) throws OsmTransferException { 680 // Download the missing members. 681 MultiFetchServerObjectReader reader = MultiFetchServerObjectReader.create(); 682 reader.append(incompleteMembers); 683 684 DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE); 685 MainApplication.getLayerManager().getEditLayer().mergeFrom(ds); 686 } 687 688 static SplitWayCommand splitBasedOnAnalyses(Way way, 689 List<Way> newWays, 690 List<OsmPrimitive> newSelection, 691 Analysis analysis, 692 int indexOfWayToKeep) { 693 if (newSelection != null && !newSelection.contains(way)) { 694 newSelection.add(way); 695 } 696 697 if (newSelection != null) { 698 newSelection.addAll(newWays); 699 } 700 701 // Perform the split. 702 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) { 703 RelationMember rm = relationAnalysis.getRelationMember(); 704 Relation relation = relationAnalysis.getRelation(); 705 Direction direction = relationAnalysis.getDirection(); 706 int position = relationAnalysis.getPosition(); 707 708 int j = position; 709 final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep); 710 for (Way wayToAdd : waysToAddBefore) { 711 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 712 j++; 713 if (direction == Direction.BACKWARDS) { 714 relation.addMember(position + 1, em); 715 } else { 716 relation.addMember(j - 1, em); 717 } 718 } 719 final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size()); 720 for (Way wayToAdd : waysToAddAfter) { 721 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 722 j++; 723 if (direction == Direction.BACKWARDS) { 724 relation.addMember(position, em); 725 } else { 726 relation.addMember(j, em); 727 } 728 } 729 } 730 731 EnumSet<WarningType> warnings = analysis.getWarningTypes(); 732 733 if (warnings.contains(WarningType.ROLE)) { 402 734 warningNotifier.accept( 403 735 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.")); 404 } else if (warn me) {736 } else if (warnings.contains(WarningType.GENERIC)) { 405 737 warningNotifier.accept( 406 738 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.")); … … 411 743 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1, 412 744 way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1), 413 commandList,745 analysis.getCommands(), 414 746 newSelection, 415 747 way, … … 524 856 return relationSpecialTypes; 525 857 } 858 859 /** 860 * What to do when the split way is part of relations, and the order of the new parts in the relation cannot be 861 * determined without downloading missing relation members. 862 */ 863 public enum WhenRelationOrderUncertain { 864 /** 865 * Ask the user to consent to downloading the missing members. The user can abort the operation or choose to 866 * proceed without downloading anything. 867 */ 868 ASK_USER_FOR_CONSENT_TO_DOWNLOAD, 869 /** 870 * If there are relation members missing, and these are needed to determine the order of the new parts in 871 * that relation, abort the split operation. 872 */ 873 ABORT, 874 /** 875 * If there are relation members missing, and these are needed to determine the order of the new parts in 876 * that relation, continue with the split operation anyway, without downloading anything. Caution: use this 877 * option with care. 878 */ 879 SPLIT_ANYWAY, 880 /** 881 * If there are relation members missing, and these are needed to determine the order of the new parts in 882 * that relation, automatically download these without prompting the user. 883 */ 884 DOWNLOAD_MISSING_MEMBERS 885 } 886 887 static class RelationAnalysis { 888 private final Relation relation; 889 private final RelationMember relationMember; 890 private final Direction direction; 891 private final boolean needsMoreMembers; 892 private final int position; 893 894 RelationAnalysis(Relation relation, 895 RelationMember relationMember, 896 Direction direction, 897 boolean needsMoreMembers, 898 int position) { 899 this.relation = relation; 900 this.relationMember = relationMember; 901 this.direction = direction; 902 this.needsMoreMembers = needsMoreMembers; 903 this.position = position; 904 } 905 906 RelationMember getRelationMember() { 907 return relationMember; 908 } 909 910 Direction getDirection() { 911 return direction; 912 } 913 914 boolean needsMoreMembers() { 915 return needsMoreMembers; 916 } 917 918 Relation getRelation() { 919 return relation; 920 } 921 922 int getPosition() { 923 return position; 924 } 925 } 926 927 enum Direction { 928 FORWARDS, 929 BACKWARDS, 930 UNKNOWN, 931 IRRELEVANT 932 } 933 934 enum WarningType { 935 GENERIC, 936 ROLE 937 } 938 939 enum MissingMemberStrategy { 940 GO_AHEAD_WITH_DOWNLOADS, 941 GO_AHEAD_WITHOUT_DOWNLOADS, 942 USER_ABORTED 943 } 526 944 } -
trunk/test/unit/org/openstreetmap/josm/command/SplitWayCommandTest.java
r15362 r15943 3 3 4 4 import static org.junit.Assert.assertEquals; 5 import static org.junit.Assert.assertFalse; 5 6 import static org.junit.Assert.assertTrue; 6 7 8 import java.io.IOException; 9 import java.io.InputStream; 7 10 import java.util.ArrayList; 8 11 import java.util.Arrays; 9 12 import java.util.Collections; 10 13 import java.util.Iterator; 14 import java.util.Optional; 11 15 12 16 import org.junit.Rule; 13 17 import org.junit.Test; 18 import org.openstreetmap.josm.TestUtils; 14 19 import org.openstreetmap.josm.command.SplitWayCommand.Strategy; 15 20 import org.openstreetmap.josm.data.UndoRedoHandler; … … 18 23 import org.openstreetmap.josm.data.osm.Node; 19 24 import org.openstreetmap.josm.data.osm.OsmPrimitive; 25 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 20 26 import org.openstreetmap.josm.data.osm.Relation; 21 27 import org.openstreetmap.josm.data.osm.RelationMember; 22 28 import org.openstreetmap.josm.data.osm.Way; 29 import org.openstreetmap.josm.io.IllegalDataException; 30 import org.openstreetmap.josm.io.OsmReader; 23 31 import org.openstreetmap.josm.testutils.JOSMTestRules; 24 32 … … 35 43 @Rule 36 44 @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") 37 public JOSMTestRules test = new JOSMTestRules().main().projection() ;45 public JOSMTestRules test = new JOSMTestRules().main().projection().preferences(); 38 46 39 47 /** … … 113 121 114 122 final Strategy strategy = wayChunks -> { 115 116 117 118 119 120 123 final Iterator<Way> it = wayChunks.iterator(); 124 for (int i = 0; i < indexOfWayToKeep; i++) { 125 it.next(); 126 } 127 return it.next(); 128 }; 121 129 final SplitWayCommand result = SplitWayCommand.splitWay( 122 w2, SplitWayCommand.buildSplitChunks(w2, Arrays.asList(n3, n4, n5)), new ArrayList< OsmPrimitive>(), strategy);130 w2, SplitWayCommand.buildSplitChunks(w2, Arrays.asList(n3, n4, n5)), new ArrayList<>(), strategy); 123 131 UndoRedoHandler.getInstance().add(result); 124 132 … … 140 148 } 141 149 150 @Test 151 public void testOneMemberOrderedRelationShowsWarningTest() { 152 final DataSet dataSet = new DataSet(); 153 154 // Positive IDs to mark that these ways are incomplete (i.e., no nodes loaded). 155 final Way w1 = new Way(1); 156 final Way w3 = new Way(3); 157 158 // The way we are going to split is complete of course. 159 final Node n1 = new Node(new LatLon(1, 0)); 160 final Node n2 = new Node(new LatLon(2, 0)); 161 final Node n3 = new Node(new LatLon(3, 0)); 162 final Way w2 = new Way(); 163 164 final Relation route = new Relation(); 165 for (OsmPrimitive p : Arrays.asList(n1, n2, n3, w1, w2, w3, route)) { 166 dataSet.addPrimitive(p); 167 } 168 w2.setNodes(Arrays.asList(n1, n2, n3)); 169 170 route.put("type", "route"); 171 route.addMember(new RelationMember("", w1)); 172 route.addMember(new RelationMember("", w2)); 173 route.addMember(new RelationMember("", w3)); 174 dataSet.setSelected(Arrays.asList(w2, n2)); 175 176 // This split cannot be safely performed without downloading extra relation members. 177 // Here we ask the split method to abort if it needs more information. 178 final Optional<SplitWayCommand> result = SplitWayCommand.splitWay( 179 w2, 180 SplitWayCommand.buildSplitChunks(w2, Collections.singletonList(n2)), 181 new ArrayList<>(), 182 Strategy.keepLongestChunk(), 183 SplitWayCommand.WhenRelationOrderUncertain.ABORT 184 ); 185 186 assertFalse(result.isPresent()); 187 } 188 189 @Test 190 public void testDoIncompleteMembersOrderedRelationCorrectOrderTest() { 191 for (int i = 0; i < 2; i++) { 192 // All these permutations should result in a split that keeps the new parts in order. 193 doIncompleteMembersOrderedRelationCorrectOrderTest(false, false, i); 194 doIncompleteMembersOrderedRelationCorrectOrderTest(true, false, i); 195 doIncompleteMembersOrderedRelationCorrectOrderTest(true, true, i); 196 doIncompleteMembersOrderedRelationCorrectOrderTest(false, true, i); 197 } 198 } 199 200 private void doIncompleteMembersOrderedRelationCorrectOrderTest(final boolean reverseWayOne, 201 final boolean reverseWayTwo, 202 final int indexOfWayToKeep) { 203 final DataSet dataSet = new DataSet(); 204 205 // Positive IDs to mark that these ways are incomplete (i.e., no nodes loaded). 206 final Way w1 = new Way(1); 207 final Way w4 = new Way(3); 208 209 // The ways we are going to split are complete of course. 210 final Node n1 = new Node(new LatLon(1, 0)); 211 final Node n2 = new Node(new LatLon(2, 0)); 212 final Node n3 = new Node(new LatLon(3, 0)); 213 final Node n4 = new Node(new LatLon(4, 0)); 214 final Node n5 = new Node(new LatLon(5, 0)); 215 final Way w2 = new Way(); 216 final Way w3 = new Way(); 217 218 final Relation route = new Relation(); 219 for (OsmPrimitive p : Arrays.asList(n1, n2, n3, n4, n5, w1, w2, w3, w4, route)) { 220 dataSet.addPrimitive(p); 221 } 222 w2.setNodes(reverseWayOne ? Arrays.asList(n3, n2, n1) : Arrays.asList(n1, n2, n3)); 223 w3.setNodes(reverseWayTwo ? Arrays.asList(n5, n4, n3) : Arrays.asList(n3, n4, n5)); 224 225 route.put("type", "route"); 226 route.addMember(new RelationMember("", w1)); 227 route.addMember(new RelationMember("", w2)); 228 route.addMember(new RelationMember("", w3)); 229 route.addMember(new RelationMember("", w4)); 230 231 Way splitWay = indexOfWayToKeep == 0 ? w2 : w3; 232 Node splitNode = indexOfWayToKeep == 0 ? n2 : n4; 233 234 dataSet.setSelected(Arrays.asList(splitWay, splitNode)); 235 236 final SplitWayCommand result = SplitWayCommand.splitWay( 237 splitWay, SplitWayCommand.buildSplitChunks(splitWay, Collections.singletonList(splitNode)), new ArrayList<>()); 238 UndoRedoHandler.getInstance().add(result); 239 240 assertEquals(5, route.getMembersCount()); 241 assertConnectedAtEnds(route.getMember(1).getWay(), route.getMember(2).getWay()); 242 assertConnectedAtEnds(route.getMember(2).getWay(), route.getMember(3).getWay()); 243 } 244 142 245 static void assertFirstLastNodeIs(Way way, Node node) { 143 246 assertTrue("First/last node of " + way + " should be " + node, node.equals(way.firstNode()) || node.equals(way.lastNode())); 144 247 } 248 249 static void assertConnectedAtEnds(Way one, Way two) { 250 Node first1 = one.firstNode(); 251 Node last1 = one.lastNode(); 252 Node first2 = two.firstNode(); 253 Node last2 = two.lastNode(); 254 255 assertTrue("Ways expected to be connected at their ends.", 256 first1 == first2 || first1 == last2 || last1 == first2 || last1 == last2); 257 } 258 259 /** 260 * Non-regression test for patch #18596 (Fix relation ordering after split-way) 261 * @throws IOException if any I/O error occurs 262 * @throws IllegalDataException if OSM parsing fails 263 */ 264 @Test 265 public void testTicket18596() throws IOException, IllegalDataException { 266 try (InputStream is = TestUtils.getRegressionDataStream(18596, "data.osm")) { 267 DataSet ds = OsmReader.parseDataSet(is, null); 268 269 Way splitWay = (Way) ds.getPrimitiveById(5, OsmPrimitiveType.WAY); 270 Node splitNode = (Node) ds.getPrimitiveById(100002, OsmPrimitiveType.NODE); 271 272 final SplitWayCommand result = SplitWayCommand.splitWay( 273 splitWay, 274 SplitWayCommand.buildSplitChunks(splitWay, Collections.singletonList(splitNode)), 275 new ArrayList<>() 276 ); 277 278 UndoRedoHandler.getInstance().add(result); 279 280 Relation relation = (Relation) ds.getPrimitiveById(8888, OsmPrimitiveType.RELATION); 281 282 assertEquals(relation.getMembersCount(), 8); 283 284 // Before the patch introduced in #18596, these asserts would fail. The two parts of 285 // way '5' would be in the wrong order, breaking the boundary relation in this test. 286 assertConnectedAtEnds(relation.getMember(4).getWay(), relation.getMember(5).getWay()); 287 assertConnectedAtEnds(relation.getMember(5).getWay(), relation.getMember(6).getWay()); 288 } 289 } 290 291 /** 292 * Non-regression test for issue #17400 ( Warn when splitting way in not fully downloaded region) 293 * 294 * Bus route 190 gets broken when the split occurs, because the two new way parts are inserted in the relation in 295 * the wrong order. 296 * 297 * @throws IOException if any I/O error occurs 298 * @throws IllegalDataException if OSM parsing fails 299 */ 300 @Test 301 public void testTicket17400() throws IOException, IllegalDataException { 302 try (InputStream is = TestUtils.getRegressionDataStream(17400, "data.osm")) { 303 DataSet ds = OsmReader.parseDataSet(is, null); 304 305 Way splitWay = (Way) ds.getPrimitiveById(253731928, OsmPrimitiveType.WAY); 306 Node splitNode = (Node) ds.getPrimitiveById(29830834, OsmPrimitiveType.NODE); 307 308 final Optional<SplitWayCommand> result = SplitWayCommand.splitWay( 309 splitWay, 310 SplitWayCommand.buildSplitChunks(splitWay, Collections.singletonList(splitNode)), 311 new ArrayList<>(), 312 Strategy.keepLongestChunk(), 313 // This split requires no additional downloads. 314 SplitWayCommand.WhenRelationOrderUncertain.ABORT 315 ); 316 317 assertTrue(result.isPresent()); 318 319 UndoRedoHandler.getInstance().add(result.get()); 320 321 // 190 Hormersdorf-Thalheim-Stollberg. 322 Relation relation = (Relation) ds.getPrimitiveById(2873422, OsmPrimitiveType.RELATION); 323 324 // One more than the original 161. 325 assertEquals(relation.getMembersCount(), 162); 326 327 // Before the patch introduced in #18596, these asserts would fail. The new parts of 328 // the Hauptstraße would be in the wrong order, breaking the bus route relation. 329 // These parts should be connected, in their relation sequence: 74---75---76. 330 // Before #18596 this would have been a broken connection: 74---75-x-76. 331 assertConnectedAtEnds(relation.getMember(74).getWay(), relation.getMember(75).getWay()); 332 assertConnectedAtEnds(relation.getMember(75).getWay(), relation.getMember(76).getWay()); 333 } 334 } 145 335 }
Note:
See TracChangeset
for help on using the changeset viewer.