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

Last change on this file since 1180 was 1169, checked in by stoecker, 15 years ago

removed usage of tab stops

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