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

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

Part one of patch by Dave Hansen <dave@…>

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