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

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

added patch by Andy Street to reduce creating new ways when unnecessary. Fixes #553.

  • Property svn:eol-style set to native
File size: 9.5 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 JComboBox c = new JComboBox(e.getValue().toArray());
174 c.setEditable(true);
175 p.add(new JLabel(e.getKey()), GBC.std());
176 p.add(Box.createHorizontalStrut(10), GBC.std());
177 p.add(c, GBC.eol());
178 components.put(e.getKey(), c);
179 } else
180 newWay.put(e.getKey(), e.getValue().iterator().next());
181 }
182
183 if (!components.isEmpty()) {
184 int answer = JOptionPane.showConfirmDialog(Main.parent, p, tr("Enter values for all conflicts."), JOptionPane.OK_CANCEL_OPTION);
185 if (answer != JOptionPane.OK_OPTION)
186 return;
187 for (Entry<String, JComboBox> e : components.entrySet())
188 newWay.put(e.getKey(), e.getValue().getEditor().getItem().toString());
189 }
190
191 LinkedList<Command> cmds = new LinkedList<Command>();
192 LinkedList<Way> deletedWays = new LinkedList<Way>(selectedWays);
193 deletedWays.remove(modifyWay);
194 cmds.add(new DeleteCommand(deletedWays));
195 cmds.add(new ChangeCommand(modifyWay, newWay));
196
197 // modify all relations containing the now-deleted ways
198 for (Relation r : relationsUsingWays) {
199 Relation newRel = new Relation(r);
200 newRel.members.clear();
201 HashSet<String> rolesToReAdd = new HashSet<String>();
202 for (RelationMember rm : r.members) {
203 // Don't copy the member if it to one of our ways, just keep a
204 // note to re-add it later on.
205 if (selectedWays.contains(rm.member)) {
206 rolesToReAdd.add(rm.role);
207 } else {
208 newRel.members.add(rm);
209 }
210 }
211 for (String role : rolesToReAdd) {
212 newRel.members.add(new RelationMember(role, modifyWay));
213 }
214 cmds.add(new ChangeCommand(r, newRel));
215 }
216 Main.main.undoRedo.add(new SequenceCommand(tr("Combine {0} ways", selectedWays.size()), cmds));
217 Main.ds.setSelected(modifyWay);
218 }
219
220 /**
221 * @return a message if combining failed, else a list of nodes.
222 */
223 private Object actuallyCombineWays(List<Way> ways, boolean ignoreDirection) {
224 // Battle plan:
225 // 1. Split the ways into small chunks of 2 nodes and weed out
226 // duplicates.
227 // 2. Take a chunk and see if others could be appended or prepended,
228 // if so, do it and remove it from the list of remaining chunks.
229 // Rather, rinse, repeat.
230 // 3. If this algorithm does not produce a single way,
231 // complain to the user.
232 // 4. Profit!
233
234 HashSet<Pair<Node,Node>> chunkSet = new HashSet<Pair<Node,Node>>();
235 for (Way w : ways)
236 chunkSet.addAll(w.getNodePairs(ignoreDirection));
237
238 LinkedList<Pair<Node,Node>> chunks = new LinkedList<Pair<Node,Node>>(chunkSet);
239
240 if (chunks.isEmpty()) {
241 return tr("All the ways were empty");
242 }
243
244 List<Node> nodeList = Pair.toArrayList(chunks.poll());
245 while (!chunks.isEmpty()) {
246 ListIterator<Pair<Node,Node>> it = chunks.listIterator();
247 boolean foundChunk = false;
248 while (it.hasNext()) {
249 Pair<Node,Node> curChunk = it.next();
250 if (curChunk.a == nodeList.get(nodeList.size() - 1)) { // append
251 nodeList.add(curChunk.b);
252 } else if (curChunk.b == nodeList.get(0)) { // prepend
253 nodeList.add(0, curChunk.a);
254 } else if (ignoreDirection && curChunk.b == nodeList.get(nodeList.size() - 1)) { // append
255 nodeList.add(curChunk.a);
256 } else if (ignoreDirection && curChunk.a == nodeList.get(0)) { // prepend
257 nodeList.add(0, curChunk.b);
258 } else {
259 continue;
260 }
261
262 foundChunk = true;
263 it.remove();
264 break;
265 }
266 if (!foundChunk) break;
267 }
268
269 if (!chunks.isEmpty()) {
270 return tr("Could not combine ways "
271 + "(They could not be merged into a single string of nodes)");
272 }
273
274 return nodeList;
275 }
276
277 /**
278 * Enable the "Combine way" menu option if more then one way is selected
279 */
280 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
281 boolean first = false;
282 for (OsmPrimitive osm : newSelection) {
283 if (osm instanceof Way) {
284 if (first) {
285 setEnabled(true);
286 return;
287 }
288 first = true;
289 }
290 }
291 setEnabled(false);
292 }
293}
Note: See TracBrowser for help on using the repository browser.