001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.Arrays; 007import java.util.Collection; 008import java.util.TreeSet; 009 010import org.openstreetmap.josm.data.coor.EastNorth; 011import org.openstreetmap.josm.data.osm.Node; 012import org.openstreetmap.josm.data.osm.OsmPrimitive; 013import org.openstreetmap.josm.data.osm.Way; 014import org.openstreetmap.josm.data.osm.WaySegment; 015import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper; 016import org.openstreetmap.josm.data.validation.Severity; 017import org.openstreetmap.josm.data.validation.Test; 018import org.openstreetmap.josm.data.validation.TestError; 019import org.openstreetmap.josm.tools.Geometry; 020import org.openstreetmap.josm.tools.bugreport.BugReport; 021 022/** 023 * Find highways that have sharp angles 024 * @author Taylor Smock 025 * @since 15406 026 */ 027public class SharpAngles extends Test { 028 private static final int SHARPANGLESCODE = 3800; 029 /** The code for a sharp angle */ 030 private static final int SHARP_ANGLES = SHARPANGLESCODE + 0; 031 /** The maximum angle for sharp angles */ 032 private double maxAngle = 45.0; // degrees 033 /** The length that at least one way segment must be shorter than */ 034 private double maxLength = 10.0; // meters 035 /** Specific highway types to ignore */ 036 private final Collection<String> ignoreHighways = new TreeSet<>( 037 Arrays.asList("platform", "rest_area", "services", "via_ferrata")); 038 039 /** 040 * Construct a new {@code IntersectionIssues} object 041 */ 042 public SharpAngles() { 043 super(tr("Sharp angles"), tr("Check for sharp angles on roads")); 044 } 045 046 @Override 047 public void visit(Way way) { 048 if (!way.isUsable()) return; 049 if (shouldBeTestedForSharpAngles(way)) { 050 try { 051 checkWayForSharpAngles(way); 052 } catch (RuntimeException e) { 053 throw BugReport.intercept(e).put("way", way); 054 } 055 } 056 } 057 058 /** 059 * Check whether or not a way should be checked for sharp angles 060 * @param way The way that needs to be checked 061 * @return {@code true} if the way should be checked. 062 */ 063 public boolean shouldBeTestedForSharpAngles(Way way) { 064 return (way.hasKey("highway") && !way.hasTag("area", "yes") && !way.hasKey("via_ferrata_scale") && 065 !ignoreHighways.contains(way.get("highway"))); 066 } 067 068 /** 069 * Check nodes in a way for sharp angles 070 * @param way A way to check for sharp angles 071 */ 072 public void checkWayForSharpAngles(Way way) { 073 Node node1 = null; 074 Node node2 = null; 075 Node node3 = null; 076 int i = -2; 077 for (Node node : way.getNodes()) { 078 node1 = node2; 079 node2 = node3; 080 node3 = node; 081 checkAngle(node1, node2, node3, i, way, false); 082 i++; 083 } 084 if (way.isClosed() && way.getNodesCount() > 2) { 085 node1 = node2; 086 node2 = node3; 087 // Get the second node, not the first node, since a closed way has first node == last node 088 node3 = way.getNode(1); 089 checkAngle(node1, node2, node3, i, way, true); 090 } 091 } 092 093 private void checkAngle(Node node1, Node node2, Node node3, int i, Way way, boolean last) { 094 if (node1 == null || node2 == null || node3 == null) return; 095 EastNorth n1 = node1.getEastNorth(); 096 EastNorth n2 = node2.getEastNorth(); 097 EastNorth n3 = node3.getEastNorth(); 098 double angle = Math.toDegrees(Math.abs(Geometry.getCornerAngle(n1, n2, n3))); 099 if (angle < maxAngle) { 100 processSharpAngleForErrorCreation(angle, i, way, last, node2); 101 } 102 } 103 104 private void processSharpAngleForErrorCreation(double angle, int i, Way way, boolean last, Node pointNode) { 105 WaySegment ws1 = new WaySegment(way, i); 106 WaySegment ws2 = new WaySegment(way, last ? 0 : i + 1); 107 double d1 = ws1.getFirstNode().getEastNorth().distance(ws1.getSecondNode().getEastNorth()); 108 double d2 = ws2.getFirstNode().getEastNorth().distance(ws2.getSecondNode().getEastNorth()); 109 double shorterLen = Math.min(d1, d2); 110 if (shorterLen < maxLength) { 111 createNearlyOverlappingError(angle, way, pointNode); 112 } 113 } 114 115 private void createNearlyOverlappingError(double angle, Way way, OsmPrimitive primitive) { 116 Severity severity = getSeverity(angle); 117 if (severity != Severity.OTHER || (ValidatorPrefHelper.PREF_OTHER.get() || ValidatorPrefHelper.PREF_OTHER_UPLOAD.get())) { 118 int addCode = severity == Severity.OTHER ? 1 : 0; 119 TestError.Builder testError = TestError.builder(this, severity, SHARP_ANGLES + addCode) 120 .primitives(way) 121 .highlight(primitive) 122 .message(tr("Sharp angle")); 123 errors.add(testError.build()); 124 } 125 } 126 127 private Severity getSeverity(double angle) { 128 return angle < maxAngle * 2 / 3 ? Severity.WARNING : Severity.OTHER; 129 } 130 131 /** 132 * Set the maximum length for the shortest segment 133 * @param length The max length in meters 134 */ 135 public void setMaxLength(double length) { 136 maxLength = length; 137 } 138 139 /** 140 * Add a highway to ignore 141 * @param highway The highway type to ignore (e.g., if you want to ignore residential roads, use "residential") 142 */ 143 public void addIgnoredHighway(String highway) { 144 ignoreHighways.add(highway); 145 } 146 147 /** 148 * Set the maximum angle 149 * @param angle The maximum angle in degrees. 150 */ 151 public void setMaxAngle(double angle) { 152 maxAngle = angle; 153 } 154 155}