From e9d14f87365f3fe0230775b549d1de36427c6435 Mon Sep 17 00:00:00 2001 From: Gabe Reichenberger Date: Wed, 2 Dec 2020 11:06:40 -0800 Subject: [PATCH] tollValidationCheck (#432) * new atlas check for tollescape * tollEscapeCheck first iteration * 3 case toll validation check * 3 case check, finishing unit tests. * spotless apply * removing dockerfile to exclude it * updated instructions, status: in false positive analysis phase. * integration test fixes * integration test fixes * spotless apply and successful test. * removing prints * updating equals --> equalsIgnoreCase * code smell cleanup * code smells continued * code smells continued * code smells continued * code smells continued * code smells continued * code smells continued * code smells continued * fixing config * addressed comments regarding tollValidationCheck and added serislVersionUID for suddenHighwayTypeChangeCheck * applied all rcommendations successfully. * updated naming convention for angle related variables. * had to update config variable and test config value to reflect updated variable names. * new config value for limiting recursion while searching for nearby toll features and added more documentation for the config values.. * Fixed naming of angle difference function and addressed comments * spotless apply * Added auto fix suggestions for case 1 and 2 --- config/configuration.json | 11 + docs/available_checks.md | 1 + docs/checks/tollValidationCheck | 46 ++ .../edges/SuddenHighwayTypeChangeCheck.java | 1 + .../validation/tag/TollValidationCheck.java | 634 ++++++++++++++++++ .../tag/TollValidationCheckTest.java | 57 ++ .../tag/TollValidationCheckTestRule.java | 91 +++ 7 files changed, 841 insertions(+) create mode 100644 docs/checks/tollValidationCheck create mode 100644 src/main/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheck.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheckTest.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheckTestRule.java diff --git a/config/configuration.json b/config/configuration.json index 0535aa8ec..684aae1de 100644 --- a/config/configuration.json +++ b/config/configuration.json @@ -963,6 +963,17 @@ }, "minHighwayType": "tertiary" }, + "TollValidationCheck": { + "minimumHighwayType": "tertiary", + "maxAngleDiffForContiguousWays": 40.0, + "minInAndOutEdges": 1.0, + "maxIterationForNearbySearch": 15.0, + "challenge": { + "description": "Toll tags might be incorrect since tolls could possibly be avoided", + "blurb": "Toll Escape", + "instruction": "Please see instructions per feature for actions to take." + } + }, "UnusualLayerTagsCheck": { "challenge": { "description": "Tunnels (negative), junctions (zero) and bridges (zero or positive) should have meaningful layer tags attached to them. A missing layer tag implies layer value 0. If there is an explicit layer tag, then it must be between -5 and 5.", diff --git a/docs/available_checks.md b/docs/available_checks.md index ac1456fc0..c5ddc0998 100644 --- a/docs/available_checks.md +++ b/docs/available_checks.md @@ -85,6 +85,7 @@ This document is a list of tables with a description and link to documentation f | [RoadNameSpellingConsistencyCheck](checks/RoadNameSpellingConsistencyCheck.md) | The purpose of this check is to identify road segments that have a name Tag with a different spelling from that of other segments of the same road. This check is primarily meant to catch small errors in spelling, such as a missing letter, letter accent mixups, or capitalization errors. | | ShortNameCheck | The short name check will validate that any and all names contain at least 2 letters in the name. | | [StreetNameIntegersOnlyCheck](checks/streetNameIntegersOnlyCheck.md) | The purpose of this check is to identify streets whose names contain integers only. | +| [TollValidationCheck](checks/tollValidationCheck) | The purpose of this check is to identify ways that need to have their toll tags investigated/added/removed. | [TunnelBridgeHeightLimitCheck](checks/tunnelBridgeHeightLimitCheck.md) | The purpose of this check is to identify roads with limited vertical clearance which do not have a maxheight tag. | | [UnusualLayerTagsCheck](checks/unusualLayerTagsCheck.md) | The purpose of this check is to identify layer tag values when accompanied by invalid tunnel and bridge tags. | | [ConditionalRestrictionCheck](checks/conditionalRestrictionCheck.md) | The purpose of this check is to identify elements that have a :conditional tag that does not respect the established format. | diff --git a/docs/checks/tollValidationCheck b/docs/checks/tollValidationCheck new file mode 100644 index 000000000..fc36d822b --- /dev/null +++ b/docs/checks/tollValidationCheck @@ -0,0 +1,46 @@ +# TollValidationCheck + +#### Description +The purpose of this check is to identify ways that need to have their toll tags investigated/added/removed. + +#### Configurables +- ***minimumHighwayType***: minimum highway type to include in this check. Tolls mainly reside on highways. +- ***maxAngleDiffForContiguousWays***: maximum angle difference between edges to be considered contiguous ways. This is important since most of the toll related investigation is on highways with small angle turns. +- ***minInAndOutEdges***: minimum count of Main in AND out edges from given way. This is important because there is logic that relies on an edge that does have at least one edge on the downstream and upstream side. +- ***maxIterationForNearbySearch***: maximum amount of iterations to complete while searching for nearby toll features. + +#### Live Examples +Way Intersects Toll Feature - Missing toll tag +1. The way [id:824285021](https://www.openstreetmap.org/way/824285021) intersects a toll feature and needs a toll=yes tag. + +Inconsistent Toll Tags - way sandwiched between 2 toll=yes ways but does not have a toll tag +1. The way [id:498038529](https://www.openstreetmap.org/way/498038529) is inconsistent with its surrounding toll tags. + +Escapable Toll - Way has routes that escape toll feature so toll should be investigated for removal +1. The way [id:2039409](https://www.openstreetmap.org/way/546540482) is deemed "escapable" since on either side of it there are ways with toll=no or no toll tag at all before +intersecting a toll feature. There needs to be an investigation as to if the way in question or the surrounding ways have been modeled properly. + +#### Code Review +This check evaluates [Edges](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Edge.java) +, it attempts to find large jumps in highway classifications. + +##### Validating the Object +We first validate that the incoming object is: +* An Edge +* A Main edge +* The Edge is Car Navigable +* The Edge is of a specified minimum highway type (tertiary) +* is not private access + +##### Flagging the Edge +##### Three scenarios of TollValidationCheck +###### Scenario 1 +* Way intersects toll feature but does not have a toll tag. +###### Scenario 2 +* Way has inconsistent toll tagging compared to surrounding ways. +###### Scenario 3 +* Toll is escapable so the edge in question should not have toll tag since a toll is not required to drive on the road. + + +To learn more about the code, please look at the comments in the source code for the check. +[TollValidationCheck.java](../../src/main/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheck.java) diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/linear/edges/SuddenHighwayTypeChangeCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/linear/edges/SuddenHighwayTypeChangeCheck.java index 9d5593a7a..3a1a590b4 100644 --- a/src/main/java/org/openstreetmap/atlas/checks/validation/linear/edges/SuddenHighwayTypeChangeCheck.java +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/linear/edges/SuddenHighwayTypeChangeCheck.java @@ -29,6 +29,7 @@ public class SuddenHighwayTypeChangeCheck extends BaseCheck private static final List FALLBACK_INSTRUCTIONS = Collections .singletonList(SUDDEN_HIGHWAY_TYPE_CHANGE_INSTRUCTION); private static final String HIGHWAY_MINIMUM_DEFAULT = HighwayTag.RESIDENTIAL.toString(); + private static final long serialVersionUID = -4091313755808560402L; private final HighwayTag minHighwayType; /** diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheck.java new file mode 100644 index 000000000..9ac9ce008 --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheck.java @@ -0,0 +1,634 @@ +package org.openstreetmap.atlas.checks.validation.tag; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.openstreetmap.atlas.checks.atlas.predicates.TypePredicates; +import org.openstreetmap.atlas.checks.base.BaseCheck; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.geography.Heading; +import org.openstreetmap.atlas.geography.atlas.change.FeatureChange; +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity; +import org.openstreetmap.atlas.geography.atlas.items.Area; +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity; +import org.openstreetmap.atlas.geography.atlas.items.AtlasObject; +import org.openstreetmap.atlas.geography.atlas.items.Edge; +import org.openstreetmap.atlas.geography.atlas.items.Node; +import org.openstreetmap.atlas.tags.AccessTag; +import org.openstreetmap.atlas.tags.BarrierTag; +import org.openstreetmap.atlas.tags.HighwayTag; +import org.openstreetmap.atlas.tags.TollTag; +import org.openstreetmap.atlas.utilities.configuration.Configuration; +import org.openstreetmap.atlas.utilities.scalars.Angle; +import org.openstreetmap.atlas.utilities.scalars.Counter; + +/** + * This check attempts to validate toll tags based on 3 scenarios. 1. Edge intersects toll feature + * but is missing toll tag 2. Edge has inconsistent toll tag compared to surrounding edges 3. Edge + * has route that can escape toll feature so the toll tag is modeled incorrectly. + * + * @author greichenberger + */ +public class TollValidationCheck extends BaseCheck +{ + private static final long serialVersionUID = -4286937145318778446L; + private static final String INTERSECTS_TOLL_FEATURE = "Way {0, number, #} intersects toll feature but is missing toll tag, please investigate toll tag addition."; + private static final String ESCAPABLE_TOLL = "Toll tags need to be investigated for removal on way {0, number, #}. Please check ways {1, number, #} and {2, number, #} and affected nearby ways for modeling issues. Nearby toll features " + + "that might be helpful are: upstream {3, number, #} and downstream {4, number, #}."; + private static final String INCONSISTENT_TOLL_TAGS = "Way {0, number, #} has an inconsistent toll tag with its surrounding ways. Please check for proper toll tag modeling."; + private static final List FALLBACK_INSTRUCTIONS = Arrays.asList(INTERSECTS_TOLL_FEATURE, + ESCAPABLE_TOLL, INCONSISTENT_TOLL_TAGS); + private static final String HIGHWAY_MINIMUM_DEFAULT = HighwayTag.RESIDENTIAL.toString(); + private static final Double MAX_ANGLE_DIFF_DEFAULT = 40.0; + private static final double MIN_IN_OUT_EDGES_DEFAULT = 1.0; + private static final double MAX_ITERATION_FOR_SEARCH_DEFAULT = 15.0; + private final HighwayTag minHighwayType; + private final double minInAndOutEdges; + private final double maxAngleDiffForContiguousWays; + private final double maxIterationForNearbySearch; + + /** + * @param configuration + * config file params if any. + */ + public TollValidationCheck(final Configuration configuration) + { + super(configuration); + final String highwayType = this.configurationValue(configuration, "minHighwayType", + HIGHWAY_MINIMUM_DEFAULT); + this.minHighwayType = Enum.valueOf(HighwayTag.class, highwayType.toUpperCase()); + this.maxAngleDiffForContiguousWays = this.configurationValue(configuration, + "maxAngleDiffForContiguousWays", MAX_ANGLE_DIFF_DEFAULT); + this.minInAndOutEdges = this.configurationValue(configuration, "minInAndOutEdges", + MIN_IN_OUT_EDGES_DEFAULT); + this.maxIterationForNearbySearch = this.configurationValue(configuration, + "maxIterationForNearbySearch", MAX_ITERATION_FOR_SEARCH_DEFAULT); + } + + /** + * @param object + * The {@link AtlasObject} you are checking + * @return validation check + */ + @Override + public boolean validCheckForObject(final AtlasObject object) + { + return TypePredicates.IS_EDGE.test(object) && ((Edge) object).isMainEdge() + && ((Edge) object).highwayTag().isMoreImportantThan(this.minHighwayType) + && !isFlagged(object.getOsmIdentifier()) + && !this.isPrivateAccess(object.getOsmTags()); + } + + /** + * @param object + * object in question + * @return flag + */ + @Override + protected Optional flag(final AtlasObject object) + { + final Edge edgeInQuestion = ((Edge) object).getMainEdge(); + final Map edgeInQuestionTags = edgeInQuestion.getOsmTags(); + final Set alreadyCheckedNearbyTollEdges = new HashSet<>(); + final Set alreadyCheckedObjectIds = new HashSet<>(); + + // Case One: Edge intersects toll feature but is missing toll tag. + if (this.isCaseOne(edgeInQuestion, edgeInQuestionTags)) + { + markAsFlagged(edgeInQuestion.getOsmIdentifier()); + return Optional.of(this + .createFlag(object, + this.getLocalizedInstruction(0, edgeInQuestion.getOsmIdentifier())) + .addFixSuggestion(FeatureChange.add( + (AtlasEntity) ((CompleteEntity) CompleteEntity + .from((AtlasEntity) object)).withAddedTag(TollTag.KEY, + TollTag.YES.toString().toLowerCase()), + object.getAtlas()))); + } + + // Case Two: Inconsistent toll tags on edge. + if (this.isCaseTwo(edgeInQuestion, edgeInQuestionTags)) + { + markAsFlagged(edgeInQuestion.getOsmIdentifier()); + return Optional.of(this + .createFlag(object, + this.getLocalizedInstruction(2, edgeInQuestion.getOsmIdentifier())) + .addFixSuggestion(FeatureChange.add( + (AtlasEntity) ((CompleteEntity) CompleteEntity + .from((AtlasEntity) object)).withAddedTag(TollTag.KEY, + TollTag.YES.toString().toLowerCase()), + object.getAtlas()))); + } + + final Edge escapableInEdge = this + .edgeProvingBackwardsIsEscapable(edgeInQuestion, alreadyCheckedObjectIds) + .orElse(null); + final Edge escapableOutEdge = this + .edgeProvingForwardIsEscapable(edgeInQuestion, alreadyCheckedObjectIds) + .orElse(null); + + // Case three: tag modeling needs to be investigate on and around edge in question/proved + // escapable routes + if (this.escapableEdgesNullChecker(escapableInEdge, escapableOutEdge) && this + .isCaseThree(edgeInQuestion, edgeInQuestionTags, escapableInEdge, escapableOutEdge)) + { + markAsFlagged(object.getOsmIdentifier()); + final Counter counter = new Counter(); + final Long nearbyTollFeatureUpstream = this.getNearbyTollFeatureInEdgeSide( + edgeInQuestion, alreadyCheckedNearbyTollEdges, counter).orElse(null); + counter.reset(); + final Long nearbyTollFeatureDownstream = this.getNearbyTollFeatureOutEdgeSide( + edgeInQuestion, alreadyCheckedNearbyTollEdges, counter).orElse(null); + return Optional.of(this.createFlag(object, + this.getLocalizedInstruction(1, edgeInQuestion.getOsmIdentifier(), + escapableInEdge.getOsmIdentifier(), escapableOutEdge.getOsmIdentifier(), + nearbyTollFeatureUpstream, nearbyTollFeatureDownstream))); + } + return Optional.empty(); + } + + @Override + protected List getFallbackInstructions() + { + return FALLBACK_INSTRUCTIONS; + } + + /** + * @param edge1 + * just an edge + * @param edge2 + * just another edge + * @return angle in degrees between edges (segments) + */ + private Angle angleDiffBetweenEdges(final Edge edge1, final Edge edge2) + { + final Optional edge1heading = edge1.asPolyLine().finalHeading(); + final Optional edge2heading = edge2.asPolyLine().initialHeading(); + if (edge1heading.isPresent() && edge2heading.isPresent()) + { + return edge1heading.get().difference(edge2heading.get()); + } + return Angle.NONE; + } + + /** + * @param tags + * some osm tags + * @return boolean if the barrier tag contains 'toll' + */ + private boolean barrierTagContainsToll(final Map tags) + { + return tags.get(BarrierTag.KEY).contains(TollTag.KEY); + } + + /** + * @param tags + * some edge tags + * @param tags2 + * some other edge tags + * @return both sets of tags have toll=yes + */ + private boolean bothTollYesTag(final Map tags, final Map tags2) + { + return this.hasTollYesTag(tags) && this.hasTollYesTag(tags2); + } + + /** + * @param tags + * some osm tags + * @return boolean if tags contains key 'barrier' + */ + private boolean containsBarrierTag(final Map tags) + { + return tags.containsKey(BarrierTag.KEY); + } + + /** + * @param tags + * some osm tags + * @return boolean if tags contains key 'highway' + */ + private boolean containsHighwayTag(final Map tags) + { + return tags.containsKey(HighwayTag.KEY); + } + + /** + * @param tags + * some osm tags + * @return boolean if tags contains key 'toll' + */ + private boolean containsTollTag(final Map tags) + { + return tags.containsKey(TollTag.KEY); + } + + /** + * @param edge + * some edge + * @return boolean if edge intersects toll feature + */ + private boolean edgeIntersectsTollFeature(final Edge edge) + { + final Iterable intersectingAreas = edge.getAtlas().areasIntersecting(edge.bounds()); + final Iterable edgeNodes = edge.connectedNodes(); + for (final Area area : intersectingAreas) + { + final boolean areaContainsPolyline = area.asPolygon().overlaps(edge.asPolyLine()); + final Map areaTags = area.getOsmTags(); + if (areaContainsPolyline && this.containsBarrierTag(areaTags) + && this.barrierTagContainsToll(areaTags)) + { + return true; + } + } + + for (final Node node : edgeNodes) + { + final Map nodeTags = node.getOsmTags(); + if ((this.containsHighwayTag(nodeTags) && this.highwayTagContainsToll(nodeTags)) + || this.containsBarrierTag(nodeTags) && this.barrierTagContainsToll(nodeTags)) + { + return true; + } + } + return false; + } + + /** + * @param edge + * edge in question + * @param alreadyCheckedObjectIds + * already been touched IDs + * @return edge that proves backwards is escapable + */ + private Optional edgeProvingBackwardsIsEscapable(final Edge edge, + final Set alreadyCheckedObjectIds) + { + final Set inEdges = this.getInEdges(edge); + + for (final Edge inEdge : inEdges) + { + if (inEdges.size() >= this.minInAndOutEdges + && !alreadyCheckedObjectIds.contains(inEdge.getIdentifier()) + && inEdge.highwayTag().isMoreImportantThan(this.minHighwayType) + && this.hasSameHighwayTag(edge, inEdge) + && this.angleDiffBetweenEdges(inEdge, edge) + .asDegrees() <= this.maxAngleDiffForContiguousWays) + { + alreadyCheckedObjectIds.add(inEdge.getIdentifier()); + final Map keySet = inEdge.getOsmTags(); + + if ((!this.containsTollTag(keySet)) || (this.containsTollTag(keySet) + && keySet.get(TollTag.KEY).equalsIgnoreCase(TollTag.NO.toString()))) + { + return Optional.of(inEdge); + } + + if (!this.edgeIntersectsTollFeature(inEdge) && this.containsTollTag(keySet) + && keySet.get(TollTag.KEY).equalsIgnoreCase(TollTag.YES.toString())) + { + return this.edgeProvingBackwardsIsEscapable(inEdge, alreadyCheckedObjectIds); + } + } + } + return Optional.empty(); + } + + /** + * @param edge + * edge in question + * @param alreadyCheckedObjectIds + * already been touched Ids + * @return edge proving forward is escapable. + */ + private Optional edgeProvingForwardIsEscapable(final Edge edge, + final Set alreadyCheckedObjectIds) + { + final Set outEdges = this.getOutEdges(edge); + for (final Edge outEdge : outEdges) + { + if (outEdges.size() >= this.minInAndOutEdges + && !alreadyCheckedObjectIds.contains(outEdge.getIdentifier()) + && outEdge.highwayTag().isMoreImportantThan(this.minHighwayType) + && this.hasSameHighwayTag(edge, outEdge) + && this.angleDiffBetweenEdges(edge, outEdge) + .asDegrees() <= this.maxAngleDiffForContiguousWays) + { + alreadyCheckedObjectIds.add(outEdge.getIdentifier()); + final Map keySet = outEdge.getOsmTags(); + + if ((!this.containsTollTag(keySet)) || (this.containsTollTag(keySet) + && keySet.get(TollTag.KEY).equalsIgnoreCase(TollTag.NO.toString()))) + { + return Optional.of(outEdge); + } + if (!this.edgeIntersectsTollFeature(outEdge) && this.containsTollTag(keySet) + && keySet.get(TollTag.KEY).equalsIgnoreCase(TollTag.YES.toString())) + { + return this.edgeProvingForwardIsEscapable(outEdge, alreadyCheckedObjectIds); + } + } + } + return Optional.empty(); + } + + /** + * @param escapableInEdge + * escapable in edge + * @param escapableOutEdge + * escapable out edge + * @return boolean for if they are both null + */ + private boolean escapableEdgesNullChecker(final Edge escapableInEdge, + final Edge escapableOutEdge) + { + return escapableInEdge != null && escapableOutEdge != null; + } + + /** + * @param edge + * an edge + * @param alreadyCheckedNearbyTollEdges + * edge that have already been touched when recursing. + * @return Id for intersecting toll feature. + */ + private Optional getAreaOrNodeIntersectionId(final Edge edge, + final Set alreadyCheckedNearbyTollEdges) + { + alreadyCheckedNearbyTollEdges.add(edge.getIdentifier()); + final Iterable intersectingAreas = edge.getAtlas().areasIntersecting(edge.bounds()); + final Iterable edgeNodes = edge.connectedNodes(); + for (final Area area : intersectingAreas) + { + final boolean areaContainsPolyline = area.asPolygon().overlaps(edge.asPolyLine()); + final Map areaTags = area.getOsmTags(); + if (areaContainsPolyline && this.containsBarrierTag(areaTags) + && (this.barrierTagContainsToll(areaTags))) + { + return Optional.of(area.getOsmIdentifier()); + } + } + + for (final Node node : edgeNodes) + { + final Map nodeTags = node.getOsmTags(); + if ((this.containsHighwayTag(nodeTags) && this.highwayTagContainsToll(nodeTags)) + || (this.containsBarrierTag(nodeTags) && this.barrierTagContainsToll(nodeTags))) + { + return Optional.of(node.getOsmIdentifier()); + } + } + return Optional.empty(); + } + + /** + * @param edge + * some edge + * @return in edges that are car navigable and positive (eliminates reverse edges) + */ + private Set getInEdges(final Edge edge) + { + return edge.inEdges().stream().filter( + someEdge -> someEdge.isMainEdge() && HighwayTag.isCarNavigableHighway(someEdge)) + .collect(Collectors.toSet()); + } + + /** + * @param edge + * edge in question + * @return nearby toll feature id on the in edge side of the edge in question (upstream) + */ + private Optional getNearbyTollFeatureInEdgeSide(final Edge edge, + final Set alreadyCheckedNearbyTollEdges, final Counter counter) + { + final Set inEdges = this.getInEdges(edge); + for (final Edge inEdge : inEdges) + { + if (inEdges.size() >= this.minInAndOutEdges && this.edgeIntersectsTollFeature(inEdge) + && !alreadyCheckedNearbyTollEdges.contains(inEdge.getIdentifier())) + { + return this.getAreaOrNodeIntersectionId(inEdge, alreadyCheckedNearbyTollEdges); + } + if (counter.getValue() <= this.maxIterationForNearbySearch + && inEdges.size() >= this.minInAndOutEdges + && !this.edgeIntersectsTollFeature(inEdge) + && !alreadyCheckedNearbyTollEdges.contains(inEdge.getIdentifier())) + { + alreadyCheckedNearbyTollEdges.add(inEdge.getIdentifier()); + counter.add(1); + return this.getNearbyTollFeatureInEdgeSide(inEdge, alreadyCheckedNearbyTollEdges, + counter); + } + } + return Optional.empty(); + } + + /** + * @param edge + * edge in question + * @return nearby toll feature id on the out edge side of the edge in question (downstream) + */ + private Optional getNearbyTollFeatureOutEdgeSide(final Edge edge, + final Set alreadyCheckedNearbyTollEdges, final Counter counter) + { + final Set outEdges = this.getOutEdges(edge); + + for (final Edge outEdge : outEdges) + { + if (outEdges.size() >= this.minInAndOutEdges && this.edgeIntersectsTollFeature(outEdge) + && !alreadyCheckedNearbyTollEdges.contains(outEdge.getIdentifier())) + { + return this.getAreaOrNodeIntersectionId(outEdge, alreadyCheckedNearbyTollEdges); + } + if (counter.getValue() <= this.maxIterationForNearbySearch + && outEdges.size() >= this.minInAndOutEdges + && !this.edgeIntersectsTollFeature(outEdge) + && !alreadyCheckedNearbyTollEdges.contains(outEdge.getIdentifier())) + { + alreadyCheckedNearbyTollEdges.add(outEdge.getIdentifier()); + counter.add(1); + return this.getNearbyTollFeatureOutEdgeSide(outEdge, alreadyCheckedNearbyTollEdges, + counter); + } + } + return Optional.empty(); + } + + /** + * @param edge + * some edge + * @return out edges that are car navigable and positive (eliminates reverse edges) + */ + private Set getOutEdges(final Edge edge) + { + return edge.outEdges().stream().filter( + someEdge -> someEdge.isMainEdge() && HighwayTag.isCarNavigableHighway(someEdge)) + .collect(Collectors.toSet()); + } + + /** + * @param edge + * some edge + * @return tag inconsistencies between 3 consecutive edges. + */ + private boolean hasInconsistentTollTag(final Edge edge) + { + final Set inEdges = edge.inEdges().stream() + .filter(inEdge -> inEdge.getOsmIdentifier() != edge.getOsmIdentifier() + && inEdge.isMainEdge() && HighwayTag.isCarNavigableHighway(inEdge)) + .collect(Collectors.toSet()); + final Set outEdges = edge.outEdges().stream() + .filter(outEdge -> outEdge.getOsmIdentifier() != edge.getOsmIdentifier() + && outEdge.isMainEdge() && HighwayTag.isCarNavigableHighway(outEdge)) + .collect(Collectors.toSet()); + if (inEdges.size() == 1 && outEdges.size() == 1) + { + return this.inconsistentTollTagLogic(inEdges, outEdges, edge); + } + return false; + } + + /** + * @param edge1 + * some edge + * @param edge2 + * some other edge + * @return boolean regarding if they have same highway tag?\ + */ + private boolean hasSameHighwayTag(final Edge edge1, final Edge edge2) + { + if (HighwayTag.highwayTag(edge1).isPresent() && HighwayTag.highwayTag(edge2).isPresent()) + { + return edge1.highwayTag().equals(edge2.highwayTag()); + } + return false; + } + + /** + * @param tags + * some edge tags + * @return if tags contains toll=yes + */ + private boolean hasTollYesTag(final Map tags) + { + return tags.keySet().stream().anyMatch(tag -> tag.equals(TollTag.KEY)) + && tags.get(TollTag.KEY).equalsIgnoreCase(TollTag.YES.toString()); + } + + /** + * @param tags + * some osm tags + * @return boolean for if the highway tag contains 'toll' + */ + private boolean highwayTagContainsToll(final Map tags) + { + return tags.get(HighwayTag.KEY).contains(TollTag.KEY); + } + + /** + * @param inEdges + * some inedges + * @param outEdges + * some outedges + * @param edge + * some edge + * @return boolean for inconsistent tagging + */ + private boolean inconsistentTollTagLogic(final Set inEdges, final Set outEdges, + final Edge edge) + { + for (final Edge inEdge : inEdges) + { + for (final Edge outEdge : outEdges) + { + if (this.hasSameHighwayTag(edge, inEdge) && this.hasSameHighwayTag(edge, outEdge) + && this.angleDiffBetweenEdges(edge, outEdge) + .asDegrees() <= this.maxAngleDiffForContiguousWays + && this.angleDiffBetweenEdges(inEdge, edge) + .asDegrees() <= this.maxAngleDiffForContiguousWays) + { + final Map inEdgeOsmTags = inEdge.getOsmTags(); + final Map outEdgeOsmTags = outEdge.getOsmTags(); + if (this.bothTollYesTag(inEdgeOsmTags, outEdgeOsmTags)) + { + return true; + } + } + } + } + return false; + } + + /** + * @param edgeInQuestion + * the edge in question + * @param edgeInQuestionTags + * tags of edge in question + * @return boolean if is case one This case checks if an edge is intersecting a toll feature and + * is missing a toll tag. + */ + private boolean isCaseOne(final Edge edgeInQuestion, + final Map edgeInQuestionTags) + { + return !this.hasTollYesTag(edgeInQuestionTags) + && this.edgeIntersectsTollFeature(edgeInQuestion); + } + + /** + * @param edgeInQuestion + * edge in question + * @param edgeInQuestionTags + * edge in question osm tags + * @param escapableInEdge + * edge that proves edge in question is toll escapable + * @param escapableOutEdge + * edge that proves edge in question is toll escapable + * @return boolean if is case three This case checks edges with a toll tag to see if it has has + * a route that can escape the toll, if so there is a modeling issue either on the edge + * in question or nearby on the escapable route. + */ + private boolean isCaseThree(final Edge edgeInQuestion, + final Map edgeInQuestionTags, final Edge escapableInEdge, + final Edge escapableOutEdge) + { + return this.hasTollYesTag(edgeInQuestionTags) + && !this.edgeIntersectsTollFeature(edgeInQuestion) + && !this.hasInconsistentTollTag(escapableOutEdge) + && !this.hasInconsistentTollTag(escapableInEdge); + } + + /** + * @param edgeInQuestion + * edge in question + * @param edgeInQuestionTags + * edge in question tags + * @return boolean if is case two This case checks for a way without a toll tag between 2 ways + * that do have a toll=yes tag + */ + private boolean isCaseTwo(final Edge edgeInQuestion, + final Map edgeInQuestionTags) + { + return !this.hasTollYesTag(edgeInQuestionTags) + && this.hasInconsistentTollTag(edgeInQuestion); + } + + /** + * @param tags + * some edge tags + * @return boolean regarding access=private tags. + */ + private boolean isPrivateAccess(final Map tags) + { + if (tags.containsKey(AccessTag.KEY)) + { + return tags.get(AccessTag.KEY).equalsIgnoreCase(AccessTag.PRIVATE.toString()); + } + return false; + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheckTest.java new file mode 100644 index 000000000..ad33acbdf --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheckTest.java @@ -0,0 +1,57 @@ +package org.openstreetmap.atlas.checks.validation.tag; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.openstreetmap.atlas.checks.configuration.ConfigurationResolver; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.checks.validation.verifier.ConsumerBasedExpectedCheckVerifier; + +/** + * Tests for {@link TollValidationCheck} + * + * @author v-garei + */ +public class TollValidationCheckTest +{ + + @Rule + public TollValidationCheckTestRule setup = new TollValidationCheckTestRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + private final TollValidationCheck check = new TollValidationCheck( + ConfigurationResolver.inlineConfiguration("{\"TollValidationCheck\": " + "{" + + "\"minimumHighwayType\": \"tertiary\"," + + "\"maxAngleDiffForContiguousWays\": 40.0," + "\"minInAndOutEdges\": 1.0," + + "\"maxIterationForNearbySearch\": 15.0}}")); + + private static void verifyFixSuggestions(final CheckFlag flag, final int count) + { + Assert.assertEquals(count, flag.getFixSuggestions().size()); + } + + @Test + public void escapableWayNeedsTollTagRemoved() + { + this.verifier.actual(this.setup.escapableWayNeedsTollTagRemoved(), this.check); + this.verifier.verifyExpectedSize(2); + } + + @Test + public void inconsistentTollTags() + { + this.verifier.actual(this.setup.inconsistentTollTags(), this.check); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verifyFixSuggestions(flag, 1)); + } + + @Test + public void intersectingTollFeatureWithoutTag() + { + this.verifier.actual(this.setup.intersectingTollFeatureWithoutTag(), this.check); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verifyFixSuggestions(flag, 1)); + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheckTestRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheckTestRule.java new file mode 100644 index 000000000..532254bdb --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/tag/TollValidationCheckTestRule.java @@ -0,0 +1,91 @@ +package org.openstreetmap.atlas.checks.validation.tag; + +import org.openstreetmap.atlas.geography.atlas.Atlas; +import org.openstreetmap.atlas.utilities.testing.CoreTestRule; +import org.openstreetmap.atlas.utilities.testing.TestAtlas; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Area; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Edge; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Loc; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Node; + +/** + * Tests for {@link TollValidationCheck} + * + * @author v-garei + */ +public class TollValidationCheckTestRule extends CoreTestRule +{ + + private static final String WAY1_NODE1 = "40.9130354, 29.4700719"; + private static final String WAY1_NODE2 = "40.9123887, 29.4698597"; + + private static final String WAY2_NODE2 = "40.9118904, 29.4696993"; + + private static final String WAY3_NODE2 = "40.9082867, 29.4685152"; + + private static final String WAY4_NODE1 = "40.91344, 29.47000"; + + private static final String WAY5_NODE2 = "40.91168, 29.46935"; + + private static final String AREA_NODE1 = "40.9127774, 29.4698422"; + private static final String AREA_NODE2 = "40.9125929, 29.4704725"; + private static final String AREA_NODE3 = "40.9124979, 29.4704238"; + private static final String AREA_NODE4 = "40.9126826, 29.4697936"; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = WAY4_NODE1)), + @Node(coordinates = @Loc(value = WAY1_NODE1)), + @Node(coordinates = @Loc(value = WAY1_NODE2)), + @Node(coordinates = @Loc(value = WAY2_NODE2)), + @Node(coordinates = @Loc(value = WAY5_NODE2)) }, edges = { + @Edge(id = "6000001", coordinates = { @Loc(value = WAY4_NODE1), + @Loc(value = WAY1_NODE1) }, tags = { "highway=motorway", "toll=no" }), + @Edge(id = "7000001", coordinates = { @Loc(value = WAY1_NODE1), + @Loc(value = WAY1_NODE2) }, tags = { "highway=motorway", "toll=yes" }), + @Edge(id = "8000001", coordinates = { @Loc(value = WAY1_NODE2), + @Loc(value = WAY2_NODE2) }, tags = { "highway=motorway", "toll=yes" }), + @Edge(id = "9000001", coordinates = { @Loc(value = WAY2_NODE2), + @Loc(value = WAY5_NODE2) }, tags = { "highway=motorway", "toll=no" }) }) + private Atlas escapableWayNeedsTollTagRemoved; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = WAY1_NODE1)), + @Node(coordinates = @Loc(value = WAY1_NODE2)), + @Node(coordinates = @Loc(value = WAY2_NODE2)), + @Node(coordinates = @Loc(value = WAY3_NODE2)) }, edges = { + @Edge(id = "3000001", coordinates = { @Loc(value = WAY1_NODE1), + @Loc(value = WAY1_NODE2) }, tags = { "highway=motorway", "toll=yes" }), + @Edge(id = "4000001", coordinates = { @Loc(value = WAY1_NODE2), + @Loc(value = WAY2_NODE2) }, tags = "highway=motorway"), + @Edge(id = "5000001", coordinates = { @Loc(value = WAY2_NODE2), + @Loc(value = WAY3_NODE2) }, tags = { "highway=motorway", + "toll=yes" }) }) + private Atlas inconsistentTollTags; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = WAY1_NODE1)), + @Node(coordinates = @Loc(value = WAY1_NODE2)), + @Node(coordinates = @Loc(value = AREA_NODE1)), + @Node(coordinates = @Loc(value = AREA_NODE2)), + @Node(coordinates = @Loc(value = AREA_NODE3)), + @Node(coordinates = @Loc(value = AREA_NODE4)) }, edges = { + @Edge(id = "1000001", coordinates = { @Loc(value = WAY1_NODE1), + @Loc(value = WAY1_NODE2) }, tags = "highway=motorway") }, areas = { + @Area(coordinates = { @Loc(value = AREA_NODE1), + @Loc(value = AREA_NODE2), @Loc(value = AREA_NODE3), + @Loc(value = AREA_NODE4) }, tags = { "building=yes", + "barrier=toll_booth" }) }) + private Atlas intersectingTollFeatureWithoutTag; + + public Atlas escapableWayNeedsTollTagRemoved() + { + return this.escapableWayNeedsTollTagRemoved; + } + + public Atlas inconsistentTollTags() + { + return this.inconsistentTollTags; + } + + public Atlas intersectingTollFeatureWithoutTag() + { + return this.intersectingTollFeatureWithoutTag; + } +}