From 522cb004b78d9f4052a6bdfd3594693e6487d9ba Mon Sep 17 00:00:00 2001 From: atiannicelli Date: Wed, 4 Nov 2020 14:23:28 -0500 Subject: [PATCH] Add railway level_crossing check (#371) * Add railway level_crossing check * Add documentation and fix bug found in first version * Fix code smell issues * Fix code smell complexity issue * Add levelcrossing doc to available checks doc * skipping rails that are also highways in some cases * fix spotlessJava failure. * add fix suggestions * Update to deal with layers better. * Ignore construction and proposed rails. * Make it smell good. * fix smells * kick travis to retest * one more smell removed. * convert rail list to filter * Add rail list to configuration file. * Fix PR review requested changes * Add enum for isValidLevelCrossingNode function --- config/configuration.json | 11 + docs/available_checks.md | 3 +- docs/checks/LevelCrossingOnRailwayCheck.md | 87 ++++ .../LevelCrossingOnRailwayCheck.java | 400 ++++++++++++++++++ .../LevelCrossingOnRailwayCheckTest.java | 136 ++++++ .../LevelCrossingOnRailwayCheckTestRule.java | 354 ++++++++++++++++ 6 files changed, 990 insertions(+), 1 deletion(-) create mode 100644 docs/checks/LevelCrossingOnRailwayCheck.md create mode 100644 src/main/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheck.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheckTest.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheckTestRule.java diff --git a/config/configuration.json b/config/configuration.json index cc7fb4c22..0535aa8ec 100644 --- a/config/configuration.json +++ b/config/configuration.json @@ -461,6 +461,17 @@ "tags":"highway" } }, + "LevelCrossingOnRailwayCheck": { + "layer.default": 0, + "railway.filter": "railway->rail,tram,disused,preserved,miniature,light_rail,subway,narrow_gauge", + "challenge": { + "description": "Tasks contain features which are missing or incorrectly tagged as railway:level_crossing.", + "blurb": "Fix level crossing railway/highway intersections", + "instruction": "Open your favorite editor and fix the railway/highway intersection nodes.", + "difficulty": "NORMAL", + "defaultPriority": "LOW" + } + }, "LineCrossingBuildingCheck": { "challenge": { "description": "The water body has invalid crossings by line item(s).", diff --git a/docs/available_checks.md b/docs/available_checks.md index a93ba5165..ac1456fc0 100644 --- a/docs/available_checks.md +++ b/docs/available_checks.md @@ -45,8 +45,9 @@ This document is a list of tables with a description and link to documentation f | :--------- | :---------------- | | BigNodeBadDataCheck | The purpose of this check is to flag any BigNodes that have may have some bad data. | | ConnectivityCheck | This check identifies nodes that should be connected to nearby nodes or edges. | -| [DuplicateNodeCheck](docs/checks/duplicateNodeCheck.md) | The purpose of this check is to identify Nodes that are in the exact same location. | +| [DuplicateNodeCheck](checks/duplicateNodeCheck.md) | The purpose of this check is to identify Nodes that are in the exact same location. | | [InvalidMiniRoundaboutCheck](checks/invalidMiniRoundaboutCheck.md) | The purpose of this check is to identify invalid mini-roundabouts (i.e. roundabouts that share the same rules as other roundabouts, but present as painted circles rather than physical circles). | +| [LevelCrossingOnRailwayCheck](checks/LevelCrossingOnRailwayCheck.md) | This check identifies incorrectly tagged or missing nodes at railway/highway intersections. | | NodeValenceCheck | This check identifies nodes with too many connections. | | [OrphanNodeCheck](tutorials/tutorial2-OrphanNodeCheck.md) | The purpose of this check is to identify untagged and unconnected Nodes in OSM. | diff --git a/docs/checks/LevelCrossingOnRailwayCheck.md b/docs/checks/LevelCrossingOnRailwayCheck.md new file mode 100644 index 000000000..50f229422 --- /dev/null +++ b/docs/checks/LevelCrossingOnRailwayCheck.md @@ -0,0 +1,87 @@ +# Level Crossing on Railway Check + +#### Description + +The Purpose of this check is to detect and flag nodes under the four scenarios below: + +1. When a railway crosses a highway on the same layer and intersection node is missing. +2. When a railway crosses a highway on the same layer and intersection node `railway=level_crossing` tag doesn't exist. +3. When a node `railway=level_crossing` tag exists, but the node is not the intersection of a highway and railway +4. When an area or way contain `railway=level_crossing` tag + +#### Live Example + +*Case 1: Bridge over Railway on same layer.* + +Highway bridge goes over Railway, but layer missing on both railway and highway. The bridge should have a layer=1 tag +https://www.openstreetmap.org/way/80459517 + +*Case 2: Railway crosses a highway with no level_crossing tag* + +The intersection node (OSM ID: 273135212) is missing a railway=level_crossing tag. Add the appropriate tag to the node. +https://www.openstreetmap.org/node/273135212 + +*Case 3: Railway crosses highway with no intersection node.* + +The intersection of railway (OSM ID: 550221984) and highway (OSM ID: 637449204) is missing an intersection node. Add an appropriate intersection node. +https://www.openstreetmap.org/way/550221984 and https://www.openstreetmap.org/way/637449204 overlap without an intersection node. + +*Case 4: Node is not a Level_crossing* + +The node (OSM ID: 4147274783) is tagged with railway=level_crossing but is not the intersection of a railway and highway. Remove tag or add missing way. +https://www.openstreetmap.org/node/4147274783 + +*Case 5: Non-node tagged with railway=level_crossing* + +Unable to find non-node features tagged with railway=level_crossing in OSM maps. Tested manually. + +#### Code Review + +In [Atlas](https://github.com/osmlab/atlas), OSM elements are represented as Edges, Points, Lines, +Nodes & Relations; in our case, we’re working with +[Edges](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Edge.java), +[Nodes](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Node.java), and +[Lines](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Line.java). +In OpenStreetMap, railways are [Ways](https://wiki.openstreetmap.org/wiki/Way) classified with +the `railway=rail`, `railway=tram`, `railway=disused`, `railway=miniature`, or `railway=preserved` tags. Highways are +[Ways](https://wiki.openstreetmap.org/wiki/Way) classified with `highway=*` tags. In this check we are interested in +the intersection of highweays and railways. These are represented by [Nodes](https://wiki.openstreetmap.org/wiki/Node). +We’ll use this information to filter our potential flag candidates. + +A Car Navigable highway is any higway tagged with any of the following "highway:" tag values: MOTORWAY, TRUNK, +PRIMARY, SECONDARY, TERTIARY, UNCLASSIFIED, RESIDENTIAL, SERVICE, MOTORWAY_LINK, TRUNK_LINK, PRIMARY_LINK, +SECONDARY_LINK, TERTIARY_LINK, LIVING_STREET, TRACK, ROAD. + +A valid intersection of a car navigable highway and railway should have a [Node](https://wiki.openstreetmap.org/wiki/Node) +at the intersection of the ways that is classified with a `railway=level_crossing`. Nodes at intersections are only +necessary if the railway and highway ways are on the same layer. It is common for a way that is classified with a +`bridge=yes` or `tunnel=yes` to go over or under another way without an intersection node. In those cases a node at the +intersection is not necessary, and if a node does exist at that intersection then the `railway=level_crossing` tag +should not be used. This check assumes that if a highway or railway has been tagged to indicate that this way is a +bridge or tunnel and the layer tag has not been set, then there is an implied layer of 1 for bridges and -1 for +tunnels. + +Note: Railways and highways that are under construction will be ignored by this check. A way is under construction if +any tag key or tag value contains the string "construction". + +The code has three major sections to check for all possible types of railway/highway intersections. + +1. The first section is specifically designed to look at all [Nodes](https://wiki.openstreetmap.org/wiki/Node). Each +node is examined to count the number of highways and railways that contain this node. + 1. If a node is contained in at least one highway and at least one railway on the same layer and is not tagged + with `railway=level_crossing` then it is flagged. A fix suggestion exists for this type of issue and will suggest + to add a railway=level_crossing tag to the Node. + 1. If a node is tagged with `railway=level_crossing` but is not contained in any railways or highways or all +highways and railways are on separate layers then it is flagged. A fix suggestion exists for this issue and will suggest +to remove the invalid tag. + +2. The second section examines all objects that are not Nodes or Points. If any non-node or point object is tagged +with `railway=level_crossing` then it is flagged. A fix suggestion exists for this issue and it will suggest to remove +the invalid tag. + +3. The final section of code examines all railway [Lines](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Line.java). +Each railway is traversed to find any intersection with a car navigable highway. If an intersection is found that is +missing a node then it is flagged. This issue type does not include a fix suggestion. + +To learn more about the code, please look at the comments in the source code for the check. +[LevelCrossingOnRailwayCheck.java](../../src/main/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheck.java) \ No newline at end of file diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheck.java new file mode 100644 index 000000000..5f10e4841 --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheck.java @@ -0,0 +1,400 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.openstreetmap.atlas.checks.base.BaseCheck; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.geography.Location; +import org.openstreetmap.atlas.geography.atlas.Atlas; +import org.openstreetmap.atlas.geography.atlas.change.FeatureChange; +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity; +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity; +import org.openstreetmap.atlas.geography.atlas.items.AtlasItem; +import org.openstreetmap.atlas.geography.atlas.items.AtlasObject; +import org.openstreetmap.atlas.geography.atlas.items.Edge; +import org.openstreetmap.atlas.geography.atlas.items.Line; +import org.openstreetmap.atlas.geography.atlas.items.LocationItem; +import org.openstreetmap.atlas.geography.atlas.items.Node; +import org.openstreetmap.atlas.tags.ConstructionDateTag; +import org.openstreetmap.atlas.tags.ConstructionTag; +import org.openstreetmap.atlas.tags.HighwayTag; +import org.openstreetmap.atlas.tags.LayerTag; +import org.openstreetmap.atlas.tags.RailwayTag; +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; + +/** + * This check is to detect and flag nodes under the three scenarios below: 1) When a railway crosses + * a highway, but intersection node is missing. 2) When railway/highway intersection node exists, + * but railway=level_crossing tag is missing. 3) When tag railway=level_crossing exists, on a node, + * but is lacking of either highway or railway going through the node (osmose 7090), or not on a + * node, instead, on the related way features (osmose 9015) + * + * @author aiannicelli + */ +public class LevelCrossingOnRailwayCheck extends BaseCheck +{ + /** + * NodeCheck is used to indicate the output of the isValidLevelCrossingNode function. + * NODE_IGNORE indicates that something about the node or ways connected should just skip this + * node NODE_VALID indicates a valid level crossing at this node. NODE_NO_RAILWAY indicates an + * invalid level crossing because no valid railway exists NODE_NO_HIGHWAY indicates an invalid + * level crossing because no valid highway exists NODE_NO_LAYERS indicates that no highway and + * railway intersect at the same layer + */ + private enum NodeCheck + { + NODE_IGNORE, + NODE_VALID, + NODE_NO_RAILWAY, + NODE_NO_HIGHWAY, + NODE_NO_LAYERS; + } + + private static final String RAILWAY_FILTER_DEFAULT = "railway->rail,tram,disused,preserved,miniature,light_rail,subway,narrow_gauge"; + private final TaggableFilter railwayFilter; + private static final Long OSM_LAYER_DEFAULT = 0L; + private final Long layerDefault; + private static final String INVALID_TAGGED_OBJECT = "The object (OSM ID: {0,number,#}) has `railway=level_crossing` " + + "but is not a node. To fix: Remove `railway=level_crossing` tag."; + private static final int INVALID_TAGGED_OBJECT_INDEX = 0; + private static final String NODE_MISSING_LC_TAG = "The intersection node (OSM ID: {0,number,#}) is " + + "missing a `railway=level_crossing` tag. This means that there are at least one valid railway and one " + + "car navigable highway on the same layer at this node. To fix: If the two ways should be on different " + + "layers then adjust the layer tags for each way appropriately. If the two ways do intersect on the same " + + "layer then add the `railway=level_crossing` tag to this node."; + private static final int NODE_MISSING_LC_TAG_INDEX = 1; + private static final String NODE_INVALID_LC_TAG_NO_HIGHWAY = "The node (OSM ID: {0,number,#}) has " + + "`railway=level_crossing` tag, but there is no car navigable highway at this intersection. " + + "To fix: Remove railway=level_crossing tag."; + private static final int NODE_INVALID_LC_TAG_NO_HIGHWAY_INDEX = 2; + private static final String NODE_INVALID_LC_TAG_NO_RAILWAY = "The node (OSM ID: {0,number,#}) has " + + "`railway=level_crossing` tag, but there are no existing rails at this intersection. " + + "To fix: Remove railway=level_crossing tag."; + private static final int NODE_INVALID_LC_TAG_NO_RAILWAY_INDEX = 3; + private static final String NODE_INVALID_LC_TAG_LAYERS = "The node (OSM ID: {0,number,#}) has `railway=level_crossing` " + + "tag, but there are no railway and highway intersection on the same layer. " + + "To fix: If the railway and highway should be on the same layer then update the layer tags for both ways " + + "to be equal. If the ways are on different layers then remove railway=level_crossing tag."; + private static final int NODE_INVALID_LC_TAG_LAYERS_INDEX = 4; + private static final String INTERSECTION_MISSING_NODE = "The railway (OSM ID: {0,number,#}) has one or more car " + + "navigable intersections on the same layer that are missing intersection nodes. To fix: " + + "If highway and railway do cross at the same layer then add appropriate intersection node(s) with " + + "`railway=level_crossing` tag. If highway and railway are on different layers then update the " + + "appropriate layer tag for the way that goes under or over the other way."; + private static final int INTERSECTION_MISSING_NODE_INDEX = 5; + + private static final List FALLBACK_INSTRUCTIONS = Arrays.asList(INVALID_TAGGED_OBJECT, + NODE_MISSING_LC_TAG, NODE_INVALID_LC_TAG_NO_HIGHWAY, NODE_INVALID_LC_TAG_NO_RAILWAY, + NODE_INVALID_LC_TAG_LAYERS, INTERSECTION_MISSING_NODE); + private static final List CONSTRUCTION_TAGS = List.of(HighwayTag.KEY, RailwayTag.KEY); + private static final long serialVersionUID = -2063033332877849846L; + + /** + * constructor + * + * @param configuration + * the JSON configuration for this check + */ + public LevelCrossingOnRailwayCheck(final Configuration configuration) + { + + super(configuration); + this.layerDefault = this.configurationValue(configuration, "layer.default", + OSM_LAYER_DEFAULT); + this.railwayFilter = this.configurationValue(configuration, "railway.filter", + RAILWAY_FILTER_DEFAULT, TaggableFilter::forDefinition); + } + + /** + * Object check looks for the vaild objects to check for level_crossing tag. + * + * @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) + { + /*- + * The following objects should be checked: + * 1) Any node. + * 2) Any object that is tagged with railway=level_crossing. + * 3) Any object that is tagged as a railway as indicted in railway.filter. + */ + return object instanceof Node + || Validators.isOfType(object, RailwayTag.class, RailwayTag.LEVEL_CROSSING) + || this.railwayFilter.test(object); + } + + /** + * Create a Return Flags for level_crossing objects. + * + * @param object + * the atlas object supplied by the Atlas-Checks framework for evaluation + * @return an optional {@link CheckFlag} object that contains flagged issue details + */ + @Override + protected Optional flag(final AtlasObject object) + { + /*- + * The following invalid situations are to be flagged: + * 1) object is node and + * a) is marked as a level crossing but does in not an intersection of highway and railway + * b) is not tagged as a level crossing and is an intersection of highway and railway. + * 2) object is not a node or point and is tagged with railway=level_crossing. + * 3) object is railway and intersects a highway on the same layer but there is no node. + */ + + final Optional flagIncorrectlyTagged = this.flagIncorrectlyTagged(object); + if (!flagIncorrectlyTagged.isEmpty()) + { + return flagIncorrectlyTagged; + } + final Optional flagNonNodeTagged = this.flagNonNodeTagged(object); + if (!flagNonNodeTagged.isEmpty()) + { + return flagNonNodeTagged; + } + final Optional flagInvalidIntersections = this.flagInvalidIntersections(object); + if (!flagInvalidIntersections.isEmpty()) + { + return flagInvalidIntersections; + } + return Optional.empty(); + } + + @Override + protected List getFallbackInstructions() + { + return FALLBACK_INSTRUCTIONS; + } + + /** + * Flag nodes incorrectly tagged with level_crossing or missing level_crossing tag. + * + * @param object + * the atlas object supplied by the Atlas-Checks framework for evaluation + * @return an optional {@link CheckFlag} object that contains flagged issue details + */ + private Optional flagIncorrectlyTagged(final AtlasObject object) + { + if (object instanceof Node) + { + final Node node = (Node) object; + + final NodeCheck nodeCheck = this.isValidLevelCrossingNode(node); + if (Validators.isOfType(node, RailwayTag.class, RailwayTag.LEVEL_CROSSING) + && nodeCheck != NodeCheck.NODE_VALID && nodeCheck != NodeCheck.NODE_IGNORE) + { + // This is a node that is tagged with railway=level_crossing and is not a + // railway/highway intersection + final int instructIndex; + switch (nodeCheck) + { + case NODE_NO_RAILWAY: + instructIndex = NODE_INVALID_LC_TAG_NO_RAILWAY_INDEX; + break; + case NODE_NO_HIGHWAY: + instructIndex = NODE_INVALID_LC_TAG_NO_HIGHWAY_INDEX; + break; + default: + instructIndex = NODE_INVALID_LC_TAG_LAYERS_INDEX; + break; + } + return Optional.of(this + .createFlag(object, + this.getLocalizedInstruction(instructIndex, + object.getOsmIdentifier())) + .addFixSuggestion(FeatureChange.add( + (AtlasEntity) ((CompleteEntity) CompleteEntity + .from((AtlasEntity) object)).withRemovedTag(RailwayTag.KEY), + object.getAtlas()))); + } + if (!Validators.isOfType(node, RailwayTag.class, RailwayTag.LEVEL_CROSSING) + && nodeCheck == NodeCheck.NODE_VALID) + { + // This is a valid railway/highway intersect node that is not tagged with + // railway=level_crossing + return Optional.of(this + .createFlag(object, + this.getLocalizedInstruction(NODE_MISSING_LC_TAG_INDEX, + object.getOsmIdentifier())) + .addFixSuggestion(FeatureChange.add( + (AtlasEntity) ((CompleteEntity) CompleteEntity + .from((AtlasEntity) object)).withAddedTag(RailwayTag.KEY, + RailwayTag.LEVEL_CROSSING.toString().toLowerCase()), + object.getAtlas()))); + + } + } + return Optional.empty(); + } + + /** + * Flag all railway/highway intersections that are missing an intersection node + * + * @param object + * the atlas object supplied by the Atlas-Checks framework for evaluation + * @return an optional {@link CheckFlag} object that contains flagged issue details + */ + private Optional flagInvalidIntersections(final AtlasObject object) + { + if (object instanceof Line && this.railwayFilter.test(object)) + { + final Line railway = (Line) object; + final Atlas atlas = railway.getAtlas(); + final List badIntersectingHighways = new ArrayList<>(); + + Iterables.asList(atlas.edgesIntersecting(railway.bounds())) + .forEach(highway -> badIntersectingHighways + .addAll(this.missingNodesAtIntersectionOnSameLayer(railway, highway))); + if (!badIntersectingHighways.isEmpty()) + { + return Optional.of(this.createFlag(object, + this.getLocalizedInstruction(INTERSECTION_MISSING_NODE_INDEX, + railway.getOsmIdentifier()), + badIntersectingHighways)); + } + } + + return Optional.empty(); + } + + /** + * Flag all objects that are not nodes or points that are tagged with railway=level_crossing + * + * @param object + * the atlas object supplied by the Atlas-Checks framework for evaluation + * @return an optional {@link CheckFlag} object that contains flagged issue details + */ + private Optional flagNonNodeTagged(final AtlasObject object) + { + if (!(object instanceof LocationItem) + && Validators.isOfType(object, RailwayTag.class, RailwayTag.LEVEL_CROSSING)) + { + return Optional.of(this + .createFlag(object, + this.getLocalizedInstruction(INVALID_TAGGED_OBJECT_INDEX, + object.getOsmIdentifier())) + .addFixSuggestion(FeatureChange.add( + (AtlasEntity) ((CompleteEntity) CompleteEntity + .from((AtlasEntity) object)).withRemovedTag(RailwayTag.KEY), + object.getAtlas()))); + } + return Optional.empty(); + } + + /** + * Checks if the tags of an object indicate a way that is invalid. Invalid ways for this check + * are under construction or have both rail and highway tags. + * + * @param object + * Object to check + * @return true if the object is under construction, otherwise false + */ + private boolean ignoreWay(final AtlasObject object) + { + return object.getTags().keySet().stream() + .anyMatch(tag -> tag.equals(ConstructionTag.KEY) + || tag.startsWith("construction:") && !tag.equals(ConstructionDateTag.KEY)) + || CONSTRUCTION_TAGS.stream() + .anyMatch(tag -> ConstructionTag.KEY.equals(object.getTags().get(tag))) + || (HighwayTag.highwayTag(object).isPresent() && RailwayTag.isRailway(object)) + || Validators.isOfType(object, RailwayTag.class, RailwayTag.PROPOSED); + } + + /** + * Indicate if a node is a valid level_crossing intersection. + * + * @param node + * A node to check for all intersecting ways to see if a railway and highway + * intersect on the same layer + * @return an int that indicates an invalid intersection, valid intersection, or failure. 0 - + * indicates the node is a valid level crossing. Positive values indicate invalid level + * crossing. Negative values indicates that an intersecting way is under construction + */ + private NodeCheck isValidLevelCrossingNode(final Node node) + { + final Atlas atlas = node.getAtlas(); + + // check for any ways at this node to ignore. + if (Iterables.asList(atlas.itemsContaining(node.getLocation())).stream() + .anyMatch(this::ignoreWay)) + { + return NodeCheck.NODE_IGNORE; + } + // Get railway connections to this node + final List connectedRailways = Iterables + .asList(atlas.itemsContaining(node.getLocation())).stream() + .filter(this.railwayFilter::test).collect(Collectors.toList()); + if (connectedRailways.isEmpty()) + { + // Node has no railways through it + return NodeCheck.NODE_NO_RAILWAY; + } + // Get car navigable connections to this node + final List connectedHighways = Iterables + .asList(atlas.itemsContaining(node.getLocation())).stream() + .filter(HighwayTag::isCarNavigableHighway).collect(Collectors.toList()); + if (connectedHighways.isEmpty()) + { + // Node has no highways through it + return NodeCheck.NODE_NO_HIGHWAY; + } + + // For each railway, check that there is a highway on the same layer that + // is not the same way as the railway. + for (final AtlasObject railway : connectedRailways) + { + final Long railwayLayer = LayerTag.getTaggedOrImpliedValue(railway, this.layerDefault); + for (final AtlasObject highway : connectedHighways) + { + final Long highwayLayer = LayerTag.getTaggedOrImpliedValue(highway, + this.layerDefault); + if (railwayLayer.equals(highwayLayer) + && railway.getOsmIdentifier() != highway.getOsmIdentifier()) + { + return NodeCheck.NODE_VALID; + } + } + + } + return NodeCheck.NODE_NO_LAYERS; + } + + /** + * Flag an invalid intersection of a railway and highway at a specific location + * + * @param railway + * the Line that represents the railway for evaluation + * @param highway + * the Edge that represents the highway for evaluation + * @return an optional {@link CheckFlag} object that contains flagged issue details + */ + private List missingNodesAtIntersectionOnSameLayer(final Line railway, + final Edge highway) + { + final Long railwayLayer = LayerTag.getTaggedOrImpliedValue(railway, this.layerDefault); + final Long highwayLayer = LayerTag.getTaggedOrImpliedValue(highway, this.layerDefault); + + if (Edge.isMainEdgeIdentifier(highway.getIdentifier()) + && HighwayTag.isCarNavigableHighway(highway) && !this.ignoreWay(railway) + && !this.ignoreWay(highway) && railwayLayer.equals(highwayLayer)) + { + return railway.asPolyLine().intersections(highway.asPolyLine()).stream() + .filter(location -> !(railway.asPolyLine().contains(location)) + || !(highway.asPolyLine().contains(location))) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheckTest.java new file mode 100644 index 000000000..a02fabe33 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheckTest.java @@ -0,0 +1,136 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import java.util.List; +import java.util.stream.Collectors; + +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; + +/** + * Unit tests for {@link LevelCrossingOnRailwayCheck}. + * + * @author atiannicelli + */ +public class LevelCrossingOnRailwayCheckTest +{ + @Rule + public LevelCrossingOnRailwayCheckTestRule setup = new LevelCrossingOnRailwayCheckTestRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + private static void verifyObjectsAndSuggestions(final CheckFlag flag, final int objectCount, + final int fixCount) + { + Assert.assertEquals(objectCount, flag.getFlaggedObjects().size()); + Assert.assertEquals(fixCount, flag.getFixSuggestions().size()); + final List objectIds = flag.getFlaggedObjects().stream() + .filter(object -> object.getProperties().containsKey("identifier")) + .map(object -> Long.valueOf(object.getProperties().get("identifier"))) + .collect(Collectors.toList()); + flag.getFixSuggestions().forEach( + suggestion -> Assert.assertTrue(objectIds.contains(suggestion.getIdentifier()))); + } + + @Test + public void bridgeLayersTest() + { + this.verifier.actual(this.setup.getBridgeLayers(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(3); + } + + @Test + public void ignoreConstructionTest() + { + this.verifier.actual(this.setup.getIgnoreConstruction(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(0); + } + + @Test + public void invalidIntersectionNoHighwayTest() + { + this.verifier.actual(this.setup.getInvalidIntersectionNoHighway(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verifyObjectsAndSuggestions(flag, 1, 1)); + } + + @Test + public void invalidIntersectionNoRailwayTest() + { + this.verifier.actual(this.setup.getInvalidIntersectionNoRailway(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verifyObjectsAndSuggestions(flag, 1, 1)); + } + + @Test + public void invalidObjectWithTagTest() + { + this.verifier.actual(this.setup.getInvalidObjectsWithTag(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(5); + this.verifier.verify(flag -> verifyObjectsAndSuggestions(flag, 1, 1)); + } + + @Test + public void nodeAtIntersectionTest() + { + this.verifier.actual(this.setup.getNoIntersectionNode(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.globallyVerify(flags -> Assert.assertEquals(2, flags.size())); + } + + @Test + public void validIntersectionLayerConfigDefaultTest() + { + this.verifier.actual(this.setup.getValidIntersectionLayers(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver + .inlineConfiguration("{ \"LevelCrossingOnRailwayCheck\": {" + + " \"enabled\": true," + " \"layer.default\": 1" + " }}"))); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verifyObjectsAndSuggestions(flag, 2, 0)); + } + + @Test + public void validIntersectionLayerTest() + { + this.verifier.actual(this.setup.getValidIntersectionLayers(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(0); + } + + @Test + public void validIntersectionLayerZeroTest() + { + this.verifier.actual(this.setup.getValidIntersectionLayerZero(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(0); + } + + @Test + public void validIntersectionNoLayerConfigRailFilterTest() + { + this.verifier.actual(this.setup.getValidIntersectionNoLayer(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.inlineConfiguration( + "{ \"LevelCrossingOnRailwayCheck\": {" + " \"enabled\": true," + + " \"railway.filter\": \"railway->light_rail\"" + " }}"))); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(flag -> verifyObjectsAndSuggestions(flag, 1, 1)); + } + + @Test + public void validIntersectionNoLayerTest() + { + this.verifier.actual(this.setup.getValidIntersectionNoLayer(), + new LevelCrossingOnRailwayCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(0); + } + +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheckTestRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheckTestRule.java new file mode 100644 index 000000000..811dfea32 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/LevelCrossingOnRailwayCheckTestRule.java @@ -0,0 +1,354 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +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.Line; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Loc; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Node; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Point; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Relation; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Relation.Member; + +/** + * Unit test rule for {@link LevelCrossingOnRailwayCheck}. + * + * @author atiannicelli + */ +public class LevelCrossingOnRailwayCheckTestRule extends CoreTestRule +{ + private static final String TEST_1 = "20.538246,10.546134"; + private static final String TEST_2 = "20.535768,10.543755"; + private static final String TEST_3 = "20.535773,10.548353"; + private static final String R_NODE_1 = "42.6572418,-71.1448285"; + private static final String R_NODE_2 = "42.6567733,-71.1450118"; + private static final String H1_NODE_1 = "42.6571529,-71.1452251"; + private static final String H1_NODE_2 = "42.6569201,-71.1446396"; + private static final String H2_NODE_1 = "42.657005,-71.1452115"; + private static final String H2_NODE_2 = "42.6568232,-71.1447537"; + private static final String INT1 = "42.6570284,-71.144912"; + + @TestAtlas( + // This atlas contains invalid features with level_crossing tag. + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = TEST_1), tags = { + "railway=level_crossing" }), + @Node(id = "223456789000000", coordinates = @Loc(value = TEST_2), tags = {}), + @Node(id = "323456789000000", coordinates = @Loc(value = TEST_3), tags = {}) }, + // points + points = { @Point(id = "423456789000000", coordinates = @Loc(value = TEST_1), tags = { + "railway=level_crossing" }) }, + // edges + edges = { @Edge(id = "523456789000000", coordinates = { @Loc(value = TEST_1), + @Loc(value = TEST_2) }, tags = { "railway=level_crossing" }) }, + // lines + lines = { @Line(id = "623456789000000", coordinates = { @Loc(value = TEST_1), + @Loc(value = TEST_2) }, tags = { "railway=level_crossing" }) }, + // areas + areas = { @Area(id = "723456789000000", coordinates = { @Loc(value = TEST_1), + @Loc(value = TEST_2), + @Loc(value = TEST_3) }, tags = { "railway=level_crossing" }) }, + // relations + relations = { + @Relation(id = "823456789000000", members = @Member(id = "123456789000000", role = "member", type = "node"), tags = { + "railway=level_crossing" }) }) + private Atlas invalidObjectsWithTag; + + @TestAtlas( + /* + * This test atlas includes nodes, edges, and lines to test for missing intersections. + * Highways are represented as edges in atlas so this test includes two highways. One + * highway has a valid intersection with the railway line and the other highway does + * not. + */ + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = R_NODE_1), tags = {}), + @Node(id = "223456789000000", coordinates = @Loc(value = R_NODE_2), tags = {}), + @Node(id = "323456789000000", coordinates = @Loc(value = H1_NODE_1), tags = {}), + @Node(id = "423456789000000", coordinates = @Loc(value = H1_NODE_2), tags = {}), + @Node(id = "523456789000000", coordinates = @Loc(value = INT1), tags = {}), + @Node(id = "623456789000000", coordinates = @Loc(value = H2_NODE_1), tags = {}), + @Node(id = "723456789000000", coordinates = @Loc(value = H2_NODE_2), tags = {}) }, + // edges + edges = { @Edge(id = "233456789000000", coordinates = { @Loc(value = H1_NODE_1), + @Loc(value = H1_NODE_2), @Loc(value = INT1) }, tags = { "highway=secondary" }), + @Edge(id = "333456789000000", coordinates = { @Loc(value = H2_NODE_1), + @Loc(value = H2_NODE_2) }, tags = { "highway=secondary" }) }, + // lines + lines = { @Line(id = "133456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), @Loc(value = INT1) }, tags = { "railway=rail" }) }) + + private Atlas noIntersectionNode; + + /* + * Valid intersections tests: Test to following valid intersections + */ + + @TestAtlas( + /*- + * With Level Crossing Tag and no layer tags + * 1.1) highway(edge)/railway=rail(line) intersection + * 1.2) highway(edge)/railway=tram(line) intersection + * 1.3) highway(edge)/railway=disused(line) intersection + * 1.4) highway(edge)/railway=preserved(line) intersection + * 1.5) highway(edge)/railway=miniature(line) intersection + */ + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = R_NODE_1), tags = {}), + @Node(id = "223456789000000", coordinates = @Loc(value = R_NODE_2), tags = {}), + @Node(id = "323456789000000", coordinates = @Loc(value = H1_NODE_1), tags = {}), + @Node(id = "423456789000000", coordinates = @Loc(value = H1_NODE_2), tags = {}), + @Node(id = "523456789000000", coordinates = @Loc(value = INT1), tags = { + "railway=level_crossing" }), + @Node(id = "623456789000000", coordinates = @Loc(value = H2_NODE_1), tags = {}), + @Node(id = "723456789000000", coordinates = @Loc(value = H2_NODE_2), tags = {}) }, + // edges + edges = { + // 1.1-1.4: intersecting edge with no layer + @Edge(id = "113456789000000", coordinates = { @Loc(value = H1_NODE_1), + @Loc(value = H1_NODE_2), + @Loc(value = INT1) }, tags = { "highway=secondary" }) }, + // lines + lines = { + // 1.1: intersecting rail no layer + @Line(id = "133456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), + @Loc(value = INT1) }, tags = { "railway=rail" }), + // 1.2: intersecting tram no layer + @Line(id = "233456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), + @Loc(value = INT1) }, tags = { "railway=tram" }), + // 1.3: intersecting disused no layer + @Line(id = "333456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), + @Loc(value = INT1) }, tags = { "railway=disused" }), + // 1.4: intersecting preserved no layer + @Line(id = "433456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), + @Loc(value = INT1) }, tags = { "railway=preserved" }), + // 1.5: intersecting miniature no layer + @Line(id = "533456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), + @Loc(value = INT1) }, tags = { "railway=miniature" }) }) + + private Atlas validIntersectionNoLayer; + + @TestAtlas( + /* + * Ignore intersections with construction. Generally this would fail because the node + * should be tagged but is not. This test should pass because we want to ignore + * construction. + */ + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = R_NODE_1), tags = {}), + @Node(id = "223456789000000", coordinates = @Loc(value = R_NODE_2), tags = {}), + @Node(id = "323456789000000", coordinates = @Loc(value = H1_NODE_1), tags = {}), + @Node(id = "423456789000000", coordinates = @Loc(value = H1_NODE_2), tags = {}), + @Node(id = "523456789000000", coordinates = @Loc(value = INT1), tags = {}) }, + // edges + edges = { + // 1.1-1.4: intersecting edge with no layer + @Edge(id = "113456789000000", coordinates = { @Loc(value = H1_NODE_1), + @Loc(value = H1_NODE_2), @Loc(value = INT1) }, tags = { + "highway=secondary", "construction:lanes=2" }) }, + // lines + lines = { + // 1.1: intersecting rail no layer + @Line(id = "133456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), + @Loc(value = INT1) }, tags = { "railway=rail" }) }) + + private Atlas ignoreConstruction; + + @TestAtlas( + /* + * Test invalid level crossing with no railway + */ + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = R_NODE_1), tags = {}), + @Node(id = "223456789000000", coordinates = @Loc(value = R_NODE_2), tags = {}), + @Node(id = "323456789000000", coordinates = @Loc(value = H1_NODE_1), tags = {}), + @Node(id = "423456789000000", coordinates = @Loc(value = H1_NODE_2), tags = {}), + @Node(id = "523456789000000", coordinates = @Loc(value = INT1), tags = { + "railway=level_crossing" }) }, + // edges + edges = { @Edge(id = "113456789000000", coordinates = { @Loc(value = H1_NODE_1), + @Loc(value = H1_NODE_2), + @Loc(value = INT1) }, tags = { "highway=secondary" }) }) + + private Atlas invalidIntersectionNoRailway; + + @TestAtlas( + /* + * Test invalid level crossing with no highway + */ + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = R_NODE_1), tags = {}), + @Node(id = "223456789000000", coordinates = @Loc(value = R_NODE_2), tags = {}), + @Node(id = "323456789000000", coordinates = @Loc(value = H1_NODE_1), tags = {}), + @Node(id = "423456789000000", coordinates = @Loc(value = H1_NODE_2), tags = {}), + @Node(id = "523456789000000", coordinates = @Loc(value = INT1), tags = { + "railway=level_crossing" }) }, + // lines + lines = { @Line(id = "133456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), @Loc(value = INT1) }, tags = { "railway=rail" }) }) + + private Atlas invalidIntersectionNoHighway; + + @TestAtlas( + /* + * Ignore intersections with construction. Generally this would fail because the node + * should be tagged but is not. This test should pass because we want to ignore + * construction. + */ + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = R_NODE_1), tags = {}), + @Node(id = "223456789000000", coordinates = @Loc(value = R_NODE_2), tags = {}), + @Node(id = "323456789000000", coordinates = @Loc(value = H1_NODE_1), tags = {}), + @Node(id = "423456789000000", coordinates = @Loc(value = H1_NODE_2), tags = {}), + @Node(id = "523456789000000", coordinates = @Loc(value = INT1), tags = { + "railway=level_crossing" }) }, + // edges + edges = { + // 1.1-1.4: intersecting edge with no layer + @Edge(id = "113456789000000", coordinates = { @Loc(value = H1_NODE_1), + @Loc(value = H1_NODE_2), + @Loc(value = INT1) }, tags = { "highway=residential" }) }, + // lines + lines = { + // 1.1: intersecting rail no layer + @Line(id = "133456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2), + @Loc(value = INT1) }, tags = { "railway=disused", "layer=0" }) }) + + private Atlas validIntersectionLayerZero; + + @TestAtlas( + /*- + * Valid intersections With no intersection node + * 3.1) highway(edge)/railway(line) intersection with edge layer > 0 + * 3.2) highway(edge)/railway(line) intersection with edge layer < 0 + * 3.3) highway(edge)/railway(line) intersection with line layer > 0 + * 3.4) highway(edge)/railway(line) intersection with line layer < 0 + */ + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = R_NODE_1), tags = {}), + @Node(id = "223456789000000", coordinates = @Loc(value = R_NODE_2), tags = {}), + @Node(id = "623456789000000", coordinates = @Loc(value = H2_NODE_1), tags = {}), + @Node(id = "723456789000000", coordinates = @Loc(value = H2_NODE_2), tags = {}) }, + // edges + edges = { + // 3.3 3.4: no intersection edge with no layer + @Edge(id = "113456789000000", coordinates = { @Loc(value = H2_NODE_1), + @Loc(value = H2_NODE_2) }, tags = { "highway=secondary" }), + // 3.1: no intersection edge with layer tag = 2 + @Edge(id = "213456789000000", coordinates = { @Loc(value = H2_NODE_1), + @Loc(value = H2_NODE_2) }, tags = { "highway=secondary", "layer=2" }), + // 3.2: no intersection edge with layer tag = -2 + @Edge(id = "313456789000000", coordinates = { @Loc(value = H2_NODE_1), + @Loc(value = H2_NODE_2) }, tags = { "highway=secondary", + "layer=-2" }) }, + // lines + lines = { + // 3.3: non intersecting rail layer = 1 + @Line(id = "133456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2) }, tags = { "railway=rail", "layer=1" }), + // 3.4: non intersecting rail layer = -1 + @Line(id = "233456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2) }, tags = { "railway=rail", "layer=-1" }) }) + + private Atlas validIntersectionLayers; + + @TestAtlas( + /* + * Bridge Layer test. Test that an intersection with no intersection node on a bridge or + * tunnel is flagged appropriately. + */ + // nodes + nodes = { + @Node(id = "123456789000000", coordinates = @Loc(value = R_NODE_1), tags = {}), + @Node(id = "223456789000000", coordinates = @Loc(value = R_NODE_2), tags = {}), + @Node(id = "623456789000000", coordinates = @Loc(value = H2_NODE_1), tags = {}), + @Node(id = "723456789000000", coordinates = @Loc(value = H2_NODE_2), tags = {}) }, + // edges + edges = { + // no intersection edge with bridge tag + @Edge(id = "113456789000000", coordinates = { @Loc(value = H2_NODE_1), + @Loc(value = H2_NODE_2) }, tags = { "highway=secondary", + "bridge=yes" }), + // no intersection edge with tunnel tag + @Edge(id = "213456789000000", coordinates = { @Loc(value = H2_NODE_1), + @Loc(value = H2_NODE_2) }, tags = { "highway=secondary", + "tunnel=yes" }), + // no intersection edge with no tag + @Edge(id = "313456789000000", coordinates = { @Loc(value = H2_NODE_1), + @Loc(value = H2_NODE_2) }, tags = { "highway=secondary" }) }, + // lines + lines = { + // no intersection line with bridge tag + @Line(id = "133456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2) }, tags = { "railway=rail", "bridge=yes" }), + // no intersection line with tunnel tag + @Line(id = "233456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2) }, tags = { "railway=rail", "tunnel=yes" }), + // no intersection line with no tag + @Line(id = "333456789000000", coordinates = { @Loc(value = R_NODE_1), + @Loc(value = R_NODE_2) }, tags = { "railway=rail" }) }) + + private Atlas bridgeLayers; + + public Atlas getBridgeLayers() + { + return this.bridgeLayers; + } + + public Atlas getIgnoreConstruction() + { + return this.ignoreConstruction; + } + + public Atlas getInvalidIntersectionNoHighway() + { + return this.invalidIntersectionNoHighway; + } + + public Atlas getInvalidIntersectionNoRailway() + { + return this.invalidIntersectionNoRailway; + } + + public Atlas getInvalidObjectsWithTag() + { + return this.invalidObjectsWithTag; + } + + public Atlas getNoIntersectionNode() + { + return this.noIntersectionNode; + } + + public Atlas getValidIntersectionLayerZero() + { + return this.validIntersectionLayerZero; + } + + public Atlas getValidIntersectionLayers() + { + return this.validIntersectionLayers; + } + + public Atlas getValidIntersectionNoLayer() + { + return this.validIntersectionNoLayer; + } +}