001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.util.ArrayList; 008import java.util.List; 009import java.util.Map.Entry; 010import java.util.stream.Collectors; 011 012import org.openstreetmap.josm.data.osm.Node; 013import org.openstreetmap.josm.data.osm.OsmPrimitive; 014import org.openstreetmap.josm.data.osm.Way; 015import org.openstreetmap.josm.data.validation.Severity; 016import org.openstreetmap.josm.data.validation.Test; 017import org.openstreetmap.josm.data.validation.TestError; 018 019/** 020 * Find nodes with direction tag and invalid number of parent ways or position in way. See #20019. 021 * @author Gerd Petermann 022 * @since 17349 023 */ 024public class DirectionNodes extends Test { 025 private static final int MULTIPLE_WAYS_CODE = 4000; 026 private static final int END_NODE_CODE = 4001; 027 private static final int NO_WAY_CODE = 4002; 028 private static final int NO_SUITABLE_WAY = 4003; 029 030 private static final String INVALID_USE_MSG = tr("Invalid usage of direction on node"); 031 private static final String DISPUTED_USE_MSG = tr("Disputed usage of direction on node"); 032 033 /** 034 * Construct a new {@code DirectionNodes} object 035 */ 036 public DirectionNodes() { 037 super(tr("Direction nodes"), tr("Check for nodes which have a ''forward'' or ''backward'' direction")); 038 } 039 040 @Override 041 public void visit(Node n) { 042 if (!n.isUsable() || !n.isTagged()) 043 return; 044 for (Entry<String, String> tag : n.getKeys().entrySet()) { 045 if (("forward".equals(tag.getValue()) || "backward".equals(tag.getValue())) 046 && ("direction".equals(tag.getKey()) || tag.getKey().endsWith(":direction"))) { 047 checkParents(n, tag.getKey() + "=forward|backbard"); 048 } 049 } 050 } 051 052 private static boolean isSuitableParentWay(Way w) { 053 return w.hasKey("highway", "railway", "waterway") || w.hasTag("man_made", "pipeline"); 054 } 055 056 private void checkParents(Node n, String tag) { 057 final List<Way> ways = new ArrayList<>(); 058 int count = 0; 059 int countHighWays = 0; 060 for (Way w : n.getParentWays()) { 061 if (isSuitableParentWay(w)) { 062 ways.add(w); 063 if (w.hasKey("highway")) 064 countHighWays++; 065 } 066 count++; 067 } 068 069 // ignore minor highways (footway, path etc) if a major highway is found 070 if (countHighWays > 1 && (n.hasKey("highway") || n.hasTag("traffic_sign", "city_limit"))) { 071 List<Way> minor = ways.stream().filter(w -> !w.hasTag("highway", Highways.CLASSIFIED_HIGHWAYS)) 072 .collect(Collectors.toList()); 073 if (minor.size() != countHighWays) { 074 ways.removeAll(minor); 075 } 076 } 077 boolean needsParentWays = n.isNew() 078 || (!n.isOutsideDownloadArea() && n.getDataSet().getDataSourceArea() != null); 079 TestError.Builder builder = null; 080 if (ways.isEmpty() && needsParentWays) { 081 if (count == 0) { 082 builder = TestError.builder(this, Severity.ERROR, NO_WAY_CODE).message(INVALID_USE_MSG, 083 marktr("Unconnected node with {0}. Use angle or cardinal direction"), tag); 084 } else { 085 builder = TestError.builder(this, Severity.WARNING, NO_SUITABLE_WAY).message(INVALID_USE_MSG, 086 marktr("Node with {0} should be connected to a linear way"), tag); 087 } 088 } else if (ways.size() == 1) { 089 Way w = ways.get(0); 090 if (w.firstNode() == n || w.lastNode() == n) { 091 builder = TestError.builder(this, Severity.OTHER, END_NODE_CODE).message(DISPUTED_USE_MSG, 092 marktr("Node with {0} on end of way"), tag); 093 } 094 } else if (ways.size() > 1) { 095 builder = TestError.builder(this, Severity.OTHER, MULTIPLE_WAYS_CODE).message(DISPUTED_USE_MSG, 096 marktr("Node with {0} on a connection of multiple ways"), tag); 097 } 098 if (builder != null) { 099 List<OsmPrimitive> primitives = new ArrayList<>(); 100 primitives.add(n); 101 primitives.addAll(ways); 102 errors.add(builder.primitives(primitives).highlight(n).build()); 103 } 104 } 105}