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

Last change on this file since 2683 was 2628, checked in by stoecker, 14 years ago

close #2086 - handle restriction relation for way splitting

  • Property svn:eol-style set to native
File size: 18.0 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 for (RelationMember rm : r.getMembers()) {
334 if (rm.isWay() && rm.getMember() == way) {
335 boolean insert = true;
336 if ("restriction".equals(type))
337 {
338 /* this code assumes the restriction is correct. No real error checking done */
339 String role = rm.getRole();
340 if("from".equals(role) || "to".equals(role))
341 {
342 OsmPrimitive via = null;
343 for (RelationMember rmv : r.getMembers()) {
344 if("via".equals(rmv.getRole())){
345 via = rmv.getMember();
346 }
347 }
348 List<Node> nodes = new ArrayList<Node>();
349 if(via != null) {
350 if(via instanceof Node) {
351 nodes.add((Node)via);
352 } else if(via instanceof Way) {
353 nodes.add(((Way)via).lastNode());
354 nodes.add(((Way)via).firstNode());
355 }
356 }
357 Way res = null;
358 for(Node n : nodes) {
359 if(changedWay.isFirstLastNode(n)) {
360 res = way;
361 }
362 }
363 if(res == null)
364 {
365 for (Way wayToAdd : newWays) {
366 for(Node n : nodes) {
367 if(wayToAdd.isFirstLastNode(n)) {
368 res = wayToAdd;
369 }
370 }
371 }
372 if(res != null)
373 {
374 if (c == null) {
375 c = new Relation(r);
376 }
377 c.addMember(new RelationMember(role, res));
378 c.removeMembersFor(way);
379 insert = false;
380 }
381 }
382 else
383 insert = false;
384 }
385 else if(!"via".equals(role))
386 warnme = true;
387 }
388 else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
389 warnme = true;
390 }
391 if (c == null) {
392 c = new Relation(r);
393 }
394
395 if(insert)
396 {
397 int j = i;
398 boolean backwards = "backward".equals(rm.getRole());
399 for (Way wayToAdd : newWays) {
400 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
401 if (em.hasRole() && !("multipolygon".equals(type))) {
402 warnmerole = true;
403 }
404
405 j++;
406 if (backwards) {
407 c.addMember(i, em);
408 } else {
409 c.addMember(j, em);
410 }
411 }
412 i = j;
413 }
414 }
415 i++;
416 }
417
418 if (c != null) {
419 commandList.add(new ChangeCommand(r, c));
420 }
421 }
422 if (warnmerole) {
423 JOptionPane.showMessageDialog(
424 Main.parent,
425 tr("<html>A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"),
426 tr("Warning"),
427 JOptionPane.WARNING_MESSAGE);
428 } else if (warnme) {
429 JOptionPane.showMessageDialog(
430 Main.parent,
431 tr("<html>A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"),
432 tr("Warning"),
433 JOptionPane.WARNING_MESSAGE);
434 }
435
436
437 return new SplitWayResult(new SequenceCommand(tr("Split way {0} into {1} parts",
438 way.getDisplayName(DefaultNameFormatter.getInstance()),
439 wayChunks.size()),
440 commandList), newSelection);
441 }
442
443 @Override
444 protected void updateEnabledState() {
445 if (getCurrentDataSet() == null) {
446 setEnabled(false);
447 } else {
448 updateEnabledState(getCurrentDataSet().getSelected());
449 }
450 }
451
452 @Override
453 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
454 if (selection == null) {
455 setEnabled(false);
456 return;
457 }
458 setEnabled(checkSelection(selection));
459 }
460}
Note: See TracBrowser for help on using the repository browser.