Skip to content

Commit

Permalink
New check: TunnelBridgeHeightLimitCheck (osmlab#343) (osmlab#350)
Browse files Browse the repository at this point in the history
* New check: TunnelBridgeHeightLimitCheck

* Replacing deprecated language

* CheckStyle fixes

* Making highway type filter configurable (code review remark)

* CheckStyle

* Flagging all Edges of a related OSM Way (code review remark)

* Adding the tunnel/bridge maxheight check to the list of available checks
  • Loading branch information
ladwlo authored Oct 7, 2020
1 parent f51e3ba commit 6c987dd
Show file tree
Hide file tree
Showing 6 changed files with 492 additions and 0 deletions.
10 changes: 10 additions & 0 deletions config/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -1120,5 +1120,15 @@
"difficulty": "NORMAL",
"defaultPriority": "LOW"
}
},
"TunnelBridgeHeightLimitCheck": {
"highway.filter":"highway->motorway_link,trunk_link,primary,primary_link,secondary,secondary_link",
"challenge": {
"description": "Tunnels, covered roads and roads crossing with bridges should have a 'maxheight' or 'maxheight:physical' tags.",
"blurb": "Missing maxheight tag",
"instruction": "Open your favorite editor and check the instruction for the task instructions, then add missing tags to each of the flagged ways.",
"difficulty": "NORMAL",
"defaultPriority": "LOW"
}
}
}
1 change: 1 addition & 0 deletions docs/available_checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,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. |
| [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. |

Expand Down
32 changes: 32 additions & 0 deletions docs/checks/tunnelBridgeHeightLimitCheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Tunnel/Bridge Height Limit Check

#### Description

This check identifies roads of limited vertical clearance (tunnels, covered roads, and roads crossed by bridges) that do not have 'maxheight' or 'maxheight:physical' tags set.
The valid highway classes for this check (configurable) are:
MOTORWAY_LINK, TRUNK_LINK, PRIMARY, PRIMARY_LINK, SECONDARY, SECONDARY_LINK

This is a port of Osmose check #7130.

#### Live Examples

1. Way [id:17659494](https://www.openstreetmap.org/way/17659494) is a tunnel without maxheight tag.
2. Way [id:601626442](https://www.openstreetmap.org/way/601626442) is a covered road without maxheight tag.
3. Way [id:174319379](https://www.openstreetmap.org/way/174319379) passes under a pair of bridges:
[id:11778321](https://www.openstreetmap.org/way/11778321) and [id:11778325](https://www.openstreetmap.org/way/11778325).
But it does not have a maxheight tag.

#### Code Review

The check ensures that the Atlas object being evaluated is an [Edge](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Edge.java)
which is a _master_ edge (positive ID) and its related OSM Way ID has not yet been flagged. This is done in the validation step.
Once an edge is found to be valid, the verification step first checks (using standard Validators) if the edge is a _tunnel_ or a _covered_ road,
it has an expected _highway_ class and no _maxheight_ tags are present. The edge is then flagged.
If the edge has not been flagged, but it is tagged as a _bridge_, then any edges that intersect the bridge's bounding box are retrieved.
For each of those new target objects, the algorithm verifies the same criteria (the edge is a master edge, with OSM ID that has not yet been flagged,
it is of expected highway class and does not have maxheight tags). Additionally, it checks if the edge's polyline properly crosses the bridge's polyline,
to exclude any edges that might have been caught in the bounding box but do not actually intersect with the bridge, or only touch it at either end.
For each edge which passed through all these filters, a separate fixing instruction is added and a single flag is created.

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

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.openstreetmap.atlas.checks.base.BaseCheck;
import org.openstreetmap.atlas.checks.flag.CheckFlag;
import org.openstreetmap.atlas.geography.PolyLine;
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.BridgeTag;
import org.openstreetmap.atlas.tags.CoveredTag;
import org.openstreetmap.atlas.tags.MaxHeightTag;
import org.openstreetmap.atlas.tags.TunnelTag;
import org.openstreetmap.atlas.tags.annotations.validation.Validators;
import org.openstreetmap.atlas.tags.filters.TaggableFilter;
import org.openstreetmap.atlas.utilities.collections.Iterables;
import org.openstreetmap.atlas.utilities.configuration.Configuration;

/**
* Flags highways (of certain classes) which should have a 'maxheight' or 'maxheight:*' tag but do
* not have either. This is a port of Osmose check #7130.<br>
* <b>Target objects:</b><br>
* 1. Tunnels<br>
* 2. Covered ways<br>
* 3. Ways passing under bridges<br>
* <b>Target highway classes (configurable):</b><br>
* MOTORWAY_LINK, TRUNK_LINK, PRIMARY, PRIMARY_LINK, SECONDARY, SECONDARY_LINK
*
* @author ladwlo
*/
public class TunnelBridgeHeightLimitCheck extends BaseCheck<Long>
{

private static final long serialVersionUID = 7912181047816225229L;

private static final String FALLBACK_INSTRUCTION_TEMPLATE = "Way {0,number,#} %s but vehicle height limit is not specified. Add a 'maxheight' or 'maxheight:physical' tag according to an existing legal or physical restriction.";
private static final int TUNNEL_CASE_INDEX = 0;
private static final int COVERED_CASE_INDEX = 1;
private static final int BRIDGE_CASE_INDEX = 2;
private static final List<String> FALLBACK_CASES = Arrays.asList("is a tunnel", "is covered",
"passes under bridge ({1,number,#})");
private static final List<String> FALLBACK_INSTRUCTIONS = FALLBACK_CASES.stream()
.map(caseDescription -> String.format(FALLBACK_INSTRUCTION_TEMPLATE, caseDescription))
.collect(Collectors.toList());
private static final String MAXHEIGHT_PHYSICAL = "maxheight:physical";
private static final String HIGHWAY_FILTER_DEFAULT = "highway->motorway_link,trunk_link,primary,primary_link,secondary,secondary_link";

private final TaggableFilter highwayFilter;

/**
* Default constructor.
*
* @param configuration
* the JSON configuration for this check
*/
public TunnelBridgeHeightLimitCheck(final Configuration configuration)
{
super(configuration);
this.highwayFilter = configurationValue(configuration, "highway.filter",
HIGHWAY_FILTER_DEFAULT, TaggableFilter::forDefinition);
}

/**
* 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 object instanceof Edge && ((Edge) object).isMainEdge()
&& !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)
{
// case 1 (tunnel) & 2 (covered highway)
if ((TunnelTag.isTunnel(object) || this.isCovered(object))
&& this.isHighwayWithoutMaxHeight(object))
{
final Long osmId = object.getOsmIdentifier();
markAsFlagged(osmId);
final Set<Edge> edgesToFlag = new OsmWayWalker((Edge) object).collectEdges();
final int instructionIndex = TunnelTag.isTunnel(object) ? TUNNEL_CASE_INDEX
: COVERED_CASE_INDEX;
final String instruction = getLocalizedInstruction(instructionIndex, osmId);
final CheckFlag flag = createFlag(edgesToFlag, instruction);
return Optional.of(flag);
}
// case 3 (road passing under bridge)
if (BridgeTag.isBridge(object))
{
final Edge bridge = (Edge) object;
final PolyLine bridgeAsPolyLine = bridge.asPolyLine();
final Set<Long> wayIdsToFlag = new HashSet<>();
final Set<Edge> edgesToFlag = new HashSet<>();
Iterables.stream(bridge.getAtlas().edgesIntersecting(bridge.bounds()))
.filter(edge -> edge.isMainEdge()
&& edge.getOsmIdentifier() != bridge.getOsmIdentifier()
&& !isFlagged(edge.getOsmIdentifier())
&& this.isHighwayWithoutMaxHeight(edge)
&& this.edgeCrossesBridge(edge.asPolyLine(), bridgeAsPolyLine))
.forEach(edge ->
{
final long wayId = edge.getOsmIdentifier();
markAsFlagged(wayId);
wayIdsToFlag.add(wayId);
edgesToFlag.addAll(new OsmWayWalker(edge).collectEdges());
});
if (!wayIdsToFlag.isEmpty())
{
final CheckFlag checkFlag = new CheckFlag(getTaskIdentifier(bridge));
wayIdsToFlag.forEach(
wayId -> checkFlag.addInstruction(getLocalizedInstruction(BRIDGE_CASE_INDEX,
wayId, bridge.getOsmIdentifier())));
checkFlag.addObjects(edgesToFlag);
return Optional.of(checkFlag);
}
}
return Optional.empty();
}

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

// check if the two polylines intersect at any location other than the bridge's endpoints
private boolean edgeCrossesBridge(final PolyLine edge, final PolyLine bridge)
{
return edge.intersections(bridge).stream()
.anyMatch(loc -> !loc.equals(bridge.first()) && !loc.equals(bridge.last()));
}

private boolean isCovered(final AtlasObject object)
{
return Validators.isOfType(object, CoveredTag.class, CoveredTag.YES, CoveredTag.ARCADE,
CoveredTag.COLONNADE);
}

private boolean isHighwayWithoutMaxHeight(final AtlasObject object)
{
return this.highwayFilter.test(object)
&& !Validators.hasValuesFor(object, MaxHeightTag.class)
&& object.getTag(MAXHEIGHT_PHYSICAL).isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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;
import org.openstreetmap.atlas.utilities.configuration.Configuration;

/**
* Test cases for {@link TunnelBridgeHeightLimitCheck}
*
* @author ladwlo
*/
public class TunnelBridgeHeightLimitCheckTest
{

@Rule
public TunnelBridgeHeightLimitCheckTestRule setup = new TunnelBridgeHeightLimitCheckTestRule();

@Rule
public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier();

private final Configuration inlineConfiguration = ConfigurationResolver
.inlineConfiguration("{\"TunnelBridgeHeightLimitCheck\":{}}");

@Test
public void bidirectionalTunnelWithoutMaxHeightIsFlaggedOnce()
{
this.verifier.actual(this.setup.getBidirectionalTunnelWithoutMaxHeight(),
new TunnelBridgeHeightLimitCheck(this.inlineConfiguration));
this.verifier.globallyVerify(flags -> Assert.assertEquals(1, flags.size()));
}

@Test
public void bridgeWithoutCrossingRoadsIsIgnored()
{
this.verifier.actual(this.setup.getBridgeWithNoCrossingRoads(),
new TunnelBridgeHeightLimitCheck(this.inlineConfiguration));
this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size()));
}

@Test
public void coveredRoadWithoutMaxHeightSplitIntoTwoEdgesIsFlaggedOnce()
{
this.verifier.actual(this.setup.getCoveredRoadWithoutMaxHeightSplitIntoTwoEdges(),
new TunnelBridgeHeightLimitCheck(this.inlineConfiguration));
this.verifier.globallyVerify(flags -> Assert.assertEquals(1, flags.size()));
}

@Test
public void lowClassTunnelWithoutMaxHeightIsIgnored()
{
this.verifier.actual(this.setup.getLowClassTunnelWithoutMaxHeight(),
new TunnelBridgeHeightLimitCheck(this.inlineConfiguration));
this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size()));
}

@Test
public void roadWithoutMaxHeightPassingUnderBridgeIsFlagged()
{
this.verifier.actual(this.setup.getRoadsPassingUnderBridge(),
new TunnelBridgeHeightLimitCheck(this.inlineConfiguration));
this.verifier.globallyVerify(flags ->
{
Assert.assertEquals(1, flags.size());
// task is created for the expected bridge object
Assert.assertEquals("1000000001", flags.get(0).getIdentifier());
// both edges of the flagged Way are included
Assert.assertEquals(2, flags.get(0).getFlaggedObjects().size());
// both edges have the expected OSM ID
flags.get(0).getFlaggedObjects().forEach(object -> Assert.assertEquals("4000",
object.getProperties().get("osmIdentifier")));
});
}

@Test
public void tunnelAndCoveredRoadWithMaxHeightAreIgnored()
{
this.verifier.actual(this.setup.getTunnelAndCoveredRoadWithMaxHeight(),
new TunnelBridgeHeightLimitCheck(this.inlineConfiguration));
this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size()));
}

@Test
public void uncoveredRoadWithoutMaxHeightIsIgnored()
{
this.verifier.actual(this.setup.getUncoveredRoadWithoutMaxHeight(),
new TunnelBridgeHeightLimitCheck(this.inlineConfiguration));
this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size()));
}
}
Loading

0 comments on commit 6c987dd

Please sign in to comment.