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

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

removed OptionPaneUtil
cleanup of deprecated Layer API
cleanup of deprecated APIs in OsmPrimitive and Way
cleanup of imports

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