source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/ConnectivityRelations.java@ 16299

Last change on this file since 16299 was 16299, checked in by GerdP, 4 years ago

fix spotbugs: RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE

File size: 17.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.util.ArrayList;
8import java.util.Collections;
9import java.util.Comparator;
10import java.util.HashMap;
11import java.util.List;
12import java.util.Map;
13import java.util.Map.Entry;
14import java.util.Set;
15import java.util.regex.Pattern;
16
17import org.openstreetmap.josm.data.osm.Node;
18import org.openstreetmap.josm.data.osm.OsmPrimitive;
19import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
20import org.openstreetmap.josm.data.osm.Relation;
21import org.openstreetmap.josm.data.osm.RelationMember;
22import org.openstreetmap.josm.data.osm.Way;
23import org.openstreetmap.josm.data.validation.Severity;
24import org.openstreetmap.josm.data.validation.Test;
25import org.openstreetmap.josm.data.validation.TestError;
26import org.openstreetmap.josm.tools.Logging;
27
28/**
29 * Check for inconsistencies in lane information between relation and members.
30 */
31public class ConnectivityRelations extends Test {
32
33 protected static final int INCONSISTENT_LANE_COUNT = 3900;
34
35 protected static final int UNKNOWN_CONNECTIVITY_ROLE = 3901;
36
37 protected static final int NO_CONNECTIVITY_TAG = 3902;
38
39 protected static final int MALFORMED_CONNECTIVITY_TAG = 3903;
40
41 protected static final int MISSING_COMMA_CONNECTIVITY_TAG = 3904;
42
43 protected static final int TOO_MANY_ROLES = 3905;
44
45 protected static final int MISSING_ROLE = 3906;
46
47 protected static final int MEMBER_MISSING_LANES = 3907;
48
49 protected static final int CONNECTIVITY_IMPLIED = 3908;
50
51 private static final String CONNECTIVITY_TAG = "connectivity";
52 private static final String VIA = "via";
53 private static final String TO = "to";
54 private static final String FROM = "from";
55 private static final int BW = -1000;
56 private static final Pattern OPTIONAL_LANE_PATTERN = Pattern.compile("\\([0-9-]+\\)");
57 private static final Pattern TO_LANE_PATTERN = Pattern.compile("\\p{Zs}*[,:;]\\p{Zs}*");
58 private static final Pattern MISSING_COMMA_PATTERN = Pattern.compile("[0-9]+\\([0-9]+\\)|\\([0-9]+\\)[0-9]+");
59 private static final Pattern LANE_TAG_PATTERN = Pattern.compile(".*:lanes");
60
61 /**
62 * Constructor
63 */
64 public ConnectivityRelations() {
65 super(tr("Connectivity Relations"), tr("Validates connectivity relations"));
66 }
67
68 /**
69 * Convert the connectivity tag into a map of values
70 *
71 * @param relation A relation with a {@code connectivity} tag.
72 * @return A Map in the form of {@code Map<Lane From, Map<Lane To, Optional>>} May contain nulls when errors are encountered
73 */
74 public static Map<Integer, Map<Integer, Boolean>> parseConnectivityTag(Relation relation) {
75 String cnTag = relation.get(CONNECTIVITY_TAG);
76 if (cnTag == null) {
77 return Collections.emptyMap();
78 }
79 final String joined = cnTag.replace("bw", Integer.toString(BW));
80
81
82 final Map<Integer, Map<Integer, Boolean>> result = new HashMap<>();
83 String[] lanes = joined.split("\\|", -1);
84 for (int i = 0; i < lanes.length; i++) {
85 String[] lane = lanes[i].split(":", -1);
86 int laneNumber;
87 //Ignore connections from bw, since we cannot derive a lane number from bw
88 if (!"bw".equals(lane[0])) {
89 laneNumber = Integer.parseInt(lane[0].trim());
90 } else {
91 laneNumber = BW;
92 }
93 Map<Integer, Boolean> connections = new HashMap<>();
94 String[] toLanes = TO_LANE_PATTERN.split(lane[1]);
95 for (int j = 0; j < toLanes.length; j++) {
96 String toLane = toLanes[j].trim();
97 try {
98 if (OPTIONAL_LANE_PATTERN.matcher(toLane).matches()) {
99 toLane = toLane.replace("(", "").replace(")", "").trim();
100 if (!"bw".equals(toLane)) {
101 connections.put(Integer.parseInt(toLane), Boolean.TRUE);
102 } else
103 connections.put(BW, Boolean.TRUE);
104 } else {
105 if (!toLane.contains("bw")) {
106 connections.put(Integer.parseInt(toLane), Boolean.FALSE);
107 } else {
108 connections.put(BW, Boolean.FALSE);
109 }
110 }
111 } catch (NumberFormatException e) {
112 if (MISSING_COMMA_PATTERN.matcher(toLane).matches()) {
113 connections.put(null, true);
114 } else {
115 connections.put(null, null);
116 }
117 }
118 }
119 result.put(laneNumber, connections);
120 }
121 return result;
122 }
123
124 @Override
125 public void visit(Relation r) {
126 if (r.hasTag("type", CONNECTIVITY_TAG)) {
127 if (!r.hasKey(CONNECTIVITY_TAG)) {
128 errors.add(TestError.builder(this, Severity.WARNING, NO_CONNECTIVITY_TAG)
129 .message(tr("No 'connectivity' tag in connectivity relation")).primitives(r).build());
130 } else if (!r.hasIncompleteMembers()) {
131 boolean badRole = checkForBadRole(r);
132 boolean missingRole = checkForMissingRole(r);
133 if (!badRole && !missingRole) {
134 Map<String, Integer> roleLanes = checkForInconsistentLanes(r);
135 checkForImpliedConnectivity(r, roleLanes);
136 }
137 }
138 }
139 }
140
141 /**
142 * Compare lane tags of members to values in the {@code connectivity} tag of the relation
143 *
144 * @param relation A relation with a {@code connectivity} tag.
145 * @return A Map in the form of {@code Map<Role, Lane Count>}
146 */
147 private Map<String, Integer> checkForInconsistentLanes(Relation relation) {
148 StringBuilder lanelessRoles = new StringBuilder();
149 int lanelessRolesCount = 0;
150 // Lane count from connectivity tag
151 Map<Integer, Map<Integer, Boolean>> connTagLanes = parseConnectivityTag(relation);
152 // If the ways involved in the connectivity tag are assuming a standard 2-way bi-directional highway
153 boolean defaultLanes = true;
154 for (Entry<Integer, Map<Integer, Boolean>> thisEntry : connTagLanes.entrySet()) {
155 for (Entry<Integer, Boolean> thisEntry2 : thisEntry.getValue().entrySet()) {
156 Logging.debug("Checking: " + thisEntry2.toString());
157 if (thisEntry2.getKey() != null && thisEntry2.getKey() > 1) {
158 defaultLanes = false;
159 break;
160 }
161 }
162 if (!defaultLanes) {
163 break;
164 }
165 }
166 // Lane count from member tags
167 Map<String, Integer> roleLanes = new HashMap<>();
168 for (RelationMember rM : relation.getMembers()) {
169 // Check lanes
170 if (rM.getType() == OsmPrimitiveType.WAY) {
171 OsmPrimitive prim = rM.getMember();
172 if (!VIA.equals(rM.getRole())) {
173 Map<String, String> primKeys = prim.getKeys();
174 List<Long> laneCounts = new ArrayList<>();
175 long maxLaneCount;
176 if (prim.hasTag("lanes")) {
177 laneCounts.add(Long.parseLong(prim.get("lanes")));
178 }
179 for (Entry<String, String> entry : primKeys.entrySet()) {
180 String thisKey = entry.getKey();
181 String thisValue = entry.getValue();
182 if (LANE_TAG_PATTERN.matcher(thisKey).matches()) {
183 //Count bar characters
184 long count = thisValue.chars().filter(ch -> ch == '|').count() + 1;
185 laneCounts.add(count);
186 }
187 }
188
189 if (!laneCounts.equals(Collections.emptyList())) {
190 maxLaneCount = Collections.max(laneCounts);
191 roleLanes.put(rM.getRole(), (int) maxLaneCount);
192 } else {
193 String addString = "'" + rM.getRole() + "'";
194 StringBuilder sb = new StringBuilder(addString);
195 if (lanelessRoles.length() > 0) {
196 sb.insert(0, " and ");
197 }
198 lanelessRoles.append(sb.toString());
199 lanelessRolesCount++;
200 }
201 }
202 }
203 }
204
205 if (lanelessRoles.toString().isEmpty()) {
206 boolean fromCheck = roleLanes.get(FROM) < Collections
207 .max(connTagLanes.entrySet(), Comparator.comparingInt(Map.Entry::getKey)).getKey();
208 boolean toCheck = false;
209 for (Entry<Integer, Map<Integer, Boolean>> to : connTagLanes.entrySet()) {
210 if (!to.getValue().containsKey(null)) {
211 toCheck = roleLanes.get(TO) < Collections
212 .max(to.getValue().entrySet(), Comparator.comparingInt(Map.Entry::getKey)).getKey();
213 } else {
214 if (to.getValue().containsValue(true)) {
215 errors.add(TestError.builder(this, Severity.ERROR, MISSING_COMMA_CONNECTIVITY_TAG)
216 .message(tr("Connectivity tag missing comma between optional and non-optional values")).primitives(relation)
217 .build());
218 } else {
219 errors.add(TestError.builder(this, Severity.ERROR, MALFORMED_CONNECTIVITY_TAG)
220 .message(tr("Connectivity tag contains unusual data")).primitives(relation)
221 .build());
222 }
223 }
224 }
225 if (fromCheck || toCheck) {
226 errors.add(TestError.builder(this, Severity.WARNING, INCONSISTENT_LANE_COUNT)
227 .message(tr("Inconsistent lane numbering between relation and member tags")).primitives(relation)
228 .build());
229 }
230 } else if (!defaultLanes) {
231 errors.add(TestError.builder(this, Severity.WARNING, MEMBER_MISSING_LANES)
232 .message(trn("Relation {0} member missing lanes tag", "Relation {0} members missing 'lanes' or '*:lanes' tag",
233 lanelessRolesCount, lanelessRoles)).primitives(relation)
234 .build());
235 }
236 return roleLanes;
237 }
238
239 /**
240 * Check the relation to see if the connectivity described is already implied by other data
241 *
242 * @param relation A relation with a {@code connectivity} tag.
243 * @param roleLanes The lane counts for each relation role
244 */
245 private void checkForImpliedConnectivity(Relation relation, Map<String, Integer> roleLanes) {
246 boolean connImplied = true;
247 Map<Integer, Map<Integer, Boolean>> connTagLanes = parseConnectivityTag(relation);
248 // Don't flag connectivity as already implied when:
249 // - Lane counts are different on the roads
250 // - Placement tags convey the connectivity
251 // - The relation passes through an intersection
252 // - If via member is a node, it's connected to ways not in the relation
253 // - If a via member is a way, ways not in the relation connect to its nodes
254 // - Highways that appear to be merging have a different cumulative number of lanes than
255 // the highway that they're merging into
256
257 connImplied = checkMemberTagsForImpliedConnectivity(relation, roleLanes) && !checkForIntersectionAtMembers(relation);
258 // Check if connectivity tag implies default connectivity
259 if (connImplied) {
260 for (Entry<Integer, Map<Integer, Boolean>> to : connTagLanes.entrySet()) {
261 int fromLane = to.getKey();
262 for (Entry<Integer, Boolean> lane : to.getValue().entrySet()) {
263 if (lane.getKey() != null && fromLane != lane.getKey()) {
264 connImplied = false;
265 break;
266 }
267 }
268 if (!connImplied)
269 break;
270 }
271 }
272
273 if (connImplied) {
274 errors.add(TestError.builder(this, Severity.WARNING, CONNECTIVITY_IMPLIED)
275 .message(tr("This connectivity may already be implied")).primitives(relation)
276 .build());
277 }
278 }
279
280 /**
281 * Check to see if there is an intersection present at the via member
282 *
283 * @param relation A relation with a {@code connectivity} tag.
284 * @return A Boolean that indicates whether an intersection is present at the via member
285 */
286 private static boolean checkForIntersectionAtMembers(Relation relation) {
287 OsmPrimitive viaPrim = relation.findRelationMembers("via").get(0);
288 Set<OsmPrimitive> relationMembers = relation.getMemberPrimitives();
289
290 if (viaPrim.getType() == OsmPrimitiveType.NODE) {
291 Node viaNode = (Node) viaPrim;
292 List<Way> parentWays = viaNode.getParentWays();
293 if (parentWays.size() > 2) {
294 for (Way thisWay : parentWays) {
295 if (!relationMembers.contains(thisWay) && thisWay.hasTag("highway")) {
296 return true;
297 }
298 }
299 }
300 } else if (viaPrim.getType() == OsmPrimitiveType.WAY) {
301 Way viaWay = (Way) viaPrim;
302 for (Node thisNode : viaWay.getNodes()) {
303 List<Way> parentWays = thisNode.getParentWays();
304 if (parentWays.size() > 2) {
305 for (Way thisWay : parentWays) {
306 if (!relationMembers.contains(thisWay) && thisWay.hasTag("highway")) {
307 return true;
308 }
309 }
310 }
311 }
312 }
313 return false;
314 }
315
316 /**
317 * Check the relation to see if the connectivity described is already implied by the relation members' tags
318 *
319 * @param relation A relation with a {@code connectivity} tag.
320 * @param roleLanes The lane counts for each relation role
321 * @return Whether connectivity is already implied by tags on relation members
322 */
323 private static boolean checkMemberTagsForImpliedConnectivity(Relation relation, Map<String, Integer> roleLanes) {
324 // The members have different lane counts
325 if (roleLanes.containsKey(TO) && roleLanes.containsKey(FROM) && (!roleLanes.get(TO).equals(roleLanes.get(FROM)))) {
326 return false;
327 }
328
329 // The members don't have placement tags defining the connectivity
330 List<RelationMember> members = relation.getMembers();
331 Map<String, OsmPrimitive> toFromMembers = new HashMap<>();
332 for (RelationMember mem : members) {
333 if (mem.getRole().equals(FROM)) {
334 toFromMembers.put(FROM, mem.getMember());
335 } else if (mem.getRole().equals(TO)) {
336 toFromMembers.put(TO, mem.getMember());
337 }
338 }
339
340 return toFromMembers.get(TO).hasKey("placement") || toFromMembers.get(FROM).hasKey("placement");
341 }
342
343 /**
344 * Check if the roles of the relation are appropriate
345 *
346 * @param relation A relation with a {@code connectivity} tag.
347 * @return Whether one or more of the relation's members has an unusual role
348 */
349 private boolean checkForBadRole(Relation relation) {
350 // Check role names
351 int viaWays = 0;
352 int viaNodes = 0;
353 for (RelationMember relationMember : relation.getMembers()) {
354 if (relationMember.getMember() instanceof Way) {
355 if (relationMember.hasRole(VIA))
356 viaWays++;
357 else if (!relationMember.hasRole(FROM) && !relationMember.hasRole(TO)) {
358 return true;
359 }
360 } else if (relationMember.getMember() instanceof Node) {
361 if (!relationMember.hasRole(VIA)) {
362 return true;
363 }
364 viaNodes++;
365 }
366 }
367 return mixedViaNodeAndWay(relation, viaWays, viaNodes);
368 }
369
370 /**
371 * Check if the relation contains all necessary roles
372 *
373 * @param relation A relation with a {@code connectivity} tag.
374 * @return Whether the relation is missing one or more of the critical {@code from}, {@code via}, or {@code to} roles
375 */
376 private static boolean checkForMissingRole(Relation relation) {
377 List<String> necessaryRoles = new ArrayList<>();
378 necessaryRoles.add(FROM);
379 necessaryRoles.add(VIA);
380 necessaryRoles.add(TO);
381
382 List<String> roleList = new ArrayList<>();
383 for (RelationMember relationMember: relation.getMembers()) {
384 roleList.add(relationMember.getRole());
385 }
386
387 return !roleList.containsAll(necessaryRoles);
388 }
389
390 /**
391 * Check if the relation's roles are on appropriate objects
392 *
393 * @param relation A relation with a {@code connectivity} tag.
394 * @param viaWays The number of ways in the relation with the {@code via} role
395 * @param viaNodes The number of nodes in the relation with the {@code via} role
396 * @return Whether the relation is missing one or more of the critical 'from', 'via', or 'to' roles
397 */
398 private boolean mixedViaNodeAndWay(Relation relation, int viaWays, int viaNodes) {
399 String message = "";
400 if (viaNodes > 1) {
401 if (viaWays > 0) {
402 message = tr("Relation should not contain mixed 'via' ways and nodes");
403 } else {
404 message = tr("Multiple 'via' roles only allowed with ways");
405 }
406 }
407 if (message.isEmpty()) {
408 return false;
409 } else {
410 errors.add(TestError.builder(this, Severity.WARNING, TOO_MANY_ROLES)
411 .message(message).primitives(relation).build());
412 return true;
413 }
414 }
415
416}
Note: See TracBrowser for help on using the repository browser.