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.ArrayList; 010import java.util.Collection; 011import java.util.List; 012 013import javax.swing.JOptionPane; 014 015import org.openstreetmap.josm.Main; 016import org.openstreetmap.josm.command.Command; 017import org.openstreetmap.josm.command.MoveCommand; 018import org.openstreetmap.josm.command.SequenceCommand; 019import org.openstreetmap.josm.data.osm.Node; 020import org.openstreetmap.josm.data.osm.OsmPrimitive; 021import org.openstreetmap.josm.data.osm.Way; 022import org.openstreetmap.josm.gui.Notification; 023import org.openstreetmap.josm.tools.Shortcut; 024 025/** 026 * Aligns all selected nodes into a straight line (useful for 027 * roads that should be straight, but have side roads and 028 * therefore need multiple nodes) 029 * 030 * @author Matthew Newton 031 */ 032public final class AlignInLineAction extends JosmAction { 033 034 /** 035 * Constructs a new {@code AlignInLineAction}. 036 */ 037 public AlignInLineAction() { 038 super(tr("Align Nodes in Line"), "alignline", tr("Move the selected nodes in to a line."), 039 Shortcut.registerShortcut("tools:alignline", tr("Tool: {0}", tr("Align Nodes in Line")), KeyEvent.VK_L, Shortcut.DIRECT), true); 040 putValue("help", ht("/Action/AlignInLine")); 041 } 042 043 // the joy of single return values only... 044 private void nodePairFurthestApart(List<Node> nodes, Node[] resultOut) { 045 if(resultOut.length < 2) 046 throw new IllegalArgumentException(); 047 // Find from the selected nodes two that are the furthest apart. 048 // Let's call them A and B. 049 double distance = 0; 050 051 Node nodea = null; 052 Node nodeb = null; 053 054 for (int i = 0; i < nodes.size()-1; i++) { 055 Node n = nodes.get(i); 056 for (int j = i+1; j < nodes.size(); j++) { 057 Node m = nodes.get(j); 058 double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth())); 059 if (dist > distance) { 060 nodea = n; 061 nodeb = m; 062 distance = dist; 063 } 064 } 065 } 066 resultOut[0] = nodea; 067 resultOut[1] = nodeb; 068 } 069 070 private void showWarning() { 071 new Notification( 072 tr("Please select at least three nodes.")) 073 .setIcon(JOptionPane.INFORMATION_MESSAGE) 074 .show(); 075 } 076 077 private static int indexWrap(int size, int i) { 078 i = i % size; // -2 % 5 = -2, -7 % 5 = -2, -5 % 5 = 0 079 if (i < 0) { 080 i = size + i; 081 } 082 return i; 083 } 084 // get the node in w at index i relative to refI 085 private static Node getNodeRelative(Way w, int refI, int i) { 086 int absI = indexWrap(w.getNodesCount(), refI + i); 087 if(w.isClosed() && refI + i < 0) { 088 absI--; // node duplicated in closed ways 089 } 090 return w.getNode(absI); 091 } 092 093 /** 094 * The general algorithm here is to find the two selected nodes 095 * that are furthest apart, and then to align all other selected 096 * nodes onto the straight line between these nodes. 097 */ 098 099 100 /** 101 * Operation depends on the selected objects: 102 */ 103 @Override 104 public void actionPerformed(ActionEvent e) { 105 if (!isEnabled()) 106 return; 107 108 Node[] anchors = new Node[2]; // oh, java I love you so much.. 109 110 List<Node> selectedNodes = new ArrayList<Node>(getCurrentDataSet().getSelectedNodes()); 111 Collection<Way> selectedWays = getCurrentDataSet().getSelectedWays(); 112 List<Node> nodes = new ArrayList<Node>(); 113 114 //// Decide what to align based on selection: 115 116 /// Only ways selected -> Align their nodes. 117 if ((selectedNodes.isEmpty()) && (selectedWays.size() == 1)) { // TODO: handle multiple ways 118 for (Way way : selectedWays) { 119 nodes.addAll(way.getNodes()); 120 } 121 // use the nodes furthest apart as anchors 122 nodePairFurthestApart(nodes, anchors); 123 } 124 /// More than 3 nodes selected -> align those nodes 125 else if(selectedNodes.size() >= 3) { 126 nodes.addAll(selectedNodes); 127 // use the nodes furthest apart as anchors 128 nodePairFurthestApart(nodes, anchors); 129 } 130 /// One node selected -> align that node to the relevant neighbors 131 else if (selectedNodes.size() == 1) { 132 Node n = selectedNodes.iterator().next(); 133 134 Way w = null; 135 if(selectedWays.size() == 1) { 136 w = selectedWays.iterator().next(); 137 if (!w.containsNode(n)) 138 // warning 139 return; 140 } else { 141 List<Way> refWays = OsmPrimitive.getFilteredList(n.getReferrers(), Way.class); 142 if (refWays.size() == 1) { // node used in only one way 143 w = refWays.iterator().next(); 144 } 145 } 146 if (w == null || w.getNodesCount() < 3) 147 // warning, need at least 3 nodes 148 return; 149 150 // Find anchors 151 int nodeI = w.getNodes().indexOf(n); 152 // End-node in non-circular way selected: align this node with the two neighbors. 153 if ((nodeI == 0 || nodeI == w.getNodesCount()-1) && !w.isClosed()) { 154 int direction = nodeI == 0 ? 1 : -1; 155 anchors[0] = w.getNode(nodeI + direction); 156 anchors[1] = w.getNode(nodeI + direction*2); 157 } else { 158 // o---O---o 159 anchors[0] = getNodeRelative(w, nodeI, 1); 160 anchors[1] = getNodeRelative(w, nodeI, -1); 161 } 162 nodes.add(n); 163 } 164 165 if (anchors[0] == null || anchors[1] == null) { 166 showWarning(); 167 return; 168 } 169 170 171 Collection<Command> cmds = new ArrayList<Command>(nodes.size()); 172 173 createAlignNodesCommands(anchors, nodes, cmds); 174 175 // Do it! 176 Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Line"), cmds)); 177 Main.map.repaint(); 178 } 179 180 private void createAlignNodesCommands(Node[] anchors, Collection<Node> nodes, Collection<Command> cmds) { 181 Node nodea = anchors[0]; 182 Node nodeb = anchors[1]; 183 184 // The anchors are aligned per definition 185 nodes.remove(nodea); 186 nodes.remove(nodeb); 187 188 // Find out co-ords of A and B 189 double ax = nodea.getEastNorth().east(); 190 double ay = nodea.getEastNorth().north(); 191 double bx = nodeb.getEastNorth().east(); 192 double by = nodeb.getEastNorth().north(); 193 194 // OK, for each node to move, work out where to move it! 195 for (Node n : nodes) { 196 // Get existing co-ords of node to move 197 double nx = n.getEastNorth().east(); 198 double ny = n.getEastNorth().north(); 199 200 if (ax == bx) { 201 // Special case if AB is vertical... 202 nx = ax; 203 } else if (ay == by) { 204 // ...or horizontal 205 ny = ay; 206 } else { 207 // Otherwise calculate position by solving y=mx+c 208 double m1 = (by - ay) / (bx - ax); 209 double c1 = ay - (ax * m1); 210 double m2 = (-1) / m1; 211 double c2 = n.getEastNorth().north() - (n.getEastNorth().east() * m2); 212 213 nx = (c2 - c1) / (m1 - m2); 214 ny = (m1 * nx) + c1; 215 } 216 double newX = nx - n.getEastNorth().east(); 217 double newY = ny - n.getEastNorth().north(); 218 // Add the command to move the node to its new position. 219 cmds.add(new MoveCommand(n, newX, newY)); 220 } 221 } 222 223 @Override 224 protected void updateEnabledState() { 225 setEnabled(getCurrentDataSet() != null && !getCurrentDataSet().getSelected().isEmpty()); 226 } 227 228 @Override 229 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 230 setEnabled(selection != null && !selection.isEmpty()); 231 } 232}