source: osm/applications/editors/josm/plugins/merge-overlap/src/mergeoverlap/MergeOverlapAction.java

Last change on this file was 36134, checked in by taylor.smock, 2 years ago

Use DeleteCommand.delete where possible and fix some potential NPEs from its usage

File size: 19.4 KB
Line 
1package mergeoverlap;
2
3import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.applyAutomaticTagConflictResolution;
4import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing;
5import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing;
6import static org.openstreetmap.josm.tools.I18n.tr;
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.HashMap;
14import java.util.HashSet;
15import java.util.LinkedHashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19import java.util.Set;
20
21import org.openstreetmap.josm.actions.JosmAction;
22import org.openstreetmap.josm.actions.corrector.ReverseWayTagCorrector;
23import org.openstreetmap.josm.command.ChangeCommand;
24import org.openstreetmap.josm.command.Command;
25import org.openstreetmap.josm.command.DeleteCommand;
26import org.openstreetmap.josm.command.SequenceCommand;
27import org.openstreetmap.josm.command.SplitWayCommand;
28import org.openstreetmap.josm.data.UndoRedoHandler;
29import org.openstreetmap.josm.data.osm.Node;
30import org.openstreetmap.josm.data.osm.NodeGraph;
31import org.openstreetmap.josm.data.osm.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.Relation;
33import org.openstreetmap.josm.data.osm.TagCollection;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.gui.MainApplication;
36import org.openstreetmap.josm.tools.Logging;
37import org.openstreetmap.josm.tools.Pair;
38import org.openstreetmap.josm.tools.Shortcut;
39import org.openstreetmap.josm.tools.UserCancelException;
40import org.openstreetmap.josm.tools.Utils;
41
42import mergeoverlap.hack.MyCombinePrimitiveResolverDialog;
43
44/**
45 * Merge overlapping part of ways.
46 */
47public class MergeOverlapAction extends JosmAction {
48
49 Map<Way, List<Relation>> relations = new HashMap<>();
50 Map<Way, Way> oldWays = new HashMap<>();
51 Map<Relation, Relation> newRelations = new HashMap<>();
52 Set<Way> deletes = new HashSet<>();
53
54 /**
55 * Constructs a new {@code MergeOverlapAction}.
56 */
57 public MergeOverlapAction() {
58 super(tr("Merge overlap"), "merge_overlap",
59 tr("Merge overlap of ways."),
60 Shortcut.registerShortcut("tools:mergeoverlap",tr("More tools: {0}", tr("Merge overlap")), KeyEvent.VK_O,
61 Shortcut.ALT_CTRL), true);
62 }
63
64 /**
65 * The action button has been clicked
66 *
67 * @param e
68 * Action Event
69 */
70 @Override
71 public void actionPerformed(ActionEvent e) {
72
73 // List of selected ways
74 List<Way> ways = new ArrayList<>();
75 relations.clear();
76 newRelations.clear();
77
78 // For every selected way
79 for (OsmPrimitive osm : getLayerManager().getEditDataSet().getSelected()) {
80 if (osm instanceof Way && !osm.isDeleted()) {
81 Way way = (Way) osm;
82 ways.add(way);
83 List<Relation> rels = new ArrayList<>(Utils.filteredCollection(way.getReferrers(), Relation.class));
84 relations.put(way, rels);
85 }
86 }
87
88 List<Way> sel = new ArrayList<>(ways);
89 Collection<Command> cmds = new LinkedList<>();
90
91 // *****
92 // split
93 // *****
94 for (Way way : ways) {
95 Set<Node> nodes = new HashSet<>();
96 for (Way opositWay : ways) {
97 if (way != opositWay) {
98 List<NodePos> nodesPos = new LinkedList<>();
99
100 int pos = 0;
101 for (Node node : way.getNodes()) {
102 int opositPos = 0;
103 for (Node opositNode : opositWay.getNodes()) {
104 if (node == opositNode) {
105 if (opositWay.isClosed()) {
106 opositPos %= opositWay.getNodesCount() - 1;
107 }
108 nodesPos.add(new NodePos(node, pos, opositPos));
109 break;
110 }
111 opositPos++;
112 }
113 pos++;
114 }
115
116 NodePos start = null;
117 NodePos end = null;
118 int increment = 0;
119
120 boolean hasFirst = false;
121 for (NodePos node : nodesPos) {
122 if (start == null) {
123 start = node;
124 } else {
125 if (end == null) {
126 if (follows(way, opositWay, start, node, 1)) {
127 end = node;
128 increment = +1;
129 } else if (follows(way, opositWay, start, node, -1)) {
130 end = node;
131 increment = -1;
132 } else {
133 start = node;
134 end = null;
135 }
136 } else {
137 if (follows(way, opositWay, end, node, increment)) {
138 end = node;
139 } else {
140 hasFirst = addNodes(start, end, way, nodes, hasFirst);
141 start = node;
142 end = null;
143 }
144 }
145 }
146 }
147
148 if (start != null && end != null) {
149 hasFirst = addNodes(start, end, way, nodes, hasFirst);
150 start = null;
151 end = null;
152 }
153 }
154 }
155 if (!nodes.isEmpty() && !way.isClosed() || nodes.size() >= 2) {
156 List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(way, new ArrayList<>(nodes));
157 SplitWayCommand result = SplitWayCommand.splitWay(way, wayChunks, Collections.emptyList());
158
159 cmds.add(result);
160 sel.remove(way);
161 sel.add(result.getOriginalWay());
162 sel.addAll(result.getNewWays());
163 List<Relation> rels = relations.remove(way);
164 relations.put(result.getOriginalWay(), rels);
165 for (Way w : result.getNewWays()) {
166 relations.put(w, rels);
167 }
168 }
169 }
170
171 // *****
172 // merge
173 // *****
174 ways = new ArrayList<>(sel);
175 while (!ways.isEmpty()) {
176 Way way = ways.get(0);
177 List<Way> combine = new ArrayList<>();
178 combine.add(way);
179 for (Way opositWay : ways) {
180 if (way != opositWay && way.getNodesCount() == opositWay.getNodesCount()) {
181 boolean equals1 = true;
182 for (int i = 0; i < way.getNodesCount(); i++) {
183 if (way.getNode(i) != opositWay.getNode(i)) {
184 equals1 = false;
185 break;
186 }
187 }
188 boolean equals2 = true;
189 for (int i = 0; i < way.getNodesCount(); i++) {
190 if (way.getNode(i) != opositWay.getNode(way.getNodesCount() - i - 1)) {
191 equals2 = false;
192 break;
193 }
194 }
195 if (equals1 || equals2) {
196 combine.add(opositWay);
197 }
198 }
199 }
200 ways.removeAll(combine);
201 if (combine.size() > 1) {
202 sel.removeAll(combine);
203 // combine
204 Pair<Way, List<Command>> combineResult;
205 try {
206 combineResult = combineWaysWorker(combine);
207 } catch (UserCancelException ex) {
208 Logging.trace(ex);
209 return;
210 }
211 sel.add(combineResult.a);
212 cmds.addAll(combineResult.b);
213 }
214 }
215
216 for (Map.Entry<Relation, Relation> entry : newRelations.entrySet()) {
217 cmds.add(new ChangeCommand(entry.getKey(), entry.getValue()));
218 }
219
220 List<Way> del = new LinkedList<>();
221 for (Way w : deletes) {
222 if (w.getDataSet() != null && !w.isDeleted()) {
223 del.add(w);
224 }
225 }
226 if (!del.isEmpty()) {
227 final Command deleteCommand = DeleteCommand.delete(del);
228 if (deleteCommand != null) {
229 cmds.add(deleteCommand);
230 }
231 }
232
233 // Commit
234 if (!cmds.isEmpty()) {
235 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Merge Overlap (combine)"), cmds));
236 getLayerManager().getEditDataSet().setSelected(sel);
237 MainApplication.getMap().repaint();
238 }
239
240 relations.clear();
241 newRelations.clear();
242 oldWays.clear();
243 }
244
245 private static class NodePos {
246 Node node;
247 int pos;
248 int opositPos;
249
250 NodePos(Node n, int p, int op) {
251 node = n;
252 pos = p;
253 opositPos = op;
254 }
255
256 @Override
257 public String toString() {
258 return "NodePos: " + pos + ", " + opositPos + ", " + node;
259 }
260 }
261
262 private static boolean addNodes(NodePos start, NodePos end, Way way,
263 Set<Node> nodes, boolean hasFirst) {
264 if (way.isClosed() || (start.node != way.getNode(0) && start.node != way.getNode(way.getNodesCount() - 1))) {
265 hasFirst = hasFirst || start.node == way.getNode(0);
266 nodes.add(start.node);
267 }
268 if (way.isClosed() || (end.node != way.getNode(0) && end.node != way.getNode(way.getNodesCount() - 1))) {
269 if (hasFirst && (end.node == way.getNode(way.getNodesCount() - 1))) {
270 nodes.remove(way.getNode(0));
271 } else {
272 nodes.add(end.node);
273 }
274 }
275 return hasFirst;
276 }
277
278 private static boolean follows(Way way1, Way way2, NodePos np1, NodePos np2,
279 int incr) {
280 if (way2.isClosed() && incr == 1 && np1.opositPos == way2.getNodesCount() - 2) {
281 return np2.pos == np1.pos + 1 && np2.opositPos == 0;
282 } else if (way2.isClosed() && incr == 1 && np1.opositPos == 0) {
283 return np2.pos == np1.pos && np2.opositPos == 0
284 || np2.pos == np1.pos + 1 && np2.opositPos == 1;
285 } else if (way2.isClosed() && incr == -1 && np1.opositPos == 0) {
286 return np2.pos == np1.pos && np2.opositPos == 0 || np2.pos == np1.pos + 1
287 && np2.opositPos == way2.getNodesCount() - 2;
288 } else {
289 return np2.pos == np1.pos + 1 && np2.opositPos == np1.opositPos + incr;
290 }
291 }
292
293 /**
294 * @param ways The ways to be combined
295 * @return null if ways cannot be combined. Otherwise returns the combined
296 * ways and the commands to combine
297 * @throws UserCancelException If the user cancelled the operation
298 */
299 private Pair<Way, List<Command>> combineWaysWorker(Collection<Way> ways) throws UserCancelException {
300
301 // prepare and clean the list of ways to combine
302 if (ways == null || ways.isEmpty())
303 return null;
304 ways.remove(null); // just in case - remove all null ways from the collection
305
306 // remove duplicates, preserving order
307 ways = new LinkedHashSet<>(ways);
308
309 // try to build a new way which includes all the combined ways
310 NodeGraph graph = NodeGraph.createUndirectedGraphFromNodeWays(ways);
311 List<Node> path = graph.buildSpanningPath();
312
313 // check whether any ways have been reversed in the process
314 // and build the collection of tags used by the ways to combine
315 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
316
317 List<Way> reversedWays = new LinkedList<>();
318 List<Way> unreversedWays = new LinkedList<>();
319 for (Way w : ways) {
320 if ((path.indexOf(w.getNode(0)) + 1) == path.lastIndexOf(w.getNode(1))) {
321 unreversedWays.add(w);
322 } else {
323 reversedWays.add(w);
324 }
325 }
326 // reverse path if all ways have been reversed
327 if (unreversedWays.isEmpty()) {
328 Collections.reverse(path);
329 unreversedWays = reversedWays;
330 reversedWays = null;
331 }
332 if ((reversedWays != null) && !reversedWays.isEmpty()) {
333 // filter out ways that have no direction-dependent tags
334 unreversedWays = ReverseWayTagCorrector.irreversibleWays(unreversedWays);
335 reversedWays = ReverseWayTagCorrector.irreversibleWays(reversedWays);
336 // reverse path if there are more reversed than unreversed ways with
337 // direction-dependent tags
338 if (reversedWays.size() > unreversedWays.size()) {
339 Collections.reverse(path);
340 List<Way> tempWays = unreversedWays;
341 unreversedWays = reversedWays;
342 reversedWays = tempWays;
343 }
344 // if there are still reversed ways with direction-dependent tags,
345 // reverse their tags
346 if (!reversedWays.isEmpty()) {
347 List<Way> unreversedTagWays = new ArrayList<>(ways);
348 unreversedTagWays.removeAll(reversedWays);
349 ReverseWayTagCorrector reverseWayTagCorrector = new ReverseWayTagCorrector();
350 List<Way> reversedTagWays = new ArrayList<>();
351 Collection<Command> changePropertyCommands = null;
352 for (Way w : reversedWays) {
353 Way wnew = new Way(w);
354 reversedTagWays.add(wnew);
355 changePropertyCommands = reverseWayTagCorrector.execute(w, wnew);
356 }
357 if ((changePropertyCommands != null) && !changePropertyCommands.isEmpty()) {
358 for (Command c : changePropertyCommands) {
359 c.executeCommand();
360 }
361 }
362 wayTags = TagCollection.unionOfAllPrimitives(reversedTagWays);
363 wayTags.add(TagCollection.unionOfAllPrimitives(unreversedTagWays));
364 }
365 }
366
367 // create the new way and apply the new node list
368 Way targetWay = getTargetWay(ways);
369 Way modifiedTargetWay = new Way(targetWay);
370 modifiedTargetWay.setNodes(path);
371
372 TagCollection completeWayTags = new TagCollection(wayTags);
373 applyAutomaticTagConflictResolution(completeWayTags);
374 normalizeTagCollectionBeforeEditing(completeWayTags, ways);
375 TagCollection tagsToEdit = new TagCollection(completeWayTags);
376 completeTagCollectionForEditing(tagsToEdit);
377
378 MyCombinePrimitiveResolverDialog dialog = MyCombinePrimitiveResolverDialog.getInstance();
379 dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues());
380 dialog.setTargetPrimitive(targetWay);
381 Set<Relation> parentRelations = getParentRelations(ways);
382 dialog.getRelationMemberConflictResolverModel().populate(parentRelations, ways, oldWays);
383 dialog.prepareDefaultDecisions();
384
385 // resolve tag conflicts if necessary
386 if (askForMergeTag(ways) || duplicateParentRelations(ways)) {
387 dialog.setVisible(true);
388 if (!dialog.isApplied())
389 throw new UserCancelException();
390 }
391
392 List<Command> cmds = new LinkedList<>();
393 deletes.addAll(ways);
394 deletes.remove(targetWay);
395
396 cmds.add(new ChangeCommand(getLayerManager().getEditDataSet(), targetWay, modifiedTargetWay));
397 cmds.addAll(dialog.buildWayResolutionCommands());
398 dialog.buildRelationCorrespondance(newRelations, oldWays);
399
400 return new Pair<>(targetWay, cmds);
401 }
402
403 private static Way getTargetWay(Collection<Way> combinedWays) {
404 // init with an arbitrary way
405 Way targetWay = combinedWays.iterator().next();
406
407 // look for the first way already existing on the server
408 for (Way w : combinedWays) {
409 targetWay = w;
410 if (!w.isNew()) {
411 break;
412 }
413 }
414 return targetWay;
415 }
416
417 /**
418 * @return has tag to be merged (=> ask)
419 */
420 private static boolean askForMergeTag(Collection<Way> ways) {
421 for (Way way : ways) {
422 for (Way oposite : ways) {
423 for (String key : way.getKeys().keySet()) {
424 if (!"source".equals(key) && oposite.hasKey(key)
425 && !way.get(key).equals(oposite.get(key))) {
426 return true;
427 }
428 }
429 }
430 }
431 return false;
432 }
433
434 /**
435 * @return has duplicate parent relation
436 */
437 private boolean duplicateParentRelations(Collection<Way> ways) {
438 Set<Relation> duplicateRelations = new HashSet<>();
439 for (Way w : ways) {
440 List<Relation> rs = getParentRelations(w);
441 for (Relation r : rs) {
442 if (duplicateRelations.contains(r)) {
443 return true;
444 }
445 }
446 duplicateRelations.addAll(rs);
447 }
448 return false;
449 }
450
451 /**
452 * Replies the set of referring relations
453 *
454 * @return the set of referring relations
455 */
456 private List<Relation> getParentRelations(Way way) {
457 List<Relation> rels = new ArrayList<>();
458 for (Relation r : relations.get(way)) {
459 rels.add(newRelations.getOrDefault(r, r));
460 }
461 return rels;
462 }
463
464 public static Relation getNew(Relation r, Map<Relation, Relation> newRelations) {
465 if (newRelations.containsValue(r)) {
466 return r;
467 } else {
468 Relation c = new Relation(r);
469 newRelations.put(r, c);
470 return c;
471 }
472 }
473/*
474 private Way getOld(Way r) {
475 return getOld(r, oldWays);
476 }*/
477
478 public static Way getOld(Way w, Map<Way, Way> oldWays) {
479 return oldWays.getOrDefault(w, w);
480 }
481
482 /**
483 * Replies the set of referring relations
484 *
485 * @return the set of referring relations
486 */
487 private Set<Relation> getParentRelations(Collection<Way> ways) {
488 Set<Relation> ret = new HashSet<>();
489 for (Way w : ways) {
490 ret.addAll(getParentRelations(w));
491 }
492 return ret;
493 }
494
495 /** Enable this action only if something is selected */
496 @Override
497 protected void updateEnabledState() {
498 if (getLayerManager().getEditDataSet() == null) {
499 setEnabled(false);
500 } else {
501 updateEnabledState(getLayerManager().getEditDataSet().getSelected());
502 }
503 }
504
505 /** Enable this action only if something is selected */
506 @Override
507 protected void updateEnabledState(
508 Collection<? extends OsmPrimitive> selection) {
509 if (selection == null) {
510 setEnabled(false);
511 return;
512 }
513 for (OsmPrimitive primitive : selection) {
514 if (!(primitive instanceof Way) || primitive.isDeleted()) {
515 setEnabled(false);
516 return;
517 }
518 }
519 setEnabled(selection.size() >= 2);
520 }
521}
Note: See TracBrowser for help on using the repository browser.