diff --git a/config/configuration.json b/config/configuration.json index f2ce447af..52e6096e3 100644 --- a/config/configuration.json +++ b/config/configuration.json @@ -114,6 +114,14 @@ } } }, + "BoundaryIntersectionCheck": { + "challenge": { + "description": "Task contains Boundaries information describing their intersection", + "blurb": "Boundaries Intersecting", + "instruction": "Open your favorite editor, check the instruction and modify the points/ways that are causing boundaries to intersect.", + "difficulty": "NORMAL" + } + }, "BuildingRoadIntersectionCheck": { "car.navigable": true, "challenge": { diff --git a/docs/available_checks.md b/docs/available_checks.md index 69d9b09dd..0ca45df35 100644 --- a/docs/available_checks.md +++ b/docs/available_checks.md @@ -63,6 +63,7 @@ This document is a list of tables with a description and link to documentation f ## Relations | Check Name | Check Description | | :--------- | :---------------- | +| BoundaryIntersectionCheck | This check is designed to scan relations marked as boundaries or with ways marked as boundaries and flag them for intersections with other boundaries of the same type. | | InvalidMultiPolygonRelationCheck | This check is designed to scan through MultiPolygon relations and flag them for invalid geometry. | | [InvalidTurnRestrictionCheck](checks/invalidTurnRestrictionCheck.md) | The purpose of this check is to identify invalid turn restrictions in OSM. Invalid turn restrictions occur in a variety of ways from invalid members, Edge geometry issues, not being routable, or wrong topology. | | [MissingRelationType](checks/missingRelationType.md) | The purpose of this check is to identify Relations without relation type. | diff --git a/src/main/java/org/openstreetmap/atlas/checks/utility/RelationIntersections.java b/src/main/java/org/openstreetmap/atlas/checks/utility/RelationIntersections.java new file mode 100644 index 000000000..f87f36d3f --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/utility/RelationIntersections.java @@ -0,0 +1,37 @@ +package org.openstreetmap.atlas.checks.utility; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.openstreetmap.atlas.geography.atlas.items.LineItem; +import org.openstreetmap.atlas.geography.atlas.items.Relation; + +/** + * @author srachanski + */ +public class RelationIntersections +{ + + private final Map>> intersections = new HashMap<>(); + + public void addIntersection(final Relation relation, final LineItem lineItem) + { + this.intersections.computeIfAbsent(relation, k -> new HashMap<>()); + final long osmIdentifier = lineItem.getOsmIdentifier(); + this.intersections.get(relation).computeIfAbsent(osmIdentifier, k -> new HashSet<>()); + this.intersections.get(relation).get(osmIdentifier).add(lineItem); + } + + public Map> getLineItemMap(final Relation relation) + { + return this.intersections.get(relation); + } + + public Set getRelations() + { + return this.intersections.keySet(); + } + +} diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheck.java b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheck.java new file mode 100644 index 000000000..38f2e1108 --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheck.java @@ -0,0 +1,402 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang.StringUtils; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; +import org.openstreetmap.atlas.checks.base.BaseCheck; +import org.openstreetmap.atlas.checks.flag.CheckFlag; +import org.openstreetmap.atlas.geography.atlas.items.Area; +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity; +import org.openstreetmap.atlas.geography.atlas.items.AtlasObject; +import org.openstreetmap.atlas.geography.atlas.items.ItemType; +import org.openstreetmap.atlas.geography.atlas.items.LineItem; +import org.openstreetmap.atlas.geography.atlas.items.Relation; +import org.openstreetmap.atlas.geography.atlas.items.RelationMember; +import org.openstreetmap.atlas.geography.atlas.items.RelationMemberList; +import org.openstreetmap.atlas.geography.atlas.multi.MultiLine; +import org.openstreetmap.atlas.geography.atlas.multi.MultiRelation; +import org.openstreetmap.atlas.tags.BoundaryTag; +import org.openstreetmap.atlas.tags.RelationTypeTag; +import org.openstreetmap.atlas.tags.annotations.validation.Validators; +import org.openstreetmap.atlas.utilities.configuration.Configuration; + +/** + * @author srachanski + */ +public class BoundaryIntersectionCheck extends BaseCheck +{ + + private static final String DELIMITER = ", "; + private static final int INDEX = 0; + private static final String TYPE = "type"; + private static final String BOUNDARY = "boundary"; + private static final String INVALID_BOUNDARY_FORMAT = "Boundary {0} with way {1} is crossing invalidly with boundary {2} with way {3} at coordinates {4}."; + private static final String INSTRUCTION_FORMAT = INVALID_BOUNDARY_FORMAT + + " Two boundaries should not intersect each other."; + private static final List FALLBACK_INSTRUCTIONS = Arrays.asList(INSTRUCTION_FORMAT, + INVALID_BOUNDARY_FORMAT); + + private static boolean isRelationTypeBoundaryWithBoundaryTag(final AtlasObject object) + { + return Validators.isOfType(object, RelationTypeTag.class, RelationTypeTag.BOUNDARY) + && Validators.hasValuesFor(object, BoundaryTag.class); + } + + public BoundaryIntersectionCheck(final Configuration configuration) + { + super(configuration); + } + + @Override + public boolean validCheckForObject(final AtlasObject object) + { + return object instanceof Relation && isRelationTypeBoundaryWithBoundaryTag(object); + } + + @Override + protected Optional flag(final AtlasObject object) + { + return this.processRelation(object); + } + + @Override + protected List getFallbackInstructions() + { + return FALLBACK_INSTRUCTIONS; + } + + private void addInstruction(final Set instructions, final BoundaryPart lineItem, + final long osmIdentifier, final Coordinate[] intersectingPoints, + final String firstBoundaries, final String secondBoundaries) + { + final String instruction = this.getLocalizedInstruction(INDEX, firstBoundaries, + Long.toString(lineItem.getOsmIdentifier()), secondBoundaries, + Long.toString(osmIdentifier), this.coordinatesToList(intersectingPoints)); + instructions.add(instruction); + } + + private boolean checkAreaAsBoundary(final Area area, final Set boundaryTags) + { + return area.relations().stream() + .anyMatch(relationToCheck -> BoundaryIntersectionCheck + .isRelationTypeBoundaryWithBoundaryTag(relationToCheck) + && boundaryTags.contains(relationToCheck.getTag(BOUNDARY).get())) + || boundaryTags.contains(area.getTag(BOUNDARY).orElse("")); + } + + private boolean checkLineItemAsBoundary(final LineItem lineItem, final Set boundaryTags) + { + return lineItem.relations().stream() + .anyMatch(relationToCheck -> BoundaryIntersectionCheck + .isRelationTypeBoundaryWithBoundaryTag(relationToCheck) + && boundaryTags.contains(relationToCheck.getTag(BOUNDARY).get())) + || boundaryTags.contains(lineItem.getTag(BOUNDARY).orElse("")); + } + + private String coordinatesToList(final Coordinate[] locations) + { + return Stream.of(locations) + .map(coordinate -> String.format("(%s, %s)", coordinate.getX(), coordinate.getY())) + .collect(Collectors.joining(DELIMITER)); + } + + private Set getBoundaries(final LineItem currentLineItem) + { + final Set relations = currentLineItem.relations().stream() + .filter(relation -> relation instanceof MultiRelation).map(AtlasEntity::relations) + .flatMap(Collection::stream).collect(Collectors.toSet()); + relations.addAll(currentLineItem.relations()); + return relations.stream() + .filter(BoundaryIntersectionCheck::isRelationTypeBoundaryWithBoundaryTag) + .collect(Collectors.toSet()); + } + + private Set getBoundaries(final Area area) + { + final Set relations = area.relations().stream() + .filter(relation -> relation instanceof MultiRelation).map(AtlasEntity::relations) + .flatMap(Collection::stream).collect(Collectors.toSet()); + relations.addAll(area.relations()); + return relations.stream() + .filter(BoundaryIntersectionCheck::isRelationTypeBoundaryWithBoundaryTag) + .collect(Collectors.toSet()); + } + + private Set getBoundaryParts(final Relation relation) + { + final RelationMemberList relationMemberLineItems = relation.membersOfType(ItemType.EDGE, + ItemType.LINE, ItemType.AREA); + return relationMemberLineItems.stream().map(RelationMember::getEntity).map(entity -> + { + final String tag = entity.getTags().get(BOUNDARY); + final Set boundaryTags = entity.relations().stream() + .filter(currentRelation -> BOUNDARY.equals(currentRelation.getTag(TYPE).get())) + .map(currentRelation -> currentRelation.getTag(BOUNDARY) + .orElse(StringUtils.EMPTY)) + .collect(Collectors.toSet()); + boundaryTags.add(tag); + boundaryTags.remove(StringUtils.EMPTY); + return new BoundaryPart(entity, boundaryTags); + }).collect(Collectors.toSet()); + } + + private Geometry getGeometryForIntersection(final String wktFirst) throws ParseException + { + + final WKTReader wktReader = new WKTReader(); + Geometry geometry1 = wktReader.read(wktFirst); + if (geometry1.getGeometryType().equals(Geometry.TYPENAME_POLYGON)) + { + geometry1 = geometry1.getBoundary(); + } + return geometry1; + } + + private Coordinate[] getIntersectionPoints(final String wktFirst, final String wktSecond) + { + try + { + final Geometry geometry1 = this.getGeometryForIntersection(wktFirst); + final Geometry geometry2 = this.getGeometryForIntersection(wktSecond); + return geometry1.intersection(geometry2).getCoordinates(); + } + catch (final ParseException e) + { + throw new IllegalStateException(e); + } + } + + private List getLineItems(final LineItem lineItem) + { + if (lineItem instanceof MultiLine) + { + final List lines = new ArrayList<>(); + ((MultiLine) lineItem).getSubLines().forEach(lines::add); + return lines; + } + return Collections.singletonList(lineItem); + } + + private Predicate getPredicateForAreaSelection(final BoundaryPart boundaryPart, + final Set boundaryTags) + { + return areaToCheck -> + { + if (this.checkAreaAsBoundary(areaToCheck, boundaryTags)) + { + return this.isCrossingNotTouching(boundaryPart.getWktGeometry(), + areaToCheck.toWkt()); + } + return false; + }; + } + + private Predicate getPredicateForLineItemsSelection(final BoundaryPart boundaryPart, + final Set boundaryTags) + { + return lineItemToCheck -> + { + if (this.checkLineItemAsBoundary(lineItemToCheck, boundaryTags)) + { + return this.isCrossingNotTouching(boundaryPart.getWktGeometry(), + lineItemToCheck.toWkt()); + } + return false; + }; + } + + private Map getRelationMap(final AtlasObject object) + { + final Map tagToRelation = new HashMap<>(); + if (object instanceof MultiRelation) + { + ((MultiRelation) object).relations().stream() + .filter(BoundaryIntersectionCheck::isRelationTypeBoundaryWithBoundaryTag) + .forEach(relation -> tagToRelation.put(relation.getTag(BOUNDARY).get(), + relation)); + } + tagToRelation.put(object.getTag(BOUNDARY).get(), (Relation) object); + return tagToRelation; + } + + private boolean isAnyGeometryInvalid(final Geometry geometry1, final Geometry geometry2) + { + return !geometry1.isValid() || !geometry1.isSimple() || !geometry2.isValid() + || !geometry2.isSimple(); + } + + private boolean isCrossingNotTouching(final String wktFirst, final String wktSecond) + { + final WKTReader wktReader = new WKTReader(); + try + { + final Geometry geometry1 = wktReader.read(wktFirst); + final Geometry geometry2 = wktReader.read(wktSecond); + if (geometry1.equals(geometry2)) + { + return false; + } + if (this.isAnyGeometryInvalid(geometry1, geometry2)) + { + return false; + } + return this.isIntersectingNotTouching(geometry1, geometry2); + } + catch (final ParseException e) + { + return false; + } + } + + private boolean isGeometryPairOfLineType(final Geometry lineString, final Geometry lineString2) + { + return lineString.getGeometryType().equals(Geometry.TYPENAME_LINESTRING) + && lineString2.getGeometryType().equals(Geometry.TYPENAME_LINESTRING); + } + + private boolean isIntersectingNotTouching(final Geometry geometry1, final Geometry geometry2) + { + return geometry1.intersects(geometry2) + && (geometry1.crosses(geometry2) || (geometry1.overlaps(geometry2) + && !this.isGeometryPairOfLineType(geometry1, geometry2))); + } + + private String objectsToString(final Set objects) + { + return objects.stream().map(object -> Long.toString(object.getOsmIdentifier())) + .collect(Collectors.joining(DELIMITER)); + } + + private void processAreas(final RelationBoundary relationBoundary, + final Set instructions, final Set objectsToFlag, + final Set matchedTags, final BoundaryPart currentBoundaryPart, + final Iterable areasIntersecting, final Set currentMatchedTags) + { + areasIntersecting.forEach(area -> + { + final Set matchingBoundaries = this.getBoundaries(area).stream() + .filter(boundary -> relationBoundary.getTagToRelation() + .containsKey(boundary.getTag(BOUNDARY).orElse(StringUtils.EMPTY))) + .filter(boundary -> !relationBoundary + .containsRelationId(boundary.getOsmIdentifier())) + .collect(Collectors.toSet()); + if (!matchingBoundaries.isEmpty()) + { + currentMatchedTags.addAll(matchingBoundaries.stream() + .map(relation -> relation.getTag(BOUNDARY)).filter(Optional::isPresent) + .map(Optional::get).collect(Collectors.toSet())); + objectsToFlag.addAll(matchingBoundaries); + final Coordinate[] intersectingPoints = this + .getIntersectionPoints(currentBoundaryPart.getWktGeometry(), area.toWkt()); + final String firstBoundaries = this.objectsToString( + relationBoundary.getRelationsByBoundaryTags(currentMatchedTags)); + final String secondBoundaries = this.objectsToString(matchingBoundaries); + if (intersectingPoints.length != 0 + && firstBoundaries.hashCode() < secondBoundaries.hashCode()) + { + this.addInstruction(instructions, currentBoundaryPart, area.getOsmIdentifier(), + intersectingPoints, firstBoundaries, secondBoundaries); + } + } + matchedTags.addAll(currentMatchedTags); + }); + } + + private void processLineItems(final RelationBoundary relationBoundary, + final Set instructions, final Set objectsToFlag, + final Set matchedTags, final BoundaryPart currentBoundaryPart, + final Iterable lineItemsIntersecting, final Set currentMatchedTags) + { + lineItemsIntersecting.forEach(lineItem -> + { + final Set matchingBoundaries = this.getBoundaries(lineItem).stream() + .filter(boundary -> relationBoundary.getTagToRelation() + .containsKey(boundary.getTag(BOUNDARY).get())) + .filter(boundary -> !relationBoundary + .containsRelationId(boundary.getOsmIdentifier())) + .collect(Collectors.toSet()); + if (!matchingBoundaries.isEmpty()) + { + currentMatchedTags.addAll(matchingBoundaries.stream() + .map(relation -> relation.getTag(BOUNDARY)).filter(Optional::isPresent) + .map(Optional::get).collect(Collectors.toSet())); + objectsToFlag.addAll(matchingBoundaries); + + final List lineItems = this.getLineItems(lineItem); + lineItems.forEach(line -> + { + objectsToFlag.add(line); + final Coordinate[] intersectingPoints = this.getIntersectionPoints( + currentBoundaryPart.getWktGeometry(), line.toWkt()); + final String firstBoundaries = this.objectsToString( + relationBoundary.getRelationsByBoundaryTags(currentMatchedTags)); + final String secondBoundaries = this.objectsToString(matchingBoundaries); + if (intersectingPoints.length != 0 + && firstBoundaries.hashCode() < secondBoundaries.hashCode()) + { + this.addInstruction(instructions, currentBoundaryPart, + line.getOsmIdentifier(), intersectingPoints, firstBoundaries, + secondBoundaries); + } + }); + } + matchedTags.addAll(currentMatchedTags); + }); + } + + private Optional processRelation(final AtlasObject object) + { + final Map tagToRelation = this.getRelationMap(object); + final RelationBoundary relationBoundary = new RelationBoundary(tagToRelation, + this.getBoundaryParts((Relation) object)); + final Set instructions = new HashSet<>(); + final Set objectsToFlag = new HashSet<>(); + final Set matchedTags = new HashSet<>(); + for (final BoundaryPart currentBoundaryPart : relationBoundary.getBoundaryParts()) + { + final Iterable lineItemsIntersecting = object.getAtlas() + .lineItemsIntersecting(currentBoundaryPart.getBounds(), + this.getPredicateForLineItemsSelection(currentBoundaryPart, + relationBoundary.getTagToRelation().keySet())); + final Iterable areasIntersecting = object.getAtlas().areasIntersecting( + currentBoundaryPart.getBounds(), this.getPredicateForAreaSelection( + currentBoundaryPart, relationBoundary.getTagToRelation().keySet())); + final Set currentMatchedTags = new HashSet<>(); + this.processLineItems(relationBoundary, instructions, objectsToFlag, matchedTags, + currentBoundaryPart, lineItemsIntersecting, currentMatchedTags); + this.processAreas(relationBoundary, instructions, objectsToFlag, matchedTags, + currentBoundaryPart, areasIntersecting, currentMatchedTags); + + objectsToFlag.addAll(relationBoundary.getRelationsByBoundaryTags(matchedTags)); + objectsToFlag.add(currentBoundaryPart.getAtlasEntity()); + } + if (instructions.isEmpty()) + { + return Optional.empty(); + } + else + { + final CheckFlag checkFlag = new CheckFlag(this.getTaskIdentifier(object)); + instructions.forEach(checkFlag::addInstruction); + checkFlag.addObjects(objectsToFlag); + return Optional.of(checkFlag); + } + } + +} diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryPart.java b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryPart.java new file mode 100644 index 000000000..56dacf83a --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryPart.java @@ -0,0 +1,47 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import java.util.Set; + +import org.openstreetmap.atlas.geography.Rectangle; +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity; + +/** + * @author srachanski + */ +public class BoundaryPart +{ + + private final Set boundaryTags; + private final AtlasEntity atlasEntity; + + public BoundaryPart(final AtlasEntity entity, final Set boundaryTags) + { + this.atlasEntity = entity; + this.boundaryTags = boundaryTags; + } + + public AtlasEntity getAtlasEntity() + { + return this.atlasEntity; + } + + public Set getBoundaryTags() + { + return this.boundaryTags; + } + + public Rectangle getBounds() + { + return this.atlasEntity.bounds(); + } + + public long getOsmIdentifier() + { + return this.atlasEntity.getOsmIdentifier(); + } + + public String getWktGeometry() + { + return this.atlasEntity.toWkt(); + } +} diff --git a/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/RelationBoundary.java b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/RelationBoundary.java new file mode 100644 index 000000000..6d938c290 --- /dev/null +++ b/src/main/java/org/openstreetmap/atlas/checks/validation/intersections/RelationBoundary.java @@ -0,0 +1,46 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.openstreetmap.atlas.geography.atlas.items.Relation; + +/** + * @author srachanski + */ +public class RelationBoundary +{ + + private final Map tagToRelation; + private final Set boundaryParts; + + public RelationBoundary(final Map tagToRelation, + final Set boundaryParts) + { + this.tagToRelation = tagToRelation; + this.boundaryParts = boundaryParts; + } + + public boolean containsRelationId(final long osmIdentifier) + { + return this.tagToRelation.values().stream().map(Relation::getOsmIdentifier) + .anyMatch(id -> id == osmIdentifier); + } + + public Set getBoundaryParts() + { + return this.boundaryParts; + } + + public Set getRelationsByBoundaryTags(final Set tags) + { + return this.tagToRelation.keySet().stream().filter(tags::contains) + .map(this.tagToRelation::get).collect(Collectors.toSet()); + } + + public Map getTagToRelation() + { + return this.tagToRelation; + } +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheckTest.java b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheckTest.java new file mode 100644 index 000000000..2f90a9e3b --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheckTest.java @@ -0,0 +1,114 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.openstreetmap.atlas.checks.configuration.ConfigurationResolver; +import org.openstreetmap.atlas.checks.validation.verifier.ConsumerBasedExpectedCheckVerifier; +import org.openstreetmap.atlas.utilities.configuration.Configuration; + +/** + * @author srachanski + */ +public class BoundaryIntersectionCheckTest +{ + + @Rule + public BoundaryIntersectionCheckTestRule setup = new BoundaryIntersectionCheckTestRule(); + + @Rule + public ConsumerBasedExpectedCheckVerifier verifier = new ConsumerBasedExpectedCheckVerifier(); + + private final Configuration configuration = ConfigurationResolver.emptyConfiguration(); + + @Test + public void testInvalidThreeCrossingItemsAtlas() + { + this.verifier.actual(this.setup.crossingBoundariesTwoAreasIntersectOneOther(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(2, flags.size())); + } + + @Test + public void testInvalidTwoCrossingItemsAtlas() + { + this.verifier.actual(this.setup.crossingBoundariesTwoAreasIntersectEachOther(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(1, flags.size())); + this.verifier.verify(flag -> + { + Assert.assertEquals(4, flag.getFlaggedObjects().size()); + Assert.assertEquals(1, flag.getInstructions().split("\n").length); + }); + } + + @Test + public void testInvalidTwoCrossingItemsWithEdgesAtlas() + { + this.verifier.actual(this.setup.crossingBoundariesTwoAreasIntersectEachOtherWithEdges(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(1, flags.size())); + this.verifier.verify(flag -> + { + Assert.assertEquals(4, flag.getFlaggedObjects().size()); + Assert.assertEquals(1, flag.getInstructions().split("\n").length); + }); + } + + @Test + public void testTouchingObjects() + { + this.verifier.actual(this.setup.boundariesTouchEachOther(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + + @Test + public void testValidCrossingObjectsOneMissingBoundarySpecificTag() + { + this.verifier.actual(this.setup.crossingOneMissingBoundarySpecificTag(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + + @Test + public void testValidCrossingObjectsOneMissingType() + { + this.verifier.actual(this.setup.crossingOneWithWrongType(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + + @Test + public void testValidCrossingObjectsWithDifferentTypes() + { + this.verifier.actual(this.setup.crossingBoundariesWithDifferentTypes(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + + @Test + public void testValidNonCrossingObjects() + { + this.verifier.actual(this.setup.nonCrossingBoundariesTwoSeparate(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + + @Test + public void testValidNonCrossingObjectsOneContainOther() + { + this.verifier.actual(this.setup.nonCrossingOneContainOther(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + + @Test + public void testValidNonCrossingObjectsWithEdges() + { + this.verifier.actual(this.setup.nonCrossingBoundariesTwoSeparateWithEdges(), + new BoundaryIntersectionCheck(this.configuration)); + this.verifier.globallyVerify(flags -> Assert.assertEquals(0, flags.size())); + } + +} diff --git a/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheckTestRule.java b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheckTestRule.java new file mode 100644 index 000000000..feebebdf5 --- /dev/null +++ b/src/test/java/org/openstreetmap/atlas/checks/validation/intersections/BoundaryIntersectionCheckTestRule.java @@ -0,0 +1,352 @@ +package org.openstreetmap.atlas.checks.validation.intersections; + +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.Edge; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Line; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Loc; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Node; +import org.openstreetmap.atlas.utilities.testing.TestAtlas.Relation; + +/** + * {@link BoundaryIntersectionCheckTest} data generator + * + * @author srachanski + */ +public class BoundaryIntersectionCheckTestRule extends CoreTestRule +{ + private static final String COORD_1 = "0, 0"; + private static final String COORD_2 = "0, 2"; + private static final String COORD_3 = "2, 2"; + private static final String COORD_4 = "2, 0"; + private static final String COORD_5 = "0, 3"; + private static final String COORD_6 = "3, 3"; + private static final String COORD_7 = "3, 0"; + private static final String COORD_8 = "0, 1"; + private static final String COORD_9 = "0, 2"; + private static final String COORD_10 = "2, 2"; + private static final String COORD_11 = "2, 1"; + private static final String COORD_12 = "3, 1"; + private static final String COORD_13 = "3, 2"; + private static final String COORD_14 = "5, 2"; + private static final String COORD_15 = "5, 1"; + private static final String COORD_16 = "1, 0"; + private static final String COORD_17 = "1, 3"; + private static final String COORD_18 = "4, 3"; + private static final String COORD_19 = "4, 0"; + private static final String COORD_20 = "1, 1"; + private static final String COORD_21 = "1, 2"; + + private static final String LINE_ONE = "1000001"; + private static final String LINE_TWO = "2000001"; + private static final String LINE_THREE = "3000001"; + private static final String LINE_FOUR = "4000001"; + private static final String LINE_FIVE = "5000001"; + private static final String LINE_SIX = "6000001"; + private static final String LINE_SEVEN = "7000001"; + private static final String LINE_EIGHT = "8000001"; + private static final String LINE_NINE = "9000001"; + + private static final String EDGE_ONE = "11000001"; + private static final String EDGE_TWO = "12000001"; + + private static final String RELATION_ONE = "21000011"; + private static final String RELATION_TWO = "22000011"; + private static final String RELATION_THREE = "23000011"; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_1)), + @Node(coordinates = @Loc(value = COORD_2)), @Node(coordinates = @Loc(value = COORD_3)), + @Node(coordinates = @Loc(value = COORD_4)), @Node(coordinates = @Loc(value = COORD_5)), + @Node(coordinates = @Loc(value = COORD_6)), + @Node(coordinates = @Loc(value = COORD_7)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_2), + @Loc(value = COORD_3), @Loc(value = COORD_4), + @Loc(value = COORD_1) }, id = LINE_ONE), + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_5), + @Loc(value = COORD_6), @Loc(value = COORD_7), + @Loc(value = COORD_1) }, id = LINE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_TWO, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }) }) + private Atlas crossingBoundariesTwoAreasTouchEachOther; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_1)), + @Node(coordinates = @Loc(value = COORD_2)), @Node(coordinates = @Loc(value = COORD_3)), + @Node(coordinates = @Loc(value = COORD_4)), @Node(coordinates = @Loc(value = COORD_5)), + @Node(coordinates = @Loc(value = COORD_6)), + @Node(coordinates = @Loc(value = COORD_7)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_15), + @Loc(value = COORD_3), @Loc(value = COORD_4), + @Loc(value = COORD_1) }, id = LINE_ONE), + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_5), + @Loc(value = COORD_6), + @Loc(value = COORD_1) }, id = LINE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=political" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_TWO, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }) }) + private Atlas crossingBoundariesWithDifferentTypes; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_1)), + @Node(coordinates = @Loc(value = COORD_2)), @Node(coordinates = @Loc(value = COORD_3)), + @Node(coordinates = @Loc(value = COORD_4)), @Node(coordinates = @Loc(value = COORD_5)), + @Node(coordinates = @Loc(value = COORD_6)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_2), + @Loc(value = COORD_3), @Loc(value = COORD_4), + @Loc(value = COORD_1) }, id = LINE_ONE), + @Line(coordinates = { @Loc(value = COORD_20), @Loc(value = COORD_5), + @Loc(value = COORD_6), + @Loc(value = COORD_20) }, id = LINE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_TWO, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }) }) + private Atlas crossingBoundariesTwoAreasIntersectEachOther; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_1)), + @Node(coordinates = @Loc(value = COORD_2)), @Node(coordinates = @Loc(value = COORD_3)), + @Node(coordinates = @Loc(value = COORD_4)), @Node(coordinates = @Loc(value = COORD_5)), + @Node(coordinates = @Loc(value = COORD_6)), + @Node(coordinates = @Loc(value = COORD_20)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_2), + @Loc(value = COORD_3), @Loc(value = COORD_4), + @Loc(value = COORD_1) }, id = LINE_ONE, tags = { "type=boundary", + "boundary=administrative" }), + @Line(coordinates = { @Loc(value = COORD_20), @Loc(value = COORD_5), + @Loc(value = COORD_6), @Loc(value = COORD_1) }, id = LINE_TWO, tags = { + "type=boundary", "boundary=administrative" }) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line") }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_TWO, role = "outer", type = "line") }) }) + private Atlas crossingBoundariesWithOnlyTagsOnWays; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_1)), + @Node(coordinates = @Loc(value = COORD_2)), @Node(coordinates = @Loc(value = COORD_3)), + @Node(coordinates = @Loc(value = COORD_4)), @Node(coordinates = @Loc(value = COORD_5)), + @Node(coordinates = @Loc(value = COORD_6)), + @Node(coordinates = @Loc(value = COORD_20)) }, edges = { + @Edge(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_2), + @Loc(value = COORD_3), @Loc(value = COORD_4), + @Loc(value = COORD_1) }, id = EDGE_ONE), + @Edge(coordinates = { @Loc(value = COORD_20), @Loc(value = COORD_5), + @Loc(value = COORD_6), + @Loc(value = COORD_20) }, id = EDGE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = EDGE_ONE, role = "outer", type = "edge") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = EDGE_TWO, role = "outer", type = "edge") }, tags = { + "type=boundary", "boundary=administrative" }) }) + private Atlas crossingBoundariesTwoAreasIntersectEachOtherWithEdges; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_8)), + @Node(coordinates = @Loc(value = COORD_9)), @Node(coordinates = @Loc(value = COORD_10)), + @Node(coordinates = @Loc(value = COORD_11)), + @Node(coordinates = @Loc(value = COORD_12)), + @Node(coordinates = @Loc(value = COORD_13)), + @Node(coordinates = @Loc(value = COORD_14)), + @Node(coordinates = @Loc(value = COORD_15)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_8), @Loc(value = COORD_9), + @Loc(value = COORD_10), @Loc(value = COORD_11), + @Loc(value = COORD_8) }, id = LINE_ONE), + @Line(coordinates = { @Loc(value = COORD_12), @Loc(value = COORD_13), + @Loc(value = COORD_14), @Loc(value = COORD_15), + @Loc(value = COORD_12) }, id = LINE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_TWO, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }) }) + private Atlas nonCrossingBoundariesTwoSeparate; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_8)), + @Node(coordinates = @Loc(value = COORD_9)), @Node(coordinates = @Loc(value = COORD_10)), + @Node(coordinates = @Loc(value = COORD_11)), + @Node(coordinates = @Loc(value = COORD_12)), + @Node(coordinates = @Loc(value = COORD_13)), + @Node(coordinates = @Loc(value = COORD_14)), + @Node(coordinates = @Loc(value = COORD_15)) }, edges = { + @Edge(coordinates = { @Loc(value = COORD_8), @Loc(value = COORD_9), + @Loc(value = COORD_10), @Loc(value = COORD_11), + @Loc(value = COORD_8) }, id = EDGE_ONE), + @Edge(coordinates = { @Loc(value = COORD_12), @Loc(value = COORD_13), + @Loc(value = COORD_14), @Loc(value = COORD_15), + @Loc(value = COORD_12) }, id = EDGE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = EDGE_ONE, role = "outer", type = "edge") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = EDGE_TWO, role = "outer", type = "edge") }, tags = { + "type=boundary", "boundary=administrative" }) }) + private Atlas nonCrossingBoundariesTwoSeparateWithEdges; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_8)), + @Node(coordinates = @Loc(value = COORD_9)), @Node(coordinates = @Loc(value = COORD_10)), + @Node(coordinates = @Loc(value = COORD_11)), + @Node(coordinates = @Loc(value = COORD_12)), + @Node(coordinates = @Loc(value = COORD_13)), + @Node(coordinates = @Loc(value = COORD_14)), + @Node(coordinates = @Loc(value = COORD_15)), + @Node(coordinates = @Loc(value = COORD_16)), + @Node(coordinates = @Loc(value = COORD_17)), + @Node(coordinates = @Loc(value = COORD_18)), + @Node(coordinates = @Loc(value = COORD_19)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_8), + @Loc(value = COORD_9) }, id = LINE_ONE), + @Line(coordinates = { @Loc(value = COORD_9), + @Loc(value = COORD_10) }, id = LINE_TWO), + @Line(coordinates = { @Loc(value = COORD_10), + @Loc(value = COORD_11) }, id = LINE_THREE), + @Line(coordinates = { @Loc(value = COORD_11), + @Loc(value = COORD_8) }, id = LINE_FOUR), + @Line(coordinates = { @Loc(value = COORD_12), @Loc(value = COORD_13), + @Loc(value = COORD_14), @Loc(value = COORD_15), + @Loc(value = COORD_12) }, id = LINE_FIVE), + @Line(coordinates = { @Loc(value = COORD_16), + @Loc(value = COORD_17) }, id = LINE_SIX), + @Line(coordinates = { @Loc(value = COORD_17), + @Loc(value = COORD_18) }, id = LINE_SEVEN), + @Line(coordinates = { @Loc(value = COORD_18), + @Loc(value = COORD_19) }, id = LINE_EIGHT), + @Line(coordinates = { @Loc(value = COORD_19), + @Loc(value = COORD_16) }, id = LINE_NINE) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line"), + @Relation.Member(id = LINE_TWO, role = "outer", type = "line"), + @Relation.Member(id = LINE_THREE, role = "outer", type = "line"), + @Relation.Member(id = LINE_FOUR, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_FIVE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_THREE, members = { + @Relation.Member(id = LINE_SIX, role = "outer", type = "line"), + @Relation.Member(id = LINE_SEVEN, role = "outer", type = "line"), + @Relation.Member(id = LINE_EIGHT, role = "outer", type = "line"), + @Relation.Member(id = LINE_NINE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }) }) + private Atlas crossingBoundariesTwoAreasIntersectOneOther; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_1)), + @Node(coordinates = @Loc(value = COORD_2)), @Node(coordinates = @Loc(value = COORD_3)), + @Node(coordinates = @Loc(value = COORD_4)), @Node(coordinates = @Loc(value = COORD_5)), + @Node(coordinates = @Loc(value = COORD_6)), + @Node(coordinates = @Loc(value = COORD_7)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_19), + @Loc(value = COORD_6), @Loc(value = COORD_5), + @Loc(value = COORD_1) }, id = LINE_ONE), + @Line(coordinates = { @Loc(value = COORD_20), @Loc(value = COORD_11), + @Loc(value = COORD_3), @Loc(value = COORD_21), + @Loc(value = COORD_20) }, id = LINE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_TWO, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }) }) + private Atlas nonCrossingOneContainOther; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_1)), + @Node(coordinates = @Loc(value = COORD_2)), @Node(coordinates = @Loc(value = COORD_3)), + @Node(coordinates = @Loc(value = COORD_4)), @Node(coordinates = @Loc(value = COORD_5)), + @Node(coordinates = @Loc(value = COORD_6)), + @Node(coordinates = @Loc(value = COORD_7)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_2), + @Loc(value = COORD_3), @Loc(value = COORD_4), + @Loc(value = COORD_1) }, id = LINE_ONE), + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_5), + @Loc(value = COORD_6), @Loc(value = COORD_7), + @Loc(value = COORD_1) }, id = LINE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_TWO, role = "outer", type = "line") }, tags = { + "type=nonBoundary", + "boundary=administrative" }) }) + private Atlas crossingOneWithWrongType; + + @TestAtlas(nodes = { @Node(coordinates = @Loc(value = COORD_1)), + @Node(coordinates = @Loc(value = COORD_2)), @Node(coordinates = @Loc(value = COORD_3)), + @Node(coordinates = @Loc(value = COORD_4)), @Node(coordinates = @Loc(value = COORD_5)), + @Node(coordinates = @Loc(value = COORD_6)), + @Node(coordinates = @Loc(value = COORD_7)) }, lines = { + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_2), + @Loc(value = COORD_3), @Loc(value = COORD_4), + @Loc(value = COORD_1) }, id = LINE_ONE), + @Line(coordinates = { @Loc(value = COORD_1), @Loc(value = COORD_5), + @Loc(value = COORD_6), @Loc(value = COORD_7), + @Loc(value = COORD_1) }, id = LINE_TWO) }, relations = { + @Relation(id = RELATION_ONE, members = { + @Relation.Member(id = LINE_ONE, role = "outer", type = "line") }, tags = { + "type=boundary", "boundary=administrative" }), + @Relation(id = RELATION_TWO, members = { + @Relation.Member(id = LINE_TWO, role = "outer", type = "line") }, tags = { + "type=nonBoundary" }) }) + private Atlas crossingOneMissingBoundarySpecificTag; + + public Atlas boundariesTouchEachOther() + { + return this.crossingBoundariesTwoAreasTouchEachOther; + } + + public Atlas crossingBoundariesTwoAreasIntersectEachOther() + { + return this.crossingBoundariesTwoAreasIntersectEachOther; + } + + public Atlas crossingBoundariesTwoAreasIntersectEachOtherWithEdges() + { + return this.crossingBoundariesTwoAreasIntersectEachOtherWithEdges; + } + + public Atlas crossingBoundariesTwoAreasIntersectOneOther() + { + return this.crossingBoundariesTwoAreasIntersectOneOther; + } + + public Atlas crossingBoundariesWithDifferentTypes() + { + return this.crossingBoundariesWithDifferentTypes; + } + + public Atlas crossingBoundariesWithOnlyTagsOnWays() + { + return this.crossingBoundariesWithOnlyTagsOnWays; + } + + public Atlas crossingOneMissingBoundarySpecificTag() + { + return this.crossingOneMissingBoundarySpecificTag; + } + + public Atlas crossingOneWithWrongType() + { + return this.crossingOneWithWrongType; + } + + public Atlas nonCrossingBoundariesTwoSeparate() + { + return this.nonCrossingBoundariesTwoSeparate; + } + + public Atlas nonCrossingBoundariesTwoSeparateWithEdges() + { + return this.nonCrossingBoundariesTwoSeparateWithEdges; + } + + public Atlas nonCrossingOneContainOther() + { + return this.nonCrossingOneContainOther; + } +}