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

Last change on this file since 17379 was 17362, checked in by GerdP, 3 years ago

see #19885: memory leak with "temporary" objects in validator and actions

  • fix memory leaks in SplitWayAction and SplitWayCommand
  • fix (not yet used) counter of modified relations
  • Property svn:eol-style set to native
File size: 13.5 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("Tools: {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 if (getValue() != 1) {
226 newWays.forEach(w -> w.setNodes(null)); // see 19885
227 }
228 }
229 }
230
231 @Override
232 protected void buttonAction(int buttonIndex, ActionEvent evt) {
233 super.buttonAction(buttonIndex, evt);
234 toggleSaveState(); // necessary since #showDialog() does not handle it due to the non-modal dialog
235 if (getValue() == 1) {
236 doSplitWay(selectedWay, list.getSelectedValue(), newWays, selection);
237 }
238 }
239 }
240
241 static class SegmentListCellRenderer extends DefaultListCellRenderer {
242 @Override
243 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
244 final Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
245 final String name = DefaultNameFormatter.getInstance().format((Way) value);
246 // get rid of id from DefaultNameFormatter.decorateNameWithId()
247 final String nameWithoutId = name
248 .replace(tr(" [id: {0}]", ((Way) value).getId()), "")
249 .replace(tr(" [id: {0}]", ((Way) value).getUniqueId()), "");
250 ((JLabel) c).setText(tr("Segment {0}: {1}", index + 1, nameWithoutId));
251 return c;
252 }
253 }
254
255 /**
256 * Determine which ways to split.
257 * @param selectedWays List of user selected ways.
258 * @param selectedNodes List of user selected nodes.
259 * @return List of ways to split
260 */
261 static List<Way> getApplicableWays(List<Way> selectedWays, List<Node> selectedNodes) {
262 if (selectedNodes.isEmpty())
263 return null;
264
265 // Special case - one of the selected ways touches (not cross) way that we want to split
266 if (selectedNodes.size() == 1) {
267 Node n = selectedNodes.get(0);
268 List<Way> referredWays = n.getParentWays();
269 Way inTheMiddle = null;
270 for (Way w: referredWays) {
271 // Need to look at all nodes see #11184 for a case where node n is
272 // firstNode, lastNode and also in the middle
273 if (selectedWays.contains(w) && w.isInnerNode(n)) {
274 if (inTheMiddle == null) {
275 inTheMiddle = w;
276 } else {
277 inTheMiddle = null;
278 break;
279 }
280 }
281 }
282 if (inTheMiddle != null)
283 return Collections.singletonList(inTheMiddle);
284 }
285
286 // List of ways shared by all nodes
287 return UnJoinNodeWayAction.getApplicableWays(selectedWays, selectedNodes);
288 }
289
290 static void doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) {
291 final MapFrame map = MainApplication.getMap();
292 final boolean isMapModeDraw = map != null && map.mapMode == map.mapModeDraw;
293
294 Optional<SplitWayCommand> splitWayCommand = SplitWayCommand.doSplitWay(
295 way,
296 wayToKeep,
297 newWays,
298 !isMapModeDraw ? newSelection : null,
299 SplitWayCommand.WhenRelationOrderUncertain.ASK_USER_FOR_CONSENT_TO_DOWNLOAD
300 );
301
302 splitWayCommand.ifPresent(result -> {
303 UndoRedoHandler.getInstance().add(result);
304 List<? extends PrimitiveId> newSel = result.getNewSelection();
305 if (newSel != null && !newSel.isEmpty()) {
306 way.getDataSet().setSelected(newSel);
307 }
308 });
309 if (!splitWayCommand.isPresent()) {
310 newWays.forEach(w -> w.setNodes(null)); // see 19885
311 }
312 }
313
314 @Override
315 protected void updateEnabledState() {
316 updateEnabledStateOnCurrentSelection();
317 }
318
319 @Override
320 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
321 // Selection still can be wrong, but let SplitWayAction process and tell user what's wrong
322 setEnabled(OsmUtils.isOsmCollectionEditable(selection)
323 && selection.stream().anyMatch(o -> o instanceof Node && !o.isIncomplete()));
324 }
325}
Note: See TracBrowser for help on using the repository browser.