source: josm/trunk/src/org/openstreetmap/josm/actions/OrthogonalizeAction.java@ 1750

Last change on this file since 1750 was 1640, checked in by stoecker, 15 years ago

little bit more refactoring of coordinate access - patch by jttt

File size: 12.1 KB
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.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.LinkedList;
12
13import javax.swing.JOptionPane;
14
15import org.openstreetmap.josm.Main;
16import org.openstreetmap.josm.command.Command;
17import org.openstreetmap.josm.command.MoveCommand;
18import org.openstreetmap.josm.command.SequenceCommand;
19import org.openstreetmap.josm.data.coor.EastNorth;
20import org.openstreetmap.josm.data.coor.LatLon;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitive;
23import org.openstreetmap.josm.data.osm.Way;
24import org.openstreetmap.josm.tools.DontShowAgainInfo;
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 OrthogonalizeAction extends JosmAction {
37
38 public OrthogonalizeAction() {
39 super(tr("Orthogonalize Shape"),
40 "ortho",
41 tr("Move nodes so all angles are 90 or 270 degree"),
42 Shortcut.registerShortcut("tools:orthogonalize", tr("Tool: {0}", tr("Orthogonalize Shape")),
43 KeyEvent.VK_Q,
44 Shortcut.GROUP_EDIT), true);
45 }
46
47 public void actionPerformed(ActionEvent e) {
48
49 Collection<OsmPrimitive> sel = Main.ds.getSelected();
50
51 ArrayList<Node> dirnodes = new ArrayList<Node>();
52
53 // Check the selection if it is suitible for the orthogonalization
54 for (OsmPrimitive osm : sel) {
55 // Check if not more than two nodes in the selection
56 if(osm instanceof Node) {
57 if(dirnodes.size() == 2) {
58 JOptionPane.showMessageDialog(Main.parent, tr("Only two nodes allowed"));
59 return;
60 }
61 dirnodes.add((Node) osm);
62 continue;
63 }
64 // Check if selection consists now only of ways
65 if (!(osm instanceof Way)) {
66 JOptionPane.showMessageDialog(Main.parent, tr("Selection must consist only of ways."));
67 return;
68 }
69
70 // Check if every way is made of at least four segments and closed
71 Way way = (Way)osm;
72 if ((way.nodes.size() < 5) || (!way.nodes.get(0).equals(way.nodes.get(way.nodes.size() - 1)))) {
73 JOptionPane.showMessageDialog(Main.parent, tr("Please select one or more closed ways of at least four nodes."));
74 return;
75 }
76
77 // Check if every edge in the way is a definite edge of at least 45 degrees of direction change
78 // Otherwise, two segments could be turned into same direction and intersection would fail.
79 // Or changes of shape would be too serious.
80 for (int i1=0; i1 < way.nodes.size()-1; i1++) {
81 int i2 = (i1+1) % (way.nodes.size()-1);
82 int i3 = (i1+2) % (way.nodes.size()-1);
83 double angle1 =Math.abs(way.nodes.get(i1).getEastNorth().heading(way.nodes.get(i2).getEastNorth()));
84 double angle2 = Math.abs(way.nodes.get(i2).getEastNorth().heading(way.nodes.get(i3).getEastNorth()));
85 double delta = Math.abs(angle2 - angle1);
86 while(delta > Math.PI) delta -= Math.PI;
87 if(delta < Math.PI/4) {
88 JOptionPane.showMessageDialog(Main.parent, tr("Please select ways with almost right angles to orthogonalize."));
89 return;
90 }
91 }
92 }
93
94 if ("EPSG:4326".equals(Main.proj.toString())) {
95 String msg = tr("<html>You are using the EPSG:4326 projection which might lead<br>" +
96 "to undesirable results when doing rectangular alignments.<br>" +
97 "Change your projection to get rid of this warning.<br>" +
98 "Do you want to continue?");
99
100 if (!DontShowAgainInfo.show("align_rectangular_4326", msg, false)) {
101 return;
102 }
103 }
104 // Check, if selection held neither none nor two nodes
105 if(dirnodes.size() == 1) {
106 JOptionPane.showMessageDialog(Main.parent, tr("Only one node selected"));
107 return;
108 }
109
110 // Now all checks are done and we can now do the neccessary computations
111 // From here it is assumed that the above checks hold
112 Collection<Command> cmds = new LinkedList<Command>();
113 double align_to_heading = 0.0;
114 boolean use_dirnodes = false;
115
116 if (dirnodes.size() == 2) {
117 // When selection contains two nodes, use the nodes to compute a direction
118 // to align all ways to
119 align_to_heading = normalize_angle(dirnodes.get(0).getEastNorth().heading(dirnodes.get(1).getEastNorth()));
120 use_dirnodes = true;
121 }
122
123 for (OsmPrimitive osm : sel) {
124 if(!(osm instanceof Way))
125 continue;
126
127 Way way = (Way)osm;
128 int nodes = way.nodes.size();
129 int sides = nodes - 1;
130 // Copy necessary data into a more suitable data structure
131 EastNorth en[] = new EastNorth[sides];
132 for (int i=0; i < sides; i++) {
133 en[i] = new EastNorth(way.nodes.get(i).getEastNorth().east(), way.nodes.get(i).getEastNorth().north());
134 }
135
136 if (! use_dirnodes) {
137 // To find orientation of all segments, compute weighted average of all segment's headings
138 // all headings are mapped into [-PI/4, PI/4] by PI/2 rotations so both main orientations are mapped into one
139 // the headings are weighted by the length of the segment establishing it, so a longer segment, that is more
140 // likely to have the correct orientation, has more influence in the computing than a short segment, that is easier to misalign.
141 double headings[] = new double[sides];
142 double weights[] = new double[sides];
143 for (int i=0; i < sides; i++) {
144 headings[i] = normalize_angle(way.nodes.get(i).getEastNorth().heading(way.nodes.get(i+1).getEastNorth()));
145 weights[i] = way.nodes.get(i).getEastNorth().distance(way.nodes.get(i+1).getEastNorth());
146 }
147
148 // CAVEAT: for orientations near -PI/4 or PI/4 the mapping into ONE orientation fails
149 // resulting in a heading-difference between adjacent sides of almost PI/2
150 // and a totally wrong average
151 // check for this (use PI/3 as arbitray limit) and rotate into ONE orientation
152 double angle_diff_max = 0.0;
153 for (int i=0; i < sides; i++) {
154 double diff = 0.0;
155 if (i == 0) {
156 diff = heading_diff(headings[i], headings[sides - 1]);
157 } else {
158 diff = heading_diff(headings[i], headings[i - 1]);
159 }
160 if (diff > angle_diff_max) angle_diff_max = diff;
161 }
162
163 if (angle_diff_max > Math.PI/3) {
164 // rearrange headings: everything < 0 gets PI/2-rotated
165 for (int i=0; i < sides; i++) {
166 if (headings[i] < 0)
167 headings[i] += Math.PI/2;
168 }
169 }
170
171 // TODO:
172 // use angle_diff_max as an indicator that the way is already orthogonal
173 // e.g. if angle_diff_max is less then Math.toRadians(0.5)
174 // and do nothing in that case (?)
175
176 // Compute the weighted average of the headings of all segments
177 double sum_weighted_headings = 0.0;
178 double sum_weights = 0.0;
179 for (int i=0; i < sides; i++) {
180 sum_weighted_headings += headings[i] * weights[i];
181 sum_weights += weights[i];
182 }
183 align_to_heading = normalize_angle(sum_weighted_headings/sum_weights);
184 }
185
186
187 for (int i=0; i < sides; i++) {
188 // Compute handy indices of three nodes to be used in one loop iteration.
189 // We use segments (i1,i2) and (i2,i3), align them and compute the new
190 // position of the i2-node as the intersection of the realigned (i1,i2), (i2,i3) segments
191 // Not the most efficient algorithm, but we don't handle millions of nodes...
192 int i1 = i;
193 int i2 = (i+1)%sides;
194 int i3 = (i+2)%sides;
195 double heading1, heading2;
196 double delta1, delta2;
197 // Compute neccessary rotation of first segment to align it with main orientation
198 heading1 = normalize_angle(en[i1].heading(en[i2]), align_to_heading);
199 delta1 = align_to_heading - heading1;
200 // Compute neccessary rotation of second segment to align it with main orientation
201 heading2 = normalize_angle(en[i2].heading(en[i3]), align_to_heading);
202 delta2 = align_to_heading - heading2;
203 // To align a segment, rotate around its center
204 EastNorth pivot1 = new EastNorth((en[i1].east()+en[i2].east())/2, (en[i1].north()+en[i2].north())/2);
205 EastNorth A=en[i1].rotate(pivot1, delta1);
206 EastNorth B=en[i2].rotate(pivot1, delta1);
207 EastNorth pivot2 = new EastNorth((en[i2].east()+en[i3].east())/2, (en[i2].north()+en[i3].north())/2);
208 EastNorth C=en[i2].rotate(pivot2, delta2);
209 EastNorth D=en[i3].rotate(pivot2, delta2);
210
211 // compute intersection of segments
212 double u=det(B.east() - A.east(), B.north() - A.north(),
213 C.east() - D.east(), C.north() - D.north());
214
215 // Check for parallel segments and do nothing if they are
216 // In practice this will probably only happen when a way has
217 // been duplicated
218
219 if (u == 0) continue;
220
221 // q is a number between 0 and 1
222 // It is the point in the segment where the intersection occurs
223 // if the segment is scaled to length 1
224
225 double q = det(B.north() - C.north(), B.east() - C.east(),
226 D.north() - C.north(), D.east() - C.east()) / u;
227 EastNorth intersection = new EastNorth(
228 B.east() + q * (A.east() - B.east()),
229 B.north() + q * (A.north() - B.north()));
230
231 Node n = way.nodes.get(i2);
232
233 LatLon ill = Main.proj.eastNorth2latlon(intersection);
234 if (!ill.equalsEpsilon(n.getCoor())) {
235 double dx = intersection.east()-n.getEastNorth().east();
236 double dy = intersection.north()-n.getEastNorth().north();
237 cmds.add(new MoveCommand(n, dx, dy));
238 }
239 }
240 }
241
242 if (cmds.size() > 0) {
243 Main.main.undoRedo.add(new SequenceCommand(tr("Orthogonalize"), cmds));
244 Main.map.repaint();
245 }
246 }
247
248 static double det(double a, double b, double c, double d)
249 {
250 return a * d - b * c;
251 }
252
253 static double normalize_angle(double h) {
254 return normalize_angle(h, 0.0);
255 }
256 static double normalize_angle(double h, double align_to) {
257 double llimit = -Math.PI/4;
258 double ulimit = Math.PI/4;
259 while (h - align_to > ulimit) h -= Math.PI/2;
260 while (h - align_to < llimit) h += Math.PI/2;
261
262 return h;
263 }
264
265 static double heading_diff(double h1, double h2) {
266 double heading_delta = h1 > h2 ? h1 - h2 : h2 - h1;
267 return heading_delta;
268 }
269}
Note: See TracBrowser for help on using the repository browser.