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

Last change on this file since 13158 was 13158, checked in by Don-vip, 6 years ago

javadoc

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