From dcf3f22e5a25361fb35e4c673b95457575a66ca3 Mon Sep 17 00:00:00 2001
From: marxin <mliska@suse.cz>
Date: Sun, 4 Mar 2018 10:25:28 +0100
Subject: [PATCH] Introduce almost square check for buildings.

---
 src/org/openstreetmap/josm/data/osm/Way.java       | 36 ++++++++++++
 .../josm/data/validation/OsmValidator.java         |  2 +
 .../validation/tests/RightAngleBuildingTest.java   | 66 ++++++++++++++++++++++
 src/org/openstreetmap/josm/tools/Geometry.java     | 10 ++++
 .../org/openstreetmap/josm/data/osm/WayTest.java   |  1 +
 .../org/openstreetmap/josm/tools/GeometryTest.java | 36 ++++++++++++
 6 files changed, 151 insertions(+)
 create mode 100644 src/org/openstreetmap/josm/data/validation/tests/RightAngleBuildingTest.java

diff --git a/src/org/openstreetmap/josm/data/osm/Way.java b/src/org/openstreetmap/josm/data/osm/Way.java
index 8fd0cef49..f28cef9ff 100644
--- a/src/org/openstreetmap/josm/data/osm/Way.java
+++ b/src/org/openstreetmap/josm/data/osm/Way.java
@@ -15,6 +15,8 @@ import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.CopyList;
+import org.openstreetmap.josm.tools.Geometry;
+import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Pair;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -805,4 +807,38 @@ public final class Way extends OsmPrimitive implements IWay {
             n.clearCachedStyle();
         }
     }
+
+    /**
+     * Returns angles of vertices.
+     *
+     * @return angles of the way
+     * @since xxx
+     */
+    public List<Pair<Double, Node>> getAngles() {
+        List<Pair<Double, Node>> angles = new ArrayList<Pair<Double, Node>>();
+
+        List<Node> nodes = getNodes();
+        int count = getNodesCount();
+
+        for (int i = 0; i < count; i++)
+            Logging.info(nodes.get(i).toString());
+
+        for (int i = 1; i < count - 1; i++)
+        {
+            Node n0 = nodes.get(i - 1);
+            Node n1 = nodes.get(i);
+            Node n2 = nodes.get(i + 1);
+
+            double angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(n0.getEastNorth(), n1.getEastNorth(),
+                    n2.getEastNorth()));
+            angles.add(new Pair<Double, Node>(angle, n1));
+        }
+
+        angles.add(new Pair<Double, Node>(Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
+                nodes.get(count - 2).getEastNorth(),
+                nodes.get(0).getEastNorth(),
+                nodes.get(1).getEastNorth())), nodes.get(0)));
+
+        return angles;
+    }
 }
diff --git a/src/org/openstreetmap/josm/data/validation/OsmValidator.java b/src/org/openstreetmap/josm/data/validation/OsmValidator.java
index 30a2661fe..4cf70c621 100644
--- a/src/org/openstreetmap/josm/data/validation/OsmValidator.java
+++ b/src/org/openstreetmap/josm/data/validation/OsmValidator.java
@@ -52,6 +52,7 @@ import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
 import org.openstreetmap.josm.data.validation.tests.PowerLines;
 import org.openstreetmap.josm.data.validation.tests.PublicTransportRouteTest;
 import org.openstreetmap.josm.data.validation.tests.RelationChecker;
+import org.openstreetmap.josm.data.validation.tests.RightAngleBuildingTest;
 import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay;
 import org.openstreetmap.josm.data.validation.tests.SimilarNamedWays;
 import org.openstreetmap.josm.data.validation.tests.TagChecker;
@@ -139,6 +140,7 @@ public final class OsmValidator {
         ApiCapabilitiesTest.class, // 3400 .. 3499
         LongSegment.class, // 3500 .. 3599
         PublicTransportRouteTest.class, // 3600 .. 3699
+        RightAngleBuildingTest.class, // 3700 .. 3799
     };
 
     /**
diff --git a/src/org/openstreetmap/josm/data/validation/tests/RightAngleBuildingTest.java b/src/org/openstreetmap/josm/data/validation/tests/RightAngleBuildingTest.java
new file mode 100644
index 000000000..15ee55250
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/validation/tests/RightAngleBuildingTest.java
@@ -0,0 +1,66 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.Pair;
+
+/**
+ * Checks for buildings with angles close to right angle.
+ *
+ * @since xxx
+ */
+public class RightAngleBuildingTest extends Test {
+
+    /** Maximum angle difference from right angle that is considered as invalid. */
+    protected double maxAngleDelta;
+
+    /** Minimum angle difference from right angle that is considered as invalid. */
+    protected double minAngleDelta;
+
+    /**
+     * Constructs a new {@code RightAngleBuildingTest} test.
+     */
+    public RightAngleBuildingTest() {
+        super(tr("Almost right angle buildings"),
+                tr("Checks for buildings that have angles close to right angle and are not orthogonalized."));
+    }
+
+    @Override
+    public void visit(Way w) {
+        if (!w.isUsable() || !w.isClosed() || !isBuilding(w)) return;
+
+        for (Pair<Double, Node> pair: w.getAngles())
+            if (!checkAngle(w, pair.a, pair.b))
+                return;
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor) {
+        super.startTest(monitor);
+        maxAngleDelta = Config.getPref().getDouble("validator.RightAngleBuilding.maximumDelta", 10.0);
+        minAngleDelta = Config.getPref().getDouble("validator.RightAngleBuilding.minimumDelta", 0.001);
+    }
+
+    private boolean checkAngle(Way w, double angle, Node n) {
+        double difference = Math.abs(angle - 90);
+
+        if (difference > minAngleDelta && difference < maxAngleDelta) {
+            errors.add(TestError.builder(this, Severity.WARNING, 3701)
+                    .message(tr("Building with an almost square angle"))
+                    .primitives(w)
+                    .highlight(n)
+                    .build());
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java
index 379192e92..83e1771f5 100644
--- a/src/org/openstreetmap/josm/tools/Geometry.java
+++ b/src/org/openstreetmap/josm/tools/Geometry.java
@@ -792,6 +792,16 @@ public final class Geometry {
         return result;
     }
 
+    /** Get angles in radians and return it's value in range [0, 180].
+    *
+    * @param angle the angle in radians
+    * @return normalized angle
+    * @since xxx
+    */
+   public static double getNormalizedAngleInDegrees(double angle) {
+       return Math.abs(180 * angle / Math.PI);
+   }
+
     /**
      * Compute the centroid/barycenter of nodes
      * @param nodes Nodes for which the centroid is wanted
diff --git a/test/unit/org/openstreetmap/josm/data/osm/WayTest.java b/test/unit/org/openstreetmap/josm/data/osm/WayTest.java
index e6c63c596..bcd51cddc 100644
--- a/test/unit/org/openstreetmap/josm/data/osm/WayTest.java
+++ b/test/unit/org/openstreetmap/josm/data/osm/WayTest.java
@@ -11,6 +11,7 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 import org.openstreetmap.josm.JOSMFixture;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.tools.Geometry;
 
 /**
  * Unit tests of the {@code Way} class.
diff --git a/test/unit/org/openstreetmap/josm/tools/GeometryTest.java b/test/unit/org/openstreetmap/josm/tools/GeometryTest.java
index 1581e3cbd..d364a689b 100644
--- a/test/unit/org/openstreetmap/josm/tools/GeometryTest.java
+++ b/test/unit/org/openstreetmap/josm/tools/GeometryTest.java
@@ -1,6 +1,8 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.tools;
 
+import static org.junit.Assert.assertEquals;
+
 import java.io.FileInputStream;
 import java.util.List;
 
@@ -9,7 +11,9 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.search.SearchCompiler;
@@ -108,4 +112,36 @@ public class GeometryTest {
             Assert.assertEquals(15093.201209424187, areaAndPerimeter.getPerimeter(), 1e-3);
         }
     }
+
+    /**
+     * Test of {@link Geometry#getNormalizedAngleInDegrees(double) method.
+     */
+    @Test
+    public void testRightAngle() {
+        Node n1 = new Node(1);
+        Node n2 = new Node(2);
+        Node n3 = new Node(3);
+        n1.setCoor(new LatLon(10.22873540462851, 6.169719398316592));
+        n2.setCoor(new LatLon(10.229332494162811, 6.16978130985785));
+        n3.setCoor(new LatLon(10.22924937004949, 6.17060908367496));
+
+        double angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(n1.getEastNorth(),
+                n2.getEastNorth(), n3.getEastNorth()));
+        Assert.assertEquals(90, angle, 1e-8);
+        angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(n1.getEastNorth(),
+                n2.getEastNorth(), n1.getEastNorth()));
+        Assert.assertEquals(0, angle, 1e-8);
+
+        n1.setCoor(new LatLon(10.2295011, 6.1693106));
+        n2.setCoor(new LatLon(10.2294958, 6.16930635));
+        n3.setCoor(new LatLon(10.2294895, 6.1693039));
+
+        angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(n1.getEastNorth(),
+                n2.getEastNorth(), n3.getEastNorth()));
+        Assert.assertEquals(162.66381817961337, angle, 1e-5);
+
+        angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(n3.getEastNorth(),
+                n2.getEastNorth(), n1.getEastNorth()));
+        Assert.assertEquals(162.66381817961337, angle, 1e-5);
+    }
 }
-- 
2.16.3

