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 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 shorterLen = Math.min(ws1.toWay().getLength(), ws2.toWay().getLength());
108        if (shorterLen < maxLength) {
109            createNearlyOverlappingError(angle, way, pointNode);
110        }
111    }
112
113    private void createNearlyOverlappingError(double angle, Way way, OsmPrimitive primitive) {
114        Severity severity = getSeverity(angle);
115        if (severity != Severity.OTHER || (ValidatorPrefHelper.PREF_OTHER.get() || ValidatorPrefHelper.PREF_OTHER_UPLOAD.get())) {
116            int addCode = severity == Severity.OTHER ? 1 : 0;
117            TestError.Builder testError = TestError.builder(this, severity, SHARP_ANGLES + addCode)
118                    .primitives(way)
119                    .highlight(primitive)
120                    .message(tr("Sharp angle"));
121            errors.add(testError.build());
122        }
123    }
124
125    private Severity getSeverity(double angle) {
126        return angle < maxAngle * 2 / 3 ? Severity.WARNING : Severity.OTHER;
127    }
128
129    /**
130     * Set the maximum length for the shortest segment
131     * @param length The max length in meters
132     */
133    public void setMaxLength(double length) {
134        maxLength = length;
135    }
136
137    /**
138     * Add a highway to ignore
139     * @param highway The highway type to ignore (e.g., if you want to ignore residential roads, use "residential")
140     */
141    public void addIgnoredHighway(String highway) {
142        ignoreHighways.add(highway);
143    }
144
145    /**
146     * Set the maximum angle
147     * @param angle The maximum angle in degrees.
148     */
149    public void setMaxAngle(double angle) {
150        maxAngle = angle;
151    }
152
153}