forked from osmlab/atlas-checks
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New check: TunnelBridgeHeightLimitCheck (osmlab#343) (osmlab#350)
* 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
Showing
6 changed files
with
492 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
164 changes: 164 additions & 0 deletions
164
...main/java/org/openstreetmap/atlas/checks/validation/tag/TunnelBridgeHeightLimitCheck.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
92 changes: 92 additions & 0 deletions
92
.../java/org/openstreetmap/atlas/checks/validation/tag/TunnelBridgeHeightLimitCheckTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())); | ||
} | ||
} |
Oops, something went wrong.