source: josm/trunk/src/org/openstreetmap/josm/actions/CombineWayAction.java@ 17534

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

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

  • use ChangeNodesCommand

Simplifies code and avoids another memory leak

  • Property svn:eol-style set to native
File size: 14.1 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.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashSet;
14import java.util.LinkedHashSet;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Objects;
18import java.util.Set;
19import java.util.stream.Collectors;
20import java.util.stream.IntStream;
21
22import javax.swing.JOptionPane;
23
24import org.openstreetmap.josm.actions.corrector.ReverseWayTagCorrector;
25import org.openstreetmap.josm.command.ChangeNodesCommand;
26import org.openstreetmap.josm.command.Command;
27import org.openstreetmap.josm.command.DeleteCommand;
28import org.openstreetmap.josm.command.SequenceCommand;
29import org.openstreetmap.josm.data.UndoRedoHandler;
30import org.openstreetmap.josm.data.osm.DataSet;
31import org.openstreetmap.josm.data.osm.Node;
32import org.openstreetmap.josm.data.osm.NodeGraph;
33import org.openstreetmap.josm.data.osm.OsmPrimitive;
34import org.openstreetmap.josm.data.osm.OsmUtils;
35import org.openstreetmap.josm.data.osm.TagCollection;
36import org.openstreetmap.josm.data.osm.Way;
37import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
38import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
39import org.openstreetmap.josm.data.preferences.BooleanProperty;
40import org.openstreetmap.josm.data.validation.Test;
41import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
42import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay;
43import org.openstreetmap.josm.gui.ExtendedDialog;
44import org.openstreetmap.josm.gui.MainApplication;
45import org.openstreetmap.josm.gui.Notification;
46import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
47import org.openstreetmap.josm.gui.util.GuiHelper;
48import org.openstreetmap.josm.tools.Logging;
49import org.openstreetmap.josm.tools.Pair;
50import org.openstreetmap.josm.tools.Shortcut;
51import org.openstreetmap.josm.tools.UserCancelException;
52
53/**
54 * Combines multiple ways into one.
55 * @since 213
56 */
57public class CombineWayAction extends JosmAction {
58
59 private static final BooleanProperty PROP_REVERSE_WAY = new BooleanProperty("tag-correction.reverse-way", true);
60
61 /**
62 * Constructs a new {@code CombineWayAction}.
63 */
64 public CombineWayAction() {
65 super(tr("Combine Way"), "combineway", tr("Combine several ways into one."),
66 Shortcut.registerShortcut("tools:combineway", tr("Tools: {0}", tr("Combine Way")), KeyEvent.VK_C, Shortcut.DIRECT), true);
67 setHelpId(ht("/Action/CombineWay"));
68 }
69
70 protected static boolean confirmChangeDirectionOfWays() {
71 return new ExtendedDialog(MainApplication.getMainFrame(),
72 tr("Change directions?"),
73 tr("Reverse and Combine"), tr("Cancel"))
74 .setButtonIcons("wayflip", "cancel")
75 .setContent(tr("The ways can not be combined in their current directions. "
76 + "Do you want to reverse some of them?"))
77 .toggleEnable("combineway-reverse")
78 .showDialog()
79 .getValue() == 1;
80 }
81
82 protected static void warnCombiningImpossible() {
83 String msg = tr("Could not combine ways<br>"
84 + "(They could not be merged into a single string of nodes)");
85 new Notification(msg)
86 .setIcon(JOptionPane.INFORMATION_MESSAGE)
87 .show();
88 }
89
90 protected static Way getTargetWay(Collection<Way> combinedWays) {
91 // init with an arbitrary way
92 Way targetWay = combinedWays.iterator().next();
93
94 // look for the first way already existing on
95 // the server
96 for (Way w : combinedWays) {
97 targetWay = w;
98 if (!w.isNew()) {
99 break;
100 }
101 }
102 return targetWay;
103 }
104
105 /**
106 * Combine multiple ways into one.
107 * @param ways the way to combine to one way
108 * @return null if ways cannot be combined. Otherwise returns the combined ways and the commands to combine
109 * @throws UserCancelException if the user cancelled a dialog.
110 */
111 public static Pair<Way, Command> combineWaysWorker(Collection<Way> ways) throws UserCancelException {
112
113 // prepare and clean the list of ways to combine
114 //
115 if (ways == null || ways.isEmpty())
116 return null;
117 ways.remove(null); // just in case - remove all null ways from the collection
118
119 // remove duplicates, preserving order
120 ways = new LinkedHashSet<>(ways);
121 // remove incomplete ways
122 ways.removeIf(OsmPrimitive::isIncomplete);
123 // we need at least two ways
124 if (ways.size() < 2)
125 return null;
126
127 List<DataSet> dataSets = ways.stream().map(Way::getDataSet).filter(Objects::nonNull).distinct().collect(Collectors.toList());
128 if (dataSets.size() != 1) {
129 throw new IllegalArgumentException("Cannot combine ways of multiple data sets.");
130 }
131
132 // try to build a new way which includes all the combined ways
133 List<Node> path = new LinkedList<>(tryJoin(ways));
134 if (path.isEmpty()) {
135 warnCombiningImpossible();
136 return null;
137 }
138 // check whether any ways have been reversed in the process
139 // and build the collection of tags used by the ways to combine
140 //
141 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
142
143 final List<Command> reverseWayTagCommands = new LinkedList<>();
144 List<Way> reversedWays = new LinkedList<>();
145 List<Way> unreversedWays = new LinkedList<>();
146 detectReversedWays(ways, path, reversedWays, unreversedWays);
147 // reverse path if all ways have been reversed
148 if (unreversedWays.isEmpty()) {
149 Collections.reverse(path);
150 unreversedWays = reversedWays;
151 reversedWays = null;
152 }
153 if ((reversedWays != null) && !reversedWays.isEmpty()) {
154 if (!confirmChangeDirectionOfWays()) return null;
155 // filter out ways that have no direction-dependent tags
156 unreversedWays = ReverseWayTagCorrector.irreversibleWays(unreversedWays);
157 reversedWays = ReverseWayTagCorrector.irreversibleWays(reversedWays);
158 // reverse path if there are more reversed than unreversed ways with direction-dependent tags
159 if (reversedWays.size() > unreversedWays.size()) {
160 Collections.reverse(path);
161 List<Way> tempWays = unreversedWays;
162 unreversedWays = null;
163 reversedWays = tempWays;
164 }
165 // if there are still reversed ways with direction-dependent tags, reverse their tags
166 if (!reversedWays.isEmpty() && Boolean.TRUE.equals(PROP_REVERSE_WAY.get())) {
167 List<Way> unreversedTagWays = new ArrayList<>(ways);
168 unreversedTagWays.removeAll(reversedWays);
169 ReverseWayTagCorrector reverseWayTagCorrector = new ReverseWayTagCorrector();
170 List<Way> reversedTagWays = new ArrayList<>(reversedWays.size());
171 for (Way w : reversedWays) {
172 Way wnew = new Way(w);
173 reversedTagWays.add(wnew);
174 reverseWayTagCommands.addAll(reverseWayTagCorrector.execute(w, wnew));
175 }
176 if (!reverseWayTagCommands.isEmpty()) {
177 // commands need to be executed for CombinePrimitiveResolverDialog
178 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Reverse Ways"), reverseWayTagCommands));
179 }
180 wayTags = TagCollection.unionOfAllPrimitives(reversedTagWays);
181 wayTags.add(TagCollection.unionOfAllPrimitives(unreversedTagWays));
182 }
183 }
184
185 // create the new way and apply the new node list
186 //
187 Way targetWay = getTargetWay(ways);
188
189 final List<Command> resolution;
190 try {
191 resolution = CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, Collections.singleton(targetWay));
192 } finally {
193 if (!reverseWayTagCommands.isEmpty()) {
194 // undo reverseWayTagCorrector and merge into SequenceCommand below
195 UndoRedoHandler.getInstance().undo();
196 }
197 }
198
199 List<Command> cmds = new LinkedList<>();
200 List<Way> deletedWays = new LinkedList<>(ways);
201 deletedWays.remove(targetWay);
202
203 cmds.add(new ChangeNodesCommand(dataSets.get(0), targetWay, path));
204 cmds.addAll(reverseWayTagCommands);
205 cmds.addAll(resolution);
206 cmds.add(new DeleteCommand(dataSets.get(0), deletedWays));
207 final Command sequenceCommand = new SequenceCommand(/* for correct i18n of plural forms - see #9110 */
208 trn("Combine {0} way", "Combine {0} ways", ways.size(), ways.size()), cmds);
209
210 return new Pair<>(targetWay, sequenceCommand);
211 }
212
213 protected static void detectReversedWays(Collection<Way> ways, List<Node> path, List<Way> reversedWays,
214 List<Way> unreversedWays) {
215 for (Way w: ways) {
216 // Treat zero or one-node ways as unreversed as Combine action action is a good way to fix them (see #8971)
217 if (w.getNodesCount() < 2) {
218 unreversedWays.add(w);
219 } else {
220 int last = path.lastIndexOf(w.getNode(0));
221
222 boolean foundStartSegment = IntStream.rangeClosed(path.indexOf(w.getNode(0)), last)
223 .anyMatch(i -> path.get(i) == w.getNode(0) && i + 1 < path.size() && w.getNode(1) == path.get(i + 1));
224 if (foundStartSegment) {
225 unreversedWays.add(w);
226 } else {
227 reversedWays.add(w);
228 }
229 }
230 }
231 }
232
233 protected static List<Node> tryJoin(Collection<Way> ways) {
234 List<Node> path = joinWithMultipolygonCode(ways);
235 if (path.isEmpty()) {
236 NodeGraph graph = NodeGraph.createNearlyUndirectedGraphFromNodeWays(ways);
237 path = graph.buildSpanningPathNoRemove();
238 }
239 return path;
240 }
241
242 /**
243 * Use {@link Multipolygon#joinWays(Collection)} to join ways.
244 * @param ways the ways
245 * @return List of nodes of the combined ways or null if ways could not be combined to a single way.
246 * Result may contain overlapping segments.
247 */
248 private static List<Node> joinWithMultipolygonCode(Collection<Way> ways) {
249 // sort so that old unclosed ways appear first
250 LinkedList<Way> toJoin = new LinkedList<>(ways);
251 toJoin.sort((o1, o2) -> {
252 int d = Boolean.compare(o1.isNew(), o2.isNew());
253 if (d == 0)
254 d = Boolean.compare(o1.isClosed(), o2.isClosed());
255 return d;
256 });
257 Collection<JoinedWay> list = Multipolygon.joinWays(toJoin);
258 if (list.size() == 1) {
259 // ways form a single line string
260 return Collections.unmodifiableList(new ArrayList<>(list.iterator().next().getNodes()));
261 }
262 return Collections.emptyList();
263 }
264
265 @Override
266 public void actionPerformed(ActionEvent event) {
267 final DataSet ds = getLayerManager().getEditDataSet();
268 if (ds == null)
269 return;
270 Collection<Way> selectedWays = new LinkedHashSet<>(ds.getSelectedWays());
271 selectedWays.removeIf(Way::isEmpty);
272 if (selectedWays.size() < 2) {
273 new Notification(
274 tr("Please select at least two ways to combine."))
275 .setIcon(JOptionPane.INFORMATION_MESSAGE)
276 .setDuration(Notification.TIME_SHORT)
277 .show();
278 return;
279 }
280
281 // see #18083: check if we will combine ways at nodes outside of the download area
282 Set<Node> endNodesOutside = new HashSet<>();
283 for (Way w : selectedWays) {
284 final Node[] endnodes = {w.firstNode(), w.lastNode()};
285 for (Node n : endnodes) {
286 if (!n.isNew() && n.isOutsideDownloadArea() && !endNodesOutside.add(n)) {
287 new Notification(tr("Combine ways refused<br>" + "(A shared node is outside of the download area)"))
288 .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
289 return;
290
291 }
292 }
293 }
294
295 // combine and update gui
296 Pair<Way, Command> combineResult;
297 try {
298 combineResult = combineWaysWorker(selectedWays);
299 } catch (UserCancelException ex) {
300 Logging.trace(ex);
301 return;
302 }
303
304 if (combineResult == null)
305 return;
306
307 final Way selectedWay = combineResult.a;
308 UndoRedoHandler.getInstance().add(combineResult.b);
309 Test test = new OverlappingWays();
310 test.startTest(null);
311 test.visit(combineResult.a);
312 test.endTest();
313 if (test.getErrors().isEmpty()) {
314 test = new SelfIntersectingWay();
315 test.startTest(null);
316 test.visit(combineResult.a);
317 test.endTest();
318 }
319 if (!test.getErrors().isEmpty()) {
320 new Notification(test.getErrors().get(0).getMessage())
321 .setIcon(JOptionPane.WARNING_MESSAGE)
322 .setDuration(Notification.TIME_SHORT)
323 .show();
324 }
325 if (selectedWay != null) {
326 GuiHelper.runInEDT(() -> ds.setSelected(selectedWay));
327 }
328 }
329
330 @Override
331 protected void updateEnabledState() {
332 updateEnabledStateOnCurrentSelection();
333 }
334
335 @Override
336 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
337 int numWays = 0;
338 if (OsmUtils.isOsmCollectionEditable(selection)) {
339 for (OsmPrimitive osm : selection) {
340 if (osm instanceof Way && !osm.isIncomplete() && ++numWays >= 2) {
341 break;
342 }
343 }
344 }
345 setEnabled(numWays >= 2);
346 }
347
348}
Note: See TracBrowser for help on using the repository browser.