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

Last change on this file since 2095 was 2095, checked in by Gubaer, 15 years ago

rewrite of MergeNodesAction
new: new conflict resolution dialog for conflicts during node merging. Can resolve conflicts in relation members too.

  • Property svn:eol-style set to native
File size: 21.0 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.ActionEvent;
7import java.awt.event.KeyEvent;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Map;
15import java.util.Set;
16import java.util.Stack;
17
18import javax.swing.JOptionPane;
19import javax.swing.SwingUtilities;
20
21import org.openstreetmap.josm.Main;
22import org.openstreetmap.josm.command.ChangeCommand;
23import org.openstreetmap.josm.command.Command;
24import org.openstreetmap.josm.command.DeleteCommand;
25import org.openstreetmap.josm.command.SequenceCommand;
26import org.openstreetmap.josm.data.osm.DataSet;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.Relation;
30import org.openstreetmap.josm.data.osm.RelationMember;
31import org.openstreetmap.josm.data.osm.Tag;
32import org.openstreetmap.josm.data.osm.TagCollection;
33import org.openstreetmap.josm.data.osm.Way;
34import org.openstreetmap.josm.gui.ExtendedDialog;
35import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
36import org.openstreetmap.josm.tools.Pair;
37import org.openstreetmap.josm.tools.Shortcut;
38
39/**
40 * Combines multiple ways into one.
41 *
42 */
43public class CombineWayAction extends JosmAction {
44
45 public CombineWayAction() {
46 super(tr("Combine Way"), "combineway", tr("Combine several ways into one."),
47 Shortcut.registerShortcut("tools:combineway", tr("Tool: {0}", tr("Combine Way")), KeyEvent.VK_C, Shortcut.GROUP_EDIT), true);
48 }
49
50 protected Set<OsmPrimitive> intersect(Set<? extends OsmPrimitive> s1, Set<? extends OsmPrimitive> s2) {
51 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>(s1);
52 ret.retainAll(s2);
53 return ret;
54 }
55
56 protected boolean confirmCombiningWithConflictsInRelationMemberships() {
57 ExtendedDialog ed = new ExtendedDialog(Main.parent,
58 tr("Combine ways with different memberships?"),
59 new String[] {tr("Combine Anyway"), tr("Cancel")});
60 ed.setButtonIcons(new String[] {"combineway.png", "cancel.png"});
61 ed.setContent(tr("The selected ways have differing relation memberships. "
62 + "Do you still want to combine them?"));
63 ed.showDialog();
64
65 return ed.getValue() == 1;
66 }
67
68 protected boolean confirmChangeDirectionOfWays() {
69 ExtendedDialog ed = new ExtendedDialog(Main.parent,
70 tr("Change directions?"),
71 new String[] {tr("Reverse and Combine"), tr("Cancel")});
72 ed.setButtonIcons(new String[] {"wayflip.png", "cancel.png"});
73 ed.setContent(tr("The ways can not be combined in their current directions. "
74 + "Do you want to reverse some of them?"));
75 ed.showDialog();
76 return ed.getValue() == 1;
77 }
78
79 protected void warnCombiningImpossible() {
80 String msg = tr("Could not combine ways "
81 + "(They could not be merged into a single string of nodes)");
82 JOptionPane.showMessageDialog(
83 Main.parent,
84 msg, //FIXME: not sure whether this fits in a dialog
85 tr("Information"),
86 JOptionPane.INFORMATION_MESSAGE
87 );
88 return;
89 }
90
91 protected Way getTargetWay(Collection<Way> combinedWays) {
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;
99 if (w.getId() != 0) {
100 break;
101 }
102 }
103 return targetWay;
104 }
105
106 protected void completeTagCollectionWithMissingTags(TagCollection tc, Collection<Way> combinedWays) {
107 for (String key: tc.getKeys()) {
108 // make sure the empty value is in the tag set such that we can delete the tag
109 // in the conflict dialog if necessary
110 //
111 tc.add(new Tag(key,""));
112 for (Way w: combinedWays) {
113 if (w.get(key) == null) {
114 tc.add(new Tag(key)); // add a tag with key and empty value
115 }
116 }
117 }
118 // remove irrelevant tags
119 //
120 tc.removeByKey("created_by");
121 }
122
123 public void combineWays(Collection<Way> ways) {
124
125 // prepare and clean the list of ways to combine
126 //
127 if (ways == null || ways.isEmpty())
128 return;
129 ways.remove(null); // just in case - remove all null ways from the collection
130 ways = new HashSet<Way>(ways); // remove duplicates
131
132 // build the list of relations referring to the ways to combine
133 //
134 WayReferringRelations referringRelations = new WayReferringRelations(ways);
135 referringRelations.build(getCurrentDataSet());
136
137 // build the collection of tags used by the ways to combine
138 //
139 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
140
141
142 // try to build a new way out of the combination of ways
143 // which are combined
144 //
145 NodeGraph graph = NodeGraph.createDirectedGraphFromWays(ways);
146 List<Node> path = graph.buildSpanningPath();
147 if (path == null) {
148 graph = NodeGraph.createUndirectedGraphFromNodeWays(ways);
149 path = graph.buildSpanningPath();
150 if (path != null) {
151 if (!confirmChangeDirectionOfWays())
152 return;
153 } else {
154 warnCombiningImpossible();
155 return;
156 }
157 }
158
159 // create the new way and apply the new node list
160 //
161 Way targetWay = getTargetWay(ways);
162 Way modifiedTargetWay = new Way(targetWay);
163 modifiedTargetWay.setNodes(path);
164
165 TagCollection completeWayTags = new TagCollection(wayTags);
166 completeTagCollectionWithMissingTags(completeWayTags, ways);
167
168 CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
169 dialog.getTagConflictResolverModel().populate(completeWayTags);
170 dialog.setTargetPrimitive(targetWay);
171 dialog.getRelationMemberConflictResolverModel().populate(
172 referringRelations.getRelations(),
173 referringRelations.getWays()
174 );
175 dialog.prepareDefaultDecisions();
176
177 // resolve tag conflicts if necessary
178 //
179 if (!wayTags.isApplicableToPrimitive() || !referringRelations.getRelations().isEmpty()) {
180 dialog.setVisible(true);
181 if (dialog.isCancelled())
182 return;
183 }
184
185
186
187 LinkedList<Command> cmds = new LinkedList<Command>();
188 LinkedList<Way> deletedWays = new LinkedList<Way>(ways);
189 deletedWays.remove(targetWay);
190
191 cmds.add(new DeleteCommand(deletedWays));
192 cmds.add(new ChangeCommand(targetWay, modifiedTargetWay));
193 cmds.addAll(dialog.buildResolutionCommands());
194 final SequenceCommand sequenceCommand = new SequenceCommand(tr("Combine {0} ways", ways.size()), cmds);
195
196 // update gui
197 final Way selectedWay = targetWay;
198 Runnable guiTask = new Runnable() {
199 public void run() {
200 Main.main.undoRedo.add(sequenceCommand);
201 getCurrentDataSet().setSelected(selectedWay);
202 }
203 };
204 if (SwingUtilities.isEventDispatchThread()) {
205 guiTask.run();
206 } else {
207 SwingUtilities.invokeLater(guiTask);
208 }
209 }
210
211
212 public void actionPerformed(ActionEvent event) {
213 if (getCurrentDataSet() == null)
214 return;
215 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
216 Set<Way> selectedWays = OsmPrimitive.getFilteredSet(selection, Way.class);
217 if (selectedWays.size() < 2) {
218 JOptionPane.showMessageDialog(
219 Main.parent,
220 tr("Please select at least two ways to combine."),
221 tr("Information"),
222 JOptionPane.INFORMATION_MESSAGE
223 );
224 return;
225 }
226 combineWays(selectedWays);
227 }
228
229 @Override
230 protected void updateEnabledState() {
231 if (getCurrentDataSet() == null) {
232 setEnabled(false);
233 return;
234 }
235 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
236 int numWays = 0;
237
238 for (OsmPrimitive osm : selection)
239 if (osm instanceof Way) {
240 numWays++;
241 }
242 setEnabled(numWays >= 2);
243 }
244
245 /**
246 * This is a collection of relations referring to at least one out of a set of
247 * ways.
248 *
249 *
250 */
251 static private class WayReferringRelations {
252 /**
253 * the map references between relations and ways. The key is a ways, the value is a
254 * set of relations referring to that way.
255 */
256 private Map<Way, Set<Relation>> wayRelationMap;
257
258 /**
259 *
260 * @param ways a collection of ways
261 */
262 public WayReferringRelations(Collection<Way> ways) {
263 wayRelationMap = new HashMap<Way, Set<Relation>>();
264 if (ways == null) return;
265 ways.remove(null); // just in case - remove null values
266 for (Way way: ways) {
267 if (!wayRelationMap.containsKey(way)) {
268 wayRelationMap.put(way, new HashSet<Relation>());
269 }
270 }
271 }
272
273 /**
274 * build the sets of referring relations from the relations in the dataset <code>ds</code>
275 *
276 * @param ds the data set
277 */
278 public void build(DataSet ds) {
279 for (Relation r: ds.relations) {
280 if (r.isDeleted() || r.incomplete) {
281 continue;
282 }
283 Set<Way> referringWays = OsmPrimitive.getFilteredSet(r.getMemberPrimitives(), Way.class);
284 for (Way w : wayRelationMap.keySet()) {
285 if (referringWays.contains(w)) {
286 wayRelationMap.get(w).add(r);
287 }
288 }
289 }
290 }
291
292 /**
293 * Replies the ways
294 * @return the ways
295 */
296 public Set<Way> getWays() {
297 return wayRelationMap.keySet();
298 }
299
300 /**
301 * Replies the set of referring relations
302 *
303 * @return the set of referring relations
304 */
305 public Set<Relation> getRelations() {
306 HashSet<Relation> ret = new HashSet<Relation>();
307 for (Way w: wayRelationMap.keySet()) {
308 ret.addAll(wayRelationMap.get(w));
309 }
310 return ret;
311 }
312
313 /**
314 * Replies the set of referring relations for a specific way
315 *
316 * @return the set of referring relations
317 */
318 public Set<Relation> getRelations(Way way) {
319 return wayRelationMap.get(way);
320 }
321
322 protected Command buildRelationUpdateCommand(Relation relation, Collection<Way> ways, Way targetWay) {
323 List<RelationMember> newMembers = new ArrayList<RelationMember>();
324 for (RelationMember rm : relation.getMembers()) {
325 if (ways.contains(rm.getMember())) {
326 RelationMember newMember = new RelationMember(rm.getRole(),targetWay);
327 newMembers.add(newMember);
328 } else {
329 newMembers.add(rm);
330 }
331 }
332 Relation newRelation = new Relation(relation);
333 newRelation.setMembers(newMembers);
334 return new ChangeCommand(relation, newRelation);
335 }
336
337 public List<Command> buildRelationUpdateCommands(Way targetWay) {
338 Collection<Way> toRemove = getWays();
339 toRemove.remove(targetWay);
340 ArrayList<Command> cmds = new ArrayList<Command>();
341 for (Relation r : getRelations()) {
342 Command cmd = buildRelationUpdateCommand(r, toRemove, targetWay);
343 cmds.add(cmd);
344 }
345 return cmds;
346 }
347 }
348
349 static public class NodePair {
350 private Node a;
351 private Node b;
352 public NodePair(Node a, Node b) {
353 this.a =a;
354 this.b = b;
355 }
356
357 public NodePair(Pair<Node,Node> pair) {
358 this.a = pair.a;
359 this.b = pair.b;
360 }
361
362 public NodePair(NodePair other) {
363 this.a = other.a;
364 this.b = other.b;
365 }
366
367 public Node getA() {
368 return a;
369 }
370
371 public Node getB() {
372 return b;
373 }
374
375 public boolean isAdjacentToA(NodePair other) {
376 return other.getA() == a || other.getB() == a;
377 }
378
379 public boolean isAdjacentToB(NodePair other) {
380 return other.getA() == b || other.getB() == b;
381 }
382
383 public boolean isSuccessorOf(NodePair other) {
384 return other.getB() == a;
385 }
386
387 public boolean isPredecessorOf(NodePair other) {
388 return b == other.getA();
389 }
390
391 public NodePair swap() {
392 return new NodePair(b,a);
393 }
394
395 @Override
396 public String toString() {
397 return new StringBuilder()
398 .append("[")
399 .append(a.getId())
400 .append(",")
401 .append(b.getId())
402 .append("]")
403 .toString();
404 }
405
406 public boolean contains(Node n) {
407 return a == n || b == n;
408 }
409
410 @Override
411 public int hashCode() {
412 final int prime = 31;
413 int result = 1;
414 result = prime * result + ((a == null) ? 0 : a.hashCode());
415 result = prime * result + ((b == null) ? 0 : b.hashCode());
416 return result;
417 }
418 @Override
419 public boolean equals(Object obj) {
420 if (this == obj)
421 return true;
422 if (obj == null)
423 return false;
424 if (getClass() != obj.getClass())
425 return false;
426 NodePair other = (NodePair) obj;
427 if (a == null) {
428 if (other.a != null)
429 return false;
430 } else if (!a.equals(other.a))
431 return false;
432 if (b == null) {
433 if (other.b != null)
434 return false;
435 } else if (!b.equals(other.b))
436 return false;
437 return true;
438 }
439 }
440
441
442 static public class NodeGraph {
443 static public List<NodePair> buildNodePairs(Way way, boolean directed) {
444 ArrayList<NodePair> pairs = new ArrayList<NodePair>();
445 for (Pair<Node,Node> pair: way.getNodePairs(false /* don't sort */)) {
446 pairs.add(new NodePair(pair));
447 if (!directed) {
448 pairs.add(new NodePair(pair).swap());
449 }
450 }
451 return pairs;
452 }
453
454 static public List<NodePair> buildNodePairs(List<Way> ways, boolean directed) {
455 ArrayList<NodePair> pairs = new ArrayList<NodePair>();
456 for (Way w: ways) {
457 pairs.addAll(buildNodePairs(w, directed));
458 }
459 return pairs;
460 }
461
462 static public List<NodePair> eliminateDuplicateNodePairs(List<NodePair> pairs) {
463 ArrayList<NodePair> cleaned = new ArrayList<NodePair>();
464 for(NodePair p: pairs) {
465 if (!cleaned.contains(p) && !cleaned.contains(p.swap())) {
466 cleaned.add(p);
467 }
468 }
469 return cleaned;
470 }
471
472 static public NodeGraph createDirectedGraphFromNodePairs(List<NodePair> pairs) {
473 NodeGraph graph = new NodeGraph();
474 for (NodePair pair: pairs) {
475 graph.add(pair);
476 }
477 return graph;
478 }
479
480 static public NodeGraph createDirectedGraphFromWays(Collection<Way> ways) {
481 NodeGraph graph = new NodeGraph();
482 for (Way w: ways) {
483 graph.add(buildNodePairs(w, true /* directed */));
484 }
485 return graph;
486 }
487
488 static public NodeGraph createUndirectedGraphFromNodeList(List<NodePair> pairs) {
489 NodeGraph graph = new NodeGraph();
490 for (NodePair pair: pairs) {
491 graph.add(pair);
492 graph.add(pair.swap());
493 }
494 return graph;
495 }
496
497 static public NodeGraph createUndirectedGraphFromNodeWays(Collection<Way> ways) {
498 NodeGraph graph = new NodeGraph();
499 for (Way w: ways) {
500 graph.add(buildNodePairs(w, false /* undirected */));
501 }
502 return graph;
503 }
504
505 private Set<NodePair> edges;
506 private int numUndirectedEges = 0;
507
508 protected void computeNumEdges() {
509 Set<NodePair> undirectedEdges = new HashSet<NodePair>();
510 for (NodePair pair: edges) {
511 if (!undirectedEdges.contains(pair) && ! undirectedEdges.contains(pair.swap())) {
512 undirectedEdges.add(pair);
513 }
514 }
515 numUndirectedEges = undirectedEdges.size();
516 }
517
518 public NodeGraph() {
519 edges = new HashSet<NodePair>();
520 }
521
522 public void add(NodePair pair) {
523 if (!edges.contains(pair)) {
524 edges.add(pair);
525 }
526 }
527
528 public void add(List<NodePair> pairs) {
529 for (NodePair pair: pairs) {
530 add(pair);
531 }
532 }
533
534 protected Node getStartNode() {
535 return edges.iterator().next().getA();
536 }
537
538 protected Set<Node> getNodes(Stack<NodePair> pairs) {
539 HashSet<Node> nodes = new HashSet<Node>();
540 for (NodePair pair: pairs) {
541 nodes.add(pair.getA());
542 nodes.add(pair.getB());
543 }
544 return nodes;
545 }
546
547 protected List<NodePair> getOutboundPairs(NodePair pair) {
548 LinkedList<NodePair> outbound = new LinkedList<NodePair>();
549 for (NodePair candidate:edges) {
550 if (candidate.equals(pair)) {
551 continue;
552 }
553 if (candidate.isSuccessorOf(pair)) {
554 outbound.add(candidate);
555 }
556 }
557 return outbound;
558 }
559
560 protected List<NodePair> getOutboundPairs(Node node) {
561 LinkedList<NodePair> outbound = new LinkedList<NodePair>();
562 for (NodePair candidate:edges) {
563 if (candidate.getA() == node) {
564 outbound.add(candidate);
565 }
566 }
567 return outbound;
568 }
569
570 protected Set<Node> getNodes() {
571 Set<Node> nodes = new HashSet<Node>();
572 for (NodePair pair: edges) {
573 nodes.add(pair.getA());
574 nodes.add(pair.getB());
575 }
576 return nodes;
577 }
578
579 protected boolean isSpanningWay(Stack<NodePair> way) {
580 return numUndirectedEges == way.size();
581 }
582
583
584 protected boolean advance(Stack<NodePair> path) {
585 // found a spanning path ?
586 //
587 if (isSpanningWay(path))
588 return true;
589
590 // advance with one of the possible follow up nodes
591 //
592 Stack<NodePair> nextPairs = new Stack<NodePair>();
593 nextPairs.addAll(getOutboundPairs(path.peek()));
594 while(!nextPairs.isEmpty()) {
595 NodePair next = nextPairs.pop();
596 if (path.contains(next) || path.contains(next.swap())) {
597 continue;
598 }
599 path.push(next);
600 if (advance(path)) return true;
601 path.pop();
602 }
603 return false;
604 }
605
606 protected List<Node> buildPathFromNodePairs(Stack<NodePair> path) {
607 LinkedList<Node> ret = new LinkedList<Node>();
608 for (NodePair pair: path) {
609 ret.add(pair.getA());
610 }
611 ret.add(path.peek().getB());
612 return ret;
613 }
614
615 protected List<Node> buildSpanningPath(Node startNode) {
616 if (startNode == null)
617 return null;
618 Stack<NodePair> path = new Stack<NodePair>();
619 // advance with one of the possible follow up nodes
620 //
621 Stack<NodePair> nextPairs = new Stack<NodePair>();
622 nextPairs.addAll(getOutboundPairs(startNode));
623 while(!nextPairs.isEmpty()) {
624 path.push(nextPairs.pop());
625 if (advance(path))
626 return buildPathFromNodePairs(path);
627 path.pop();
628 }
629 return null;
630 }
631
632 public List<Node> buildSpanningPath() {
633 computeNumEdges();
634 for (Node n : getNodes()) {
635 List<Node> path = buildSpanningPath(n);
636 if (path != null)
637 return path;
638 }
639 return null;
640 }
641 }
642}
Note: See TracBrowser for help on using the repository browser.