1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.data.validation.tests;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
5 |
|
---|
6 | import java.util.Arrays;
|
---|
7 | import java.util.Collection;
|
---|
8 | import java.util.Objects;
|
---|
9 | import java.util.TreeSet;
|
---|
10 |
|
---|
11 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
12 | import org.openstreetmap.josm.data.osm.Node;
|
---|
13 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
14 | import org.openstreetmap.josm.data.osm.Way;
|
---|
15 | import org.openstreetmap.josm.data.osm.WaySegment;
|
---|
16 | import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
|
---|
17 | import org.openstreetmap.josm.data.validation.Severity;
|
---|
18 | import org.openstreetmap.josm.data.validation.Test;
|
---|
19 | import org.openstreetmap.josm.data.validation.TestError;
|
---|
20 | import org.openstreetmap.josm.tools.Geometry;
|
---|
21 | import org.openstreetmap.josm.tools.bugreport.BugReport;
|
---|
22 |
|
---|
23 | /**
|
---|
24 | * Find highways that have sharp angles
|
---|
25 | * @author Taylor Smock
|
---|
26 | * @since 15406
|
---|
27 | */
|
---|
28 | public class SharpAngles extends Test {
|
---|
29 | private static final int SHARPANGLESCODE = 3800;
|
---|
30 | /** The code for a sharp angle */
|
---|
31 | private static final int SHARP_ANGLES = SHARPANGLESCODE + 0;
|
---|
32 | /** The maximum angle for sharp angles */
|
---|
33 | private double maxAngle = 45.0; // degrees
|
---|
34 | /** The length that at least one way segment must be shorter than */
|
---|
35 | private double maxLength = 10.0; // meters
|
---|
36 | /** Specific highway types to ignore */
|
---|
37 | private Collection<String> ignoreHighways = new TreeSet<>(
|
---|
38 | Arrays.asList("platform", "rest_area", "services", "via_ferrata"));
|
---|
39 |
|
---|
40 | /**
|
---|
41 | * Construct a new {@code IntersectionIssues} object
|
---|
42 | */
|
---|
43 | public SharpAngles() {
|
---|
44 | super(tr("Sharp angles"), tr("Check for sharp angles on roads"));
|
---|
45 | }
|
---|
46 |
|
---|
47 | @Override
|
---|
48 | public void visit(Way way) {
|
---|
49 | if (!way.isUsable()) return;
|
---|
50 | if (shouldBeTestedForSharpAngles(way)) {
|
---|
51 | try {
|
---|
52 | checkWayForSharpAngles(way);
|
---|
53 | } catch (RuntimeException e) {
|
---|
54 | throw BugReport.intercept(e).put("way", way);
|
---|
55 | }
|
---|
56 | }
|
---|
57 | }
|
---|
58 |
|
---|
59 | /**
|
---|
60 | * Check whether or not a way should be checked for sharp angles
|
---|
61 | * @param way The way that needs to be checked
|
---|
62 | * @return {@code true} if the way should be checked.
|
---|
63 | */
|
---|
64 | public boolean shouldBeTestedForSharpAngles(Way way) {
|
---|
65 | return (way.hasKey("highway") && !way.hasTag("area", "yes") && !way.hasKey("via_ferrata_scale") &&
|
---|
66 | !ignoreHighways.contains(way.get("highway")));
|
---|
67 | }
|
---|
68 |
|
---|
69 | /**
|
---|
70 | * Check nodes in a way for sharp angles
|
---|
71 | * @param way A way to check for sharp angles
|
---|
72 | */
|
---|
73 | public void checkWayForSharpAngles(Way way) {
|
---|
74 | Node node1 = null;
|
---|
75 | Node node2 = null;
|
---|
76 | Node node3 = null;
|
---|
77 | int i = -2;
|
---|
78 | for (Node node : way.getNodes()) {
|
---|
79 | node1 = node2;
|
---|
80 | node2 = node3;
|
---|
81 | node3 = node;
|
---|
82 | checkAngle(node1, node2, node3, i, way, false);
|
---|
83 | i++;
|
---|
84 | }
|
---|
85 | if (way.isClosed() && way.getNodesCount() > 2) {
|
---|
86 | node1 = node2;
|
---|
87 | node2 = node3;
|
---|
88 | // Get the second node, not the first node, since a closed way has first node == last node
|
---|
89 | node3 = way.getNode(1);
|
---|
90 | checkAngle(node1, node2, node3, i, way, true);
|
---|
91 | }
|
---|
92 | }
|
---|
93 |
|
---|
94 | private void checkAngle(Node node1, Node node2, Node node3, int i, Way way, boolean last) {
|
---|
95 | if (node1 == null || node2 == null || node3 == null) return;
|
---|
96 | EastNorth n1 = node1.getEastNorth();
|
---|
97 | EastNorth n2 = node2.getEastNorth();
|
---|
98 | EastNorth n3 = node3.getEastNorth();
|
---|
99 | double angle = Math.toDegrees(Math.abs(Geometry.getCornerAngle(n1, n2, n3)));
|
---|
100 | if (angle < maxAngle) {
|
---|
101 | processSharpAngleForErrorCreation(angle, i, way, last, node2);
|
---|
102 | }
|
---|
103 | }
|
---|
104 |
|
---|
105 | private void processSharpAngleForErrorCreation(double angle, int i, Way way, boolean last, Node pointNode) {
|
---|
106 | WaySegment ws1 = new WaySegment(way, i);
|
---|
107 | WaySegment ws2 = new WaySegment(way, last ? 0 : i + 1);
|
---|
108 | double shorterLen = Math.min(ws1.toWay().getLength(), ws2.toWay().getLength());
|
---|
109 | if (shorterLen < maxLength) {
|
---|
110 | createNearlyOverlappingError(angle, way, pointNode);
|
---|
111 | }
|
---|
112 | }
|
---|
113 |
|
---|
114 | private void createNearlyOverlappingError(double angle, Way way, OsmPrimitive primitive) {
|
---|
115 | Severity severity = getSeverity(angle);
|
---|
116 | if (severity != Severity.OTHER || (ValidatorPrefHelper.PREF_OTHER.get() || ValidatorPrefHelper.PREF_OTHER_UPLOAD.get())) {
|
---|
117 | int addCode = severity == Severity.OTHER ? 1 : 0;
|
---|
118 | TestError.Builder testError = TestError.builder(this, severity, SHARP_ANGLES + addCode)
|
---|
119 | .primitives(way)
|
---|
120 | .highlight(primitive)
|
---|
121 | .message(tr("Sharp angle"));
|
---|
122 | errors.add(testError.build());
|
---|
123 | }
|
---|
124 | }
|
---|
125 |
|
---|
126 | private Severity getSeverity(double angle) {
|
---|
127 | return angle < maxAngle * 2 / 3 ? Severity.WARNING : Severity.OTHER;
|
---|
128 | }
|
---|
129 |
|
---|
130 | /**
|
---|
131 | * Set the maximum length for the shortest segment
|
---|
132 | * @param length The max length in meters
|
---|
133 | */
|
---|
134 | public void setMaxLength(double length) {
|
---|
135 | maxLength = length;
|
---|
136 | }
|
---|
137 |
|
---|
138 | /**
|
---|
139 | * Add a highway to ignore
|
---|
140 | * @param highway The highway type to ignore (e.g., if you want to ignore residential roads, use "residential")
|
---|
141 | */
|
---|
142 | public void addIgnoredHighway(String highway) {
|
---|
143 | ignoreHighways.add(highway);
|
---|
144 | }
|
---|
145 |
|
---|
146 | /**
|
---|
147 | * Set the maximum angle
|
---|
148 | * @param angle The maximum angle in degrees.
|
---|
149 | */
|
---|
150 | public void setMaxAngle(double angle) {
|
---|
151 | maxAngle = angle;
|
---|
152 | }
|
---|
153 |
|
---|
154 | @Override
|
---|
155 | public int hashCode() {
|
---|
156 | return 31 * super.hashCode() + Objects.hash(ignoreHighways, maxAngle, maxLength);
|
---|
157 | }
|
---|
158 |
|
---|
159 | @Override
|
---|
160 | public boolean equals(Object obj) {
|
---|
161 | if (this == obj)
|
---|
162 | return true;
|
---|
163 | if (!super.equals(obj) || getClass() != obj.getClass())
|
---|
164 | return false;
|
---|
165 | SharpAngles other = (SharpAngles) obj;
|
---|
166 | return Objects.equals(ignoreHighways, other.ignoreHighways)
|
---|
167 | && Double.doubleToLongBits(maxAngle) == Double.doubleToLongBits(other.maxAngle)
|
---|
168 | && Double.doubleToLongBits(maxLength) == Double.doubleToLongBits(other.maxLength);
|
---|
169 | }
|
---|
170 | }
|
---|