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

Last change on this file since 12942 was 12901, checked in by Don-vip, 7 years ago

SonarQube - squid:S1612 - Lambdas should be replaced with method references

  • Property svn:eol-style set to native
File size: 25.0 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.Component;
9import java.awt.GridBagLayout;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.Iterator;
16import java.util.List;
17import java.util.concurrent.atomic.AtomicInteger;
18
19import javax.swing.DefaultListCellRenderer;
20import javax.swing.JLabel;
21import javax.swing.JList;
22import javax.swing.JOptionPane;
23import javax.swing.JPanel;
24import javax.swing.ListSelectionModel;
25
26import org.openstreetmap.josm.Main;
27import org.openstreetmap.josm.command.Command;
28import org.openstreetmap.josm.command.SplitWayCommand;
29import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
30import org.openstreetmap.josm.data.osm.Node;
31import org.openstreetmap.josm.data.osm.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.PrimitiveId;
33import org.openstreetmap.josm.data.osm.Relation;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.data.osm.WaySegment;
36import org.openstreetmap.josm.gui.ExtendedDialog;
37import org.openstreetmap.josm.gui.MainApplication;
38import org.openstreetmap.josm.gui.MapFrame;
39import org.openstreetmap.josm.gui.Notification;
40import org.openstreetmap.josm.gui.layer.OsmDataLayer;
41import org.openstreetmap.josm.tools.GBC;
42import org.openstreetmap.josm.tools.Shortcut;
43
44/**
45 * Splits a way into multiple ways (all identical except for their node list).
46 *
47 * Ways are just split at the selected nodes. The nodes remain in their
48 * original order. Selected nodes at the end of a way are ignored.
49 */
50public class SplitWayAction extends JosmAction {
51
52 /**
53 * Represents the result of a {@link SplitWayAction}
54 * @see SplitWayAction#splitWay
55 * @see SplitWayAction#split
56 * @deprecated To be removed end of 2017. Use {@link SplitWayCommand} instead
57 */
58 @Deprecated
59 public static class SplitWayResult {
60 private final Command command;
61 private final List<? extends PrimitiveId> newSelection;
62 private final Way originalWay;
63 private final List<Way> newWays;
64
65 /**
66 * @param command The command to be performed to split the way (which is saved for later retrieval with {@link #getCommand})
67 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection})
68 * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay})
69 * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getOriginalWay})
70 */
71 public SplitWayResult(Command command, List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
72 this.command = command;
73 this.newSelection = newSelection;
74 this.originalWay = originalWay;
75 this.newWays = newWays;
76 }
77
78 /**
79 * @param command The command to be performed to split the way (which is saved for later retrieval with {@link #getCommand})
80 * @since 12828
81 */
82 protected SplitWayResult(SplitWayCommand command) {
83 this.command = command;
84 this.newSelection = command.getNewSelection();
85 this.originalWay = command.getOriginalWay();
86 this.newWays = command.getNewWays();
87 }
88
89 /**
90 * Replies the command to be performed to split the way
91 * @return The command to be performed to split the way
92 */
93 public Command getCommand() {
94 return command;
95 }
96
97 /**
98 * Replies the new list of selected primitives ids
99 * @return The new list of selected primitives ids
100 */
101 public List<? extends PrimitiveId> getNewSelection() {
102 return newSelection;
103 }
104
105 /**
106 * Replies the original way being split
107 * @return The original way being split
108 */
109 public Way getOriginalWay() {
110 return originalWay;
111 }
112
113 /**
114 * Replies the resulting new ways
115 * @return The resulting new ways
116 */
117 public List<Way> getNewWays() {
118 return newWays;
119 }
120 }
121
122 /**
123 * Create a new SplitWayAction.
124 */
125 public SplitWayAction() {
126 super(tr("Split Way"), "splitway", tr("Split a way at the selected node."),
127 Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true);
128 putValue("help", ht("/Action/SplitWay"));
129 }
130
131 /**
132 * Called when the action is executed.
133 *
134 * This method performs an expensive check whether the selection clearly defines one
135 * of the split actions outlined above, and if yes, calls the splitWay method.
136 */
137 @Override
138 public void actionPerformed(ActionEvent e) {
139
140 if (SegmentToKeepSelectionDialog.DISPLAY_COUNT.get() > 0) {
141 new Notification(tr("Cannot split since another split operation is already in progress"))
142 .setIcon(JOptionPane.WARNING_MESSAGE).show();
143 return;
144 }
145
146 Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
147
148 List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
149 List<Way> selectedWays = OsmPrimitive.getFilteredList(selection, Way.class);
150 List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
151
152 if (applicableWays == null) {
153 new Notification(
154 tr("The current selection cannot be used for splitting - no node is selected."))
155 .setIcon(JOptionPane.WARNING_MESSAGE)
156 .show();
157 return;
158 } else if (applicableWays.isEmpty()) {
159 new Notification(
160 tr("The selected nodes do not share the same way."))
161 .setIcon(JOptionPane.WARNING_MESSAGE)
162 .show();
163 return;
164 }
165
166 // If several ways have been found, remove ways that doesn't have selected
167 // node in the middle
168 if (applicableWays.size() > 1) {
169 for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) {
170 Way w = it.next();
171 for (Node n : selectedNodes) {
172 if (!w.isInnerNode(n)) {
173 it.remove();
174 break;
175 }
176 }
177 }
178 }
179
180 if (applicableWays.isEmpty()) {
181 new Notification(
182 trn("The selected node is not in the middle of any way.",
183 "The selected nodes are not in the middle of any way.",
184 selectedNodes.size()))
185 .setIcon(JOptionPane.WARNING_MESSAGE)
186 .show();
187 return;
188 } else if (applicableWays.size() > 1) {
189 new Notification(
190 trn("There is more than one way using the node you selected. Please select the way also.",
191 "There is more than one way using the nodes you selected. Please select the way also.",
192 selectedNodes.size()))
193 .setIcon(JOptionPane.WARNING_MESSAGE)
194 .show();
195 return;
196 }
197
198 // Finally, applicableWays contains only one perfect way
199 final Way selectedWay = applicableWays.get(0);
200 final List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(selectedWay, selectedNodes);
201 if (wayChunks != null) {
202 List<Relation> selectedRelations = OsmPrimitive.getFilteredList(selection, Relation.class);
203 final List<OsmPrimitive> sel = new ArrayList<>(selectedWays.size() + selectedRelations.size());
204 sel.addAll(selectedWays);
205 sel.addAll(selectedRelations);
206
207 final List<Way> newWays = createNewWaysFromChunks(selectedWay, wayChunks);
208 final Way wayToKeep = SplitWayCommand.Strategy.keepLongestChunk().determineWayToKeep(newWays);
209
210 if (ExpertToggleAction.isExpert() && !selectedWay.isNew()) {
211 final ExtendedDialog dialog = new SegmentToKeepSelectionDialog(selectedWay, newWays, wayToKeep, sel);
212 dialog.toggleEnable("way.split.segment-selection-dialog");
213 if (!dialog.toggleCheckState()) {
214 dialog.setModal(false);
215 dialog.showDialog();
216 return; // splitting is performed in SegmentToKeepSelectionDialog.buttonAction()
217 }
218 }
219 if (wayToKeep != null) {
220 doSplitWay(selectedWay, wayToKeep, newWays, sel);
221 }
222 }
223 }
224
225 /**
226 * A dialog to query which way segment should reuse the history of the way to split.
227 */
228 static class SegmentToKeepSelectionDialog extends ExtendedDialog {
229 static final AtomicInteger DISPLAY_COUNT = new AtomicInteger();
230 final transient Way selectedWay;
231 final transient List<Way> newWays;
232 final JList<Way> list;
233 final transient List<OsmPrimitive> selection;
234 final transient Way wayToKeep;
235
236 SegmentToKeepSelectionDialog(Way selectedWay, List<Way> newWays, Way wayToKeep, List<OsmPrimitive> selection) {
237 super(Main.parent, tr("Which way segment should reuse the history of {0}?", selectedWay.getId()),
238 new String[]{tr("Ok"), tr("Cancel")}, true);
239
240 this.selectedWay = selectedWay;
241 this.newWays = newWays;
242 this.selection = selection;
243 this.wayToKeep = wayToKeep;
244 this.list = new JList<>(newWays.toArray(new Way[newWays.size()]));
245 configureList();
246
247 setButtonIcons("ok", "cancel");
248 final JPanel pane = new JPanel(new GridBagLayout());
249 pane.add(new JLabel(getTitle()), GBC.eol().fill(GBC.HORIZONTAL));
250 pane.add(list, GBC.eop().fill(GBC.HORIZONTAL));
251 setContent(pane);
252 setDefaultCloseOperation(HIDE_ON_CLOSE);
253 }
254
255 private void configureList() {
256 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
257 list.addListSelectionListener(e -> {
258 final Way selected = list.getSelectedValue();
259 if (selected != null && MainApplication.isDisplayingMapView() && selected.getNodesCount() > 1) {
260 final Collection<WaySegment> segments = new ArrayList<>(selected.getNodesCount() - 1);
261 final Iterator<Node> it = selected.getNodes().iterator();
262 Node previousNode = it.next();
263 while (it.hasNext()) {
264 final Node node = it.next();
265 segments.add(WaySegment.forNodePair(selectedWay, previousNode, node));
266 previousNode = node;
267 }
268 setHighlightedWaySegments(segments);
269 }
270 });
271 list.setCellRenderer(new SegmentListCellRenderer());
272 }
273
274 protected void setHighlightedWaySegments(Collection<WaySegment> segments) {
275 selectedWay.getDataSet().setHighlightedWaySegments(segments);
276 MainApplication.getMap().mapView.repaint();
277 }
278
279 @Override
280 public void setVisible(boolean visible) {
281 super.setVisible(visible);
282 if (visible) {
283 DISPLAY_COUNT.incrementAndGet();
284 list.setSelectedValue(wayToKeep, true);
285 } else {
286 setHighlightedWaySegments(Collections.<WaySegment>emptyList());
287 DISPLAY_COUNT.decrementAndGet();
288 }
289 }
290
291 @Override
292 protected void buttonAction(int buttonIndex, ActionEvent evt) {
293 super.buttonAction(buttonIndex, evt);
294 toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog
295 if (getValue() == 1) {
296 doSplitWay(selectedWay, list.getSelectedValue(), newWays, selection);
297 }
298 }
299 }
300
301 static class SegmentListCellRenderer extends DefaultListCellRenderer {
302 @Override
303 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
304 final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
305 final String name = DefaultNameFormatter.getInstance().format((Way) value);
306 // get rid of id from DefaultNameFormatter.decorateNameWithId()
307 final String nameWithoutId = name
308 .replace(tr(" [id: {0}]", ((Way) value).getId()), "")
309 .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), "");
310 ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId));
311 return c;
312 }
313 }
314
315 /**
316 * Determines which way chunk should reuse the old id and its history
317 *
318 * @since 8954
319 * @since 10599 (functional interface)
320 * @deprecated to be removed end of 2017. Use {@link org.openstreetmap.josm.command.SplitWayCommand.Strategy} instead
321 */
322 @Deprecated
323 @FunctionalInterface
324 public interface Strategy {
325
326 /**
327 * Determines which way chunk should reuse the old id and its history.
328 *
329 * @param wayChunks the way chunks
330 * @return the way to keep
331 */
332 Way determineWayToKeep(Iterable<Way> wayChunks);
333
334 /**
335 * Returns a strategy which selects the way chunk with the highest node count to keep.
336 * @return strategy which selects the way chunk with the highest node count to keep
337 */
338 static Strategy keepLongestChunk() {
339 return SplitWayCommand.Strategy.keepLongestChunk()::determineWayToKeep;
340 }
341
342 /**
343 * Returns a strategy which selects the first way chunk.
344 * @return strategy which selects the first way chunk
345 */
346 static Strategy keepFirstChunk() {
347 return SplitWayCommand.Strategy.keepFirstChunk()::determineWayToKeep;
348 }
349 }
350
351 /**
352 * Determine which ways to split.
353 * @param selectedWays List of user selected ways.
354 * @param selectedNodes List of user selected nodes.
355 * @return List of ways to split
356 */
357 static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
358 if (selectedNodes.isEmpty())
359 return null;
360
361 // Special case - one of the selected ways touches (not cross) way that we want to split
362 if (selectedNodes.size() == 1) {
363 Node n = selectedNodes.get(0);
364 List<Way> referredWays = n.getParentWays();
365 Way inTheMiddle = null;
366 for (Way w: referredWays) {
367 // Need to look at all nodes see #11184 for a case where node n is
368 // firstNode, lastNode and also in the middle
369 if (selectedWays.contains(w) && w.isInnerNode(n)) {
370 if (inTheMiddle == null) {
371 inTheMiddle = w;
372 } else {
373 inTheMiddle = null;
374 break;
375 }
376 }
377 }
378 if (inTheMiddle != null)
379 return Collections.singletonList(inTheMiddle);
380 }
381
382 // List of ways shared by all nodes
383 return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes);
384 }
385
386 /**
387 * Splits the nodes of {@code wayToSplit} into a list of node sequences
388 * which are separated at the nodes in {@code splitPoints}.
389 *
390 * This method displays warning messages if {@code wayToSplit} and/or
391 * {@code splitPoints} aren't consistent.
392 *
393 * Returns null, if building the split chunks fails.
394 *
395 * @param wayToSplit the way to split. Must not be null.
396 * @param splitPoints the nodes where the way is split. Must not be null.
397 * @return the list of chunks
398 * @deprecated To be removed end of 2017. Use {@link SplitWayCommand#buildSplitChunks} instead
399 */
400 @Deprecated
401 public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) {
402 return SplitWayCommand.buildSplitChunks(wayToSplit, splitPoints);
403 }
404
405 /**
406 * Creates new way objects for the way chunks and transfers the keys from the original way.
407 * @param way the original way whose keys are transferred
408 * @param wayChunks the way chunks
409 * @return the new way objects
410 * @deprecated To be removed end of 2017. Use {@link SplitWayCommand#createNewWaysFromChunks} instead
411 */
412 @Deprecated
413 protected static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) {
414 return SplitWayCommand.createNewWaysFromChunks(way, wayChunks);
415 }
416
417 /**
418 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
419 * the result of this process in an instance of {@link SplitWayResult}.
420 *
421 * Note that changes are not applied to the data yet. You have to
422 * submit the command in {@link SplitWayResult#getCommand()} first,
423 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
424 *
425 * @param layer the layer which the way belongs to.
426 * @param way the way to split. Must not be null.
427 * @param wayChunks the list of way chunks into the way is split. Must not be null.
428 * @param selection The list of currently selected primitives
429 * @return the result from the split operation
430 * @deprecated to be removed end of 2017. Use {@link #splitWay(Way, List, Collection)} instead
431 */
432 @Deprecated
433 public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks,
434 Collection<? extends OsmPrimitive> selection) {
435 return splitWay(way, wayChunks, selection);
436 }
437
438 /**
439 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
440 * the result of this process in an instance of {@link SplitWayResult}.
441 *
442 * Note that changes are not applied to the data yet. You have to
443 * submit the command in {@link SplitWayResult#getCommand()} first,
444 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
445 *
446 * @param way the way to split. Must not be null.
447 * @param wayChunks the list of way chunks into the way is split. Must not be null.
448 * @param selection The list of currently selected primitives
449 * @return the result from the split operation
450 * @since 12718
451 * @deprecated to be removed end of 2017. Use {@link #splitWay(Way, List, Collection)} instead
452 */
453 @Deprecated
454 public static SplitWayResult splitWay(Way way, List<List<Node>> wayChunks,
455 Collection<? extends OsmPrimitive> selection) {
456 return splitWay(way, wayChunks, selection, Strategy.keepLongestChunk());
457 }
458
459 /**
460 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
461 * the result of this process in an instance of {@link SplitWayResult}.
462 * The {@link org.openstreetmap.josm.actions.SplitWayAction.Strategy} is used to determine which
463 * way chunk should reuse the old id and its history.
464 *
465 * Note that changes are not applied to the data yet. You have to
466 * submit the command in {@link SplitWayResult#getCommand()} first,
467 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
468 *
469 * @param layer the layer which the way belongs to.
470 * @param way the way to split. Must not be null.
471 * @param wayChunks the list of way chunks into the way is split. Must not be null.
472 * @param selection The list of currently selected primitives
473 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
474 * @return the result from the split operation
475 * @since 8954
476 * @deprecated to be removed end of 2017. Use {@link #splitWay(Way, List, Collection, Strategy)} instead
477 */
478 @Deprecated
479 public static SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks,
480 Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) {
481 return splitWay(way, wayChunks, selection, splitStrategy);
482 }
483
484 /**
485 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
486 * the result of this process in an instance of {@link SplitWayResult}.
487 * The {@link org.openstreetmap.josm.actions.SplitWayAction.Strategy} is used to determine which
488 * way chunk should reuse the old id and its history.
489 *
490 * Note that changes are not applied to the data yet. You have to
491 * submit the command in {@link SplitWayResult#getCommand()} first,
492 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
493 *
494 * @param way the way to split. Must not be null.
495 * @param wayChunks the list of way chunks into the way is split. Must not be null.
496 * @param selection The list of currently selected primitives
497 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
498 * @return the result from the split operation
499 * @since 12718
500 * @deprecated to be removed end of 2017. Use {@link SplitWayCommand#splitWay} instead
501 */
502 @Deprecated
503 public static SplitWayResult splitWay(Way way, List<List<Node>> wayChunks,
504 Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) {
505 SplitWayCommand cmd = SplitWayCommand.splitWay(way, wayChunks, selection, splitStrategy::determineWayToKeep);
506 return cmd != null ? new SplitWayResult(cmd) : null;
507 }
508
509 static void doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) {
510 final MapFrame map = MainApplication.getMap();
511 final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw;
512 final SplitWayCommand result = SplitWayCommand.doSplitWay(way, wayToKeep, newWays, !isMapModeDraw ? newSelection : null);
513 MainApplication.undoRedo.add(result);
514 List<? extends PrimitiveId> newSel = result.getNewSelection();
515 if (newSel != null && !newSel.isEmpty()) {
516 MainApplication.getLayerManager().getEditDataSet().setSelected(newSel);
517 }
518 }
519
520 /**
521 * Splits the way {@code way} at the nodes in {@code atNodes} and replies
522 * the result of this process in an instance of {@link SplitWayResult}.
523 *
524 * Note that changes are not applied to the data yet. You have to
525 * submit the command in {@link SplitWayResult#getCommand()} first,
526 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
527 *
528 * Replies null if the way couldn't be split at the given nodes.
529 *
530 * @param layer the layer which the way belongs to.
531 * @param way the way to split. Must not be null.
532 * @param atNodes the list of nodes where the way is split. Must not be null.
533 * @param selection The list of currently selected primitives
534 * @return the result from the split operation
535 * @deprecated to be removed end of 2017. Use {@link #split(Way, List, Collection)} instead
536 */
537 @Deprecated
538 public static SplitWayResult split(OsmDataLayer layer, Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
539 return split(way, atNodes, selection);
540 }
541
542 /**
543 * Splits the way {@code way} at the nodes in {@code atNodes} and replies
544 * the result of this process in an instance of {@link SplitWayResult}.
545 *
546 * Note that changes are not applied to the data yet. You have to
547 * submit the command in {@link SplitWayResult#getCommand()} first,
548 * i.e. {@code Main.main.undoredo.add(result.getCommand())}.
549 *
550 * Replies null if the way couldn't be split at the given nodes.
551 *
552 * @param way the way to split. Must not be null.
553 * @param atNodes the list of nodes where the way is split. Must not be null.
554 * @param selection The list of currently selected primitives
555 * @return the result from the split operation
556 * @since 12718
557 * @deprecated to be removed end of 2017. Use {@link #splitWay(Way, List, Collection)} instead
558 */
559 @Deprecated
560 public static SplitWayResult split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
561 List<List<Node>> chunks = buildSplitChunks(way, atNodes);
562 return chunks != null ? splitWay(way, chunks, selection) : null;
563 }
564
565 @Override
566 protected void updateEnabledState() {
567 updateEnabledStateOnCurrentSelection();
568 }
569
570 @Override
571 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
572 if (selection == null) {
573 setEnabled(false);
574 return;
575 }
576 for (OsmPrimitive primitive: selection) {
577 if (primitive instanceof Node) {
578 setEnabled(true); // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
579 return;
580 }
581 }
582 setEnabled(false);
583 }
584}
Note: See TracBrowser for help on using the repository browser.