source: josm/trunk/src/org/openstreetmap/josm/actions/MergeNodesAction.java@ 1912

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

Updated w.getNodes().contains(..) to w.containsNode(...)

  • Property svn:eol-style set to native
File size: 12.1 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.Map;
15import java.util.Set;
16import java.util.TreeMap;
17import java.util.TreeSet;
18import java.util.Map.Entry;
19
20import javax.swing.Box;
21import javax.swing.JComboBox;
22import javax.swing.JLabel;
23import javax.swing.JOptionPane;
24import javax.swing.JPanel;
25
26import org.openstreetmap.josm.Main;
27import org.openstreetmap.josm.command.ChangeCommand;
28import org.openstreetmap.josm.command.Command;
29import org.openstreetmap.josm.command.DeleteCommand;
30import org.openstreetmap.josm.command.SequenceCommand;
31import org.openstreetmap.josm.data.osm.Node;
32import org.openstreetmap.josm.data.osm.OsmPrimitive;
33import org.openstreetmap.josm.data.osm.Relation;
34import org.openstreetmap.josm.data.osm.RelationMember;
35import org.openstreetmap.josm.data.osm.TigerUtils;
36import org.openstreetmap.josm.data.osm.Way;
37import org.openstreetmap.josm.data.osm.visitor.CollectBackReferencesVisitor;
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/**
46 * Merge two or more nodes into one node.
47 * (based on Combine ways)
48 *
49 * @author Matthew Newton
50 *
51 */
52public class MergeNodesAction extends JosmAction {
53
54 public MergeNodesAction() {
55 super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."),
56 Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.GROUP_EDIT), true);
57 }
58
59 public void actionPerformed(ActionEvent event) {
60 if (!isEnabled())
61 return;
62
63 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
64 LinkedList<Node> selectedNodes = new LinkedList<Node>();
65
66 // the selection check should stop this procedure starting if
67 // nothing but node are selected - otherwise we don't care
68 // anyway as long as we have at least two nodes
69 for (OsmPrimitive osm : selection)
70 if (osm instanceof Node) {
71 selectedNodes.add((Node)osm);
72 }
73
74 if (selectedNodes.size() < 2) {
75 OptionPaneUtil.showMessageDialog(
76 Main.parent,
77 tr("Please select at least two nodes to merge."),
78 tr("Warning"),
79 JOptionPane.WARNING_MESSAGE
80 );
81 return;
82 }
83
84 // Find which node to merge into (i.e. which one will be left)
85 // - this should be combined from two things:
86 // 1. It will be the first node in the list that has a
87 // positive ID number, OR the first node.
88 // 2. It will be at the position of the first node in the
89 // list.
90 //
91 // *However* - there is the problem that the selection list is
92 // _not_ in the order that the nodes were clicked on, meaning
93 // that the user doesn't know which node will be chosen (so
94 // (2) is not implemented yet.) :-(
95 Node useNode = null;
96 for (Node n: selectedNodes) {
97 if (n.id > 0) {
98 useNode = n;
99 break;
100 }
101 }
102 if (useNode == null) {
103 useNode = selectedNodes.iterator().next();
104 }
105
106 mergeNodes(selectedNodes, useNode);
107 }
108
109 /**
110 * really do the merging - returns the node that is left
111 */
112 public Node mergeNodes(LinkedList<Node> allNodes, Node dest) {
113 Node newNode = new Node(dest);
114
115 // Check whether all ways have identical relationship membership. More
116 // specifically: If one of the selected ways is a member of relation X
117 // in role Y, then all selected ways must be members of X in role Y.
118
119 // FIXME: In a later revision, we should display some sort of conflict
120 // dialog like we do for tags, to let the user choose which relations
121 // should be kept.
122
123 // Step 1, iterate over all relations and figure out which of our
124 // selected ways are members of a relation.
125 HashMap<Pair<Relation,String>, HashSet<Node>> backlinks =
126 new HashMap<Pair<Relation,String>, HashSet<Node>>();
127 HashSet<Relation> relationsUsingNodes = new HashSet<Relation>();
128 for (Relation r : getCurrentDataSet().relations) {
129 if (r.deleted || r.incomplete) {
130 continue;
131 }
132 for (RelationMember rm : r.members) {
133 if (rm.member instanceof Node) {
134 for (Node n : allNodes) {
135 if (rm.member == n) {
136 Pair<Relation,String> pair = new Pair<Relation,String>(r, rm.role);
137 HashSet<Node> nodelinks = new HashSet<Node>();
138 if (backlinks.containsKey(pair)) {
139 nodelinks = backlinks.get(pair);
140 } else {
141 nodelinks = new HashSet<Node>();
142 backlinks.put(pair, nodelinks);
143 }
144 nodelinks.add(n);
145
146 // this is just a cache for later use
147 relationsUsingNodes.add(r);
148 }
149 }
150 }
151 }
152 }
153
154 // Complain to the user if the ways don't have equal memberships.
155 for (HashSet<Node> nodelinks : backlinks.values()) {
156 if (!nodelinks.containsAll(allNodes)) {
157 int option = new ExtendedDialog(Main.parent,
158 tr("Merge nodes with different memberships?"),
159 tr("The selected nodes have differing relation memberships. "
160 + "Do you still want to merge them?"),
161 new String[] {tr("Merge Anyway"), tr("Cancel")},
162 new String[] {"mergenodes.png", "cancel.png"}).getValue();
163 if (option == 1) {
164 break;
165 }
166 return null;
167 }
168 }
169
170 // collect properties for later conflict resolving
171 Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
172 for (Node n : allNodes) {
173 for (Entry<String,String> e : n.entrySet()) {
174 if (!props.containsKey(e.getKey())) {
175 props.put(e.getKey(), new TreeSet<String>());
176 }
177 props.get(e.getKey()).add(e.getValue());
178 }
179 }
180
181 // display conflict dialog
182 Map<String, JComboBox> components = new HashMap<String, JComboBox>();
183 JPanel p = new JPanel(new GridBagLayout());
184 for (Entry<String, Set<String>> e : props.entrySet()) {
185 if (TigerUtils.isTigerTag(e.getKey())) {
186 String combined = TigerUtils.combineTags(e.getKey(), e.getValue());
187 newNode.put(e.getKey(), combined);
188 } else if (e.getValue().size() > 1) {
189 JComboBox c = new JComboBox(e.getValue().toArray());
190 c.setEditable(true);
191 p.add(new JLabel(e.getKey()), GBC.std());
192 p.add(Box.createHorizontalStrut(10), GBC.std());
193 p.add(c, GBC.eol());
194 components.put(e.getKey(), c);
195 } else {
196 newNode.put(e.getKey(), e.getValue().iterator().next());
197 }
198 }
199
200 if (!components.isEmpty()) {
201 int answer = new ExtendedDialog(Main.parent,
202 tr("Enter values for all conflicts."),
203 p,
204 new String[] {tr("Solve Conflicts"), tr("Cancel")},
205 new String[] {"dialogs/conflict.png", "cancel.png"}).getValue();
206 if (answer != 1)
207 return null;
208 for (Entry<String, JComboBox> e : components.entrySet()) {
209 newNode.put(e.getKey(), e.getValue().getEditor().getItem().toString());
210 }
211 }
212
213 LinkedList<Command> cmds = new LinkedList<Command>();
214 cmds.add(new ChangeCommand(dest, newNode));
215
216 Collection<OsmPrimitive> del = new HashSet<OsmPrimitive>();
217
218 for (Way w : getCurrentDataSet().ways) {
219 if (w.deleted || w.incomplete || w.getNodesCount() < 1) {
220 continue;
221 }
222 boolean modify = false;
223 for (Node sn : allNodes) {
224 if (sn == dest) {
225 continue;
226 }
227 if (w.containsNode(sn)) {
228 modify = true;
229 }
230 }
231 if (!modify) {
232 continue;
233 }
234 // OK - this way contains one or more nodes to change
235 ArrayList<Node> nn = new ArrayList<Node>();
236 Node lastNode = null;
237 for (Node pushNode: w.getNodes()) {
238 if (allNodes.contains(pushNode)) {
239 pushNode = dest;
240 }
241 if (pushNode != lastNode) {
242 nn.add(pushNode);
243 }
244 lastNode = pushNode;
245 }
246 if (nn.size() < 2) {
247 CollectBackReferencesVisitor backRefs =
248 new CollectBackReferencesVisitor(getCurrentDataSet(), false);
249 w.visit(backRefs);
250 if (!backRefs.data.isEmpty()) {
251 OptionPaneUtil.showMessageDialog(
252 Main.parent,
253 tr("Cannot merge nodes: " +
254 "Would have to delete a way that is still used."),
255 tr("Warning"),
256 JOptionPane.WARNING_MESSAGE
257 );
258 return null;
259 }
260 del.add(w);
261 } else {
262 Way newWay = new Way(w);
263 newWay.setNodes(nn);
264 cmds.add(new ChangeCommand(w, newWay));
265 }
266 }
267
268 // delete any merged nodes
269 del.addAll(allNodes);
270 del.remove(dest);
271 if (!del.isEmpty()) {
272 cmds.add(new DeleteCommand(del));
273 }
274
275 // modify all relations containing the now-deleted nodes
276 for (Relation r : relationsUsingNodes) {
277 Relation newRel = new Relation(r);
278 newRel.members.clear();
279 HashSet<String> rolesToReAdd = new HashSet<String>();
280 for (RelationMember rm : r.members) {
281 // Don't copy the member if it points to one of our nodes,
282 // just keep a note to re-add it later on.
283 if (allNodes.contains(rm.member)) {
284 rolesToReAdd.add(rm.role);
285 } else {
286 newRel.members.add(rm);
287 }
288 }
289 for (String role : rolesToReAdd) {
290 newRel.members.add(new RelationMember(role, dest));
291 }
292 cmds.add(new ChangeCommand(r, newRel));
293 }
294
295 Main.main.undoRedo.add(new SequenceCommand(tr("Merge {0} nodes", allNodes.size()), cmds));
296 getCurrentDataSet().setSelected(dest);
297
298 return dest;
299 }
300
301
302 /**
303 * Enable the "Merge Nodes" menu option if more then one node is selected
304 */
305 @Override
306 public void updateEnabledState() {
307 if (getCurrentDataSet() == null || getCurrentDataSet().getSelected().isEmpty()) {
308 setEnabled(false);
309 return;
310 }
311 boolean ok = true;
312 if (getCurrentDataSet().getSelected().size() < 2) {
313 setEnabled(false);
314 return;
315 }
316 for (OsmPrimitive osm : getCurrentDataSet().getSelected()) {
317 if (!(osm instanceof Node)) {
318 ok = false;
319 break;
320 }
321 }
322 setEnabled(ok);
323 }
324}
Note: See TracBrowser for help on using the repository browser.