hello
I am mapping a lot of buildings recently and used the "Align in
Rectangle" a lot. I was glad to hear and use the replacement
"Orthogonalize", but I found some issues.
(I sent Frederik Ramm and Harald Kucharek private mails about this but I
do not want to bother them any more as I think it's probably better to
discuss this on this list.)
I found two "problems" with the code:
1. if a building has sides that are (almost) heading to -PI/4 or +PI/4
it may happen that the function calculates a totally wrong new heading
of the building. this is due to the fact that the normalization of the
headings maps adjacent sides to different "edges" of the
normalization-range which in turn makes the calculation of the average
fail.
2. when more than one building is selected all buildings get the _same_
averaged heading. I changed the algorithm to do the following (which
seems to me to be more intuitive):
* if two additional nodes are selected _all_ ways are aligned to the
heading of the two nodes
* if only ways (no nodes) are selected every way get its own heading.
the attached patch adresses/solves these issues and does also the following:
* cosmetics: the OrthogonalizeAction.java in SVN does not use
solely tabs for indentation but those mixed with spaces. the patch
replaces all those spaces with tabs.
* in the current code the normalizations of the headings is done the
first two times to [ 0 , PI/2 ] but the second two times to
[ -PI/4 , PI/4 ]. IMO the latter one is the better way to go and the
patch changes this.
* it introduces a normalization helper-function.
hopefully the code i wrote reaches your design- and quality-criteria. :-)
hermann
--- OrthogonalizeAction.java 2008-11-18 16:12:34.000000000 +0100
+++ OrthogonalizeAction.java.patched_averagebug 2008-11-18 16:05:32.000000000
+0100
@@ -38,199 +38,235 @@
public final class OrthogonalizeAction extends JosmAction {
public OrthogonalizeAction() {
- super(tr("Orthogonalize shape"),
- "ortho",
- tr("Move nodes so all angles are 0/90/180/270deg"),
- ShortCut.registerShortCut("tools:orthogonalize", tr("Tool: {0}",
tr("Orthogonalize")),
- KeyEvent.VK_Q,
- ShortCut.GROUP_EDIT), true);
+ super(tr("Orthogonalize shape"),
+ "ortho",
+ tr("Move nodes so all angles are 0/90/180/270deg"),
+ ShortCut.registerShortCut("tools:orthogonalize",
tr("Tool: {0}", tr("Orthogonalize")),
+ KeyEvent.VK_Q,
+ ShortCut.GROUP_EDIT), true);
}
public void actionPerformed(ActionEvent e) {
-
+
Collection<OsmPrimitive> sel = Main.ds.getSelected();
-
- ArrayList<Node> dirnodes = new ArrayList<Node>();
-
- // Check the selection if it is suitible for the orthogonalization
+
+ ArrayList<Node> dirnodes = new ArrayList<Node>();
+
+ // Check the selection if it is suitible for the
orthogonalization
+ for (OsmPrimitive osm : sel) {
+ // Check if not more than two nodes in the selection
+ if(osm instanceof Node) {
+ if(dirnodes.size() == 2) {
+
JOptionPane.showMessageDialog(Main.parent, tr("Only two nodes allowed"));
+ return;
+ }
+ dirnodes.add((Node) osm);
+ continue;
+ }
+ // Check if selection consists now only of ways
+ if (!(osm instanceof Way)) {
+ JOptionPane.showMessageDialog(Main.parent,
tr("Selection must consist only of ways."));
+ return;
+ }
+
+ // Check if every way is made of at least four segments
and closed
+ Way way = (Way)osm;
+ if ((way.nodes.size() < 5) ||
(!way.nodes.get(0).equals(way.nodes.get(way.nodes.size() - 1)))) {
+ JOptionPane.showMessageDialog(Main.parent,
tr("Please select closed way(s) of at least four nodes."));
+ return;
+ }
+
+ // Check if every edge in the way is a definite edge of
at least 45 degrees of direction change
+ // Otherwise, two segments could be turned into same
direction and intersection would fail.
+ // Or changes of shape would be too serious.
+ for (int i1=0; i1 < way.nodes.size()-1; i1++) {
+ int i2 = (i1+1) % (way.nodes.size()-1);
+ int i3 = (i1+2) % (way.nodes.size()-1);
+ double angle1
=Math.abs(way.nodes.get(i1).eastNorth.heading(way.nodes.get(i2).eastNorth));
+ double angle2 =
Math.abs(way.nodes.get(i2).eastNorth.heading(way.nodes.get(i3).eastNorth));
+ double delta = Math.abs(angle2 - angle1);
+ while(delta > Math.PI) delta -= Math.PI;
+ if(delta < Math.PI/4) {
+ JOptionPane.showMessageDialog(Main.parent,
tr("Please select ways with almost right angles to orthogonalize."));
+ return;
+ }
+ }
+ }
+
+ if ("EPSG:4326".equals(Main.proj.toString())) {
+ String msg = tr("<html>You are using the EPSG:4326
projection which might lead<br>" +
+ "to undesirable results when doing rectangular
alignments.<br>" +
+ "Change your projection to get rid of this
warning.<br>" +
+ "Do you want to continue?");
+
+ if (!DontShowAgainInfo.show("align_rectangular_4326",
msg, false)) {
+ return;
+ }
+ }
+ // Check, if selection held neither none nor two nodes
+ if(dirnodes.size() == 1) {
+ JOptionPane.showMessageDialog(Main.parent, tr("Only one
node selected"));
+ return;
+ }
+
+ // Now all checks are done and we can now do the neccessary
computations
+ // From here it is assumed that the above checks hold
+ Collection<Command> cmds = new LinkedList<Command>();
+ double align_to_heading = 0.0;
+ boolean use_dirnodes = false;
+
+ if(dirnodes.size() == 2) {
+ // When selection contains two nodes, use the nodes to
compute a direction
+ // to align all ways to
+ align_to_heading =
normalize_angle(dirnodes.get(0).eastNorth.heading(dirnodes.get(1).eastNorth));
+ use_dirnodes = true;
+ }
+
for (OsmPrimitive osm : sel) {
- // Check if not more than two nodes in the selection
- if(osm instanceof Node) {
- if(dirnodes.size() == 2) {
- JOptionPane.showMessageDialog(Main.parent, tr("Only two
nodes allowed"));
- return;
- }
- dirnodes.add((Node) osm);
- continue;
- }
- // Check if selection consists now only of ways
- if (!(osm instanceof Way)) {
- JOptionPane.showMessageDialog(Main.parent, tr("Selection must
consist only of ways."));
- return;
- }
-
- // Check if every way is made of at least four segments and closed
- Way way = (Way)osm;
- if ((way.nodes.size() < 5) ||
(!way.nodes.get(0).equals(way.nodes.get(way.nodes.size() - 1)))) {
- JOptionPane.showMessageDialog(Main.parent, tr("Please select
closed way(s) of at least four nodes."));
- return;
- }
-
- // Check if every edge in the way is a definite edge of at least
45 degrees of direction change
- // Otherwise, two segments could be turned into same direction and
intersection would fail.
- // Or changes of shape would be too serious.
- for (int i1=0; i1 < way.nodes.size()-1; i1++) {
- int i2 = (i1+1) % (way.nodes.size()-1);
- int i3 = (i1+2) % (way.nodes.size()-1);
- double angle1
=Math.abs(way.nodes.get(i1).eastNorth.heading(way.nodes.get(i2).eastNorth));
- double angle2 =
Math.abs(way.nodes.get(i2).eastNorth.heading(way.nodes.get(i3).eastNorth));
- double delta = Math.abs(angle2 - angle1);
- while(delta > Math.PI) delta -= Math.PI;
- if(delta < Math.PI/4) {
- JOptionPane.showMessageDialog(Main.parent, tr("Please
select ways with almost right angles to orthogonalize."));
- return;
- }
- }
- }
-
- if ("EPSG:4326".equals(Main.proj.toString())) {
- String msg = tr("<html>You are using the EPSG:4326 projection
which might lead<br>" +
- "to undesirable results when doing rectangular alignments.<br>"
+
- "Change your projection to get rid of this warning.<br>" +
- "Do you want to continue?");
-
- if (!DontShowAgainInfo.show("align_rectangular_4326", msg, false))
{
- return;
- }
- }
- // Check, if selection held neither none nor two nodes
- if(dirnodes.size() == 1) {
- JOptionPane.showMessageDialog(Main.parent, tr("Only one node
selected"));
- return;
- }
-
- // Now all checks are done and we can now do the neccessary
computations
- // From here it is assumed that the above checks hold
- Collection<Command> cmds = new LinkedList<Command>();
- double align_to_heading;
-
-
- if(dirnodes.size() == 2) { // When selection contained two nodes, use
the nodes to compute a direction to align to
- double heading;
- heading =
dirnodes.get(0).eastNorth.heading(dirnodes.get(1).eastNorth);
- while(heading > Math.PI/4) heading -= Math.PI/2;
- align_to_heading=heading;
- } else { // Otherwise compute the alignment direction from the ways
in the collection
- // First, compute the weighted average of the headings of all segments
- double sum_weighted_headings = 0.0;
- double sum_weights = 0.0;
- for (OsmPrimitive osm : sel) {
- if(!(osm instanceof Way))
- continue;
- Way way = (Way)osm;
- int nodes = way.nodes.size();
- int sides = nodes - 1;
- // To find orientation of all segments, compute
weighted average of all segment's headings
- // all headings are mapped into [0, 3*4*PI) by PI/2 rotations so
both main orientations are mapped into one
- // the headings are weighted by the length of the segment
establishing it, so a longer segment, that is more
- // likely to have the correct orientation, has more influence in
the computing than a short segment, that is easier to misalign.
- for (int i=0; i < sides; i++) {
- double heading;
- double weight;
- heading =
way.nodes.get(i).eastNorth.heading(way.nodes.get(i+1).eastNorth);
- //Put into [0, PI/4) to find main direction
- while(heading > Math.PI/4) heading -= Math.PI/2;
- weight =
way.nodes.get(i).eastNorth.distance(way.nodes.get(i+1).eastNorth);
- sum_weighted_headings += heading*weight;
- sum_weights += weight;
- }
- }
- align_to_heading = sum_weighted_headings/sum_weights;
- }
-
- for (OsmPrimitive osm : sel) {
- if(!(osm instanceof Way))
- continue;
- Way myWay = (Way)osm;
- int nodes = myWay.nodes.size();
- int sides = nodes - 1;
-
- // Copy necessary data into a more suitable data structure
- EastNorth en[] = new EastNorth[sides];
- for (int i=0; i < sides; i++) {
- en[i] = new EastNorth(myWay.nodes.get(i).eastNorth.east(),
myWay.nodes.get(i).eastNorth.north());
- }
-
- for (int i=0; i < sides; i++) {
- // Compute handy indices of three nodes to be used in one loop
iteration.
- // We use segments (i1,i2) and (i2,i3), align them and compute
the new
- // position of the i2-node as the intersection of the
realigned (i1,i2), (i2,i3) segments
- // Not the most efficient algorithm, but we don't handle
millions of nodes...
- int i1 = i;
- int i2 = (i+1)%sides;
- int i3 = (i+2)%sides;
- double heading1, heading2;
- double delta1, delta2;
- // Compute neccessary rotation of first segment to align it
with main orientation
- heading1 = en[i1].heading(en[i2]);
- //Put into [-PI/4, PI/4) because we want a minimum of rotation
so we don't swap node positions
- while(heading1 - align_to_heading > Math.PI/4) heading1 -=
Math.PI/2;
- while(heading1 - align_to_heading < -Math.PI/4) heading1 +=
Math.PI/2;
- delta1 = align_to_heading - heading1;
- // Compute neccessary rotation of second segment to align it
with main orientation
- heading2 = en[i2].heading(en[i3]);
- //Put into [-PI/4, PI/4) because we want a minimum of rotation
so we don't swap node positions
- while(heading2 - align_to_heading > Math.PI/4) heading2 -=
Math.PI/2;
- while(heading2 - align_to_heading < -Math.PI/4) heading2 +=
Math.PI/2;
- delta2 = align_to_heading - heading2;
- // To align a segment, rotate around its center
- EastNorth pivot1 = new
EastNorth((en[i1].east()+en[i2].east())/2, (en[i1].north()+en[i2].north())/2);
- EastNorth A=en[i1].rotate(pivot1, delta1);
- EastNorth B=en[i2].rotate(pivot1, delta1);
- EastNorth pivot2 = new
EastNorth((en[i2].east()+en[i3].east())/2, (en[i2].north()+en[i3].north())/2);
- EastNorth C=en[i2].rotate(pivot2, delta2);
- EastNorth D=en[i3].rotate(pivot2, delta2);
-
- // compute intersection of segments
- double u=det(B.east() - A.east(), B.north() - A.north(),
- C.east() - D.east(), C.north() - D.north());
-
- // Check for parallel segments and do nothing if they are
- // In practice this will probably only happen when a way has
- // been duplicated
-
- if (u == 0) continue;
-
- // q is a number between 0 and 1
- // It is the point in the segment where the intersection occurs
- // if the segment is scaled to length 1
-
- double q = det(B.north() - C.north(), B.east() - C.east(),
- D.north() - C.north(), D.east() - C.east()) / u;
- EastNorth intersection = new EastNorth(
- B.east() + q * (A.east() - B.east()),
- B.north() + q * (A.north() - B.north()));
-
- Node n = myWay.nodes.get(i2);
-
- LatLon ill = Main.proj.eastNorth2latlon(intersection);
- if (!ill.equalsEpsilon(n.coor)) {
- double dx = intersection.east()-n.eastNorth.east();
- double dy = intersection.north()-n.eastNorth.north();
- cmds.add(new MoveCommand(n, dx, dy));
- }
- }
- }
-
- if (cmds.size() > 0) {
- Main.main.undoRedo.add(new SequenceCommand(tr("Orthogonalize"),
cmds));
- Main.map.repaint();
- }
- }
-
- static double det(double a, double b, double c, double d)
- {
- return a * d - b * c;
- }
+ if(!(osm instanceof Way))
+ continue;
+
+ Way way = (Way)osm;
+ int nodes = way.nodes.size();
+ int sides = nodes - 1;
+ // Copy necessary data into a more suitable data
structure
+ EastNorth en[] = new EastNorth[sides];
+ for (int i=0; i < sides; i++) {
+ en[i] = new
EastNorth(way.nodes.get(i).eastNorth.east(),
way.nodes.get(i).eastNorth.north());
+ }
+
+ if (! use_dirnodes) {
+ // To find orientation of all segments, compute
weighted average of all segment's headings
+ // all headings are mapped into [-PI/4, PI/4]
by PI/2 rotations so both main orientations are mapped into one
+ // the headings are weighted by the length of
the segment establishing it, so a longer segment, that is more
+ // likely to have the correct orientation, has
more influence in the computing than a short segment, that is easier to
misalign.
+ double headings[] = new double[sides];
+ double weights[] = new double[sides];
+ for (int i=0; i < sides; i++) {
+ headings[i] =
normalize_angle(way.nodes.get(i).eastNorth.heading(way.nodes.get(i+1).eastNorth));
+ weights[i] =
way.nodes.get(i).eastNorth.distance(way.nodes.get(i+1).eastNorth);
+ }
+
+ // CAVEAT: for orientations near -PI/4 or PI/4
the mapping into ONE orientation fails
+ // resulting in a heading-difference
between adjacent sides of almost PI/2
+ // and a totally wrong average
+ // check for this (use PI/3 as arbitray limit)
and rotate into ONE orientation
+ double angle_diff_max = 0.0;
+ for (int i=0; i < sides; i++) {
+ double diff = 0.0;
+ if (i == 0) {
+ diff =
heading_diff(headings[i], headings[sides - 1]);
+ } else {
+ diff =
heading_diff(headings[i], headings[i - 1]);
+ }
+ if (diff > angle_diff_max)
angle_diff_max = diff;
+ }
+
+ if (angle_diff_max > Math.PI/3) {
+ // rearrange headings: everything < 0
gets PI/2-rotated
+ for (int i=0; i < sides; i++) {
+ if (headings[i] < 0)
+ headings[i] += Math.PI/2;
+ }
+ }
+
+ // TODO:
+ // use angle_diff_max as an indicator that the
way is already orthogonal
+ // e.g. if angle_diff_max is less then
Math.toRadians(0.5)
+ // and do nothing in that case (?)
+
+ // Compute the weighted average of the headings
of all segments
+ double sum_weighted_headings = 0.0;
+ double sum_weights = 0.0;
+ for (int i=0; i < sides; i++) {
+ sum_weighted_headings += headings[i] *
weights[i];
+ sum_weights += weights[i];
+ }
+ align_to_heading =
normalize_angle(sum_weighted_headings/sum_weights);
+ }
+
+
+ for (int i=0; i < sides; i++) {
+ // Compute handy indices of three nodes to be
used in one loop iteration.
+ // We use segments (i1,i2) and (i2,i3), align
them and compute the new
+ // position of the i2-node as the intersection
of the realigned (i1,i2), (i2,i3) segments
+ // Not the most efficient algorithm, but we
don't handle millions of nodes...
+ int i1 = i;
+ int i2 = (i+1)%sides;
+ int i3 = (i+2)%sides;
+ double heading1, heading2;
+ double delta1, delta2;
+ // Compute neccessary rotation of first segment
to align it with main orientation
+ heading1 =
normalize_angle(en[i1].heading(en[i2]), align_to_heading);
+ delta1 = align_to_heading - heading1;
+ // Compute neccessary rotation of second
segment to align it with main orientation
+ heading2 =
normalize_angle(en[i2].heading(en[i3]), align_to_heading);
+ delta2 = align_to_heading - heading2;
+ // To align a segment, rotate around its center
+ EastNorth pivot1 = new
EastNorth((en[i1].east()+en[i2].east())/2, (en[i1].north()+en[i2].north())/2);
+ EastNorth A=en[i1].rotate(pivot1, delta1);
+ EastNorth B=en[i2].rotate(pivot1, delta1);
+ EastNorth pivot2 = new
EastNorth((en[i2].east()+en[i3].east())/2, (en[i2].north()+en[i3].north())/2);
+ EastNorth C=en[i2].rotate(pivot2, delta2);
+ EastNorth D=en[i3].rotate(pivot2, delta2);
+
+ // compute intersection of segments
+ double u=det(B.east() - A.east(), B.north() -
A.north(),
+ C.east() - D.east(), C.north() -
D.north());
+
+ // Check for parallel segments and do nothing
if they are
+ // In practice this will probably only happen
when a way has
+ // been duplicated
+
+ if (u == 0) continue;
+
+ // q is a number between 0 and 1
+ // It is the point in the segment where the
intersection occurs
+ // if the segment is scaled to length 1
+
+ double q = det(B.north() - C.north(), B.east()
- C.east(),
+ D.north() - C.north(), D.east() -
C.east()) / u;
+ EastNorth intersection = new EastNorth(
+ B.east() + q * (A.east() -
B.east()),
+ B.north() + q * (A.north() -
B.north()));
+
+ Node n = way.nodes.get(i2);
+
+ LatLon ill =
Main.proj.eastNorth2latlon(intersection);
+ if (!ill.equalsEpsilon(n.coor)) {
+ double dx =
intersection.east()-n.eastNorth.east();
+ double dy =
intersection.north()-n.eastNorth.north();
+ cmds.add(new MoveCommand(n, dx, dy));
+ }
+ }
+ }
+
+ if (cmds.size() > 0) {
+ Main.main.undoRedo.add(new
SequenceCommand(tr("Orthogonalize"), cmds));
+ Main.map.repaint();
+ }
+ }
+
+ static double det(double a, double b, double c, double d)
+ {
+ return a * d - b * c;
+ }
+
+ static double normalize_angle(double h) {
+ return normalize_angle(h, 0.0);
+ }
+ static double normalize_angle(double h, double align_to) {
+ double llimit = -Math.PI/4;
+ double ulimit = Math.PI/4;
+ while (h - align_to > ulimit) h -= Math.PI/2;
+ while (h - align_to < llimit) h += Math.PI/2;
+
+ return h;
+ }
+
+ static double heading_diff(double h1, double h2) {
+ double heading_delta = h1 > h2 ? h1 - h2 : h2 - h1;
+ return heading_delta;
+ }
}
_______________________________________________
josm-dev mailing list
[email protected]
http://lists.openstreetmap.org/listinfo/josm-dev