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

Last change on this file since 15652 was 14654, checked in by simon04, 5 years ago

Deprecate OsmPrimitive.getFilteredList/getFilteredSet in favour of Stream

Most use-cases involved filtering referring primitives. This can now be
accomplished using OsmPrimitive.referrers involving the Stream API and
thus avoids creation of intermediate collections.

  • Property svn:eol-style set to native
File size: 12.7 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.command.SplitWayCommand;
27import org.openstreetmap.josm.data.UndoRedoHandler;
28import org.openstreetmap.josm.data.osm.DataSet;
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.OsmUtils;
33import org.openstreetmap.josm.data.osm.PrimitiveId;
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.tools.GBC;
41import org.openstreetmap.josm.tools.Shortcut;
42
43/**
44 * Splits a way into multiple ways (all identical except for their node list).
45 *
46 * Ways are just split at the selected nodes. The nodes remain in their
47 * original order. Selected nodes at the end of a way are ignored.
48 */
49public class SplitWayAction extends JosmAction {
50
51 /**
52 * Create a new SplitWayAction.
53 */
54 public SplitWayAction() {
55 super(tr("Split Way"), "splitway", tr("Split a way at the selected node."),
56 Shortcut.registerShortcut("tools:splitway", tr("Tool: {0}", tr("Split Way")), KeyEvent.VK_P, Shortcut.DIRECT), true);
57 setHelpId(ht("/Action/SplitWay"));
58 }
59
60 /**
61 * Called when the action is executed.
62 *
63 * This method performs an expensive check whether the selection clearly defines one
64 * of the split actions outlined above, and if yes, calls the splitWay method.
65 */
66 @Override
67 public void actionPerformed(ActionEvent e) {
68 runOn(getLayerManager().getEditDataSet());
69 }
70
71 /**
72 * Run the action on the given dataset.
73 * @param ds dataset
74 * @since 14542
75 */
76 public static void runOn(DataSet ds) {
77
78 if (SegmentToKeepSelectionDialog.DISPLAY_COUNT.get() > 0) {
79 new Notification(tr("Cannot split since another split operation is already in progress"))
80 .setIcon(JOptionPane.WARNING_MESSAGE).show();
81 return;
82 }
83
84 List<Node> selectedNodes = new ArrayList<>(ds.getSelectedNodes());
85 List<Way> selectedWays = new ArrayList<>(ds.getSelectedWays());
86 List<Way> applicableWays = getApplicableWays(selectedWays, selectedNodes);
87
88 if (applicableWays == null) {
89 new Notification(
90 tr("The current selection cannot be used for splitting - no node is selected."))
91 .setIcon(JOptionPane.WARNING_MESSAGE)
92 .show();
93 return;
94 } else if (applicableWays.isEmpty()) {
95 new Notification(
96 tr("The selected nodes do not share the same way."))
97 .setIcon(JOptionPane.WARNING_MESSAGE)
98 .show();
99 return;
100 }
101
102 // If several ways have been found, remove ways that doesn't have selected
103 // node in the middle
104 if (applicableWays.size() > 1) {
105 for (Iterator<Way> it = applicableWays.iterator(); it.hasNext();) {
106 Way w = it.next();
107 for (Node n : selectedNodes) {
108 if (!w.isInnerNode(n)) {
109 it.remove();
110 break;
111 }
112 }
113 }
114 }
115
116 if (applicableWays.isEmpty()) {
117 new Notification(
118 trn("The selected node is not in the middle of any way.",
119 "The selected nodes are not in the middle of any way.",
120 selectedNodes.size()))
121 .setIcon(JOptionPane.WARNING_MESSAGE)
122 .show();
123 return;
124 } else if (applicableWays.size() > 1) {
125 new Notification(
126 trn("There is more than one way using the node you selected. Please select the way also.",
127 "There is more than one way using the nodes you selected. Please select the way also.",
128 selectedNodes.size()))
129 .setIcon(JOptionPane.WARNING_MESSAGE)
130 .show();
131 return;
132 }
133
134 // Finally, applicableWays contains only one perfect way
135 final Way selectedWay = applicableWays.get(0);
136 final List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(selectedWay, selectedNodes);
137 if (wayChunks != null) {
138 final List<OsmPrimitive> sel = new ArrayList<>(ds.getSelectedRelations());
139 sel.addAll(selectedWays);
140
141 final List<Way> newWays = SplitWayCommand.createNewWaysFromChunks(selectedWay, wayChunks);
142 final Way wayToKeep = SplitWayCommand.Strategy.keepLongestChunk().determineWayToKeep(newWays);
143
144 if (ExpertToggleAction.isExpert() && !selectedWay.isNew()) {
145 final ExtendedDialog dialog = new SegmentToKeepSelectionDialog(selectedWay, newWays, wayToKeep, sel);
146 dialog.toggleEnable("way.split.segment-selection-dialog");
147 if (!dialog.toggleCheckState()) {
148 dialog.setModal(false);
149 dialog.showDialog();
150 return; // splitting is performed in SegmentToKeepSelectionDialog.buttonAction()
151 }
152 }
153 if (wayToKeep != null) {
154 doSplitWay(selectedWay, wayToKeep, newWays, sel);
155 }
156 }
157 }
158
159 /**
160 * A dialog to query which way segment should reuse the history of the way to split.
161 */
162 static class SegmentToKeepSelectionDialog extends ExtendedDialog {
163 static final AtomicInteger DISPLAY_COUNT = new AtomicInteger();
164 final transient Way selectedWay;
165 final transient List<Way> newWays;
166 final JList<Way> list;
167 final transient List<OsmPrimitive> selection;
168 final transient Way wayToKeep;
169
170 SegmentToKeepSelectionDialog(Way selectedWay, List<Way> newWays, Way wayToKeep, List<OsmPrimitive> selection) {
171 super(MainApplication.getMainFrame(), tr("Which way segment should reuse the history of {0}?", selectedWay.getId()),
172 new String[]{tr("Ok"), tr("Cancel")}, true);
173
174 this.selectedWay = selectedWay;
175 this.newWays = newWays;
176 this.selection = selection;
177 this.wayToKeep = wayToKeep;
178 this.list = new JList<>(newWays.toArray(new Way[0]));
179 configureList();
180
181 setButtonIcons("ok", "cancel");
182 final JPanel pane = new JPanel(new GridBagLayout());
183 pane.add(new JLabel(getTitle()), GBC.eol().fill(GBC.HORIZONTAL));
184 pane.add(list, GBC.eop().fill(GBC.HORIZONTAL));
185 setContent(pane);
186 setDefaultCloseOperation(HIDE_ON_CLOSE);
187 }
188
189 private void configureList() {
190 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
191 list.addListSelectionListener(e -> {
192 final Way selected = list.getSelectedValue();
193 if (selected != null && MainApplication.isDisplayingMapView() && selected.getNodesCount() > 1) {
194 final Collection<WaySegment> segments = new ArrayList<>(selected.getNodesCount() - 1);
195 final Iterator<Node> it = selected.getNodes().iterator();
196 Node previousNode = it.next();
197 while (it.hasNext()) {
198 final Node node = it.next();
199 segments.add(WaySegment.forNodePair(selectedWay, previousNode, node));
200 previousNode = node;
201 }
202 setHighlightedWaySegments(segments);
203 }
204 });
205 list.setCellRenderer(new SegmentListCellRenderer());
206 }
207
208 protected void setHighlightedWaySegments(Collection<WaySegment> segments) {
209 selectedWay.getDataSet().setHighlightedWaySegments(segments);
210 MainApplication.getMap().mapView.repaint();
211 }
212
213 @Override
214 public void setVisible(boolean visible) {
215 super.setVisible(visible);
216 if (visible) {
217 DISPLAY_COUNT.incrementAndGet();
218 list.setSelectedValue(wayToKeep, true);
219 } else {
220 setHighlightedWaySegments(Collections.<WaySegment>emptyList());
221 DISPLAY_COUNT.decrementAndGet();
222 }
223 }
224
225 @Override
226 protected void buttonAction(int buttonIndex, ActionEvent evt) {
227 super.buttonAction(buttonIndex, evt);
228 toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog
229 if (getValue() == 1) {
230 doSplitWay(selectedWay, list.getSelectedValue(), newWays, selection);
231 }
232 }
233 }
234
235 static class SegmentListCellRenderer extends DefaultListCellRenderer {
236 @Override
237 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
238 final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
239 final String name = DefaultNameFormatter.getInstance().format((Way) value);
240 // get rid of id from DefaultNameFormatter.decorateNameWithId()
241 final String nameWithoutId = name
242 .replace(tr(" [id: {0}]", ((Way) value).getId()), "")
243 .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), "");
244 ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId));
245 return c;
246 }
247 }
248
249 /**
250 * Determine which ways to split.
251 * @param selectedWays List of user selected ways.
252 * @param selectedNodes List of user selected nodes.
253 * @return List of ways to split
254 */
255 static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
256 if (selectedNodes.isEmpty())
257 return null;
258
259 // Special case - one of the selected ways touches (not cross) way that we want to split
260 if (selectedNodes.size() == 1) {
261 Node n = selectedNodes.get(0);
262 List<Way> referredWays = n.getParentWays();
263 Way inTheMiddle = null;
264 for (Way w: referredWays) {
265 // Need to look at all nodes see #11184 for a case where node n is
266 // firstNode, lastNode and also in the middle
267 if (selectedWays.contains(w) && w.isInnerNode(n)) {
268 if (inTheMiddle == null) {
269 inTheMiddle = w;
270 } else {
271 inTheMiddle = null;
272 break;
273 }
274 }
275 }
276 if (inTheMiddle != null)
277 return Collections.singletonList(inTheMiddle);
278 }
279
280 // List of ways shared by all nodes
281 return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes);
282 }
283
284 static void doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) {
285 final MapFrame map = MainApplication.getMap();
286 final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw;
287 final SplitWayCommand result = SplitWayCommand.doSplitWay(way, wayToKeep, newWays, !isMapModeDraw ? newSelection : null);
288 UndoRedoHandler.getInstance().add(result);
289 List<? extends PrimitiveId> newSel = result.getNewSelection();
290 if (newSel != null && !newSel.isEmpty()) {
291 way.getDataSet().setSelected(newSel);
292 }
293 }
294
295 @Override
296 protected void updateEnabledState() {
297 updateEnabledStateOnCurrentSelection();
298 }
299
300 @Override
301 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
302 // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
303 setEnabled(OsmUtils.isOsmCollectionEditable(selection)
304 && selection.stream().anyMatch(o -> o instanceof Node && !o.isIncomplete()));
305 }
306}
Note: See TracBrowser for help on using the repository browser.