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

Last change on this file since 1814 was 1814, checked in by Gubaer, 15 years ago

removed dependencies to Main.ds, removed Main.ds
removed AddVisitor, NameVisitor, DeleteVisitor - unnecessary double dispatching for these simple cases

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