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

Last change on this file since 734 was 734, checked in by stoecker, 16 years ago

No longer make a created_by conflict for way or node joining. In case of conflicts use JOSM as new value.

  • Property svn:eol-style set to native
File size: 9.6 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.GridBagLayout;
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.util.Collection;
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.ListIterator;
15import java.util.Map;
16import java.util.Set;
17import java.util.TreeMap;
18import java.util.TreeSet;
19import java.util.Map.Entry;
20
21import javax.swing.Box;
22import javax.swing.JComboBox;
23import javax.swing.JLabel;
24import javax.swing.JOptionPane;
25import javax.swing.JPanel;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.command.ChangeCommand;
29import org.openstreetmap.josm.command.Command;
30import org.openstreetmap.josm.command.DeleteCommand;
31import org.openstreetmap.josm.command.SequenceCommand;
32import org.openstreetmap.josm.data.SelectionChangedListener;
33import org.openstreetmap.josm.data.osm.DataSet;
34import org.openstreetmap.josm.data.osm.Node;
35import org.openstreetmap.josm.data.osm.OsmPrimitive;
36import org.openstreetmap.josm.data.osm.Relation;
37import org.openstreetmap.josm.data.osm.RelationMember;
38import org.openstreetmap.josm.data.osm.TigerUtils;
39import org.openstreetmap.josm.data.osm.Way;
40import org.openstreetmap.josm.tools.GBC;
41import org.openstreetmap.josm.tools.Pair;
42
43/**
44 * Combines multiple ways into one.
45 *
46 * @author Imi
47 */
48public class CombineWayAction extends JosmAction implements SelectionChangedListener {
49
50 public CombineWayAction() {
51 super(tr("Combine Way"), "combineway", tr("Combine several ways into one."), KeyEvent.VK_C, 0, true);
52 DataSet.selListeners.add(this);
53 }
54
55 public void actionPerformed(ActionEvent event) {
56 Collection<OsmPrimitive> selection = Main.ds.getSelected();
57 LinkedList<Way> selectedWays = new LinkedList<Way>();
58
59 for (OsmPrimitive osm : selection)
60 if (osm instanceof Way)
61 selectedWays.add((Way)osm);
62
63 if (selectedWays.size() < 2) {
64 JOptionPane.showMessageDialog(Main.parent, tr("Please select at least two ways to combine."));
65 return;
66 }
67
68 // Check whether all ways have identical relationship membership. More
69 // specifically: If one of the selected ways is a member of relation X
70 // in role Y, then all selected ways must be members of X in role Y.
71
72 // FIXME: In a later revision, we should display some sort of conflict
73 // dialog like we do for tags, to let the user choose which relations
74 // should be kept.
75
76 // Step 1, iterate over all relations and figure out which of our
77 // selected ways are members of a relation.
78 HashMap<Pair<Relation,String>, HashSet<Way>> backlinks =
79 new HashMap<Pair<Relation,String>, HashSet<Way>>();
80 HashSet<Relation> relationsUsingWays = new HashSet<Relation>();
81 for (Relation r : Main.ds.relations) {
82 if (r.deleted || r.incomplete) continue;
83 for (RelationMember rm : r.members) {
84 if (rm.member instanceof Way) {
85 for(Way w : selectedWays) {
86 if (rm.member == w) {
87 Pair<Relation,String> pair = new Pair<Relation,String>(r, rm.role);
88 HashSet<Way> waylinks = new HashSet<Way>();
89 if (backlinks.containsKey(pair)) {
90 waylinks = backlinks.get(pair);
91 } else {
92 waylinks = new HashSet<Way>();
93 backlinks.put(pair, waylinks);
94 }
95 waylinks.add(w);
96
97 // this is just a cache for later use
98 relationsUsingWays.add(r);
99 }
100 }
101 }
102 }
103 }
104
105 // Complain to the user if the ways don't have equal memberships.
106 for (HashSet<Way> waylinks : backlinks.values()) {
107 if (!waylinks.containsAll(selectedWays)) {
108 int option = JOptionPane.showConfirmDialog(Main.parent,
109 tr("The selected ways have differing relation memberships. "
110 + "Do you still want to combine them?"),
111 tr("Combine ways with different memberships?"),
112 JOptionPane.YES_NO_OPTION);
113 if (option == JOptionPane.YES_OPTION)
114 break;
115 return;
116 }
117 }
118
119 // collect properties for later conflict resolving
120 Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
121 for (Way w : selectedWays) {
122 for (Entry<String,String> e : w.entrySet()) {
123 if (!props.containsKey(e.getKey()))
124 props.put(e.getKey(), new TreeSet<String>());
125 props.get(e.getKey()).add(e.getValue());
126 }
127 }
128
129 List<Node> nodeList = null;
130 Object firstTry = actuallyCombineWays(selectedWays, false);
131 if (firstTry instanceof List) {
132 nodeList = (List<Node>) firstTry;
133 } else {
134 Object secondTry = actuallyCombineWays(selectedWays, true);
135 if (secondTry instanceof List) {
136 int option = JOptionPane.showConfirmDialog(Main.parent,
137 tr("The ways can not be combined in their current directions. "
138 + "Do you want to reverse some of them?"), tr("Change directions?"),
139 JOptionPane.YES_NO_OPTION);
140 if (option != JOptionPane.YES_OPTION) {
141 return;
142 }
143 nodeList = (List<Node>) secondTry;
144 } else {
145 JOptionPane.showMessageDialog(Main.parent, secondTry);
146 return;
147 }
148 }
149
150 // Find the most appropriate way to modify.
151
152 // Eventually this might want to be the way with the longest
153 // history or the longest selected way but for now just attempt
154 // to reuse an existing id.
155 Way modifyWay = selectedWays.peek();
156 for (Way w : selectedWays) {
157 modifyWay = w;
158 if (w.id != 0) break;
159 }
160 Way newWay = new Way(modifyWay);
161
162 newWay.nodes.clear();
163 newWay.nodes.addAll(nodeList);
164
165 // display conflict dialog
166 Map<String, JComboBox> components = new HashMap<String, JComboBox>();
167 JPanel p = new JPanel(new GridBagLayout());
168 for (Entry<String, Set<String>> e : props.entrySet()) {
169 if (TigerUtils.isTigerTag(e.getKey())) {
170 String combined = TigerUtils.combineTags(e.getKey(), e.getValue());
171 newWay.put(e.getKey(), combined);
172 } else if (e.getValue().size() > 1) {
173 if("created_by".equals(e.getKey()))
174 {
175 newWay.put("created_by", "JOSM");
176 }
177 else
178 {
179 JComboBox c = new JComboBox(e.getValue().toArray());
180 c.setEditable(true);
181 p.add(new JLabel(e.getKey()), GBC.std());
182 p.add(Box.createHorizontalStrut(10), GBC.std());
183 p.add(c, GBC.eol());
184 components.put(e.getKey(), c);
185 }
186 } else
187 newWay.put(e.getKey(), e.getValue().iterator().next());
188 }
189
190 if (!components.isEmpty()) {
191 int answer = JOptionPane.showConfirmDialog(Main.parent, p, tr("Enter values for all conflicts."), JOptionPane.OK_CANCEL_OPTION);
192 if (answer != JOptionPane.OK_OPTION)
193 return;
194 for (Entry<String, JComboBox> e : components.entrySet())
195 newWay.put(e.getKey(), e.getValue().getEditor().getItem().toString());
196 }
197
198 LinkedList<Command> cmds = new LinkedList<Command>();
199 LinkedList<Way> deletedWays = new LinkedList<Way>(selectedWays);
200 deletedWays.remove(modifyWay);
201 cmds.add(new DeleteCommand(deletedWays));
202 cmds.add(new ChangeCommand(modifyWay, newWay));
203
204 // modify all relations containing the now-deleted ways
205 for (Relation r : relationsUsingWays) {
206 Relation newRel = new Relation(r);
207 newRel.members.clear();
208 HashSet<String> rolesToReAdd = new HashSet<String>();
209 for (RelationMember rm : r.members) {
210 // Don't copy the member if it to one of our ways, just keep a
211 // note to re-add it later on.
212 if (selectedWays.contains(rm.member)) {
213 rolesToReAdd.add(rm.role);
214 } else {
215 newRel.members.add(rm);
216 }
217 }
218 for (String role : rolesToReAdd) {
219 newRel.members.add(new RelationMember(role, modifyWay));
220 }
221 cmds.add(new ChangeCommand(r, newRel));
222 }
223 Main.main.undoRedo.add(new SequenceCommand(tr("Combine {0} ways", selectedWays.size()), cmds));
224 Main.ds.setSelected(modifyWay);
225 }
226
227 /**
228 * @return a message if combining failed, else a list of nodes.
229 */
230 private Object actuallyCombineWays(List<Way> ways, boolean ignoreDirection) {
231 // Battle plan:
232 // 1. Split the ways into small chunks of 2 nodes and weed out
233 // duplicates.
234 // 2. Take a chunk and see if others could be appended or prepended,
235 // if so, do it and remove it from the list of remaining chunks.
236 // Rather, rinse, repeat.
237 // 3. If this algorithm does not produce a single way,
238 // complain to the user.
239 // 4. Profit!
240
241 HashSet<Pair<Node,Node>> chunkSet = new HashSet<Pair<Node,Node>>();
242 for (Way w : ways)
243 chunkSet.addAll(w.getNodePairs(ignoreDirection));
244
245 LinkedList<Pair<Node,Node>> chunks = new LinkedList<Pair<Node,Node>>(chunkSet);
246
247 if (chunks.isEmpty()) {
248 return tr("All the ways were empty");
249 }
250
251 List<Node> nodeList = Pair.toArrayList(chunks.poll());
252 while (!chunks.isEmpty()) {
253 ListIterator<Pair<Node,Node>> it = chunks.listIterator();
254 boolean foundChunk = false;
255 while (it.hasNext()) {
256 Pair<Node,Node> curChunk = it.next();
257 if (curChunk.a == nodeList.get(nodeList.size() - 1)) { // append
258 nodeList.add(curChunk.b);
259 } else if (curChunk.b == nodeList.get(0)) { // prepend
260 nodeList.add(0, curChunk.a);
261 } else if (ignoreDirection && curChunk.b == nodeList.get(nodeList.size() - 1)) { // append
262 nodeList.add(curChunk.a);
263 } else if (ignoreDirection && curChunk.a == nodeList.get(0)) { // prepend
264 nodeList.add(0, curChunk.b);
265 } else {
266 continue;
267 }
268
269 foundChunk = true;
270 it.remove();
271 break;
272 }
273 if (!foundChunk) break;
274 }
275
276 if (!chunks.isEmpty()) {
277 return tr("Could not combine ways "
278 + "(They could not be merged into a single string of nodes)");
279 }
280
281 return nodeList;
282 }
283
284 /**
285 * Enable the "Combine way" menu option if more then one way is selected
286 */
287 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
288 boolean first = false;
289 for (OsmPrimitive osm : newSelection) {
290 if (osm instanceof Way) {
291 if (first) {
292 setEnabled(true);
293 return;
294 }
295 first = true;
296 }
297 }
298 setEnabled(false);
299 }
300}
Note: See TracBrowser for help on using the repository browser.