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

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