001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.util.ArrayList; 011import java.util.Arrays; 012import java.util.Collection; 013import java.util.Collections; 014import java.util.HashSet; 015import java.util.Iterator; 016import java.util.LinkedList; 017import java.util.List; 018import java.util.Set; 019 020import javax.swing.JOptionPane; 021 022import org.openstreetmap.josm.Main; 023import org.openstreetmap.josm.command.AddCommand; 024import org.openstreetmap.josm.command.ChangeCommand; 025import org.openstreetmap.josm.command.Command; 026import org.openstreetmap.josm.command.SequenceCommand; 027import org.openstreetmap.josm.data.osm.Node; 028import org.openstreetmap.josm.data.osm.OsmPrimitive; 029import org.openstreetmap.josm.data.osm.PrimitiveId; 030import org.openstreetmap.josm.data.osm.Relation; 031import org.openstreetmap.josm.data.osm.RelationMember; 032import org.openstreetmap.josm.data.osm.Way; 033import org.openstreetmap.josm.gui.DefaultNameFormatter; 034import org.openstreetmap.josm.gui.Notification; 035import org.openstreetmap.josm.gui.layer.OsmDataLayer; 036import org.openstreetmap.josm.tools.CheckParameterUtil; 037import org.openstreetmap.josm.tools.Shortcut; 038 039/** 040 * Splits a way into multiple ways (all identical except for their node list). 041 * 042 * Ways are just split at the selected nodes. The nodes remain in their 043 * original order. Selected nodes at the end of a way are ignored. 044 */ 045 046public class SplitWayAction extends JosmAction { 047 048 /** 049 * Represents the result of a {@link SplitWayAction} 050 * @see SplitWayAction#splitWay 051 * @see SplitWayAction#split 052 */ 053 public static class SplitWayResult { 054 private final Command command; 055 private final List<? extends PrimitiveId> newSelection; 056 private Way originalWay; 057 private List<Way> newWays; 058 059 /** 060 * @param command The command to be performed to split the way (which is saved for later retrieval by the {@link #getCommand} method) 061 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval by the {@link #getNewSelection} method) 062 * @param originalWay The original way being split (which is saved for later retrieval by the {@link #getOriginalWay} method) 063 * @param newWays The resulting new ways (which is saved for later retrieval by the {@link #getOriginalWay} method) 064 */ 065 public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) { 066 this.command = command; 067 this.newSelection = newSelection; 068 this.originalWay = originalWay; 069 this.newWays = newWays; 070 } 071 072 /** 073 * Replies the command to be performed to split the way 074 * @return The command to be performed to split the way 075 */ 076 public Command getCommand() { 077 return command; 078 } 079 080 /** 081 * Replies the new list of selected primitives ids 082 * @return The new list of selected primitives ids 083 */ 084 public List<? extends PrimitiveId> getNewSelection() { 085 return newSelection; 086 } 087 088 /** 089 * Replies the original way being split 090 * @return The original way being split 091 */ 092 public Way getOriginalWay() { 093 return originalWay; 094 } 095 096 /** 097 * Replies the resulting new ways 098 * @return The resulting new ways 099 */ 100 public List<Way> getNewWays() { 101 return newWays; 102 } 103 } 104 105 /** 106 * Create a new SplitWayAction. 107 */ 108 public SplitWayAction() { 109 super(tr("Split Way"), "splitway", tr("Split a way at the selected node."), 110 Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true); 111 putValue("help", ht("/Action/SplitWay")); 112 } 113 114 /** 115 * Called when the action is executed. 116 * 117 * This method performs an expensive check whether the selection clearly defines one 118 * of the split actions outlined above, and if yes, calls the splitWay method. 119 */ 120 @Override 121 public void actionPerformed(ActionEvent e) { 122 123 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected(); 124 125 List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class); 126 List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class); 127 List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class); 128 List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes); 129 130 if (applicableWays == null) { 131 new Notification( 132 tr("The current selection cannot be used for splitting - no node is selected.")) 133 .setIcon(JOptionPane.WARNING_MESSAGE) 134 .show(); 135 return; 136 } else if (applicableWays.isEmpty()) { 137 new Notification( 138 tr("The selected nodes do not share the same way.")) 139 .setIcon(JOptionPane.WARNING_MESSAGE) 140 .show(); 141 return; 142 } 143 144 // If several ways have been found, remove ways that doesn't have selected node in the middle 145 if (applicableWays.size() > 1) { 146 WAY_LOOP: 147 for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) { 148 Way w = it.next(); 149 for (Node n : selectedNodes) { 150 if (!w.isInnerNode(n)) { 151 it.remove(); 152 continue WAY_LOOP; 153 } 154 } 155 } 156 } 157 158 if (applicableWays.isEmpty()) { 159 new Notification( 160 trn("The selected node is not in the middle of any way.", 161 "The selected nodes are not in the middle of any way.", 162 selectedNodes.size())) 163 .setIcon(JOptionPane.WARNING_MESSAGE) 164 .show(); 165 return; 166 } else if (applicableWays.size() > 1) { 167 new Notification( 168 trn("There is more than one way using the node you selected. Please select the way also.", 169 "There is more than one way using the nodes you selected. Please select the way also.", 170 selectedNodes.size())) 171 .setIcon(JOptionPane.WARNING_MESSAGE) 172 .show(); 173 return; 174 } 175 176 // Finally, applicableWays contains only one perfect way 177 Way selectedWay = applicableWays.get(0); 178 179 List<List<Node>> wayChunks = buildSplitChunks(selectedWay, selectedNodes); 180 if (wayChunks != null) { 181 List<OsmPrimitive> sel = new ArrayList<>(selectedWays.size() + selectedRelations.size()); 182 sel.addAll(selectedWays); 183 sel.addAll(selectedRelations); 184 SplitWayResult result = splitWay(getEditLayer(),selectedWay, wayChunks, sel); 185 Main.main.undoRedo.add(result.getCommand()); 186 getCurrentDataSet().setSelected(result.getNewSelection()); 187 } 188 } 189 190 private List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) { 191 if (selectedNodes.isEmpty()) 192 return null; 193 194 // Special case - one of the selected ways touches (not cross) way that we want to split 195 if (selectedNodes.size() == 1) { 196 Node n = selectedNodes.get(0); 197 List<Way> referedWays = OsmPrimitive.getFilteredList(n.getReferrers(), Way.class); 198 Way inTheMiddle = null; 199 boolean foundSelected = false; 200 for (Way w: referedWays) { 201 if (selectedWays.contains(w)) { 202 foundSelected = true; 203 } 204 if (w.getNode(0) != n && w.getNode(w.getNodesCount() - 1) != n) { 205 if (inTheMiddle == null) { 206 inTheMiddle = w; 207 } else { 208 inTheMiddle = null; 209 break; 210 } 211 } 212 } 213 if (foundSelected && inTheMiddle != null) 214 return Collections.singletonList(inTheMiddle); 215 } 216 217 // List of ways shared by all nodes 218 List<Way> result = new ArrayList<>(OsmPrimitive.getFilteredList(selectedNodes.get(0).getReferrers(), Way.class)); 219 for (int i=1; i<selectedNodes.size(); i++) { 220 List<OsmPrimitive> ref = selectedNodes.get(i).getReferrers(); 221 for (Iterator<Way> it = result.iterator(); it.hasNext(); ) { 222 if (!ref.contains(it.next())) { 223 it.remove(); 224 } 225 } 226 } 227 228 // Remove broken ways 229 for (Iterator<Way> it = result.iterator(); it.hasNext(); ) { 230 if (it.next().getNodesCount() <= 2) { 231 it.remove(); 232 } 233 } 234 235 if (selectedWays.isEmpty()) 236 return result; 237 else { 238 // Return only selected ways 239 for (Iterator<Way> it = result.iterator(); it.hasNext(); ) { 240 if (!selectedWays.contains(it.next())) { 241 it.remove(); 242 } 243 } 244 return result; 245 } 246 } 247 248 /** 249 * Splits the nodes of {@code wayToSplit} into a list of node sequences 250 * which are separated at the nodes in {@code splitPoints}. 251 * 252 * This method displays warning messages if {@code wayToSplit} and/or 253 * {@code splitPoints} aren't consistent. 254 * 255 * Returns null, if building the split chunks fails. 256 * 257 * @param wayToSplit the way to split. Must not be null. 258 * @param splitPoints the nodes where the way is split. Must not be null. 259 * @return the list of chunks 260 */ 261 public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints){ 262 CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit"); 263 CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints"); 264 265 Set<Node> nodeSet = new HashSet<>(splitPoints); 266 List<List<Node>> wayChunks = new LinkedList<>(); 267 List<Node> currentWayChunk = new ArrayList<>(); 268 wayChunks.add(currentWayChunk); 269 270 Iterator<Node> it = wayToSplit.getNodes().iterator(); 271 while (it.hasNext()) { 272 Node currentNode = it.next(); 273 boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext(); 274 currentWayChunk.add(currentNode); 275 if (nodeSet.contains(currentNode) && !atEndOfWay) { 276 currentWayChunk = new ArrayList<>(); 277 currentWayChunk.add(currentNode); 278 wayChunks.add(currentWayChunk); 279 } 280 } 281 282 // Handle circular ways specially. 283 // If you split at a circular way at two nodes, you just want to split 284 // it at these points, not also at the former endpoint. 285 // So if the last node is the same first node, join the last and the 286 // first way chunk. 287 List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1); 288 if (wayChunks.size() >= 2 289 && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1) 290 && !nodeSet.contains(wayChunks.get(0).get(0))) { 291 if (wayChunks.size() == 2) { 292 new Notification( 293 tr("You must select two or more nodes to split a circular way.")) 294 .setIcon(JOptionPane.WARNING_MESSAGE) 295 .show(); 296 return null; 297 } 298 lastWayChunk.remove(lastWayChunk.size() - 1); 299 lastWayChunk.addAll(wayChunks.get(0)); 300 wayChunks.remove(wayChunks.size() - 1); 301 wayChunks.set(0, lastWayChunk); 302 } 303 304 if (wayChunks.size() < 2) { 305 if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) { 306 new Notification( 307 tr("You must select two or more nodes to split a circular way.")) 308 .setIcon(JOptionPane.WARNING_MESSAGE) 309 .show(); 310 } else { 311 new Notification( 312 tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)")) 313 .setIcon(JOptionPane.WARNING_MESSAGE) 314 .show(); 315 } 316 return null; 317 } 318 return wayChunks; 319 } 320 321 /** 322 * Splits the way {@code way} into chunks of {@code wayChunks} and replies 323 * the result of this process in an instance of {@link SplitWayResult}. 324 * 325 * Note that changes are not applied to the data yet. You have to 326 * submit the command in {@link SplitWayResult#getCommand()} first, 327 * i.e. {@code Main.main.undoredo.add(result.getCommand())}. 328 * 329 * @param layer the layer which the way belongs to. Must not be null. 330 * @param way the way to split. Must not be null. 331 * @param wayChunks the list of way chunks into the way is split. Must not be null. 332 * @param selection The list of currently selected primitives 333 * @return the result from the split operation 334 */ 335 public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) { 336 // build a list of commands, and also a new selection list 337 Collection<Command> commandList = new ArrayList<>(wayChunks.size()); 338 List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size()); 339 newSelection.addAll(selection); 340 341 Iterator<List<Node>> chunkIt = wayChunks.iterator(); 342 Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn", 343 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west")); 344 345 // First, change the original way 346 Way changedWay = new Way(way); 347 changedWay.setNodes(chunkIt.next()); 348 commandList.add(new ChangeCommand(way, changedWay)); 349 if (!newSelection.contains(way)) { 350 newSelection.add(way); 351 } 352 353 List<Way> newWays = new ArrayList<>(); 354 // Second, create new ways 355 while (chunkIt.hasNext()) { 356 Way wayToAdd = new Way(); 357 wayToAdd.setKeys(way.getKeys()); 358 newWays.add(wayToAdd); 359 wayToAdd.setNodes(chunkIt.next()); 360 commandList.add(new AddCommand(layer,wayToAdd)); 361 newSelection.add(wayToAdd); 362 363 } 364 boolean warnmerole = false; 365 boolean warnme = false; 366 // now copy all relations to new way also 367 368 for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) { 369 if (!r.isUsable()) { 370 continue; 371 } 372 Relation c = null; 373 String type = r.get("type"); 374 if (type == null) { 375 type = ""; 376 } 377 378 int i_c = 0, i_r = 0; 379 List<RelationMember> relationMembers = r.getMembers(); 380 for (RelationMember rm: relationMembers) { 381 if (rm.isWay() && rm.getMember() == way) { 382 boolean insert = true; 383 if ("restriction".equals(type)) 384 { 385 /* this code assumes the restriction is correct. No real error checking done */ 386 String role = rm.getRole(); 387 if("from".equals(role) || "to".equals(role)) 388 { 389 OsmPrimitive via = null; 390 for (RelationMember rmv : r.getMembers()) { 391 if("via".equals(rmv.getRole())){ 392 via = rmv.getMember(); 393 } 394 } 395 List<Node> nodes = new ArrayList<>(); 396 if(via != null) { 397 if(via instanceof Node) { 398 nodes.add((Node)via); 399 } else if(via instanceof Way) { 400 nodes.add(((Way)via).lastNode()); 401 nodes.add(((Way)via).firstNode()); 402 } 403 } 404 Way res = null; 405 for(Node n : nodes) { 406 if(changedWay.isFirstLastNode(n)) { 407 res = way; 408 } 409 } 410 if(res == null) 411 { 412 for (Way wayToAdd : newWays) { 413 for(Node n : nodes) { 414 if(wayToAdd.isFirstLastNode(n)) { 415 res = wayToAdd; 416 } 417 } 418 } 419 if(res != null) 420 { 421 if (c == null) { 422 c = new Relation(r); 423 } 424 c.addMember(new RelationMember(role, res)); 425 c.removeMembersFor(way); 426 insert = false; 427 } 428 } else { 429 insert = false; 430 } 431 } 432 else if(!"via".equals(role)) { 433 warnme = true; 434 } 435 } 436 else if (!("route".equals(type)) && !("multipolygon".equals(type))) { 437 warnme = true; 438 } 439 if (c == null) { 440 c = new Relation(r); 441 } 442 443 if(insert) 444 { 445 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) { 446 warnmerole = true; 447 } 448 449 Boolean backwards = null; 450 int k = 1; 451 while (i_r - k >= 0 || i_r + k < relationMembers.size()) { 452 if ((i_r - k >= 0) && relationMembers.get(i_r - k).isWay()){ 453 Way w = relationMembers.get(i_r - k).getWay(); 454 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) { 455 backwards = false; 456 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) { 457 backwards = true; 458 } 459 break; 460 } 461 if ((i_r + k < relationMembers.size()) && relationMembers.get(i_r + k).isWay()){ 462 Way w = relationMembers.get(i_r + k).getWay(); 463 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) { 464 backwards = true; 465 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) { 466 backwards = false; 467 } 468 break; 469 } 470 k++; 471 } 472 473 int j = i_c; 474 for (Way wayToAdd : newWays) { 475 RelationMember em = new RelationMember(rm.getRole(), wayToAdd); 476 j++; 477 if ((backwards != null) && backwards) { 478 c.addMember(i_c, em); 479 } else { 480 c.addMember(j, em); 481 } 482 } 483 i_c = j; 484 } 485 } 486 i_c++; i_r++; 487 } 488 489 if (c != null) { 490 commandList.add(new ChangeCommand(layer,r, c)); 491 } 492 } 493 if (warnmerole) { 494 new Notification( 495 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.")) 496 .setIcon(JOptionPane.WARNING_MESSAGE) 497 .show(); 498 } else if (warnme) { 499 new Notification( 500 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.")) 501 .setIcon(JOptionPane.WARNING_MESSAGE) 502 .show(); 503 } 504 505 return new SplitWayResult( 506 new SequenceCommand( 507 /* for correct i18n of plural forms - see #9110 */ 508 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", wayChunks.size(), 509 way.getDisplayName(DefaultNameFormatter.getInstance()), wayChunks.size()), 510 commandList 511 ), 512 newSelection, 513 way, 514 newWays 515 ); 516 } 517 518 /** 519 * Splits the way {@code way} at the nodes in {@code atNodes} and replies 520 * the result of this process in an instance of {@link SplitWayResult}. 521 * 522 * Note that changes are not applied to the data yet. You have to 523 * submit the command in {@link SplitWayResult#getCommand()} first, 524 * i.e. {@code Main.main.undoredo.add(result.getCommand())}. 525 * 526 * Replies null if the way couldn't be split at the given nodes. 527 * 528 * @param layer the layer which the way belongs to. Must not be null. 529 * @param way the way to split. Must not be null. 530 * @param atNodes the list of nodes where the way is split. Must not be null. 531 * @param selection The list of currently selected primitives 532 * @return the result from the split operation 533 */ 534 public static SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) { 535 List<List<Node>> chunks = buildSplitChunks(way, atNodes); 536 if (chunks == null) return null; 537 return splitWay(layer,way, chunks, selection); 538 } 539 540 @Override 541 protected void updateEnabledState() { 542 if (getCurrentDataSet() == null) { 543 setEnabled(false); 544 } else { 545 updateEnabledState(getCurrentDataSet().getSelected()); 546 } 547 } 548 549 @Override 550 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 551 if (selection == null) { 552 setEnabled(false); 553 return; 554 } 555 for (OsmPrimitive primitive: selection) { 556 if (primitive instanceof Node) { 557 setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong 558 return; 559 } 560 } 561 setEnabled(false); 562 } 563}