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

Last change on this file since 1530 was 1530, checked in by framm, 15 years ago

upload more precise version number in changeset. drop some "created_by" special cases.

  • Property svn:eol-style set to native
File size: 12.0 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 = Main.ds.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 if (selectedWays.size() < 2) {
67 JOptionPane.showMessageDialog(Main.parent, tr("Please select at least two ways to combine."));
68 return;
69 }
70
71 // Check whether all ways have identical relationship membership. More
72 // specifically: If one of the selected ways is a member of relation X
73 // in role Y, then all selected ways must be members of X in role Y.
74
75 // FIXME: In a later revision, we should display some sort of conflict
76 // dialog like we do for tags, to let the user choose which relations
77 // should be kept.
78
79 // Step 1, iterate over all relations and figure out which of our
80 // selected ways are members of a relation.
81 HashMap<Pair<Relation,String>, HashSet<Way>> backlinks =
82 new HashMap<Pair<Relation,String>, HashSet<Way>>();
83 HashSet<Relation> relationsUsingWays = new HashSet<Relation>();
84 for (Relation r : Main.ds.relations) {
85 if (r.deleted || r.incomplete) continue;
86 for (RelationMember rm : r.members) {
87 if (rm.member instanceof Way) {
88 for(Way w : selectedWays) {
89 if (rm.member == w) {
90 Pair<Relation,String> pair = new Pair<Relation,String>(r, rm.role);
91 HashSet<Way> waylinks = new HashSet<Way>();
92 if (backlinks.containsKey(pair)) {
93 waylinks = backlinks.get(pair);
94 } else {
95 waylinks = new HashSet<Way>();
96 backlinks.put(pair, waylinks);
97 }
98 waylinks.add(w);
99
100 // this is just a cache for later use
101 relationsUsingWays.add(r);
102 }
103 }
104 }
105 }
106 }
107
108 // Complain to the user if the ways don't have equal memberships.
109 for (HashSet<Way> waylinks : backlinks.values()) {
110 if (!waylinks.containsAll(selectedWays)) {
111 int option = new ExtendedDialog(Main.parent,
112 tr("Combine ways with different memberships?"),
113 tr("The selected ways have differing relation memberships. "
114 + "Do you still want to combine them?"),
115 new String[] {tr("Combine Anyway"), tr("Cancel")},
116 new String[] {"combineway.png", "cancel.png"}).getValue();
117 if (option == 1) break;
118
119 return;
120 }
121 }
122
123 // collect properties for later conflict resolving
124 Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
125 for (Way w : selectedWays) {
126 for (Entry<String,String> e : w.entrySet()) {
127 if (!props.containsKey(e.getKey()))
128 props.put(e.getKey(), new TreeSet<String>());
129 props.get(e.getKey()).add(e.getValue());
130 }
131 }
132
133 List<Node> nodeList = null;
134 Object firstTry = actuallyCombineWays(selectedWays, false);
135 if (firstTry instanceof List) {
136 nodeList = (List<Node>) firstTry;
137 } else {
138 Object secondTry = actuallyCombineWays(selectedWays, true);
139 if (secondTry instanceof List) {
140 int option = new ExtendedDialog(Main.parent,
141 tr("Change directions?"),
142 tr("The ways can not be combined in their current directions. "
143 + "Do you want to reverse some of them?"),
144 new String[] {tr("Reverse and Combine"), tr("Cancel")},
145 new String[] {"wayflip.png", "cancel.png"}).getValue();
146 if (option != 1) return;
147 nodeList = (List<Node>) secondTry;
148 } else {
149 JOptionPane.showMessageDialog(Main.parent, secondTry);
150 return;
151 }
152 }
153
154 // Find the most appropriate way to modify.
155
156 // Eventually this might want to be the way with the longest
157 // history or the longest selected way but for now just attempt
158 // to reuse an existing id.
159 Way modifyWay = selectedWays.peek();
160 for (Way w : selectedWays) {
161 modifyWay = w;
162 if (w.id != 0) break;
163 }
164 Way newWay = new Way(modifyWay);
165
166 newWay.nodes.clear();
167 newWay.nodes.addAll(nodeList);
168
169 // display conflict dialog
170 Map<String, JComboBox> components = new HashMap<String, JComboBox>();
171 JPanel p = new JPanel(new GridBagLayout());
172 for (Entry<String, Set<String>> e : props.entrySet()) {
173 if (TigerUtils.isTigerTag(e.getKey())) {
174 String combined = TigerUtils.combineTags(e.getKey(), e.getValue());
175 newWay.put(e.getKey(), combined);
176 } else if (e.getValue().size() > 1) {
177 JComboBox c = new JComboBox(e.getValue().toArray());
178 c.setEditable(true);
179 p.add(new JLabel(e.getKey()), GBC.std());
180 p.add(Box.createHorizontalStrut(10), GBC.std());
181 p.add(c, GBC.eol());
182 components.put(e.getKey(), c);
183 } else {
184 newWay.put(e.getKey(), e.getValue().iterator().next());
185 }
186 }
187
188 if (!components.isEmpty()) {
189 int answer = new ExtendedDialog(Main.parent,
190 tr("Enter values for all conflicts."),
191 p,
192 new String[] {tr("Solve Conflicts"), tr("Cancel")},
193 new String[] {"dialogs/conflict.png", "cancel.png"}).getValue();
194 if (answer != 1) return;
195
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.