From fe52307d3e0f2e538a22ba4c4d5536c2b4269cc2 Mon Sep 17 00:00:00 2001 From: Taylor Smock <45215054+taylorsmock@users.noreply.github.com> Date: Wed, 9 Sep 2020 12:03:27 -0600 Subject: [PATCH] Water Area checks (#346) * Partially working water area test Signed-off-by: Taylor Smock * More false positives, but catches issues Signed-off-by: Taylor Smock * Improve check (ignore dams, better intersection handling) Signed-off-by: Taylor Smock * WaterAreaCheck: Continue through area checks instead of stopping at the first problem Signed-off-by: Taylor Smock * WaterAreaCheck: Remove unnecessary line Signed-off-by: Taylor Smock * WaterAreaCheck: Add default config values Signed-off-by: Taylor Smock * WaterAreaCheck: Expand variable names * Lambda functions have expanded variables * Other variables should be expanded Signed-off-by: Taylor Smock * WaterAreaCheck: Move discrete checks to separate functions Signed-off-by: Taylor Smock * WaterAreaCheck: Configuration: FIXUP: Failing tests * ConfigurationDeserializer#locateMap had an issue with a configuration variable Signed-off-by: Taylor Smock * WaterAreaCheck: Add docs Signed-off-by: Taylor Smock * WaterAreaCheckTest: FIXUP: Remove extraneous TODO comment Signed-off-by: Taylor Smock --- config/configuration.json | 21 ++ docs/available_checks.md | 5 +- docs/checks/waterAreaCheck.md | 38 ++ .../validation/areas/WaterAreaCheck.java | 342 ++++++++++++++++++ .../validation/areas/WaterAreaCheckTest.java | 90 +++++ .../areas/WaterAreaCheckTestRule.java | 263 ++++++++++++++ 6 files changed, 757 insertions(+), 2 deletions(-) create mode 100644 docs/checks/waterAreaCheck.md create mode 100644 src/main/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheck.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheckTest.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheckTestRule.java diff --git a/config/configuration.json b/config/configuration.json index cf49c83cc..3565f4b10 100644 --- a/config/configuration.json +++ b/config/configuration.json @@ -1068,5 +1068,26 @@ "difficulty": "EASY", "defaultPriority": "LOW" } + }, + "WaterAreaCheck": { + "intersect.minimum.limit": 0.01, + "water.tags.crossing.ignore": [ + "waterway->dam" + ], + "water.tags.filters": [ + "natural->water&water->*|waterway->riverbank" + ], + "water.tags.filtersrequireswaterway": [ + "natural->water&water->river,stream_pool,canal,lock|waterway->riverbank" + ], + "waterway.tags.filters": [ + "waterway->*" + ], + "challenge": { + "description": "Overlapping waterway areas", + "blurb": "Edit overlapping waterway features so they either validly intersect or do not overlap.", + "instruction": "Open your favorite editor and edit the features overlapping the ocean so they either validly overlap or do not overlap.", + "difficulty": "MEDIUM" + } } } diff --git a/docs/available_checks.md b/docs/available_checks.md index 8f87edebf..22783c7af 100644 --- a/docs/available_checks.md +++ b/docs/available_checks.md @@ -8,9 +8,10 @@ This document is a list of tables with a description and link to documentation f | [OceanBleedingCheck](checks/oceanBleedingCheck.md) | The purpose of this check is to identify streets, buildings, and railways that bleed into (intersect) an ocean feature. | | [OverlappingAOIPolygonCheck](checks/overlappingAOIPolygonCheck.md) | The purpose of this check is to identify areas of interest (AOIs) that are overlapping one another. | | [PedestrianAreaOverlappingEdgeCheck](checks/pedestrianAreaOverlappingEdgeCheck.md) | The purpose of this check is to identify pedestrian areas overlapping with roads that are not snapped to car navigable edges. | -| [PoolSizeCheck](tutorials/tutorial1-PoolSizeCheck.md) | The purpose of this check is to identify pools that are larger than 5,000,000 square meters or smaller than 5 square meters. This check was created to be used as a tutorial for developing new checks. | +| [PoolSizeCheck](tutorials/tutorial1-PoolSizeCheck.md) | The purpose of this check is to identify pools that are larger than 5,000,000 square meters or smaller than 5 square meters. This check was created to be used as a tutorial for developing new checks. | | [ShadowDetectionCheck](checks/shadowDetectionCheck.md) | The purpose of this check is to identify floating buildings. | | [SpikyBuildingCheck](checks/spikyBuildingCheck.md) | The purpose of this check is to identify buildings with extremely sharp angles in their geometry. | +| [WaterAreaCheck](checks/waterAreaCheck.md) | Find overlapping water areas and water areas that should have a waterway. | | [WaterbodyAndIslandSizeCheck](checks/waterbodyAndIslandSizeCheck.md) | The purpose of this check is to identify waterbodies and islands which are either too small or too large in size. | ## Highways @@ -97,4 +98,4 @@ This document is a list of tables with a description and link to documentation f | LineCrossingBuildingCheck | The purpose of this check is to identify line items (edges or lines) that are crossing buildings invalidly. | | LineCrossingWaterBodyCheck | The purpose of this check is to identify line items (edges or lines) and optionally buildings, that cross water bodies invalidly. | | MalformedPolyLineCheck | This check identifies lines that have only one point, or none, and the ones that are too long. | -| [SelfIntersectingPolylineCheck](checks/selfIntersectingPolylineCheck.md) | The purpose of this check is to identify all edges/lines/areas in Atlas that have self-intersecting polylines, or geometries that intersects itself in some form. | \ No newline at end of file +| [SelfIntersectingPolylineCheck](checks/selfIntersectingPolylineCheck.md) | The purpose of this check is to identify all edges/lines/areas in Atlas that have self-intersecting polylines, or geometries that intersects itself in some form. | diff --git a/docs/checks/waterAreaCheck.md b/docs/checks/waterAreaCheck.md new file mode 100644 index 000000000..a118eb2c4 --- /dev/null +++ b/docs/checks/waterAreaCheck.md @@ -0,0 +1,38 @@ +# Water Area Check + +#### Description +This check identifies waterbodies that cross each other or are missing a waterway (if the water area requires a water way). + +#### Live Example +1) This [riverbank](https://www.openstreetmap.org/way/661564606) is missing a river waterway (on 2020-09-09). + +2) This [pond](https://www.openstreetmap.org/way/448755487) is overlapping another [pond](https://www.openstreetmap.org/way/455609665). + +#### 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 [Areas](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Area.java). + +Our first goal is to validate the incoming Atlas object. Valid features for this check will satisfy +the following conditions: +* Must be an Area with one of the following tags: + * `natural=WATER` AND `water=*` + * `wterway=RIVERBANK` + +After the preliminary filtering, each object goes through a series of checks, the first of which checks to see if there should be a waterway inside the object, the second checks to ensure that water has a place to go (waterway exists the water body), and the third checks for overlapping waterways. + + +### Sample configuration (defaults) +``` +{ + "WaterAreaCheck": { + "intersect.minimum.limit": 0.01, + "water.tags.crossing.ignore": ["waterway->dam"], + "water.tags.filters": ["waterway->*"], + "water.tags.filtersrequireswaterway": ["natural->water&water->river,stream_pool,canal,lock|waterway->riverbank"], + "waterway.tags.filters": ["natural->water&water->*|waterway->riverbank"] + } +} +``` + +To learn more about the code, please look at the comments in the source code for the check. +[WaterAreaCheck.java](../../src/main/java/org/openstreetmap/atlas/checks/validation/areas/WaterbodyAreaCheck.java) diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheck.java new file mode 100644 index 000000000..aa2baaf8b --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheck.java @@ -0,0 +1,342 @@ +package org.openstreetmap.atlas.checks.validation.areas; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.Pair; +import org.openstreetmap.atlas.checks.base.BaseCheck; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.checks.utility.IntersectionUtilities; +import org.openstreetmap.atlas.geography.Location; +import org.openstreetmap.atlas.geography.PolyLine; +import org.openstreetmap.atlas.geography.Polygon; +import org.openstreetmap.atlas.geography.Segment; +import org.openstreetmap.atlas.geography.atlas.items.Area; +import org.openstreetmap.atlas.geography.atlas.items.AtlasObject; +import org.openstreetmap.atlas.geography.atlas.items.Line; +import org.openstreetmap.atlas.geography.atlas.items.LineItem; +import org.openstreetmap.atlas.tags.filters.TaggableFilter; +import org.openstreetmap.atlas.utilities.collections.Iterables; +import org.openstreetmap.atlas.utilities.configuration.Configuration; + +/** + * This checks water areas for overlaps and missing waterways (where appropriate). + * + * @author Taylor Smock + */ +public class WaterAreaCheck extends BaseCheck +{ + private static final long serialVersionUID = -2567398383133412329L; + + private static final List WATER_FILTERS = Arrays + .asList("natural->water&water->*|waterway->riverbank"); + private static final List WATER_FILTERS_WATERWAY = Arrays + .asList("natural->water&water->river,stream_pool,canal,lock|waterway->riverbank"); + private static final List WATERWAY_FILTERS = Arrays.asList("waterway->*"); + + // https://wiki.openstreetmap.org/wiki/Tag:waterway%3Ddam specifies that dams + // may cross other waterways + // Probably should not cross other waterways with the same tag though. + private static final List WATERWAY_CROSSING_IGNORE = Arrays.asList("waterway->dam"); + + private static final String INSTRUCTION_MISSING_WATERWAY = "Waterway area (id={0,number,#}) is missing a waterway way."; + private static final String INSTRUCTION_NO_EXITING_WATERWAY = "Waterway area (id={0,number,#}) has a waterway way, but there are none entering/exiting."; + private static final String INSTRUCTION_WATERWAY_INTERSECTION = "Waterway area (id={0,number,#}) intersects with at least one other waterway area (id={1})."; + + private static final List FALLBACK_INSTRUCTIONS = Arrays.asList( + INSTRUCTION_MISSING_WATERWAY, INSTRUCTION_NO_EXITING_WATERWAY, + INSTRUCTION_WATERWAY_INTERSECTION); + + private static final double MINIMUM_PROPORTION_DEFAULT = 0.01; + + private final double minimumIntersect; + + // List of TaggableFilters where each filter represents all tags for water areas + // that should not overlap + private final List areaFilters = new ArrayList<>(); + private final List waterRequiringWaterwayFilters = new ArrayList<>(); + private final List waterwayFilters = new ArrayList<>(); + private final List waterwayCrossingIgnore = new ArrayList<>(); + + /** + * Check if an object matches a filter + * + * @param filters + * The filters to check against + * @param object + * The object to check + * @return {@code true} if the object matches *any* filter + */ + public static boolean matchesFilter(final List filters, + final AtlasObject object) + { + return filters.parallelStream().anyMatch(filter -> filter.test(object)); + } + + /** + * Check if two objects match the same filter + * + * @param filters + * The filters to check + * @param object1 + * An AtlasObject to check + * @param object2 + * Another AtlasObject to check + * @return {@code true} if both objects match the same filter + */ + public static boolean matchesSameFilter(final List filters, + final AtlasObject object1, final AtlasObject object2) + { + return filters.parallelStream() + .anyMatch(filter -> filter.test(object1) && filter.test(object2)); + } + + /** + * Create a new WaterAreaCheck + * + * @param configuration + * The configuration for the new Check + */ + public WaterAreaCheck(final Configuration configuration) + { + super(configuration); + this.minimumIntersect = this.configurationValue(configuration, "intersect.minimum.limit", + MINIMUM_PROPORTION_DEFAULT); + List filtersString = configurationValue(configuration, "water.tags.filters", + WATER_FILTERS); + filtersString.forEach(string -> this.areaFilters.add(TaggableFilter.forDefinition(string))); + filtersString = configurationValue(configuration, "waterway.tags.filters", + WATERWAY_FILTERS); + filtersString + .forEach(string -> this.waterwayFilters.add(TaggableFilter.forDefinition(string))); + filtersString = configurationValue(configuration, "water.tags.filtersrequireswaterway", + WATER_FILTERS_WATERWAY); + filtersString.forEach(string -> this.waterRequiringWaterwayFilters + .add(TaggableFilter.forDefinition(string))); + filtersString = configurationValue(configuration, "water.tags.crossing.ignore", + WATERWAY_CROSSING_IGNORE); + filtersString.forEach( + string -> this.waterwayCrossingIgnore.add(TaggableFilter.forDefinition(string))); + } + + @Override + public boolean validCheckForObject(final AtlasObject object) + { + return object instanceof Area && (!isFlagged(object.getOsmIdentifier())) + && matchesFilter(this.areaFilters, object); + } + + @Override + protected Optional flag(final AtlasObject object) + { + final Area area = (Area) object; + final Polygon areaPolygon = area.getClosedGeometry(); + final List waterways = Iterables + .stream(area.getAtlas().linesIntersecting(areaPolygon, + atlasObject -> matchesFilter(this.waterwayFilters, atlasObject))) + .collectToList(); + CheckFlag flag = checkForMissingWaterway(null, area, waterways); + flag = checkForNoExitingWays(flag, areaPolygon, area, waterways); + flag = checkForOverlappingWaterways(flag, area); + + if (flag != null) + { + super.markAsFlagged(object.getOsmIdentifier()); + } + return Optional.ofNullable(flag); + } + + @Override + protected List getFallbackInstructions() + { + return FALLBACK_INSTRUCTIONS; + } + + /** + * Check if an object is already flagged + * + * @param objects + * The objects to check + * @return {@code true} if *all* objects are flagged + */ + private boolean alreadyFlagged(final List objects) + { + return this.getFlaggedIdentifiers().containsAll(objects.parallelStream() + .map(AtlasObject::getOsmIdentifier).collect(Collectors.toList())); + } + + /** + * Check a waterway area for a missing waterway way + * + * @param flag + * The flag to add data to. May be null. + * @param area + * The area to check + * @param waterways + * The waterways intersecting with the waterway area + * @return The modified CheckFlag (or new CheckFlag, if the passed CheckFlag was null) + */ + private CheckFlag checkForMissingWaterway(final CheckFlag flag, final Area area, + final List waterways) + { + CheckFlag returnFlag = flag; + if (waterways.isEmpty() && matchesFilter(this.waterRequiringWaterwayFilters, area)) + { + if (returnFlag == null) + { + returnFlag = new CheckFlag(this.getTaskIdentifier(area)); + } + returnFlag.addInstruction(this.getLocalizedInstruction( + FALLBACK_INSTRUCTIONS.indexOf(INSTRUCTION_MISSING_WATERWAY), + area.getOsmIdentifier())); + } + return returnFlag; + } + + /** + * Check a waterway area for exiting and entering waterways (only checks for one or the other) + * + * @param flag + * The flag to add data to. May be null. + * @param areaPolygon + * The area polygon (needed to avoid recalculating the area polygon) + * @param area + * The area to check for exiting waterways + * @param waterways + * The waterways that intersect with the waterway area + * @return The modified CheckFlag (or new CheckFlag, if the passed CheckFlag was null) + */ + private CheckFlag checkForNoExitingWays(final CheckFlag flag, final Polygon areaPolygon, + final Area area, final List waterways) + { + CheckFlag returnFlag = flag; + if (!waterways.isEmpty()) + { + final List areaSegments = areaPolygon + .segments().stream().filter(segment -> waterways.parallelStream() + .map(LineItem::asPolyLine).anyMatch(segment::intersects)) + .collect(Collectors.toList()); + if (areaSegments.isEmpty()) + { + if (returnFlag == null) + { + returnFlag = new CheckFlag(this.getTaskIdentifier(area)); + } + returnFlag.addInstruction(this.getLocalizedInstruction( + FALLBACK_INSTRUCTIONS.indexOf(INSTRUCTION_NO_EXITING_WATERWAY), + area.getOsmIdentifier())); + } + } + return returnFlag; + } + + /** + * Check for overlapping waterways + * + * @param flag + * The flag to add data to. May be null. + * @param area + * The area to check for overlapping waterways + * @return The modified CheckFlag (or new CheckFlag, if the passed CheckFlag was null) + */ + private CheckFlag checkForOverlappingWaterways(final CheckFlag flag, final Area area) + { + CheckFlag returnFlag = flag; + + final List>> possibleAreaIntersections = area.getClosedGeometry() + .segments().stream() + .map(segment -> Pair.of(segment, + Iterables + .stream(area.getAtlas().areasIntersecting(segment.bounds(), + atlasObject -> matchesFilter(this.areaFilters, atlasObject) + && !area.equals(atlasObject) + && area.getClosedGeometry().intersects( + atlasObject.getClosedGeometry()))) + .collectToList())) + .filter(pair -> !pair.getRight().isEmpty()).collect(Collectors.toList()); + + final List areaIntersections = possibleAreaIntersections.stream() + .flatMap(pair -> pair.getRight().stream()).distinct() + .filter(tArea -> !intersections(area.getClosedGeometry(), tArea.getClosedGeometry()) + .isEmpty()) + .filter(tArea -> matchesFilter(this.waterwayCrossingIgnore, tArea) + && matchesFilter(this.waterwayCrossingIgnore, area) + || !matchesFilter(this.waterwayCrossingIgnore, tArea) + && !matchesFilter(this.waterwayCrossingIgnore, area)) + .collect(Collectors.toList()); + if (!areaIntersections.isEmpty() && !alreadyFlagged(areaIntersections)) + { + if (returnFlag == null) + { + returnFlag = new CheckFlag(this.getTaskIdentifier(area)); + } + returnFlag.addPoints(possibleAreaIntersections.stream() + .filter(pair -> !alreadyFlagged(pair.getRight())).map(Pair::getLeft) + .map(Segment::middle).collect(Collectors.toList())); + returnFlag.addInstruction(this.getLocalizedInstruction( + FALLBACK_INSTRUCTIONS.indexOf(INSTRUCTION_WATERWAY_INTERSECTION), + area.getOsmIdentifier(), + areaIntersections.stream().map(AtlasObject::getOsmIdentifier).distinct() + .map(Objects::toString).collect(Collectors.joining(", ")))); + areaIntersections.forEach(returnFlag::addObject); + areaIntersections.stream().map(AtlasObject::getOsmIdentifier) + .forEach(super::markAsFlagged); + } + + // Sometimes there will be two waterways that share every exterior intersection, + // but one or the other cuts a corner somewhere. + final List areaOverlaps = possibleAreaIntersections.stream() + .flatMap(pair -> pair.getRight().stream()).distinct() + .filter(tArea -> IntersectionUtilities.findIntersectionPercentage( + tArea.getClosedGeometry(), + area.getClosedGeometry()) >= this.minimumIntersect) + .collect(Collectors.toList()); + if (!areaOverlaps.isEmpty()) + { + if (returnFlag == null) + { + returnFlag = new CheckFlag(this.getTaskIdentifier(area)); + } + returnFlag.addInstruction(this.getLocalizedInstruction( + FALLBACK_INSTRUCTIONS.indexOf(INSTRUCTION_WATERWAY_INTERSECTION), + area.getOsmIdentifier(), + areaOverlaps.stream().map(AtlasObject::getOsmIdentifier).distinct() + .map(Objects::toString).collect(Collectors.joining(", ")))); + areaOverlaps.forEach(returnFlag::addObject); + } + return returnFlag; + } + + /** + * Get the intersections between two polylines. Unlike {@link PolyLine#intersections}, this does + * not include points that are shared between the two lines. + * + * @param line1 + * A line to check for intersections + * @param line2 + * A line to check for intersections + * @return The intersections of line1 and line2 where line1 and line2 are not connected. + */ + private Set intersections(final PolyLine line1, final PolyLine line2) + { + // An intersection is any shared point OR overlap + if (line1.intersects(line2)) + { + final Set intersections = line1.intersections(line2); + // Remove intersections that are points on both lines + intersections.removeIf( + intersection -> line1.contains(intersection) && line2.contains(intersection)); + if (!intersections.isEmpty()) + { + return intersections; + } + } + return Collections.emptySet(); + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheckTest.java new file mode 100644 index 000000000..e8dbbcd09 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheckTest.java @@ -0,0 +1,90 @@ +package org.openstreetmap.atlas.checks.validation.areas; + +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; + +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 WaterAreaCheck + * + * @author Taylor Smock + */ +public class WaterAreaCheckTest +{ + @Rule + public WaterAreaCheckTestRule setup = new WaterAreaCheckTestRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + /** + * This was a test for a false-false positive. The false positive occurred due to the age of the + * Belize pbf, where a waterway was drawn in April 2019 was not present. + */ + @Test + public void testBrazilRiverFalsePositive() + { + this.verifier.actual(this.setup.getBrazilRiverFalsePositive(), + new WaterAreaCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyEmpty(); + } + + @Test + public void testMangoCreekAndUnnamedWaterwayBad() + { + this.verifier.actual(this.setup.getMangoCreekAtlasBad(), + new WaterAreaCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(f -> assertTrue(f.getFlaggedObjects().parallelStream() + .filter(i -> i.getProperties().containsKey("identifier")) + .allMatch(i -> Arrays.asList("601378260", "265672061") + .contains(i.getProperties().get("identifier"))))); + } + + @Test + public void testMangoCreekAndUnnamedWaterwayGood() + { + this.verifier.actual(this.setup.getMangoCreekAtlasGood(), + new WaterAreaCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyEmpty(); + } + + @Test + public void testMangoCreekAndUnnamedWaterwayMissingWaterwayLineBad() + { + this.verifier.actual(this.setup.getMangoCreekAtlasBadWaterway(), + new WaterAreaCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(1); + this.verifier.verify(f -> assertTrue(f.getFlaggedObjects().parallelStream() + .allMatch(p -> "601378260".equals(p.getProperties().get("identifier"))))); + } + + @Test + public void testMopanRiverFalsePositive() + { + this.verifier.actual(this.setup.getMopanRiverFalsePositive(), + new WaterAreaCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyEmpty(); + } + + @Test + public void testOverlappingPonds() + { + this.verifier.actual(this.setup.getOverlappingPonds(), + new WaterAreaCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyExpectedSize(1); + } + + @Test + public void testPondAndPier() + { + this.verifier.actual(this.setup.getPondAndPier(), + new WaterAreaCheck(ConfigurationResolver.emptyConfiguration())); + this.verifier.verifyEmpty(); + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheckTestRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheckTestRule.java new file mode 100644 index 000000000..310425b9f --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/areas/WaterAreaCheckTestRule.java @@ -0,0 +1,263 @@ +package org.openstreetmap.atlas.checks.validation.areas; + +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.Line; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Loc; + +/** + * Atlases for WaterAreaCheckTest + * + * @author Taylor Smock + */ +public class WaterAreaCheckTestRule extends CoreTestRule +{ + + // Mango Creek waterway area at 16.5504092, -88.4017051. It is simplified a bit. + private static final String MCA1 = "16.5531584, -88.4067505"; + private static final String MCA2 = "16.5502345, -88.4020858"; + private static final String MCA3 = "16.5504092, -88.4017051"; + private static final String MCA4 = "16.5506639, -88.4012453"; + private static final String MCA5 = "16.5513499, -88.4013399"; + private static final String MCA6 = "16.551958, -88.4017335"; + private static final String MCA7 = "16.5537792, -88.4067808"; + // The unnamed waterway connected to the Mango Creek area + private static final String UA1 = "16.5541018, -88.4017725"; + private static final String UA2 = MCA6; + private static final String UA3 = MCA5; + private static final String UA4 = "16.5540768, -88.4011307"; + // Downstream Mango Creek waterway area + private static final String MCB1 = MCA4; + private static final String MCB2 = MCA3; + private static final String MCB3 = MCA2; + private static final String MCB4 = "16.5425772, -88.3997794"; + private static final String MCB5 = "16.5431686, -88.3989688"; + private static final String MCB6 = "16.5442013, -88.3972504"; + // Mango Creek waterway (starting outside of the waterway area) + private static final String MCW1 = "16.5534022, -88.4073261"; + private static final String MCW2 = "16.5534353, -88.4068047"; + private static final String MCW3 = "16.5511222, -88.402569"; + private static final String MCW4 = MCA3; + private static final String MCW5 = "16.549232, -88.4011271"; + private static final String MCW6 = "16.5435986, -88.3992923"; + private static final String MCW7 = MCB5; + // Unnamed waterway (starting outside of the waterway area) + private static final String UW1 = "16.5542841, -88.4014292"; + private static final String UW2 = "16.5520628, -88.4014292"; + private static final String UW3 = MCW4; + + @TestAtlas(areas = { + @Area(id = "265672061", coordinates = { @Loc(value = MCA1), @Loc(value = MCA2), + @Loc(value = MCA3), @Loc(value = MCA4), @Loc(value = MCA5), @Loc(value = MCA6), + @Loc(value = MCA7), @Loc(value = MCA1) }, tags = { "natural=water", + "water=river", "waterway=riverbank" }), + @Area(id = "601378260", coordinates = { @Loc(value = UA1), @Loc(value = UA2), + @Loc(value = UA3), @Loc(value = UA4), + @Loc(value = UA1) }, tags = { "waterway=riverbank" }), + @Area(id = "265672060", coordinates = { @Loc(value = MCB1), @Loc(value = MCB2), + @Loc(value = MCB3), @Loc(value = MCB4), @Loc(value = MCB5), @Loc(value = MCB6), + @Loc(value = MCB1) }, tags = { "natural=water", "water=river" }) }, lines = { + @Line(id = "265670075", coordinates = { @Loc(value = MCW1), + @Loc(value = MCW2), @Loc(value = MCW3), @Loc(value = MCW4), + @Loc(value = MCW5), @Loc(value = MCW6), + @Loc(value = MCW7) }, tags = { "name=Mango Creek", + "waterway=river" }), + @Line(id = "265625753", coordinates = { @Loc(value = UW1), + @Loc(value = UW2), + @Loc(value = UW3) }, tags = { "waterway=river" }) }) + private Atlas mangoCreekAtlasGood; + + // Make a bad node for the main Mango Creek area + private static final String MCA6B = "16.55196571318903, -88.40172679447748"; + @TestAtlas(areas = { + @Area(id = "265672061", coordinates = { @Loc(value = MCA1), @Loc(value = MCA2), + @Loc(value = MCA3), @Loc(value = MCA4), @Loc(value = MCA5), @Loc(value = MCA6B), + @Loc(value = MCA7), @Loc(value = MCA1) }, tags = { "natural=water", + "water=river", "waterway=riverbank" }), + @Area(id = "601378260", coordinates = { @Loc(value = UA1), @Loc(value = UA2), + @Loc(value = UA3), @Loc(value = UA4), + @Loc(value = UA1) }, tags = { "waterway=riverbank" }), + @Area(id = "265672060", coordinates = { @Loc(value = MCB1), @Loc(value = MCB2), + @Loc(value = MCB3), @Loc(value = MCB4), @Loc(value = MCB5), @Loc(value = MCB6), + @Loc(value = MCB1) }, tags = { "natural=water", "water=river" }) }, lines = { + @Line(id = "265670075", coordinates = { @Loc(value = MCW1), + @Loc(value = MCW2), @Loc(value = MCW3), @Loc(value = MCW4), + @Loc(value = MCW5), @Loc(value = MCW6), + @Loc(value = MCW7) }, tags = { "name=Mango Creek", + "waterway=river" }), + @Line(id = "265625753", coordinates = { @Loc(value = UW1), + @Loc(value = UW2), + @Loc(value = UW3) }, tags = { "waterway=river" }) }) + private Atlas mangoCreekAtlasBad; + + // Remove a waterway from the Mango Creek area + @TestAtlas(areas = { + @Area(id = "265672061", coordinates = { @Loc(value = MCA1), @Loc(value = MCA2), + @Loc(value = MCA3), @Loc(value = MCA4), @Loc(value = MCA5), @Loc(value = MCA6), + @Loc(value = MCA7), @Loc(value = MCA1) }, tags = { "natural=water", + "water=river", "waterway=riverbank" }), + @Area(id = "601378260", coordinates = { @Loc(value = UA1), @Loc(value = UA2), + @Loc(value = UA3), @Loc(value = UA4), + @Loc(value = UA1) }, tags = { "waterway=riverbank" }), + @Area(id = "265672060", coordinates = { @Loc(value = MCB1), @Loc(value = MCB2), + @Loc(value = MCB3), @Loc(value = MCB4), @Loc(value = MCB5), @Loc(value = MCB6), + @Loc(value = MCB1) }, tags = { "natural=water", "water=river" }) }, lines = { + @Line(id = "265670075", coordinates = { @Loc(value = MCW1), + @Loc(value = MCW2), @Loc(value = MCW3), @Loc(value = MCW4), + @Loc(value = MCW5), @Loc(value = MCW6), + @Loc(value = MCW7) }, tags = { "name=Mango Creek", + "waterway=river" }) }) + private Atlas mangoCreekAtlasBadWaterway; + + // False positive anabranch/braided stream on Belize River around 17.2037351, + // -89.0095902. This "false positive" occured due to the age of the Belize pbf + // file. + private static final String BA1 = "17.2053526, -89.009155"; + private static final String BA2 = "17.2050073, -89.0092547"; + private static final String BA3 = "17.2046956, -89.0094701"; + private static final String BA4 = "17.2042592, -89.0095549"; + private static final String BA5 = "17.2039225, -89.0096072"; + private static final String BA6 = "17.2037351, -89.0095902"; + private static final String BA7 = "17.2036013, -89.0094864"; + private static final String BA8 = "17.2034191, -89.009327"; + private static final String BA9 = "17.2032691, -89.0090662"; + private static final String BA10 = "17.2029231, -89.0086411"; + private static final String BA11 = "17.2027293, -89.0083175"; + private static final String BA12 = "17.2026462, -89.0081074"; + private static final String BA13 = "17.2025609, -89.0079866"; + private static final String BA14 = "17.202457, -89.0076702"; + private static final String BA15 = "17.2022656, -89.0072307"; + private static final String BA16 = "17.2021861, -89.0069771"; + private static final String BAA1 = "17.2037668, -89.0096328"; + private static final String BAA2 = "17.2037933, -89.0095804"; + private static final String BAA3 = "17.2036708, -89.0095106"; + private static final String BAA4 = "17.2036708, -89.0095106"; + private static final String BAA5 = "17.2032762, -89.0090171"; + private static final String BAA6 = "17.2031122, -89.0088239"; + private static final String BAA7 = "17.2030268, -89.0087348"; + private static final String BAA8 = "17.2029956, -89.0087023"; + private static final String BAA9 = "17.2029162, -89.0086684"; + private static final String BAA10 = "17.2030969, -89.0089044"; + private static final String BAA11 = "17.2032711, -89.009119"; + private static final String BAA12 = "17.2034094, -89.0093604"; + private static final String BAA13 = "17.2036759, -89.0095964"; + + @TestAtlas(areas = { @Area(id = "527986191", coordinates = { @Loc(value = BAA1), + @Loc(value = BAA2), @Loc(value = BAA3), @Loc(value = BAA4), @Loc(value = BAA5), + @Loc(value = BAA6), @Loc(value = BAA7), @Loc(value = BAA8), @Loc(value = BAA9), + @Loc(value = BAA10), @Loc(value = BAA11), @Loc(value = BAA12), @Loc(value = BAA13), + @Loc(value = BAA1) }, tags = { "natural=water", "water=river" }) }, lines = { + @Line(id = "684050065", coordinates = { @Loc(value = BA1), @Loc(value = BA2), + @Loc(value = BA3), @Loc(value = BA4), @Loc(value = BA5), + @Loc(value = BA6), @Loc(value = BA7), @Loc(value = BA8), + @Loc(value = BA9), @Loc(value = BA10), @Loc(value = BA11), + @Loc(value = BA12), @Loc(value = BA13), @Loc(value = BA14), + @Loc(value = BA15), + @Loc(value = BA16) }, tags = { "waterway=stream" }) }) + private Atlas brazilRiverFalsePositive; + + // Overlapping ponds (simplified, originals at 17.2441802, -88.9930825) + private static final String PO11 = "17.2443388, -88.9931603"; + private static final String PO12 = "17.2443229, -88.9929948"; + private static final String PO13 = "17.2441959, -88.9930348"; + private static final String PO14 = "17.2441959, -88.9931429"; + private static final String PO21 = "17.2443326, -88.9931606"; + private static final String PO22 = "17.2443357, -88.992988"; + private static final String PO23 = "17.2441926, -88.9930271"; + private static final String PO24 = "17.2442051, -88.9931443"; + @TestAtlas(areas = { + @Area(id = "448755488", coordinates = { @Loc(value = PO11), @Loc(value = PO12), + @Loc(value = PO13), @Loc(value = PO14), + @Loc(value = PO11) }, tags = { "natural=water", "water=pond" }), + @Area(id = "455609652", coordinates = { @Loc(value = PO21), @Loc(value = PO22), + @Loc(value = PO23), @Loc(value = PO24), + @Loc(value = PO21) }, tags = { "natural=water", "water=pond" }) }) + private Atlas overlappingPonds; + + // Make a pier over a pond (uses same coordinates as the overlapping ponds, but + // one of the ponds is a pier) + @TestAtlas(areas = { + @Area(id = "448755488", coordinates = { @Loc(value = PO11), @Loc(value = PO12), + @Loc(value = PO13), @Loc(value = PO14), + @Loc(value = PO11) }, tags = { "natural=water", "water=pond" }), + @Area(id = "455609652", coordinates = { @Loc(value = PO21), @Loc(value = PO22), + @Loc(value = PO23), @Loc(value = PO24), + @Loc(value = PO21) }, tags = { "man_made=pier" }) }) + private Atlas pondAndPier; + + // Mopan River at 17.1692143, -89.1223818 + private static final String MR11 = "17.1692399, -89.1235319"; + private static final String MR12 = "17.1690338, -89.122337"; + private static final String MR13 = "17.1692143, -89.1223818"; + private static final String MR14 = "17.1693629, -89.1235427"; + private static final String MR21 = "17.1697448, -89.1238431"; + private static final String MR22 = "17.1694217, -89.1224173"; + private static final String MR23 = MR13; + private static final String MR24 = "17.1696525, -89.123768"; + private static final String MR31 = "17.168961, -89.1212887"; + private static final String MR32 = MR22; + private static final String MR33 = MR13; + private static final String MR34 = MR12; + private static final String MR35 = "17.168626, -89.1212833"; + private static final String MRW11 = "17.1692897, -89.1235861"; + private static final String MRW12 = "17.1691097, -89.1219496"; + private static final String MRW21 = "17.1696881, -89.1238189"; + private static final String MRW22 = MRW12; + private static final String MRW23 = "17.1684911, -89.1207273"; + @TestAtlas(areas = { + @Area(id = "591224029", coordinates = { @Loc(value = MR11), @Loc(value = MR12), + @Loc(value = MR13), @Loc(value = MR14), + @Loc(value = MR11) }, tags = { "waterway=riverbank" }), + @Area(id = "591224028", coordinates = { @Loc(value = MR21), @Loc(value = MR22), + @Loc(value = MR23), @Loc(value = MR24), + @Loc(value = MR21) }, tags = { "waterway=riverbank" }), + @Area(id = "481131794", coordinates = { @Loc(value = MR31), @Loc(value = MR32), + @Loc(value = MR33), @Loc(value = MR34), @Loc(value = MR35), + @Loc(value = MR31) }, tags = { "waterway=riverbank", + "water=river" }) }, lines = { + @Line(id = "287672438", coordinates = { @Loc(value = MRW11), + @Loc(value = MRW12) }, tags = { "waterway=river", + "source=bing", "name=Mopan River" }), + @Line(id = "31955806", coordinates = { @Loc(value = MRW21), + @Loc(value = MRW22), @Loc(value = MRW23) }, tags = { + "waterway=river", "source=bing", + "name=Mopan River", "boat=yes" }) }) + private Atlas mopanRiverFalsePositive; + + public Atlas getBrazilRiverFalsePositive() + { + return this.brazilRiverFalsePositive; + } + + public Atlas getMangoCreekAtlasBad() + { + return this.mangoCreekAtlasBad; + } + + public Atlas getMangoCreekAtlasBadWaterway() + { + return this.mangoCreekAtlasBadWaterway; + } + + public Atlas getMangoCreekAtlasGood() + { + return this.mangoCreekAtlasGood; + } + + public Atlas getMopanRiverFalsePositive() + { + return this.mopanRiverFalsePositive; + } + + public Atlas getOverlappingPonds() + { + return this.overlappingPonds; + } + + public Atlas getPondAndPier() + { + return this.pondAndPier; + } +}