diff --git a/config/configuration.json b/config/configuration.json index 9727ff6cb..af5983417 100644 --- a/config/configuration.json +++ b/config/configuration.json @@ -42,6 +42,15 @@ "defaultPriority": "LOW" } }, + "AddressStreetNameCheck": { + "bounds.size": 100.0, + "challenge": { + "description": "Tasks contain nodes with addr:street names that don't match the surrounding roads", + "blurb": "Nodes with mismatched addr:street names", + "instruction": "Open your favorite editor and edit the node street names", + "difficulty": "HARD" + } + }, "AreasWithHighwayTagCheck":{ "tag.filters":"highway->*&area->yes", "challenge":{ diff --git a/docs/checks/addressStreetNameCheck.md b/docs/checks/addressStreetNameCheck.md new file mode 100644 index 000000000..5e84fa1ec --- /dev/null +++ b/docs/checks/addressStreetNameCheck.md @@ -0,0 +1,26 @@ +# Address Street Name Check + +This check flags Points that have an `addr:street` value that does not match any of the names of surrounding streets. +The search distance for surrounding streets is based on a configurable value that has a default of 100 meters. + +#### Live Examples + +1. Node [id:847673678](https://www.openstreetmap.org/node/847673678) has an `addr:street` value of Stykkishólmsvegur that does not match any name of surrounding streets. +2. Node [id:2416844306](https://www.openstreetmap.org/node/2416844306) has an `addr:street` value of Vitatorg that is a typo of the nearby street V**í**tatorg. + +#### Code Review + +In [Atlas](https://github.com/osmlab/atlas), OSM elements are represented as Edges, Points, Lines, Nodes, Areas & Relations; +in our case, we’re are looking at +[Points](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Point.java) and +[Edges](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Edge.java). + +This check first validates objects by checking that they are Points with an `addr:street` tag. + +If an object is valid, the check gathers all name tag values from surrounding roads (Edges). The surrounding roads are defined by a +configurable search distance that is 100m by default. All name tag values are collected (`name` and localized name tags). + +The `addr:street` is then compared against the collected list of street names. If no match is found the Point is flagged. + +To learn more about the code, please look at the comments in the source code for the check. +[AddressStreetNameCheck](../../src/main/java/org/openstreetmap/atlas/checks/validation/tag/AddressStreetNameCheck.java) diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheck.java new file mode 100644 index 000000000..0b101f1c1 --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheck.java @@ -0,0 +1,100 @@ +package org.openstreetmap.atlas.checks.validation.points; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +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.items.Point; +import org.openstreetmap.atlas.tags.AddressStreetTag; +import org.openstreetmap.atlas.tags.annotations.validation.Validators; +import org.openstreetmap.atlas.tags.names.NameTag; +import org.openstreetmap.atlas.utilities.collections.Iterables; +import org.openstreetmap.atlas.utilities.configuration.Configuration; +import org.openstreetmap.atlas.utilities.scalars.Distance; + +/** + * This flags {@link Point}s where their addr:street tag value does not match any of the name tag + * values of {@link Edge}s within a configurable search distance. + * + * @author bbreithaupt + */ +public class AddressStreetNameCheck extends BaseCheck +{ + + private static final long serialVersionUID = 5401402333350044455L; + + private static final List FALLBACK_INSTRUCTIONS = Collections.singletonList( + "Address node {0,number,#} has an addr:street value that does not match the name of any roads within {1,number,#} meters."); + private static final Double SEARCH_DISTANCE_DEFAULT = 100.0; + + // Distance to search for Edges around a Point + private final Distance searchDistance; + + /** + * 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 AddressStreetNameCheck(final Configuration configuration) + { + super(configuration); + this.searchDistance = configurationValue(configuration, "bounds.size", + SEARCH_DISTANCE_DEFAULT, Distance::meters); + } + + /** + * 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 Point && Validators.hasValuesFor(object, AddressStreetTag.class); + } + + /** + * 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 flag(final AtlasObject object) + { + // Gather the values of all name tags of all edges that are within the search distance + final Set streetNameValues = Iterables + .stream(object.getAtlas().edgesIntersecting( + ((Point) object).getLocation().boxAround(this.searchDistance), + Edge::isMasterEdge)) + .flatMap(edge -> edge.getTags(tag -> tag.startsWith(NameTag.KEY)).values()) + .collectToSet(); + + // Flag the object if there are edges within the search distance and the addr:street values + // is not present in the set of Edge name tag values + return !streetNameValues.isEmpty() + && !streetNameValues.contains(object.tag(AddressStreetTag.KEY)) + ? Optional + .of(this.createFlag(object, + this.getLocalizedInstruction(0, object.getOsmIdentifier(), + this.searchDistance.asMeters()))) + : Optional.empty(); + } + + @Override + protected List getFallbackInstructions() + { + return FALLBACK_INSTRUCTIONS; + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheckTest.java new file mode 100644 index 000000000..ce2d4acdc --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheckTest.java @@ -0,0 +1,55 @@ +package org.openstreetmap.atlas.checks.validation.points; + +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; + +/** + * Unit test for {@link AddressStreetNameCheck} + * + * @author bbreithaupt + */ +public class AddressStreetNameCheckTest +{ + @Rule + public AddressStreetNameCheckTestRule setup = new AddressStreetNameCheckTestRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + @Test + public void validAddressStreetTagTest() + { + this.verifier.actual(this.setup.validAddressStreetTagAtlas(), + new AddressStreetNameCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + + @Test + public void validAddressStreetLocalizedTagTest() + { + this.verifier.actual(this.setup.validAddressStreetLocalizedTagAtlas(), + new AddressStreetNameCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + + @Test + public void invalidAddressStreetTagTest() + { + this.verifier.actual(this.setup.invalidAddressStreetTagAtlas(), + new AddressStreetNameCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.globallyVerify(flags -> Assert.assertEquals(1, flags.size())); + } + + @Test + public void validAddressStreetTagConfigNoEdgeInRangeTest() + { + this.verifier.actual(this.setup.validAddressStreetTagAtlas(), + new AddressStreetNameCheck(ConfigurationResolver + .inlineConfiguration("{\"AddressStreetNameCheck.bounds.size\":1.0}"))); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheckTestRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheckTestRule.java new file mode 100644 index 000000000..c7de246c1 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/points/AddressStreetNameCheckTestRule.java @@ -0,0 +1,81 @@ +package org.openstreetmap.atlas.checks.validation.points; + +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; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Point; + +/** + * Test rule for {@link AddressStreetNameCheckTest} + * + * @author bbreithaupt + */ +public class AddressStreetNameCheckTestRule extends CoreTestRule +{ + private static final String TEST_1 = "48.1780944662566,-122.645324334797"; + private static final String TEST_2 = "48.1784193930508,-122.644707774486"; + private static final String TEST_3 = "48.1789233570657,-122.645035943684"; + private static final String TEST_4 = "48.1785221755876,-122.645165222459"; + + @TestAtlas( + // points + points = { + @Point(coordinates = @Loc(value = TEST_4), tags = { "addr:street=1st st" }) }, + // nodes + nodes = { @Node(coordinates = @Loc(value = TEST_1)), + @Node(coordinates = @Loc(value = TEST_2)), + @Node(coordinates = @Loc(value = TEST_3)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_2) }, tags = { + "highway=residential", "name=1st st" }), + @Edge(coordinates = { @Loc(value = TEST_2), @Loc(value = TEST_3) }, tags = { + "highway=residential", "name=2nd st" }) }) + private Atlas validAddressStreetTagAtlas; + + @TestAtlas( + // points + points = { @Point(coordinates = @Loc(value = TEST_4), tags = { + "addr:street=Rue de adresse" }) }, + // nodes + nodes = { @Node(coordinates = @Loc(value = TEST_1)), + @Node(coordinates = @Loc(value = TEST_2)) }, + // edges + edges = { @Edge(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_2) }, tags = { + "highway=residential", "name=1st st", "name:fr=Rue de adresse" }) }) + private Atlas validAddressStreetLocalizedTagAtlas; + + @TestAtlas( + // points + points = { + @Point(coordinates = @Loc(value = TEST_4), tags = { "addr:street=3rd st" }) }, + // nodes + nodes = { @Node(coordinates = @Loc(value = TEST_1)), + @Node(coordinates = @Loc(value = TEST_2)), + @Node(coordinates = @Loc(value = TEST_3)) }, + // edges + edges = { + @Edge(coordinates = { @Loc(value = TEST_1), @Loc(value = TEST_2) }, tags = { + "highway=residential", "name=1st st", "name:fr=Rue de adresse" }), + @Edge(coordinates = { @Loc(value = TEST_2), @Loc(value = TEST_3) }, tags = { + "highway=residential", "name=2nd st" }) }) + private Atlas invalidAddressStreetTagAtlas; + + public Atlas validAddressStreetTagAtlas() + { + return this.validAddressStreetTagAtlas; + } + + public Atlas validAddressStreetLocalizedTagAtlas() + { + return this.validAddressStreetLocalizedTagAtlas; + } + + public Atlas invalidAddressStreetTagAtlas() + { + return this.invalidAddressStreetTagAtlas; + } +}