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

Last change on this file since 487 was 469, checked in by gebner, 16 years ago

Remove ctrl+shift escapes from most actions.

File size: 9.3 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.LinkedList;
12import java.util.List;
13import java.util.ListIterator;
14import java.util.ArrayList;
15import java.util.Map;
16import java.util.Set;
17import java.util.TreeMap;
18import java.util.TreeSet;
19import java.util.Map.Entry;
20import java.util.HashSet;
21
22import javax.swing.Box;
23import javax.swing.JComboBox;
24import javax.swing.JLabel;
25import javax.swing.JOptionPane;
26import javax.swing.JPanel;
27
28import org.openstreetmap.josm.Main;
29import org.openstreetmap.josm.command.ChangeCommand;
30import org.openstreetmap.josm.command.Command;
31import org.openstreetmap.josm.command.DeleteCommand;
32import org.openstreetmap.josm.command.SequenceCommand;
33import org.openstreetmap.josm.data.SelectionChangedListener;
34import org.openstreetmap.josm.data.osm.DataSet;
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.Way;
39import org.openstreetmap.josm.data.osm.Node;
40import org.openstreetmap.josm.tools.Pair;
41import org.openstreetmap.josm.tools.GBC;
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 } else {
116 return;
117 }
118 }
119 }
120
121 // collect properties for later conflict resolving
122 Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
123 for (Way w : selectedWays) {
124 for (Entry<String,String> e : w.entrySet()) {
125 if (!props.containsKey(e.getKey()))
126 props.put(e.getKey(), new TreeSet<String>());
127 props.get(e.getKey()).add(e.getValue());
128 }
129 }
130
131 List<Node> nodeList = null;
132 Object firstTry = actuallyCombineWays(selectedWays, false);
133 if (firstTry instanceof List) {
134 nodeList = (List<Node>) firstTry;
135 } else {
136 Object secondTry = actuallyCombineWays(selectedWays, true);
137 if (secondTry instanceof List) {
138 int option = JOptionPane.showConfirmDialog(Main.parent,
139 tr("The ways can not be combined in their current directions. "
140 + "Do you want to reverse some of them?"), tr("Change directions?"),
141 JOptionPane.YES_NO_OPTION);
142 if (option != JOptionPane.YES_OPTION) {
143 return;
144 }
145 nodeList = (List<Node>) secondTry;
146 } else {
147 JOptionPane.showMessageDialog(Main.parent, (String) secondTry);
148 return;
149 }
150 }
151
152 Way newWay = new Way(selectedWays.get(0));
153 newWay.nodes.clear();
154 newWay.nodes.addAll(nodeList);
155
156 // display conflict dialog
157 Map<String, JComboBox> components = new HashMap<String, JComboBox>();
158 JPanel p = new JPanel(new GridBagLayout());
159 for (Entry<String, Set<String>> e : props.entrySet()) {
160 if (e.getValue().size() > 1) {
161 JComboBox c = new JComboBox(e.getValue().toArray());
162 c.setEditable(true);
163 p.add(new JLabel(e.getKey()), GBC.std());
164 p.add(Box.createHorizontalStrut(10), GBC.std());
165 p.add(c, GBC.eol());
166 components.put(e.getKey(), c);
167 } else
168 newWay.put(e.getKey(), e.getValue().iterator().next());
169 }
170
171 if (!components.isEmpty()) {
172 int answer = JOptionPane.showConfirmDialog(Main.parent, p, tr("Enter values for all conflicts."), JOptionPane.OK_CANCEL_OPTION);
173 if (answer != JOptionPane.OK_OPTION)
174 return;
175 for (Entry<String, JComboBox> e : components.entrySet())
176 newWay.put(e.getKey(), e.getValue().getEditor().getItem().toString());
177 }
178
179 LinkedList<Command> cmds = new LinkedList<Command>();
180 cmds.add(new DeleteCommand(selectedWays.subList(1, selectedWays.size())));
181 cmds.add(new ChangeCommand(selectedWays.peek(), newWay));
182
183 // modify all relations containing the now-deleted ways
184 for (Relation r : relationsUsingWays) {
185 Relation newRel = new Relation(r);
186 newRel.members.clear();
187 HashSet<String> rolesToReAdd = new HashSet<String>();
188 for (RelationMember rm : r.members) {
189 // Don't copy the member if it to one of our ways, just keep a
190 // note to re-add it later on.
191 if (selectedWays.contains(rm.member)) {
192 rolesToReAdd.add(rm.role);
193 } else {
194 newRel.members.add(rm);
195 }
196 }
197 for (String role : rolesToReAdd) {
198 newRel.members.add(new RelationMember(role, selectedWays.peek()));
199 }
200 cmds.add(new ChangeCommand(r, newRel));
201 }
202 Main.main.undoRedo.add(new SequenceCommand(tr("Combine {0} ways", selectedWays.size()), cmds));
203 Main.ds.setSelected(selectedWays.peek());
204 }
205
206 /**
207 * @return a message if combining failed, else a list of nodes.
208 */
209 private Object actuallyCombineWays(List<Way> ways, boolean ignoreDirection) {
210 // Battle plan:
211 // 1. Split the ways into small chunks of 2 nodes and weed out
212 // duplicates.
213 // 2. Take a chunk and see if others could be appended or prepended,
214 // if so, do it and remove it from the list of remaining chunks.
215 // Rather, rinse, repeat.
216 // 3. If this algorithm does not produce a single way,
217 // complain to the user.
218 // 4. Profit!
219
220 HashSet<Pair<Node,Node>> chunkSet = new HashSet<Pair<Node,Node>>();
221 for (Way w : ways) {
222 if (w.nodes.size() == 0) continue;
223 Node lastN = null;
224 for (Node n : w.nodes) {
225 if (lastN == null) {
226 lastN = n;
227 continue;
228 }
229
230 Pair<Node,Node> np = new Pair<Node,Node>(lastN, n);
231 if (ignoreDirection) {
232 Pair.sort(np);
233 }
234 chunkSet.add(np);
235
236 lastN = n;
237 }
238 }
239 LinkedList<Pair<Node,Node>> chunks = new LinkedList<Pair<Node,Node>>(chunkSet);
240
241 if (chunks.isEmpty()) {
242 return tr("All the ways were empty");
243 }
244
245 List<Node> nodeList = Pair.toArrayList(chunks.poll());
246 while (!chunks.isEmpty()) {
247 ListIterator<Pair<Node,Node>> it = chunks.listIterator();
248 boolean foundChunk = false;
249 while (it.hasNext()) {
250 Pair<Node,Node> curChunk = it.next();
251 if (curChunk.a == nodeList.get(nodeList.size() - 1)) { // append
252 nodeList.add(curChunk.b);
253 } else if (curChunk.b == nodeList.get(0)) { // prepend
254 nodeList.add(0, curChunk.a);
255 } else if (ignoreDirection && curChunk.b == nodeList.get(nodeList.size() - 1)) { // append
256 nodeList.add(curChunk.a);
257 } else if (ignoreDirection && curChunk.a == nodeList.get(0)) { // prepend
258 nodeList.add(0, curChunk.b);
259 } else {
260 continue;
261 }
262
263 foundChunk = true;
264 it.remove();
265 break;
266 }
267 if (!foundChunk) break;
268 }
269
270 if (!chunks.isEmpty()) {
271 return tr("Could not combine ways "
272 + "(They could not be merged into a single string of nodes)");
273 }
274
275 return nodeList;
276 }
277
278 /**
279 * Enable the "Combine way" menu option if more then one way is selected
280 */
281 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
282 boolean first = false;
283 for (OsmPrimitive osm : newSelection) {
284 if (osm instanceof Way) {
285 if (first) {
286 setEnabled(true);
287 return;
288 }
289 first = true;
290 }
291 }
292 setEnabled(false);
293 }
294}
Note: See TracBrowser for help on using the repository browser.