Skip to content

Commit

Permalink
Overlapping AOI Polygon Check (osmlab#58)
Browse files Browse the repository at this point in the history
* initial commit - intersection logic & default configurable aoi tags

* Created tag combination test.

* Reworked to use one configurable filter list and give one instruction

* Added config and unit tests

* Added overlappingAOIPolygonCheck.md, changed configurable name

* Removed PARK from some default AOI tag filters

* changed unit test/rule names

* correct typo

* code clean up

* changed validCheckForObject ordering; added/updated comments

* code clean up

* added minimum overlap unit test

* update docs

* refactored hasMinimumOverlapProportion into a utility

* fix function name

* move utilities to more global location

* intersectionPercentage unit tests
  • Loading branch information
Bentleysb authored and jklamer committed Jul 16, 2018
1 parent 5ef6488 commit 5254ae4
Show file tree
Hide file tree
Showing 7 changed files with 648 additions and 0 deletions.
16 changes: 16 additions & 0 deletions config/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,22 @@
"difficulty": "HARD"
}
},
"OverlappingAOIPolygonCheck":{
"aoi.tags.filters": ["amenity->FESTIVAL_GROUNDS", "amenity->GRAVE_YARD|landuse->CEMETERY",
"boundary->NATIONAL_PARK,PROTECTED_AREA|leisure->NATURE_RESERVE,PARK",
"historic->BATTLEFIELD", "landuse->FOREST|natural->WOOD",
"landuse->RECREATION_GROUND|leisure->RECREATION_GROUND",
"landuse->VILLAGE_GREEN|leisure->PARK", "leisure->GARDEN",
"leisure->GOLF_COURSE|sport->GOLF", "leisure->PARK&name->*", "natural->BEACH",
"tourism->ZOO"],
"intersect.minimum.limit":0.01,
"challenge": {
"description": "Tasks containing overlapping AOI ways",
"blurb": "Overlapping AOI Polygon",
"instruction": "Correct the areas as necessary to not have overlapping ways representing the same AOI",
"difficulty": "HARD"
}
},
"RoundaboutClosedLoopCheck": {
"challenge": {
"description": "A roundabout should be formed by one-way edges with no dead-end nodes.",
Expand Down
161 changes: 161 additions & 0 deletions docs/checks/overlappingAOIPolygonCheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Overlapping AOI Polygon Check

This check flags polygons that overlap and represent the same Area of Interest (AOI).

AOIs are defined through the configurable value `aoi.tags.filters`. This is a list of tag filters. If a polygon has any of the tags in any of the filters, it is considered an AOI.

The defaults AOI tag filters are:

* `amenity->FESTIVAL_GROUNDS`
* `amenity->GRAVE_YARD|landuse->CEMETERY`
* `boundary->NATIONAL_PARK,PROTECTED_AREA|leisure->NATURE_RESERVE,PARK`
* `historic->BATTLEFIELD`
* `landuse->FOREST|natural->WOOD`
* `landuse->RECREATION_GROUND|leisure->RECREATION_GROUND`
* `landuse->VILLAGE_GREEN|leisure->PARK`
* `leisure->GARDEN`
* `leisure->GOLF_COURSE|sport->GOLF`
* `leisure->PARK&name->*`
* `natural->BEACH`
* `tourism->ZOO`

#### Live Examples

1. The way [id:99881325](https://www.openstreetmap.org/way/99881325) overlaps way [id:173830769](https://www.openstreetmap.org/way/173830769) and they are both tagged with `leisure=PARK`.
2. The way [id:54177792](https://www.openstreetmap.org/way/54177792), with tag `landuse=FOREST`, overlaps way [id:338963545](https://www.openstreetmap.org/way/338963545), with similar tag `natural=WOOD`.

#### 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).

First we check if the object is an Area and it has a tag that identifies it as an AOI.

```java
@Override
public boolean validCheckForObject(final AtlasObject object)
{
// Checks for areas, that are not flagged, and pass any of the TaggableFilters in
// aoiFilters. aoiFiltersTest() and aoiFilters is used in place of a single TaggableFilter
// so that each filter may be tested separately later.
return object instanceof Area && !this.isFlagged(object.getIdentifier())
&& aoiFiltersTest(object);
}
```

The AOI tag test is performed by looping through all the tag filters in the configurable value.

```java
private boolean aoiFiltersTest(final AtlasObject object)
{
return this.aoiFilters.stream().anyMatch(filter -> filter.test(object));
}
```

Then the Areas that overlap the initial Area are found, and tested to see if the minimum overlap is met (configurable) and if they have the same or similar AOI tag.

```java
@Override
protected Optional<CheckFlag> flag(final AtlasObject object)
{
final Area aoi = (Area) object;
final Polygon aoiPolygon = aoi.asPolygon();
final Rectangle aoiBounds = aoiPolygon.bounds();
boolean hasOverlap = false;

// Set of overlapping area AOIs
final Set<Area> overlappingAreas = Iterables
.stream(object.getAtlas().areasIntersecting(aoiBounds,
area -> area.getIdentifier() != aoi.getIdentifier()
&& !this.isFlagged(area.getIdentifier())
&& area.intersects(aoiPolygon) && aoiFiltersTest(area)))
.collectToSet();

final CheckFlag flag = new CheckFlag(this.getTaskIdentifier(object));
flag.addObject(object);

// Test each overlapping AOI to see if it overlaps enough and passes the same AOI filter as
// the object
for (final Area area : overlappingAreas)
{
if (this.hasMinimumOverlapProportion(aoiPolygon, area.asPolygon())
&& aoiFiltersTest(object, area))
{
flag.addObject(area);
flag.addInstruction(this.getLocalizedInstruction(0, object.getOsmIdentifier(),
area.getOsmIdentifier()));
this.markAsFlagged(area.getIdentifier());
hasOverlap = true;
}
}

if (hasOverlap)
{
this.markAsFlagged(object.getIdentifier());
return Optional.of(flag);
}

return Optional.empty();
}
```

The minimum overlap test uses the configurable double value `intersect.minimum.limit` and compares the areas by clipping one against the other.

```java
private boolean hasMinimumOverlapProportion(final Polygon polygon, final Polygon otherPolygon)
{

Clip clip = null;
try
{
clip = polygon.clip(otherPolygon, Clip.ClipType.AND);
}
catch (final TopologyException e)
{
System.out
.println(String.format("Error clipping [%s] and [%s].", polygon, otherPolygon));
}

// Skip if nothing is returned
if (clip == null)
{
return false;
}

// Sum intersection area
long intersectionArea = 0;
for (final PolyLine polyline : clip.getClip())
{
if (polyline != null && polyline instanceof Polygon)
{
final Polygon clippedPolygon = (Polygon) polyline;
intersectionArea += clippedPolygon.surface().asDm7Squared();
}
}

// Avoid division by zero
if (intersectionArea == 0)
{
return false;
}

// Pick the smaller building's area as baseline
final long baselineArea = Math.min(polygon.surface().asDm7Squared(),
otherPolygon.surface().asDm7Squared());
final double proportion = (double) intersectionArea / baselineArea;

return proportion >= this.minimumIntersect;
}
```

The similar AOI tag test loops through the AOI filters and finds the one that the initial Area matches and tests it against the overlapping Area.

```java
private boolean aoiFiltersTest(final AtlasObject object, final Area area)
{
return this.aoiFilters.stream()
.anyMatch(filter -> filter.test(object) && filter.test(area));
}
```

To learn more about the code, please look at the comments in the source code for the check.
[OverlappingAOIPolygonCheck](../../src/main/java/org/openstreetmap/atlas/checks/validation/areas/OverlappingAOIPolygonCheck.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.openstreetmap.atlas.checks.utility;

import org.openstreetmap.atlas.geography.PolyLine;
import org.openstreetmap.atlas.geography.Polygon;
import org.openstreetmap.atlas.geography.clipping.Clip;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vividsolutions.jts.geom.TopologyException;

/**
* A set of utilities that are common among intersection checks.
*
* @author bbreithaupt
*/
public final class IntersectionUtilities
{
private static final Logger logger = LoggerFactory.getLogger(IntersectionUtilities.class);

private IntersectionUtilities()
{
}

/**
* Find the percentage of overlap for given {@link Polygon}s.
*
* @param polygon
* {@link Polygon} to check for intersection
* @param otherPolygon
* Another {@link Polygon} to check against for intersection
* @return percentage of overlap as a double; 0 if unable to clip
*/
public static double findIntersectionPercentage(final Polygon polygon,
final Polygon otherPolygon)
{
Clip clip = null;
try
{
clip = polygon.clip(otherPolygon, Clip.ClipType.AND);
}
catch (final TopologyException e)
{
logger.warn(String.format("Skipping intersection check. Error clipping [%s] and [%s].",
polygon, otherPolygon), e);
}

// Skip if nothing is returned
if (clip == null)
{
return 0.0;
}

// Sum intersection area
long intersectionArea = 0;
for (final PolyLine polyline : clip.getClip())
{
if (polyline != null && polyline instanceof Polygon)
{
final Polygon clippedPolygon = (Polygon) polyline;
intersectionArea += clippedPolygon.surface().asDm7Squared();
}
}

// Avoid division by zero
if (intersectionArea == 0)
{
return 0.0;
}

// Pick the smaller building's area as baseline
final long baselineArea = Math.min(polygon.surface().asDm7Squared(),
otherPolygon.surface().asDm7Squared());
return (double) intersectionArea / baselineArea;
}
}
Loading

0 comments on commit 5254ae4

Please sign in to comment.