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

Last change on this file since 2876 was 2842, checked in by mjulius, 14 years ago

fix messages for actions

  • Property svn:eol-style set to native
File size: 10.5 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.combineTigerTags;
5import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing;
6import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing;
7import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
8import static org.openstreetmap.josm.tools.I18n.tr;
9
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.HashSet;
15import java.util.LinkedHashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.JOptionPane;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.command.ChangeCommand;
24import org.openstreetmap.josm.command.Command;
25import org.openstreetmap.josm.command.DeleteCommand;
26import org.openstreetmap.josm.command.SequenceCommand;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.RelationToChildReference;
30import org.openstreetmap.josm.data.osm.TagCollection;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.gui.DefaultNameFormatter;
33import org.openstreetmap.josm.gui.HelpAwareOptionPane;
34import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
35import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
36import org.openstreetmap.josm.gui.layer.OsmDataLayer;
37import org.openstreetmap.josm.tools.CheckParameterUtil;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.Shortcut;
40/**
41 * Merges a collection of nodes into one node.
42 *
43 */
44public class MergeNodesAction extends JosmAction {
45
46 public MergeNodesAction() {
47 super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."),
48 Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.GROUP_EDIT), true);
49 putValue("help", ht("/Action/MergeNodesAction"));
50 }
51
52 public void actionPerformed(ActionEvent event) {
53 if (!isEnabled())
54 return;
55 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
56 LinkedHashSet<Node> selectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
57 if (selectedNodes.size() < 2) {
58 JOptionPane.showMessageDialog(
59 Main.parent,
60 tr("Please select at least two nodes to merge."),
61 tr("Warning"),
62 JOptionPane.WARNING_MESSAGE
63 );
64 return;
65 }
66
67 Node targetNode = selectTargetNode(selectedNodes);
68 Command cmd = mergeNodes(Main.main.getEditLayer(), selectedNodes, targetNode);
69 if (cmd != null) {
70 Main.main.undoRedo.add(cmd);
71 Main.main.getEditLayer().data.setSelected(targetNode);
72 }
73 }
74
75 /**
76 * Find which node to merge into (i.e. which one will be left)
77 * The last selected node will become the target node the remaining
78 * nodes are merged to.
79 *
80 * @param candidates the collection of candidate nodes
81 * @return the selected target node
82 */
83 public static Node selectTargetNode(LinkedHashSet<Node> candidates) {
84 Node targetNode = null;
85 for (Node n : candidates) { // pick last one
86 targetNode = n;
87 }
88 return targetNode;
89 }
90
91 /**
92 * Fixes the parent ways referring to one of the nodes.
93 *
94 * Replies null, if the ways could not be fixed, i.e. because a way would have to be deleted
95 * which is referred to by a relation.
96 *
97 * @param nodesToDelete the collection of nodes to be deleted
98 * @param targetNode the target node the other nodes are merged to
99 * @return a list of commands; null, if the ways could not be fixed
100 */
101 protected static List<Command> fixParentWays(Collection<OsmPrimitive> nodesToDelete, Node targetNode) {
102 List<Command> cmds = new ArrayList<Command>();
103 Set<Way> waysToDelete = new HashSet<Way>();
104
105 for (Way w: OsmPrimitive.getFilteredList(OsmPrimitive.getReferrer(nodesToDelete), Way.class)) {
106 ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
107 for (Node n: w.getNodes()) {
108 if (! nodesToDelete.contains(n) && n != targetNode) {
109 newNodes.add(n);
110 } else if (newNodes.isEmpty()) {
111 newNodes.add(targetNode);
112 } else if (newNodes.get(newNodes.size()-1) != targetNode) {
113 // make sure we collapse a sequence of deleted nodes
114 // to exactly one occurrence of the merged target node
115 //
116 newNodes.add(targetNode);
117 } else {
118 // drop the node
119 }
120 }
121 if (newNodes.size() < 2) {
122 if (w.getReferrers().isEmpty()) {
123 waysToDelete.add(w);
124 } else {
125 ButtonSpec[] options = new ButtonSpec[] {
126 new ButtonSpec(
127 tr("Abort Merging"),
128 ImageProvider.get("cancel"),
129 tr("Click to abort merging nodes"),
130 null /* no special help topic */
131 )
132 };
133 HelpAwareOptionPane.showOptionDialog(
134 Main.parent,
135 tr(
136 "Cannot merge nodes: Would have to delete way ''{0}'' which is still used.",
137 w.getDisplayName(DefaultNameFormatter.getInstance())
138 ),
139 tr("Warning"),
140 JOptionPane.WARNING_MESSAGE,
141 null, /* no icon */
142 options,
143 options[0],
144 ht("/Action/MergeNodes#WaysToDeleteStillInUse")
145 );
146 return null;
147 }
148 } else if(newNodes.size() < 2 && w.getReferrers().isEmpty()) {
149 waysToDelete.add(w);
150 } else {
151 Way newWay = new Way(w);
152 newWay.setNodes(newNodes);
153 cmds.add(new ChangeCommand(w, newWay));
154 }
155 }
156 if (!waysToDelete.isEmpty()) {
157 cmds.add(new DeleteCommand(waysToDelete));
158 }
159 return cmds;
160 }
161
162 /**
163 * Merges the nodes in <code>nodes</code> onto one of the nodes. Uses the dataset
164 * managed by <code>layer</code> as reference.
165 *
166 * @param layer layer the reference data layer. Must not be null.
167 * @param nodes the collection of nodes. Ignored if null.
168 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
169 * @throw IllegalArgumentException thrown if layer is null
170 */
171 public static Command mergeNodes(OsmDataLayer layer,Collection<Node> nodes, Node targetNode) {
172 CheckParameterUtil.ensureParameterNotNull(layer, "layer");
173 CheckParameterUtil.ensureParameterNotNull(targetNode, "targetNode");
174 if (nodes == null)
175 return null;
176
177 Set<RelationToChildReference> relationToNodeReferences = RelationToChildReference.getRelationToChildReferences(nodes);
178
179 // build the tag collection
180 //
181 TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes);
182 combineTigerTags(nodeTags);
183 normalizeTagCollectionBeforeEditing(nodeTags, nodes);
184 TagCollection nodeTagsToEdit = new TagCollection(nodeTags);
185 completeTagCollectionForEditing(nodeTagsToEdit);
186
187 // launch a conflict resolution dialog, if necessary
188 //
189 CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
190 dialog.getTagConflictResolverModel().populate(nodeTagsToEdit, nodeTags.getKeysWithMultipleValues());
191 dialog.getRelationMemberConflictResolverModel().populate(relationToNodeReferences);
192 dialog.setTargetPrimitive(targetNode);
193 dialog.prepareDefaultDecisions();
194 // conflict resolution is necessary if there are conflicts in the merged tags
195 // or if at least one of the merged nodes is referred to by a relation
196 //
197 if (! nodeTags.isApplicableToPrimitive() || relationToNodeReferences.size() > 1) {
198 dialog.setVisible(true);
199 if (dialog.isCancelled())
200 return null;
201 }
202 LinkedList<Command> cmds = new LinkedList<Command>();
203
204 // the nodes we will have to delete
205 //
206 Collection<OsmPrimitive> nodesToDelete = new HashSet<OsmPrimitive>(nodes);
207 nodesToDelete.remove(targetNode);
208
209 // fix the ways referring to at least one of the merged nodes
210 //
211 Collection<Way> waysToDelete= new HashSet<Way>();
212 List<Command> wayFixCommands = fixParentWays(
213 nodesToDelete,
214 targetNode
215 );
216 if (wayFixCommands == null)
217 return null;
218 cmds.addAll(wayFixCommands);
219
220 // build the commands
221 //
222 if (!nodesToDelete.isEmpty()) {
223 cmds.add(new DeleteCommand(nodesToDelete));
224 }
225 if (!waysToDelete.isEmpty()) {
226 cmds.add(new DeleteCommand(waysToDelete));
227 }
228 cmds.addAll(dialog.buildResolutionCommands());
229 Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds);
230 return cmd;
231 }
232
233 @Override
234 protected void updateEnabledState() {
235 if (getCurrentDataSet() == null) {
236 setEnabled(false);
237 } else {
238 updateEnabledState(getCurrentDataSet().getSelected());
239 }
240 }
241
242 @Override
243 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
244 if (selection == null || selection.isEmpty()) {
245 setEnabled(false);
246 return;
247 }
248 boolean ok = true;
249 if (selection.size() < 2) {
250 setEnabled(false);
251 return;
252 }
253 for (OsmPrimitive osm : selection) {
254 if (!(osm instanceof Node)) {
255 ok = false;
256 break;
257 }
258 }
259 setEnabled(ok);
260 }
261}
Note: See TracBrowser for help on using the repository browser.