001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.command; 003 004import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.GO_AHEAD_WITHOUT_DOWNLOADS; 005import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.GO_AHEAD_WITH_DOWNLOADS; 006import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.USER_ABORTED; 007import static org.openstreetmap.josm.command.SplitWayCommand.WhenRelationOrderUncertain.ASK_USER_FOR_CONSENT_TO_DOWNLOAD; 008import static org.openstreetmap.josm.tools.I18n.tr; 009import static org.openstreetmap.josm.tools.I18n.trn; 010 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.EnumSet; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.Iterator; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Map; 022import java.util.Objects; 023import java.util.Optional; 024import java.util.Set; 025import java.util.function.Consumer; 026import java.util.stream.Collectors; 027 028import javax.swing.JOptionPane; 029 030import org.openstreetmap.josm.data.osm.DataSet; 031import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 032import org.openstreetmap.josm.data.osm.Node; 033import org.openstreetmap.josm.data.osm.OsmPrimitive; 034import org.openstreetmap.josm.data.osm.PrimitiveId; 035import org.openstreetmap.josm.data.osm.Relation; 036import org.openstreetmap.josm.data.osm.RelationMember; 037import org.openstreetmap.josm.data.osm.Way; 038import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 039import org.openstreetmap.josm.gui.ExceptionDialogUtil; 040import org.openstreetmap.josm.gui.MainApplication; 041import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 042import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 043import org.openstreetmap.josm.io.MultiFetchServerObjectReader; 044import org.openstreetmap.josm.io.OsmTransferException; 045import org.openstreetmap.josm.spi.preferences.Config; 046import org.openstreetmap.josm.tools.CheckParameterUtil; 047import org.openstreetmap.josm.tools.Logging; 048/** 049 * Splits a way into multiple ways (all identical except for their node list). 050 * 051 * Ways are just split at the selected nodes. The nodes remain in their 052 * original order. Selected nodes at the end of a way are ignored. 053 * 054 * @since 12828 ({@code SplitWayAction} converted to a {@link Command}) 055 */ 056public class SplitWayCommand extends SequenceCommand { 057 058 private static volatile Consumer<String> warningNotifier = Logging::warn; 059 private static final String DOWNLOAD_MISSING_PREF_KEY = "split_way_download_missing_members"; 060 061 private static final class RelationInformation { 062 boolean warnme; 063 boolean insert; 064 Relation relation; 065 } 066 067 /** 068 * Sets the global warning notifier. 069 * @param notifier warning notifier in charge of displaying warning message, if any. Must not be null 070 */ 071 public static void setWarningNotifier(Consumer<String> notifier) { 072 warningNotifier = Objects.requireNonNull(notifier); 073 } 074 075 private final List<? extends PrimitiveId> newSelection; 076 private final Way originalWay; 077 private final List<Way> newWays; 078 079 /** Map<Restriction type, type to treat it as> */ 080 private static final Map<String, String> relationSpecialTypes = new HashMap<>(); 081 static { 082 relationSpecialTypes.put("restriction", "restriction"); 083 relationSpecialTypes.put("destination_sign", "restriction"); 084 relationSpecialTypes.put("connectivity", "restriction"); 085 } 086 087 /** 088 * Create a new {@code SplitWayCommand}. 089 * @param name The description text 090 * @param commandList The sequence of commands that should be executed. 091 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection}) 092 * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay}) 093 * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getNewWays}) 094 */ 095 public SplitWayCommand(String name, Collection<Command> commandList, 096 List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) { 097 super(name, commandList); 098 this.newSelection = newSelection; 099 this.originalWay = originalWay; 100 this.newWays = newWays; 101 } 102 103 /** 104 * Replies the new list of selected primitives ids 105 * @return The new list of selected primitives ids 106 */ 107 public List<? extends PrimitiveId> getNewSelection() { 108 return newSelection; 109 } 110 111 /** 112 * Replies the original way being split 113 * @return The original way being split 114 */ 115 public Way getOriginalWay() { 116 return originalWay; 117 } 118 119 /** 120 * Replies the resulting new ways 121 * @return The resulting new ways 122 */ 123 public List<Way> getNewWays() { 124 return newWays; 125 } 126 127 /** 128 * Determines which way chunk should reuse the old id and its history 129 */ 130 @FunctionalInterface 131 public interface Strategy { 132 133 /** 134 * Determines which way chunk should reuse the old id and its history. 135 * 136 * @param wayChunks the way chunks 137 * @return the way to keep 138 */ 139 Way determineWayToKeep(Iterable<Way> wayChunks); 140 141 /** 142 * Returns a strategy which selects the way chunk with the highest node count to keep. 143 * @return strategy which selects the way chunk with the highest node count to keep 144 */ 145 static Strategy keepLongestChunk() { 146 return wayChunks -> { 147 Way wayToKeep = null; 148 for (Way i : wayChunks) { 149 if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) { 150 wayToKeep = i; 151 } 152 } 153 return wayToKeep; 154 }; 155 } 156 157 /** 158 * Returns a strategy which selects the first way chunk. 159 * @return strategy which selects the first way chunk 160 */ 161 static Strategy keepFirstChunk() { 162 return wayChunks -> wayChunks.iterator().next(); 163 } 164 } 165 166 /** 167 * Splits the nodes of {@code wayToSplit} into a list of node sequences 168 * which are separated at the nodes in {@code splitPoints}. 169 * 170 * This method displays warning messages if {@code wayToSplit} and/or 171 * {@code splitPoints} aren't consistent. 172 * 173 * Returns null, if building the split chunks fails. 174 * 175 * @param wayToSplit the way to split. Must not be null. 176 * @param splitPoints the nodes where the way is split. Must not be null. 177 * @return the list of chunks 178 */ 179 public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) { 180 CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit"); 181 CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints"); 182 183 Set<Node> nodeSet = new HashSet<>(splitPoints); 184 List<List<Node>> wayChunks = new LinkedList<>(); 185 List<Node> currentWayChunk = new ArrayList<>(); 186 wayChunks.add(currentWayChunk); 187 188 Iterator<Node> it = wayToSplit.getNodes().iterator(); 189 while (it.hasNext()) { 190 Node currentNode = it.next(); 191 boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext(); 192 currentWayChunk.add(currentNode); 193 if (nodeSet.contains(currentNode) && !atEndOfWay) { 194 currentWayChunk = new ArrayList<>(); 195 currentWayChunk.add(currentNode); 196 wayChunks.add(currentWayChunk); 197 } 198 } 199 200 // Handle circular ways specially. 201 // If you split at a circular way at two nodes, you just want to split 202 // it at these points, not also at the former endpoint. 203 // So if the last node is the same first node, join the last and the 204 // first way chunk. 205 List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1); 206 if (wayChunks.size() >= 2 207 && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1) 208 && !nodeSet.contains(wayChunks.get(0).get(0))) { 209 if (wayChunks.size() == 2) { 210 warningNotifier.accept(tr("You must select two or more nodes to split a circular way.")); 211 return null; 212 } 213 lastWayChunk.remove(lastWayChunk.size() - 1); 214 lastWayChunk.addAll(wayChunks.get(0)); 215 wayChunks.remove(wayChunks.size() - 1); 216 wayChunks.set(0, lastWayChunk); 217 } 218 219 if (wayChunks.size() < 2) { 220 if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) { 221 warningNotifier.accept( 222 tr("You must select two or more nodes to split a circular way.")); 223 } else { 224 warningNotifier.accept( 225 tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)")); 226 } 227 return null; 228 } 229 return wayChunks; 230 } 231 232 /** 233 * Creates new way objects for the way chunks and transfers the keys from the original way. 234 * @param way the original way whose keys are transferred 235 * @param wayChunks the way chunks 236 * @return the new way objects 237 */ 238 public static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) { 239 final List<Way> newWays = new ArrayList<>(); 240 for (List<Node> wayChunk : wayChunks) { 241 Way wayToAdd = new Way(); 242 wayToAdd.setKeys(way.getKeys()); 243 wayToAdd.setNodes(wayChunk); 244 newWays.add(wayToAdd); 245 } 246 return newWays; 247 } 248 249 /** 250 * Splits the way {@code way} into chunks of {@code wayChunks} and replies 251 * the result of this process in an instance of {@link SplitWayCommand}. 252 * 253 * Note that changes are not applied to the data yet. You have to 254 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}. 255 * 256 * @param way the way to split. Must not be null. 257 * @param wayChunks the list of way chunks into the way is split. Must not be null. 258 * @param selection The list of currently selected primitives 259 * @return the result from the split operation 260 */ 261 public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) { 262 return splitWay(way, wayChunks, selection, Strategy.keepLongestChunk()); 263 } 264 265 /** 266 * Splits the way {@code way} into chunks of {@code wayChunks} and replies the result of this process in an instance 267 * of {@link SplitWayCommand}. The {@link SplitWayCommand.Strategy} is used to determine which way chunk should 268 * reuse the old id and its history. 269 * <p> 270 * If the split way is part of relations, and the order of the new parts in these relations cannot be determined due 271 * to missing relation members, the user will be asked to consent to downloading these missing members. 272 * <p> 273 * Note that changes are not applied to the data yet. You have to submit the command first, i.e. {@code 274 * UndoRedoHandler.getInstance().add(result)}. 275 * 276 * @param way the way to split. Must not be null. 277 * @param wayChunks the list of way chunks into the way is split. Must not be null. 278 * @param selection The list of currently selected primitives 279 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history 280 * @return the result from the split operation 281 */ 282 public static SplitWayCommand splitWay(Way way, 283 List<List<Node>> wayChunks, 284 Collection<? extends OsmPrimitive> selection, 285 Strategy splitStrategy) { 286 287 // This method could be refactored to use an Optional in the future, but would need to be deprecated first 288 // to phase out use by plugins. 289 return splitWay(way, wayChunks, selection, splitStrategy, ASK_USER_FOR_CONSENT_TO_DOWNLOAD).orElse(null); 290 } 291 292 /** 293 * Splits the way {@code way} into chunks of {@code wayChunks} and replies the result of this process in an instance 294 * of {@link SplitWayCommand}. The {@link SplitWayCommand.Strategy} is used to determine which way chunk should 295 * reuse the old id and its history. 296 * <p> 297 * Note that changes are not applied to the data yet. You have to submit the command first, i.e. {@code 298 * UndoRedoHandler.getInstance().add(result)}. 299 * 300 * @param way the way to split. Must not be null. 301 * @param wayChunks the list of way chunks into the way is split. Must not be null. 302 * @param selection The list of currently selected primitives 303 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its 304 * history 305 * @param whenRelationOrderUncertain What to do when the split way is part of relations, and the order of the new 306 * parts in the relation cannot be determined without downloading missing relation 307 * members. 308 * @return The result from the split operation, may be an empty {@link Optional} if the operation is aborted. 309 */ 310 public static Optional<SplitWayCommand> splitWay(Way way, 311 List<List<Node>> wayChunks, 312 Collection<? extends OsmPrimitive> selection, 313 Strategy splitStrategy, 314 WhenRelationOrderUncertain whenRelationOrderUncertain) { 315 // build a list of commands, and also a new selection list 316 final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size()); 317 newSelection.addAll(selection); 318 319 // Create all potential new ways 320 final List<Way> newWays = createNewWaysFromChunks(way, wayChunks); 321 322 // Determine which part reuses the existing way 323 final Way wayToKeep = splitStrategy.determineWayToKeep(newWays); 324 325 return wayToKeep != null 326 ? doSplitWay(way, wayToKeep, newWays, newSelection, whenRelationOrderUncertain) 327 : Optional.empty(); 328 } 329 330 /** 331 * Effectively constructs the {@link SplitWayCommand}. 332 * This method is only public for {@code SplitWayAction}. 333 * 334 * @param way the way to split. Must not be null. 335 * @param wayToKeep way chunk which should reuse the old id and its history 336 * @param newWays potential new ways 337 * @param newSelection new selection list to update (optional: can be null) 338 * @param whenRelationOrderUncertain Action to perform when the order of the new parts in relations the way is 339 * member of could not be reliably determined. See 340 * {@link WhenRelationOrderUncertain}. 341 * @return the {@code SplitWayCommand} 342 */ 343 public static Optional<SplitWayCommand> doSplitWay(Way way, 344 Way wayToKeep, 345 List<Way> newWays, 346 List<OsmPrimitive> newSelection, 347 WhenRelationOrderUncertain whenRelationOrderUncertain) { 348 if (whenRelationOrderUncertain == null) whenRelationOrderUncertain = ASK_USER_FOR_CONSENT_TO_DOWNLOAD; 349 350 final int indexOfWayToKeep = newWays.indexOf(wayToKeep); 351 newWays.remove(wayToKeep); 352 353 // Figure out the order of relation members (if any). 354 Analysis analysis = analyseSplit(way, wayToKeep, newWays); 355 356 // If there are relations that cannot be split properly without downloading more members, 357 // present the user with an option to do so, or to abort the split. 358 Set<Relation> relationsNeedingMoreMembers = new HashSet<>(); 359 Set<OsmPrimitive> incompleteMembers = new HashSet<>(); 360 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) { 361 if (!relationAnalysis.getNeededIncompleteMembers().isEmpty()) { 362 incompleteMembers.addAll(relationAnalysis.getNeededIncompleteMembers()); 363 relationsNeedingMoreMembers.add(relationAnalysis.getRelation()); 364 } 365 } 366 367 MissingMemberStrategy missingMemberStrategy; 368 if (relationsNeedingMoreMembers.isEmpty()) { 369 // The split can be performed without any extra downloads. 370 missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS; 371 } else { 372 switch (whenRelationOrderUncertain) { 373 case ASK_USER_FOR_CONSENT_TO_DOWNLOAD: 374 // If the analysis shows that for some relations missing members should be downloaded, offer the user the 375 // chance to consent to this. 376 377 // Only ask the user about downloading missing members when they haven't consented to this before. 378 if (ConditionalOptionPaneUtil.getDialogReturnValue(DOWNLOAD_MISSING_PREF_KEY) == Integer.MAX_VALUE) { 379 // User has previously told us downloading missing relation members is fine. 380 missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS; 381 } else { 382 // Ask the user. 383 missingMemberStrategy = offerToDownloadMissingMembersIfNeeded(analysis, relationsNeedingMoreMembers.size()); 384 } 385 break; 386 case SPLIT_ANYWAY: 387 missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS; 388 break; 389 case DOWNLOAD_MISSING_MEMBERS: 390 missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS; 391 break; 392 case ABORT: 393 default: 394 missingMemberStrategy = USER_ABORTED; 395 break; 396 } 397 } 398 399 try { 400 switch (missingMemberStrategy) { 401 case GO_AHEAD_WITH_DOWNLOADS: 402 try { 403 downloadMissingMembers(incompleteMembers); 404 } catch (OsmTransferException e) { 405 ExceptionDialogUtil.explainException(e); 406 return Optional.empty(); 407 } 408 // If missing relation members were downloaded, perform the analysis again to find the relation 409 // member order for all relations. 410 analysis.cleanup(); 411 analysis = analyseSplit(way, wayToKeep, newWays); 412 break; 413 case GO_AHEAD_WITHOUT_DOWNLOADS: 414 // Proceed with the split with the information we have. 415 // This can mean that there are no missing members we want, or that the user chooses to continue 416 // the split without downloading them. 417 break; 418 case USER_ABORTED: 419 default: 420 return Optional.empty(); 421 } 422 return Optional.of(splitBasedOnAnalyses(way, newWays, newSelection, analysis, indexOfWayToKeep)); 423 } finally { 424 // see #19885 425 wayToKeep.setNodes(null); 426 analysis.cleanup(); 427 } 428 } 429 430 static Analysis analyseSplit(Way way, 431 Way wayToKeep, 432 List<Way> newWays) { 433 Collection<Command> commandList = new ArrayList<>(); 434 Collection<String> nowarnroles = Config.getPref().getList("way.split.roles.nowarn", 435 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west")); 436 437 // Change the original way 438 final List<Node> changedWayNodes = wayToKeep.getNodes(); 439 commandList.add(new ChangeNodesCommand(way, changedWayNodes)); 440 for (Way wayToAdd : newWays) { 441 commandList.add(new AddCommand(way.getDataSet(), wayToAdd)); 442 } 443 444 List<RelationAnalysis> relationAnalyses = new ArrayList<>(); 445 EnumSet<WarningType> warnings = EnumSet.noneOf(WarningType.class); 446 int numberOfRelations = 0; 447 448 for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(way))) { 449 if (!r.isUsable()) { 450 continue; 451 } 452 453 numberOfRelations++; 454 boolean isSimpleCase = true; 455 456 Relation c = null; 457 String type = Optional.ofNullable(r.get("type")).orElse(""); 458 // Known types of ordered relations. 459 boolean isOrderedRelation = "route".equals(type) || "multipolygon".equals(type) || "boundary".equals(type); 460 461 for (int ir = 0; ir < r.getMembersCount(); ir++) { 462 RelationMember rm = r.getMember(ir); 463 if (rm.getMember() == way) { 464 boolean insert = true; 465 if (relationSpecialTypes.containsKey(type) && "restriction".equals(relationSpecialTypes.get(type))) { 466 RelationInformation rValue = treatAsRestriction(r, rm, c, newWays, way, changedWayNodes); 467 if (rValue.warnme) warnings.add(WarningType.GENERIC); 468 insert = rValue.insert; 469 c = rValue.relation; // Value.relation is null or contains a modified copy 470 } else if (!isOrderedRelation) { 471 // Warn the user when relations that are not a route or multipolygon are modified as a result 472 // of splitting up the way, because we can't tell if this might break anything. 473 warnings.add(WarningType.GENERIC); 474 } 475 476 if (insert) { 477 if (c == null) { 478 c = new Relation(r); 479 } 480 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) { 481 warnings.add(WarningType.ROLE); 482 } 483 484 // Attempt to determine the direction the ways in the relation are ordered. 485 Direction direction = Direction.UNKNOWN; 486 Set<Way> missingWays = new HashSet<>(); 487 if (isOrderedRelation) { 488 if (way.lastNode() == way.firstNode()) { 489 // Self-closing way. 490 direction = Direction.IRRELEVANT; 491 } else { 492 // For ordered relations, looking beyond the nearest neighbour members is not required, 493 // and can even cause the wrong direction to be guessed (with closed loops). 494 if (ir - 1 >= 0 && r.getMember(ir - 1).isWay()) { 495 Way w = r.getMember(ir - 1).getWay(); 496 if (w.isIncomplete()) 497 missingWays.add(w); 498 else { 499 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) { 500 direction = Direction.FORWARDS; 501 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) { 502 direction = Direction.BACKWARDS; 503 } 504 } 505 } 506 if (ir + 1 < r.getMembersCount() && r.getMember(ir + 1).isWay()) { 507 Way w = r.getMember(ir + 1).getWay(); 508 if (w.isIncomplete()) 509 missingWays.add(w); 510 else { 511 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) { 512 direction = Direction.BACKWARDS; 513 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) { 514 direction = Direction.FORWARDS; 515 } 516 } 517 } 518 519 if (direction == Direction.UNKNOWN && missingWays.isEmpty()) { 520 // we cannot detect the direction and no way is missing. 521 // We can safely assume that the direction doesn't matter. 522 direction = Direction.IRRELEVANT; 523 } 524 } 525 } else { 526 int k = 1; 527 while (ir - k >= 0 || ir + k < r.getMembersCount()) { 528 if (ir - k >= 0 && r.getMember(ir - k).isWay()) { 529 Way w = r.getMember(ir - k).getWay(); 530 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) { 531 direction = Direction.FORWARDS; 532 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) { 533 direction = Direction.BACKWARDS; 534 } 535 break; 536 } 537 if (ir + k < r.getMembersCount() && r.getMember(ir + k).isWay()) { 538 Way w = r.getMember(ir + k).getWay(); 539 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) { 540 direction = Direction.BACKWARDS; 541 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) { 542 direction = Direction.FORWARDS; 543 } 544 break; 545 } 546 k++; 547 } 548 } 549 550 if (direction == Direction.UNKNOWN) { 551 // We don't have enough information to determine the order of the new ways in this relation. 552 // This may cause relations to be saved with the two new way sections in reverse order. 553 // 554 // This often breaks routes. 555 // 556 } else { 557 missingWays = Collections.emptySet(); 558 } 559 relationAnalyses.add(new RelationAnalysis(c, rm, direction, missingWays)); 560 isSimpleCase = false; 561 } 562 } 563 } 564 if (c != null && isSimpleCase) { 565 if (!r.getMembers().equals(c.getMembers())) { 566 commandList.add(new ChangeMembersCommand(r, new ArrayList<>(c.getMembers()))); 567 } 568 c.setMembers(null); // see #19885 569 } 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 /** 591 * Unlink temporary copies of relations. See #19885 592 */ 593 void cleanup() { 594 for (RelationAnalysis ra : relationAnalyses) { 595 if (ra.relation.getDataSet() == null) 596 ra.relation.setMembers(null); 597 } 598 } 599 600 List<RelationAnalysis> getRelationAnalyses() { 601 return relationAnalyses; 602 } 603 604 Collection<Command> getCommands() { 605 return commands; 606 } 607 608 EnumSet<WarningType> getWarningTypes() { 609 return warningTypes; 610 } 611 612 public int getNumberOfRelations() { 613 return numberOfRelations; 614 } 615 } 616 617 static MissingMemberStrategy offerToDownloadMissingMembersIfNeeded(Analysis analysis, 618 int numRelationsNeedingMoreMembers) { 619 String[] options = { 620 tr("Yes, download the missing members"), 621 tr("No, abort the split operation"), 622 tr("No, perform the split without downloading") 623 }; 624 625 String msgMemberOfRelations = trn( 626 "This way is part of a relation.", 627 "This way is part of {0} relations.", 628 analysis.getNumberOfRelations(), 629 analysis.getNumberOfRelations() 630 ); 631 632 String msgReferToRelations; 633 if (analysis.getNumberOfRelations() == 1) { 634 msgReferToRelations = tr("this relation"); 635 } else if (analysis.getNumberOfRelations() == numRelationsNeedingMoreMembers) { 636 msgReferToRelations = tr("these relations"); 637 } else { 638 msgReferToRelations = trn( 639 "one relation", 640 "{0} relations", 641 numRelationsNeedingMoreMembers, 642 numRelationsNeedingMoreMembers 643 ); 644 } 645 646 String msgRelationsMissingData = tr( 647 "For {0} the correct order of the new way parts could not be determined. " + 648 "To fix this, some missing relation members should be downloaded first.", 649 msgReferToRelations 650 ); 651 652 JMultilineLabel msg = new JMultilineLabel(msgMemberOfRelations + " " + msgRelationsMissingData); 653 msg.setMaxWidth(600); 654 655 int ret = JOptionPane.showOptionDialog( 656 MainApplication.getMainFrame(), 657 msg, 658 tr("Download missing relation members?"), 659 JOptionPane.OK_CANCEL_OPTION, 660 JOptionPane.QUESTION_MESSAGE, 661 null, 662 options, 663 options[0] 664 ); 665 666 switch (ret) { 667 case JOptionPane.OK_OPTION: 668 // Ask the user if they want to do this automatically from now on. We only ask this for the download 669 // action, because automatically cancelling is confusing (the user can't tell why this happened), and 670 // automatically performing the split without downloading missing members despite needing them is 671 // likely to break a lot of routes. The user also can't tell the difference between a split that needs 672 // no downloads at all, and this special case where downloading missing relation members will prevent 673 // broken relations. 674 ConditionalOptionPaneUtil.showMessageDialog( 675 DOWNLOAD_MISSING_PREF_KEY, 676 MainApplication.getMainFrame(), 677 tr("Missing relation members will be downloaded. Should this be done automatically from now on?"), 678 tr("Downloading missing relation members"), 679 JOptionPane.INFORMATION_MESSAGE 680 ); 681 return GO_AHEAD_WITH_DOWNLOADS; 682 case JOptionPane.CANCEL_OPTION: 683 return GO_AHEAD_WITHOUT_DOWNLOADS; 684 default: 685 return USER_ABORTED; 686 } 687 } 688 689 static void downloadMissingMembers(Set<OsmPrimitive> incompleteMembers) throws OsmTransferException { 690 // Download the missing members. 691 MultiFetchServerObjectReader reader = MultiFetchServerObjectReader.create(); 692 reader.append(incompleteMembers); 693 694 DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE); 695 MainApplication.getLayerManager().getEditLayer().mergeFrom(ds); 696 } 697 698 static SplitWayCommand splitBasedOnAnalyses(Way way, 699 List<Way> newWays, 700 List<OsmPrimitive> newSelection, 701 Analysis analysis, 702 int indexOfWayToKeep) { 703 if (newSelection != null && !newSelection.contains(way)) { 704 newSelection.add(way); 705 } 706 707 if (newSelection != null) { 708 newSelection.addAll(newWays); 709 } 710 711 // Perform the split. 712 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) { 713 RelationMember rm = relationAnalysis.getRelationMember(); 714 Relation relation = relationAnalysis.getRelation(); 715 Direction direction = relationAnalysis.getDirection(); 716 717 int position = -1; 718 for (int i = 0; i < relation.getMembersCount(); i++) { 719 // search for identical member (can't use indexOf() as it uses equals() 720 if (rm == relation.getMember(i)) { 721 position = i; 722 break; 723 } 724 } 725 726 // sanity check 727 if (position < 0) { 728 throw new AssertionError("Relation member not found"); 729 } 730 731 int j = position; 732 final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep); 733 for (Way wayToAdd : waysToAddBefore) { 734 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 735 j++; 736 if (direction == Direction.BACKWARDS) { 737 relation.addMember(position + 1, em); 738 } else { 739 relation.addMember(j - 1, em); 740 } 741 } 742 final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size()); 743 for (Way wayToAdd : waysToAddAfter) { 744 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 745 j++; 746 if (direction == Direction.BACKWARDS) { 747 relation.addMember(position, em); 748 } else { 749 relation.addMember(j, em); 750 } 751 } 752 } 753 754 // add one command for each complex case with relations 755 final DataSet ds = way.getDataSet(); 756 for (Relation r : analysis.getRelationAnalyses().stream().map(RelationAnalysis::getRelation).collect(Collectors.toSet())) { 757 Relation orig = (Relation) ds.getPrimitiveById(r); 758 analysis.getCommands().add(new ChangeMembersCommand(orig, new ArrayList<>(r.getMembers()))); 759 r.setMembers(null); // see #19885 760 } 761 762 EnumSet<WarningType> warnings = analysis.getWarningTypes(); 763 764 if (warnings.contains(WarningType.ROLE)) { 765 warningNotifier.accept( 766 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.")); 767 } else if (warnings.contains(WarningType.GENERIC)) { 768 warningNotifier.accept( 769 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.")); 770 } 771 772 return new SplitWayCommand( 773 /* for correct i18n of plural forms - see #9110 */ 774 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1, 775 way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1), 776 analysis.getCommands(), 777 newSelection, 778 way, 779 newWays 780 ); 781 } 782 783 private static RelationInformation treatAsRestriction(Relation r, 784 RelationMember rm, Relation c, Collection<Way> newWays, Way way, 785 List<Node> changedWayNodes) { 786 RelationInformation relationInformation = new RelationInformation(); 787 /* this code assumes the restriction is correct. No real error checking done */ 788 String role = rm.getRole(); 789 String type = Optional.ofNullable(r.get("type")).orElse(""); 790 if ("from".equals(role) || "to".equals(role)) { 791 List<Node> nodes = new ArrayList<>(); 792 for (OsmPrimitive via : findVias(r, type)) { 793 if (via instanceof Node) { 794 nodes.add((Node) via); 795 } else if (via instanceof Way) { 796 nodes.add(((Way) via).lastNode()); 797 nodes.add(((Way) via).firstNode()); 798 } 799 } 800 Way res = null; 801 for (Node n : nodes) { 802 if (changedWayNodes.get(0) == n || changedWayNodes.get(changedWayNodes.size() - 1) == n) { 803 res = way; 804 } 805 } 806 if (res == null) { 807 for (Way wayToAdd : newWays) { 808 for (Node n : nodes) { 809 if (wayToAdd.isFirstLastNode(n)) { 810 res = wayToAdd; 811 } 812 } 813 } 814 if (res != null) { 815 if (c == null) { 816 c = new Relation(r); 817 } 818 c.addMember(new RelationMember(role, res)); 819 c.removeMembersFor(way); 820 } 821 } 822 } else if (!"via".equals(role)) { 823 relationInformation.warnme = true; 824 } else { 825 relationInformation.insert = true; 826 } 827 relationInformation.relation = c; 828 return relationInformation; 829 } 830 831 static List<? extends OsmPrimitive> findVias(Relation r, String type) { 832 if (type != null) { 833 switch (type) { 834 case "connectivity": 835 case "restriction": 836 return r.findRelationMembers("via"); 837 case "destination_sign": 838 // Prefer intersection over sign, see #12347 839 List<? extends OsmPrimitive> intersections = r.findRelationMembers("intersection"); 840 return intersections.isEmpty() ? r.findRelationMembers("sign") : intersections; 841 default: 842 break; 843 } 844 } 845 return Collections.emptyList(); 846 } 847 848 /** 849 * Splits the way {@code way} at the nodes in {@code atNodes} and replies 850 * the result of this process in an instance of {@link SplitWayCommand}. 851 * 852 * Note that changes are not applied to the data yet. You have to 853 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}. 854 * 855 * Replies null if the way couldn't be split at the given nodes. 856 * 857 * @param way the way to split. Must not be null. 858 * @param atNodes the list of nodes where the way is split. Must not be null. 859 * @param selection The list of currently selected primitives 860 * @return the result from the split operation 861 */ 862 public static SplitWayCommand split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) { 863 List<List<Node>> chunks = buildSplitChunks(way, atNodes); 864 return chunks != null ? splitWay(way, chunks, selection) : null; 865 } 866 867 /** 868 * Add relations that are treated in a specific way. 869 * @param relationType The value in the {@code type} key 870 * @param treatAs The type of relation to treat the {@code relationType} as. 871 * Currently only supports relations that can be handled like "restriction" 872 * relations. 873 * @return the previous value associated with relationType, or null if there was no mapping 874 * @since 15078 875 */ 876 public static String addSpecialRelationType(String relationType, String treatAs) { 877 return relationSpecialTypes.put(relationType, treatAs); 878 } 879 880 /** 881 * Get the types of relations that are treated differently 882 * @return {@code Map<Relation Type, Type of Relation it is to be treated as>} 883 * @since 15078 884 */ 885 public static Map<String, String> getSpecialRelationTypes() { 886 return relationSpecialTypes; 887 } 888 889 /** 890 * What to do when the split way is part of relations, and the order of the new parts in the relation cannot be 891 * determined without downloading missing relation members. 892 */ 893 public enum WhenRelationOrderUncertain { 894 /** 895 * Ask the user to consent to downloading the missing members. The user can abort the operation or choose to 896 * proceed without downloading anything. 897 */ 898 ASK_USER_FOR_CONSENT_TO_DOWNLOAD, 899 /** 900 * If there are relation members missing, and these are needed to determine the order of the new parts in 901 * that relation, abort the split operation. 902 */ 903 ABORT, 904 /** 905 * If there are relation members missing, and these are needed to determine the order of the new parts in 906 * that relation, continue with the split operation anyway, without downloading anything. Caution: use this 907 * option with care. 908 */ 909 SPLIT_ANYWAY, 910 /** 911 * If there are relation members missing, and these are needed to determine the order of the new parts in 912 * that relation, automatically download these without prompting the user. 913 */ 914 DOWNLOAD_MISSING_MEMBERS 915 } 916 917 static class RelationAnalysis { 918 private final Relation relation; 919 private final RelationMember relationMember; 920 private final Direction direction; 921 private final Set<Way> neededIncompleteMembers; 922 923 RelationAnalysis(Relation relation, 924 RelationMember relationMember, 925 Direction direction, 926 Set<Way> neededIncompleteMembers) { 927 this.relation = relation; 928 this.relationMember = relationMember; 929 this.direction = direction; 930 this.neededIncompleteMembers = neededIncompleteMembers; 931 } 932 933 RelationMember getRelationMember() { 934 return relationMember; 935 } 936 937 Direction getDirection() { 938 return direction; 939 } 940 941 public Set<Way> getNeededIncompleteMembers() { 942 return neededIncompleteMembers; 943 } 944 945 Relation getRelation() { 946 return relation; 947 } 948 } 949 950 enum Direction { 951 FORWARDS, 952 BACKWARDS, 953 UNKNOWN, 954 IRRELEVANT 955 } 956 957 enum WarningType { 958 GENERIC, 959 ROLE 960 } 961 962 enum MissingMemberStrategy { 963 GO_AHEAD_WITH_DOWNLOADS, 964 GO_AHEAD_WITHOUT_DOWNLOADS, 965 USER_ABORTED 966 } 967}