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

Last change on this file since 36407 was 36134, checked in by taylor.smock, 19 months 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.