Index: src/org/openstreetmap/josm/data/validation/OsmValidator.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/OsmValidator.java	(revision 15351)
+++ src/org/openstreetmap/josm/data/validation/OsmValidator.java	(working copy)
@@ -61,6 +61,7 @@
 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.SharpAngles;
 import org.openstreetmap.josm.data.validation.tests.SimilarNamedWays;
 import org.openstreetmap.josm.data.validation.tests.TagChecker;
 import org.openstreetmap.josm.data.validation.tests.TurnrestrictionTest;
@@ -148,6 +149,7 @@
         LongSegment.class, // 3500 .. 3599
         PublicTransportRouteTest.class, // 3600 .. 3699
         RightAngleBuildingTest.class, // 3700 .. 3799
+        SharpAngles.class, // 3800 .. 3899
     };
 
     /**
Index: src/org/openstreetmap/josm/data/validation/tests/SharpAngles.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/SharpAngles.java	(nonexistent)
+++ src/org/openstreetmap/josm/data/validation/tests/SharpAngles.java	(working copy)
@@ -0,0 +1,196 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.TreeSet;
+
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WaySegment;
+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.tools.Geometry;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Find highways that have sharp angles
+ * @author Taylor Smock
+ *
+ */
+public class SharpAngles extends Test {
+    private static final int SHARPANGLESCODE = 3800;
+    /** The code for a sharp angle */
+    protected static final int SHARP_ANGLES = SHARPANGLESCODE + 0;
+    /** The maximum angle for sharp angles */
+    protected double maxAngle = 45.0; // degrees
+    /** The length that at least one way segment must be shorter than */
+    protected double maxLength = 10.0; // meters
+    /** The stepping points for severity */
+    protected Map<Double, Severity> severityBreakPoints = new LinkedHashMap<>();
+    /** Specific highway types to ignore */
+    protected Collection<String> ignoreHighways = new TreeSet<>();
+
+    ArrayList<Way> allWays;
+    /**
+     * Construct a new {@code IntersectionIssues} object
+     */
+    public SharpAngles() {
+        super(tr("Sharp angles"), tr("Check for sharp angles on roads"));
+        setBreakPoints();
+        addIgnoredHighway("rest_area");
+        addIgnoredHighway("platform");
+        addIgnoredHighway("services");
+        addIgnoredHighway("via_ferrata"); // mountainside paths
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor) {
+        super.startTest(monitor);
+        allWays = new ArrayList<>();
+    }
+
+    @Override
+    public void endTest() {
+        Way pWay = null;
+        try {
+            for (Way way : allWays) {
+                pWay = way;
+                checkWayForSharpAngles(way);
+            }
+        } catch (Exception e) {
+            if (pWay != null) {
+                Logging.debug("Way https://osm.org/way/{0} caused an error ({1})", pWay.getOsmId(), e);
+            }
+            Logging.warn(e);
+        }
+        allWays = null;
+        super.endTest();
+    }
+
+    @Override
+    public void visit(Way way) {
+        if (!way.isUsable()) return;
+        if (way.hasKey("highway") && !way.hasTag("area", "yes") &&
+                    !ignoreHighways.contains(way.get("highway"))) {
+            allWays.add(way);
+        }
+    }
+
+    /**
+     * Check nodes in a way for sharp angles
+     * @param way A way to check for sharp angles
+     */
+    public void checkWayForSharpAngles(Way way) {
+        Node node1 = null;
+        Node node2 = null;
+        Node node3 = null;
+        int i = -2;
+        for (Node node : way.getNodes()) {
+            node1 = node2;
+            node2 = node3;
+            node3 = node;
+            checkAngle(node1, node2, node3, i, way, false);
+            i++;
+        }
+        if (way.isClosed() && way.getNodesCount() > 2) {
+            node1 = node2;
+            node2 = node3;
+            // Get the second node, not the first node, since a closed way has first node == second node
+            node3 = way.getNode(1);
+            checkAngle(node1, node2, node3, i, way, true);
+        }
+    }
+
+    private void checkAngle(Node node1, Node node2, Node node3, int i, Way way, boolean last) {
+        if (node1 == null || node2 == null || node3 == null) return;
+        EastNorth n1 = node1.getEastNorth();
+        EastNorth n2 = node2.getEastNorth();
+        EastNorth n3 = node3.getEastNorth();
+        double angle = Math.toDegrees(Math.abs(Geometry.getCornerAngle(n1, n2, n3)));
+        if (angle < maxAngle) {
+            processSharpAngleForErrorCreation(angle, i, way, last, node2);
+        }
+    }
+
+    private void processSharpAngleForErrorCreation(double angle, int i, Way way, boolean last, Node pointNode) {
+        List<WaySegment> waysegmentList = new ArrayList<>();
+        waysegmentList.add(new WaySegment(way, i));
+        if (last) {
+            waysegmentList.add(new WaySegment(way, 0));
+        } else {
+            waysegmentList.add(new WaySegment(way, i+1));
+        }
+        Optional<WaySegment> possibleShortSegment = waysegmentList.stream()
+                .min(Comparator.comparing(segment -> segment.toWay().getLength()));
+        if (possibleShortSegment.isPresent() && possibleShortSegment.get().toWay().getLength() < maxLength) {
+            createNearlyOverlappingError(angle, Collections.singleton(way), pointNode);
+        }
+    }
+
+    private void createNearlyOverlappingError(double angle,
+            Collection<? extends OsmPrimitive> primitiveIssues, OsmPrimitive primitive) {
+        TestError.Builder testError = TestError.builder(this, getSeverity(angle), SHARP_ANGLES)
+                .primitives(primitiveIssues)
+                .highlight(primitive)
+                .message(tr("Sharp angle"));
+        errors.add(testError.build());
+    }
+
+    private Severity getSeverity(double angle) {
+        Severity rSeverity = Severity.OTHER;
+        for (Entry<Double, Severity> entry : severityBreakPoints.entrySet()) {
+            if (angle < entry.getKey()) {
+                rSeverity = entry.getValue();
+            }
+        }
+        return rSeverity;
+    }
+
+    /**
+     * Set the maximum length for the shortest segment
+     * @param length The max length in meters
+     */
+    public void setMaxLength(double length) {
+        maxLength = length;
+    }
+
+    /**
+     * Add a highway to ignore
+     * @param highway
+     */
+    public void addIgnoredHighway(String highway) {
+        ignoreHighways.add(highway);
+    }
+
+    /**
+     * Set the maximum angle
+     * @param angle The maximum angle in degrees.
+     */
+    public void setMaxAngle(double angle) {
+        maxAngle = angle;
+        setBreakPoints();
+    }
+
+    /**
+     * Set the breakpoints for the test
+     */
+    private void setBreakPoints() {
+        severityBreakPoints.put(maxAngle, Severity.OTHER);
+        severityBreakPoints.put(maxAngle * 2 / 3, Severity.WARNING);
+        severityBreakPoints.put(maxAngle / 3, Severity.ERROR);
+    }
+}
Index: test/unit/org/openstreetmap/josm/data/validation/tests/SharpAnglesTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/data/validation/tests/SharpAnglesTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/data/validation/tests/SharpAnglesTest.java	(working copy)
@@ -0,0 +1,145 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.validation.tests;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.openstreetmap.josm.JOSMFixture;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+
+/**
+ * JUnit Test of the Sharp Angles validation test.
+ */
+
+public class SharpAnglesTest {
+    private SharpAngles angles;
+
+    /**
+     * Setup test.
+     * @throws Exception if an error occurs
+     */
+    @Before
+    public void setUp() throws Exception {
+        JOSMFixture.createUnitTestFixture().init();
+        angles = new SharpAngles();
+        angles.initialize();
+        angles.startTest(null);
+    }
+
+    /**
+     * Check a closed loop with no sharp angles
+     */
+    @Test
+    public void closedLoopNoSharpAngles() {
+        Way way = TestUtils.newWay("highway=residential",
+                new Node(new LatLon(0, 0)), new Node(new LatLon(0.1, 0.1)),
+                new Node(new LatLon(0.1, -0.2)), new Node(new LatLon(-0.1, -0.1)));
+        way.addNode(way.firstNode());
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(0, angles.getErrors().size());
+    }
+
+    /**
+     * Check a closed loop with a sharp angle
+     */
+    @Test
+    public void closedLoopSharpAngles() {
+        Way way = TestUtils.newWay("highway=residential",
+                new Node(new LatLon(0, 0)), new Node(new LatLon(0.1, 0.1)),
+                new Node(new LatLon(0.1, -0.2)));
+        way.addNode(way.firstNode());
+        angles.setMaxLength(Double.MAX_VALUE);
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(1, angles.getErrors().size());
+    }
+
+    /**
+     * Check a way for multiple sharp angles
+     */
+    @Test
+    public void testMultipleSharpAngles() {
+        Way way = TestUtils.newWay("highway=residential",
+                new Node(new LatLon(0.005069377713748322, -0.0014832642674429382)),
+                new Node(new LatLon(0.005021097951663415, 0.0008636686205880686)),
+                new Node(new LatLon(0.005085470967776624, -0.00013411313295197088)),
+                new Node(new LatLon(0.005031826787678042, 0.0020116540789620915)));
+        angles.setMaxLength(Double.MAX_VALUE);
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(2,  angles.getErrors().size());
+    }
+
+    /**
+     * Check for no sharp angles
+     */
+    @Test
+    public void testNoSharpAngles() {
+        Way way = TestUtils.newWay("highway=residential",
+                new Node(new LatLon(0, 0)), new Node(new LatLon(0.1, 0.1)),
+                new Node(new LatLon(0.2, 0.3)), new Node(new LatLon(0.3, 0.1)));
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(0, angles.getErrors().size());
+    }
+
+    /**
+     * Ensure that we aren't accidentally using the same node twice.
+     * This was found during initial testing. See way 10041221 (on 20190914)
+     */
+    @Test
+    public void testCheckBadAnglesFromSameNodeTwice() {
+        Way way = TestUtils.newWay("highway=service oneway=yes",
+                new Node(new LatLon(52.8903308, 8.4302322)),
+                new Node(new LatLon(52.8902468, 8.4302138)),
+                new Node(new LatLon(52.8902191, 8.4302282)),
+                new Node(new LatLon(52.8901155, 8.4304753)),
+                new Node(new LatLon(52.8900669, 8.430763)),
+                new Node(new LatLon(52.8901138, 8.4308262)),
+                new Node(new LatLon(52.8902482, 8.4307568)));
+        way.addNode(way.firstNode());
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(0, angles.getErrors().size());
+    }
+
+    /**
+     * Check that special cases are ignored
+     */
+    @Test
+    public void testIgnoredCases() {
+        Way way = TestUtils.newWay("highway=residential",
+                new Node(new LatLon(0, 0)), new Node(new LatLon(0.1, 0.1)),
+                new Node(new LatLon(0, 0.01)));
+        angles.setMaxLength(Double.MAX_VALUE);
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(1, angles.getErrors().size());
+
+        way.put("highway", "rest_area");
+        angles.startTest(null);
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(0, angles.getErrors().size());
+
+        way.put("highway", "residential");
+        angles.startTest(null);
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(1, angles.getErrors().size());
+        way.put("area", "yes");
+        angles.startTest(null);
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(0, angles.getErrors().size());
+        way.put("area", "no");
+        angles.startTest(null);
+        angles.visit(way);
+        angles.endTest();
+        Assert.assertEquals(1, angles.getErrors().size());
+    }
+}
\ No newline at end of file
