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

Last change on this file since 6111 was 6084, checked in by bastiK, 11 years ago

see #8902 - add missing @Override annotations (patch by shinigami)

  • 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 @Override
120 public void actionPerformed(ActionEvent e) {
121
122 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
123
124 List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
125 List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
126 List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class);
127 List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
128
129 if (applicableWays == null) {
130 JOptionPane.showMessageDialog(
131 Main.parent,
132 tr("The current selection cannot be used for splitting - no node is selected."),
133 tr("Warning"),
134 JOptionPane.WARNING_MESSAGE);
135 return;
136 } else if (applicableWays.isEmpty()) {
137 JOptionPane.showMessageDialog(Main.parent,
138 tr("The selected nodes do not share the same way."),
139 tr("Warning"),
140 JOptionPane.WARNING_MESSAGE);
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 JOptionPane.showMessageDialog(Main.parent,
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 tr("Warning"),
164 JOptionPane.WARNING_MESSAGE);
165 return;
166 } else if (applicableWays.size() > 1) {
167 JOptionPane.showMessageDialog(Main.parent,
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 tr("Warning"),
172 JOptionPane.WARNING_MESSAGE);
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<OsmPrimitive>(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<Way>(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 static public 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<Node>(splitPoints);
266 List<List<Node>> wayChunks = new LinkedList<List<Node>>();
267 List<Node> currentWayChunk = new ArrayList<Node>();
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<Node>();
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 JOptionPane.showMessageDialog(
293 Main.parent,
294 tr("You must select two or more nodes to split a circular way."),
295 tr("Warning"),
296 JOptionPane.WARNING_MESSAGE);
297 return null;
298 }
299 lastWayChunk.remove(lastWayChunk.size() - 1);
300 lastWayChunk.addAll(wayChunks.get(0));
301 wayChunks.remove(wayChunks.size() - 1);
302 wayChunks.set(0, lastWayChunk);
303 }
304
305 if (wayChunks.size() < 2) {
306 if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
307 JOptionPane.showMessageDialog(
308 Main.parent,
309 tr("You must select two or more nodes to split a circular way."),
310 tr("Warning"),
311 JOptionPane.WARNING_MESSAGE);
312 } else {
313 JOptionPane.showMessageDialog(
314 Main.parent,
315 tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"),
316 tr("Warning"),
317 JOptionPane.WARNING_MESSAGE);
318 }
319 return null;
320 }
321 return wayChunks;
322 }
323
324 /**
325 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
326 * the result of this process in an instance of {@link SplitWayResult}.
327 *
328 * Note that changes are not applied to the data yet. You have to
329 * submit the command in {@link SplitWayResult#getCommand()} first,
330 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
331 *
332 * @param layer the layer which the way belongs to. Must not be null.
333 * @param way the way to split. Must not be null.
334 * @param wayChunks the list of way chunks into the way is split. Must not be null.
335 * @param selection The list of currently selected primitives
336 * @return the result from the split operation
337 */
338 public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
339 // build a list of commands, and also a new selection list
340 Collection<Command> commandList = new ArrayList<Command>(wayChunks.size());
341 List<OsmPrimitive> newSelection = new ArrayList<OsmPrimitive>(selection.size() + wayChunks.size());
342 newSelection.addAll(selection);
343
344 Iterator<List<Node>> chunkIt = wayChunks.iterator();
345 Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn",
346 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
347
348 // First, change the original way
349 Way changedWay = new Way(way);
350 changedWay.setNodes(chunkIt.next());
351 commandList.add(new ChangeCommand(way, changedWay));
352 if (!newSelection.contains(way)) {
353 newSelection.add(way);
354 }
355
356 List<Way> newWays = new ArrayList<Way>();
357 // Second, create new ways
358 while (chunkIt.hasNext()) {
359 Way wayToAdd = new Way();
360 wayToAdd.setKeys(way.getKeys());
361 newWays.add(wayToAdd);
362 wayToAdd.setNodes(chunkIt.next());
363 commandList.add(new AddCommand(layer,wayToAdd));
364 newSelection.add(wayToAdd);
365
366 }
367 boolean warnmerole = false;
368 boolean warnme = false;
369 // now copy all relations to new way also
370
371 for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
372 if (!r.isUsable()) {
373 continue;
374 }
375 Relation c = null;
376 String type = r.get("type");
377 if (type == null) {
378 type = "";
379 }
380
381 int i_c = 0, i_r = 0;
382 List<RelationMember> relationMembers = r.getMembers();
383 for (RelationMember rm: relationMembers) {
384 if (rm.isWay() && rm.getMember() == way) {
385 boolean insert = true;
386 if ("restriction".equals(type))
387 {
388 /* this code assumes the restriction is correct. No real error checking done */
389 String role = rm.getRole();
390 if("from".equals(role) || "to".equals(role))
391 {
392 OsmPrimitive via = null;
393 for (RelationMember rmv : r.getMembers()) {
394 if("via".equals(rmv.getRole())){
395 via = rmv.getMember();
396 }
397 }
398 List<Node> nodes = new ArrayList<Node>();
399 if(via != null) {
400 if(via instanceof Node) {
401 nodes.add((Node)via);
402 } else if(via instanceof Way) {
403 nodes.add(((Way)via).lastNode());
404 nodes.add(((Way)via).firstNode());
405 }
406 }
407 Way res = null;
408 for(Node n : nodes) {
409 if(changedWay.isFirstLastNode(n)) {
410 res = way;
411 }
412 }
413 if(res == null)
414 {
415 for (Way wayToAdd : newWays) {
416 for(Node n : nodes) {
417 if(wayToAdd.isFirstLastNode(n)) {
418 res = wayToAdd;
419 }
420 }
421 }
422 if(res != null)
423 {
424 if (c == null) {
425 c = new Relation(r);
426 }
427 c.addMember(new RelationMember(role, res));
428 c.removeMembersFor(way);
429 insert = false;
430 }
431 } else {
432 insert = false;
433 }
434 }
435 else if(!"via".equals(role)) {
436 warnme = true;
437 }
438 }
439 else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
440 warnme = true;
441 }
442 if (c == null) {
443 c = new Relation(r);
444 }
445
446 if(insert)
447 {
448 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
449 warnmerole = true;
450 }
451
452 Boolean backwards = null;
453 int k = 1;
454 while (i_r - k >= 0 || i_r + k < relationMembers.size()) {
455 if ((i_r - k >= 0) && relationMembers.get(i_r - k).isWay()){
456 Way w = relationMembers.get(i_r - k).getWay();
457 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
458 backwards = false;
459 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
460 backwards = true;
461 }
462 break;
463 }
464 if ((i_r + k < relationMembers.size()) && relationMembers.get(i_r + k).isWay()){
465 Way w = relationMembers.get(i_r + k).getWay();
466 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
467 backwards = true;
468 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
469 backwards = false;
470 }
471 break;
472 }
473 k++;
474 }
475
476 int j = i_c;
477 for (Way wayToAdd : newWays) {
478 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
479 j++;
480 if ((backwards != null) && backwards) {
481 c.addMember(i_c, em);
482 } else {
483 c.addMember(j, em);
484 }
485 }
486 i_c = j;
487 }
488 }
489 i_c++; i_r++;
490 }
491
492 if (c != null) {
493 commandList.add(new ChangeCommand(layer,r, c));
494 }
495 }
496 if (warnmerole) {
497 JOptionPane.showMessageDialog(
498 Main.parent,
499 tr("<html>A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"),
500 tr("Warning"),
501 JOptionPane.WARNING_MESSAGE);
502 } else if (warnme) {
503 JOptionPane.showMessageDialog(
504 Main.parent,
505 tr("<html>A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"),
506 tr("Warning"),
507 JOptionPane.WARNING_MESSAGE);
508 }
509
510 return new SplitWayResult(
511 new SequenceCommand(
512 tr("Split way {0} into {1} parts", way.getDisplayName(DefaultNameFormatter.getInstance()),wayChunks.size()),
513 commandList
514 ),
515 newSelection,
516 way,
517 newWays
518 );
519 }
520
521 /**
522 * Splits the way {@code way} at the nodes in {@code atNodes} and replies
523 * the result of this process in an instance of {@link SplitWayResult}.
524 *
525 * Note that changes are not applied to the data yet. You have to
526 * submit the command in {@link SplitWayResult#getCommand()} first,
527 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
528 *
529 * Replies null if the way couldn't be split at the given nodes.
530 *
531 * @param layer the layer which the way belongs to. Must not be null.
532 * @param way the way to split. Must not be null.
533 * @param atNodes the list of nodes where the way is split. Must not be null.
534 * @param selection The list of currently selected primitives
535 * @return the result from the split operation
536 */
537 static public SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
538 List<List<Node>> chunks = buildSplitChunks(way, atNodes);
539 if (chunks == null) return null;
540 return splitWay(layer,way, chunks, selection);
541 }
542
543 @Override
544 protected void updateEnabledState() {
545 if (getCurrentDataSet() == null) {
546 setEnabled(false);
547 } else {
548 updateEnabledState(getCurrentDataSet().getSelected());
549 }
550 }
551
552 @Override
553 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
554 if (selection == null) {
555 setEnabled(false);
556 return;
557 }
558 for (OsmPrimitive primitive: selection) {
559 if (primitive instanceof Node) {
560 setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
561 return;
562 }
563 }
564 setEnabled(false);
565 }
566}
Note: See TracBrowser for help on using the repository browser.