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}