| 1 | // License: GPL. See LICENSE file for details. |
| 2 | // |
| 3 | package org.openstreetmap.josm.actions; |
| 4 | |
| 5 | import static org.openstreetmap.josm.tools.I18n.tr; |
| 6 | |
| 7 | import java.awt.event.ActionEvent; |
| 8 | import java.awt.event.KeyEvent; |
| 9 | import java.util.Collection; |
| 10 | import java.util.LinkedList; |
| 11 | |
| 12 | import javax.swing.JOptionPane; |
| 13 | |
| 14 | import org.openstreetmap.josm.Main; |
| 15 | import org.openstreetmap.josm.command.AddCommand; |
| 16 | import org.openstreetmap.josm.command.Command; |
| 17 | import org.openstreetmap.josm.command.MoveCommand; |
| 18 | import org.openstreetmap.josm.command.SequenceCommand; |
| 19 | import org.openstreetmap.josm.data.coor.EastNorth; |
| 20 | import org.openstreetmap.josm.data.osm.Node; |
| 21 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
| 22 | import org.openstreetmap.josm.data.osm.Way; |
| 23 | import org.openstreetmap.josm.tools.ShortCut; |
| 24 | |
| 25 | /** |
| 26 | * Align edges of a way so all angles are right angles. |
| 27 | * |
| 28 | * 1. Find orientation of all edges |
| 29 | * 2. Compute main orientation, weighted by length of edge, normalized to angles between 0 and pi/2 |
| 30 | * 3. Rotate every edge around its center to align with main orientation or perpendicular to it |
| 31 | * 4. Compute new intersection points of two adjascent edges |
| 32 | * 5. Move nodes to these points |
| 33 | */ |
| 34 | public final class AlignOrthogonallyAction extends JosmAction { |
| 35 | |
| 36 | public AlignOrthogonallyAction() { |
| 37 | super(tr("Align Nodes to make shape orthogonally"), "alignortho", tr("Move the selected nodes so all angles are orthogonally."), |
| 38 | ShortCut.registerShortCut("tools:alignortho", tr("Tool: {0}", tr("Align orthonormal")), KeyEvent.VK_T, ShortCut.GROUP_EDIT), true); |
| 39 | } |
| 40 | |
| 41 | public void actionPerformed(ActionEvent e) { |
| 42 | |
| 43 | Collection<OsmPrimitive> sel = Main.ds.getSelected(); |
| 44 | |
| 45 | // Check the selection if it is suitible for the orthogonalization |
| 46 | for (OsmPrimitive osm : sel) { |
| 47 | // Check if only ways are in the collection |
| 48 | if (!(osm instanceof Way)) { |
| 49 | JOptionPane.showMessageDialog(Main.parent, tr("Selection must consist only of ways.")); |
| 50 | return; |
| 51 | } |
| 52 | |
| 53 | // Check if every way is made of at least four segments and closed |
| 54 | Way way = (Way)osm; |
| 55 | if ((way.nodes.size() < 5) || (!way.nodes.get(0).equals(way.nodes.get(way.nodes.size() - 1)))) { |
| 56 | JOptionPane.showMessageDialog(Main.parent, tr("Please select closed way(s) of at least four nodes.")); |
| 57 | return; |
| 58 | } |
| 59 | |
| 60 | // Check if every edge in the way is a definite edge of at least 45 degrees of direction change |
| 61 | // Otherwise, two segments could be turned into same direction and intersection would fail. |
| 62 | // Or changes of shape would be too serious. |
| 63 | for (int i1=0; i1 < way.nodes.size()-1; i1++) { |
| 64 | int i2 = (i1+1) % (way.nodes.size()-1); |
| 65 | int i3 = (i1+2) % (way.nodes.size()-1); |
| 66 | double angle1 =Math.abs(way.nodes.get(i1).eastNorth.heading(way.nodes.get(i2).eastNorth)); |
| 67 | double angle2 = Math.abs(way.nodes.get(i2).eastNorth.heading(way.nodes.get(i3).eastNorth)); |
| 68 | double delta = Math.abs(angle2 - angle1); |
| 69 | while(delta > Math.PI) delta -= Math.PI; |
| 70 | if(delta < Math.PI/4) { |
| 71 | JOptionPane.showMessageDialog(Main.parent, tr("Please select ways with edges close to right angles.")); |
| 72 | return; |
| 73 | } |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // Now all checks are done and we can now do the neccessary computations |
| 78 | // From here it is assumed that the above checks hold |
| 79 | Collection<Command> cmds = new LinkedList<Command>(); |
| 80 | |
| 81 | // First, compute the weighted average of the headings of all segments |
| 82 | double sum_weighted_headings = 0.0; |
| 83 | double sum_weights = 0.0; |
| 84 | for (OsmPrimitive osm : sel) { |
| 85 | Way way = (Way)osm; |
| 86 | int nodes = way.nodes.size(); |
| 87 | int sides = nodes - 1; |
| 88 | // to find orientation of all segments, compute weighted average of all segment's headings |
| 89 | // all headings are mapped into [0, 3*4*PI) by PI/2 rotations so both main orientations are mapped into one |
| 90 | // the headings are weighted by the length of the segment establishing it, so a longer segment, that is more |
| 91 | // likely to have the correct orientation, has more influence in the computing than a short segment, that is easier to misalign. |
| 92 | for (int i=0; i < sides; i++) { |
| 93 | double heading; |
| 94 | double weight; |
| 95 | heading = way.nodes.get(i).eastNorth.heading(way.nodes.get(i+1).eastNorth); |
| 96 | //Put into [0, PI/4) to find main direction |
| 97 | while(heading > Math.PI/4) heading -= Math.PI/2; |
| 98 | weight = way.nodes.get(i).eastNorth.distance(way.nodes.get(i+1).eastNorth); |
| 99 | sum_weighted_headings += heading*weight; |
| 100 | sum_weights += weight; |
| 101 | } |
| 102 | } |
| 103 | double avg_heading = sum_weighted_headings/sum_weights; |
| 104 | |
| 105 | for (OsmPrimitive osm : sel) { |
| 106 | Way myWay = (Way)osm; |
| 107 | int nodes = myWay.nodes.size(); |
| 108 | int sides = nodes - 1; |
| 109 | |
| 110 | // Copy necessary data into a more suitable data structure |
| 111 | EastNorth en[] = new EastNorth[sides]; |
| 112 | for (int i=0; i < sides; i++) { |
| 113 | en[i] = new EastNorth(myWay.nodes.get(i).eastNorth.east(), myWay.nodes.get(i).eastNorth.north()); |
| 114 | } |
| 115 | |
| 116 | for (int i=0; i < sides; i++) { |
| 117 | // Compute handy indices of three nodes to be used in one loop iteration. |
| 118 | // We use segments (i1,i2) and (i2,i3), align them and compute the new |
| 119 | // position of the i2-node as the intersection of the realigned (i1,i2), (i2,i3) segments |
| 120 | |
| 121 | // Compute handy indices so we don't have to deal with index-wrap-around all the time |
| 122 | int i1 = i; |
| 123 | int i2 = (i+1)%sides; |
| 124 | int i3 = (i+2)%sides; |
| 125 | double heading1, heading2; |
| 126 | double delta1, delta2; |
| 127 | // compute neccessary rotation of first segment to align it with main orientation |
| 128 | heading1 = en[i1].heading(en[i2]); |
| 129 | //Put into [-PI/4, PI/4) because we want a minimum of rotation so we don't swap node positions |
| 130 | while(heading1 - avg_heading > Math.PI/4) heading1 -= Math.PI/2; |
| 131 | while(heading1 - avg_heading < -Math.PI/4) heading1 += Math.PI/2; |
| 132 | delta1 = avg_heading - heading1; |
| 133 | // compute neccessary rotation of second segment to align it with main orientation |
| 134 | heading2 = en[i2].heading(en[i3]); |
| 135 | //Put into [-PI/4, PI/4) because we want a minimum of rotation so we don't swap node positions |
| 136 | while(heading2 - avg_heading > Math.PI/4) heading2 -= Math.PI/2; |
| 137 | while(heading2 - avg_heading < -Math.PI/4) heading2 += Math.PI/2; |
| 138 | delta2 = avg_heading - heading2; |
| 139 | // To align a segment, rotate around its center |
| 140 | EastNorth pivot1 = new EastNorth((en[i1].east()+en[i2].east())/2, (en[i1].north()+en[i2].north())/2); |
| 141 | EastNorth A=en[i1].rotate(pivot1, delta1); |
| 142 | EastNorth B=en[i2].rotate(pivot1, delta1); |
| 143 | EastNorth pivot2 = new EastNorth((en[i2].east()+en[i3].east())/2, (en[i2].north()+en[i3].north())/2); |
| 144 | EastNorth C=en[i2].rotate(pivot2, delta2); |
| 145 | EastNorth D=en[i3].rotate(pivot2, delta2); |
| 146 | |
| 147 | // compute intersection of segments |
| 148 | double u=det(B.east() - A.east(), B.north() - A.north(), C.east() - D.east(), C.north() - D.north()); |
| 149 | |
| 150 | // Check for parallel segments and do nothing if they are |
| 151 | // In practice this will probably only happen when a way has been duplicated |
| 152 | |
| 153 | if (u == 0) continue; |
| 154 | |
| 155 | // q is a number between 0 and 1 |
| 156 | // It is the point in the segment where the intersection occurs |
| 157 | // if the segment is scaled to lenght 1 |
| 158 | |
| 159 | double q = det(B.north() - C.north(), B.east() - C.east(), D.north() - C.north(), D.east() - C.east()) / u; |
| 160 | EastNorth intersection = new EastNorth( |
| 161 | B.east() + q * (A.east() - B.east()), |
| 162 | B.north() + q * (A.north() - B.north())); |
| 163 | |
| 164 | Node n = myWay.nodes.get(i1); |
| 165 | double dx = intersection.east()-n.eastNorth.east(); |
| 166 | double dy = intersection.north()-n.eastNorth.north(); |
| 167 | cmds.add(new MoveCommand(n, dx, dy)); |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | Main.main.undoRedo.add(new SequenceCommand(tr("Align Segments orthogonally"), cmds)); |
| 172 | Main.map.repaint(); |
| 173 | } |
| 174 | |
| 175 | static double det(double a, double b, double c, double d) |
| 176 | { |
| 177 | return a * d - b * c; |
| 178 | } |
| 179 | |
| 180 | } |