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