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

Last change on this file since 8932 was 8870, checked in by Don-vip, 9 years ago

sonar - squid:S2325 - "private" methods that don't access instance data should be "static"

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