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

Last change on this file since 406 was 406, checked in by framm, 17 years ago
  • modify CombineWayAction not to allow merging of ways if they have different relation memberships (either different relations, or different roles in the same relation). Fixes bug #359
File size: 8.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.LinkedList;
12import java.util.List;
13import java.util.ListIterator;
14import java.util.ArrayList;
15import java.util.Map;
16import java.util.Set;
17import java.util.TreeMap;
18import java.util.TreeSet;
19import java.util.Map.Entry;
20import java.util.HashSet;
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.SelectionChangedListener;
34import org.openstreetmap.josm.data.osm.DataSet;
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.Way;
39import org.openstreetmap.josm.data.osm.Node;
40import org.openstreetmap.josm.data.osm.NodePair;
41import org.openstreetmap.josm.tools.GBC;
42
43/**
44 * Combines multiple ways into one.
45 *
46 * @author Imi
47 */
48public class CombineWayAction extends JosmAction implements SelectionChangedListener {
49
50 public CombineWayAction() {
51 super(tr("Combine Way"), "combineway", tr("Combine several ways into one."), KeyEvent.VK_C, KeyEvent.CTRL_MASK | KeyEvent.SHIFT_MASK, true);
52 DataSet.selListeners.add(this);
53 }
54
55 public void actionPerformed(ActionEvent event) {
56 Collection<OsmPrimitive> selection = Main.ds.getSelected();
57 LinkedList<Way> selectedWays = new LinkedList<Way>();
58
59 for (OsmPrimitive osm : selection)
60 if (osm instanceof Way)
61 selectedWays.add((Way)osm);
62
63 if (selectedWays.size() < 2) {
64 JOptionPane.showMessageDialog(Main.parent, tr("Please select at least two ways to combine."));
65 return;
66 }
67
68 // Check whether all ways have identical relationship membership. More
69 // specifically: If one of the selected ways is a member of relation X
70 // in role Y, then all selected ways must be members of X in role Y.
71
72 // FIXME: In a later revision, we should display some sort of conflict
73 // dialog like we do for tags, to let the user choose which relations
74 // should be kept.
75
76 // Step 1, iterate over all relations and create counters indicating
77 // how many of the selected ways are part of relation X in role Y
78 // (hashMap backlinks contains keys formed like X@Y)
79 HashMap<String, Integer> backlinks = new HashMap<String, Integer>();
80 HashSet<Relation> relationsUsingWays = new HashSet<Relation>();
81 for (Relation r : Main.ds.relations) {
82 if (r.deleted) continue;
83 for (RelationMember rm : r.members) {
84 if (rm.member instanceof Way) {
85 for(Way w : selectedWays) {
86 if (rm.member == w) {
87 String hash = Long.toString(r.id) + "@" + rm.role;
88 System.out.println(hash);
89 if (backlinks.containsKey(hash)) {
90 backlinks.put(hash, new Integer(backlinks.get(hash)+1));
91 } else {
92 backlinks.put(hash, 1);
93 }
94 // this is just a cache for later use
95 relationsUsingWays.add(r);
96 }
97 }
98 }
99 }
100 }
101
102 // Step 2, all values of the backlinks HashMap must now equal the size
103 // of the selection.
104 for (Integer i : backlinks.values()) {
105 if (i.intValue() != selectedWays.size()) {
106 JOptionPane.showMessageDialog(Main.parent, tr("The selected ways cannot be combined as they have differing relation memberships."));
107 return;
108 }
109 }
110
111 // collect properties for later conflict resolving
112 Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
113 for (Way w : selectedWays) {
114 for (Entry<String,String> e : w.entrySet()) {
115 if (!props.containsKey(e.getKey()))
116 props.put(e.getKey(), new TreeSet<String>());
117 props.get(e.getKey()).add(e.getValue());
118 }
119 }
120
121 List<Node> nodeList = null;
122 Object firstTry = actuallyCombineWays(selectedWays, false);
123 if (firstTry instanceof List) {
124 nodeList = (List<Node>) firstTry;
125 } else {
126 Object secondTry = actuallyCombineWays(selectedWays, true);
127 if (secondTry instanceof List) {
128 int option = JOptionPane.showConfirmDialog(Main.parent,
129 tr("The ways can not be combined in their current directions. "
130 + "Do you want to reverse some of them?"), tr("Change directions?"),
131 JOptionPane.YES_NO_OPTION);
132 if (option != JOptionPane.YES_OPTION) {
133 return;
134 }
135 nodeList = (List<Node>) secondTry;
136 } else {
137 JOptionPane.showMessageDialog(Main.parent, (String) secondTry);
138 return;
139 }
140 }
141
142 Way newWay = new Way(selectedWays.get(0));
143 newWay.nodes.clear();
144 newWay.nodes.addAll(nodeList);
145
146 // display conflict dialog
147 Map<String, JComboBox> components = new HashMap<String, JComboBox>();
148 JPanel p = new JPanel(new GridBagLayout());
149 for (Entry<String, Set<String>> e : props.entrySet()) {
150 if (e.getValue().size() > 1) {
151 JComboBox c = new JComboBox(e.getValue().toArray());
152 c.setEditable(true);
153 p.add(new JLabel(e.getKey()), GBC.std());
154 p.add(Box.createHorizontalStrut(10), GBC.std());
155 p.add(c, GBC.eol());
156 components.put(e.getKey(), c);
157 } else
158 newWay.put(e.getKey(), e.getValue().iterator().next());
159 }
160
161 if (!components.isEmpty()) {
162 int answer = JOptionPane.showConfirmDialog(Main.parent, p, tr("Enter values for all conflicts."), JOptionPane.OK_CANCEL_OPTION);
163 if (answer != JOptionPane.OK_OPTION)
164 return;
165 for (Entry<String, JComboBox> e : components.entrySet())
166 newWay.put(e.getKey(), e.getValue().getEditor().getItem().toString());
167 }
168
169 LinkedList<Command> cmds = new LinkedList<Command>();
170 cmds.add(new DeleteCommand(selectedWays.subList(1, selectedWays.size())));
171 cmds.add(new ChangeCommand(selectedWays.peek(), newWay));
172
173 // modify all relations containing the now-deleted ways
174 for (Relation r : relationsUsingWays) {
175 Relation newRel = new Relation(r);
176 newRel.members.clear();
177 for (RelationMember rm : r.members) {
178 // only copy member if it is either the first of all the selected
179 // ways (indexOf==0) or not one if the selected ways (indexOf==-1)
180 if (selectedWays.indexOf(rm.member) < 1) {
181 newRel.members.add(new RelationMember(rm));
182 }
183 }
184 cmds.add(new ChangeCommand(r, newRel));
185 }
186 Main.main.undoRedo.add(new SequenceCommand(tr("Combine {0} ways", selectedWays.size()), cmds));
187 Main.ds.setSelected(selectedWays.peek());
188 }
189
190 /**
191 * @return a message if combining failed, else a list of nodes.
192 */
193 private Object actuallyCombineWays(List<Way> ways, boolean ignoreDirection) {
194 // Battle plan:
195 // 1. Split the ways into small chunks of 2 nodes and weed out
196 // duplicates.
197 // 2. Take a chunk and see if others could be appended or prepended,
198 // if so, do it and remove it from the list of remaining chunks.
199 // Rather, rinse, repeat.
200 // 3. If this algorithm does not produce a single way,
201 // complain to the user.
202 // 4. Profit!
203
204 HashSet<NodePair> chunkSet = new HashSet<NodePair>();
205 for (Way w : ways) {
206 if (w.nodes.size() == 0) continue;
207 Node lastN = null;
208 for (Node n : w.nodes) {
209 if (lastN == null) {
210 lastN = n;
211 continue;
212 }
213
214 NodePair np = new NodePair(lastN, n);
215 if (ignoreDirection) {
216 np.sort();
217 }
218 chunkSet.add(np);
219
220 lastN = n;
221 }
222 }
223 LinkedList<NodePair> chunks = new LinkedList<NodePair>(chunkSet);
224
225 if (chunks.isEmpty()) {
226 return tr("All the ways were empty");
227 }
228
229 List<Node> nodeList = chunks.poll().toArrayList();
230 while (!chunks.isEmpty()) {
231 ListIterator<NodePair> it = chunks.listIterator();
232 boolean foundChunk = false;
233 while (it.hasNext()) {
234 NodePair curChunk = it.next();
235 if (curChunk.a == nodeList.get(nodeList.size() - 1)) { // append
236 nodeList.add(curChunk.b);
237 } else if (curChunk.b == nodeList.get(0)) { // prepend
238 nodeList.add(0, curChunk.a);
239 } else if (ignoreDirection && curChunk.b == nodeList.get(nodeList.size() - 1)) { // append
240 nodeList.add(curChunk.a);
241 } else if (ignoreDirection && curChunk.a == nodeList.get(0)) { // prepend
242 nodeList.add(0, curChunk.b);
243 } else {
244 continue;
245 }
246
247 foundChunk = true;
248 it.remove();
249 break;
250 }
251 if (!foundChunk) break;
252 }
253
254 if (!chunks.isEmpty()) {
255 return tr("Could not combine ways "
256 + "(They could not be merged into a single string of nodes)");
257 }
258
259 return nodeList;
260 }
261
262 /**
263 * Enable the "Combine way" menu option if more then one way is selected
264 */
265 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
266 boolean first = false;
267 for (OsmPrimitive osm : newSelection) {
268 if (osm instanceof Way) {
269 if (first) {
270 setEnabled(true);
271 return;
272 }
273 first = true;
274 }
275 }
276 setEnabled(false);
277 }
278}
Note: See TracBrowser for help on using the repository browser.