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