001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.util.Collection; 010import java.util.HashSet; 011import java.util.Iterator; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Set; 015 016import javax.swing.JOptionPane; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.command.Command; 020import org.openstreetmap.josm.command.MoveCommand; 021import org.openstreetmap.josm.command.SequenceCommand; 022import org.openstreetmap.josm.data.osm.Node; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.Way; 025import org.openstreetmap.josm.gui.Notification; 026import org.openstreetmap.josm.tools.Shortcut; 027 028/** 029 * Distributes the selected nodes to equal distances along a line. 030 * 031 * @author Teemu Koskinen 032 */ 033public final class DistributeAction extends JosmAction { 034 035 /** 036 * Constructs a new {@code DistributeAction}. 037 */ 038 public DistributeAction() { 039 super(tr("Distribute Nodes"), "distribute", 040 tr("Distribute the selected nodes to equal distances along a line."), 041 Shortcut.registerShortcut("tools:distribute", tr("Tool: {0}", tr("Distribute Nodes")), KeyEvent.VK_B, Shortcut.SHIFT), 042 true); 043 putValue("help", ht("/Action/DistributeNodes")); 044 } 045 046 /** 047 * Perform action. 048 * Select method according to user selection. 049 * Case 1: One Way (no self-crossing) and at most 2 nodes contains by this way: 050 * Distribute nodes keeping order along the way 051 * Case 2: Other 052 * Distribute nodes 053 */ 054 @Override 055 public void actionPerformed(ActionEvent e) { 056 if (!isEnabled()) 057 return; 058 059 // Collect user selected objects 060 Collection<OsmPrimitive> selected = getLayerManager().getEditDataSet().getSelected(); 061 Collection<Way> ways = new LinkedList<>(); 062 Collection<Node> nodes = new HashSet<>(); 063 for (OsmPrimitive osm : selected) { 064 if (osm instanceof Node) { 065 nodes.add((Node) osm); 066 } else if (osm instanceof Way) { 067 ways.add((Way) osm); 068 } 069 } 070 071 Set<Node> ignoredNodes = removeNodesWithoutCoordinates(nodes); 072 if (!ignoredNodes.isEmpty()) { 073 Main.warn(tr("Ignoring {0} nodes with null coordinates", ignoredNodes.size())); 074 ignoredNodes.clear(); 075 } 076 077 // Switch between algorithms 078 Collection<Command> cmds; 079 if (checkDistributeWay(ways, nodes)) { 080 cmds = distributeWay(ways, nodes); 081 } else if (checkDistributeNodes(ways, nodes)) { 082 cmds = distributeNodes(nodes); 083 } else { 084 new Notification( 085 tr("Please select :\n" + 086 "* One no self-crossing way with at most two of its nodes;\n" + 087 "* Three nodes.")) 088 .setIcon(JOptionPane.INFORMATION_MESSAGE) 089 .setDuration(Notification.TIME_SHORT) 090 .show(); 091 return; 092 } 093 094 if (cmds.isEmpty()) { 095 return; 096 } 097 098 // Do it! 099 Main.main.undoRedo.add(new SequenceCommand(tr("Distribute Nodes"), cmds)); 100 } 101 102 /** 103 * Test if one way, no self-crossing, is selected with at most two of its nodes. 104 * @param ways Selected ways 105 * @param nodes Selected nodes 106 * @return true in this case 107 */ 108 private static boolean checkDistributeWay(Collection<Way> ways, Collection<Node> nodes) { 109 if (ways.size() == 1 && nodes.size() <= 2) { 110 Way w = ways.iterator().next(); 111 Set<Node> unduplicated = new HashSet<>(w.getNodes()); 112 if (unduplicated.size() != w.getNodesCount()) { 113 // No self crossing way 114 return false; 115 } 116 for (Node node: nodes) { 117 if (!w.containsNode(node)) { 118 return false; 119 } 120 } 121 return true; 122 } 123 return false; 124 } 125 126 /** 127 * Distribute nodes contained by a way, keeping nodes order. 128 * If one or two nodes are selected, keep these nodes in place. 129 * @param ways Selected ways, must be collection of size 1. 130 * @param nodes Selected nodes, at most two nodes. 131 * @return Collection of command to be executed. 132 */ 133 private static Collection<Command> distributeWay(Collection<Way> ways, 134 Collection<Node> nodes) { 135 Way w = ways.iterator().next(); 136 Collection<Command> cmds = new LinkedList<>(); 137 138 if (w.getNodesCount() == nodes.size() || w.getNodesCount() <= 2) { 139 // Nothing to do 140 return cmds; 141 } 142 143 double xa, ya; // Start point 144 double dx, dy; // Segment increment 145 if (nodes.isEmpty()) { 146 Node na = w.firstNode(); 147 nodes.add(na); 148 Node nb = w.lastNode(); 149 nodes.add(nb); 150 xa = na.getEastNorth().east(); 151 ya = na.getEastNorth().north(); 152 dx = (nb.getEastNorth().east() - xa) / (w.getNodesCount() - 1); 153 dy = (nb.getEastNorth().north() - ya) / (w.getNodesCount() - 1); 154 } else if (nodes.size() == 1) { 155 Node n = nodes.iterator().next(); 156 int nIdx = w.getNodes().indexOf(n); 157 Node na = w.firstNode(); 158 Node nb = w.lastNode(); 159 dx = (nb.getEastNorth().east() - na.getEastNorth().east()) / 160 (w.getNodesCount() - 1); 161 dy = (nb.getEastNorth().north() - na.getEastNorth().north()) / 162 (w.getNodesCount() - 1); 163 xa = n.getEastNorth().east() - dx * nIdx; 164 ya = n.getEastNorth().north() - dy * nIdx; 165 } else { 166 Iterator<Node> it = nodes.iterator(); 167 Node na = it.next(); 168 Node nb = it.next(); 169 List<Node> wayNodes = w.getNodes(); 170 int naIdx = wayNodes.indexOf(na); 171 int nbIdx = wayNodes.indexOf(nb); 172 dx = (nb.getEastNorth().east() - na.getEastNorth().east()) / (nbIdx - naIdx); 173 dy = (nb.getEastNorth().north() - na.getEastNorth().north()) / (nbIdx - naIdx); 174 xa = na.getEastNorth().east() - dx * naIdx; 175 ya = na.getEastNorth().north() - dy * naIdx; 176 } 177 178 for (int i = 0; i < w.getNodesCount(); i++) { 179 Node n = w.getNode(i); 180 if (!n.isLatLonKnown() || nodes.contains(n)) { 181 continue; 182 } 183 double x = xa + i * dx; 184 double y = ya + i * dy; 185 cmds.add(new MoveCommand(n, x - n.getEastNorth().east(), 186 y - n.getEastNorth().north())); 187 } 188 return cmds; 189 } 190 191 /** 192 * Test if nodes oriented algorithm applies to the selection. 193 * @param ways Selected ways 194 * @param nodes Selected nodes 195 * @return true in this case 196 */ 197 private static Boolean checkDistributeNodes(Collection<Way> ways, Collection<Node> nodes) { 198 return ways.isEmpty() && nodes.size() >= 3; 199 } 200 201 /** 202 * Distribute nodes when only nodes are selected. 203 * The general algorithm here is to find the two selected nodes 204 * that are furthest apart, and then to distribute all other selected 205 * nodes along the straight line between these nodes. 206 * @param nodes nodes to distribute 207 * @return Commands to execute to perform action 208 * @throws IllegalArgumentException if nodes is empty 209 */ 210 private static Collection<Command> distributeNodes(Collection<Node> nodes) { 211 // Find from the selected nodes two that are the furthest apart. 212 // Let's call them A and B. 213 double distance = 0; 214 215 Node nodea = null; 216 Node nodeb = null; 217 218 Collection<Node> itnodes = new LinkedList<>(nodes); 219 for (Node n : nodes) { 220 itnodes.remove(n); 221 for (Node m : itnodes) { 222 double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth())); 223 if (dist > distance) { 224 nodea = n; 225 nodeb = m; 226 distance = dist; 227 } 228 } 229 } 230 231 if (nodea == null || nodeb == null) { 232 throw new IllegalArgumentException(); 233 } 234 235 // Remove the nodes A and B from the list of nodes to move 236 nodes.remove(nodea); 237 nodes.remove(nodeb); 238 239 // Find out co-ords of A and B 240 double ax = nodea.getEastNorth().east(); 241 double ay = nodea.getEastNorth().north(); 242 double bx = nodeb.getEastNorth().east(); 243 double by = nodeb.getEastNorth().north(); 244 245 // A list of commands to do 246 Collection<Command> cmds = new LinkedList<>(); 247 248 // Amount of nodes between A and B plus 1 249 int num = nodes.size()+1; 250 251 // Current number of node 252 int pos = 0; 253 while (!nodes.isEmpty()) { 254 pos++; 255 Node s = null; 256 257 // Find the node that is furthest from B (i.e. closest to A) 258 distance = 0.0; 259 for (Node n : nodes) { 260 double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth())); 261 if (dist > distance) { 262 s = n; 263 distance = dist; 264 } 265 } 266 267 if (s != null) { 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 279 return cmds; 280 } 281 282 /** 283 * Remove nodes without knowned coordinates from a collection. 284 * @param col Collection of nodes to check 285 * @return Set of nodes without coordinates 286 */ 287 private static Set<Node> removeNodesWithoutCoordinates(Collection<Node> col) { 288 Set<Node> result = new HashSet<>(); 289 for (Iterator<Node> it = col.iterator(); it.hasNext();) { 290 Node n = it.next(); 291 if (!n.isLatLonKnown()) { 292 it.remove(); 293 result.add(n); 294 } 295 } 296 return result; 297 } 298 299 @Override 300 protected void updateEnabledState() { 301 updateEnabledStateOnCurrentSelection(); 302 } 303 304 @Override 305 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 306 setEnabled(selection != null && !selection.isEmpty()); 307 } 308}