source: josm/trunk/src/org/openstreetmap/josm/actions/DistributeAction.java@ 8308

Last change on this file since 8308 was 8247, checked in by Balaitous, 9 years ago

fix #11302 - "distribute nodes" acts weirdly if the way is not reasonably straight already

When one way (no self-crossing) is selected with at most two of its nodes, distibute node keeping nodes order along the way. Selected nodes are kept in place.
In any other case, no changes are made to the algorithm.

  • Property svn:eol-style set to native
File size: 10.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.util.Collection;
10import java.util.HashSet;
11import java.util.Iterator;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Set;
15
16import javax.swing.JOptionPane;
17
18import org.openstreetmap.josm.Main;
19import org.openstreetmap.josm.command.Command;
20import org.openstreetmap.josm.command.MoveCommand;
21import org.openstreetmap.josm.command.SequenceCommand;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.Way;
25import org.openstreetmap.josm.gui.Notification;
26import org.openstreetmap.josm.tools.Shortcut;
27
28/**
29 * Distributes the selected nodes to equal distances along a line.
30 *
31 * @author Teemu Koskinen
32 */
33public final class DistributeAction extends JosmAction {
34
35 private static final String ACTION_SHORT_NAME = "Distribute Nodes";
36
37 /**
38 * Constructs a new {@code DistributeAction}.
39 */
40 public DistributeAction() {
41 super(tr(ACTION_SHORT_NAME), "distribute",
42 tr("Distribute the selected nodes to equal distances along a line."),
43 Shortcut.registerShortcut("tools:distribute",
44 tr("Tool: {0}", tr(ACTION_SHORT_NAME)),
45 KeyEvent.VK_B, Shortcut.SHIFT),
46 true);
47 putValue("help", ht("/Action/DistributeNodes"));
48 }
49
50 /**
51 * Perform action.
52 * Select method according to user selection.
53 * Case 1: One Way (no self-crossing) and at most 2 nodes contains by this way:
54 * Distribute nodes keeping order along the way
55 * Case 2: Other
56 * Distribute nodes
57 */
58 @Override
59 public void actionPerformed(ActionEvent e) {
60 if (!isEnabled())
61 return;
62
63 // Collect user selected objects
64 Collection<OsmPrimitive> selected = getCurrentDataSet().getSelected();
65 Collection<Way> ways = new LinkedList<>();
66 Collection<Node> nodes = new HashSet<>();
67 for(OsmPrimitive osm : selected) {
68 if(osm instanceof Node) {
69 nodes.add((Node) osm);
70 } else if(osm instanceof Way) {
71 ways.add((Way) osm);
72 }
73 }
74
75 Set<Node> ignoredNodes = removeNodesWithoutCoordinates(nodes);
76 if (!ignoredNodes.isEmpty()) {
77 Main.warn(tr("Ignoring {0} nodes with null coordinates", ignoredNodes.size()));
78 ignoredNodes.clear();
79 }
80
81 // Switch between algorithms
82 Collection<Command> cmds;
83 if(checkDistributeWay(ways, nodes)) {
84 cmds = distributeWay(ways, nodes);
85 } else if(checkDistributeNodes(ways, nodes)) {
86 cmds = distributeNodes(nodes);
87 } else {
88 new Notification(
89 tr("Please select :\n" +
90 "* One no self-crossing way with at most two of its nodes;\n" +
91 "* Three nodes."))
92 .setIcon(JOptionPane.INFORMATION_MESSAGE)
93 .setDuration(Notification.TIME_SHORT)
94 .show();
95 return;
96 }
97
98 if(cmds.isEmpty()) {
99 return;
100 }
101
102 // Do it!
103 Main.main.undoRedo.add(new SequenceCommand(tr(ACTION_SHORT_NAME), cmds));
104 Main.map.repaint();
105 }
106
107 /**
108 * Test if one way, no self-crossing, is selected with at most two of its nodes.
109 * @param ways Selected ways
110 * @param nodes Selected nodes
111 * @return true in this case
112 */
113 private Boolean checkDistributeWay(Collection<Way> ways, Collection<Node> nodes) {
114 if(ways.size() == 1 && nodes.size() <= 2) {
115 Way w = ways.iterator().next();
116 Set<Node> unduplicated = new HashSet<>(w.getNodes());
117 if(unduplicated.size() != w.getNodesCount()) {
118 // No self crossing way
119 return false;
120 }
121 for(Node node: nodes) {
122 if(!w.containsNode(node)) {
123 return false;
124 }
125 }
126 return true;
127 }
128 return false;
129 }
130
131 /**
132 * Distribute nodes contained by a way, keeping nodes order.
133 * If one or two nodes are selected, keep these nodes in place.
134 * @param ways Selected ways, must be collection of size 1.
135 * @param nodes Selected nodes, at most two nodes.
136 * @return Collection of command to be executed.
137 */
138 private Collection<Command> distributeWay(Collection<Way> ways,
139 Collection<Node> nodes) {
140 Way w = ways.iterator().next();
141 Collection<Command> cmds = new LinkedList<>();
142
143 if(w.getNodesCount() == nodes.size() || w.getNodesCount() <= 2) {
144 // Nothing to do
145 return cmds;
146 }
147
148 double xa, ya; // Start point
149 double dx, dy; // Segment increment
150 if(nodes.isEmpty()) {
151 Node na = w.firstNode();
152 nodes.add(na);
153 Node nb = w.lastNode();
154 nodes.add(nb);
155 xa = na.getEastNorth().east();
156 ya = na.getEastNorth().north();
157 dx = (nb.getEastNorth().east() - xa) / (w.getNodesCount() - 1);
158 dy = (nb.getEastNorth().north() - ya) / (w.getNodesCount() - 1);
159 } else if(nodes.size() == 1) {
160 Node n = nodes.iterator().next();
161 int nIdx = w.getNodes().indexOf(n);
162 Node na = w.firstNode();
163 Node nb = w.lastNode();
164 dx = (nb.getEastNorth().east() - na.getEastNorth().east()) /
165 (w.getNodesCount() - 1);
166 dy = (nb.getEastNorth().north() - na.getEastNorth().north()) /
167 (w.getNodesCount() - 1);
168 xa = n.getEastNorth().east() - dx * nIdx;
169 ya = n.getEastNorth().north() - dy * nIdx;
170 } else {
171 Iterator<Node> it = nodes.iterator();
172 Node na = it.next();
173 Node nb = it.next();
174 List<Node> wayNodes = w.getNodes();
175 int naIdx = wayNodes.indexOf(na);
176 int nbIdx = wayNodes.indexOf(nb);
177 dx = (nb.getEastNorth().east() - na.getEastNorth().east()) / (nbIdx - naIdx);
178 dy = (nb.getEastNorth().north() - na.getEastNorth().north()) / (nbIdx - naIdx);
179 xa = na.getEastNorth().east() - dx * naIdx;
180 ya = na.getEastNorth().north() - dy * naIdx;
181 }
182
183 for(int i = 0; i < w.getNodesCount(); i++) {
184 Node n = w.getNode(i);
185 if (!n.isLatLonKnown() || nodes.contains(n)) {
186 continue;
187 }
188 double x = xa + i * dx;
189 double y = ya + i * dy;
190 cmds.add(new MoveCommand(n, x - n.getEastNorth().east(),
191 y - n.getEastNorth().north()));
192 }
193 return cmds;
194 }
195
196 /**
197 * Test if nodes oriented algorithm applies to the selection.
198 * @param ways Selected ways
199 * @param nodes Selected nodes
200 * @return true in this case
201 */
202 private Boolean checkDistributeNodes(Collection<Way> ways, Collection<Node> nodes) {
203 return ways.isEmpty() && nodes.size() >= 3;
204 }
205
206 /**
207 * Distribute nodes when only nodes are selected.
208 * The general algorithm here is to find the two selected nodes
209 * that are furthest apart, and then to distribute all other selected
210 * nodes along the straight line between these nodes.
211 * @return Commands to execute to perform action
212 */
213 private Collection<Command> distributeNodes(Collection<Node> nodes) {
214 // Find from the selected nodes two that are the furthest apart.
215 // Let's call them A and B.
216 double distance = 0;
217
218 Node nodea = null;
219 Node nodeb = null;
220
221 Collection<Node> itnodes = new LinkedList<>(nodes);
222 for (Node n : nodes) {
223 itnodes.remove(n);
224 for (Node m : itnodes) {
225 double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth()));
226 if (dist > distance) {
227 nodea = n;
228 nodeb = m;
229 distance = dist;
230 }
231 }
232 }
233
234 // Remove the nodes A and B from the list of nodes to move
235 nodes.remove(nodea);
236 nodes.remove(nodeb);
237
238 // Find out co-ords of A and B
239 double ax = nodea.getEastNorth().east();
240 double ay = nodea.getEastNorth().north();
241 double bx = nodeb.getEastNorth().east();
242 double by = nodeb.getEastNorth().north();
243
244 // A list of commands to do
245 Collection<Command> cmds = new LinkedList<>();
246
247 // Amount of nodes between A and B plus 1
248 int num = nodes.size()+1;
249
250 // Current number of node
251 int pos = 0;
252 while (!nodes.isEmpty()) {
253 pos++;
254 Node s = null;
255
256 // Find the node that is furthest from B (i.e. closest to A)
257 distance = 0.0;
258 for (Node n : nodes) {
259 double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth()));
260 if (dist > distance) {
261 s = n;
262 distance = dist;
263 }
264 }
265
266 // First move the node to A's position, then move it towards B
267 double dx = ax - s.getEastNorth().east() + (bx-ax)*pos/num;
268 double dy = ay - s.getEastNorth().north() + (by-ay)*pos/num;
269
270 cmds.add(new MoveCommand(s, dx, dy));
271
272 //remove moved node from the list
273 nodes.remove(s);
274 }
275
276 return cmds;
277 }
278
279 /**
280 * Remove nodes without knowned coordinates from a collection.
281 * @param col Collection of nodes to check
282 * @return Set of nodes without coordinates
283 */
284 private Set<Node> removeNodesWithoutCoordinates(Collection<Node> col) {
285 Set<Node> result = new HashSet<>();
286 for (Iterator<Node> it = col.iterator(); it.hasNext();) {
287 Node n = it.next();
288 if (!n.isLatLonKnown()) {
289 it.remove();
290 result.add(n);
291 }
292 }
293 return result;
294 }
295
296 @Override
297 protected void updateEnabledState() {
298 if (getCurrentDataSet() == null) {
299 setEnabled(false);
300 } else {
301 updateEnabledState(getCurrentDataSet().getSelected());
302 }
303 }
304
305 @Override
306 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
307 setEnabled(selection != null && !selection.isEmpty());
308 }
309}
Note: See TracBrowser for help on using the repository browser.