Ticket #1621: AlignOrthogonallyAction.java

File AlignOrthogonallyAction.java, 10.5 KB (added by hkucharek, 17 years ago)

Alignment action with orientation given by two selected nodes

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