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

Last change on this file since 8068 was 7534, checked in by Don-vip, 10 years ago

see #10511 - add unit test

  • Property svn:eol-style set to native
File size: 23.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
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.Notification;
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 * Represents the result of a {@link SplitWayAction}
50 * @see SplitWayAction#splitWay
51 * @see SplitWayAction#split
52 */
53 public static class SplitWayResult {
54 private final Command command;
55 private final List<? extends PrimitiveId> newSelection;
56 private Way originalWay;
57 private List<Way> newWays;
58
59 /**
60 * @param command The command to be performed to split the way (which is saved for later retrieval by the {@link #getCommand} method)
61 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval by the {@link #getNewSelection} method)
62 * @param originalWay The original way being split (which is saved for later retrieval by the {@link #getOriginalWay} method)
63 * @param newWays The resulting new ways (which is saved for later retrieval by the {@link #getOriginalWay} method)
64 */
65 public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
66 this.command = command;
67 this.newSelection = newSelection;
68 this.originalWay = originalWay;
69 this.newWays = newWays;
70 }
71
72 /**
73 * Replies the command to be performed to split the way
74 * @return The command to be performed to split the way
75 */
76 public Command getCommand() {
77 return command;
78 }
79
80 /**
81 * Replies the new list of selected primitives ids
82 * @return The new list of selected primitives ids
83 */
84 public List<? extends PrimitiveId> getNewSelection() {
85 return newSelection;
86 }
87
88 /**
89 * Replies the original way being split
90 * @return The original way being split
91 */
92 public Way getOriginalWay() {
93 return originalWay;
94 }
95
96 /**
97 * Replies the resulting new ways
98 * @return The resulting new ways
99 */
100 public List<Way> getNewWays() {
101 return newWays;
102 }
103 }
104
105 /**
106 * Create a new SplitWayAction.
107 */
108 public SplitWayAction() {
109 super(tr("Split Way"), "splitway", tr("Split a way at the selected node."),
110 Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true);
111 putValue("help", ht("/Action/SplitWay"));
112 }
113
114 /**
115 * Called when the action is executed.
116 *
117 * This method performs an expensive check whether the selection clearly defines one
118 * of the split actions outlined above, and if yes, calls the splitWay method.
119 */
120 @Override
121 public void actionPerformed(ActionEvent e) {
122
123 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
124
125 List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
126 List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
127 List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class);
128 List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
129
130 if (applicableWays == null) {
131 new Notification(
132 tr("The current selection cannot be used for splitting - no node is selected."))
133 .setIcon(JOptionPane.WARNING_MESSAGE)
134 .show();
135 return;
136 } else if (applicableWays.isEmpty()) {
137 new Notification(
138 tr("The selected nodes do not share the same way."))
139 .setIcon(JOptionPane.WARNING_MESSAGE)
140 .show();
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 new Notification(
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 .setIcon(JOptionPane.WARNING_MESSAGE)
164 .show();
165 return;
166 } else if (applicableWays.size() > 1) {
167 new Notification(
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 .setIcon(JOptionPane.WARNING_MESSAGE)
172 .show();
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<>(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<>(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 public static 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<>(splitPoints);
266 List<List<Node>> wayChunks = new LinkedList<>();
267 List<Node> currentWayChunk = new ArrayList<>();
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<>();
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 new Notification(
293 tr("You must select two or more nodes to split a circular way."))
294 .setIcon(JOptionPane.WARNING_MESSAGE)
295 .show();
296 return null;
297 }
298 lastWayChunk.remove(lastWayChunk.size() - 1);
299 lastWayChunk.addAll(wayChunks.get(0));
300 wayChunks.remove(wayChunks.size() - 1);
301 wayChunks.set(0, lastWayChunk);
302 }
303
304 if (wayChunks.size() < 2) {
305 if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
306 new Notification(
307 tr("You must select two or more nodes to split a circular way."))
308 .setIcon(JOptionPane.WARNING_MESSAGE)
309 .show();
310 } else {
311 new Notification(
312 tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"))
313 .setIcon(JOptionPane.WARNING_MESSAGE)
314 .show();
315 }
316 return null;
317 }
318 return wayChunks;
319 }
320
321 /**
322 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
323 * the result of this process in an instance of {@link SplitWayResult}.
324 *
325 * Note that changes are not applied to the data yet. You have to
326 * submit the command in {@link SplitWayResult#getCommand()} first,
327 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
328 *
329 * @param layer the layer which the way belongs to. Must not be null.
330 * @param way the way to split. Must not be null.
331 * @param wayChunks the list of way chunks into the way is split. Must not be null.
332 * @param selection The list of currently selected primitives
333 * @return the result from the split operation
334 */
335 public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
336 // build a list of commands, and also a new selection list
337 Collection<Command> commandList = new ArrayList<>(wayChunks.size());
338 List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
339 newSelection.addAll(selection);
340
341 Iterator<List<Node>> chunkIt = wayChunks.iterator();
342 Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn",
343 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
344
345 // First, change the original way
346 Way changedWay = new Way(way);
347 changedWay.setNodes(chunkIt.next());
348 commandList.add(new ChangeCommand(way, changedWay));
349 if (!newSelection.contains(way)) {
350 newSelection.add(way);
351 }
352
353 List<Way> newWays = new ArrayList<>();
354 // Second, create new ways
355 while (chunkIt.hasNext()) {
356 Way wayToAdd = new Way();
357 wayToAdd.setKeys(way.getKeys());
358 newWays.add(wayToAdd);
359 wayToAdd.setNodes(chunkIt.next());
360 commandList.add(new AddCommand(layer, wayToAdd));
361 newSelection.add(wayToAdd);
362 }
363 boolean warnmerole = false;
364 boolean warnme = false;
365 // now copy all relations to new way also
366
367 for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
368 if (!r.isUsable()) {
369 continue;
370 }
371 Relation c = null;
372 String type = r.get("type");
373 if (type == null) {
374 type = "";
375 }
376
377 int i_c = 0, i_r = 0;
378 List<RelationMember> relationMembers = r.getMembers();
379 for (RelationMember rm: relationMembers) {
380 if (rm.isWay() && rm.getMember() == way) {
381 boolean insert = true;
382 if ("restriction".equals(type)) {
383 /* this code assumes the restriction is correct. No real error checking done */
384 String role = rm.getRole();
385 if("from".equals(role) || "to".equals(role)) {
386 OsmPrimitive via = null;
387 for (RelationMember rmv : r.getMembers()) {
388 if ("via".equals(rmv.getRole())){
389 via = rmv.getMember();
390 }
391 }
392 List<Node> nodes = new ArrayList<>();
393 if (via != null) {
394 if (via instanceof Node) {
395 nodes.add((Node)via);
396 } else if (via instanceof Way) {
397 nodes.add(((Way)via).lastNode());
398 nodes.add(((Way)via).firstNode());
399 }
400 }
401 Way res = null;
402 for (Node n : nodes) {
403 if(changedWay.isFirstLastNode(n)) {
404 res = way;
405 }
406 }
407 if (res == null) {
408 for (Way wayToAdd : newWays) {
409 for(Node n : nodes) {
410 if(wayToAdd.isFirstLastNode(n)) {
411 res = wayToAdd;
412 }
413 }
414 }
415 if (res != null) {
416 if (c == null) {
417 c = new Relation(r);
418 }
419 c.addMember(new RelationMember(role, res));
420 c.removeMembersFor(way);
421 insert = false;
422 }
423 } else {
424 insert = false;
425 }
426 } else if(!"via".equals(role)) {
427 warnme = true;
428 }
429 } else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
430 warnme = true;
431 }
432 if (c == null) {
433 c = new Relation(r);
434 }
435
436 if (insert) {
437 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
438 warnmerole = true;
439 }
440
441 Boolean backwards = null;
442 int k = 1;
443 while (i_r - k >= 0 || i_r + k < relationMembers.size()) {
444 if ((i_r - k >= 0) && relationMembers.get(i_r - k).isWay()){
445 Way w = relationMembers.get(i_r - k).getWay();
446 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
447 backwards = false;
448 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
449 backwards = true;
450 }
451 break;
452 }
453 if ((i_r + k < relationMembers.size()) && relationMembers.get(i_r + k).isWay()){
454 Way w = relationMembers.get(i_r + k).getWay();
455 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
456 backwards = true;
457 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
458 backwards = false;
459 }
460 break;
461 }
462 k++;
463 }
464
465 int j = i_c;
466 for (Way wayToAdd : newWays) {
467 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
468 j++;
469 if ((backwards != null) && backwards) {
470 c.addMember(i_c, em);
471 } else {
472 c.addMember(j, em);
473 }
474 }
475 i_c = j;
476 }
477 }
478 i_c++;
479 i_r++;
480 }
481
482 if (c != null) {
483 commandList.add(new ChangeCommand(layer, r, c));
484 }
485 }
486 if (warnmerole) {
487 new Notification(
488 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
489 .setIcon(JOptionPane.WARNING_MESSAGE)
490 .show();
491 } else if (warnme) {
492 new Notification(
493 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."))
494 .setIcon(JOptionPane.WARNING_MESSAGE)
495 .show();
496 }
497
498 return new SplitWayResult(
499 new SequenceCommand(
500 /* for correct i18n of plural forms - see #9110 */
501 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", wayChunks.size(),
502 way.getDisplayName(DefaultNameFormatter.getInstance()), wayChunks.size()),
503 commandList
504 ),
505 newSelection,
506 way,
507 newWays
508 );
509 }
510
511 /**
512 * Splits the way {@code way} at the nodes in {@code atNodes} and replies
513 * the result of this process in an instance of {@link SplitWayResult}.
514 *
515 * Note that changes are not applied to the data yet. You have to
516 * submit the command in {@link SplitWayResult#getCommand()} first,
517 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
518 *
519 * Replies null if the way couldn't be split at the given nodes.
520 *
521 * @param layer the layer which the way belongs to. Must not be null.
522 * @param way the way to split. Must not be null.
523 * @param atNodes the list of nodes where the way is split. Must not be null.
524 * @param selection The list of currently selected primitives
525 * @return the result from the split operation
526 */
527 public static SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
528 List<List<Node>> chunks = buildSplitChunks(way, atNodes);
529 if (chunks == null) return null;
530 return splitWay(layer,way, chunks, selection);
531 }
532
533 @Override
534 protected void updateEnabledState() {
535 if (getCurrentDataSet() == null) {
536 setEnabled(false);
537 } else {
538 updateEnabledState(getCurrentDataSet().getSelected());
539 }
540 }
541
542 @Override
543 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
544 if (selection == null) {
545 setEnabled(false);
546 return;
547 }
548 for (OsmPrimitive primitive: selection) {
549 if (primitive instanceof Node) {
550 setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
551 return;
552 }
553 }
554 setEnabled(false);
555 }
556}
Note: See TracBrowser for help on using the repository browser.