source: josm/trunk/src/org/openstreetmap/josm/actions/SplitWayAction.java@ 8441

Last change on this file since 8441 was 8276, checked in by Balaitous, 9 years ago

fix #11184 - "split way" is splitting an other way instead of the selected.
The case where the selected node is both one end and an intermediate node of the selected way was not taken into account.
Add test case.

  • Property svn:eol-style set to native
File size: 23.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashSet;
15import java.util.Iterator;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.JOptionPane;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.command.AddCommand;
24import org.openstreetmap.josm.command.ChangeCommand;
25import org.openstreetmap.josm.command.Command;
26import org.openstreetmap.josm.command.SequenceCommand;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.PrimitiveId;
30import org.openstreetmap.josm.data.osm.Relation;
31import org.openstreetmap.josm.data.osm.RelationMember;
32import org.openstreetmap.josm.data.osm.Way;
33import org.openstreetmap.josm.gui.DefaultNameFormatter;
34import org.openstreetmap.josm.gui.Notification;
35import org.openstreetmap.josm.gui.layer.OsmDataLayer;
36import org.openstreetmap.josm.tools.CheckParameterUtil;
37import org.openstreetmap.josm.tools.Shortcut;
38
39/**
40 * Splits a way into multiple ways (all identical except for their node list).
41 *
42 * Ways are just split at the selected nodes. The nodes remain in their
43 * original order. Selected nodes at the end of a way are ignored.
44 */
45
46public class SplitWayAction extends JosmAction {
47
48 /**
49 * Represents the result of a {@link SplitWayAction}
50 * @see SplitWayAction#splitWay
51 * @see SplitWayAction#split
52 */
53 public static class SplitWayResult {
54 private final Command command;
55 private final List<? extends PrimitiveId> newSelection;
56 private Way originalWay;
57 private List<Way> newWays;
58
59 /**
60 * @param command The command to be performed to split the way (which is saved for later retrieval by the {@link #getCommand} method)
61 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval by the {@link #getNewSelection} method)
62 * @param originalWay The original way being split (which is saved for later retrieval by the {@link #getOriginalWay} method)
63 * @param newWays The resulting new ways (which is saved for later retrieval by the {@link #getOriginalWay} method)
64 */
65 public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
66 this.command = command;
67 this.newSelection = newSelection;
68 this.originalWay = originalWay;
69 this.newWays = newWays;
70 }
71
72 /**
73 * Replies the command to be performed to split the way
74 * @return The command to be performed to split the way
75 */
76 public Command getCommand() {
77 return command;
78 }
79
80 /**
81 * Replies the new list of selected primitives ids
82 * @return The new list of selected primitives ids
83 */
84 public List<? extends PrimitiveId> getNewSelection() {
85 return newSelection;
86 }
87
88 /**
89 * Replies the original way being split
90 * @return The original way being split
91 */
92 public Way getOriginalWay() {
93 return originalWay;
94 }
95
96 /**
97 * Replies the resulting new ways
98 * @return The resulting new ways
99 */
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 =
128 OsmPrimitive.getFilteredList( selection, Relation.class);
129 List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
130
131 if (applicableWays == null) {
132 new Notification(
133 tr("The current selection cannot be used for splitting - no node is selected."))
134 .setIcon(JOptionPane.WARNING_MESSAGE)
135 .show();
136 return;
137 } else if (applicableWays.isEmpty()) {
138 new Notification(
139 tr("The selected nodes do not share the same way."))
140 .setIcon(JOptionPane.WARNING_MESSAGE)
141 .show();
142 return;
143 }
144
145 // If several ways have been found, remove ways that doesn't have selected
146 // node in the middle
147 if (applicableWays.size() > 1) {
148 for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) {
149 Way w = it.next();
150 for (Node n : selectedNodes) {
151 if (!w.isInnerNode(n)) {
152 it.remove();
153 break;
154 }
155 }
156 }
157 }
158
159 if (applicableWays.isEmpty()) {
160 new Notification(
161 trn("The selected node is not in the middle of any way.",
162 "The selected nodes are not in the middle of any way.",
163 selectedNodes.size()))
164 .setIcon(JOptionPane.WARNING_MESSAGE)
165 .show();
166 return;
167 } else if (applicableWays.size() > 1) {
168 new Notification(
169 trn("There is more than one way using the node you selected. Please select the way also.",
170 "There is more than one way using the nodes you selected. Please select the way also.",
171 selectedNodes.size()))
172 .setIcon(JOptionPane.WARNING_MESSAGE)
173 .show();
174 return;
175 }
176
177 // Finally, applicableWays contains only one perfect way
178 Way selectedWay = applicableWays.get(0);
179
180 List<List<Node>> wayChunks = buildSplitChunks(selectedWay, selectedNodes);
181 if (wayChunks != null) {
182 List<OsmPrimitive> sel = new ArrayList<>(selectedWays.size() + selectedRelations.size());
183 sel.addAll(selectedWays);
184 sel.addAll(selectedRelations);
185 SplitWayResult result = splitWay(getEditLayer(),selectedWay, wayChunks, sel);
186 Main.main.undoRedo.add(result.getCommand());
187 getCurrentDataSet().setSelected(result.getNewSelection());
188 }
189 }
190
191 /**
192 * Determine witch ways to split.
193 * @param selectedWays List of user selected ways.
194 * @param selectedNodes List of user selected nodes.
195 * @return List of ways to split
196 */
197 private List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
198 if (selectedNodes.isEmpty())
199 return null;
200
201 // Special case - one of the selected ways touches (not cross) way that we
202 // want to split
203 if (selectedNodes.size() == 1) {
204 Node n = selectedNodes.get(0);
205 List<Way> referedWays =
206 OsmPrimitive.getFilteredList(n.getReferrers(), Way.class);
207 Way inTheMiddle = null;
208 for (Way w: referedWays) {
209 // Need to look at all nodes see #11184 for a case where node n is
210 // firstNode, lastNode and also in the middle
211 if (selectedWays.contains(w) && w.isInnerNode(n)) {
212 if (inTheMiddle == null) {
213 inTheMiddle = w;
214 } else {
215 inTheMiddle = null;
216 break;
217 }
218 }
219 }
220 if (inTheMiddle != null)
221 return Collections.singletonList(inTheMiddle);
222 }
223
224 // List of ways shared by all nodes
225 List<Way> result =
226 new ArrayList<>(OsmPrimitive.getFilteredList(selectedNodes.get(0).getReferrers(),
227 Way.class));
228 for (int i=1; i<selectedNodes.size(); i++) {
229 List<OsmPrimitive> ref = selectedNodes.get(i).getReferrers();
230 for (Iterator<Way> it = result.iterator(); it.hasNext(); ) {
231 if (!ref.contains(it.next())) {
232 it.remove();
233 }
234 }
235 }
236
237 // Remove broken ways
238 for (Iterator<Way> it = result.iterator(); it.hasNext(); ) {
239 if (it.next().getNodesCount() <= 2) {
240 it.remove();
241 }
242 }
243
244 if (selectedWays.isEmpty())
245 return result;
246 else {
247 // Return only selected ways
248 for (Iterator<Way> it = result.iterator(); it.hasNext(); ) {
249 if (!selectedWays.contains(it.next())) {
250 it.remove();
251 }
252 }
253 return result;
254 }
255 }
256
257 /**
258 * Splits the nodes of {@code wayToSplit} into a list of node sequences
259 * which are separated at the nodes in {@code splitPoints}.
260 *
261 * This method displays warning messages if {@code wayToSplit} and/or
262 * {@code splitPoints} aren't consistent.
263 *
264 * Returns null, if building the split chunks fails.
265 *
266 * @param wayToSplit the way to split. Must not be null.
267 * @param splitPoints the nodes where the way is split. Must not be null.
268 * @return the list of chunks
269 */
270 public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints){
271 CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
272 CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
273
274 Set<Node> nodeSet = new HashSet<>(splitPoints);
275 List<List<Node>> wayChunks = new LinkedList<>();
276 List<Node> currentWayChunk = new ArrayList<>();
277 wayChunks.add(currentWayChunk);
278
279 Iterator<Node> it = wayToSplit.getNodes().iterator();
280 while (it.hasNext()) {
281 Node currentNode = it.next();
282 boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
283 currentWayChunk.add(currentNode);
284 if (nodeSet.contains(currentNode) && !atEndOfWay) {
285 currentWayChunk = new ArrayList<>();
286 currentWayChunk.add(currentNode);
287 wayChunks.add(currentWayChunk);
288 }
289 }
290
291 // Handle circular ways specially.
292 // If you split at a circular way at two nodes, you just want to split
293 // it at these points, not also at the former endpoint.
294 // So if the last node is the same first node, join the last and the
295 // first way chunk.
296 List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
297 if (wayChunks.size() >= 2
298 && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
299 && !nodeSet.contains(wayChunks.get(0).get(0))) {
300 if (wayChunks.size() == 2) {
301 new Notification(
302 tr("You must select two or more nodes to split a circular way."))
303 .setIcon(JOptionPane.WARNING_MESSAGE)
304 .show();
305 return null;
306 }
307 lastWayChunk.remove(lastWayChunk.size() - 1);
308 lastWayChunk.addAll(wayChunks.get(0));
309 wayChunks.remove(wayChunks.size() - 1);
310 wayChunks.set(0, lastWayChunk);
311 }
312
313 if (wayChunks.size() < 2) {
314 if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
315 new Notification(
316 tr("You must select two or more nodes to split a circular way."))
317 .setIcon(JOptionPane.WARNING_MESSAGE)
318 .show();
319 } else {
320 new Notification(
321 tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"))
322 .setIcon(JOptionPane.WARNING_MESSAGE)
323 .show();
324 }
325 return null;
326 }
327 return wayChunks;
328 }
329
330 /**
331 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
332 * the result of this process in an instance of {@link SplitWayResult}.
333 *
334 * Note that changes are not applied to the data yet. You have to
335 * submit the command in {@link SplitWayResult#getCommand()} first,
336 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
337 *
338 * @param layer the layer which the way belongs to. Must not be null.
339 * @param way the way to split. Must not be null.
340 * @param wayChunks the list of way chunks into the way is split. Must not be null.
341 * @param selection The list of currently selected primitives
342 * @return the result from the split operation
343 */
344 public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
345 // build a list of commands, and also a new selection list
346 Collection<Command> commandList = new ArrayList<>(wayChunks.size());
347 List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
348 newSelection.addAll(selection);
349
350 Iterator<List<Node>> chunkIt = wayChunks.iterator();
351 Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn",
352 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
353
354 // First, change the original way
355 Way changedWay = new Way(way);
356 changedWay.setNodes(chunkIt.next());
357 commandList.add(new ChangeCommand(way, changedWay));
358 if (!newSelection.contains(way)) {
359 newSelection.add(way);
360 }
361
362 List<Way> newWays = new ArrayList<>();
363 // Second, create new ways
364 while (chunkIt.hasNext()) {
365 Way wayToAdd = new Way();
366 wayToAdd.setKeys(way.getKeys());
367 newWays.add(wayToAdd);
368 wayToAdd.setNodes(chunkIt.next());
369 commandList.add(new AddCommand(layer, wayToAdd));
370 newSelection.add(wayToAdd);
371 }
372 boolean warnmerole = false;
373 boolean warnme = false;
374 // now copy all relations to new way also
375
376 for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
377 if (!r.isUsable()) {
378 continue;
379 }
380 Relation c = null;
381 String type = r.get("type");
382 if (type == null) {
383 type = "";
384 }
385
386 int i_c = 0, i_r = 0;
387 List<RelationMember> relationMembers = r.getMembers();
388 for (RelationMember rm: relationMembers) {
389 if (rm.isWay() && rm.getMember() == way) {
390 boolean insert = true;
391 if ("restriction".equals(type)) {
392 /* this code assumes the restriction is correct. No real error checking done */
393 String role = rm.getRole();
394 if("from".equals(role) || "to".equals(role)) {
395 OsmPrimitive via = null;
396 for (RelationMember rmv : r.getMembers()) {
397 if ("via".equals(rmv.getRole())){
398 via = rmv.getMember();
399 }
400 }
401 List<Node> nodes = new ArrayList<>();
402 if (via != null) {
403 if (via instanceof Node) {
404 nodes.add((Node)via);
405 } else if (via instanceof Way) {
406 nodes.add(((Way)via).lastNode());
407 nodes.add(((Way)via).firstNode());
408 }
409 }
410 Way res = null;
411 for (Node n : nodes) {
412 if(changedWay.isFirstLastNode(n)) {
413 res = way;
414 }
415 }
416 if (res == null) {
417 for (Way wayToAdd : newWays) {
418 for(Node n : nodes) {
419 if(wayToAdd.isFirstLastNode(n)) {
420 res = wayToAdd;
421 }
422 }
423 }
424 if (res != null) {
425 if (c == null) {
426 c = new Relation(r);
427 }
428 c.addMember(new RelationMember(role, res));
429 c.removeMembersFor(way);
430 insert = false;
431 }
432 } else {
433 insert = false;
434 }
435 } else if(!"via".equals(role)) {
436 warnme = true;
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 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
447 warnmerole = true;
448 }
449
450 Boolean backwards = null;
451 int k = 1;
452 while (i_r - k >= 0 || i_r + k < relationMembers.size()) {
453 if ((i_r - k >= 0) && relationMembers.get(i_r - k).isWay()){
454 Way w = relationMembers.get(i_r - k).getWay();
455 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
456 backwards = false;
457 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
458 backwards = true;
459 }
460 break;
461 }
462 if ((i_r + k < relationMembers.size()) && relationMembers.get(i_r + k).isWay()){
463 Way w = relationMembers.get(i_r + k).getWay();
464 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
465 backwards = true;
466 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
467 backwards = false;
468 }
469 break;
470 }
471 k++;
472 }
473
474 int j = i_c;
475 for (Way wayToAdd : newWays) {
476 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
477 j++;
478 if ((backwards != null) && backwards) {
479 c.addMember(i_c, em);
480 } else {
481 c.addMember(j, em);
482 }
483 }
484 i_c = j;
485 }
486 }
487 i_c++;
488 i_r++;
489 }
490
491 if (c != null) {
492 commandList.add(new ChangeCommand(layer, r, c));
493 }
494 }
495 if (warnmerole) {
496 new Notification(
497 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
498 .setIcon(JOptionPane.WARNING_MESSAGE)
499 .show();
500 } else if (warnme) {
501 new Notification(
502 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
503 .setIcon(JOptionPane.WARNING_MESSAGE)
504 .show();
505 }
506
507 return new SplitWayResult(
508 new SequenceCommand(
509 /* for correct i18n of plural forms - see #9110 */
510 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", wayChunks.size(),
511 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 public static 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}
Note: See TracBrowser for help on using the repository browser.