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

Last change on this file was 19418, checked in by stoecker, 2 weeks ago

fix wrong menu name, fix #24375

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