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