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

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

Fix NPE in case SplitWayAction show message dialog

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