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

Last change on this file since 3965 was 3515, checked in by stoecker, 14 years ago

fixed #5431 - splitting way fails when outer-node is also inner node

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