Skip to content

Commit

Permalink
Turn Lanes Value Check (#639)
Browse files Browse the repository at this point in the history
* Turn Lanes Value Check

* Turn Lanes Check

* Revert file

* Turn Lanes Value Check

* Update doc

* Update doc

* Turn Lanes Value Check

Co-authored-by: Elaine Shum <[email protected]>
  • Loading branch information
elaineseattle and Elaine Shum authored Dec 8, 2021
1 parent 85615cf commit 9fde267
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 1 deletion.
9 changes: 9 additions & 0 deletions config/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,15 @@
"tags":"tags"
}
},
"InvalidTurnLanesValueCheck": {
"challenge":{
"description":"Tasks contain invalid values for the turn:lanes tag.",
"blurb":"Invalid Turn Lanes Values",
"instruction":"Change the turn:lanes values to have a valid and representative value.",
"difficulty":"EASY",
"tags":"lanes,highway"
}
},
"SourceMaxspeedCheck": {
"countries.denylist": ["UK"],
"values": [
Expand Down
3 changes: 2 additions & 1 deletion docs/available_checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ This document is a list of tables with a description and link to documentation f
| ImproperAndUnknownRoadNameCheck | This check flags improper road name values. |
| [InvalidAccessTagCheck](checks/invalidAccessTagCheck.md) | The purpose of this check is to identify invalid access tags. |
| [InvalidCharacterNameTagCheck](checks/invalidCharacterNameTagCheck.md) | The purpose of this checks is to identify Lines, Areas and Relations with invalid characters in name and localized name tags. |
| [InvalidLanesTagCheck](docs/checks/invalidLanesTagCheck.md) | The purpose of this check is to identify highways in OSM with an invalid lanes tag value. |
| [InvalidLanesTagCheck](checks/invalidLanesTagCheck.md) | The purpose of this check is to identify highways in OSM with an invalid lanes tag value. |
| InvalidTagsCheck | This flags features based on configurable filters. Each filter passed contains the atlas entity classes to check and a taggable filter to test objects against. If a feature is of one of the given classes and passes the associated TaggableFilter, it is flagged. |
| [InvalidTurnLanesValueCheck](checks/invalidTurnLanesValueCheck.md) | The purpose of this check is to identify highways in OSM with an invalid turn:lanes value. |
| [LongNameCheck](checks/longNameCheck.md) | This check flags features with names longer than a configurable length. |
| [MixedCaseNameCheck](checks/mixedCaseNameCheck.md) | The purpose of this check is to identify names that contain invalid mixed cases so that they can be edited to be the standard format. |
| [RoadNameGapCheck](checks/RoadNameGapCheck.md) | The purpose of this check is to identify edge connected between two edges whose name tag is same. Flag the edge if the edge has a name tag different to name tag of edges connected to it or if there is no name tag itself.
Expand Down
124 changes: 124 additions & 0 deletions docs/checks/invalidTurnLanesValueCheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Invalid Turn Lanes Value Check

This check flags roads that have invalid `turn:lanes` tag values.

Valid values contain keywords found in
https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/tags/TurnTag.java
i.e.
```java
enum TurnType
{
LEFT,
SHARP_LEFT,
SLIGHT_LEFT,
THROUGH,
RIGHT,
SHARP_RIGHT,
SLIGHT_RIGHT,
REVERSE,
MERGE_TO_LEFT,
MERGE_TO_RIGHT,
NONE;
}
String TURN_LANE_DELIMITER = "\\|";
String TURN_TYPE_DELIMITER = ";";
```
e.g. "turn:lanes":"through|through|right" is valid
e.g. "turn:lanes":"through|through|righ" is not valid


#### Live Examples

1. The way [id:730457851](https://www.openstreetmap.org/way/730457851) has an invalid `turn:lanes`
2. The way [id:836279618](https://www.openstreetmap.org/way/836279618) has an invalid `turn:lanes:forward`

#### Code Review

In [Atlas](https://github.com/osmlab/atlas), OSM elements are represented as Edges, Points, Lines, Nodes & Relations; in our case, we’re are looking at [Edges](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Edge.java).

Our first goal is to validate the incoming Atlas object. Valid features for this check will satisfy the following conditions:

* Is an Edge
* Has a `highway` tag that is car navigable
* Has a `turn:lanes` or `turn:lanes:forward` or `turn:lanes:backward` tag
* Is not an OSM way that has already been flagged

```java
@Override
public boolean validCheckForObject(final AtlasObject object)
{
return TurnLanesTag.hasTurnLane(object) && HighwayTag.isCarNavigableHighway(object)
&& object instanceof Edge && ((Edge) object).isMainEdge()
&& !this.isFlagged(object.getOsmIdentifier());
}
```

The valid objects (i.e. `turn:lanes` or `turn:lanes:forward` or `turn:lanes:backward`)
are then trimmed to remove all the keywords found in
https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/tags/TurnTag.java
i.e.

```java
enum TurnType
{
LEFT,
SHARP_LEFT,
SLIGHT_LEFT,
THROUGH,
RIGHT,
SHARP_RIGHT,
SLIGHT_RIGHT,
REVERSE,
MERGE_TO_LEFT,
MERGE_TO_RIGHT,
NONE;
}
String TURN_LANE_DELIMITER = "\\|";
String TURN_TYPE_DELIMITER = ";";
```
if the resulting trimmed string is non-empty, that means `turn:lanes` is malformed

```java
@Override
protected Optional<CheckFlag> flag(final AtlasObject object)
{
final String turnLanesTag = object.getTag(TurnLanesTag.KEY).orElse("");
final String turnLanesForwardTag = object.getTag(TurnLanesForwardTag.KEY).orElse("");
final String turnLanesBackwardTag = object.getTag(TurnLanesBackwardTag.KEY).orElse("");

if (!this.trimKeywords(turnLanesTag).isEmpty()
|| !this.trimKeywords(turnLanesForwardTag).isEmpty()
|| !this.trimKeywords(turnLanesBackwardTag).isEmpty())
{
this.markAsFlagged(object.getOsmIdentifier());

return Optional.of(this.createFlag(new OsmWayWalker((Edge) object).collectEdges(),
this.getLocalizedInstruction(0, object.getOsmIdentifier())));
}
return Optional.empty();
}
```

Please note that the keywords "LEFT" and "RIGHT" are trimmed towards the end so that the phrases
"MERGE_TO_LEFT" AND "MERGE_TO_RIGHT" are trimmed first.
```java
public final String trimKeywords(final String input)
{
String result = input.toLowerCase();
for (final TurnType turnType : TurnTag.TurnType.values())
{
if (turnType != TurnTag.TurnType.LEFT && turnType != TurnTag.TurnType.RIGHT)
{
result = result.replaceAll(turnType.name().toLowerCase(), "");
}
}
result = result.replaceAll(TurnTag.TurnType.LEFT.name().toLowerCase(), "");
result = result.replaceAll(TurnTag.TurnType.RIGHT.name().toLowerCase(), "");
result = result.replaceAll(TurnTag.TURN_LANE_DELIMITER.toLowerCase(), "");
result = result.replaceAll(TurnTag.TURN_TYPE_DELIMITER.toLowerCase(), "");
return result.trim();
}
```

To learn more about the code, please look at the comments in the source code for the check.
[InvalidTurnLanesValueCheck](../../src/main/java/org/openstreetmap/atlas/checks/validation/tag/InvalidTurnLanesValueCheck.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.openstreetmap.atlas.checks.validation.tag;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.openstreetmap.atlas.checks.base.BaseCheck;
import org.openstreetmap.atlas.checks.flag.CheckFlag;
import org.openstreetmap.atlas.geography.atlas.items.AtlasObject;
import org.openstreetmap.atlas.geography.atlas.items.Edge;
import org.openstreetmap.atlas.geography.atlas.walker.OsmWayWalker;
import org.openstreetmap.atlas.tags.HighwayTag;
import org.openstreetmap.atlas.tags.TurnLanesBackwardTag;
import org.openstreetmap.atlas.tags.TurnLanesForwardTag;
import org.openstreetmap.atlas.tags.TurnLanesTag;
import org.openstreetmap.atlas.tags.TurnTag;
import org.openstreetmap.atlas.tags.TurnTag.TurnType;
import org.openstreetmap.atlas.utilities.configuration.Configuration;

/**
* Flags {@link Edge}s that have the {@code highway} tag and a {@code lanes} tag with an invalid
* value. The valid {@code lanes} values are configurable.
*
* @author mselaineleong
*/
public class InvalidTurnLanesValueCheck extends BaseCheck<Long>
{
private static final long serialVersionUID = -1459761692833694715L;

private static final List<String> FALLBACK_INSTRUCTIONS = Arrays
.asList("Way {0,number,#} has an invalid turn:lanes value.");

/**
* The default constructor that must be supplied. The Atlas Checks framework will generate the
* checks with this constructor, supplying a configuration that can be used to adjust any
* parameters that the check uses during operation.
*
* @param configuration
* the JSON configuration for this check
*/
public InvalidTurnLanesValueCheck(final Configuration configuration)
{
super(configuration);
}

public final String trimKeywords(final String input)
{
String result = input.toLowerCase();
for (final TurnType turnType : TurnTag.TurnType.values())
{
if (turnType != TurnTag.TurnType.LEFT && turnType != TurnTag.TurnType.RIGHT)
{
result = result.replaceAll(turnType.name().toLowerCase(), "");
}
}
result = result.replaceAll(TurnTag.TurnType.LEFT.name().toLowerCase(), "");
result = result.replaceAll(TurnTag.TurnType.RIGHT.name().toLowerCase(), "");
result = result.replaceAll(TurnTag.TURN_LANE_DELIMITER.toLowerCase(), "");
result = result.replaceAll(TurnTag.TURN_TYPE_DELIMITER.toLowerCase(), "");
return result.trim();
}

/**
* This function will validate if the supplied atlas object is valid for the check.
*
* @param object
* the atlas object supplied by the Atlas-Checks framework for evaluation
* @return {@code true} if this object should be checked
*/
@Override
public boolean validCheckForObject(final AtlasObject object)
{
return TurnLanesTag.hasTurnLane(object) && HighwayTag.isCarNavigableHighway(object)
&& object instanceof Edge && ((Edge) object).isMainEdge()
&& !this.isFlagged(object.getOsmIdentifier());
}

/**
* This is the actual function that will check to see whether the object needs to be flagged.
*
* @param object
* the atlas object supplied by the Atlas-Checks framework for evaluation
* @return an optional {@link CheckFlag} object that
*/
@Override
protected Optional<CheckFlag> flag(final AtlasObject object)
{
final String turnLanesTag = object.getTag(TurnLanesTag.KEY).orElse("");
final String turnLanesForwardTag = object.getTag(TurnLanesForwardTag.KEY).orElse("");
final String turnLanesBackwardTag = object.getTag(TurnLanesBackwardTag.KEY).orElse("");

if (!this.trimKeywords(turnLanesTag).isEmpty()
|| !this.trimKeywords(turnLanesForwardTag).isEmpty()
|| !this.trimKeywords(turnLanesBackwardTag).isEmpty())
{
this.markAsFlagged(object.getOsmIdentifier());

return Optional.of(this.createFlag(new OsmWayWalker((Edge) object).collectEdges(),
this.getLocalizedInstruction(0, object.getOsmIdentifier())));
}
return Optional.empty();
}

@Override
protected List<String> getFallbackInstructions()
{
return FALLBACK_INSTRUCTIONS;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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.validation.verifier.ConsumerBasedExpectedCheckVerifier;

/**
* Tests for {@link InvalidTurnLanesValueCheck}
*
* @author mselaineleong
*/
public class InvalidTurnLanesValueCheckTest
{
@Rule
public InvalidTurnLanesValueCheckTestRule setup = new InvalidTurnLanesValueCheckTestRule();

@Rule
public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier();

@Test
public void invalidTurnLanesValue()
{
this.verifier.actual(this.setup.invalidTurnLanesValue(),
new InvalidTurnLanesValueCheck(ConfigurationResolver.emptyConfiguration()));
this.verifier.globallyVerify(flags -> Assert.assertEquals(1, flags.size()));
}

@Test
public void validTurnLanesValue()
{
this.verifier.actual(this.setup.validTurnLanesValue(),
new InvalidTurnLanesValueCheck(ConfigurationResolver.emptyConfiguration()));
this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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.Edge;
import org.openstreetmap.atlas.utilities.testing.TestAtlas.Loc;
import org.openstreetmap.atlas.utilities.testing.TestAtlas.Node;

/**
* Tests for {@link InvalidLanesTagCheck}
*
* @author mselaineleong
*/

public class InvalidTurnLanesValueCheckTestRule extends CoreTestRule
{
private static final String TEST_1 = "47.2136626201459,-122.443275382856";
private static final String TEST_2 = "47.2138327316739,-122.44258668766";
private static final String TEST_3 = "47.2136626201459,-122.441897992465";
private static final String TEST_4 = "47.2138114677627,-122.440990166979";
private static final String TEST_5 = "47.2136200921786,-122.44001973284";
private static final String TEST_6 = "47.2135137721113,-122.439127559518";
private static final String TEST_7 = "47.2136200921786,-122.438157125378";

@TestAtlas(
// nodes
nodes = { @Node(coordinates = @Loc(value = TEST_1)),
@Node(coordinates = @Loc(value = TEST_3)),
@Node(coordinates = @Loc(value = TEST_5)),
@Node(coordinates = @Loc(value = TEST_7)) },
// edges
edges = { @Edge(id = "1000000001", coordinates = { @Loc(value = TEST_1),
@Loc(value = TEST_2), @Loc(value = TEST_3) }, tags = { "highway=motorway" }),
@Edge(id = "1001000001", coordinates = { @Loc(value = TEST_3),
@Loc(value = TEST_4), @Loc(value = TEST_5) }, tags = {
"highway=motorway", "lanes=1.5", "turn:lanes=through|right" }),
@Edge(id = "1002000001", coordinates = { @Loc(value = TEST_5),
@Loc(value = TEST_6),
@Loc(value = TEST_7) }, tags = { "highway=motorway" }) })
private Atlas validTurnLanesValue;

@TestAtlas(
// nodes
nodes = { @Node(coordinates = @Loc(value = TEST_1)),
@Node(coordinates = @Loc(value = TEST_3)),
@Node(coordinates = @Loc(value = TEST_5)),
@Node(coordinates = @Loc(value = TEST_7), tags = { "barrier=toll_booth" }) },
// edges
edges = { @Edge(id = "1000000001", coordinates = { @Loc(value = TEST_1),
@Loc(value = TEST_2), @Loc(value = TEST_3) }, tags = { "highway=motorway" }),
@Edge(id = "1001000001", coordinates = { @Loc(value = TEST_3),
@Loc(value = TEST_4),
@Loc(value = TEST_5) }, tags = { "highway=motorway", "lanes=11",
"turn:lanes=throug|throug|slight_right" }),
@Edge(id = "1002000001", coordinates = { @Loc(value = TEST_5),
@Loc(value = TEST_6),
@Loc(value = TEST_7) }, tags = { "highway=motorway" }) })
private Atlas invalidTurnLanesValue;

public Atlas invalidTurnLanesValue()
{
return this.invalidTurnLanesValue;
}

public Atlas validTurnLanesValue()
{
return this.validTurnLanesValue;
}
}

0 comments on commit 9fde267

Please sign in to comment.