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

Last change on this file since 2876 was 2792, checked in by jttt, 14 years ago

#4323 splitting way freezes JOSM completely

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