From 6e623b8c80238b932d1a5d0b81786c28abd05154 Mon Sep 17 00:00:00 2001 From: Gabe Reichenberger Date: Wed, 30 Dec 2020 12:53:19 -0800 Subject: [PATCH] Concerning angle building check (#452) * updated with angles over 90 degrees * False Positive Phase. Added node range for buildings * this and final * all necessary documentation as well as unit tests complete. Awaiting FPR analysis from editors. * adjusting angle range after first round of FPR analysis. * Fixing Maproulette challenge attributes. * Added tests for number of angles per suggested comment! --- config/configuration.json | 18 ++ docs/available_checks.md | 1 + docs/checks/concerningAngleBuildingCheck.md | 40 +++ .../areas/ConcerningAngleBuildingCheck.java | 234 ++++++++++++++++++ .../ConcerningAngleBuildingCheckTest.java | 55 ++++ .../ConcerningAngleBuildingCheckTestRule.java | 101 ++++++++ 6 files changed, 449 insertions(+) create mode 100644 docs/checks/concerningAngleBuildingCheck.md create mode 100644 src/main/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheck.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheckTest.java create mode 100644 src/test/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheckTestRule.java diff --git a/config/configuration.json b/config/configuration.json index 3194dc236..7b33f4f7b 100644 --- a/config/configuration.json +++ b/config/configuration.json @@ -133,6 +133,24 @@ "tags":"building,highway" } }, + "ConcerningAngleBuildingCheck": { + "angles": { + "minLowAngleDiff": 80.0, + "maxLowAngleDiff": 89.7, + "minHighAngleDiff": 90.3, + "maxHighAngleDiff": 100.0 + }, + "angleCounts": { + "min": 4.0, + "max": 16.0 + }, + "challenge": { + "description": "Tasks contain buildings with angles that need to be squared.", + "blurb": "Concerning angles in buildings", + "instructions": "Adjust the building's angles to be right angles.", + "difficulty": "EASY" + } + }, "ConflictingAreaTagCombination": { "challenge": { "description": "Task contains Area's with mutually exclusive tag combinations.", diff --git a/docs/available_checks.md b/docs/available_checks.md index 7d6a4bdea..1ea381b37 100644 --- a/docs/available_checks.md +++ b/docs/available_checks.md @@ -4,6 +4,7 @@ This document is a list of tables with a description and link to documentation f ## Areas | Check Name | Check Description | | :--------- | :---------------- | +| [ConcerningAngleBuildingCheck](checks/concerningAngleBuildingCheck.md) | This check attempts to flag building that have angles between 80 and 89.9 degrees or between 90.1 and 100 degrees and need to be squared. | | [AreasWithHighwayTagCheck](checks/areasWithHighwayTagCheck.md) | The purpose of this check is to identify Areas attributed with highway tags. | | [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. | diff --git a/docs/checks/concerningAngleBuildingCheck.md b/docs/checks/concerningAngleBuildingCheck.md new file mode 100644 index 000000000..723d9f3b7 --- /dev/null +++ b/docs/checks/concerningAngleBuildingCheck.md @@ -0,0 +1,40 @@ +# AcuteAngleBuildingCheck + +#### Description +This check attempts to flag building who have angles between 80 and 89.9 degrees or between 90.1 and 100 degrees that need to be squared. + +#### Configurables +angles: +- ***minLowAngleDiff***: minimum angle difference between two building segments below 90 degrees. +- ***maxLowAngleDiff***: maximum angle difference between two building segments below 90 degrees. +- ***minHighAngleDiff***: minimum angle difference between two building segments above 90 degrees. +- ***maxHighAngleDiff***: maximum angle difference between two building segments above 90 degrees. + +angleCounts: +- ***min***: minimum amount of nodes the building can have. +- ***max***: maximum amount of nodes the building can have. + + +#### Live Examples + +Building needs to have angles squared +1. The way [id:791199437](https://www.openstreetmap.org/way/791199437) needs to have its angle squared. + +#### Code Review +This check evaluates [Relations](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Relation.java) and +[Areas](https://github.com/osmlab/atlas/blob/dev/src/main/java/org/openstreetmap/atlas/geography/atlas/items/Area.java), +it attempts to find buildings that have acute angles. + +##### Validating the Object +We first validate that the incoming object: +* Is an area or relation AND is a building. + +or + +* Is part of a building. + +##### Flagging the Edge +* Iterate through all Area/Relation based polygons associated with a building and check each angle. + +To learn more about the code, please look at the comments in the source code for the check. +[ConcerningAngleBuildingCheck.java](../../src/main/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheck.java) diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheck.java new file mode 100644 index 000000000..8bbfc0880 --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheck.java @@ -0,0 +1,234 @@ +package org.openstreetmap.atlas.checks.validation.areas; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.openstreetmap.atlas.checks.base.BaseCheck; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.geography.Heading; +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.ItemType; +import org.openstreetmap.atlas.geography.atlas.items.Relation; +import org.openstreetmap.atlas.geography.atlas.items.RelationMember; +import org.openstreetmap.atlas.tags.BuildingPartTag; +import org.openstreetmap.atlas.tags.BuildingTag; +import org.openstreetmap.atlas.tags.annotations.validation.Validators; +import org.openstreetmap.atlas.utilities.configuration.Configuration; + +/** + * Auto generated Check template + * + * @author v-garei + */ +public class ConcerningAngleBuildingCheck extends BaseCheck +{ + private static final long serialVersionUID = 8586559001979110697L; + private static final String CONCERNING_ANGLE_INSTRUCTIONS = "Area {0, number, #} has concerning angles, please make all angles right angles."; + private static final List FALLBACK_INSTRUCTIONS = Collections + .singletonList(CONCERNING_ANGLE_INSTRUCTIONS); + private static final double ANGLE_DEFAULT = 90.0; + private static final double MIN_LOW_ANGLE_DIFF_DEFAULT = 80.0; + private static final double MAX_LOW_ANGLE_DIFF_DEFAULT = 89.9; + private static final double MIN_HIGH_ANGLE_DIFF_DEFAULT = 90.1; + private static final double MAX_HIGH_ANGLE_DIFF_DEFAULT = 100.0; + private static final double MIN_ANGLE_COUNT_DEFAULT = 4.0; + private static final double MAX_ANGLE_COUNT_DEFAULT = 16.0; + private final double minLowAngleDiff; + private final double maxLowAngleDiff; + private final double minHighAngleDiff; + private final double maxHighAngleDiff; + private final double minAngleCount; + private final double maxAngleCount; + + /** + * 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 ConcerningAngleBuildingCheck(final Configuration configuration) + { + super(configuration); + this.minLowAngleDiff = this.configurationValue(configuration, "angles.minLowAngleDiff", + MIN_LOW_ANGLE_DIFF_DEFAULT); + this.maxLowAngleDiff = this.configurationValue(configuration, "angles.maxLowAngleDiff", + MAX_LOW_ANGLE_DIFF_DEFAULT); + this.minHighAngleDiff = this.configurationValue(configuration, "angles.minHighAngleDiff", + MIN_HIGH_ANGLE_DIFF_DEFAULT); + this.maxHighAngleDiff = this.configurationValue(configuration, "angles.maxHighAngleDiff", + MAX_HIGH_ANGLE_DIFF_DEFAULT); + this.minAngleCount = this.configurationValue(configuration, "angleCounts.min", + MIN_ANGLE_COUNT_DEFAULT); + this.maxAngleCount = this.configurationValue(configuration, "angleCounts.max", + MAX_ANGLE_COUNT_DEFAULT); + } + + /** + * 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 !isFlagged(object.getOsmIdentifier()) && this.isBuildingOrPart(object) + && (object instanceof Area + || (object instanceof Relation && ((Relation) object).isMultiPolygon())); + } + + /** + * 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) + { + markAsFlagged(object.getOsmIdentifier()); + final Set buildingPolygons = this.getPolygons(object).collect(Collectors.toSet()); + if (!buildingPolygons.isEmpty()) + { + for (final Polygon polygon : buildingPolygons) + { + if (this.buildingAngleCountWithinValidRange(polygon) + && this.hasConcerningAngles(polygon)) + { + return Optional.of(this.createFlag(object, + this.getLocalizedInstruction(0, object.getOsmIdentifier()))); + } + } + } + return Optional.empty(); + } + + @Override + protected List getFallbackInstructions() + { + return FALLBACK_INSTRUCTIONS; + } + + /** + * checks to make sure building node count fits within desired range + * + * @param polygon + * building being checked + * @return boolean ensuring that the node count fits within desired range + */ + private boolean buildingAngleCountWithinValidRange(final Polygon polygon) + { + final List polygonSegments = polygon.segments(); + return polygonSegments.size() >= this.minAngleCount + && polygonSegments.size() <= this.maxAngleCount; + } + + /** + * Get angle diff between 2 segments + * + * @param segment1 + * a segment + * @param segment2 + * a connecting segment + * @return angle difference between headings of segments + */ + private double getAngleDiff(final Segment segment1, final Segment segment2) + { + final Optional segmentOneHeading = segment1.heading(); + final Optional segmentTwoHeading = segment2.heading(); + if (segmentOneHeading.isPresent() && segmentTwoHeading.isPresent()) + { + return segmentOneHeading.get().difference(segmentTwoHeading.get()).asDegrees(); + } + return ANGLE_DEFAULT; + } + + /** + * Gets all of the polygons contained in this object, if this object has any. + * + * @param object + * any atlas object + * @return A singleton stream if object is an Area, a stream if object is a Multipolygon, or an + * empty stream if object is neither + */ + private Stream getPolygons(final AtlasObject object) + { + if (object instanceof Area) + { + return Stream.of(((Area) object).asPolygon()); + } + else if (((Relation) object).isMultiPolygon()) + { + return ((Relation) object).members().stream().map(this::toPolygon) + .flatMap(Optional::stream); + } + return Stream.empty(); + } + + /** + * Checks if angle fits within concerning range set in config. + * + * @param polygon + * building + * @return boolean if the angle fits within concerning ranges + */ + private boolean hasConcerningAngles(final Polygon polygon) + { + final List segments = polygon.segments(); + final int segmentSize = segments.size(); + for (final Segment segment : segments) + { + if (segments.indexOf(segment) < segmentSize - 2) + { + final Segment nextSegment = segments.get(segments.indexOf(segment) + 1); + return (this.getAngleDiff(segment, nextSegment) < this.maxLowAngleDiff + && this.getAngleDiff(segment, nextSegment) > this.minLowAngleDiff) + || (this.getAngleDiff(segment, nextSegment) < this.maxHighAngleDiff + && this.getAngleDiff(segment, nextSegment) > this.minHighAngleDiff); + } + } + return false; + } + + /** + * Given an object, returns true if that object has a building tag or building part tag + * indicating that it is either a building or a building part. + * + * @param object + * any AtlasObject + * @return true if object is a building or a building part, false otherwise + */ + private boolean isBuildingOrPart(final AtlasObject object) + { + return BuildingTag.isBuilding(object) + || Validators.isNotOfType(object, BuildingPartTag.class, BuildingPartTag.NO); + } + + /** + * Converts a RelationMember to a polygon if that member is an area. + * + * @param member + * any RelationMember object + * @return an polygon containing the geometry of member if it is an area, otherwise an empty + * optional. + */ + private Optional toPolygon(final RelationMember member) + { + if (member.getEntity().getType().equals(ItemType.AREA)) + { + return Optional.of(((Area) member.getEntity()).asPolygon()); + } + return Optional.empty(); + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheckTest.java new file mode 100644 index 000000000..0b9fd0350 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheckTest.java @@ -0,0 +1,55 @@ +package org.openstreetmap.atlas.checks.validation.areas; + +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 {@link ConcerningAngleBuildingCheck} + * + * @author v-garei + */ +public class ConcerningAngleBuildingCheckTest +{ + @Rule + public ConcerningAngleBuildingCheckTestRule setup = new ConcerningAngleBuildingCheckTestRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + private final ConcerningAngleBuildingCheck check = new ConcerningAngleBuildingCheck( + ConfigurationResolver.inlineConfiguration("{\"ConcerningAngleBuildingCheck\": {" + + "\"angles\": " + "{\"minLowAngleDiff\": 80.0," + "\"maxLowAngleDiff\": 89.9," + + "\"minHighAngleDiff\": 90.1," + "\"maxHighAngleDiff\": 100.0" + "}," + + "\"angleCounts\": {" + "\"min\": 4.0," + "\"max\": 16.0}}}")); + + @Test + public void needsSquaredAngleTruePositive() + { + this.verifier.actual(this.setup.needsSquaredAngleTruePositive(), this.check); + this.verifier.verifyExpectedSize(1); + } + + @Test + public void overSixteenAnglesFalsePositive() + { + this.verifier.actual(this.setup.overSixteenAnglesFalsePositive(), this.check); + this.verifier.verifyEmpty(); + } + + @Test + public void squaredAngleFalsePositive() + { + this.verifier.actual(this.setup.squaredAngleFalsePositive(), this.check); + this.verifier.verifyEmpty(); + } + + @Test + public void underFourAnglesFalsePositive() + { + this.verifier.actual(this.setup.underFourAnglesFalsePositive(), this.check); + this.verifier.verifyEmpty(); + } + +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheckTestRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheckTestRule.java new file mode 100644 index 000000000..cf8b479a4 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/areas/ConcerningAngleBuildingCheckTestRule.java @@ -0,0 +1,101 @@ +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.Loc; + +/** + * Tests for {@link ConcerningAngleBuildingCheck} + * + * @author v-garei + */ +public class ConcerningAngleBuildingCheckTestRule extends CoreTestRule +{ + + private static final String POLYGON_NODE_1 = "47.8037110, -122.2112443"; + private static final String POLYGON_NODE_2 = "47.8037110, -122.2111731"; + private static final String POLYGON_NODE_3 = "47.8036180, -122.2111731"; + private static final String POLYGON_NODE_4 = "47.8036180, -122.2112443"; + + private static final String POLYGON_2_NODE_1 = "-27.5859601, 151.9420256"; + private static final String POLYGON_2_NODE_2 = "-27.5859912, 151.9422828"; + private static final String POLYGON_2_NODE_3 = "-27.5861153, 151.9422659"; + private static final String POLYGON_2_NODE_4 = "-27.5860886, 151.9420075"; + + private static final String POLYGON_3_NODE_1 = "47.8036993, -122.2116721"; + private static final String POLYGON_3_NODE_2 = "47.8037000, -122.2116279"; + private static final String POLYGON_3_NODE_3 = "47.8037049, -122.2116281"; + private static final String POLYGON_3_NODE_4 = "47.8037057, -122.2115764"; + private static final String POLYGON_3_NODE_5 = "47.8037142, -122.2115767"; + private static final String POLYGON_3_NODE_6 = "47.8037161, -122.2114610"; + private static final String POLYGON_3_NODE_7 = "47.8037069, -122.2114607"; + private static final String POLYGON_3_NODE_8 = "47.8037078, -122.2114072"; + private static final String POLYGON_3_NODE_9 = "47.8037023, -122.2114070"; + private static final String POLYGON_3_NODE_10 = "47.8037030, -122.2113641"; + private static final String POLYGON_3_NODE_11 = "47.8036220, -122.2113612"; + private static final String POLYGON_3_NODE_12 = "47.8036215, -122.2113906"; + private static final String POLYGON_3_NODE_13 = "47.8036088, -122.2113901"; + private static final String POLYGON_3_NODE_14 = "47.8036094, -122.2113492"; + private static final String POLYGON_3_NODE_15 = "47.8035703, -122.2113478"; + private static final String POLYGON_3_NODE_16 = "47.8035701, -122.2113626"; + private static final String POLYGON_3_NODE_17 = "47.8035304, -122.2113612"; + private static final String POLYGON_3_NODE_18 = "47.8035297, -122.2114031"; + private static final String POLYGON_3_NODE_19 = "47.8034664, -122.2114008"; + private static final String POLYGON_3_NODE_20 = "47.8034631, -122.2116262"; + private static final String POLYGON_3_NODE_21 = "47.8036192, -122.2116387"; + private static final String POLYGON_3_NODE_22 = "47.8036187, -122.2116692"; + + @TestAtlas(areas = { @Area(coordinates = { @Loc(value = POLYGON_2_NODE_1), + @Loc(value = POLYGON_2_NODE_2), @Loc(value = POLYGON_2_NODE_3), + @Loc(value = POLYGON_2_NODE_4) }, tags = "building=yes") }) + + private Atlas needsSquaredAngleTruePositive; + + @TestAtlas(areas = { @Area(coordinates = { @Loc(value = POLYGON_3_NODE_1), + @Loc(value = POLYGON_3_NODE_2), @Loc(value = POLYGON_3_NODE_3), + @Loc(value = POLYGON_3_NODE_4), @Loc(value = POLYGON_3_NODE_5), + @Loc(value = POLYGON_3_NODE_6), @Loc(value = POLYGON_3_NODE_7), + @Loc(value = POLYGON_3_NODE_8), @Loc(value = POLYGON_3_NODE_9), + @Loc(value = POLYGON_3_NODE_10), @Loc(value = POLYGON_3_NODE_11), + @Loc(value = POLYGON_3_NODE_12), @Loc(value = POLYGON_3_NODE_13), + @Loc(value = POLYGON_3_NODE_14), @Loc(value = POLYGON_3_NODE_15), + @Loc(value = POLYGON_3_NODE_16), @Loc(value = POLYGON_3_NODE_17), + @Loc(value = POLYGON_3_NODE_18), @Loc(value = POLYGON_3_NODE_19), + @Loc(value = POLYGON_3_NODE_20), @Loc(value = POLYGON_3_NODE_21), + @Loc(value = POLYGON_3_NODE_22) }, tags = "building=yes") }) + + private Atlas overSixteenAnglesFalsePositive; + + @TestAtlas(areas = { @Area(coordinates = { @Loc(value = POLYGON_NODE_1), + @Loc(value = POLYGON_NODE_2), @Loc(value = POLYGON_NODE_3), + @Loc(value = POLYGON_NODE_4) }, tags = "building=yes") }) + + private Atlas squaredAngleFalsePositive; + + @TestAtlas(areas = { @Area(coordinates = { @Loc(value = POLYGON_NODE_1), + @Loc(value = POLYGON_NODE_2), @Loc(value = POLYGON_NODE_3) }, tags = "building=yes") }) + + private Atlas underFourAnglesFalsePositive; + + public Atlas needsSquaredAngleTruePositive() + { + return this.needsSquaredAngleTruePositive; + } + + public Atlas overSixteenAnglesFalsePositive() + { + return this.overSixteenAnglesFalsePositive; + } + + public Atlas squaredAngleFalsePositive() + { + return this.squaredAngleFalsePositive; + } + + public Atlas underFourAnglesFalsePositive() + { + return this.underFourAnglesFalsePositive; + } +}