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

Last change on this file since 15943 was 15943, checked in by GerdP, 4 years ago

see #18596 Fix relation ordering after split-way
Patch by JeroenHoek, slightly modified

  • download needed relation members if wanted
  • improve member ordering of route relations

TODO:

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