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

Last change on this file was 19078, checked in by taylor.smock, 7 weeks ago

Fix #4142: Track fully downloaded objects (patch by stoecker, GerdP, and myself)

The serialization move from PrimitiveData to AbstractPrimitive should be
reverted prior to 24.05 (see #23677).

The serialization move was required since we want to ensure that all downstream
users of AbstractPrimitive were not using the flags field, which was done by
making the field private instead of protected. They may still be using that
field (via updateFlags) which would not be caught by compile-time or runtime
errors.

Additionally, a good chunk of common functionality was moved up from
OsmPrimitive, even though much of it wasn't useful for PrimitiveData.

  • Property svn:eol-style set to native
File size: 17.8 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 } else if (!checkAndConfirmOutlyingOperation("splitway", tr("Split way confirmation"),
147 tr("You are about to split a way that may have referrers that are not yet downloaded.")
148 + "<br/>"
149 + tr("This can lead to broken relations.") + "<br/>"
150 + tr("Do you really want to split?"),
151 tr("The selected area is incomplete. Continue?"),
152 applicableWays, null)) {
153 return;
154 }
155
156 // Finally, applicableWays contains only one perfect way
157 final Way selectedWay = applicableWays.get(0);
158 final List<OsmPrimitive> sel = new ArrayList<>(ds.getSelectedRelations());
159 sel.addAll(selectedWays);
160 doSplitWayShowSegmentSelection(selectedWay, selectedNodes, sel);
161 }
162
163 /**
164 * Perform way splitting after presenting the user with a choice which way segment history should be preserved (in expert mode)
165 * @param splitWay The way to split
166 * @param splitNodes The nodes at which the way should be split
167 * @param selection (Optional) selection which should be updated
168 *
169 * @since 18759
170 */
171 public static void doSplitWayShowSegmentSelection(Way splitWay, List<Node> splitNodes, List<OsmPrimitive> selection) {
172 final List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(splitWay, splitNodes);
173 if (wayChunks != null) {
174 final List<Way> newWays = SplitWayCommand.createNewWaysFromChunks(splitWay, wayChunks);
175 final Way wayToKeep = SplitWayCommand.Strategy.keepLongestChunk().determineWayToKeep(newWays);
176
177 if (ExpertToggleAction.isExpert() && !splitWay.isNew()) {
178 final ExtendedDialog dialog = new SegmentToKeepSelectionDialog(splitWay, newWays, wayToKeep, splitNodes, selection);
179 dialog.toggleEnable("way.split.segment-selection-dialog");
180 if (!dialog.toggleCheckState()) {
181 dialog.setModal(false);
182 dialog.showDialog();
183 return; // splitting is performed in SegmentToKeepSelectionDialog.buttonAction()
184 }
185 }
186 if (wayToKeep != null) {
187 doSplitWay(splitWay, wayToKeep, newWays, selection);
188 }
189 }
190 }
191
192 /**
193 * A dialog to query which way segment should reuse the history of the way to split.
194 */
195 static class SegmentToKeepSelectionDialog extends ExtendedDialog {
196 static final AtomicInteger DISPLAY_COUNT = new AtomicInteger();
197 final transient Way selectedWay;
198 final JList<Way> list;
199 final transient List<OsmPrimitive> selection;
200 final transient List<Node> selectedNodes;
201 final SplitWayDataSetListener dataSetListener;
202 transient List<Way> newWays;
203 transient Way wayToKeep;
204
205 SegmentToKeepSelectionDialog(
206 Way selectedWay, List<Way> newWays, Way wayToKeep, List<Node> selectedNodes, List<OsmPrimitive> selection) {
207 super(MainApplication.getMainFrame(), tr("Which way segment should reuse the history of {0}?", selectedWay.getId()),
208 new String[]{tr("Ok"), tr("Cancel")}, true);
209
210 this.selectedWay = selectedWay;
211 this.newWays = newWays;
212 this.selectedNodes = selectedNodes;
213 this.selection = selection;
214 this.wayToKeep = wayToKeep;
215 this.list = new JList<>(newWays.toArray(new Way[0]));
216 this.dataSetListener = new SplitWayDataSetListener();
217
218 configureList();
219
220 setButtonIcons("ok", "cancel");
221 final JPanel pane = new JPanel(new GridBagLayout());
222 pane.add(new JLabel(getTitle()), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
223 pane.add(list, GBC.eop().fill(GridBagConstraints.HORIZONTAL));
224 setContent(pane);
225 setDefaultCloseOperation(HIDE_ON_CLOSE);
226 }
227
228 private void configureList() {
229 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
230 list.addListSelectionListener(e -> {
231 final Way selected = list.getSelectedValue();
232 if (selected != null && MainApplication.isDisplayingMapView() && selected.getNodesCount() > 1) {
233 final Collection<WaySegment> segments = new ArrayList<>(selected.getNodesCount() - 1);
234 final Iterator<Node> it = selected.getNodes().iterator();
235 Node previousNode = it.next();
236 while (it.hasNext()) {
237 final Node node = it.next();
238 segments.add(WaySegment.forNodePair(selectedWay, previousNode, node));
239 previousNode = node;
240 }
241 setHighlightedWaySegments(segments);
242 }
243 });
244 list.setCellRenderer(new SegmentListCellRenderer());
245 }
246
247 protected void setHighlightedWaySegments(Collection<WaySegment> segments) {
248 final DataSet ds = selectedWay.getDataSet();
249 if (ds != null) {
250 ds.setHighlightedWaySegments(segments);
251 MainApplication.getMap().mapView.repaint();
252 }
253 }
254
255 @Override
256 public void setVisible(boolean visible) {
257 super.setVisible(visible);
258 final DataSet ds = selectedWay.getDataSet();
259 if (visible) {
260 DISPLAY_COUNT.incrementAndGet();
261 list.setSelectedValue(wayToKeep, true);
262 if (ds != null) {
263 ds.addDataSetListener(dataSetListener);
264 }
265 list.requestFocusInWindow();
266 } else {
267 if (ds != null) {
268 ds.removeDataSetListener(dataSetListener);
269 }
270 setHighlightedWaySegments(Collections.emptyList());
271 DISPLAY_COUNT.decrementAndGet();
272 if (getValue() != 1 && selectedWay.getDataSet() != null) {
273 newWays.forEach(w -> w.setNodes(null)); // see 19885
274 }
275 }
276 }
277
278 @Override
279 protected void buttonAction(int buttonIndex, ActionEvent evt) {
280 super.buttonAction(buttonIndex, evt);
281 toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog
282 if (getValue() == 1) {
283 doSplitWay(selectedWay, list.getSelectedValue(), newWays, selection);
284 }
285 }
286
287 private final class SplitWayDataSetListener implements DataSetListener {
288
289 @Override
290 public void primitivesAdded(PrimitivesAddedEvent event) {
291 }
292
293 @Override
294 public void primitivesRemoved(PrimitivesRemovedEvent event) {
295 if (event.getPrimitives().stream().anyMatch(p -> p instanceof Way)) {
296 updateWaySegments();
297 }
298 }
299
300 @Override
301 public void tagsChanged(TagsChangedEvent event) {}
302
303 @Override
304 public void nodeMoved(NodeMovedEvent event) {}
305
306 @Override
307 public void wayNodesChanged(WayNodesChangedEvent event) {
308 updateWaySegments();
309 }
310
311 @Override
312 public void relationMembersChanged(RelationMembersChangedEvent event) {}
313
314 @Override
315 public void otherDatasetChange(AbstractDatasetChangedEvent event) {}
316
317 @Override
318 public void dataChanged(DataChangedEvent event) {}
319
320 private void updateWaySegments() {
321 if (!selectedWay.isUsable()) {
322 setVisible(false);
323 return;
324 }
325
326 List<List<Node>> chunks = SplitWayCommand.buildSplitChunks(selectedWay, selectedNodes);
327 if (chunks == null) {
328 setVisible(false);
329 return;
330 }
331
332 newWays = SplitWayCommand.createNewWaysFromChunks(selectedWay, chunks);
333 if (list.getSelectedIndex() < newWays.size()) {
334 wayToKeep = newWays.get(list.getSelectedIndex());
335 } else {
336 wayToKeep = SplitWayCommand.Strategy.keepLongestChunk().determineWayToKeep(newWays);
337 }
338 list.setListData(newWays.toArray(new Way[0]));
339 list.setSelectedValue(wayToKeep, true);
340 }
341 }
342 }
343
344 static class SegmentListCellRenderer extends DefaultListCellRenderer {
345 @Override
346 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
347 final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
348 final String name = DefaultNameFormatter.getInstance().format((Way) value);
349 // get rid of id from DefaultNameFormatter.decorateNameWithId()
350 final String nameWithoutId = name
351 .replace(tr(" [id: {0}]", ((Way) value).getId()), "")
352 .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), "");
353 ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId));
354 return c;
355 }
356 }
357
358 /**
359 * Determine which ways to split.
360 * @param selectedWays List of user selected ways.
361 * @param selectedNodes List of user selected nodes.
362 * @return List of ways to split
363 */
364 static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
365 if (selectedNodes.isEmpty())
366 return null;
367
368 // Special case - one of the selected ways touches (not cross) way that we want to split
369 if (selectedNodes.size() == 1) {
370 final Node n = selectedNodes.get(0);
371 List<Way> referredWays = n.getParentWays();
372 Way inTheMiddle = null;
373 for (Way w: referredWays) {
374 // Need to look at all nodes see #11184 for a case where node n is
375 // firstNode, lastNode and also in the middle
376 if (selectedWays.contains(w) && w.isInnerNode(n)) {
377 if (inTheMiddle == null) {
378 inTheMiddle = w;
379 } else {
380 inTheMiddle = null;
381 break;
382 }
383 }
384 }
385 if (inTheMiddle != null)
386 return Collections.singletonList(inTheMiddle);
387 }
388
389 // List of ways shared by all nodes
390 return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes);
391 }
392
393 static void doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) {
394 final MapFrame map = MainApplication.getMap();
395 final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw;
396
397 Optional<SplitWayCommand> splitWayCommand = SplitWayCommand.doSplitWay(
398 way,
399 wayToKeep,
400 newWays,
401 !isMapModeDraw ? newSelection : null,
402 SplitWayCommand.WhenRelationOrderUncertain.ASK_USER_FOR_CONSENT_TO_DOWNLOAD
403 );
404
405 splitWayCommand.ifPresent(result -> {
406 UndoRedoHandler.getInstance().add(result);
407 List<? extends PrimitiveId> newSel = result.getNewSelection();
408 if (!Utils.isEmpty(newSel)) {
409 way.getDataSet().setSelected(newSel);
410 }
411 });
412 if (!splitWayCommand.isPresent()) {
413 newWays.forEach(w -> w.setNodes(null)); // see 19885
414 }
415 }
416
417 @Override
418 protected void updateEnabledState() {
419 updateEnabledStateOnCurrentSelection();
420 }
421
422 @Override
423 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
424 // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
425 setEnabled(OsmUtils.isOsmCollectionEditable(selection)
426 && selection.stream().anyMatch(o -> o instanceof Node && !o.isIncomplete()));
427 }
428}
Note: See TracBrowser for help on using the repository browser.