Skip to content

Commit

Permalink
Concerning angle building check (osmlab#452)
Browse files Browse the repository at this point in the history
* 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!
  • Loading branch information
reichg authored Dec 30, 2020
1 parent 56533dc commit 6e623b8
Show file tree
Hide file tree
Showing 6 changed files with 449 additions and 0 deletions.
18 changes: 18 additions & 0 deletions config/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions docs/available_checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
40 changes: 40 additions & 0 deletions docs/checks/concerningAngleBuildingCheck.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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<Long>
{
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<String> 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<CheckFlag> flag(final AtlasObject object)
{
markAsFlagged(object.getOsmIdentifier());
final Set<Polygon> 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<String> 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<Segment> 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<Heading> segmentOneHeading = segment1.heading();
final Optional<Heading> 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<Polygon> 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<Segment> 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<Polygon> toPolygon(final RelationMember member)
{
if (member.getEntity().getType().equals(ItemType.AREA))
{
return Optional.of(((Area) member.getEntity()).asPolygon());
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Loading

0 comments on commit 6e623b8

Please sign in to comment.