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

Last change on this file since 6046 was 5570, checked in by jttt, 11 years ago

Split way - allow special case when selected way touches way that we want to split (but it is not selected)

  • Property svn:eol-style set to native
File size: 23.6 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
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.layer.OsmDataLayer;
35import org.openstreetmap.josm.tools.CheckParameterUtil;
36import org.openstreetmap.josm.tools.Shortcut;
37
38/**
39 * Splits a way into multiple ways (all identical except for their node list).
40 *
41 * Ways are just split at the selected nodes. The nodes remain in their
42 * original order. Selected nodes at the end of a way are ignored.
43 */
44
45public class SplitWayAction extends JosmAction {
46
47 /**
48 * Represents the result of a {@link SplitWayAction}
49 * @see SplitWayAction#splitWay
50 * @see SplitWayAction#split
51 */
52 public static class SplitWayResult {
53 private final Command command;
54 private final List<? extends PrimitiveId> newSelection;
55 private Way originalWay;
56 private List<Way> newWays;
57
58 /**
59 * @param command The command to be performed to split the way (which is saved for later retrieval by the {@link #getCommand} method)
60 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval by the {@link #getNewSelection} method)
61 * @param originalWay The original way being split (which is saved for later retrieval by the {@link #getOriginalWay} method)
62 * @param newWays The resulting new ways (which is saved for later retrieval by the {@link #getOriginalWay} method)
63 */
64 public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
65 this.command = command;
66 this.newSelection = newSelection;
67 this.originalWay = originalWay;
68 this.newWays = newWays;
69 }
70
71 /**
72 * Replies the command to be performed to split the way
73 * @return The command to be performed to split the way
74 */
75 public Command getCommand() {
76 return command;
77 }
78
79 /**
80 * Replies the new list of selected primitives ids
81 * @return The new list of selected primitives ids
82 */
83 public List<? extends PrimitiveId> getNewSelection() {
84 return newSelection;
85 }
86
87 /**
88 * Replies the original way being split
89 * @return The original way being split
90 */
91 public Way getOriginalWay() {
92 return originalWay;
93 }
94
95 /**
96 * Replies the resulting new ways
97 * @return The resulting new ways
98 */
99 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}
Note: See TracBrowser for help on using the repository browser.