forked from osmlab/atlas-checks
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Overlapping AOI Polygon Check (osmlab#58)
* 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
Showing
7 changed files
with
648 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
75 changes: 75 additions & 0 deletions
75
src/main/java/org/openstreetmap/atlas/checks/utility/IntersectionUtilities.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.