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