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

Last change on this file was 19050, checked in by taylor.smock, 2 days ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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