001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.command; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.HashMap; 012import java.util.HashSet; 013import java.util.Iterator; 014import java.util.LinkedList; 015import java.util.List; 016import java.util.Map; 017import java.util.Objects; 018import java.util.Optional; 019import java.util.Set; 020import java.util.function.Consumer; 021 022import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 023import org.openstreetmap.josm.data.osm.Node; 024import org.openstreetmap.josm.data.osm.OsmPrimitive; 025import org.openstreetmap.josm.data.osm.PrimitiveId; 026import org.openstreetmap.josm.data.osm.Relation; 027import org.openstreetmap.josm.data.osm.RelationMember; 028import org.openstreetmap.josm.data.osm.Way; 029import org.openstreetmap.josm.spi.preferences.Config; 030import org.openstreetmap.josm.tools.CheckParameterUtil; 031import org.openstreetmap.josm.tools.Logging; 032 033/** 034 * Splits a way into multiple ways (all identical except for their node list). 035 * 036 * Ways are just split at the selected nodes. The nodes remain in their 037 * original order. Selected nodes at the end of a way are ignored. 038 * 039 * @since 12828 ({@code SplitWayAction} converted to a {@link Command}) 040 */ 041public class SplitWayCommand extends SequenceCommand { 042 043 private static volatile Consumer<String> warningNotifier = Logging::warn; 044 045 private static final class RelationInformation { 046 boolean warnme; 047 boolean insert; 048 Relation relation; 049 } 050 051 /** 052 * Sets the global warning notifier. 053 * @param notifier warning notifier in charge of displaying warning message, if any. Must not be null 054 */ 055 public static void setWarningNotifier(Consumer<String> notifier) { 056 warningNotifier = Objects.requireNonNull(notifier); 057 } 058 059 private final List<? extends PrimitiveId> newSelection; 060 private final Way originalWay; 061 private final List<Way> newWays; 062 /** Map<Restriction type, type to treat it as> */ 063 private static final Map<String, String> relationSpecialTypes = new HashMap<>(); 064 static { 065 relationSpecialTypes.put("restriction", "restriction"); 066 relationSpecialTypes.put("destination_sign", "restriction"); 067 } 068 069 /** 070 * Create a new {@code SplitWayCommand}. 071 * @param name The description text 072 * @param commandList The sequence of commands that should be executed. 073 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection}) 074 * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay}) 075 * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getOriginalWay}) 076 */ 077 public SplitWayCommand(String name, Collection<Command> commandList, 078 List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) { 079 super(name, commandList); 080 this.newSelection = newSelection; 081 this.originalWay = originalWay; 082 this.newWays = newWays; 083 } 084 085 /** 086 * Replies the new list of selected primitives ids 087 * @return The new list of selected primitives ids 088 */ 089 public List<? extends PrimitiveId> getNewSelection() { 090 return newSelection; 091 } 092 093 /** 094 * Replies the original way being split 095 * @return The original way being split 096 */ 097 public Way getOriginalWay() { 098 return originalWay; 099 } 100 101 /** 102 * Replies the resulting new ways 103 * @return The resulting new ways 104 */ 105 public List<Way> getNewWays() { 106 return newWays; 107 } 108 109 /** 110 * Determines which way chunk should reuse the old id and its history 111 */ 112 @FunctionalInterface 113 public interface Strategy { 114 115 /** 116 * Determines which way chunk should reuse the old id and its history. 117 * 118 * @param wayChunks the way chunks 119 * @return the way to keep 120 */ 121 Way determineWayToKeep(Iterable<Way> wayChunks); 122 123 /** 124 * Returns a strategy which selects the way chunk with the highest node count to keep. 125 * @return strategy which selects the way chunk with the highest node count to keep 126 */ 127 static Strategy keepLongestChunk() { 128 return wayChunks -> { 129 Way wayToKeep = null; 130 for (Way i : wayChunks) { 131 if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) { 132 wayToKeep = i; 133 } 134 } 135 return wayToKeep; 136 }; 137 } 138 139 /** 140 * Returns a strategy which selects the first way chunk. 141 * @return strategy which selects the first way chunk 142 */ 143 static Strategy keepFirstChunk() { 144 return wayChunks -> wayChunks.iterator().next(); 145 } 146 } 147 148 /** 149 * Splits the nodes of {@code wayToSplit} into a list of node sequences 150 * which are separated at the nodes in {@code splitPoints}. 151 * 152 * This method displays warning messages if {@code wayToSplit} and/or 153 * {@code splitPoints} aren't consistent. 154 * 155 * Returns null, if building the split chunks fails. 156 * 157 * @param wayToSplit the way to split. Must not be null. 158 * @param splitPoints the nodes where the way is split. Must not be null. 159 * @return the list of chunks 160 */ 161 public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) { 162 CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit"); 163 CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints"); 164 165 Set<Node> nodeSet = new HashSet<>(splitPoints); 166 List<List<Node>> wayChunks = new LinkedList<>(); 167 List<Node> currentWayChunk = new ArrayList<>(); 168 wayChunks.add(currentWayChunk); 169 170 Iterator<Node> it = wayToSplit.getNodes().iterator(); 171 while (it.hasNext()) { 172 Node currentNode = it.next(); 173 boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext(); 174 currentWayChunk.add(currentNode); 175 if (nodeSet.contains(currentNode) && !atEndOfWay) { 176 currentWayChunk = new ArrayList<>(); 177 currentWayChunk.add(currentNode); 178 wayChunks.add(currentWayChunk); 179 } 180 } 181 182 // Handle circular ways specially. 183 // If you split at a circular way at two nodes, you just want to split 184 // it at these points, not also at the former endpoint. 185 // So if the last node is the same first node, join the last and the 186 // first way chunk. 187 List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1); 188 if (wayChunks.size() >= 2 189 && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1) 190 && !nodeSet.contains(wayChunks.get(0).get(0))) { 191 if (wayChunks.size() == 2) { 192 warningNotifier.accept(tr("You must select two or more nodes to split a circular way.")); 193 return null; 194 } 195 lastWayChunk.remove(lastWayChunk.size() - 1); 196 lastWayChunk.addAll(wayChunks.get(0)); 197 wayChunks.remove(wayChunks.size() - 1); 198 wayChunks.set(0, lastWayChunk); 199 } 200 201 if (wayChunks.size() < 2) { 202 if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) { 203 warningNotifier.accept( 204 tr("You must select two or more nodes to split a circular way.")); 205 } else { 206 warningNotifier.accept( 207 tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)")); 208 } 209 return null; 210 } 211 return wayChunks; 212 } 213 214 /** 215 * Creates new way objects for the way chunks and transfers the keys from the original way. 216 * @param way the original way whose keys are transferred 217 * @param wayChunks the way chunks 218 * @return the new way objects 219 */ 220 public static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) { 221 final List<Way> newWays = new ArrayList<>(); 222 for (List<Node> wayChunk : wayChunks) { 223 Way wayToAdd = new Way(); 224 wayToAdd.setKeys(way.getKeys()); 225 wayToAdd.setNodes(wayChunk); 226 newWays.add(wayToAdd); 227 } 228 return newWays; 229 } 230 231 /** 232 * Splits the way {@code way} into chunks of {@code wayChunks} and replies 233 * the result of this process in an instance of {@link SplitWayCommand}. 234 * 235 * Note that changes are not applied to the data yet. You have to 236 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}. 237 * 238 * @param way the way to split. Must not be null. 239 * @param wayChunks the list of way chunks into the way is split. Must not be null. 240 * @param selection The list of currently selected primitives 241 * @return the result from the split operation 242 */ 243 public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) { 244 return splitWay(way, wayChunks, selection, Strategy.keepLongestChunk()); 245 } 246 247 /** 248 * Splits the way {@code way} into chunks of {@code wayChunks} and replies 249 * the result of this process in an instance of {@link SplitWayCommand}. 250 * The {@link SplitWayCommand.Strategy} is used to determine which 251 * way chunk should reuse the old id and its history. 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 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history 260 * @return the result from the split operation 261 */ 262 public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks, 263 Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) { 264 // build a list of commands, and also a new selection list 265 final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size()); 266 newSelection.addAll(selection); 267 268 // Create all potential new ways 269 final List<Way> newWays = createNewWaysFromChunks(way, wayChunks); 270 271 // Determine which part reuses the existing way 272 final Way wayToKeep = splitStrategy.determineWayToKeep(newWays); 273 274 return wayToKeep != null ? doSplitWay(way, wayToKeep, newWays, newSelection) : null; 275 } 276 277 /** 278 * Effectively constructs the {@link SplitWayCommand}. 279 * This method is only public for {@code SplitWayAction}. 280 * 281 * @param way the way to split. Must not be null. 282 * @param wayToKeep way chunk which should reuse the old id and its history 283 * @param newWays potential new ways 284 * @param newSelection new selection list to update (optional: can be null) 285 * @return the {@code SplitWayCommand} 286 */ 287 public static SplitWayCommand doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) { 288 289 Collection<Command> commandList = new ArrayList<>(newWays.size()); 290 Collection<String> nowarnroles = Config.getPref().getList("way.split.roles.nowarn", 291 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west")); 292 293 // Change the original way 294 final Way changedWay = new Way(way); 295 changedWay.setNodes(wayToKeep.getNodes()); 296 commandList.add(new ChangeCommand(way, changedWay)); 297 if (/*!isMapModeDraw &&*/ newSelection != null && !newSelection.contains(way)) { 298 newSelection.add(way); 299 } 300 final int indexOfWayToKeep = newWays.indexOf(wayToKeep); 301 newWays.remove(wayToKeep); 302 303 if (/*!isMapModeDraw &&*/ newSelection != null) { 304 newSelection.addAll(newWays); 305 } 306 for (Way wayToAdd : newWays) { 307 commandList.add(new AddCommand(way.getDataSet(), wayToAdd)); 308 } 309 310 boolean warnmerole = false; 311 boolean warnme = false; 312 // now copy all relations to new way also 313 314 for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(way))) { 315 if (!r.isUsable()) { 316 continue; 317 } 318 Relation c = null; 319 String type = Optional.ofNullable(r.get("type")).orElse(""); 320 321 int ic = 0; 322 int ir = 0; 323 List<RelationMember> relationMembers = r.getMembers(); 324 for (RelationMember rm: relationMembers) { 325 if (rm.isWay() && rm.getMember() == way) { 326 boolean insert = true; 327 if (relationSpecialTypes.containsKey(type) && "restriction".equals(relationSpecialTypes.get(type))) { 328 RelationInformation rValue = treatAsRestriction(r, rm, c, newWays, way, changedWay); 329 warnme = rValue.warnme; 330 insert = rValue.insert; 331 c = rValue.relation; 332 } else if (!("route".equals(type)) && !("multipolygon".equals(type))) { 333 warnme = true; 334 } 335 if (c == null) { 336 c = new Relation(r); 337 } 338 339 if (insert) { 340 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) { 341 warnmerole = true; 342 } 343 344 Boolean backwards = null; 345 int k = 1; 346 while (ir - k >= 0 || ir + k < relationMembers.size()) { 347 if ((ir - k >= 0) && relationMembers.get(ir - k).isWay()) { 348 Way w = relationMembers.get(ir - k).getWay(); 349 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) { 350 backwards = Boolean.FALSE; 351 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) { 352 backwards = Boolean.TRUE; 353 } 354 break; 355 } 356 if ((ir + k < relationMembers.size()) && relationMembers.get(ir + k).isWay()) { 357 Way w = relationMembers.get(ir + k).getWay(); 358 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) { 359 backwards = Boolean.TRUE; 360 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) { 361 backwards = Boolean.FALSE; 362 } 363 break; 364 } 365 k++; 366 } 367 368 int j = ic; 369 final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep); 370 for (Way wayToAdd : waysToAddBefore) { 371 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 372 j++; 373 if (Boolean.TRUE.equals(backwards)) { 374 c.addMember(ic + 1, em); 375 } else { 376 c.addMember(j - 1, em); 377 } 378 } 379 final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size()); 380 for (Way wayToAdd : waysToAddAfter) { 381 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 382 j++; 383 if (Boolean.TRUE.equals(backwards)) { 384 c.addMember(ic, em); 385 } else { 386 c.addMember(j, em); 387 } 388 } 389 ic = j; 390 } 391 } 392 ic++; 393 ir++; 394 } 395 396 if (c != null) { 397 commandList.add(new ChangeCommand(r.getDataSet(), r, c)); 398 } 399 } 400 if (warnmerole) { 401 warningNotifier.accept( 402 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.")); 403 } else if (warnme) { 404 warningNotifier.accept( 405 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.")); 406 } 407 408 return new SplitWayCommand( 409 /* for correct i18n of plural forms - see #9110 */ 410 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1, 411 way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1), 412 commandList, 413 newSelection, 414 way, 415 newWays 416 ); 417 } 418 419 private static RelationInformation treatAsRestriction(Relation r, 420 RelationMember rm, Relation c, Collection<Way> newWays, Way way, 421 Way changedWay) { 422 RelationInformation relationInformation = new RelationInformation(); 423 /* this code assumes the restriction is correct. No real error checking done */ 424 String role = rm.getRole(); 425 String type = Optional.ofNullable(r.get("type")).orElse(""); 426 if ("from".equals(role) || "to".equals(role)) { 427 OsmPrimitive via = findVia(r, type); 428 List<Node> nodes = new ArrayList<>(); 429 if (via != null) { 430 if (via instanceof Node) { 431 nodes.add((Node) via); 432 } else if (via instanceof Way) { 433 nodes.add(((Way) via).lastNode()); 434 nodes.add(((Way) via).firstNode()); 435 } 436 } 437 Way res = null; 438 for (Node n : nodes) { 439 if (changedWay.isFirstLastNode(n)) { 440 res = way; 441 } 442 } 443 if (res == null) { 444 for (Way wayToAdd : newWays) { 445 for (Node n : nodes) { 446 if (wayToAdd.isFirstLastNode(n)) { 447 res = wayToAdd; 448 } 449 } 450 } 451 if (res != null) { 452 if (c == null) { 453 c = new Relation(r); 454 } 455 c.addMember(new RelationMember(role, res)); 456 c.removeMembersFor(way); 457 relationInformation.insert = false; 458 } 459 } else { 460 relationInformation.insert = false; 461 } 462 } else if (!"via".equals(role)) { 463 relationInformation.warnme = true; 464 } 465 relationInformation.relation = c; 466 return relationInformation; 467 } 468 469 static OsmPrimitive findVia(Relation r, String type) { 470 if (type != null) { 471 switch (type) { 472 case "restriction": 473 return findRelationMember(r, "via").orElse(null); 474 case "destination_sign": 475 // Prefer intersection over sign, see #12347 476 return findRelationMember(r, "intersection").orElse(findRelationMember(r, "sign").orElse(null)); 477 default: 478 return null; 479 } 480 } 481 return null; 482 } 483 484 static Optional<OsmPrimitive> findRelationMember(Relation r, String role) { 485 return r.getMembers().stream().filter(rmv -> role.equals(rmv.getRole())) 486 .map(RelationMember::getMember).findAny(); 487 } 488 489 /** 490 * Splits the way {@code way} at the nodes in {@code atNodes} and replies 491 * the result of this process in an instance of {@link SplitWayCommand}. 492 * 493 * Note that changes are not applied to the data yet. You have to 494 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}. 495 * 496 * Replies null if the way couldn't be split at the given nodes. 497 * 498 * @param way the way to split. Must not be null. 499 * @param atNodes the list of nodes where the way is split. Must not be null. 500 * @param selection The list of currently selected primitives 501 * @return the result from the split operation 502 */ 503 public static SplitWayCommand split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) { 504 List<List<Node>> chunks = buildSplitChunks(way, atNodes); 505 return chunks != null ? splitWay(way, chunks, selection) : null; 506 } 507 508 /** 509 * Add relations that are treated in a specific way. 510 * @param relationType The value in the {@code type} key 511 * @param treatAs The type of relation to treat the {@code relationType} as. 512 * Currently only supports relations that can be handled like "restriction" 513 * relations. 514 * @return the previous value associated with relationType, or null if there was no mapping 515 * @since 15078 516 */ 517 public static String addSpecialRelationType(String relationType, String treatAs) { 518 return relationSpecialTypes.put(relationType, treatAs); 519 } 520 521 /** 522 * Get the types of relations that are treated differently 523 * @return {@code Map<Relation Type, Type of Relation it is to be treated as>} 524 * @since 15078 525 */ 526 public static Map<String, String> getSpecialRelationTypes() { 527 return relationSpecialTypes; 528 } 529}