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

Last change on this file since 1862 was 1862, checked in by jttt, 15 years ago

Way refactoring - added method that will in future replace public field nodes

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